| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
…#2387) Python emitted zero `imports` edges, leaving `deps`, `impact` and `map` blind on every Python repo, and `import lib as L` + `L.f()` resolved to nothing, so the callee was reported as dead code. Two defects sat behind that: 1. Neither resolver had a Python branch. A dotted module path is not a filesystem path, so it fell through to the bare-specifier fallback and was echoed back unchanged, matching no file node. Python's relative imports share JS's leading-dot spelling but mean "climb the package tree", so they were mis-resolved by the generic relative branch too. 2. The extractor put the alias in `Import.source` for `import lib as L`, which can never resolve to a file, and `import a, b` collapsed into one record naming only `a`. Resolution derives the import root from package layout — walk up while each level has `__init__.py` — so the PyPA "src layout" and a flat layout work under one rule with no configuration, and honours pyproject-declared roots (`pythonpath`, `package-dir`, poetry `from`, setuptools `where`) for roots no convention implies. Module bindings are now recorded in a new `namespaceBindings` field, which call resolution reads to resolve `L.f()` as "f, as declared in the module L refers to". `from pkg import submod` is resolved as a submodule when `pkg/submod.py` exists, so it both binds a namespace and depends on the module that actually changed rather than only on `pkg/__init__.py`. Mirrored in crates/codegraph-core; both engines verified to produce identical edge sets on the fixtures.
Greptile SummaryThe follow-up completes Python import resolution across the TypeScript/WASM and native Rust engines.
Confidence Score: 5/5The PR appears safe to merge. The previously reported aliased submodule failure is fixed, and no blocking failure remains. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
Source["Python import statement"] --> Extract["Extract source, local binding, and rename pair"]
Extract --> Resolve["Resolve module path or imported submodule"]
Resolve --> ImportEdge["Emit file-level imports edge"]
Resolve --> Namespace["Map local alias to module file"]
Namespace --> Call["Resolve alias.function() in that file"]
Call --> CallEdge["Emit scoped call edge"]
Reviews (2): Last reviewed commit: "fix(python): preserve local alias in ali..." | Re-trigger Greptile |
Sorry, something went wrong.
| if (name !== local) importedOriginalNames.set(local, name); | ||
| // `from pkg import submod` binds a module too, but which reading applies | ||
| // depends on whether `pkg/submod.py` exists — a question only the | ||
| // resolver can answer (#2387). | ||
| const submodule = resolvePythonSubmodule( | ||
| path.join(rootDir, relPath), | ||
| imp.source, | ||
| original, | ||
| rootDir, | ||
| ctx.allFiles, | ||
| ); |
There was a problem hiding this comment.
Aliased submodule binding is lost
When Python uses from pkg import submod as alias, extraction records only submod, so this branch keys namespaceImports under the original name while alias.f() looks up the local alias. The call therefore produces no edge to f in pkg/submod.py, leaving dependency, impact, map, and dead-code results incorrect in both engines.
Knowledge Base Used:
Sorry, something went wrong.
There was a problem hiding this comment.
Fixed — handlePyImportFrom (and the mirrored handle_import_from_stmt in python.rs) now records the local alias in names and preserves the pre-alias name in renamedImports/renamed_imports, exactly like the existing mechanism for JS's import { X as Y } (#1730). importNamePairs/import_name_pairs already recover the pre-alias name from there for resolvePythonSubmodule, so alias.f() now resolves correctly in both engines — no resolver-side changes were needed in build-edges.ts/incremental.ts/resolve.rs. Added extractor unit tests in both engines plus an integration test (from pipeline.stages import transform as tr; tr.run_transform([])) covering the call-through-alias case. Pushed in 230ca0b.
Sorry, something went wrong.
Codegraph Impact Analysis29 functions changed → 56 callers affected across 9 files
|
Sorry, something went wrong.
…2410) `from pkg import submod as alias` recorded the pre-alias name (`submod`) as the local binding in both engines, so `alias.f()` had no local name to key namespace/submodule resolution off of and resolved to nothing. This is the `from ... import` analogue of the `import lib as L` alias-loss this PR already fixes for the plain import form. Mirrors the existing renamedImports/renamed_imports mechanism used for JS's `import { X as Y }` (#1730): `names` now carries the local alias, and the pre-alias name is preserved in renamedImports/renamed_imports for resolvePythonSubmodule/barrel tracing to look up. import_name_pairs already recovers it from there, so no downstream resolver change is needed. Adds extractor unit tests in both engines plus an integration regression test for the call-through-alias case. docs check acknowledged: bug fix only, no new language/feature/architecture change. Found in Greptile review of #2410. Impact: 1 functions changed, 4 affected
|
Addressed Greptile's review feedback:
Pushed in 230ca0b. |
Sorry, something went wrong.
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Closes #2387.
The problem
Python emitted zero imports edges, so deps, impact and map were blind on every Python repo, and import lib as L + L.f() resolved to nothing — leaving the callee reported as dead code. Four actively-developed Optave Python services are affected.
Two distinct defects, both reproduced on the minimal fixture from the issue before any change:
Neither resolver had a Python branch. A dotted module path (pipeline.util) is not a filesystem path, so it fell through to the bare-specifier fallback and was echoed back unchanged, matching no file node. Python's relative imports share JS's leading-dot spelling but mean "climb the package tree", so from ..util import x was mis-resolved by the generic relative branch as well.
The extractor put the alias in Import.source. For import lib as L, source was L — an alias can never resolve to a file. A multi-module import a, b also collapsed into a single record naming only a, silently losing b.
The approach
Import roots are derived from package layout, not assumed to be the repo root: walk up from the importing file while each level is a package (contains __init__.py), and stop at the first ancestor that is not. That ancestor is what would be on sys.path at runtime, so the PyPA-endorsed "src layout" (src/pipeline/…, imported as from pipeline…) and a flat layout resolve under one rule with no configuration. The src-layout case is exactly what made data-analytics-pipeline-svc resolve nothing.
Roots that no convention implies are read from pyproject.toml — [tool.pytest.ini_options] pythonpath, [tool.setuptools] package-dir, [tool.setuptools.packages.find] where, and poetry's packages[].from. That covers the pythonpath = ["src", "scripts"] case observed on the same repo, where scripts/ is importable but has no package marker.
Module bindings are modelled explicitly. A new Import.namespaceBindings field records local names bound to a module rather than a symbol. Call resolution reads it to interpret L.strip_block() as "strip_block, as declared in the module L refers to" — scoped to that module's file and authoritative, so a miss can't be claimed by an unrelated same-named function elsewhere. The field is defined generally (JavaScript's import * as ns is the same concept) but only Python populates it here.
from pkg import submod is resolved as a submodule when pkg/submod.py exists — Python's two readings of that statement are indistinguishable without knowing which files exist, so the resolver decides rather than the extractor. It both binds a namespace (so submod.f() resolves) and emits an imports edge to the submodule, so deps/impact name the module that actually changed instead of only the package barrel.
Import-root caches are cleared per rebuild alongside the Cargo one, since pyproject.toml isn't a watched extension and layout-derived roots go stale the moment an __init__.py is added or removed.
Engine parity
Mirrored in crates/codegraph-core/ (extractor, resolver, edge emission, call resolution, NAPI cache-clear export). Native and WASM were compared directly on five fixtures — Python minimal, Python src-layout, and three JS controls — and produce identical edge sets.
Worth knowing for future parity work: --engine wasm does not exercise the JS resolver when the native addon is present, because resolveImportPath prefers native regardless of the flag. The JS path has to be tested with the addon unavailable.
Verification
Found in passing, not fixed here