Skip to content

Commit

Permalink
Merge pull request #14097 from aozgaa/MissingPropertyFix
Browse files Browse the repository at this point in the history
Missing property fix
  • Loading branch information
aozgaa authored Feb 16, 2017
2 parents f2f462f + 6e198f9 commit 8a5bebe
Show file tree
Hide file tree
Showing 20 changed files with 288 additions and 18 deletions.
2 changes: 2 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ namespace ts {
getSignaturesOfType,
getIndexTypeOfType,
getBaseTypes,
getBaseTypeOfLiteralType,
getWidenedType,
getTypeFromTypeNode,
getParameterType: getTypeAtPosition,
getReturnTypeOfSignature,
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3315,6 +3315,14 @@
"category": "Message",
"code": 90015
},
"Add declaration for missing property '{0}'": {
"category": "Message",
"code": 90016
},
"Add index signature for missing property '{0}'": {
"category": "Message",
"code": 90017
},
"Octal literal types must use ES2015 syntax. Use the syntax '{0}'.": {
"category": "Error",
"code": 8017
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2388,6 +2388,8 @@
getSignaturesOfType(type: Type, kind: SignatureKind): Signature[];
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.
Expand Down
26 changes: 17 additions & 9 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2122,15 +2122,15 @@ 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.");
}

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]);

Expand All @@ -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)) {
Expand Down Expand Up @@ -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;
}
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[0].changes, change => change.fileName === fileName);
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.");
}
Expand Down Expand Up @@ -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 {
Expand Down
67 changes: 67 additions & 0 deletions src/services/codefixes/fixAddMissingMember.ts
Original file line number Diff line number Diff line change
@@ -0,0 +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 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.getWidenedType(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_signature_for_missing_property_0), [token.getText()]),
changes: [{
fileName: sourceFile.fileName,
textChanges: [{
span: { start: startPos, length: 0 },
newText: `[name: string]: ${typeString};`
}]
}]
}];
}
}
1 change: 1 addition & 0 deletions src/services/codefixes/fixes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/// <reference path="fixClassIncorrectlyImplementsInterface.ts" />
/// <reference path="fixAddMissingMember.ts" />
/// <reference path="fixClassDoesntImplementInheritedAbstractMember.ts" />
/// <reference path="fixClassSuperMustPrecedeThisAccess.ts" />
/// <reference path="fixConstructorForDerivedNeedSuperCall.ts" />
Expand Down
16 changes: 10 additions & 6 deletions src/services/codefixes/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 = "";
Expand All @@ -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:
Expand Down Expand Up @@ -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, sigString = "()", newlineChar: string): string {
return `${visibility}${name}${sigString}${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 ";
}
Expand Down
1 change: 1 addition & 0 deletions src/services/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"formatting/smartIndenter.ts",
"formatting/tokenRange.ts",
"codeFixProvider.ts",
"codefixes/fixAddMissingMember.ts",
"codefixes/fixExtendsInterfaceBecomesImplements.ts",
"codefixes/fixClassIncorrectlyImplementsInterface.ts",
"codefixes/fixClassDoesntImplementInheritedAbstractMember.ts",
Expand Down
22 changes: 22 additions & 0 deletions tests/cases/fourslash/codeFixUndeclaredClassInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// <reference path='fourslash.ts' />

//// 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);
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// <reference path='fourslash.ts' />

//// class A<T> {
//// a: number;
//// b: string;
//// constructor(public x: T) {}
//// }
//// [|class B {
//// constructor() {
//// this.x = new A(3);
//// }
//// }|]

verify.rangeAfterCodeFix(`
class B {
x: A<number>;
constructor() {
this.x = new A(3);
}
}
`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference path='fourslash.ts' />

//// [|class A {
//// constructor() {
//// this.x = 10;
//// }
//// }|]

verify.rangeAfterCodeFix(`
class A {
[name: string]: number;
constructor() {
this.x = 10;
}
}
`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 1);
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference path='fourslash.ts' />

//// [|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);
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// <reference path='fourslash.ts' />

//// [|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);
17 changes: 17 additions & 0 deletions tests/cases/fourslash/codeFixUndeclaredPropertyNumericLiteral.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference path='fourslash.ts' />

//// [|class A {
//// constructor() {
//// this.x = 10;
//// }
//// }|]

verify.rangeAfterCodeFix(`
class A {
x: number;
constructor() {
this.x = 10;
}
}
`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0);
19 changes: 19 additions & 0 deletions tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// <reference path='fourslash.ts' />

//// [|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: any; d: any; 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);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// <reference path='fourslash.ts' />

// @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);
Loading

0 comments on commit 8a5bebe

Please sign in to comment.