From a5569a07ea2c09bdb73e1262007c8d501fccd8ec Mon Sep 17 00:00:00 2001 From: Josh66 Date: Mon, 28 Sep 2026 13:27:12 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_018CQubRS1bXvaoApgPyLf66 --- __tests__/function-ref.test.ts | 10 ++++++++++ src/resolution/name-matcher.ts | 5 +++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/__tests__/function-ref.test.ts b/__tests__/function-ref.test.ts index 9138efd0..0be8f8d6 100644 --- a/__tests__/function-ref.test.ts +++ b/__tests__/function-ref.test.ts @@ -1439,6 +1439,16 @@ def init(): namespace_import_binds_root: [{ 'pkg/__init__.py': '', 'pkg/settings.py': header, 'c.py': 'import pkg.settings\nfrom decoy import Decoy\n\nsettings = Bag()\nsettings.conn = Decoy()\n', 'd.py': 'from pkg import settings\n\ndef cb(pool):\n pool.submit(settings.conn.fetch)\n' }, ['cb -> Store::fetch']], + // `other.pkg.settings as settings` is not an explicit alias of `pkg.settings`: the + // write goes to the other module, so pkg's global keeps its type (the consumer's + // relative import names pkg/settings.py exactly). + alias_of_longer_module_is_not_explicit: [{ 'pkg/__init__.py': '', 'pkg/settings.py': header, + 'other/__init__.py': '', 'other/pkg/__init__.py': '', 'other/pkg/settings.py': 'conn = None\n', + 'reset.py': 'import pkg.settings\nimport other.pkg.settings as settings\nfrom decoy import Decoy\n\ndef reset():\n settings.conn = Decoy()\n', + 'pkg/d.py': 'from . import settings\n\ndef cb(pool):\n pool.submit(settings.conn.fetch)\n' }, ['cb -> Store::fetch']], + alias_in_string_is_not_explicit: [{ 'pkg/__init__.py': '', 'pkg/settings.py': 'conn = None\n', + 'reset.py': 'import pkg.settings\nfrom decoy import Decoy\nnote = "import pkg.settings as settings"\n\ndef reset():\n settings.conn = Decoy()\n', + 'd.py': 'from pkg import settings\n\ndef cb(pool):\n pool.submit(settings.conn.fetch)\n' }, []], explicit_alias_equal_to_leaf: [{ 'pkg/__init__.py': '', 'pkg/settings.py': header, 'reset.py': 'import pkg.settings as settings\nfrom decoy import Decoy\n\ndef reset():\n settings.conn = Decoy()\n', 'd.py': 'from pkg import settings\n\ndef cb(pool):\n pool.submit(settings.conn.fetch)\n' }, []], diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index fae96780..3ae55181 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -685,9 +685,10 @@ function pythonModuleAliases(filePath: string, moduleFile: string, context: Reso const files = pythonModuleFiles(dotted, filePath, context); if (!files.includes(moduleFile)) continue; // The mapping cannot tell `import a.b` from `import a.b as b`; the source line can. - const explicit = new RegExp(`\\bimport\\s[^\\n]*\\b${m.source.replace(/\./g, '\\.')}\\s+as\\s+${m.localName}\\b`); + // Exactly this module (not `other.a.b`), outside string literals. + const explicit = new RegExp(`\\bimport\\s[^\\n]*(? explicit.test(line)); + !pythonMemberLines(filePath, context).some(line => explicit.test(blankPythonStrings(line))); (files.length === 1 ? aliases : ambiguous).add(plainDotted ? m.source : m.localName); } return { aliases: [...aliases], ambiguous: [...ambiguous] };