@taiga-ui/eslint-plugin-experience-next 0.472.0 → 0.474.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
@@ -54,12 +54,13 @@ export default [
54
54
  | no-playwright-empty-fill | Enforce `clear()` over `fill('')` in Playwright tests | ✅ | 🔧 | |
55
55
  | no-project-as-in-ng-template | `ngProjectAs` has no effect inside `<ng-template>` or dynamic outlets | ✅ | | |
56
56
  | no-redundant-type-annotation | Disallow redundant type annotations when the type is already inferred from the initializer | ✅ | 🔧 | |
57
- | no-side-effects-in-computed | Disallow observable side effects inside Angular `computed()` callbacks | ✅ | | |
57
+ | no-side-effects-in-computed | Disallow side effects and effectful helper calls inside Angular `computed()` callbacks | ✅ | | |
58
58
  | no-signal-reads-after-await-in-reactive-context | Disallow bare signal reads after `await` inside reactive callbacks | ✅ | | |
59
59
  | no-string-literal-concat | Disallow string literal concatenation; merge adjacent literals into one | ✅ | 🔧 | |
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 | ✅ | 🔧 | |
@@ -680,8 +681,9 @@ const doubled = computed(() => {
680
681
  <sup>`✅ Recommended`</sup>
681
682
 
682
683
  `computed()` should only derive a value from its inputs. This rule reports observable side effects inside Angular
683
- `computed()` callbacks, including signal writes (`.set()`, `.update()`, `.mutate()`), assignments to captured state,
684
- `++/--`, `delete`, and property mutations on objects that were not created inside the computation itself.
684
+ `computed()` callbacks, including signal writes (`.set()`, `.update()`, `.mutate()`), `effect()`, `inject()`,
685
+ assignments to captured state, `++/--`, `delete`, property mutations on objects that were not created inside the
686
+ computation itself, and calls to local helper functions or methods when their bodies perform those operations.
685
687
 
686
688
  ```ts
687
689
  // ❌ error
@@ -690,8 +692,12 @@ import {computed, signal} from '@angular/core';
690
692
  const source = signal(0);
691
693
  const target = signal(0);
692
694
 
693
- const derived = computed(() => {
695
+ function syncTarget(): void {
694
696
  target.set(source() + 1);
697
+ }
698
+
699
+ const derived = computed(() => {
700
+ syncTarget();
695
701
  return target();
696
702
  });
697
703
  ```
@@ -863,6 +869,98 @@ const x = {foo: bar};
863
869
 
864
870
  ---
865
871
 
872
+ ## prefer-combined-if-control-flow
873
+
874
+ <sup>`✅ Recommended`</sup> <sup>`Fixable`</sup>
875
+
876
+ Combine consecutive `if` statements when they have no `else` branch and use the same `return`, `break`, `continue`, or
877
+ `throw` statement. The autofix merges their conditions with `||`, while intentionally skipping cases with intervening
878
+ code or comments that should remain a separate control-flow boundary.
879
+
880
+ ```ts
881
+ // ❌ error
882
+ while (true) {
883
+ if (a) continue;
884
+ if (b && c) continue;
885
+ }
886
+
887
+ // ✅ after autofix
888
+ while (true) {
889
+ if (a || (b && c)) continue;
890
+ }
891
+ ```
892
+
893
+ ```ts
894
+ // ❌ error
895
+ if (a || b) {
896
+ return;
897
+ }
898
+
899
+ if (c) {
900
+ return;
901
+ }
902
+
903
+ // ✅ after autofix
904
+ if (a || b || c) {
905
+ return;
906
+ }
907
+ ```
908
+
909
+ ```ts
910
+ // ❌ error
911
+ if (isInvalid) return result;
912
+
913
+ if (isLegacy && shouldStop) return result;
914
+
915
+ // ✅ after autofix
916
+ if (isInvalid || (isLegacy && shouldStop)) return result;
917
+ ```
918
+
919
+ ```ts
920
+ // ❌ error
921
+ while (true) {
922
+ if (isDone) break;
923
+ if (hasError) break;
924
+ }
925
+
926
+ // ✅ after autofix
927
+ while (true) {
928
+ if (isDone || hasError) break;
929
+ }
930
+ ```
931
+
932
+ ```ts
933
+ // ❌ error
934
+ if (isFatal) throw error;
935
+
936
+ if (isExpired && shouldAbort) throw error;
937
+
938
+ // ✅ after autofix
939
+ if (isFatal || (isExpired && shouldAbort)) throw error;
940
+ ```
941
+
942
+ ```ts
943
+ // not changed — different control flow
944
+ while (true) {
945
+ if (isDone) continue;
946
+ if (hasError) break;
947
+ }
948
+ ```
949
+
950
+ ```ts
951
+ // not changed — comment keeps branches separate
952
+ if (a) {
953
+ return value;
954
+ }
955
+
956
+ // explain why this branch exists
957
+ if (b) {
958
+ return value;
959
+ }
960
+ ```
961
+
962
+ ---
963
+
866
964
  ## prefer-deep-imports
867
965
 
868
966
  <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
@@ -2025,7 +2021,7 @@ const config$2 = {
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
2023
  const NG_DEV_MODE = 'ngDevMode';
2028
- const createRule$e = ESLintUtils.RuleCreator((name) => name);
2024
+ const createRule$f = ESLintUtils.RuleCreator((name) => name);
2029
2025
  function getVariableName(node) {
2030
2026
  if (node.parent.type !== AST_NODE_TYPES$1.VariableDeclarator) {
2031
2027
  return undefined;
@@ -2096,7 +2092,7 @@ function getNgDevModeDeclarationFix(program, fixer) {
2096
2092
  }
2097
2093
  return fixer.insertTextBeforeRange([0, 0], 'declare const ngDevMode: boolean;\n');
2098
2094
  }
2099
- const rule$h = createRule$e({
2095
+ const rule$i = createRule$f({
2100
2096
  create(context) {
2101
2097
  const { sourceCode } = context;
2102
2098
  const program = sourceCode.ast;
@@ -2164,8 +2160,8 @@ const DEFAULT_OPTIONS = {
2164
2160
  importDeclaration: '^@taiga-ui*',
2165
2161
  projectName: String.raw `(?<=^@taiga-ui/)([-\w]+)`,
2166
2162
  };
2167
- const createRule$d = ESLintUtils.RuleCreator((name) => name);
2168
- const rule$g = createRule$d({
2163
+ const createRule$e = ESLintUtils.RuleCreator((name) => name);
2164
+ const rule$h = createRule$e({
2169
2165
  create(context) {
2170
2166
  const { currentProject, deepImport, ignoreImports, importDeclaration, projectName, } = { ...DEFAULT_OPTIONS, ...context.options[0] };
2171
2167
  const hasNonCodeExtension = (source) => {
@@ -2252,13 +2248,13 @@ const rule$g = createRule$d({
2252
2248
  name: 'no-deep-imports',
2253
2249
  });
2254
2250
 
2255
- const createRule$c = ESLintUtils.RuleCreator((name) => name);
2251
+ const createRule$d = ESLintUtils.RuleCreator((name) => name);
2256
2252
  const resolveCacheByOptions = new WeakMap();
2257
2253
  const nearestFileUpCache = new Map();
2258
2254
  const markerCache = new Map();
2259
2255
  const indexFileCache = new Map();
2260
2256
  const indexExportsCache = new Map();
2261
- var noDeepImportsToIndexedPackages = createRule$c({
2257
+ var noDeepImportsToIndexedPackages = createRule$d({
2262
2258
  create(context) {
2263
2259
  const parserServices = ESLintUtils.getParserServices(context);
2264
2260
  const program = parserServices.program;
@@ -2364,13 +2360,9 @@ var noDeepImportsToIndexedPackages = createRule$c({
2364
2360
  return {
2365
2361
  ImportDeclaration(node) {
2366
2362
  const importSpecifier = node.source.value;
2367
- if (typeof importSpecifier !== 'string') {
2368
- return;
2369
- }
2370
- if (!importSpecifier.includes('/')) {
2371
- return;
2372
- }
2373
- if (!isExternalModuleSpecifier(importSpecifier)) {
2363
+ if (typeof importSpecifier !== 'string' ||
2364
+ !importSpecifier.includes('/') ||
2365
+ !isExternalModuleSpecifier(importSpecifier)) {
2374
2366
  return;
2375
2367
  }
2376
2368
  const packageRootSpecifier = getPackageRootSpecifier(importSpecifier);
@@ -2438,10 +2430,8 @@ function getPackageRootSpecifier(importSpecifier) {
2438
2430
  return pathParts[0] ?? importSpecifier;
2439
2431
  }
2440
2432
  function getSubpath(importSpecifier, packageRootSpecifier) {
2441
- if (importSpecifier === packageRootSpecifier) {
2442
- return null;
2443
- }
2444
- if (!importSpecifier.startsWith(`${packageRootSpecifier}/`)) {
2433
+ if (importSpecifier === packageRootSpecifier ||
2434
+ !importSpecifier.startsWith(`${packageRootSpecifier}/`)) {
2445
2435
  return null;
2446
2436
  }
2447
2437
  return importSpecifier.slice(packageRootSpecifier.length + 1);
@@ -2516,10 +2506,8 @@ function getOrderedChildren(node) {
2516
2506
  ];
2517
2507
  return children.filter((child) => child !== undefined && child !== null);
2518
2508
  }
2519
- if (node.type === AST_NODE_TYPES.BlockStatement) {
2520
- return node.body;
2521
- }
2522
- if (node.type === AST_NODE_TYPES.Program) {
2509
+ if (node.type === AST_NODE_TYPES.BlockStatement ||
2510
+ node.type === AST_NODE_TYPES.Program) {
2523
2511
  return node.body;
2524
2512
  }
2525
2513
  if (node.type === AST_NODE_TYPES.IfStatement) {
@@ -2580,10 +2568,7 @@ function getOrderedChildren(node) {
2580
2568
  function walkSynchronousAst(root, visitor) {
2581
2569
  traverse(root, true);
2582
2570
  function traverse(node, isRoot = false) {
2583
- if (visitor(node) === false) {
2584
- return false;
2585
- }
2586
- if (!isRoot && isFunctionLike$1(node)) {
2571
+ if (visitor(node) === false || (!isRoot && isFunctionLike$1(node))) {
2587
2572
  return false;
2588
2573
  }
2589
2574
  if (node.type === AST_NODE_TYPES.AwaitExpression) {
@@ -2733,6 +2718,9 @@ function appendObjectPropertyReactiveScopes(scopes, call, object, labels) {
2733
2718
  function isAngularEffectCall(node, program) {
2734
2719
  return isAngularCoreCall(node, program, 'effect');
2735
2720
  }
2721
+ function isAngularInjectCall(node, program) {
2722
+ return isAngularCoreCall(node, program, 'inject');
2723
+ }
2736
2724
  function isAngularUntrackedCall(node, program) {
2737
2725
  return isAngularCoreCall(node, program, 'untracked');
2738
2726
  }
@@ -2864,10 +2852,8 @@ function isWritableSignalWrite(node, checker, esTreeNodeToTSNodeMap) {
2864
2852
  return false;
2865
2853
  }
2866
2854
  const { object, property } = node.callee;
2867
- if (property.type !== AST_NODE_TYPES.Identifier) {
2868
- return false;
2869
- }
2870
- if (!SIGNAL_WRITE_METHODS.has(property.name)) {
2855
+ if (property.type !== AST_NODE_TYPES.Identifier ||
2856
+ !SIGNAL_WRITE_METHODS.has(property.name)) {
2871
2857
  return false;
2872
2858
  }
2873
2859
  return isSignalType(object, checker, esTreeNodeToTSNodeMap);
@@ -2941,10 +2927,8 @@ const createUntrackedRule = ESLintUtils.RuleCreator((name) => `${UNTRACKED_RULES
2941
2927
  function collectReadsInsideUntracked(root, checker, esTreeNodeToTSNodeMap, program) {
2942
2928
  const reads = [];
2943
2929
  walkSynchronousAst(root, (node) => {
2944
- if (node.type !== AST_NODE_TYPES.CallExpression) {
2945
- return;
2946
- }
2947
- if (!isAngularUntrackedCall(node, program)) {
2930
+ if (node.type !== AST_NODE_TYPES.CallExpression ||
2931
+ !isAngularUntrackedCall(node, program)) {
2948
2932
  return;
2949
2933
  }
2950
2934
  const [arg] = node.arguments;
@@ -2963,7 +2947,7 @@ function collectReadsInsideUntracked(root, checker, esTreeNodeToTSNodeMap, progr
2963
2947
  });
2964
2948
  return reads;
2965
2949
  }
2966
- const rule$f = createUntrackedRule({
2950
+ const rule$g = createUntrackedRule({
2967
2951
  create(context) {
2968
2952
  const parserServices = ESLintUtils.getParserServices(context);
2969
2953
  const checker = parserServices.program.getTypeChecker();
@@ -3049,15 +3033,15 @@ const config$1 = {
3049
3033
  },
3050
3034
  };
3051
3035
 
3052
- const createRule$b = ESLintUtils.RuleCreator((name) => name);
3053
- const rule$e = createRule$b({
3036
+ const createRule$c = ESLintUtils.RuleCreator((name) => name);
3037
+ const rule$f = createRule$c({
3054
3038
  create(context) {
3055
3039
  const checkImplicitPublic = (node) => {
3056
3040
  const classRef = getClass(node);
3057
- if (!classRef || node.kind === 'constructor' || !!node?.accessibility) {
3058
- return;
3059
- }
3060
- if (node.key?.type === AST_NODE_TYPES.PrivateIdentifier) {
3041
+ if (!classRef ||
3042
+ node.kind === 'constructor' ||
3043
+ !!node?.accessibility ||
3044
+ node.key?.type === AST_NODE_TYPES.PrivateIdentifier) {
3061
3045
  return;
3062
3046
  }
3063
3047
  const name = node?.key?.name ||
@@ -3121,9 +3105,9 @@ function getClass(node) {
3121
3105
  return getClass(node.parent);
3122
3106
  }
3123
3107
 
3124
- const createRule$a = ESLintUtils.RuleCreator((name) => name);
3108
+ const createRule$b = ESLintUtils.RuleCreator((name) => name);
3125
3109
  const LEGACY_PEER_DEPS_PATTERN = /^legacy-peer-deps\s*=\s*true$/i;
3126
- const rule$d = createRule$a({
3110
+ const rule$e = createRule$b({
3127
3111
  create(context) {
3128
3112
  return {
3129
3113
  Program(node) {
@@ -3161,8 +3145,8 @@ const rule$d = createRule$a({
3161
3145
  name: 'no-legacy-peer-deps',
3162
3146
  });
3163
3147
 
3164
- const createRule$9 = ESLintUtils.RuleCreator((name) => name);
3165
- const rule$c = createRule$9({
3148
+ const createRule$a = ESLintUtils.RuleCreator((name) => name);
3149
+ const rule$d = createRule$a({
3166
3150
  create(context) {
3167
3151
  const services = ESLintUtils.getParserServices(context);
3168
3152
  const checker = services.program.getTypeChecker();
@@ -3327,7 +3311,7 @@ const config = {
3327
3311
  },
3328
3312
  };
3329
3313
 
3330
- const createRule$8 = ESLintUtils.RuleCreator((name) => name);
3314
+ const createRule$9 = ESLintUtils.RuleCreator((name) => name);
3331
3315
  function collectArrayExpressions(node) {
3332
3316
  const result = [];
3333
3317
  if (node.type === AST_NODE_TYPES.ArrayExpression) {
@@ -3353,7 +3337,7 @@ function collectArrayExpressions(node) {
3353
3337
  }
3354
3338
  return result;
3355
3339
  }
3356
- const rule$b = createRule$8({
3340
+ const rule$c = createRule$9({
3357
3341
  create(context) {
3358
3342
  const parserServices = ESLintUtils.getParserServices(context);
3359
3343
  const typeChecker = parserServices.program.getTypeChecker();
@@ -3481,7 +3465,12 @@ function unwrapExpression(expression) {
3481
3465
  return current;
3482
3466
  }
3483
3467
 
3484
- const createRule$7 = ESLintUtils.RuleCreator((name) => name);
3468
+ const createRule$8 = ESLintUtils.RuleCreator((name) => name);
3469
+ function isFunctionLikeScope(node) {
3470
+ return (node?.type === AST_NODE_TYPES.ArrowFunctionExpression ||
3471
+ node?.type === AST_NODE_TYPES.FunctionDeclaration ||
3472
+ node?.type === AST_NODE_TYPES.FunctionExpression);
3473
+ }
3485
3474
  function isReactiveCallback(node) {
3486
3475
  return (node?.type === AST_NODE_TYPES.ArrowFunctionExpression ||
3487
3476
  node?.type === AST_NODE_TYPES.FunctionExpression);
@@ -3526,7 +3515,7 @@ function getSymbolAtNode(node, checker, esTreeNodeToTSNodeMap) {
3526
3515
  }
3527
3516
  return checker.getSymbolAtLocation(tsNode) ?? null;
3528
3517
  }
3529
- function isLocalIdentifier(node, context) {
3518
+ function isLocalIdentifier(node, context, localScopes) {
3530
3519
  const symbol = getSymbolAtNode(node, context.checker, context.esTreeNodeToTSNodeMap);
3531
3520
  if (!symbol) {
3532
3521
  return false;
@@ -3534,10 +3523,24 @@ function isLocalIdentifier(node, context) {
3534
3523
  return (symbol.declarations ?? []).some((declaration) => {
3535
3524
  const estreeDeclaration = context.tsNodeToESTreeNodeMap.get(declaration);
3536
3525
  return (!!estreeDeclaration &&
3537
- isNodeInsideSynchronousReactiveScope(estreeDeclaration, context.callback));
3526
+ isDeclaredInsideLocalScope(estreeDeclaration, localScopes));
3527
+ });
3528
+ }
3529
+ function isDeclaredInsideLocalScope(node, localScopes) {
3530
+ return localScopes.some((scope) => isNodeInsideFunctionScope(node, scope));
3531
+ }
3532
+ function isNodeInsideFunctionScope(node, scope) {
3533
+ let found = false;
3534
+ walkSynchronousAst(scope, (inner) => {
3535
+ if (inner !== node) {
3536
+ return;
3537
+ }
3538
+ found = true;
3539
+ return false;
3538
3540
  });
3541
+ return found;
3539
3542
  }
3540
- function isLocallyCreatedExpression(node, context) {
3543
+ function isLocallyCreatedExpression(node, context, localScopes) {
3541
3544
  const expression = unwrapExpression(node);
3542
3545
  switch (expression.type) {
3543
3546
  case AST_NODE_TYPES.ArrayExpression:
@@ -3545,65 +3548,227 @@ function isLocallyCreatedExpression(node, context) {
3545
3548
  case AST_NODE_TYPES.ObjectExpression:
3546
3549
  return true;
3547
3550
  case AST_NODE_TYPES.Identifier:
3548
- return isLocalIdentifier(expression, context);
3551
+ return isLocalIdentifier(expression, context, localScopes);
3549
3552
  case AST_NODE_TYPES.MemberExpression:
3550
- return isLocallyCreatedExpression(expression.object, context);
3553
+ return isLocallyCreatedExpression(expression.object, context, localScopes);
3551
3554
  default:
3552
3555
  return false;
3553
3556
  }
3554
3557
  }
3555
- function hasObservableMutationTarget(node, context) {
3558
+ function hasObservableMutationTarget(node, context, localScopes) {
3556
3559
  return collectMutationTargets(node).some((target) => {
3557
3560
  if (target.type === AST_NODE_TYPES.Identifier) {
3558
- return !isLocalIdentifier(target, context);
3561
+ return !isLocalIdentifier(target, context, localScopes);
3559
3562
  }
3560
- return !isLocallyCreatedExpression(target.object, context);
3563
+ return !isLocallyCreatedExpression(target.object, context, localScopes);
3561
3564
  });
3562
3565
  }
3563
- function reportSideEffect(node, inspectionContext, report) {
3566
+ function reportSideEffect(node, context, report) {
3564
3567
  const key = String(node.range);
3565
- if (inspectionContext.reported.has(key)) {
3568
+ if (context.reported.has(key)) {
3566
3569
  return;
3567
3570
  }
3568
- inspectionContext.reported.add(key);
3571
+ context.reported.add(key);
3569
3572
  report(node);
3570
3573
  }
3571
- function inspectComputedBody(root, inspectionContext, report) {
3574
+ function isDirectAngularSideEffectCall(node, context) {
3575
+ return (isWritableSignalWrite(node, context.checker, context.esTreeNodeToTSNodeMap) ||
3576
+ isAngularEffectCall(node, context.program) ||
3577
+ isAngularInjectCall(node, context.program));
3578
+ }
3579
+ function isInspectableFunctionContainer(node) {
3580
+ return (!!node &&
3581
+ (isFunctionLikeScope(node) ||
3582
+ node.type === AST_NODE_TYPES.MethodDefinition ||
3583
+ node.type === AST_NODE_TYPES.Property ||
3584
+ node.type === AST_NODE_TYPES.PropertyDefinition ||
3585
+ node.type === AST_NODE_TYPES.VariableDeclarator));
3586
+ }
3587
+ function resolveFunctionLikeFromContainer(node, context, seenSymbols = new Set()) {
3588
+ if (isFunctionLikeScope(node)) {
3589
+ return [node];
3590
+ }
3591
+ if ((node.type === AST_NODE_TYPES.MethodDefinition &&
3592
+ isFunctionLikeScope(node.value)) ||
3593
+ (node.type === AST_NODE_TYPES.Property && isFunctionLikeScope(node.value))) {
3594
+ return [node.value];
3595
+ }
3596
+ if (node.type === AST_NODE_TYPES.Property &&
3597
+ node.value.type === AST_NODE_TYPES.Identifier) {
3598
+ return resolveFunctionLikeFromIdentifier(node.value, context, seenSymbols);
3599
+ }
3600
+ if (node.type === AST_NODE_TYPES.PropertyDefinition &&
3601
+ node.value &&
3602
+ isFunctionLikeScope(node.value)) {
3603
+ return [node.value];
3604
+ }
3605
+ if (node.type === AST_NODE_TYPES.PropertyDefinition &&
3606
+ node.value?.type === AST_NODE_TYPES.Identifier) {
3607
+ return resolveFunctionLikeFromIdentifier(node.value, context, seenSymbols);
3608
+ }
3609
+ if (node.type === AST_NODE_TYPES.VariableDeclarator) {
3610
+ const { init } = node;
3611
+ if (init && isFunctionLikeScope(init)) {
3612
+ return [init];
3613
+ }
3614
+ if (init?.type === AST_NODE_TYPES.Identifier) {
3615
+ return resolveFunctionLikeFromIdentifier(init, context, seenSymbols);
3616
+ }
3617
+ }
3618
+ return [];
3619
+ }
3620
+ function resolveFunctionLikeFromIdentifier(node, context, seenSymbols = new Set()) {
3621
+ const symbol = getSymbolAtNode(node, context.checker, context.esTreeNodeToTSNodeMap);
3622
+ if (!symbol) {
3623
+ return [];
3624
+ }
3625
+ const symbolId = `${symbol.name}:${symbol.declarations?.[0]?.pos ?? -1}`;
3626
+ if (seenSymbols.has(symbolId)) {
3627
+ return [];
3628
+ }
3629
+ seenSymbols.add(symbolId);
3630
+ return (symbol.declarations ?? []).flatMap((declaration) => {
3631
+ const estreeDeclaration = context.tsNodeToESTreeNodeMap.get(declaration);
3632
+ if (!estreeDeclaration || !isInspectableFunctionContainer(estreeDeclaration)) {
3633
+ return [];
3634
+ }
3635
+ return resolveFunctionLikeFromContainer(estreeDeclaration, context, seenSymbols);
3636
+ });
3637
+ }
3638
+ function resolveCalledFunctions(node, context) {
3639
+ const resolved = new Map();
3640
+ const tsNode = context.esTreeNodeToTSNodeMap.get(node);
3641
+ const signature = tsNode ? context.checker.getResolvedSignature(tsNode) : undefined;
3642
+ const declarations = new Set();
3643
+ if (signature?.declaration) {
3644
+ declarations.add(signature.declaration);
3645
+ }
3646
+ const callee = unwrapExpression(node.callee);
3647
+ if (callee.type === AST_NODE_TYPES.Identifier ||
3648
+ callee.type === AST_NODE_TYPES.MemberExpression) {
3649
+ const symbol = getSymbolAtNode(callee, context.checker, context.esTreeNodeToTSNodeMap);
3650
+ for (const declaration of symbol?.declarations ?? []) {
3651
+ declarations.add(declaration);
3652
+ }
3653
+ }
3654
+ for (const declaration of declarations) {
3655
+ const estreeDeclaration = context.tsNodeToESTreeNodeMap.get(declaration);
3656
+ if (!estreeDeclaration || !isInspectableFunctionContainer(estreeDeclaration)) {
3657
+ continue;
3658
+ }
3659
+ for (const fn of resolveFunctionLikeFromContainer(estreeDeclaration, context)) {
3660
+ resolved.set(String(fn.range), fn);
3661
+ }
3662
+ }
3663
+ return [...resolved.values()];
3664
+ }
3665
+ function functionHasObservableSideEffects(root, context, localScopes, visitedFunctions) {
3666
+ let hasSideEffect = false;
3572
3667
  walkSynchronousAst(root, (node) => {
3573
3668
  if (node.type === AST_NODE_TYPES.CallExpression) {
3574
- if (isWritableSignalWrite(node, inspectionContext.checker, inspectionContext.esTreeNodeToTSNodeMap)) {
3575
- reportSideEffect(node, inspectionContext, report);
3669
+ if (isDirectAngularSideEffectCall(node, context)) {
3670
+ hasSideEffect = true;
3671
+ return false;
3576
3672
  }
3577
- if (isAngularUntrackedCall(node, inspectionContext.program)) {
3673
+ if (isAngularUntrackedCall(node, context.program)) {
3674
+ const [arg] = node.arguments;
3675
+ if (isReactiveCallback(arg) &&
3676
+ functionHasObservableSideEffects(arg, context, [...localScopes, arg], visitedFunctions)) {
3677
+ hasSideEffect = true;
3678
+ }
3679
+ return false;
3680
+ }
3681
+ if (isReactiveCallback(node.callee)) {
3682
+ if (functionHasObservableSideEffects(node.callee, context, [...localScopes, node.callee], visitedFunctions)) {
3683
+ hasSideEffect = true;
3684
+ }
3685
+ return false;
3686
+ }
3687
+ for (const calledFunction of resolveCalledFunctions(node, context)) {
3688
+ const key = String(calledFunction.range);
3689
+ if (visitedFunctions.has(key)) {
3690
+ continue;
3691
+ }
3692
+ visitedFunctions.add(key);
3693
+ const calledFunctionHasSideEffects = functionHasObservableSideEffects(calledFunction, context, [...localScopes, calledFunction], visitedFunctions);
3694
+ visitedFunctions.delete(key);
3695
+ if (!calledFunctionHasSideEffects) {
3696
+ continue;
3697
+ }
3698
+ hasSideEffect = true;
3699
+ return false;
3700
+ }
3701
+ }
3702
+ if (node.type === AST_NODE_TYPES.AssignmentExpression &&
3703
+ hasObservableMutationTarget(node.left, context, localScopes)) {
3704
+ hasSideEffect = true;
3705
+ return false;
3706
+ }
3707
+ if (node.type === AST_NODE_TYPES.UpdateExpression &&
3708
+ hasObservableMutationTarget(node.argument, context, localScopes)) {
3709
+ hasSideEffect = true;
3710
+ return false;
3711
+ }
3712
+ if (node.type === AST_NODE_TYPES.UnaryExpression &&
3713
+ node.operator === 'delete' &&
3714
+ hasObservableMutationTarget(node.argument, context, localScopes)) {
3715
+ hasSideEffect = true;
3716
+ return false;
3717
+ }
3718
+ return;
3719
+ });
3720
+ return hasSideEffect;
3721
+ }
3722
+ function inspectComputedBody(root, context, localScopes, visitedFunctions, report) {
3723
+ walkSynchronousAst(root, (node) => {
3724
+ if (node.type === AST_NODE_TYPES.CallExpression) {
3725
+ if (isDirectAngularSideEffectCall(node, context)) {
3726
+ reportSideEffect(node, context, report);
3727
+ return false;
3728
+ }
3729
+ if (isAngularUntrackedCall(node, context.program)) {
3578
3730
  const [arg] = node.arguments;
3579
3731
  if (isReactiveCallback(arg)) {
3580
- inspectComputedBody(arg, inspectionContext, report);
3732
+ inspectComputedBody(arg, context, [...localScopes, arg], visitedFunctions, report);
3581
3733
  }
3582
3734
  return false;
3583
3735
  }
3584
- if (node.callee.type === AST_NODE_TYPES.ArrowFunctionExpression ||
3585
- node.callee.type === AST_NODE_TYPES.FunctionExpression) {
3586
- inspectComputedBody(node.callee, inspectionContext, report);
3736
+ if (isReactiveCallback(node.callee)) {
3737
+ inspectComputedBody(node.callee, context, [...localScopes, node.callee], visitedFunctions, report);
3738
+ return false;
3739
+ }
3740
+ for (const calledFunction of resolveCalledFunctions(node, context)) {
3741
+ const key = String(calledFunction.range);
3742
+ if (visitedFunctions.has(key)) {
3743
+ continue;
3744
+ }
3745
+ visitedFunctions.add(key);
3746
+ const calledFunctionHasSideEffects = functionHasObservableSideEffects(calledFunction, context, [...localScopes, calledFunction], visitedFunctions);
3747
+ visitedFunctions.delete(key);
3748
+ if (!calledFunctionHasSideEffects) {
3749
+ continue;
3750
+ }
3751
+ reportSideEffect(node, context, report);
3587
3752
  return false;
3588
3753
  }
3589
3754
  }
3590
3755
  if (node.type === AST_NODE_TYPES.AssignmentExpression &&
3591
- hasObservableMutationTarget(node.left, inspectionContext)) {
3592
- reportSideEffect(node.left, inspectionContext, report);
3756
+ hasObservableMutationTarget(node.left, context, localScopes)) {
3757
+ reportSideEffect(node.left, context, report);
3593
3758
  }
3594
3759
  if (node.type === AST_NODE_TYPES.UpdateExpression &&
3595
- hasObservableMutationTarget(node.argument, inspectionContext)) {
3596
- reportSideEffect(node.argument, inspectionContext, report);
3760
+ hasObservableMutationTarget(node.argument, context, localScopes)) {
3761
+ reportSideEffect(node.argument, context, report);
3597
3762
  }
3598
3763
  if (node.type === AST_NODE_TYPES.UnaryExpression &&
3599
3764
  node.operator === 'delete' &&
3600
- hasObservableMutationTarget(node.argument, inspectionContext)) {
3601
- reportSideEffect(node.argument, inspectionContext, report);
3765
+ hasObservableMutationTarget(node.argument, context, localScopes)) {
3766
+ reportSideEffect(node.argument, context, report);
3602
3767
  }
3603
3768
  return;
3604
3769
  });
3605
3770
  }
3606
- const rule$a = createRule$7({
3771
+ const rule$b = createRule$8({
3607
3772
  create(context) {
3608
3773
  const parserServices = ESLintUtils.getParserServices(context);
3609
3774
  const checker = parserServices.program.getTypeChecker();
@@ -3617,15 +3782,14 @@ const rule$a = createRule$7({
3617
3782
  if (scope.kind !== 'computed()') {
3618
3783
  continue;
3619
3784
  }
3620
- const inspectionContext = {
3621
- callback: scope.callback,
3785
+ const analysisContext = {
3622
3786
  checker,
3623
3787
  esTreeNodeToTSNodeMap,
3624
3788
  program,
3625
3789
  reported: new Set(),
3626
3790
  tsNodeToESTreeNodeMap,
3627
3791
  };
3628
- inspectComputedBody(scope.callback, inspectionContext, (reportNode) => {
3792
+ inspectComputedBody(scope.callback, analysisContext, [scope.callback], new Set([String(scope.callback.range)]), (reportNode) => {
3629
3793
  context.report({
3630
3794
  data: { expression: sourceCode.getText(reportNode) },
3631
3795
  messageId: 'sideEffectInComputed',
@@ -3650,7 +3814,7 @@ const rule$a = createRule$7({
3650
3814
  name: 'no-side-effects-in-computed',
3651
3815
  });
3652
3816
 
3653
- const rule$9 = createUntrackedRule({
3817
+ const rule$a = createUntrackedRule({
3654
3818
  create(context) {
3655
3819
  const parserServices = ESLintUtils.getParserServices(context);
3656
3820
  const checker = parserServices.program.getTypeChecker();
@@ -3699,7 +3863,7 @@ const rule$9 = createUntrackedRule({
3699
3863
  name: 'no-signal-reads-after-await-in-reactive-context',
3700
3864
  });
3701
3865
 
3702
- const createRule$6 = ESLintUtils.RuleCreator((name) => name);
3866
+ const createRule$7 = ESLintUtils.RuleCreator((name) => name);
3703
3867
  function isStringLiteral(node) {
3704
3868
  return (node.type === AST_NODE_TYPES.Literal &&
3705
3869
  typeof node.value === 'string');
@@ -3763,7 +3927,7 @@ function hasTemplateLiteralAncestor(node) {
3763
3927
  }
3764
3928
  return false;
3765
3929
  }
3766
- const rule$8 = createRule$6({
3930
+ const rule$9 = createRule$7({
3767
3931
  create(context) {
3768
3932
  const { sourceCode } = context;
3769
3933
  let parserServices = null;
@@ -4183,7 +4347,7 @@ function buildReactiveCallReplacement(outerUntrackedCall, reactiveCall, sourceCo
4183
4347
  }
4184
4348
  return dedent$1(text, reactiveCall.loc.start.column - outerUntrackedCall.parent.loc.start.column);
4185
4349
  }
4186
- const rule$7 = createUntrackedRule({
4350
+ const rule$8 = createUntrackedRule({
4187
4351
  create(context) {
4188
4352
  const parserServices = ESLintUtils.getParserServices(context);
4189
4353
  const checker = parserServices.program.getTypeChecker();
@@ -4207,20 +4371,12 @@ const rule$7 = createUntrackedRule({
4207
4371
  }
4208
4372
  return {
4209
4373
  CallExpression(node) {
4210
- if (!isAngularUntrackedCall(node, program)) {
4211
- return;
4212
- }
4213
- if (findEnclosingReactiveScope(node, program) ||
4214
- findEnclosingReactiveScopeAfterAsyncBoundary(node, program)) {
4215
- return;
4216
- }
4217
- if (isAllowedImperativeAngularContext(node)) {
4218
- return;
4219
- }
4220
- if (isAllowedDeferredCallbackContext(node, checker, esTreeNodeToTSNodeMap)) {
4221
- return;
4222
- }
4223
- if (isAllowedLazyAngularFactoryContext(node, program)) {
4374
+ if (!isAngularUntrackedCall(node, program) ||
4375
+ findEnclosingReactiveScope(node, program) ||
4376
+ findEnclosingReactiveScopeAfterAsyncBoundary(node, program) ||
4377
+ isAllowedImperativeAngularContext(node) ||
4378
+ isAllowedDeferredCallbackContext(node, checker, esTreeNodeToTSNodeMap) ||
4379
+ isAllowedLazyAngularFactoryContext(node, program)) {
4224
4380
  return;
4225
4381
  }
4226
4382
  const reactiveCall = getFixableReactiveCall(node, program);
@@ -4327,10 +4483,8 @@ function hasOpaqueSynchronousCalls(root, checker, esTreeNodeToTSNodeMap, program
4327
4483
  }
4328
4484
  return;
4329
4485
  }
4330
- if (node.type !== AST_NODE_TYPES.CallExpression) {
4331
- return;
4332
- }
4333
- if (isAngularUntrackedCall(node, program) ||
4486
+ if (node.type !== AST_NODE_TYPES.CallExpression ||
4487
+ isAngularUntrackedCall(node, program) ||
4334
4488
  isSignalReadCall(node, checker, esTreeNodeToTSNodeMap) ||
4335
4489
  isWritableSignalWrite(node, checker, esTreeNodeToTSNodeMap)) {
4336
4490
  return;
@@ -4340,7 +4494,7 @@ function hasOpaqueSynchronousCalls(root, checker, esTreeNodeToTSNodeMap, program
4340
4494
  });
4341
4495
  return found;
4342
4496
  }
4343
- const rule$6 = createUntrackedRule({
4497
+ const rule$7 = createUntrackedRule({
4344
4498
  create(context) {
4345
4499
  const parserServices = ESLintUtils.getParserServices(context);
4346
4500
  const checker = parserServices.program.getTypeChecker();
@@ -4361,14 +4515,12 @@ const rule$6 = createUntrackedRule({
4361
4515
  return;
4362
4516
  }
4363
4517
  const { reads } = collectSignalUsages(arg, checker, esTreeNodeToTSNodeMap, program);
4364
- if (reads.length > 0) {
4518
+ if (reads.length > 0 ||
4519
+ hasOpaqueSynchronousCalls(arg, checker, esTreeNodeToTSNodeMap, program)) {
4365
4520
  // Snapshot reads inside reactive callbacks are a valid Angular
4366
4521
  // pattern even when the snapshot later influences branching.
4367
4522
  return;
4368
4523
  }
4369
- if (hasOpaqueSynchronousCalls(arg, checker, esTreeNodeToTSNodeMap, program)) {
4370
- return;
4371
- }
4372
4524
  // Only fix when the parent is a plain ExpressionStatement so we can
4373
4525
  // replace statement-for-statement without breaking surrounding structure.
4374
4526
  const parent = untrackedCall.parent;
@@ -4430,8 +4582,8 @@ const rule$6 = createUntrackedRule({
4430
4582
  name: 'no-useless-untracked',
4431
4583
  });
4432
4584
 
4433
- const createRule$5 = ESLintUtils.RuleCreator((name) => name);
4434
- const rule$5 = createRule$5({
4585
+ const createRule$6 = ESLintUtils.RuleCreator((name) => name);
4586
+ const rule$6 = createRule$6({
4435
4587
  create(context, [{ printWidth }]) {
4436
4588
  const sourceCode = context.sourceCode;
4437
4589
  const getLineEndIndex = (lineStartIndex) => {
@@ -4683,6 +4835,169 @@ const rule$5 = createRule$5({
4683
4835
  name: 'object-single-line',
4684
4836
  });
4685
4837
 
4838
+ const createRule$5 = ESLintUtils.RuleCreator((name) => name);
4839
+ const EMPTY_ARGUMENT = '__EMPTY_ARGUMENT__';
4840
+ function getParenthesizedInner(node) {
4841
+ const maybeNode = node;
4842
+ if (maybeNode.type === 'ParenthesizedExpression') {
4843
+ return maybeNode.expression ?? null;
4844
+ }
4845
+ return null;
4846
+ }
4847
+ function unwrapParenthesized(node) {
4848
+ let current = node;
4849
+ let inner = getParenthesizedInner(current);
4850
+ while (inner) {
4851
+ current = inner;
4852
+ inner = getParenthesizedInner(current);
4853
+ }
4854
+ return current;
4855
+ }
4856
+ function isSupportedControlFlowStatement(node) {
4857
+ return (node.type === AST_NODE_TYPES.BreakStatement ||
4858
+ node.type === AST_NODE_TYPES.ContinueStatement ||
4859
+ node.type === AST_NODE_TYPES.ReturnStatement ||
4860
+ node.type === AST_NODE_TYPES.ThrowStatement);
4861
+ }
4862
+ function getControlFlowStatement(node) {
4863
+ if (isSupportedControlFlowStatement(node)) {
4864
+ return node;
4865
+ }
4866
+ if (node.type === AST_NODE_TYPES.BlockStatement &&
4867
+ node.body.length === 1 &&
4868
+ isSupportedControlFlowStatement(node.body[0])) {
4869
+ return node.body[0];
4870
+ }
4871
+ return null;
4872
+ }
4873
+ function getControlFlowSignature(node, sourceCode) {
4874
+ if (node.alternate) {
4875
+ return null;
4876
+ }
4877
+ const controlFlowStatement = getControlFlowStatement(node.consequent);
4878
+ if (!controlFlowStatement) {
4879
+ return null;
4880
+ }
4881
+ switch (controlFlowStatement.type) {
4882
+ case AST_NODE_TYPES.BreakStatement:
4883
+ return controlFlowStatement.label
4884
+ ? `break:${sourceCode.getText(controlFlowStatement.label)}`
4885
+ : `break:${EMPTY_ARGUMENT}`;
4886
+ case AST_NODE_TYPES.ContinueStatement:
4887
+ return controlFlowStatement.label
4888
+ ? `continue:${sourceCode.getText(controlFlowStatement.label)}`
4889
+ : `continue:${EMPTY_ARGUMENT}`;
4890
+ case AST_NODE_TYPES.ReturnStatement:
4891
+ return controlFlowStatement.argument
4892
+ ? `return:${sourceCode.getText(unwrapParenthesized(controlFlowStatement.argument))}`
4893
+ : `return:${EMPTY_ARGUMENT}`;
4894
+ case AST_NODE_TYPES.ThrowStatement:
4895
+ return `throw:${sourceCode.getText(unwrapParenthesized(controlFlowStatement.argument))}`;
4896
+ }
4897
+ }
4898
+ function hasNonWhitespaceBetween(sourceCode, left, right) {
4899
+ return sourceCode.text.slice(left.range[1], right.range[0]).trim() !== '';
4900
+ }
4901
+ function needsParenthesesInOrChain(node) {
4902
+ if (getParenthesizedInner(node)) {
4903
+ return false;
4904
+ }
4905
+ switch (node.type) {
4906
+ case AST_NODE_TYPES.AssignmentExpression:
4907
+ case AST_NODE_TYPES.ConditionalExpression:
4908
+ case AST_NODE_TYPES.SequenceExpression:
4909
+ case AST_NODE_TYPES.TSAsExpression:
4910
+ case AST_NODE_TYPES.TSSatisfiesExpression:
4911
+ case AST_NODE_TYPES.YieldExpression:
4912
+ return true;
4913
+ case AST_NODE_TYPES.LogicalExpression:
4914
+ return node.operator !== '||';
4915
+ default:
4916
+ return false;
4917
+ }
4918
+ }
4919
+ function renderTest(node, sourceCode) {
4920
+ const text = sourceCode.getText(node);
4921
+ return needsParenthesesInOrChain(node) ? `(${text})` : text;
4922
+ }
4923
+ const rule$5 = createRule$5({
4924
+ create(context) {
4925
+ const { sourceCode } = context;
4926
+ function checkBody(statements) {
4927
+ let i = 0;
4928
+ while (i < statements.length) {
4929
+ const statement = statements[i];
4930
+ if (statement.type !== AST_NODE_TYPES.IfStatement) {
4931
+ i++;
4932
+ continue;
4933
+ }
4934
+ const signature = getControlFlowSignature(statement, sourceCode);
4935
+ if (!signature) {
4936
+ i++;
4937
+ continue;
4938
+ }
4939
+ const group = [statement];
4940
+ let j = i + 1;
4941
+ while (j < statements.length) {
4942
+ const nextStatement = statements[j];
4943
+ if (nextStatement.type !== AST_NODE_TYPES.IfStatement) {
4944
+ break;
4945
+ }
4946
+ if (!hasNonWhitespaceBetween(sourceCode, group[group.length - 1], nextStatement) &&
4947
+ sourceCode.getCommentsInside(nextStatement).length === 0 &&
4948
+ getControlFlowSignature(nextStatement, sourceCode) === signature) {
4949
+ group.push(nextStatement);
4950
+ j++;
4951
+ continue;
4952
+ }
4953
+ break;
4954
+ }
4955
+ if (group.length > 1) {
4956
+ for (const [index, ifStatement] of group.entries()) {
4957
+ context.report({
4958
+ ...(index === 0
4959
+ ? {
4960
+ fix(fixer) {
4961
+ const firstIf = group[0];
4962
+ const lastIf = group[group.length - 1];
4963
+ const condition = group
4964
+ .map((item) => renderTest(item.test, sourceCode))
4965
+ .join(' || ');
4966
+ return fixer.replaceTextRange([firstIf.range[0], lastIf.range[1]], `if (${condition}) ${sourceCode.getText(firstIf.consequent)}`);
4967
+ },
4968
+ }
4969
+ : {}),
4970
+ messageId: 'preferCombinedIfControlFlow',
4971
+ node: ifStatement,
4972
+ });
4973
+ }
4974
+ }
4975
+ i = j;
4976
+ }
4977
+ }
4978
+ return {
4979
+ BlockStatement(node) {
4980
+ checkBody(node.body);
4981
+ },
4982
+ Program(node) {
4983
+ checkBody(node.body);
4984
+ },
4985
+ };
4986
+ },
4987
+ meta: {
4988
+ docs: {
4989
+ description: 'Combine consecutive if statements that use the same return, break, continue, or throw statement into a single if statement.',
4990
+ },
4991
+ fixable: 'code',
4992
+ messages: {
4993
+ preferCombinedIfControlFlow: 'Combine consecutive if statements with identical return, break, continue, or throw statements.',
4994
+ },
4995
+ schema: [],
4996
+ type: 'suggestion',
4997
+ },
4998
+ name: 'prefer-combined-if-control-flow',
4999
+ });
5000
+
4686
5001
  const MESSAGE_ID$1 = 'prefer-deep-imports';
4687
5002
  const ERROR_MESSAGE = 'Import via root entry point is prohibited when nested entry points exist';
4688
5003
  const createRule$4 = ESLintUtils.RuleCreator(() => ERROR_MESSAGE);
@@ -4700,14 +5015,10 @@ var preferDeepImports = createRule$4({
4700
5015
  return;
4701
5016
  }
4702
5017
  const rootPackageName = getRootPackageName(rawImportPath);
4703
- if (!rootPackageName) {
4704
- return;
4705
- }
4706
- if (!allowedPackages.includes(rootPackageName)) {
4707
- return;
4708
- }
4709
- if (!isStrictMode &&
4710
- isAlreadyNestedImport(rawImportPath, rootPackageName)) {
5018
+ if (!rootPackageName ||
5019
+ !allowedPackages.includes(rootPackageName) ||
5020
+ (!isStrictMode &&
5021
+ isAlreadyNestedImport(rawImportPath, rootPackageName))) {
4711
5022
  return;
4712
5023
  }
4713
5024
  const importedSymbols = extractNamedImportedSymbols(node);
@@ -4938,10 +5249,7 @@ function mapSymbolsToEntryPointsUsingTypeChecker(importedSymbols, candidateEntry
4938
5249
  for (const importedSymbol of importedSymbols) {
4939
5250
  for (const relativeEntryDir of candidateEntryPoints) {
4940
5251
  const exportedNames = exportTableByEntryPoint.get(relativeEntryDir);
4941
- if (!exportedNames) {
4942
- continue;
4943
- }
4944
- if (!exportedNames.has(importedSymbol)) {
5252
+ if (!exportedNames?.has(importedSymbol)) {
4945
5253
  continue;
4946
5254
  }
4947
5255
  symbolToEntryPoint.set(importedSymbol, relativeEntryDir);
@@ -5224,10 +5532,8 @@ function isDomImperativeCall(node, checker, esTreeNodeToTSNodeMap) {
5224
5532
  }
5225
5533
  const signature = checker.getResolvedSignature(tsNode);
5226
5534
  const declaration = signature?.declaration;
5227
- if (!declaration) {
5228
- return false;
5229
- }
5230
- if (!LIB_DOM_FILE_PATTERN.test(declaration.getSourceFile().fileName)) {
5535
+ if (!declaration ||
5536
+ !LIB_DOM_FILE_PATTERN.test(declaration.getSourceFile().fileName)) {
5231
5537
  return false;
5232
5538
  }
5233
5539
  const returnType = checker.typeToString(checker.getReturnTypeOfSignature(signature));
@@ -5459,18 +5765,15 @@ function getReturnedExpression(node) {
5459
5765
  }
5460
5766
  function getWrappedSignalGetter(node, checker, esTreeNodeToTSNodeMap) {
5461
5767
  const [arg] = node.arguments;
5462
- if (!arg || arg.type === AST_NODE_TYPES.SpreadElement) {
5463
- return null;
5464
- }
5465
- if (arg.type !== AST_NODE_TYPES.ArrowFunctionExpression &&
5466
- arg.type !== AST_NODE_TYPES.FunctionExpression) {
5768
+ if (!arg ||
5769
+ arg.type === AST_NODE_TYPES.SpreadElement ||
5770
+ (arg.type !== AST_NODE_TYPES.ArrowFunctionExpression &&
5771
+ arg.type !== AST_NODE_TYPES.FunctionExpression)) {
5467
5772
  return null;
5468
5773
  }
5469
5774
  const body = getReturnedExpression(arg);
5470
- if (body?.type !== AST_NODE_TYPES.CallExpression) {
5471
- return null;
5472
- }
5473
- if (!isSignalReadCall(body, checker, esTreeNodeToTSNodeMap)) {
5775
+ if (body?.type !== AST_NODE_TYPES.CallExpression ||
5776
+ !isSignalReadCall(body, checker, esTreeNodeToTSNodeMap)) {
5474
5777
  return null;
5475
5778
  }
5476
5779
  const getter = unwrapExpression(body.callee);
@@ -5943,24 +6246,25 @@ const plugin = {
5943
6246
  'class-property-naming': classPropertyNaming,
5944
6247
  'decorator-key-sort': config$3,
5945
6248
  'flat-exports': flatExports,
5946
- 'host-attributes-sort': rule$i,
6249
+ 'host-attributes-sort': rule$j,
5947
6250
  'html-logical-properties': config$2,
5948
- 'injection-token-description': rule$h,
5949
- 'no-deep-imports': rule$g,
6251
+ 'injection-token-description': rule$i,
6252
+ 'no-deep-imports': rule$h,
5950
6253
  'no-deep-imports-to-indexed-packages': noDeepImportsToIndexedPackages,
5951
- 'no-fully-untracked-effect': rule$f,
6254
+ 'no-fully-untracked-effect': rule$g,
5952
6255
  'no-href-with-router-link': config$1,
5953
- 'no-implicit-public': rule$e,
5954
- 'no-legacy-peer-deps': rule$d,
5955
- 'no-playwright-empty-fill': rule$c,
6256
+ 'no-implicit-public': rule$f,
6257
+ 'no-legacy-peer-deps': rule$e,
6258
+ 'no-playwright-empty-fill': rule$d,
5956
6259
  'no-project-as-in-ng-template': config,
5957
- 'no-redundant-type-annotation': rule$b,
5958
- 'no-side-effects-in-computed': rule$a,
5959
- 'no-signal-reads-after-await-in-reactive-context': rule$9,
5960
- 'no-string-literal-concat': rule$8,
5961
- 'no-untracked-outside-reactive-context': rule$7,
5962
- 'no-useless-untracked': rule$6,
5963
- 'object-single-line': rule$5,
6260
+ 'no-redundant-type-annotation': rule$c,
6261
+ 'no-side-effects-in-computed': rule$b,
6262
+ 'no-signal-reads-after-await-in-reactive-context': rule$a,
6263
+ 'no-string-literal-concat': rule$9,
6264
+ 'no-untracked-outside-reactive-context': rule$8,
6265
+ 'no-useless-untracked': rule$7,
6266
+ 'object-single-line': rule$6,
6267
+ 'prefer-combined-if-control-flow': rule$5,
5964
6268
  'prefer-deep-imports': preferDeepImports,
5965
6269
  'prefer-multi-arg-push': rule$4,
5966
6270
  '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.472.0",
3
+ "version": "0.474.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;
@@ -17,6 +17,7 @@ export interface SignalUsage {
17
17
  readonly writes: TSESTree.CallExpression[];
18
18
  }
19
19
  export declare function isAngularEffectCall(node: TSESTree.CallExpression, program: TSESTree.Program): boolean;
20
+ export declare function isAngularInjectCall(node: TSESTree.CallExpression, program: TSESTree.Program): boolean;
20
21
  export declare function isAngularUntrackedCall(node: TSESTree.CallExpression, program: TSESTree.Program): boolean;
21
22
  export declare function getReactiveScopes(node: TSESTree.CallExpression, program: TSESTree.Program): ReactiveScope[];
22
23
  export declare function isNodeInsideSynchronousReactiveScope(node: TSESTree.Node, callback: ReactiveCallback): boolean;