Skip to content

Commit 95ee9ae

Browse files
authored
Merge pull request #17750 from Microsoft/importFixInferQuote
Add support to infer the quote style for import code fix
2 parents d352e3b + 09487b8 commit 95ee9ae

9 files changed

+163
-11
lines changed

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,7 @@ namespace ts {
999999
export interface StringLiteral extends LiteralExpression {
10001000
kind: SyntaxKind.StringLiteral;
10011001
/* @internal */ textSourceNode?: Identifier | StringLiteral | NumericLiteral; // Allows a StringLiteral to get its text from another node (used by transforms).
1002+
/* @internal */ singleQuote?: boolean;
10021003
}
10031004

10041005
// Note: 'brands' in our syntax nodes serve to give us a small amount of nominal typing.

src/compiler/utilities.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -344,15 +344,20 @@ namespace ts {
344344
// or a (possibly escaped) quoted form of the original text if it's string-like.
345345
switch (node.kind) {
346346
case SyntaxKind.StringLiteral:
347-
return '"' + escapeText(node.text) + '"';
347+
if ((<StringLiteral>node).singleQuote) {
348+
return "'" + escapeText(node.text, CharacterCodes.singleQuote) + "'";
349+
}
350+
else {
351+
return '"' + escapeText(node.text, CharacterCodes.doubleQuote) + '"';
352+
}
348353
case SyntaxKind.NoSubstitutionTemplateLiteral:
349-
return "`" + escapeText(node.text) + "`";
354+
return "`" + escapeText(node.text, CharacterCodes.backtick) + "`";
350355
case SyntaxKind.TemplateHead:
351-
return "`" + escapeText(node.text) + "${";
356+
return "`" + escapeText(node.text, CharacterCodes.backtick) + "${";
352357
case SyntaxKind.TemplateMiddle:
353-
return "}" + escapeText(node.text) + "${";
358+
return "}" + escapeText(node.text, CharacterCodes.backtick) + "${";
354359
case SyntaxKind.TemplateTail:
355-
return "}" + escapeText(node.text) + "`";
360+
return "}" + escapeText(node.text, CharacterCodes.backtick) + "`";
356361
case SyntaxKind.NumericLiteral:
357362
return node.text;
358363
}
@@ -2356,7 +2361,9 @@ namespace ts {
23562361
// the language service. These characters should be escaped when printing, and if any characters are added,
23572362
// the map below must be updated. Note that this regexp *does not* include the 'delete' character.
23582363
// There is no reason for this other than that JSON.stringify does not handle it either.
2359-
const escapedCharsRegExp = /[\\\"\u0000-\u001f\t\v\f\b\r\n\u2028\u2029\u0085]/g;
2364+
const doubleQuoteEscapedCharsRegExp = /[\\\"\u0000-\u001f\t\v\f\b\r\n\u2028\u2029\u0085]/g;
2365+
const singleQuoteEscapedCharsRegExp = /[\\\'\u0000-\u001f\t\v\f\b\r\n\u2028\u2029\u0085]/g;
2366+
const backtickQuoteEscapedCharsRegExp = /[\\\`\u0000-\u001f\t\v\f\b\r\n\u2028\u2029\u0085]/g;
23602367
const escapedCharsMap = createMapFromTemplate({
23612368
"\0": "\\0",
23622369
"\t": "\\t",
@@ -2367,18 +2374,23 @@ namespace ts {
23672374
"\n": "\\n",
23682375
"\\": "\\\\",
23692376
"\"": "\\\"",
2377+
"\'": "\\\'",
2378+
"\`": "\\\`",
23702379
"\u2028": "\\u2028", // lineSeparator
23712380
"\u2029": "\\u2029", // paragraphSeparator
23722381
"\u0085": "\\u0085" // nextLine
23732382
});
23742383

2375-
23762384
/**
23772385
* Based heavily on the abstract 'Quote'/'QuoteJSONString' operation from ECMA-262 (24.3.2.2),
23782386
* but augmented for a few select characters (e.g. lineSeparator, paragraphSeparator, nextLine)
23792387
* Note that this doesn't actually wrap the input in double quotes.
23802388
*/
2381-
export function escapeString(s: string): string {
2389+
export function escapeString(s: string, quoteChar?: CharacterCodes.doubleQuote | CharacterCodes.singleQuote | CharacterCodes.backtick): string {
2390+
const escapedCharsRegExp =
2391+
quoteChar === CharacterCodes.backtick ? backtickQuoteEscapedCharsRegExp :
2392+
quoteChar === CharacterCodes.singleQuote ? singleQuoteEscapedCharsRegExp :
2393+
doubleQuoteEscapedCharsRegExp;
23822394
return s.replace(escapedCharsRegExp, getReplacement);
23832395
}
23842396

@@ -2400,8 +2412,8 @@ namespace ts {
24002412
}
24012413

24022414
const nonAsciiCharacters = /[^\u0000-\u007F]/g;
2403-
export function escapeNonAsciiString(s: string): string {
2404-
s = escapeString(s);
2415+
export function escapeNonAsciiString(s: string, quoteChar?: CharacterCodes.doubleQuote | CharacterCodes.singleQuote | CharacterCodes.backtick): string {
2416+
s = escapeString(s, quoteChar);
24052417
// Replace non-ASCII characters with '\uNNNN' escapes if any exist.
24062418
// Otherwise just return the original string.
24072419
return nonAsciiCharacters.test(s) ?

src/services/codefixes/importFixes.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,9 @@ namespace ts.codefix {
394394
: isNamespaceImport
395395
? createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(symbolName)))
396396
: createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, createIdentifier(symbolName))]));
397-
const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, createLiteral(moduleSpecifierWithoutQuotes));
397+
const moduleSpecifierLiteral = createLiteral(moduleSpecifierWithoutQuotes);
398+
moduleSpecifierLiteral.singleQuote = getSingleQuoteStyleFromExistingImports();
399+
const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, moduleSpecifierLiteral);
398400
if (!lastImportDeclaration) {
399401
changeTracker.insertNodeAt(sourceFile, getSourceFileImportLocation(sourceFile), importDecl, { suffix: `${context.newLineCharacter}${context.newLineCharacter}` });
400402
}
@@ -435,6 +437,24 @@ namespace ts.codefix {
435437
return position;
436438
}
437439

440+
function getSingleQuoteStyleFromExistingImports() {
441+
const firstModuleSpecifier = forEach(sourceFile.statements, node => {
442+
if (isImportDeclaration(node) || isExportDeclaration(node)) {
443+
if (node.moduleSpecifier && isStringLiteral(node.moduleSpecifier)) {
444+
return node.moduleSpecifier;
445+
}
446+
}
447+
else if (isImportEqualsDeclaration(node)) {
448+
if (isExternalModuleReference(node.moduleReference) && isStringLiteral(node.moduleReference.expression)) {
449+
return node.moduleReference.expression;
450+
}
451+
}
452+
});
453+
if (firstModuleSpecifier) {
454+
return sourceFile.text.charCodeAt(firstModuleSpecifier.getStart()) === CharacterCodes.singleQuote;
455+
}
456+
}
457+
438458
function getModuleSpecifierForNewImport() {
439459
const fileName = sourceFile.fileName;
440460
const moduleFileName = moduleSymbol.valueDeclaration.getSourceFile().fileName;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//// [|import { v2 } from './module2';
4+
////
5+
//// f1/*0*/();|]
6+
7+
// @Filename: module1.ts
8+
//// export function f1() {}
9+
10+
// @Filename: module2.ts
11+
//// export var v2 = 6;
12+
13+
verify.importFixAtPosition([
14+
`import { v2 } from './module2';
15+
import { f1 } from './module1';
16+
17+
f1();`
18+
]);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//// [|import { v2 } from "./module2";
4+
////
5+
//// f1/*0*/();|]
6+
7+
// @Filename: module1.ts
8+
//// export function f1() {}
9+
10+
// @Filename: module2.ts
11+
//// export var v2 = 6;
12+
13+
verify.importFixAtPosition([
14+
`import { v2 } from "./module2";
15+
import { f1 } from "./module1";
16+
17+
f1();`
18+
]);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//// [|import m2 = require('./module2');
4+
////
5+
//// f1/*0*/();|]
6+
7+
// @Filename: module1.ts
8+
//// export function f1() {}
9+
10+
// @Filename: module2.ts
11+
//// export var v2 = 6;
12+
13+
verify.importFixAtPosition([
14+
`import m2 = require('./module2');
15+
import { f1 } from './module1';
16+
17+
f1();`
18+
]);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//// [|export { v2 } from './module2';
4+
////
5+
//// f1/*0*/();|]
6+
7+
// @Filename: module1.ts
8+
//// export function f1() {}
9+
10+
// @Filename: module2.ts
11+
//// export var v2 = 6;
12+
13+
verify.importFixAtPosition([
14+
`import { f1 } from './module1';
15+
16+
export { v2 } from './module2';
17+
18+
f1();`
19+
]);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//// [|import { v2 } from "./module2";
4+
//// import { v3 } from './module3';
5+
////
6+
//// f1/*0*/();|]
7+
8+
// @Filename: module1.ts
9+
//// export function f1() {}
10+
11+
// @Filename: module2.ts
12+
//// export var v2 = 6;
13+
14+
// @Filename: module3.ts
15+
//// export var v3 = 6;
16+
17+
verify.importFixAtPosition([
18+
`import { v2 } from "./module2";
19+
import { v3 } from './module3';
20+
import { f1 } from "./module1";
21+
22+
f1();`
23+
]);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
//// [|import { v2 } from './module2';
4+
//// import { v3 } from "./module3";
5+
////
6+
//// f1/*0*/();|]
7+
8+
// @Filename: module1.ts
9+
//// export function f1() {}
10+
11+
// @Filename: module2.ts
12+
//// export var v2 = 6;
13+
14+
// @Filename: module3.ts
15+
//// export var v3 = 6;
16+
17+
verify.importFixAtPosition([
18+
`import { v2 } from './module2';
19+
import { v3 } from "./module3";
20+
import { f1 } from './module1';
21+
22+
f1();`
23+
]);

0 commit comments

Comments
 (0)