weavatrix 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/bin/weavatrix-mcp.mjs +6 -0
- package/package.json +50 -0
- package/skill/SKILL.md +56 -0
- package/src/analysis/coverage-reports.js +232 -0
- package/src/analysis/dead-check.js +136 -0
- package/src/analysis/dep-check.js +310 -0
- package/src/analysis/dep-rules.js +221 -0
- package/src/analysis/duplicates-worker.js +11 -0
- package/src/analysis/duplicates.compute.js +215 -0
- package/src/analysis/duplicates.js +15 -0
- package/src/analysis/duplicates.run.js +51 -0
- package/src/analysis/duplicates.tokenize.js +182 -0
- package/src/analysis/endpoints.js +156 -0
- package/src/analysis/findings.js +52 -0
- package/src/analysis/graph-analysis.aggregate.js +280 -0
- package/src/analysis/graph-analysis.js +7 -0
- package/src/analysis/graph-analysis.refs.js +146 -0
- package/src/analysis/graph-analysis.summaries.js +62 -0
- package/src/analysis/internal-audit.collect.js +117 -0
- package/src/analysis/internal-audit.js +9 -0
- package/src/analysis/internal-audit.reach.js +47 -0
- package/src/analysis/internal-audit.run.js +189 -0
- package/src/analysis/manifests.js +169 -0
- package/src/analysis/source-complexity.ast.js +97 -0
- package/src/analysis/source-complexity.constants.js +55 -0
- package/src/analysis/source-complexity.js +14 -0
- package/src/analysis/source-complexity.report.js +76 -0
- package/src/analysis/source-complexity.walk.js +174 -0
- package/src/build-graph.js +102 -0
- package/src/config.js +5 -0
- package/src/graph/build-worker.js +30 -0
- package/src/graph/builder/lang-csharp.js +40 -0
- package/src/graph/builder/lang-css.js +29 -0
- package/src/graph/builder/lang-go.js +53 -0
- package/src/graph/builder/lang-html.js +29 -0
- package/src/graph/builder/lang-java.js +29 -0
- package/src/graph/builder/lang-js.js +168 -0
- package/src/graph/builder/lang-python.js +78 -0
- package/src/graph/builder/lang-rust.js +45 -0
- package/src/graph/builder/spec-pkg.js +87 -0
- package/src/graph/graph-filter.js +44 -0
- package/src/graph/internal-builder.build.js +217 -0
- package/src/graph/internal-builder.js +12 -0
- package/src/graph/internal-builder.langs.js +86 -0
- package/src/graph/internal-builder.resolvers.js +116 -0
- package/src/graph/layout.js +35 -0
- package/src/infra/infra-items.js +255 -0
- package/src/infra/infra-registry.js +184 -0
- package/src/infra/infra.detect.js +119 -0
- package/src/infra/infra.js +38 -0
- package/src/infra/infra.match.js +102 -0
- package/src/infra/infra.scan.js +93 -0
- package/src/mcp/catalog.mjs +68 -0
- package/src/mcp/graph-context.mjs +276 -0
- package/src/mcp/tools-actions.mjs +132 -0
- package/src/mcp/tools-graph.mjs +274 -0
- package/src/mcp/tools-health.mjs +209 -0
- package/src/mcp/tools-impact.mjs +189 -0
- package/src/mcp-rg.mjs +51 -0
- package/src/mcp-server.mjs +171 -0
- package/src/mcp-source-tools.mjs +139 -0
- package/src/process.js +77 -0
- package/src/scan/discover.inventory.js +78 -0
- package/src/scan/discover.js +5 -0
- package/src/scan/discover.list.js +79 -0
- package/src/scan/discover.stack.js +227 -0
- package/src/scan/search.core.js +24 -0
- package/src/scan/search.git.js +102 -0
- package/src/scan/search.js +9 -0
- package/src/scan/search.node.js +83 -0
- package/src/scan/search.preview.js +49 -0
- package/src/scan/search.rg.js +145 -0
- package/src/security/advisory-store.js +177 -0
- package/src/security/installed.js +247 -0
- package/src/security/malware-file-heuristics.js +121 -0
- package/src/security/malware-heuristics.exclusions.js +134 -0
- package/src/security/malware-heuristics.js +15 -0
- package/src/security/malware-heuristics.roots.js +142 -0
- package/src/security/malware-heuristics.scan.js +87 -0
- package/src/security/malware-heuristics.sweep.js +122 -0
- package/src/security/malware-scoring.js +84 -0
- package/src/security/match.js +85 -0
- package/src/security/registry-sig.classify.js +136 -0
- package/src/security/registry-sig.js +18 -0
- package/src/security/registry-sig.rules.js +188 -0
- package/src/security/typosquat.js +72 -0
- package/src/util.js +27 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// JS / TS / JSX / TSX extractor for the built-in graph builder.
|
|
2
|
+
// Symbols: functions, generators, classes, methods, top-level const/let/var (data + arrow/fn consts).
|
|
3
|
+
// Imports: ESM `import`, CJS `require`, path-aliases (tsconfig/vite), and barrel re-exports (`export … from`).
|
|
4
|
+
// Calls: bare `foo()`. Heritage: `class X extends Y`. Everything runs through the shared ctx (see internal-builder.js).
|
|
5
|
+
const CALLABLE = /arrow_function|function|function_expression|generator_function/;
|
|
6
|
+
|
|
7
|
+
const FUNCS = `
|
|
8
|
+
(function_declaration name: (identifier) @fn)
|
|
9
|
+
(generator_function_declaration name: (identifier) @fn)
|
|
10
|
+
(class_declaration name: (_) @class)
|
|
11
|
+
(method_definition name: (_) @method)`;
|
|
12
|
+
const TOPVARS = `
|
|
13
|
+
(program (lexical_declaration (variable_declarator) @decl))
|
|
14
|
+
(program (variable_declaration (variable_declarator) @decl))
|
|
15
|
+
(program (export_statement (lexical_declaration (variable_declarator) @decl)))
|
|
16
|
+
(program (export_statement (variable_declaration (variable_declarator) @decl)))`;
|
|
17
|
+
const REQUIRE = `(variable_declarator name: (_) @lhs value: (call_expression function: (identifier) @req arguments: (arguments (string (string_fragment) @src))))`;
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
family: "js",
|
|
21
|
+
grammars: ["javascript", "typescript", "tsx"],
|
|
22
|
+
exts: { ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript", ".ts": "typescript", ".tsx": "tsx" },
|
|
23
|
+
isWeb: false,
|
|
24
|
+
calls: `(call_expression function: (identifier) @callee)`,
|
|
25
|
+
heritage: [`(class_heritage (identifier) @super)`, `(class_heritage (extends_clause value: (identifier) @super))`],
|
|
26
|
+
|
|
27
|
+
pass1(ctx) {
|
|
28
|
+
const { grammar, tree, fileRel, caps, field, addSym, addImportEdge, addExternalImport, markExported, imports, resolveJsImport, resolveAlias } = ctx;
|
|
29
|
+
// exported-ness of a declaration = an export_statement ancestor (export function/class/const …).
|
|
30
|
+
// BOUNDED climb: web-tree-sitter's node.parent is O(depth) (re-walks a cursor from the root each call),
|
|
31
|
+
// so an UNBOUNDED walk to `program` for every symbol was O(depth^3) and HUNG the build for minutes/hours
|
|
32
|
+
// on deeply-nested / minified / bundled JS (regression). An export_statement is always a NEAR ancestor of
|
|
33
|
+
// the declaration head (≤ ~5 hops), and hitting a statement_block means we're nested inside a function →
|
|
34
|
+
// not a module-level export → bail immediately. See [[graph-builder-internalization]].
|
|
35
|
+
// early-out on statement_block (we're nested inside a function → not a module-level export) keeps this
|
|
36
|
+
// O(1) for nested symbols; program is the top; the hop cap is the backstop. A method of an EXPORTED class
|
|
37
|
+
// is only ~4 hops to its export_statement (method → class_body → class_declaration → export_statement),
|
|
38
|
+
// so we do NOT bail at class_body — that preserves the exported flag for such methods.
|
|
39
|
+
const isExportedDecl = (node) => {
|
|
40
|
+
let p = node.parent;
|
|
41
|
+
for (let hops = 0; p && hops < 6; hops++) {
|
|
42
|
+
if (p.type === "export_statement") return true;
|
|
43
|
+
if (p.type === "program" || p.type === "statement_block") return false;
|
|
44
|
+
p = p.parent;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
};
|
|
48
|
+
// a bare (package) specifier = non-relative AND not a path alias; alias-that-missed is a broken local, not a dep
|
|
49
|
+
const isBareSpec = (spec) => !!spec && !spec.startsWith(".") && resolveAlias(spec) == null;
|
|
50
|
+
// broken local import (relative or alias path resolving to no file) — recorded for the "unresolved-import"
|
|
51
|
+
// finding. Asset imports (svg/css-modules-adjacent/fonts/…) and ?query-suffixed specs that resolve once
|
|
52
|
+
// the query is stripped (Vite ?raw/?url/?worker) are NOT code-graph concerns.
|
|
53
|
+
const ASSET_RE = /\.(svg|png|jpe?g|gif|webp|avif|ico|bmp|woff2?|ttf|eot|otf|mp[34]|webm|wasm|pdf|txt|md|html?)$/i;
|
|
54
|
+
const recordUnresolved = (rawSpec, kind, line) => {
|
|
55
|
+
const clean = String(rawSpec || "").split("?")[0];
|
|
56
|
+
if (!clean || ASSET_RE.test(clean)) return;
|
|
57
|
+
if (clean !== rawSpec && resolveJsImport(fileRel, clean)) return; // only the ?query broke resolution
|
|
58
|
+
addExternalImport({ spec: rawSpec, kind, line, unresolved: true });
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ---- symbols (export flag captured at declaration time) ----
|
|
62
|
+
for (const cap of caps(grammar, FUNCS, tree.rootNode)) {
|
|
63
|
+
addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name !== "class", {
|
|
64
|
+
sourceNode: cap.node.parent,
|
|
65
|
+
...(isExportedDecl(cap.node) ? { exported: true } : {})
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
for (const cap of caps(grammar, TOPVARS, tree.rootNode)) {
|
|
69
|
+
const nameNode = field(cap.node, "name"); if (!nameNode || nameNode.type !== "identifier") continue;
|
|
70
|
+
const val = field(cap.node, "value");
|
|
71
|
+
addSym(nameNode.text, nameNode.startPosition.row + 1, !!(val && CALLABLE.test(val.type)), {
|
|
72
|
+
sourceNode: val || cap.node,
|
|
73
|
+
...(isExportedDecl(cap.node) ? { exported: true } : {})
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- ESM imports ----
|
|
78
|
+
for (const cap of caps(grammar, `(import_statement) @imp`, tree.rootNode)) {
|
|
79
|
+
const node = cap.node; const srcNode = field(node, "source");
|
|
80
|
+
const rawSpec = srcNode ? srcNode.text.replace(/^['"`]|['"`]$/g, "") : "";
|
|
81
|
+
const tgt = resolveJsImport(fileRel, rawSpec);
|
|
82
|
+
if (!tgt) {
|
|
83
|
+
if (isBareSpec(rawSpec)) addExternalImport({ spec: rawSpec, kind: "esm", line: node.startPosition.row + 1 });
|
|
84
|
+
else if (rawSpec) recordUnresolved(rawSpec, "esm", node.startPosition.row + 1);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
addImportEdge(tgt);
|
|
88
|
+
const clause = node.namedChildren.find((c) => c.type === "import_clause"); if (!clause) continue;
|
|
89
|
+
for (const c of clause.namedChildren) {
|
|
90
|
+
if (c.type === "identifier") imports.set(c.text, { imported: "default", targetFile: tgt });
|
|
91
|
+
else if (c.type === "namespace_import") { const id = c.namedChildren.find((x) => x.type === "identifier"); if (id) imports.set(id.text, { imported: "*", targetFile: tgt }); }
|
|
92
|
+
else if (c.type === "named_imports") for (const s of c.namedChildren) { if (s.type !== "import_specifier") continue; const nm = field(s, "name"), al = field(s, "alias"); if (nm) imports.set((al || nm).text, { imported: nm.text, targetFile: tgt }); }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---- CJS require (declarator form: local bindings + import edges) ----
|
|
97
|
+
for (const cap of caps(grammar, REQUIRE, tree.rootNode)) {
|
|
98
|
+
if (cap.name !== "src") continue;
|
|
99
|
+
let dv = cap.node; while (dv && dv.type !== "variable_declarator") dv = dv.parent;
|
|
100
|
+
const tgt = resolveJsImport(fileRel, cap.node.text); if (!tgt) continue;
|
|
101
|
+
addImportEdge(tgt);
|
|
102
|
+
const lhs = dv && field(dv, "name");
|
|
103
|
+
if (lhs && lhs.type === "identifier") imports.set(lhs.text, { imported: "*", targetFile: tgt });
|
|
104
|
+
else if (lhs && lhs.type === "object_pattern") for (const p of lhs.namedChildren) { const key = field(p, "key") || (p.type === "shorthand_property_identifier_pattern" ? p : null); const val = field(p, "value"); const local = (val && val.type === "identifier") ? val : key; if (key && local) imports.set(local.text, { imported: key.text, targetFile: tgt }); }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---- CJS require, ALL forms (side-effect require("dotenv/config") included): bare-pkg records +
|
|
108
|
+
// non-literal require(x) → dynamic marker so dep analysis can suppress false "unused" positives ----
|
|
109
|
+
for (const cap of caps(grammar, `(call_expression function: (identifier) @fn)`, tree.rootNode)) {
|
|
110
|
+
if (cap.node.text !== "require") continue;
|
|
111
|
+
const call = cap.node.parent; const args = field(call, "arguments");
|
|
112
|
+
const arg = args && args.namedChildren ? args.namedChildren[0] : null;
|
|
113
|
+
const line = call.startPosition.row + 1;
|
|
114
|
+
if (arg && arg.type === "string") {
|
|
115
|
+
const rawSpec = arg.text.replace(/^['"`]|['"`]$/g, "");
|
|
116
|
+
if (resolveJsImport(fileRel, rawSpec)) continue;
|
|
117
|
+
if (isBareSpec(rawSpec)) addExternalImport({ spec: rawSpec, kind: "cjs", line });
|
|
118
|
+
else recordUnresolved(rawSpec, "cjs", line);
|
|
119
|
+
} else {
|
|
120
|
+
addExternalImport({ dynamic: true, kind: "cjs", line });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---- dynamic import(): literal bare → package record; literal local → dynamic marker with target;
|
|
125
|
+
// non-literal → bare dynamic marker ----
|
|
126
|
+
for (const cap of caps(grammar, `(call_expression function: (import) @imp)`, tree.rootNode)) {
|
|
127
|
+
const call = cap.node.parent; const args = field(call, "arguments");
|
|
128
|
+
const arg = args && args.namedChildren ? args.namedChildren[0] : null;
|
|
129
|
+
const line = call.startPosition.row + 1;
|
|
130
|
+
if (arg && arg.type === "string") {
|
|
131
|
+
const rawSpec = arg.text.replace(/^['"`]|['"`]$/g, "");
|
|
132
|
+
const tgt = resolveJsImport(fileRel, rawSpec);
|
|
133
|
+
if (tgt) addExternalImport({ dynamic: true, spec: rawSpec, target: tgt, kind: "dynamic", line });
|
|
134
|
+
else if (isBareSpec(rawSpec)) addExternalImport({ spec: rawSpec, kind: "dynamic", line });
|
|
135
|
+
else if (rawSpec) recordUnresolved(rawSpec, "dynamic", line);
|
|
136
|
+
} else {
|
|
137
|
+
addExternalImport({ dynamic: true, kind: "dynamic", line });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---- re-exports (barrel/index files): edge so the real target isn't falsely DEAD; bare source → external ----
|
|
142
|
+
for (const cap of caps(grammar, `(export_statement source: (string (string_fragment) @src))`, tree.rootNode)) {
|
|
143
|
+
if (cap.name !== "src") continue;
|
|
144
|
+
const rawSpec = cap.node.text;
|
|
145
|
+
const tgt = resolveJsImport(fileRel, rawSpec);
|
|
146
|
+
if (tgt) addImportEdge(tgt);
|
|
147
|
+
else if (isBareSpec(rawSpec)) addExternalImport({ spec: rawSpec, kind: "reexport", line: cap.node.startPosition.row + 1 });
|
|
148
|
+
else if (rawSpec) recordUnresolved(rawSpec, "reexport", cap.node.startPosition.row + 1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---- export markers beyond declarations: `export { a, b }`, `export default X`, CJS module.exports ----
|
|
152
|
+
for (const cap of caps(grammar, `(export_statement (export_clause (export_specifier name: (identifier) @n)))`, tree.rootNode)) {
|
|
153
|
+
let st = cap.node.parent; while (st && st.type !== "export_statement") st = st.parent;
|
|
154
|
+
if (st && !field(st, "source")) markExported(cap.node.text); // with a source it's a re-export, not a local symbol
|
|
155
|
+
}
|
|
156
|
+
for (const cap of caps(grammar, `(export_statement value: (identifier) @def)`, tree.rootNode)) markExported(cap.node.text);
|
|
157
|
+
for (const cap of caps(grammar, `(assignment_expression left: (member_expression) @lhs right: (identifier) @rhs)`, tree.rootNode)) {
|
|
158
|
+
if (cap.name !== "rhs") continue;
|
|
159
|
+
const lhs = field(cap.node.parent, "left"); const l = lhs ? lhs.text : "";
|
|
160
|
+
if (l === "module.exports" || l.startsWith("exports.") || l.startsWith("module.exports.")) markExported(cap.node.text);
|
|
161
|
+
}
|
|
162
|
+
for (const cap of caps(grammar, `(assignment_expression left: (member_expression) right: (object (shorthand_property_identifier) @p))`, tree.rootNode)) {
|
|
163
|
+
let ae = cap.node.parent; while (ae && ae.type !== "assignment_expression") ae = ae.parent;
|
|
164
|
+
const lhs = ae && field(ae, "left");
|
|
165
|
+
if (lhs && (lhs.text === "module.exports" || lhs.text.startsWith("exports."))) markExported(cap.node.text);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Python extractor. Symbols: def/class (methods included) + module-level assignments (data consts).
|
|
2
|
+
// Imports: `import a.b [as c]` and `from [.]*mod import name [as alias]` with dotted + relative (leading-dot)
|
|
3
|
+
// resolution to <path>.py / <path>/__init__.py — tried from the repo root, src/ (src-layout), and the
|
|
4
|
+
// importing file's own directory (script-style sys.path). Absolute imports that resolve to NO repo file
|
|
5
|
+
// are recorded as external imports (stdlib → builtin, else ecosystem "PyPI") for dependency analysis.
|
|
6
|
+
// Calls: bare `foo()`. Heritage: class superclasses.
|
|
7
|
+
import { pySpecToPkg, PY_STDLIB } from "./spec-pkg.js";
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
family: "py",
|
|
11
|
+
grammars: ["python"],
|
|
12
|
+
exts: { ".py": "python", ".pyi": "python" },
|
|
13
|
+
isWeb: false,
|
|
14
|
+
calls: `(call function: (identifier) @callee)`,
|
|
15
|
+
heritage: [`(class_definition superclasses: (argument_list (identifier) @super))`],
|
|
16
|
+
|
|
17
|
+
pass1(ctx) {
|
|
18
|
+
const { grammar, tree, fileRel, caps, field, addSym, addImportEdge, addExternalImport, imports, resolvePyPath, pyBaseDir, pyTopDirs } = ctx;
|
|
19
|
+
// the importing file's own dir is on sys.path when it runs as a script — try siblings for absolute
|
|
20
|
+
// imports, EXCEPT stdlib names (a sibling logging.py must not shadow `import logging`; py3 absolute
|
|
21
|
+
// imports always pick the stdlib, and a wrong internal edge distorts dead-code + reachability)
|
|
22
|
+
const scriptDir = pyBaseDir(fileRel, 1);
|
|
23
|
+
const resolveAbs = (parts) => resolvePyPath("", parts) || (scriptDir && !PY_STDLIB.has(parts[0]) ? resolvePyPath(scriptDir, parts) : null);
|
|
24
|
+
const recordExternal = (dottedName, line) => {
|
|
25
|
+
const top = String(dottedName).split(".")[0];
|
|
26
|
+
if (pyTopDirs && pyTopDirs.has(top)) return; // repo-local namespace package (no __init__.py) — internal, not a dep
|
|
27
|
+
const r = pySpecToPkg(top);
|
|
28
|
+
if (r && !r.ambiguous) addExternalImport({ spec: dottedName, pkg: r.pkg, builtin: r.builtin, ecosystem: "PyPI", kind: "py-import", line });
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ---- symbols (decorated defs are framework-entered — @app.route/@app.event/@pytest.fixture — the
|
|
32
|
+
// flag keeps dead-code checks from calling them dead; tree wraps them in decorated_definition) ----
|
|
33
|
+
for (const cap of caps(grammar, `(function_definition name: (identifier) @fn) (class_definition name: (identifier) @class)`, tree.rootNode)) {
|
|
34
|
+
const decorated = cap.node.parent?.parent?.type === "decorated_definition";
|
|
35
|
+
addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "fn", {
|
|
36
|
+
sourceNode: cap.node.parent,
|
|
37
|
+
...(decorated ? { decorated: true } : {})
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
for (const cap of caps(grammar, `(module (expression_statement (assignment left: (identifier) @var)))`, tree.rootNode))
|
|
41
|
+
addSym(cap.node.text, cap.node.startPosition.row + 1, false, { sourceNode: cap.node.parent });
|
|
42
|
+
|
|
43
|
+
// ---- import a.b [as c] ----
|
|
44
|
+
for (const cap of caps(grammar, `(import_statement) @imp`, tree.rootNode)) {
|
|
45
|
+
for (const child of cap.node.namedChildren) {
|
|
46
|
+
const modNode = child.type === "aliased_import" ? field(child, "name") : (child.type === "dotted_name" ? child : null);
|
|
47
|
+
if (!modNode) continue;
|
|
48
|
+
const parts = modNode.text.split(".");
|
|
49
|
+
const tgt = resolveAbs(parts);
|
|
50
|
+
if (tgt) { addImportEdge(tgt); const local = child.type === "aliased_import" ? field(child, "alias")?.text : parts[0]; if (local) imports.set(local, { imported: "*", targetFile: tgt }); }
|
|
51
|
+
else recordExternal(modNode.text, modNode.startPosition.row + 1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- from [.]*mod import name [, name as alias] ----
|
|
56
|
+
for (const cap of caps(grammar, `(import_from_statement) @imp`, tree.rootNode)) {
|
|
57
|
+
const node = cap.node; const modNode = field(node, "module_name");
|
|
58
|
+
let dots = 0, modParts = [];
|
|
59
|
+
if (modNode) {
|
|
60
|
+
if (modNode.type === "relative_import") { const t = modNode.text; while (dots < t.length && t[dots] === ".") dots++; const rest = t.slice(dots); modParts = rest ? rest.split(".") : []; }
|
|
61
|
+
else if (modNode.type === "dotted_name") modParts = modNode.text.split(".");
|
|
62
|
+
}
|
|
63
|
+
const baseDir = pyBaseDir(fileRel, dots);
|
|
64
|
+
const names = node.namedChildren.filter((c) => c !== modNode && (c.type === "dotted_name" || c.type === "aliased_import"));
|
|
65
|
+
let externalDone = false; // one record per statement, not per imported name
|
|
66
|
+
for (const nm of names) {
|
|
67
|
+
const impName = nm.type === "aliased_import" ? field(nm, "name")?.text : nm.text;
|
|
68
|
+
const local = nm.type === "aliased_import" ? field(nm, "alias")?.text : impName;
|
|
69
|
+
if (!impName) continue;
|
|
70
|
+
const asSub = resolvePyPath(baseDir, [...modParts, impName]) || (dots === 0 ? resolveAbs([...modParts, impName]) : null); // name may itself be a submodule file
|
|
71
|
+
if (asSub) { addImportEdge(asSub); if (local) imports.set(local, { imported: "*", targetFile: asSub }); continue; }
|
|
72
|
+
const modFile = resolvePyPath(baseDir, modParts) || (dots === 0 ? resolveAbs(modParts) : null);
|
|
73
|
+
if (modFile) { addImportEdge(modFile); if (local) imports.set(local, { imported: impName, targetFile: modFile }); }
|
|
74
|
+
else if (dots === 0 && modParts.length && !externalDone) { externalDone = true; recordExternal(modParts.join("."), node.startPosition.row + 1); }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Rust extractor. Symbols: fn (incl. impl methods) + struct/enum/trait/type/mod + const/static
|
|
2
|
+
// (+ macro_rules!/union via optional queries). Calls: `foo()` / `x.foo()` / `path::foo()` (name only).
|
|
3
|
+
// Heritage: `impl Trait for Type` resolves only when the impl block sits inside a tracked symbol
|
|
4
|
+
// (e.g. a mod) — a top-level impl has no enclosing symbol to anchor the edge, so v1 usually skips it.
|
|
5
|
+
// `use` paths map to files only through the crate's module tree, which needs Cargo layout resolution —
|
|
6
|
+
// no import edges in v1; cross-file connectivity comes from the name-based calls pass plus the
|
|
7
|
+
// same-folder scope (mod.rs convention).
|
|
8
|
+
const SYMS_CORE = `
|
|
9
|
+
(function_item name: (identifier) @method)
|
|
10
|
+
(struct_item name: (type_identifier) @class)
|
|
11
|
+
(enum_item name: (type_identifier) @class)
|
|
12
|
+
(trait_item name: (type_identifier) @class)
|
|
13
|
+
(type_item name: (type_identifier) @class)
|
|
14
|
+
(mod_item name: (identifier) @class)
|
|
15
|
+
(const_item name: (identifier) @field)
|
|
16
|
+
(static_item name: (identifier) @field)`;
|
|
17
|
+
// Grammar-version-dependent node types, compiled SEPARATELY (one unknown type voids its whole query).
|
|
18
|
+
const SYMS_OPTIONAL = [
|
|
19
|
+
`(macro_definition name: (identifier) @method)`,
|
|
20
|
+
`(union_item name: (type_identifier) @class)`,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
family: "rust",
|
|
25
|
+
grammars: ["rust"],
|
|
26
|
+
exts: { ".rs": "rust" },
|
|
27
|
+
isWeb: false,
|
|
28
|
+
calls: `
|
|
29
|
+
(call_expression function: (identifier) @callee)
|
|
30
|
+
(call_expression function: (field_expression field: (field_identifier) @callee))
|
|
31
|
+
(call_expression function: (scoped_identifier name: (identifier) @callee))`,
|
|
32
|
+
heritage: [
|
|
33
|
+
`(impl_item trait: (type_identifier) @super)`,
|
|
34
|
+
`(impl_item trait: (generic_type type: (type_identifier) @super))`,
|
|
35
|
+
],
|
|
36
|
+
|
|
37
|
+
pass1(ctx) {
|
|
38
|
+
const { grammar, tree, caps, addSym } = ctx;
|
|
39
|
+
for (const src of [SYMS_CORE, ...SYMS_OPTIONAL]) {
|
|
40
|
+
for (const cap of caps(grammar, src, tree.rootNode)) {
|
|
41
|
+
addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "method", { sourceNode: cap.node.parent });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// specToPkg(spec) — map an import specifier to its npm package name, a Node builtin, or null for
|
|
2
|
+
// relative/absolute/URL specifiers. The core primitive for dependency analysis (unused/missing deps):
|
|
3
|
+
// "axios/lib/core" → axios, "@scope/name/sub" → @scope/name, "node:fs/promises" + "fs/promises" → fs
|
|
4
|
+
// (builtin), "./x" → null. See DEPS_SECURITY_PLAN.md (P0).
|
|
5
|
+
|
|
6
|
+
// Builtins importable WITHOUT the node: prefix. Prefix-only builtins (node:test, node:sea, node:sqlite)
|
|
7
|
+
// are deliberately NOT here — bare "test" is a legitimate npm package name; node:-prefixed specifiers
|
|
8
|
+
// are always classified builtin regardless of this set.
|
|
9
|
+
export const NODE_BUILTINS = new Set([
|
|
10
|
+
"assert", "async_hooks", "buffer", "child_process", "cluster", "console", "constants", "crypto",
|
|
11
|
+
"dgram", "diagnostics_channel", "dns", "domain", "events", "fs", "http", "http2", "https", "inspector",
|
|
12
|
+
"module", "net", "os", "path", "perf_hooks", "process", "punycode", "querystring", "readline", "repl",
|
|
13
|
+
"stream", "string_decoder", "sys", "timers", "tls", "trace_events", "tty", "url", "util", "v8", "vm",
|
|
14
|
+
"wasi", "worker_threads", "zlib",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function specToPkg(spec) {
|
|
18
|
+
const s = String(spec || "").trim();
|
|
19
|
+
if (!s || s.startsWith(".") || s.startsWith("/") || /^[A-Za-z]:[\\/]/.test(s)) return null; // relative/absolute/local
|
|
20
|
+
if (s.startsWith("#")) return null; // package.json "imports" subpath (self-internal), not a dependency
|
|
21
|
+
if (/^(https?|data|file):/.test(s)) return null; // URL-style imports aren't manifest deps
|
|
22
|
+
if (s.startsWith("node:")) return { pkg: s.slice(5).split("/")[0], builtin: true };
|
|
23
|
+
if (s === "bun" || s.startsWith("bun:")) return { pkg: s, builtin: true }; // Bun runtime modules (bun:test, bun:sqlite) — never npm deps
|
|
24
|
+
if (s.startsWith("npm:")) return specToPkg(s.slice(4)); // Deno/import-map npm: specifiers alias real npm packages
|
|
25
|
+
if (/^[a-z][\w+.-]*:/.test(s)) return null; // other scheme-style specifiers (jsr:, deno:, virtual:, …) aren't manifest deps
|
|
26
|
+
const parts = s.split("/");
|
|
27
|
+
const pkg = s.startsWith("@") ? (parts.length >= 2 ? `${parts[0]}/${parts[1]}` : s) : parts[0];
|
|
28
|
+
if (NODE_BUILTINS.has(pkg)) return { pkg, builtin: true };
|
|
29
|
+
return { pkg, builtin: false };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---- Go: import path → module (declared in go.mod) or stdlib. Stdlib packages have a dotless first
|
|
33
|
+
// segment (fmt, net/http, encoding/json — and cgo's "C"); everything else is a module path. Prefer the
|
|
34
|
+
// longest go.mod require prefix (exact module identity); fall back to a host-convention guess so
|
|
35
|
+
// MISSING modules still get a sane name (github.com/owner/repo, gopkg.in/pkg.v3, k8s.io/client-go).
|
|
36
|
+
const GO_DEEP_HOSTS = new Set(["github.com", "gitlab.com", "bitbucket.org", "golang.org", "go.googlesource.com"]);
|
|
37
|
+
export function goSpecToPkg(importPath, { requires = [], ownModule = "" } = {}) {
|
|
38
|
+
const s = String(importPath || "").trim();
|
|
39
|
+
if (!s) return null;
|
|
40
|
+
const segs = s.split("/");
|
|
41
|
+
if (!segs[0].includes(".")) return { pkg: s, builtin: true }; // stdlib + "C"
|
|
42
|
+
if (ownModule && (s === ownModule || s.startsWith(ownModule + "/"))) return null; // own module → internal (broken path, not a dep)
|
|
43
|
+
let best = "";
|
|
44
|
+
for (const r of requires) { const p = typeof r === "string" ? r : r.path; if ((s === p || s.startsWith(p + "/")) && p.length > best.length) best = p; }
|
|
45
|
+
if (best) return { pkg: best, builtin: false };
|
|
46
|
+
let n = GO_DEEP_HOSTS.has(segs[0]) ? 3 : 2;
|
|
47
|
+
if (segs[0] === "gopkg.in") n = /\.v\d+$/.test(segs[1] || "") ? 2 : 3;
|
|
48
|
+
return { pkg: segs.slice(0, Math.min(n, segs.length)).join("/"), builtin: false };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---- Python: top-level imported module → PyPI distribution (or stdlib). PY_STDLIB ≈ CPython 3.12
|
|
52
|
+
// sys.stdlib_module_names (public names) + legacy stalwarts, so old codebases classify cleanly too.
|
|
53
|
+
export const PY_STDLIB = new Set(("__future__ __main__ _thread abc aifc argparse array ast asyncio atexit audioop base64 bdb binascii bisect builtins bz2 " +
|
|
54
|
+
"calendar cgi cgitb chunk cmath cmd code codecs codeop collections colorsys compileall concurrent configparser contextlib contextvars copy copyreg cProfile crypt csv ctypes curses " +
|
|
55
|
+
"dataclasses datetime dbm decimal difflib dis distutils doctest email encodings ensurepip enum errno faulthandler fcntl filecmp fileinput fnmatch fractions ftplib functools " +
|
|
56
|
+
"gc getopt getpass gettext glob graphlib grp gzip hashlib heapq hmac html http imaplib imghdr imp importlib inspect io ipaddress itertools json keyword " +
|
|
57
|
+
"linecache locale logging lzma mailbox mailcap marshal math mimetypes mmap modulefinder msilib msvcrt multiprocessing netrc nntplib ntpath numbers " +
|
|
58
|
+
"operator optparse os ossaudiodev pathlib pdb pickle pickletools pipes pkgutil platform plistlib poplib posixpath pprint profile pstats pty pwd py_compile pyclbr pydoc queue quopri " +
|
|
59
|
+
"random re readline reprlib resource rlcompleter runpy sched secrets select selectors shelve shlex shutil signal site smtplib sndhdr socket socketserver spwd sqlite3 ssl stat statistics " +
|
|
60
|
+
"sre_compile sre_constants sre_parse string stringprep struct subprocess sunau symtable sys sysconfig syslog tabnanny tarfile telnetlib tempfile termios textwrap threading time timeit tkinter token tokenize tomllib " +
|
|
61
|
+
"trace traceback tracemalloc tty turtle types typing unicodedata unittest urllib uu uuid venv warnings wave weakref webbrowser winreg winsound wsgiref xdrlib xml xmlrpc zipapp zipfile zipimport zlib zoneinfo").split(" "));
|
|
62
|
+
|
|
63
|
+
// import name → PyPI dist where they differ. Generic python-X / X-python / X[2]-binary equivalence is
|
|
64
|
+
// handled by the matcher in dep-check; this map covers the truly irregular names.
|
|
65
|
+
export const PY_IMPORT_TO_DIST = {
|
|
66
|
+
yaml: "PyYAML", cv2: "opencv-python", PIL: "Pillow", sklearn: "scikit-learn", skimage: "scikit-image",
|
|
67
|
+
bs4: "beautifulsoup4", dateutil: "python-dateutil", dotenv: "python-dotenv", jose: "python-jose",
|
|
68
|
+
magic: "python-magic", multipart: "python-multipart", docx: "python-docx", pptx: "python-pptx",
|
|
69
|
+
fitz: "PyMuPDF", OpenSSL: "pyOpenSSL", Crypto: "pycryptodome", nacl: "PyNaCl", jwt: "PyJWT",
|
|
70
|
+
MySQLdb: "mysqlclient", attr: "attrs", attrs: "attrs", git: "GitPython", github: "PyGithub",
|
|
71
|
+
kafka: "kafka-python", grpc: "grpcio", serial: "pyserial", usb: "pyusb", zmq: "pyzmq",
|
|
72
|
+
websocket: "websocket-client", socks: "PySocks", telegram: "python-telegram-bot",
|
|
73
|
+
speech_recognition: "SpeechRecognition", wx: "wxPython", cairo: "pycairo", gi: "PyGObject",
|
|
74
|
+
mpl_toolkits: "matplotlib", pkg_resources: "setuptools", Levenshtein: "python-Levenshtein",
|
|
75
|
+
ldap: "python-ldap", memcache: "python-memcached", slugify: "python-slugify", decouple: "python-decouple",
|
|
76
|
+
engineio: "python-engineio", socketio: "python-socketio", ruamel: "ruamel.yaml", flask: "Flask",
|
|
77
|
+
win32api: "pywin32", win32com: "pywin32", win32con: "pywin32", win32gui: "pywin32", pythoncom: "pywin32", pywintypes: "pywin32",
|
|
78
|
+
};
|
|
79
|
+
// namespace roots shared by many dists (google.protobuf/google.cloud.* …) — too ambiguous to name one
|
|
80
|
+
// package; "src" is a repo-layout artifact (stale sys.path import), never a PyPI dist.
|
|
81
|
+
export const PY_AMBIGUOUS_TOP = new Set(["google", "src"]);
|
|
82
|
+
export function pySpecToPkg(topModule) {
|
|
83
|
+
const t = String(topModule || "").trim();
|
|
84
|
+
if (!t) return null;
|
|
85
|
+
if (PY_STDLIB.has(t)) return { pkg: t, builtin: true };
|
|
86
|
+
return { pkg: PY_IMPORT_TO_DIST[t] || t, builtin: false, ambiguous: PY_AMBIGUOUS_TOP.has(t) };
|
|
87
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Pure graph filters applied to a built graph.json (no I/O). graph-builder itself has no test/scope
|
|
2
|
+
// filter, so we post-process the graph object: drop test nodes, keep only a subpath, etc.
|
|
3
|
+
|
|
4
|
+
export function isTestPath(path) {
|
|
5
|
+
return /(^|[\\/])(__tests?__|tests?)([\\/]|$)|\.(test|itest|spec|e2e)\.|_test\.go$|(^|[\\/])test_[^\\/]*\.py$/i.test(String(path || ""));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Filter a built graph.json by test-mode: drop test nodes ("no-tests") or keep only tests + the
|
|
9
|
+
// nodes they depend on ("tests-only"). graph-builder update has no test filter, so we do it post-build.
|
|
10
|
+
export function filterGraphForMode(graph, mode) {
|
|
11
|
+
if (mode !== "no-tests" && mode !== "tests-only") return graph;
|
|
12
|
+
const nodes = graph.nodes || [];
|
|
13
|
+
const links = graph.links || [];
|
|
14
|
+
const endpoint = (value) => (value && typeof value === "object" ? value.id : value);
|
|
15
|
+
let keep;
|
|
16
|
+
if (mode === "no-tests") {
|
|
17
|
+
keep = new Set(nodes.filter((node) => !isTestPath(node.source_file)).map((node) => node.id));
|
|
18
|
+
} else {
|
|
19
|
+
const testIds = new Set(nodes.filter((node) => isTestPath(node.source_file)).map((node) => node.id));
|
|
20
|
+
keep = new Set(testIds);
|
|
21
|
+
for (const link of links) {
|
|
22
|
+
if (testIds.has(endpoint(link.source))) keep.add(endpoint(link.target)); // a test's dependency
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
...graph,
|
|
27
|
+
nodes: nodes.filter((node) => keep.has(node.id)),
|
|
28
|
+
links: links.filter((link) => keep.has(endpoint(link.source)) && keep.has(endpoint(link.target)))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Keep only nodes under a subpath (path-scope), drop links touching removed nodes.
|
|
33
|
+
export function filterGraphByScope(graph, scope) {
|
|
34
|
+
if (!scope) return graph;
|
|
35
|
+
const norm = (p) => String(p || "").replace(/\\/g, "/");
|
|
36
|
+
const prefix = norm(scope).replace(/\/+$/, "") + "/";
|
|
37
|
+
const endpoint = (value) => (value && typeof value === "object" ? value.id : value);
|
|
38
|
+
const keep = new Set((graph.nodes || []).filter((node) => (norm(node.source_file) + "/").startsWith(prefix)).map((node) => node.id));
|
|
39
|
+
return {
|
|
40
|
+
...graph,
|
|
41
|
+
nodes: (graph.nodes || []).filter((node) => keep.has(node.id)),
|
|
42
|
+
links: (graph.links || []).filter((link) => keep.has(endpoint(link.source)) && keep.has(endpoint(link.target)))
|
|
43
|
+
};
|
|
44
|
+
}
|