Fix type when annotated with a JSDoc function type (#22692)

* Fix type when annotated with a JSDoc function type

Previously,
1. A variable annotated with a JSDoc function type would not require all
its parameters to be provided. This should only apply to functions
without a type annotation.
2. A parameter in a function with a JSDoc function type annotation would
still have the type 'any'.
3. Two `var` declarations in a Typescript and Javascript file,
respectively, would error even when they had identical function types.

* Update baselines and add constructor test

* Handle ConstructorType too

* Add test:method sig inside literal type

* Contextually type parameters by parent sig's JSDoc

Instead of a syntactic check in getJSDocTag

* Remove redundant check:isUntypedSignatureInJSFile

* Positive check for value signatures

Instead of excluding type signatures piecemeal.
This commit is contained in:
Nathan Shively-Sanders 2018-03-19 16:00:45 -07:00 committed by GitHub
parent d88041fc0a
commit b56093f3ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 326 additions and 67 deletions

View File

@ -6691,7 +6691,11 @@ namespace ts {
let hasThisParameter: boolean;
const iife = getImmediatelyInvokedFunctionExpression(declaration);
const isJSConstructSignature = isJSDocConstructSignature(declaration);
const isUntypedSignatureInJSFile = !iife && !isJSConstructSignature && isInJavaScriptFile(declaration) && !hasJSDocParameterTags(declaration);
const isUntypedSignatureInJSFile = !iife &&
isInJavaScriptFile(declaration) &&
isValueSignatureDeclaration(declaration) &&
!hasJSDocParameterTags(declaration) &&
!getJSDocType(declaration);
// If this is a JSDoc construct signature, then skip the first parameter in the
// parameter list. The first parameter represents the return type of the construct
@ -9100,7 +9104,8 @@ namespace ts {
}
function isContextSensitiveFunctionOrObjectLiteralMethod(func: Node): func is FunctionExpression | ArrowFunction | MethodDeclaration {
return (isFunctionExpressionOrArrowFunction(func) || isObjectLiteralMethod(func)) && isContextSensitiveFunctionLikeDeclaration(func);
return (isInJavaScriptFile(func) && isFunctionDeclaration(func) || isFunctionExpressionOrArrowFunction(func) || isObjectLiteralMethod(func)) &&
isContextSensitiveFunctionLikeDeclaration(func);
}
function getTypeWithoutSignatures(type: Type): Type {
@ -14168,49 +14173,49 @@ namespace ts {
// Return contextual type of parameter or undefined if no contextual type is available
function getContextuallyTypedParameterType(parameter: ParameterDeclaration): Type | undefined {
const func = parameter.parent;
if (isContextSensitiveFunctionOrObjectLiteralMethod(func)) {
const iife = getImmediatelyInvokedFunctionExpression(func);
if (iife && iife.arguments) {
const indexOfParameter = func.parameters.indexOf(parameter);
if (parameter.dotDotDotToken) {
const restTypes: Type[] = [];
for (let i = indexOfParameter; i < iife.arguments.length; i++) {
restTypes.push(getWidenedLiteralType(checkExpression(iife.arguments[i])));
}
return restTypes.length ? createArrayType(getUnionType(restTypes)) : undefined;
if (!isContextSensitiveFunctionOrObjectLiteralMethod(func)) {
return undefined;
}
const iife = getImmediatelyInvokedFunctionExpression(func);
if (iife && iife.arguments) {
const indexOfParameter = func.parameters.indexOf(parameter);
if (parameter.dotDotDotToken) {
const restTypes: Type[] = [];
for (let i = indexOfParameter; i < iife.arguments.length; i++) {
restTypes.push(getWidenedLiteralType(checkExpression(iife.arguments[i])));
}
const links = getNodeLinks(iife);
const cached = links.resolvedSignature;
links.resolvedSignature = anySignature;
const type = indexOfParameter < iife.arguments.length ?
getWidenedLiteralType(checkExpression(iife.arguments[indexOfParameter])) :
parameter.initializer ? undefined : undefinedWideningType;
links.resolvedSignature = cached;
return type;
return restTypes.length ? createArrayType(getUnionType(restTypes)) : undefined;
}
const links = getNodeLinks(iife);
const cached = links.resolvedSignature;
links.resolvedSignature = anySignature;
const type = indexOfParameter < iife.arguments.length ?
getWidenedLiteralType(checkExpression(iife.arguments[indexOfParameter])) :
parameter.initializer ? undefined : undefinedWideningType;
links.resolvedSignature = cached;
return type;
}
const contextualSignature = getContextualSignature(func);
if (contextualSignature) {
const funcHasRestParameters = hasRestParameter(func);
const len = func.parameters.length - (funcHasRestParameters ? 1 : 0);
let indexOfParameter = func.parameters.indexOf(parameter);
if (getThisParameter(func) !== undefined && !contextualSignature.thisParameter) {
Debug.assert(indexOfParameter !== 0); // Otherwise we should not have called `getContextuallyTypedParameterType`.
indexOfParameter -= 1;
}
const contextualSignature = getContextualSignature(func);
if (contextualSignature) {
const funcHasRestParameters = hasRestParameter(func);
const len = func.parameters.length - (funcHasRestParameters ? 1 : 0);
let indexOfParameter = func.parameters.indexOf(parameter);
if (getThisParameter(func) !== undefined && !contextualSignature.thisParameter) {
Debug.assert(indexOfParameter !== 0); // Otherwise we should not have called `getContextuallyTypedParameterType`.
indexOfParameter -= 1;
}
if (indexOfParameter < len) {
return getTypeAtPosition(contextualSignature, indexOfParameter);
}
if (indexOfParameter < len) {
return getTypeAtPosition(contextualSignature, indexOfParameter);
}
// If last parameter is contextually rest parameter get its type
if (funcHasRestParameters &&
indexOfParameter === (func.parameters.length - 1) &&
isRestParameterIndex(contextualSignature, func.parameters.length - 1)) {
return getTypeOfSymbol(lastOrUndefined(contextualSignature.parameters));
}
// If last parameter is contextually rest parameter get its type
if (funcHasRestParameters &&
indexOfParameter === (func.parameters.length - 1) &&
isRestParameterIndex(contextualSignature, func.parameters.length - 1)) {
return getTypeOfSymbol(lastOrUndefined(contextualSignature.parameters));
}
}
return undefined;
}
// In a variable, parameter or property declaration with a type annotation,
@ -14773,7 +14778,16 @@ namespace ts {
// union type of return types from these signatures
function getContextualSignature(node: FunctionExpression | ArrowFunction | MethodDeclaration): Signature {
Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node));
const type = getContextualTypeForFunctionLikeDeclaration(node);
let type: Type;
if (isInJavaScriptFile(node)) {
const jsdoc = getJSDocType(node);
if (jsdoc) {
type = getTypeFromTypeNode(jsdoc);
}
}
if (!type) {
type = getContextualTypeForFunctionLikeDeclaration(node);
}
if (!type) {
return undefined;
}

View File

@ -1958,6 +1958,18 @@ namespace ts {
return false;
}
export type ValueSignatureDeclaration =
| FunctionDeclaration
| MethodDeclaration
| ConstructorDeclaration
| AccessorDeclaration
| FunctionExpression
| ArrowFunction;
export function isValueSignatureDeclaration(node: Node): node is ValueSignatureDeclaration {
return isFunctionExpression(node) || isArrowFunction(node) || isMethodOrAccessor(node) || isFunctionDeclaration(node) || isConstructorDeclaration(node);
}
function walkUp(node: Node, kind: SyntaxKind) {
while (node && node.kind === kind) {
node = node.parent;

View File

@ -20,12 +20,12 @@ const obj = {
/** @type {function(number): number} */
method1(n1) {
>method1 : (n1: any) => any
>n1 : any
>method1 : (n1: number) => number
>n1 : number
return n1 + 42;
>n1 + 42 : any
>n1 : any
>n1 + 42 : number
>n1 : number
>42 : 42
},
@ -44,10 +44,10 @@ const obj = {
/** @type {function(number): number} */
arrowFunc: (num) => num + 42
>arrowFunc : (arg0: number) => number
>(num) => num + 42 : (num: any) => any
>num : any
>num + 42 : any
>num : any
>(num) => num + 42 : (num: number) => number
>num : number
>num + 42 : number
>num : number
>42 : 42
}
obj.foo = 'string'

View File

@ -1,11 +1,9 @@
tests/cases/conformance/jsdoc/0.js(5,3): error TS2322: Type 'number' is not assignable to type 'string | undefined'.
tests/cases/conformance/jsdoc/0.js(7,3): error TS2322: Type '(n1: any) => string' is not assignable to type '(arg0: number) => number'.
tests/cases/conformance/jsdoc/0.js(7,3): error TS2322: Type '(n1: number) => string' is not assignable to type '(arg0: number) => number'.
Type 'string' is not assignable to type 'number'.
tests/cases/conformance/jsdoc/0.js(11,3): error TS2322: Type '(n1: any) => string' is not assignable to type '(arg0: number) => number'.
tests/cases/conformance/jsdoc/0.js(11,3): error TS2322: Type '(n1: number) => string' is not assignable to type '(arg0: number) => number'.
Type 'string' is not assignable to type 'number'.
tests/cases/conformance/jsdoc/0.js(13,3): error TS2322: Type '(num?: string) => string' is not assignable to type '(arg0: number) => number'.
Types of parameters 'num' and 'arg0' are incompatible.
Type 'number' is not assignable to type 'string | undefined'.
tests/cases/conformance/jsdoc/0.js(13,15): error TS2322: Type '"0"' is not assignable to type 'number'.
tests/cases/conformance/jsdoc/0.js(15,3): error TS2322: Type 'undefined' is not assignable to type 'string'.
tests/cases/conformance/jsdoc/0.js(19,5): error TS2322: Type 'number' is not assignable to type 'string'.
tests/cases/conformance/jsdoc/0.js(22,22): error TS2345: Argument of type '"0"' is not assignable to parameter of type 'number'.
@ -22,21 +20,19 @@ tests/cases/conformance/jsdoc/0.js(22,22): error TS2345: Argument of type '"0"'
/** @type {function(number): number} */
method1(n1) {
~~~~~~~
!!! error TS2322: Type '(n1: any) => string' is not assignable to type '(arg0: number) => number'.
!!! error TS2322: Type '(n1: number) => string' is not assignable to type '(arg0: number) => number'.
!!! error TS2322: Type 'string' is not assignable to type 'number'.
return "42";
},
/** @type {function(number): number} */
method2: (n1) => "lol",
~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2322: Type '(n1: any) => string' is not assignable to type '(arg0: number) => number'.
!!! error TS2322: Type '(n1: number) => string' is not assignable to type '(arg0: number) => number'.
!!! error TS2322: Type 'string' is not assignable to type 'number'.
/** @type {function(number): number} */
arrowFunc: (num="0") => num + 42,
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2322: Type '(num?: string) => string' is not assignable to type '(arg0: number) => number'.
!!! error TS2322: Types of parameters 'num' and 'arg0' are incompatible.
!!! error TS2322: Type 'number' is not assignable to type 'string | undefined'.
~~~~~~~
!!! error TS2322: Type '"0"' is not assignable to type 'number'.
/** @type {string} */
lol
~~~

View File

@ -14,8 +14,8 @@ const obj = {
/** @type {function(number): number} */
method1(n1) {
>method1 : (n1: any) => string
>n1 : any
>method1 : (n1: number) => string
>n1 : number
return "42";
>"42" : "42"
@ -24,18 +24,18 @@ const obj = {
/** @type {function(number): number} */
method2: (n1) => "lol",
>method2 : (arg0: number) => number
>(n1) => "lol" : (n1: any) => string
>n1 : any
>(n1) => "lol" : (n1: number) => string
>n1 : number
>"lol" : "lol"
/** @type {function(number): number} */
arrowFunc: (num="0") => num + 42,
>arrowFunc : (arg0: number) => number
>(num="0") => num + 42 : (num?: string) => string
>num : string
>(num="0") => num + 42 : (num?: number) => number
>num : number
>"0" : "0"
>num + 42 : string
>num : string
>num + 42 : number
>num : number
>42 : 42
/** @type {string} */

View File

@ -60,6 +60,12 @@ var obj;
/** @type {Function} */
var Func;
/** @type {(s: string) => number} */
var f;
/** @type {new (s: string) => { s: string }} */
var ctor;
//// [b.ts]
var S: string;
@ -82,6 +88,8 @@ var nullable: number | null;
var Obj: any;
var obj: any;
var Func: Function;
var f: (s: string) => number;
var ctor: new (s: string) => { s: string };
//// [a.js]
@ -125,6 +133,10 @@ var Obj;
var obj;
/** @type {Function} */
var Func;
/** @type {(s: string) => number} */
var f;
/** @type {new (s: string) => { s: string }} */
var ctor;
//// [b.js]
var S;
var s;
@ -146,3 +158,5 @@ var nullable;
var Obj;
var obj;
var Func;
var f;
var ctor;

View File

@ -79,6 +79,14 @@ var obj;
var Func;
>Func : Symbol(Func, Decl(a.js, 58, 3), Decl(b.ts, 19, 3))
/** @type {(s: string) => number} */
var f;
>f : Symbol(f, Decl(a.js, 61, 3), Decl(b.ts, 20, 3))
/** @type {new (s: string) => { s: string }} */
var ctor;
>ctor : Symbol(ctor, Decl(a.js, 64, 3), Decl(b.ts, 21, 3))
=== tests/cases/conformance/jsdoc/b.ts ===
var S: string;
>S : Symbol(S, Decl(a.js, 1, 3), Decl(b.ts, 0, 3))
@ -143,3 +151,12 @@ var Func: Function;
>Func : Symbol(Func, Decl(a.js, 58, 3), Decl(b.ts, 19, 3))
>Function : Symbol(Function, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --))
var f: (s: string) => number;
>f : Symbol(f, Decl(a.js, 61, 3), Decl(b.ts, 20, 3))
>s : Symbol(s, Decl(b.ts, 20, 8))
var ctor: new (s: string) => { s: string };
>ctor : Symbol(ctor, Decl(a.js, 64, 3), Decl(b.ts, 21, 3))
>s : Symbol(s, Decl(b.ts, 21, 15))
>s : Symbol(s, Decl(b.ts, 21, 30))

View File

@ -79,6 +79,14 @@ var obj;
var Func;
>Func : Function
/** @type {(s: string) => number} */
var f;
>f : (s: string) => number
/** @type {new (s: string) => { s: string }} */
var ctor;
>ctor : new (s: string) => { s: string; }
=== tests/cases/conformance/jsdoc/b.ts ===
var S: string;
>S : string
@ -146,3 +154,12 @@ var Func: Function;
>Func : Function
>Function : Function
var f: (s: string) => number;
>f : (s: string) => number
>s : string
var ctor: new (s: string) => { s: string };
>ctor : new (s: string) => { s: string; }
>s : string
>s : string

View File

@ -0,0 +1,18 @@
tests/cases/conformance/jsdoc/a.js(3,5): error TS2322: Type '1' is not assignable to type 'string'.
tests/cases/conformance/jsdoc/a.js(7,5): error TS2322: Type '1' is not assignable to type 'string'.
==== tests/cases/conformance/jsdoc/a.js (2 errors) ====
/** @type {function(string): void} */
const f = (value) => {
value = 1 // should error
~~~~~
!!! error TS2322: Type '1' is not assignable to type 'string'.
};
/** @type {(s: string) => void} */
function g(s) {
s = 1 // Should error
~
!!! error TS2322: Type '1' is not assignable to type 'string'.
}

View File

@ -0,0 +1,19 @@
=== tests/cases/conformance/jsdoc/a.js ===
/** @type {function(string): void} */
const f = (value) => {
>f : Symbol(f, Decl(a.js, 1, 5))
>value : Symbol(value, Decl(a.js, 1, 11))
value = 1 // should error
>value : Symbol(value, Decl(a.js, 1, 11))
};
/** @type {(s: string) => void} */
function g(s) {
>g : Symbol(g, Decl(a.js, 3, 2))
>s : Symbol(s, Decl(a.js, 5, 11))
s = 1 // Should error
>s : Symbol(s, Decl(a.js, 5, 11))
}

View File

@ -0,0 +1,24 @@
=== tests/cases/conformance/jsdoc/a.js ===
/** @type {function(string): void} */
const f = (value) => {
>f : (arg0: string) => void
>(value) => { value = 1 // should error} : (value: string) => void
>value : string
value = 1 // should error
>value = 1 : 1
>value : string
>1 : 1
};
/** @type {(s: string) => void} */
function g(s) {
>g : (s: string) => void
>s : string
s = 1 // Should error
>s = 1 : 1
>s : string
>1 : 1
}

View File

@ -0,0 +1,26 @@
tests/cases/conformance/jsdoc/a.js(11,1): error TS2554: Expected 1 arguments, but got 0.
tests/cases/conformance/jsdoc/a.js(12,1): error TS2554: Expected 1 arguments, but got 0.
tests/cases/conformance/jsdoc/a.js(13,1): error TS2554: Expected 1 arguments, but got 0.
==== tests/cases/conformance/jsdoc/a.js (3 errors) ====
/** @type {function(string): void} */
const f = (value) => {
};
/** @type {(s: string) => void} */
function g(s) {
}
/** @type {{(s: string): void}} */
function h(s) {
}
f() // should error
~~~
!!! error TS2554: Expected 1 arguments, but got 0.
g() // should error
~~~
!!! error TS2554: Expected 1 arguments, but got 0.
h()
~~~
!!! error TS2554: Expected 1 arguments, but got 0.

View File

@ -0,0 +1,27 @@
=== tests/cases/conformance/jsdoc/a.js ===
/** @type {function(string): void} */
const f = (value) => {
>f : Symbol(f, Decl(a.js, 1, 5))
>value : Symbol(value, Decl(a.js, 1, 11))
};
/** @type {(s: string) => void} */
function g(s) {
>g : Symbol(g, Decl(a.js, 2, 2))
>s : Symbol(s, Decl(a.js, 4, 11))
}
/** @type {{(s: string): void}} */
function h(s) {
>h : Symbol(h, Decl(a.js, 5, 1))
>s : Symbol(s, Decl(a.js, 7, 11))
}
f() // should error
>f : Symbol(f, Decl(a.js, 1, 5))
g() // should error
>g : Symbol(g, Decl(a.js, 2, 2))
h()
>h : Symbol(h, Decl(a.js, 5, 1))

View File

@ -0,0 +1,31 @@
=== tests/cases/conformance/jsdoc/a.js ===
/** @type {function(string): void} */
const f = (value) => {
>f : (arg0: string) => void
>(value) => {} : (value: string) => void
>value : string
};
/** @type {(s: string) => void} */
function g(s) {
>g : (s: string) => void
>s : string
}
/** @type {{(s: string): void}} */
function h(s) {
>h : (s: string) => void
>s : string
}
f() // should error
>f() : void
>f : (arg0: string) => void
g() // should error
>g() : void
>g : (s: string) => void
h()
>h() : void
>h : (s: string) => void

View File

@ -63,6 +63,12 @@ var obj;
/** @type {Function} */
var Func;
/** @type {(s: string) => number} */
var f;
/** @type {new (s: string) => { s: string }} */
var ctor;
// @filename: b.ts
var S: string;
var s: string;
@ -84,3 +90,5 @@ var nullable: number | null;
var Obj: any;
var obj: any;
var Func: Function;
var f: (s: string) => number;
var ctor: new (s: string) => { s: string };

View File

@ -0,0 +1,15 @@
// @allowJs: true
// @checkJs: true
// @noEmit: true
// @strict: true
// @noImplicitAny: true
// @Filename: a.js
/** @type {function(string): void} */
const f = (value) => {
value = 1 // should error
};
/** @type {(s: string) => void} */
function g(s) {
s = 1 // Should error
}

View File

@ -0,0 +1,20 @@
// @allowJs: true
// @checkJs: true
// @noEmit: true
// @strict: true
// @noImplicitAny: true
// @Filename: a.js
/** @type {function(string): void} */
const f = (value) => {
};
/** @type {(s: string) => void} */
function g(s) {
}
/** @type {{(s: string): void}} */
function h(s) {
}
f() // should error
g() // should error
h()

View File

@ -5,6 +5,7 @@
verify.codeFix({
description: "Annotate with type from JSDoc",
index: 0,
newFileContent:
`/** @type {function(*, ...number, ...boolean): void} */
var x: (arg0: any, arg1: number[], ...rest: boolean[]) => void = (x, ys, ...zs) => { };`,