* fix(resolution): resolve method values on untyped Python module globals (#1820)
A method value whose receiver is a module global without a static type
(`thread_pool_exec(settings.conn.fetch, ...)`, where `conn = None` is
rebound by `global conn; conn = Backend()`) resolved to a `variable` node
and produced no edge. The global's type is now the set of classes its
module assigns to it, at module scope or in a function that declares it
`global`. One class resolves to its own method; several bind to the
nearest declaration every candidate inherits, as a base-typed receiver
does.
No edge (a wrong callback edge is worse than none) when:
- any binding of the global in its module is not a whole constructor
call: a factory, a conditional, a tuple target, `for`/`with`/import,
a star import, or an annotation the constructor contradicts;
- the caller or an enclosing function binds the name itself: parameter,
assignment (any target position), loop, comprehension, `as`, `case`
pattern, lambda parameter;
- the receiver is a deeper chain (`settings.conn.pool.fetch`);
- the importing file rebinds the name, or imports it from two sources.
Statements are read with their continuation lines, and only statement
lines (bracket depth 0) can assign. Not seen: rebinding the global from
another module (`settings.conn = X`) or through `globals()`.
`pkg.mod.Cls` now resolves through `import pkg.mod`, unless two namespace
imports share the last segment.
Python import mappings also read parenthesized from-imports
(`from x import (\n A,\n B,\n)`), which previously yielded the
single name `(` and hid every class imported that way from receiver and
inheritance resolution. Comments and docstrings are stripped first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit 0aab32a18b)
* fix(resolution): account for writes to a Python module global from other files (#2074)
The module-global receiver type was read from the global's own module only,
so `settings.conn = Decoy()` in another module, or `globals()["conn"] = X`,
left a stale type and a wrong edge.
- A production write `<module>.<name> = Cls(...)` from another file joins
the type set (resolved in the writing file); any other production write
(another value, a tuple target, `setattr`/`patch.object`) makes the type
unknown. The module is matched through each file's imports, including
aliases and relative imports.
- Test files install doubles (`settings.conn = MagicMock()`,
`monkeypatch.setattr(settings, "conn", ...)`): they do not change the
production type, but a ref inside such a test resolves nothing.
- `globals()[...] = ...` with the global's name or a computed key, and
`globals().update(...)`, in the global's module make the type unknown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit 2055711226)
* fix(resolution): match a module global's writers by file, not dotted suffix
Review follow-up to the cross-module write scan.
- A write now lands on the file its import names: relative imports resolve
exactly, absolute ones by path suffix; when a spelling could name another
file sharing the tail (`x/settings.py` and `y/settings.py` for
`import settings`), a write through it makes the type unknown instead of
feeding or clearing the wrong module.
- `import a.b` binds `a`: the module is spelled `a.b`; the mapping's
last-segment name is not treated as an alias of it.
- Test doubles are recognised by the narrow test-suite set plus any
`conftest.py`; examples, fixtures and benchmarks are production writers.
- Writes inside `if __name__ == "__main__":` are script code, not module
state, in the global's module and elsewhere.
- Namespace-dict writes: `globals()`, `vars()` and `sys.modules[__name__]`
are read per statement (continuations joined, strings ignored); any use
other than a literal-key read or `.get`, or a literal-key write of the
name, makes the type unknown. `<module>.__dict__[...]` / `.update(` from
another file does too.
- `settings.conn: Store = Store()` and `settings.conn = (Store())` are
plain constructor writes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit 7aab29269a)
* fix(resolution): explicit dotted aliases, one-line __main__ guards, scoped vars()
Review follow-up.
- `import pkg.settings as settings` binds `settings` even though it equals
the last segment; the import mapping cannot tell it from a plain
`import pkg.settings`, so the source line decides.
- `if __name__ == "__main__": stmt` on one line is script code, and a
`globals()` write inside a `__main__` block no longer counts.
- `vars()` is the module dict only at module scope; inside a function it
is the locals, so `return vars()` there no longer makes the type unknown.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit 0a5fb69797)
* fix(resolution): explicit-alias check matches the exact module, outside strings
Review follow-up: `import other.pkg.settings as settings` or a string
`"import pkg.settings as settings"` no longer marks a plain
`import pkg.settings` as explicitly aliased, which attached a write to the
wrong module.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66
(cherry picked from commit a5569a07ea)
* test(resolution): give the Python module-global adversarial table its own timeout
It indexes one project per case, which takes about nine seconds on macOS,
past vitest's five-second default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(changelog): Python module globals and multi-line imports (#2074)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Josh66 <info@aska-systems.de>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>