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

Fix quotemark avoid-template issues #4408

Merged
merged 4 commits into from
Jan 10, 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
117 changes: 54 additions & 63 deletions src/rules/quotemarkRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ 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 = "'" | '"';
type QUOTEMARK = "'" | '"' | "`";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed "quote mark" to "quotemark" everywhere to be consistent

type JSX_QUOTEMARK = "'" | '"';

interface Options {
quoteMark: QUOTE_MARK;
jsxQuoteMark: JSX_QUOTE_MARK;
quotemark: QUOTEMARK;
jsxQuotemark: JSX_QUOTEMARK;
avoidEscape: boolean;
avoidTemplate: boolean;
}
Expand All @@ -52,9 +52,11 @@ export class Rule extends Lint.Rules.AbstractRule {
* \`"${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.
Note that backticks may still be used if \`"${OPTION_AVOID_ESCAPE}"\` is enabled and both single and double quotes are
present in the string (the latter option takes precedence).
* \`"${OPTION_AVOID_ESCAPE}"\` allows you to use the "other" quotemark in cases where escaping would normally be required.
For example, \`[true, "${OPTION_DOUBLE}", "${OPTION_AVOID_ESCAPE}"]\` would not report a failure on the string literal
\`'Hello "World"'\`.`,
For example, \`[true, "${OPTION_DOUBLE}", "${OPTION_AVOID_ESCAPE}"]\` would not report a failure on the string literal
\`'Hello "World"'\`.`,
options: {
type: "array",
items: {
Expand Down Expand Up @@ -87,15 +89,14 @@ export class Rule extends Lint.Rules.AbstractRule {

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
const args = this.ruleArguments;

const quoteMark = getQuotemarkPreference(args);
const jsxQuoteMark = getJSXQuotemarkPreference(args);
const quotemark = getQuotemarkPreference(args);
const jsxQuotemark = getJSXQuotemarkPreference(args, quotemark);

return this.applyWithFunction(sourceFile, walk, {
avoidEscape: hasArg(OPTION_AVOID_ESCAPE),
avoidTemplate: hasArg(OPTION_AVOID_TEMPLATE),
jsxQuoteMark,
quoteMark,
jsxQuotemark,
quotemark,
});

function hasArg(name: string): boolean {
Expand All @@ -114,108 +115,98 @@ function walk(ctx: Lint.WalkContext<Options>) {
node.parent.kind !== ts.SyntaxKind.TaggedTemplateExpression &&
isSameLine(sourceFile, node.getStart(sourceFile), node.end))
) {
const expectedQuoteMark =
const expectedQuotemark =
node.parent.kind === ts.SyntaxKind.JsxAttribute
? options.jsxQuoteMark
: options.quoteMark;
const actualQuoteMark = sourceFile.text[node.end - 1];
? options.jsxQuotemark
: options.quotemark;
const actualQuotemark = sourceFile.text[node.end - 1];

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

let fixQuoteMark = expectedQuoteMark;

const needsQuoteEscapes = node.text.includes(expectedQuoteMark);
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 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)) {
fixQuoteMark = expectedQuoteMark === "'" ? "`" : "'";

// It contains all of the other kinds of quotes. Escaping is
// unavoidable, sadly.
if (node.text.includes(fixQuoteMark)) {
// If we are expecting double quotes, use single quotes to avoid escaping.
// Otherwise, just use double quotes.
const alternativeFixQuotemark = expectedQuotemark === '"' ? "'" : '"';

if (node.text.includes(alternativeFixQuotemark)) {
// It also includes the alternative fix quote mark. 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.
fixQuotemark = expectedQuotemark === "'" ? "`" : "'";

if (fixQuotemark === actualQuotemark) {
// We were already using the best quote mark for this scenario
return;
} else if (node.text.includes(fixQuotemark)) {
// It contains all of the other kinds of quotes. Escaping is unavoidable, sadly.
return;
}
} else {
fixQuotemark = alternativeFixQuotemark;
}
}

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(fixQuotemark, "g"), `\\${fixQuotemark}`);
}

text = text.replace(new RegExp(`\\\\${actualQuoteMark}`, "g"), actualQuoteMark);
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),
Rule.FAILURE_STRING(actualQuotemark, fixQuotemark),
new Lint.Replacement(start, node.end - start, fixQuotemark + text + fixQuotemark),
);
}
ts.forEachChild(node, cb);
});
}

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also simplified these two helper functions to reduce duplication and the extra typedefs


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

for (const arg of args) {
function getQuotemarkPreference(ruleArguments: any[]): QUOTEMARK {
for (const arg of ruleArguments) {
switch (arg) {
case OPTION_SINGLE:
return "'";
case OPTION_DOUBLE:
return '"';
case OPTION_BACKTICK:
return quoteFromOption[arg as QUOTE_PREF] as QUOTE_MARK;
return "`";
default:
continue;
}
}

// 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) {
function getJSXQuotemarkPreference(ruleArguments: any[], regularQuotemarkPreference: QUOTEMARK): JSX_QUOTEMARK {
for (const arg of ruleArguments) {
switch (arg) {
case OPTION_JSX_SINGLE:
return "'";
case OPTION_JSX_DOUBLE:
return jsxQuoteFromOption[arg as JSX_QUOTE_PREF] as JSX_QUOTE_MARK;
return '"';
default:
continue;
}
}

// 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 : '"';
return regularQuotemarkPreference !== "`" ? regularQuotemarkPreference : '"';
}
5 changes: 0 additions & 5 deletions test/rules/quotemark/avoid-template/tslint.json

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ bar
foo``;
`${foo}`;

this.toastCtrl.present('Please tick "Yes" to confirm');

Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@ bar
foo``;
`${foo}`;

this.toastCtrl.present(`Please tick "Yes" to confirm`);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [1]

[0]: ` should be "
[1]: ` should be '
5 changes: 5 additions & 0 deletions test/rules/quotemark/double-avoid-template/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"quotemark": [true, "double", "avoid-escape", "avoid-template"]
}
}
2 changes: 1 addition & 1 deletion test/rules/quotemark/single-avoid-escape/tslint.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"rules": {
"quotemark": [true, "avoid-escape", "single"]
"quotemark": [true, "single", "avoid-escape"]
}
}
20 changes: 20 additions & 0 deletions test/rules/quotemark/single-avoid-template/test.ts.fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'fo`o';

"a 'quote'";

'a "quote"';

`a "quote" 'quote'`;

// Allow multi-line templates
`
foo
bar
`;

// Allow tagged templates and templates with substitutions
foo``;
`${foo}`;

this.toastCtrl.present('Please tick "Yes" to confirm');

26 changes: 26 additions & 0 deletions test/rules/quotemark/single-avoid-template/test.ts.lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
`fo\`o`;
~~~~~~~ [0]

`a 'quote'`;
~~~~~~~~~~~ [1]

`a "quote"`;
~~~~~~~~~~~ [0]

`a "quote" 'quote'`;

// Allow multi-line templates
`
foo
bar
`;

// Allow tagged templates and templates with substitutions
foo``;
`${foo}`;

this.toastCtrl.present(`Please tick "Yes" to confirm`);
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [0]

[0]: ` should be '
[1]: ` should be "
5 changes: 5 additions & 0 deletions test/rules/quotemark/single-avoid-template/tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"rules": {
"quotemark": [true, "single", "avoid-escape", "avoid-template"]
}
}