diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a8af11e780305..5befe3b1086f0 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4327,6 +4327,12 @@ namespace ts { return getTypeFromBindingPattern(declaration.name, /*includePatternInType*/ false, /*reportErrors*/ true); } + // Important to do this *after* attempt has been made to resolve via initializer + if (declaration.kind === SyntaxKind.Parameter) { + const inferredType = getParameterTypeFromBody(declaration); + if (inferredType) return inferredType; + } + // No type specified and nothing can be inferred return undefined; } @@ -12798,6 +12804,14 @@ namespace ts { return undefined; } + function getParameterTypeFromBody(parameter: ParameterDeclaration): Type { + const func = parameter.parent; + if (!func.body || isRestParameter(parameter)) return; + + const types = checkAndAggregateParameterExpressionTypes(parameter); + return types ? getWidenedType(getIntersectionType(types)) : undefined; + } + // Return contextual type of parameter or undefined if no contextual type is available function getContextuallyTypedParameterType(parameter: ParameterDeclaration): Type { const func = parameter.parent; @@ -16729,6 +16743,36 @@ namespace ts { return true; } + function checkAndAggregateParameterExpressionTypes(parameter: ParameterDeclaration): Type[] { + const func = parameter.parent; + const usageTypes: Type[] = []; + const invocations: CallExpression[] = []; + + function seekInvocations(f: FunctionBody) { + forEachInvocation(f, invocation => { + invocations.push(invocation); + invocation.arguments.filter(isFunctionExpressionOrArrowFunction).forEach(arg => seekInvocations(arg.body)); + }); + } + + seekInvocations(func.body); + + invocations.forEach(invocation => { + const usages = invocation.arguments + .map((arg, i) => ({ arg, symbol: getSymbolAtLocation(arg), i })) + .filter(({ symbol }) => symbol && symbol.valueDeclaration === parameter); + if (!usages.length) return; + const funcSymbol = getSymbolAtLocation(invocation.expression); + if (!funcSymbol || !isFunctionLike(funcSymbol.valueDeclaration)) return; + const sig = getSignatureFromDeclaration(funcSymbol.valueDeclaration); + const parameterTypes = sig.parameters.map(getTypeOfParameter); + const argumentTypes = usages.map(({ i }) => parameterTypes[i]).filter(t => !!t); + usageTypes.splice(0, 0, ...argumentTypes); + }); + + return usageTypes.length ? usageTypes : undefined; + } + function checkAndAggregateReturnExpressionTypes(func: FunctionLikeDeclaration, checkMode: CheckMode): Type[] { const functionFlags = getFunctionFlags(func); const aggregatedTypes: Type[] = []; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 76d242e8b08d8..50e44b777b1d3 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -757,6 +757,20 @@ namespace ts { return false; } + export function forEachInvocation(body: Block, visitor: (stmt: CallExpression) => T): T { + + return traverse(body); + + function traverse(node: Node): T { + switch (node.kind) { + case SyntaxKind.CallExpression: + return visitor(node); + default: + return forEachChild(node, traverse); + } + } + } + // Warning: This has the same semantics as the forEach family of functions, // in that traversal terminates in the event that 'visitor' supplies a truthy value. export function forEachReturnStatement(body: Block, visitor: (stmt: ReturnStatement) => T): T { diff --git a/tests/baselines/reference/controlFlowPropertyDeclarations.types b/tests/baselines/reference/controlFlowPropertyDeclarations.types index 286124ca346ae..71b23141644b9 100644 --- a/tests/baselines/reference/controlFlowPropertyDeclarations.types +++ b/tests/baselines/reference/controlFlowPropertyDeclarations.types @@ -196,8 +196,8 @@ function hyphenToCamelCase(string) { * Determines if the specified string consists entirely of whitespace. */ function isEmpty(string) { ->isEmpty : (string: any) => boolean ->string : any +>isEmpty : (string: string) => boolean +>string : string return !/[^\s]/.test(string); >!/[^\s]/.test(string) : boolean @@ -205,7 +205,7 @@ function isEmpty(string) { >/[^\s]/.test : (string: string) => boolean >/[^\s]/ : RegExp >test : (string: string) => boolean ->string : any +>string : string } /** @@ -216,15 +216,15 @@ function isEmpty(string) { * @return {boolean} */ function isConvertiblePixelValue(value) { ->isConvertiblePixelValue : (value: any) => boolean ->value : any +>isConvertiblePixelValue : (value: string) => boolean +>value : string return /^\d+px$/.test(value); >/^\d+px$/.test(value) : boolean >/^\d+px$/.test : (string: string) => boolean >/^\d+px$/ : RegExp >test : (string: string) => boolean ->value : any +>value : string } export class HTMLtoJSX { diff --git a/tests/baselines/reference/parameterInference.js b/tests/baselines/reference/parameterInference.js new file mode 100644 index 0000000000000..8859effa561fe --- /dev/null +++ b/tests/baselines/reference/parameterInference.js @@ -0,0 +1,57 @@ +//// [parameterInference.ts] +// CASE 1 +function foo(s) { + Math.sqrt(s) +} +// => function foo(s: number): void + +// CASE 2 +declare function swapNumberString(n: string): number; +declare function swapNumberString(n: number): string; + +function subs(s) { + return swapNumberString(s); +} +// => function subs(s: string): number +// NOTE: Still broken, needs to deal with overloads. Should have been inferred as: +// => (s: string) => number & (s: number) => string + +// CASE 3 +function f3(x: number){ + return x; +} + +function g3(x){ return f3(x); }; +// => function g3(x: number): number + +// CASE 4 +declare function f4(g: Function) +function g4(x) { + f4(() => { + Math.sqrt(x) + }) +} + + +//// [parameterInference.js] +// CASE 1 +function foo(s) { + Math.sqrt(s); +} +function subs(s) { + return swapNumberString(s); +} +// => function subs(s: string): number +// NOTE: Still broken, needs to deal with overloads. Should have been inferred as: +// => (s: string) => number & (s: number) => string +// CASE 3 +function f3(x) { + return x; +} +function g3(x) { return f3(x); } +; +function g4(x) { + f4(function () { + Math.sqrt(x); + }); +} diff --git a/tests/baselines/reference/parameterInference.symbols b/tests/baselines/reference/parameterInference.symbols new file mode 100644 index 0000000000000..2124c29a145da --- /dev/null +++ b/tests/baselines/reference/parameterInference.symbols @@ -0,0 +1,74 @@ +=== tests/cases/compiler/parameterInference.ts === +// CASE 1 +function foo(s) { +>foo : Symbol(foo, Decl(parameterInference.ts, 0, 0)) +>s : Symbol(s, Decl(parameterInference.ts, 1, 13)) + + Math.sqrt(s) +>Math.sqrt : Symbol(Math.sqrt, Decl(lib.d.ts, --, --)) +>Math : Symbol(Math, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) +>sqrt : Symbol(Math.sqrt, Decl(lib.d.ts, --, --)) +>s : Symbol(s, Decl(parameterInference.ts, 1, 13)) +} +// => function foo(s: number): void + +// CASE 2 +declare function swapNumberString(n: string): number; +>swapNumberString : Symbol(swapNumberString, Decl(parameterInference.ts, 3, 1), Decl(parameterInference.ts, 7, 53)) +>n : Symbol(n, Decl(parameterInference.ts, 7, 34)) + +declare function swapNumberString(n: number): string; +>swapNumberString : Symbol(swapNumberString, Decl(parameterInference.ts, 3, 1), Decl(parameterInference.ts, 7, 53)) +>n : Symbol(n, Decl(parameterInference.ts, 8, 34)) + +function subs(s) { +>subs : Symbol(subs, Decl(parameterInference.ts, 8, 53)) +>s : Symbol(s, Decl(parameterInference.ts, 10, 14)) + + return swapNumberString(s); +>swapNumberString : Symbol(swapNumberString, Decl(parameterInference.ts, 3, 1), Decl(parameterInference.ts, 7, 53)) +>s : Symbol(s, Decl(parameterInference.ts, 10, 14)) +} +// => function subs(s: string): number +// NOTE: Still broken, needs to deal with overloads. Should have been inferred as: +// => (s: string) => number & (s: number) => string + +// CASE 3 +function f3(x: number){ +>f3 : Symbol(f3, Decl(parameterInference.ts, 12, 1)) +>x : Symbol(x, Decl(parameterInference.ts, 18, 12)) + + return x; +>x : Symbol(x, Decl(parameterInference.ts, 18, 12)) +} + +function g3(x){ return f3(x); }; +>g3 : Symbol(g3, Decl(parameterInference.ts, 20, 1)) +>x : Symbol(x, Decl(parameterInference.ts, 22, 12)) +>f3 : Symbol(f3, Decl(parameterInference.ts, 12, 1)) +>x : Symbol(x, Decl(parameterInference.ts, 22, 12)) + +// => function g3(x: number): number + +// CASE 4 +declare function f4(g: Function) +>f4 : Symbol(f4, Decl(parameterInference.ts, 22, 32)) +>g : Symbol(g, Decl(parameterInference.ts, 26, 20)) +>Function : Symbol(Function, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) + +function g4(x) { +>g4 : Symbol(g4, Decl(parameterInference.ts, 26, 32)) +>x : Symbol(x, Decl(parameterInference.ts, 27, 12)) + + f4(() => { +>f4 : Symbol(f4, Decl(parameterInference.ts, 22, 32)) + + Math.sqrt(x) +>Math.sqrt : Symbol(Math.sqrt, Decl(lib.d.ts, --, --)) +>Math : Symbol(Math, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) +>sqrt : Symbol(Math.sqrt, Decl(lib.d.ts, --, --)) +>x : Symbol(x, Decl(parameterInference.ts, 27, 12)) + + }) +} + diff --git a/tests/baselines/reference/parameterInference.types b/tests/baselines/reference/parameterInference.types new file mode 100644 index 0000000000000..aef600dc9db92 --- /dev/null +++ b/tests/baselines/reference/parameterInference.types @@ -0,0 +1,80 @@ +=== tests/cases/compiler/parameterInference.ts === +// CASE 1 +function foo(s) { +>foo : (s: number) => void +>s : number + + Math.sqrt(s) +>Math.sqrt(s) : number +>Math.sqrt : (x: number) => number +>Math : Math +>sqrt : (x: number) => number +>s : number +} +// => function foo(s: number): void + +// CASE 2 +declare function swapNumberString(n: string): number; +>swapNumberString : { (n: string): number; (n: number): string; } +>n : string + +declare function swapNumberString(n: number): string; +>swapNumberString : { (n: string): number; (n: number): string; } +>n : number + +function subs(s) { +>subs : (s: string) => number +>s : string + + return swapNumberString(s); +>swapNumberString(s) : number +>swapNumberString : { (n: string): number; (n: number): string; } +>s : string +} +// => function subs(s: string): number +// NOTE: Still broken, needs to deal with overloads. Should have been inferred as: +// => (s: string) => number & (s: number) => string + +// CASE 3 +function f3(x: number){ +>f3 : (x: number) => number +>x : number + + return x; +>x : number +} + +function g3(x){ return f3(x); }; +>g3 : (x: number) => number +>x : number +>f3(x) : number +>f3 : (x: number) => number +>x : number + +// => function g3(x: number): number + +// CASE 4 +declare function f4(g: Function) +>f4 : (g: Function) => any +>g : Function +>Function : Function + +function g4(x) { +>g4 : (x: number) => void +>x : number + + f4(() => { +>f4(() => { Math.sqrt(x) }) : any +>f4 : (g: Function) => any +>() => { Math.sqrt(x) } : () => void + + Math.sqrt(x) +>Math.sqrt(x) : number +>Math.sqrt : (x: number) => number +>Math : Math +>sqrt : (x: number) => number +>x : number + + }) +} + diff --git a/tests/cases/compiler/parameterInference.ts b/tests/cases/compiler/parameterInference.ts new file mode 100644 index 0000000000000..b3f3ab21a2c68 --- /dev/null +++ b/tests/cases/compiler/parameterInference.ts @@ -0,0 +1,68 @@ +// CASE 1 - Propagation of parameter types from invocations +function f1(s) { + Math.sqrt(s) +} +// => function f1(s: number): void + +// CASE 2 - Inference from invocation of overloaded functions +declare function swapNumberString(n: string): number; +declare function swapNumberString(n: number): string; + +function f2(s) { + return swapNumberString(s); +} +// => function f2(s: string): number +// TODO: Broken, needs to deal with overloads. Should have been inferred as: +// => (s: string) => number & (s: number) => string + +// CASE 3 - Propagation of explicitly annotated parameter types +declare function f3(x: number) + +function g3(x){ + return f3(x); +} +// => function g3(x: number): any + +// CASE 4 - Inference from invocation sites within function expressions passed as arguments +declare function f4(g: Function) + +function g4(x) { + f4(() => { + Math.sqrt(x) + }) +} +// => function g4(x: number): void + +// CASE 5 - Ensure termination for recursive invocations +function f5(s) { + f5(s) +} +// => function f5(s: any): void + +// CASE 6 - Inference as intersection from multiple usages +function f6(x) { + Math.sqrt(x) + "".indexOf(x) +} +// => function f6(x: string & number): void + +// CASE 7 - Inference as overloaded function from control-flow discriminated usages +declare function isNumber(x: T): x is T & number + +function f7(x) { + return isNumber(x) ? Math.sqrt(x) : "".indexOf(x) +} +// function f7(x: string & number & T): number +// TODO: Broken, generic type parameter leaks and x is inferred too conservatively (string & number) +// Should be: +// => (s: string) => number & (s: number) => number + +// CASE 8 - Inference by substitution of constraints for generic functions +declare function generic(t: T) + +function f8(x) { + generic(x) +} +// function f8(x: T): void +// TODO: Broken, type parameter leaks. Should be inferred as: +// function f8(x: {}): void