Skip to content
Merged
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
55 changes: 55 additions & 0 deletions packages/adapters/python/src/usage/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,97 +199,152 @@
const attribute = /^([\p{XID_Start}_][\p{XID_Continue}]*)\s*\.\s*TYPE_CHECKING$/u.exec(condition);
return attribute !== null && names.modules.has(attribute[1]!.normalize("NFKC"));
}
/** Top-level arguments of the call whose "(" is at `open`, or undefined if it never closes. */
function callArguments(text: string, open: number): string[] | undefined {
const args: string[] = [];
let depth = 0;
let start = open + 1;
for (let i = open; i < text.length; i++) {
const c = text[i]!;
if ("([{".includes(c)) depth++;
else if (")]}".includes(c)) {
depth--;
if (depth === 0) {
args.push(text.slice(start, i).trim());
return args.filter((arg, k) => arg !== "" || k < args.length - 1);
}
} else if (c === "," && depth === 1) {
args.push(text.slice(start, i).trim());
start = i + 1;
}
}
return undefined;
}

/**
* The relative-import level a `__import__` call passes: a number when it is a
* plain integer literal, undefined when absent or not known statically.
*/
function importLevel(args: string[]): number | undefined {
let value: string | undefined;
let positional = 0;
let starred = false;
for (const arg of args) {
const keyword = /^([\p{XID_Start}_][\p{XID_Continue}]*)\s*=(?!=)\s*(.*)$/su.exec(arg);
if (keyword) {
if (keyword[1] === "level") value = keyword[2]!;
} else if (arg.startsWith("*")) {
starred = true;
} else {
if (positional === 4 && !starred) value = arg;
positional++;
}
}
if (value === undefined) return undefined;
let literal = value.trim();
while (/^\(.*\)$/s.test(literal)) literal = literal.slice(1, -1).trim();
if (!/^(?:0[xX][\da-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+|\d[\d_]*)$/.test(literal)) return undefined;
return Number(literal.replace(/_/g, ""));
}
const DYNAMIC =
/(?<![\p{XID_Continue}.])(?<!\.\s*)(?:importlib\s*\.\s*import_module|(?:(?:importlib|builtins|__builtins__)\s*\.\s*)?__import__|import_module)\s*\(\s*(?:name\s*=\s*)?__S(\d+)__(?=\s*[,)])/gu;

const clean = (dotted: string) => dotted.replace(/\s+/g, "");

export const extractPythonImports: PythonImportExtractor = (source) => {
const { statements, strings, byteStrings } = splitPythonStatements(source);
const imports: PythonImport[] = [];
const code: string[] = [];
let typeCheckingIndent: number | undefined;
let inlineSuite: { line: number; typeOnly: boolean } | undefined;
const typeNames: TypeCheckingNames = { flags: new Set(), modules: new Set() };

for (const stmt of statements) {
if (typeCheckingIndent !== undefined && stmt.indent <= typeCheckingIndent) {
typeCheckingIndent = undefined;
}
if (isTypeCheckingHeader(stmt.text, typeNames, stmt.indent === 0)) {
// Keep the outermost guard: ending an inner block does not end it.
typeCheckingIndent ??= stmt.indent;
continue;
}
if (inlineSuite?.line !== stmt.line) inlineSuite = undefined;
let typeOnly = typeCheckingIndent !== undefined || (inlineSuite?.typeOnly ?? false);
let text = stmt.text;
let conditional = stmt.indent > 0 || inlineSuite !== undefined;
const header = clauseHeader(text);
if (header && header.length < text.length) {
if (isTypeCheckingHeader(header, typeNames, stmt.indent === 0)) typeOnly = true;
text = text.slice(header.length).trimStart();
conditional = true;
inlineSuite = { line: stmt.endLine, typeOnly };
}
updateTypeCheckingNames(typeNames, stmt.text, text, !conditional);
const base = { line: stmt.line, endLine: stmt.endLine, conditional, typeOnly };

const plain = /^import\s+(.+)$/s.exec(text);
if (plain) {
for (const part of plain[1]!.split(",")) {
const m = /^\s*(.+?)(?:\s+as\s+([\p{XID_Start}_][\p{XID_Continue}]*))?\s*$/su.exec(part);
if (!m || !DOTTED.test(m[1]!.trim())) continue;
const module = clean(m[1]!).normalize("NFKC");
// `import a.b` binds `a`; `import a.b as c` binds `c`.
const local = (m[2] ?? module.split(".")[0]!).normalize("NFKC");
imports.push({ module, form: "static", names: [], local, ...base });
}
continue;
}
const from =
/^from\s+(\.*)\s*([\p{XID_Start}_][\p{XID_Continue}.\s]*?)?\s+import\s+(.+)$/su.exec(text);
if (from) {
if (from[1] !== "" || from[2] === undefined || !DOTTED.test(from[2].trim())) continue;
const names = from[3]!
.replace(/[()]/g, " ")
.split(",")
.map((part) =>
part
.trim()
.split(/\s+as\s+/)[0]!
.trim(),
)
.filter((name) => name === "*" || IDENT.test(name))
.map((name) => name.normalize("NFKC"));
imports.push({ module: clean(from[2]).normalize("NFKC"), form: "static", names, ...base });
continue;
}
code.push(text);
for (const m of text.matchAll(DYNAMIC)) {
if (byteStrings.has(Number(m[1]))) continue;
const literal = strings[Number(m[1])] ?? "";
if (literal === "" || literal.startsWith(".") || /\s/u.test(literal) || !DOTTED.test(literal))
continue;
// __import__(name, globals, locals, fromlist, level): a positive level is
// package-relative, never an external dependency. An unknown level keeps
// the credit, as before.
if (/__import__$/.test(m[0].slice(0, m[0].indexOf("(")).trim())) {
const args = callArguments(text, m.index + m[0].indexOf("("));
const level = args && importLevel(args);
if (level !== undefined && level > 0) continue;
}
imports.push({ module: literal, form: "dynamic", names: [], ...base });
}
}

const locals = new Set(
imports.map((imp) => imp.local).filter((l): l is string => l !== undefined),
);
const attributes = new Map<string, Set<string>>();
if (locals.size > 0) {
const body = code.join("\n");
for (const m of body.matchAll(
/(?<![\p{XID_Continue}.])([\p{XID_Start}_][\p{XID_Continue}]*)\s*\.\s*([\p{XID_Start}_][\p{XID_Continue}]*)/gu,
)) {
const local = m[1]!.normalize("NFKC");
if (!locals.has(local)) continue;
let set = attributes.get(local);
if (!set) attributes.set(local, (set = new Set()));
set.add(m[2]!.normalize("NFKC"));
}
}
return { imports, attributes };
};

Check notice on line 350 in packages/adapters/python/src/usage/imports.ts

View check run for this annotation

codefactor.io / CodeFactor

packages/adapters/python/src/usage/imports.ts#L254-L350

Complex Method
37 changes: 37 additions & 0 deletions packages/adapters/python/src/usage/usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -821,3 +821,40 @@ it("invalidates TYPE_CHECKING aliases rebound through NFKC-equivalent identifier
);
}
});

it("does not credit a relative __import__ level as an external dependency", () => {
const source = [
'__import__("rel_kw", level=1)',
'__import__("rel_kw2", level = 2)',
'__import__("rel_pos", globals(), locals(), [], 1)',
'__import__("rel_hex", globals(), locals(), [], 0x1)',
'__import__("rel_paren", globals(), locals(), [], (1))',
'builtins.__import__("rel_b", level=1)',
'importlib.__import__("rel_i", None, None, None, 3)',
'__import__(name="rel_named", level=1)',
'__import__("abs_zero", level=0)',
'__import__("abs_zero_pos", globals(), locals(), [], 0)',
'__import__("abs_plain")',
'__import__("abs_four", globals(), locals(), [])',
'__import__("abs_unknown", level=lvl)',
'__import__("abs_unknown_pos", globals(), locals(), [], n + 1)',
'__import__("abs_star", *args)',
'__import__("abs_kw", fromlist=["x"], level=0)',
'__import__("abs_nested", f(level=1))',
'importlib.import_module(".rel_mod", package="app")',
'importlib.import_module(".rel_mod2", "app")',
'importlib.import_module("abs_mod", package="app")',
].join("\n");
assert.deepEqual(modules(source), [
"abs_zero",
"abs_zero_pos",
"abs_plain",
"abs_four",
"abs_unknown",
"abs_unknown_pos",
"abs_star",
"abs_kw",
"abs_nested",
"abs_mod",
]);
});
Loading