Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tslint] blueprint-html-components fixer! #3162

Merged
merged 8 commits into from
Nov 16, 2018
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
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function getAllMatches(className: string) {
let currentMatch: RegExpMatchArray | null;
// tslint:disable-next-line:no-conditional-assignment
while ((currentMatch = BLUEPRINT_CLASSNAME_PATTERN.exec(className)) != null) {
ptMatches.push({ match: currentMatch[1], index: currentMatch.index });
ptMatches.push({ match: currentMatch[1], index: currentMatch.index || 0 });
}
return ptMatches;
}
Expand Down
36 changes: 33 additions & 3 deletions packages/tslint-config/src/rules/blueprintHtmlComponentsRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import * as Lint from "tslint";
import * as ts from "typescript";
import { addImportToFile } from "./utils/addImportToFile";
import { replaceTagName } from "./utils/replaceTagName";

const PATTERN = /^(h[1-6]|code|pre|blockquote|table)$/;

Expand All @@ -31,14 +33,42 @@ export class Rule extends Lint.Rules.AbstractRule {
}

function walk(ctx: Lint.WalkContext<void>): void {
return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void {
const tagFailures: Array<{
jsxTag: ts.JsxTagNameExpression;
newTagName: string;
replacements: Lint.Replacement[];
}> = [];

// walk file and build up array of failures
ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void {
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
const match = PATTERN.exec(node.tagName.getFullText());
if (match != null) {
const name = match[1].charAt(0).toUpperCase() + match[1].slice(1);
ctx.addFailureAt(node.tagName.getFullStart(), node.tagName.getFullWidth(), Rule.getFailure(name));
const newTagName = match[1].charAt(0).toUpperCase() + match[1].slice(1);
const replacements = [replaceTagName(node.tagName, newTagName)];

if (ts.isJsxOpeningElement(node)) {
// find closing tag after this opening tag to replace both in one failure
const [closingNode] = node.parent!.getChildren().filter(ts.isJsxClosingElement);
replacements.push(replaceTagName(closingNode.tagName, newTagName));
}

tagFailures.push({ jsxTag: node.tagName, newTagName, replacements });
}
}
return ts.forEachChild(node, cb);
});

if (tagFailures.length === 0) {
return;
}

// collect all potential new imports into one replacement (in first failure), after processing entire file.
const importsToAdd = addImportToFile(ctx.sourceFile, tagFailures.map(m => m.newTagName), "@blueprintjs/core");
tagFailures[0].replacements.push(importsToAdd);

// add all failures at the end
tagFailures.forEach(({ jsxTag, newTagName, replacements }) =>
ctx.addFailureAt(jsxTag.getFullStart(), jsxTag.getFullWidth(), Rule.getFailure(newTagName), replacements),
);
}
10 changes: 7 additions & 3 deletions packages/tslint-config/src/rules/blueprintIconComponentsRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,19 @@ function walk(ctx: Lint.WalkContext<string>): void {
if (ts.isJsxAttribute(node) && node.name.text === "icon") {
const { initializer } = node;
const option = ctx.options;
if (ts.isStringLiteral(initializer) && option === OPTION_COMPONENT) {
if (initializer === undefined) {
// no-op
} else if (ts.isStringLiteral(initializer) && option === OPTION_COMPONENT) {
// "tick" -> <TickIcon />
giladgray marked this conversation as resolved.
Show resolved Hide resolved
const iconName = `<${pascalCase(initializer.text)}Icon />`;
addFailure(ctx, node, Rule.componentMessage(iconName), `{${iconName}}`);
} else if (ts.isJsxExpression(initializer) && option === OPTION_LITERAL) {
// <TickIcon /> -> "tick"
const match = /<(\w+)Icon /.exec(initializer.getText());
const literal = match == null ? undefined : `"${dashCase(match[1])}"`;
addFailure(ctx, node, Rule.literalMessage(literal), literal);
if (match != null) {
const message = Rule.literalMessage(`"${dashCase(match[1])}"`);
addFailure(ctx, node, message, message);
}
}
}
return ts.forEachChild(node, cb);
Expand Down
6 changes: 2 additions & 4 deletions packages/tslint-config/src/rules/utils/addImportToFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ export function addImportToFile(file: ts.SourceFile, imports: string[], packageN
) {
const existingImports = packageToModify.importClause.namedBindings.elements.map(el => el.name.getText());
// Poor man's lodash.uniq without the dep.
const newImports = Array.from(new Set(existingImports.concat(imports))).sort((a, b) =>
a.toLowerCase().localeCompare(b.toLowerCase()),
);
const newImports = Array.from(new Set(existingImports.concat(imports))).sort();
const importString = `{ ${newImports.join(", ")} }`;
return Replacement.replaceNode(packageToModify.importClause.namedBindings, importString);
} else {
Expand All @@ -34,7 +32,7 @@ export function addImportToFile(file: ts.SourceFile, imports: string[], packageN
return compare(imp.moduleSpecifier.getText().slice(1, -1), packageName) === 1;
});
const startIndex = newImportIndex === -1 ? 0 : allImports[newImportIndex].getStart();
return Replacement.appendText(startIndex, `import { ${imports.join(", ")} } from "${packageName}";\n`);
return Replacement.appendText(startIndex, `import { ${imports.sort().join(", ")} } from "${packageName}";\n`);
}
}

Expand Down
13 changes: 13 additions & 0 deletions packages/tslint-config/src/rules/utils/replaceTagName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright 2018 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the terms of the LICENSE file distributed with this project.
*/

import { Replacement } from "tslint";
import { JsxTagNameExpression } from "typescript";

/** Replace the name of a JSX tag. */
export function replaceTagName(tagName: JsxTagNameExpression, newTagName: string) {
return new Replacement(tagName.getFullStart(), tagName.getFullWidth(), newTagName);
}
3 changes: 2 additions & 1 deletion packages/tslint-config/src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"compilerOptions": {
"lib": ["es6", "dom"],
"module": "commonjs",
"outDir": "../lib/rules"
"outDir": "../lib/rules",
"strict": true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Blockquote, Code, H2, H5, Pre } from "@blueprintjs/core";
<H5>Subtitle</H5>
<Pre>block</Pre>
<H2>Title</H2>
<Code>code element</Code>
<Blockquote />

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<h5>Subtitle</h5>
~~ [err % ("H5")]
<pre>block</pre>
~~~ [err % ("Pre")]
<h2>Title</h2>
~~ [err % ("H2")]
<code>code element</code>
~~~~ [err % ("Code")]
<blockquote />
~~~~~~~~~~ [err % ("Blockquote")]

[err]: use Blueprint <%s> component instead of JSX intrinsic element.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Blockquote, Code, H1, H3, Pre } from "@blueprintjs/core";
<H3>Subtitle</H3>
<Pre>block</Pre>

<H1>Title</H1>
<Code>code element</Code>
<Blockquote />
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { H3, Pre } from "@blueprintjs/core";
<H3>Subtitle</H3>
<Pre>block</Pre>
giladgray marked this conversation as resolved.
Show resolved Hide resolved

<h1>Title</h1>
~~ [err % ("H1")]

<code>code element</code>
~~~~ [err % ("Code")]

<blockquote />
~~~~~~~~~~ [err % ("Blockquote")]

<H3>Subtitle</H3>
<Pre>block</Pre>

[err]: use Blueprint <%s> component instead of JSX intrinsic element.