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

Commit

Permalink
quotemark: Add 'no-template' option (#2766)
Browse files Browse the repository at this point in the history
  • Loading branch information
andy-hanson authored and nchen63 committed May 14, 2017
1 parent 0144b5f commit 13cf172
Show file tree
Hide file tree
Showing 14 changed files with 115 additions and 30 deletions.
7 changes: 6 additions & 1 deletion src/configs/all.ts
Expand Up @@ -210,7 +210,12 @@ export const rules = {
"prefer-method-signature": true,
"prefer-switch": true,
"prefer-template": true,
"quotemark": [true, "double", "avoid-escape"],
"quotemark": [
true,
"double",
"avoid-escape",
"avoid-template",
],
"return-undefined": true,
"semicolon": [true, "always"],
"space-before-function-paren": [true, {
Expand Down
2 changes: 1 addition & 1 deletion src/rules/interfaceNameRule.ts
Expand Up @@ -46,7 +46,7 @@ export class Rule extends Lint.Rules.AbstractRule {
/* tslint:enable:object-literal-sort-keys */

public static FAILURE_STRING = "interface name must start with a capitalized I";
public static FAILURE_STRING_NO_PREFIX = `interface name must not have an "I" prefix`;
public static FAILURE_STRING_NO_PREFIX = 'interface name must not have an "I" prefix';

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk, { never: this.ruleArguments.indexOf(OPTION_NEVER) !== -1 });
Expand Down
2 changes: 1 addition & 1 deletion src/rules/interfaceOverTypeLiteralRule.ts
Expand Up @@ -24,7 +24,7 @@ export class Rule extends Lint.Rules.AbstractRule {
public static metadata: Lint.IRuleMetadata = {
ruleName: "interface-over-type-literal",
description: "Prefer an interface declaration over a type literal (`type T = { ... }`)",
rationale: `Interfaces are generally preferred over type literals because interfaces can be implemented, extended and merged.`,
rationale: "Interfaces are generally preferred over type literals because interfaces can be implemented, extended and merged.",
optionsDescription: "Not configurable.",
options: null,
optionExamples: [true],
Expand Down
2 changes: 1 addition & 1 deletion src/rules/maxFileLineCountRule.ts
Expand Up @@ -39,7 +39,7 @@ export class Rule extends Lint.Rules.AbstractRule {

public static FAILURE_STRING(lineCount: number, lineLimit: number) {
return `This file has ${lineCount} lines, which exceeds the maximum of ${lineLimit} lines allowed. ` +
`Consider breaking this file up into smaller parts`;
"Consider breaking this file up into smaller parts";
}

public isEnabled(): boolean {
Expand Down
4 changes: 2 additions & 2 deletions src/rules/noStringThrowRule.ts
Expand Up @@ -24,8 +24,8 @@ export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "no-string-throw",
description: `Flags throwing plain strings or concatenations of strings ` +
`because only Errors produce proper stack traces.`,
description: "Flags throwing plain strings or concatenations of strings " +
"because only Errors produce proper stack traces.",
hasFix: true,
options: null,
optionsDescription: "Not configurable.",
Expand Down
2 changes: 1 addition & 1 deletion src/rules/noUnnecessaryTypeAssertionRule.ts
Expand Up @@ -22,7 +22,7 @@ export class Rule extends Lint.Rules.TypedRule {
/* tslint:disable:object-literal-sort-keys */
public static metadata: Lint.IRuleMetadata = {
ruleName: "no-unnecessary-type-assertion",
description: `Warns if a type assertion does not change the type of an expression.`,
description: "Warns if a type assertion does not change the type of an expression.",
options: null,
optionsDescription: "Not configurable",
type: "typescript",
Expand Down
2 changes: 1 addition & 1 deletion src/rules/objectLiteralKeyQuotesRule.ts
Expand Up @@ -74,7 +74,7 @@ export class Rule extends Lint.Rules.AbstractRule {
};
/* tslint:enable:object-literal-sort-keys */

public static INCONSISTENT_PROPERTY = `All property names in this object literal must be consistently quoted or unquoted.`;
public static INCONSISTENT_PROPERTY = "All property names in this object literal must be consistently quoted or unquoted.";
public static UNNEEDED_QUOTES(name: string) {
return `Unnecessarily quoted property '${name}' found.`;
}
Expand Down
70 changes: 49 additions & 21 deletions src/rules/quotemarkRule.ts
Expand Up @@ -15,6 +15,7 @@
* limitations under the License.
*/

import { isNoSubstitutionTemplateLiteral, isSameLine, isStringLiteral } from "tsutils";
import * as ts from "typescript";

import * as Lint from "../index";
Expand All @@ -23,12 +24,14 @@ const OPTION_SINGLE = "single";
const OPTION_DOUBLE = "double";
const OPTION_JSX_SINGLE = "jsx-single";
const OPTION_JSX_DOUBLE = "jsx-double";
const OPTION_AVOID_TEMPLATE = "avoid-template";
const OPTION_AVOID_ESCAPE = "avoid-escape";

interface Options {
quoteMark: string;
jsxQuoteMark: string;
quoteMark: '"' | "'";
jsxQuoteMark: '"' | "'";
avoidEscape: boolean;
avoidTemplate: boolean;
}

export class Rule extends Lint.Rules.AbstractRule {
Expand All @@ -44,6 +47,7 @@ export class Rule extends Lint.Rules.AbstractRule {
* \`"${OPTION_DOUBLE}"\` enforces double quotes.
* \`"${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.
* \`"${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"'\`.`,
Expand All @@ -57,7 +61,7 @@ export class Rule extends Lint.Rules.AbstractRule {
maxLength: 5,
},
optionExamples: [
[true, OPTION_SINGLE, OPTION_AVOID_ESCAPE],
[true, OPTION_SINGLE, OPTION_AVOID_ESCAPE, OPTION_AVOID_TEMPLATE],
[true, OPTION_SINGLE, OPTION_JSX_DOUBLE],
],
type: "style",
Expand All @@ -75,39 +79,63 @@ export class Rule extends Lint.Rules.AbstractRule {

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
const args = this.ruleArguments;
if (args.length > 0) {
if (args[0] !== OPTION_SINGLE && args[0] !== OPTION_DOUBLE) {
throw new Error(`First argument to 'quotemark' rule should be "${OPTION_SINGLE}" or "${OPTION_DOUBLE}"`);
}
}
const quoteMark = args[0] === OPTION_SINGLE ? "'" : '"';
return this.applyWithFunction(sourceFile, walk, {
avoidEscape: args.indexOf(OPTION_AVOID_ESCAPE) !== -1,
jsxQuoteMark: args.indexOf(OPTION_JSX_SINGLE) !== -1
? "'"
: args.indexOf(OPTION_JSX_DOUBLE) !== -1 ? '"' : quoteMark,
avoidEscape: hasArg(OPTION_AVOID_ESCAPE),
avoidTemplate: hasArg(OPTION_AVOID_TEMPLATE),
jsxQuoteMark: hasArg(OPTION_JSX_SINGLE) ? "'" : hasArg(OPTION_JSX_DOUBLE) ? '"' : quoteMark,
quoteMark,
});

function hasArg(name: string): boolean {
return args.indexOf(name) !== -1;
}
}
}

function walk(ctx: Lint.WalkContext<Options>) {
return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void {
if (node.kind === ts.SyntaxKind.StringLiteral) {
const expectedQuoteMark = node.parent!.kind === ts.SyntaxKind.JsxAttribute ? ctx.options.jsxQuoteMark : ctx.options.quoteMark;
const actualQuoteMark = ctx.sourceFile.text[node.end - 1];
const { sourceFile, options } = ctx;
ts.forEachChild(sourceFile, function cb(node) {
if (isStringLiteral(node)
|| options.avoidTemplate && isNoSubstitutionTemplateLiteral(node)
&& node.parent!.kind !== ts.SyntaxKind.TaggedTemplateExpression
&& 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;
}
const start = node.getStart(ctx.sourceFile);
let text = ctx.sourceFile.text.substring(start + 1, node.end - 1);
if ((node as ts.StringLiteral).text.includes(expectedQuoteMark)) {
if (ctx.options.avoidEscape) {

let fixQuoteMark = expectedQuoteMark;

const needsQuoteEscapes = node.text.includes(expectedQuoteMark);
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 (node.text.includes(fixQuoteMark)) {
return;
}
text = text.replace(new RegExp(expectedQuoteMark, "g"), `\\${expectedQuoteMark}`);
}
text = text.replace(new RegExp(`\\\\${actualQuoteMark}`, "g"), actualQuoteMark);

return ctx.addFailure(start, node.end, Rule.FAILURE_STRING(actualQuoteMark, expectedQuoteMark),
new Lint.Replacement(start, node.end - start, expectedQuoteMark + text + expectedQuoteMark),
);
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 ts.forEachChild(node, cb);
ts.forEachChild(node, cb);
});
}
2 changes: 1 addition & 1 deletion src/test/parse.ts
Expand Up @@ -61,7 +61,7 @@ export function parseErrorsFromMarkup(text: string): LintError[] {
const lines = textWithMarkup.map(parseLine);

if (lines.length > 0 && !(lines[0] instanceof CodeLine)) {
throw lintSyntaxError(`text cannot start with an error mark line.`);
throw lintSyntaxError("text cannot start with an error mark line.");
}

const messageSubstitutionLines = lines.filter((l) => l instanceof MessageSubstitutionLine) as MessageSubstitutionLine[];
Expand Down
18 changes: 18 additions & 0 deletions test/rules/quotemark/avoid-template/test.ts.fix
@@ -0,0 +1,18 @@
"fo`o";

"a 'quote'";

'a "quote"';

`a "quote" 'quote'`;

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

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

23 changes: 23 additions & 0 deletions test/rules/quotemark/avoid-template/test.ts.lint
@@ -0,0 +1,23 @@
`fo\`o`;
~~~~~~~ [0]

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

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

`a "quote" 'quote'`;

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

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

[0]: ` should be "
[1]: ` should be '
5 changes: 5 additions & 0 deletions test/rules/quotemark/avoid-template/tslint.json
@@ -0,0 +1,5 @@
{
"rules": {
"quotemark": [true, "double", "avoid-escape", "avoid-template"]
}
}
3 changes: 3 additions & 0 deletions test/rules/quotemark/double/test.ts.fix
Expand Up @@ -4,3 +4,6 @@ var singleWithinDouble = "'singleWithinDouble'";
var doubleWithinSingle = "\"doubleWithinSingle\"";
var tabNewlineWithinSingle = "tab\tNewline\nWithinSingle";
"escaped'quotemark";

// "avoid-template" option is not set.
`foo`;
3 changes: 3 additions & 0 deletions test/rules/quotemark/double/test.ts.lint
Expand Up @@ -8,3 +8,6 @@ var tabNewlineWithinSingle = 'tab\tNewline\nWithinSingle';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be "]
'escaped\'quotemark';
~~~~~~~~~~~~~~~~~~~~ [' should be "]

// "avoid-template" option is not set.
`foo`;

0 comments on commit 13cf172

Please sign in to comment.