diff --git a/src/lib/analysis/analyzers/analyzers.test.ts b/src/lib/analysis/analyzers/analyzers.test.ts index 412c520..51146fa 100644 --- a/src/lib/analysis/analyzers/analyzers.test.ts +++ b/src/lib/analysis/analyzers/analyzers.test.ts @@ -182,6 +182,38 @@ class Dog(Animal): const render = graph.nodes.find((n) => n.id === "widget.js::render"); expect(render?.parent).toBe("class::widget.js::Widget"); }); + + test("python: un metodo compartido por dos clases no se atribuye a la clase equivocada", async () => { + const graph = await run(pythonAnalyzer, [ + [ + "dup.py", + `class A: + def __init__(self): + pass + def a_only(self): + pass + +class B: + def __init__(self): + pass + def b_only(self): + pass +`, + ], + ]); + + // Unique methods attach to their own class. + expect(graph.nodes.find((n) => n.id === "dup.py::a_only")?.parent).toBe( + "class::dup.py::A", + ); + expect(graph.nodes.find((n) => n.id === "dup.py::b_only")?.parent).toBe( + "class::dup.py::B", + ); + // The shared __init__ is ambiguous: parented to the module, never the wrong class. + expect(graph.nodes.find((n) => n.id === "dup.py::__init__")?.parent).toBe( + "mod::dup.py", + ); + }); }); describe("typescript", () => { diff --git a/src/lib/analysis/analyzers/shared.ts b/src/lib/analysis/analyzers/shared.ts index 72a60be..a2b3e01 100644 --- a/src/lib/analysis/analyzers/shared.ts +++ b/src/lib/analysis/analyzers/shared.ts @@ -4,6 +4,9 @@ import type { Graph, GraphNode, GraphEdge, SourceFile } from "../types"; type Node = Parser.SyntaxNode; +// Cap parsing of a single file so a pathological input can't block the event loop. +const MAX_PARSE_MICROS = 5_000_000; // 5s + export type LangSpec = { language: string; wasm: string; @@ -77,7 +80,9 @@ function stripQuotes(text: string): string { type FileFacts = { file: string; defs: Set; - methodClass: Map; // function name -> owning class name + // function name -> owning class, or null when the name is owned by more than + // one class in the file (ambiguous: don't attribute it to the wrong class). + methodClass: Map; classes: Set; extendsRel: { cls: string; base: string }[]; calls: { caller: string | null; callee: string }[]; @@ -90,19 +95,34 @@ async function parseFile( paths: Set, ): Promise { const { parser, language } = await getParser(spec.wasm); + parser.setTimeoutMicros(MAX_PARSE_MICROS); const tree = parser.parse(source.content); + // parse() returns null when the timeout is hit; skip the file instead of crashing. + if (!tree) { + return { + file: source.path, + defs: new Set(), + methodClass: new Map(), + classes: new Set(), + extendsRel: [], + calls: [], + imports: new Set(), + }; + } const root = tree.rootNode; const classNodeTypes = spec.classNodeTypes ?? new Set(); const defs = new Set(); - const methodClass = new Map(); + const methodClass = new Map(); const defQuery = language.query(spec.funcDefQuery); for (const { node } of defQuery.captures(root)) { const name = resolveName(node); if (!name) continue; defs.add(name); const cls = enclosingClassName(node, classNodeTypes); - if (cls) methodClass.set(name, cls); + if (!cls) continue; + // Same method name in two classes (e.g. __init__) -> ambiguous, mark null. + methodClass.set(name, methodClass.has(name) && methodClass.get(name) !== cls ? null : cls); } const classes = new Set();