devlensio 1.0.4 → 1.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/dist/graph/edges/callEdges.js +97 -5
- package/dist/graph/edges/callEdges.test.js +154 -0
- package/dist/parser/extractors/classes.js +23 -4
- package/dist/parser/extractors/functions.d.ts +10 -3
- package/dist/parser/extractors/functions.js +135 -65
- package/dist/parser/extractors/hooks.js +5 -29
- package/dist/parser/extractors/objectMethods.js +3 -4
- package/dist/parser/index.js +11 -6
- package/dist/parser/overloads.d.ts +2 -0
- package/dist/parser/overloads.js +96 -0
- package/dist/parser/overloads.test.d.ts +1 -0
- package/dist/parser/overloads.test.js +175 -0
- package/dist/parser/typeUtils.d.ts +2 -0
- package/dist/parser/typeUtils.js +2 -0
- package/dist/types.d.ts +6 -0
- 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/calls.go +134 -2
- package/extractors/go/contract.go +1 -0
- package/extractors/go/exports.go +107 -0
- package/extractors/go/exports_test.go +64 -0
- package/extractors/go/extractor.go +1 -0
- package/extractors/go/lookup.go +63 -7
- package/extractors/go/nodes.go +28 -0
- package/extractors/go/parser.go +51 -8
- package/extractors/java/devlens_java_extractor.jar +0 -0
- package/extractors/java/src/devlens/extractor/ExportsMapBuilder.java +148 -0
- package/extractors/java/src/devlens/extractor/Extractor.java +34 -0
- package/extractors/java/src/devlens/extractor/LookupMaps.java +12 -0
- package/extractors/java/src/devlens/extractor/Overloads.java +290 -0
- package/extractors/java/src/devlens/extractor/Parser.java +39 -0
- package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +9 -0
- package/extractors/java/src/devlens/extractor/edges/Calls.java +42 -36
- package/extractors/java/src/devlens/extractor/edges/Routes.java +10 -3
- package/extractors/python/devlens_extractors_python/edges/calls.py +127 -26
- package/extractors/python/devlens_extractors_python/exports_map.py +263 -0
- package/extractors/python/devlens_extractors_python/extractor.py +7 -1
- package/extractors/python/devlens_extractors_python/lookup.py +6 -0
- package/extractors/python/devlens_extractors_python/parser/__init__.py +6 -0
- package/extractors/python/devlens_extractors_python/parser/functions.py +103 -6
- package/extractors/python/devlens_extractors_python/parser/overloads.py +105 -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/src/calls.rs +27 -16
- package/extractors/rust/src/contract.rs +2 -0
- package/extractors/rust/src/exports.rs +387 -0
- package/extractors/rust/src/exports_test.rs +105 -0
- package/extractors/rust/src/extractor.rs +2 -0
- package/extractors/rust/src/lookup.rs +60 -0
- package/extractors/rust/src/main.rs +4 -0
- package/extractors/rust/src/nodes.rs +19 -0
- package/extractors/rust/src/parser.rs +66 -1
- package/extractors/rust/src/routes.rs +2 -1
- package/package.json +1 -1
|
@@ -61,16 +61,110 @@ def call_name(func: ast.AST) -> str | None:
|
|
|
61
61
|
|
|
62
62
|
def extract_calls(body: list[ast.stmt]) -> list[str]:
|
|
63
63
|
"""Collect every called name in a function body, deduped."""
|
|
64
|
+
return extract_calls_with_sites(body)[0]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _infer_arg_type(arg: ast.AST) -> str:
|
|
68
|
+
"""Cheap literal arg-type tag in the same vocabulary as parameter
|
|
69
|
+
annotations (str/int/bool/list/dict); anything else stays unknown."""
|
|
70
|
+
if isinstance(arg, ast.Constant):
|
|
71
|
+
if isinstance(arg.value, bool):
|
|
72
|
+
return "bool"
|
|
73
|
+
if isinstance(arg.value, str):
|
|
74
|
+
return "str"
|
|
75
|
+
if isinstance(arg.value, (int, float)):
|
|
76
|
+
return "int" if isinstance(arg.value, int) else "float"
|
|
77
|
+
return "unknown"
|
|
78
|
+
if isinstance(arg, ast.JoinedStr):
|
|
79
|
+
return "str"
|
|
80
|
+
if isinstance(arg, ast.List):
|
|
81
|
+
return "list"
|
|
82
|
+
if isinstance(arg, ast.Dict):
|
|
83
|
+
return "dict"
|
|
84
|
+
if isinstance(arg, ast.Tuple):
|
|
85
|
+
return "tuple"
|
|
86
|
+
return "unknown"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def extract_calls_with_sites(body: list[ast.stmt]) -> tuple[list[str], list[dict]]:
|
|
90
|
+
"""ONE scope-guarded walk producing both outputs: the legacy deduped call
|
|
91
|
+
name list and structured call sites {name, argCount, argTypes, hasSpread}.
|
|
92
|
+
argCount counts positional args + keyword args; *args/**kwargs set
|
|
93
|
+
hasSpread (runtime arity unknowable). One record per
|
|
94
|
+
(name, argCount, argTypes) triple so same-arity different-type call sites
|
|
95
|
+
both survive (each can hit a different overload sibling)."""
|
|
64
96
|
calls: list[str] = []
|
|
97
|
+
sites: list[dict] = []
|
|
98
|
+
seen_sites: set[str] = set()
|
|
65
99
|
|
|
66
100
|
def visit(node: ast.AST) -> None:
|
|
67
|
-
if isinstance(node, ast.Call):
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
101
|
+
if not isinstance(node, ast.Call):
|
|
102
|
+
return
|
|
103
|
+
name = call_name(node.func)
|
|
104
|
+
if not name:
|
|
105
|
+
return
|
|
106
|
+
if name not in calls:
|
|
107
|
+
calls.append(name)
|
|
108
|
+
|
|
109
|
+
arg_types: list[str] = []
|
|
110
|
+
has_spread = False
|
|
111
|
+
for a in node.args:
|
|
112
|
+
if isinstance(a, ast.Starred):
|
|
113
|
+
has_spread = True
|
|
114
|
+
arg_types.append("unknown")
|
|
115
|
+
else:
|
|
116
|
+
arg_types.append(_infer_arg_type(a))
|
|
117
|
+
for kw in node.keywords:
|
|
118
|
+
if kw.arg is None: # **kwargs
|
|
119
|
+
has_spread = True
|
|
120
|
+
arg_types.append("unknown")
|
|
121
|
+
else:
|
|
122
|
+
arg_types.append(_infer_arg_type(kw.value))
|
|
123
|
+
|
|
124
|
+
arg_count = len(node.args) + len(node.keywords)
|
|
125
|
+
key = f"{name}/{arg_count}/{','.join(arg_types)}"
|
|
126
|
+
if key in seen_sites:
|
|
127
|
+
return
|
|
128
|
+
seen_sites.add(key)
|
|
129
|
+
site = {"name": name, "argCount": arg_count, "argTypes": arg_types}
|
|
130
|
+
if has_spread:
|
|
131
|
+
site["hasSpread"] = True
|
|
132
|
+
sites.append(site)
|
|
71
133
|
|
|
72
134
|
_walk_scope(body, visit)
|
|
73
|
-
return
|
|
135
|
+
return calls, sites
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _param_records(node: ast.FunctionDef | ast.AsyncFunctionDef) -> list[dict]:
|
|
139
|
+
"""Typed parameter records {name, type, isOptional?, isRest?} for arity and
|
|
140
|
+
type-shape matching. vararg/kwarg ride as isRest entries; defaults mark
|
|
141
|
+
isOptional. Annotation text is the type vocabulary used by callSites."""
|
|
142
|
+
records: list[dict] = []
|
|
143
|
+
a = node.args
|
|
144
|
+
positional = a.posonlyargs + a.args
|
|
145
|
+
defaults = a.defaults # aligns with positional[-len(defaults):]
|
|
146
|
+
kw_defaults = a.kw_defaults # aligns with kwonlyargs (None = required)
|
|
147
|
+
|
|
148
|
+
def record(arg: ast.arg, optional: bool, rest: bool) -> None:
|
|
149
|
+
rec: dict = {"name": arg.arg,
|
|
150
|
+
"type": ast.unparse(arg.annotation) if arg.annotation else "unknown"}
|
|
151
|
+
if rest:
|
|
152
|
+
rec["isRest"] = True
|
|
153
|
+
if optional:
|
|
154
|
+
rec["isOptional"] = True
|
|
155
|
+
records.append(rec)
|
|
156
|
+
|
|
157
|
+
offset = len(positional) - len(defaults)
|
|
158
|
+
for i, arg in enumerate(positional):
|
|
159
|
+
record(arg, i >= offset, False)
|
|
160
|
+
# kwonly: kw_defaults aligns 1:1 with kwonlyargs (None = required)
|
|
161
|
+
for i, arg in enumerate(a.kwonlyargs):
|
|
162
|
+
record(arg, kw_defaults[i] is not None, False)
|
|
163
|
+
if a.vararg:
|
|
164
|
+
record(a.vararg, False, True)
|
|
165
|
+
if a.kwarg:
|
|
166
|
+
record(a.kwarg, False, True)
|
|
167
|
+
return records
|
|
74
168
|
|
|
75
169
|
|
|
76
170
|
def _match_orm_call(call: ast.Call) -> dict | None:
|
|
@@ -140,12 +234,15 @@ def extract_function(node: ast.FunctionDef | ast.AsyncFunctionDef, rel_path: str
|
|
|
140
234
|
raw = ast.get_source_segment(source, node) or ""
|
|
141
235
|
params = [a.arg for a in node.args.posonlyargs + node.args.args + node.args.kwonlyargs]
|
|
142
236
|
decorators = [ast.unparse(d) for d in node.decorator_list]
|
|
237
|
+
calls, call_sites = extract_calls_with_sites(node.body)
|
|
143
238
|
|
|
144
239
|
# hasErrorHandling/throws use ast.walk deliberately — mirrors the JS
|
|
145
240
|
# extractor, which scans the whole function subtree for try/raise.
|
|
146
241
|
metadata = {
|
|
147
242
|
"params": params,
|
|
148
|
-
"
|
|
243
|
+
"parameters": _param_records(node),
|
|
244
|
+
"calls": calls,
|
|
245
|
+
"callSites": call_sites,
|
|
149
246
|
"ormOps": extract_orm_ops(node.body),
|
|
150
247
|
"isAsync": is_async,
|
|
151
248
|
"hasErrorHandling": any(isinstance(n, (ast.Try, ast.TryStar)) for n in ast.walk(node)),
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Overload disambiguation — same-name function-likes in the same file.
|
|
2
|
+
|
|
3
|
+
Node ids are ``rel/path.py::name``, so two same-name defs in one file (Python
|
|
4
|
+
shadowing, dynamic redefinition) produced two nodes with ONE id; downstream
|
|
5
|
+
dedupe silently dropped all but the first and every CALLS edge picked one
|
|
6
|
+
arbitrarily.
|
|
7
|
+
|
|
8
|
+
Convention (mirrors the TypeScript reference, src/parser/overloads.ts — keep
|
|
9
|
+
in sync, see MULTI_LANGUAGE_EXPANSION.md):
|
|
10
|
+
- unique name in file → id unchanged
|
|
11
|
+
- distinct arities in group → ``#arity`` suffix (the def's OWN param count,
|
|
12
|
+
never ordinals)
|
|
13
|
+
- same arity, different types → ``#arity#<sig8>`` where sig8 = first 8 hex
|
|
14
|
+
chars of sha-256 over the signature string
|
|
15
|
+
"(T1,T2)" (rest params prefixed "...",
|
|
16
|
+
optional suffixed "?", untyped "unknown")
|
|
17
|
+
- signature-identical defs collapse to one node (prefer the one with a
|
|
18
|
+
body; Python defs always have bodies so the first by line order wins)
|
|
19
|
+
Groups are per file; the same name in different files or on different classes
|
|
20
|
+
(dotted names) never collides.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import hashlib
|
|
25
|
+
|
|
26
|
+
_OVERLOADABLE_TYPES = {"FUNCTION", "METHOD"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _param_types(node: dict) -> list[str]:
|
|
30
|
+
parameters = node.get("metadata", {}).get("parameters")
|
|
31
|
+
if not isinstance(parameters, list):
|
|
32
|
+
return []
|
|
33
|
+
types: list[str] = []
|
|
34
|
+
for p in parameters:
|
|
35
|
+
t = p.get("type") if isinstance(p, dict) else None
|
|
36
|
+
t = t if isinstance(t, str) and t else "unknown"
|
|
37
|
+
if p.get("isRest"):
|
|
38
|
+
types.append(f"...{t}")
|
|
39
|
+
elif p.get("isOptional"):
|
|
40
|
+
types.append(f"{t}?")
|
|
41
|
+
else:
|
|
42
|
+
types.append(t)
|
|
43
|
+
return types
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _signature_key(node: dict) -> str:
|
|
47
|
+
return f"({','.join(_param_types(node))})"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _arity(node: dict) -> int:
|
|
51
|
+
params = node.get("metadata", {}).get("params")
|
|
52
|
+
return len(params) if isinstance(params, list) else 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _has_body(node: dict) -> bool:
|
|
56
|
+
return node.get("metadata", {}).get("isOverloadSignature") is not True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def apply_overload_disambiguation(nodes: list[dict], rel_path: str) -> None:
|
|
60
|
+
"""Suffix colliding ids in place; mark signature-identical duplicates with
|
|
61
|
+
metadata.isDuplicateOverload for the caller to drop."""
|
|
62
|
+
groups: dict[str, list[dict]] = {}
|
|
63
|
+
for node in nodes:
|
|
64
|
+
if node.get("type") in _OVERLOADABLE_TYPES:
|
|
65
|
+
groups.setdefault(node["name"], []).append(node)
|
|
66
|
+
|
|
67
|
+
for name, group in groups.items():
|
|
68
|
+
if len(group) < 2:
|
|
69
|
+
continue
|
|
70
|
+
group.sort(key=lambda n: n["startLine"])
|
|
71
|
+
|
|
72
|
+
# Collapse signature-identical defs, preferring the one with a body.
|
|
73
|
+
by_signature: dict[str, dict] = {}
|
|
74
|
+
keys: dict[int, str] = {}
|
|
75
|
+
for node in group:
|
|
76
|
+
key = _signature_key(node)
|
|
77
|
+
keys[id(node)] = key
|
|
78
|
+
existing = by_signature.get(key)
|
|
79
|
+
if existing is None:
|
|
80
|
+
by_signature[key] = node
|
|
81
|
+
elif not _has_body(existing) and _has_body(node):
|
|
82
|
+
by_signature[key] = node
|
|
83
|
+
survivors = sorted(by_signature.values(), key=lambda n: n["startLine"])
|
|
84
|
+
if len(survivors) < len(group):
|
|
85
|
+
kept = {id(n) for n in survivors}
|
|
86
|
+
for node in group:
|
|
87
|
+
if id(node) not in kept:
|
|
88
|
+
node["metadata"]["isDuplicateOverload"] = True
|
|
89
|
+
|
|
90
|
+
# Single survivor after collapse → the name is unique again, no suffix.
|
|
91
|
+
if len(survivors) < 2:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Distinct arities within the group → readable #arity suffix.
|
|
95
|
+
arities = {_arity(n) for n in survivors}
|
|
96
|
+
use_arity_only = len(arities) == len(survivors)
|
|
97
|
+
|
|
98
|
+
for node in survivors:
|
|
99
|
+
arity = _arity(node)
|
|
100
|
+
if use_arity_only:
|
|
101
|
+
suffix = f"#{arity}"
|
|
102
|
+
else:
|
|
103
|
+
sig8 = hashlib.sha256(keys[id(node)].encode("utf-8")).hexdigest()[:8]
|
|
104
|
+
suffix = f"#{arity}#{sig8}"
|
|
105
|
+
node["id"] = f"{rel_path}::{name}{suffix}"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// metadata.calls already carries the raw strings (parser); resolved targets
|
|
19
19
|
// are written back as metadata.resolvedCalls.
|
|
20
20
|
|
|
21
|
-
use crate::contract::{
|
|
21
|
+
use crate::contract::{edge_with_meta, CodeEdge};
|
|
22
22
|
use crate::extractor::{Options, ParsedRepo};
|
|
23
23
|
use crate::fingerprint::ParsedManifest;
|
|
24
24
|
use crate::lookup::LookupMaps;
|
|
@@ -54,9 +54,10 @@ pub fn detect_calls(
|
|
|
54
54
|
}
|
|
55
55
|
let base_module = crate::walker::module_path_for_file(&pf.rel_path);
|
|
56
56
|
let mut resolved: Vec<String> = vec![];
|
|
57
|
-
for
|
|
58
|
-
if let Some(target) = resolve_call(
|
|
59
|
-
|
|
57
|
+
for site in &item.call_sites {
|
|
58
|
+
if let Some((target, matched_by)) = resolve_call(
|
|
59
|
+
&site.name,
|
|
60
|
+
site.arg_count,
|
|
60
61
|
item,
|
|
61
62
|
&pf.rel_path,
|
|
62
63
|
&base_module,
|
|
@@ -65,7 +66,13 @@ pub fn detect_calls(
|
|
|
65
66
|
mf,
|
|
66
67
|
tp,
|
|
67
68
|
) {
|
|
68
|
-
|
|
69
|
+
let mut meta = serde_json::Map::new();
|
|
70
|
+
meta.insert(
|
|
71
|
+
"calledName".to_string(),
|
|
72
|
+
serde_json::Value::from(site.name.clone()),
|
|
73
|
+
);
|
|
74
|
+
meta.insert("matchedBy".to_string(), serde_json::Value::from(matched_by));
|
|
75
|
+
edges.push(edge_with_meta(&from, &target, "CALLS", meta));
|
|
69
76
|
resolved.push(target);
|
|
70
77
|
}
|
|
71
78
|
}
|
|
@@ -91,8 +98,10 @@ pub fn detect_calls(
|
|
|
91
98
|
|
|
92
99
|
/// Resolve one call string → target node id (or None).
|
|
93
100
|
#[allow(clippy::too_many_arguments)]
|
|
101
|
+
#[allow(clippy::too_many_arguments)]
|
|
94
102
|
fn resolve_call(
|
|
95
103
|
call: &str,
|
|
104
|
+
arg_count: usize,
|
|
96
105
|
item: &ParsedItem,
|
|
97
106
|
rel_path: &str,
|
|
98
107
|
base_module: &str,
|
|
@@ -100,7 +109,7 @@ fn resolve_call(
|
|
|
100
109
|
lookup: &LookupMaps,
|
|
101
110
|
mf: &ParsedManifest,
|
|
102
111
|
tp: &mut ThirdPartyRegistry,
|
|
103
|
-
) -> Option<String> {
|
|
112
|
+
) -> Option<(String, &'static str)> {
|
|
104
113
|
// `x.method()` — receiver form
|
|
105
114
|
if let Some((recv, method)) = call.split_once('.') {
|
|
106
115
|
if recv == "self" {
|
|
@@ -109,12 +118,12 @@ fn resolve_call(
|
|
|
109
118
|
if owner.is_empty() {
|
|
110
119
|
return None;
|
|
111
120
|
}
|
|
112
|
-
return method_of_owner(owner, method, rel_path, lookup);
|
|
121
|
+
return method_of_owner(owner, method, rel_path, lookup).map(|t| (t, "name"));
|
|
113
122
|
}
|
|
114
123
|
// other receivers: no type resolution in V1 — metadata only
|
|
115
124
|
return None;
|
|
116
125
|
}
|
|
117
|
-
resolve_path_call(call, rel_path, base_module, module_map, lookup, mf, tp)
|
|
126
|
+
resolve_path_call(call, arg_count, rel_path, base_module, module_map, lookup, mf, tp)
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
/// Resolve a path-form call (`a::b::c`, `Type::m`, plain `bar`) to a node id.
|
|
@@ -126,21 +135,23 @@ fn resolve_call(
|
|
|
126
135
|
/// crate → lazy [crate]/name::member (gated).
|
|
127
136
|
pub fn resolve_path_call(
|
|
128
137
|
call: &str,
|
|
138
|
+
arg_count: usize,
|
|
129
139
|
rel_path: &str,
|
|
130
140
|
base_module: &str,
|
|
131
141
|
module_map: &ModuleMap,
|
|
132
142
|
lookup: &LookupMaps,
|
|
133
143
|
mf: &ParsedManifest,
|
|
134
144
|
tp: &mut ThirdPartyRegistry,
|
|
135
|
-
) -> Option<String> {
|
|
145
|
+
) -> Option<(String, &'static str)> {
|
|
136
146
|
let segs: Vec<&str> = call.split("::").collect();
|
|
137
147
|
if segs.is_empty() {
|
|
138
148
|
return None;
|
|
139
149
|
}
|
|
140
150
|
if segs.len() == 1 {
|
|
141
|
-
// plain name → same-file FUNCTION, then crate-wide
|
|
142
|
-
|
|
143
|
-
|
|
151
|
+
// plain name → same-file FUNCTION, then crate-wide; arity filters
|
|
152
|
+
// cross-file same-name candidates before path proximity decides
|
|
153
|
+
if let Some(res) = lookup.closest_by_path_arity(segs[0], rel_path, arg_count) {
|
|
154
|
+
return Some(res);
|
|
144
155
|
}
|
|
145
156
|
return None;
|
|
146
157
|
}
|
|
@@ -167,7 +178,7 @@ pub fn resolve_path_call(
|
|
|
167
178
|
// handlers actually live (brought into scope via `use crate::controllers::*;`).
|
|
168
179
|
if let Some((file, rest)) = module_map.resolve(&full_path, base_module) {
|
|
169
180
|
if let Some(id) = resolve_in_file(&file, &rest, rel_path, lookup) {
|
|
170
|
-
return Some(id);
|
|
181
|
+
return Some((id, "name"));
|
|
171
182
|
}
|
|
172
183
|
// don't return None — fall through to glob unroll + crate-root
|
|
173
184
|
}
|
|
@@ -181,7 +192,7 @@ pub fn resolve_path_call(
|
|
|
181
192
|
let cand = format!("{}::{}", g, full_path);
|
|
182
193
|
if let Some((file, rest)) = module_map.resolve(&cand, base_module) {
|
|
183
194
|
if let Some(id) = resolve_in_file(&file, &rest, rel_path, lookup) {
|
|
184
|
-
return Some(id);
|
|
195
|
+
return Some((id, "name"));
|
|
185
196
|
}
|
|
186
197
|
}
|
|
187
198
|
}
|
|
@@ -196,7 +207,7 @@ pub fn resolve_path_call(
|
|
|
196
207
|
// misresolve against the caller's own module.
|
|
197
208
|
if let Some((file, rest)) = module_map.resolve_use(&full_path, base_module) {
|
|
198
209
|
if let Some(id) = resolve_in_file(&file, &rest, rel_path, lookup) {
|
|
199
|
-
return Some(id);
|
|
210
|
+
return Some((id, "name"));
|
|
200
211
|
}
|
|
201
212
|
}
|
|
202
213
|
|
|
@@ -207,7 +218,7 @@ pub fn resolve_path_call(
|
|
|
207
218
|
}
|
|
208
219
|
if crate::thirdparty::is_external_crate(first, mf) {
|
|
209
220
|
let member = full_path.split("::").skip(1).collect::<Vec<_>>().join("::");
|
|
210
|
-
return tp.member_node(first, &member).map(|n| n.id.clone());
|
|
221
|
+
return tp.member_node(first, &member).map(|n| (n.id.clone(), "name"));
|
|
211
222
|
}
|
|
212
223
|
None
|
|
213
224
|
}
|
|
@@ -95,6 +95,8 @@ pub struct ExtractorResult {
|
|
|
95
95
|
pub nodes: Vec<CodeNode>,
|
|
96
96
|
pub edges: Vec<CodeEdge>,
|
|
97
97
|
pub routes: Vec<RouteNode>,
|
|
98
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
99
|
+
pub exports: Option<crate::exports::ExportsMap>,
|
|
98
100
|
pub stats: Stats,
|
|
99
101
|
pub errors: Vec<ExtractorError>,
|
|
100
102
|
}
|