* Normalize one value expression to zero or more function names. Recursion is * bounded (wrapper layers only); anything that isn't a recognized * function-value shape yields [].
( node: SyntaxNode, spec: FnRefSpec, source: string, depth: number )
| 523 | * function-value shape yields []. |
| 524 | */ |
| 525 | function normalizeValue( |
| 526 | node: SyntaxNode, |
| 527 | spec: FnRefSpec, |
| 528 | source: string, |
| 529 | depth: number |
| 530 | ): NormalizedRef[] { |
| 531 | if (depth > 4) return []; |
| 532 | const type = node.type; |
| 533 | |
| 534 | // Bare identifier |
| 535 | if (spec.idTypes.has(type)) { |
| 536 | return [{ name: getNodeText(node, source), node }]; |
| 537 | } |
| 538 | |
| 539 | // Transparent layers (argument, value_argument, literal_element, |
| 540 | // expression_list, block_argument). expression_list fans out (Go `a, b = f, g`). |
| 541 | const layerField = spec.layers?.get(type); |
| 542 | if (spec.layers?.has(type)) { |
| 543 | // Labeled-argument param-forward skip (Swift/Kotlin): `value: value` / |
| 544 | // `delay: delay` — when the label EQUALS the value identifier, the value |
| 545 | // is a forwarded local/parameter, not a function reference (Alamofire |
| 546 | // A/B finding; same rationale as the `this.x = x` assignment skip). |
| 547 | if (type === 'value_argument') { |
| 548 | const label = getChildByField(node, 'name'); |
| 549 | const value = getChildByField(node, 'value') ?? node.namedChild(node.namedChildCount - 1); |
| 550 | if ( |
| 551 | label && |
| 552 | value && |
| 553 | getNodeText(label, source).trim() === getNodeText(value, source).trim() |
| 554 | ) { |
| 555 | return []; |
| 556 | } |
| 557 | } |
| 558 | if (layerField) { |
| 559 | const inner = getChildByField(node, layerField); |
| 560 | return inner ? normalizeValue(inner, spec, source, depth + 1) : []; |
| 561 | } |
| 562 | const results: NormalizedRef[] = []; |
| 563 | for (let i = 0; i < node.namedChildCount; i++) { |
| 564 | const child = node.namedChild(i); |
| 565 | if (child) results.push(...normalizeValue(child, spec, source, depth + 1)); |
| 566 | } |
| 567 | return results; |
| 568 | } |
| 569 | |
| 570 | // Unary wrappers: &fn / @Fn / `fn _` |
| 571 | const unwrapField = spec.unwrap?.get(type); |
| 572 | if (spec.unwrap?.has(type)) { |
| 573 | // C-family `pointer_expression` covers BOTH `&x` (address-of — a function |
| 574 | // value) and `*x` (dereference — a data read, never a function value). |
| 575 | // Only `&` qualifies; without this, fmt's `*begin` reads resolved to its |
| 576 | // free `begin()` functions. |
| 577 | if (type === 'pointer_expression' && node.child(0)?.type !== '&') return []; |
| 578 | const inner = unwrapField ? getChildByField(node, unwrapField) : node.namedChild(0); |
| 579 | if (!inner) return []; |
| 580 | // C++ `&Widget::on_click` — keep the QUALIFIED name. Resolution scopes the |
| 581 | // method to that class (more precise than a bare-name match, and exempt |
| 582 | // from the cpp bare-ids-are-free-functions rule since `&Cls::m` is an |
no test coverage detected