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

Commit 6d6a901

Browse files
ericbfadidahiya
authored andcommitted
[quotemark] Excuse more backtick edge cases (#4642)
This edge cases were previously flagged when they should be ignored, as changing them breaks typescript. This commit makes it so they are ignored. It also organizes a little better, using functions instead of multi-layered conditionals (it was getting confusing).
1 parent cf65288 commit 6d6a901

File tree

7 files changed

+208
-39
lines changed

7 files changed

+208
-39
lines changed

src/rules/quotemarkRule.ts

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17+
18+
import { lt } from "semver";
1719
import {
1820
isExportDeclaration,
1921
isImportDeclaration,
@@ -24,6 +26,7 @@ import {
2426
import * as ts from "typescript";
2527

2628
import * as Lint from "../index";
29+
import { getNormalizedTypescriptVersion } from "../verify/parse";
2730

2831
const OPTION_SINGLE = "single";
2932
const OPTION_DOUBLE = "double";
@@ -134,13 +137,14 @@ function walk(ctx: Lint.WalkContext<Options>) {
134137
(isExportDeclaration(node.parent) ||
135138
// This captures `import blah from "package"`
136139
isImportDeclaration(node.parent) ||
137-
// This captures kebab-case property names in object literals (only when the node is not at the end of the parent node)
138-
(node.parent.kind === ts.SyntaxKind.PropertyAssignment &&
139-
node.end !== node.parent.end) ||
140-
// This captures the kebab-case property names in type definitions
141-
node.parent.kind === ts.SyntaxKind.PropertySignature ||
140+
// This captures quoted names in object literal keys
141+
isNameInAssignment(node) ||
142+
// This captures quoted signatures (property or method)
143+
isSignature(node) ||
142144
// This captures literal types in generic type constraints
143-
node.parent.parent.kind === ts.SyntaxKind.TypeReference)
145+
isTypeConstraint(node) ||
146+
// Whether this is the type in a typeof check with older tsc
147+
isTypeCheckWithOldTsc(node))
144148
) {
145149
return;
146150
}
@@ -245,3 +249,95 @@ function getJSXQuotemarkPreference(
245249
// If the regular pref is backtick, use double quotes instead.
246250
return regularQuotemarkPreference !== "`" ? regularQuotemarkPreference : '"';
247251
}
252+
253+
/**
254+
* Whether this node is a type constraint in a generic type.
255+
* @param node The node to check
256+
* @return Whether this node is a type constraint
257+
*/
258+
function isTypeConstraint(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) {
259+
let parent = node.parent.parent;
260+
261+
// If this node doesn't have a grandparent, it's not a type constraint
262+
if (parent == undefined) {
263+
return false;
264+
}
265+
266+
// Iterate through all levels of union, intersection, or parethesized types
267+
while (
268+
parent.kind === ts.SyntaxKind.UnionType ||
269+
parent.kind === ts.SyntaxKind.IntersectionType ||
270+
parent.kind === ts.SyntaxKind.ParenthesizedType
271+
) {
272+
parent = parent.parent;
273+
}
274+
275+
return (
276+
// If the next level is a type reference, the node is a type constraint
277+
parent.kind === ts.SyntaxKind.TypeReference ||
278+
// If the next level is a type parameter, the node is a type constraint
279+
parent.kind === ts.SyntaxKind.TypeParameter
280+
);
281+
}
282+
283+
/**
284+
* Whether this node is the signature of a property or method in a type.
285+
* @param node The node to check
286+
* @return Whether this node is a property/method signature.
287+
*/
288+
function isSignature(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) {
289+
let parent = node.parent;
290+
291+
if (hasOldTscBacktickBehavior() && node.parent.kind === ts.SyntaxKind.LastTypeNode) {
292+
// In older versions, there's a "LastTypeNode" here
293+
parent = parent.parent;
294+
}
295+
296+
return (
297+
// This captures the kebab-case property names in type definitions
298+
parent.kind === ts.SyntaxKind.PropertySignature ||
299+
// This captures the kebab-case method names in type definitions
300+
parent.kind === ts.SyntaxKind.MethodSignature
301+
);
302+
}
303+
304+
/**
305+
* Whether this node is the method or property name in an assignment/declaration.
306+
* @param node The node to check
307+
* @return Whether this node is the name in an assignment/decleration.
308+
*/
309+
function isNameInAssignment(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) {
310+
if (
311+
node.parent.kind !== ts.SyntaxKind.PropertyAssignment &&
312+
node.parent.kind !== ts.SyntaxKind.MethodDeclaration
313+
) {
314+
// If the node is neither a property assignment or method declaration, it's not a name in an assignment
315+
return false;
316+
}
317+
318+
return (
319+
// In old typescript versions, don't change values either
320+
hasOldTscBacktickBehavior() ||
321+
// If this node is not at the end of the parent
322+
node.end !== node.parent.end
323+
);
324+
}
325+
326+
function isTypeCheckWithOldTsc(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) {
327+
if (!hasOldTscBacktickBehavior()) {
328+
// This one only affects older typescript versions
329+
return false;
330+
}
331+
332+
if (node.parent.kind !== ts.SyntaxKind.BinaryExpression) {
333+
// If this isn't in a binary expression
334+
return false;
335+
}
336+
337+
// If this node has a sibling that is a TypeOf
338+
return node.parent.getChildren().some(n => n.kind === ts.SyntaxKind.TypeOfExpression);
339+
}
340+
341+
function hasOldTscBacktickBehavior() {
342+
return lt(getNormalizedTypescriptVersion(), "2.7.1");
343+
}

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

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,27 @@ var single = `single`;
66
var singleWithinDouble = `'singleWithinDouble'`;
77
var doubleWithinSingle = `"doubleWithinSingle"`;
88
var tabNewlineWithinSingle = `tab\tNewline\nWithinSingle`;
9+
10+
var array: Array<"literal string"> = [];
11+
var arrayTwo: Array<"literal string" | number> = [];
12+
var arrayThree: Array<"literal string" | "hello world"> = [];
13+
var arrayFour: Array<"literal string" | "hello world" | "foo bar"> = [];
914
var array: Array<"literal string"> = [];
15+
var arrayTwo: Array<"literal string" & number> = [];
16+
var arrayFour: Array<"literal string" | "hello world" & "foo bar"> = [];
17+
18+
function test<T extends "generic">() {
19+
20+
}
21+
22+
function test<T extends ("generic" & number)>() {
23+
24+
}
25+
26+
const callback = <U extends "generic">() => `hi` as number | string
27+
1028
var hello: `world`;
1129
`escaped'quotemark`;
1230

1331
// "avoid-template" option is not set.
1432
`foo`;
15-
16-
const object: {
17-
"hello-kebab"
18-
: number
19-
"kebab-case": number
20-
"another-kebab": `hello-value`
21-
} = {
22-
"hello-kebab"
23-
: 4
24-
"kebab-case": 3,
25-
"another-kebab": `hello-value`
26-
};

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

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,41 @@ import { Something } from "some-package"
22
export { SomethingElse } from "another-package"
33

44
var single = 'single';
5-
~~~~~~~~ [' should be `]
5+
~~~~~~~~ [single]
66
var double = "married";
7-
~~~~~~~~~ [" should be `]
7+
~~~~~~~~~ [double]
88
var singleWithinDouble = "'singleWithinDouble'";
9-
~~~~~~~~~~~~~~~~~~~~~~ [" should be `]
9+
~~~~~~~~~~~~~~~~~~~~~~ [double]
1010
var doubleWithinSingle = '"doubleWithinSingle"';
11-
~~~~~~~~~~~~~~~~~~~~~~ [' should be `]
11+
~~~~~~~~~~~~~~~~~~~~~~ [single]
1212
var tabNewlineWithinSingle = 'tab\tNewline\nWithinSingle';
13-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be `]
13+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [single]
14+
15+
var array: Array<"literal string"> = [];
16+
var arrayTwo: Array<"literal string" | number> = [];
17+
var arrayThree: Array<"literal string" | "hello world"> = [];
18+
var arrayFour: Array<"literal string" | "hello world" | "foo bar"> = [];
1419
var array: Array<"literal string"> = [];
20+
var arrayTwo: Array<"literal string" & number> = [];
21+
var arrayFour: Array<"literal string" | "hello world" & "foo bar"> = [];
22+
23+
function test<T extends "generic">() {
24+
25+
}
26+
27+
function test<T extends ("generic" & number)>() {
28+
29+
}
30+
31+
const callback = <U extends "generic">() => "hi" as number | string
32+
~~~~ [double]
33+
1534
var hello: "world";
16-
~~~~~~~ [" should be `]
35+
~~~~~~~ [double]
1736
'escaped\'quotemark';
18-
~~~~~~~~~~~~~~~~~~~~ [' should be `]
37+
~~~~~~~~~~~~~~~~~~~~ [single]
1938

2039
// "avoid-template" option is not set.
2140
`foo`;
22-
23-
const object: {
24-
"hello-kebab"
25-
: number
26-
"kebab-case": number
27-
"another-kebab": "hello-value"
28-
~~~~~~~~~~~~~ [" should be `]
29-
} = {
30-
"hello-kebab"
31-
: 4
32-
"kebab-case": 3,
33-
"another-kebab": "hello-value"
34-
~~~~~~~~~~~~~ [" should be `]
35-
};
41+
[single]: ' should be `
42+
[double]: " should be `
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
if (typeof v === "string") {}
2+
3+
if (typeof `string` === 'number') {}
4+
5+
const object: {
6+
"optional-prop"?: "hello-optional"
7+
"another-kebab": "hello-value"
8+
} = {
9+
"optional-prop": undefined,
10+
"another-kebab": "hello-value"
11+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[typescript]: <2.7.1
2+
if (typeof v === "string") {}
3+
4+
if (typeof "string" === 'number') {}
5+
~~~~~~~~ [double]
6+
7+
const object: {
8+
"optional-prop"?: "hello-optional"
9+
"another-kebab": "hello-value"
10+
} = {
11+
"optional-prop": undefined,
12+
"another-kebab": "hello-value"
13+
};
14+
[single]: ' should be `
15+
[double]: " should be `
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
if (typeof v === `string`) {}
2+
3+
if (typeof `string` === `number`) {}
4+
5+
const object: {
6+
"optional-prop"?: `hello-optional`
7+
"optional-function"?(): void
8+
"another-kebab": `hello-value`
9+
} = {
10+
"optional-prop": undefined,
11+
"optional-function"() {},
12+
"another-kebab": `hello-value`
13+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[typescript]: >=2.7.1
2+
if (typeof v === "string") {}
3+
~~~~~~~~ [double]
4+
5+
if (typeof "string" === 'number') {}
6+
~~~~~~~~ [double]
7+
~~~~~~~~ [single]
8+
9+
const object: {
10+
"optional-prop"?: `hello-optional`
11+
"optional-function"?(): void
12+
"another-kebab": "hello-value"
13+
~~~~~~~~~~~~~ [double]
14+
} = {
15+
"optional-prop": undefined,
16+
"optional-function"() {},
17+
"another-kebab": "hello-value"
18+
~~~~~~~~~~~~~ [double]
19+
};
20+
[single]: ' should be `
21+
[double]: " should be `

0 commit comments

Comments
 (0)