weavatrix 0.3.10 → 0.3.12
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 +12 -4
- package/docs/releases/v0.3.11.md +55 -0
- package/docs/releases/v0.3.12.md +44 -0
- package/package.json +4 -1
- package/src/analysis/cargo-manifests.js +7 -0
- package/src/analysis/dead-check.js +4 -4
- package/src/analysis/duplicates.compute.js +5 -1
- package/src/analysis/hot-path-review.js +25 -3
- package/src/analysis/internal-audit/supply-chain.js +1 -1
- package/src/analysis/manifests.js +4 -1
- package/src/analysis/static-test-reachability.js +19 -2
- package/src/analysis-kit.mjs +17 -0
- package/src/graph/builder/lang-rust-calls.js +63 -0
- package/src/graph/builder/lang-rust.js +31 -6
- package/src/graph/builder/lang-solidity.js +126 -0
- package/src/graph/builder/lang-sql.js +263 -0
- package/src/graph/builder/pass2-resolution.js +15 -2
- package/src/graph/freshness-probe.js +1 -1
- package/src/graph/incremental-refresh.js +1 -1
- package/src/graph/internal-builder.build.js +18 -7
- package/src/graph/internal-builder.langs.js +3 -1
- package/src/graph/internal-builder.pass2.js +4 -2
- package/src/graph/internal-builder.resolvers.js +66 -1
- package/src/graph/parser-artifact-boundary.js +1 -0
- package/src/mcp/actions/graph-lifecycle.mjs +1 -1
- package/src/mcp/architecture-starter.mjs +1 -1
- package/src/mcp/graph/context-seeds.mjs +23 -7
- package/src/precision/typescript-provider/client.js +7 -0
- package/src/security/advisory-store.js +0 -1
- package/src/security/malware-heuristics.exclusions.js +1 -1
- package/src/security/registry-sig.rules.js +1 -1
package/README.md
CHANGED
|
@@ -186,10 +186,18 @@ realpath escapes. Report suspected vulnerabilities privately as described in [SE
|
|
|
186
186
|
|
|
187
187
|
## Languages
|
|
188
188
|
|
|
189
|
-
JavaScript · TypeScript · TSX · Python · Go · Java · C# · Rust · HTML · CSS — parsed with
|
|
189
|
+
JavaScript · TypeScript · TSX · Python · Go · Java · C# · Rust · Solidity · HTML · CSS — parsed with
|
|
190
190
|
[web-tree-sitter](https://github.com/tree-sitter/tree-sitter) WASM grammars; no Python install and no
|
|
191
191
|
native compilation.
|
|
192
192
|
|
|
193
|
+
SQL is indexed without a grammar: `.sql` files contribute tables, views, columns, functions, indexes
|
|
194
|
+
and triggers as first-class graph symbols, and SQL found in string literals of any other language
|
|
195
|
+
links the enclosing function to the table it queries. That makes schema objects visible to
|
|
196
|
+
`change_impact`/`get_dependents` (who touches this table?) and lets the dead-code check flag columns
|
|
197
|
+
no statement references — conservatively: verdicts require literal-SQL evidence in the repo, and
|
|
198
|
+
`SELECT *`-consumed tables never have their columns judged by name (ORM-generated SQL stays invisible
|
|
199
|
+
and is therefore never judged either).
|
|
200
|
+
|
|
193
201
|
Test surfaces are classified per file (path conventions plus `.weavatrix.json` overrides) and, for
|
|
194
202
|
Rust, per symbol: `#[cfg(test)]` modules and `#[test]`/`#[bench]` items inside production `.rs` files
|
|
195
203
|
carry a node-level `test_surface` flag, so dead-code, query, hot-path and hub tools treat them as
|
|
@@ -204,9 +212,9 @@ npm run benchmark # full TS/JS/Python/Go/Java/Rust + MCP lifecycle gate
|
|
|
204
212
|
npm run benchmark:real # locally available real repos vs source-free 0.2.1 baselines
|
|
205
213
|
```
|
|
206
214
|
|
|
207
|
-
Maintained JavaScript/TypeScript under `src`, `bin`, `scripts`, `test`
|
|
208
|
-
|
|
209
|
-
|
|
215
|
+
Maintained JavaScript/TypeScript under `src`, `bin`, `scripts`, `test` has a hard 300-line physical
|
|
216
|
+
ceiling enforced by the release suite; larger concerns split into owner-focused modules behind slim
|
|
217
|
+
facades. The weavatrix.com landing site lives in its own repository (`weavatrix-site`).
|
|
210
218
|
|
|
211
219
|
## Release history
|
|
212
220
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Weavatrix 0.3.11
|
|
2
|
+
|
|
3
|
+
0.3.11 teaches the graph two new surfaces. SQL schemas become first-class graph
|
|
4
|
+
citizens with honest, evidence-gated dead-code verdicts, and Solidity joins the
|
|
5
|
+
parsed languages at zero dependency cost. The weavatrix.com site moved to its
|
|
6
|
+
own repository, so the engine repo now ships engine only.
|
|
7
|
+
|
|
8
|
+
## SQL: the graph reaches the database schema
|
|
9
|
+
|
|
10
|
+
No SQL grammar ships in the pinned tree-sitter-wasms, so `.sql` indexing runs
|
|
11
|
+
on a dependency-free statement scanner (a new `textOnly` language-module class)
|
|
12
|
+
built for the graph's needs — symbols and references, not SQL semantics.
|
|
13
|
+
String literals and comments are blanked before structural scanning, so quoted
|
|
14
|
+
text can never fabricate a reference.
|
|
15
|
+
|
|
16
|
+
- `CREATE TABLE`/`VIEW`/`FUNCTION`/`PROCEDURE`/`INDEX`/`TRIGGER` become graph
|
|
17
|
+
symbols; table columns (including `ALTER TABLE ADD COLUMN`) are member
|
|
18
|
+
symbols with `member_of`, and tables carry their columns as `field_types`.
|
|
19
|
+
- References resolve inside `.sql` files (a view's `SELECT`, an index's `ON`
|
|
20
|
+
table, a trigger's `EXECUTE FUNCTION`, foreign-key `REFERENCES`) — and, the
|
|
21
|
+
real point, from application code: literal SQL found in string literals of
|
|
22
|
+
any indexed language links the enclosing function to the table it queries.
|
|
23
|
+
`change_impact` and `get_dependents` on a table now answer "which code
|
|
24
|
+
touches this?"
|
|
25
|
+
- Dead-code verdicts stay honest. Schema objects are judged only when the
|
|
26
|
+
repository demonstrably uses literal SQL the scanner can read — an
|
|
27
|
+
ORM-driven repo produces silence, not guesses. Columns of a
|
|
28
|
+
`SELECT *`-consumed table are never judged by name, and indexes/triggers
|
|
29
|
+
(DB-engine surface) are never judged at all. A flagged column reports
|
|
30
|
+
"no SQL statement in the indexed sources references it".
|
|
31
|
+
|
|
32
|
+
## Solidity
|
|
33
|
+
|
|
34
|
+
The tree-sitter-solidity grammar already shipped inside the pinned
|
|
35
|
+
tree-sitter-wasms package — 0.3.11 registers it (and pins its SHA-256 in the
|
|
36
|
+
parser-artifact allowlist), so Solidity support adds zero new dependencies.
|
|
37
|
+
Contracts, interfaces, libraries, functions, constructors, modifiers, events,
|
|
38
|
+
errors, structs, enums and state variables are extracted with visibility and
|
|
39
|
+
membership; `is` inheritance, modifier invocations, `emit`, `new Contract()`
|
|
40
|
+
and `using X for Y` all feed the call/heritage resolvers. Imports resolve
|
|
41
|
+
through relative paths, Foundry remappings (`remappings.txt` / `foundry.toml`)
|
|
42
|
+
and root-anchored specs; npm-style specs (`@openzeppelin/...`) are recorded as
|
|
43
|
+
external dependencies. `.sol` files share directory scope like Go/C#/Rust,
|
|
44
|
+
which makes Solidity's plain `import "./Base.sol"` — a statement that names no
|
|
45
|
+
symbols — resolvable for siblings without guessing.
|
|
46
|
+
|
|
47
|
+
`extractorSchemaV` 6 → 7; cached graphs rebuild once, automatically.
|
|
48
|
+
|
|
49
|
+
## Repository
|
|
50
|
+
|
|
51
|
+
The weavatrix.com landing site (pages, Cloudflare Worker config, deploy
|
|
52
|
+
workflow and its asset checks) moved byte-identically to the separate
|
|
53
|
+
`weavatrix-site` repository, removing ~1.2 MB of page assets from the engine
|
|
54
|
+
repo. The MCPB icon the site used to own is vendored at `mcpb/icon.png`; the
|
|
55
|
+
published npm package contents are unchanged.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Weavatrix 0.3.12
|
|
2
|
+
|
|
3
|
+
0.3.12 makes the read-only core the provably-pure base of a layered stack. The
|
|
4
|
+
core stays exactly what it was for its users — the same 34 offline analysis tools
|
|
5
|
+
— but it now exposes a small read-only analysis surface for the packages built on
|
|
6
|
+
top of it, and machine gates prove it can neither modify code nor reach anything
|
|
7
|
+
external. Rust cross-crate call resolution and a `query_graph` seeding fix round
|
|
8
|
+
out the release.
|
|
9
|
+
|
|
10
|
+
## The read-only base of the stack
|
|
11
|
+
|
|
12
|
+
Weavatrix now anchors a stack of growing responsibility:
|
|
13
|
+
`weavatrix-online ⊃ weavatrix-refactor ⊃ weavatrix`. You install one package and
|
|
14
|
+
get everything below it. This release adds only what the base owes the layers
|
|
15
|
+
above; it registers no new tools and changes no existing tool.
|
|
16
|
+
|
|
17
|
+
- New `weavatrix/analysis-kit` export: a narrow, read-only surface (graph
|
|
18
|
+
identity, cycle detection, architecture verification, the bundled tree-sitter
|
|
19
|
+
grammars, the read-only TypeScript language-server client, dead-code signals)
|
|
20
|
+
that upper-layer packages compose. Nothing here writes, applies, or describes
|
|
21
|
+
an edit.
|
|
22
|
+
- Two release gates make the base's guarantees machine-checked, alongside the
|
|
23
|
+
existing ADR 0001 no-network gate: the core source contains **no edit-plan,
|
|
24
|
+
applier, or refactoring-builder marker**, and **no literal URL** at all. A
|
|
25
|
+
vulnerability finding references the advisory id rather than an external link;
|
|
26
|
+
the URL-matching malware heuristics are matchers over scanned code, not links
|
|
27
|
+
the core follows. Installing the base alone can neither modify your code nor
|
|
28
|
+
reach the network.
|
|
29
|
+
|
|
30
|
+
## Rust cross-crate call resolution
|
|
31
|
+
|
|
32
|
+
Rust call edges now resolve across crates, inline-test coverage is reported
|
|
33
|
+
honestly, and same-crate call noise is reduced — cross-crate `references` edges
|
|
34
|
+
land where a caller reaches a definition in another crate.
|
|
35
|
+
|
|
36
|
+
## query_graph seeding
|
|
37
|
+
|
|
38
|
+
A `query_graph` question that reduced entirely to stop words (a bare
|
|
39
|
+
`architecture`), or inferred a language absent from the repository (`contract`
|
|
40
|
+
inferring Solidity where there are no `.sol` files), previously returned a
|
|
41
|
+
misleading "No nodes matched". Both now fall back only when they would otherwise
|
|
42
|
+
return nothing: the stop filter relaxes when every token is a stop word, and a
|
|
43
|
+
requested language that matches zero nodes is dropped. Queries that already
|
|
44
|
+
worked are unchanged.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weavatrix",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local repository intelligence MCP for AI coding agents: an always-fresh architecture graph with production-first evidence — blast radius, dead code, endpoints, clones, history, Health and architecture safeguards, with honestly labeled precision.",
|
|
6
6
|
"author": "Sergii Ziborov <sergii.ziborov@gmail.com>",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"./mcp-runtime": "./src/mcp-runtime.mjs",
|
|
25
25
|
"./extension-api": "./src/mcp/extension-api.mjs",
|
|
26
26
|
"./extension/local-services": "./src/extension/local-services.mjs",
|
|
27
|
+
"./analysis-kit": "./src/analysis-kit.mjs",
|
|
27
28
|
"./package.json": "./package.json"
|
|
28
29
|
},
|
|
29
30
|
"files": [
|
|
@@ -44,6 +45,8 @@
|
|
|
44
45
|
"docs/releases/v0.3.8.md",
|
|
45
46
|
"docs/releases/v0.3.9.md",
|
|
46
47
|
"docs/releases/v0.3.10.md",
|
|
48
|
+
"docs/releases/v0.3.11.md",
|
|
49
|
+
"docs/releases/v0.3.12.md",
|
|
47
50
|
"docs/releases/v0.2.19.md",
|
|
48
51
|
"README.md",
|
|
49
52
|
"SECURITY.md",
|
|
@@ -19,6 +19,13 @@ export function parseCargoToml(text) {
|
|
|
19
19
|
for (const raw of source.split(/\r?\n/)) {
|
|
20
20
|
const line = raw.replace(/(^|\s)#.*$/, "").trim();
|
|
21
21
|
if (!line) continue;
|
|
22
|
+
// Array-of-tables headers ([[bin]], [[example]], [[test]], [[bench]]) are neither the [package]
|
|
23
|
+
// table nor a dependency table. Recognise them so their `name =`/`path =` keys are not mis-read:
|
|
24
|
+
// a [[bin]] renamed to a crate ([[bin]] name = "foo") would otherwise overwrite the package name and
|
|
25
|
+
// make every foo:: import look like a self-reference, and a [[bin]] after [dependencies] would leak
|
|
26
|
+
// phantom "name"/"path" dependencies.
|
|
27
|
+
const arrayHeader = /^\[\[([^\]]+)]]$/.exec(line);
|
|
28
|
+
if (arrayHeader) { section = arrayHeader[1].trim(); continue; }
|
|
22
29
|
const header = /^\[([^\]]+)]$/.exec(line);
|
|
23
30
|
if (header) { section = header[1].trim(); workspace ||= section === "workspace"; continue; }
|
|
24
31
|
const kv = /^([A-Za-z0-9_-]+)\s*=\s*(.+)$/.exec(line);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// and is fully testable with no filesystem. It only needs {nodes, links} + source text. See [[graph-builder-internalization]].
|
|
7
7
|
import { posix } from "node:path";
|
|
8
8
|
import { isStructuralRelation } from "../graph/relations.js";
|
|
9
|
+
import { createSqlDeadVerdict } from "../graph/builder/lang-sql.js";
|
|
9
10
|
import { createPathClassifier, hasPathClass } from "../path-classification.js";
|
|
10
11
|
|
|
11
12
|
const IDENT_RE = /[A-Za-z_$][\w$]*/g;
|
|
@@ -198,12 +199,11 @@ export function computeDead(graph, sources, { entrySet = new Set() } = {}) {
|
|
|
198
199
|
return isDecorated(n); // framework-registered via decorator
|
|
199
200
|
};
|
|
200
201
|
|
|
202
|
+
const sqlVerdict = createSqlDeadVerdict({ nodes, links, ep, bareName }); // gated SQL verdicts — ORM blindness stays silent
|
|
201
203
|
const deadSymbols = [];
|
|
202
204
|
for (const n of symById.values()) {
|
|
203
|
-
if (isReferenced(n)) continue;
|
|
204
|
-
|
|
205
|
-
const test = isTestFile(n.source_file) || n.test_surface === true;
|
|
206
|
-
deadSymbols.push({ id: n.id, file: n.source_file, label: n.label, test, reason: "no inbound edge and name unreferenced outside its file" });
|
|
205
|
+
if (isReferenced(n) || sqlVerdict.veto(n)) continue;
|
|
206
|
+
deadSymbols.push({ id: n.id, file: n.source_file, label: n.label, test: isTestFile(n.source_file) || n.test_surface === true, reason: sqlVerdict.reason(n) || "no inbound edge and name unreferenced outside its file" });
|
|
207
207
|
}
|
|
208
208
|
const deadSet = new Set(deadSymbols.map((s) => s.id));
|
|
209
209
|
|
|
@@ -111,7 +111,7 @@ function symbolRanges(graph) {
|
|
|
111
111
|
if (!line) continue;
|
|
112
112
|
let arr = byFile.get(n.source_file);
|
|
113
113
|
if (!arr) byFile.set(n.source_file, (arr = []));
|
|
114
|
-
arr.push({ id: n.id, label: n.label || n.id, line, symbolKind: n.symbol_kind || "" });
|
|
114
|
+
arr.push({ id: n.id, label: n.label || n.id, line, symbolKind: n.symbol_kind || "", testSurface: n.test_surface === true });
|
|
115
115
|
}
|
|
116
116
|
for (const arr of byFile.values()) arr.sort((a, b) => a.line - b.line);
|
|
117
117
|
return byFile;
|
|
@@ -184,6 +184,10 @@ export function computeDuplicates(repoPath, graphJsonPath, opts = {}) {
|
|
|
184
184
|
id: syms[i].id, label: syms[i].label, file, start, end,
|
|
185
185
|
n: toks.strict.length, declarative,
|
|
186
186
|
...classificationFields(pathInfo),
|
|
187
|
+
// Inline test surfaces (Rust `#[cfg(test)] mod tests`, `#[test]` fns) live in production files, so a
|
|
188
|
+
// path check alone misses them; the graph already flags the symbol. OR it in so skip-tests suppresses
|
|
189
|
+
// them by default and two #[test] fns are never reported as a production clone.
|
|
190
|
+
test: hasPathClass(pathInfo, "test", "e2e") || syms[i].testSurface === true,
|
|
187
191
|
fp: { strict: fingerprints(toks.strict), renamed: fingerprints(toks.renamed) },
|
|
188
192
|
});
|
|
189
193
|
}
|
|
@@ -36,7 +36,7 @@ function coverageForRange(record, startLine, endLine) {
|
|
|
36
36
|
return total ? covered / total : Number.isFinite(record.pct) ? record.pct : null
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function graphRiskBySymbol(graph, symbolIds) {
|
|
39
|
+
function graphRiskBySymbol(graph, symbolIds, testCallerIds) {
|
|
40
40
|
const state = new Map([...symbolIds].map((id) => [id, {
|
|
41
41
|
incoming: 0, outgoing: 0, callers: new Set(), callees: new Set(),
|
|
42
42
|
}]))
|
|
@@ -50,7 +50,10 @@ function graphRiskBySymbol(graph, symbolIds) {
|
|
|
50
50
|
item.outgoing++
|
|
51
51
|
item.callees.add(target)
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
// A test caller (inline #[cfg(test)] symbol or a test-path file) must not inflate a production
|
|
54
|
+
// symbol's fan-in — the same reason test symbols are excluded as candidates. Skipped only for the
|
|
55
|
+
// incoming/coupling side; unless the caller opted tests in (testCallerIds is then null).
|
|
56
|
+
if (state.has(target) && !(testCallerIds && testCallerIds.has(source))) {
|
|
54
57
|
const item = state.get(target)
|
|
55
58
|
item.incoming++
|
|
56
59
|
item.callers.add(source)
|
|
@@ -135,7 +138,26 @@ export function computeHotPathReview(graph, options = {}) {
|
|
|
135
138
|
eligible.push({node, complexity, file, classification: test ? 'test' : classified ? 'classified' : 'production'})
|
|
136
139
|
}
|
|
137
140
|
|
|
138
|
-
|
|
141
|
+
// Test callers (inline test_surface symbols and test-path files) that must not count toward a
|
|
142
|
+
// production symbol's fan-in. Built over ALL nodes because a caller may sit outside the review scope.
|
|
143
|
+
const testCallerIds = options.includeTests === true ? null : (() => {
|
|
144
|
+
const ids = new Set()
|
|
145
|
+
const classifyCache = new Map()
|
|
146
|
+
for (const node of graph?.nodes || []) {
|
|
147
|
+
const id = String(node?.id || '')
|
|
148
|
+
if (!id.includes('#')) continue
|
|
149
|
+
let isTest = node?.test_surface === true
|
|
150
|
+
const file = normalize(node?.source_file)
|
|
151
|
+
if (!isTest && file) {
|
|
152
|
+
let info = classifyCache.get(file)
|
|
153
|
+
if (info === undefined) classifyCache.set(file, (info = classifier.explain(file)))
|
|
154
|
+
isTest = hasPathClass(info, 'test', 'e2e')
|
|
155
|
+
}
|
|
156
|
+
if (isTest) ids.add(id)
|
|
157
|
+
}
|
|
158
|
+
return ids
|
|
159
|
+
})()
|
|
160
|
+
const riskById = graphRiskBySymbol(graph, new Set(eligible.map(({node}) => String(node.id))), testCallerIds)
|
|
139
161
|
const candidates = []
|
|
140
162
|
for (const entry of eligible) {
|
|
141
163
|
const {node, complexity, file} = entry
|
|
@@ -74,7 +74,7 @@ export async function runSupplyChainChecks(repoPath, {
|
|
|
74
74
|
version: hit.pkg.version,
|
|
75
75
|
reachability,
|
|
76
76
|
evidence: [
|
|
77
|
-
{ file: hit.adv.
|
|
77
|
+
{ file: hit.adv.id, line: 0, snippet: `installed via ${hit.pkg.source}${hit.pkg.dev ? " (dev)" : ""}` },
|
|
78
78
|
...reachability.evidence.slice(0, 5).map((item) => ({ file: item.file, line: item.line, snippet: `${item.typeOnly ? "type-only " : ""}${item.kind}` })),
|
|
79
79
|
],
|
|
80
80
|
source: "osv",
|
|
@@ -107,7 +107,10 @@ export function parsePipfileDeps(text) {
|
|
|
107
107
|
let present = false;
|
|
108
108
|
for (const raw of String(text || "").split(/\r?\n/)) {
|
|
109
109
|
const line = raw.replace(/(^|\s)#.*$/, "").trim();
|
|
110
|
-
|
|
110
|
+
// Match single [section] and array-of-tables [[section]] headers alike. Without the [[...]] case a
|
|
111
|
+
// `[[source]]` table following [packages] would leave section === "packages" and its name/url/verify_ssl
|
|
112
|
+
// keys would leak as phantom dependencies (the same blind spot fixed in the Cargo.toml parser).
|
|
113
|
+
const sec = line.match(/^\[+([^\]]+)\]+$/);
|
|
111
114
|
if (sec) { section = sec[1].trim().toLowerCase(); continue; }
|
|
112
115
|
if (section !== "packages" && section !== "dev-packages") continue;
|
|
113
116
|
const kv = line.match(/^["']?([A-Za-z0-9][\w.-]*)["']?\s*=/);
|
|
@@ -37,6 +37,13 @@ export function computeStaticTestReachability(graph, {
|
|
|
37
37
|
if (!classificationCache.has(normalized)) classificationCache.set(normalized, classifier.explain(normalized, { content: "" }));
|
|
38
38
|
return classificationCache.get(normalized);
|
|
39
39
|
};
|
|
40
|
+
// Files that carry inline test surfaces (Rust `#[cfg(test)]` modules / `#[test]` fns live inside the
|
|
41
|
+
// production file, so path classification alone never sees them). The graph flags these per symbol.
|
|
42
|
+
// These files are NOT seeded as whole-file BFS sources: at file granularity we cannot tell an inline
|
|
43
|
+
// test's out-edge from a same-file production call, so seeding the file would propagate its PRODUCTION
|
|
44
|
+
// call chains as if a test reached them. They are self-covered at distance 0 instead (an honest,
|
|
45
|
+
// conservative signal: "this file has its own tests"), never over-claiming reach to other files.
|
|
46
|
+
const testSurfaceFiles = new Set();
|
|
40
47
|
const isTest = (file) => hasPathClass(classify(file), "test", "e2e");
|
|
41
48
|
const isProduct = (file) => {
|
|
42
49
|
const info = classify(file);
|
|
@@ -50,6 +57,7 @@ export function computeStaticTestReachability(graph, {
|
|
|
50
57
|
if (!file) continue;
|
|
51
58
|
idToFile.set(String(node.id), file);
|
|
52
59
|
files.add(file);
|
|
60
|
+
if (node?.test_surface === true) testSurfaceFiles.add(file);
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
// Collapse symbol/file edges to directed file dependencies. Multiple symbol edges retain the best
|
|
@@ -73,6 +81,13 @@ export function computeStaticTestReachability(graph, {
|
|
|
73
81
|
const productFiles = [...files].filter(isProduct).filter((file) => pathInScope(file, scope)).sort((a, b) => a.localeCompare(b));
|
|
74
82
|
const productSet = new Set(productFiles);
|
|
75
83
|
const nearest = new Map();
|
|
84
|
+
// A production file that carries its own inline tests is self-covered at distance 0: its tests exercise
|
|
85
|
+
// its own production symbols. This is the honest floor that fixes a false "0 reachable" for repos whose
|
|
86
|
+
// tests are all inline (Rust); it never asserts reach to OTHER files, so it cannot over-claim.
|
|
87
|
+
const inlineCovered = productFiles.filter((file) => testSurfaceFiles.has(file));
|
|
88
|
+
for (const file of inlineCovered) {
|
|
89
|
+
nearest.set(file, [{ test: file, distance: 0, score: 3, confidence: confidenceLabel(3), path: [file] }]);
|
|
90
|
+
}
|
|
76
91
|
const queue = tests.map((test) => ({ file: test, test, distance: 0, score: 3, path: [test] }));
|
|
77
92
|
const seen = new Map(tests.map((test) => [`${test}\0${test}`, 0]));
|
|
78
93
|
let cursor = 0;
|
|
@@ -115,8 +130,10 @@ export function computeStaticTestReachability(graph, {
|
|
|
115
130
|
return {
|
|
116
131
|
kind: "staticTestReachability",
|
|
117
132
|
actualCoverage: "NOT_AVAILABLE",
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
// Inline-test files self-cover rather than seed the traversal, but they ARE test-bearing files, so
|
|
134
|
+
// the reported counts include them — otherwise a Rust repo would read "0 test files" beside real reach.
|
|
135
|
+
testFiles: tests.length + inlineCovered.length,
|
|
136
|
+
totalTestFiles: allTests.length + inlineCovered.length,
|
|
120
137
|
productFiles: productFiles.length,
|
|
121
138
|
reachableFiles: reachable.length,
|
|
122
139
|
unreachableFiles: unreachable.length,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Read-only analysis primitives the weavatrix-refactor package composes into refactoring
|
|
2
|
+
// plan producers. Everything exported here is PURE READ: graph identity helpers, cycle
|
|
3
|
+
// detection, architecture verification, the bundled tree-sitter grammars, the read-only
|
|
4
|
+
// TypeScript language-server client, and dead-code risk signals. None of it writes, applies,
|
|
5
|
+
// or references applying a change — the core catalog and the release gate keep the core
|
|
6
|
+
// provably free of any source-write or edit-plan path. The refactoring engines live in
|
|
7
|
+
// weavatrix-refactor and import this surface.
|
|
8
|
+
|
|
9
|
+
export {graphEndpointId, fileOfId} from './graph/node-id.js'
|
|
10
|
+
export {Parser, Query, EXT_LANG, ensureParser} from './graph/internal-builder.langs.js'
|
|
11
|
+
export {querySymbolPrecision} from './precision/symbol-query.js'
|
|
12
|
+
export {createTypeScriptLspClient} from './precision/typescript-provider/client.js'
|
|
13
|
+
export {verifyArchitecture} from './analysis/architecture/contract-verification.js'
|
|
14
|
+
export {normalizeArchitectureContract} from './analysis/architecture-contract.js'
|
|
15
|
+
export {buildFileImportGraph, findSccs} from './analysis/structure/dependency-graph.js'
|
|
16
|
+
export {isFrameworkEntryFile} from './analysis/dead-check.js'
|
|
17
|
+
export {hasDynamicCode, REFLECTION_CODE_RE} from './analysis/dead-code-review/policy.js'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Rust scoped-call resolution (`Path::method()`), split from lang-rust.js. Resolved with the full
|
|
2
|
+
// qualifier in hand so an external associated function (`OpenOptions::new()`, `Vec::from()`) never binds
|
|
3
|
+
// its bare final name to an unrelated same-named function in the directory. Only heads with real evidence
|
|
4
|
+
// produce an edge: crate/self/super anchors, `use`-bound names (including sibling-crate modules),
|
|
5
|
+
// fully-qualified sibling crates, and local types whose exact impl method is indexed. A std/external
|
|
6
|
+
// type head is left unresolved on purpose.
|
|
7
|
+
|
|
8
|
+
// Symbol kinds that can own an associated function via `Type::method`.
|
|
9
|
+
const TYPE_KINDS = new Set(["struct", "enum", "trait", "type", "union"]);
|
|
10
|
+
|
|
11
|
+
// `pathParts` and `inlineAncestors` stay owned by lang-rust.js (pass 1 uses them too) and are passed in,
|
|
12
|
+
// so this module never imports back from lang-rust.js.
|
|
13
|
+
export function rustScopedCalls(ctx, { pathParts, inlineAncestors }) {
|
|
14
|
+
const { grammar, tree, fileRel, caps, enclosing, links, nodeById, importedLocals,
|
|
15
|
+
dirSymbols, resolveNamedSymbol, resolveRustMethod, resolveRustPath, resolveRustCratePath } = ctx;
|
|
16
|
+
const dir = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : "";
|
|
17
|
+
const localSyms = dirSymbols?.get(dir);
|
|
18
|
+
const emitCall = (node, target) => {
|
|
19
|
+
const caller = enclosing(fileRel, node.startPosition.row + 1);
|
|
20
|
+
if (caller && target && target !== caller.id) {
|
|
21
|
+
links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: node.startPosition.row + 1 });
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
for (const cap of caps(grammar, `(call_expression function: (scoped_identifier) @path)`, tree.rootNode)) {
|
|
25
|
+
const node = cap.node;
|
|
26
|
+
const segments = pathParts(node);
|
|
27
|
+
if (segments.length < 2) continue;
|
|
28
|
+
const head = segments[0];
|
|
29
|
+
const method = segments.at(-1);
|
|
30
|
+
|
|
31
|
+
if (["crate", "self", "super"].includes(head)) {
|
|
32
|
+
const resolved = resolveRustPath(fileRel, segments, { inlineModules: inlineAncestors(node), unqualified: false });
|
|
33
|
+
if (resolved) emitCall(node, resolveNamedSymbol(resolved.targetFile, method, "value"));
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const bound = importedLocals?.get(fileRel)?.get(head);
|
|
38
|
+
if (bound?.targetFile) {
|
|
39
|
+
emitCall(node, resolveNamedSymbol(bound.targetFile, method, "value"));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const crateResolved = resolveRustCratePath ? resolveRustCratePath(head, segments.slice(1, -1)) : null;
|
|
44
|
+
if (crateResolved) {
|
|
45
|
+
emitCall(node, resolveNamedSymbol(crateResolved.targetFile, method, "value"));
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Local type's associated function: resolve the exact impl member. Nothing else (enum-variant
|
|
50
|
+
// construction, external types) has an indexed target, so it produces no edge.
|
|
51
|
+
const headId = localSyms?.get(head);
|
|
52
|
+
if (headId && TYPE_KINDS.has(nodeById.get(headId)?.symbol_kind)) {
|
|
53
|
+
emitCall(node, resolveRustMethod(dir, head, method));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Head is a locally-declared outlined module used without `use` (`mod utils; utils::init()`), the
|
|
58
|
+
// dominant flat-file idiom. resolveRustPath matches only a REAL repo module file, so an external type
|
|
59
|
+
// head (OpenOptions, Vec) resolves to null and still produces no edge — no dir-scope mis-binding.
|
|
60
|
+
const modResolved = resolveRustPath?.(fileRel, segments.slice(0, -1), { inlineModules: inlineAncestors(node), unqualified: true });
|
|
61
|
+
if (modResolved) emitCall(node, resolveNamedSymbol(modResolved.targetFile, method, "value"));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// File dependencies follow Rust's module tree: outlined `mod`, `use` trees, public re-exports, and anchored
|
|
4
4
|
// `crate/self/super` paths resolve to repo-local .rs or */mod.rs files. External crates deliberately stay out
|
|
5
5
|
// of this adapter; Cargo dependency analysis owns them.
|
|
6
|
+
import { rustScopedCalls } from "./lang-rust-calls.js";
|
|
7
|
+
|
|
6
8
|
const SYMS_CORE = `
|
|
7
9
|
(function_item name: (identifier) @function)
|
|
8
10
|
(struct_item name: (type_identifier) @struct)
|
|
@@ -141,17 +143,19 @@ export default {
|
|
|
141
143
|
grammars: ["rust"],
|
|
142
144
|
exts: { ".rs": "rust" },
|
|
143
145
|
isWeb: false,
|
|
146
|
+
// Bare `foo()` and method `x.foo()` calls resolve by name in the generic pass-2 loop. Scoped
|
|
147
|
+
// `Path::foo()` calls are handled in pass2() below with the full qualifier in hand, so an external
|
|
148
|
+
// `OpenOptions::new()` cannot bind its bare `new` to an unrelated same-named function in the folder.
|
|
144
149
|
calls: `
|
|
145
150
|
(call_expression function: (identifier) @callee)
|
|
146
|
-
(call_expression function: (field_expression field: (field_identifier) @callee))
|
|
147
|
-
(call_expression function: (scoped_identifier name: (identifier) @callee))`,
|
|
151
|
+
(call_expression function: (field_expression field: (field_identifier) @callee))`,
|
|
148
152
|
heritage: [
|
|
149
153
|
`(impl_item trait: (type_identifier) @super)`,
|
|
150
154
|
`(impl_item trait: (generic_type type: (type_identifier) @super))`,
|
|
151
155
|
],
|
|
152
156
|
|
|
153
157
|
pass1(ctx) {
|
|
154
|
-
const { grammar, tree, fileRel, caps, field, addSym, addImportEdge, addExternalImport, imports, resolveRustMod, resolveRustPath, links, nameToId } = ctx;
|
|
158
|
+
const { grammar, tree, fileRel, caps, field, addSym, addImportEdge, addExternalImport, imports, resolveRustMod, resolveRustPath, resolveRustCratePath, links, nameToId } = ctx;
|
|
155
159
|
const owned = [];
|
|
156
160
|
for (const src of [SYMS_CORE, ...SYMS_OPTIONAL]) {
|
|
157
161
|
for (const cap of caps(grammar, src, tree.rootNode)) {
|
|
@@ -224,9 +228,16 @@ export default {
|
|
|
224
228
|
// A `use` alias or imported item name shadows any same-named crate for the rest of this file. The
|
|
225
229
|
// crate's OWN root name (e.g. `use anyhow::{self}`) is excluded so it still counts as a dependency.
|
|
226
230
|
if (leaf.local && leaf.local !== "_" && cleanSegment(leaf.local) !== cleanSegment(leaf.segments[0])) localBindings.add(cleanSegment(leaf.local));
|
|
227
|
-
const
|
|
231
|
+
const crate = cleanSegment(leaf.segments[0]);
|
|
232
|
+
let resolved = resolveRustPath(fileRel, leaf.segments, { inlineModules: ancestors, unqualified: true });
|
|
233
|
+
// A head that is not a local module may be a sibling workspace crate (`use radiochron::wlan`):
|
|
234
|
+
// resolve it INTO that crate so its items bind here and cross-crate calls resolve in pass 2. It is
|
|
235
|
+
// still recorded as a dependency import so the sibling crate is not flagged an unused Cargo dep.
|
|
236
|
+
if (!resolved && resolveRustCratePath && isExternalCrate(crate, localBindings)) {
|
|
237
|
+
resolved = resolveRustCratePath(crate, leaf.segments.slice(1));
|
|
238
|
+
if (resolved) addExternalImport({ spec: leaf.segments.join("::"), pkg: crate.replace(/_/g, "-"), builtin: false, ecosystem: "crates.io", kind: "rust-use", line: use.startPosition.row + 1 });
|
|
239
|
+
}
|
|
228
240
|
if (!resolved) {
|
|
229
|
-
const crate = cleanSegment(leaf.segments[0]);
|
|
230
241
|
if (isExternalCrate(crate, localBindings)) {
|
|
231
242
|
addExternalImport({ spec: leaf.segments.join("::"), pkg: crate.replace(/_/g, "-"), builtin: false, ecosystem: "crates.io", kind: "rust-use", line: use.startPosition.row + 1 });
|
|
232
243
|
}
|
|
@@ -250,7 +261,16 @@ export default {
|
|
|
250
261
|
const segments = pathParts(node);
|
|
251
262
|
if (!["crate", "self", "super"].includes(segments[0])) {
|
|
252
263
|
const crate = cleanSegment(segments[0]);
|
|
253
|
-
|
|
264
|
+
// A fully-qualified sibling-crate path (`radiochron::events::recent`) is a real cross-crate
|
|
265
|
+
// dependency: emit the file edge AND record the dependency import so the sibling crate still
|
|
266
|
+
// counts as used, rather than being logged as an external crates.io crate.
|
|
267
|
+
const crateResolved = resolveRustCratePath && isExternalCrate(crate, localBindings)
|
|
268
|
+
? resolveRustCratePath(crate, segments.slice(1))
|
|
269
|
+
: null;
|
|
270
|
+
if (crateResolved) {
|
|
271
|
+
emit(crateResolved.targetFile, { line: node.startPosition.row + 1, specifier: node.text });
|
|
272
|
+
addExternalImport({ spec: segments.join("::"), pkg: crate.replace(/_/g, "-"), builtin: false, ecosystem: "crates.io", kind: "rust-path", line: node.startPosition.row + 1 });
|
|
273
|
+
} else if (isExternalCrate(crate, localBindings)) {
|
|
254
274
|
addExternalImport({ spec: segments.join("::"), pkg: crate.replace(/_/g, "-"), builtin: false, ecosystem: "crates.io", kind: "rust-path", line: node.startPosition.row + 1 });
|
|
255
275
|
}
|
|
256
276
|
continue;
|
|
@@ -271,4 +291,9 @@ export default {
|
|
|
271
291
|
}
|
|
272
292
|
}
|
|
273
293
|
},
|
|
294
|
+
|
|
295
|
+
// Scoped `Path::method()` calls are resolved in lang-rust-calls.js with the full qualifier in hand.
|
|
296
|
+
pass2(ctx) {
|
|
297
|
+
rustScopedCalls(ctx, { pathParts, inlineAncestors });
|
|
298
|
+
},
|
|
274
299
|
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Solidity extractor. Symbols: contract/interface/library (space "both" so `is Base` heritage and
|
|
2
|
+
// `new Vault()` both resolve through the value-space resolver), functions/constructors/modifiers/
|
|
3
|
+
// events/errors/structs/enums/state vars (members carry memberOf + visibility). Imports: relative
|
|
4
|
+
// paths, Foundry remappings (remappings.txt / foundry.toml), and root-anchored specs resolve to repo
|
|
5
|
+
// files; everything else (npm-style @openzeppelin/…, absent lib/ submodules) is RECORDED as an
|
|
6
|
+
// external import for dependency analysis. Calls: bare `f()`, modifier invocations, `emit Event(…)`,
|
|
7
|
+
// `new Contract(…)`, and `using Lib for T` all feed the calls resolver. Same-dir symbols share scope
|
|
8
|
+
// (see sharesDirScope): Solidity's flat project namespace makes plain `import "./Base.sol"` — which
|
|
9
|
+
// names no symbols — resolvable for siblings without guessing; cross-dir plain imports still produce
|
|
10
|
+
// the file-level import edge, so blast radius stays correct even where symbol edges are unknowable.
|
|
11
|
+
import { specToPkg } from "./spec-pkg.js";
|
|
12
|
+
|
|
13
|
+
const CONTAINERS = new Set(["contract_declaration", "interface_declaration", "library_declaration"]);
|
|
14
|
+
const KIND_BY_CONTAINER = {
|
|
15
|
+
contract_declaration: "contract",
|
|
16
|
+
interface_declaration: "interface",
|
|
17
|
+
library_declaration: "library",
|
|
18
|
+
};
|
|
19
|
+
// external/public functions are the deployed ABI — callable by any transaction, not just repo code.
|
|
20
|
+
const VISIBILITY = { external: "public", public: "public", internal: "protected", private: "private" };
|
|
21
|
+
|
|
22
|
+
function enclosingContainer(node) {
|
|
23
|
+
for (let parent = node?.parent; parent; parent = parent.parent) {
|
|
24
|
+
if (CONTAINERS.has(parent.type)) return parent;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const identifierOf = (node) => node?.namedChildren?.find((child) => child.type === "identifier") || null;
|
|
30
|
+
const childOfType = (node, type) => node?.namedChildren?.find((child) => child.type === type) || null;
|
|
31
|
+
const parameterCount = (node) => (node?.namedChildren || []).filter((child) => child.type === "parameter").length;
|
|
32
|
+
|
|
33
|
+
export default {
|
|
34
|
+
family: "solidity",
|
|
35
|
+
grammars: ["solidity"],
|
|
36
|
+
exts: { ".sol": "solidity" },
|
|
37
|
+
isWeb: false,
|
|
38
|
+
calls: `[(call_expression (identifier) @callee) (modifier_invocation (identifier) @callee) (emit_statement (identifier) @callee) (new_expression (type_name (user_defined_type (identifier) @callee))) (using_directive (type_alias (identifier) @callee))]`,
|
|
39
|
+
heritage: [`(inheritance_specifier (user_defined_type (identifier) @base))`],
|
|
40
|
+
|
|
41
|
+
pass1(ctx) {
|
|
42
|
+
const { grammar, tree, fileRel, caps, addSym, addImportEdge, addExternalImport, imports, links, nameToId, resolveSolidityImport } = ctx;
|
|
43
|
+
|
|
44
|
+
// ---- containers (contract/interface/library) ----
|
|
45
|
+
for (const cap of caps(grammar, `[(contract_declaration (identifier) @c) (interface_declaration (identifier) @c) (library_declaration (identifier) @c)]`, tree.rootNode)) {
|
|
46
|
+
addSym(cap.node.text, cap.node.startPosition.row + 1, false, {
|
|
47
|
+
sourceNode: cap.node.parent,
|
|
48
|
+
selectionNode: cap.node,
|
|
49
|
+
symbolKind: KIND_BY_CONTAINER[cap.node.parent.type],
|
|
50
|
+
symbolSpace: "both",
|
|
51
|
+
exported: true,
|
|
52
|
+
moduleDeclaration: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- functions (free + members), constructors, modifiers ----
|
|
57
|
+
const ownedMembers = [];
|
|
58
|
+
const addMember = (name, node, selection, extra) => {
|
|
59
|
+
const container = enclosingContainer(node);
|
|
60
|
+
const ownerName = container ? identifierOf(container)?.text || null : null;
|
|
61
|
+
const id = addSym(name, (selection || node).startPosition.row + 1, extra.callable !== false, {
|
|
62
|
+
sourceNode: node,
|
|
63
|
+
selectionNode: selection || undefined,
|
|
64
|
+
...(ownerName ? { memberOf: ownerName } : { exported: true, moduleDeclaration: true }),
|
|
65
|
+
...extra,
|
|
66
|
+
});
|
|
67
|
+
if (ownerName && id) ownedMembers.push({ ownerName, id });
|
|
68
|
+
return id;
|
|
69
|
+
};
|
|
70
|
+
for (const cap of caps(grammar, `(function_definition (identifier) @fn)`, tree.rootNode)) {
|
|
71
|
+
const definition = cap.node.parent;
|
|
72
|
+
const visibility = VISIBILITY[childOfType(definition, "visibility")?.text || ""];
|
|
73
|
+
addMember(cap.node.text, definition, cap.node, {
|
|
74
|
+
symbolKind: enclosingContainer(definition) ? "method" : "function",
|
|
75
|
+
...(visibility ? { visibility } : {}),
|
|
76
|
+
parameterCount: parameterCount(definition),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
for (const cap of caps(grammar, `(constructor_definition) @ctor`, tree.rootNode))
|
|
80
|
+
addMember("constructor", cap.node, null, { symbolKind: "constructor", parameterCount: parameterCount(cap.node) });
|
|
81
|
+
for (const cap of caps(grammar, `(modifier_definition (identifier) @m)`, tree.rootNode))
|
|
82
|
+
addMember(cap.node.text, cap.node.parent, cap.node, { symbolKind: "modifier", parameterCount: parameterCount(cap.node.parent) });
|
|
83
|
+
|
|
84
|
+
// ---- data/type declarations ----
|
|
85
|
+
for (const cap of caps(grammar, `[(event_definition (identifier) @n) (error_declaration (identifier) @n)]`, tree.rootNode))
|
|
86
|
+
addMember(cap.node.text, cap.node.parent, cap.node, { callable: false, symbolKind: cap.node.parent.type === "event_definition" ? "event" : "error" });
|
|
87
|
+
for (const cap of caps(grammar, `[(struct_declaration (identifier) @n) (enum_declaration (identifier) @n)]`, tree.rootNode))
|
|
88
|
+
addMember(cap.node.text, cap.node.parent, cap.node, { callable: false, symbolKind: cap.node.parent.type === "struct_declaration" ? "struct" : "enum", symbolSpace: "both" });
|
|
89
|
+
// capture the DECLARATION and take its first identifier child (the name): an initializer such as
|
|
90
|
+
// `uint x = OTHER_CONST;` can place a second bare identifier under the same declaration node.
|
|
91
|
+
for (const cap of caps(grammar, `(constant_variable_declaration) @d`, tree.rootNode)) {
|
|
92
|
+
const name = identifierOf(cap.node);
|
|
93
|
+
if (name) addMember(name.text, cap.node, name, { callable: false, symbolKind: "constant" });
|
|
94
|
+
}
|
|
95
|
+
for (const cap of caps(grammar, `(state_variable_declaration) @d`, tree.rootNode)) {
|
|
96
|
+
const name = identifierOf(cap.node);
|
|
97
|
+
const visibility = VISIBILITY[childOfType(cap.node, "visibility")?.text || ""] || "protected"; // Solidity default: internal
|
|
98
|
+
if (name) addMember(name.text, cap.node, name, { callable: false, symbolKind: "variable", visibility });
|
|
99
|
+
}
|
|
100
|
+
for (const member of ownedMembers) {
|
|
101
|
+
const ownerId = nameToId.get(member.ownerName);
|
|
102
|
+
if (ownerId) links.push({ source: ownerId, target: member.id, relation: "contains", confidence: "EXTRACTED" });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---- imports ----
|
|
106
|
+
for (const cap of caps(grammar, `(import_directive) @imp`, tree.rootNode)) {
|
|
107
|
+
const specNode = childOfType(cap.node, "string");
|
|
108
|
+
if (!specNode) continue;
|
|
109
|
+
const spec = specNode.text.replace(/^["']|["']$/g, "");
|
|
110
|
+
const line = specNode.startPosition.row + 1;
|
|
111
|
+
const target = resolveSolidityImport(fileRel, spec);
|
|
112
|
+
if (target) {
|
|
113
|
+
addImportEdge(target, { specifier: spec, line });
|
|
114
|
+
// `import {A, B} from "./x.sol"` — bind each named symbol. A lone identifier may instead be a
|
|
115
|
+
// `* as Alias` namespace name; binding it the same way is harmless (no same-named symbol → no edge).
|
|
116
|
+
for (const named of cap.node.namedChildren.filter((child) => child.type === "identifier"))
|
|
117
|
+
imports.set(named.text, { imported: named.text, targetFile: target });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (spec.startsWith(".")) { addExternalImport({ spec, kind: "sol-import", line, unresolved: true }); continue; }
|
|
121
|
+
const r = specToPkg(spec);
|
|
122
|
+
if (r) addExternalImport({ spec, pkg: r.pkg, builtin: false, kind: "sol-import", line });
|
|
123
|
+
else addExternalImport({ spec, kind: "sol-import", line, unresolved: true });
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
};
|