diff --git a/src/rules/quotemarkRule.ts b/src/rules/quotemarkRule.ts index e85447a2315..39c3124405b 100644 --- a/src/rules/quotemarkRule.ts +++ b/src/rules/quotemarkRule.ts @@ -14,6 +14,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { lt } from "semver"; import { isExportDeclaration, isImportDeclaration, @@ -24,6 +26,7 @@ import { import * as ts from "typescript"; import * as Lint from "../index"; +import { getNormalizedTypescriptVersion } from "../verify/parse"; const OPTION_SINGLE = "single"; const OPTION_DOUBLE = "double"; @@ -134,13 +137,14 @@ function walk(ctx: Lint.WalkContext) { (isExportDeclaration(node.parent) || // This captures `import blah from "package"` isImportDeclaration(node.parent) || - // This captures kebab-case property names in object literals (only when the node is not at the end of the parent node) - (node.parent.kind === ts.SyntaxKind.PropertyAssignment && - node.end !== node.parent.end) || - // This captures the kebab-case property names in type definitions - node.parent.kind === ts.SyntaxKind.PropertySignature || + // This captures quoted names in object literal keys + isNameInAssignment(node) || + // This captures quoted signatures (property or method) + isSignature(node) || // This captures literal types in generic type constraints - node.parent.parent.kind === ts.SyntaxKind.TypeReference) + isTypeConstraint(node) || + // Whether this is the type in a typeof check with older tsc + isTypeCheckWithOldTsc(node)) ) { return; } @@ -245,3 +249,95 @@ function getJSXQuotemarkPreference( // If the regular pref is backtick, use double quotes instead. return regularQuotemarkPreference !== "`" ? regularQuotemarkPreference : '"'; } + +/** + * Whether this node is a type constraint in a generic type. + * @param node The node to check + * @return Whether this node is a type constraint + */ +function isTypeConstraint(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) { + let parent = node.parent.parent; + + // If this node doesn't have a grandparent, it's not a type constraint + if (parent == undefined) { + return false; + } + + // Iterate through all levels of union, intersection, or parethesized types + while ( + parent.kind === ts.SyntaxKind.UnionType || + parent.kind === ts.SyntaxKind.IntersectionType || + parent.kind === ts.SyntaxKind.ParenthesizedType + ) { + parent = parent.parent; + } + + return ( + // If the next level is a type reference, the node is a type constraint + parent.kind === ts.SyntaxKind.TypeReference || + // If the next level is a type parameter, the node is a type constraint + parent.kind === ts.SyntaxKind.TypeParameter + ); +} + +/** + * Whether this node is the signature of a property or method in a type. + * @param node The node to check + * @return Whether this node is a property/method signature. + */ +function isSignature(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) { + let parent = node.parent; + + if (hasOldTscBacktickBehavior() && node.parent.kind === ts.SyntaxKind.LastTypeNode) { + // In older versions, there's a "LastTypeNode" here + parent = parent.parent; + } + + return ( + // This captures the kebab-case property names in type definitions + parent.kind === ts.SyntaxKind.PropertySignature || + // This captures the kebab-case method names in type definitions + parent.kind === ts.SyntaxKind.MethodSignature + ); +} + +/** + * Whether this node is the method or property name in an assignment/declaration. + * @param node The node to check + * @return Whether this node is the name in an assignment/decleration. + */ +function isNameInAssignment(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) { + if ( + node.parent.kind !== ts.SyntaxKind.PropertyAssignment && + node.parent.kind !== ts.SyntaxKind.MethodDeclaration + ) { + // If the node is neither a property assignment or method declaration, it's not a name in an assignment + return false; + } + + return ( + // In old typescript versions, don't change values either + hasOldTscBacktickBehavior() || + // If this node is not at the end of the parent + node.end !== node.parent.end + ); +} + +function isTypeCheckWithOldTsc(node: ts.StringLiteral | ts.NoSubstitutionTemplateLiteral) { + if (!hasOldTscBacktickBehavior()) { + // This one only affects older typescript versions + return false; + } + + if (node.parent.kind !== ts.SyntaxKind.BinaryExpression) { + // If this isn't in a binary expression + return false; + } + + // If this node has a sibling that is a TypeOf + return node.parent.getChildren().some(n => n.kind === ts.SyntaxKind.TypeOfExpression); +} + +function hasOldTscBacktickBehavior() { + return lt(getNormalizedTypescriptVersion(), "2.7.1"); +} diff --git a/test/rules/quotemark/backtick/test.ts.fix b/test/rules/quotemark/backtick/test.ts.fix index 856d96dd3c1..c2c13346619 100644 --- a/test/rules/quotemark/backtick/test.ts.fix +++ b/test/rules/quotemark/backtick/test.ts.fix @@ -6,21 +6,27 @@ var single = `single`; var singleWithinDouble = `'singleWithinDouble'`; var doubleWithinSingle = `"doubleWithinSingle"`; var tabNewlineWithinSingle = `tab\tNewline\nWithinSingle`; + +var array: Array<"literal string"> = []; +var arrayTwo: Array<"literal string" | number> = []; +var arrayThree: Array<"literal string" | "hello world"> = []; +var arrayFour: Array<"literal string" | "hello world" | "foo bar"> = []; var array: Array<"literal string"> = []; +var arrayTwo: Array<"literal string" & number> = []; +var arrayFour: Array<"literal string" | "hello world" & "foo bar"> = []; + +function test() { + +} + +function test() { + +} + +const callback = () => `hi` as number | string + var hello: `world`; `escaped'quotemark`; // "avoid-template" option is not set. `foo`; - -const object: { - "hello-kebab" - : number - "kebab-case": number - "another-kebab": `hello-value` -} = { - "hello-kebab" - : 4 - "kebab-case": 3, - "another-kebab": `hello-value` -}; diff --git a/test/rules/quotemark/backtick/test.ts.lint b/test/rules/quotemark/backtick/test.ts.lint index 19cc40ffe1a..e556b68b42d 100644 --- a/test/rules/quotemark/backtick/test.ts.lint +++ b/test/rules/quotemark/backtick/test.ts.lint @@ -2,34 +2,41 @@ import { Something } from "some-package" export { SomethingElse } from "another-package" var single = 'single'; - ~~~~~~~~ [' should be `] + ~~~~~~~~ [single] var double = "married"; - ~~~~~~~~~ [" should be `] + ~~~~~~~~~ [double] var singleWithinDouble = "'singleWithinDouble'"; - ~~~~~~~~~~~~~~~~~~~~~~ [" should be `] + ~~~~~~~~~~~~~~~~~~~~~~ [double] var doubleWithinSingle = '"doubleWithinSingle"'; - ~~~~~~~~~~~~~~~~~~~~~~ [' should be `] + ~~~~~~~~~~~~~~~~~~~~~~ [single] var tabNewlineWithinSingle = 'tab\tNewline\nWithinSingle'; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be `] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [single] + +var array: Array<"literal string"> = []; +var arrayTwo: Array<"literal string" | number> = []; +var arrayThree: Array<"literal string" | "hello world"> = []; +var arrayFour: Array<"literal string" | "hello world" | "foo bar"> = []; var array: Array<"literal string"> = []; +var arrayTwo: Array<"literal string" & number> = []; +var arrayFour: Array<"literal string" | "hello world" & "foo bar"> = []; + +function test() { + +} + +function test() { + +} + +const callback = () => "hi" as number | string + ~~~~ [double] + var hello: "world"; - ~~~~~~~ [" should be `] + ~~~~~~~ [double] 'escaped\'quotemark'; -~~~~~~~~~~~~~~~~~~~~ [' should be `] +~~~~~~~~~~~~~~~~~~~~ [single] // "avoid-template" option is not set. `foo`; - -const object: { - "hello-kebab" - : number - "kebab-case": number - "another-kebab": "hello-value" - ~~~~~~~~~~~~~ [" should be `] -} = { - "hello-kebab" - : 4 - "kebab-case": 3, - "another-kebab": "hello-value" - ~~~~~~~~~~~~~ [" should be `] -}; +[single]: ' should be ` +[double]: " should be ` diff --git a/test/rules/quotemark/backtick/test<2.7.1.ts.fix b/test/rules/quotemark/backtick/test<2.7.1.ts.fix new file mode 100644 index 00000000000..30b5e84aaa9 --- /dev/null +++ b/test/rules/quotemark/backtick/test<2.7.1.ts.fix @@ -0,0 +1,11 @@ +if (typeof v === "string") {} + +if (typeof `string` === 'number') {} + +const object: { + "optional-prop"?: "hello-optional" + "another-kebab": "hello-value" +} = { + "optional-prop": undefined, + "another-kebab": "hello-value" +}; diff --git a/test/rules/quotemark/backtick/test<2.7.1.ts.lint b/test/rules/quotemark/backtick/test<2.7.1.ts.lint new file mode 100644 index 00000000000..c4ee7762f0c --- /dev/null +++ b/test/rules/quotemark/backtick/test<2.7.1.ts.lint @@ -0,0 +1,15 @@ +[typescript]: <2.7.1 +if (typeof v === "string") {} + +if (typeof "string" === 'number') {} + ~~~~~~~~ [double] + +const object: { + "optional-prop"?: "hello-optional" + "another-kebab": "hello-value" +} = { + "optional-prop": undefined, + "another-kebab": "hello-value" +}; +[single]: ' should be ` +[double]: " should be ` diff --git a/test/rules/quotemark/backtick/test>=2.7.1.ts.fix b/test/rules/quotemark/backtick/test>=2.7.1.ts.fix new file mode 100644 index 00000000000..f3e005b4d9f --- /dev/null +++ b/test/rules/quotemark/backtick/test>=2.7.1.ts.fix @@ -0,0 +1,13 @@ +if (typeof v === `string`) {} + +if (typeof `string` === `number`) {} + +const object: { + "optional-prop"?: `hello-optional` + "optional-function"?(): void + "another-kebab": `hello-value` +} = { + "optional-prop": undefined, + "optional-function"() {}, + "another-kebab": `hello-value` +}; diff --git a/test/rules/quotemark/backtick/test>=2.7.1.ts.lint b/test/rules/quotemark/backtick/test>=2.7.1.ts.lint new file mode 100644 index 00000000000..1f33caa5d7b --- /dev/null +++ b/test/rules/quotemark/backtick/test>=2.7.1.ts.lint @@ -0,0 +1,21 @@ +[typescript]: >=2.7.1 +if (typeof v === "string") {} + ~~~~~~~~ [double] + +if (typeof "string" === 'number') {} + ~~~~~~~~ [double] + ~~~~~~~~ [single] + +const object: { + "optional-prop"?: `hello-optional` + "optional-function"?(): void + "another-kebab": "hello-value" + ~~~~~~~~~~~~~ [double] +} = { + "optional-prop": undefined, + "optional-function"() {}, + "another-kebab": "hello-value" + ~~~~~~~~~~~~~ [double] +}; +[single]: ' should be ` +[double]: " should be `