Repository: colbymchenry/codegraph (2026-07-16T20:55:36Z)

Issue:
[python] matching built-ins
Input
```python
  def format_fields(values):
        # nested local helper, only reachable inside format_fields
        def join(vals):
                return "-".join(sorted(vals))

        return join(values)
  def report_missing(unresolved):
        # str.join on a literal: no relation to the local join above
        missing_list = ", ".join(sorted(unresolved))
        return f"Could not resolve: {missing_list}"
```
Output
```

  $ codegraph callees report_missing
  Callees of "report_missing" (1):
  function    join
    repro.py:3

  $ codegraph callers join
  Callers of "join" (2):
  function    format_fields
    repro.py:1
  function    report_missing
    repro.py:9
```

  1. `report_missing` calls `str.join`, a builtin method on a string literal. The graph resolves it
  by bare name to the nested join at line 3. Both edges are fabricated: `report_missing` has zero
  project callees, and join has exactly one caller (`format_fields`).
  2. Two distinct failures stacked: attribute calls on non-project receivers `(", ".join(...))`
  shouldn't resolve to project symbols at all, and a function nested inside `format_fields` is
  lexically unreachable from `report_missing`, so scope alone rules the edge out.
  3. Impact scales with name commonality. `join` here, but same mechanism fires on `get`, `run`,
  `close`, `update` across modules: impact/callers (blast radius before editing) then reports
  dependents that don't exist, and affected can select wrong test files.

I believe this can be fixed in all languages that have built-ins by using a `literalReceiverTypes` in all of the `src/extraction/languages/*.ts` files.  But a bit hacky and drift prone. 

OR: don't patch extraction but resolver instead:  a nested local is not reachable from outside its container and should not be part of the graph attribution; exclude candidates whose containing symbol is a function other than the caller (or the caller's ancestor/children). 



Discussion:
Perhaps this can help people trying to fix this? Was my minimal viable solution:

https://github.com/h8d13/codegraph/commit/f59ae3c2e9ef0d2577005d7d613234e97f5cd0bb
Fixed in #1317 (merged to main, ships in the next release). Both of your proposed fixes landed — they turned out to be independent defects, so it's an "and" rather than an "or":

1. **Literal receivers**: a member call on any literal (`", ".join(...)`, `"x".upper()`, `[].append` equivalents across languages) now emits no call reference at all — implemented once in the shared extractor rather than per-language `literalReceiverTypes` lists, exactly to avoid the drift you were worried about.
2. **Nested-local scope**: your reachability argument, applied at the name matcher — a function nested inside another function only resolves as a call target from refs *inside* its container's line range. Class methods and top-level symbols are structurally unaffected.

Your repro is pinned as a regression test: `join` reports exactly one caller (`format_fields`), and `report_missing` has zero project callees. Controls: psf/requests re-indexes byte-identical; excalidraw sheds 27 edges that can only have been literal-receiver refs — the `get`/`run`/`close`/`update` commonality you predicted is exactly what stops leaking into impact and affected-test results.

`codegraph index -f` after upgrading rebuilds the edges.
Tysm :) Will try again on release! 



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 955349b47..536110e7c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ### Fixes
 
+- Method calls on literals (`", ".join(...)` in Python, `"x".split(...)` in JavaScript, and the like) no longer produce call edges to unrelated project functions that happen to share the builtin's name — a codebase with a function called `join`, `get`, or `update` could show phantom callers from every string-builtin use. Additionally, a function nested inside another function is now only matched as a call target from inside its container, since it isn't reachable from anywhere else. Blast-radius and affected-test results get cleaner on Python and JavaScript codebases especially. (#1230)
 - Go method calls through a struct field (`target.conn.Exec(...)`) no longer bind to unrelated same-named local methods when the field's type is external — `conn *sql.DB` calls were being attributed to a local interface that happened to declare `Exec`, fabricating internal dependencies. Chained field calls now resolve by inferring the field's declared type from the struct definition: in-project types (including unexported ones like chi's `tree *node`) gain correct, validated call edges that never existed before, and external types (standard library, third-party modules) are left unlinked instead of guessed. (#1276)
 - TypeScript/JavaScript method calls through an imported singleton (`import { store } from './store'; store.notify()`) now resolve to the class method instead of the exported constant, so `codegraph callers` sees cross-file callers of the method — previously only same-file calls were attributed and a method used everywhere could look unused. The same declaration-based type inference applies across the languages that share it (Python, Java, Kotlin, Go, and more), and a failed inference keeps the old edge rather than guessing. (#1292)
 - `codegraph node <symbol> -f <file>` now prints the symbol's source body. Pinning an ambiguous name to a specific file (the whole point of `-f` when many files define the same function) returned only the location and caller trail with no code. (#1284)
diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts
index 9ee791c1f..f80673846 100644
--- a/src/extraction/tree-sitter.ts
+++ b/src/extraction/tree-sitter.ts
@@ -362,6 +362,30 @@ const INSTANTIATION_KINDS: ReadonlySet<string> = new Set([
 /**
  * TreeSitterExtractor - Main extraction class
  */
+/**
+ * tree-sitter node types (across grammars) for literal expressions in method-
+ * call RECEIVER position. A literal's methods are the language's builtins —
+ * `", ".join`, `"x".toUpperCase()`, `5.times`, `[].concat` — never project
+ * symbols, so a member call on one must not emit a `calls` ref that bare-name
+ * matching could bind to an unrelated same-named project function (#1230).
+ */
+const LITERAL_RECEIVER_TYPES = new Set([
+  // strings
+  'string', 'string_literal', 'interpreted_string_literal', 'raw_string_literal',
+  'template_string', 'concatenated_string', 'formatted_string', 'f_string',
+  'line_string_literal', 'string_content', 'heredoc_body',
+  // numbers
+  'number', 'number_literal', 'integer', 'integer_literal', 'float',
+  'float_literal', 'int_literal', 'decimal_integer_literal', 'real_literal',
+  // chars / runes / regex / booleans / null-likes
+  'char_literal', 'character_literal', 'rune_literal', 'regex', 'regex_literal',
+  'true', 'false', 'boolean_literal', 'bool_literal', 'none', 'null', 'nil',
+  'null_literal', 'undefined',
+  // collection literals
+  'list', 'list_literal', 'array', 'array_literal', 'array_creation_expression',
+  'dictionary', 'dict_literal', 'object', 'tuple', 'set',
+]);
+
 export class TreeSitterExtractor {
   private filePath: string;
   private language: Language;
@@ -4351,6 +4375,16 @@ export class TreeSitterExtractor {
               getChildByField(func, 'operand') ||
               getChildByField(func, 'argument') ||
               func.namedChild(0);
+            // A LITERAL receiver — `", ".join(...)`, `"x".toUpperCase()`,
+            // `5.times`, `[].concat(...)` — calls a builtin of the literal's
+            // type, never a project symbol. The bare-name fallback below let
+            // these exact-match an unrelated same-named project function
+            // (`", ".join` bound to a local `join` defined inside a DIFFERENT
+            // function, #1230). Emit nothing: a silent miss, never a wrong
+            // edge. Nested calls in the arguments are visited independently.
+            if (receiver && LITERAL_RECEIVER_TYPES.has(receiver.type)) {
+              return;
+            }
             const SKIP_RECEIVERS = new Set(['self', 'this', 'cls', 'super']);
             if (receiver && (receiver.type === 'identifier' || receiver.type === 'simple_identifier' || receiver.type === 'field_identifier')) {
               const receiverName = getNodeText(receiver, this.source);
diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts
index fca29e28a..329dc5d40 100644
--- a/src/resolution/name-matcher.ts
+++ b/src/resolution/name-matcher.ts
@@ -340,6 +340,42 @@ export function matchFunctionRef(
   return null;
 }
 
+/**
+ * A function nested inside another FUNCTION is only callable from within its
+ * container — Python, JS/TS, and every closure language scope it lexically.
+ * Resolving a bare name from elsewhere to a nested local fabricates an edge
+ * scope already rules out: `join(...)` in one function must never bind to a
+ * `join` defined inside a DIFFERENT function (#1230). A candidate whose
+ * qualifiedName parent is a same-file function/method is kept only when the
+ * ref originates inside that parent's line range. Class members are
+ * unaffected (their parent resolves to a class-like node), as are top-level
+ * symbols and C++ namespace-prefixed names (the prefix has no node).
+ */
+function isLexicallyReachable(
+  candidate: Node,
+  ref: UnresolvedRef,
+  context: ResolutionContext
+): boolean {
+  if (candidate.kind !== 'function') return true;
+  const qn = candidate.qualifiedName;
+  if (!qn || !qn.includes('::')) return true;
+  const parentQn = qn.slice(0, qn.lastIndexOf('::'));
+  const containers = context
+    .getNodesByQualifiedName(parentQn)
+    .filter(
+      (p) =>
+        p.filePath === candidate.filePath &&
+        (p.kind === 'function' || p.kind === 'method') &&
+        p.startLine <= candidate.startLine &&
+        p.endLine >= candidate.endLine
+    );
+  if (containers.length === 0) return true;
+  return (
+    ref.filePath === candidate.filePath &&
+    containers.some((p) => ref.line >= p.startLine && ref.line <= p.endLine)
+  );
+}
+
 /**
  * Try to resolve a reference by exact name match
  */
@@ -357,7 +393,9 @@ export function matchByExactName(
   // findBestMatch — O(K²) per package, the dominant cost of "Resolving refs" on
   // large import-heavy (front-end + back-end) repos (#915).
   const candidates = applyLanguageGate(context.getNodesByName(ref.referenceName), ref)
-    .filter((n) => n.kind !== 'import');
+    .filter((n) => n.kind !== 'import')
+    // Nested locals are only reachable from inside their container (#1230).
+    .filter((n) => isLexicallyReachable(n, ref, context));
 
   if (candidates.length === 0) {
     return null;


Test patch:
diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts
index 3965ea9d2..eca1778ff 100644
--- a/__tests__/resolution.test.ts
+++ b/__tests__/resolution.test.ts
@@ -2541,6 +2541,57 @@ func main() {
     });
   });
 
+  describe('Literal receivers and nested-local scope (#1230)', () => {
+    // Two stacked fabrications: `", ".join(...)` (a builtin on a string
+    // literal) exact-matched a project function named `join` — one that was
+    // moreover nested inside a DIFFERENT function and thus lexically
+    // unreachable. Literal receivers now emit no call ref at all, and
+    // exact-match refuses candidates nested in a function the ref isn't in.
+    it("str-literal builtin calls don't bind to project symbols; nested locals only resolve from inside their container", async () => {
+      const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1230-'));
+      try {
+        fs.writeFileSync(
+          path.join(tmpDir, 'repro.py'),
+          `def format_fields(values):
+    def join(vals):
+        return "-".join(sorted(vals))
+
+    return join(values)
+
+
+def report_missing(unresolved):
+    missing_list = ", ".join(sorted(unresolved))
+    return f"Could not resolve: {missing_list}"
+`
+        );
+
+        const cg = CodeGraph.initSync(tmpDir);
+        await cg.indexAll();
+
+        const join = (await cg.searchNodes('join', { limit: 5 })).find(
+          (r) => r.node.kind === 'function' && r.node.name === 'join'
+        );
+        expect(join).toBeDefined();
+
+        // Exactly one caller: the enclosing format_fields. Neither
+        // report_missing (literal receiver) nor join itself (its own literal
+        // "-".join) may appear.
+        const callers = await cg.getCallers(join!.node.id);
+        expect(callers.map((c) => c.node.name)).toEqual(['format_fields']);
+
+        // report_missing has zero project callees.
+        const reportMissing = (await cg.searchNodes('report_missing', { limit: 5 })).find(
+          (r) => r.node.kind === 'function'
+        );
+        const callees = await cg.getCallees(reportMissing!.node.id);
+        expect(callees.filter((c) => c.node.name === 'join')).toHaveLength(0);
+        cg.close();
+      } finally {
+        fs.rmSync(tmpDir, { recursive: true, force: true });
+      }
+    }, 30000);
+  });
+
   describe('Go field-chain receiver calls (#1276)', () => {
     // `target.conn.Exec(...)` where `conn *sql.DB` used to emit a BARE `Exec`
     // ref, which exact-matched the only local `Exec` — an unrelated