@taiga-ui/eslint-plugin-experience-next 0.450.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 CHANGED
@@ -49,6 +49,7 @@ export default [
49
49
  | no-href-with-router-link | Do not use href and routerLink attributes together on the same element | | 🔧 | |
50
50
  | no-implicit-public | Require explicit `public` modifier for class members and parameter properties | ✅ | 🔧 | |
51
51
  | no-playwright-empty-fill | Enforce `clear()` over `fill('')` in Playwright tests | ✅ | 🔧 | |
52
+ | no-string-literal-concat | Disallow string literal concatenation; merge adjacent literals into one | ✅ | 🔧 | |
52
53
  | object-single-line | Enforce single-line formatting for single-property objects when it fits `printWidth` | ✅ | 🔧 | |
53
54
  | prefer-deep-imports | Allow deep imports of Taiga UI packages | | 🔧 | |
54
55
  | prefer-multi-arg-push | Combine consecutive `.push()` calls on the same array into a single multi-argument call | ✅ | 🔧 | |
@@ -283,6 +284,93 @@ await page.getByLabel('Name').clear();
283
284
 
284
285
  ---
285
286
 
287
+ ## no-string-literal-concat
288
+
289
+ Disallows concatenating string literals with `+`. Adjacent string literals are always mergeable into one — splitting
290
+ them with `+` adds noise without benefit, and multi-line splits are especially easy to miss.
291
+
292
+ Replaces the built-in `no-useless-concat` rule, which only catches same-line concatenation.
293
+
294
+ ```ts
295
+ // ❌ error
296
+ const msg = 'Hello, ' + 'world!';
297
+
298
+ // ✅ after autofix
299
+ const msg = 'Hello, world!';
300
+ ```
301
+
302
+ ```ts
303
+ // ❌ error — also caught across lines
304
+ it(
305
+ 'returns the last day of month when' +
306
+ ' the result month has fewer days',
307
+ () => { ... },
308
+ );
309
+
310
+ // ✅ after autofix
311
+ it('returns the last day of month when the result month has fewer days', () => {
312
+ ...
313
+ });
314
+ ```
315
+
316
+ ```ts
317
+ // ❌ error — string variables concatenated with +
318
+ const a = 'hello';
319
+ const b = 'world';
320
+ const c = a + b;
321
+
322
+ // ✅ after autofix
323
+ const c = `${a}${b}`;
324
+ ```
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
+
369
+ > For mixed concatenation (`'prefix' + variable`) use the standard `prefer-template` rule, which is already enabled in
370
+ > `recommended`. Template literals (`` `foo` + `bar` ``) and tagged templates are not flagged by this rule.
371
+
372
+ ---
373
+
286
374
  ## object-single-line
287
375
 
288
376
  Single-property object literals that fit within `printWidth` characters on one line are collapsed to a single line.
package/index.d.ts CHANGED
@@ -18,28 +18,31 @@ declare const plugin: {
18
18
  'flat-exports': import("@typescript-eslint/utils/ts-eslint").RuleModule<"spreadArrays", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
19
19
  name: string;
20
20
  };
21
- 'injection-token-description': import("@typescript-eslint/utils/ts-eslint").RuleModule<"invalid-injection-token-description", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
21
+ 'injection-token-description': import("@typescript-eslint/utils/ts-eslint").RuleModule<"invalid-injection-token-description", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
22
22
  name: string;
23
23
  };
24
- 'no-deep-imports': import("@typescript-eslint/utils/ts-eslint").RuleModule<"no-deep-imports", [{
24
+ 'no-deep-imports': import("@typescript-eslint/utils/ts-eslint").RuleModule<"no-deep-imports", {
25
25
  currentProject: string;
26
26
  deepImport: string;
27
27
  ignoreImports: string[];
28
28
  importDeclaration: string;
29
29
  projectName: string;
30
- }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
30
+ }[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
31
31
  name: string;
32
32
  };
33
- 'no-deep-imports-to-indexed-packages': import("@typescript-eslint/utils/ts-eslint").RuleModule<"deepImport", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
33
+ 'no-deep-imports-to-indexed-packages': import("@typescript-eslint/utils/ts-eslint").RuleModule<"deepImport", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
34
34
  name: string;
35
35
  };
36
36
  'no-href-with-router-link': import("eslint").Rule.RuleModule;
37
- 'no-implicit-public': import("@typescript-eslint/utils/ts-eslint").RuleModule<"implicitPublic", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
37
+ 'no-implicit-public': import("@typescript-eslint/utils/ts-eslint").RuleModule<"implicitPublic", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
38
38
  name: string;
39
39
  };
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<"flattenTemplate" | "mergeLiterals" | "useTemplate", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
44
+ name: string;
45
+ };
43
46
  'object-single-line': import("@typescript-eslint/utils/ts-eslint").RuleModule<"oneLine", [{
44
47
  printWidth: number;
45
48
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
package/index.esm.js CHANGED
@@ -87,23 +87,25 @@ var htmlEslint = defineConfig([
87
87
  String.raw `\[style\.border-top(\.[a-z]+)?\]`,
88
88
  String.raw `\[style\.border-bottom(\.[a-z]+)?\]`,
89
89
  ],
90
- message: 'Use logical CSS properties instead of directional properties. Replace:\n' +
91
- '• left inset-inline-start\n' +
92
- 'right → inset-inline-end\n' +
93
- 'top → inset-block-start\n' +
94
- 'bottom → inset-block-end\n' +
95
- 'margin-leftmargin-inline-start\n' +
96
- '• margin-right → margin-inline-end\n' +
97
- '• margin-top → margin-block-start\n' +
98
- '• margin-bottom → margin-block-end\n' +
99
- 'padding-leftpadding-inline-start\n' +
100
- '• padding-right → padding-inline-end\n' +
101
- '• padding-top → padding-block-start\n' +
102
- '• padding-bottom → padding-block-end\n' +
103
- 'border-leftborder-inline-start\n' +
104
- '• border-right → border-inline-end\n' +
105
- '• border-top → border-block-start\n' +
106
- '• border-bottom → border-block-end',
90
+ message: `
91
+ Use logical CSS properties instead of directional properties. Replace:
92
+ left → inset-inline-start
93
+ right → inset-inline-end
94
+ top → inset-block-start
95
+ bottominset-block-end
96
+ • margin-left → margin-inline-start
97
+ • margin-right → margin-inline-end
98
+ • margin-top → margin-block-start
99
+ margin-bottommargin-block-end
100
+ • padding-left → padding-inline-start
101
+ • padding-right → padding-inline-end
102
+ • padding-top → padding-block-start
103
+ padding-bottompadding-block-end
104
+ • border-left → border-inline-start
105
+ • border-right → border-inline-end
106
+ • border-top → border-block-start
107
+ • border-bottom → border-block-end
108
+ `,
107
109
  tagPatterns: ['.*'],
108
110
  },
109
111
  ],
@@ -346,6 +348,7 @@ var recommended = defineConfig([
346
348
  ],
347
349
  '@stylistic/quotes': ['error', 'single', { avoidEscape: true }],
348
350
  '@stylistic/type-annotation-spacing': 'error',
351
+ '@taiga-ui/experience-next/no-string-literal-concat': 'error',
349
352
  '@typescript-eslint/adjacent-overload-signatures': 'off',
350
353
  '@typescript-eslint/array-type': [
351
354
  'error',
@@ -706,7 +709,6 @@ var recommended = defineConfig([
706
709
  ],
707
710
  'no-return-assign': ['error', 'always'],
708
711
  'no-unneeded-ternary': 'error',
709
- 'no-useless-concat': 'error',
710
712
  'no-useless-escape': 'error',
711
713
  'no-useless-rename': [
712
714
  'error',
@@ -777,6 +779,7 @@ var recommended = defineConfig([
777
779
  'sonarjs/no-identical-functions': 'error',
778
780
  'sonarjs/no-inverted-boolean-check': 'error',
779
781
  'spaced-comment': ['error', 'always', { markers: ['/'] }],
782
+ 'template-curly-spacing': ['error', 'never'],
780
783
  'unicorn/consistent-empty-array-spread': 'error',
781
784
  'unicorn/escape-case': 'error',
782
785
  'unicorn/filename-case': ['error', { case: 'kebabCase' }],
@@ -907,6 +910,7 @@ var recommended = defineConfig([
907
910
  'error',
908
911
  { decorators: ['Component', 'Directive', 'NgModule', 'Pipe'] },
909
912
  ],
913
+ 'no-multi-str': 'error',
910
914
  'rxjs/no-compat': 'error',
911
915
  'rxjs/no-connectable': 'error',
912
916
  'rxjs/no-cyclic-action': 'error',
@@ -1313,8 +1317,8 @@ function intersect(a, b) {
1313
1317
  return a.some((type) => origin.has(type));
1314
1318
  }
1315
1319
 
1316
- const createRule$c = ESLintUtils.RuleCreator((name) => name);
1317
- var classPropertyNaming = createRule$c({
1320
+ const createRule$d = ESLintUtils.RuleCreator((name) => name);
1321
+ var classPropertyNaming = createRule$d({
1318
1322
  create(context, [configs]) {
1319
1323
  const parserServices = ESLintUtils.getParserServices(context);
1320
1324
  const typeChecker = parserServices.program.getTypeChecker();
@@ -1347,8 +1351,8 @@ var classPropertyNaming = createRule$c({
1347
1351
  },
1348
1352
  };
1349
1353
  },
1350
- defaultOptions: [[]],
1351
1354
  meta: {
1355
+ defaultOptions: [[]],
1352
1356
  docs: {
1353
1357
  description: 'Enforce custom naming for class properties based on their type.',
1354
1358
  },
@@ -1483,9 +1487,9 @@ function isExternalPureTuple(typeChecker, type) {
1483
1487
  return typeArgs.every((item) => isClassType(item));
1484
1488
  }
1485
1489
 
1486
- const createRule$b = ESLintUtils.RuleCreator((name) => name);
1490
+ const createRule$c = ESLintUtils.RuleCreator((name) => name);
1487
1491
  const MESSAGE_ID$5 = 'spreadArrays';
1488
- var flatExports = createRule$b({
1492
+ var flatExports = createRule$c({
1489
1493
  create(context) {
1490
1494
  const parserServices = ESLintUtils.getParserServices(context);
1491
1495
  const typeChecker = parserServices.program.getTypeChecker();
@@ -1595,7 +1599,6 @@ var flatExports = createRule$b({
1595
1599
  },
1596
1600
  };
1597
1601
  },
1598
- defaultOptions: [],
1599
1602
  meta: {
1600
1603
  docs: {
1601
1604
  description: 'Ensure exported const tuples contain spread arrays in pure entity chains.',
@@ -1612,8 +1615,8 @@ var flatExports = createRule$b({
1612
1615
 
1613
1616
  const MESSAGE_ID$4 = 'invalid-injection-token-description';
1614
1617
  const ERROR_MESSAGE$3 = "InjectionToken's description should contain token's name";
1615
- const createRule$a = ESLintUtils.RuleCreator((name) => name);
1616
- const rule$7 = createRule$a({
1618
+ const createRule$b = ESLintUtils.RuleCreator((name) => name);
1619
+ const rule$8 = createRule$b({
1617
1620
  create(context) {
1618
1621
  return {
1619
1622
  'NewExpression[callee.name="InjectionToken"]'(node) {
@@ -1651,7 +1654,6 @@ const rule$7 = createRule$a({
1651
1654
  },
1652
1655
  };
1653
1656
  },
1654
- defaultOptions: [],
1655
1657
  meta: {
1656
1658
  docs: { description: ERROR_MESSAGE$3 },
1657
1659
  fixable: 'code',
@@ -1681,8 +1683,8 @@ const DEFAULT_OPTIONS = {
1681
1683
  importDeclaration: '^@taiga-ui*',
1682
1684
  projectName: String.raw `(?<=^@taiga-ui/)([-\w]+)`,
1683
1685
  };
1684
- const createRule$9 = ESLintUtils.RuleCreator((name) => name);
1685
- const rule$6 = createRule$9({
1686
+ const createRule$a = ESLintUtils.RuleCreator((name) => name);
1687
+ const rule$7 = createRule$a({
1686
1688
  create(context) {
1687
1689
  const { currentProject, deepImport, ignoreImports, importDeclaration, projectName, } = { ...DEFAULT_OPTIONS, ...context.options[0] };
1688
1690
  const hasNonCodeExtension = (source) => {
@@ -1730,8 +1732,8 @@ const rule$6 = createRule$9({
1730
1732
  },
1731
1733
  };
1732
1734
  },
1733
- defaultOptions: [DEFAULT_OPTIONS],
1734
1735
  meta: {
1736
+ defaultOptions: [DEFAULT_OPTIONS],
1735
1737
  docs: { description: ERROR_MESSAGE$2 },
1736
1738
  fixable: 'code',
1737
1739
  messages: { [MESSAGE_ID$3]: ERROR_MESSAGE$2 },
@@ -1769,13 +1771,13 @@ const rule$6 = createRule$9({
1769
1771
  name: 'no-deep-imports',
1770
1772
  });
1771
1773
 
1772
- const createRule$8 = ESLintUtils.RuleCreator((name) => name);
1774
+ const createRule$9 = ESLintUtils.RuleCreator((name) => name);
1773
1775
  const resolveCacheByOptions = new WeakMap();
1774
1776
  const nearestFileUpCache = new Map();
1775
1777
  const markerCache = new Map();
1776
1778
  const indexFileCache = new Map();
1777
1779
  const indexExportsCache = new Map();
1778
- var noDeepImportsToIndexedPackages = createRule$8({
1780
+ var noDeepImportsToIndexedPackages = createRule$9({
1779
1781
  create(context) {
1780
1782
  const parserServices = ESLintUtils.getParserServices(context);
1781
1783
  const program = parserServices.program;
@@ -1923,7 +1925,6 @@ var noDeepImportsToIndexedPackages = createRule$8({
1923
1925
  },
1924
1926
  };
1925
1927
  },
1926
- defaultOptions: [],
1927
1928
  meta: {
1928
1929
  docs: {
1929
1930
  description: 'Disallow deep imports only when package root index.ts (or index.d.ts) re-exports that subpath, and the package is marked by ng-package.json or package.json',
@@ -2018,8 +2019,8 @@ const config = {
2018
2019
  },
2019
2020
  };
2020
2021
 
2021
- const createRule$7 = ESLintUtils.RuleCreator((name) => name);
2022
- const rule$5 = createRule$7({
2022
+ const createRule$8 = ESLintUtils.RuleCreator((name) => name);
2023
+ const rule$6 = createRule$8({
2023
2024
  create(context) {
2024
2025
  const checkImplicitPublic = (node) => {
2025
2026
  const classRef = getClass(node);
@@ -2069,7 +2070,6 @@ const rule$5 = createRule$7({
2069
2070
  },
2070
2071
  };
2071
2072
  },
2072
- defaultOptions: [],
2073
2073
  meta: {
2074
2074
  docs: {
2075
2075
  description: 'Require explicit `public` modifier for class members and parameter properties',
@@ -2091,8 +2091,8 @@ function getClass(node) {
2091
2091
  return getClass(node.parent);
2092
2092
  }
2093
2093
 
2094
- const createRule$6 = ESLintUtils.RuleCreator((name) => name);
2095
- const rule$4 = createRule$6({
2094
+ const createRule$7 = ESLintUtils.RuleCreator((name) => name);
2095
+ const rule$5 = createRule$7({
2096
2096
  create(context) {
2097
2097
  const services = ESLintUtils.getParserServices(context);
2098
2098
  const checker = services.program.getTypeChecker();
@@ -2129,7 +2129,6 @@ const rule$4 = createRule$6({
2129
2129
  },
2130
2130
  };
2131
2131
  },
2132
- defaultOptions: [],
2133
2132
  meta: {
2134
2133
  docs: {
2135
2134
  description: "Enforce Playwright clear() instead of fill('') for emptying fields",
@@ -2180,6 +2179,170 @@ function isPlaywrightLocatorType(type) {
2180
2179
  });
2181
2180
  }
2182
2181
 
2182
+ const createRule$6 = ESLintUtils.RuleCreator((name) => name);
2183
+ function isStringLiteral(node) {
2184
+ return (node.type === AST_NODE_TYPES.Literal &&
2185
+ typeof node.value === 'string');
2186
+ }
2187
+ function collectParts(node) {
2188
+ if (node.type === AST_NODE_TYPES.BinaryExpression && node.operator === '+') {
2189
+ return [...collectParts(node.left), ...collectParts(node.right)];
2190
+ }
2191
+ return [node];
2192
+ }
2193
+ function isRootConcat(node) {
2194
+ const { parent } = node;
2195
+ return (parent.type !== AST_NODE_TYPES.BinaryExpression ||
2196
+ parent.operator !== '+');
2197
+ }
2198
+ function isStringType(type, checker) {
2199
+ if (type.isUnion()) {
2200
+ return type.types.every((t) => isStringType(t, checker));
2201
+ }
2202
+ return type.isStringLiteral() || checker.typeToString(type) === 'string';
2203
+ }
2204
+ function buildMergedString(parts) {
2205
+ const combined = parts.map((p) => p.value).join('');
2206
+ const quote = combined.includes("'") && !combined.includes('"') ? '"' : "'";
2207
+ const escaped = combined
2208
+ .replaceAll('\\', '\\\\')
2209
+ .replaceAll('\r', String.raw `\r`)
2210
+ .replaceAll('\n', String.raw `\n`)
2211
+ .replaceAll('\t', String.raw `\t`)
2212
+ .replaceAll(new RegExp(quote, 'g'), `\\${quote}`);
2213
+ return `${quote}${escaped}${quote}`;
2214
+ }
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
+ : ''}`)
2234
+ .join('');
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;
2245
+ }
2246
+ const rule$4 = createRule$6({
2247
+ create(context) {
2248
+ const { sourceCode } = context;
2249
+ let parserServices = null;
2250
+ let checker = null;
2251
+ try {
2252
+ parserServices = ESLintUtils.getParserServices(context);
2253
+ checker = parserServices.program.getTypeChecker();
2254
+ }
2255
+ catch {
2256
+ // Type checking not available — only literal concatenation will be checked
2257
+ }
2258
+ function isStringNode(node) {
2259
+ if (isStringLiteral(node)) {
2260
+ return true;
2261
+ }
2262
+ if (!parserServices || !checker) {
2263
+ return false;
2264
+ }
2265
+ const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
2266
+ return isStringType(checker.getTypeAtLocation(tsNode), checker);
2267
+ }
2268
+ const getText = (n) => sourceCode.getText(n);
2269
+ const wrapExpr = (expr) => `\${${getText(expr)}}`;
2270
+ return {
2271
+ BinaryExpression(node) {
2272
+ if (node.operator !== '+' || !isRootConcat(node)) {
2273
+ return;
2274
+ }
2275
+ // Comments between parts serve as inline documentation — preserve them
2276
+ if (sourceCode.getCommentsInside(node).length > 0) {
2277
+ return;
2278
+ }
2279
+ const parts = collectParts(node);
2280
+ if (!parts.every((p) => p.type !== AST_NODE_TYPES.TemplateLiteral && isStringNode(p))) {
2281
+ return;
2282
+ }
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
+ }
2292
+ context.report({
2293
+ fix: (fixer) => fixer.replaceText(template, `\`${templateContent(template, (expr) => (expr === node ? partsToTemplateContent(parts, getText) : wrapExpr(expr)))}\``),
2294
+ messageId: allLiterals ? 'mergeLiterals' : 'useTemplate',
2295
+ node,
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;
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
+ });
2327
+ },
2328
+ };
2329
+ },
2330
+ meta: {
2331
+ docs: {
2332
+ description: 'Disallow string concatenation. Merge adjacent string literals into one; use template literals for string variables.',
2333
+ },
2334
+ fixable: 'code',
2335
+ messages: {
2336
+ flattenTemplate: 'Flatten nested template literal into its parent.',
2337
+ mergeLiterals: 'Merge string literals instead of concatenating them.',
2338
+ useTemplate: 'Use a template literal instead of string concatenation.',
2339
+ },
2340
+ schema: [],
2341
+ type: 'suggestion',
2342
+ },
2343
+ name: 'no-string-literal-concat',
2344
+ });
2345
+
2183
2346
  const createRule$5 = ESLintUtils.RuleCreator((name) => name);
2184
2347
  const rule$3 = createRule$5({
2185
2348
  create(context, [{ printWidth }]) {
@@ -2440,8 +2603,8 @@ const rule$3 = createRule$5({
2440
2603
  },
2441
2604
  };
2442
2605
  },
2443
- defaultOptions: [{ printWidth: 90 }],
2444
2606
  meta: {
2607
+ defaultOptions: [{ printWidth: 90 }],
2445
2608
  docs: {
2446
2609
  description: 'Enforce single-line formatting for single-property objects when possible (Prettier-friendly)',
2447
2610
  },
@@ -2526,13 +2689,13 @@ var preferDeepImports = createRule$4({
2526
2689
  },
2527
2690
  };
2528
2691
  },
2529
- defaultOptions: [
2530
- {
2531
- importFilter: [],
2532
- strict: false,
2533
- },
2534
- ],
2535
2692
  meta: {
2693
+ defaultOptions: [
2694
+ {
2695
+ importFilter: [],
2696
+ strict: false,
2697
+ },
2698
+ ],
2536
2699
  docs: { description: ERROR_MESSAGE },
2537
2700
  fixable: 'code',
2538
2701
  messages: { [MESSAGE_ID$1]: ERROR_MESSAGE },
@@ -2889,7 +3052,6 @@ const rule$2 = createRule$3({
2889
3052
  },
2890
3053
  };
2891
3054
  },
2892
- defaultOptions: [],
2893
3055
  meta: {
2894
3056
  docs: {
2895
3057
  description: 'Enforce combining consecutive .push() calls on the same array into a single call.',
@@ -3041,13 +3203,13 @@ const rule$1 = createRule$2({
3041
3203
  },
3042
3204
  };
3043
3205
  },
3044
- defaultOptions: [
3045
- {
3046
- decorators: DEFAULT_DECORATORS,
3047
- exceptions: DEFAULT_EXCEPTIONS,
3048
- },
3049
- ],
3050
3206
  meta: {
3207
+ defaultOptions: [
3208
+ {
3209
+ decorators: DEFAULT_DECORATORS,
3210
+ exceptions: DEFAULT_EXCEPTIONS,
3211
+ },
3212
+ ],
3051
3213
  docs: {
3052
3214
  description: 'Shorten TuiXxxComponent / TuiYyyDirective in Angular metadata (supports configurable decorators and exceptions).',
3053
3215
  },
@@ -3244,8 +3406,8 @@ var standaloneImportsSort = createRule$1({
3244
3406
  },
3245
3407
  };
3246
3408
  },
3247
- defaultOptions: [{ decorators: ['Component', 'Directive', 'NgModule', 'Pipe'] }],
3248
3409
  meta: {
3410
+ defaultOptions: [{ decorators: ['Component', 'Directive', 'NgModule', 'Pipe'] }],
3249
3411
  docs: { description: 'Sort Angular standalone imports inside decorators.' },
3250
3412
  fixable: 'code',
3251
3413
  messages: { incorrectOrder: 'Order in imports should be [{{expected}}]' },
@@ -3351,7 +3513,6 @@ const rule = createRule({
3351
3513
  },
3352
3514
  };
3353
3515
  },
3354
- defaultOptions: [],
3355
3516
  meta: {
3356
3517
  docs: {
3357
3518
  description: `Ensures that keys and values are valid in a ${DOC_EXAMPLE_INTERFACE_NAME} interface.`,
@@ -3379,12 +3540,13 @@ const plugin = {
3379
3540
  'class-property-naming': classPropertyNaming,
3380
3541
  'decorator-key-sort': config$1,
3381
3542
  'flat-exports': flatExports,
3382
- 'injection-token-description': rule$7,
3383
- 'no-deep-imports': rule$6,
3543
+ 'injection-token-description': rule$8,
3544
+ 'no-deep-imports': rule$7,
3384
3545
  'no-deep-imports-to-indexed-packages': noDeepImportsToIndexedPackages,
3385
3546
  'no-href-with-router-link': config,
3386
- 'no-implicit-public': rule$5,
3387
- 'no-playwright-empty-fill': rule$4,
3547
+ 'no-implicit-public': rule$6,
3548
+ 'no-playwright-empty-fill': rule$5,
3549
+ 'no-string-literal-concat': rule$4,
3388
3550
  'object-single-line': rule$3,
3389
3551
  'prefer-deep-imports': preferDeepImports,
3390
3552
  'prefer-multi-arg-push': rule$2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.450.0",
3
+ "version": "0.452.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
- export declare const rule: ESLintUtils.RuleModule<"invalid-injection-token-description", [], unknown, ESLintUtils.RuleListener> & {
2
+ export declare const rule: ESLintUtils.RuleModule<"invalid-injection-token-description", readonly unknown[], unknown, ESLintUtils.RuleListener> & {
3
3
  name: string;
4
4
  };
5
5
  export default rule;
@@ -1,5 +1,5 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
- declare const _default: ESLintUtils.RuleModule<"deepImport", [], unknown, ESLintUtils.RuleListener> & {
2
+ declare const _default: ESLintUtils.RuleModule<"deepImport", readonly unknown[], unknown, ESLintUtils.RuleListener> & {
3
3
  name: string;
4
4
  };
5
5
  export default _default;
@@ -1,11 +1,11 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
- export declare const rule: ESLintUtils.RuleModule<"no-deep-imports", [{
2
+ export declare const rule: ESLintUtils.RuleModule<"no-deep-imports", {
3
3
  currentProject: string;
4
4
  deepImport: string;
5
5
  ignoreImports: string[];
6
6
  importDeclaration: string;
7
7
  projectName: string;
8
- }], unknown, ESLintUtils.RuleListener> & {
8
+ }[], unknown, ESLintUtils.RuleListener> & {
9
9
  name: string;
10
10
  };
11
11
  export default rule;
@@ -1,5 +1,5 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
- export declare const rule: ESLintUtils.RuleModule<"implicitPublic", [], unknown, ESLintUtils.RuleListener> & {
2
+ export declare const rule: ESLintUtils.RuleModule<"implicitPublic", readonly unknown[], unknown, ESLintUtils.RuleListener> & {
3
3
  name: string;
4
4
  };
5
5
  export default rule;
@@ -0,0 +1,6 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ type MessageId = 'flattenTemplate' | 'mergeLiterals' | 'useTemplate';
3
+ export declare const rule: ESLintUtils.RuleModule<MessageId, [], unknown, ESLintUtils.RuleListener> & {
4
+ name: string;
5
+ };
6
+ export default rule;