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

[quotemark] Excuse more backtick edge cases #4642

Merged
merged 5 commits into from
Apr 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 102 additions & 6 deletions src/rules/quotemarkRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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";
Expand Down Expand Up @@ -134,13 +137,14 @@ function walk(ctx: Lint.WalkContext<Options>) {
(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;
}
Expand Down Expand Up @@ -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");
}
30 changes: 18 additions & 12 deletions test/rules/quotemark/backtick/test.ts.fix
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends "generic">() {

}

function test<T extends ("generic" & number)>() {

}

const callback = <U extends "generic">() => `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`
};
49 changes: 28 additions & 21 deletions test/rules/quotemark/backtick/test.ts.lint
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends "generic">() {

}

function test<T extends ("generic" & number)>() {

}

const callback = <U extends "generic">() => "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 `
11 changes: 11 additions & 0 deletions test/rules/quotemark/backtick/test<2.7.1.ts.fix
Original file line number Diff line number Diff line change
@@ -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"
};
15 changes: 15 additions & 0 deletions test/rules/quotemark/backtick/test<2.7.1.ts.lint
Original file line number Diff line number Diff line change
@@ -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 `
13 changes: 13 additions & 0 deletions test/rules/quotemark/backtick/test>=2.7.1.ts.fix
Original file line number Diff line number Diff line change
@@ -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`
};
21 changes: 21 additions & 0 deletions test/rules/quotemark/backtick/test>=2.7.1.ts.lint
Original file line number Diff line number Diff line change
@@ -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 `