@taiga-ui/eslint-plugin-experience-next 0.451.0 → 0.453.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 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
@@ -418,7 +418,7 @@ var recommended = defineConfig([
418
418
  '@typescript-eslint/no-base-to-string': 'off',
419
419
  '@typescript-eslint/no-confusing-non-null-assertion': 'error',
420
420
  '@typescript-eslint/no-confusing-void-expression': 'off',
421
- '@typescript-eslint/no-deprecated': 'off',
421
+ '@typescript-eslint/no-deprecated': 'off', // TODO: should be error?
422
422
  '@typescript-eslint/no-duplicate-enum-values': 'error',
423
423
  '@typescript-eslint/no-duplicate-type-constituents': 'error',
424
424
  '@typescript-eslint/no-empty-function': [
@@ -435,7 +435,7 @@ var recommended = defineConfig([
435
435
  },
436
436
  ],
437
437
  '@typescript-eslint/no-empty-object-type': 'error',
438
- '@typescript-eslint/no-explicit-any': 'off',
438
+ '@typescript-eslint/no-explicit-any': 'off', // TODO: should be error?
439
439
  '@typescript-eslint/no-extra-non-null-assertion': 'error',
440
440
  '@typescript-eslint/no-extraneous-class': [
441
441
  'error',
@@ -446,14 +446,15 @@ var recommended = defineConfig([
446
446
  allowWithDecorator: true,
447
447
  },
448
448
  ],
449
- '@typescript-eslint/no-floating-promises': 'off',
449
+ '@typescript-eslint/no-floating-promises': 'off', // TODO: should be error, just void before Promise
450
450
  '@typescript-eslint/no-for-in-array': 'error',
451
451
  '@typescript-eslint/no-implied-eval': 'error',
452
452
  '@typescript-eslint/no-import-type-side-effects': 'off', // verbatimModuleSyntax should be false
453
453
  '@typescript-eslint/no-inferrable-types': 'error',
454
454
  '@typescript-eslint/no-invalid-void-type': 'off',
455
+ '@typescript-eslint/no-loop-func': 'off', // TODO: should be error?
455
456
  '@typescript-eslint/no-magic-numbers': 'off',
456
- '@typescript-eslint/no-misused-promises': 'off',
457
+ '@typescript-eslint/no-misused-promises': 'off', // TODO: ['error', {checksVoidReturn: {attributes: false}}],
457
458
  '@typescript-eslint/no-misused-spread': 'off',
458
459
  '@typescript-eslint/no-namespace': ['error', { allowDeclarations: true }],
459
460
  '@typescript-eslint/no-non-null-asserted-nullish-coalescing': 'error',
@@ -491,22 +492,22 @@ var recommended = defineConfig([
491
492
  },
492
493
  },
493
494
  ],
494
- '@typescript-eslint/no-shadow': 'off',
495
+ '@typescript-eslint/no-shadow': 'off', // TODO: should be error?
495
496
  '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'error',
496
497
  '@typescript-eslint/no-unnecessary-condition': 'error',
497
498
  '@typescript-eslint/no-unnecessary-qualifier': 'error',
498
- '@typescript-eslint/no-unnecessary-template-expression': 'off',
499
+ '@typescript-eslint/no-unnecessary-template-expression': 'error',
499
500
  '@typescript-eslint/no-unnecessary-type-arguments': 'error',
500
501
  '@typescript-eslint/no-unnecessary-type-assertion': 'error',
501
502
  '@typescript-eslint/no-unnecessary-type-constraint': 'error',
502
- '@typescript-eslint/no-unnecessary-type-parameters': 'off',
503
- '@typescript-eslint/no-unsafe-argument': 'off',
504
- '@typescript-eslint/no-unsafe-assignment': 'off',
505
- '@typescript-eslint/no-unsafe-call': 'off',
503
+ '@typescript-eslint/no-unnecessary-type-parameters': 'off', // TODO: should be error?
504
+ '@typescript-eslint/no-unsafe-argument': 'off', // TODO: should be error?
505
+ '@typescript-eslint/no-unsafe-assignment': 'off', // TODO: should be error?
506
+ '@typescript-eslint/no-unsafe-call': 'off', // TODO: should be error?
506
507
  '@typescript-eslint/no-unsafe-declaration-merging': 'error',
507
- '@typescript-eslint/no-unsafe-member-access': 'off',
508
- '@typescript-eslint/no-unsafe-return': 'off',
509
- '@typescript-eslint/no-unsafe-type-assertion': 'off',
508
+ '@typescript-eslint/no-unsafe-member-access': 'off', // TODO: should be error?
509
+ '@typescript-eslint/no-unsafe-return': 'off', // TODO: should be error?
510
+ '@typescript-eslint/no-unsafe-type-assertion': 'off', // TODO: should be error?
510
511
  '@typescript-eslint/no-unused-expressions': [
511
512
  'error',
512
513
  {
@@ -540,7 +541,7 @@ var recommended = defineConfig([
540
541
  '@typescript-eslint/prefer-find': 'error',
541
542
  '@typescript-eslint/prefer-for-of': 'error',
542
543
  '@typescript-eslint/prefer-includes': 'error',
543
- '@typescript-eslint/prefer-nullish-coalescing': 'off',
544
+ '@typescript-eslint/prefer-nullish-coalescing': 'off', // TODO: ['error', {ignorePrimitives: {boolean: true, number: true, string: true}}]
544
545
  '@typescript-eslint/prefer-optional-chain': 'error',
545
546
  '@typescript-eslint/prefer-readonly': ['error'],
546
547
  '@typescript-eslint/prefer-readonly-parameter-types': 'off',
@@ -559,9 +560,9 @@ var recommended = defineConfig([
559
560
  '@typescript-eslint/require-array-sort-compare': 'error',
560
561
  '@typescript-eslint/require-await': 'error',
561
562
  '@typescript-eslint/restrict-plus-operands': 'error',
562
- '@typescript-eslint/restrict-template-expressions': 'off',
563
+ '@typescript-eslint/restrict-template-expressions': 'off', // TODO: should be error?
563
564
  '@typescript-eslint/sort-type-constituents': 'error',
564
- '@typescript-eslint/strict-boolean-expressions': 'off',
565
+ '@typescript-eslint/strict-boolean-expressions': 'off', // TODO: should be error?
565
566
  '@typescript-eslint/switch-exhaustiveness-check': [
566
567
  'error',
567
568
  {
@@ -601,6 +602,7 @@ var recommended = defineConfig([
601
602
  'import/no-absolute-path': 'error',
602
603
  'import/no-cycle': 'error',
603
604
  'import/no-duplicates': ['error', { 'prefer-inline': true }],
605
+ 'import/no-extraneous-dependencies': 'off',
604
606
  'import/no-mutable-exports': 'error',
605
607
  'import/no-named-as-default': 'error',
606
608
  'import/no-self-import': 'error',
@@ -643,7 +645,7 @@ var recommended = defineConfig([
643
645
  skipTemplates: false,
644
646
  },
645
647
  ],
646
- 'no-loop-func': 'error',
648
+ 'no-loop-func': 'off',
647
649
  'no-nested-ternary': 'error',
648
650
  'no-prototype-builtins': 'off',
649
651
  'no-restricted-imports': [
@@ -708,6 +710,7 @@ var recommended = defineConfig([
708
710
  },
709
711
  ],
710
712
  'no-return-assign': ['error', 'always'],
713
+ 'no-shadow': 'off',
711
714
  'no-unneeded-ternary': 'error',
712
715
  'no-useless-escape': 'error',
713
716
  'no-useless-rename': [
@@ -765,7 +768,8 @@ var recommended = defineConfig([
765
768
  'promise/always-return': 'off',
766
769
  'promise/catch-or-return': 'off',
767
770
  'promise/no-callback-in-promise': 'off',
768
- 'promise/no-nesting': 'off',
771
+ 'promise/no-nesting': 'off', // TODO: should be error?
772
+ 'promise/no-return-in-finally': 'error',
769
773
  'promise/param-names': 'error',
770
774
  'regexp/no-unused-capturing-group': [
771
775
  'error',
@@ -776,6 +780,8 @@ var recommended = defineConfig([
776
780
  ],
777
781
  'simple-import-sort/exports': 'error',
778
782
  'simple-import-sort/imports': 'error',
783
+ 'sonarjs/cognitive-complexity': 'off', // TODO: should be error? ['error', 15]
784
+ 'sonarjs/no-duplicate-string': 'off', // TODO: should be error? ['error', {threshold: 5}]
779
785
  'sonarjs/no-identical-functions': 'error',
780
786
  'sonarjs/no-inverted-boolean-check': 'error',
781
787
  'spaced-comment': ['error', 'always', { markers: ['/'] }],
@@ -784,18 +790,25 @@ var recommended = defineConfig([
784
790
  'unicorn/escape-case': 'error',
785
791
  'unicorn/filename-case': ['error', { case: 'kebabCase' }],
786
792
  'unicorn/new-for-builtins': 'error',
793
+ 'unicorn/no-array-for-each': 'off', // TODO: should be error
787
794
  'unicorn/no-array-method-this-argument': 'error',
788
795
  'unicorn/no-array-push-push': 'error',
789
796
  'unicorn/no-await-in-promise-methods': 'error',
790
797
  'unicorn/no-empty-file': 'error',
791
798
  'unicorn/no-magic-array-flat-depth': 'error',
799
+ 'unicorn/no-negated-condition': 'error',
792
800
  'unicorn/no-negation-in-equality-check': 'error',
793
801
  'unicorn/no-new-array': 'error',
802
+ 'unicorn/no-object-as-default-parameter': 'off',
794
803
  'unicorn/no-single-promise-in-promise-methods': 'error',
795
804
  'unicorn/no-typeof-undefined': 'error',
796
805
  'unicorn/no-unnecessary-polyfills': 'error',
797
806
  'unicorn/no-useless-spread': 'error',
807
+ 'unicorn/no-useless-undefined': 'error',
808
+ 'unicorn/prefer-dom-node-append': 'off', // TODO: should be error?
798
809
  'unicorn/prefer-logical-operator-over-ternary': 'error',
810
+ 'unicorn/prefer-number-properties': 'error',
811
+ 'unicorn/prefer-prototype-methods': 'error',
799
812
  'unicorn/prefer-query-selector': 'error',
800
813
  'unicorn/prefer-set-size': 'error',
801
814
  'unicorn/prefer-string-raw': 'error',
@@ -1021,7 +1034,6 @@ var recommended = defineConfig([
1021
1034
  extends: [jest.configs['flat/recommended']],
1022
1035
  rules: {
1023
1036
  '@typescript-eslint/no-extraneous-class': 'off',
1024
- '@typescript-eslint/no-shadow': 'off',
1025
1037
  'compat/compat': 'off',
1026
1038
  'jest/expect-expect': 'off',
1027
1039
  'jest/max-expects': 'off',
@@ -1078,7 +1090,7 @@ var recommended = defineConfig([
1078
1090
  { maxNumberOfTopLevelDescribes: 1 },
1079
1091
  ],
1080
1092
  'jest/unbound-method': 'off',
1081
- 'jest/valid-title': 'error',
1093
+ 'jest/valid-title': 'off',
1082
1094
  },
1083
1095
  },
1084
1096
  {
@@ -1301,7 +1313,7 @@ function getFieldTypes(type, checker) {
1301
1313
  typeNames.push('string');
1302
1314
  }
1303
1315
  else {
1304
- const symbol = type.getSymbol() || type.aliasSymbol;
1316
+ const symbol = type.getSymbol() ?? type.aliasSymbol;
1305
1317
  if (symbol) {
1306
1318
  typeNames.push(symbol.getName());
1307
1319
  }
@@ -1391,7 +1403,7 @@ const config$1 = {
1391
1403
  return {
1392
1404
  ClassDeclaration(node) {
1393
1405
  const decorators = Array.from(node.decorators ?? []);
1394
- decorators.forEach((decorator) => {
1406
+ for (const decorator of decorators) {
1395
1407
  const { expression } = decorator;
1396
1408
  const decoratorName = expression.callee?.name ?? '';
1397
1409
  if (decoratorName in (order || {})) {
@@ -1423,7 +1435,7 @@ const config$1 = {
1423
1435
  }
1424
1436
  }
1425
1437
  }
1426
- });
1438
+ }
1427
1439
  },
1428
1440
  };
1429
1441
  },
@@ -1534,20 +1546,20 @@ var flatExports = createRule$c({
1534
1546
  if (!isPureArray(arr)) {
1535
1547
  continue;
1536
1548
  }
1537
- arr.elements.forEach((meta, index) => {
1549
+ for (const [index, meta] of arr.elements.entries()) {
1538
1550
  if (!meta.isArrayLike) {
1539
- return;
1551
+ continue;
1540
1552
  }
1541
1553
  const elementNode = arr.node.elements[index];
1542
1554
  if (elementNode?.type !== AST_NODE_TYPES.Identifier) {
1543
- return;
1555
+ continue;
1544
1556
  }
1545
1557
  const hasLocalArrayMeta = arrays.has(meta.name);
1546
- const isExternalPure = !hasLocalArrayMeta
1547
- ? isExternalPureTuple(typeChecker, meta.type)
1548
- : false;
1558
+ const isExternalPure = hasLocalArrayMeta
1559
+ ? false
1560
+ : isExternalPureTuple(typeChecker, meta.type);
1549
1561
  if (!hasLocalArrayMeta && !isExternalPure) {
1550
- return;
1562
+ continue;
1551
1563
  }
1552
1564
  context.report({
1553
1565
  data: { name: meta.name },
@@ -1557,7 +1569,7 @@ var flatExports = createRule$c({
1557
1569
  messageId: MESSAGE_ID$5,
1558
1570
  node: elementNode,
1559
1571
  });
1560
- });
1572
+ }
1561
1573
  }
1562
1574
  },
1563
1575
  VariableDeclarator(node) {
@@ -1622,7 +1634,7 @@ const rule$8 = createRule$b({
1622
1634
  'NewExpression[callee.name="InjectionToken"]'(node) {
1623
1635
  let token;
1624
1636
  let name;
1625
- const [description] = node?.arguments || [];
1637
+ const [description] = node?.arguments ?? [];
1626
1638
  if (!description) {
1627
1639
  return;
1628
1640
  }
@@ -1701,8 +1713,8 @@ const rule$7 = createRule$a({
1701
1713
  const filePath = path
1702
1714
  .relative(context.cwd, context.filename)
1703
1715
  .replaceAll(/\\+/g, '/');
1704
- const [currentFileProjectName] = (currentProject && new RegExp(currentProject, 'g').exec(filePath)) || [];
1705
- const [importSourceProjectName] = source?.match(new RegExp(projectName, 'g')) || [];
1716
+ const [currentFileProjectName] = (currentProject && new RegExp(currentProject, 'g').exec(filePath)) ?? [];
1717
+ const [importSourceProjectName] = source?.match(new RegExp(projectName, 'g')) ?? [];
1706
1718
  return Boolean(currentFileProjectName &&
1707
1719
  importSourceProjectName &&
1708
1720
  currentFileProjectName === importSourceProjectName);
@@ -1986,7 +1998,7 @@ const config = {
1986
1998
  let routerLinkAttribute = null;
1987
1999
  let hasRouterLink = false;
1988
2000
  let hasHref = false;
1989
- for (const attr of htmlNode.attributes || []) {
2001
+ for (const attr of htmlNode.attributes ?? []) {
1990
2002
  const attrName = attr.key.value;
1991
2003
  if (attrName?.toLowerCase() === 'href') {
1992
2004
  hasHref = true;
@@ -2003,8 +2015,8 @@ const config = {
2003
2015
  ? fixer.removeRange(hrefAttribute.range)
2004
2016
  : null,
2005
2017
  messageId: MESSAGE_ID$2,
2006
- node: (routerLinkAttribute ||
2007
- hrefAttribute ||
2018
+ node: (routerLinkAttribute ??
2019
+ hrefAttribute ??
2008
2020
  htmlNode),
2009
2021
  });
2010
2022
  }
@@ -2180,7 +2192,7 @@ function isPlaywrightLocatorType(type) {
2180
2192
  }
2181
2193
 
2182
2194
  const createRule$6 = ESLintUtils.RuleCreator((name) => name);
2183
- function isStringLiteralNode(node) {
2195
+ function isStringLiteral(node) {
2184
2196
  return (node.type === AST_NODE_TYPES.Literal &&
2185
2197
  typeof node.value === 'string');
2186
2198
  }
@@ -2212,19 +2224,36 @@ function buildMergedString(parts) {
2212
2224
  .replaceAll(new RegExp(quote, 'g'), `\\${quote}`);
2213
2225
  return `${quote}${escaped}${quote}`;
2214
2226
  }
2215
- function buildTemplateLiteral(parts, getText) {
2216
- const inner = parts
2217
- .map((part) => {
2218
- if (isStringLiteralNode(part)) {
2219
- return part.value
2220
- .replaceAll('\\', '\\\\')
2221
- .replaceAll('`', '\\`')
2222
- .replaceAll('${', '\\${');
2223
- }
2224
- return `\${${getText(part)}}`;
2225
- })
2227
+ function escapeForTemplateLiteral(value) {
2228
+ return value.replaceAll('\\', '\\\\').replaceAll('`', '\\`').replaceAll('${', '\\${');
2229
+ }
2230
+ function partsToTemplateContent(parts, getText) {
2231
+ return parts
2232
+ .map((part) => isStringLiteral(part)
2233
+ ? escapeForTemplateLiteral(part.value)
2234
+ : `\${${getText(part)}}`)
2235
+ .join('');
2236
+ }
2237
+ /**
2238
+ * Returns the raw content between the backticks of a TemplateLiteral,
2239
+ * delegating each expression slot to `renderExpr`.
2240
+ */
2241
+ function templateContent(template, renderExpr) {
2242
+ return template.quasis
2243
+ .map((quasi, i) => `${quasi.value.raw}${i < template.expressions.length
2244
+ ? renderExpr(template.expressions[i], i)
2245
+ : ''}`)
2226
2246
  .join('');
2227
- return `\`${inner}\``;
2247
+ }
2248
+ function hasTemplateLiteralAncestor(node) {
2249
+ let current = node.parent;
2250
+ while (current != null) {
2251
+ if (current.type === AST_NODE_TYPES.TemplateLiteral) {
2252
+ return true;
2253
+ }
2254
+ current = current.parent;
2255
+ }
2256
+ return false;
2228
2257
  }
2229
2258
  const rule$4 = createRule$6({
2230
2259
  create(context) {
@@ -2239,45 +2268,73 @@ const rule$4 = createRule$6({
2239
2268
  // Type checking not available — only literal concatenation will be checked
2240
2269
  }
2241
2270
  function isStringNode(node) {
2242
- if (isStringLiteralNode(node)) {
2271
+ if (isStringLiteral(node)) {
2243
2272
  return true;
2244
2273
  }
2245
2274
  if (!parserServices || !checker) {
2246
2275
  return false;
2247
2276
  }
2248
2277
  const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
2249
- const type = checker.getTypeAtLocation(tsNode);
2250
- return isStringType(type, checker);
2278
+ return isStringType(checker.getTypeAtLocation(tsNode), checker);
2251
2279
  }
2280
+ const getText = (n) => sourceCode.getText(n);
2281
+ const wrapExpr = (expr) => `\${${getText(expr)}}`;
2252
2282
  return {
2253
2283
  BinaryExpression(node) {
2254
- if (node.operator !== '+') {
2284
+ if (node.operator !== '+' || !isRootConcat(node)) {
2255
2285
  return;
2256
2286
  }
2257
- if (!isRootConcat(node)) {
2287
+ // Comments between parts serve as inline documentation — preserve them
2288
+ if (sourceCode.getCommentsInside(node).length > 0) {
2258
2289
  return;
2259
2290
  }
2260
2291
  const parts = collectParts(node);
2261
- const allLiterals = parts.every(isStringLiteralNode);
2262
- if (allLiterals) {
2263
- context.report({
2264
- fix(fixer) {
2265
- return fixer.replaceText(node, buildMergedString(parts));
2266
- },
2267
- messageId: 'mergeLiterals',
2268
- node,
2269
- });
2292
+ if (!parts.every((p) => p.type !== AST_NODE_TYPES.TemplateLiteral && isStringNode(p))) {
2270
2293
  return;
2271
2294
  }
2272
- if (parts.every((part) => part.type !== AST_NODE_TYPES.TemplateLiteral &&
2273
- isStringNode(part))) {
2295
+ const allLiterals = parts.every(isStringLiteral);
2296
+ // Direct child of a template expression → inline parts to avoid
2297
+ // nested template literals like `${`${a}${b}`}`
2298
+ if (node.parent.type === AST_NODE_TYPES.TemplateLiteral) {
2299
+ const template = node.parent;
2300
+ // Tagged templates: changing quasis/expressions count alters behaviour
2301
+ if (template.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
2302
+ return;
2303
+ }
2274
2304
  context.report({
2275
- fix(fixer) {
2276
- return fixer.replaceText(node, buildTemplateLiteral(parts, (n) => sourceCode.getText(n)));
2277
- },
2278
- messageId: 'useTemplate',
2305
+ fix: (fixer) => fixer.replaceText(template, `\`${templateContent(template, (expr) => (expr === node ? partsToTemplateContent(parts, getText) : wrapExpr(expr)))}\``),
2306
+ messageId: allLiterals ? 'mergeLiterals' : 'useTemplate',
2279
2307
  node,
2280
2308
  });
2309
+ return;
2310
+ }
2311
+ // Nested inside a template but not direct child — would produce
2312
+ // `${`${a}${b}`.method()}`, so skip
2313
+ if (hasTemplateLiteralAncestor(node)) {
2314
+ return;
2315
+ }
2316
+ context.report({
2317
+ fix: (fixer) => fixer.replaceText(node, allLiterals
2318
+ ? buildMergedString(parts)
2319
+ : `\`${partsToTemplateContent(parts, getText)}\``),
2320
+ messageId: allLiterals ? 'mergeLiterals' : 'useTemplate',
2321
+ node,
2322
+ });
2323
+ },
2324
+ TemplateLiteral(node) {
2325
+ // Tagged templates: changing quasis/expressions count alters behaviour
2326
+ if (node.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
2327
+ return;
2328
+ }
2329
+ for (const [i, expr] of node.expressions.entries()) {
2330
+ if (expr.type === AST_NODE_TYPES.TemplateLiteral &&
2331
+ expr.parent.type !== AST_NODE_TYPES.TaggedTemplateExpression) {
2332
+ context.report({
2333
+ fix: (fixer) => fixer.replaceText(node, `\`${templateContent(node, (e, j) => (j === i ? templateContent(expr, wrapExpr) : wrapExpr(e)))}\``),
2334
+ messageId: 'flattenTemplate',
2335
+ node: expr,
2336
+ });
2337
+ }
2281
2338
  }
2282
2339
  },
2283
2340
  };
@@ -2288,6 +2345,7 @@ const rule$4 = createRule$6({
2288
2345
  },
2289
2346
  fixable: 'code',
2290
2347
  messages: {
2348
+ flattenTemplate: 'Flatten nested template literal into its parent.',
2291
2349
  mergeLiterals: 'Merge string literals instead of concatenating them.',
2292
2350
  useTemplate: 'Use a template literal instead of string concatenation.',
2293
2351
  },
@@ -2336,7 +2394,7 @@ const rule$3 = createRule$5({
2336
2394
  return false;
2337
2395
  }
2338
2396
  const [onlyProperty] = node.properties;
2339
- return !onlyProperty ? false : !isForbiddenProperty(onlyProperty);
2397
+ return onlyProperty ? !isForbiddenProperty(onlyProperty) : false;
2340
2398
  };
2341
2399
  const unwrapExpression = (expression) => {
2342
2400
  let current = expression;
@@ -2975,7 +3033,7 @@ const rule$2 = createRule$3({
2975
3033
  j++;
2976
3034
  }
2977
3035
  if (group.length > 1) {
2978
- group.forEach((groupStmt, idx) => {
3036
+ for (const [idx, groupStmt] of group.entries()) {
2979
3037
  context.report({
2980
3038
  ...(idx === 0
2981
3039
  ? {
@@ -2992,7 +3050,7 @@ const rule$2 = createRule$3({
2992
3050
  messageId: 'preferMultiArgPush',
2993
3051
  node: groupStmt,
2994
3052
  });
2995
- });
3053
+ }
2996
3054
  }
2997
3055
  i = j;
2998
3056
  }
@@ -3087,7 +3145,7 @@ const rule$1 = createRule$2({
3087
3145
  : importedAs.replace(/(?:Component|Directive)$/, '');
3088
3146
  const fullText = sourceCode.getText();
3089
3147
  const regex = new RegExp(String.raw `\b${importedAs}\b`, 'g');
3090
- const usageCount = (fullText.match(regex) || []).length;
3148
+ const usageCount = (fullText.match(regex) ?? []).length;
3091
3149
  const shouldDeleteImport = usageCount <= 2;
3092
3150
  context.report({
3093
3151
  data: { newName: short, oldName: importedAs },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.451.0",
3
+ "version": "0.453.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,9 +31,9 @@
31
31
  "glob": "13.0.6"
32
32
  },
33
33
  "peerDependencies": {
34
- "@eslint/compat": "^2.0.3",
34
+ "@eslint/compat": "^2.0.4",
35
35
  "@eslint/markdown": "^8.0.1",
36
- "@html-eslint/eslint-plugin": "^0.58.1",
36
+ "@html-eslint/eslint-plugin": "^0.59.0",
37
37
  "@html-eslint/parser": "^0.58.1",
38
38
  "@smarttools/eslint-plugin-rxjs": "^1.0.22",
39
39
  "@stylistic/eslint-plugin": "^5.10.0",
@@ -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
  };