From 21fc30f69bdb10aa3ef2a67ac528a68103983c27 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 31 May 2017 17:41:35 -0700 Subject: [PATCH] Add support across files and static, js methods --- src/compiler/checker.ts | 7 +- src/compiler/diagnosticMessages.json | 10 +- src/compiler/types.ts | 1 + src/compiler/utilities.ts | 4 + src/services/codefixes/fixAddMissingMember.ts | 132 ++++++++++++------ src/services/codefixes/helpers.ts | 66 ++++----- .../codeFixUndeclaredAcrossFiles2.ts | 4 +- 7 files changed, 137 insertions(+), 87 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 9ff1c4b41b9..e9b197f2863 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -110,6 +110,7 @@ namespace ts { getParameterType: getTypeAtPosition, getReturnTypeOfSignature, getNonNullableType, + getBaseTypeVariableOfClass, typeToTypeNode: nodeBuilder.typeToTypeNode, indexInfoToIndexSignatureDeclaration: nodeBuilder.indexInfoToIndexSignatureDeclaration, signatureToSignatureDeclaration: nodeBuilder.signatureToSignatureDeclaration, @@ -681,10 +682,6 @@ namespace ts { return nodeLinks[nodeId] || (nodeLinks[nodeId] = { flags: 0 }); } - function getObjectFlags(type: Type): ObjectFlags { - return type.flags & TypeFlags.Object ? (type).objectFlags : 0; - } - function isGlobalSourceFile(node: Node) { return node.kind === SyntaxKind.SourceFile && !isExternalOrCommonJsModule(node); } @@ -2282,7 +2279,7 @@ namespace ts { function typeToString(type: Type, enclosingDeclaration?: Node, flags?: TypeFormatFlags): string { const typeNode = nodeBuilder.typeToTypeNode(type, enclosingDeclaration, toNodeBuilderFlags(flags) | NodeBuilderFlags.IgnoreErrors | NodeBuilderFlags.WriteTypeParametersInQualifiedName); - Debug.assert(typeNode !== undefined, "should always get typenode?"); + Debug.assert(typeNode !== undefined, "should always get typenode"); const options = { removeComments: true }; const writer = createTextWriter(""); const printer = createPrinter(options, writer); diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 3803a9fbc23..f0b33ac9fdf 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3555,11 +3555,11 @@ "category": "Message", "code": 90015 }, - "Add declaration for missing property '{0}'.": { + "Declare property '{0}'.": { "category": "Message", "code": 90016 }, - "Add index signature for missing property '{0}'.": { + "Add index signature for property '{0}'.": { "category": "Message", "code": 90017 }, @@ -3583,10 +3583,14 @@ "category": "Message", "code": 90022 }, - "Add declaration for missing method '{0}'.": { + "Declare method '{0}'.": { "category": "Message", "code": 90023 }, + "Declare static method '{0}'.": { + "category": "Message", + "code": 90024 + }, "Convert function to an ES2015 class": { "category": "Message", diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 561d84b5d71..ee02d37addb 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2530,6 +2530,7 @@ namespace ts { */ /* @internal */ getParameterType(signature: Signature, parameterIndex: number): Type; getNonNullableType(type: Type): Type; + /* @internal */ getBaseTypeVariableOfClass(symbol: Symbol): Type | undefined; /** Note that the resulting nodes cannot be checked. */ typeToTypeNode(type: Type, enclosingDeclaration?: Node, flags?: NodeBuilderFlags): TypeNode; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 506d2728ffd..187d90559f1 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -733,6 +733,10 @@ namespace ts { return false; } + export function getObjectFlags(type: Type): ObjectFlags { + return type.flags & TypeFlags.Object ? (type).objectFlags : 0; + } + export function isChildOfNodeWithKind(node: Node, kind: SyntaxKind): boolean { while (node) { if (node.kind === kind) { diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index 231e320c93b..358c2255789 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -8,116 +8,151 @@ namespace ts.codefix { function getActionsForAddMissingMember(context: CodeFixContext): CodeAction[] | undefined { - const sourceFile = context.sourceFile; + const tokenSourceFile = context.sourceFile; const start = context.span.start; - // This is the identifier of the missing property. eg: + // The identifier of the missing property. eg: // this.missing = 1; // ^^^^^^^ - const token = getTokenAtPosition(sourceFile, start, /*includeJsDocComment*/ false); + const token = getTokenAtPosition(tokenSourceFile, start, /*includeJsDocComment*/ false); if (token.kind !== SyntaxKind.Identifier) { return undefined; } - if (!isPropertyAccessExpression(token.parent) || token.parent.expression.kind !== SyntaxKind.ThisKeyword) { + if (!isPropertyAccessExpression(token.parent)) { return undefined; } - const classMemberDeclaration = getThisContainer(token, /*includeArrowFunctions*/ false); - if (!isClassElement(classMemberDeclaration)) { - return undefined; + const tokenName = token.getText(tokenSourceFile); + + let makeStatic = false; + let classDeclaration: ClassLikeDeclaration; + + if (token.parent.expression.kind === SyntaxKind.ThisKeyword) { + const containingClassMemberDeclaration = getThisContainer(token, /*includeArrowFunctions*/ false); + if (!isClassElement(containingClassMemberDeclaration)) { + return undefined; + } + + classDeclaration = containingClassMemberDeclaration.parent; + + // Property accesses on `this` in a static method are accesses of a static member. + makeStatic = classDeclaration && hasModifier(containingClassMemberDeclaration, ModifierFlags.Static); + } + else { + + const checker = context.program.getTypeChecker(); + const leftExpression = token.parent.expression; + const leftExpressionType = checker.getTypeAtLocation(leftExpression); + + if (leftExpressionType.flags & TypeFlags.Object) { + const symbol = leftExpressionType.symbol; + if (symbol.flags & SymbolFlags.Class) { + classDeclaration = symbol.declarations && symbol.declarations[0]; + if (getObjectFlags(leftExpressionType) & ObjectFlags.Anonymous && symbol.flags & SymbolFlags.Class && !checker.getBaseTypeVariableOfClass(symbol)) { + makeStatic = true; + } + } + } } - const classDeclaration = classMemberDeclaration.parent; if (!classDeclaration || !isClassLike(classDeclaration)) { return undefined; } - const tokenName = token.getText(sourceFile); - const isStatic = hasModifier(classMemberDeclaration, ModifierFlags.Static); + const classDeclarationSourceFile = getSourceFileOfNode(classDeclaration); + const classOpenBrace = getOpenBraceOfClassLike(classDeclaration, classDeclarationSourceFile); - return isInJavaScriptFile(sourceFile) ? getActionsForAddMissingMemberInJavaScriptFile() : getActionsForAddMissingMemberInTypeScriptFile(); + return isInJavaScriptFile(classDeclarationSourceFile) ? + getActionsForAddMissingMemberInJavaScriptFile(classDeclaration, makeStatic) : + getActionsForAddMissingMemberInTypeScriptFile(classDeclaration, makeStatic); - function getActionsForAddMissingMemberInJavaScriptFile(): CodeAction[] | undefined { - if (isStatic) { + function getActionsForAddMissingMemberInJavaScriptFile(classDeclaration: ClassLikeDeclaration, makeStatic: boolean): CodeAction[] | undefined { + let actions: CodeAction[]; + + const methodCodeAction = getActionForMethodDeclaration(); + if (methodCodeAction) { + actions = [methodCodeAction]; + } + + if (makeStatic) { if (classDeclaration.kind === SyntaxKind.ClassExpression) { - return undefined; + return actions; } const className = classDeclaration.name.getText(); - return [{ + const initializeStaticAction = { description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Initialize_static_property_0), [tokenName]), changes: [{ - fileName: sourceFile.fileName, + fileName: classDeclarationSourceFile.fileName, textChanges: [{ span: { start: classDeclaration.getEnd(), length: 0 }, newText: `${context.newLineCharacter}${className}.${tokenName} = undefined;${context.newLineCharacter}` }] }] - }]; + }; + (actions || (actions = [])).push(initializeStaticAction); + return actions; } else { const classConstructor = getFirstConstructorWithBody(classDeclaration); if (!classConstructor) { - return undefined; + return actions; } - return [{ + const initializeAction = { description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Initialize_property_0_in_the_constructor), [tokenName]), changes: [{ - fileName: sourceFile.fileName, + fileName: classDeclarationSourceFile.fileName, textChanges: [{ span: { start: classConstructor.body.getEnd() - 1, length: 0 }, newText: `this.${tokenName} = undefined;${context.newLineCharacter}` }] }] - }]; + }; + + (actions || (actions = [])).push(initializeAction); + return actions; } } - function getActionsForAddMissingMemberInTypeScriptFile(): CodeAction[] | undefined { - const openBrace = getOpenBraceOfClassLike(classDeclaration, sourceFile); + function getActionsForAddMissingMemberInTypeScriptFile(classDeclaration: ClassLikeDeclaration, makeStatic: boolean): CodeAction[] | undefined { let actions: CodeAction[]; - if (token.parent.parent.kind === SyntaxKind.CallExpression) { - const callExpression = token.parent.parent; - const methodDeclaration = createMethodFromCallExpression(callExpression, tokenName); - - const methodDeclarationChangeTracker = textChanges.ChangeTracker.fromCodeFixContext(context); - methodDeclarationChangeTracker.insertNodeAfter(sourceFile, openBrace, methodDeclaration, { suffix: context.newLineCharacter }); - actions = [{ - description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_declaration_for_missing_method_0), [tokenName]), - changes: methodDeclarationChangeTracker.getChanges() - }]; + const methodCodeAction = getActionForMethodDeclaration(); + if (methodCodeAction) { + actions = [methodCodeAction]; } let typeNode: TypeNode; if (token.parent.parent.kind === SyntaxKind.BinaryExpression) { const binaryExpression = token.parent.parent as BinaryExpression; + const otherExpression = token.parent === binaryExpression.left ? binaryExpression.right : binaryExpression.left; const checker = context.program.getTypeChecker(); - const widenedType = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(binaryExpression.right))); + const widenedType = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(otherExpression))); typeNode = checker.typeToTypeNode(widenedType, classDeclaration); } typeNode = typeNode || createKeywordTypeNode(SyntaxKind.AnyKeyword); const property = createProperty( /*decorators*/undefined, - /*modifiers*/ isStatic ? [createToken(SyntaxKind.StaticKeyword)] : undefined, + /*modifiers*/ makeStatic ? [createToken(SyntaxKind.StaticKeyword)] : undefined, tokenName, /*questionToken*/ undefined, typeNode, /*initializer*/ undefined); const propertyChangeTracker = textChanges.ChangeTracker.fromCodeFixContext(context); - propertyChangeTracker.insertNodeAfter(sourceFile, openBrace, property, { suffix: context.newLineCharacter }); + propertyChangeTracker.insertNodeAfter(classDeclarationSourceFile, classOpenBrace, property, { suffix: context.newLineCharacter }); (actions || (actions = [])).push({ - description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_declaration_for_missing_property_0), [tokenName]), + description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Declare_property_0), [tokenName]), changes: propertyChangeTracker.getChanges() }); - if (!isStatic) { + if (!makeStatic) { + // Index signatures cannot have the static modifier. const stringTypeNode = createKeywordTypeNode(SyntaxKind.StringKeyword); const indexingParameter = createParameter( /*decorators*/ undefined, @@ -134,15 +169,32 @@ namespace ts.codefix { typeNode); const indexSignatureChangeTracker = textChanges.ChangeTracker.fromCodeFixContext(context); - indexSignatureChangeTracker.insertNodeAfter(sourceFile, openBrace, indexSignature, { suffix: context.newLineCharacter }); + indexSignatureChangeTracker.insertNodeAfter(classDeclarationSourceFile, classOpenBrace, indexSignature, { suffix: context.newLineCharacter }); actions.push({ - description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_signature_for_missing_property_0), [tokenName]), + description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_signature_for_property_0), [tokenName]), changes: indexSignatureChangeTracker.getChanges() }); } return actions; } + + function getActionForMethodDeclaration(): CodeAction | undefined { + if (token.parent.parent.kind === SyntaxKind.CallExpression) { + const callExpression = token.parent.parent; + const methodDeclaration = createMethodFromCallExpression(callExpression, tokenName, /*includeTypeScriptSyntax*/ true, makeStatic); + + const methodDeclarationChangeTracker = textChanges.ChangeTracker.fromCodeFixContext(context); + methodDeclarationChangeTracker.insertNodeAfter(classDeclarationSourceFile, classOpenBrace, methodDeclaration, { suffix: context.newLineCharacter }); + return { + description: formatStringFromArgs(getLocaleSpecificMessage(makeStatic ? + Diagnostics.Declare_method_0 : + Diagnostics.Declare_static_method_0), + [tokenName]), + changes: methodDeclarationChangeTracker.getChanges() + }; + } + } } } diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 5b81f71b6c9..c2c51d5c788 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -142,46 +142,50 @@ namespace ts.codefix { } } - export function createMethodFromCallExpression(callExpression: CallExpression, methodName: string): MethodDeclaration { - const argCount = callExpression.arguments.length; - const parameters: ParameterDeclaration[] = []; - for (let i = 0; i < argCount; i++) { - const typeNode = createKeywordTypeNode(SyntaxKind.AnyKeyword); - const newParameter = createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - `arg${i}`, - /*questionToken*/ undefined, - typeNode, - /*initializer*/ undefined); - parameters.push(newParameter); - } + export function createMethodFromCallExpression(callExpression: CallExpression, methodName: string, includeTypeScriptSyntax: boolean, makeStatic: boolean): MethodDeclaration { + const parameters = createDummyParameters(callExpression.arguments.length, /*names*/ undefined, /*minArgumentCount*/ undefined, includeTypeScriptSyntax); - const typeArgCount = callExpression.typeArguments ? callExpression.typeArguments.length : 0; let typeParameters: TypeParameterDeclaration[]; - for (let i = 0; i < typeArgCount; i++) { - const name = typeArgCount < 8 ? String.fromCharCode(CharacterCodes.T + i) : `T${i}`; - const typeParameter = createTypeParameterDeclaration(name, /*constraint*/ undefined, /*defaultType*/ undefined); - - (typeParameters ? typeParameters : typeParameters = []).push(typeParameter); + if (includeTypeScriptSyntax) { + const typeArgCount = callExpression.typeArguments ? callExpression.typeArguments.length : 0; + for (let i = 0; i < typeArgCount; i++) { + const name = typeArgCount < 8 ? String.fromCharCode(CharacterCodes.T + i) : `T${i}`; + const typeParameter = createTypeParameterDeclaration(name, /*constraint*/ undefined, /*defaultType*/ undefined); + (typeParameters ? typeParameters : typeParameters = []).push(typeParameter); + } } const newMethod = createMethod( /*decorators*/ undefined, - /*modifiers*/ undefined, + /*modifiers*/ makeStatic ? [createToken(SyntaxKind.StaticKeyword)] : undefined, /*asteriskToken*/ undefined, methodName, /*questionToken*/ undefined, typeParameters, parameters, - /*type*/ undefined, + /*type*/ includeTypeScriptSyntax ? createKeywordTypeNode(SyntaxKind.AnyKeyword) : undefined, createStubbedMethodBody() ); - return newMethod; } + function createDummyParameters(argCount: number, names: string[] | undefined, minArgumentCount: number | undefined, addAnyType: boolean) { + const parameters: ParameterDeclaration[] = []; + for (let i = 0; i < argCount; i++) { + const newParameter = createParameter( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*dotDotDotToken*/ undefined, + /*name*/ names && names[i] || `arg${i}`, + /*questionToken*/ minArgumentCount !== undefined && i >= minArgumentCount ? createToken(SyntaxKind.QuestionToken) : undefined, + /*type*/ addAnyType ? createKeywordTypeNode(SyntaxKind.AnyKeyword) : undefined, + /*initializer*/ undefined); + parameters.push(newParameter); + } + + return parameters; + } + function createMethodImplementingSignatures(signatures: Signature[], name: PropertyName, optional: boolean, modifiers: Modifier[] | undefined): MethodDeclaration { /** This is *a* signature with the maximal number of arguments, * such that if there is a "maximal" signature without rest arguments, @@ -203,19 +207,7 @@ namespace ts.codefix { const maxNonRestArgs = maxArgsSignature.parameters.length - (maxArgsSignature.hasRestParameter ? 1 : 0); const maxArgsParameterSymbolNames = maxArgsSignature.parameters.map(symbol => symbol.getName()); - const parameters: ParameterDeclaration[] = []; - for (let i = 0; i < maxNonRestArgs; i++) { - const anyType = createKeywordTypeNode(SyntaxKind.AnyKeyword); - const newParameter = createParameter( - /*decorators*/ undefined, - /*modifiers*/ undefined, - /*dotDotDotToken*/ undefined, - maxArgsParameterSymbolNames[i], - /*questionToken*/ i >= minArgumentCount ? createToken(SyntaxKind.QuestionToken) : undefined, - anyType, - /*initializer*/ undefined); - parameters.push(newParameter); - } + const parameters = createDummyParameters(maxNonRestArgs, maxArgsParameterSymbolNames, minArgumentCount, /*addAnyType*/ true); if (someSigHasRestParameter) { const anyArrayType = createArrayTypeNode(createKeywordTypeNode(SyntaxKind.AnyKeyword)); diff --git a/tests/cases/fourslash/codeFixUndeclaredAcrossFiles2.ts b/tests/cases/fourslash/codeFixUndeclaredAcrossFiles2.ts index cc01f8d50ee..6064ee5ae30 100644 --- a/tests/cases/fourslash/codeFixUndeclaredAcrossFiles2.ts +++ b/tests/cases/fourslash/codeFixUndeclaredAcrossFiles2.ts @@ -25,10 +25,10 @@ verify.applyCodeFix(/*errorCode*/undefined, 0); verify.rangeIs(` export class C { - m1() { + m1(): any { throw new Error("Method not implemented."); } - static m0(arg0, arg1, arg2) { + static m0(arg0: any, arg1: any, arg2: any): any { throw new Error("Method not implemented."); } x: number;