Skip to content

Commit 44173aa

Browse files
author
Andy Hanson
committed
Fix insertNodeAtClassStart for empty class with comment
1 parent a004571 commit 44173aa

16 files changed

+160
-54
lines changed

src/compiler/utilities.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -4020,12 +4020,14 @@ namespace ts {
40204020
}
40214021

40224022
/** Add a value to a set, and return true if it wasn't already present. */
4023-
export function addToSeen(seen: Map<true>, key: string | number): boolean {
4023+
export function addToSeen(seen: Map<true>, key: string | number): boolean;
4024+
export function addToSeen<T>(seen: Map<T>, key: string | number, value: T): boolean;
4025+
export function addToSeen<T>(seen: Map<T>, key: string | number, value: T = true as any): boolean {
40244026
key = String(key);
40254027
if (seen.has(key)) {
40264028
return false;
40274029
}
4028-
seen.set(key, true);
4030+
seen.set(key, value);
40294031
return true;
40304032
}
40314033

src/services/codefixes/helpers.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ namespace ts.codefix {
2525
}
2626

2727
const declaration = declarations[0];
28-
// Clone name to remove leading trivia.
29-
const name = getSynthesizedDeepClone(getNameOfDeclaration(declaration)) as PropertyName;
28+
const name = getSynthesizedDeepCloneWithoutTrivia(getNameOfDeclaration(declaration)) as PropertyName;
3029
const visibilityModifier = createVisibilityModifier(getModifierFlags(declaration));
3130
const modifiers = visibilityModifier ? createNodeArray([visibilityModifier]) : undefined;
3231
const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration));
@@ -69,7 +68,7 @@ namespace ts.codefix {
6968

7069
for (const signature of signatures) {
7170
// Need to ensure nodes are fresh each time so they can have different positions.
72-
outputMethod(signature, getSynthesizedDeepClones(modifiers), getSynthesizedDeepClone(name));
71+
outputMethod(signature, getSynthesizedDeepClones(modifiers), getSynthesizedDeepCloneWithoutTrivia(name));
7372
}
7473

7574
if (declarations.length > signatures.length) {
@@ -104,7 +103,7 @@ namespace ts.codefix {
104103
}
105104

106105
function getSynthesizedDeepClones<T extends Node>(nodes: NodeArray<T> | undefined): NodeArray<T> | undefined {
107-
return nodes && createNodeArray(nodes.map(getSynthesizedDeepClone));
106+
return nodes && createNodeArray(nodes.map(getSynthesizedDeepCloneWithoutTrivia));
108107
}
109108

110109
export function createMethodFromCallExpression(

src/services/textChanges.ts

+27-24
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,7 @@ namespace ts.textChanges {
208208
export class ChangeTracker {
209209
private readonly changes: Change[] = [];
210210
private readonly deletedNodesInLists: true[] = []; // Stores ids of nodes in lists that we already deleted. Used to avoid deleting `, ` twice in `a, b`.
211-
// Map from class id to nodes to insert at the start
212-
private readonly nodesInsertedAtClassStarts = createMap<{ sourceFile: SourceFile, cls: ClassLikeDeclaration, members: ClassElement[] }>();
211+
private readonly classesWithNodesInsertedAtStart = createMap<ClassDeclaration>(); // Set<ClassDeclaration> implemented as Map<node id, ClassDeclaration>
213212

214213
public static fromContext(context: TextChangesContext): ChangeTracker {
215214
return new ChangeTracker(getNewLineOrDefaultFromHost(context.host, context.formatContext.options), context.formatContext);
@@ -339,8 +338,7 @@ namespace ts.textChanges {
339338
}
340339

341340
public insertNodeBefore(sourceFile: SourceFile, before: Node, newNode: Node, blankLineBetween = false) {
342-
const pos = getAdjustedStartPosition(sourceFile, before, {}, Position.Start);
343-
return this.replaceRange(sourceFile, { pos, end: pos }, newNode, this.getOptionsForInsertNodeBefore(before, blankLineBetween));
341+
this.insertNodeAt(sourceFile, getAdjustedStartPosition(sourceFile, before, {}, Position.Start), newNode, this.getOptionsForInsertNodeBefore(before, blankLineBetween));
344342
}
345343

346344
public insertModifierBefore(sourceFile: SourceFile, modifier: SyntaxKind, before: Node): void {
@@ -435,21 +433,20 @@ namespace ts.textChanges {
435433
}
436434

437435
public insertNodeAtClassStart(sourceFile: SourceFile, cls: ClassLikeDeclaration, newElement: ClassElement): void {
438-
const firstMember = firstOrUndefined(cls.members);
439-
if (!firstMember) {
440-
const id = getNodeId(cls).toString();
441-
const newMembers = this.nodesInsertedAtClassStarts.get(id);
442-
if (newMembers) {
443-
Debug.assert(newMembers.sourceFile === sourceFile && newMembers.cls === cls);
444-
newMembers.members.push(newElement);
436+
const clsStart = cls.getStart(sourceFile);
437+
let prefix = "";
438+
let suffix = this.newLineCharacter;
439+
if (addToSeen(this.classesWithNodesInsertedAtStart, getNodeId(cls), cls)) {
440+
prefix = this.newLineCharacter;
441+
// For `class C {\n}`, don't add the trailing "\n"
442+
if (cls.members.length === 0 && !(positionsAreOnSameLine as any)(...getClassBraceEnds(cls, sourceFile), sourceFile)) { // TODO: GH#4130 remove 'as any'
443+
suffix = "";
445444
}
446-
else {
447-
this.nodesInsertedAtClassStarts.set(id, { sourceFile, cls, members: [newElement] });
448-
}
449-
}
450-
else {
451-
this.insertNodeBefore(sourceFile, firstMember, newElement);
452445
}
446+
447+
const indentation = formatting.SmartIndenter.findFirstNonWhitespaceColumn(getLineStartPositionForPosition(clsStart, sourceFile), clsStart, sourceFile, this.formatContext.options)
448+
+ this.formatContext.options.indentSize;
449+
this.insertNodeAt(sourceFile, cls.members.pos, newElement, { indentation, prefix, suffix });
453450
}
454451

455452
public insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node): this {
@@ -601,12 +598,14 @@ namespace ts.textChanges {
601598
return this;
602599
}
603600

604-
private finishInsertNodeAtClassStart(): void {
605-
this.nodesInsertedAtClassStarts.forEach(({ sourceFile, cls, members }) => {
606-
const newCls = cls.kind === SyntaxKind.ClassDeclaration
607-
? updateClassDeclaration(cls, cls.decorators, cls.modifiers, cls.name, cls.typeParameters, cls.heritageClauses, members)
608-
: updateClassExpression(cls, cls.modifiers, cls.name, cls.typeParameters, cls.heritageClauses, members);
609-
this.replaceNode(sourceFile, cls, newCls);
601+
private finishClassesWithNodesInsertedAtStart(): void {
602+
this.classesWithNodesInsertedAtStart.forEach(cls => {
603+
const sourceFile = cls.getSourceFile();
604+
const [openBraceEnd, closeBraceEnd] = getClassBraceEnds(cls, sourceFile);
605+
// For `class C { }` remove the whitespace inside the braces.
606+
if (positionsAreOnSameLine(openBraceEnd, closeBraceEnd, sourceFile) && openBraceEnd !== closeBraceEnd - 1) {
607+
this.deleteRange(sourceFile, createTextRange(openBraceEnd, closeBraceEnd - 1));
608+
}
610609
});
611610
}
612611

@@ -617,11 +616,15 @@ namespace ts.textChanges {
617616
* so we can only call this once and can't get the non-formatted text separately.
618617
*/
619618
public getChanges(validate?: ValidateNonFormattedText): FileTextChanges[] {
620-
this.finishInsertNodeAtClassStart();
619+
this.finishClassesWithNodesInsertedAtStart();
621620
return changesToText.getTextChangesFromChanges(this.changes, this.newLineCharacter, this.formatContext, validate);
622621
}
623622
}
624623

624+
function getClassBraceEnds(cls: ClassLikeDeclaration, sourceFile: SourceFile): [number, number] {
625+
return [findChildOfKind(cls, SyntaxKind.OpenBraceToken, sourceFile).end, findChildOfKind(cls, SyntaxKind.CloseBraceToken, sourceFile).end];
626+
}
627+
625628
export type ValidateNonFormattedText = (node: Node, text: string) => void;
626629

627630
namespace changesToText {

src/services/utilities.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,12 @@ namespace ts {
14881488
return visited;
14891489
}
14901490

1491+
export function getSynthesizedDeepCloneWithoutTrivia<T extends Node>(node: T): T {
1492+
const clone = getSynthesizedDeepClone(node);
1493+
suppressLeadingAndTrailingTrivia(clone);
1494+
return clone;
1495+
}
1496+
14911497
export function getSynthesizedDeepClones<T extends Node>(nodes: NodeArray<T> | undefined): NodeArray<T> | undefined {
14921498
return nodes && createNodeArray(nodes.map(getSynthesizedDeepClone), nodes.hasTrailingComma);
14931499
}

tests/cases/fourslash/codeFixAddMissingMember.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ verify.codeFix({
1111
index: 0,
1212
newFileContent: `class C {
1313
foo: number;
14+
1415
method() {
1516
this.foo = 10;
1617
}

tests/cases/fourslash/codeFixAddMissingMember2.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ verify.codeFix({
1111
index: 1,
1212
newFileContent: `class C {
1313
[x: string]: number;
14+
1415
method() {
1516
this.foo = 10;
1617
}

tests/cases/fourslash/codeFixAddMissingMember3.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ verify.codeFix({
1111
index: 0,
1212
newFileContent: `class C {
1313
static foo: number;
14+
1415
static method() {
1516
this.foo = 10;
1617
}

tests/cases/fourslash/codeFixAddMissingMember_all.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ verify.codeFixAll({
1717
y(): any {
1818
throw new Error("Method not implemented.");
1919
}
20+
2021
method() {
2122
this.x = 0;
2223
this.y();

tests/cases/fourslash/codeFixAddMissingMember_all_js.ts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ verify.codeFixAll({
2121
y() {
2222
throw new Error("Method not implemented.");
2323
}
24+
2425
constructor() {
2526
this.x = undefined;
2627
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////abstract class A {
4+
//// abstract m() : void;
5+
////}
6+
////
7+
////class B extends A {
8+
//// // comment
9+
////}
10+
11+
verify.codeFix({
12+
description: "Implement inherited abstract class",
13+
newFileContent:
14+
`abstract class A {
15+
abstract m() : void;
16+
}
17+
18+
class B extends A {
19+
m(): void {
20+
throw new Error("Method not implemented.");
21+
}
22+
// comment
23+
}`,
24+
});

tests/cases/fourslash/codeFixClassImplementClassMultipleSignatures2.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
//// method(a: string): Function;
77
//// method(a: string | number, b?: string | number): boolean | Function { return a + b as any; }
88
////}
9-
////class C implements A {[| |]}
9+
////class C implements A { }
1010

1111
verify.codeFix({
1212
description: "Implement interface 'A'",

tests/cases/fourslash/codeFixClassImplementInterfaceComments.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ verify.codeFix({
3636
/**close-brace prefix*/ }
3737
/**close-brace prefix*/ }
3838
class C implements N.I {
39-
/** property prefix */ a /** colon prefix */: N.E.a;
40-
/** property prefix */ b /** colon prefix */: N.E;
41-
/**method signature prefix */ foo /**open angle prefix */<X>(a: X): string {
39+
a: N.E.a;
40+
b: N.E;
41+
foo<X>(a: X): string {
4242
throw new Error("Method not implemented.");
4343
}
4444
}`,

tests/cases/fourslash/codeFixClassImplementInterfaceMemberOrdering.ts

-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ class C implements I {
8484
20: any;
8585
21: any;
8686
22: any;
87-
/** a nice safe prime */
8887
23: any;
8988
}`,
9089
});

tests/cases/fourslash/codeFixClassImplementInterfaceTypeParamInstantiateNumber.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/// <reference path='fourslash.ts' />
22

33
////interface I<T> { x: T; }
4-
////class C implements I<number> {[| |]}
4+
////class C implements I<number> { }
55

66
verify.codeFix({
77
description: "Implement interface 'I<number>'",
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/// <reference path='fourslash.ts' />
22

3-
//// class A {[|
4-
//// |]static foo0() {
3+
//// class A {
4+
//// static foo0() {
55
//// this.m1(1,2,3);
66
//// A.m2(1,2);
77
//// this.prop1 = 10;
@@ -12,51 +12,89 @@
1212
verify.codeFix({
1313
description: "Declare static method 'm1'",
1414
index: 0,
15-
newRangeContent: `
15+
newFileContent:
16+
`class A {
1617
static m1(arg0: any, arg1: any, arg2: any): any {
1718
throw new Error("Method not implemented.");
1819
}
19-
`,
20+
21+
static foo0() {
22+
this.m1(1,2,3);
23+
A.m2(1,2);
24+
this.prop1 = 10;
25+
A.prop2 = "asdf";
26+
}
27+
}`,
2028
});
2129

2230
verify.codeFix({
2331
description: "Declare static method 'm2'",
2432
index: 0,
25-
newRangeContent: `
33+
newFileContent:
34+
`class A {
2635
static m2(arg0: any, arg1: any): any {
2736
throw new Error("Method not implemented.");
2837
}
38+
2939
static m1(arg0: any, arg1: any, arg2: any): any {
3040
throw new Error("Method not implemented.");
3141
}
32-
`,
42+
43+
static foo0() {
44+
this.m1(1,2,3);
45+
A.m2(1,2);
46+
this.prop1 = 10;
47+
A.prop2 = "asdf";
48+
}
49+
}`,
3350
});
3451

3552
verify.codeFix({
3653
description: "Declare static property 'prop1'",
3754
index: 0,
38-
newRangeContent: `
55+
newFileContent:
56+
`class A {
3957
static prop1: number;
58+
4059
static m2(arg0: any, arg1: any): any {
4160
throw new Error("Method not implemented.");
4261
}
62+
4363
static m1(arg0: any, arg1: any, arg2: any): any {
4464
throw new Error("Method not implemented.");
4565
}
46-
`,
66+
67+
static foo0() {
68+
this.m1(1,2,3);
69+
A.m2(1,2);
70+
this.prop1 = 10;
71+
A.prop2 = "asdf";
72+
}
73+
}`,
4774
});
4875

4976
verify.codeFix({
5077
description: "Declare static property 'prop2'",
5178
index: 1, // fix at index 0 is to change the spelling to 'prop1'
52-
newRangeContent: `
79+
newFileContent:
80+
`class A {
5381
static prop2: string;
82+
5483
static prop1: number;
84+
5585
static m2(arg0: any, arg1: any): any {
5686
throw new Error("Method not implemented.");
5787
}
88+
5889
static m1(arg0: any, arg1: any, arg2: any): any {
5990
throw new Error("Method not implemented.");
6091
}
61-
`,
92+
93+
static foo0() {
94+
this.m1(1,2,3);
95+
A.m2(1,2);
96+
this.prop1 = 10;
97+
A.prop2 = "asdf";
98+
}
99+
}`,
62100
});

0 commit comments

Comments
 (0)