diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 3eb8267de79..0edc0e978c3 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -6771,17 +6771,20 @@ namespace ts { return links.resolvedSignature; } + /** + * A JS function gets a synthetic rest parameter if it references `arguments` AND: + * 1. It has no parameters but at least one `@param` with a type that starts with `...` + * OR + * 2. It has at least one parameter, and the last parameter has a matching `@param` with a type that starts with `...` + */ function maybeAddJsSyntheticRestParameter(declaration: SignatureDeclaration, parameters: Symbol[]): boolean { - // JS functions get a free rest parameter if: - // a) The last parameter has `...` preceding its type - // b) It references `arguments` somewhere - const lastParam = lastOrUndefined(declaration.parameters); - const lastParamTags = lastParam && getJSDocParameterTags(lastParam); - const lastParamVariadicType = firstDefined(lastParamTags, p => - p.typeExpression && isJSDocVariadicType(p.typeExpression.type) ? p.typeExpression.type : undefined); - if (!lastParamVariadicType && !containsArgumentsReference(declaration)) { + if (!containsArgumentsReference(declaration)) { return false; } + const lastParam = lastOrUndefined(declaration.parameters); + const lastParamTags = lastParam ? getJSDocParameterTags(lastParam) : getJSDocTags(declaration).filter(isJSDocParameterTag); + const lastParamVariadicType = firstDefined(lastParamTags, p => + p.typeExpression && isJSDocVariadicType(p.typeExpression.type) ? p.typeExpression.type : undefined); const syntheticArgsSymbol = createSymbol(SymbolFlags.Variable, "args" as __String); syntheticArgsSymbol.type = lastParamVariadicType ? createArrayType(getTypeFromTypeNode(lastParamVariadicType.type)) : anyArrayType; @@ -21459,9 +21462,24 @@ namespace ts { function checkJSDocParameterTag(node: JSDocParameterTag) { checkSourceElement(node.typeExpression); if (!getParameterSymbolFromJSDoc(node)) { - error(node.name, - Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name, - idText(node.name.kind === SyntaxKind.QualifiedName ? node.name.right : node.name)); + const decl = getHostSignatureFromJSDoc(node); + // don't issue an error for invalid hosts -- just functions -- + // and give a better error message when the host function mentions `arguments` + // but the tag doesn't have an array type + if (decl) { + if (!containsArgumentsReference(decl)) { + error(node.name, + Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name, + idText(node.name.kind === SyntaxKind.QualifiedName ? node.name.right : node.name)); + } + else if (findLast(getJSDocTags(decl), isJSDocParameterTag) === node && + node.typeExpression && node.typeExpression.type && + !isArrayType(getTypeFromTypeNode(node.typeExpression.type))) { + error(node.name, + Diagnostics.The_last_param_tag_of_a_function_that_uses_arguments_must_have_an_array_type, + idText(node.name.kind === SyntaxKind.QualifiedName ? node.name.right : node.name)); + } + } } } @@ -24488,18 +24506,19 @@ namespace ts { const paramTag = parent.parent; if (isJSDocTypeExpression(parent) && isJSDocParameterTag(paramTag)) { // Else we will add a diagnostic, see `checkJSDocVariadicType`. - const param = getParameterSymbolFromJSDoc(paramTag); - if (param) { - const host = getHostSignatureFromJSDoc(paramTag); + const host = getHostSignatureFromJSDoc(paramTag); + if (host) { /* - Only return an array type if the corresponding parameter is marked as a rest parameter. + Only return an array type if the corresponding parameter is marked as a rest parameter, or if there are no parameters. So in the following situation we will not create an array type: /** @param {...number} a * / function f(a) {} Because `a` will just be of type `number | undefined`. A synthetic `...args` will also be added, which *will* get an array type. */ - const lastParamDeclaration = host && last(host.parameters); - if (lastParamDeclaration.symbol === param && isRestParameter(lastParamDeclaration)) { + const lastParamDeclaration = lastOrUndefined(host.parameters); + const symbol = getParameterSymbolFromJSDoc(paramTag); + if (!lastParamDeclaration || + symbol && lastParamDeclaration.symbol === symbol && isRestParameter(lastParamDeclaration)) { return createArrayType(type); } } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 9f95a66711b..d0e97d188a3 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3720,6 +3720,10 @@ "category": "Error", "code": 8028 }, + "The last @param tag of a function that uses 'arguments' must have an array type.": { + "category": "Error", + "code": 8029 + }, "Only identifiers/qualified-names with optional type arguments are currently supported in a class 'extends' clause.": { "category": "Error", "code": 9002 diff --git a/tests/baselines/reference/jsdocPrefixPostfixParsing.types b/tests/baselines/reference/jsdocPrefixPostfixParsing.types index cf69cf9d40c..733a266391e 100644 --- a/tests/baselines/reference/jsdocPrefixPostfixParsing.types +++ b/tests/baselines/reference/jsdocPrefixPostfixParsing.types @@ -16,7 +16,7 @@ * @param {...number?[]!} k - (number[] | null)[] */ function f(x, y, z, a, b, c, d, e, f, g, h, i, j, k) { ->f : (x: number[], y: number[], z: number[], a: (number | null)[], b: number[] | null, c: number[] | null, d: number | null | undefined, e: number | null | undefined, f: number | null | undefined, g: number | null | undefined, h: number | null | undefined, i: number[] | undefined, j: number[] | null | undefined, ...args: (number | null)[][]) => void +>f : (x: number[], y: number[], z: number[], a: (number | null)[], b: number[] | null, c: number[] | null, d: number | null | undefined, e: number | null | undefined, f: number | null | undefined, g: number | null | undefined, h: number | null | undefined, i: number[] | undefined, j: number[] | null | undefined, k: (number | null)[] | undefined) => void >x : number[] >y : number[] >z : number[] diff --git a/tests/baselines/reference/paramTagOnCallExpression.symbols b/tests/baselines/reference/paramTagOnCallExpression.symbols new file mode 100644 index 00000000000..0ac0f3d60a6 --- /dev/null +++ b/tests/baselines/reference/paramTagOnCallExpression.symbols @@ -0,0 +1,14 @@ +=== tests/cases/conformance/jsdoc/decls.d.ts === +declare function factory(type: string): {}; +>factory : Symbol(factory, Decl(decls.d.ts, 0, 0)) +>type : Symbol(type, Decl(decls.d.ts, 0, 25)) + +=== tests/cases/conformance/jsdoc/a.js === +// from util +/** @param {function} ctor - A big long explanation follows */ +exports.inherits = factory('inherits') +>exports.inherits : Symbol(inherits, Decl(a.js, 0, 0)) +>exports : Symbol(inherits, Decl(a.js, 0, 0)) +>inherits : Symbol(inherits, Decl(a.js, 0, 0)) +>factory : Symbol(factory, Decl(decls.d.ts, 0, 0)) + diff --git a/tests/baselines/reference/paramTagOnCallExpression.types b/tests/baselines/reference/paramTagOnCallExpression.types new file mode 100644 index 00000000000..c5aa2c7c611 --- /dev/null +++ b/tests/baselines/reference/paramTagOnCallExpression.types @@ -0,0 +1,17 @@ +=== tests/cases/conformance/jsdoc/decls.d.ts === +declare function factory(type: string): {}; +>factory : (type: string) => {} +>type : string + +=== tests/cases/conformance/jsdoc/a.js === +// from util +/** @param {function} ctor - A big long explanation follows */ +exports.inherits = factory('inherits') +>exports.inherits = factory('inherits') : {} +>exports.inherits : {} +>exports : typeof "tests/cases/conformance/jsdoc/a" +>inherits : {} +>factory('inherits') : {} +>factory : (type: string) => {} +>'inherits' : "inherits" + diff --git a/tests/baselines/reference/paramTagOnFunctionUsingArguments.errors.txt b/tests/baselines/reference/paramTagOnFunctionUsingArguments.errors.txt new file mode 100644 index 00000000000..f9ad715c49b --- /dev/null +++ b/tests/baselines/reference/paramTagOnFunctionUsingArguments.errors.txt @@ -0,0 +1,31 @@ +tests/cases/conformance/jsdoc/a.js(2,20): error TS8029: The last @param tag of a function that uses 'arguments' must have an array type. +tests/cases/conformance/jsdoc/a.js(19,9): error TS2345: Argument of type '1' is not assignable to parameter of type 'string'. + + +==== tests/cases/conformance/jsdoc/decls.d.ts (0 errors) ==== + declare function factory(type: string): {}; +==== tests/cases/conformance/jsdoc/a.js (2 errors) ==== + /** + * @param {string} first + ~~~~~ +!!! error TS8029: The last @param tag of a function that uses 'arguments' must have an array type. + */ + function concat(/* first, second, ... */) { + var s = '' + for (var i = 0, l = arguments.length; i < l; i++) { + s += arguments[i] + } + return s + } + + /** + * @param {...string} strings + */ + function correct() { + arguments + } + + correct(1,2,3) // oh no + ~ +!!! error TS2345: Argument of type '1' is not assignable to parameter of type 'string'. + \ No newline at end of file diff --git a/tests/baselines/reference/paramTagOnFunctionUsingArguments.symbols b/tests/baselines/reference/paramTagOnFunctionUsingArguments.symbols new file mode 100644 index 00000000000..4d8688e9877 --- /dev/null +++ b/tests/baselines/reference/paramTagOnFunctionUsingArguments.symbols @@ -0,0 +1,47 @@ +=== tests/cases/conformance/jsdoc/decls.d.ts === +declare function factory(type: string): {}; +>factory : Symbol(factory, Decl(decls.d.ts, 0, 0)) +>type : Symbol(type, Decl(decls.d.ts, 0, 25)) + +=== tests/cases/conformance/jsdoc/a.js === +/** + * @param {string} first + */ +function concat(/* first, second, ... */) { +>concat : Symbol(concat, Decl(a.js, 0, 0)) + + var s = '' +>s : Symbol(s, Decl(a.js, 4, 5)) + + for (var i = 0, l = arguments.length; i < l; i++) { +>i : Symbol(i, Decl(a.js, 5, 10)) +>l : Symbol(l, Decl(a.js, 5, 17)) +>arguments.length : Symbol(IArguments.length, Decl(lib.d.ts, --, --)) +>arguments : Symbol(arguments) +>length : Symbol(IArguments.length, Decl(lib.d.ts, --, --)) +>i : Symbol(i, Decl(a.js, 5, 10)) +>l : Symbol(l, Decl(a.js, 5, 17)) +>i : Symbol(i, Decl(a.js, 5, 10)) + + s += arguments[i] +>s : Symbol(s, Decl(a.js, 4, 5)) +>arguments : Symbol(arguments) +>i : Symbol(i, Decl(a.js, 5, 10)) + } + return s +>s : Symbol(s, Decl(a.js, 4, 5)) +} + +/** + * @param {...string} strings + */ +function correct() { +>correct : Symbol(correct, Decl(a.js, 9, 1)) + + arguments +>arguments : Symbol(arguments) +} + +correct(1,2,3) // oh no +>correct : Symbol(correct, Decl(a.js, 9, 1)) + diff --git a/tests/baselines/reference/paramTagOnFunctionUsingArguments.types b/tests/baselines/reference/paramTagOnFunctionUsingArguments.types new file mode 100644 index 00000000000..275e248291c --- /dev/null +++ b/tests/baselines/reference/paramTagOnFunctionUsingArguments.types @@ -0,0 +1,57 @@ +=== tests/cases/conformance/jsdoc/decls.d.ts === +declare function factory(type: string): {}; +>factory : (type: string) => {} +>type : string + +=== tests/cases/conformance/jsdoc/a.js === +/** + * @param {string} first + */ +function concat(/* first, second, ... */) { +>concat : (...args: any[]) => string + + var s = '' +>s : string +>'' : "" + + for (var i = 0, l = arguments.length; i < l; i++) { +>i : number +>0 : 0 +>l : number +>arguments.length : number +>arguments : IArguments +>length : number +>i < l : boolean +>i : number +>l : number +>i++ : number +>i : number + + s += arguments[i] +>s += arguments[i] : string +>s : string +>arguments[i] : any +>arguments : IArguments +>i : number + } + return s +>s : string +} + +/** + * @param {...string} strings + */ +function correct() { +>correct : (...args: string[]) => void + + arguments +>arguments : IArguments +} + +correct(1,2,3) // oh no +>correct(1,2,3) : void +>correct : (...args: string[]) => void +>1 : 1 +>2 : 2 +>3 : 3 + diff --git a/tests/cases/conformance/jsdoc/paramTagOnCallExpression.ts b/tests/cases/conformance/jsdoc/paramTagOnCallExpression.ts new file mode 100644 index 00000000000..6a00f7db8bf --- /dev/null +++ b/tests/cases/conformance/jsdoc/paramTagOnCallExpression.ts @@ -0,0 +1,10 @@ +// @noEmit: true +// @allowJs: true +// @checkJs: true +// @Filename: decls.d.ts +declare function factory(type: string): {}; +// @Filename: a.js + +// from util +/** @param {function} ctor - A big long explanation follows */ +exports.inherits = factory('inherits') diff --git a/tests/cases/conformance/jsdoc/paramTagOnFunctionUsingArguments.ts b/tests/cases/conformance/jsdoc/paramTagOnFunctionUsingArguments.ts new file mode 100644 index 00000000000..0c5b0fbb365 --- /dev/null +++ b/tests/cases/conformance/jsdoc/paramTagOnFunctionUsingArguments.ts @@ -0,0 +1,27 @@ +// @noEmit: true +// @allowJs: true +// @checkJs: true +// @strict: true +// @Filename: decls.d.ts +declare function factory(type: string): {}; +// @Filename: a.js + +/** + * @param {string} first + */ +function concat(/* first, second, ... */) { + var s = '' + for (var i = 0, l = arguments.length; i < l; i++) { + s += arguments[i] + } + return s +} + +/** + * @param {...string} strings + */ +function correct() { + arguments +} + +correct(1,2,3) // oh no