* Emit same-file `references` edges from a symbol to the file-scope const/var it reads (TS/JS). * The engine doesn't edge const→consumer, so impact analysis misses "change this table, affect * its readers" (the ReScript-PR false positive). Same-file only (resolution is unambiguous), * disti
()
| 701 | * additive. Shadowed targets are pruned — see below. |
| 702 | */ |
| 703 | private flushValueRefs(): void { |
| 704 | const scopes = this.valueRefScopes; |
| 705 | const targets = this.fileScopeValues; |
| 706 | const fileScopeCounts = this.fileScopeValueCounts; |
| 707 | this.valueRefScopes = []; |
| 708 | this.fileScopeValues = new Map(); |
| 709 | this.fileScopeValueCounts = new Map(); |
| 710 | if (!this.valueRefsEnabled || !TreeSitterExtractor.VALUE_REF_LANGS.has(this.language)) return; |
| 711 | if (targets.size === 0 || scopes.length === 0 || isGeneratedFile(this.filePath)) return; |
| 712 | |
| 713 | // Prune SHADOWED targets. A target re-bound in an INNER scope (a |
| 714 | // bundled/Emscripten `const Module` re-declared as a nested `var Module`; a |
| 715 | // Go package `const Timeout` shadowed by a local `Timeout := …`; a Python |
| 716 | // module `CONFIG` shadowed by a local `CONFIG = …`) resolves to the inner |
| 717 | // binding for nested readers, so a file-scope edge is a false positive. |
| 718 | // Inner re-bindings aren't graph nodes, so detect them at the syntax level: |
| 719 | // count every declarator of the name across the tree and compare against how |
| 720 | // many FILE-SCOPE nodes carry it. A real shadow makes (declarators > |
| 721 | // file-scope nodes) — the excess is the local binding. A conditional |
| 722 | // module-level def (`try: X = a; except: X = b`) makes them EQUAL (both |
| 723 | // declarators are file-scope nodes), so it's correctly kept. Complements the |
| 724 | // path-based isGeneratedFile() check, which can't catch content-minified |
| 725 | // bundles. |
| 726 | // |
| 727 | // Declarator node types are per-grammar; a file only contains its own |
| 728 | // language's nodes, so matching all of them in one switch is safe. |
| 729 | if (this.tree) { |
| 730 | const declCounts = new Map<string, number>(); |
| 731 | const bump = (nameNode: SyntaxNode | null) => { |
| 732 | // `simple_identifier` is Kotlin's name node (a property declarator's name). |
| 733 | if (nameNode && (nameNode.type === 'identifier' || nameNode.type === 'simple_identifier')) { |
| 734 | const nm = getNodeText(nameNode, this.source); |
| 735 | if (targets.has(nm)) declCounts.set(nm, (declCounts.get(nm) ?? 0) + 1); |
| 736 | } |
| 737 | }; |
| 738 | const dstack: SyntaxNode[] = [this.tree.rootNode]; |
| 739 | let dvisited = 0; |
| 740 | while (dstack.length > 0 && dvisited < TreeSitterExtractor.MAX_VALUE_REF_NODES) { |
| 741 | const n = dstack.pop()!; |
| 742 | dvisited++; |
| 743 | switch (n.type) { |
| 744 | case 'variable_declarator': // TS/JS/tsx |
| 745 | case 'const_spec': // Go `const X = …` |
| 746 | case 'var_spec': // Go `var X = …` |
| 747 | bump(n.namedChild(0)); |
| 748 | break; |
| 749 | case 'const_item': // Rust `const X: T = …` |
| 750 | case 'static_item': // Rust `static X: T = …` |
| 751 | bump(getChildByField(n, 'name')); |
| 752 | break; |
| 753 | case 'let_declaration': // Rust `let x = …` (locals — the shadow source) |
| 754 | case 'short_var_declaration': // Go `x, Y := …` |
| 755 | case 'assignment': { // Python `X = …` / `X: T = …` / `A, B = …` |
| 756 | const left = getChildByField(n, 'left') ?? getChildByField(n, 'pattern') ?? n.namedChild(0); |
| 757 | if (left?.type === 'identifier') bump(left); |
| 758 | else if (left) for (const c of left.namedChildren) bump(c); |
| 759 | break; |
| 760 | } |
no test coverage detected