Skip to content

Commit c8061d0

Browse files
committed
fix(39332): handle quotes preference in interface method signatures
1 parent 619658b commit c8061d0

12 files changed

+159
-66
lines changed

src/services/codefixes/fixClassDoesntImplementInheritedAbstractMember.ts

Lines changed: 1 addition & 1 deletion
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, context, preferences, importAdder, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member));
45+
createMissingMemberNodes(classDeclaration, abstractAndNonPrivateExtendsSymbols, sourceFile, context, preferences, importAdder, member => changeTracker.insertNodeAtClassStart(sourceFile, classDeclaration, member));
4646
importAdder.writeFixes(changeTracker);
4747
}
4848

src/services/codefixes/fixClassIncorrectlyImplementsInterface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ namespace ts.codefix {
6464
}
6565

6666
const importAdder = createImportAdder(sourceFile, context.program, preferences, context.host);
67-
createMissingMemberNodes(classDeclaration, nonPrivateAndNotExistedInHeritageClauseMembers, context, preferences, importAdder, member => insertInterfaceMemberNode(sourceFile, classDeclaration, member));
67+
createMissingMemberNodes(classDeclaration, nonPrivateAndNotExistedInHeritageClauseMembers, sourceFile, context, preferences, importAdder, member => insertInterfaceMemberNode(sourceFile, classDeclaration, member));
6868
importAdder.writeFixes(changeTracker);
6969

7070
function createMissingIndexSignatureDeclaration(type: InterfaceType, kind: IndexKind): void {

src/services/codefixes/helpers.ts

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ namespace ts.codefix {
77
* @param importAdder If provided, type annotations will use identifier type references instead of ImportTypeNodes, and the missing imports will be added to the importAdder.
88
* @returns Empty string iff there are no member insertions.
99
*/
10-
export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: readonly Symbol[], context: TypeConstructionContext, preferences: UserPreferences, importAdder: ImportAdder | undefined, addClassElement: (node: ClassElement) => void): void {
10+
export function createMissingMemberNodes(classDeclaration: ClassLikeDeclaration, possiblyMissingSymbols: readonly Symbol[], sourceFile: SourceFile, context: TypeConstructionContext, preferences: UserPreferences, importAdder: ImportAdder | undefined, addClassElement: (node: ClassElement) => void): void {
1111
const classMembers = classDeclaration.symbol.members!;
1212
for (const symbol of possiblyMissingSymbols) {
1313
if (!classMembers.has(symbol.escapedName)) {
14-
addNewNodeForMemberSymbol(symbol, classDeclaration, context, preferences, importAdder, addClassElement);
14+
addNewNodeForMemberSymbol(symbol, classDeclaration, sourceFile, context, preferences, importAdder, addClassElement);
1515
}
1616
}
1717
}
@@ -31,7 +31,7 @@ namespace ts.codefix {
3131
/**
3232
* @returns Empty string iff there we can't figure out a representation for `symbol` in `enclosingDeclaration`.
3333
*/
34-
function addNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, context: TypeConstructionContext, preferences: UserPreferences, importAdder: ImportAdder | undefined, addClassElement: (node: Node) => void): void {
34+
function addNewNodeForMemberSymbol(symbol: Symbol, enclosingDeclaration: ClassLikeDeclaration, sourceFile: SourceFile, context: TypeConstructionContext, preferences: UserPreferences, importAdder: ImportAdder | undefined, addClassElement: (node: Node) => void): void {
3535
const declarations = symbol.getDeclarations();
3636
if (!(declarations && declarations.length)) {
3737
return undefined;
@@ -45,11 +45,12 @@ namespace ts.codefix {
4545
const type = checker.getWidenedType(checker.getTypeOfSymbolAtLocation(symbol, enclosingDeclaration));
4646
const optional = !!(symbol.flags & SymbolFlags.Optional);
4747
const ambient = !!(enclosingDeclaration.flags & NodeFlags.Ambient);
48+
const quotePreference = getQuotePreference(sourceFile, preferences);
4849

4950
switch (declaration.kind) {
5051
case SyntaxKind.PropertySignature:
5152
case SyntaxKind.PropertyDeclaration:
52-
const flags = preferences.quotePreference === "single" ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : undefined;
53+
const flags = quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : undefined;
5354
let typeNode = checker.typeToTypeNode(type, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context));
5455
if (importAdder) {
5556
const importableReference = tryGetAutoImportableReferenceFromImportTypeNode(typeNode, type, scriptTarget);
@@ -88,7 +89,7 @@ namespace ts.codefix {
8889
name,
8990
emptyArray,
9091
typeNode,
91-
ambient ? undefined : createStubbedMethodBody(preferences)));
92+
ambient ? undefined : createStubbedMethodBody(quotePreference)));
9293
}
9394
else {
9495
Debug.assertNode(accessor, isSetAccessorDeclaration, "The counterpart to a getter should be a setter");
@@ -99,7 +100,7 @@ namespace ts.codefix {
99100
modifiers,
100101
name,
101102
createDummyParameters(1, [parameterName], [typeNode], 1, /*inJs*/ false),
102-
ambient ? undefined : createStubbedMethodBody(preferences)));
103+
ambient ? undefined : createStubbedMethodBody(quotePreference)));
103104
}
104105
}
105106
break;
@@ -121,36 +122,37 @@ namespace ts.codefix {
121122
if (declarations.length === 1) {
122123
Debug.assert(signatures.length === 1, "One declaration implies one signature");
123124
const signature = signatures[0];
124-
outputMethod(signature, modifiers, name, ambient ? undefined : createStubbedMethodBody(preferences));
125+
outputMethod(quotePreference, signature, modifiers, name, ambient ? undefined : createStubbedMethodBody(quotePreference));
125126
break;
126127
}
127128

128129
for (const signature of signatures) {
129130
// Need to ensure nodes are fresh each time so they can have different positions.
130-
outputMethod(signature, getSynthesizedDeepClones(modifiers, /*includeTrivia*/ false), getSynthesizedDeepClone(name, /*includeTrivia*/ false));
131+
outputMethod(quotePreference, signature, getSynthesizedDeepClones(modifiers, /*includeTrivia*/ false), getSynthesizedDeepClone(name, /*includeTrivia*/ false));
131132
}
132133

133134
if (!ambient) {
134135
if (declarations.length > signatures.length) {
135136
const signature = checker.getSignatureFromDeclaration(declarations[declarations.length - 1] as SignatureDeclaration)!;
136-
outputMethod(signature, modifiers, name, createStubbedMethodBody(preferences));
137+
outputMethod(quotePreference, signature, modifiers, name, createStubbedMethodBody(quotePreference));
137138
}
138139
else {
139140
Debug.assert(declarations.length === signatures.length, "Declarations and signatures should match count");
140-
addClassElement(createMethodImplementingSignatures(signatures, name, optional, modifiers, preferences));
141+
addClassElement(createMethodImplementingSignatures(signatures, name, optional, modifiers, quotePreference));
141142
}
142143
}
143144
break;
144145
}
145146

146-
function outputMethod(signature: Signature, modifiers: NodeArray<Modifier> | undefined, name: PropertyName, body?: Block): void {
147-
const method = signatureToMethodDeclaration(context, signature, enclosingDeclaration, modifiers, name, optional, body, importAdder);
147+
function outputMethod(quotePreference: QuotePreference, signature: Signature, modifiers: NodeArray<Modifier> | undefined, name: PropertyName, body?: Block): void {
148+
const method = signatureToMethodDeclaration(context, quotePreference, signature, enclosingDeclaration, modifiers, name, optional, body, importAdder);
148149
if (method) addClassElement(method);
149150
}
150151
}
151152

152153
function signatureToMethodDeclaration(
153154
context: TypeConstructionContext,
155+
quotePreference: QuotePreference,
154156
signature: Signature,
155157
enclosingDeclaration: ClassLikeDeclaration,
156158
modifiers: NodeArray<Modifier> | undefined,
@@ -162,7 +164,8 @@ namespace ts.codefix {
162164
const program = context.program;
163165
const checker = program.getTypeChecker();
164166
const scriptTarget = getEmitScriptTarget(program.getCompilerOptions());
165-
const signatureDeclaration = <MethodDeclaration>checker.signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, NodeBuilderFlags.NoTruncation | NodeBuilderFlags.SuppressAnyReturnType, getNoopSymbolTrackerWithResolver(context));
167+
const flags = NodeBuilderFlags.NoTruncation | NodeBuilderFlags.SuppressAnyReturnType | (quotePreference === QuotePreference.Single ? NodeBuilderFlags.UseSingleQuotesForStringLiteralType : 0);
168+
const signatureDeclaration = <MethodDeclaration>checker.signatureToSignatureDeclaration(signature, SyntaxKind.MethodDeclaration, enclosingDeclaration, flags, getNoopSymbolTrackerWithResolver(context));
166169
if (!signatureDeclaration) {
167170
return undefined;
168171
}
@@ -266,6 +269,7 @@ namespace ts.codefix {
266269
isIdentifier(arg) ? arg.text : isPropertyAccessExpression(arg) && isIdentifier(arg.name) ? arg.name.text : undefined);
267270
const contextualType = checker.getContextualType(call);
268271
const returnType = (inJs || !contextualType) ? undefined : checker.typeToTypeNode(contextualType, contextNode, /*flags*/ undefined, tracker);
272+
const quotePreference = getQuotePreference(context.sourceFile, context.preferences);
269273
return factory.createMethodDeclaration(
270274
/*decorators*/ undefined,
271275
/*modifiers*/ modifierFlags ? factory.createNodeArray(factory.createModifiersFromModifierFlags(modifierFlags)) : undefined,
@@ -276,7 +280,7 @@ namespace ts.codefix {
276280
factory.createTypeParameterDeclaration(CharacterCodes.T + typeArguments!.length - 1 <= CharacterCodes.Z ? String.fromCharCode(CharacterCodes.T + i) : `T${i}`)),
277281
/*parameters*/ createDummyParameters(args.length, names, types, /*minArgumentCount*/ undefined, inJs),
278282
/*type*/ returnType,
279-
body ? createStubbedMethodBody(context.preferences) : undefined);
283+
body ? createStubbedMethodBody(quotePreference) : undefined);
280284
}
281285

282286
export function typeToAutoImportableTypeNode(checker: TypeChecker, importAdder: ImportAdder, type: Type, contextNode: Node, scriptTarget: ScriptTarget, flags?: NodeBuilderFlags, tracker?: SymbolTracker): TypeNode | undefined {
@@ -312,7 +316,7 @@ namespace ts.codefix {
312316
name: PropertyName,
313317
optional: boolean,
314318
modifiers: readonly Modifier[] | undefined,
315-
preferences: UserPreferences,
319+
quotePreference: QuotePreference,
316320
): MethodDeclaration {
317321
/** This is *a* signature with the maximal number of arguments,
318322
* such that if there is a "maximal" signature without rest arguments,
@@ -355,7 +359,7 @@ namespace ts.codefix {
355359
/*typeParameters*/ undefined,
356360
parameters,
357361
/*returnType*/ undefined,
358-
preferences);
362+
quotePreference);
359363
}
360364

361365
function createStubbedMethod(
@@ -365,7 +369,7 @@ namespace ts.codefix {
365369
typeParameters: readonly TypeParameterDeclaration[] | undefined,
366370
parameters: readonly ParameterDeclaration[],
367371
returnType: TypeNode | undefined,
368-
preferences: UserPreferences
372+
quotePreference: QuotePreference
369373
): MethodDeclaration {
370374
return factory.createMethodDeclaration(
371375
/*decorators*/ undefined,
@@ -376,17 +380,17 @@ namespace ts.codefix {
376380
typeParameters,
377381
parameters,
378382
returnType,
379-
createStubbedMethodBody(preferences));
383+
createStubbedMethodBody(quotePreference));
380384
}
381385

382-
function createStubbedMethodBody(preferences: UserPreferences): Block {
386+
function createStubbedMethodBody(quotePreference: QuotePreference): Block {
383387
return factory.createBlock(
384388
[factory.createThrowStatement(
385389
factory.createNewExpression(
386390
factory.createIdentifier("Error"),
387391
/*typeArguments*/ undefined,
388392
// TODO Handle auto quote preference.
389-
[factory.createStringLiteral("Method not implemented.", /*isSingleQuote*/ preferences.quotePreference === "single")]))],
393+
[factory.createStringLiteral("Method not implemented.", /*isSingleQuote*/ quotePreference === QuotePreference.Single)]))],
390394
/*multiline*/ true);
391395
}
392396

tests/cases/fourslash/codeFixClassImplementInterface1_optionQuote.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.

tests/cases/fourslash/codeFixClassImplementInterfaceAutoImports.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { B, C, D } from './types2';
3434
export class C implements Base {
3535
a: A;
3636
b<T extends B = B>(p1: C): D<C> {
37-
throw new Error("Method not implemented.");
37+
throw new Error('Method not implemented.');
3838
}
3939
}`,
4040
});

tests/cases/fourslash/codeFixClassImplementInterfaceAutoImports_typeOnly.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import type { B, C, D } from './types2';
3636
export class C implements Base {
3737
a: A;
3838
b<T extends B = B>(p1: C): D<C> {
39-
throw new Error("Method not implemented.");
39+
throw new Error('Method not implemented.');
4040
}
4141
}`,
4242
});

tests/cases/fourslash/codeFixClassImplementInterface_optionQuote.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @filename: a.ts
4+
////export interface I {
5+
//// a(): void;
6+
//// b(x: "x", y: "a" | "b"): "b";
7+
////
8+
//// c: "c";
9+
//// d: { e: "e"; };
10+
////}
11+
// @filename: b.ts
12+
////import { I } from "./a";
13+
////class Foo implements I {}
14+
15+
goTo.file("b.ts")
16+
verify.codeFix({
17+
description: [ts.Diagnostics.Implement_interface_0.message, "I"],
18+
index: 0,
19+
newFileContent:
20+
`import { I } from "./a";
21+
class Foo implements I {
22+
a(): void {
23+
throw new Error("Method not implemented.");
24+
}
25+
b(x: "x", y: "a" | "b"): "b" {
26+
throw new Error("Method not implemented.");
27+
}
28+
c: "c";
29+
d: { e: "e"; };
30+
}`,
31+
preferences: { quotePreference: "auto" }
32+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
// @filename: a.ts
4+
////export interface I {
5+
//// a(): void;
6+
//// b(x: 'x', y: 'a' | 'b'): 'b';
7+
////
8+
//// c: 'c';
9+
//// d: { e: 'e'; };
10+
////}
11+
// @filename: b.ts
12+
////import { I } from './a';
13+
////class Foo implements I {}
14+
15+
goTo.file("b.ts")
16+
verify.codeFix({
17+
description: [ts.Diagnostics.Implement_interface_0.message, "I"],
18+
index: 0,
19+
newFileContent:
20+
`import { I } from './a';
21+
class Foo implements I {
22+
a(): void {
23+
throw new Error('Method not implemented.');
24+
}
25+
b(x: 'x', y: 'a' | 'b'): 'b' {
26+
throw new Error('Method not implemented.');
27+
}
28+
c: 'c';
29+
d: { e: 'e'; };
30+
}`,
31+
preferences: { quotePreference: "auto" }
32+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////interface I {
4+
//// a(): void;
5+
//// b(x: "x", y: "a" | "b"): "b";
6+
////
7+
//// c: "c";
8+
//// d: { e: "e"; };
9+
////}
10+
////class Foo implements I {}
11+
12+
verify.codeFix({
13+
description: [ts.Diagnostics.Implement_interface_0.message, "I"],
14+
newFileContent:
15+
`interface I {
16+
a(): void;
17+
b(x: "x", y: "a" | "b"): "b";
18+
19+
c: "c";
20+
d: { e: "e"; };
21+
}
22+
class Foo implements I {
23+
a(): void {
24+
throw new Error("Method not implemented.");
25+
}
26+
b(x: "x", y: "a" | "b"): "b" {
27+
throw new Error("Method not implemented.");
28+
}
29+
c: "c";
30+
d: { e: "e"; };
31+
}`,
32+
preferences: { quotePreference: "double" }
33+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/// <reference path='fourslash.ts' />
2+
3+
////interface I {
4+
//// a(): void;
5+
//// b(x: 'x', y: 'a' | 'b'): 'b';
6+
////
7+
//// c: 'c';
8+
//// d: { e: 'e'; };
9+
////}
10+
////class Foo implements I {}
11+
12+
verify.codeFix({
13+
description: [ts.Diagnostics.Implement_interface_0.message, "I"],
14+
newFileContent:
15+
`interface I {
16+
a(): void;
17+
b(x: 'x', y: 'a' | 'b'): 'b';
18+
19+
c: 'c';
20+
d: { e: 'e'; };
21+
}
22+
class Foo implements I {
23+
a(): void {
24+
throw new Error('Method not implemented.');
25+
}
26+
b(x: 'x', y: 'a' | 'b'): 'b' {
27+
throw new Error('Method not implemented.');
28+
}
29+
c: 'c';
30+
d: { e: 'e'; };
31+
}`,
32+
preferences: { quotePreference: "single" }
33+
});

tests/cases/fourslash/fourslash.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ declare namespace FourSlashInterface {
592592
filesToSearch?: ReadonlyArray<string>;
593593
}
594594
interface UserPreferences {
595-
readonly quotePreference?: "double" | "single";
595+
readonly quotePreference?: "auto" | "double" | "single"
596596
readonly includeCompletionsForModuleExports?: boolean;
597597
readonly includeInsertTextCompletions?: boolean;
598598
readonly includeAutomaticOptionalChainCompletions?: boolean;

0 commit comments

Comments
 (0)