From 9897c6949256a6281326b87746d4933a437dad12 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Mon, 13 Feb 2017 11:18:56 -0800 Subject: [PATCH 1/7] wip --- src/services/codefixes/fixAddMissingMember.ts | 74 +++++++++++++++++++ src/services/codefixes/fixes.ts | 1 + src/services/codefixes/helpers.ts | 12 ++- ...codeFixUndeclaredPropertyNumericLiteral.ts | 17 +++++ 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 src/services/codefixes/fixAddMissingMember.ts create mode 100644 tests/cases/fourslash/codeFixUndeclaredPropertyNumericLiteral.ts diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts new file mode 100644 index 0000000000000..c51c63bdc2742 --- /dev/null +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -0,0 +1,74 @@ +/* @internal */ +namespace ts.codefix { + registerCodeFix({ + errorCodes: [Diagnostics.Property_0_does_not_exist_on_type_1.code], + getCodeActions: getActionsForAddMissingMember + }); + + function getActionsForAddMissingMember(context: CodeFixContext): CodeAction[] | undefined { + + const sourceFile = context.sourceFile; + const start = context.span.start; + // This is the identifier in the case of a class declaration + // or the class keyword token in the case of a class expression. + const token = getTokenAtPosition(sourceFile, start); + const checker = context.program.getTypeChecker(); + + if(!(token.parent && token.parent.kind === SyntaxKind.PropertyAccessExpression)) { + return undefined; + } + + if((token.parent as PropertyAccessExpression).expression.kind !== SyntaxKind.ThisKeyword) { + return undefined; + } + 1 + 1; + + let typeString: string = 'any'; + // if binary expression, try to infer type for LHS, else use any + if(token.parent.parent.kind === SyntaxKind.BinaryExpression) + { + const binaryExpression = token.parent.parent as BinaryExpression; + binaryExpression.operatorToken; + + const type = checker.getTypeAtLocation(binaryExpression.right); + typeString = checker.typeToString(type); + } + + const classDeclaration = getContainingClass(token); + const startPos = classDeclaration.members.pos; + return [{ + description: getLocaleSpecificMessage(Diagnostics.Implement_inherited_abstract_class), + changes: [{ + fileName: sourceFile.fileName, + textChanges: [{ + span: { start: startPos, length: 0 }, + newText: `${token.getFullText(sourceFile)}: ${typeString};` + }] + }] + }]; + } + + + + + // x needs to be a `this` construct. ie + // this.. + // Want to infer type of x when possible. ie: + // * assignment, + // * function call argument: foo(this.x) where foo(x: SomeType) + // * expression with a type assertion: this.x as MyFavoriteType + // * access expression: this.x.push("asdf") ... probably an array? + // * + // What if there are multiple usages of this.x? Create intersection over all usages? + + // needs to be in a class + // inferred type might be error. then add any. + // either make indexable of the inferred type + // add named member of the inferred type. +} + +// // class C { +// // constructor() { +// // this.x = 1; +// // } +// // } \ No newline at end of file diff --git a/src/services/codefixes/fixes.ts b/src/services/codefixes/fixes.ts index 3bd173e04f604..76be34c67cda5 100644 --- a/src/services/codefixes/fixes.ts +++ b/src/services/codefixes/fixes.ts @@ -1,4 +1,5 @@ /// +/// /// /// /// diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 7bee4f5a0ed91..60efdef1e8101 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -32,7 +32,7 @@ namespace ts.codefix { const declaration = declarations[0] as Declaration; const name = declaration.name ? declaration.name.getText() : undefined; - const visibility = getVisibilityPrefix(getModifierFlags(declaration)); + const visibility = getVisibilityPrefixWithSpace(getModifierFlags(declaration)); switch (declaration.kind) { case SyntaxKind.GetAccessor: @@ -138,11 +138,15 @@ namespace ts.codefix { } } - function getMethodBodyStub(newLineChar: string) { - return ` {${newLineChar}throw new Error('Method not implemented.');${newLineChar}}${newLineChar}`; + export function getStubbedMethod(visibility: string, name: string, signature: string = '()', newlineChar: string): string { + return `${visibility}${name}${signature}${getMethodBodyStub(newlineChar)}`; } - function getVisibilityPrefix(flags: ModifierFlags): string { + function getMethodBodyStub(newlineChar: string) { + return ` {${newlineChar}throw new Error('Method not implemented.');${newlineChar}}${newlineChar}`; + } + + function getVisibilityPrefixWithSpace(flags: ModifierFlags): string { if (flags & ModifierFlags.Public) { return "public "; } diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyNumericLiteral.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyNumericLiteral.ts new file mode 100644 index 0000000000000..bb11d9e76e125 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyNumericLiteral.ts @@ -0,0 +1,17 @@ +/// + +//// [|class A { +//// constructor() { +//// this.x = 10; +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + x: number; + + constructor() { + this.x = 10; + } +} +`); \ No newline at end of file From 92e4c6b7dbab5fa2de5fce40a36bf4bae9dcb208 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Feb 2017 17:20:04 -0800 Subject: [PATCH 2/7] Get Widened Type --- src/compiler/checker.ts | 1 + src/compiler/diagnosticMessages.json | 8 +++ src/compiler/types.ts | 1 + src/services/codefixes/fixAddMissingMember.ts | 54 +++++++++++-------- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 704812b018510..c593fcd673778 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -83,6 +83,7 @@ namespace ts { getSignaturesOfType, getIndexTypeOfType, getBaseTypes, + getWidenedType, getTypeFromTypeNode, getParameterType: getTypeAtPosition, getReturnTypeOfSignature, diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index ec6067751a28f..a01676207beb2 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3303,6 +3303,14 @@ "category": "Message", "code": 90015 }, + "Add declaration for missing property '{0}'": { + "category": "Message", + "code": 90016 + }, + "Add index accessor for missing property '{0}'": { + "category": "Message", + "code": 90017 + }, "Octal literal types must use ES2015 syntax. Use the syntax '{0}'.": { "category": "Error", "code": 8017 diff --git a/src/compiler/types.ts b/src/compiler/types.ts index bdc4eea98da09..2b121627b458d 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2382,6 +2382,7 @@ getSignaturesOfType(type: Type, kind: SignatureKind): Signature[]; getIndexTypeOfType(type: Type, kind: IndexKind): Type; getBaseTypes(type: InterfaceType): BaseType[]; + getWidenedType(type: Type): Type; getReturnTypeOfSignature(signature: Signature): Type; /** * Gets the type of a parameter at a given position in a signature. diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index c51c63bdc2742..24fc6b16abb81 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -12,32 +12,41 @@ namespace ts.codefix { // This is the identifier in the case of a class declaration // or the class keyword token in the case of a class expression. const token = getTokenAtPosition(sourceFile, start); - const checker = context.program.getTypeChecker(); - if(!(token.parent && token.parent.kind === SyntaxKind.PropertyAccessExpression)) { + const classDeclaration = getContainingClass(token); + if (!classDeclaration) { return undefined; } - if((token.parent as PropertyAccessExpression).expression.kind !== SyntaxKind.ThisKeyword) { + const startPos = classDeclaration.members.pos; + + if (!(token.parent && token.parent.kind === SyntaxKind.PropertyAccessExpression)) { return undefined; } - 1 + 1; + + if ((token.parent as PropertyAccessExpression).expression.kind !== SyntaxKind.ThisKeyword) { + return undefined; + } + + // if function call, synthesize function declaration + if(token.parent.parent.kind == SyntaxKind.CallExpression) { + + } let typeString: string = 'any'; + // if binary expression, try to infer type for LHS, else use any - if(token.parent.parent.kind === SyntaxKind.BinaryExpression) - { + if (token.parent.parent.kind === SyntaxKind.BinaryExpression) { const binaryExpression = token.parent.parent as BinaryExpression; binaryExpression.operatorToken; - - const type = checker.getTypeAtLocation(binaryExpression.right); + + const checker = context.program.getTypeChecker(); + const type = checker.getWidenedType(checker.getTypeAtLocation(binaryExpression.right)); typeString = checker.typeToString(type); } - const classDeclaration = getContainingClass(token); - const startPos = classDeclaration.members.pos; return [{ - description: getLocaleSpecificMessage(Diagnostics.Implement_inherited_abstract_class), + description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_declaration_for_missing_property_0), [token.getText()]), changes: [{ fileName: sourceFile.fileName, textChanges: [{ @@ -45,14 +54,19 @@ namespace ts.codefix { newText: `${token.getFullText(sourceFile)}: ${typeString};` }] }] + }, + { + description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_accessor_for_missing_property_0), [token.getText()]), + changes: [{ + fileName: sourceFile.fileName, + textChanges: [{ + span: { start: startPos, length: 0 }, + newText: `[name: string]: ${typeString};` + }] + }] }]; } - - - - // x needs to be a `this` construct. ie - // this.. // Want to infer type of x when possible. ie: // * assignment, // * function call argument: foo(this.x) where foo(x: SomeType) @@ -65,10 +79,4 @@ namespace ts.codefix { // inferred type might be error. then add any. // either make indexable of the inferred type // add named member of the inferred type. -} - -// // class C { -// // constructor() { -// // this.x = 1; -// // } -// // } \ No newline at end of file +} \ No newline at end of file From f047a6ea31d284d9d5b5a532ab41d30791921826 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Feb 2017 17:27:07 -0800 Subject: [PATCH 3/7] wip testing --- src/harness/fourslash.ts | 28 ++++++++++++------- .../codeFixUndeclaredPropertyObjectLiteral.ts | 17 +++++++++++ tests/cases/fourslash/fourslash.ts | 2 +- tests/cases/fourslash/unusedImports2FS.ts | 2 +- .../fourslash/unusedLocalsInFunction3.ts | 2 +- 5 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 34aa9580d6d78..a722cb2072421 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -2122,7 +2122,7 @@ namespace FourSlash { * Because codefixes are only applied on the working file, it is unsafe * to apply this more than once (consider a refactoring across files). */ - public verifyRangeAfterCodeFix(expectedText: string, errorCode?: number, includeWhiteSpace?: boolean) { + public verifyRangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number, index?: number) { const ranges = this.getRanges(); if (ranges.length !== 1) { this.raiseError("Exactly one range should be specified in the testfile."); @@ -2130,7 +2130,7 @@ namespace FourSlash { const fileName = this.activeFile.fileName; - this.applyCodeFixActions(fileName, this.getCodeFixActions(fileName, errorCode)); + this.applyCodeAction(fileName, this.getCodeFixActions(fileName, errorCode), index); const actualText = this.rangeText(ranges[0]); @@ -2155,7 +2155,7 @@ namespace FourSlash { public verifyFileAfterCodeFix(expectedContents: string, fileName?: string) { fileName = fileName ? fileName : this.activeFile.fileName; - this.applyCodeFixActions(fileName, this.getCodeFixActions(fileName)); + this.applyCodeAction(fileName, this.getCodeFixActions(fileName)); const actualContents: string = this.getFileContent(fileName); if (this.removeWhitespace(actualContents) !== this.removeWhitespace(expectedContents)) { @@ -2193,12 +2193,20 @@ namespace FourSlash { return actions; } - private applyCodeFixActions(fileName: string, actions: ts.CodeAction[]): void { - if (!(actions && actions.length === 1)) { - this.raiseError(`Should find exactly one codefix, but ${actions ? actions.length : "none"} found.`); + private applyCodeAction(fileName: string, actions: ts.CodeAction[], index?: number): void { + if (index === undefined) { + if (!(actions && actions.length === 1)) { + this.raiseError(`Should find exactly one codefix, but ${actions ? actions.length : "none"} found.`); + } + index = 0; } - - const fileChanges = ts.find(actions[0].changes, change => change.fileName === fileName); + else { + if (!(actions && actions.length >= index + 1)) { + this.raiseError(`Should find at least ${index + 1} codefix(es), but ${actions ? actions.length : "none"} found.`); + } + } + + const fileChanges = ts.find(actions[index].changes, change => change.fileName === fileName); if (!fileChanges) { this.raiseError("The CodeFix found doesn't provide any changes in this file."); } @@ -3535,8 +3543,8 @@ namespace FourSlashInterface { this.DocCommentTemplate(/*expectedText*/ undefined, /*expectedOffset*/ undefined, /*empty*/ true); } - public rangeAfterCodeFix(expectedText: string, errorCode?: number, includeWhiteSpace?: boolean): void { - this.state.verifyRangeAfterCodeFix(expectedText, errorCode, includeWhiteSpace); + public rangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number, index?: number): void { + this.state.verifyRangeAfterCodeFix(expectedText, includeWhiteSpace, errorCode, index); } public importFixAtPosition(expectedTextArray: string[], errorCode?: number): void { diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts new file mode 100644 index 0000000000000..bc19f792fb30e --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts @@ -0,0 +1,17 @@ +/// + +//// [|class A { +//// constructor() { +//// this.x = { a: 10, b: "hello" }; +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + x: { a: number, b: string }; + + constructor() { + this.x = 10; + } +} +`); \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 661d7cba6a495..fda5b1a30cdf7 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -225,7 +225,7 @@ declare namespace FourSlashInterface { noMatchingBracePositionInCurrentFile(bracePosition: number): void; DocCommentTemplate(expectedText: string, expectedOffset: number, empty?: boolean): void; noDocCommentTemplate(): void; - rangeAfterCodeFix(expectedText: string, errorCode?: number, includeWhiteSpace?: boolean): void; + rangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number): void; importFixAtPosition(expectedTextArray: string[], errorCode?: number): void; navigationBar(json: any): void; diff --git a/tests/cases/fourslash/unusedImports2FS.ts b/tests/cases/fourslash/unusedImports2FS.ts index 5387569336247..8865dea56a671 100644 --- a/tests/cases/fourslash/unusedImports2FS.ts +++ b/tests/cases/fourslash/unusedImports2FS.ts @@ -16,4 +16,4 @@ //// //// } -verify.rangeAfterCodeFix(`import {Calculator} from "./file1"`, /*errorCode*/ undefined, /*includeWhiteSpace*/ true); +verify.rangeAfterCodeFix(`import {Calculator} from "./file1"`, /*includeWhiteSpace*/ true, /*errorCode*/ undefined); diff --git a/tests/cases/fourslash/unusedLocalsInFunction3.ts b/tests/cases/fourslash/unusedLocalsInFunction3.ts index 7aa9b20a33ce3..0164873acd87e 100644 --- a/tests/cases/fourslash/unusedLocalsInFunction3.ts +++ b/tests/cases/fourslash/unusedLocalsInFunction3.ts @@ -7,4 +7,4 @@ //// z+1; ////} -verify.rangeAfterCodeFix("var x,z = 1;", 6133); +verify.rangeAfterCodeFix("var x,z = 1;", /*includeWhiteSpace*/ undefined, 6133); From 18cba86e747d507f0a4ec979cc19cbebb8df02e2 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Tue, 14 Feb 2017 18:10:21 -0800 Subject: [PATCH 4/7] add tests --- .../codeFixUndeclaredClassInstance.ts | 22 +++++++++++++++++++ ...ixUndeclaredClassInstanceWithTypeParams.ts | 22 +++++++++++++++++++ ...codeFixUndeclaredPropertyNumericLiteral.ts | 2 +- .../codeFixUndeclaredPropertyObjectLiteral.ts | 8 +++---- tests/cases/fourslash/fourslash.ts | 2 +- 5 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 tests/cases/fourslash/codeFixUndeclaredClassInstance.ts create mode 100644 tests/cases/fourslash/codeFixUndeclaredClassInstanceWithTypeParams.ts diff --git a/tests/cases/fourslash/codeFixUndeclaredClassInstance.ts b/tests/cases/fourslash/codeFixUndeclaredClassInstance.ts new file mode 100644 index 0000000000000..024ec144092c8 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredClassInstance.ts @@ -0,0 +1,22 @@ +/// + +//// class A { +//// a: number; +//// b: string; +//// constructor(public x: any) {} +//// } +//// [|class B { +//// constructor() { +//// this.x = new A(3); +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class B { + x: A; + + constructor() { + this.x = new A(3); + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixUndeclaredClassInstanceWithTypeParams.ts b/tests/cases/fourslash/codeFixUndeclaredClassInstanceWithTypeParams.ts new file mode 100644 index 0000000000000..34e359feea89f --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredClassInstanceWithTypeParams.ts @@ -0,0 +1,22 @@ +/// + +//// class A { +//// a: number; +//// b: string; +//// constructor(public x: T) {} +//// } +//// [|class B { +//// constructor() { +//// this.x = new A(3); +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class B { + x: A; + + constructor() { + this.x = new A(3); + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyNumericLiteral.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyNumericLiteral.ts index bb11d9e76e125..eaae355904b16 100644 --- a/tests/cases/fourslash/codeFixUndeclaredPropertyNumericLiteral.ts +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyNumericLiteral.ts @@ -14,4 +14,4 @@ class A { this.x = 10; } } -`); \ No newline at end of file +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts index bc19f792fb30e..3f5c5f998874e 100644 --- a/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts @@ -8,10 +8,10 @@ verify.rangeAfterCodeFix(` class A { - x: { a: number, b: string }; - + x: { a: number; b: string; }; + constructor() { - this.x = 10; + this.x = { a: 10, b: "hello" }; } } -`); \ No newline at end of file +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index fda5b1a30cdf7..34afb71ec85ea 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -225,7 +225,7 @@ declare namespace FourSlashInterface { noMatchingBracePositionInCurrentFile(bracePosition: number): void; DocCommentTemplate(expectedText: string, expectedOffset: number, empty?: boolean): void; noDocCommentTemplate(): void; - rangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number): void; + rangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number, index?: number): void; importFixAtPosition(expectedTextArray: string[], errorCode?: number): void; navigationBar(json: any): void; From 9110461294c4e660a7be5fd7b3e2dbe8bd8b2db4 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 15 Feb 2017 14:24:25 -0800 Subject: [PATCH 5/7] use getBaseTypeOfLiteralType --- src/compiler/checker.ts | 2 +- src/compiler/types.ts | 2 +- src/services/codefixes/fixAddMissingMember.ts | 20 +++++++++++++++++-- src/services/tsconfig.json | 1 + 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c593fcd673778..426704a343526 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -83,7 +83,7 @@ namespace ts { getSignaturesOfType, getIndexTypeOfType, getBaseTypes, - getWidenedType, + getBaseTypeOfLiteralType, getTypeFromTypeNode, getParameterType: getTypeAtPosition, getReturnTypeOfSignature, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 2b121627b458d..52c9cecd8e801 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2382,7 +2382,7 @@ getSignaturesOfType(type: Type, kind: SignatureKind): Signature[]; getIndexTypeOfType(type: Type, kind: IndexKind): Type; getBaseTypes(type: InterfaceType): BaseType[]; - getWidenedType(type: Type): Type; + getBaseTypeOfLiteralType(type: Type): Type; getReturnTypeOfSignature(signature: Signature): Type; /** * Gets the type of a parameter at a given position in a signature. diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index 24fc6b16abb81..fbb8b3a483b3b 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -30,6 +30,22 @@ namespace ts.codefix { // if function call, synthesize function declaration if(token.parent.parent.kind == SyntaxKind.CallExpression) { + const callExpression = token.parent.parent as CallExpression; + if(callExpression.typeArguments) { + /** + * We can't in general know which arguments should use the type of the expression + * or the type of the type argument in the declaration. Consider + * ``` + * class A { + * constructor(a: number){ + * this.foo(a,1,true); + * } + * } + * ``` + */ + return undefined; + } + } @@ -41,8 +57,8 @@ namespace ts.codefix { binaryExpression.operatorToken; const checker = context.program.getTypeChecker(); - const type = checker.getWidenedType(checker.getTypeAtLocation(binaryExpression.right)); - typeString = checker.typeToString(type); + const widenedType = checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(binaryExpression.right)); + typeString = checker.typeToString(widenedType); } return [{ diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index b4e8289f367bd..d88cf137afa8b 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -78,6 +78,7 @@ "formatting/smartIndenter.ts", "formatting/tokenRange.ts", "codeFixProvider.ts", + "codefixes/fixAddMissingMember.ts", "codefixes/fixExtendsInterfaceBecomesImplements.ts", "codefixes/fixClassIncorrectlyImplementsInterface.ts", "codefixes/fixClassDoesntImplementInheritedAbstractMember.ts", From cf3b4d6b0092ca174ccd519b9016c7921e77747c Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Wed, 15 Feb 2017 15:15:09 -0800 Subject: [PATCH 6/7] cleanup --- src/harness/fourslash.ts | 2 +- src/services/codefixes/fixAddMissingMember.ts | 163 +++++++----------- src/services/codefixes/helpers.ts | 8 +- 3 files changed, 71 insertions(+), 102 deletions(-) diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index a722cb2072421..49d1336c87464 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -2205,7 +2205,7 @@ namespace FourSlash { this.raiseError(`Should find at least ${index + 1} codefix(es), but ${actions ? actions.length : "none"} found.`); } } - + const fileChanges = ts.find(actions[index].changes, change => change.fileName === fileName); if (!fileChanges) { this.raiseError("The CodeFix found doesn't provide any changes in this file."); diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index fbb8b3a483b3b..ac03f606d59f3 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -1,98 +1,67 @@ -/* @internal */ -namespace ts.codefix { - registerCodeFix({ - errorCodes: [Diagnostics.Property_0_does_not_exist_on_type_1.code], - getCodeActions: getActionsForAddMissingMember - }); - - function getActionsForAddMissingMember(context: CodeFixContext): CodeAction[] | undefined { - - const sourceFile = context.sourceFile; - const start = context.span.start; - // This is the identifier in the case of a class declaration - // or the class keyword token in the case of a class expression. - const token = getTokenAtPosition(sourceFile, start); - - const classDeclaration = getContainingClass(token); - if (!classDeclaration) { - return undefined; - } - - const startPos = classDeclaration.members.pos; - - if (!(token.parent && token.parent.kind === SyntaxKind.PropertyAccessExpression)) { - return undefined; - } - - if ((token.parent as PropertyAccessExpression).expression.kind !== SyntaxKind.ThisKeyword) { - return undefined; - } - - // if function call, synthesize function declaration - if(token.parent.parent.kind == SyntaxKind.CallExpression) { - const callExpression = token.parent.parent as CallExpression; - if(callExpression.typeArguments) { - /** - * We can't in general know which arguments should use the type of the expression - * or the type of the type argument in the declaration. Consider - * ``` - * class A { - * constructor(a: number){ - * this.foo(a,1,true); - * } - * } - * ``` - */ - return undefined; - } - - - } - - let typeString: string = 'any'; - - // if binary expression, try to infer type for LHS, else use any - if (token.parent.parent.kind === SyntaxKind.BinaryExpression) { - const binaryExpression = token.parent.parent as BinaryExpression; - binaryExpression.operatorToken; - - const checker = context.program.getTypeChecker(); - const widenedType = checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(binaryExpression.right)); - typeString = checker.typeToString(widenedType); - } - - return [{ - description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_declaration_for_missing_property_0), [token.getText()]), - changes: [{ - fileName: sourceFile.fileName, - textChanges: [{ - span: { start: startPos, length: 0 }, - newText: `${token.getFullText(sourceFile)}: ${typeString};` - }] - }] - }, - { - description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_accessor_for_missing_property_0), [token.getText()]), - changes: [{ - fileName: sourceFile.fileName, - textChanges: [{ - span: { start: startPos, length: 0 }, - newText: `[name: string]: ${typeString};` - }] - }] - }]; - } - - // Want to infer type of x when possible. ie: - // * assignment, - // * function call argument: foo(this.x) where foo(x: SomeType) - // * expression with a type assertion: this.x as MyFavoriteType - // * access expression: this.x.push("asdf") ... probably an array? - // * - // What if there are multiple usages of this.x? Create intersection over all usages? - - // needs to be in a class - // inferred type might be error. then add any. - // either make indexable of the inferred type - // add named member of the inferred type. +/* @internal */ +namespace ts.codefix { + registerCodeFix({ + errorCodes: [Diagnostics.Property_0_does_not_exist_on_type_1.code], + getCodeActions: getActionsForAddMissingMember + }); + + function getActionsForAddMissingMember(context: CodeFixContext): CodeAction[] | undefined { + + const sourceFile = context.sourceFile; + const start = context.span.start; + // This is the identifier of the missing property. eg: + // this.missing = 1; + // ^^^^^^^ + const token = getTokenAtPosition(sourceFile, start); + + if (token.kind != SyntaxKind.Identifier) { + return undefined; + } + + const classDeclaration = getContainingClass(token); + if (!classDeclaration) { + return undefined; + } + + if (!(token.parent && token.parent.kind === SyntaxKind.PropertyAccessExpression)) { + return undefined; + } + + if ((token.parent as PropertyAccessExpression).expression.kind !== SyntaxKind.ThisKeyword) { + return undefined; + } + + let typeString = "any"; + + if (token.parent.parent.kind === SyntaxKind.BinaryExpression) { + const binaryExpression = token.parent.parent as BinaryExpression; + + const checker = context.program.getTypeChecker(); + const widenedType = checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(binaryExpression.right)); + typeString = checker.typeToString(widenedType); + } + + const startPos = classDeclaration.members.pos; + + return [{ + description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_declaration_for_missing_property_0), [token.getText()]), + changes: [{ + fileName: sourceFile.fileName, + textChanges: [{ + span: { start: startPos, length: 0 }, + newText: `${token.getFullText(sourceFile)}: ${typeString};` + }] + }] + }, + { + description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_accessor_for_missing_property_0), [token.getText()]), + changes: [{ + fileName: sourceFile.fileName, + textChanges: [{ + span: { start: startPos, length: 0 }, + newText: `[name: string]: ${typeString};` + }] + }] + }]; + } } \ No newline at end of file diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 60efdef1e8101..3eab994f84ce5 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -58,7 +58,7 @@ namespace ts.codefix { if (declarations.length === 1) { Debug.assert(signatures.length === 1); const sigString = checker.signatureToString(signatures[0], enclosingDeclaration, TypeFormatFlags.SuppressAnyReturnType, SignatureKind.Call); - return `${visibility}${name}${sigString}${getMethodBodyStub(newlineChar)}`; + return getStubbedMethod(visibility, name, sigString, newlineChar); } let result = ""; @@ -78,7 +78,7 @@ namespace ts.codefix { bodySig = createBodySignatureWithAnyTypes(signatures, enclosingDeclaration, checker); } const sigString = checker.signatureToString(bodySig, enclosingDeclaration, TypeFormatFlags.SuppressAnyReturnType, SignatureKind.Call); - result += `${visibility}${name}${sigString}${getMethodBodyStub(newlineChar)}`; + result += getStubbedMethod(visibility, name, sigString, newlineChar); return result; default: @@ -138,8 +138,8 @@ namespace ts.codefix { } } - export function getStubbedMethod(visibility: string, name: string, signature: string = '()', newlineChar: string): string { - return `${visibility}${name}${signature}${getMethodBodyStub(newlineChar)}`; + export function getStubbedMethod(visibility: string, name: string, sigString = "()", newlineChar: string): string { + return `${visibility}${name}${sigString}${getMethodBodyStub(newlineChar)}`; } function getMethodBodyStub(newlineChar: string) { From 1b6cf97766f48bd4a8072b31f6de8c0f02984da8 Mon Sep 17 00:00:00 2001 From: Arthur Ozga Date: Thu, 16 Feb 2017 13:37:35 -0800 Subject: [PATCH 7/7] widen type, index signature, and add tests --- src/compiler/checker.ts | 1 + src/compiler/diagnosticMessages.json | 2 +- src/compiler/types.ts | 1 + src/services/codefixes/fixAddMissingMember.ts | 4 ++-- ...xUndeclaredIndexSignatureNumericLiteral.ts | 17 ++++++++++++++ ...FixUndeclaredPropertyFunctionEmptyClass.ts | 20 +++++++++++++++++ ...UndeclaredPropertyFunctionNonEmptyClass.ts | 22 +++++++++++++++++++ .../codeFixUndeclaredPropertyObjectLiteral.ts | 10 +++++---- ...edPropertyObjectLiteralStrictNullChecks.ts | 21 ++++++++++++++++++ .../codeFixUndeclaredPropertyThisType.ts | 17 ++++++++++++++ 10 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 tests/cases/fourslash/codeFixUndeclaredIndexSignatureNumericLiteral.ts create mode 100644 tests/cases/fourslash/codeFixUndeclaredPropertyFunctionEmptyClass.ts create mode 100644 tests/cases/fourslash/codeFixUndeclaredPropertyFunctionNonEmptyClass.ts create mode 100644 tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteralStrictNullChecks.ts create mode 100644 tests/cases/fourslash/codeFixUndeclaredPropertyThisType.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 426704a343526..ceae9739c40cf 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -84,6 +84,7 @@ namespace ts { getIndexTypeOfType, getBaseTypes, getBaseTypeOfLiteralType, + getWidenedType, getTypeFromTypeNode, getParameterType: getTypeAtPosition, getReturnTypeOfSignature, diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index a01676207beb2..04fb3a10d54f1 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3307,7 +3307,7 @@ "category": "Message", "code": 90016 }, - "Add index accessor for missing property '{0}'": { + "Add index signature for missing property '{0}'": { "category": "Message", "code": 90017 }, diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 52c9cecd8e801..e5d726199ca28 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2383,6 +2383,7 @@ getIndexTypeOfType(type: Type, kind: IndexKind): Type; getBaseTypes(type: InterfaceType): BaseType[]; getBaseTypeOfLiteralType(type: Type): Type; + getWidenedType(type: Type): Type; getReturnTypeOfSignature(signature: Signature): Type; /** * Gets the type of a parameter at a given position in a signature. diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts index ac03f606d59f3..6ae2ba3f51c99 100644 --- a/src/services/codefixes/fixAddMissingMember.ts +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -37,7 +37,7 @@ namespace ts.codefix { const binaryExpression = token.parent.parent as BinaryExpression; const checker = context.program.getTypeChecker(); - const widenedType = checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(binaryExpression.right)); + const widenedType = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(binaryExpression.right))); typeString = checker.typeToString(widenedType); } @@ -54,7 +54,7 @@ namespace ts.codefix { }] }, { - description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_accessor_for_missing_property_0), [token.getText()]), + description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_signature_for_missing_property_0), [token.getText()]), changes: [{ fileName: sourceFile.fileName, textChanges: [{ diff --git a/tests/cases/fourslash/codeFixUndeclaredIndexSignatureNumericLiteral.ts b/tests/cases/fourslash/codeFixUndeclaredIndexSignatureNumericLiteral.ts new file mode 100644 index 0000000000000..2e49a8184eb73 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredIndexSignatureNumericLiteral.ts @@ -0,0 +1,17 @@ +/// + +//// [|class A { +//// constructor() { +//// this.x = 10; +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + [name: string]: number; + + constructor() { + this.x = 10; + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 1); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyFunctionEmptyClass.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyFunctionEmptyClass.ts new file mode 100644 index 0000000000000..6f0a5de3557b1 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyFunctionEmptyClass.ts @@ -0,0 +1,20 @@ +/// + +//// [|class A { +//// constructor() { +//// this.x = function(x: number, y?: A){ +//// return x > 0 ? x : y; +//// } +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + x: (x: number, y?: A) => A; + constructor() { + this.x = function(x: number, y?: A){ + return x > 0 ? x : y; + } + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyFunctionNonEmptyClass.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyFunctionNonEmptyClass.ts new file mode 100644 index 0000000000000..b05fb0d42b568 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyFunctionNonEmptyClass.ts @@ -0,0 +1,22 @@ +/// + +//// [|class A { +//// y: number; +//// constructor(public a: number) { +//// this.x = function(x: number, y?: A){ +//// return x > 0 ? x : y; +//// } +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + x: (x: number, y?: A) => number | A; + y: number; + constructor(public a: number) { + this.x = function(x: number, y?: A){ + return x > 0 ? x : y; + } + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts index 3f5c5f998874e..a2647c5a21e33 100644 --- a/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts @@ -2,16 +2,18 @@ //// [|class A { //// constructor() { -//// this.x = { a: 10, b: "hello" }; +//// let e: any = 10; +//// this.x = { a: 10, b: "hello", c: undefined, d: null, e: e }; //// } //// }|] verify.rangeAfterCodeFix(` class A { - x: { a: number; b: string; }; + x: { a: number; b: string; c: any; d: any; e: any; }; constructor() { - this.x = { a: 10, b: "hello" }; + let e: any = 10; + this.x = { a: 10, b: "hello", c: undefined, d: null, e: e }; } } -`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteralStrictNullChecks.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteralStrictNullChecks.ts new file mode 100644 index 0000000000000..0b0ab8cdd59a2 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteralStrictNullChecks.ts @@ -0,0 +1,21 @@ +/// + +// @strictNullChecks: true + +//// [|class A { +//// constructor() { +//// let e: any = 10; +//// this.x = { a: 10, b: "hello", c: undefined, d: null, e: e }; +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + x: { a: number; b: string; c: undefined; d: null; e: any; }; + + constructor() { + let e: any = 10; + this.x = { a: 10, b: "hello", c: undefined, d: null, e: e }; + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyThisType.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyThisType.ts new file mode 100644 index 0000000000000..56f9b34d809ce --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyThisType.ts @@ -0,0 +1,17 @@ +/// + +//// [|class A { +//// constructor() { +//// this.mythis = this; +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + mythis: this; + + constructor() { + this.mythis = this; + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);