Skip to content
This repository was archived by the owner on Mar 25, 2021. It is now read-only.

Commit c98a859

Browse files
ericbfJosh Goldberg
authored and
Josh Goldberg
committed
[quotemark] Exclude some cases from backtick rule (#4693)
* [quotemark] Exclude some cases from backtick rule This commit makes quotemark backtick ignore use strict declarations, enum members, lookup types, and strings containing octal escape sequences. * [quotemark] Fix comment on use strict check call I had copy pasted and forgotten to change it. This changes that comment. * Revert unrelated change, fix octal escape sequence check This commit makes it so that if a string has a literal backslash instead of an actual octal escape sequence, it is not flagged.
1 parent d79cd18 commit c98a859

File tree

3 files changed

+169
-19
lines changed

3 files changed

+169
-19
lines changed

src/rules/quotemarkRule.ts

+64-19
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717

1818
import { lt } from "semver";
1919
import {
20+
isEnumMember,
2021
isExportDeclaration,
22+
isExpressionStatement,
2123
isImportDeclaration,
24+
isIndexedAccessTypeNode,
25+
isLiteralTypeNode,
2226
isNoSubstitutionTemplateLiteral,
2327
isSameLine,
2428
isStringLiteral,
@@ -38,6 +42,7 @@ const OPTION_AVOID_ESCAPE = "avoid-escape";
3842

3943
type QUOTEMARK = "'" | '"' | "`";
4044
type JSX_QUOTEMARK = "'" | '"';
45+
type StringLiteralLike = ts.StringLiteral | ts.NoSubstitutionTemplateLiteral;
4146

4247
interface Options {
4348
quotemark: QUOTEMARK;
@@ -131,21 +136,7 @@ function walk(ctx: Lint.WalkContext<Options>) {
131136
const actualQuotemark = sourceFile.text[node.end - 1];
132137

133138
// Don't use backticks instead of single/double quotes when it breaks TypeScript syntax.
134-
if (
135-
expectedQuotemark === "`" &&
136-
// This captures `export blah from "package"`
137-
(isExportDeclaration(node.parent) ||
138-
// This captures `import blah from "package"`
139-
isImportDeclaration(node.parent) ||
140-
// This captures quoted names in object literal keys
141-
isNameInAssignment(node) ||
142-
// This captures quoted signatures (property or method)
143-
isSignature(node) ||
144-
// This captures literal types in generic type constraints
145-
isTypeConstraint(node) ||
146-
// Whether this is the type in a typeof check with older tsc
147-
isTypeCheckWithOldTsc(node))
148-
) {
139+
if (expectedQuotemark === "`" && isNotValidToUseBackticksInNode(node, sourceFile)) {
149140
return;
150141
}
151142

@@ -250,12 +241,37 @@ function getJSXQuotemarkPreference(
250241
return regularQuotemarkPreference !== "`" ? regularQuotemarkPreference : '"';
251242
}
252243

244+
function isNotValidToUseBackticksInNode(node: StringLiteralLike, sourceFile: ts.SourceFile) {
245+
return (
246+
// This captures `export blah from "package"`
247+
isExportDeclaration(node.parent) ||
248+
// This captures `import blah from "package"`
249+
isImportDeclaration(node.parent) ||
250+
// This captures quoted names in object literal keys
251+
isNameInAssignment(node) ||
252+
// This captures quoted signatures (property or method)
253+
isSignature(node) ||
254+
// This captures literal types in generic type constraints
255+
isTypeConstraint(node) ||
256+
// Older TS doesn't narrow a type when backticks are used to compare typeof
257+
isTypeCheckWithOldTsc(node) ||
258+
// Enum members can't use backticks
259+
isEnumMember(node.parent) ||
260+
// Typescript converts old octal escape sequences to just the numbers therein
261+
containsOctalEscapeSequence(node, sourceFile) ||
262+
// Use strict declarations have to be single or double quoted
263+
isUseStrictDeclaration(node) ||
264+
// Lookup type parameters must be single/double quoted
265+
isLookupTypeParameter(node)
266+
);
267+
}
268+
253269
/**
254270
* Whether this node is a type constraint in a generic type.
255271
* @param node The node to check
256272
* @return Whether this node is a type constraint
257273
*/
258-
function isTypeConstraint(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) {
274+
function isTypeConstraint(node: StringLiteralLike) {
259275
let parent = node.parent.parent;
260276

261277
// If this node doesn't have a grandparent, it's not a type constraint
@@ -285,7 +301,7 @@ function isTypeConstraint(node: ts.StringLiteral | ts.NoSubstitutionTemplateLite
285301
* @param node The node to check
286302
* @return Whether this node is a property/method signature.
287303
*/
288-
function isSignature(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) {
304+
function isSignature(node: StringLiteralLike) {
289305
let parent = node.parent;
290306

291307
if (hasOldTscBacktickBehavior() && node.parent.kind === ts.SyntaxKind.LastTypeNode) {
@@ -306,7 +322,7 @@ function isSignature(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral)
306322
* @param node The node to check
307323
* @return Whether this node is the name in an assignment/decleration.
308324
*/
309-
function isNameInAssignment(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) {
325+
function isNameInAssignment(node: StringLiteralLike) {
310326
if (
311327
node.parent.kind !== ts.SyntaxKind.PropertyAssignment &&
312328
node.parent.kind !== ts.SyntaxKind.MethodDeclaration
@@ -323,7 +339,7 @@ function isNameInAssignment(node: ts.StringLiteral | ts.NoSubstitutionTemplateLi
323339
);
324340
}
325341

326-
function isTypeCheckWithOldTsc(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) {
342+
function isTypeCheckWithOldTsc(node: StringLiteralLike) {
327343
if (!hasOldTscBacktickBehavior()) {
328344
// This one only affects older typescript versions
329345
return false;
@@ -338,6 +354,35 @@ function isTypeCheckWithOldTsc(node: ts.StringLiteral | ts.NoSubstitutionTemplat
338354
return node.parent.getChildren().some(n => n.kind === ts.SyntaxKind.TypeOfExpression);
339355
}
340356

357+
function containsOctalEscapeSequence(node: StringLiteralLike, sourceFile: ts.SourceFile) {
358+
// Octal sequences can go from 1-377 (255 in octal), but let's match the prefix, which will at least be \1-\77
359+
// Using node.getText here strips the backslashes from the string. We also need to make sure there isn't an even
360+
// number of backslashes (then it would not be an escape sequence, but a literal backslash followed by numbers).
361+
const matches = node.getText(sourceFile).match(/(\\)+[1-7][0-7]?/g);
362+
363+
if (matches != undefined) {
364+
for (const match of matches) {
365+
const numBackslashes = match.match(/\\/g)!.length;
366+
367+
if (numBackslashes % 2 === 1) {
368+
// There was an odd number of backslashes preceeding this node – it's an octal escape sequence
369+
return true;
370+
}
371+
}
372+
}
373+
374+
return false;
375+
}
376+
377+
function isUseStrictDeclaration(node: StringLiteralLike) {
378+
return node.text === "use strict" && isExpressionStatement(node.parent);
379+
}
380+
381+
function isLookupTypeParameter(node: StringLiteralLike) {
382+
return isLiteralTypeNode(node.parent) && isIndexedAccessTypeNode(node.parent.parent);
383+
}
384+
385+
/** Versions of typescript below 2.7.1 treat backticks differently */
341386
function hasOldTscBacktickBehavior() {
342387
return lt(getNormalizedTypescriptVersion(), "2.7.1");
343388
}

test/rules/quotemark/backtick/test.ts.fix

+48
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,34 @@
1+
"use strict"
2+
13
import { Something } from "some-package"
24
export { SomethingElse } from "another-package"
35

6+
enum Sides {
7+
'<- Left',
8+
'Right ->'
9+
}
10+
11+
const octalEscapeSequence = '\1'
12+
const octalEscapeSequence2 = '\7'
13+
const octalEscapeSequence3 = '\77'
14+
const octalEscapeSequence4 = '\377'
15+
const octalEscapeSequence5 = '\123'
16+
// Prefix (\47) is an octal escape sequence
17+
const octalEscapeSequence6 = '\477'
18+
const octalEscapeSequence7 = '\\77 \77 \\37 \\77'
19+
20+
const notOctalEscapeSequence = `\877`
21+
const notOctalEscapeSequence2 = `\017`
22+
const notOctalEscapeSequence3 = `\t`
23+
const notOctalEscapeSequence4 = `\0`
24+
const notOctalEscapeSequence4 = `\\77 \\37 \\\\47 \\\\\\77`
25+
26+
function strictFunction() {
27+
"use strict"
28+
29+
const str = `use strict`
30+
}
31+
432
var single = `single`;
533
var double = `married`;
634
var singleWithinDouble = `'singleWithinDouble'`;
@@ -23,6 +51,26 @@ function test<T extends ("generic" & number)>() {
2351

2452
}
2553

54+
declare var obj: {
55+
helloWorldString: string
56+
}
57+
58+
interface obj2 {
59+
helloWorldString: string
60+
}
61+
62+
type objHello = typeof obj["helloWorldString"]
63+
type objHello2 = obj2["helloWorldString"]
64+
let helloValue = obj[`helloWorldString`]
65+
66+
helloValue = obj[`helloWorldString`]
67+
68+
const obj3: {
69+
value: typeof obj["helloWorldString"]
70+
} = {
71+
value: obj[`helloWorldString`]
72+
}
73+
2674
const callback = <U extends "generic">() => `hi` as number | string
2775

2876
var hello: `world`;

test/rules/quotemark/backtick/test.ts.lint

+57
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,40 @@
1+
"use strict"
2+
13
import { Something } from "some-package"
24
export { SomethingElse } from "another-package"
35

6+
enum Sides {
7+
'<- Left',
8+
'Right ->'
9+
}
10+
11+
const octalEscapeSequence = '\1'
12+
const octalEscapeSequence2 = '\7'
13+
const octalEscapeSequence3 = '\77'
14+
const octalEscapeSequence4 = '\377'
15+
const octalEscapeSequence5 = '\123'
16+
// Prefix (\47) is an octal escape sequence
17+
const octalEscapeSequence6 = '\477'
18+
const octalEscapeSequence7 = '\\77 \77 \\37 \\77'
19+
20+
const notOctalEscapeSequence = '\877'
21+
~~~~~~ [single]
22+
const notOctalEscapeSequence2 = '\017'
23+
~~~~~~ [single]
24+
const notOctalEscapeSequence3 = '\t'
25+
~~~~ [single]
26+
const notOctalEscapeSequence4 = '\0'
27+
~~~~ [single]
28+
const notOctalEscapeSequence4 = '\\77 \\37 \\\\47 \\\\\\77'
29+
~~~~~~~~~~~~~~~~~~~~~~~~~~~ [single]
30+
31+
function strictFunction() {
32+
"use strict"
33+
34+
const str = "use strict"
35+
~~~~~~~~~~~~ [double]
36+
}
37+
438
var single = 'single';
539
~~~~~~~~ [single]
640
var double = "married";
@@ -28,6 +62,29 @@ function test<T extends ("generic" & number)>() {
2862

2963
}
3064

65+
declare var obj: {
66+
helloWorldString: string
67+
}
68+
69+
interface obj2 {
70+
helloWorldString: string
71+
}
72+
73+
type objHello = typeof obj["helloWorldString"]
74+
type objHello2 = obj2["helloWorldString"]
75+
let helloValue = obj["helloWorldString"]
76+
~~~~~~~~~~~~~~~~~~ [double]
77+
78+
helloValue = obj["helloWorldString"]
79+
~~~~~~~~~~~~~~~~~~ [double]
80+
81+
const obj3: {
82+
value: typeof obj["helloWorldString"]
83+
} = {
84+
value: obj["helloWorldString"]
85+
~~~~~~~~~~~~~~~~~~ [double]
86+
}
87+
3188
const callback = <U extends "generic">() => "hi" as number | string
3289
~~~~ [double]
3390

0 commit comments

Comments
 (0)