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

quotemark: Add 'no-template' option #2766

Merged
merged 2 commits into from
May 14, 2017
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
7 changes: 6 additions & 1 deletion src/configs/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ var tabNewlineWithinSingle = 'tab\tNewline\nWithinSingle';
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [' should be "]
'escaped\'quotemark';
~~~~~~~~~~~~~~~~~~~~ [' should be "]

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