devlensio 0.6.2 → 1.0.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/README.md +89 -31
- package/dist/extractors/detectLanguage.d.ts +2 -0
- package/dist/extractors/detectLanguage.js +33 -0
- package/dist/extractors/index.d.ts +6 -0
- package/dist/extractors/index.js +116 -0
- package/dist/extractors/runner.d.ts +7 -0
- package/dist/extractors/runner.js +194 -0
- package/dist/extractors/types.d.ts +37 -0
- package/dist/extractors/types.js +8 -0
- package/dist/graph/buildLookup.d.ts +1 -0
- package/dist/graph/buildLookup.js +2 -1
- package/dist/graph/edges/callEdges.js +19 -5
- package/dist/graph/edges/callEdges.test.d.ts +1 -0
- package/dist/graph/edges/callEdges.test.js +200 -0
- package/dist/graph/edges/importEdges.js +12 -0
- package/dist/graph/edges/inheritanceEdges.d.ts +3 -0
- package/dist/graph/edges/inheritanceEdges.js +73 -0
- package/dist/graph/edges/inheritanceEdges.test.d.ts +1 -0
- package/dist/graph/edges/inheritanceEdges.test.js +140 -0
- package/dist/graph/index.js +4 -0
- package/dist/parser/classes.test.d.ts +1 -0
- package/dist/parser/classes.test.js +360 -0
- package/dist/parser/extractors/classes.d.ts +5 -0
- package/dist/parser/extractors/classes.js +241 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +7 -1
- package/dist/pipeline/index.d.ts +2 -1
- package/dist/pipeline/index.js +30 -42
- package/dist/scoring/index.js +8 -0
- package/dist/scoring/index.test.js +43 -0
- package/dist/scoring/nodeScorer.js +2 -0
- package/dist/scoring/pruneDisconnected.d.ts +8 -0
- package/dist/scoring/pruneDisconnected.js +66 -0
- package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
- package/dist/scoring/pruneDisconnected.test.js +123 -0
- package/dist/summarizer/prompts.d.ts +1 -1
- package/dist/types.d.ts +5 -5
- package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
- package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
- package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
- package/extractors/go/build.mjs +43 -0
- package/extractors/go/calls.go +289 -0
- package/extractors/go/contract.go +140 -0
- package/extractors/go/extractor.go +161 -0
- package/extractors/go/fingerprint.go +167 -0
- package/extractors/go/go.mod +3 -0
- package/extractors/go/imports.go +90 -0
- package/extractors/go/inheritance.go +138 -0
- package/extractors/go/lookup.go +106 -0
- package/extractors/go/main.go +57 -0
- package/extractors/go/nodes.go +177 -0
- package/extractors/go/orm_edges.go +277 -0
- package/extractors/go/parser.go +469 -0
- package/extractors/go/routes.go +521 -0
- package/extractors/go/tests.go +45 -0
- package/extractors/go/thirdparty.go +138 -0
- package/extractors/go/typeload.go +178 -0
- package/extractors/go/walker.go +67 -0
- package/extractors/java/build.mjs +90 -0
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/Contract.java +171 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
- package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
- package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
- package/extractors/java/src/devlens/extractor/Main.java +66 -0
- package/extractors/java/src/devlens/extractor/Parser.java +522 -0
- package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
- package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
- package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
- package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
- package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
- package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
- package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
- package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
- package/extractors/python/devlens_extractors_python/__init__.py +3 -0
- package/extractors/python/devlens_extractors_python/__main__.py +33 -0
- package/extractors/python/devlens_extractors_python/contract.py +94 -0
- package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
- package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
- package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
- package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
- package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
- package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
- package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
- package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
- package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
- package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
- package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
- package/extractors/python/devlens_extractors_python/extractor.py +107 -0
- package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
- package/extractors/python/devlens_extractors_python/lookup.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
- package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
- package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
- package/extractors/python/devlens_extractors_python/third_party.py +103 -0
- package/extractors/python/pyproject.toml +16 -0
- package/extractors/python/setup.mjs +54 -0
- package/extractors/rust/Cargo.toml +29 -0
- package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
- package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
- package/extractors/rust/build.mjs +85 -0
- package/extractors/rust/src/calls.rs +295 -0
- package/extractors/rust/src/contract.rs +236 -0
- package/extractors/rust/src/enrich.rs +107 -0
- package/extractors/rust/src/extractor.rs +199 -0
- package/extractors/rust/src/fingerprint.rs +238 -0
- package/extractors/rust/src/imports.rs +101 -0
- package/extractors/rust/src/inheritance.rs +233 -0
- package/extractors/rust/src/lookup.rs +226 -0
- package/extractors/rust/src/main.rs +53 -0
- package/extractors/rust/src/module_map.rs +131 -0
- package/extractors/rust/src/nodes.rs +174 -0
- package/extractors/rust/src/orm_edges.rs +125 -0
- package/extractors/rust/src/parser.rs +859 -0
- package/extractors/rust/src/routes.rs +1265 -0
- package/extractors/rust/src/tests.rs +109 -0
- package/extractors/rust/src/thirdparty.rs +114 -0
- package/extractors/rust/src/walker.rs +82 -0
- package/package.json +22 -4
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Function extraction — FUNCTION/METHOD nodes + metadata.calls + ormOps.
|
|
2
|
+
|
|
3
|
+
The scope rule lives here: calls inside nested functions belong to the
|
|
4
|
+
inner function, not the outer one.
|
|
5
|
+
|
|
6
|
+
ORM call patterns are COLLECTED here at parse time (one AST walk) and
|
|
7
|
+
RESOLVED later by edges/orm_edges.py via lookup maps — no re-parsing.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
import ast
|
|
11
|
+
|
|
12
|
+
from ..contract import code_node
|
|
13
|
+
|
|
14
|
+
NESTED_SCOPES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)
|
|
15
|
+
|
|
16
|
+
# ── ORM patterns (parse-time facts; resolution lives in edges/orm_edges.py) ──
|
|
17
|
+
DJANGO_QUERY_METHODS = {
|
|
18
|
+
"get", "filter", "all", "exclude", "first", "last", "count", "exists",
|
|
19
|
+
"values", "values_list", "only", "defer", "select_related",
|
|
20
|
+
"prefetch_related", "aggregate", "annotate", "distinct", "order_by",
|
|
21
|
+
"reverse", "raw", "iterator", "latest", "earliest", "in_bulk",
|
|
22
|
+
}
|
|
23
|
+
DJANGO_WRITE_METHODS = {
|
|
24
|
+
"create", "update", "get_or_create", "update_or_create",
|
|
25
|
+
"bulk_create", "bulk_update", "delete",
|
|
26
|
+
}
|
|
27
|
+
SQLA_STATEMENT_FUNCS = {"select", "insert", "update", "delete"}
|
|
28
|
+
SESSION_MUTATORS = {"add", "add_all", "merge", "delete"} # session.add(obj) etc.
|
|
29
|
+
INSTANCE_MUTATORS = {"save", "delete"} # obj.save() / obj.delete()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _walk_scope(body: list[ast.stmt], visit) -> None:
|
|
33
|
+
"""Walk statements, applying the scope rule (never descend into nested
|
|
34
|
+
FunctionDef/ClassDef/Lambda). Shared by extract_calls + extract_orm_ops."""
|
|
35
|
+
def walk(node: ast.AST) -> None:
|
|
36
|
+
if isinstance(node, NESTED_SCOPES):
|
|
37
|
+
return
|
|
38
|
+
visit(node)
|
|
39
|
+
for child in ast.iter_child_nodes(node):
|
|
40
|
+
walk(child)
|
|
41
|
+
for stmt in body:
|
|
42
|
+
walk(stmt)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def call_name(func: ast.AST) -> str | None:
|
|
46
|
+
"""'foo()' → 'foo' · 'obj.method()' → 'obj.method' · 'requests.get()' → 'requests.get'.
|
|
47
|
+
Subscript callees (foo[0]()) and other exotic forms → None."""
|
|
48
|
+
if isinstance(func, ast.Name):
|
|
49
|
+
return func.id
|
|
50
|
+
if isinstance(func, ast.Attribute):
|
|
51
|
+
parts: list[str] = []
|
|
52
|
+
node = func
|
|
53
|
+
while isinstance(node, ast.Attribute):
|
|
54
|
+
parts.append(node.attr)
|
|
55
|
+
node = node.value
|
|
56
|
+
if isinstance(node, ast.Name):
|
|
57
|
+
parts.append(node.id)
|
|
58
|
+
return ".".join(reversed(parts))
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def extract_calls(body: list[ast.stmt]) -> list[str]:
|
|
63
|
+
"""Collect every called name in a function body, deduped."""
|
|
64
|
+
calls: list[str] = []
|
|
65
|
+
|
|
66
|
+
def visit(node: ast.AST) -> None:
|
|
67
|
+
if isinstance(node, ast.Call):
|
|
68
|
+
name = call_name(node.func)
|
|
69
|
+
if name:
|
|
70
|
+
calls.append(name)
|
|
71
|
+
|
|
72
|
+
_walk_scope(body, visit)
|
|
73
|
+
return list(dict.fromkeys(calls)) # dedupe, keep first-seen order
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _match_orm_call(call: ast.Call) -> dict | None:
|
|
77
|
+
"""One Call → ORM op dict {kind, pattern, target?, arg?, root?} or None.
|
|
78
|
+
|
|
79
|
+
Patterns:
|
|
80
|
+
X.objects.<query|write>(...) django_objects (target = X)
|
|
81
|
+
var.query(X) sqlalchemy_query (target = X)
|
|
82
|
+
var.get(X, pk) session_get (target = X)
|
|
83
|
+
var.add/delete/merge(X) session_mutate (arg = instance var)
|
|
84
|
+
inst.save()/delete() instance_mutate (arg = var, heuristic)
|
|
85
|
+
select/insert/update/delete(X) sqlalchemy_<fn> (root verified at resolve)
|
|
86
|
+
"""
|
|
87
|
+
func = call.func
|
|
88
|
+
|
|
89
|
+
# X.objects.<method>(...) — Django ORM queryset/manager
|
|
90
|
+
if isinstance(func, ast.Attribute) and isinstance(func.value, ast.Attribute) \
|
|
91
|
+
and func.value.attr == "objects" and isinstance(func.value.value, ast.Name):
|
|
92
|
+
m = func.attr
|
|
93
|
+
if m in DJANGO_QUERY_METHODS:
|
|
94
|
+
return {"kind": "read", "pattern": "django_objects", "target": func.value.value.id}
|
|
95
|
+
if m in DJANGO_WRITE_METHODS:
|
|
96
|
+
return {"kind": "write", "pattern": "django_objects", "target": func.value.value.id}
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
# var.<attr>(...) — session / instance patterns
|
|
100
|
+
if isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
|
|
101
|
+
var, m = func.value.id, func.attr
|
|
102
|
+
if m == "query" and call.args and isinstance(call.args[0], ast.Name):
|
|
103
|
+
return {"kind": "read", "pattern": "sqlalchemy_query", "target": call.args[0].id}
|
|
104
|
+
if m == "get" and len(call.args) >= 2 and isinstance(call.args[0], ast.Name):
|
|
105
|
+
return {"kind": "read", "pattern": "session_get", "target": call.args[0].id}
|
|
106
|
+
if m in SESSION_MUTATORS and call.args and isinstance(call.args[0], ast.Name):
|
|
107
|
+
return {"kind": "write", "pattern": "session_mutate", "arg": call.args[0].id}
|
|
108
|
+
if m in INSTANCE_MUTATORS and not call.args:
|
|
109
|
+
return {"kind": "write", "pattern": "instance_mutate", "arg": var}
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
# select(X) / insert(X) / update(X) / delete(X) — SQLAlchemy 2.0 statements
|
|
113
|
+
if isinstance(func, ast.Name) and func.id in SQLA_STATEMENT_FUNCS \
|
|
114
|
+
and call.args and isinstance(call.args[0], ast.Name):
|
|
115
|
+
kind = "read" if func.id == "select" else "write"
|
|
116
|
+
return {"kind": kind, "pattern": f"sqlalchemy_{func.id}",
|
|
117
|
+
"target": call.args[0].id, "root": func.id}
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def extract_orm_ops(body: list[ast.stmt]) -> list[dict]:
|
|
122
|
+
"""Collect ORM access patterns in a function body (scope-guarded)."""
|
|
123
|
+
ops: list[dict] = []
|
|
124
|
+
|
|
125
|
+
def visit(node: ast.AST) -> None:
|
|
126
|
+
if isinstance(node, ast.Call):
|
|
127
|
+
op = _match_orm_call(node)
|
|
128
|
+
if op:
|
|
129
|
+
ops.append(op)
|
|
130
|
+
|
|
131
|
+
_walk_scope(body, visit)
|
|
132
|
+
return ops
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def extract_function(node: ast.FunctionDef | ast.AsyncFunctionDef, rel_path: str,
|
|
136
|
+
source: str, parent_class: str | None = None) -> dict:
|
|
137
|
+
is_async = isinstance(node, ast.AsyncFunctionDef)
|
|
138
|
+
name = f"{parent_class}.{node.name}" if parent_class else node.name
|
|
139
|
+
|
|
140
|
+
raw = ast.get_source_segment(source, node) or ""
|
|
141
|
+
params = [a.arg for a in node.args.posonlyargs + node.args.args + node.args.kwonlyargs]
|
|
142
|
+
decorators = [ast.unparse(d) for d in node.decorator_list]
|
|
143
|
+
|
|
144
|
+
# hasErrorHandling/throws use ast.walk deliberately — mirrors the JS
|
|
145
|
+
# extractor, which scans the whole function subtree for try/raise.
|
|
146
|
+
metadata = {
|
|
147
|
+
"params": params,
|
|
148
|
+
"calls": extract_calls(node.body),
|
|
149
|
+
"ormOps": extract_orm_ops(node.body),
|
|
150
|
+
"isAsync": is_async,
|
|
151
|
+
"hasErrorHandling": any(isinstance(n, (ast.Try, ast.TryStar)) for n in ast.walk(node)),
|
|
152
|
+
"throws": any(isinstance(n, ast.Raise) for n in ast.walk(node)),
|
|
153
|
+
"lineCount": (node.end_lineno or node.lineno) - node.lineno,
|
|
154
|
+
"decorators": decorators,
|
|
155
|
+
}
|
|
156
|
+
if parent_class:
|
|
157
|
+
metadata["parentClass"] = parent_class
|
|
158
|
+
|
|
159
|
+
return code_node(
|
|
160
|
+
rel_path, name,
|
|
161
|
+
"METHOD" if parent_class else "FUNCTION",
|
|
162
|
+
node.lineno, node.end_lineno or node.lineno, raw, metadata,
|
|
163
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
# Directories to Ingore
|
|
6
|
+
IGNORE_DIRS = {
|
|
7
|
+
".git", "__pycache__", ".venv", "venv", "env", "node_modules",
|
|
8
|
+
"dist", "build", ".next", "coverage", ".mypy_cache", ".pytest_cache",
|
|
9
|
+
".tox", ".idea", ".vscode", ".eggs", "site-packages", "migrations",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
_TEST_FILE_RE = re.compile(r"(?:^|/)(?:test_[^/]+|[^/]+_test)\.py$")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def walk_python_files(repo_path: str):
|
|
16
|
+
"""Yield relative paths of every .py file, pruning ignored dirs.
|
|
17
|
+
|
|
18
|
+
dirnames[:] = [...] is the in-place prune that makes os.walk skip
|
|
19
|
+
those directories entirely (same role as the JS parser's IGNORE_DIRS)."""
|
|
20
|
+
root = Path(repo_path)
|
|
21
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
22
|
+
dirnames[:] = [d for d in dirnames if d not in IGNORE_DIRS]
|
|
23
|
+
for fn in sorted(filenames):
|
|
24
|
+
if fn.endswith(".py"):
|
|
25
|
+
rel = Path(dirpath).relative_to(root).as_posix()
|
|
26
|
+
yield f"{rel}/{fn}" if rel != "." else fn
|
|
27
|
+
|
|
28
|
+
def is_test_file(rel_path: str) -> bool:
|
|
29
|
+
"""test_foo.py or foo_test.py, at any depth."""
|
|
30
|
+
return bool(_TEST_FILE_RE.search(rel_path))
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Third-party node creation — [pip]/pkg and [pip]/pkg::name nodes.
|
|
2
|
+
|
|
3
|
+
Mirrors the JS side's [npm]/... convention (src/graph/thirdPartyLibs.ts +
|
|
4
|
+
importEdges.ts): one package node per import, per-name method nodes for
|
|
5
|
+
named imports, all deduped in a single registry.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
# Exact stdlib module set from the running interpreter (Python 3.10+).
|
|
12
|
+
STDLIB_MODULES = frozenset(sys.stdlib_module_names)
|
|
13
|
+
|
|
14
|
+
# Runtime libraries — real architectural decisions (mirrors RUNTIME_INCLUSION_SET)
|
|
15
|
+
RUNTIME_PACKAGES = {
|
|
16
|
+
"fastapi", "flask", "django", "starlette", "uvicorn", "gunicorn",
|
|
17
|
+
"sqlalchemy", "pydantic", "alembic", "psycopg2", "psycopg", "asyncpg",
|
|
18
|
+
"pymongo", "motor", "redis", "celery", "requests", "httpx", "aiohttp",
|
|
19
|
+
"numpy", "pandas", "scipy", "scikit-learn", "tensorflow", "torch",
|
|
20
|
+
"pyjwt", "passlib", "bcrypt", "cryptography", "boto3", "stripe",
|
|
21
|
+
"openai", "anthropic", "langchain", "sentry-sdk", "loguru",
|
|
22
|
+
"beautifulsoup4", "scrapy", "pillow", "opencv-python", "matplotlib",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Dev tooling — low signal (mirrors the JS devtool category)
|
|
26
|
+
DEVTOOL_PACKAGES = {
|
|
27
|
+
"pytest", "black", "flake8", "mypy", "ruff", "isort", "coverage",
|
|
28
|
+
"tox", "nox", "pre-commit", "sphinx", "mkdocs", "hypothesis",
|
|
29
|
+
"responses", "vcrpy", "faker", "factory-boy", "freezegun",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class ThirdPartyRegistry:
|
|
33
|
+
"""Creates and caches [pip]/... nodes. One registry per extraction —
|
|
34
|
+
the same package imported in 20 files yields ONE node.
|
|
35
|
+
|
|
36
|
+
`allowed` gates third-party inclusion: only packages in the set get
|
|
37
|
+
nodes. Empty set → NO third-party nodes at all (the engine controls
|
|
38
|
+
this via options.includedThirdPartyLibs)."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, raw_dependencies: dict[str, str],
|
|
41
|
+
allowed: set[str] | None = None):
|
|
42
|
+
self._nodes: dict[str, dict] = {}
|
|
43
|
+
self.raw_dependencies = raw_dependencies
|
|
44
|
+
self.allowed = allowed if allowed is not None else set()
|
|
45
|
+
|
|
46
|
+
def _permitted(self, pkg: str) -> bool:
|
|
47
|
+
return pkg in self.allowed
|
|
48
|
+
|
|
49
|
+
def _base_metadata(self, pkg: str) -> dict:
|
|
50
|
+
version = self.raw_dependencies.get(pkg, "unknown")
|
|
51
|
+
if pkg in RUNTIME_PACKAGES:
|
|
52
|
+
category = "runtime"
|
|
53
|
+
elif pkg in DEVTOOL_PACKAGES:
|
|
54
|
+
category = "devtool"
|
|
55
|
+
else:
|
|
56
|
+
category = "unknown"
|
|
57
|
+
return {"isThirdParty": True, "packageVersion": version, "category": category}
|
|
58
|
+
|
|
59
|
+
def package_node(self, pkg: str) -> dict | None:
|
|
60
|
+
"""[pip]/pkg node, or None when the package is not permitted."""
|
|
61
|
+
if not self._permitted(pkg):
|
|
62
|
+
return None
|
|
63
|
+
node_id = f"[pip]/{pkg}"
|
|
64
|
+
if node_id not in self._nodes:
|
|
65
|
+
self._nodes[node_id] = {
|
|
66
|
+
"id": node_id,
|
|
67
|
+
"name": pkg,
|
|
68
|
+
"type": "THIRD_PARTY",
|
|
69
|
+
"filePath": node_id,
|
|
70
|
+
"startLine": 0,
|
|
71
|
+
"endLine": 0,
|
|
72
|
+
"metadata": self._base_metadata(pkg),
|
|
73
|
+
}
|
|
74
|
+
return self._nodes[node_id]
|
|
75
|
+
|
|
76
|
+
def method_node(self, pkg: str, name: str) -> dict | None:
|
|
77
|
+
"""Named import member: from flask import Flask → [pip]/flask::Flask.
|
|
78
|
+
None when the package is not permitted."""
|
|
79
|
+
if not self._permitted(pkg):
|
|
80
|
+
return None
|
|
81
|
+
node_id = f"[pip]/{pkg}::{name}"
|
|
82
|
+
if node_id not in self._nodes:
|
|
83
|
+
pkg_node = self.package_node(pkg)
|
|
84
|
+
if pkg_node is None:
|
|
85
|
+
return None
|
|
86
|
+
self._nodes[node_id] = {
|
|
87
|
+
"id": node_id,
|
|
88
|
+
"name": f"{pkg}.{name}",
|
|
89
|
+
"type": "THIRD_PARTY",
|
|
90
|
+
"filePath": f"[pip]/{pkg}",
|
|
91
|
+
"startLine": 0,
|
|
92
|
+
"endLine": 0,
|
|
93
|
+
"metadata": {
|
|
94
|
+
**pkg_node["metadata"],
|
|
95
|
+
"parentPackageId": pkg_node["id"],
|
|
96
|
+
"methodName": name,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
return self._nodes[node_id]
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def nodes(self) -> list[dict]:
|
|
103
|
+
return list(self._nodes.values())
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "devlens-extractors-python"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "DevLens Python extractor — AST-based code analysis producing the DevLens JSON contract"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
dev = ["pytest>=7"]
|
|
14
|
+
|
|
15
|
+
[tool.setuptools.packages.find]
|
|
16
|
+
include = ["devlens_extractors_python*"]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// DevLens Python extractor — venv bootstrap (cross-platform: Linux/macOS/Windows).
|
|
3
|
+
//
|
|
4
|
+
// Runs on `npm install` (postinstall) on the INSTALLING machine: a venv is
|
|
5
|
+
// machine-specific (absolute paths, platform binaries) so it can never live
|
|
6
|
+
// in the npm tarball — it must be created here. Node is guaranteed present
|
|
7
|
+
// (it's an npm package), so this works everywhere bash isn't.
|
|
8
|
+
//
|
|
9
|
+
// Idempotent + non-fatal:
|
|
10
|
+
// - python3 missing → warn, exit 0 (Python analysis unavailable)
|
|
11
|
+
// - .venv already present → skip
|
|
12
|
+
// - otherwise → create venv + editable-install the package
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const IS_WIN = process.platform === "win32";
|
|
21
|
+
const VENV_PYTHON = join(HERE, ".venv", IS_WIN ? "Scripts/python.exe" : "bin/python");
|
|
22
|
+
|
|
23
|
+
function which(cmd) {
|
|
24
|
+
const r = spawnSync(IS_WIN ? "where" : "which", [cmd], { stdio: "pipe" });
|
|
25
|
+
return r.status === 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// python3 on Windows is usually `python` (the Store alias is python3 too,
|
|
29
|
+
// but `python` is the safe bet on CI images).
|
|
30
|
+
const pythonCmd = which("python3") ? "python3" : which("python") ? "python" : null;
|
|
31
|
+
|
|
32
|
+
if (!pythonCmd) {
|
|
33
|
+
console.error("devlens: python3 not found — Python extraction unavailable (install Python 3.11+)");
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (existsSync(VENV_PYTHON)) {
|
|
38
|
+
console.error("devlens: python extractor venv already present — skipping");
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.error(`devlens: creating python extractor venv at ${join(HERE, ".venv")}`);
|
|
43
|
+
mkdirSync(join(HERE, ".venv"), { recursive: true });
|
|
44
|
+
let r = spawnSync(pythonCmd, ["-m", "venv", ".venv"], { cwd: HERE, stdio: "inherit" });
|
|
45
|
+
if (r.status !== 0) {
|
|
46
|
+
console.error("devlens: `python -m venv` failed — Python extraction unavailable");
|
|
47
|
+
process.exit(0); // non-fatal: don't break npm install
|
|
48
|
+
}
|
|
49
|
+
r = spawnSync(VENV_PYTHON, ["-m", "pip", "install", "--quiet", "-e", "."], { cwd: HERE, stdio: "inherit" });
|
|
50
|
+
if (r.status !== 0) {
|
|
51
|
+
console.error("devlens: pip install failed — Python extraction unavailable");
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
console.error("devlens: python extractor ready");
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "devlens_rust_extractor"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
description = "DevLens Rust codebase graph extractor (JSON over stdin/stdout)"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
|
|
8
|
+
# 100% pure-Rust dependency tree — no C code. This is what makes the
|
|
9
|
+
# no-sudo cross-compile matrix possible (rust-lld links musl/windows-gnu,
|
|
10
|
+
# zig links darwin). Do NOT add any crate with C/cc dependencies.
|
|
11
|
+
[dependencies]
|
|
12
|
+
syn = { version = "2", features = ["full", "visit", "extra-traits"] }
|
|
13
|
+
proc-macro2 = { version = "1", features = ["span-locations"] }
|
|
14
|
+
quote = "1"
|
|
15
|
+
serde = { version = "1", features = ["derive"] }
|
|
16
|
+
serde_json = "1"
|
|
17
|
+
toml = "0.8"
|
|
18
|
+
sha2 = "0.10"
|
|
19
|
+
|
|
20
|
+
# Static-binary hygiene: panic=abort avoids unwinding runtime bloat; strip
|
|
21
|
+
# symbols in release. lto keeps the binary lean.
|
|
22
|
+
[profile.release]
|
|
23
|
+
panic = "abort"
|
|
24
|
+
lto = true
|
|
25
|
+
codegen-units = 1
|
|
26
|
+
strip = "symbols"
|
|
27
|
+
|
|
28
|
+
[profile.release.package."*"]
|
|
29
|
+
opt-level = 3
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// DevLens Rust extractor — cross-compiled static binaries (Node script, so it
|
|
3
|
+
// runs on Windows too; never bash).
|
|
4
|
+
//
|
|
5
|
+
// The Rust extractor ships as per-platform static binaries inside the
|
|
6
|
+
// devlensio tarball (files whitelist). Node is guaranteed on every machine
|
|
7
|
+
// that installs an npm package; the Rust toolchain is a build-time dep (same
|
|
8
|
+
// class as JDK for java / Go toolchain for go).
|
|
9
|
+
//
|
|
10
|
+
// Cross-compile matrix (all sudo-free — the dep tree is 100% pure Rust):
|
|
11
|
+
// linux-amd64/arm64 → musl targets linked by rust-lld: true static
|
|
12
|
+
// binaries (no glibc), zero runtime deps — matches
|
|
13
|
+
// Go's CGO_ENABLED=0 claim
|
|
14
|
+
// windows-amd64 → x86_64-pc-windows-gnu via cargo-zigbuild: zig
|
|
15
|
+
// bundles the mingw-w64 import libs (rust-lld alone
|
|
16
|
+
// can't see kernel32.lib etc. without a system mingw)
|
|
17
|
+
// darwin-amd64/arm64 → cargo-zigbuild: zig is the Mach-O linker, no macOS
|
|
18
|
+
// SDK needed for pure-Rust crates
|
|
19
|
+
//
|
|
20
|
+
// Prereqs on the publishing machine: rustup (stable + targets added),
|
|
21
|
+
// cargo-zigbuild, and a zig binary (found via env ZIG, ~/zig/zig-*/,
|
|
22
|
+
// or PATH).
|
|
23
|
+
//
|
|
24
|
+
// Usage: node build.mjs (idempotent — run from this directory)
|
|
25
|
+
|
|
26
|
+
import { execFileSync } from "node:child_process";
|
|
27
|
+
import { existsSync, mkdirSync, copyFileSync, readdirSync, rmSync } from "node:fs";
|
|
28
|
+
import { dirname, join } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
|
|
32
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
|
|
34
|
+
// [platform dir, cargo target triple, build kind]
|
|
35
|
+
const TARGETS = [
|
|
36
|
+
["linux-amd64", "x86_64-unknown-linux-musl", "rust-lld"],
|
|
37
|
+
["linux-arm64", "aarch64-unknown-linux-musl", "rust-lld"],
|
|
38
|
+
["windows-amd64", "x86_64-pc-windows-gnu", "zig"],
|
|
39
|
+
["darwin-amd64", "x86_64-apple-darwin", "zig"],
|
|
40
|
+
["darwin-arm64", "aarch64-apple-darwin", "zig"],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const cargo =
|
|
44
|
+
process.env.CARGO ||
|
|
45
|
+
(existsSync(join(homedir(), ".cargo", "bin", "cargo"))
|
|
46
|
+
? join(homedir(), ".cargo", "bin", "cargo")
|
|
47
|
+
: "cargo");
|
|
48
|
+
|
|
49
|
+
function findZig() {
|
|
50
|
+
if (process.env.ZIG && existsSync(process.env.ZIG)) return process.env.ZIG;
|
|
51
|
+
const zigDir = join(homedir(), "zig");
|
|
52
|
+
if (existsSync(zigDir)) {
|
|
53
|
+
for (const entry of readdirSync(zigDir).sort().reverse()) {
|
|
54
|
+
const cand = join(zigDir, entry, "zig");
|
|
55
|
+
if (existsSync(cand)) return cand;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return "zig"; // fall back to PATH
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function run(cmd, args, opts = {}) {
|
|
62
|
+
execFileSync(cmd, args, { stdio: "inherit", ...opts });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const zig = findZig();
|
|
66
|
+
for (const [platformDir, triple, kind] of TARGETS) {
|
|
67
|
+
const outDir = join(HERE, "bin", platformDir);
|
|
68
|
+
mkdirSync(outDir, { recursive: true });
|
|
69
|
+
const exe = platformDir.startsWith("windows") ? "devlens_rust_extractor.exe" : "devlens_rust_extractor";
|
|
70
|
+
console.log(`==> building ${platformDir} (${triple}, ${kind})`);
|
|
71
|
+
const env = { ...process.env };
|
|
72
|
+
let args;
|
|
73
|
+
if (kind === "rust-lld") {
|
|
74
|
+
// plain cargo build: rust-lld is the linker for pure-Rust musl targets
|
|
75
|
+
env.RUSTFLAGS = "-C linker=rust-lld";
|
|
76
|
+
args = ["build", "--target", triple, "--release"];
|
|
77
|
+
} else {
|
|
78
|
+
// zigbuild: zig must be on PATH (it spawns `zig`)
|
|
79
|
+
env.PATH = `${dirname(zig)}${process.platform === "win32" ? ";" : ":"}${env.PATH}`;
|
|
80
|
+
args = ["zigbuild", "--target", triple, "--release"];
|
|
81
|
+
}
|
|
82
|
+
run(cargo, args, { cwd: HERE, env });
|
|
83
|
+
copyFileSync(join(HERE, "target", triple, "release", exe), join(outDir, exe));
|
|
84
|
+
}
|
|
85
|
+
console.log("==> Done: extractors/rust/bin/<platform>/devlens_rust_extractor[.exe]");
|