Add preferTemplate autofix
MODIFY
packages/@romejs/compiler/lint/rules/js/preferTemplate.test.ts
packages/@romejs/compiler/lint/rules/js/preferTemplate.test.ts
@@ -16,7 +16,15 @@ test(
{
invalid: [
`const foo = 'bar'; console.log(foo + 'baz')`,
- `console.log((1 * 2) + 'baz')`,
+ `console.log((1 * 2) + 'foo')`,
+ `console.log(1 + 'foo' + 2 + 'bar' + 'baz' + 3)`,
+ `console.log((1 + 'foo') * 2)`,
+ `console.log((1 * (2 + 'foo')) + 'bar')`,
+ `console.log('foo' + 'bar')`,
+ `console.log('foo' + '"bar')`,
+ `console.log(\`foo\` + 1)`,
+ `console.log('foo' + \`bar\${\`baz\${'bat' + 'bam'}\`}\` + 'boo')`,
+ `console.log('foo' + 1 + 2)`,
],
},
{

MODIFY
packages/@romejs/compiler/lint/rules/js/preferTemplate.ts
packages/@romejs/compiler/lint/rules/js/preferTemplate.ts
* LICENSE file in the root directory of this source tree.
*/
-import {Path} from "@romejs/compiler";
+import {Path, TransformExitResult} from "@romejs/compiler";
import {
+ AnyJSExpression,
+ AnyNode,
+ JSBinaryExpression,
+ JSStringLiteral,
+ JSTemplateElement,
JSTemplateLiteral,
+ jsStringLiteral,
jsTemplateElement,
jsTemplateLiteral,
} from "@romejs/ast";
import {descriptions} from "@romejs/diagnostics";
-import {TransformExitResult} from "@romejs/compiler/types";
-import {removeShallowLoc} from "@romejs/js-ast-utils";
-export default {
- name: "preferTemplate",
- enter(path: Path): TransformExitResult {
- const {node} = path;
+type TemplatePart = AnyJSExpression | JSTemplateElement;
+type StaticString = JSStringLiteral | JSTemplateElement;
+
+// expr + expr
+function isBinaryAddExpression(node: AnyNode): node is JSBinaryExpression {
+ return node.type === "JSBinaryExpression" && node.operator === "+";
+}
+
+// 'str' + 'str'
+// 'str' + expr
+// expr + 'str'
+// expr + (expr + 'str')
+// (expr + 'str') + expr
+function isUnnecessaryStringConcatExpression(
+ node: AnyNode,
+): node is JSBinaryExpression {
+ if (!isBinaryAddExpression(node)) {
+ return false;
+ }
+
+ if (node.left.type === "JSBinaryExpression") {
+ if (isUnnecessaryStringConcatExpression(node.left)) {
+ return true;
+ }
+ }
+
+ if (node.right.type === "JSBinaryExpression") {
+ if (isUnnecessaryStringConcatExpression(node.right)) {
+ return true;
+ }
+ }
+
+ if (node.left.type === "JSStringLiteral" && !node.left.value.includes("`")) {
+ return true;
+ }
+
+ if (node.right.type === "JSStringLiteral" && !node.right.value.includes("`")) {
+ return true;
+ }
+
+ if (node.left.type === "JSTemplateLiteral") {
+ return true;
+ }
+
+ if (node.right.type === "JSTemplateLiteral") {
+ return true;
+ }
+
+ return false;
+}
+
+// expr + expr + expr + ...
+function collectBinaryAddExpressionExpressions(
+ node: JSBinaryExpression,
+): Array<AnyJSExpression> {
+ let expressions: Array<AnyJSExpression> = [];
+
+ if (isBinaryAddExpression(node.left)) {
+ expressions = expressions.concat(
+ collectBinaryAddExpressionExpressions(node.left),
+ );
+ } else {
+ expressions.push(node.left);
+ }
+
+ if (isBinaryAddExpression(node.right)) {
+ expressions = expressions.concat(
+ collectBinaryAddExpressionExpressions(node.right),
+ );
+ } else {
+ expressions.push(node.right);
+ }
+
+ return expressions;
+}
+
+// zips template.quasis and template.expressions into one array
+function zipTemplateLiteralParts(
+ template: JSTemplateLiteral,
+): Array<TemplatePart> {
+ let templateParts = [];
+
+ for (let i = 0; i < template.quasis.length; i++) {
+ templateParts.push(template.quasis[i]);
+
+ if (i + 1 < template.quasis.length) {
+ templateParts.push(template.expressions[i]);
+ }
+ }
+
+ return templateParts;
+}
+
+// flattens an array of expressions into TemplateLiteral parts
+function flattenExpressionsToTemplateParts(
+ expressions: Array<AnyJSExpression>,
+): Array<TemplatePart> {
+ let parts: Array<TemplatePart> = [];
+ let queue: Array<TemplatePart> = [...expressions];
+
+ while (true) {
+ let node = queue.shift();
+ if (!node) {
+ break;
+ }
+
+ if (node.type === "JSTemplateLiteral") {
+ queue = [...zipTemplateLiteralParts(node), ...queue];
+ } else {
+ parts.push(node);
+ }
+ }
+
+ return parts;
+}
+
+// 'str' + 'str' + expr -> 'strstr' + expr
+function combineTemplateParts(expressions: Array<TemplatePart>) {
+ let reducedExpressions: Array<AnyJSExpression> = [];
+ let index = 0;
+
+ while (index < expressions.length) {
+ let current = expressions[index];
if (
- node.type === "JSBinaryExpression" &&
- node.operator === "+" &&
- ((node.left.type === "JSStringLiteral" && !node.left.value.includes("`")) ||
- (node.right.type === "JSStringLiteral" && !node.right.value.includes("`")))
+ current.type === "JSStringLiteral" ||
+ current.type === "JSTemplateElement"
) {
- let autofix: undefined | JSTemplateLiteral;
+ let strings: Array<StaticString> = [current];
- if (node.right.type === "JSStringLiteral") {
- const quasis = [
- jsTemplateElement.create({
- raw: "",
- cooked: "",
- }),
- jsTemplateElement.create({
- raw: node.right.value,
- cooked: node.right.value,
- }),
- ];
- const expressions = [removeShallowLoc(node.left)];
- autofix = jsTemplateLiteral.create({
- expressions,
- quasis,
- loc: node.loc,
- });
+ while (index + 1 < expressions.length) {
+ let next = expressions[index + 1];
+ if (next.type === "JSStringLiteral" || next.type === "JSTemplateElement") {
+ strings.push(next);
+ index++;
+ } else {
+ break;
+ }
}
- if (node.left.type === "JSStringLiteral") {
- const quasis = [
- jsTemplateElement.create({
- raw: node.left.value,
- cooked: node.left.value,
+ if (strings.length === 1 && current.type === "JSStringLiteral") {
+ reducedExpressions.push(current);
+ } else {
+ reducedExpressions.push(
+ jsStringLiteral.create({
+ value: strings.map((string) => {
+ if (string.type === "JSTemplateElement") {
+ return string.raw;
+ } else {
+ return string.value;
+ }
+ }).join(""),
}),
- jsTemplateElement.create({
- raw: "",
+ );
+ }
+ } else {
+ reducedExpressions.push(current);
+ }
+
+ index++;
+ }
+
+ return reducedExpressions;
+}
+
+function createEmptyQuasis(isTail: boolean = false) {
+ return jsTemplateElement.create({
cooked: "",
- }),
- ];
-
- // We need to remove the location or else if we were to show a preview the source map would resolve to the end of
- // this node
- const expressions = [removeShallowLoc(node.right)];
- autofix = jsTemplateLiteral.create({
- expressions,
- quasis,
- loc: node.loc,
+ raw: "",
+ tail: isTail,
});
}
- if (autofix === undefined) {
- path.context.addNodeDiagnostic(
- node,
- descriptions.LINT.JS_PREFER_TEMPLATE,
+// 'str' + expr + 'str' -> `str${expr}str`
+function convertTemplatePartsToTemplateLiteral(
+ nodes: Array<TemplatePart>,
+): JSStringLiteral | JSTemplateLiteral {
+ let templateExpressions: Array<AnyJSExpression> = [];
+ let templateQuasis: Array<JSTemplateElement> = [];
+
+ for (let index = 0; index < nodes.length; index++) {
+ let node = nodes[index];
+ let isTail = index === nodes.length - 1;
+ let isHead = index === 0;
+
+ if (node.type === "JSTemplateElement") {
+ templateQuasis.push(node);
+ } else if (node.type === "JSStringLiteral") {
+ templateQuasis.push(
+ jsTemplateElement.create({
+ cooked: node.value,
+ raw: node.value,
+ tail: isTail,
+ }),
);
} else {
+ templateExpressions.push(node);
+
+ let next = nodes[index + 1];
+ let isNextQuasis =
+ next?.type === "JSStringLiteral" || next?.type === "JSTemplateElement";
+
+ if (isTail || isHead || !isNextQuasis) {
+ templateQuasis.push(createEmptyQuasis(isTail));
+ }
+ }
+ }
+
+ return jsTemplateLiteral.create({
+ expressions: templateExpressions,
+ quasis: templateQuasis,
+ });
+}
+
+// Only convert to a template literal when at least one of the expressions isn't a string
+// Concatenating together only strings is allowed as it's sometimes used to break up long lines
+// Ignore:
+// str + str
+// str + str + str
+// Replace:
+// str + expr
+// str + expr + str
+function shouldReplace(expressions: Array<AnyJSExpression>): boolean {
+ for (let expression of expressions) {
+ if (expression.type !== "JSStringLiteral") {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+export default {
+ name: "preferTemplate",
+ enter(path: Path): TransformExitResult {
+ const {node} = path;
+
+ if (isUnnecessaryStringConcatExpression(node)) {
+ const expressions = collectBinaryAddExpressionExpressions(node);
+
+ if (shouldReplace(expressions)) {
+ const templateParts = flattenExpressionsToTemplateParts(expressions);
+ const combinedParts = combineTemplateParts(templateParts);
+ const template = convertTemplatePartsToTemplateLiteral(combinedParts);
+
return path.context.addFixableDiagnostic(
{
old: node,
- fixed: autofix,
+ fixed: template,
},
descriptions.LINT.JS_PREFER_TEMPLATE,
);

Add preferTemplate autofix

Co-Authored-By: Jamie Kyle <jamiebuilds@users.noreply.github.com>