1717
1818import { lt } from "semver" ;
1919import {
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
3943type QUOTEMARK = "'" | '"' | "`" ;
4044type JSX_QUOTEMARK = "'" | '"' ;
45+ type StringLiteralLike = ts . StringLiteral | ts . NoSubstitutionTemplateLiteral ;
4146
4247interface 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 */
341386function hasOldTscBacktickBehavior ( ) {
342387 return lt ( getNormalizedTypescriptVersion ( ) , "2.7.1" ) ;
343388}
0 commit comments