@dsivd/prestations-ng 19.0.6-beta.2 → 19.0.7

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CONTRIBUTING.md +25 -0
  3. package/ESLINT_PLUGIN.md +198 -0
  4. package/README.md +3 -0
  5. package/UPGRADING_V19.md +38 -143
  6. package/dsivd-prestations-ng-19.0.7.tgz +0 -0
  7. package/eslint/configs/template-base.mjs +10 -0
  8. package/eslint/configs/template-recommended.mjs +24 -0
  9. package/eslint/configs/template-rules.mjs +17 -0
  10. package/eslint/configs/ts-base.mjs +10 -0
  11. package/eslint/configs/ts-recommended.mjs +142 -0
  12. package/eslint/configs/ts-rules.mjs +14 -0
  13. package/eslint/index.mjs +20 -5
  14. package/eslint/rules/index.mjs +7 -1
  15. package/eslint/rules/no-direct-signal-mutation.mjs +240 -136
  16. package/eslint/rules/no-uninvoked-signal-in-template.mjs +170 -0
  17. package/eslint/signal-names.mjs +232 -0
  18. package/eslint/template-ast.mjs +26 -0
  19. package/fesm2022/dsivd-prestations-ng.mjs +7 -9
  20. package/fesm2022/dsivd-prestations-ng.mjs.map +1 -1
  21. package/package.json +1 -1
  22. package/src/eslint/configs/__tests__/configs.test.mjs +135 -0
  23. package/src/eslint/configs/template-base.mjs +10 -0
  24. package/src/eslint/configs/template-recommended.mjs +24 -0
  25. package/src/eslint/configs/template-rules.mjs +17 -0
  26. package/src/eslint/configs/ts-base.mjs +10 -0
  27. package/src/eslint/configs/ts-recommended.mjs +142 -0
  28. package/src/eslint/configs/ts-rules.mjs +14 -0
  29. package/src/eslint/index.mjs +20 -5
  30. package/src/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +86 -4
  31. package/src/eslint/rules/__tests__/no-uninvoked-signal-in-template.test.mjs +291 -0
  32. package/src/eslint/rules/index.mjs +7 -1
  33. package/src/eslint/rules/no-direct-signal-mutation.mjs +240 -136
  34. package/src/eslint/rules/no-uninvoked-signal-in-template.mjs +170 -0
  35. package/src/eslint/signal-names.mjs +232 -0
  36. package/src/eslint/template-ast.mjs +26 -0
  37. package/types/dsivd-prestations-ng.d.ts +0 -2
  38. package/dsivd-prestations-ng-19.0.6-beta.2.tgz +0 -0
  39. package/eslint/rules/__tests__/no-direct-signal-mutation.test.mjs +0 -98
@@ -0,0 +1,142 @@
1
+ import eslint from '@eslint/js';
2
+ import rxjs from '@smarttools/eslint-plugin-rxjs';
3
+ import angular from 'angular-eslint';
4
+ import importX from 'eslint-plugin-import-x';
5
+ import simpleImportSort from 'eslint-plugin-simple-import-sort';
6
+ import tseslint from 'typescript-eslint';
7
+
8
+ import tsRules from './ts-rules.mjs';
9
+
10
+ /**
11
+ * The house TypeScript preset: the shared baselines, this library's own rules, and the
12
+ * conventions every prestations-ng project follows.
13
+ *
14
+ * Meant for the `extends` of a `**\/*.ts` block. It brings its own parser and plugins,
15
+ * so do not spread `angular.configs.tsRecommended` alongside it — declaring the same
16
+ * plugin twice from two different copies makes ESLint fail outright.
17
+ */
18
+ const tsRecommended = (plugin) => [
19
+ eslint.configs.recommended,
20
+ ...tseslint.configs.recommended, // covers: no-explicit-any, ban-ts-comment,
21
+ // no-unused-expressions, no-var, no-unused-vars…
22
+ ...tseslint.configs.stylistic, // covers: array-type, consistent-type-assertions,
23
+ // dot-notation, no-inferrable-types (default options),
24
+ // no-empty-function (error), prefer-for-of,
25
+ // prefer-function-type…
26
+ ...angular.configs.tsRecommended, // covers: prefer-inject, no-empty-lifecycle-method,
27
+ // use-lifecycle-interface…
28
+ ...tsRules(plugin),
29
+ {
30
+ name: 'prestations-ng/ts-recommended',
31
+ plugins: {
32
+ rxjs,
33
+ 'simple-import-sort': simpleImportSort,
34
+ 'import-x': importX,
35
+ },
36
+ rules: {
37
+ // ── @angular-eslint ──────────────────────────────────────────────
38
+ '@angular-eslint/component-selector': [
39
+ 'error',
40
+ {
41
+ type: 'element',
42
+ prefix: 'app',
43
+ style: 'kebab-case',
44
+ },
45
+ ],
46
+ '@angular-eslint/directive-selector': [
47
+ 'error',
48
+ {
49
+ type: 'attribute',
50
+ prefix: 'app',
51
+ style: 'camelCase',
52
+ },
53
+ ],
54
+ '@angular-eslint/no-uncalled-signals': ['error'],
55
+ '@angular-eslint/prefer-signal-model': ['error'],
56
+ '@angular-eslint/prefer-signals': ['error'],
57
+
58
+ // ── @typescript-eslint ───────────────────────────────────────────
59
+ '@typescript-eslint/explicit-function-return-type': [
60
+ 'error',
61
+ {
62
+ allowExpressions: true,
63
+ allowTypedFunctionExpressions: true,
64
+ allowHigherOrderFunctions: true,
65
+ allowDirectConstAssertionInArrowFunctions: true,
66
+ },
67
+ ],
68
+ '@typescript-eslint/explicit-member-accessibility': [
69
+ 'error',
70
+ { accessibility: 'no-public' },
71
+ ],
72
+ '@typescript-eslint/member-ordering': 'error',
73
+ '@typescript-eslint/no-base-to-string': 'warn',
74
+ // stylistic enables no-inferrable-types with default options (ignoreParameters: false);
75
+ // override here to allow typed parameters explicitly
76
+ '@typescript-eslint/no-inferrable-types': [
77
+ 'error',
78
+ { ignoreParameters: true },
79
+ ],
80
+ // stylistic sets no-empty-function to error; downgrade to warn
81
+ '@typescript-eslint/no-empty-function': 'warn',
82
+ '@typescript-eslint/prefer-includes': 'warn',
83
+ '@typescript-eslint/return-await': ['error', 'never'],
84
+ '@typescript-eslint/typedef': ['error', { parameter: true }],
85
+ '@typescript-eslint/unified-signatures': 'error',
86
+ // off here — @typescript-eslint/no-shadow handles it correctly for TS
87
+ 'no-shadow': 'off',
88
+ '@typescript-eslint/no-shadow': ['error', { hoist: 'all' }],
89
+ '@typescript-eslint/no-unused-vars': [
90
+ 'error',
91
+ {
92
+ varsIgnorePattern: '^_',
93
+ argsIgnorePattern: '^_',
94
+ caughtErrorsIgnorePattern: '^_',
95
+ },
96
+ ],
97
+
98
+ // ── eslint core ──────────────────────────────────────────────────
99
+ 'arrow-body-style': 'error',
100
+ 'arrow-parens': ['error', 'as-needed'],
101
+ curly: 'error',
102
+ eqeqeq: ['error', 'always'],
103
+ 'guard-for-in': 'error',
104
+ 'no-bitwise': 'error',
105
+ 'no-caller': 'error',
106
+ 'no-console': ['error', { allow: ['log', 'warn', 'error'] }],
107
+ 'no-eval': 'error',
108
+ 'no-extra-boolean-cast': 'off',
109
+ 'no-multiple-empty-lines': 'error',
110
+ 'no-new-wrappers': 'error',
111
+ 'no-restricted-imports': [
112
+ 'error',
113
+ { paths: ['rxjs/Rx', 'primeng/primeng', 'primeng'] },
114
+ ],
115
+ 'no-return-assign': 'error',
116
+ 'no-throw-literal': 'error',
117
+ 'no-undef-init': 'error',
118
+ 'no-useless-concat': 'error',
119
+ 'object-shorthand': ['error', 'always', { avoidQuotes: true }],
120
+ 'one-var': ['error', 'never'],
121
+ 'prefer-arrow-callback': 'error',
122
+ 'prefer-template': 'error',
123
+ radix: 'error',
124
+
125
+ // ── rxjs ─────────────────────────────────────────────────────────
126
+ 'rxjs/no-implicit-any-catch': ['error', { allowExplicitAny: true }],
127
+ 'rxjs/no-sharereplay': 'off',
128
+
129
+ // ── import-x ─────────────────────────────────────────────────────
130
+ 'import-x/no-cycle': ['warn', { maxDepth: 3, ignoreExternal: true }],
131
+ 'import-x/no-deprecated': 'warn',
132
+ 'import-x/first': 'error',
133
+ 'import-x/newline-after-import': 'error',
134
+ 'import-x/no-duplicates': 'error',
135
+
136
+ // ── simple-import-sort ───────────────────────────────────────────
137
+ 'simple-import-sort/imports': 'error',
138
+ 'simple-import-sort/exports': 'error',
139
+ },
140
+ },
141
+ ];
142
+ export default tsRecommended;
@@ -0,0 +1,14 @@
1
+ import tsBase from './ts-base.mjs';
2
+
3
+ // Only the rules written by this library. `tsRecommended` includes it, alongside the
4
+ // house style; use this layer directly to opt out of the latter.
5
+ const tsRules = (plugin) => [
6
+ tsBase(plugin),
7
+ {
8
+ name: 'prestations-ng/ts-rules',
9
+ rules: {
10
+ '@dsivd/prestations-ng/no-direct-signal-mutation': 'error',
11
+ },
12
+ },
13
+ ];
14
+ export default tsRules;
package/eslint/index.mjs CHANGED
@@ -1,7 +1,22 @@
1
- import { noDirectSignalMutation } from './rules/index.mjs';
1
+ import templateRecommended from './configs/template-recommended.mjs';
2
+ import templateRules from './configs/template-rules.mjs';
3
+ import tsRecommended from './configs/ts-recommended.mjs';
4
+ import tsRules from './configs/ts-rules.mjs';
5
+ import rules from './rules/index.mjs';
2
6
 
3
- export default {
4
- rules: {
5
- 'no-direct-signal-mutation': noDirectSignalMutation,
6
- },
7
+ const plugin = {
8
+ meta: { name: '@dsivd/prestations-ng' },
9
+ rules,
7
10
  };
11
+
12
+ const configs = {
13
+ // The house presets: baselines, this library's rules, and the shared conventions.
14
+ tsRecommended: tsRecommended(plugin),
15
+ templateRecommended: templateRecommended(plugin),
16
+ // Only this library's own rules, for a project that supplies its own style.
17
+ tsRules: tsRules(plugin),
18
+ templateRules: templateRules(plugin),
19
+ };
20
+
21
+ export { configs, plugin, rules };
22
+ export default { configs, plugin, rules };
@@ -1 +1,7 @@
1
- export { default as noDirectSignalMutation } from './no-direct-signal-mutation.mjs';
1
+ import noDirectSignalMutation from './no-direct-signal-mutation.mjs';
2
+ import noUninvokedSignalInTemplate from './no-uninvoked-signal-in-template.mjs';
3
+
4
+ export default {
5
+ 'no-direct-signal-mutation': noDirectSignalMutation,
6
+ 'no-uninvoked-signal-in-template': noUninvokedSignalInTemplate,
7
+ };
@@ -1,8 +1,222 @@
1
+ import { handlerExpressionOf, isComponentReceiver } from "../template-ast.mjs";
2
+
1
3
  /**
2
4
  * ESLint rule to detect direct mutations of signal/getter results.
3
5
  * Warns against patterns like: model().property = value
4
6
  * Suggests using .update() or .set() instead.
7
+ *
8
+ * Applies to TypeScript files and to Angular templates, where the mutation is also
9
+ * hidden in every two-way binding on a call result: [(ngModel)]="model().property".
5
10
  */
11
+
12
+ // Assignment operators accepted by the Angular expression parser.
13
+ const TEMPLATE_ASSIGNMENTS = new Set([
14
+ "=",
15
+ "+=",
16
+ "-=",
17
+ "*=",
18
+ "/=",
19
+ "%=",
20
+ "**=",
21
+ "&&=",
22
+ "||=",
23
+ "??=",
24
+ ]);
25
+
26
+ const TEMPLATE_MEMBER_READS = new Set([
27
+ "PropertyRead",
28
+ "SafePropertyRead",
29
+ "KeyedRead",
30
+ "SafeKeyedRead",
31
+ ]);
32
+
33
+ const VIEW_QUERY_FACTORIES = new Set([
34
+ "viewChild",
35
+ "viewChildren",
36
+ "contentChild",
37
+ "contentChildren",
38
+ ]);
39
+
40
+ // ── TypeScript ───────────────────────────────────────────────────────────────
41
+
42
+ function getCalledName(callee) {
43
+ if (!callee) {
44
+ return null;
45
+ }
46
+ if (callee.type === "Identifier") {
47
+ return callee.name;
48
+ }
49
+ if (
50
+ callee.type === "MemberExpression" &&
51
+ !callee.computed &&
52
+ callee.property.type === "Identifier"
53
+ ) {
54
+ return callee.property.name;
55
+ }
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * Finds the first/root function call used as the base of a mutation chain.
61
+ * Examples:
62
+ * - model().property = x => model()
63
+ * - this.model().deep.prop = x => this.model()
64
+ * - this.query().el().x = y => this.query()
65
+ */
66
+ function getFirstCallExpression(node) {
67
+ if (!node) {
68
+ return null;
69
+ }
70
+ if (node.type === "MemberExpression") {
71
+ return getFirstCallExpression(node.object);
72
+ }
73
+ if (node.type === "CallExpression") {
74
+ if (node.callee.type === "MemberExpression") {
75
+ const nestedCall = getFirstCallExpression(node.callee.object);
76
+ return nestedCall || node;
77
+ }
78
+ return node;
79
+ }
80
+ return null;
81
+ }
82
+
83
+ function isViewQueryFactoryCall(node) {
84
+ if (node?.type !== "CallExpression") {
85
+ return false;
86
+ }
87
+ // `viewChild.required(…)` unwraps to its factory.
88
+ let callee = node.callee;
89
+ while (callee?.type === "MemberExpression") {
90
+ callee = callee.object;
91
+ }
92
+ return callee?.type === "Identifier" && VIEW_QUERY_FACTORIES.has(callee.name);
93
+ }
94
+
95
+ function walkAst(node, visitor, visited = new WeakSet()) {
96
+ if (!node || typeof node !== "object") {
97
+ return;
98
+ }
99
+ if (visited.has(node)) {
100
+ return;
101
+ }
102
+ visited.add(node);
103
+
104
+ visitor(node);
105
+
106
+ for (const key of Object.keys(node)) {
107
+ // ESLint AST nodes include cyclic parent references.
108
+ if (key === "parent") {
109
+ continue;
110
+ }
111
+ const child = node[key];
112
+ if (Array.isArray(child)) {
113
+ for (const element of child) {
114
+ walkAst(element, visitor, visited);
115
+ }
116
+ } else {
117
+ walkAst(child, visitor, visited);
118
+ }
119
+ }
120
+ }
121
+
122
+ // A view query signal is read-only: `.set()` does not exist on it, so a mutation
123
+ // reached through one must not be reported.
124
+ function collectViewQueryAccessorNames(ast) {
125
+ const names = new Set();
126
+
127
+ walkAst(ast, (node) => {
128
+ if (
129
+ node.type === "VariableDeclarator" &&
130
+ node.id?.type === "Identifier" &&
131
+ isViewQueryFactoryCall(node.init)
132
+ ) {
133
+ names.add(node.id.name);
134
+ return;
135
+ }
136
+
137
+ if (
138
+ (node.type === "PropertyDefinition" || node.type === "ClassProperty") &&
139
+ node.key?.type === "Identifier" &&
140
+ isViewQueryFactoryCall(node.value)
141
+ ) {
142
+ names.add(node.key.name);
143
+ }
144
+ });
145
+
146
+ return names;
147
+ }
148
+
149
+ function isViewQueryAccessorCall(callee, viewQueryAccessorNames) {
150
+ const calledName = getCalledName(callee);
151
+ return calledName ? viewQueryAccessorNames.has(calledName) : false;
152
+ }
153
+
154
+ function isSignalLikeRootCall(node, viewQueryAccessorNames) {
155
+ if (node.arguments.length !== 0) {
156
+ return false;
157
+ }
158
+
159
+ // model().prop = ... (local signal-like accessor)
160
+ if (node.callee.type === "Identifier") {
161
+ return !viewQueryAccessorNames.has(node.callee.name);
162
+ }
163
+
164
+ // this.model().prop = ... (component/service signal accessor)
165
+ // component.model().prop = ... should not be assumed to be a signal read.
166
+ if (node.callee.type === "MemberExpression") {
167
+ if (node.callee.object?.type !== "ThisExpression") {
168
+ return false;
169
+ }
170
+ return !isViewQueryAccessorCall(node.callee, viewQueryAccessorNames);
171
+ }
172
+
173
+ return false;
174
+ }
175
+
176
+ // ── Angular templates ────────────────────────────────────────────────────────
177
+
178
+ // Template counterpart of `getFirstCallExpression`.
179
+ function getFirstTemplateCall(node) {
180
+ if (!node) {
181
+ return null;
182
+ }
183
+ if (TEMPLATE_MEMBER_READS.has(node.type)) {
184
+ return getFirstTemplateCall(node.receiver);
185
+ }
186
+ if (node.type === "Call" || node.type === "SafeCall") {
187
+ return getFirstTemplateCall(node.receiver) || node;
188
+ }
189
+ return null;
190
+ }
191
+
192
+ /**
193
+ * Template counterpart of `isSignalLikeRootCall`. A template has no bare identifier:
194
+ * `model()` and `this.model()` both read the member directly on the component, so both
195
+ * collapse to `isComponentReceiver`. `map.get(key)` (arguments) and
196
+ * `component.inputElement()` (foreign receiver) are therefore not assumed to be signal
197
+ * reads, as in TypeScript.
198
+ */
199
+ function isSignalLikeRootTemplateCall(node) {
200
+ if (node.args?.length !== 0) {
201
+ return false;
202
+ }
203
+ const callee = node.receiver;
204
+ if (!callee || !TEMPLATE_MEMBER_READS.has(callee.type)) {
205
+ return false;
206
+ }
207
+ return isComponentReceiver(callee.receiver);
208
+ }
209
+
210
+ // Template counterpart of the TypeScript checks: a member of a signal-like call
211
+ // result, such as `model().property` or `this.model().array[0]`.
212
+ function isTemplateCallResultMember(node) {
213
+ if (!node || !TEMPLATE_MEMBER_READS.has(node.type)) {
214
+ return false;
215
+ }
216
+ const rootCall = getFirstTemplateCall(node.receiver);
217
+ return !!rootCall && isSignalLikeRootTemplateCall(rootCall);
218
+ }
219
+
6
220
  export default {
7
221
  meta: {
8
222
  type: "problem",
@@ -19,157 +233,47 @@ export default {
19
233
  },
20
234
 
21
235
  create(context) {
22
- const sourceCode = context.sourceCode;
23
- const viewQueryFactoryNames = new Set([
24
- "viewChild",
25
- "viewChildren",
26
- "contentChild",
27
- "contentChildren",
28
- ]);
29
- const viewQueryAccessorNames = collectViewQueryAccessorNames(sourceCode.ast);
236
+ const viewQueryAccessorNames = collectViewQueryAccessorNames(
237
+ context.sourceCode.ast,
238
+ );
239
+
240
+ const report = (node) =>
241
+ context.report({ node, messageId: "directMutation" });
30
242
 
31
243
  return {
244
+ // ── TypeScript ───────────────────────────────────────────────────────────
32
245
  AssignmentExpression(node) {
33
246
  if (node.left.type !== "MemberExpression") {
34
247
  return;
35
248
  }
36
249
 
37
- const rootCallExpression = getFirstCallExpression(node.left.object);
38
- if (rootCallExpression && isSignalLikeRootCall(rootCallExpression)) {
39
- context.report({
40
- node,
41
- messageId: "directMutation",
42
- });
250
+ const rootCall = getFirstCallExpression(node.left.object);
251
+ if (rootCall && isSignalLikeRootCall(rootCall, viewQueryAccessorNames)) {
252
+ report(node);
43
253
  }
44
254
  },
45
- };
46
-
47
- function isSignalLikeRootCall(node) {
48
- if (node.arguments.length !== 0) {
49
- return false;
50
- }
51
-
52
- // model().prop = ... (local signal-like accessor)
53
- if (node.callee.type === "Identifier") {
54
- return !viewQueryAccessorNames.has(node.callee.name);
55
- }
56
-
57
- // this.model().prop = ... (component/service signal accessor)
58
- // component.model().prop = ... should not be assumed to be a signal read.
59
- if (node.callee.type === "MemberExpression") {
60
- if (node.callee.object?.type !== "ThisExpression") {
61
- return false;
62
- }
63
- return !isViewQueryAccessorCall(node.callee);
64
- }
65
-
66
- return false;
67
- }
68
-
69
- function isViewQueryAccessorCall(callee) {
70
- const calledName = getCalledName(callee);
71
- return calledName ? viewQueryAccessorNames.has(calledName) : false;
72
- }
73
-
74
- function getCalledName(callee) {
75
- if (!callee) {
76
- return null;
77
- }
78
- if (callee.type === "Identifier") {
79
- return callee.name;
80
- }
81
- if (
82
- callee.type === "MemberExpression" &&
83
- !callee.computed &&
84
- callee.property.type === "Identifier"
85
- ) {
86
- return callee.property.name;
87
- }
88
- return null;
89
- }
90
-
91
- /**
92
- * Finds the first/root function call used as the base of a mutation chain.
93
- * Examples:
94
- * - model().property = x => model()
95
- * - this.model().deep.prop = x => this.model()
96
- * - this.query().el().x = y => this.query()
97
- */
98
- function getFirstCallExpression(node) {
99
- if (!node) {
100
- return null;
101
- }
102
- if (node.type === "MemberExpression") {
103
- return getFirstCallExpression(node.object);
104
- }
105
- if (node.type === "CallExpression") {
106
- if (node.callee.type === "MemberExpression") {
107
- const nestedCall = getFirstCallExpression(node.callee.object);
108
- return nestedCall || node;
109
- }
110
- return node;
111
- }
112
- return null;
113
- }
114
-
115
- function collectViewQueryAccessorNames(ast) {
116
- const names = new Set();
117
-
118
- walkAst(ast, (node) => {
119
- if (
120
- node.type === "VariableDeclarator" &&
121
- node.id?.type === "Identifier" &&
122
- isViewQueryFactoryCall(node.init)
123
- ) {
124
- names.add(node.id.name);
125
- return;
126
- }
127
255
 
256
+ // ── Angular templates ────────────────────────────────────────────────────
257
+ Binary(node) {
128
258
  if (
129
- (node.type === "PropertyDefinition" || node.type === "ClassProperty") &&
130
- node.key?.type === "Identifier" &&
131
- isViewQueryFactoryCall(node.value)
259
+ TEMPLATE_ASSIGNMENTS.has(node.operation) &&
260
+ isTemplateCallResultMember(node.left)
132
261
  ) {
133
- names.add(node.key.name);
262
+ report(node);
134
263
  }
135
- });
136
-
137
- return names;
138
- }
139
-
140
- function isViewQueryFactoryCall(node) {
141
- return (
142
- node?.type === "CallExpression" &&
143
- node.callee?.type === "Identifier" &&
144
- viewQueryFactoryNames.has(node.callee.name)
145
- );
146
- }
147
-
148
- function walkAst(node, visitor, visited = new WeakSet()) {
149
- if (!node || typeof node !== "object") {
150
- return;
151
- }
152
- if (visited.has(node)) {
153
- return;
154
- }
155
- visited.add(node);
156
-
157
- visitor(node);
264
+ },
158
265
 
159
- for (const key of Object.keys(node)) {
160
- // ESLint AST nodes include cyclic parent references.
161
- if (key === "parent") {
162
- continue;
266
+ BoundEvent(node) {
267
+ // `[(x)]="model().property"` makes Angular assign into the value of the signal.
268
+ // Reporting on the BoundEvent handler reports the binding exactly once.
269
+ if (!node.name?.endsWith("Change")) {
270
+ return;
163
271
  }
164
- const child = node[key];
165
- if (Array.isArray(child)) {
166
- for (const element of child) {
167
- walkAst(element, visitor, visited);
168
- }
169
- } else {
170
- walkAst(child, visitor, visited);
272
+ const handler = handlerExpressionOf(node);
273
+ if (isTemplateCallResultMember(handler)) {
274
+ report(handler);
171
275
  }
172
- }
173
- }
276
+ },
277
+ };
174
278
  },
175
279
  };