Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions __tests__/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,59 @@ def external_caller():
expect(externalCalls).toHaveLength(0);
});

it('resolves Python imports inside a line-wrapped `from pkg import (...)` list', async () => {
// extractPythonImports re-parses raw source with a regex, separately from
// the tree-sitter AST. That regex was `from\s+([\w.]+)\s+import\s+([^#\n]+)`
// — the `[^#\n]+` capture stops at the first newline. PEP 8 wraps a long
// parenthesized import list across multiple physical lines, so every name
// after line one of the statement fell outside the match and got no
// ImportMapping at all. Reproduces with as few as two names split across
// two lines; both names below are checked so a fix that only recovers the
// last name (rather than every name after line one) still fails this.
fs.mkdirSync(path.join(tempDir, 'services'));
fs.writeFileSync(path.join(tempDir, 'services', '__init__.py'), '');
fs.writeFileSync(
path.join(tempDir, 'services', 'rentabilite.py'),
'def compute():\n return 42\n'
);
fs.writeFileSync(
path.join(tempDir, 'services', 'echeancier.py'),
'def upcoming():\n return []\n'
);
fs.writeFileSync(
path.join(tempDir, 'main.py'),
`from .services import (echeancier,
rentabilite)


def etudes():
return rentabilite.compute()


def dashboard():
return echeancier.upcoming()
`
);

cg = await CodeGraph.init(tempDir, { index: true });

const etudes = cg.getNodesByKind('function').filter((n) => n.name === 'etudes')[0];
expect(etudes).toBeDefined();
const etudesCalls = cg.getOutgoingEdges(etudes!.id).filter((e) => e.kind === 'calls');
expect(etudesCalls).toHaveLength(1);
const etudesTarget = cg.getNode(etudesCalls[0]!.target);
expect(etudesTarget?.name).toBe('compute');
expect(etudesTarget?.filePath.replace(/\\/g, '/')).toBe('services/rentabilite.py');

const dashboard = cg.getNodesByKind('function').filter((n) => n.name === 'dashboard')[0];
expect(dashboard).toBeDefined();
const dashboardCalls = cg.getOutgoingEdges(dashboard!.id).filter((e) => e.kind === 'calls');
expect(dashboardCalls).toHaveLength(1);
const dashboardTarget = cg.getNode(dashboardCalls[0]!.target);
expect(dashboardTarget?.name).toBe('upcoming');
expect(dashboardTarget?.filePath.replace(/\\/g, '/')).toBe('services/echeancier.py');
});

it('attaches Go methods to their receiver type across files (#583, cross-file half)', async () => {
// In Go a type's methods are commonly declared in a different file from the
// `type` declaration (`type Box` in box.go, `func (b *Box) Get()` in
Expand Down
12 changes: 9 additions & 3 deletions src/resolution/import-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -902,12 +902,18 @@ function extractJSImports(content: string): ImportMapping[] {
function extractPythonImports(content: string): ImportMapping[] {
const mappings: ImportMapping[] = [];

// from X import Y
const fromImportRegex = /from\s+([\w.]+)\s+import\s+([^#\n]+)/g;
// from X import Y — either a parenthesized list, which PEP 8 line-wrapping
// routinely spreads across multiple physical lines (`from pkg import (\n a,\n b as c,\n)`),
// or a single-line list. `[^#\n]+` alone stops at the first line break, so a
// wrapped list silently lost every name after line one — including aliased
// ones, which is why real trees (which wrap) kept reporting no callers
// for names imported anywhere but a statement's first line.
const fromImportRegex = /from\s+([\w.]+)\s+import\s+(?:\(([\s\S]*?)\)|([^#\n]+))/g;
let match;

while ((match = fromImportRegex.exec(content)) !== null) {
const [, source, imports] = match;
const [, source, parenImports, plainImports] = match;
const imports = parenImports !== undefined ? parenImports : plainImports;
const names = imports!.split(',').map((s) => s.trim());

for (const name of names) {
Expand Down