@taiga-ui/eslint-plugin-experience-next 0.514.0 → 0.516.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
@@ -96,6 +96,7 @@ from third-party plugins. The exact severities and file globs live in
96
96
  | [no-infinite-loop](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-infinite-loop.md) | Disallow `while (true)` and `for` loops without an explicit condition | ✅ | | |
97
97
  | [no-legacy-peer-deps](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-legacy-peer-deps.md) | Disallow `legacy-peer-deps=true` in `.npmrc` | ✅ | | |
98
98
  | [no-nested-interactive](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-nested-interactive.md) | Disallow interactive HTML elements nested inside other interactive elements | ✅ | | |
99
+ | [no-nested-ternary-in-template](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-nested-ternary-in-template.md) | Disallow nested ternary expressions in Angular 19+ templates | ✅ | 🔧 | |
99
100
  | [no-obsolete-attrs](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-obsolete-attrs.md) | Disallow obsolete HTML attributes | ✅ | | |
100
101
  | [no-obsolete-tags](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-obsolete-tags.md) | Disallow obsolete HTML tags | ✅ | | |
101
102
  | [no-playwright-empty-fill](https://github.com/taiga-family/toolkit/tree/main/projects/eslint-plugin-experience-next/docs/no-playwright-empty-fill.md) | Enforce `clear()` over `fill('')` in Playwright tests | ✅ | 🔧 | |
@@ -4,5 +4,6 @@ export declare const modernAngularRules: {
4
4
  readonly modernStyles: 17;
5
5
  readonly preferControlFlow: 17;
6
6
  readonly preferSignals: 17;
7
+ readonly templateLet: 19;
7
8
  readonly templateLiteral: 19;
8
9
  };
package/index.d.ts CHANGED
@@ -102,6 +102,9 @@ declare const plugin: {
102
102
  'no-nested-interactive': import("eslint").Rule.RuleModule & {
103
103
  name: string;
104
104
  };
105
+ 'no-nested-ternary-in-template': import("eslint").Rule.RuleModule & {
106
+ name: string;
107
+ };
105
108
  'no-obsolete-attrs': import("eslint").Rule.RuleModule & {
106
109
  name: string;
107
110
  };
package/index.esm.js CHANGED
@@ -514,6 +514,7 @@ const modernAngularRules = {
514
514
  modernStyles: 17,
515
515
  preferControlFlow: 17,
516
516
  preferSignals: 17,
517
+ templateLet: 19,
517
518
  templateLiteral: 19,
518
519
  };
519
520
 
@@ -1310,6 +1311,7 @@ var recommended = defineConfig([
1310
1311
  '@taiga-ui/experience-next/no-duplicate-in-head': 'error',
1311
1312
  '@taiga-ui/experience-next/no-href-with-router-link': 'error',
1312
1313
  '@taiga-ui/experience-next/no-nested-interactive': 'error',
1314
+ '@taiga-ui/experience-next/no-nested-ternary-in-template': angularVersion >= modernAngularRules.templateLet ? 'error' : 'off',
1313
1315
  '@taiga-ui/experience-next/no-obsolete-attrs': 'error',
1314
1316
  '@taiga-ui/experience-next/no-obsolete-tags': 'error',
1315
1317
  '@taiga-ui/experience-next/no-project-as-in-ng-template': 'error',
@@ -33246,11 +33248,6 @@ function hasElementAttribute(element, name) {
33246
33248
  return (details?.startsWith('attr.') === true && names.includes(details.slice(5)));
33247
33249
  }));
33248
33250
  }
33249
- function hasOutputBinding(element, name) {
33250
- {
33251
- return element.outputs.length > 0;
33252
- }
33253
- }
33254
33251
  function getElementAttributeLikes(element) {
33255
33252
  const seen = new Set();
33256
33253
  return [
@@ -33282,6 +33279,12 @@ function sourceSpanToLoc(span) {
33282
33279
  },
33283
33280
  };
33284
33281
  }
33282
+ function containsAbsoluteSourceSpan(container, child) {
33283
+ return container.start <= child.start && child.end <= container.end;
33284
+ }
33285
+ function getAbsoluteSourceSpanText(text, span) {
33286
+ return text.slice(span.start, span.end);
33287
+ }
33285
33288
 
33286
33289
  var dist$3 = {};
33287
33290
 
@@ -46269,7 +46272,7 @@ function buildMultilineStartTag(node, sourceText) {
46269
46272
  closing,
46270
46273
  ].join('\n');
46271
46274
  }
46272
- const rule$U = createRule({
46275
+ const rule$V = createRule({
46273
46276
  name: 'attrs-newline',
46274
46277
  rule: {
46275
46278
  create(context) {
@@ -46449,7 +46452,7 @@ function sameOrder(a, b) {
46449
46452
  return a.length === b.length && a.every((value, index) => value === b[index]);
46450
46453
  }
46451
46454
 
46452
- const rule$T = createRule({
46455
+ const rule$U = createRule({
46453
46456
  create(context, [order]) {
46454
46457
  const decorators = new Set(Object.keys(order));
46455
46458
  return {
@@ -46561,7 +46564,7 @@ const INLINE_HTML_ELEMENTS = new Set([
46561
46564
  'wbr',
46562
46565
  ]);
46563
46566
 
46564
- const MESSAGE_ID$h = 'expectAfter';
46567
+ const MESSAGE_ID$i = 'expectAfter';
46565
46568
  const SKIP_CHILDREN = new Set(['code', 'pre']);
46566
46569
  function isChildNode(node) {
46567
46570
  return (node instanceof dist$4.TmplAstBoundText ||
@@ -46585,7 +46588,7 @@ function getNodeLabel(node) {
46585
46588
  }
46586
46589
  return node instanceof dist$4.TmplAstBoundText ? 'binding' : 'text';
46587
46590
  }
46588
- const rule$S = createRule({
46591
+ const rule$T = createRule({
46589
46592
  name: 'element-newline',
46590
46593
  rule: {
46591
46594
  create(context) {
@@ -46611,7 +46614,7 @@ const rule$S = createRule({
46611
46614
  firstChild.sourceSpan.start.offset,
46612
46615
  ], '\n'),
46613
46616
  loc: sourceSpanToLoc(firstChild.sourceSpan),
46614
- messageId: MESSAGE_ID$h,
46617
+ messageId: MESSAGE_ID$i,
46615
46618
  });
46616
46619
  return;
46617
46620
  }
@@ -46629,7 +46632,7 @@ const rule$S = createRule({
46629
46632
  child.sourceSpan.end.offset,
46630
46633
  ], '\n'),
46631
46634
  loc: sourceSpanToLoc(next.sourceSpan),
46632
- messageId: MESSAGE_ID$h,
46635
+ messageId: MESSAGE_ID$i,
46633
46636
  });
46634
46637
  return;
46635
46638
  }
@@ -46643,7 +46646,7 @@ const rule$S = createRule({
46643
46646
  lastChild.sourceSpan.end.offset,
46644
46647
  ], '\n'),
46645
46648
  loc: sourceSpanToLoc(lastChild.sourceSpan),
46646
- messageId: MESSAGE_ID$h,
46649
+ messageId: MESSAGE_ID$i,
46647
46650
  });
46648
46651
  }
46649
46652
  },
@@ -46652,7 +46655,7 @@ const rule$S = createRule({
46652
46655
  meta: {
46653
46656
  docs: { description: 'Enforce line breaks between block-level child nodes' },
46654
46657
  fixable: 'code',
46655
- messages: { [MESSAGE_ID$h]: 'There should be a linebreak after {{name}}.' },
46658
+ messages: { [MESSAGE_ID$i]: 'There should be a linebreak after {{name}}.' },
46656
46659
  schema: [],
46657
46660
  type: 'layout',
46658
46661
  },
@@ -46724,7 +46727,7 @@ const PRESETS = {
46724
46727
  $VUE: ['$CLASS', '$ID', '$VUE_ATTRIBUTE'],
46725
46728
  $VUE_ATTRIBUTE: /^v-/,
46726
46729
  };
46727
- const rule$R = createRule({
46730
+ const rule$S = createRule({
46728
46731
  create(context, [options]) {
46729
46732
  const sourceCode = context.sourceCode;
46730
46733
  const settings = {
@@ -47004,8 +47007,8 @@ const DIRECTIONAL_TO_LOGICAL = {
47004
47007
  top: 'inset-block-start',
47005
47008
  };
47006
47009
  const STYLE_PREFIX = 'style.';
47007
- const MESSAGE_ID$g = 'html-logical-properties';
47008
- const MESSAGE = `
47010
+ const MESSAGE_ID$h = 'html-logical-properties';
47011
+ const MESSAGE$1 = `
47009
47012
  Use logical CSS properties instead of directional properties. Replace:
47010
47013
  • left → inset-inline-start
47011
47014
  • right → inset-inline-end
@@ -47042,20 +47045,20 @@ const config$4 = {
47042
47045
  context.report({
47043
47046
  fix: (fixer) => fixer.replaceTextRange([propertyStart, propertyEnd], logicalProperty),
47044
47047
  loc: sourceSpanToLoc(keySpan),
47045
- messageId: MESSAGE_ID$g,
47048
+ messageId: MESSAGE_ID$h,
47046
47049
  });
47047
47050
  },
47048
47051
  };
47049
47052
  },
47050
47053
  meta: {
47051
- docs: { description: MESSAGE },
47054
+ docs: { description: MESSAGE$1 },
47052
47055
  fixable: 'code',
47053
- messages: { [MESSAGE_ID$g]: MESSAGE },
47056
+ messages: { [MESSAGE_ID$h]: MESSAGE$1 },
47054
47057
  schema: [],
47055
47058
  type: 'suggestion',
47056
47059
  },
47057
47060
  };
47058
- const rule$Q = createRule({
47061
+ const rule$R = createRule({
47059
47062
  name: 'html-logical-properties',
47060
47063
  rule: config$4,
47061
47064
  });
@@ -248297,7 +248300,7 @@ function isImportUsedOnlyAsAngularDiFirstArg(node, sourceCode) {
248297
248300
  }
248298
248301
  return hasSafeRuntimeUsage;
248299
248302
  }
248300
- const rule$P = createRule({
248303
+ const rule$Q = createRule({
248301
248304
  create(context) {
248302
248305
  const { checker, esTreeNodeToTSNodeMap, sourceCode, tsProgram } = getTypeAwareRuleContext(context);
248303
248306
  const checkCycles = context.options[0]?.checkCycles ?? true;
@@ -248959,7 +248962,7 @@ const rule$P = createRule({
248959
248962
  name: 'import-integrity',
248960
248963
  });
248961
248964
 
248962
- const MESSAGE_ID$f = 'invalid-injection-token-description';
248965
+ const MESSAGE_ID$g = 'invalid-injection-token-description';
248963
248966
  const ERROR_MESSAGE$3 = "InjectionToken's description should contain token's name";
248964
248967
  const NG_DEV_MODE = 'ngDevMode';
248965
248968
  function getVariableName(node) {
@@ -249022,7 +249025,7 @@ function getNgDevModeDeclarationFix(program, fixer) {
249022
249025
  ? fixer.insertTextBefore(firstStatement, 'declare const ngDevMode: boolean;\n\n')
249023
249026
  : fixer.insertTextBeforeRange([0, 0], 'declare const ngDevMode: boolean;\n');
249024
249027
  }
249025
- const rule$O = createRule({
249028
+ const rule$P = createRule({
249026
249029
  create(context) {
249027
249030
  const { sourceCode } = context;
249028
249031
  const program = sourceCode.ast;
@@ -249054,7 +249057,7 @@ const rule$O = createRule({
249054
249057
  }
249055
249058
  return fixes;
249056
249059
  },
249057
- messageId: MESSAGE_ID$f,
249060
+ messageId: MESSAGE_ID$g,
249058
249061
  node: description,
249059
249062
  });
249060
249063
  }
@@ -249064,14 +249067,14 @@ const rule$O = createRule({
249064
249067
  meta: {
249065
249068
  docs: { description: ERROR_MESSAGE$3 },
249066
249069
  fixable: 'code',
249067
- messages: { [MESSAGE_ID$f]: ERROR_MESSAGE$3 },
249070
+ messages: { [MESSAGE_ID$g]: ERROR_MESSAGE$3 },
249068
249071
  schema: [],
249069
249072
  type: 'problem',
249070
249073
  },
249071
249074
  name: 'injection-token-description',
249072
249075
  });
249073
249076
 
249074
- const rule$N = createRule({
249077
+ const rule$O = createRule({
249075
249078
  create(context) {
249076
249079
  const { sourceCode } = context;
249077
249080
  const namespaceImports = new Map();
@@ -249147,7 +249150,7 @@ const rule$N = createRule({
249147
249150
  name: 'no-commonjs-import-patterns',
249148
249151
  });
249149
249152
 
249150
- const MESSAGE_ID$e = 'no-deep-imports';
249153
+ const MESSAGE_ID$f = 'no-deep-imports';
249151
249154
  const ERROR_MESSAGE$2 = 'Deep imports of Taiga UI packages are prohibited';
249152
249155
  const CODE_EXTENSIONS = new Set([
249153
249156
  '.cjs',
@@ -249166,7 +249169,7 @@ const DEFAULT_OPTIONS = {
249166
249169
  importDeclaration: '^@taiga-ui*',
249167
249170
  projectName: String.raw `(?<=^@taiga-ui/)([-\w]+)`,
249168
249171
  };
249169
- const rule$M = createRule({
249172
+ const rule$N = createRule({
249170
249173
  create(context) {
249171
249174
  const { currentProject, deepImport, ignoreImports, importDeclaration, projectName, } = { ...DEFAULT_OPTIONS, ...context.options[0] };
249172
249175
  const hasNonCodeExtension = (source) => {
@@ -249208,7 +249211,7 @@ const rule$M = createRule({
249208
249211
  const [start, end] = node.source.range;
249209
249212
  return fixer.replaceTextRange([start + 1, end - 1], importSource.replaceAll(new RegExp(deepImport, 'g'), ''));
249210
249213
  },
249211
- messageId: MESSAGE_ID$e,
249214
+ messageId: MESSAGE_ID$f,
249212
249215
  node: node.source,
249213
249216
  });
249214
249217
  },
@@ -249218,7 +249221,7 @@ const rule$M = createRule({
249218
249221
  defaultOptions: [DEFAULT_OPTIONS],
249219
249222
  docs: { description: ERROR_MESSAGE$2 },
249220
249223
  fixable: 'code',
249221
- messages: { [MESSAGE_ID$e]: ERROR_MESSAGE$2 },
249224
+ messages: { [MESSAGE_ID$f]: ERROR_MESSAGE$2 },
249222
249225
  schema: [
249223
249226
  {
249224
249227
  additionalProperties: false,
@@ -249258,7 +249261,7 @@ const nearestFileUpCache = new Map();
249258
249261
  const markerCache = new Map();
249259
249262
  const indexFileCache = new Map();
249260
249263
  const indexExportsCache = new Map();
249261
- const rule$L = createRule({
249264
+ const rule$M = createRule({
249262
249265
  create(context) {
249263
249266
  const parserServices = dist$3.ESLintUtils.getParserServices(context);
249264
249267
  const program = parserServices.program;
@@ -249452,13 +249455,13 @@ const noDuplicateAttributesRule = angular.templatePlugin.rules?.['no-duplicate-a
249452
249455
  if (!noDuplicateAttributesRule) {
249453
249456
  throw new Error('angular-eslint template rule "no-duplicate-attributes" is not available');
249454
249457
  }
249455
- const rule$K = createRule({
249458
+ const rule$L = createRule({
249456
249459
  name: 'no-duplicate-attrs',
249457
249460
  rule: noDuplicateAttributesRule,
249458
249461
  });
249459
249462
 
249460
- const MESSAGE_ID$d = 'duplicateId';
249461
- const rule$J = createRule({
249463
+ const MESSAGE_ID$e = 'duplicateId';
249464
+ const rule$K = createRule({
249462
249465
  name: 'no-duplicate-id',
249463
249466
  rule: {
249464
249467
  create(context) {
@@ -249481,7 +249484,7 @@ const rule$J = createRule({
249481
249484
  context.report({
249482
249485
  data: { id },
249483
249486
  loc: sourceSpanToLoc(attr.sourceSpan),
249484
- messageId: MESSAGE_ID$d,
249487
+ messageId: MESSAGE_ID$e,
249485
249488
  });
249486
249489
  }
249487
249490
  }
@@ -249490,14 +249493,14 @@ const rule$J = createRule({
249490
249493
  },
249491
249494
  meta: {
249492
249495
  docs: { description: 'Disallow duplicate static id attributes' },
249493
- messages: { [MESSAGE_ID$d]: "The id '{{id}}' is duplicated." },
249496
+ messages: { [MESSAGE_ID$e]: "The id '{{id}}' is duplicated." },
249494
249497
  schema: [],
249495
249498
  type: 'problem',
249496
249499
  },
249497
249500
  },
249498
249501
  });
249499
249502
 
249500
- const MESSAGE_ID$c = 'duplicateTag';
249503
+ const MESSAGE_ID$d = 'duplicateTag';
249501
249504
  function getTrackingKey(node) {
249502
249505
  if (node.name === 'title' || node.name === 'base') {
249503
249506
  return node.name;
@@ -249516,7 +249519,7 @@ function getTrackingKey(node) {
249516
249519
  ? 'link[rel=canonical]'
249517
249520
  : null;
249518
249521
  }
249519
- const rule$I = createRule({
249522
+ const rule$J = createRule({
249520
249523
  name: 'no-duplicate-in-head',
249521
249524
  rule: {
249522
249525
  create(context) {
@@ -249553,7 +249556,7 @@ const rule$I = createRule({
249553
249556
  context.report({
249554
249557
  data: { tag },
249555
249558
  loc: sourceSpanToLoc(duplicate.startSourceSpan),
249556
- messageId: MESSAGE_ID$c,
249559
+ messageId: MESSAGE_ID$d,
249557
249560
  });
249558
249561
  }
249559
249562
  }
@@ -249564,7 +249567,7 @@ const rule$I = createRule({
249564
249567
  docs: {
249565
249568
  description: 'Disallow duplicate title/base/meta/link tags inside head',
249566
249569
  },
249567
- messages: { [MESSAGE_ID$c]: 'Duplicate <{{tag}}> tag in <head>.' },
249570
+ messages: { [MESSAGE_ID$d]: 'Duplicate <{{tag}}> tag in <head>.' },
249568
249571
  schema: [],
249569
249572
  type: 'problem',
249570
249573
  },
@@ -249572,7 +249575,7 @@ const rule$I = createRule({
249572
249575
  });
249573
249576
 
249574
249577
  const COMPONENT_DECORATORS = new Set(['Component']);
249575
- const rule$H = createRule({
249578
+ const rule$I = createRule({
249576
249579
  create(context) {
249577
249580
  const { sourceCode } = context;
249578
249581
  return {
@@ -250220,7 +250223,7 @@ const ANGULAR_SIGNALS_UNTRACKED_GUIDE_URL = 'https://angular.dev/guide/signals#r
250220
250223
  const ANGULAR_SIGNALS_ASYNC_GUIDE_URL = 'https://angular.dev/guide/signals#reactive-context-and-async-operations';
250221
250224
  const createUntrackedRule = createRule;
250222
250225
 
250223
- const rule$G = createUntrackedRule({
250226
+ const rule$H = createUntrackedRule({
250224
250227
  create(context) {
250225
250228
  const { checker, esTreeNodeToTSNodeMap, program } = getTypeAwareRuleContext(context);
250226
250229
  const signalNodeMap = esTreeNodeToTSNodeMap;
@@ -250258,7 +250261,7 @@ const rule$G = createUntrackedRule({
250258
250261
  name: 'no-fully-untracked-effect',
250259
250262
  });
250260
250263
 
250261
- const MESSAGE_ID$b = 'no-href-with-router-link';
250264
+ const MESSAGE_ID$c = 'no-href-with-router-link';
250262
250265
  const ERROR_MESSAGE$1 = 'Do not use href and routerLink attributes together on the same element';
250263
250266
  const config$3 = {
250264
250267
  create(context) {
@@ -250280,7 +250283,7 @@ const config$3 = {
250280
250283
  hrefAttr.sourceSpan.end.offset,
250281
250284
  ]),
250282
250285
  loc: sourceSpanToLoc(hrefAttr.sourceSpan),
250283
- messageId: MESSAGE_ID$b,
250286
+ messageId: MESSAGE_ID$c,
250284
250287
  });
250285
250288
  },
250286
250289
  };
@@ -250288,12 +250291,12 @@ const config$3 = {
250288
250291
  meta: {
250289
250292
  docs: { description: ERROR_MESSAGE$1 },
250290
250293
  fixable: 'code',
250291
- messages: { [MESSAGE_ID$b]: ERROR_MESSAGE$1 },
250294
+ messages: { [MESSAGE_ID$c]: ERROR_MESSAGE$1 },
250292
250295
  schema: [],
250293
250296
  type: 'problem',
250294
250297
  },
250295
250298
  };
250296
- const rule$F = createRule({
250299
+ const rule$G = createRule({
250297
250300
  name: 'no-href-with-router-link',
250298
250301
  rule: config$3,
250299
250302
  });
@@ -250354,7 +250357,7 @@ function getScopeRoot(node) {
250354
250357
  return (findAncestor(node, (ancestor) => ancestor.type === dist$3.AST_NODE_TYPES.Program || isFunctionLike(ancestor)) ?? node);
250355
250358
  }
250356
250359
 
250357
- const rule$E = createRule({
250360
+ const rule$F = createRule({
250358
250361
  create(context) {
250359
250362
  const checkImplicitPublic = (node) => {
250360
250363
  const classRef = getEnclosingClass(node);
@@ -250416,7 +250419,7 @@ const rule$E = createRule({
250416
250419
  name: 'no-implicit-public',
250417
250420
  });
250418
250421
 
250419
- const rule$D = createRule({
250422
+ const rule$E = createRule({
250420
250423
  create(context) {
250421
250424
  const { sourceCode } = context;
250422
250425
  return {
@@ -250472,7 +250475,7 @@ function isInfiniteLoopLiteral(node) {
250472
250475
  function isInfiniteLoopTest(test) {
250473
250476
  return test == null || isInfiniteLoopLiteral(test);
250474
250477
  }
250475
- const rule$C = createRule({
250478
+ const rule$D = createRule({
250476
250479
  create(context) {
250477
250480
  return {
250478
250481
  DoWhileStatement(node) {
@@ -250517,7 +250520,7 @@ const rule$C = createRule({
250517
250520
  });
250518
250521
 
250519
250522
  const LEGACY_PEER_DEPS_PATTERN = /^legacy-peer-deps\s*=\s*true$/i;
250520
- const rule$B = createRule({
250523
+ const rule$C = createRule({
250521
250524
  create(context) {
250522
250525
  return {
250523
250526
  Program(node) {
@@ -250557,7 +250560,7 @@ const rule$B = createRule({
250557
250560
 
250558
250561
  function isInteractiveElement(node) {
250559
250562
  const tagName = node.name.toLowerCase();
250560
- if (hasElementAttribute(node, 'tabindex') || hasOutputBinding(node)) {
250563
+ if (hasElementAttribute(node, 'tabindex')) {
250561
250564
  return true;
250562
250565
  }
250563
250566
  switch (tagName) {
@@ -250594,7 +250597,7 @@ function isInteractiveElement(node) {
250594
250597
  }
250595
250598
  }
250596
250599
 
250597
- const MESSAGE_ID$a = 'noNestedInteractive';
250600
+ const MESSAGE_ID$b = 'noNestedInteractive';
250598
250601
  function getAvailableLabelParent(stack, node, labelsWithControl) {
250599
250602
  const parent = stack[stack.length - 1];
250600
250603
  return stack.length === 1 &&
@@ -250604,7 +250607,7 @@ function getAvailableLabelParent(stack, node, labelsWithControl) {
250604
250607
  ? parent
250605
250608
  : null;
250606
250609
  }
250607
- const rule$A = createRule({
250610
+ const rule$B = createRule({
250608
250611
  name: 'no-nested-interactive',
250609
250612
  rule: {
250610
250613
  create(context) {
@@ -250625,7 +250628,7 @@ const rule$A = createRule({
250625
250628
  context.report({
250626
250629
  data: { tag: parent.name },
250627
250630
  loc: sourceSpanToLoc(node.startSourceSpan),
250628
- messageId: MESSAGE_ID$a,
250631
+ messageId: MESSAGE_ID$b,
250629
250632
  });
250630
250633
  }
250631
250634
  interactiveStack.push(node);
@@ -250641,7 +250644,7 @@ const rule$A = createRule({
250641
250644
  meta: {
250642
250645
  docs: { description: 'Disallow nested interactive elements' },
250643
250646
  messages: {
250644
- [MESSAGE_ID$a]: 'Unexpected interactive element nested inside interactive <{{tag}}>.',
250647
+ [MESSAGE_ID$b]: 'Unexpected interactive element nested inside interactive <{{tag}}>.',
250645
250648
  },
250646
250649
  schema: [],
250647
250650
  type: 'problem',
@@ -250649,6 +250652,301 @@ const rule$A = createRule({
250649
250652
  },
250650
250653
  });
250651
250654
 
250655
+ function isBoundAttribute(node) {
250656
+ return node instanceof dist$4.TmplAstBoundAttribute;
250657
+ }
250658
+ function getBoundAttributes(container) {
250659
+ return container instanceof dist$4.TmplAstTemplate
250660
+ ? [...container.inputs, ...container.templateAttrs.filter(isBoundAttribute)]
250661
+ : container.inputs;
250662
+ }
250663
+ function getContainingBoundAttribute(container, node) {
250664
+ return container
250665
+ ? (getBoundAttributes(container).find((attribute) => containsAbsoluteSourceSpan(attribute.value.sourceSpan, node.sourceSpan)) ?? null)
250666
+ : null;
250667
+ }
250668
+
250669
+ function isConditional(node) {
250670
+ return 'condition' in node && 'trueExp' in node && 'falseExp' in node;
250671
+ }
250672
+
250673
+ class TemplateIdentifierCollector extends dist$4.TmplAstRecursiveVisitor {
250674
+ constructor(names) {
250675
+ super();
250676
+ this.names = names;
250677
+ }
250678
+ visitLetDeclaration(node) {
250679
+ this.names.add(node.name);
250680
+ }
250681
+ visitReference(node) {
250682
+ this.names.add(node.name);
250683
+ }
250684
+ visitVariable(node) {
250685
+ this.names.add(node.name);
250686
+ }
250687
+ }
250688
+ function getTemplateNodes(ast) {
250689
+ if (!ast || typeof ast !== 'object' || !('templateNodes' in ast)) {
250690
+ return [];
250691
+ }
250692
+ const { templateNodes } = ast;
250693
+ return Array.isArray(templateNodes) ? templateNodes : [];
250694
+ }
250695
+ function collectTemplateIdentifiers(ast) {
250696
+ const names = new Set();
250697
+ dist$4.tmplAstVisitAll(new TemplateIdentifierCollector(names), [...getTemplateNodes(ast)]);
250698
+ return names;
250699
+ }
250700
+
250701
+ const IDENTIFIER = /^[A-Z_$][\w$]*$/i;
250702
+ const RESERVED_IDENTIFIERS = new Set([
250703
+ 'arguments',
250704
+ 'await',
250705
+ 'break',
250706
+ 'case',
250707
+ 'catch',
250708
+ 'class',
250709
+ 'const',
250710
+ 'continue',
250711
+ 'debugger',
250712
+ 'default',
250713
+ 'delete',
250714
+ 'do',
250715
+ 'else',
250716
+ 'enum',
250717
+ 'eval',
250718
+ 'export',
250719
+ 'extends',
250720
+ 'false',
250721
+ 'finally',
250722
+ 'for',
250723
+ 'function',
250724
+ 'if',
250725
+ 'implements',
250726
+ 'import',
250727
+ 'in',
250728
+ 'instanceof',
250729
+ 'interface',
250730
+ 'let',
250731
+ 'new',
250732
+ 'null',
250733
+ 'package',
250734
+ 'private',
250735
+ 'protected',
250736
+ 'public',
250737
+ 'return',
250738
+ 'static',
250739
+ 'super',
250740
+ 'switch',
250741
+ 'this',
250742
+ 'throw',
250743
+ 'true',
250744
+ 'try',
250745
+ 'typeof',
250746
+ 'undefined',
250747
+ 'var',
250748
+ 'void',
250749
+ 'while',
250750
+ 'with',
250751
+ 'yield',
250752
+ ]);
250753
+ function isIdentifier(name) {
250754
+ return IDENTIFIER.test(name) && !RESERVED_IDENTIFIERS.has(name);
250755
+ }
250756
+ function getAvailableIdentifier(baseName, unavailableNames) {
250757
+ if (!isIdentifier(baseName)) {
250758
+ throw new Error(`Expected a valid identifier, got "${baseName}"`);
250759
+ }
250760
+ if (!unavailableNames.has(baseName)) {
250761
+ return baseName;
250762
+ }
250763
+ let suffix = 2;
250764
+ let name = `${baseName}${suffix}`;
250765
+ while (unavailableNames.has(name)) {
250766
+ suffix++;
250767
+ name = `${baseName}${suffix}`;
250768
+ }
250769
+ return name;
250770
+ }
250771
+
250772
+ function isSingleLineNode(node) {
250773
+ return node.loc.start.line === node.loc.end.line;
250774
+ }
250775
+ function hasCommentLikeText(text) {
250776
+ return text.includes('//') || text.includes('/*');
250777
+ }
250778
+ function hasBlankLine(text) {
250779
+ let lineBreaks = 0;
250780
+ for (let index = 0; index < text.length; index++) {
250781
+ const char = text[index];
250782
+ if (char === '\n') {
250783
+ lineBreaks++;
250784
+ }
250785
+ else if (char === '\r') {
250786
+ lineBreaks++;
250787
+ if (text[index + 1] === '\n') {
250788
+ index++;
250789
+ }
250790
+ }
250791
+ if (lineBreaks > 1) {
250792
+ return true;
250793
+ }
250794
+ }
250795
+ return false;
250796
+ }
250797
+ function getLineBreak(text) {
250798
+ if (text.includes('\r\n')) {
250799
+ return '\r\n';
250800
+ }
250801
+ return text.includes('\r') ? '\r' : '\n';
250802
+ }
250803
+ function getLeadingIndentation(text) {
250804
+ let index = 0;
250805
+ while (index < text.length && (text[index] === ' ' || text[index] === '\t')) {
250806
+ index++;
250807
+ }
250808
+ return text.slice(0, index);
250809
+ }
250810
+ function getIndentAtOffset(text, offset) {
250811
+ const lineStart = text.lastIndexOf('\n', offset - 1) + 1;
250812
+ const indent = text.slice(lineStart, offset);
250813
+ return indent.trim() === '' ? indent : '';
250814
+ }
250815
+ function getSpacingReplacement(sourceCode, betweenText, nextLine, blankLineCount) {
250816
+ const indentation = getLeadingIndentation(sourceCode.lines[nextLine - 1] ?? '');
250817
+ return `${getLineBreak(betweenText).repeat(blankLineCount + 1)}${indentation}`;
250818
+ }
250819
+
250820
+ const MESSAGE_ID$a = 'noNestedTernaryInTemplate';
250821
+ const MESSAGE = 'Avoid nested ternary expressions in Angular templates. Move the nested branch into an @let declaration.';
250822
+ function createLetFixes(node, baseName, unavailableNames, text) {
250823
+ const name = getAvailableIdentifier(baseName, unavailableNames);
250824
+ unavailableNames.add(name);
250825
+ const nestedLets = [];
250826
+ const render = (child) => {
250827
+ if (isConditional(child)) {
250828
+ const result = createLetFixes(child, baseName, unavailableNames, text);
250829
+ nestedLets.push(...result.lets);
250830
+ return result.reference;
250831
+ }
250832
+ return getAbsoluteSourceSpanText(text, child.sourceSpan);
250833
+ };
250834
+ const condition = render(node.condition);
250835
+ const trueExp = render(node.trueExp);
250836
+ const falseExp = render(node.falseExp);
250837
+ return {
250838
+ lets: [
250839
+ ...nestedLets,
250840
+ {
250841
+ expression: `${condition} ? ${trueExp} : ${falseExp}`,
250842
+ name,
250843
+ },
250844
+ ],
250845
+ reference: name,
250846
+ };
250847
+ }
250848
+ const rule$A = createRule({
250849
+ name: 'no-nested-ternary-in-template',
250850
+ rule: {
250851
+ create(context) {
250852
+ const { sourceCode } = context;
250853
+ const conditionalStack = [];
250854
+ const containerStack = [];
250855
+ const letNames = collectTemplateIdentifiers(sourceCode.ast);
250856
+ let boundEventDepth = 0;
250857
+ let letDeclarationDepth = 0;
250858
+ function reportNestedConditional(node, options) {
250859
+ const container = containerStack[containerStack.length - 1];
250860
+ const attribute = getContainingBoundAttribute(container, node);
250861
+ const baseName = attribute?.name;
250862
+ const fixable = options.allowFix &&
250863
+ boundEventDepth === 0 &&
250864
+ letDeclarationDepth === 0 &&
250865
+ baseName &&
250866
+ isIdentifier(baseName);
250867
+ context.report({
250868
+ ...(fixable
250869
+ ? {
250870
+ fix(fixer) {
250871
+ const result = createLetFixes(node, baseName, letNames, sourceCode.text);
250872
+ const insertOffset = container?.startSourceSpan.start.offset;
250873
+ if (insertOffset === undefined) {
250874
+ return null;
250875
+ }
250876
+ const indent = getIndentAtOffset(sourceCode.text, insertOffset);
250877
+ const declarations = result.lets
250878
+ .map(({ expression, name }, index) => `${index === 0 ? '' : indent}@let ${name} = ${expression};`)
250879
+ .join('\n');
250880
+ return [
250881
+ fixer.insertTextBeforeRange([insertOffset, insertOffset], `${declarations}\n${indent}`),
250882
+ fixer.replaceTextRange([node.sourceSpan.start, node.sourceSpan.end], result.reference),
250883
+ ];
250884
+ },
250885
+ }
250886
+ : {}),
250887
+ loc: {
250888
+ end: sourceCode.getLocFromIndex(node.sourceSpan.end),
250889
+ start: sourceCode.getLocFromIndex(node.sourceSpan.start),
250890
+ },
250891
+ messageId: MESSAGE_ID$a,
250892
+ });
250893
+ }
250894
+ return {
250895
+ BoundEvent() {
250896
+ boundEventDepth++;
250897
+ },
250898
+ 'BoundEvent:exit'() {
250899
+ boundEventDepth--;
250900
+ },
250901
+ Conditional(rawNode) {
250902
+ const node = rawNode;
250903
+ if (conditionalStack.length > 0) {
250904
+ reportNestedConditional(node, {
250905
+ allowFix: conditionalStack.length === 1,
250906
+ });
250907
+ }
250908
+ conditionalStack.push(node);
250909
+ },
250910
+ 'Conditional:exit'(rawNode) {
250911
+ const node = rawNode;
250912
+ if (conditionalStack[conditionalStack.length - 1] === node) {
250913
+ conditionalStack.pop();
250914
+ }
250915
+ },
250916
+ Element(rawNode) {
250917
+ containerStack.push(rawNode);
250918
+ },
250919
+ 'Element:exit'(rawNode) {
250920
+ if (containerStack[containerStack.length - 1] === rawNode) {
250921
+ containerStack.pop();
250922
+ }
250923
+ },
250924
+ LetDeclaration() {
250925
+ letDeclarationDepth++;
250926
+ },
250927
+ 'LetDeclaration:exit'() {
250928
+ letDeclarationDepth--;
250929
+ },
250930
+ Template(rawNode) {
250931
+ containerStack.push(rawNode);
250932
+ },
250933
+ 'Template:exit'(rawNode) {
250934
+ if (containerStack[containerStack.length - 1] === rawNode) {
250935
+ containerStack.pop();
250936
+ }
250937
+ },
250938
+ };
250939
+ },
250940
+ meta: {
250941
+ docs: { description: MESSAGE },
250942
+ fixable: 'code',
250943
+ messages: { [MESSAGE_ID$a]: MESSAGE },
250944
+ schema: [],
250945
+ type: 'suggestion',
250946
+ },
250947
+ },
250948
+ });
250949
+
250652
250950
  const OBSOLETE_HTML_ATTRS = {
250653
250951
  abbr: [
250654
250952
  {
@@ -254551,49 +254849,6 @@ const rule$9 = createRule({
254551
254849
  name: 'short-tui-imports',
254552
254850
  });
254553
254851
 
254554
- function isSingleLineNode(node) {
254555
- return node.loc.start.line === node.loc.end.line;
254556
- }
254557
- function hasCommentLikeText(text) {
254558
- return text.includes('//') || text.includes('/*');
254559
- }
254560
- function hasBlankLine(text) {
254561
- let lineBreaks = 0;
254562
- for (let index = 0; index < text.length; index++) {
254563
- const char = text[index];
254564
- if (char === '\n') {
254565
- lineBreaks++;
254566
- }
254567
- else if (char === '\r') {
254568
- lineBreaks++;
254569
- if (text[index + 1] === '\n') {
254570
- index++;
254571
- }
254572
- }
254573
- if (lineBreaks > 1) {
254574
- return true;
254575
- }
254576
- }
254577
- return false;
254578
- }
254579
- function getLineBreak(text) {
254580
- if (text.includes('\r\n')) {
254581
- return '\r\n';
254582
- }
254583
- return text.includes('\r') ? '\r' : '\n';
254584
- }
254585
- function getLeadingIndentation(text) {
254586
- let index = 0;
254587
- while (index < text.length && (text[index] === ' ' || text[index] === '\t')) {
254588
- index++;
254589
- }
254590
- return text.slice(0, index);
254591
- }
254592
- function getSpacingReplacement(sourceCode, betweenText, nextLine, blankLineCount) {
254593
- const indentation = getLeadingIndentation(sourceCode.lines[nextLine - 1] ?? '');
254594
- return `${getLineBreak(betweenText).repeat(blankLineCount + 1)}${indentation}`;
254595
- }
254596
-
254597
254852
  function isFieldLikeMember(member) {
254598
254853
  return (member.type === dist$3.AST_NODE_TYPES.PropertyDefinition ||
254599
254854
  member.type === dist$3.AST_NODE_TYPES.TSAbstractPropertyDefinition);
@@ -255828,29 +256083,30 @@ const plugin = {
255828
256083
  },
255829
256084
  rules: {
255830
256085
  'array-as-const': rule$5,
255831
- 'attrs-newline': rule$U,
256086
+ 'attrs-newline': rule$V,
255832
256087
  'class-property-naming': rule$4,
255833
- 'decorator-key-sort': rule$T,
255834
- 'element-newline': rule$S,
256088
+ 'decorator-key-sort': rule$U,
256089
+ 'element-newline': rule$T,
255835
256090
  'flat-exports': rule$3,
255836
- 'host-attributes-sort': rule$R,
255837
- 'html-logical-properties': rule$Q,
255838
- 'import-integrity': rule$P,
255839
- 'injection-token-description': rule$O,
255840
- 'no-commonjs-import-patterns': rule$N,
255841
- 'no-deep-imports': rule$M,
255842
- 'no-deep-imports-to-indexed-packages': rule$L,
255843
- 'no-duplicate-attrs': rule$K,
255844
- 'no-duplicate-id': rule$J,
255845
- 'no-duplicate-in-head': rule$I,
255846
- 'no-empty-style-metadata': rule$H,
255847
- 'no-fully-untracked-effect': rule$G,
255848
- 'no-href-with-router-link': rule$F,
255849
- 'no-implicit-public': rule$E,
255850
- 'no-import-assertions': rule$D,
255851
- 'no-infinite-loop': rule$C,
255852
- 'no-legacy-peer-deps': rule$B,
255853
- 'no-nested-interactive': rule$A,
256091
+ 'host-attributes-sort': rule$S,
256092
+ 'html-logical-properties': rule$R,
256093
+ 'import-integrity': rule$Q,
256094
+ 'injection-token-description': rule$P,
256095
+ 'no-commonjs-import-patterns': rule$O,
256096
+ 'no-deep-imports': rule$N,
256097
+ 'no-deep-imports-to-indexed-packages': rule$M,
256098
+ 'no-duplicate-attrs': rule$L,
256099
+ 'no-duplicate-id': rule$K,
256100
+ 'no-duplicate-in-head': rule$J,
256101
+ 'no-empty-style-metadata': rule$I,
256102
+ 'no-fully-untracked-effect': rule$H,
256103
+ 'no-href-with-router-link': rule$G,
256104
+ 'no-implicit-public': rule$F,
256105
+ 'no-import-assertions': rule$E,
256106
+ 'no-infinite-loop': rule$D,
256107
+ 'no-legacy-peer-deps': rule$C,
256108
+ 'no-nested-interactive': rule$B,
256109
+ 'no-nested-ternary-in-template': rule$A,
255854
256110
  'no-obsolete-attrs': rule$z,
255855
256111
  'no-obsolete-tags': rule$y,
255856
256112
  'no-playwright-empty-fill': rule$x,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.514.0",
3
+ "version": "0.516.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "homepage": "https://github.com/taiga-family/toolkit#readme",
6
6
  "bugs": {
@@ -0,0 +1,5 @@
1
+ import { type Rule } from 'eslint';
2
+ export declare const rule: Rule.RuleModule & {
3
+ name: string;
4
+ };
5
+ export default rule;
@@ -1,3 +1,5 @@
1
- import { type ParseSourceSpan } from '@angular-eslint/bundled-angular-compiler';
1
+ import { type AbsoluteSourceSpan, type ParseSourceSpan } from '@angular-eslint/bundled-angular-compiler';
2
2
  import { type SourceLocation } from 'estree';
3
3
  export declare function sourceSpanToLoc(span: ParseSourceSpan): SourceLocation;
4
+ export declare function containsAbsoluteSourceSpan(container: AbsoluteSourceSpan, child: AbsoluteSourceSpan): boolean;
5
+ export declare function getAbsoluteSourceSpanText(text: string, span: AbsoluteSourceSpan): string;
@@ -0,0 +1,5 @@
1
+ import { type AST, TmplAstBoundAttribute, type TmplAstElement, TmplAstTemplate } from '@angular-eslint/bundled-angular-compiler';
2
+ export type TemplateAttributeContainer = TmplAstElement | TmplAstTemplate;
3
+ export declare function isBoundAttribute(node: unknown): node is TmplAstBoundAttribute;
4
+ export declare function getBoundAttributes(container: TemplateAttributeContainer): readonly TmplAstBoundAttribute[];
5
+ export declare function getContainingBoundAttribute(container: TemplateAttributeContainer | undefined, node: AST): TmplAstBoundAttribute | null;
@@ -0,0 +1,4 @@
1
+ import { type AST, type ASTWithSource, type Conditional } from '@angular-eslint/bundled-angular-compiler';
2
+ export declare function isAstWithSource(node: AST): node is ASTWithSource;
3
+ export declare function unwrapAstWithSource(node: AST): AST;
4
+ export declare function isConditional(node: AST): node is Conditional;
@@ -0,0 +1,10 @@
1
+ import { type TmplAstLetDeclaration, type TmplAstNode, TmplAstRecursiveVisitor, type TmplAstReference, type TmplAstVariable } from '@angular-eslint/bundled-angular-compiler';
2
+ export declare class TemplateIdentifierCollector extends TmplAstRecursiveVisitor {
3
+ private readonly names;
4
+ constructor(names: Set<string>);
5
+ visitLetDeclaration(node: TmplAstLetDeclaration): void;
6
+ visitReference(node: TmplAstReference): void;
7
+ visitVariable(node: TmplAstVariable): void;
8
+ }
9
+ export declare function getTemplateNodes(ast: unknown): readonly TmplAstNode[];
10
+ export declare function collectTemplateIdentifiers(ast: unknown): Set<string>;
@@ -0,0 +1,2 @@
1
+ export declare function isIdentifier(name: string): boolean;
2
+ export declare function getAvailableIdentifier(baseName: string, unavailableNames: ReadonlySet<string>): string;
@@ -4,4 +4,5 @@ export declare function hasCommentLikeText(text: string): boolean;
4
4
  export declare function hasBlankLine(text: string): boolean;
5
5
  export declare function getLineBreak(text: string): string;
6
6
  export declare function getLeadingIndentation(text: string): string;
7
+ export declare function getIndentAtOffset(text: string, offset: number): string;
7
8
  export declare function getSpacingReplacement(sourceCode: Readonly<TSESLint.SourceCode>, betweenText: string, nextLine: number, blankLineCount: number): string;