-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Codefix for implementing interfaces #11547
Changes from 2 commits
1438f9a
7a5cb5f
bf3e487
6b4f6b5
151f940
d1cea73
c2feab6
f74872d
2abf906
132b746
ecc029f
a66b0ae
5d9a4e3
3ac9ffa
d24236b
7758e6d
7272833
834245c
c89b97b
16dc834
cbaea99
bc21346
99ae5d9
aa6ecd4
3240000
0380f3f
1b60a97
e5279fd
04968ab
d02eb6c
3b0b696
36c5bef
1b8486d
b7b30aa
71d1744
8c35185
bc1bb0e
0ce53f0
b26ba83
11cea6a
7141a2a
55bf3e3
4441380
1ec234a
6bd35fb
4973852
1d6ef6a
d842a6f
b1e97b3
c650c33
0591e1b
263734f
4b202ab
357ed7e
f6fc320
efd16c7
bba96da
cfe50d1
ad3035d
d8b359f
6400d53
c010a0e
395d736
d7d4bf6
a94d955
43afb80
6ed8d18
389959a
69118cd
4af0e2a
680af0f
16b146f
f37640a
5d6a714
bf48564
ba80ce6
8134d64
0c1772b
f0c7713
c511aea
5cd0ea3
c22e47d
c1a41b9
2f51b36
97b3d7a
819a654
5e48e33
1338b94
b9ae36c
d724517
469745b
1ba6c86
ad01110
3cfac08
4673812
4b02099
8be8819
d75d548
4af3937
97f18c9
3fc94bb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* @internal */ | ||
namespace ts.codefix { | ||
registerCodeFix({ | ||
errorCodes: [Diagnostics.Cannot_extend_an_interface_0_Did_you_mean_implements.code], | ||
getCodeActions: (context: CodeFixContext) => { | ||
const sourceFile = context.sourceFile; | ||
const start = context.span.start; | ||
const token = getTokenAtPosition(sourceFile, start); | ||
const textChanges: TextChange[] = []; | ||
|
||
if (token.kind === SyntaxKind.Identifier && token.parent.parent.kind === SyntaxKind.HeritageClause) { | ||
const children = (<HeritageClause>token.parent.parent).getChildren(); | ||
ts.forEach(children, child => { | ||
if (child.kind === SyntaxKind.ExtendsKeyword) { | ||
textChanges.push({ newText: " implements", span: { start: child.pos, length: child.end - child.pos } }); | ||
} | ||
}); | ||
} | ||
|
||
if (textChanges.length > 0) { | ||
return [{ | ||
description: getLocaleSpecificMessage(Diagnostics.Change_extends_to_implements), | ||
changes: [{ | ||
fileName: sourceFile.fileName, | ||
textChanges: textChanges | ||
}] | ||
}]; | ||
} | ||
|
||
return undefined; | ||
} | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
///<reference path='superFixes.ts' /> | ||
///<reference path='superFixes.ts' /> | ||
///<reference path='interfaceFixes.ts' /> | ||
///<reference path='changeExtendsToImplementsFix.ts' /> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,268 @@ | ||
/* @internal */ | ||
namespace ts.codefix { | ||
registerCodeFix({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please split these into different files. let's keep each code fix in its own file. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can still share the helpers between files if you need to. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Split, and helpers moved to |
||
errorCodes: [Diagnostics.Type_0_is_not_assignable_to_type_1.code], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this code fix is not nearly close to being complete. please remove this from this patch and added by itself later on. i am not sure we should do this either. there are issues that are not fixable. and we are not detecting them and handling them correctly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed, and tests changed. |
||
getCodeActions: (context: CodeFixContext) => { | ||
const sourceFile = context.sourceFile; | ||
const start = context.span.start; | ||
const token = getTokenAtPosition(sourceFile, start); | ||
const checker = context.program.getTypeChecker(); | ||
let textChanges: TextChange[] = []; | ||
|
||
if (token.kind === SyntaxKind.Identifier && token.parent.kind === SyntaxKind.VariableDeclaration) { | ||
const variableDeclaration = <VariableDeclaration>token.parent; | ||
const membersAndStartPosObject = getMembersAndStartPosFromReference(variableDeclaration); | ||
const variableMembers = membersAndStartPosObject.members; | ||
const trackingAddedMembers: string[] = []; | ||
const startPos: number = membersAndStartPosObject.startPos; | ||
|
||
if (variableDeclaration.type.kind === SyntaxKind.TypeReference) { | ||
textChanges = textChanges.concat(getChanges(variableDeclaration.type, variableMembers, startPos, checker, /*reference*/ true, trackingAddedMembers, context.newLineCharacter)); | ||
} | ||
else if (variableDeclaration.type.kind === SyntaxKind.UnionType) { | ||
const types = (<UnionTypeNode>variableDeclaration.type).types; | ||
ts.forEach(types, t => { | ||
textChanges = textChanges.concat(getChanges(t, variableMembers, startPos, checker, /*reference*/ true, trackingAddedMembers, context.newLineCharacter)); | ||
}); | ||
} | ||
} | ||
|
||
if (textChanges.length > 0) { | ||
return [{ | ||
description: getLocaleSpecificMessage(Diagnostics.Implement_interface_on_reference), | ||
changes: [{ | ||
fileName: sourceFile.fileName, | ||
textChanges: textChanges | ||
}] | ||
}]; | ||
} | ||
|
||
return undefined; | ||
} | ||
}); | ||
|
||
registerCodeFix({ | ||
errorCodes: [Diagnostics.Class_0_incorrectly_implements_interface_1.code], | ||
getCodeActions: (context: CodeFixContext) => { | ||
const sourceFile = context.sourceFile; | ||
const start = context.span.start; | ||
const token = getTokenAtPosition(sourceFile, start); | ||
const checker = context.program.getTypeChecker(); | ||
|
||
let textChanges: TextChange[] = []; | ||
|
||
if (token.kind === SyntaxKind.Identifier && token.parent.kind === SyntaxKind.ClassDeclaration) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about classExpression? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed. |
||
const classDeclaration = <ClassDeclaration>token.parent; | ||
const startPos: number = classDeclaration.members.pos; | ||
const classMembers = getClassMembers(classDeclaration); | ||
const trackingAddedMembers: string[] = []; | ||
const interfaceClauses = ts.getClassImplementsHeritageClauseElements(classDeclaration); | ||
|
||
for (let i = 0; interfaceClauses && i < interfaceClauses.length; i++) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if the class does not have curlies? can add tests? |
||
textChanges = textChanges.concat(getChanges(interfaceClauses[i], classMembers, startPos, checker, /*reference*/ false, trackingAddedMembers, context.newLineCharacter)); | ||
} | ||
} | ||
|
||
if (textChanges.length > 0) { | ||
return [{ | ||
description: getLocaleSpecificMessage(Diagnostics.Implement_interface_on_class), | ||
changes: [{ | ||
fileName: sourceFile.fileName, | ||
textChanges: textChanges | ||
}] | ||
}]; | ||
} | ||
|
||
return undefined; | ||
} | ||
}); | ||
|
||
registerCodeFix({ | ||
errorCodes: [Diagnostics.Non_abstract_class_0_does_not_implement_inherited_abstract_member_1_from_class_2.code], | ||
getCodeActions: (context: CodeFixContext) => { | ||
const sourceFile = context.sourceFile; | ||
const start = context.span.start; | ||
const token = getTokenAtPosition(sourceFile, start); | ||
const checker = context.program.getTypeChecker(); | ||
|
||
let textChanges: TextChange[] = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what is the point of this initialization, you can just override it below. consider just leaving this as undefined, and checking for undefined below. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Refactored |
||
|
||
if (token.kind === SyntaxKind.Identifier && token.parent.kind === SyntaxKind.ClassDeclaration) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed |
||
const classDeclaration = <ClassDeclaration>token.parent; | ||
const startPos = classDeclaration.members.pos; | ||
const classMembers = getClassMembers(classDeclaration); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you just need the abstract ones. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed. |
||
const trackingAddedMembers: string[] = []; | ||
const extendsClause = ts.getClassExtendsHeritageClauseElement(classDeclaration); | ||
textChanges = textChanges.concat(getChanges(extendsClause, classMembers, startPos, checker, /*reference*/ false, trackingAddedMembers, context.newLineCharacter)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does not handle properties declared in the constructor as parameter property declarations, e.g. this should only add interface I {
a: number,
b:string;
}
class C implements I {
constructor(public a) {}
} Please add tests. |
||
} | ||
|
||
if (textChanges.length > 0) { | ||
return [{ | ||
description: getLocaleSpecificMessage(Diagnostics.Implement_inherited_abstract_class), | ||
changes: [{ | ||
fileName: sourceFile.fileName, | ||
textChanges: textChanges | ||
}] | ||
}]; | ||
} | ||
|
||
return undefined; | ||
} | ||
}); | ||
|
||
function getChanges(interfaceClause: Node, existingMembers: string[], startPos: number, checker: TypeChecker, reference: boolean, trackingAddedMembers: string[], newLineCharacter: string): TextChange[] { | ||
const type = checker.getTypeAtLocation(interfaceClause); | ||
const changesArray: TextChange[] = []; | ||
|
||
if (type && type.symbol && type.symbol.declarations) { | ||
const interfaceMembers = getMembers(<InterfaceDeclaration>type.symbol.declarations[0], checker); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about inherited members? e.g.: interface Base { a: string }
interface Derived extends Base { b: number}
class C implements Derived {} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This works, see e.g |
||
for (let j = 0; interfaceMembers && j < interfaceMembers.length; j++) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed. |
||
if (interfaceMembers[j].name && existingMembers.indexOf(interfaceMembers[j].name.getText()) === -1) { | ||
if (interfaceMembers[j].kind === SyntaxKind.PropertySignature) { | ||
const interfaceProperty = <PropertySignature>interfaceMembers[j]; | ||
if (trackingAddedMembers.indexOf(interfaceProperty.name.getText()) === -1) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this does not handle computed properties correctly. e.g. |
||
let propertyText = ""; | ||
if (reference) { | ||
propertyText = `${interfaceProperty.name.getText()} : ${getDefaultValue(interfaceProperty.type.kind)},${newLineCharacter}`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about optional ones? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and readonly. |
||
} | ||
else { | ||
propertyText = interfaceProperty.getText(); | ||
const stringToAdd = !(propertyText.match(/;$/)) ? `;${newLineCharacter}` : newLineCharacter; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. an interface property declaration can end in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add tests. |
||
propertyText += stringToAdd; | ||
} | ||
changesArray.push({ newText: propertyText, span: { start: startPos, length: 0 } }); | ||
trackingAddedMembers.push(interfaceProperty.name.getText()); | ||
} | ||
} | ||
else if (interfaceMembers[j].kind === SyntaxKind.MethodSignature || interfaceMembers[j].kind === SyntaxKind.MethodDeclaration) { | ||
const interfaceMethod = <MethodSignature>interfaceMembers[j]; | ||
handleMethods(interfaceMethod, startPos, reference, trackingAddedMembers, changesArray, newLineCharacter); | ||
} | ||
} | ||
} | ||
} | ||
|
||
if (reference && existingMembers.length === 0 && changesArray.length > 0) { | ||
let lastValue = changesArray[changesArray.length - 1].newText; | ||
lastValue = `${lastValue.substr(0, lastValue.length - (newLineCharacter.length + 1))} ${newLineCharacter}`; | ||
changesArray[changesArray.length - 1].newText = lastValue; | ||
} | ||
|
||
return changesArray; | ||
} | ||
|
||
function getMembers(declaration: InterfaceDeclaration, checker: TypeChecker): TypeElement[] { | ||
const clauses = getInterfaceBaseTypeNodes(declaration); | ||
let result: TypeElement[] = []; | ||
for (let i = 0; clauses && i < clauses.length; i++) { | ||
const type = checker.getTypeAtLocation(clauses[i]); | ||
if (type && type.symbol && type.symbol.declarations) { | ||
result = result.concat(getMembers(<InterfaceDeclaration>type.symbol.declarations[0], checker)); | ||
} | ||
} | ||
|
||
if (declaration.members) { | ||
result = result.concat(declaration.members); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
function getClassMembers(classDeclaration: ClassDeclaration): string[] { | ||
const classMembers: string[] = []; | ||
for (let i = 0; classDeclaration.members && i < classDeclaration.members.length; i++) { | ||
if (classDeclaration.members[i].name) { | ||
classMembers.push(classDeclaration.members[i].name.getText()); | ||
} | ||
} | ||
return classMembers; | ||
} | ||
|
||
function getMembersAndStartPosFromReference(variableDeclaration: VariableDeclaration): { startPos: number, members: string[] } { | ||
const children = variableDeclaration.getChildren(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const variableMembers: string[] = []; | ||
let startPos = 0; | ||
|
||
ts.forEach(children, child => { | ||
if (child.kind === SyntaxKind.ObjectLiteralExpression) { | ||
const properties = (<ObjectLiteralExpression>child).properties; | ||
if (properties) { | ||
startPos = properties.pos; | ||
} | ||
for (let j = 0; properties && j < properties.length; j++) { | ||
if (properties[j].name) { | ||
variableMembers.push(properties[j].name.getText()); | ||
} | ||
} | ||
} | ||
}); | ||
|
||
return { startPos: startPos, members: variableMembers }; | ||
} | ||
|
||
function getDefaultValue(kind: SyntaxKind): string { | ||
switch (kind) { | ||
case SyntaxKind.StringKeyword: | ||
return '""'; | ||
case SyntaxKind.BooleanKeyword: | ||
return "false"; | ||
case SyntaxKind.NumberKeyword: | ||
return "0"; | ||
default: | ||
return "null"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is neither correct nor complete. what about literal types, other primitives, null and undefined?.. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed. |
||
} | ||
} | ||
|
||
function handleMethods(interfaceMethod: MethodSignature, startPos: number, isReference: boolean, trackingAddedMembers: string[], textChanges: TextChange[], newLineCharacter: string) { | ||
const methodBody = "throw new Error('Method not Implemented');"; | ||
|
||
if (trackingAddedMembers.indexOf(interfaceMethod.name.getText())) { | ||
const methodName = interfaceMethod.name.getText(); | ||
const typeParameterArray: string[] = []; | ||
|
||
for (let i = 0; interfaceMethod.typeParameters && i < interfaceMethod.typeParameters.length; i++) { | ||
typeParameterArray.push(interfaceMethod.typeParameters[i].getText()); | ||
} | ||
|
||
const parameterArray: string[] = []; | ||
for (let j = 0; interfaceMethod.parameters && j < interfaceMethod.parameters.length; j++) { | ||
parameterArray.push(interfaceMethod.parameters[j].getText()); | ||
} | ||
|
||
let methodText = methodName; | ||
if (typeParameterArray.length > 0) { | ||
methodText += "<"; | ||
} | ||
|
||
for (let k = 0; k < typeParameterArray.length; k++) { | ||
methodText += typeParameterArray[k]; | ||
if (k !== typeParameterArray.length - 1) { | ||
methodText += ","; | ||
} | ||
} | ||
|
||
if (typeParameterArray.length > 0) { | ||
methodText += ">"; | ||
} | ||
|
||
methodText += "("; | ||
for (let k = 0; k < parameterArray.length; k++) { | ||
methodText += parameterArray[k]; | ||
if (k !== parameterArray.length - 1) { | ||
methodText += ","; | ||
} | ||
} | ||
|
||
methodText += `)`; | ||
if (interfaceMethod.type) { | ||
methodText += ":" + interfaceMethod.type.getText(); | ||
} | ||
|
||
methodText += `{${newLineCharacter}${methodBody}${newLineCharacter}`; | ||
methodText = isReference ? methodText.concat(`},${newLineCharacter}`) : methodText.concat(`}${newLineCharacter}`); | ||
|
||
textChanges.push({ newText: methodText, span: { start: startPos, length: 0 } }); | ||
trackingAddedMembers.push(interfaceMethod.name.getText()); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
//// interface I1 {} | ||
//// [|class c1 extends I1|]{} | ||
|
||
verify.codeFixAtPosition("class c1 implements I1"); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
////interface I1 {} | ||
////[|class c1<T extends string , U> extends I1|]{} | ||
|
||
verify.codeFixAtPosition("class c1<T extends string , U> implements I1"); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
//// namespace N1 { | ||
//// export interface I1 { | ||
//// f1(); | ||
//// } | ||
//// } | ||
//// interface I1 { | ||
//// f1(); | ||
//// } | ||
//// | ||
//// class C1 implements N1.I1 {[| | ||
//// |]} | ||
|
||
verify.codeFixAtPosition(`f1(){ | ||
throw new Error('Method not Implemented'); | ||
} | ||
`); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
//// interface I1 { | ||
//// f1() | ||
//// } | ||
//// | ||
//// interface I2 extends I1 { | ||
//// | ||
//// } | ||
//// | ||
//// | ||
//// class C1 implements I2 {[| | ||
//// |]} | ||
|
||
verify.codeFixAtPosition(`f1(){ | ||
throw new Error('Method not Implemented'); | ||
} | ||
`); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/// <reference path='fourslash.ts' /> | ||
|
||
//// interface I1 { | ||
//// | ||
//// } | ||
//// | ||
//// interface I2 extends I1 { | ||
//// f1(); | ||
//// } | ||
//// | ||
//// interface I3 extends I2 {} | ||
//// | ||
//// class C1 implements I3 {[| | ||
//// |]} | ||
|
||
verify.codeFixAtPosition(`f1(){ | ||
throw new Error('Method not Implemented'); | ||
} | ||
`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not sufficient. what if the class already has an implements clause. you want to first look for implements clause, and if you found one, move the interface to the implements list otherwise rename the extends to implements. one option is to generate two edits, one that always removes the extends clause all together, and another one that adds the target to the right place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also please add tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. The edit is to change extends -> implements, and if implements is already present, change implements -> ','