Skip to content

Commit

Permalink
Add "backtick" option for quotemark (palantir#4029)
Browse files Browse the repository at this point in the history
This lets users enforce only backtick "`" strings for everywhere that it's permissible. This also simplifies some logic in the preferred quote determination, combining some disparate logic into the nice self-packaged functions.

> See palantir#539
> The corresponding option in eslint is called "backtick": http://eslint.org/docs/rules/quotes
> See https://ponyfoo.com/articles/template-literals-strictly-better-strings
  • Loading branch information
ericbf authored and johnwiseheart committed Jul 19, 2018
1 parent 3cb1691 commit 559d880
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 17 deletions.
108 changes: 91 additions & 17 deletions src/rules/quotemarkRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ import * as Lint from "../index";

const OPTION_SINGLE = "single";
const OPTION_DOUBLE = "double";
const OPTION_BACKTICK = "backtick";
const OPTION_JSX_SINGLE = "jsx-single";
const OPTION_JSX_DOUBLE = "jsx-double";
const OPTION_AVOID_TEMPLATE = "avoid-template";
const OPTION_AVOID_ESCAPE = "avoid-escape";

type QUOTE_MARK = "'" | '"' | "`";
type JSX_QUOTE_MARK = "'" | '"';

interface Options {
quoteMark: '"' | "'";
jsxQuoteMark: '"' | "'";
quoteMark: QUOTE_MARK;
jsxQuoteMark: JSX_QUOTE_MARK;
avoidEscape: boolean;
avoidTemplate: boolean;
}
Expand All @@ -37,13 +41,14 @@ export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "quotemark",
description: "Requires single or double quotes for string literals.",
description: "Enforces quote character for string literals.",
hasFix: true,
optionsDescription: Lint.Utils.dedent`
Five arguments may be optionally provided:
* \`"${OPTION_SINGLE}"\` enforces single quotes.
* \`"${OPTION_DOUBLE}"\` enforces double quotes.
* \`"${OPTION_BACKTICK}"\` enforces backticks.
* \`"${OPTION_JSX_SINGLE}"\` enforces single quotes for JSX attributes.
* \`"${OPTION_JSX_DOUBLE}"\` enforces double quotes for JSX attributes.
* \`"${OPTION_AVOID_TEMPLATE}"\` forbids single-line untagged template strings that do not contain string interpolations.
Expand All @@ -54,7 +59,15 @@ export class Rule extends Lint.Rules.AbstractRule {
type: "array",
items: {
type: "string",
enum: [OPTION_SINGLE, OPTION_DOUBLE, OPTION_JSX_SINGLE, OPTION_JSX_DOUBLE, OPTION_AVOID_ESCAPE, OPTION_AVOID_TEMPLATE],
enum: [
OPTION_SINGLE,
OPTION_DOUBLE,
OPTION_BACKTICK,
OPTION_JSX_SINGLE,
OPTION_JSX_DOUBLE,
OPTION_AVOID_ESCAPE,
OPTION_AVOID_TEMPLATE,
],
},
minLength: 0,
maxLength: 5,
Expand All @@ -74,11 +87,14 @@ export class Rule extends Lint.Rules.AbstractRule {

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
const args = this.ruleArguments;
const quoteMark = getQuotemarkPreference(args) === OPTION_SINGLE ? "'" : '"';

const quoteMark = getQuotemarkPreference(args) ;
const jsxQuoteMark = getJSXQuotemarkPreference(args);

return this.applyWithFunction(sourceFile, walk, {
avoidEscape: hasArg(OPTION_AVOID_ESCAPE),
avoidTemplate: hasArg(OPTION_AVOID_TEMPLATE),
jsxQuoteMark: hasArg(OPTION_JSX_SINGLE) ? "'" : hasArg(OPTION_JSX_DOUBLE) ? '"' : quoteMark,
jsxQuoteMark,
quoteMark,
});

Expand All @@ -97,45 +113,103 @@ function walk(ctx: Lint.WalkContext<Options>) {
&& isSameLine(sourceFile, node.getStart(sourceFile), node.end)) {
const expectedQuoteMark = node.parent!.kind === ts.SyntaxKind.JsxAttribute ? options.jsxQuoteMark : options.quoteMark;
const actualQuoteMark = sourceFile.text[node.end - 1];

if (actualQuoteMark === expectedQuoteMark) {
return;
}

let fixQuoteMark = expectedQuoteMark;

const needsQuoteEscapes = node.text.includes(expectedQuoteMark);

// This string requires escapes to use the expected quote mark, but `avoid-escape` was passed
if (needsQuoteEscapes && options.avoidEscape) {
if (node.kind === ts.SyntaxKind.StringLiteral) {
return;
}

// If expecting double quotes, fix a template `a "quote"` to `a 'quote'` anyway,
// always preferring *some* quote mark over a template.
fixQuoteMark = expectedQuoteMark === '"' ? "'" : '"';
// If we are expecting double quotes, use single quotes to avoid
// escaping. Otherwise, just use double quotes.
fixQuoteMark = expectedQuoteMark === '"' ?
"'" :
'"';

// It also includes the fixQuoteMark. Let's try to use single
// quotes instead, unless we originally expected single
// quotes, in which case we will try to use backticks. This
// means that we may use backtick even with avoid-template
// in trying to avoid escaping. What is the desired priority
// here?
if (node.text.includes(fixQuoteMark)) {
return;
fixQuoteMark = expectedQuoteMark === "'" ?
"`" :
"'";

// It contains all of the other kinds of quotes. Escaping is
// unavoidable, sadly.
if (node.text.includes(fixQuoteMark)) {
return;
}
}
}

const start = node.getStart(sourceFile);
let text = sourceFile.text.substring(start + 1, node.end - 1);

if (needsQuoteEscapes) {
text = text.replace(new RegExp(fixQuoteMark, "g"), `\\${fixQuoteMark}`);
}

text = text.replace(new RegExp(`\\\\${actualQuoteMark}`, "g"), actualQuoteMark);
return ctx.addFailure(
start, node.end, Rule.FAILURE_STRING(actualQuoteMark, fixQuoteMark),
new Lint.Replacement(start, node.end - start, fixQuoteMark + text + fixQuoteMark));

return ctx.addFailure(start, node.end, Rule.FAILURE_STRING(actualQuoteMark, fixQuoteMark),
new Lint.Replacement(start, node.end - start, fixQuoteMark + text + fixQuoteMark));
}
ts.forEachChild(node, cb);
});
}

function getQuotemarkPreference(args: any[]): string | undefined {
function getQuotemarkPreference(args: any[]): QUOTE_MARK {
type QUOTE_PREF = typeof OPTION_SINGLE | typeof OPTION_DOUBLE | typeof OPTION_BACKTICK;

const quoteFromOption = {
[OPTION_SINGLE]: "'",
[OPTION_DOUBLE]: '"',
[OPTION_BACKTICK]: "`",
};

for (const arg of args) {
switch (arg) {
case OPTION_SINGLE:
case OPTION_DOUBLE:
case OPTION_BACKTICK:
return quoteFromOption[arg as QUOTE_PREF] as QUOTE_MARK;
}
}

// Default to double quotes if no pref is found.
return '"';
}

function getJSXQuotemarkPreference(args: any[]): JSX_QUOTE_MARK {
type JSX_QUOTE_PREF = typeof OPTION_JSX_SINGLE | typeof OPTION_JSX_DOUBLE;

const jsxQuoteFromOption = {
[OPTION_JSX_SINGLE]: "'",
[OPTION_JSX_DOUBLE]: '"',
};

for (const arg of args) {
if (arg === OPTION_SINGLE || arg === OPTION_DOUBLE) {
return arg as string;
switch (arg) {
case OPTION_JSX_SINGLE:
case OPTION_JSX_DOUBLE:
return jsxQuoteFromOption[arg as JSX_QUOTE_PREF] as JSX_QUOTE_MARK;
}
}
return undefined;

// The JSX preference was not found, so try to use the regular preference.
// If the regular pref is backtick, use double quotes instead.
const regularQuotemark = getQuotemarkPreference(args);

return regularQuotemark !== "`" ? regularQuotemark : '"';
}
9 changes: 9 additions & 0 deletions test/rules/quotemark/backtick/test.ts.fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
var single = `single`;
var double = `married`;
var singleWithinDouble = `'singleWithinDouble'`;
var doubleWithinSingle = `"doubleWithinSingle"`;
var tabNewlineWithinSingle = `tab\tNewline\nWithinSingle`;
`escaped'quotemark`;

// "avoid-template" option is not set.
`foo`;
15 changes: 15 additions & 0 deletions test/rules/quotemark/backtick/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
var single = 'single';
~~~~~~~~ [' should be `]
var double = "married";
~~~~~~~~~ [" should be `]
var singleWithinDouble = "'singleWithinDouble'";
~~~~~~~~~~~~~~~~~~~~~~ [" should be `]
var doubleWithinSingle = '"doubleWithinSingle"';
~~~~~~~~~~~~~~~~~~~~~~ [' should be `]
var tabNewlineWithinSingle = 'tab\tNewline\nWithinSingle';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be `]
'escaped\'quotemark';
~~~~~~~~~~~~~~~~~~~~ [' should be `]

// "avoid-template" option is not set.
`foo`;
5 changes: 5 additions & 0 deletions test/rules/quotemark/backtick/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"quotemark": [true, "backtick"]
}
}

0 comments on commit 559d880

Please sign in to comment.