circle-ir 3.32.0 → 3.34.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.
@@ -1,25 +1,43 @@
1
1
  /**
2
- * Runtime-registration extractor (issue #15 — Phase 1).
2
+ * Runtime-registration extractor (issue #15 — Phases 1, 2, 3).
3
3
  *
4
- * Recognises JS/TS framework registration patterns where a handler is wired
5
- * into a dispatch table at module-load time. Static call extraction sees the
6
- * registration call (`app.get(...)`) but not the edge from registrar → handler.
4
+ * Recognises framework registration patterns where a handler is wired into a
5
+ * dispatch table at module-load time. Static call extraction sees the
6
+ * registration call/decorator but not the edge from registrar → handler.
7
7
  *
8
8
  * Downstream consumers (e.g. dead-code reachability) read
9
9
  * `ir.runtime_registrations` and add each resolved handler as a virtual entry
10
10
  * root, eliminating "unreachable" false positives for framework handlers.
11
11
  *
12
- * Phase 1 covers:
13
- * - Express-family HTTP routes: `app.METHOD(path?, ...handlers)`
14
- * where METHOD ∈ HTTP_VERBS and receiver is express-shaped
12
+ * Phase 1 — JS/TS Express-family (shipped 3.32.0):
13
+ * - HTTP routes: `app.METHOD(path?, ...handlers)` for METHOD ∈ HTTP_VERBS
15
14
  * - Middleware: `app.use(...handlers)`
16
- * - Event listeners: `emitter.on('event', handler)` (when receiver is
17
- * express-shaped, otherwise skipped to avoid false-positive registrations)
15
+ * - Event listeners: `emitter.on('event', handler)` on express-shaped receivers
18
16
  *
19
- * Out of scope for Phase 1:
20
- * - NestJS / Python decorators (Phase 2)
21
- * - Rust trait dispatch (Phase 3)
22
- * - Subapp mounting (`app.use('/api', subApp)`) handler resolution
17
+ * Phase 2 Python decorators (3.33.0):
18
+ * - Every `@decorator` on a function emits a registration with handler =
19
+ * decorated function. Known frameworks are tagged (flask, fastapi,
20
+ * django, click, pytest, celery, numba); built-in (property,
21
+ * staticmethod, etc.) is tagged `stdlib`. Routing-style decorators
22
+ * (`@app.route`, `@app.get`, `@router.post`) are classified as
23
+ * `kind: 'http_route'` so downstream consumers can treat JS routes and
24
+ * Python routes uniformly.
25
+ *
26
+ * Phase 3 — Rust trait dispatch (3.34.0):
27
+ * - `impl Trait for Type { fn method(...) }` emits one `trait_impl`
28
+ * registration per method, recording the Self type as `receiver`, the
29
+ * trait path as `path`, and the method as both `registrar.method` and
30
+ * `handler.name`. Stdlib traits (Display, Debug, Iterator, …) are tagged
31
+ * `framework: 'stdlib'`; known web/async/serde frameworks (actix, axum,
32
+ * rocket, tokio, serde) are tagged accordingly.
33
+ * - `inventory::submit! { … }` and `#[linkme::distributed_slice]` emit
34
+ * `trait_impl` registrations with framework `'inventory'` / `'linkme'`.
35
+ *
36
+ * Out of scope:
37
+ * - Subapp mounting (`app.use('/api', subApp)`) handler resolution.
38
+ * - Cross-file trait → impl resolution scoped by `Cargo.toml` reachability
39
+ * (file-local impls only at extraction time; project-level resolution is
40
+ * deferred to a later cross-file pass).
23
41
  */
24
42
  import { getNodeText, getNodesFromCache } from '../parser.js';
25
43
  /** HTTP verb methods recognised on Express-family routers. */
@@ -45,13 +63,21 @@ const FRAMEWORK_MODULE_PATTERNS = [
45
63
  /**
46
64
  * Extract runtime-registration patterns from a parsed file.
47
65
  *
48
- * Returns `[]` for any language other than JavaScript/TypeScript in Phase 1.
66
+ * Phase 1 covers JavaScript/TypeScript. Phase 2 adds Python decorators.
67
+ * Phase 3 adds Rust trait dispatch (`impl Trait for Type`, `inventory::submit!`,
68
+ * `#[linkme::distributed_slice]`). Returns `[]` for any other language.
49
69
  */
50
70
  export function extractRuntimeRegistrations(tree, cache, language, imports) {
51
- if (language !== 'javascript' && language !== 'typescript') {
52
- return [];
71
+ if (language === 'javascript' || language === 'typescript') {
72
+ return extractJSRuntimeRegistrations(tree, cache, imports);
73
+ }
74
+ if (language === 'python') {
75
+ return extractPythonRuntimeRegistrations(tree, cache, imports);
76
+ }
77
+ if (language === 'rust') {
78
+ return extractRustRuntimeRegistrations(tree, cache);
53
79
  }
54
- return extractJSRuntimeRegistrations(tree, cache, imports);
80
+ return [];
55
81
  }
56
82
  function buildHandlerIndex(tree, cache, imports) {
57
83
  const decls = new Map();
@@ -263,4 +289,557 @@ function resolveHandler(node, index) {
263
289
  // Anything else (object literals, complex expressions): not a handler.
264
290
  return null;
265
291
  }
292
+ // =============================================================================
293
+ // Python — Phase 2
294
+ // =============================================================================
295
+ /** HTTP-route decorator method names (after the dotted prefix). */
296
+ const PY_HTTP_ROUTE_METHODS = new Set([
297
+ // Flask/Blueprint: app.route, blueprint.route, api.route
298
+ 'route',
299
+ // FastAPI / Starlette / DRF method-specific
300
+ 'get', 'post', 'put', 'patch', 'delete', 'head', 'options',
301
+ // Flask aliases (Flask 2.x): app.get/post/...
302
+ ]);
303
+ /** Flask middleware-style decorators. */
304
+ const PY_MIDDLEWARE_METHODS = new Set([
305
+ 'before_request', 'after_request', 'teardown_request',
306
+ 'before_first_request', 'teardown_appcontext',
307
+ // Starlette / FastAPI
308
+ 'middleware',
309
+ ]);
310
+ /** Event/lifecycle-style decorators. */
311
+ const PY_EVENT_METHODS = new Set([
312
+ 'errorhandler', 'on_event', 'exception_handler',
313
+ // Celery beat etc — not strictly events but lifecycle
314
+ ]);
315
+ /** Python stdlib / built-in decorators that don't register externally. */
316
+ const PY_STDLIB_DECORATORS = new Set([
317
+ 'property', 'staticmethod', 'classmethod', 'abstractmethod', 'cached_property',
318
+ 'dataclass', 'cache', 'lru_cache', 'singledispatch', 'singledispatchmethod',
319
+ 'contextmanager', 'asynccontextmanager', 'final', 'override',
320
+ 'wraps',
321
+ ]);
322
+ function summarisePythonImports(imports) {
323
+ const s = {
324
+ hasFlask: false, hasFastApi: false, hasCelery: false,
325
+ hasNumba: false, hasClick: false, hasPytest: false,
326
+ };
327
+ if (!imports)
328
+ return s;
329
+ for (const imp of imports) {
330
+ const mod = imp.from_package ?? '';
331
+ if (!mod)
332
+ continue;
333
+ if (/^flask(\b|\.)/.test(mod))
334
+ s.hasFlask = true;
335
+ if (/^fastapi(\b|\.)/.test(mod) || /^starlette(\b|\.)/.test(mod))
336
+ s.hasFastApi = true;
337
+ if (/^celery(\b|\.)/.test(mod))
338
+ s.hasCelery = true;
339
+ if (/^numba(\b|\.)/.test(mod))
340
+ s.hasNumba = true;
341
+ if (/^click(\b|\.)/.test(mod))
342
+ s.hasClick = true;
343
+ if (/^pytest(\b|\.)/.test(mod))
344
+ s.hasPytest = true;
345
+ }
346
+ return s;
347
+ }
348
+ function extractPythonRuntimeRegistrations(tree, cache, imports) {
349
+ const out = [];
350
+ const importSummary = summarisePythonImports(imports);
351
+ const decoratedDefs = getNodesFromCache(tree.rootNode, 'decorated_definition', cache);
352
+ for (const dd of decoratedDefs) {
353
+ // Find the function_definition child (skip class_definition for now —
354
+ // class-level decorators are not the dead-code use case).
355
+ let fnNode = null;
356
+ const decorators = [];
357
+ for (let i = 0; i < dd.childCount; i++) {
358
+ const child = dd.child(i);
359
+ if (!child)
360
+ continue;
361
+ if (child.type === 'decorator') {
362
+ decorators.push(child);
363
+ }
364
+ else if (child.type === 'function_definition' || child.type === 'async_function_definition') {
365
+ fnNode = child;
366
+ }
367
+ }
368
+ if (!fnNode || decorators.length === 0)
369
+ continue;
370
+ const handler = pythonHandlerFromFunctionDef(fnNode);
371
+ if (!handler)
372
+ continue;
373
+ for (const dec of decorators) {
374
+ const parsed = parsePythonDecorator(dec);
375
+ if (!parsed)
376
+ continue;
377
+ const { receiver, method, path, line, column } = parsed;
378
+ const { kind, framework } = classifyPythonDecorator(receiver, method, importSummary);
379
+ out.push({
380
+ kind,
381
+ framework,
382
+ registrar: { method, receiver, line, column },
383
+ ...(path !== undefined ? { path } : {}),
384
+ handler,
385
+ });
386
+ }
387
+ }
388
+ return out;
389
+ }
390
+ function pythonHandlerFromFunctionDef(fn) {
391
+ const nameNode = fn.childForFieldName('name');
392
+ if (!nameNode)
393
+ return null;
394
+ return {
395
+ name: getNodeText(nameNode),
396
+ line: fn.startPosition.row + 1,
397
+ column: fn.startPosition.column,
398
+ };
399
+ }
400
+ /**
401
+ * Parse a `decorator` node into receiver/method/path components.
402
+ * The decorator wraps one of: `identifier`, `attribute`, or `call`.
403
+ */
404
+ function parsePythonDecorator(dec) {
405
+ // Skip the leading `@` token; take the first non-trivial child.
406
+ let target = null;
407
+ for (let i = 0; i < dec.childCount; i++) {
408
+ const child = dec.child(i);
409
+ if (!child || child.type === '@')
410
+ continue;
411
+ target = child;
412
+ break;
413
+ }
414
+ if (!target)
415
+ return null;
416
+ const line = dec.startPosition.row + 1;
417
+ const column = dec.startPosition.column;
418
+ // @bare_decorator
419
+ if (target.type === 'identifier') {
420
+ return { receiver: '', method: getNodeText(target), line, column };
421
+ }
422
+ // @pkg.attr (no call)
423
+ if (target.type === 'attribute') {
424
+ const { receiver, method } = splitDottedAttribute(target);
425
+ return { receiver, method, line, column };
426
+ }
427
+ // @pkg.attr(...) or @bare_decorator(...)
428
+ if (target.type === 'call') {
429
+ const fnNode = target.childForFieldName('function');
430
+ if (!fnNode)
431
+ return null;
432
+ let receiver = '';
433
+ let method = '';
434
+ if (fnNode.type === 'identifier') {
435
+ method = getNodeText(fnNode);
436
+ }
437
+ else if (fnNode.type === 'attribute') {
438
+ const split = splitDottedAttribute(fnNode);
439
+ receiver = split.receiver;
440
+ method = split.method;
441
+ }
442
+ else {
443
+ // Complex expression like `make_decorator()(...)` — record textual.
444
+ method = getNodeText(fnNode);
445
+ }
446
+ // Look at first positional arg for a literal string path.
447
+ const path = extractFirstStringArg(target);
448
+ return { receiver, method, path, line, column };
449
+ }
450
+ return null;
451
+ }
452
+ /** Split `a.b.c` into receiver=`a.b`, method=`c`. */
453
+ function splitDottedAttribute(attr) {
454
+ const objectNode = attr.childForFieldName('object');
455
+ const attrNode = attr.childForFieldName('attribute');
456
+ const method = attrNode ? getNodeText(attrNode) : '';
457
+ const receiver = objectNode ? getNodeText(objectNode) : '';
458
+ return { receiver, method };
459
+ }
460
+ /** Extract first positional argument as a literal string if present. */
461
+ function extractFirstStringArg(call) {
462
+ const argsNode = call.childForFieldName('arguments');
463
+ if (!argsNode)
464
+ return undefined;
465
+ for (let i = 0; i < argsNode.childCount; i++) {
466
+ const child = argsNode.child(i);
467
+ if (!child)
468
+ continue;
469
+ if (child.type === '(' || child.type === ')' || child.type === ',')
470
+ continue;
471
+ // First real argument
472
+ if (child.type === 'string') {
473
+ return stripPythonStringQuotes(getNodeText(child));
474
+ }
475
+ // Anything else as first positional arg → no path
476
+ return undefined;
477
+ }
478
+ return undefined;
479
+ }
480
+ function stripPythonStringQuotes(s) {
481
+ // Handle prefixes like b'', r'', u'', f'' — we don't need their semantics here.
482
+ const m = s.match(/^[bBrRuUfF]{0,2}(['"])(.*)\1$/s);
483
+ if (m)
484
+ return m[2];
485
+ if (s.length >= 2 && (s[0] === '"' || s[0] === "'") && s[s.length - 1] === s[0]) {
486
+ return s.slice(1, -1);
487
+ }
488
+ return s;
489
+ }
490
+ /**
491
+ * Classify a Python decorator into kind + framework.
492
+ *
493
+ * The classification cascade:
494
+ * 1. stdlib built-in (property, staticmethod, ...) → kind=decorator, framework=stdlib
495
+ * 2. `@<framework>.<method>` where framework is recognised
496
+ * 3. `@app.<http_verb>` / `@router.<http_verb>` / `@app.route` → http_route
497
+ * 4. Middleware / event hooks on Flask-like receivers
498
+ * 5. Generic decorator → kind=decorator, framework=unknown
499
+ */
500
+ function classifyPythonDecorator(receiver, method, imp) {
501
+ // 1. stdlib built-ins (bare decorators, e.g. @property)
502
+ if (!receiver && PY_STDLIB_DECORATORS.has(method)) {
503
+ return { kind: 'decorator', framework: 'stdlib' };
504
+ }
505
+ // 2. Framework-prefixed decorators
506
+ if (receiver) {
507
+ const head = receiver.split('.')[0];
508
+ // pytest.fixture / pytest.mark.parametrize
509
+ if (head === 'pytest') {
510
+ return { kind: 'decorator', framework: 'pytest' };
511
+ }
512
+ if (head === 'click') {
513
+ return { kind: 'decorator', framework: 'click' };
514
+ }
515
+ if (head === 'numba' || head === 'nb') {
516
+ return { kind: 'decorator', framework: 'numba' };
517
+ }
518
+ if (head === 'celery') {
519
+ return { kind: 'decorator', framework: 'celery' };
520
+ }
521
+ }
522
+ // 3. HTTP-route decorators on app/router/blueprint/api receivers
523
+ if (receiver && PY_HTTP_ROUTE_METHODS.has(method)) {
524
+ const isRoutey = isPyRouterReceiver(receiver);
525
+ if (isRoutey) {
526
+ // Framework inference: import-driven
527
+ let framework = 'unknown';
528
+ if (imp.hasFlask)
529
+ framework = 'flask';
530
+ else if (imp.hasFastApi)
531
+ framework = 'fastapi';
532
+ else if (method === 'route')
533
+ framework = 'flask'; // Flask hallmark
534
+ else
535
+ framework = 'fastapi'; // verbs alone bias to FastAPI
536
+ return { kind: 'http_route', framework };
537
+ }
538
+ }
539
+ // 4. Middleware-style decorators
540
+ if (receiver && PY_MIDDLEWARE_METHODS.has(method)) {
541
+ return { kind: 'middleware', framework: imp.hasFlask ? 'flask' : (imp.hasFastApi ? 'fastapi' : 'unknown') };
542
+ }
543
+ // 5. Event-style decorators
544
+ if (receiver && PY_EVENT_METHODS.has(method)) {
545
+ return { kind: 'event_listener', framework: imp.hasFlask ? 'flask' : (imp.hasFastApi ? 'fastapi' : 'unknown') };
546
+ }
547
+ // 6. app.task / @<x>.task — celery if celery imported
548
+ if (method === 'task' && imp.hasCelery) {
549
+ return { kind: 'decorator', framework: 'celery' };
550
+ }
551
+ // 7. Django auth/method decorators (bare)
552
+ if (!receiver && (method === 'login_required' || method === 'require_http_methods' || method === 'api_view')) {
553
+ return { kind: 'decorator', framework: 'django' };
554
+ }
555
+ // Fallthrough
556
+ return { kind: 'decorator', framework: 'unknown' };
557
+ }
558
+ /** Names commonly used for Flask/FastAPI app/router/blueprint instances. */
559
+ function isPyRouterReceiver(receiver) {
560
+ const head = receiver.split('.')[0];
561
+ if (!head)
562
+ return false;
563
+ if (['app', 'router', 'blueprint', 'bp', 'api', 'application'].includes(head))
564
+ return true;
565
+ // Suffix conventions: my_router, user_bp, etc.
566
+ if (/_(router|bp|blueprint|app|api)$/.test(head))
567
+ return true;
568
+ return false;
569
+ }
570
+ // =============================================================================
571
+ // Phase 3 — Rust trait dispatch
572
+ // =============================================================================
573
+ /**
574
+ * Standard-library traits whose `impl` blocks are tagged `framework: 'stdlib'`.
575
+ * Match is by last segment of the trait path, so both `Display` and
576
+ * `std::fmt::Display` classify the same way.
577
+ */
578
+ const RUST_STDLIB_TRAITS = new Set([
579
+ // Formatting
580
+ 'Display', 'Debug', 'Write',
581
+ // Conversion
582
+ 'From', 'Into', 'TryFrom', 'TryInto', 'AsRef', 'AsMut', 'ToString', 'FromStr',
583
+ // Iteration
584
+ 'Iterator', 'IntoIterator', 'FromIterator', 'DoubleEndedIterator',
585
+ 'ExactSizeIterator', 'FusedIterator',
586
+ // Comparison + hashing
587
+ 'PartialEq', 'Eq', 'PartialOrd', 'Ord', 'Hash',
588
+ // Markers + defaults
589
+ 'Default', 'Copy', 'Clone', 'Send', 'Sync', 'Unpin', 'Sized', 'Any',
590
+ // Resource management
591
+ 'Drop',
592
+ // Async
593
+ 'Future', 'IntoFuture',
594
+ // Operators
595
+ 'Add', 'Sub', 'Mul', 'Div', 'Rem', 'Neg', 'Not',
596
+ 'AddAssign', 'SubAssign', 'MulAssign', 'DivAssign', 'RemAssign',
597
+ 'BitAnd', 'BitOr', 'BitXor', 'Shl', 'Shr',
598
+ 'Deref', 'DerefMut', 'Index', 'IndexMut',
599
+ // Closures
600
+ 'Fn', 'FnMut', 'FnOnce',
601
+ // Error + I/O
602
+ 'Error', 'Read', 'Write', 'Seek', 'BufRead',
603
+ // Misc
604
+ 'Borrow', 'BorrowMut', 'ToOwned',
605
+ ]);
606
+ /**
607
+ * Trait-path module prefixes → framework tag. Matched against the leading
608
+ * segments of `impl PathSegment::… for Type`. Longer prefixes win.
609
+ */
610
+ const RUST_TRAIT_FRAMEWORK_PREFIXES = [
611
+ { prefix: /^actix(_web)?(::|$)/, framework: 'actix' },
612
+ { prefix: /^axum(::|$)/, framework: 'axum' },
613
+ { prefix: /^rocket(::|$)/, framework: 'rocket' },
614
+ { prefix: /^tokio(::|$)/, framework: 'tokio' },
615
+ { prefix: /^serde(_\w+)?(::|$)/, framework: 'serde' },
616
+ { prefix: /^std(::|$)/, framework: 'stdlib' },
617
+ { prefix: /^core(::|$)/, framework: 'stdlib' },
618
+ { prefix: /^alloc(::|$)/, framework: 'stdlib' },
619
+ ];
620
+ /**
621
+ * Walk a Rust parse tree and emit one `RuntimeRegistration` per:
622
+ * - `impl Trait for Type` method (Self-type as receiver, trait as `path`)
623
+ * - `inventory::submit!` macro invocation
624
+ * - `#[…distributed_slice(…)]` attribute on a static/function item
625
+ */
626
+ function extractRustRuntimeRegistrations(tree, cache) {
627
+ const regs = [];
628
+ const implNodes = getNodesFromCache(tree.rootNode, 'impl_item', cache);
629
+ for (const impl of implNodes) {
630
+ collectRustImplRegistrations(impl, regs);
631
+ }
632
+ const macroNodes = getNodesFromCache(tree.rootNode, 'macro_invocation', cache);
633
+ for (const macro of macroNodes) {
634
+ const rec = parseInventorySubmit(macro);
635
+ if (rec)
636
+ regs.push(rec);
637
+ }
638
+ // Distributed-slice attributes — attribute_item is a top-level sibling of the
639
+ // decorated static/function. Walk attribute_item nodes and look ahead.
640
+ const attrNodes = getNodesFromCache(tree.rootNode, 'attribute_item', cache);
641
+ for (const attr of attrNodes) {
642
+ const rec = parseDistributedSliceAttribute(attr);
643
+ if (rec)
644
+ regs.push(rec);
645
+ }
646
+ return regs;
647
+ }
648
+ /** Emit one trait_impl registration per method in an `impl Trait for Type` block. */
649
+ function collectRustImplRegistrations(impl, regs) {
650
+ const traitNode = impl.childForFieldName('trait');
651
+ if (!traitNode)
652
+ return; // inherent impl: skip
653
+ const typeNode = impl.childForFieldName('type');
654
+ if (!typeNode)
655
+ return;
656
+ const traitText = getNodeText(traitNode).trim();
657
+ const traitLastSegment = lastRustPathSegment(stripRustGenerics(traitText));
658
+ const selfType = getNodeText(typeNode).trim();
659
+ const framework = classifyRustTrait(traitText);
660
+ const body = impl.childForFieldName('body');
661
+ if (!body)
662
+ return;
663
+ for (let i = 0; i < body.childCount; i++) {
664
+ const child = body.child(i);
665
+ if (!child || child.type !== 'function_item')
666
+ continue;
667
+ const nameNode = child.childForFieldName('name');
668
+ if (!nameNode)
669
+ continue;
670
+ const methodName = getNodeText(nameNode);
671
+ regs.push({
672
+ kind: 'trait_impl',
673
+ framework,
674
+ registrar: {
675
+ method: methodName,
676
+ receiver: selfType,
677
+ line: impl.startPosition.row + 1,
678
+ column: impl.startPosition.column,
679
+ },
680
+ path: traitLastSegment || traitText,
681
+ handler: {
682
+ name: methodName,
683
+ line: child.startPosition.row + 1,
684
+ column: child.startPosition.column,
685
+ },
686
+ });
687
+ }
688
+ }
689
+ /** Strip turbofish/generic arguments from a Rust trait path. */
690
+ function stripRustGenerics(text) {
691
+ // Drop everything starting at the first `<` so `Display<T>` → `Display`,
692
+ // `std::fmt::Display<'a>` → `std::fmt::Display`.
693
+ const idx = text.indexOf('<');
694
+ return idx >= 0 ? text.slice(0, idx) : text;
695
+ }
696
+ /** Last `::`-delimited segment of a Rust path. */
697
+ function lastRustPathSegment(path) {
698
+ const parts = path.split('::');
699
+ return parts[parts.length - 1] || path;
700
+ }
701
+ /** Classify a Rust trait path to a framework tag. */
702
+ function classifyRustTrait(traitText) {
703
+ const stripped = stripRustGenerics(traitText).trim();
704
+ const last = lastRustPathSegment(stripped);
705
+ // Stdlib by last-segment match (covers bare `Display` import).
706
+ if (RUST_STDLIB_TRAITS.has(last))
707
+ return 'stdlib';
708
+ // Framework by leading module prefix.
709
+ for (const { prefix, framework } of RUST_TRAIT_FRAMEWORK_PREFIXES) {
710
+ if (prefix.test(stripped))
711
+ return framework;
712
+ }
713
+ return 'unknown';
714
+ }
715
+ /**
716
+ * Recognise `inventory::submit! { Type::new(…) }` (or variations) and emit a
717
+ * registration with framework `'inventory'`. The handler name is the first
718
+ * identifier inside the token tree.
719
+ */
720
+ function parseInventorySubmit(macro) {
721
+ const macroName = macro.childForFieldName('macro');
722
+ if (!macroName)
723
+ return null;
724
+ const name = getNodeText(macroName).trim();
725
+ if (name !== 'inventory::submit' && name !== 'submit')
726
+ return null;
727
+ // Belt-and-braces: require an `inventory::` prefix unless the scoped form matches.
728
+ if (name === 'submit')
729
+ return null;
730
+ // Find the token_tree (the macro body).
731
+ let tokenTree = null;
732
+ for (let i = 0; i < macro.childCount; i++) {
733
+ const c = macro.child(i);
734
+ if (c && c.type === 'token_tree') {
735
+ tokenTree = c;
736
+ break;
737
+ }
738
+ }
739
+ if (!tokenTree)
740
+ return null;
741
+ const handlerName = firstIdentifierInTokenTree(tokenTree);
742
+ return {
743
+ kind: 'trait_impl',
744
+ framework: 'inventory',
745
+ registrar: {
746
+ method: 'submit',
747
+ receiver: 'inventory',
748
+ line: macro.startPosition.row + 1,
749
+ column: macro.startPosition.column,
750
+ },
751
+ path: 'inventory::submit',
752
+ handler: {
753
+ name: handlerName,
754
+ line: tokenTree.startPosition.row + 1,
755
+ column: tokenTree.startPosition.column,
756
+ },
757
+ };
758
+ }
759
+ /** Walk a token_tree and return the first non-punctuation identifier text. */
760
+ function firstIdentifierInTokenTree(tokenTree) {
761
+ for (let i = 0; i < tokenTree.childCount; i++) {
762
+ const c = tokenTree.child(i);
763
+ if (!c)
764
+ continue;
765
+ if (c.type === 'identifier' || c.type === 'scoped_identifier' || c.type === 'type_identifier') {
766
+ return getNodeText(c).trim();
767
+ }
768
+ }
769
+ return null;
770
+ }
771
+ /**
772
+ * Recognise `#[linkme::distributed_slice(…)]` (or `#[distributed_slice(…)]`)
773
+ * and emit a registration whose handler is the next sibling static/function.
774
+ */
775
+ function parseDistributedSliceAttribute(attrItem) {
776
+ // Find the inner `attribute` node carrying the path.
777
+ let attr = null;
778
+ for (let i = 0; i < attrItem.childCount; i++) {
779
+ const c = attrItem.child(i);
780
+ if (c && c.type === 'attribute') {
781
+ attr = c;
782
+ break;
783
+ }
784
+ }
785
+ if (!attr)
786
+ return null;
787
+ const pathNode = attr.child(0);
788
+ if (!pathNode)
789
+ return null;
790
+ const pathText = getNodeText(pathNode).trim();
791
+ // Accept either fully-qualified `linkme::distributed_slice` or bare
792
+ // `distributed_slice` (common with `use linkme::distributed_slice;`).
793
+ if (pathText !== 'linkme::distributed_slice' && pathText !== 'distributed_slice')
794
+ return null;
795
+ // Walk forward through following siblings of attrItem (under the same parent)
796
+ // to find the decorated static_item or function_item.
797
+ // web-tree-sitter returns fresh Node wrappers from `child(i)`, so compare by
798
+ // node `.id` rather than reference identity.
799
+ const parent = attrItem.parent;
800
+ if (!parent)
801
+ return null;
802
+ let attrIndex = -1;
803
+ for (let i = 0; i < parent.childCount; i++) {
804
+ const c = parent.child(i);
805
+ if (c && c.id === attrItem.id) {
806
+ attrIndex = i;
807
+ break;
808
+ }
809
+ }
810
+ if (attrIndex < 0)
811
+ return null;
812
+ let handlerNode = null;
813
+ for (let j = attrIndex + 1; j < parent.childCount; j++) {
814
+ const sib = parent.child(j);
815
+ if (!sib)
816
+ continue;
817
+ if (sib.type === 'attribute_item')
818
+ continue; // chained attributes
819
+ if (sib.type === 'static_item' || sib.type === 'function_item') {
820
+ handlerNode = sib;
821
+ }
822
+ break;
823
+ }
824
+ if (!handlerNode)
825
+ return null;
826
+ const nameNode = handlerNode.childForFieldName('name');
827
+ const handlerName = nameNode ? getNodeText(nameNode).trim() : null;
828
+ return {
829
+ kind: 'trait_impl',
830
+ framework: 'linkme',
831
+ registrar: {
832
+ method: 'distributed_slice',
833
+ receiver: 'linkme',
834
+ line: attrItem.startPosition.row + 1,
835
+ column: attrItem.startPosition.column,
836
+ },
837
+ path: 'linkme::distributed_slice',
838
+ handler: {
839
+ name: handlerName,
840
+ line: handlerNode.startPosition.row + 1,
841
+ column: handlerNode.startPosition.column,
842
+ },
843
+ };
844
+ }
266
845
  //# sourceMappingURL=runtime-registrations.js.map