Skip to content

v0.184.0

Choose a tag to compare

@github-actions github-actions released this 03 Nov 13:32
· 63 commits to main since this release

Upgrading: every index rebuilds once. INDEX_VERSION goes 125 → 126
because some Python call edges are no longer created (below). The MCP server
rebuilds the index when it starts; from the command line,
code-graph-mcp reindex does. To pin the binary back:
npm i -g @sdsrs/code-graph@0.183.0 (or, from source, cargo install --locked --git https://github.com/sdsrss/code-graph-mcp --tag v0.183.0 --features embed-model), then delete ~/.cache/code-graph/bin/code-graph-mcp
and set CODE_GRAPH_NO_AUTO_UPDATE=1, as described under 0.171.0; the older
binary rebuilds its own index once too.

Python: a call through an imported name no longer binds the caller's own definition

logging.FileHandler.emit(self, record) in logging/handlers.py,
codecs.IncrementalEncoder.__init__(self, errors) in encodings/utf_16.py,
or C.m() after from ext import C name a definition of another module.
When the imported module was part of the project, such a call resolved like a
bare call, so it bound every same-named definition of the caller's own file:
in the standard library, callgraph --direction callees on
BaseRotatingHandler.emit listed the 8 other emit methods of
logging/handlers.py (SocketHandler.emit, SMTPHandler.emit, …); now it
lists none of them. The call no longer binds into the caller's file when all
of these hold:

  • Every binding of the call's first name in the file is an import (import ext, import ext as e, import pkg.ext, from ext import C, under try
    or not), and the file only reads attributes rooted at it: getattr(ext, …),
    f(ext), class D(ext.Base), annotations, default values (def f(x=ext.X)),
    ext.tbl[0] = 1, and so on.
  • Nothing the name reaches may be the caller's own module. What a call
    reaches is what one of the name's imports binds followed by the call's
    attribute chain: import a and a.mod.go() reach a.mod, from c import mod and mod.go() reach c.mod. In a/mod.py or src/a/mod.py that is
    the file itself, so those calls resolve as before; so do calls through ext
    in lib/ext.py or ext/__init__.py, and through any name one of whose
    imports is relative (from . import ext, from . import *).

Nothing is bound in place of the dropped edge; a call whose first name has no
same-named definition in the caller's file resolves as before. Everything else
resolves as before too: a name the file also binds some other way (an
assignment, a parameter, def, class, a loop or with … as target,
global, del, a match capture), a file that writes an attribute rooted at
the name (ext.go = go, del ext.go, ext.sub.go += 1), and a direct
ext.go() call on a name that holds one plain-imported module everywhere in
the file (since 0.176.0 it binds what from ext import go would).

Measured against 0.183.0: the Python standard library loses 517 call edges
(497 caller–callee pairs), nltk 3.9.2 170 (158) and flask 7, all same-file;
hono, leveldb, cobra and gin keep every edge. On none of the seven is an edge
added or its confidence label changed; 303 standard-library edges that stay
carry the new "loc":1 metadata key. On the standard library the
SCIP oracle's default floor (extracted + inferred) goes from 10,996 of
14,329 judged edges correct (76.7%) to 10,971 of 13,879 (79.0%): 2,908 wrong
edges instead of 3,333. The 25 correct pairs it loses, and the 5 gold calls
its recall loses, are all explicit base-class calls (logging.Handler.close(self),
Widget.destroy(self)) that the oracle credits to a subclass override in the
caller's file; Python runs the base class's method there. 54 standard-library
definitions whose only recorded callers were such calls now show no caller.
A full standard-library index took 1.3–2.7% more CPU time than 0.183.0 built
from source (two interleaved batches of five runs), and 0.2% less than the
released 0.183.0 binary (one batch).

Not covered

  • A module patched at run time in a way other than writing an attribute
    rooted at the call's own name: setattr(ext, "go", go),
    globals()["ext"] = …, vars(ext)["go"] = go, ext.__dict__["go"] = go,
    a write through another name (alias = ext; alias.go = go, a function
    handed ext, a list holding it, a second import of it), a write to a
    segment of the import's path (pkg.b = K before from pkg import b), a
    __getattr__ or __class__ hook, a patch made in another file,
    mock.patch, sys.modules, exec. On 11,588 generated files run under
    CPython (1,973 of them inside a package they import, 792 patching the
    module through another name), every call the new rule stopped binding
    while Python ran the file's own go used setattr, globals(), vars()
    or a write through another name.
  • A module that hands the caller's own definition back: os.path.join
    called inside posixpath.py, nltk's chunk/util.py calling
    nltk.chunk.tagstr2tree, which nltk/chunk/__init__.py imports from it,
    or the demo() functions of nltk's framenet.py and lin.py, which reach
    their own file's reader through nltk.corpus's lazy loader (from nltk.corpus import framenet as fn, then fn.frames(); 11 edges).
  • A direct ext.go() call on a name that holds one plain-imported module
    everywhere in the file, where from ext import go would find nothing, keeps
    its bind into the caller's file (15 wrong edges at the default floor in the
    standard library).
  • A file that uses the name somewhere the parser does not recognise as a read
    keeps every call through it as before, for example with ext.lock:,
    yield ext.x, [ext.x for _ in y] and case ext.CONST:.
  • Two import names of one module, one kept as before and one not (import ext as m1, import ext as m2, m1.flag = 0), can store the same call edge
    twice, once with the new "loc" metadata key; refs, impact and
    callgraph list it once.
  • A file whose syntax tree nests deeper than the import scan reads (one
    expression adding 300 terms is enough; 200 is not) keeps every call
    through an imported name as before.