-
Notifications
You must be signed in to change notification settings - Fork 0
Language Analysis
How PolyGraph turns source files into a GraphModel — and how to teach it a new language. This is the provider boundary seam from the Architecture Overview.
lib/kernel/ is decoupled from the UI behind one function:
analyzeProject(files: SourceFileMap, options?) → AnalyzeResult // lib/kernel/index.ts
What it does:
- Bucket files by extension and route each bucket to the provider that claims it.
-
Run providers concurrently (
Promise.all). The TS/JS provider is synchronous; tree-sitter providers are async (the native core returns aPromise). - Merge the providers' fragments: de-duplicate nodes/edges by id, and drop edges whose source or target isn't in the final node set.
- Isolate failures. A single provider throwing (e.g. a malformed pack query) is caught and surfaced as an error — it never sinks the whole analysis.
The result also carries an unresolved list (references that couldn't be resolved) which flows through to the Problems panel.
The registry (lib/kernel/registry.ts) lists every provider. There are two species:
For TypeScript/JavaScript (incl. .mts/.cts/.mjs/.cjs, .jsx/.tsx, and .vue/.svelte shells). It uses the TypeScript type checker — so its edges are exact:
| File | Emits |
|---|---|
imports.ts |
import/export edges |
calls.ts |
type-resolved call edges (the right handle, not just any handle) |
components.ts |
JSX <Foo/> → renders edges to the component definition |
inheritance.ts |
extends / implements
|
composition.ts |
has (typed fields, resolved through Array<T> and generics) |
externals.ts |
external package nodes, enriched from package.json
|
nodes.ts |
symbol/member extraction; roles (React/Vue/Svelte/Angular/ECS) |
For very large projects the provider batches the ts-morph Project (a file-count threshold around 8000, batches of ~3000) and forgets nodes between blocks to bound memory — normal-sized repos are unaffected.
For the other 25 languages. A pack is purely declarative — no code:
language-packs/<id>/
pack.yaml # id, extensions, grammar name, import style
tags.scm # a tree-sitter query
tags.scm uses the standard capture convention:
@definition.<kind> @name ; a symbol node
@reference.<rel> @name ; an edge (resolved by name)
@import @module [@import.name]; an import edgeThe native Rust core runs them.
A napi-rs addon exposing one function:
async fn analyze(grammar, query, importStyle, filesJson) -> json- Built with
cd analyzer-core && bunx @napi-rs/cli build --release; the prebuiltanalyzer-core.nodeis committed so the app runs without a Rust toolchain. - Loaded via
process.dlopenby absolute path (lib/kernel/treesitter/core.ts) — notrequire, which webpack would try to bundle. - Runtime: tree-sitter 0.25 (grammar ABI 13–15). A grammar must depend on the version-agnostic
tree-sitter-languagecrate; one with a directtree-sitterdep conflicts with the pinned runtime. The core validates each grammar's ABI and rejects stale ones rather than crashing the C layer. - Hardened: queries are shared via
Arc<Query>, and parsing runs undercatch_unwindso a single grammar panic can't abort the sidecar. -
WebAssembly text (
.wat) has no published grammar crate, so its generatedparser.cis vendored inanalyzer-core/vendor/wat/and compiled bybuild.rsviacc.
- Python uses dotted/relative module resolution against the scanned file set.
-
Every other pack language uses by-name resolution against a global unique-definer index (separator-agnostic for
./:://), so same-scope references without an explicit import still link. These edges carryinferredconfidence (vs. the TS provider'sexact).
In the Rust core, members (method/field/property/…) and containers always become nodes; free functions/variables/constants emit only at the top level (locals fold away). Modules/namespaces are non-absorbing, so items inside a mod stay separate nodes. References attribute to the innermost enclosing emitted symbol.
Because the pack is declarative, adding a language is mostly a new folder:
-
Create the pack —
language-packs/<id>/pack.yaml(id, extensions, grammar, import style) andtags.scm(the capture query). -
Register it — add the id to
TREE_SITTER_PACKS. -
Teach the file filter — add the extensions to
lib/file-filters.tsso the scanner picks the files up. -
Wire the grammar in the core — add
"<id>" => Some(tree_sitter_<lang>::LANGUAGE.into())tolanguage_forinanalyzer-core/src/lib.rs, and the crate toCargo.toml. -
Rebuild the addon —
cd analyzer-core && bunx @napi-rs/cli build --release, and commit the newanalyzer-core.node.
A pack can later graduate to a precise, code-backed provider (like ts-morph) without changing any consumer — that's the payoff of the provider boundary. See Development & Building for the build/test loop.
PolyGraph — explore, audit, enforce, and compare software architecture across 26 languages, 100% locally. Repository · Releases · Issues · Licensed Apache-2.0 OR MIT
📖 Using PolyGraph
- Installation
- Getting Started
- The Graph Model
- Layouts & Navigation
- Impact Analysis & Insights
- Query Language
- CLI: Rules & Diff
- Exports, Workspaces & Editor
- Troubleshooting & FAQ
🔧 Internals & Contributing