17
17
18
18
import { lt } from "semver" ;
19
19
import {
20
+ isEnumMember ,
20
21
isExportDeclaration ,
22
+ isExpressionStatement ,
21
23
isImportDeclaration ,
24
+ isIndexedAccessTypeNode ,
25
+ isLiteralTypeNode ,
22
26
isNoSubstitutionTemplateLiteral ,
23
27
isSameLine ,
24
28
isStringLiteral ,
@@ -38,6 +42,7 @@ const OPTION_AVOID_ESCAPE = "avoid-escape";
38
42
39
43
type QUOTEMARK = "'" | '"' | "`" ;
40
44
type JSX_QUOTEMARK = "'" | '"' ;
45
+ type StringLiteralLike = ts . StringLiteral | ts . NoSubstitutionTemplateLiteral ;
41
46
42
47
interface Options {
43
48
quotemark : QUOTEMARK ;
@@ -131,21 +136,7 @@ function walk(ctx: Lint.WalkContext<Options>) {
131
136
const actualQuotemark = sourceFile . text [ node . end - 1 ] ;
132
137
133
138
// 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 ) ) {
149
140
return ;
150
141
}
151
142
@@ -250,12 +241,37 @@ function getJSXQuotemarkPreference(
250
241
return regularQuotemarkPreference !== "`" ? regularQuotemarkPreference : '"' ;
251
242
}
252
243
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
+
253
269
/**
254
270
* Whether this node is a type constraint in a generic type.
255
271
* @param node The node to check
256
272
* @return Whether this node is a type constraint
257
273
*/
258
- function isTypeConstraint ( node : ts . StringLiteral | ts . NoSubstitutionTemplateLiteral ) {
274
+ function isTypeConstraint ( node : StringLiteralLike ) {
259
275
let parent = node . parent . parent ;
260
276
261
277
// 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
285
301
* @param node The node to check
286
302
* @return Whether this node is a property/method signature.
287
303
*/
288
- function isSignature ( node : ts . StringLiteral | ts . NoSubstitutionTemplateLiteral ) {
304
+ function isSignature ( node : StringLiteralLike ) {
289
305
let parent = node . parent ;
290
306
291
307
if ( hasOldTscBacktickBehavior ( ) && node . parent . kind === ts . SyntaxKind . LastTypeNode ) {
@@ -306,7 +322,7 @@ function isSignature(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral)
306
322
* @param node The node to check
307
323
* @return Whether this node is the name in an assignment/decleration.
308
324
*/
309
- function isNameInAssignment ( node : ts . StringLiteral | ts . NoSubstitutionTemplateLiteral ) {
325
+ function isNameInAssignment ( node : StringLiteralLike ) {
310
326
if (
311
327
node . parent . kind !== ts . SyntaxKind . PropertyAssignment &&
312
328
node . parent . kind !== ts . SyntaxKind . MethodDeclaration
@@ -323,7 +339,7 @@ function isNameInAssignment(node: ts.StringLiteral | ts.NoSubstitutionTemplateLi
323
339
) ;
324
340
}
325
341
326
- function isTypeCheckWithOldTsc ( node : ts . StringLiteral | ts . NoSubstitutionTemplateLiteral ) {
342
+ function isTypeCheckWithOldTsc ( node : StringLiteralLike ) {
327
343
if ( ! hasOldTscBacktickBehavior ( ) ) {
328
344
// This one only affects older typescript versions
329
345
return false ;
@@ -338,6 +354,35 @@ function isTypeCheckWithOldTsc(node: ts.StringLiteral | ts.NoSubstitutionTemplat
338
354
return node . parent . getChildren ( ) . some ( n => n . kind === ts . SyntaxKind . TypeOfExpression ) ;
339
355
}
340
356
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 */
341
386
function hasOldTscBacktickBehavior ( ) {
342
387
return lt ( getNormalizedTypescriptVersion ( ) , "2.7.1" ) ;
343
388
}
0 commit comments