@taiga-ui/eslint-plugin-experience-next 0.471.0 → 0.473.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
@@ -44,7 +44,7 @@ export default [
44
44
  | decorator-key-sort | Sorts the keys of the object passed to the `@Component/@Injectable/@NgModule/@Pipe` decorator | ✅ | 🔧 | |
45
45
  | flat-exports | Spread nested arrays when exporting Angular entity collections | | 🔧 | |
46
46
  | host-attributes-sort | Sort Angular host metadata attributes using configurable attribute groups | ✅ | 🔧 | |
47
- | injection-token-description | They are required to provide a description for `InjectionToken` | ✅ | | |
47
+ | injection-token-description | Require `InjectionToken` descriptions to include the token name | ✅ | 🔧 | |
48
48
  | no-deep-imports | Disables deep imports of Taiga UI packages | ✅ | 🔧 | |
49
49
  | no-deep-imports-to-indexed-packages | Disallow deep imports from packages that expose an index.ts next to ng-package.json or package.json | ✅ | 🔧 | |
50
50
  | no-fully-untracked-effect | Disallow reactive callbacks where all signal reads are hidden inside `untracked()` | ✅ | | |
@@ -60,6 +60,7 @@ export default [
60
60
  | no-untracked-outside-reactive-context | Disallow `untracked()` outside reactive callbacks, except explicit post-`await` snapshots | ✅ | 🔧 | |
61
61
  | no-useless-untracked | Disallow provably useless `untracked()` wrappers in reactive callbacks | ✅ | 🔧 | |
62
62
  | object-single-line | Enforce single-line formatting for single-property objects when it fits `printWidth` | ✅ | 🔧 | |
63
+ | prefer-combined-if-control-flow | Combine consecutive `if` statements that use the same `return`, `break`, `continue`, or `throw` | ✅ | 🔧 | |
63
64
  | prefer-deep-imports | Allow deep imports of Taiga UI packages | | 🔧 | |
64
65
  | prefer-multi-arg-push | Combine consecutive `.push()` calls on the same array into a single multi-argument call | ✅ | 🔧 | |
65
66
  | prefer-untracked-incidental-signal-reads | Wrap likely-incidental signal reads with `untracked()` in reactive callbacks | ✅ | 🔧 | |
@@ -282,17 +283,25 @@ Use atomic presets when you want a custom order instead of one of the bundled al
282
283
 
283
284
  ## injection-token-description
284
285
 
285
- <sup>`✅ Recommended`</sup>
286
+ <sup>`✅ Recommended`</sup> <sup>`Fixable`</sup>
286
287
 
287
- The description string passed to `new InjectionToken(...)` must contain the name of the variable it is assigned to. This
288
- makes token names visible in Angular DevTools and error messages.
288
+ The description passed to `new InjectionToken(...)` must contain the name of the variable it is assigned to. The rule
289
+ accepts both direct string descriptions and Angular's `ngDevMode ? '...' : ''` pattern, and the autofix rewrites invalid
290
+ descriptions to the dev-only form. If `ngDevMode` is not declared in the file, the autofix inserts
291
+ `declare const ngDevMode: boolean;` after imports.
289
292
 
290
293
  ```ts
291
- // ❌ error — description does not mention TUI_MY_TOKEN
292
- const TUI_MY_TOKEN = new InjectionToken<string>('some description');
294
+ // ❌ error
295
+ import {InjectionToken} from '@angular/core';
296
+
297
+ export const TUI_MY_TOKEN = new InjectionToken<string>('some description');
293
298
 
294
299
  // ✅ after autofix
295
- const TUI_MY_TOKEN = new InjectionToken<string>('[TUI_MY_TOKEN]: some description');
300
+ import {InjectionToken} from '@angular/core';
301
+
302
+ declare const ngDevMode: boolean;
303
+
304
+ export const TUI_MY_TOKEN = new InjectionToken<string>(ngDevMode ? '[TUI_MY_TOKEN]: some description' : '');
296
305
  ```
297
306
 
298
307
  ---
@@ -855,6 +864,98 @@ const x = {foo: bar};
855
864
 
856
865
  ---
857
866
 
867
+ ## prefer-combined-if-control-flow
868
+
869
+ <sup>`✅ Recommended`</sup> <sup>`Fixable`</sup>
870
+
871
+ Combine consecutive `if` statements when they have no `else` branch and use the same `return`, `break`, `continue`, or
872
+ `throw` statement. The autofix merges their conditions with `||`, while intentionally skipping cases with intervening
873
+ code or comments that should remain a separate control-flow boundary.
874
+
875
+ ```ts
876
+ // ❌ error
877
+ while (true) {
878
+ if (a) continue;
879
+ if (b && c) continue;
880
+ }
881
+
882
+ // ✅ after autofix
883
+ while (true) {
884
+ if (a || (b && c)) continue;
885
+ }
886
+ ```
887
+
888
+ ```ts
889
+ // ❌ error
890
+ if (a || b) {
891
+ return;
892
+ }
893
+
894
+ if (c) {
895
+ return;
896
+ }
897
+
898
+ // ✅ after autofix
899
+ if (a || b || c) {
900
+ return;
901
+ }
902
+ ```
903
+
904
+ ```ts
905
+ // ❌ error
906
+ if (isInvalid) return result;
907
+
908
+ if (isLegacy && shouldStop) return result;
909
+
910
+ // ✅ after autofix
911
+ if (isInvalid || (isLegacy && shouldStop)) return result;
912
+ ```
913
+
914
+ ```ts
915
+ // ❌ error
916
+ while (true) {
917
+ if (isDone) break;
918
+ if (hasError) break;
919
+ }
920
+
921
+ // ✅ after autofix
922
+ while (true) {
923
+ if (isDone || hasError) break;
924
+ }
925
+ ```
926
+
927
+ ```ts
928
+ // ❌ error
929
+ if (isFatal) throw error;
930
+
931
+ if (isExpired && shouldAbort) throw error;
932
+
933
+ // ✅ after autofix
934
+ if (isFatal || (isExpired && shouldAbort)) throw error;
935
+ ```
936
+
937
+ ```ts
938
+ // not changed — different control flow
939
+ while (true) {
940
+ if (isDone) continue;
941
+ if (hasError) break;
942
+ }
943
+ ```
944
+
945
+ ```ts
946
+ // not changed — comment keeps branches separate
947
+ if (a) {
948
+ return value;
949
+ }
950
+
951
+ // explain why this branch exists
952
+ if (b) {
953
+ return value;
954
+ }
955
+ ```
956
+
957
+ ---
958
+
858
959
  ## prefer-deep-imports
859
960
 
860
961
  <sup>`Taiga-specific`</sup> <sup>`Fixable`</sup>
package/index.d.ts CHANGED
@@ -81,6 +81,9 @@ declare const plugin: {
81
81
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
82
82
  name: string;
83
83
  };
84
+ 'prefer-combined-if-control-flow': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferCombinedIfControlFlow", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
85
+ name: string;
86
+ };
84
87
  'prefer-deep-imports': import("@typescript-eslint/utils/ts-eslint").RuleModule<"prefer-deep-imports", [{
85
88
  importFilter: string[] | string;
86
89
  strict?: boolean;
package/index.esm.js CHANGED
@@ -914,6 +914,7 @@ var recommended = defineConfig([
914
914
  '@taiga-ui/experience-next/no-untracked-outside-reactive-context': 'error',
915
915
  '@taiga-ui/experience-next/no-useless-untracked': 'error',
916
916
  '@taiga-ui/experience-next/object-single-line': ['error', { printWidth: 90 }],
917
+ '@taiga-ui/experience-next/prefer-combined-if-control-flow': 'error',
917
918
  '@taiga-ui/experience-next/prefer-multi-arg-push': 'error',
918
919
  '@taiga-ui/experience-next/prefer-untracked-incidental-signal-reads': 'error',
919
920
  '@taiga-ui/experience-next/prefer-untracked-signal-getter': 'error',
@@ -1335,8 +1336,8 @@ function intersect(a, b) {
1335
1336
  return a.some((type) => origin.has(type));
1336
1337
  }
1337
1338
 
1338
- const createRule$h = ESLintUtils.RuleCreator((name) => name);
1339
- var classPropertyNaming = createRule$h({
1339
+ const createRule$i = ESLintUtils.RuleCreator((name) => name);
1340
+ var classPropertyNaming = createRule$i({
1340
1341
  create(context, [configs]) {
1341
1342
  const parserServices = ESLintUtils.getParserServices(context);
1342
1343
  const typeChecker = parserServices.program.getTypeChecker();
@@ -1505,9 +1506,9 @@ function isExternalPureTuple(typeChecker, type) {
1505
1506
  return typeArgs.every((item) => isClassType(item));
1506
1507
  }
1507
1508
 
1508
- const createRule$g = ESLintUtils.RuleCreator((name) => name);
1509
+ const createRule$h = ESLintUtils.RuleCreator((name) => name);
1509
1510
  const MESSAGE_ID$7 = 'spreadArrays';
1510
- var flatExports = createRule$g({
1511
+ var flatExports = createRule$h({
1511
1512
  create(context) {
1512
1513
  const parserServices = ESLintUtils.getParserServices(context);
1513
1514
  const typeChecker = parserServices.program.getTypeChecker();
@@ -1667,10 +1668,7 @@ function getDecoratorMetadata(decorator, allowedNames) {
1667
1668
  return null;
1668
1669
  }
1669
1670
  const callee = expr.callee;
1670
- if (callee.type !== AST_NODE_TYPES$1.Identifier) {
1671
- return null;
1672
- }
1673
- if (!allowedNames.has(callee.name)) {
1671
+ if (callee.type !== AST_NODE_TYPES$1.Identifier || !allowedNames.has(callee.name)) {
1674
1672
  return null;
1675
1673
  }
1676
1674
  const arg = expr.arguments[0];
@@ -1746,8 +1744,8 @@ const PRESETS = {
1746
1744
  $VUE: ['$CLASS', '$ID', '$VUE_ATTRIBUTE'],
1747
1745
  $VUE_ATTRIBUTE: /^v-/,
1748
1746
  };
1749
- const createRule$f = ESLintUtils.RuleCreator((name) => name);
1750
- const rule$i = createRule$f({
1747
+ const createRule$g = ESLintUtils.RuleCreator((name) => name);
1748
+ const rule$j = createRule$g({
1751
1749
  create(context, [options]) {
1752
1750
  const sourceCode = context.sourceCode;
1753
1751
  const settings = {
@@ -1842,10 +1840,8 @@ function getHostObject(metadata) {
1842
1840
  if (property.type !== AST_NODE_TYPES$1.Property ||
1843
1841
  property.kind !== 'init' ||
1844
1842
  property.computed ||
1845
- property.method) {
1846
- continue;
1847
- }
1848
- if (getStaticPropertyName(property.key) !== 'host') {
1843
+ property.method ||
1844
+ getStaticPropertyName(property.key) !== 'host') {
1849
1845
  continue;
1850
1846
  }
1851
1847
  return property.value.type === AST_NODE_TYPES$1.ObjectExpression
@@ -2024,37 +2020,109 @@ const config$2 = {
2024
2020
 
2025
2021
  const MESSAGE_ID$5 = 'invalid-injection-token-description';
2026
2022
  const ERROR_MESSAGE$3 = "InjectionToken's description should contain token's name";
2027
- const createRule$e = ESLintUtils.RuleCreator((name) => name);
2028
- const rule$h = createRule$e({
2023
+ const NG_DEV_MODE = 'ngDevMode';
2024
+ const createRule$f = ESLintUtils.RuleCreator((name) => name);
2025
+ function getVariableName(node) {
2026
+ if (node.parent.type !== AST_NODE_TYPES$1.VariableDeclarator) {
2027
+ return undefined;
2028
+ }
2029
+ const { id } = node.parent;
2030
+ return id.type === AST_NODE_TYPES$1.Identifier ? id.name : undefined;
2031
+ }
2032
+ function isStringLiteral$1(node) {
2033
+ return node.type === AST_NODE_TYPES$1.Literal && typeof node.value === 'string';
2034
+ }
2035
+ function isStringLike(node) {
2036
+ return isStringLiteral$1(node) || node.type === AST_NODE_TYPES$1.TemplateLiteral;
2037
+ }
2038
+ function getStringValue(node) {
2039
+ if (isStringLiteral$1(node)) {
2040
+ return node.value;
2041
+ }
2042
+ return node.quasis[0]?.value.raw || '';
2043
+ }
2044
+ function isEmptyString$1(node) {
2045
+ return (getStringValue(node) === '' &&
2046
+ (!('expressions' in node) || !node.expressions.length));
2047
+ }
2048
+ function isNgDevModeConditional(node) {
2049
+ return (node.type === AST_NODE_TYPES$1.ConditionalExpression &&
2050
+ node.test.type === AST_NODE_TYPES$1.Identifier &&
2051
+ node.test.name === NG_DEV_MODE &&
2052
+ isStringLike(node.consequent) &&
2053
+ isStringLike(node.alternate) &&
2054
+ isEmptyString$1(node.alternate));
2055
+ }
2056
+ function getDescriptionValue(node) {
2057
+ if (isStringLike(node)) {
2058
+ return getStringValue(node);
2059
+ }
2060
+ if (isNgDevModeConditional(node)) {
2061
+ return getStringValue(node.consequent);
2062
+ }
2063
+ return undefined;
2064
+ }
2065
+ function getDescriptionNode(node) {
2066
+ if (isStringLike(node)) {
2067
+ return node;
2068
+ }
2069
+ return isNgDevModeConditional(node) ? node.consequent : undefined;
2070
+ }
2071
+ function prependTokenName(text, name) {
2072
+ return `${text.slice(0, 1)}[${name}]: ${text.slice(1)}`;
2073
+ }
2074
+ function isNgDevModeVisible(sourceCode, node) {
2075
+ for (let scope = sourceCode.getScope(node); scope !== null; scope = scope.upper) {
2076
+ if (scope.variables.some((variable) => variable.name === NG_DEV_MODE)) {
2077
+ return true;
2078
+ }
2079
+ }
2080
+ return false;
2081
+ }
2082
+ function getNgDevModeDeclarationFix(program, fixer) {
2083
+ const lastImport = [...program.body]
2084
+ .reverse()
2085
+ .find((statement) => statement.type === AST_NODE_TYPES$1.ImportDeclaration);
2086
+ if (lastImport) {
2087
+ return fixer.insertTextAfter(lastImport, '\n\ndeclare const ngDevMode: boolean;');
2088
+ }
2089
+ const [firstStatement] = program.body;
2090
+ if (firstStatement) {
2091
+ return fixer.insertTextBefore(firstStatement, 'declare const ngDevMode: boolean;\n\n');
2092
+ }
2093
+ return fixer.insertTextBeforeRange([0, 0], 'declare const ngDevMode: boolean;\n');
2094
+ }
2095
+ const rule$i = createRule$f({
2029
2096
  create(context) {
2097
+ const { sourceCode } = context;
2098
+ const program = sourceCode.ast;
2099
+ let shouldAddNgDevModeDeclaration = true;
2030
2100
  return {
2031
2101
  'NewExpression[callee.name="InjectionToken"]'(node) {
2032
- let token;
2033
- let name;
2034
- const [description] = node?.arguments ?? [];
2035
- if (!description) {
2102
+ const [description] = node.arguments;
2103
+ if (!description || description.type === AST_NODE_TYPES$1.SpreadElement) {
2036
2104
  return;
2037
2105
  }
2038
- const isLiteral = description.type === AST_NODE_TYPES$1.Literal &&
2039
- typeof description.value === 'string';
2040
- if (isLiteral) {
2041
- token = description.value;
2042
- }
2043
- if (description.type === AST_NODE_TYPES$1.TemplateLiteral) {
2044
- token = description.quasis[0]?.value.raw || '';
2045
- }
2046
- if (node?.parent.type === AST_NODE_TYPES$1.VariableDeclarator) {
2047
- const id = node.parent.id;
2048
- if (id.type === AST_NODE_TYPES$1.Identifier) {
2049
- name = id.name;
2050
- }
2051
- }
2106
+ const name = getVariableName(node);
2107
+ const token = getDescriptionValue(description);
2108
+ const fixedDescription = getDescriptionNode(description);
2052
2109
  const report = name && token && !token.includes(name);
2053
- if (report) {
2110
+ if (report && fixedDescription) {
2054
2111
  context.report({
2055
2112
  fix: (fixer) => {
2056
- const [start, end] = description.range;
2057
- return fixer.insertTextBeforeRange([start + 1, end], `[${name}]: `);
2113
+ const isNgDevModeGuarded = isNgDevModeConditional(description);
2114
+ const fixes = [
2115
+ fixer.replaceText(isNgDevModeGuarded ? fixedDescription : description, isNgDevModeGuarded
2116
+ ? prependTokenName(sourceCode.getText(fixedDescription), name)
2117
+ : `${NG_DEV_MODE} ? ${prependTokenName(sourceCode.getText(fixedDescription), name)} : ''`),
2118
+ ];
2119
+ if (!isNgDevModeGuarded &&
2120
+ shouldAddNgDevModeDeclaration &&
2121
+ !isNgDevModeVisible(sourceCode, description)) {
2122
+ shouldAddNgDevModeDeclaration = false;
2123
+ fixes.unshift(getNgDevModeDeclarationFix(program, fixer));
2124
+ }
2125
+ return fixes;
2058
2126
  },
2059
2127
  messageId: MESSAGE_ID$5,
2060
2128
  node: description,
@@ -2092,8 +2160,8 @@ const DEFAULT_OPTIONS = {
2092
2160
  importDeclaration: '^@taiga-ui*',
2093
2161
  projectName: String.raw `(?<=^@taiga-ui/)([-\w]+)`,
2094
2162
  };
2095
- const createRule$d = ESLintUtils.RuleCreator((name) => name);
2096
- const rule$g = createRule$d({
2163
+ const createRule$e = ESLintUtils.RuleCreator((name) => name);
2164
+ const rule$h = createRule$e({
2097
2165
  create(context) {
2098
2166
  const { currentProject, deepImport, ignoreImports, importDeclaration, projectName, } = { ...DEFAULT_OPTIONS, ...context.options[0] };
2099
2167
  const hasNonCodeExtension = (source) => {
@@ -2180,13 +2248,13 @@ const rule$g = createRule$d({
2180
2248
  name: 'no-deep-imports',
2181
2249
  });
2182
2250
 
2183
- const createRule$c = ESLintUtils.RuleCreator((name) => name);
2251
+ const createRule$d = ESLintUtils.RuleCreator((name) => name);
2184
2252
  const resolveCacheByOptions = new WeakMap();
2185
2253
  const nearestFileUpCache = new Map();
2186
2254
  const markerCache = new Map();
2187
2255
  const indexFileCache = new Map();
2188
2256
  const indexExportsCache = new Map();
2189
- var noDeepImportsToIndexedPackages = createRule$c({
2257
+ var noDeepImportsToIndexedPackages = createRule$d({
2190
2258
  create(context) {
2191
2259
  const parserServices = ESLintUtils.getParserServices(context);
2192
2260
  const program = parserServices.program;
@@ -2292,13 +2360,9 @@ var noDeepImportsToIndexedPackages = createRule$c({
2292
2360
  return {
2293
2361
  ImportDeclaration(node) {
2294
2362
  const importSpecifier = node.source.value;
2295
- if (typeof importSpecifier !== 'string') {
2296
- return;
2297
- }
2298
- if (!importSpecifier.includes('/')) {
2299
- return;
2300
- }
2301
- if (!isExternalModuleSpecifier(importSpecifier)) {
2363
+ if (typeof importSpecifier !== 'string' ||
2364
+ !importSpecifier.includes('/') ||
2365
+ !isExternalModuleSpecifier(importSpecifier)) {
2302
2366
  return;
2303
2367
  }
2304
2368
  const packageRootSpecifier = getPackageRootSpecifier(importSpecifier);
@@ -2366,10 +2430,8 @@ function getPackageRootSpecifier(importSpecifier) {
2366
2430
  return pathParts[0] ?? importSpecifier;
2367
2431
  }
2368
2432
  function getSubpath(importSpecifier, packageRootSpecifier) {
2369
- if (importSpecifier === packageRootSpecifier) {
2370
- return null;
2371
- }
2372
- if (!importSpecifier.startsWith(`${packageRootSpecifier}/`)) {
2433
+ if (importSpecifier === packageRootSpecifier ||
2434
+ !importSpecifier.startsWith(`${packageRootSpecifier}/`)) {
2373
2435
  return null;
2374
2436
  }
2375
2437
  return importSpecifier.slice(packageRootSpecifier.length + 1);
@@ -2444,10 +2506,8 @@ function getOrderedChildren(node) {
2444
2506
  ];
2445
2507
  return children.filter((child) => child !== undefined && child !== null);
2446
2508
  }
2447
- if (node.type === AST_NODE_TYPES.BlockStatement) {
2448
- return node.body;
2449
- }
2450
- if (node.type === AST_NODE_TYPES.Program) {
2509
+ if (node.type === AST_NODE_TYPES.BlockStatement ||
2510
+ node.type === AST_NODE_TYPES.Program) {
2451
2511
  return node.body;
2452
2512
  }
2453
2513
  if (node.type === AST_NODE_TYPES.IfStatement) {
@@ -2508,10 +2568,7 @@ function getOrderedChildren(node) {
2508
2568
  function walkSynchronousAst(root, visitor) {
2509
2569
  traverse(root, true);
2510
2570
  function traverse(node, isRoot = false) {
2511
- if (visitor(node) === false) {
2512
- return false;
2513
- }
2514
- if (!isRoot && isFunctionLike$1(node)) {
2571
+ if (visitor(node) === false || (!isRoot && isFunctionLike$1(node))) {
2515
2572
  return false;
2516
2573
  }
2517
2574
  if (node.type === AST_NODE_TYPES.AwaitExpression) {
@@ -2792,10 +2849,8 @@ function isWritableSignalWrite(node, checker, esTreeNodeToTSNodeMap) {
2792
2849
  return false;
2793
2850
  }
2794
2851
  const { object, property } = node.callee;
2795
- if (property.type !== AST_NODE_TYPES.Identifier) {
2796
- return false;
2797
- }
2798
- if (!SIGNAL_WRITE_METHODS.has(property.name)) {
2852
+ if (property.type !== AST_NODE_TYPES.Identifier ||
2853
+ !SIGNAL_WRITE_METHODS.has(property.name)) {
2799
2854
  return false;
2800
2855
  }
2801
2856
  return isSignalType(object, checker, esTreeNodeToTSNodeMap);
@@ -2869,10 +2924,8 @@ const createUntrackedRule = ESLintUtils.RuleCreator((name) => `${UNTRACKED_RULES
2869
2924
  function collectReadsInsideUntracked(root, checker, esTreeNodeToTSNodeMap, program) {
2870
2925
  const reads = [];
2871
2926
  walkSynchronousAst(root, (node) => {
2872
- if (node.type !== AST_NODE_TYPES.CallExpression) {
2873
- return;
2874
- }
2875
- if (!isAngularUntrackedCall(node, program)) {
2927
+ if (node.type !== AST_NODE_TYPES.CallExpression ||
2928
+ !isAngularUntrackedCall(node, program)) {
2876
2929
  return;
2877
2930
  }
2878
2931
  const [arg] = node.arguments;
@@ -2891,7 +2944,7 @@ function collectReadsInsideUntracked(root, checker, esTreeNodeToTSNodeMap, progr
2891
2944
  });
2892
2945
  return reads;
2893
2946
  }
2894
- const rule$f = createUntrackedRule({
2947
+ const rule$g = createUntrackedRule({
2895
2948
  create(context) {
2896
2949
  const parserServices = ESLintUtils.getParserServices(context);
2897
2950
  const checker = parserServices.program.getTypeChecker();
@@ -2977,15 +3030,15 @@ const config$1 = {
2977
3030
  },
2978
3031
  };
2979
3032
 
2980
- const createRule$b = ESLintUtils.RuleCreator((name) => name);
2981
- const rule$e = createRule$b({
3033
+ const createRule$c = ESLintUtils.RuleCreator((name) => name);
3034
+ const rule$f = createRule$c({
2982
3035
  create(context) {
2983
3036
  const checkImplicitPublic = (node) => {
2984
3037
  const classRef = getClass(node);
2985
- if (!classRef || node.kind === 'constructor' || !!node?.accessibility) {
2986
- return;
2987
- }
2988
- if (node.key?.type === AST_NODE_TYPES.PrivateIdentifier) {
3038
+ if (!classRef ||
3039
+ node.kind === 'constructor' ||
3040
+ !!node?.accessibility ||
3041
+ node.key?.type === AST_NODE_TYPES.PrivateIdentifier) {
2989
3042
  return;
2990
3043
  }
2991
3044
  const name = node?.key?.name ||
@@ -3049,9 +3102,9 @@ function getClass(node) {
3049
3102
  return getClass(node.parent);
3050
3103
  }
3051
3104
 
3052
- const createRule$a = ESLintUtils.RuleCreator((name) => name);
3105
+ const createRule$b = ESLintUtils.RuleCreator((name) => name);
3053
3106
  const LEGACY_PEER_DEPS_PATTERN = /^legacy-peer-deps\s*=\s*true$/i;
3054
- const rule$d = createRule$a({
3107
+ const rule$e = createRule$b({
3055
3108
  create(context) {
3056
3109
  return {
3057
3110
  Program(node) {
@@ -3089,8 +3142,8 @@ const rule$d = createRule$a({
3089
3142
  name: 'no-legacy-peer-deps',
3090
3143
  });
3091
3144
 
3092
- const createRule$9 = ESLintUtils.RuleCreator((name) => name);
3093
- const rule$c = createRule$9({
3145
+ const createRule$a = ESLintUtils.RuleCreator((name) => name);
3146
+ const rule$d = createRule$a({
3094
3147
  create(context) {
3095
3148
  const services = ESLintUtils.getParserServices(context);
3096
3149
  const checker = services.program.getTypeChecker();
@@ -3255,7 +3308,7 @@ const config = {
3255
3308
  },
3256
3309
  };
3257
3310
 
3258
- const createRule$8 = ESLintUtils.RuleCreator((name) => name);
3311
+ const createRule$9 = ESLintUtils.RuleCreator((name) => name);
3259
3312
  function collectArrayExpressions(node) {
3260
3313
  const result = [];
3261
3314
  if (node.type === AST_NODE_TYPES.ArrayExpression) {
@@ -3281,7 +3334,7 @@ function collectArrayExpressions(node) {
3281
3334
  }
3282
3335
  return result;
3283
3336
  }
3284
- const rule$b = createRule$8({
3337
+ const rule$c = createRule$9({
3285
3338
  create(context) {
3286
3339
  const parserServices = ESLintUtils.getParserServices(context);
3287
3340
  const typeChecker = parserServices.program.getTypeChecker();
@@ -3409,7 +3462,7 @@ function unwrapExpression(expression) {
3409
3462
  return current;
3410
3463
  }
3411
3464
 
3412
- const createRule$7 = ESLintUtils.RuleCreator((name) => name);
3465
+ const createRule$8 = ESLintUtils.RuleCreator((name) => name);
3413
3466
  function isReactiveCallback(node) {
3414
3467
  return (node?.type === AST_NODE_TYPES.ArrowFunctionExpression ||
3415
3468
  node?.type === AST_NODE_TYPES.FunctionExpression);
@@ -3531,7 +3584,7 @@ function inspectComputedBody(root, inspectionContext, report) {
3531
3584
  return;
3532
3585
  });
3533
3586
  }
3534
- const rule$a = createRule$7({
3587
+ const rule$b = createRule$8({
3535
3588
  create(context) {
3536
3589
  const parserServices = ESLintUtils.getParserServices(context);
3537
3590
  const checker = parserServices.program.getTypeChecker();
@@ -3578,7 +3631,7 @@ const rule$a = createRule$7({
3578
3631
  name: 'no-side-effects-in-computed',
3579
3632
  });
3580
3633
 
3581
- const rule$9 = createUntrackedRule({
3634
+ const rule$a = createUntrackedRule({
3582
3635
  create(context) {
3583
3636
  const parserServices = ESLintUtils.getParserServices(context);
3584
3637
  const checker = parserServices.program.getTypeChecker();
@@ -3627,7 +3680,7 @@ const rule$9 = createUntrackedRule({
3627
3680
  name: 'no-signal-reads-after-await-in-reactive-context',
3628
3681
  });
3629
3682
 
3630
- const createRule$6 = ESLintUtils.RuleCreator((name) => name);
3683
+ const createRule$7 = ESLintUtils.RuleCreator((name) => name);
3631
3684
  function isStringLiteral(node) {
3632
3685
  return (node.type === AST_NODE_TYPES.Literal &&
3633
3686
  typeof node.value === 'string');
@@ -3691,7 +3744,7 @@ function hasTemplateLiteralAncestor(node) {
3691
3744
  }
3692
3745
  return false;
3693
3746
  }
3694
- const rule$8 = createRule$6({
3747
+ const rule$9 = createRule$7({
3695
3748
  create(context) {
3696
3749
  const { sourceCode } = context;
3697
3750
  let parserServices = null;
@@ -4111,7 +4164,7 @@ function buildReactiveCallReplacement(outerUntrackedCall, reactiveCall, sourceCo
4111
4164
  }
4112
4165
  return dedent$1(text, reactiveCall.loc.start.column - outerUntrackedCall.parent.loc.start.column);
4113
4166
  }
4114
- const rule$7 = createUntrackedRule({
4167
+ const rule$8 = createUntrackedRule({
4115
4168
  create(context) {
4116
4169
  const parserServices = ESLintUtils.getParserServices(context);
4117
4170
  const checker = parserServices.program.getTypeChecker();
@@ -4135,20 +4188,12 @@ const rule$7 = createUntrackedRule({
4135
4188
  }
4136
4189
  return {
4137
4190
  CallExpression(node) {
4138
- if (!isAngularUntrackedCall(node, program)) {
4139
- return;
4140
- }
4141
- if (findEnclosingReactiveScope(node, program) ||
4142
- findEnclosingReactiveScopeAfterAsyncBoundary(node, program)) {
4143
- return;
4144
- }
4145
- if (isAllowedImperativeAngularContext(node)) {
4146
- return;
4147
- }
4148
- if (isAllowedDeferredCallbackContext(node, checker, esTreeNodeToTSNodeMap)) {
4149
- return;
4150
- }
4151
- if (isAllowedLazyAngularFactoryContext(node, program)) {
4191
+ if (!isAngularUntrackedCall(node, program) ||
4192
+ findEnclosingReactiveScope(node, program) ||
4193
+ findEnclosingReactiveScopeAfterAsyncBoundary(node, program) ||
4194
+ isAllowedImperativeAngularContext(node) ||
4195
+ isAllowedDeferredCallbackContext(node, checker, esTreeNodeToTSNodeMap) ||
4196
+ isAllowedLazyAngularFactoryContext(node, program)) {
4152
4197
  return;
4153
4198
  }
4154
4199
  const reactiveCall = getFixableReactiveCall(node, program);
@@ -4255,10 +4300,8 @@ function hasOpaqueSynchronousCalls(root, checker, esTreeNodeToTSNodeMap, program
4255
4300
  }
4256
4301
  return;
4257
4302
  }
4258
- if (node.type !== AST_NODE_TYPES.CallExpression) {
4259
- return;
4260
- }
4261
- if (isAngularUntrackedCall(node, program) ||
4303
+ if (node.type !== AST_NODE_TYPES.CallExpression ||
4304
+ isAngularUntrackedCall(node, program) ||
4262
4305
  isSignalReadCall(node, checker, esTreeNodeToTSNodeMap) ||
4263
4306
  isWritableSignalWrite(node, checker, esTreeNodeToTSNodeMap)) {
4264
4307
  return;
@@ -4268,7 +4311,7 @@ function hasOpaqueSynchronousCalls(root, checker, esTreeNodeToTSNodeMap, program
4268
4311
  });
4269
4312
  return found;
4270
4313
  }
4271
- const rule$6 = createUntrackedRule({
4314
+ const rule$7 = createUntrackedRule({
4272
4315
  create(context) {
4273
4316
  const parserServices = ESLintUtils.getParserServices(context);
4274
4317
  const checker = parserServices.program.getTypeChecker();
@@ -4289,14 +4332,12 @@ const rule$6 = createUntrackedRule({
4289
4332
  return;
4290
4333
  }
4291
4334
  const { reads } = collectSignalUsages(arg, checker, esTreeNodeToTSNodeMap, program);
4292
- if (reads.length > 0) {
4335
+ if (reads.length > 0 ||
4336
+ hasOpaqueSynchronousCalls(arg, checker, esTreeNodeToTSNodeMap, program)) {
4293
4337
  // Snapshot reads inside reactive callbacks are a valid Angular
4294
4338
  // pattern even when the snapshot later influences branching.
4295
4339
  return;
4296
4340
  }
4297
- if (hasOpaqueSynchronousCalls(arg, checker, esTreeNodeToTSNodeMap, program)) {
4298
- return;
4299
- }
4300
4341
  // Only fix when the parent is a plain ExpressionStatement so we can
4301
4342
  // replace statement-for-statement without breaking surrounding structure.
4302
4343
  const parent = untrackedCall.parent;
@@ -4358,8 +4399,8 @@ const rule$6 = createUntrackedRule({
4358
4399
  name: 'no-useless-untracked',
4359
4400
  });
4360
4401
 
4361
- const createRule$5 = ESLintUtils.RuleCreator((name) => name);
4362
- const rule$5 = createRule$5({
4402
+ const createRule$6 = ESLintUtils.RuleCreator((name) => name);
4403
+ const rule$6 = createRule$6({
4363
4404
  create(context, [{ printWidth }]) {
4364
4405
  const sourceCode = context.sourceCode;
4365
4406
  const getLineEndIndex = (lineStartIndex) => {
@@ -4611,6 +4652,169 @@ const rule$5 = createRule$5({
4611
4652
  name: 'object-single-line',
4612
4653
  });
4613
4654
 
4655
+ const createRule$5 = ESLintUtils.RuleCreator((name) => name);
4656
+ const EMPTY_ARGUMENT = '__EMPTY_ARGUMENT__';
4657
+ function getParenthesizedInner(node) {
4658
+ const maybeNode = node;
4659
+ if (maybeNode.type === 'ParenthesizedExpression') {
4660
+ return maybeNode.expression ?? null;
4661
+ }
4662
+ return null;
4663
+ }
4664
+ function unwrapParenthesized(node) {
4665
+ let current = node;
4666
+ let inner = getParenthesizedInner(current);
4667
+ while (inner) {
4668
+ current = inner;
4669
+ inner = getParenthesizedInner(current);
4670
+ }
4671
+ return current;
4672
+ }
4673
+ function isSupportedControlFlowStatement(node) {
4674
+ return (node.type === AST_NODE_TYPES.BreakStatement ||
4675
+ node.type === AST_NODE_TYPES.ContinueStatement ||
4676
+ node.type === AST_NODE_TYPES.ReturnStatement ||
4677
+ node.type === AST_NODE_TYPES.ThrowStatement);
4678
+ }
4679
+ function getControlFlowStatement(node) {
4680
+ if (isSupportedControlFlowStatement(node)) {
4681
+ return node;
4682
+ }
4683
+ if (node.type === AST_NODE_TYPES.BlockStatement &&
4684
+ node.body.length === 1 &&
4685
+ isSupportedControlFlowStatement(node.body[0])) {
4686
+ return node.body[0];
4687
+ }
4688
+ return null;
4689
+ }
4690
+ function getControlFlowSignature(node, sourceCode) {
4691
+ if (node.alternate) {
4692
+ return null;
4693
+ }
4694
+ const controlFlowStatement = getControlFlowStatement(node.consequent);
4695
+ if (!controlFlowStatement) {
4696
+ return null;
4697
+ }
4698
+ switch (controlFlowStatement.type) {
4699
+ case AST_NODE_TYPES.BreakStatement:
4700
+ return controlFlowStatement.label
4701
+ ? `break:${sourceCode.getText(controlFlowStatement.label)}`
4702
+ : `break:${EMPTY_ARGUMENT}`;
4703
+ case AST_NODE_TYPES.ContinueStatement:
4704
+ return controlFlowStatement.label
4705
+ ? `continue:${sourceCode.getText(controlFlowStatement.label)}`
4706
+ : `continue:${EMPTY_ARGUMENT}`;
4707
+ case AST_NODE_TYPES.ReturnStatement:
4708
+ return controlFlowStatement.argument
4709
+ ? `return:${sourceCode.getText(unwrapParenthesized(controlFlowStatement.argument))}`
4710
+ : `return:${EMPTY_ARGUMENT}`;
4711
+ case AST_NODE_TYPES.ThrowStatement:
4712
+ return `throw:${sourceCode.getText(unwrapParenthesized(controlFlowStatement.argument))}`;
4713
+ }
4714
+ }
4715
+ function hasNonWhitespaceBetween(sourceCode, left, right) {
4716
+ return sourceCode.text.slice(left.range[1], right.range[0]).trim() !== '';
4717
+ }
4718
+ function needsParenthesesInOrChain(node) {
4719
+ if (getParenthesizedInner(node)) {
4720
+ return false;
4721
+ }
4722
+ switch (node.type) {
4723
+ case AST_NODE_TYPES.AssignmentExpression:
4724
+ case AST_NODE_TYPES.ConditionalExpression:
4725
+ case AST_NODE_TYPES.SequenceExpression:
4726
+ case AST_NODE_TYPES.TSAsExpression:
4727
+ case AST_NODE_TYPES.TSSatisfiesExpression:
4728
+ case AST_NODE_TYPES.YieldExpression:
4729
+ return true;
4730
+ case AST_NODE_TYPES.LogicalExpression:
4731
+ return node.operator !== '||';
4732
+ default:
4733
+ return false;
4734
+ }
4735
+ }
4736
+ function renderTest(node, sourceCode) {
4737
+ const text = sourceCode.getText(node);
4738
+ return needsParenthesesInOrChain(node) ? `(${text})` : text;
4739
+ }
4740
+ const rule$5 = createRule$5({
4741
+ create(context) {
4742
+ const { sourceCode } = context;
4743
+ function checkBody(statements) {
4744
+ let i = 0;
4745
+ while (i < statements.length) {
4746
+ const statement = statements[i];
4747
+ if (statement.type !== AST_NODE_TYPES.IfStatement) {
4748
+ i++;
4749
+ continue;
4750
+ }
4751
+ const signature = getControlFlowSignature(statement, sourceCode);
4752
+ if (!signature) {
4753
+ i++;
4754
+ continue;
4755
+ }
4756
+ const group = [statement];
4757
+ let j = i + 1;
4758
+ while (j < statements.length) {
4759
+ const nextStatement = statements[j];
4760
+ if (nextStatement.type !== AST_NODE_TYPES.IfStatement) {
4761
+ break;
4762
+ }
4763
+ if (!hasNonWhitespaceBetween(sourceCode, group[group.length - 1], nextStatement) &&
4764
+ sourceCode.getCommentsInside(nextStatement).length === 0 &&
4765
+ getControlFlowSignature(nextStatement, sourceCode) === signature) {
4766
+ group.push(nextStatement);
4767
+ j++;
4768
+ continue;
4769
+ }
4770
+ break;
4771
+ }
4772
+ if (group.length > 1) {
4773
+ for (const [index, ifStatement] of group.entries()) {
4774
+ context.report({
4775
+ ...(index === 0
4776
+ ? {
4777
+ fix(fixer) {
4778
+ const firstIf = group[0];
4779
+ const lastIf = group[group.length - 1];
4780
+ const condition = group
4781
+ .map((item) => renderTest(item.test, sourceCode))
4782
+ .join(' || ');
4783
+ return fixer.replaceTextRange([firstIf.range[0], lastIf.range[1]], `if (${condition}) ${sourceCode.getText(firstIf.consequent)}`);
4784
+ },
4785
+ }
4786
+ : {}),
4787
+ messageId: 'preferCombinedIfControlFlow',
4788
+ node: ifStatement,
4789
+ });
4790
+ }
4791
+ }
4792
+ i = j;
4793
+ }
4794
+ }
4795
+ return {
4796
+ BlockStatement(node) {
4797
+ checkBody(node.body);
4798
+ },
4799
+ Program(node) {
4800
+ checkBody(node.body);
4801
+ },
4802
+ };
4803
+ },
4804
+ meta: {
4805
+ docs: {
4806
+ description: 'Combine consecutive if statements that use the same return, break, continue, or throw statement into a single if statement.',
4807
+ },
4808
+ fixable: 'code',
4809
+ messages: {
4810
+ preferCombinedIfControlFlow: 'Combine consecutive if statements with identical return, break, continue, or throw statements.',
4811
+ },
4812
+ schema: [],
4813
+ type: 'suggestion',
4814
+ },
4815
+ name: 'prefer-combined-if-control-flow',
4816
+ });
4817
+
4614
4818
  const MESSAGE_ID$1 = 'prefer-deep-imports';
4615
4819
  const ERROR_MESSAGE = 'Import via root entry point is prohibited when nested entry points exist';
4616
4820
  const createRule$4 = ESLintUtils.RuleCreator(() => ERROR_MESSAGE);
@@ -4628,14 +4832,10 @@ var preferDeepImports = createRule$4({
4628
4832
  return;
4629
4833
  }
4630
4834
  const rootPackageName = getRootPackageName(rawImportPath);
4631
- if (!rootPackageName) {
4632
- return;
4633
- }
4634
- if (!allowedPackages.includes(rootPackageName)) {
4635
- return;
4636
- }
4637
- if (!isStrictMode &&
4638
- isAlreadyNestedImport(rawImportPath, rootPackageName)) {
4835
+ if (!rootPackageName ||
4836
+ !allowedPackages.includes(rootPackageName) ||
4837
+ (!isStrictMode &&
4838
+ isAlreadyNestedImport(rawImportPath, rootPackageName))) {
4639
4839
  return;
4640
4840
  }
4641
4841
  const importedSymbols = extractNamedImportedSymbols(node);
@@ -4866,10 +5066,7 @@ function mapSymbolsToEntryPointsUsingTypeChecker(importedSymbols, candidateEntry
4866
5066
  for (const importedSymbol of importedSymbols) {
4867
5067
  for (const relativeEntryDir of candidateEntryPoints) {
4868
5068
  const exportedNames = exportTableByEntryPoint.get(relativeEntryDir);
4869
- if (!exportedNames) {
4870
- continue;
4871
- }
4872
- if (!exportedNames.has(importedSymbol)) {
5069
+ if (!exportedNames?.has(importedSymbol)) {
4873
5070
  continue;
4874
5071
  }
4875
5072
  symbolToEntryPoint.set(importedSymbol, relativeEntryDir);
@@ -5152,10 +5349,8 @@ function isDomImperativeCall(node, checker, esTreeNodeToTSNodeMap) {
5152
5349
  }
5153
5350
  const signature = checker.getResolvedSignature(tsNode);
5154
5351
  const declaration = signature?.declaration;
5155
- if (!declaration) {
5156
- return false;
5157
- }
5158
- if (!LIB_DOM_FILE_PATTERN.test(declaration.getSourceFile().fileName)) {
5352
+ if (!declaration ||
5353
+ !LIB_DOM_FILE_PATTERN.test(declaration.getSourceFile().fileName)) {
5159
5354
  return false;
5160
5355
  }
5161
5356
  const returnType = checker.typeToString(checker.getReturnTypeOfSignature(signature));
@@ -5387,18 +5582,15 @@ function getReturnedExpression(node) {
5387
5582
  }
5388
5583
  function getWrappedSignalGetter(node, checker, esTreeNodeToTSNodeMap) {
5389
5584
  const [arg] = node.arguments;
5390
- if (!arg || arg.type === AST_NODE_TYPES.SpreadElement) {
5391
- return null;
5392
- }
5393
- if (arg.type !== AST_NODE_TYPES.ArrowFunctionExpression &&
5394
- arg.type !== AST_NODE_TYPES.FunctionExpression) {
5585
+ if (!arg ||
5586
+ arg.type === AST_NODE_TYPES.SpreadElement ||
5587
+ (arg.type !== AST_NODE_TYPES.ArrowFunctionExpression &&
5588
+ arg.type !== AST_NODE_TYPES.FunctionExpression)) {
5395
5589
  return null;
5396
5590
  }
5397
5591
  const body = getReturnedExpression(arg);
5398
- if (body?.type !== AST_NODE_TYPES.CallExpression) {
5399
- return null;
5400
- }
5401
- if (!isSignalReadCall(body, checker, esTreeNodeToTSNodeMap)) {
5592
+ if (body?.type !== AST_NODE_TYPES.CallExpression ||
5593
+ !isSignalReadCall(body, checker, esTreeNodeToTSNodeMap)) {
5402
5594
  return null;
5403
5595
  }
5404
5596
  const getter = unwrapExpression(body.callee);
@@ -5871,24 +6063,25 @@ const plugin = {
5871
6063
  'class-property-naming': classPropertyNaming,
5872
6064
  'decorator-key-sort': config$3,
5873
6065
  'flat-exports': flatExports,
5874
- 'host-attributes-sort': rule$i,
6066
+ 'host-attributes-sort': rule$j,
5875
6067
  'html-logical-properties': config$2,
5876
- 'injection-token-description': rule$h,
5877
- 'no-deep-imports': rule$g,
6068
+ 'injection-token-description': rule$i,
6069
+ 'no-deep-imports': rule$h,
5878
6070
  'no-deep-imports-to-indexed-packages': noDeepImportsToIndexedPackages,
5879
- 'no-fully-untracked-effect': rule$f,
6071
+ 'no-fully-untracked-effect': rule$g,
5880
6072
  'no-href-with-router-link': config$1,
5881
- 'no-implicit-public': rule$e,
5882
- 'no-legacy-peer-deps': rule$d,
5883
- 'no-playwright-empty-fill': rule$c,
6073
+ 'no-implicit-public': rule$f,
6074
+ 'no-legacy-peer-deps': rule$e,
6075
+ 'no-playwright-empty-fill': rule$d,
5884
6076
  'no-project-as-in-ng-template': config,
5885
- 'no-redundant-type-annotation': rule$b,
5886
- 'no-side-effects-in-computed': rule$a,
5887
- 'no-signal-reads-after-await-in-reactive-context': rule$9,
5888
- 'no-string-literal-concat': rule$8,
5889
- 'no-untracked-outside-reactive-context': rule$7,
5890
- 'no-useless-untracked': rule$6,
5891
- 'object-single-line': rule$5,
6077
+ 'no-redundant-type-annotation': rule$c,
6078
+ 'no-side-effects-in-computed': rule$b,
6079
+ 'no-signal-reads-after-await-in-reactive-context': rule$a,
6080
+ 'no-string-literal-concat': rule$9,
6081
+ 'no-untracked-outside-reactive-context': rule$8,
6082
+ 'no-useless-untracked': rule$7,
6083
+ 'object-single-line': rule$6,
6084
+ 'prefer-combined-if-control-flow': rule$5,
5892
6085
  'prefer-deep-imports': preferDeepImports,
5893
6086
  'prefer-multi-arg-push': rule$4,
5894
6087
  'prefer-untracked-incidental-signal-reads': rule$3,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.471.0",
3
+ "version": "0.473.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,5 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const rule: ESLintUtils.RuleModule<"preferCombinedIfControlFlow", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
5
+ export default rule;