Skip to content

Commit 452fe0d

Browse files
committed
feat(27615): Add missing member fix should work for type literals
1 parent 8a5d476 commit 452fe0d

12 files changed

+201
-94
lines changed

src/services/codefixes/fixAddMissingMember.ts

+50-50
Large diffs are not rendered by default.

src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ namespace ts.codefix {
4242
const abstractAndNonPrivateExtendsSymbols = checker.getPropertiesOfType(instantiatedExtendsType).filter(symbolPointsToNonPrivateAndAbstractMember);
4343

4444
const importAdder = createImportAdder(sourceFile, context.program, preferences, context.host);
45-
createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, sourceFile, context, preferences, importAdder, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member as ClassElement));
45+
createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, sourceFile, context, preferences, importAdder, member => changeTracker.insertMemberAtStart(sourceFile, classDeclaration, member as ClassElement));
4646
importAdder.writeFixes(changeTracker);
4747
}
4848

src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ namespace ts.codefix {
8080
changeTracker.insertNodeAfter(sourceFile, constructor, newElement);
8181
}
8282
else {
83-
changeTracker.insertNodeAtClassStart(sourceFile, cls, newElement);
83+
changeTracker.insertMemberAtStart(sourceFile, cls, newElement);
8484
}
8585
}
8686
}

src/services/codefixes/generateAccessors.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ namespace ts.codefix {
214214
}
215215

216216
function insertAccessor(changeTracker: textChanges.ChangeTracker, file: SourceFile, accessor: AccessorDeclaration, declaration: AcceptedDeclaration, container: ContainerDeclaration) {
217-
isParameterPropertyDeclaration(declaration, declaration.parent) ? changeTracker.insertNodeAtClassStart(file, container as ClassLikeDeclaration, accessor) :
217+
isParameterPropertyDeclaration(declaration, declaration.parent) ? changeTracker.insertMemberAtStart(file, container as ClassLikeDeclaration, accessor) :
218218
isPropertyAssignment(declaration) ? changeTracker.insertNodeAfterComma(file, declaration, accessor) :
219219
changeTracker.insertNodeAfter(file, declaration, accessor);
220220
}

src/services/codefixes/helpers.ts

+34-23
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ namespace ts.codefix {
276276
}
277277

278278
export function createSignatureDeclarationFromCallExpression(
279-
kind: SyntaxKind.MethodDeclaration | SyntaxKind.FunctionDeclaration,
279+
kind: SyntaxKind.MethodDeclaration | SyntaxKind.FunctionDeclaration | SyntaxKind.MethodSignature,
280280
context: CodeFixContextBase,
281281
importAdder: ImportAdder,
282282
call: CallExpression,
@@ -312,29 +312,40 @@ namespace ts.codefix {
312312
? undefined
313313
: checker.typeToTypeNode(contextualType, contextNode, /*flags*/ undefined, tracker);
314314

315-
if (kind === SyntaxKind.MethodDeclaration) {
316-
return factory.createMethodDeclaration(
317-
/*decorators*/ undefined,
318-
modifiers,
319-
asteriskToken,
320-
name,
321-
/*questionToken*/ undefined,
322-
typeParameters,
323-
parameters,
324-
type,
325-
isInterfaceDeclaration(contextNode) ? undefined : createStubbedMethodBody(quotePreference)
326-
);
315+
switch (kind) {
316+
case SyntaxKind.MethodDeclaration:
317+
return factory.createMethodDeclaration(
318+
/*decorators*/ undefined,
319+
modifiers,
320+
asteriskToken,
321+
name,
322+
/*questionToken*/ undefined,
323+
typeParameters,
324+
parameters,
325+
type,
326+
createStubbedMethodBody(quotePreference)
327+
);
328+
case SyntaxKind.MethodSignature:
329+
return factory.createMethodSignature(
330+
modifiers,
331+
name,
332+
/*questionToken*/ undefined,
333+
typeParameters,
334+
parameters,
335+
type
336+
);
337+
default:
338+
return factory.createFunctionDeclaration(
339+
/*decorators*/ undefined,
340+
modifiers,
341+
asteriskToken,
342+
name,
343+
typeParameters,
344+
parameters,
345+
type,
346+
createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference)
347+
);
327348
}
328-
return factory.createFunctionDeclaration(
329-
/*decorators*/ undefined,
330-
modifiers,
331-
asteriskToken,
332-
name,
333-
typeParameters,
334-
parameters,
335-
type,
336-
createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference)
337-
);
338349
}
339350

340351
export function typeToAutoImportableTypeNode(checker: TypeChecker, importAdder: ImportAdder, type: Type, contextNode: Node | undefined, scriptTarget: ScriptTarget, flags?: NodeBuilderFlags, tracker?: SymbolTracker): TypeNode | undefined {

src/services/textChanges.ts

+17-17
Original file line numberDiff line numberDiff line change
@@ -616,27 +616,27 @@ namespace ts.textChanges {
616616
});
617617
}
618618

619-
public insertNodeAtClassStart(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration, newElement: ClassElement): void {
620-
this.insertNodeAtStartWorker(sourceFile, cls, newElement);
619+
public insertMemberAtStart(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode, newElement: ClassElement | PropertySignature | MethodSignature): void {
620+
this.insertNodeAtStartWorker(sourceFile, node, newElement);
621621
}
622622

623623
public insertNodeAtObjectStart(sourceFile: SourceFile, obj: ObjectLiteralExpression, newElement: ObjectLiteralElementLike): void {
624624
this.insertNodeAtStartWorker(sourceFile, obj, newElement);
625625
}
626626

627-
private insertNodeAtStartWorker(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, newElement: ClassElement | ObjectLiteralElementLike): void {
628-
const indentation = this.guessIndentationFromExistingMembers(sourceFile, cls) ?? this.computeIndentationForNewMember(sourceFile, cls);
629-
this.insertNodeAt(sourceFile, getMembersOrProperties(cls).pos, newElement, this.getInsertNodeAtStartInsertOptions(sourceFile, cls, indentation));
627+
private insertNodeAtStartWorker(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode, newElement: ClassElement | ObjectLiteralElementLike | PropertySignature | MethodSignature): void {
628+
const indentation = this.guessIndentationFromExistingMembers(sourceFile, node) ?? this.computeIndentationForNewMember(sourceFile, node);
629+
this.insertNodeAt(sourceFile, getMembersOrProperties(node).pos, newElement, this.getInsertNodeAtStartInsertOptions(sourceFile, node, indentation));
630630
}
631631

632632
/**
633633
* Tries to guess the indentation from the existing members of a class/interface/object. All members must be on
634634
* new lines and must share the same indentation.
635635
*/
636-
private guessIndentationFromExistingMembers(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression) {
636+
private guessIndentationFromExistingMembers(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode) {
637637
let indentation: number | undefined;
638-
let lastRange: TextRange = cls;
639-
for (const member of getMembersOrProperties(cls)) {
638+
let lastRange: TextRange = node;
639+
for (const member of getMembersOrProperties(node)) {
640640
if (rangeStartPositionsAreOnSameLine(lastRange, member, sourceFile)) {
641641
// each indented member must be on a new line
642642
return undefined;
@@ -655,13 +655,13 @@ namespace ts.textChanges {
655655
return indentation;
656656
}
657657

658-
private computeIndentationForNewMember(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression) {
659-
const clsStart = cls.getStart(sourceFile);
658+
private computeIndentationForNewMember(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode) {
659+
const clsStart = node.getStart(sourceFile);
660660
return formatting.SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(clsStart, sourceFile), clsStart, sourceFile, this.formatContext.options)
661661
+ (this.formatContext.options.indentSize ?? 4);
662662
}
663663

664-
private getInsertNodeAtStartInsertOptions(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression, indentation: number): InsertNodeOptions {
664+
private getInsertNodeAtStartInsertOptions(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode, indentation: number): InsertNodeOptions {
665665
// Rules:
666666
// - Always insert leading newline.
667667
// - For object literals:
@@ -672,11 +672,11 @@ namespace ts.textChanges {
672672
// - Only insert a trailing newline if body is single-line and there are no other insertions for the node.
673673
// NOTE: This is handled in `finishClassesWithNodesInsertedAtStart`.
674674

675-
const members = getMembersOrProperties(cls);
675+
const members = getMembersOrProperties(node);
676676
const isEmpty = members.length === 0;
677-
const isFirstInsertion = addToSeen(this.classesWithNodesInsertedAtStart, getNodeId(cls), { node: cls, sourceFile });
678-
const insertTrailingComma = isObjectLiteralExpression(cls) && (!isJsonSourceFile(sourceFile) || !isEmpty);
679-
const insertLeadingComma = isObjectLiteralExpression(cls) && isJsonSourceFile(sourceFile) && isEmpty && !isFirstInsertion;
677+
const isFirstInsertion = addToSeen(this.classesWithNodesInsertedAtStart, getNodeId(node), { node, sourceFile });
678+
const insertTrailingComma = isObjectLiteralExpression(node) && (!isJsonSourceFile(sourceFile) || !isEmpty);
679+
const insertLeadingComma = isObjectLiteralExpression(node) && isJsonSourceFile(sourceFile) && isEmpty && !isFirstInsertion;
680680
return {
681681
indentation,
682682
prefix: (insertLeadingComma ? "," : "") + this.newLineCharacter,
@@ -980,8 +980,8 @@ namespace ts.textChanges {
980980
const close = findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile);
981981
return [open?.end, close?.end];
982982
}
983-
function getMembersOrProperties(cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression): NodeArray<Node> {
984-
return isObjectLiteralExpression(cls) ? cls.properties : cls.members;
983+
function getMembersOrProperties(node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode): NodeArray<Node> {
984+
return isObjectLiteralExpression(node) ? node.properties : node.members;
985985
}
986986

987987
export type ValidateNonFormattedText = (node: Node, text: string) => void;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////[|type Foo = {};|]
4+
////function f(foo: Foo) {
5+
//// foo.y;
6+
////}
7+
8+
verify.codeFix({
9+
description: [ts.Diagnostics.Declare_property_0.message, "y"],
10+
index: 0,
11+
newRangeContent:
12+
`type Foo = {
13+
y: any;
14+
};`
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////[|type Foo = {};|]
4+
////function f(foo: Foo) {
5+
//// foo.y = 1;
6+
////}
7+
8+
verify.codeFix({
9+
description: [ts.Diagnostics.Declare_property_0.message, "y"],
10+
index: 0,
11+
newRangeContent:
12+
`type Foo = {
13+
y: number;
14+
};`
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////[|type Foo = {};|]
4+
////function f(foo: Foo) {
5+
//// foo.test(1, 1, "");
6+
////}
7+
8+
verify.codeFix({
9+
description: [ts.Diagnostics.Declare_method_0.message, "test"],
10+
index: 0,
11+
newRangeContent:
12+
`type Foo = {
13+
test(arg0: number, arg1: number, arg2: string);
14+
};`
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////[|type Foo = {
4+
//// y: number;
5+
////};|]
6+
////function f(foo: Foo) {
7+
//// foo.x = 1;
8+
////}
9+
10+
verify.codeFix({
11+
description: [ts.Diagnostics.Declare_property_0.message, "x"],
12+
index: 0,
13+
newRangeContent:
14+
`type Foo = {
15+
x: number;
16+
y: number;
17+
};`
18+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////[|type Foo = {};|]
4+
////function f(foo: Foo) {
5+
//// foo.x = 1;
6+
////}
7+
8+
verify.codeFix({
9+
description: [ts.Diagnostics.Add_index_signature_for_property_0.message, "x"],
10+
index: 1,
11+
newRangeContent:
12+
`type Foo = {
13+
[x: string]: number;
14+
};`
15+
});

tests/cases/fourslash/codeFixAddMissingMember_all.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@
2424
////
2525
////enum En {}
2626
////En.A;
27+
////
28+
////type T = {};
29+
////function foo(t: T) {
30+
//// t.x;
31+
//// t.y = 1;
32+
//// t.test(1, 2);
33+
////}
2734

2835
verify.codeFixAll({
2936
fixId: "fixMissingMember",
@@ -60,5 +67,16 @@ class Unrelated {
6067
enum En {
6168
A
6269
}
63-
En.A;`,
70+
En.A;
71+
72+
type T = {
73+
x: any;
74+
y: number;
75+
test(arg0: number, arg1: number);
76+
};
77+
function foo(t: T) {
78+
t.x;
79+
t.y = 1;
80+
t.test(1, 2);
81+
}`,
6482
});

0 commit comments

Comments
 (0)