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

Commit

Permalink
[quotemark] Excuse more backtick edge cases (#4642)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
ericbf authored and adidahiya committed Apr 16, 2019
1 parent cf65288 commit 6d6a901
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 39 deletions.
108 changes: 102 additions & 6 deletions src/rules/quotemarkRule.ts
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
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
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
@@ -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
@@ -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
@@ -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
@@ -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 `

0 comments on commit 6d6a901

Please sign in to comment.