@taiga-ui/eslint-plugin-experience-next 0.451.0 → 0.452.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -1
- package/index.d.ts +1 -1
- package/index.esm.js +79 -33
- package/package.json +1 -1
- package/rules/no-string-literal-concat.d.ts +1 -1
package/README.md
CHANGED
|
@@ -323,8 +323,51 @@ const c = a + b;
|
|
|
323
323
|
const c = `${a}${b}`;
|
|
324
324
|
```
|
|
325
325
|
|
|
326
|
+
When the concatenation is a **direct expression inside a template literal**, the parts are inlined into the outer
|
|
327
|
+
template instead of producing a nested template literal:
|
|
328
|
+
|
|
329
|
+
```ts
|
|
330
|
+
// ❌ error
|
|
331
|
+
const url = `${base}${path + query}`;
|
|
332
|
+
|
|
333
|
+
// ✅ after autofix — inlined, no nesting
|
|
334
|
+
const url = `${base}${path}${query}`;
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
// ❌ error — literal concat inside template
|
|
339
|
+
const mask = `${'HH' + ':MM'}`;
|
|
340
|
+
|
|
341
|
+
// ✅ after autofix
|
|
342
|
+
const mask = `HH:MM`;
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
When the concatenation appears **inside a method call or other expression** within a template literal, the rule skips it
|
|
346
|
+
to avoid creating unreadable nested template literals like `` `${`${a}${b}`.method()}` ``.
|
|
347
|
+
|
|
348
|
+
The rule also **flattens already-nested template literals** produced by earlier autofixes or written by hand:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
// ❌ error
|
|
352
|
+
const s = `${`${dateMode}${dateTimeSeparator}`}HH:MM`;
|
|
353
|
+
|
|
354
|
+
// ✅ after autofix
|
|
355
|
+
const s = `${dateMode}${dateTimeSeparator}HH:MM`;
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Concatenation that uses **inline comments between parts** is intentionally left untouched, as the comments serve as
|
|
359
|
+
documentation:
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
// ✅ not flagged — comments are preserved
|
|
363
|
+
const urlRegex =
|
|
364
|
+
String.raw`^([a-zA-Z]+:\/\/)?` + // protocol
|
|
365
|
+
String.raw`([\w-]+\.)+[\w]{2,}` + // domain
|
|
366
|
+
String.raw`(\/\S*)?$`; // path
|
|
367
|
+
```
|
|
368
|
+
|
|
326
369
|
> For mixed concatenation (`'prefix' + variable`) use the standard `prefer-template` rule, which is already enabled in
|
|
327
|
-
> `recommended`. Template literals (`` `foo` + `bar` ``) are not flagged by this rule.
|
|
370
|
+
> `recommended`. Template literals (`` `foo` + `bar` ``) and tagged templates are not flagged by this rule.
|
|
328
371
|
|
|
329
372
|
---
|
|
330
373
|
|
package/index.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ declare const plugin: {
|
|
|
40
40
|
'no-playwright-empty-fill': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useClear", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
41
41
|
name: string;
|
|
42
42
|
};
|
|
43
|
-
'no-string-literal-concat': import("@typescript-eslint/utils/ts-eslint").RuleModule<"mergeLiterals" | "useTemplate", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
43
|
+
'no-string-literal-concat': import("@typescript-eslint/utils/ts-eslint").RuleModule<"flattenTemplate" | "mergeLiterals" | "useTemplate", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
44
44
|
name: string;
|
|
45
45
|
};
|
|
46
46
|
'object-single-line': import("@typescript-eslint/utils/ts-eslint").RuleModule<"oneLine", [{
|
package/index.esm.js
CHANGED
|
@@ -2180,7 +2180,7 @@ function isPlaywrightLocatorType(type) {
|
|
|
2180
2180
|
}
|
|
2181
2181
|
|
|
2182
2182
|
const createRule$6 = ESLintUtils.RuleCreator((name) => name);
|
|
2183
|
-
function
|
|
2183
|
+
function isStringLiteral(node) {
|
|
2184
2184
|
return (node.type === AST_NODE_TYPES.Literal &&
|
|
2185
2185
|
typeof node.value === 'string');
|
|
2186
2186
|
}
|
|
@@ -2212,19 +2212,36 @@ function buildMergedString(parts) {
|
|
|
2212
2212
|
.replaceAll(new RegExp(quote, 'g'), `\\${quote}`);
|
|
2213
2213
|
return `${quote}${escaped}${quote}`;
|
|
2214
2214
|
}
|
|
2215
|
-
function
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2215
|
+
function escapeForTemplateLiteral(value) {
|
|
2216
|
+
return value.replaceAll('\\', '\\\\').replaceAll('`', '\\`').replaceAll('${', '\\${');
|
|
2217
|
+
}
|
|
2218
|
+
function partsToTemplateContent(parts, getText) {
|
|
2219
|
+
return parts
|
|
2220
|
+
.map((part) => isStringLiteral(part)
|
|
2221
|
+
? escapeForTemplateLiteral(part.value)
|
|
2222
|
+
: `\${${getText(part)}}`)
|
|
2223
|
+
.join('');
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Returns the raw content between the backticks of a TemplateLiteral,
|
|
2227
|
+
* delegating each expression slot to `renderExpr`.
|
|
2228
|
+
*/
|
|
2229
|
+
function templateContent(template, renderExpr) {
|
|
2230
|
+
return template.quasis
|
|
2231
|
+
.map((quasi, i) => `${quasi.value.raw}${i < template.expressions.length
|
|
2232
|
+
? renderExpr(template.expressions[i], i)
|
|
2233
|
+
: ''}`)
|
|
2226
2234
|
.join('');
|
|
2227
|
-
|
|
2235
|
+
}
|
|
2236
|
+
function hasTemplateLiteralAncestor(node) {
|
|
2237
|
+
let current = node.parent;
|
|
2238
|
+
while (current != null) {
|
|
2239
|
+
if (current.type === AST_NODE_TYPES.TemplateLiteral) {
|
|
2240
|
+
return true;
|
|
2241
|
+
}
|
|
2242
|
+
current = current.parent;
|
|
2243
|
+
}
|
|
2244
|
+
return false;
|
|
2228
2245
|
}
|
|
2229
2246
|
const rule$4 = createRule$6({
|
|
2230
2247
|
create(context) {
|
|
@@ -2239,46 +2256,74 @@ const rule$4 = createRule$6({
|
|
|
2239
2256
|
// Type checking not available — only literal concatenation will be checked
|
|
2240
2257
|
}
|
|
2241
2258
|
function isStringNode(node) {
|
|
2242
|
-
if (
|
|
2259
|
+
if (isStringLiteral(node)) {
|
|
2243
2260
|
return true;
|
|
2244
2261
|
}
|
|
2245
2262
|
if (!parserServices || !checker) {
|
|
2246
2263
|
return false;
|
|
2247
2264
|
}
|
|
2248
2265
|
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
|
|
2249
|
-
|
|
2250
|
-
return isStringType(type, checker);
|
|
2266
|
+
return isStringType(checker.getTypeAtLocation(tsNode), checker);
|
|
2251
2267
|
}
|
|
2268
|
+
const getText = (n) => sourceCode.getText(n);
|
|
2269
|
+
const wrapExpr = (expr) => `\${${getText(expr)}}`;
|
|
2252
2270
|
return {
|
|
2253
2271
|
BinaryExpression(node) {
|
|
2254
|
-
if (node.operator !== '+') {
|
|
2272
|
+
if (node.operator !== '+' || !isRootConcat(node)) {
|
|
2255
2273
|
return;
|
|
2256
2274
|
}
|
|
2257
|
-
|
|
2275
|
+
// Comments between parts serve as inline documentation — preserve them
|
|
2276
|
+
if (sourceCode.getCommentsInside(node).length > 0) {
|
|
2258
2277
|
return;
|
|
2259
2278
|
}
|
|
2260
2279
|
const parts = collectParts(node);
|
|
2261
|
-
|
|
2262
|
-
if (allLiterals) {
|
|
2263
|
-
context.report({
|
|
2264
|
-
fix(fixer) {
|
|
2265
|
-
return fixer.replaceText(node, buildMergedString(parts));
|
|
2266
|
-
},
|
|
2267
|
-
messageId: 'mergeLiterals',
|
|
2268
|
-
node,
|
|
2269
|
-
});
|
|
2280
|
+
if (!parts.every((p) => p.type !== AST_NODE_TYPES.TemplateLiteral && isStringNode(p))) {
|
|
2270
2281
|
return;
|
|
2271
2282
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2283
|
+
const allLiterals = parts.every(isStringLiteral);
|
|
2284
|
+
// Direct child of a template expression → inline parts to avoid
|
|
2285
|
+
// nested template literals like `${`${a}${b}`}`
|
|
2286
|
+
if (node.parent.type === AST_NODE_TYPES.TemplateLiteral) {
|
|
2287
|
+
const template = node.parent;
|
|
2288
|
+
// Tagged templates: changing quasis/expressions count alters behaviour
|
|
2289
|
+
if (template.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
|
|
2290
|
+
return;
|
|
2291
|
+
}
|
|
2274
2292
|
context.report({
|
|
2275
|
-
fix(fixer) {
|
|
2276
|
-
|
|
2277
|
-
},
|
|
2278
|
-
messageId: 'useTemplate',
|
|
2293
|
+
fix: (fixer) => fixer.replaceText(template, `\`${templateContent(template, (expr) => (expr === node ? partsToTemplateContent(parts, getText) : wrapExpr(expr)))}\``),
|
|
2294
|
+
messageId: allLiterals ? 'mergeLiterals' : 'useTemplate',
|
|
2279
2295
|
node,
|
|
2280
2296
|
});
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
// Nested inside a template but not direct child — would produce
|
|
2300
|
+
// `${`${a}${b}`.method()}`, so skip
|
|
2301
|
+
if (hasTemplateLiteralAncestor(node)) {
|
|
2302
|
+
return;
|
|
2281
2303
|
}
|
|
2304
|
+
context.report({
|
|
2305
|
+
fix: (fixer) => fixer.replaceText(node, allLiterals
|
|
2306
|
+
? buildMergedString(parts)
|
|
2307
|
+
: `\`${partsToTemplateContent(parts, getText)}\``),
|
|
2308
|
+
messageId: allLiterals ? 'mergeLiterals' : 'useTemplate',
|
|
2309
|
+
node,
|
|
2310
|
+
});
|
|
2311
|
+
},
|
|
2312
|
+
TemplateLiteral(node) {
|
|
2313
|
+
// Tagged templates: changing quasis/expressions count alters behaviour
|
|
2314
|
+
if (node.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
node.expressions.forEach((expr, i) => {
|
|
2318
|
+
if (expr.type === AST_NODE_TYPES.TemplateLiteral &&
|
|
2319
|
+
expr.parent.type !== AST_NODE_TYPES.TaggedTemplateExpression) {
|
|
2320
|
+
context.report({
|
|
2321
|
+
fix: (fixer) => fixer.replaceText(node, `\`${templateContent(node, (e, j) => (j === i ? templateContent(expr, wrapExpr) : wrapExpr(e)))}\``),
|
|
2322
|
+
messageId: 'flattenTemplate',
|
|
2323
|
+
node: expr,
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
});
|
|
2282
2327
|
},
|
|
2283
2328
|
};
|
|
2284
2329
|
},
|
|
@@ -2288,6 +2333,7 @@ const rule$4 = createRule$6({
|
|
|
2288
2333
|
},
|
|
2289
2334
|
fixable: 'code',
|
|
2290
2335
|
messages: {
|
|
2336
|
+
flattenTemplate: 'Flatten nested template literal into its parent.',
|
|
2291
2337
|
mergeLiterals: 'Merge string literals instead of concatenating them.',
|
|
2292
2338
|
useTemplate: 'Use a template literal instead of string concatenation.',
|
|
2293
2339
|
},
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
-
type MessageId = 'mergeLiterals' | 'useTemplate';
|
|
2
|
+
type MessageId = 'flattenTemplate' | 'mergeLiterals' | 'useTemplate';
|
|
3
3
|
export declare const rule: ESLintUtils.RuleModule<MessageId, [], unknown, ESLintUtils.RuleListener> & {
|
|
4
4
|
name: string;
|
|
5
5
|
};
|