@llui/vite-plugin 0.0.19 → 0.0.21

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.
@@ -1,4 +1,5 @@
1
1
  import ts from 'typescript';
2
+ import { collectStatePathsFromSource, collectAccessorPathSets } from './collect-deps.js';
2
3
  const INTERACTIVE_ELEMENTS = new Set([
3
4
  'button',
4
5
  'a',
@@ -81,8 +82,8 @@ export function diagnose(source) {
81
82
  const diagnostics = [];
82
83
  // Collect Msg type variants for exhaustive update() check
83
84
  const msgVariants = collectMsgVariants(sf);
84
- // Collect state access paths for bitmask warning
85
- const statePaths = collectStatePaths(sf);
85
+ // Collect state access paths for bitmask warning (shared scanner with collect-deps.ts)
86
+ const statePaths = collectStatePathsFromSource(sf);
86
87
  function visit(node) {
87
88
  checkMapOnState(node, sf, diagnostics);
88
89
  checkExhaustiveUpdate(node, sf, diagnostics, msgVariants);
@@ -93,6 +94,7 @@ export function diagnose(source) {
93
94
  checkNamespaceImport(node, sf, diagnostics);
94
95
  checkSpreadChildren(node, sf, diagnostics);
95
96
  checkEmptyProps(node, sf, diagnostics);
97
+ checkStaticOn(node, sf, diagnostics);
96
98
  ts.forEachChild(node, visit);
97
99
  }
98
100
  visit(sf);
@@ -117,6 +119,7 @@ function checkNamespaceImport(node, sf, diagnostics) {
117
119
  const name = clause.namedBindings.name.text;
118
120
  const { line, column } = pos(clause.namedBindings, sf);
119
121
  diagnostics.push({
122
+ rule: 'namespace-import',
120
123
  message: `Namespace import '${name}' from '@llui/dom' at line ${line} disables compiler optimizations. Use named imports instead: import { div, text, ... } from '@llui/dom'.`,
121
124
  line,
122
125
  column,
@@ -125,6 +128,12 @@ function checkNamespaceImport(node, sf, diagnostics) {
125
128
  // Warns when a children array contains a spread — the compiler can't
126
129
  // analyze variable-length children, so it bails on template cloning and
127
130
  // falls back to runtime elSplit. Not fatal, but silent.
131
+ //
132
+ // Scope-aware: when the spread source (or an array-method's receiver)
133
+ // resolves to a locally-bounded binding — `const x = [...]`, `const x =
134
+ // fn(...)`, `const x = other.map(...)` where `other` is bounded — the
135
+ // child count is statically known and `each()` is not a usable fix.
136
+ // Those cases stay silent; only truly dynamic spreads warn.
128
137
  function checkSpreadChildren(node, sf, diagnostics) {
129
138
  if (!ts.isCallExpression(node))
130
139
  return;
@@ -136,15 +145,14 @@ function checkSpreadChildren(node, sf, diagnostics) {
136
145
  for (const arg of node.arguments) {
137
146
  if (!ts.isArrayLiteralExpression(arg))
138
147
  continue;
139
- // Look for "suspicious" spreads — ones that aren't obviously returning
140
- // Node[] from a structural primitive or user-defined view helper.
141
148
  for (const el of arg.elements) {
142
149
  if (!ts.isSpreadElement(el))
143
150
  continue;
144
- if (isStructuralSpread(el.expression))
151
+ if (isBoundedSpreadSource(el.expression, sf))
145
152
  continue;
146
153
  const { line, column } = pos(arg, sf);
147
154
  diagnostics.push({
155
+ rule: 'spread-in-children',
148
156
  message: `Spread in children array of '${node.expression.text}()' at line ${line} disables template-clone compilation. For dynamic child counts, use each() instead.`,
149
157
  line,
150
158
  column,
@@ -166,22 +174,139 @@ const ARRAY_ITERATION_METHODS = new Set([
166
174
  'reverse',
167
175
  'sort',
168
176
  ]);
169
- function isStructuralSpread(expr) {
170
- // Only keep the warning for suspect patterns: identifier spreads and
171
- // array-iteration method calls. Everything else is presumed to be a
172
- // structural primitive or user helper returning Node[].
177
+ /**
178
+ * Classify a spread-source expression as "bounded" i.e., the child
179
+ * count is statically knowable and `each()` is not an applicable fix.
180
+ * Returns true when the spread should stay silent, false when the
181
+ * spread is genuinely suspect (state-derived, unresolved, inline
182
+ * array-method call on a non-bounded receiver).
183
+ */
184
+ function isBoundedSpreadSource(expr, sf) {
185
+ // Identifier spread `...foo` — resolve the binding.
186
+ if (ts.isIdentifier(expr)) {
187
+ const init = resolveBindingInitializer(expr, sf);
188
+ if (init === null)
189
+ return false;
190
+ return isBoundedInitializer(init, sf);
191
+ }
192
+ // Call-expression spread.
173
193
  if (ts.isCallExpression(expr)) {
174
194
  const callee = expr.expression;
195
+ // Array-method call: `...x.map(...)`, `...arr.concat([...])`, etc.
196
+ if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
197
+ if (!ARRAY_ITERATION_METHODS.has(callee.name.text)) {
198
+ // Non-array-method method call (e.g. `...my.overlay()`) — presume
199
+ // structural/helper. Same as before.
200
+ return true;
201
+ }
202
+ // Array method — bounded if the receiver resolves to a bounded
203
+ // array source. Inline literals (e.g. `...[1,2,3].map(...)`)
204
+ // stay suspect intentionally so authors see the warning on the
205
+ // canonical dynamic-mapping shape.
206
+ return isBoundedArrayReceiver(callee.expression, sf);
207
+ }
208
+ // Plain function call `...fn()` — presume structural/helper.
209
+ return true;
210
+ }
211
+ // Anything else (array literal inline, etc.) — treat as suspect for
212
+ // now. Inline `...[...]` at a call site is unusual and worth flagging.
213
+ return false;
214
+ }
215
+ /**
216
+ * Is the initializer a bounded expression? Array literals and
217
+ * function-call results both qualify; method calls recurse on their
218
+ * receivers.
219
+ */
220
+ function isBoundedInitializer(init, sf) {
221
+ // `const foo = [...]` — bounded.
222
+ if (ts.isArrayLiteralExpression(init))
223
+ return true;
224
+ // `const foo = x as const` / `x as T` — look through the assertion.
225
+ if (ts.isAsExpression(init) || ts.isTypeAssertionExpression(init)) {
226
+ return isBoundedInitializer(init.expression, sf);
227
+ }
228
+ // `const foo = someCall(...)` — treat plain-call results as bounded
229
+ // structural output. Same heuristic the original syntactic rule used.
230
+ if (ts.isCallExpression(init)) {
231
+ const callee = init.expression;
232
+ if (ts.isIdentifier(callee))
233
+ return true;
175
234
  if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
176
- // `...arr.map(...)`, `...arr.filter(...)` — suspect
177
- return !ARRAY_ITERATION_METHODS.has(callee.name.text);
235
+ if (!ARRAY_ITERATION_METHODS.has(callee.name.text))
236
+ return true;
237
+ // Method call is an array method — bounded iff receiver is.
238
+ return isBoundedArrayReceiver(callee.expression, sf);
178
239
  }
179
- // Plain function call `...fn()` — presume structural/helper
240
+ }
241
+ return false;
242
+ }
243
+ /**
244
+ * A method-call receiver (the `x` in `x.map(...)`) is bounded when
245
+ * resolved to a named array-literal binding. Inline literals are
246
+ * intentionally NOT bounded here — callers who inline `[1,2,3].map(...)`
247
+ * should still see the warning.
248
+ */
249
+ function isBoundedArrayReceiver(receiver, sf) {
250
+ if (!ts.isIdentifier(receiver))
251
+ return false;
252
+ const init = resolveBindingInitializer(receiver, sf);
253
+ if (init === null)
254
+ return false;
255
+ if (ts.isArrayLiteralExpression(init))
180
256
  return true;
257
+ if (ts.isAsExpression(init) || ts.isTypeAssertionExpression(init)) {
258
+ return ts.isArrayLiteralExpression(init.expression);
181
259
  }
182
- // Identifier spread (`...arr`) — suspect
183
260
  return false;
184
261
  }
262
+ /**
263
+ * Walk the identifier's ancestor scopes looking for a matching
264
+ * VariableDeclaration. Returns its initializer (or null if the name
265
+ * resolves to a function parameter, import, or nothing at all).
266
+ */
267
+ function resolveBindingInitializer(ident, sf) {
268
+ const name = ident.text;
269
+ let scope = ident.parent;
270
+ while (scope) {
271
+ const decl = findVariableDeclarationInScope(scope, name, ident);
272
+ if (decl)
273
+ return decl.initializer ?? null;
274
+ if (scope === sf)
275
+ break;
276
+ scope = scope.parent;
277
+ }
278
+ return null;
279
+ }
280
+ /**
281
+ * Scan a scope's immediate statements for a `const/let/var name = ...`
282
+ * declaration. Does not descend into inner function bodies — those are
283
+ * visible only from within themselves.
284
+ */
285
+ function findVariableDeclarationInScope(scope, name, from) {
286
+ let found = null;
287
+ function visit(node) {
288
+ if (found)
289
+ return;
290
+ // Don't descend into nested function bodies other than the one
291
+ // containing `from` — that walking is handled by the outer loop.
292
+ if (node !== from.parent &&
293
+ (ts.isFunctionDeclaration(node) ||
294
+ ts.isFunctionExpression(node) ||
295
+ ts.isArrowFunction(node) ||
296
+ ts.isMethodDeclaration(node) ||
297
+ ts.isConstructorDeclaration(node))) {
298
+ // Still scan the parameters? No — parameters aren't VariableDeclarations.
299
+ return;
300
+ }
301
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === name) {
302
+ found = node;
303
+ return;
304
+ }
305
+ ts.forEachChild(node, visit);
306
+ }
307
+ ts.forEachChild(scope, visit);
308
+ return found;
309
+ }
185
310
  // Warns when an element helper is called with an empty props object — the
186
311
  // attrs argument is optional, so `h1({}, [...])` should be `h1([...])`.
187
312
  function checkEmptyProps(node, sf, diagnostics) {
@@ -198,6 +323,7 @@ function checkEmptyProps(node, sf, diagnostics) {
198
323
  return;
199
324
  const { line, column } = pos(firstArg, sf);
200
325
  diagnostics.push({
326
+ rule: 'empty-props',
201
327
  message: `Empty props object passed to '${node.expression.text}()' at line ${line}. The attrs argument is optional — omit it: ${node.expression.text}([...]).`,
202
328
  line,
203
329
  column,
@@ -245,6 +371,7 @@ function checkMapOnState(node, sf, diagnostics) {
245
371
  return;
246
372
  const { line, column } = pos(node, sf);
247
373
  diagnostics.push({
374
+ rule: 'map-on-state',
248
375
  message: `Array .map() on state-derived value at line ${line}. Use each() for reactive lists that update when the array changes.`,
249
376
  line,
250
377
  column,
@@ -345,6 +472,7 @@ function checkExhaustiveUpdate(node, sf, diagnostics, msgVariants) {
345
472
  return;
346
473
  const { line, column } = pos(node, sf);
347
474
  diagnostics.push({
475
+ rule: 'exhaustive-update',
348
476
  message: `update() does not handle message type${missing.length > 1 ? 's' : ''} ${missing.map((m) => `'${m}'`).join(', ')} at line ${line}.`,
349
477
  line,
350
478
  column,
@@ -367,6 +495,7 @@ function checkAccessibility(node, sf, diagnostics) {
367
495
  if (tag === 'img' && !props.has('alt')) {
368
496
  const { line, column } = pos(node, sf);
369
497
  diagnostics.push({
498
+ rule: 'accessibility',
370
499
  message: `<img> at line ${line} has no 'alt' attribute. Add alt text for screen readers, or alt='' for decorative images.`,
371
500
  line,
372
501
  column,
@@ -376,6 +505,7 @@ function checkAccessibility(node, sf, diagnostics) {
376
505
  if (props.has('onClick') && !INTERACTIVE_ELEMENTS.has(tag) && !props.has('role')) {
377
506
  const { line, column } = pos(node, sf);
378
507
  diagnostics.push({
508
+ rule: 'accessibility',
379
509
  message: `onClick on <${tag}> at line ${line} without role and tabIndex. Non-interactive elements with click handlers are not keyboard-accessible. Add role='button' and tabIndex={0}, or use <button>.`,
380
510
  line,
381
511
  column,
@@ -405,6 +535,7 @@ function checkControlledInput(node, sf, diagnostics) {
405
535
  if (!props.has('onInput') && !props.has('onChange')) {
406
536
  const { line, column } = pos(node, sf);
407
537
  diagnostics.push({
538
+ rule: 'controlled-input',
408
539
  message: `Controlled input at line ${line}: reactive 'value' binding without 'onInput' handler. The binding will overwrite user input on every state update.`,
409
540
  line,
410
541
  column,
@@ -447,6 +578,7 @@ function checkChildStaticProps(node, sf, diagnostics) {
447
578
  if (ts.isObjectLiteralExpression(prop.initializer)) {
448
579
  const { line, column } = pos(node, sf);
449
580
  diagnostics.push({
581
+ rule: 'child-static-props',
450
582
  message: `child() at line ${line}: 'props' is a static object literal. It must be a reactive accessor function (s => ({ ... })) so props update when parent state changes.`,
451
583
  line,
452
584
  column,
@@ -477,6 +609,7 @@ function checkChildStaticProps(node, sf, diagnostics) {
477
609
  const kind = ts.isArrayLiteralExpression(init) ? 'array' : 'object';
478
610
  const { line, column } = pos(keyProp, sf);
479
611
  diagnostics.push({
612
+ rule: 'child-static-props',
480
613
  message: `child() at line ${line}: the 'props' accessor returns a fresh ${kind} literal for '${keyName}'. Prop diffing uses Object.is per key, so a freshly-constructed reference reports changed every render — propsMsg will fire on every parent update. Hoist to a module-level constant, reuse a reference from state, or return null from propsMsg when the value is unchanged.`,
481
614
  line,
482
615
  column,
@@ -507,52 +640,9 @@ function getReturnedObjectLiteral(fn) {
507
640
  return null;
508
641
  }
509
642
  // ── Bitmask overflow warning ────────────────────────────────────
510
- function collectStatePaths(sf) {
511
- const paths = new Set();
512
- function visit(node) {
513
- if ((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
514
- node.parameters.length === 1) {
515
- const param = node.parameters[0].name;
516
- if (ts.isIdentifier(param)) {
517
- // Check if this looks like a reactive accessor
518
- const parent = node.parent;
519
- if (ts.isPropertyAssignment(parent)) {
520
- const key = parent.name;
521
- if (ts.isIdentifier(key) && !/^on[A-Z]/.test(key.text)) {
522
- extractAccessPaths(node.body, param.text, paths);
523
- }
524
- }
525
- else if (ts.isCallExpression(parent) && parent.arguments[0] === node) {
526
- extractAccessPaths(node.body, param.text, paths);
527
- }
528
- }
529
- }
530
- ts.forEachChild(node, visit);
531
- }
532
- visit(sf);
533
- return paths;
534
- }
535
- function extractAccessPaths(node, paramName, paths) {
536
- if (ts.isPropertyAccessExpression(node)) {
537
- const chain = resolveSimpleChain(node, paramName);
538
- if (chain)
539
- paths.add(chain);
540
- }
541
- ts.forEachChild(node, (child) => extractAccessPaths(child, paramName, paths));
542
- }
543
- function resolveSimpleChain(node, paramName) {
544
- const parts = [];
545
- let current = node;
546
- while (ts.isPropertyAccessExpression(current)) {
547
- parts.unshift(current.name.text);
548
- current = current.expression;
549
- }
550
- if (!ts.isIdentifier(current) || current.text !== paramName)
551
- return null;
552
- if (parts.length > 2)
553
- return parts.slice(0, 2).join('.');
554
- return parts.join('.');
555
- }
643
+ // The path-scan walker lives in `collect-deps.ts` and is shared with
644
+ // the runtime bit-assignment path. Keeping one scanner means one truth
645
+ // about what counts as a reactive accessor.
556
646
  function checkBitmaskOverflow(node, sf, diagnostics, paths) {
557
647
  // Only emit once, on the component() call
558
648
  if (!ts.isCallExpression(node))
@@ -585,12 +675,32 @@ function checkBitmaskOverflow(node, sf, diagnostics, paths) {
585
675
  }
586
676
  const breakdown = sorted.map(([field, n]) => `${field} (${n})`).join(', ');
587
677
  const candidateList = candidates.map((f) => `\`${f}\``).join(', ');
678
+ // Co-occurrence analysis: identify top-level fields whose every
679
+ // sub-path always fires in the same accessor sets. Those are prime
680
+ // candidates to read as a single object — the parent path (one bit)
681
+ // replaces multiple sub-paths (one bit each), saving (count - 1) bits
682
+ // toward the 31 limit without the larger surgery that `child()`
683
+ // extraction requires.
684
+ const accessorSets = collectAccessorPathSets(sf);
685
+ const cooccurringFields = findCooccurringFields(paths, accessorSets);
686
+ const cooccurrenceNote = cooccurringFields.length > 0
687
+ ? `\n\nCo-occurrence detected: ` +
688
+ cooccurringFields
689
+ .map(({ field, saved }) => `every sub-path under \`${field}\` always fires together; reading \`s.${field}\` as one unit saves ${saved} bit${saved === 1 ? '' : 's'}`)
690
+ .join('; ') +
691
+ `. Bundle those reads into a single \`s.${cooccurringFields[0].field}\` ` +
692
+ `access (e.g. \`const ${cooccurringFields[0].field} = s.${cooccurringFields[0].field}\`) ` +
693
+ `before extraction — cheaper refactor, same budget relief.`
694
+ : '';
588
695
  diagnostics.push({
696
+ rule: 'bitmask-overflow',
589
697
  message: `Component at line ${line} has ${pathCount} unique state access paths ` +
590
698
  `(${overflow} past the 31-path limit). Paths 32..${pathCount} fall back to ` +
591
699
  `FULL_MASK — their changes re-evaluate every binding in the component, ` +
592
700
  `negating the bitmask optimization for those updates.\n\n` +
593
- `Top-level fields by path count: ${breakdown}.\n\n` +
701
+ `Top-level fields by path count: ${breakdown}.` +
702
+ cooccurrenceNote +
703
+ `\n\n` +
594
704
  `Recommended fix: extract ${candidateList} into ${candidates.length === 1 ? 'a' : ''} ` +
595
705
  `child component${candidates.length === 1 ? '' : 's'} via \`child()\` ` +
596
706
  `(see /api/dom#child). Each child gets its own 31-path bitmask, so the ` +
@@ -601,4 +711,136 @@ function checkBitmaskOverflow(node, sf, diagnostics, paths) {
601
711
  column,
602
712
  });
603
713
  }
714
+ /**
715
+ * Identify top-level fields whose every sub-path fires in the SAME set
716
+ * of accessors — the signature of paths that could share a single bit
717
+ * if the author read the parent object as one unit. Returns a list of
718
+ * `{ field, saved }` records where `saved = sub-path count - 1` (the
719
+ * bits freed by collapsing to the parent read).
720
+ *
721
+ * Only meaningful for fields with 2+ sub-paths; single-path top-level
722
+ * fields already occupy exactly one bit.
723
+ */
724
+ function findCooccurringFields(paths, accessorSets) {
725
+ // Group paths by top-level field. Only fields whose depth-2 paths
726
+ // uniformly share the same appearance-signature are candidates.
727
+ const subPathsByTop = new Map();
728
+ for (const p of paths) {
729
+ const dot = p.indexOf('.');
730
+ if (dot < 0)
731
+ continue; // depth-1 path — no bundling opportunity
732
+ const top = p.slice(0, dot);
733
+ const arr = subPathsByTop.get(top) ?? [];
734
+ arr.push(p);
735
+ subPathsByTop.set(top, arr);
736
+ }
737
+ // For each path, record the set of accessors that read it.
738
+ const appearances = new Map();
739
+ for (let i = 0; i < accessorSets.length; i++) {
740
+ for (const path of accessorSets[i]) {
741
+ if (!appearances.has(path))
742
+ appearances.set(path, new Set());
743
+ appearances.get(path).add(i);
744
+ }
745
+ }
746
+ const out = [];
747
+ for (const [field, subPaths] of subPathsByTop) {
748
+ if (subPaths.length < 2)
749
+ continue;
750
+ // Compute the signature for each sub-path and check they all match.
751
+ const first = appearances.get(subPaths[0]) ?? new Set();
752
+ let uniform = true;
753
+ for (let i = 1; i < subPaths.length; i++) {
754
+ const set = appearances.get(subPaths[i]) ?? new Set();
755
+ if (!setsEqual(first, set)) {
756
+ uniform = false;
757
+ break;
758
+ }
759
+ }
760
+ if (uniform)
761
+ out.push({ field, saved: subPaths.length - 1 });
762
+ }
763
+ return out.sort((a, b) => b.saved - a.saved);
764
+ }
765
+ function setsEqual(a, b) {
766
+ if (a.size !== b.size)
767
+ return false;
768
+ for (const x of a)
769
+ if (!b.has(x))
770
+ return false;
771
+ return true;
772
+ }
773
+ // ── scope/branch `on` reads no state ────────────────────────────
774
+ // If the discriminant accessor doesn't read any state paths, the key
775
+ // never changes after mount and the subtree never rebuilds. Likely a
776
+ // bug — warn so the author can verify intent.
777
+ function checkStaticOn(node, sf, diagnostics) {
778
+ if (!ts.isCallExpression(node))
779
+ return;
780
+ if (!ts.isIdentifier(node.expression))
781
+ return;
782
+ const name = node.expression.text;
783
+ if (name !== 'scope' && name !== 'branch')
784
+ return;
785
+ const optsArg = node.arguments[0];
786
+ if (!optsArg || !ts.isObjectLiteralExpression(optsArg))
787
+ return;
788
+ const onProp = optsArg.properties.find((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === 'on');
789
+ if (!onProp)
790
+ return;
791
+ const onValue = onProp.initializer;
792
+ if (!ts.isArrowFunction(onValue) && !ts.isFunctionExpression(onValue))
793
+ return;
794
+ // Extract paths rooted at `on`'s single parameter. Zero-param
795
+ // on (`on: () => 'x'`) definitionally reads no state and must warn.
796
+ const params = onValue.parameters;
797
+ const paths = new Set();
798
+ if (params.length === 1) {
799
+ const param = params[0].name;
800
+ if (!ts.isIdentifier(param))
801
+ return;
802
+ collectPathsInBody(onValue.body, param.text, paths);
803
+ }
804
+ else if (params.length !== 0) {
805
+ return;
806
+ }
807
+ if (paths.size > 0)
808
+ return;
809
+ const { line, column } = pos(node, sf);
810
+ diagnostics.push({
811
+ rule: 'static-on',
812
+ message: `${name}() at line ${line}: 'on' reads no state — the key never ` +
813
+ `changes, so the subtree mounts once and never rebuilds. ` +
814
+ `Is this intentional? If so, consider replacing with a static ` +
815
+ `builder; if not, reference the state field(s) that drive the ` +
816
+ `discriminant.`,
817
+ line,
818
+ column,
819
+ });
820
+ }
821
+ // Minimal state-path extractor used only by checkStaticOn; it needs the
822
+ // same "chain rooted at paramName" logic as the shared collector but
823
+ // without walking into nested reactive-accessor arrows (we only care
824
+ // about reads inside `on`'s immediate body).
825
+ function collectPathsInBody(body, paramName, out) {
826
+ if (ts.isPropertyAccessExpression(body)) {
827
+ const parts = [];
828
+ let current = body;
829
+ while (ts.isPropertyAccessExpression(current)) {
830
+ parts.unshift(current.name.text);
831
+ current = current.expression;
832
+ }
833
+ if (ts.isIdentifier(current) && current.text === paramName) {
834
+ out.add(parts.slice(0, 2).join('.'));
835
+ }
836
+ }
837
+ if (ts.isElementAccessExpression(body)) {
838
+ if (ts.isIdentifier(body.expression) &&
839
+ body.expression.text === paramName &&
840
+ ts.isStringLiteral(body.argumentExpression)) {
841
+ out.add(body.argumentExpression.text);
842
+ }
843
+ }
844
+ ts.forEachChild(body, (child) => collectPathsInBody(child, paramName, out));
845
+ }
604
846
  //# sourceMappingURL=diagnostics.js.map