diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 58248a8a9ee75..6d9f13c550513 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -226,6 +226,23 @@ namespace ts { return tryFindAmbientModule(moduleName, /*withAugmentations*/ false); }, getApparentType, + getUnionType, + createAnonymousType, + createSignature, + createSymbol, + createIndexInfo, + getAnyType: () => anyType, + getStringType: () => stringType, + getNumberType: () => numberType, + createPromiseType, + createArrayType, + getBooleanType: () => booleanType, + getVoidType: () => voidType, + getUndefinedType: () => undefinedType, + getNullType: () => nullType, + getESSymbolType: () => esSymbolType, + getNeverType: () => neverType, + isSymbolAccessible, isArrayLikeType, getAllPossiblePropertiesOfTypes, getSuggestionForNonexistentProperty: (node, type) => getSuggestionForNonexistentProperty(node, type), @@ -3664,6 +3681,7 @@ namespace ts { function buildParameterDisplay(p: Symbol, writer: SymbolWriter, enclosingDeclaration?: Node, flags?: TypeFormatFlags, symbolStack?: Symbol[]) { const parameterNode = p.valueDeclaration; + if (parameterNode ? isRestParameter(parameterNode) : isTransientSymbol(p) && p.isRestParameter) { writePunctuation(writer, SyntaxKind.DotDotDotToken); } diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 45a4a04b6ab4d..2654a3d5b5293 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -213,11 +213,13 @@ namespace ts { return undefined; } - export function zipWith(arrayA: ReadonlyArray, arrayB: ReadonlyArray, callback: (a: T, b: U, index: number) => void): void { + export function zipWith(arrayA: ReadonlyArray, arrayB: ReadonlyArray, callback: (a: T, b: U, index: number) => V): V[] { + const result: V[] = []; Debug.assert(arrayA.length === arrayB.length); for (let i = 0; i < arrayA.length; i++) { - callback(arrayA[i], arrayB[i], i); + result.push(callback(arrayA[i], arrayB[i], i)); } + return result; } export function zipToMap(keys: ReadonlyArray, values: ReadonlyArray): Map { diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index f3d6d4fcc4706..b6f90b280a0a6 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3669,6 +3669,7 @@ "category": "Message", "code": 90017 }, + "Disable checking for this file.": { "category": "Message", "code": 90018 @@ -3713,7 +3714,6 @@ "category": "Message", "code": 90028 }, - "Convert function to an ES2015 class": { "category": "Message", "code": 95001 @@ -3722,34 +3722,36 @@ "category": "Message", "code": 95002 }, - "Extract symbol": { "category": "Message", "code": 95003 }, - "Extract to {0} in {1}": { "category": "Message", "code": 95004 }, - "Extract function": { "category": "Message", "code": 95005 }, - "Extract constant": { "category": "Message", "code": 95006 }, - "Extract to {0} in enclosing scope": { "category": "Message", "code": 95007 }, - "Extract to {0} in {1} scope": { "category": "Message", "code": 95008 + }, + "Infer type of '{0}' from usage.": { + "category": "Message", + "code": 95009 + }, + "Infer parameter types from usage.": { + "category": "Message", + "code": 95010 } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 578c2d23c4f39..4d5460eb163e0 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2689,6 +2689,24 @@ namespace ts { getSuggestionForNonexistentSymbol(location: Node, name: string, meaning: SymbolFlags): string | undefined; /* @internal */ getBaseConstraintOfType(type: Type): Type | undefined; + /* @internal */ getAnyType(): Type; + /* @internal */ getStringType(): Type; + /* @internal */ getNumberType(): Type; + /* @internal */ getBooleanType(): Type; + /* @internal */ getVoidType(): Type; + /* @internal */ getUndefinedType(): Type; + /* @internal */ getNullType(): Type; + /* @internal */ getESSymbolType(): Type; + /* @internal */ getNeverType(): Type; + /* @internal */ getUnionType(types: Type[], subtypeReduction?: boolean): Type; + /* @internal */ createArrayType(elementType: Type): Type; + /* @internal */ createPromiseType(type: Type): Type; + + /* @internal */ createAnonymousType(symbol: Symbol, members: SymbolTable, callSignatures: Signature[], constructSignatures: Signature[], stringIndexInfo: IndexInfo, numberIndexInfo: IndexInfo): Type; + /* @internal */ createSignature(declaration: SignatureDeclaration, typeParameters: TypeParameter[], thisParameter: Symbol | undefined, parameters: Symbol[], resolvedReturnType: Type, typePredicate: TypePredicate, minArgumentCount: number, hasRestParameter: boolean, hasLiteralTypes: boolean): Signature; + /* @internal */ createSymbol(flags: SymbolFlags, name: __String): TransientSymbol; + /* @internal */ createIndexInfo(type: Type, isReadonly: boolean, declaration?: SignatureDeclaration): IndexInfo; + /* @internal */ isSymbolAccessible(symbol: Symbol, enclosingDeclaration: Node, meaning: SymbolFlags, shouldComputeAliasToMarkVisible: boolean): SymbolAccessibilityResult; /* @internal */ tryFindAmbientModuleWithoutAugmentations(moduleName: string): Symbol | undefined; /* @internal */ getSymbolWalker(accept?: (symbol: Symbol) => boolean): SymbolWalker; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index f0eb394adb7dd..867cd2c5c8f30 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -5649,6 +5649,14 @@ namespace ts { return node.kind >= SyntaxKind.FirstJSDocTagNode && node.kind <= SyntaxKind.LastJSDocTagNode; } + export function isSetAccessor(node: Node): node is SetAccessorDeclaration { + return node.kind === SyntaxKind.SetAccessor; + } + + export function isGetAccessor(node: Node): node is GetAccessorDeclaration { + return node.kind === SyntaxKind.GetAccessor; + } + /** True if has jsdoc nodes attached to it. */ /* @internal */ export function hasJSDocNodes(node: Node): node is HasJSDoc { diff --git a/src/services/codefixes/fixes.ts b/src/services/codefixes/fixes.ts index b024dfae7cd82..7ee0aaa679973 100644 --- a/src/services/codefixes/fixes.ts +++ b/src/services/codefixes/fixes.ts @@ -13,3 +13,4 @@ /// /// /// +/// diff --git a/src/services/codefixes/inferFromUsage.ts b/src/services/codefixes/inferFromUsage.ts new file mode 100644 index 0000000000000..046eb915c1ef6 --- /dev/null +++ b/src/services/codefixes/inferFromUsage.ts @@ -0,0 +1,653 @@ +/* @internal */ +namespace ts.codefix { + registerCodeFix({ + errorCodes: [ + // Variable declarations + Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined.code, + + // Variable uses + Diagnostics.Variable_0_implicitly_has_an_1_type.code, + + // Parameter declarations + Diagnostics.Parameter_0_implicitly_has_an_1_type.code, + Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code, + + // Get Accessor declarations + Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation.code, + Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type.code, + + // Set Accessor declarations + Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation.code, + + // Property declarations + Diagnostics.Member_0_implicitly_has_an_1_type.code, + ], + getCodeActions: getActionsForAddExplicitTypeAnnotation + }); + + function getActionsForAddExplicitTypeAnnotation({ sourceFile, program, span: { start }, errorCode, cancellationToken }: CodeFixContext): CodeAction[] | undefined { + const token = getTokenAtPosition(sourceFile, start, /*includeJsDocComment*/ false); + let writer: StringSymbolWriter; + + if (isInJavaScriptFile(token)) { + return undefined; + } + + switch (token.kind) { + case SyntaxKind.Identifier: + case SyntaxKind.DotDotDotToken: + case SyntaxKind.PublicKeyword: + case SyntaxKind.PrivateKeyword: + case SyntaxKind.ProtectedKeyword: + case SyntaxKind.ReadonlyKeyword: + // Allowed + break; + default: + return undefined; + } + + const containingFunction = getContainingFunction(token); + const checker = program.getTypeChecker(); + + switch (errorCode) { + // Variable and Property declarations + case Diagnostics.Member_0_implicitly_has_an_1_type.code: + case Diagnostics.Variable_0_implicitly_has_type_1_in_some_locations_where_its_type_cannot_be_determined.code: + return getCodeActionForVariableDeclaration(token.parent); + case Diagnostics.Variable_0_implicitly_has_an_1_type.code: + return getCodeActionForVariableUsage(token); + + // Parameter declarations + case Diagnostics.Parameter_0_implicitly_has_an_1_type.code: + if (isSetAccessor(containingFunction)) { + return getCodeActionForSetAccessor(containingFunction); + } + // falls through + case Diagnostics.Rest_parameter_0_implicitly_has_an_any_type.code: + return getCodeActionForParameters(token.parent); + + // Get Accessor declarations + case Diagnostics.Property_0_implicitly_has_type_any_because_its_get_accessor_lacks_a_return_type_annotation.code: + case Diagnostics._0_which_lacks_return_type_annotation_implicitly_has_an_1_return_type.code: + return isGetAccessor(containingFunction) ? getCodeActionForGetAccessor(containingFunction) : undefined; + + // Set Accessor declarations + case Diagnostics.Property_0_implicitly_has_type_any_because_its_set_accessor_lacks_a_parameter_type_annotation.code: + return isSetAccessor(containingFunction) ? getCodeActionForSetAccessor(containingFunction) : undefined; + } + + return undefined; + + function getCodeActionForVariableDeclaration(declaration: VariableDeclaration | PropertyDeclaration | PropertySignature) { + if (!isIdentifier(declaration.name)) { + return undefined; + } + + const type = inferTypeForVariableFromUsage(declaration.name); + const typeString = type && typeToString(type, declaration); + + if (!typeString) { + return undefined; + } + + return createCodeActions(declaration.name.getText(), declaration.name.getEnd(), `: ${typeString}`); + } + + function getCodeActionForVariableUsage(token: Identifier) { + const symbol = checker.getSymbolAtLocation(token); + return symbol && symbol.valueDeclaration && getCodeActionForVariableDeclaration(symbol.valueDeclaration); + } + + function isApplicableFunctionForInference(declaration: FunctionLike): declaration is MethodDeclaration | FunctionDeclaration | ConstructorDeclaration { + switch (declaration.kind) { + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.Constructor: + return true; + case SyntaxKind.FunctionExpression: + return !!(declaration as FunctionExpression).name; + } + return false; + } + + function getCodeActionForParameters(parameterDeclaration: ParameterDeclaration): CodeAction[] { + if (!isIdentifier(parameterDeclaration.name) || !isApplicableFunctionForInference(containingFunction)) { + return undefined; + } + + const types = inferTypeForParametersFromUsage(containingFunction) || + map(containingFunction.parameters, p => isIdentifier(p.name) && inferTypeForVariableFromUsage(p.name)); + + if (!types) { + return undefined; + } + + const textChanges: TextChange[] = zipWith(containingFunction.parameters, types, (parameter, type) => { + if (type && !parameter.type && !parameter.initializer) { + const typeString = typeToString(type, containingFunction); + return typeString ? { + span: { start: parameter.end, length: 0 }, + newText: `: ${typeString}` + } : undefined; + } + }).filter(c => !!c); + + return textChanges.length ? [{ + description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Infer_parameter_types_from_usage), [parameterDeclaration.name.getText()]), + changes: [{ + fileName: sourceFile.fileName, + textChanges + }] + }] : undefined; + } + + function getCodeActionForSetAccessor(setAccessorDeclaration: SetAccessorDeclaration) { + const setAccessorParameter = setAccessorDeclaration.parameters[0]; + if (!setAccessorParameter || !isIdentifier(setAccessorDeclaration.name) || !isIdentifier(setAccessorParameter.name)) { + return undefined; + } + + const type = inferTypeForVariableFromUsage(setAccessorDeclaration.name) || + inferTypeForVariableFromUsage(setAccessorParameter.name); + const typeString = type && typeToString(type, containingFunction); + if (!typeString) { + return undefined; + } + + return createCodeActions(setAccessorDeclaration.name.getText(), setAccessorParameter.name.getEnd(), `: ${typeString}`); + } + + function getCodeActionForGetAccessor(getAccessorDeclaration: GetAccessorDeclaration) { + if (!isIdentifier(getAccessorDeclaration.name)) { + return undefined; + } + + const type = inferTypeForVariableFromUsage(getAccessorDeclaration.name); + const typeString = type && typeToString(type, containingFunction); + if (!typeString) { + return undefined; + } + + const closeParenToken = getFirstChildOfKind(getAccessorDeclaration, sourceFile, SyntaxKind.CloseParenToken); + return createCodeActions(getAccessorDeclaration.name.getText(), closeParenToken.getEnd(), `: ${typeString}`); + } + + function createCodeActions(name: string, start: number, typeString: string) { + return [{ + description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Infer_type_of_0_from_usage), [name]), + changes: [{ + fileName: sourceFile.fileName, + textChanges: [{ + span: { start, length: 0 }, + newText: typeString + }] + }] + }]; + } + + function getReferences(token: PropertyName | Token) { + const references = FindAllReferences.findReferencedSymbols( + program, + cancellationToken, + program.getSourceFiles(), + token.getSourceFile(), + token.getStart()); + + Debug.assert(!!references, "Found no references!"); + Debug.assert(references.length === 1, "Found more references than expected"); + + return map(references[0].references, r => getTokenAtPosition(program.getSourceFile(r.fileName), r.textSpan.start, /*includeJsDocComment*/ false)); + } + + function inferTypeForVariableFromUsage(token: Identifier) { + return InferFromReference.inferTypeFromReferences(getReferences(token), checker, cancellationToken); + } + + function inferTypeForParametersFromUsage(containingFunction: FunctionLikeDeclaration) { + switch (containingFunction.kind) { + case SyntaxKind.Constructor: + case SyntaxKind.FunctionExpression: + case SyntaxKind.FunctionDeclaration: + case SyntaxKind.MethodDeclaration: + const isConstructor = containingFunction.kind === SyntaxKind.Constructor; + const searchToken = isConstructor ? + >getFirstChildOfKind(containingFunction, sourceFile, SyntaxKind.ConstructorKeyword) : + containingFunction.name; + if (searchToken) { + return InferFromReference.inferTypeForParametersFromReferences(getReferences(searchToken), containingFunction, checker, cancellationToken); + } + } + } + + function getTypeAccessiblityWriter() { + if (!writer) { + let str = ""; + let typeIsAccessible = true; + + const writeText: (text: string) => void = text => str += text; + writer = { + string: () => typeIsAccessible ? str : undefined, + writeKeyword: writeText, + writeOperator: writeText, + writePunctuation: writeText, + writeSpace: writeText, + writeStringLiteral: writeText, + writeParameter: writeText, + writeProperty: writeText, + writeSymbol: writeText, + writeLine: () => str += " ", + increaseIndent: noop, + decreaseIndent: noop, + clear: () => { str = ""; typeIsAccessible = true; }, + trackSymbol: (symbol, declaration, meaning) => { + if (checker.isSymbolAccessible(symbol, declaration, meaning, /*shouldComputeAliasToMarkVisible*/ false).accessibility !== SymbolAccessibility.Accessible) { + typeIsAccessible = false; + } + }, + reportInaccessibleThisError: () => { typeIsAccessible = false; }, + reportPrivateInBaseOfClassExpression: () => { typeIsAccessible = false; }, + }; + } + writer.clear(); + return writer; + } + + function typeToString(type: Type, enclosingDeclaration: Declaration) { + const writer = getTypeAccessiblityWriter(); + checker.getSymbolDisplayBuilder().buildTypeDisplay(type, writer, enclosingDeclaration); + return writer.string(); + } + + function getFirstChildOfKind(node: Node, sourcefile: SourceFile, kind: SyntaxKind) { + for (const child of node.getChildren(sourcefile)) { + if (child.kind === kind) return child; + } + return undefined; + } + } + + namespace InferFromReference { + interface CallContext { + argumentTypes: Type[]; + returnType: UsageContext; + } + + interface UsageContext { + isNumber?: boolean; + isString?: boolean; + isNumberOrString?: boolean; + candidateTypes?: Type[]; + properties?: UnderscoreEscapedMap; + callContexts?: CallContext[]; + constructContexts?: CallContext[]; + numberIndexContext?: UsageContext; + stringIndexContext?: UsageContext; + } + + export function inferTypeFromReferences(references: Identifier[], checker: TypeChecker, cancellationToken: CancellationToken): Type | undefined { + const usageContext: UsageContext = {}; + for (const reference of references) { + cancellationToken.throwIfCancellationRequested(); + inferTypeFromContext(reference, checker, usageContext); + } + return getTypeFromUsageContext(usageContext, checker); + } + + export function inferTypeForParametersFromReferences(references: Identifier[], declaration: FunctionLikeDeclaration, checker: TypeChecker, cancellationToken: CancellationToken): (Type | undefined)[] | undefined { + if (declaration.parameters) { + const usageContext: UsageContext = {}; + for (const reference of references) { + cancellationToken.throwIfCancellationRequested(); + inferTypeFromContext(reference, checker, usageContext); + } + const isConstructor = declaration.kind === SyntaxKind.Constructor; + const callContexts = isConstructor ? usageContext.constructContexts : usageContext.callContexts; + if (callContexts) { + const paramTypes: Type[] = []; + for (let parameterIndex = 0; parameterIndex < declaration.parameters.length; parameterIndex++) { + let types: Type[] = []; + const isRestParameter = ts.isRestParameter(declaration.parameters[parameterIndex]); + for (const callContext of callContexts) { + if (callContext.argumentTypes.length > parameterIndex) { + if (isRestParameter) { + types = concatenate(types, map(callContext.argumentTypes.slice(parameterIndex), a => checker.getBaseTypeOfLiteralType(a))); + } + else { + types.push(checker.getBaseTypeOfLiteralType(callContext.argumentTypes[parameterIndex])); + } + } + } + if (types.length) { + const type = checker.getWidenedType(checker.getUnionType(types, /*subtypeReduction*/ true)); + paramTypes[parameterIndex] = isRestParameter ? checker.createArrayType(type) : type; + } + } + return paramTypes; + } + } + return undefined; + } + + function inferTypeFromContext(node: Expression, checker: TypeChecker, usageContext: UsageContext): void { + while (isRightSideOfQualifiedNameOrPropertyAccess(node)) { + node = node.parent; + } + + switch (node.parent.kind) { + case SyntaxKind.PostfixUnaryExpression: + usageContext.isNumber = true; + break; + case SyntaxKind.PrefixUnaryExpression: + inferTypeFromPrefixUnaryExpressionContext(node.parent, usageContext); + break; + case SyntaxKind.BinaryExpression: + inferTypeFromBinaryExpressionContext(node, node.parent, checker, usageContext); + break; + case SyntaxKind.CaseClause: + case SyntaxKind.DefaultClause: + inferTypeFromSwitchStatementLabelContext(node.parent, checker, usageContext); + break; + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + if ((node.parent).expression === node) { + inferTypeFromCallExpressionContext(node.parent, checker, usageContext); + } + else { + inferTypeFromContextualType(node, checker, usageContext); + } + break; + case SyntaxKind.PropertyAccessExpression: + inferTypeFromPropertyAccessExpressionContext(node.parent, checker, usageContext); + break; + case SyntaxKind.ElementAccessExpression: + inferTypeFromPropertyElementExpressionContext(node.parent, node, checker, usageContext); + break; + default: + return inferTypeFromContextualType(node, checker, usageContext); + } + } + + function inferTypeFromContextualType(node: Expression, checker: TypeChecker, usageContext: UsageContext): void { + if (isPartOfExpression(node)) { + addCandidateType(usageContext, checker.getContextualType(node)); + } + } + + function inferTypeFromPrefixUnaryExpressionContext(node: PrefixUnaryExpression, usageContext: UsageContext): void { + switch (node.operator) { + case SyntaxKind.PlusPlusToken: + case SyntaxKind.MinusMinusToken: + case SyntaxKind.MinusToken: + case SyntaxKind.TildeToken: + usageContext.isNumber = true; + break; + + case SyntaxKind.PlusToken: + usageContext.isNumberOrString = true; + break; + + // case SyntaxKind.ExclamationToken: + // no inferences here; + } + } + + function inferTypeFromBinaryExpressionContext(node: Expression, parent: BinaryExpression, checker: TypeChecker, usageContext: UsageContext): void { + switch (parent.operatorToken.kind) { + // ExponentiationOperator + case SyntaxKind.AsteriskAsteriskToken: + + // MultiplicativeOperator + case SyntaxKind.AsteriskToken: + case SyntaxKind.SlashToken: + case SyntaxKind.PercentToken: + + // ShiftOperator + case SyntaxKind.LessThanLessThanToken: + case SyntaxKind.GreaterThanGreaterThanToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + + // BitwiseOperator + case SyntaxKind.AmpersandToken: + case SyntaxKind.BarToken: + case SyntaxKind.CaretToken: + + // CompoundAssignmentOperator + case SyntaxKind.MinusEqualsToken: + case SyntaxKind.AsteriskAsteriskEqualsToken: + case SyntaxKind.AsteriskEqualsToken: + case SyntaxKind.SlashEqualsToken: + case SyntaxKind.PercentEqualsToken: + case SyntaxKind.AmpersandEqualsToken: + case SyntaxKind.BarEqualsToken: + case SyntaxKind.CaretEqualsToken: + case SyntaxKind.LessThanLessThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanEqualsToken: + + // AdditiveOperator + case SyntaxKind.MinusToken: + + // RelationalOperator + case SyntaxKind.LessThanToken: + case SyntaxKind.LessThanEqualsToken: + case SyntaxKind.GreaterThanToken: + case SyntaxKind.GreaterThanEqualsToken: + const operandType = checker.getTypeAtLocation(parent.left === node ? parent.right : parent.left); + if (operandType.flags & TypeFlags.EnumLike) { + addCandidateType(usageContext, operandType); + } + else { + usageContext.isNumber = true; + } + break; + + case SyntaxKind.PlusEqualsToken: + case SyntaxKind.PlusToken: + const otherOperandType = checker.getTypeAtLocation(parent.left === node ? parent.right : parent.left); + if (otherOperandType.flags & TypeFlags.EnumLike) { + addCandidateType(usageContext, otherOperandType); + } + else if (otherOperandType.flags & TypeFlags.NumberLike) { + usageContext.isNumber = true; + } + else if (otherOperandType.flags & TypeFlags.StringLike) { + usageContext.isString = true; + } + else { + usageContext.isNumberOrString = true; + } + break; + + // AssignmentOperators + case SyntaxKind.EqualsToken: + case SyntaxKind.EqualsEqualsToken: + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + addCandidateType(usageContext, checker.getTypeAtLocation(parent.left === node ? parent.right : parent.left)); + break; + + case SyntaxKind.InKeyword: + if (node === parent.left) { + usageContext.isString = true; + } + break; + + // LogicalOperator + case SyntaxKind.BarBarToken: + if (node === parent.left && + (node.parent.parent.kind === SyntaxKind.VariableDeclaration || isAssignmentExpression(node.parent.parent, /*excludeCompoundAssignment*/ true))) { + // var x = x || {}; + // TODO: use getFalsyflagsOfType + addCandidateType(usageContext, checker.getTypeAtLocation(parent.right)); + } + break; + + case SyntaxKind.AmpersandAmpersandToken: + case SyntaxKind.CommaToken: + case SyntaxKind.InstanceOfKeyword: + // nothing to infer here + break; + } + } + + function inferTypeFromSwitchStatementLabelContext(parent: CaseOrDefaultClause, checker: TypeChecker, usageContext: UsageContext): void { + addCandidateType(usageContext, checker.getTypeAtLocation((parent.parent.parent).expression)); + } + + function inferTypeFromCallExpressionContext(parent: CallExpression | NewExpression, checker: TypeChecker, usageContext: UsageContext): void { + const callContext: CallContext = { + argumentTypes: [], + returnType: {} + }; + + if (parent.arguments) { + for (const argument of parent.arguments) { + callContext.argumentTypes.push(checker.getTypeAtLocation(argument)); + } + } + + inferTypeFromContext(parent, checker, callContext.returnType); + if (parent.kind === SyntaxKind.CallExpression) { + (usageContext.callContexts || (usageContext.callContexts = [])).push(callContext); + } + else { + (usageContext.constructContexts || (usageContext.constructContexts = [])).push(callContext); + } + } + + function inferTypeFromPropertyAccessExpressionContext(parent: PropertyAccessExpression, checker: TypeChecker, usageContext: UsageContext): void { + const name = escapeLeadingUnderscores(parent.name.text); + if (!usageContext.properties) { + usageContext.properties = createUnderscoreEscapedMap(); + } + const propertyUsageContext = {}; + inferTypeFromContext(parent, checker, propertyUsageContext); + usageContext.properties.set(name, propertyUsageContext); + } + + function inferTypeFromPropertyElementExpressionContext(parent: ElementAccessExpression, node: Expression, checker: TypeChecker, usageContext: UsageContext): void { + if (node === parent.argumentExpression) { + usageContext.isNumberOrString = true; + return; + } + else { + const indexType = checker.getTypeAtLocation(parent); + const indexUsageContext = {}; + inferTypeFromContext(parent, checker, indexUsageContext); + if (indexType.flags & TypeFlags.NumberLike) { + usageContext.numberIndexContext = indexUsageContext; + } + else { + usageContext.stringIndexContext = indexUsageContext; + } + } + } + + function getTypeFromUsageContext(usageContext: UsageContext, checker: TypeChecker): Type | undefined { + if (usageContext.isNumberOrString && !usageContext.isNumber && !usageContext.isString) { + return checker.getUnionType([checker.getNumberType(), checker.getStringType()]); + } + else if (usageContext.isNumber) { + return checker.getNumberType(); + } + else if (usageContext.isString) { + return checker.getStringType(); + } + else if (usageContext.candidateTypes) { + return checker.getWidenedType(checker.getUnionType(map(usageContext.candidateTypes, t => checker.getBaseTypeOfLiteralType(t)), /*subtypeReduction*/ true)); + } + else if (usageContext.properties && hasCallContext(usageContext.properties.get("then" as __String))) { + const paramType = getParameterTypeFromCallContexts(0, usageContext.properties.get("then" as __String).callContexts, /*isRestParameter*/ false, checker); + const types = paramType.getCallSignatures().map(c => c.getReturnType()); + return checker.createPromiseType(types.length ? checker.getUnionType(types, /*subtypeReduction*/ true) : checker.getAnyType()); + } + else if (usageContext.properties && hasCallContext(usageContext.properties.get("push" as __String))) { + return checker.createArrayType(getParameterTypeFromCallContexts(0, usageContext.properties.get("push" as __String).callContexts, /*isRestParameter*/ false, checker)); + } + else if (usageContext.properties || usageContext.callContexts || usageContext.constructContexts || usageContext.numberIndexContext || usageContext.stringIndexContext) { + const members = createUnderscoreEscapedMap(); + const callSignatures: Signature[] = []; + const constructSignatures: Signature[] = []; + let stringIndexInfo: IndexInfo; + let numberIndexInfo: IndexInfo; + + if (usageContext.properties) { + usageContext.properties.forEach((context, name) => { + const symbol = checker.createSymbol(SymbolFlags.Property, name); + symbol.type = getTypeFromUsageContext(context, checker); + members.set(name, symbol); + }); + } + + if (usageContext.callContexts) { + for (const callContext of usageContext.callContexts) { + callSignatures.push(getSignatureFromCallContext(callContext, checker)); + } + } + + if (usageContext.constructContexts) { + for (const constructContext of usageContext.constructContexts) { + constructSignatures.push(getSignatureFromCallContext(constructContext, checker)); + } + } + + if (usageContext.numberIndexContext) { + numberIndexInfo = checker.createIndexInfo(getTypeFromUsageContext(usageContext.numberIndexContext, checker), /*isReadonly*/ false); + } + + if (usageContext.stringIndexContext) { + stringIndexInfo = checker.createIndexInfo(getTypeFromUsageContext(usageContext.stringIndexContext, checker), /*isReadonly*/ false); + } + + return checker.createAnonymousType(/*symbol*/ undefined, members, callSignatures, constructSignatures, stringIndexInfo, numberIndexInfo); + } + else { + return undefined; + } + } + + function getParameterTypeFromCallContexts(parameterIndex: number, callContexts: CallContext[], isRestParameter: boolean, checker: TypeChecker) { + let types: Type[] = []; + if (callContexts) { + for (const callContext of callContexts) { + if (callContext.argumentTypes.length > parameterIndex) { + if (isRestParameter) { + types = concatenate(types, map(callContext.argumentTypes.slice(parameterIndex), a => checker.getBaseTypeOfLiteralType(a))); + } + else { + types.push(checker.getBaseTypeOfLiteralType(callContext.argumentTypes[parameterIndex])); + } + } + } + } + + if (types.length) { + const type = checker.getWidenedType(checker.getUnionType(types, /*subtypeReduction*/ true)); + return isRestParameter ? checker.createArrayType(type) : type; + } + return undefined; + } + + function getSignatureFromCallContext(callContext: CallContext, checker: TypeChecker): Signature { + const parameters: Symbol[] = []; + for (let i = 0; i < callContext.argumentTypes.length; i++) { + const symbol = checker.createSymbol(SymbolFlags.FunctionScopedVariable, escapeLeadingUnderscores(`arg${i}`)); + symbol.type = checker.getWidenedType(checker.getBaseTypeOfLiteralType(callContext.argumentTypes[i])); + parameters.push(symbol); + } + const returnType = getTypeFromUsageContext(callContext.returnType, checker); + return checker.createSignature(/*declaration*/ undefined, /*typeParameters*/ undefined, /*thisParameter*/ undefined, parameters, returnType, /*typePredicate*/ undefined, callContext.argumentTypes.length, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false); + } + + function addCandidateType(context: UsageContext, type: Type) { + if (type && !(type.flags & TypeFlags.Any) && !(type.flags & TypeFlags.Never)) { + (context.candidateTypes || (context.candidateTypes = [])).push(type); + } + } + + function hasCallContext(usageContext: UsageContext) { + return usageContext && usageContext.callContexts; + } + } +} diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 3bb2ed1167488..189095325cc45 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3058,6 +3058,8 @@ declare namespace ts { function isCaseOrDefaultClause(node: Node): node is CaseOrDefaultClause; /** True if node is of a kind that may contain comment text. */ function isJSDocCommentContainingNode(node: Node): boolean; + function isSetAccessor(node: Node): node is SetAccessorDeclaration; + function isGetAccessor(node: Node): node is GetAccessorDeclaration; } declare namespace ts { interface ErrorCallback { diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index d41db2eb413ee..badf8ec067226 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3113,6 +3113,8 @@ declare namespace ts { function isCaseOrDefaultClause(node: Node): node is CaseOrDefaultClause; /** True if node is of a kind that may contain comment text. */ function isJSDocCommentContainingNode(node: Node): boolean; + function isSetAccessor(node: Node): node is SetAccessorDeclaration; + function isGetAccessor(node: Node): node is GetAccessorDeclaration; } declare namespace ts { function createNode(kind: SyntaxKind, pos?: number, end?: number): Node; diff --git a/tests/cases/fourslash/codeFixInferFromUsage.ts b/tests/cases/fourslash/codeFixInferFromUsage.ts new file mode 100644 index 0000000000000..5de8d98979994 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsage.ts @@ -0,0 +1,9 @@ +/// + +// @noImplicitAny: true +////[|var foo;|] +////function f() { +//// foo += 2; +////} + +verify.rangeAfterCodeFix("var foo: number;",/*includeWhiteSpace*/ undefined, /*errorCode*/ undefined, 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageGetter.ts b/tests/cases/fourslash/codeFixInferFromUsageGetter.ts new file mode 100644 index 0000000000000..f83eed1432d49 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageGetter.ts @@ -0,0 +1,10 @@ +/// + +// @noImplicitAny: true +////declare class C { +//// [|get x();|] +////} +////} +////(new C).x = 1; + +verify.rangeAfterCodeFix("get x(): number;", undefined, undefined, 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageGetter2.ts b/tests/cases/fourslash/codeFixInferFromUsageGetter2.ts new file mode 100644 index 0000000000000..a50d471ad12d3 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageGetter2.ts @@ -0,0 +1,11 @@ +/// + +// @noImplicitAny: true +////class C { +//// [|get x() |]{ +//// return undefined; +//// } +////} +////(new C).x = 1; + +verify.rangeAfterCodeFix("get x(): number", undefined, undefined, 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageInaccessibleTypes.ts b/tests/cases/fourslash/codeFixInferFromUsageInaccessibleTypes.ts new file mode 100644 index 0000000000000..ae9106be8c4f0 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageInaccessibleTypes.ts @@ -0,0 +1,20 @@ +/// + +// @noImplicitAny: true +////function f1([|a |]) { } +////function h1() { +//// class C { p: number }; +//// f1({ ofTypeC: new C() }); +////} +//// +////function f2([|a |]) { } +////function h2() { +//// interface I { a: number } +//// var i: I = {a : 1}; +//// f2(i); +//// f2(2); +//// f2(false); +////} +//// + +verify.not.codeFixAvailable(); diff --git a/tests/cases/fourslash/codeFixInferFromUsageMember.ts b/tests/cases/fourslash/codeFixInferFromUsageMember.ts new file mode 100644 index 0000000000000..c82542e5e3f25 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageMember.ts @@ -0,0 +1,11 @@ +/// + +// @noImplicitAny: true +////class C { +//// [|p;|] +//// method() { +//// this.p.push(10); +//// } +////} + +verify.rangeAfterCodeFix("p: number[];"); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageMember2.ts b/tests/cases/fourslash/codeFixInferFromUsageMember2.ts new file mode 100644 index 0000000000000..486112935eefd --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageMember2.ts @@ -0,0 +1,10 @@ +/// + +// @noImplicitAny: true +////interface I { +//// [|p;|] +////} +////var i: I; +////i.p = 0; + +verify.rangeAfterCodeFix("p: number;"); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageMember3.ts b/tests/cases/fourslash/codeFixInferFromUsageMember3.ts new file mode 100644 index 0000000000000..49f2cb66c18cd --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageMember3.ts @@ -0,0 +1,9 @@ +/// + +// @noImplicitAny: true +////class C { +//// constructor([|public p)|] { } +////} +////new C("string"); + +verify.rangeAfterCodeFix("public p: string)"); diff --git a/tests/cases/fourslash/codeFixInferFromUsageMultipleParameters.ts b/tests/cases/fourslash/codeFixInferFromUsageMultipleParameters.ts new file mode 100644 index 0000000000000..89e2c935e876f --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageMultipleParameters.ts @@ -0,0 +1,9 @@ +/// + +// @noImplicitAny: true +//// function f([|a, b, c, d: number, e = 0, ...d |]) { +//// } +//// f(1, "string", { a: 1 }, {shouldNotBeHere: 2}, {shouldNotBeHere: 2}, 3, "string"); + + +verify.rangeAfterCodeFix("a: number, b: string, c: { a: number; }, d: number, e = 0, ...d: (string | number)[]", /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 1); diff --git a/tests/cases/fourslash/codeFixInferFromUsageOptionalParam.ts b/tests/cases/fourslash/codeFixInferFromUsageOptionalParam.ts new file mode 100644 index 0000000000000..f10de4bb03ae8 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageOptionalParam.ts @@ -0,0 +1,9 @@ +/// + +// @noImplicitAny: true +////function f([|a? |]){ +////} +////f(); +////f(1); + +verify.rangeAfterCodeFix("a?: number"); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageOptionalParam2.ts b/tests/cases/fourslash/codeFixInferFromUsageOptionalParam2.ts new file mode 100644 index 0000000000000..2ca820e0327c0 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageOptionalParam2.ts @@ -0,0 +1,8 @@ +/// + +// @noImplicitAny: true +////function f([|a? |]){ +//// if (a < 9) return; +////} + +verify.rangeAfterCodeFix("a?: number"); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageRestParam.ts b/tests/cases/fourslash/codeFixInferFromUsageRestParam.ts new file mode 100644 index 0000000000000..963f84d651530 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageRestParam.ts @@ -0,0 +1,11 @@ +/// + +// @noImplicitAny: true +////function f(a: number, [|...rest |]){ +////} +////f(1); +////f(2, "s1"); +////f(3, "s1", "s2"); +////f(3, "s1", "s2", "s3", "s4"); + +verify.rangeAfterCodeFix("...rest: string[]"); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageRestParam2.ts b/tests/cases/fourslash/codeFixInferFromUsageRestParam2.ts new file mode 100644 index 0000000000000..ae8262402281e --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageRestParam2.ts @@ -0,0 +1,11 @@ +/// + +// @noImplicitAny: true +////function f(a: number, [|...rest |]){ +////} +////f(1); +////f(2, "s1"); +////f(3, false, "s2"); +////f(4, "s1", "s2", false, "s4"); + +verify.rangeAfterCodeFix("...rest: (string | boolean)[]"); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageRestParam3.ts b/tests/cases/fourslash/codeFixInferFromUsageRestParam3.ts new file mode 100644 index 0000000000000..4752176a324a2 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageRestParam3.ts @@ -0,0 +1,8 @@ +/// + +// @noImplicitAny: true +////function f(a: number, [|...rest |]){ +//// rest.push(22); +////} + +verify.rangeAfterCodeFix("...rest: number[]"); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageSetter.ts b/tests/cases/fourslash/codeFixInferFromUsageSetter.ts new file mode 100644 index 0000000000000..f515cd5a90687 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageSetter.ts @@ -0,0 +1,10 @@ +/// + +// @noImplicitAny: true +////class C { +//// set [|x(v)|] { +//// } +////} +////(new C).x = 1; + +verify.rangeAfterCodeFix("x(v: number)", undefined, undefined, 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageSetter2.ts b/tests/cases/fourslash/codeFixInferFromUsageSetter2.ts new file mode 100644 index 0000000000000..a1169a03df16b --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageSetter2.ts @@ -0,0 +1,10 @@ +/// + +// @noImplicitAny: true +////class C { +//// set [|x(v)|] { +//// } +////} +////(new C).x = 1; + +verify.rangeAfterCodeFix("x(v: number)", undefined, undefined, 1); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageVariable.ts b/tests/cases/fourslash/codeFixInferFromUsageVariable.ts new file mode 100644 index 0000000000000..f89b8c867f9d4 --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageVariable.ts @@ -0,0 +1,9 @@ +/// + +// @noImplicitAny: true +////[|var x;|] +////function f() { +//// x++; +////} + +verify.rangeAfterCodeFix("var x: number;", /*includeWhiteSpace*/ undefined, /*errorCode*/ undefined, 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixInferFromUsageVariable2.ts b/tests/cases/fourslash/codeFixInferFromUsageVariable2.ts new file mode 100644 index 0000000000000..bf8e2bb07e54d --- /dev/null +++ b/tests/cases/fourslash/codeFixInferFromUsageVariable2.ts @@ -0,0 +1,13 @@ +/// + +// @noImplicitAny: true +////[|var x; +////function f() { +//// x++; +////}|] + +verify.rangeAfterCodeFix(`var x: number; +function f() { + x++; +} +`, /*includeWhiteSpace*/ undefined, /*errorCode*/ undefined, 1); \ No newline at end of file