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; }