Skip to content

Commit c639d3a

Browse files
authored
feat(27615): Add missing member fix should work for type literals (#47212)
1 parent f57bdaa commit c639d3a

12 files changed

+215
-98
lines changed

Diff for: src/services/codefixes/fixAddMissingMember.ts

+61-53
Large diffs are not rendered by default.

Diff for: 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

Diff for: 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
}

Diff for: 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
}

Diff for: src/services/codefixes/helpers.ts

+36-23
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ namespace ts.codefix {
277277
}
278278

279279
export function createSignatureDeclarationFromCallExpression(
280-
kind: SyntaxKind.MethodDeclaration | SyntaxKind.FunctionDeclaration,
280+
kind: SyntaxKind.MethodDeclaration | SyntaxKind.FunctionDeclaration | SyntaxKind.MethodSignature,
281281
context: CodeFixContextBase,
282282
importAdder: ImportAdder,
283283
call: CallExpression,
@@ -313,29 +313,42 @@ namespace ts.codefix {
313313
? undefined
314314
: checker.typeToTypeNode(contextualType, contextNode, /*flags*/ undefined, tracker);
315315

316-
if (kind === 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-
isInterfaceDeclaration(contextNode) ? undefined : createStubbedMethodBody(quotePreference)
327-
);
316+
switch (kind) {
317+
case SyntaxKind.MethodDeclaration:
318+
return factory.createMethodDeclaration(
319+
/*decorators*/ undefined,
320+
modifiers,
321+
asteriskToken,
322+
name,
323+
/*questionToken*/ undefined,
324+
typeParameters,
325+
parameters,
326+
type,
327+
createStubbedMethodBody(quotePreference)
328+
);
329+
case SyntaxKind.MethodSignature:
330+
return factory.createMethodSignature(
331+
modifiers,
332+
name,
333+
/*questionToken*/ undefined,
334+
typeParameters,
335+
parameters,
336+
type
337+
);
338+
case SyntaxKind.FunctionDeclaration:
339+
return factory.createFunctionDeclaration(
340+
/*decorators*/ undefined,
341+
modifiers,
342+
asteriskToken,
343+
name,
344+
typeParameters,
345+
parameters,
346+
type,
347+
createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference)
348+
);
349+
default:
350+
Debug.fail("Unexpected kind");
328351
}
329-
return factory.createFunctionDeclaration(
330-
/*decorators*/ undefined,
331-
modifiers,
332-
asteriskToken,
333-
name,
334-
typeParameters,
335-
parameters,
336-
type,
337-
createStubbedBody(Diagnostics.Function_not_implemented.message, quotePreference)
338-
);
339352
}
340353

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

Diff for: src/services/textChanges.ts

+18-18
Original file line numberDiff line numberDiff line change
@@ -618,27 +618,27 @@ namespace ts.textChanges {
618618
});
619619
}
620620

621-
public insertNodeAtClassStart(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration, newElement: ClassElement): void {
622-
this.insertNodeAtStartWorker(sourceFile, cls, newElement);
621+
public insertMemberAtStart(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | TypeLiteralNode, newElement: ClassElement | PropertySignature | MethodSignature): void {
622+
this.insertNodeAtStartWorker(sourceFile, node, newElement);
623623
}
624624

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

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

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

660-
private computeIndentationForNewMember(sourceFile: SourceFile, cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression) {
661-
const clsStart = cls.getStart(sourceFile);
662-
return formatting.SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(clsStart, sourceFile), clsStart, sourceFile, this.formatContext.options)
660+
private computeIndentationForNewMember(sourceFile: SourceFile, node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode) {
661+
const nodeStart = node.getStart(sourceFile);
662+
return formatting.SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(nodeStart, sourceFile), nodeStart, sourceFile, this.formatContext.options)
663663
+ (this.formatContext.options.indentSize ?? 4);
664664
}
665665

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

677-
const members = getMembersOrProperties(cls);
677+
const members = getMembersOrProperties(node);
678678
const isEmpty = members.length === 0;
679-
const isFirstInsertion = addToSeen(this.classesWithNodesInsertedAtStart, getNodeId(cls), { node: cls, sourceFile });
680-
const insertTrailingComma = isObjectLiteralExpression(cls) && (!isJsonSourceFile(sourceFile) || !isEmpty);
681-
const insertLeadingComma = isObjectLiteralExpression(cls) && isJsonSourceFile(sourceFile) && isEmpty && !isFirstInsertion;
679+
const isFirstInsertion = addToSeen(this.classesWithNodesInsertedAtStart, getNodeId(node), { node, sourceFile });
680+
const insertTrailingComma = isObjectLiteralExpression(node) && (!isJsonSourceFile(sourceFile) || !isEmpty);
681+
const insertLeadingComma = isObjectLiteralExpression(node) && isJsonSourceFile(sourceFile) && isEmpty && !isFirstInsertion;
682682
return {
683683
indentation,
684684
prefix: (insertLeadingComma ? "," : "") + this.newLineCharacter,
@@ -998,8 +998,8 @@ namespace ts.textChanges {
998998
const close = findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile);
999999
return [open?.end, close?.end];
10001000
}
1001-
function getMembersOrProperties(cls: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression): NodeArray<Node> {
1002-
return isObjectLiteralExpression(cls) ? cls.properties : cls.members;
1001+
function getMembersOrProperties(node: ClassLikeDeclaration | InterfaceDeclaration | ObjectLiteralExpression | TypeLiteralNode): NodeArray<Node> {
1002+
return isObjectLiteralExpression(node) ? node.properties : node.members;
10031003
}
10041004

10051005
export type ValidateNonFormattedText = (node: Node, text: string) => void;

Diff for: tests/cases/fourslash/codeFixAddMissingMember22.ts

+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;
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+
});

Diff for: tests/cases/fourslash/codeFixAddMissingMember23.ts

+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+
});

Diff for: tests/cases/fourslash/codeFixAddMissingMember24.ts

+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+
});

Diff for: tests/cases/fourslash/codeFixAddMissingMember25.ts

+18
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+
});

Diff for: tests/cases/fourslash/codeFixAddMissingMember26.ts

+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.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+
});

Diff for: 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)