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 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/configs/all.ts
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",
"no-template",
Copy link
Contributor

Choose a reason for hiding this comment

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

rename to avoid-template since some templates are allowed

],
"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
75 changes: 54 additions & 21 deletions src/rules/quotemarkRule.ts
Expand Up @@ -15,6 +15,7 @@
* limitations under the License.
*/

import { isNoSubstitutionTemplateLiteral, 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_NO_TEMPLATE = "no-template";
const OPTION_AVOID_ESCAPE = "avoid-escape";

interface Options {
quoteMark: string;
jsxQuoteMark: string;
quoteMark: '"' | "'";
jsxQuoteMark: '"' | "'";
avoidEscape: boolean;
noTemplate: 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_NO_TEMPLATE}"\` forbids single-line template strings that do not contain string interpolations.
Copy link
Contributor

Choose a reason for hiding this comment

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

also "untagged"

* \`"${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_NO_TEMPLATE],
[true, OPTION_SINGLE, OPTION_JSX_DOUBLE],
],
type: "style",
Expand All @@ -75,39 +79,68 @@ 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),
jsxQuoteMark: hasArg(OPTION_JSX_SINGLE) ? "'" : hasArg(OPTION_JSX_DOUBLE) ? '"' : quoteMark,
noTemplate: hasArg(OPTION_NO_TEMPLATE),
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)
|| isNoSubstitutionTemplateLiteral(node)
Copy link
Contributor

Choose a reason for hiding this comment

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

needs to check if the option is set

&& node.parent!.kind !== ts.SyntaxKind.TaggedTemplateExpression
&& !isMultiLine(node, sourceFile)) {
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);
});
}

function isMultiLine(node: ts.Node, sourceFile: ts.SourceFile): boolean {
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line !==
Copy link
Contributor

Choose a reason for hiding this comment

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

you can avoid that boilerplate code by using tsutils.isSameLine

return !isSameLine(sourceFile, node.getStart(sourceFile), node.end);

sourceFile.getLineAndCharacterOfPosition(node.end).line;
}
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/no-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/no-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/no-template/tslint.json
@@ -0,0 +1,5 @@
{
"rules": {
"quotemark": [true, "double", "no-template", "avoid-escape"]
}
}