Skip to content

Commit 125a8fa

Browse files
authored
Merge pull request #14133 from aozgaa/MissingPropertyFix-2.2
Missing property fix 2.2
2 parents 510b384 + b62b467 commit 125a8fa

20 files changed

+289
-19
lines changed

src/compiler/checker.ts

+2
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ namespace ts {
8383
getSignaturesOfType,
8484
getIndexTypeOfType,
8585
getBaseTypes,
86+
getBaseTypeOfLiteralType,
87+
getWidenedType,
8688
getTypeFromTypeNode,
8789
getParameterType: getTypeAtPosition,
8890
getReturnTypeOfSignature,

src/compiler/diagnosticMessages.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -3303,10 +3303,18 @@
33033303
"category": "Message",
33043304
"code": 90014
33053305
},
3306-
"Add {0} to existing import declaration from {1}": {
3306+
"Add {0} to existing import declaration from {1}.": {
33073307
"category": "Message",
33083308
"code": 90015
33093309
},
3310+
"Add declaration for missing property '{0}'.": {
3311+
"category": "Message",
3312+
"code": 90016
3313+
},
3314+
"Add index signature for missing property '{0}'": {
3315+
"category": "Message",
3316+
"code": 90017
3317+
},
33103318
"Octal literal types must use ES2015 syntax. Use the syntax '{0}'.": {
33113319
"category": "Error",
33123320
"code": 8017

src/compiler/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2382,6 +2382,8 @@
23822382
getSignaturesOfType(type: Type, kind: SignatureKind): Signature[];
23832383
getIndexTypeOfType(type: Type, kind: IndexKind): Type;
23842384
getBaseTypes(type: InterfaceType): BaseType[];
2385+
getBaseTypeOfLiteralType(type: Type): Type;
2386+
getWidenedType(type: Type): Type;
23852387
getReturnTypeOfSignature(signature: Signature): Type;
23862388
/**
23872389
* Gets the type of a parameter at a given position in a signature.

src/harness/fourslash.ts

+17-9
Original file line numberDiff line numberDiff line change
@@ -2122,15 +2122,15 @@ namespace FourSlash {
21222122
* Because codefixes are only applied on the working file, it is unsafe
21232123
* to apply this more than once (consider a refactoring across files).
21242124
*/
2125-
public verifyRangeAfterCodeFix(expectedText: string, errorCode?: number, includeWhiteSpace?: boolean) {
2125+
public verifyRangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number, index?: number) {
21262126
const ranges = this.getRanges();
21272127
if (ranges.length !== 1) {
21282128
this.raiseError("Exactly one range should be specified in the testfile.");
21292129
}
21302130

21312131
const fileName = this.activeFile.fileName;
21322132

2133-
this.applyCodeFixActions(fileName, this.getCodeFixActions(fileName, errorCode));
2133+
this.applyCodeAction(fileName, this.getCodeFixActions(fileName, errorCode), index);
21342134

21352135
const actualText = this.rangeText(ranges[0]);
21362136

@@ -2155,7 +2155,7 @@ namespace FourSlash {
21552155
public verifyFileAfterCodeFix(expectedContents: string, fileName?: string) {
21562156
fileName = fileName ? fileName : this.activeFile.fileName;
21572157

2158-
this.applyCodeFixActions(fileName, this.getCodeFixActions(fileName));
2158+
this.applyCodeAction(fileName, this.getCodeFixActions(fileName));
21592159

21602160
const actualContents: string = this.getFileContent(fileName);
21612161
if (this.removeWhitespace(actualContents) !== this.removeWhitespace(expectedContents)) {
@@ -2193,12 +2193,20 @@ namespace FourSlash {
21932193
return actions;
21942194
}
21952195

2196-
private applyCodeFixActions(fileName: string, actions: ts.CodeAction[]): void {
2197-
if (!(actions && actions.length === 1)) {
2198-
this.raiseError(`Should find exactly one codefix, but ${actions ? actions.length : "none"} found.`);
2196+
private applyCodeAction(fileName: string, actions: ts.CodeAction[], index?: number): void {
2197+
if (index === undefined) {
2198+
if (!(actions && actions.length === 1)) {
2199+
this.raiseError(`Should find exactly one codefix, but ${actions ? actions.length : "none"} found.`);
2200+
}
2201+
index = 0;
2202+
}
2203+
else {
2204+
if (!(actions && actions.length >= index + 1)) {
2205+
this.raiseError(`Should find at least ${index + 1} codefix(es), but ${actions ? actions.length : "none"} found.`);
2206+
}
21992207
}
22002208

2201-
const fileChanges = ts.find(actions[0].changes, change => change.fileName === fileName);
2209+
const fileChanges = ts.find(actions[index].changes, change => change.fileName === fileName);
22022210
if (!fileChanges) {
22032211
this.raiseError("The CodeFix found doesn't provide any changes in this file.");
22042212
}
@@ -3535,8 +3543,8 @@ namespace FourSlashInterface {
35353543
this.DocCommentTemplate(/*expectedText*/ undefined, /*expectedOffset*/ undefined, /*empty*/ true);
35363544
}
35373545

3538-
public rangeAfterCodeFix(expectedText: string, errorCode?: number, includeWhiteSpace?: boolean): void {
3539-
this.state.verifyRangeAfterCodeFix(expectedText, errorCode, includeWhiteSpace);
3546+
public rangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number, index?: number): void {
3547+
this.state.verifyRangeAfterCodeFix(expectedText, includeWhiteSpace, errorCode, index);
35403548
}
35413549

35423550
public importFixAtPosition(expectedTextArray: string[], errorCode?: number): void {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
registerCodeFix({
4+
errorCodes: [Diagnostics.Property_0_does_not_exist_on_type_1.code],
5+
getCodeActions: getActionsForAddMissingMember
6+
});
7+
8+
function getActionsForAddMissingMember(context: CodeFixContext): CodeAction[] | undefined {
9+
10+
const sourceFile = context.sourceFile;
11+
const start = context.span.start;
12+
// This is the identifier of the missing property. eg:
13+
// this.missing = 1;
14+
// ^^^^^^^
15+
const token = getTokenAtPosition(sourceFile, start);
16+
17+
if (token.kind != SyntaxKind.Identifier) {
18+
return undefined;
19+
}
20+
21+
const classDeclaration = getContainingClass(token);
22+
if (!classDeclaration) {
23+
return undefined;
24+
}
25+
26+
if (!(token.parent && token.parent.kind === SyntaxKind.PropertyAccessExpression)) {
27+
return undefined;
28+
}
29+
30+
if ((token.parent as PropertyAccessExpression).expression.kind !== SyntaxKind.ThisKeyword) {
31+
return undefined;
32+
}
33+
34+
let typeString = "any";
35+
36+
if (token.parent.parent.kind === SyntaxKind.BinaryExpression) {
37+
const binaryExpression = token.parent.parent as BinaryExpression;
38+
39+
const checker = context.program.getTypeChecker();
40+
const widenedType = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(binaryExpression.right)));
41+
typeString = checker.typeToString(widenedType);
42+
}
43+
44+
const startPos = classDeclaration.members.pos;
45+
46+
return [{
47+
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_declaration_for_missing_property_0), [token.getText()]),
48+
changes: [{
49+
fileName: sourceFile.fileName,
50+
textChanges: [{
51+
span: { start: startPos, length: 0 },
52+
newText: `${token.getFullText(sourceFile)}: ${typeString};`
53+
}]
54+
}]
55+
},
56+
{
57+
description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_signature_for_missing_property_0), [token.getText()]),
58+
changes: [{
59+
fileName: sourceFile.fileName,
60+
textChanges: [{
61+
span: { start: startPos, length: 0 },
62+
newText: `[name: string]: ${typeString};`
63+
}]
64+
}]
65+
}];
66+
}
67+
}

src/services/codefixes/fixes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/// <reference path="fixClassIncorrectlyImplementsInterface.ts" />
2+
/// <reference path="fixAddMissingMember.ts" />
23
/// <reference path="fixClassDoesntImplementInheritedAbstractMember.ts" />
34
/// <reference path="fixClassSuperMustPrecedeThisAccess.ts" />
45
/// <reference path="fixConstructorForDerivedNeedSuperCall.ts" />

src/services/codefixes/helpers.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ namespace ts.codefix {
3232

3333
const declaration = declarations[0] as Declaration;
3434
const name = declaration.name ? declaration.name.getText() : undefined;
35-
const visibility = getVisibilityPrefix(getModifierFlags(declaration));
35+
const visibility = getVisibilityPrefixWithSpace(getModifierFlags(declaration));
3636

3737
switch (declaration.kind) {
3838
case SyntaxKind.GetAccessor:
@@ -58,7 +58,7 @@ namespace ts.codefix {
5858
if (declarations.length === 1) {
5959
Debug.assert(signatures.length === 1);
6060
const sigString = checker.signatureToString(signatures[0], enclosingDeclaration, TypeFormatFlags.SuppressAnyReturnType, SignatureKind.Call);
61-
return `${visibility}${name}${sigString}${getMethodBodyStub(newlineChar)}`;
61+
return getStubbedMethod(visibility, name, sigString, newlineChar);
6262
}
6363

6464
let result = "";
@@ -78,7 +78,7 @@ namespace ts.codefix {
7878
bodySig = createBodySignatureWithAnyTypes(signatures, enclosingDeclaration, checker);
7979
}
8080
const sigString = checker.signatureToString(bodySig, enclosingDeclaration, TypeFormatFlags.SuppressAnyReturnType, SignatureKind.Call);
81-
result += `${visibility}${name}${sigString}${getMethodBodyStub(newlineChar)}`;
81+
result += getStubbedMethod(visibility, name, sigString, newlineChar);
8282

8383
return result;
8484
default:
@@ -138,11 +138,15 @@ namespace ts.codefix {
138138
}
139139
}
140140

141-
function getMethodBodyStub(newLineChar: string) {
142-
return ` {${newLineChar}throw new Error('Method not implemented.');${newLineChar}}${newLineChar}`;
141+
export function getStubbedMethod(visibility: string, name: string, sigString = "()", newlineChar: string): string {
142+
return `${visibility}${name}${sigString}${getMethodBodyStub(newlineChar)}`;
143143
}
144144

145-
function getVisibilityPrefix(flags: ModifierFlags): string {
145+
function getMethodBodyStub(newlineChar: string) {
146+
return ` {${newlineChar}throw new Error('Method not implemented.');${newlineChar}}${newlineChar}`;
147+
}
148+
149+
function getVisibilityPrefixWithSpace(flags: ModifierFlags): string {
146150
if (flags & ModifierFlags.Public) {
147151
return "public ";
148152
}

src/services/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"formatting/smartIndenter.ts",
7979
"formatting/tokenRange.ts",
8080
"codeFixProvider.ts",
81+
"codefixes/fixAddMissingMember.ts",
8182
"codefixes/fixExtendsInterfaceBecomesImplements.ts",
8283
"codefixes/fixClassIncorrectlyImplementsInterface.ts",
8384
"codefixes/fixClassDoesntImplementInheritedAbstractMember.ts",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// class A {
4+
//// a: number;
5+
//// b: string;
6+
//// constructor(public x: any) {}
7+
//// }
8+
//// [|class B {
9+
//// constructor() {
10+
//// this.x = new A(3);
11+
//// }
12+
//// }|]
13+
14+
verify.rangeAfterCodeFix(`
15+
class B {
16+
x: A;
17+
18+
constructor() {
19+
this.x = new A(3);
20+
}
21+
}
22+
`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// class A<T> {
4+
//// a: number;
5+
//// b: string;
6+
//// constructor(public x: T) {}
7+
//// }
8+
//// [|class B {
9+
//// constructor() {
10+
//// this.x = new A(3);
11+
//// }
12+
//// }|]
13+
14+
verify.rangeAfterCodeFix(`
15+
class B {
16+
x: A<number>;
17+
18+
constructor() {
19+
this.x = new A(3);
20+
}
21+
}
22+
`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// [|class A {
4+
//// constructor() {
5+
//// this.x = 10;
6+
//// }
7+
//// }|]
8+
9+
verify.rangeAfterCodeFix(`
10+
class A {
11+
[name: string]: number;
12+
13+
constructor() {
14+
this.x = 10;
15+
}
16+
}
17+
`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 1);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// [|class A {
4+
//// constructor() {
5+
//// this.x = function(x: number, y?: A){
6+
//// return x > 0 ? x : y;
7+
//// }
8+
//// }
9+
//// }|]
10+
11+
verify.rangeAfterCodeFix(`
12+
class A {
13+
x: (x: number, y?: A) => A;
14+
constructor() {
15+
this.x = function(x: number, y?: A){
16+
return x > 0 ? x : y;
17+
}
18+
}
19+
}
20+
`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// [|class A {
4+
//// y: number;
5+
//// constructor(public a: number) {
6+
//// this.x = function(x: number, y?: A){
7+
//// return x > 0 ? x : y;
8+
//// }
9+
//// }
10+
//// }|]
11+
12+
verify.rangeAfterCodeFix(`
13+
class A {
14+
x: (x: number, y?: A) => number | A;
15+
y: number;
16+
constructor(public a: number) {
17+
this.x = function(x: number, y?: A){
18+
return x > 0 ? x : y;
19+
}
20+
}
21+
}
22+
`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// [|class A {
4+
//// constructor() {
5+
//// this.x = 10;
6+
//// }
7+
//// }|]
8+
9+
verify.rangeAfterCodeFix(`
10+
class A {
11+
x: number;
12+
13+
constructor() {
14+
this.x = 10;
15+
}
16+
}
17+
`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
//// [|class A {
4+
//// constructor() {
5+
//// let e: any = 10;
6+
//// this.x = { a: 10, b: "hello", c: undefined, d: null, e: e };
7+
//// }
8+
//// }|]
9+
10+
verify.rangeAfterCodeFix(`
11+
class A {
12+
x: { a: number; b: string; c: any; d: any; e: any; };
13+
14+
constructor() {
15+
let e: any = 10;
16+
this.x = { a: 10, b: "hello", c: undefined, d: null, e: e };
17+
}
18+
}
19+
`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);

0 commit comments

Comments
 (0)