From f137df644182aba5f8918365a64717ec5b3c9d71 Mon Sep 17 00:00:00 2001 From: Andy Hanson <anhans@microsoft.com> Date: Thu, 12 Jul 2018 12:25:08 -0700 Subject: [PATCH] Stricter test that JSDoc @type tag matches function signature --- src/compiler/checker.ts | 25 +++++++---------- src/compiler/diagnosticMessages.json | 2 +- .../reference/checkJsdocTypeTag5.errors.txt | 5 +++- .../reference/checkJsdocTypeTag6.errors.txt | 19 +++++++++++-- .../reference/checkJsdocTypeTag6.symbols | 25 +++++++++++++++++ .../reference/checkJsdocTypeTag6.types | 28 +++++++++++++++++++ .../conformance/jsdoc/checkJsdocTypeTag6.ts | 10 +++++++ 7 files changed, 94 insertions(+), 20 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 35173231e50c9..9b3ed0ada23c5 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4682,13 +4682,7 @@ namespace ts { } } // Use contextual parameter type if one is available - let type: Type | undefined; - if (declaration.symbol.escapedName === "this") { - type = getContextualThisParameterType(func); - } - else { - type = getContextuallyTypedParameterType(declaration); - } + const type = declaration.symbol.escapedName === "this" ? getContextualThisParameterType(func) : getContextuallyTypedParameterType(declaration); if (type) { return addOptionality(type, isOptional); } @@ -16209,7 +16203,7 @@ namespace ts { // If the given type is an object or union type with a single signature, and if that signature has at // least as many parameters as the given function, return the signature. Otherwise return undefined. - function getContextualCallSignature(type: Type, node: FunctionExpression | ArrowFunction | MethodDeclaration): Signature | undefined { + function getContextualCallSignature(type: Type, node: SignatureDeclaration): Signature | undefined { const signatures = getSignaturesOfType(type, SignatureKind.Call); if (signatures.length === 1) { const signature = signatures[0]; @@ -16220,7 +16214,7 @@ namespace ts { } /** If the contextual signature has fewer parameters than the function expression, do not use it */ - function isAritySmaller(signature: Signature, target: FunctionExpression | ArrowFunction | MethodDeclaration) { + function isAritySmaller(signature: Signature, target: SignatureDeclaration) { let targetParameterCount = 0; for (; targetParameterCount < target.parameters.length; targetParameterCount++) { const param = target.parameters[targetParameterCount]; @@ -23533,12 +23527,13 @@ namespace ts { // yielded values. The only way to trigger these errors is to try checking its return type. getReturnTypeOfSignature(getSignatureFromDeclaration(node)); } - // A js function declaration can have a @type tag instead of a return type node, but that type must have a call signature - if (isInJavaScriptFile(node)) { - const typeTag = getJSDocTypeTag(node); - if (typeTag && typeTag.typeExpression && !getSignaturesOfType(getTypeFromTypeNode(typeTag.typeExpression), SignatureKind.Call).length) { - error(typeTag, Diagnostics.The_type_of_a_function_declaration_must_be_callable); - } + } + + // A js function declaration can have a @type tag instead of a return type node, but that type must have a call signature + if (isInJavaScriptFile(node)) { + const typeTag = getJSDocTypeTag(node); + if (typeTag && typeTag.typeExpression && !getContextualCallSignature(getTypeFromTypeNode(typeTag.typeExpression), node)) { + error(typeTag, Diagnostics.The_type_of_a_function_declaration_must_match_the_function_s_signature); } } } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 6ad040a503d33..73be2b46b790a 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4021,7 +4021,7 @@ "category": "Error", "code": 8029 }, - "The type of a function declaration must be callable.": { + "The type of a function declaration must match the function's signature.": { "category": "Error", "code": 8030 }, diff --git a/tests/baselines/reference/checkJsdocTypeTag5.errors.txt b/tests/baselines/reference/checkJsdocTypeTag5.errors.txt index 56f6b261d3f96..022817b999744 100644 --- a/tests/baselines/reference/checkJsdocTypeTag5.errors.txt +++ b/tests/baselines/reference/checkJsdocTypeTag5.errors.txt @@ -4,11 +4,12 @@ tests/cases/conformance/jsdoc/test.js(7,24): error TS2322: Type 'number' is not tests/cases/conformance/jsdoc/test.js(10,17): error TS2322: Type 'number' is not assignable to type 'string'. tests/cases/conformance/jsdoc/test.js(12,14): error TS2322: Type 'number' is not assignable to type 'string'. tests/cases/conformance/jsdoc/test.js(14,24): error TS2322: Type 'number' is not assignable to type 'string'. +tests/cases/conformance/jsdoc/test.js(28,5): error TS8030: The type of a function declaration must match the function's signature. tests/cases/conformance/jsdoc/test.js(34,5): error TS2322: Type '1 | 2' is not assignable to type '2 | 3'. Type '1' is not assignable to type '2 | 3'. -==== tests/cases/conformance/jsdoc/test.js (7 errors) ==== +==== tests/cases/conformance/jsdoc/test.js (8 errors) ==== // all 6 should error on return statement/expression /** @type {(x: number) => string} */ function h(x) { return x } @@ -49,6 +50,8 @@ tests/cases/conformance/jsdoc/test.js(34,5): error TS2322: Type '1 | 2' is not a /** @typedef {{(s: string): 0 | 1; (b: boolean): 2 | 3 }} Gioconda */ /** @type {Gioconda} */ + ~~~~~~~~~~~~~~~~ +!!! error TS8030: The type of a function declaration must match the function's signature. function monaLisa(sb) { return typeof sb === 'string' ? 1 : 2; } diff --git a/tests/baselines/reference/checkJsdocTypeTag6.errors.txt b/tests/baselines/reference/checkJsdocTypeTag6.errors.txt index 18a297eadaeca..cc86b6115aff0 100644 --- a/tests/baselines/reference/checkJsdocTypeTag6.errors.txt +++ b/tests/baselines/reference/checkJsdocTypeTag6.errors.txt @@ -1,12 +1,13 @@ -tests/cases/conformance/jsdoc/test.js(1,5): error TS8030: The type of a function declaration must be callable. +tests/cases/conformance/jsdoc/test.js(1,5): error TS8030: The type of a function declaration must match the function's signature. tests/cases/conformance/jsdoc/test.js(7,5): error TS2322: Type '(prop: any) => void' is not assignable to type '{ prop: string; }'. Property 'prop' is missing in type '(prop: any) => void'. +tests/cases/conformance/jsdoc/test.js(10,5): error TS8030: The type of a function declaration must match the function's signature. -==== tests/cases/conformance/jsdoc/test.js (2 errors) ==== +==== tests/cases/conformance/jsdoc/test.js (3 errors) ==== /** @type {number} */ ~~~~~~~~~~~~~~ -!!! error TS8030: The type of a function declaration must be callable. +!!! error TS8030: The type of a function declaration must match the function's signature. function f() { return 1 } @@ -17,4 +18,16 @@ tests/cases/conformance/jsdoc/test.js(7,5): error TS2322: Type '(prop: any) => v !!! error TS2322: Type '(prop: any) => void' is not assignable to type '{ prop: string; }'. !!! error TS2322: Property 'prop' is missing in type '(prop: any) => void'. } + + /** @type {(a: number) => number} */ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! error TS8030: The type of a function declaration must match the function's signature. + function add1(a, b) { return a + b; } + + /** @type {(a: number, b: number) => number} */ + function add2(a, b) { return a + b; } + + // TODO: Should be an error since signature doesn't match. + /** @type {(a: number, b: number, c: number) => number} */ + function add3(a, b) { return a + b; } \ No newline at end of file diff --git a/tests/baselines/reference/checkJsdocTypeTag6.symbols b/tests/baselines/reference/checkJsdocTypeTag6.symbols index 73d0dd0401a9f..b7ddd9f289b4e 100644 --- a/tests/baselines/reference/checkJsdocTypeTag6.symbols +++ b/tests/baselines/reference/checkJsdocTypeTag6.symbols @@ -12,3 +12,28 @@ var g = function (prop) { >prop : Symbol(prop, Decl(test.js, 6, 18)) } +/** @type {(a: number) => number} */ +function add1(a, b) { return a + b; } +>add1 : Symbol(add1, Decl(test.js, 7, 1)) +>a : Symbol(a, Decl(test.js, 10, 14)) +>b : Symbol(b, Decl(test.js, 10, 16)) +>a : Symbol(a, Decl(test.js, 10, 14)) +>b : Symbol(b, Decl(test.js, 10, 16)) + +/** @type {(a: number, b: number) => number} */ +function add2(a, b) { return a + b; } +>add2 : Symbol(add2, Decl(test.js, 10, 37)) +>a : Symbol(a, Decl(test.js, 13, 14)) +>b : Symbol(b, Decl(test.js, 13, 16)) +>a : Symbol(a, Decl(test.js, 13, 14)) +>b : Symbol(b, Decl(test.js, 13, 16)) + +// TODO: Should be an error since signature doesn't match. +/** @type {(a: number, b: number, c: number) => number} */ +function add3(a, b) { return a + b; } +>add3 : Symbol(add3, Decl(test.js, 13, 37)) +>a : Symbol(a, Decl(test.js, 17, 14)) +>b : Symbol(b, Decl(test.js, 17, 16)) +>a : Symbol(a, Decl(test.js, 17, 14)) +>b : Symbol(b, Decl(test.js, 17, 16)) + diff --git a/tests/baselines/reference/checkJsdocTypeTag6.types b/tests/baselines/reference/checkJsdocTypeTag6.types index cb1e0e6cb9fa1..a2d9ffbbd7af6 100644 --- a/tests/baselines/reference/checkJsdocTypeTag6.types +++ b/tests/baselines/reference/checkJsdocTypeTag6.types @@ -14,3 +14,31 @@ var g = function (prop) { >prop : any } +/** @type {(a: number) => number} */ +function add1(a, b) { return a + b; } +>add1 : (a: any, b: any) => number +>a : any +>b : any +>a + b : any +>a : any +>b : any + +/** @type {(a: number, b: number) => number} */ +function add2(a, b) { return a + b; } +>add2 : (a: number, b: number) => number +>a : number +>b : number +>a + b : number +>a : number +>b : number + +// TODO: Should be an error since signature doesn't match. +/** @type {(a: number, b: number, c: number) => number} */ +function add3(a, b) { return a + b; } +>add3 : (a: number, b: number) => number +>a : number +>b : number +>a + b : number +>a : number +>b : number + diff --git a/tests/cases/conformance/jsdoc/checkJsdocTypeTag6.ts b/tests/cases/conformance/jsdoc/checkJsdocTypeTag6.ts index 641ac08e56ceb..5f629d42b3b58 100644 --- a/tests/cases/conformance/jsdoc/checkJsdocTypeTag6.ts +++ b/tests/cases/conformance/jsdoc/checkJsdocTypeTag6.ts @@ -11,3 +11,13 @@ function f() { /** @type {{ prop: string }} */ var g = function (prop) { } + +/** @type {(a: number) => number} */ +function add1(a, b) { return a + b; } + +/** @type {(a: number, b: number) => number} */ +function add2(a, b) { return a + b; } + +// TODO: Should be an error since signature doesn't match. +/** @type {(a: number, b: number, c: number) => number} */ +function add3(a, b) { return a + b; }