Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
140 commits
Select commit Hold shift + click to select a range
8d758a9
perf(binders): group defs once and intersect with import targets
claude Sep 24, 2026
a79e1ef
fix(resolve): fix Python roots and TS, Go and doc-link resolution gaps
claude Sep 24, 2026
cbf64ac
feat(graph): resolve soft refs only when they land on an in-repo file
claude Sep 24, 2026
47807d9
fix(symbols): keep symbols named __proto__ in symbols.json
claude Sep 24, 2026
aafde96
feat(workspaces): check declared deps, report resolution health, fix …
claude Sep 24, 2026
814af1f
fix(index): make persisted-index reuse honour everything it depends on
claude Sep 24, 2026
bedb420
fix(extract): recover declarations the AST walk dropped or misread
claude Sep 24, 2026
eeb9241
fix(neighbors): report every relation to a neighbour, strongest first
claude Sep 24, 2026
5632500
feat(navigation): one symbol syntax, explained misses, honest CLI flags
claude Sep 24, 2026
0edc2d5
perf(ignore): answer gitignore rules without a full-path regex
claude Sep 24, 2026
ad9ae3a
perf(pool): hash before extracting a stat-changed file
claude Sep 24, 2026
6343f9c
fix(extract): name CommonJS default exports and walk anonymous defaul…
claude Sep 24, 2026
e97f12b
feat(graph): resolve soft refs only when they land on an in-repo file
claude Sep 24, 2026
48e50b2
fix(extract): index Elixir definitions with a `when` guard
claude Sep 24, 2026
756f8b7
perf(resolve): index Java types and C# namespace prefixes
claude Sep 24, 2026
b3170cf
perf(scan): let docs take the stat fastpath and read their text lazily
claude Sep 24, 2026
37ad6d6
fix(extract): scan imports from code only, in linear time, on both tiers
claude Sep 24, 2026
52a858d
fix(cache): reject a persisted ref whose soft flag is not `true`
claude Sep 24, 2026
58f5cb1
fix(extract): make a Lua table function a member of its table
claude Sep 24, 2026
42cab94
fix(extract): scope `export { … }` lists to the bindings they name
claude Sep 24, 2026
ffd4b61
fix(scip): nest symbols by their real parent and map every kind
claude Sep 24, 2026
11c26f1
feat(resolve): import edges for Kotlin, Scala, Dart, Lua, shell and E…
claude Sep 24, 2026
d22432e
feat(extract): index TypeScript parameter properties and PHP promoted…
claude Sep 24, 2026
22894f7
fix(scan): apply --scope/--include/--exclude inside the walk, as an i…
claude Sep 24, 2026
c7b3611
fix(resolve): Rust lib names, renamed deps, uniform paths and #[path]…
claude Sep 24, 2026
abaebb9
fix(grep): bound time and output, share one universe and dialect acro…
claude Sep 24, 2026
2c83d25
fix(walk): skip in-repo file symlinks as aliases of their target
claude Sep 24, 2026
420ddf7
feat(scip): emit implementation relationships and package identity
claude Sep 24, 2026
23465aa
perf(resolve): sort only the manifests, not the whole file set
claude Sep 24, 2026
48810e5
fix(git): read history and diffs relative to --repo, immune to user c…
claude Sep 24, 2026
4f1d3f6
feat(extract): honour a Python module's `__all__`
claude Sep 24, 2026
3117448
fix(walk): keep build/out/target/tmp directories that git tracks
claude Sep 24, 2026
0f01302
feat(extract): extract Vue, Svelte and Astro components from their sc…
claude Sep 24, 2026
6f84f1e
feat(analytics): couple indexed files by confidence, rank only change…
claude Sep 24, 2026
2188218
fix(cli): warn when a read command answers from an empty scan
claude Sep 24, 2026
f3c31c2
fix(extract): index Ruby singleton-class, inline-visibility and facto…
claude Sep 24, 2026
6e963bb
fix(rewrite): restate grep/rg/git grep faithfully, or refuse
claude Sep 24, 2026
8ffefb5
fix(extract): index minified JavaScript without its symbols, and flag it
claude Sep 24, 2026
40a0f65
feat(grep): export grepRepoEx and keep a timed-out answer consistent
claude Sep 24, 2026
a99cb56
fix(grep): pass rg only exclusions it reads the way the engine does
claude Sep 24, 2026
e825db7
fix(rewrite): smart case on fixed strings, refuse an empty path
claude Sep 24, 2026
9b794c0
fix(extract): declare every name of a multi-name declaration
claude Sep 24, 2026
6d0b296
fix(delta): trace importers of deleted and renamed files
claude Sep 24, 2026
19c30b0
Merge branch 'wp/resolve' into wp/integrate
claude Sep 25, 2026
447f729
Merge branch 'wp/graph-extras' into wp/integrate
claude Sep 25, 2026
268151e
fix(resolution): count the new language resolvers as supported
claude Sep 25, 2026
7cd5760
perf(delta): read the diff before the index, and leave the index out …
claude Sep 25, 2026
17f41cd
fix(mcp): make every result honour the official SDK's schema contract
claude Sep 25, 2026
dfcced9
feat(delta): serve the review over MCP and gate CI with --fail-on
claude Sep 25, 2026
13fd914
fix(mcp): validate every tools/call before the repo is walked
claude Sep 25, 2026
91f5edc
fix(mcp): look symbols and profiles up by own key only
claude Sep 25, 2026
4f25b51
perf(preload): read a fresh index one artifact at a time
claude Sep 25, 2026
f169fa0
fix(mcp): normalize file arguments and enforce enum-like options
claude Sep 25, 2026
9017eae
fix(rules): gate literals on the full list and reject configs that ch…
claude Sep 25, 2026
868a900
feat(mcp): keep a member's parent in concise declarations
claude Sep 25, 2026
57ad935
fix(extract): summarize a file from its description, not its license
claude Sep 25, 2026
414a39a
feat(mcp): add a memory profile and place every tool in one
claude Sep 25, 2026
e64d0af
fix(rules): stop reporting live package files and unimportable langua…
claude Sep 25, 2026
a925f7b
perf(index): restamp graph.json when only the commit moved
claude Sep 25, 2026
1e8c781
perf(search): score only candidate files and dress only the kept rows
claude Sep 25, 2026
088aa5e
fix(literals): read Go build variants as one holder, drop CI and mani…
claude Sep 25, 2026
84385f4
fix(search): let the stem bridge reach a word that is its own stem
claude Sep 25, 2026
c8ddfa4
fix(mcp): answer ping during a tool call and honour cancellation
claude Sep 25, 2026
d46a603
fix(search): search a stopword that is the whole query or a declared …
claude Sep 25, 2026
f1e66cf
perf(mcp): key every call on one canonical spelling of its repo
claude Sep 25, 2026
42eb965
fix(extract): index bundles without their copies of the sources' symbols
claude Sep 25, 2026
6adce9d
fix(search): index re-exports as prose, not as declarations
claude Sep 25, 2026
ee6ad1c
fix(edit): verify symbolic edits instead of splicing guessed spans
claude Sep 25, 2026
5369d44
perf(mcp): forget only the edited file after a symbolic edit
claude Sep 25, 2026
2de09cc
feat(cli): add `status`, a freshness report for the persisted index
claude Sep 25, 2026
4052c52
fix(repomap): rank production code, and show each file's public surfa…
claude Sep 25, 2026
a608060
fix(mcp): offer a persisted artifact only when it is the answer
claude Sep 25, 2026
1b29558
docs(lsp): state the real timeout precedence and document its env vars
claude Sep 25, 2026
87763ea
fix(onboard): take the tagline from the top of the README, not a rele…
claude Sep 25, 2026
e69e12f
fix(search): demote fixture and snapshot trees below tests
claude Sep 25, 2026
5c47083
feat(mcp): send progress notifications during a call's scan phases
claude Sep 25, 2026
1252319
fix(binders): bind a call site by its receiver, imports and package
claude Sep 25, 2026
e495d11
fix(viz): keep mermaid node ids clear of flowchart keywords, label no…
claude Sep 25, 2026
2a8f26f
docs(mcp): drop the stale fixed-protocol claim from the server header
claude Sep 25, 2026
8e59545
fix(extract): keep comments and bodies out of signatures
claude Sep 25, 2026
c99098d
feat(search): split all-lowercase compound file names into corpus words
claude Sep 25, 2026
50e605e
fix(edit): follow operator continuations in regex-tier spans
claude Sep 25, 2026
5e5beac
fix(delta,complexity): review the first commit, and refuse unknown fi…
claude Sep 25, 2026
6ae0363
feat(scan): explain skipped paths with `scan --why` and `scan --skipped`
claude Sep 25, 2026
e8646c5
fix(impact): no impossible Go dependents, and a package's files have …
claude Sep 25, 2026
113cfb3
perf(edit): skip the outline re-extraction for capped files
claude Sep 25, 2026
2b15c03
fix(edit): never insert above Rust inner docs and attributes
claude Sep 25, 2026
f8081e8
fix(playground): give the rules command the scan, so the literals gat…
claude Sep 25, 2026
a574600
docs(edit): note the symbol cap on post-edit verification
claude Sep 25, 2026
907158a
fix(tests-map): Go tests cover their package, and a test its namesake
claude Sep 25, 2026
7e299f6
fix(search): make --rank graph a real, scale-free prior, and say it i…
claude Sep 25, 2026
e0393e8
fix(extract): choose capped call sites in source order, not name order
claude Sep 25, 2026
0074519
fix(extract): stop indexing Dart call sites and keywords as functions
claude Sep 25, 2026
43b875f
fix(search): whole-number result limits everywhere, and "appears" for…
claude Sep 25, 2026
8877c5c
fix(extract): record each config scalar once, as a number or a string
claude Sep 25, 2026
bf47100
docs(search): state what compound path splitting was measured to fix
claude Sep 25, 2026
6f4f137
fix(mcp): keep deleted files out of watched sessions
claude Sep 25, 2026
a251b76
fix(extract): keep one-letter callees in the AST tier
claude Sep 25, 2026
49b9125
fix(extract): read setext headings and skip indented code in markdown
claude Sep 25, 2026
e06391f
fix(embed): degrade a broken model.json to lexical instead of exiting 2
claude Sep 25, 2026
b1516c8
fix(lsp): anchor requests on the declared identifier
claude Sep 25, 2026
0ec52df
perf(index): prove freshness from freshness.json, not the 142MB cache…
claude Sep 25, 2026
789ce11
fix(lsp): report the server's own stderr when it fails to start
claude Sep 25, 2026
3bef7ff
fix(search): make --semantic honour --exact/--rank/--explain and keep…
claude Sep 25, 2026
764e960
perf(mcp): let --watch prove freshness instead of re-walking every call
claude Sep 25, 2026
29da123
feat(embed): embed each symbol's doc comment and skip re-exports
claude Sep 25, 2026
21042be
fix(deadcode): per-symbol reference evidence, callables by default, m…
claude Sep 25, 2026
feb54a3
fix(extract): read Python strings and non-leaf strings in the AST tier
claude Sep 25, 2026
5a593f1
fix(complexity): count code only, skip containers, score nested funct…
claude Sep 25, 2026
88424cc
perf(lsp): pool language-server sessions for the MCP server's lifetime
claude Sep 25, 2026
9e06834
Merge branch 'wp/mcp' into wp/integrate
claude Sep 25, 2026
263d652
perf(embed): reuse stored vectors instead of re-embedding the corpus …
claude Sep 25, 2026
cd3499e
feat(hierarchy): Go interface implementations by assertion and method…
claude Sep 25, 2026
ba8bc68
feat(callgraph): overrides edges and dispatch, and deadcode honours them
claude Sep 25, 2026
7c4a77f
fix(extract): read declarations, docs and spans on the regex tier
claude Sep 25, 2026
b44fe61
feat(cli): find, refs and outline commands
claude Sep 25, 2026
40ccbde
feat(extract): index reStructuredText docs like markdown
claude Sep 25, 2026
f52cb83
feat(navigation): symbol-at file:line, on the CLI and as MCP symbol_at
claude Sep 25, 2026
a30c442
feat(mcp): impact and neighbors tools
claude Sep 25, 2026
370cc78
feat(navigation): callpath <A> <B> and MCP call_path
claude Sep 25, 2026
6adfefc
feat(callers): opt-in caller id per site (--with-caller, MCP withCaller)
claude Sep 25, 2026
b5ac96d
chore(integration): merge wp/infra
claude Sep 25, 2026
b33a707
chore(integration): merge wp/extract-ast
claude Sep 25, 2026
90c30df
chore(integration): merge wp/extract-imports
claude Sep 25, 2026
15ac352
fix(integration): keep two tests' premises true after extract-imports
claude Sep 25, 2026
d99c335
chore(integration): merge wp/navigation
claude Sep 25, 2026
6379aa6
fix(integration): align navigation's tests with resolve, extract-impo…
claude Sep 25, 2026
acd34bb
chore(integration): merge wp/search
claude Sep 25, 2026
0eaa51b
fix(integration): one --scope reading and one universe for grep
claude Sep 25, 2026
cb3d17d
chore(integration): merge wp/analytics
claude Sep 25, 2026
8e6ca4b
fix(integration): delta's narrowing hint and churn's path filter
claude Sep 25, 2026
58f377f
chore(integration): merge wp/edit
claude Sep 25, 2026
a74af0f
fix(integration): edit-safety's regex-tier premise after extract-imports
claude Sep 25, 2026
1230771
fix(integration): a symbolic edit also revokes the --watch proven walk
claude Sep 25, 2026
7ea4710
fix(integration): retry a pooled LSP query whose server died unseen
claude Sep 25, 2026
cf0675c
chore: bump EXTRACTOR_VERSION to 15
claude Sep 25, 2026
cb2e929
docs(integration): regex-tier symbols can carry an end line now
claude Sep 25, 2026
6b35d4a
build: rebuild bundle
claude Sep 25, 2026
a08497f
fix(integration): drop mcp.ts searchIndex import left over from the m…
claude Sep 25, 2026
f80c222
fix(ci): read pre-epoch --since windows on newer git, detect ripgrep …
claude Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
871 changes: 792 additions & 79 deletions README.md

Large diffs are not rendered by default.

408 changes: 368 additions & 40 deletions scripts/engine.browser.d.mts

Large diffs are not rendered by default.

591 changes: 422 additions & 169 deletions scripts/engine.browser.mjs

Large diffs are not rendered by default.

408 changes: 368 additions & 40 deletions scripts/engine.d.mts

Large diffs are not rendered by default.

17,001 changes: 12,877 additions & 4,124 deletions scripts/engine.mjs

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4465,15 +4465,15 @@ <h2 id="compare-heading">How it compares</h2>
a moment, so an unchanged repo must re-render identically. -->
<script id="quality-data" type="application/json">{
"schemaVersion": 1,
"engineVersion": "2.26.0",
"engineVersion": "2.30.1",
"base": {
"languages": 17,
"files": 18,
"symbols": 265,
"calls": 47,
"relations": 21,
"docLabels": 147,
"sigLabels": 29
"files": 23,
"symbols": 346,
"calls": 54,
"relations": 24,
"docLabels": 188,
"sigLabels": 32
},
"extraction": [
{
Expand Down Expand Up @@ -4553,7 +4553,7 @@ <h2 id="compare-heading">How it compares</h2>
"symbolsRecall": 1,
"symbolsF1": 1,
"kindAccuracy": 1,
"exportedAccuracy": 0.9444,
"exportedAccuracy": 0.9583,
"docCoverage": 1,
"sigCoverage": 1,
"callsF1": 1,
Expand Down Expand Up @@ -4727,7 +4727,7 @@ <h2 id="compare-heading">How it compares</h2>
"id": "grammar-vocabulary",
"label": "Grammar vocabulary",
"authority": "each tree-sitter grammar's own declared node types, read at runtime from the parser",
"value": "21 grammars, 208 declaration-ish types unhandled",
"value": "21 grammars, 209 declaration-ish types unhandled",
"scope": "Names every construct a grammar declares that no extraction rule covers. The denominator is the grammar's, not ours — which is what makes it able to find what nobody thought to label."
},
{
Expand Down
7 changes: 5 additions & 2 deletions site/playground/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function buildCommandTable(engine, mount) {
else if (token === "--exact") opts.exact = true;
else if (token === "--limit") {
const value = Number(tokens[++i]);
if (!Number.isFinite(value) || value <= 0) throw new Error("--limit needs a positive number.");
if (!Number.isInteger(value) || value <= 0) throw new Error("--limit needs a positive whole number.");
opts.limit = value;
} else if (token.startsWith("--")) throw new Error(`Unknown flag ${token}. Supported: --limit <n>, --no-fuzzy, --exact, --explain.`);
else rest.push(token);
Expand Down Expand Up @@ -235,7 +235,10 @@ export function buildCommandTable(engine, mount) {
} catch (error) {
throw new Error(`The rules config is not valid JSON: ${error.message}`);
}
return { kind: "json", data: engine.checkRules(session.artifacts.graph, engine.parseRules(parsed)) };
// The scan lets the `literals` builtin see every duplication, not
// only the headline graph.json carries.
const { scan, graph } = session.artifacts;
return { kind: "json", data: engine.checkRules(graph, engine.parseRules(parsed), { scan }) };
},
},
rewrite: {
Expand Down
18 changes: 9 additions & 9 deletions site/quality.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"schemaVersion": 1,
"engineVersion": "2.26.0",
"engineVersion": "2.30.1",
"base": {
"languages": 17,
"files": 18,
"symbols": 265,
"calls": 47,
"relations": 21,
"docLabels": 147,
"sigLabels": 29
"files": 23,
"symbols": 346,
"calls": 54,
"relations": 24,
"docLabels": 188,
"sigLabels": 32
},
"extraction": [
{
Expand Down Expand Up @@ -88,7 +88,7 @@
"symbolsRecall": 1,
"symbolsF1": 1,
"kindAccuracy": 1,
"exportedAccuracy": 0.9444,
"exportedAccuracy": 0.9583,
"docCoverage": 1,
"sigCoverage": 1,
"callsF1": 1,
Expand Down Expand Up @@ -262,7 +262,7 @@
"id": "grammar-vocabulary",
"label": "Grammar vocabulary",
"authority": "each tree-sitter grammar's own declared node types, read at runtime from the parser",
"value": "21 grammars, 208 declaration-ish types unhandled",
"value": "21 grammars, 209 declaration-ish types unhandled",
"scope": "Names every construct a grammar declares that no extraction rule covers. The denominator is the grammar's, not ours — which is what makes it able to find what nobody thought to label."
},
{
Expand Down
125 changes: 125 additions & 0 deletions src/ast/aliases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// The names an import statement binds that a call site or a base-class list
// uses INSTEAD of the declaration's own name — what the call binder needs to
// see `salute("a")` as a call to `greet`, `ft.cast(...)` as a call into the
// `typing` module, or `class Blueprint(SansioBlueprint)` as extending
// `Blueprint`.
//
// Kept apart from collectAll: imports sit at the top of a module (Python's may
// sit under a top-level `if`/`try`), so a shallow walk over the root reads them
// all without touching the one-pass visitor.
import type { ImportAlias } from "../types.js";
import { byStr } from "../sort.js";
import type { TSNode } from "./node.js";

const MAX_ALIASES = 256;

// Python statements that hold module-level code without opening a new scope —
// `if TYPE_CHECKING:`, `try: … except ImportError:` — where imports routinely sit.
const PY_TOP_BLOCKS = new Set([
"if_statement", "elif_clause", "else_clause", "try_statement", "except_clause",
"finally_clause", "with_statement", "block",
]);

function unquote(text: string): string {
return text.replace(/^['"`]|['"`]$/g, "");
}

// JS/TS: `import d, { a as b } from "s"`, `import * as ns from "s"`, and the
// module's own `export default X` (recorded as `default` → X, without `from`,
// so a default import elsewhere can find the declaration it denotes).
function readJs(node: TSNode, out: ImportAlias[]): void {
if (node.type === "import_statement") {
const source = node.childForFieldName("source");
if (!source) return;
const from = unquote(source.text);
for (const clause of node.namedChildren) {
if (clause.type !== "import_clause") continue;
for (const part of clause.namedChildren) {
if (part.type === "identifier") out.push({ local: part.text, name: "default", from });
else if (part.type === "namespace_import") {
const id = part.namedChildren.find((c) => c.type === "identifier");
if (id) out.push({ local: id.text, name: "*", from });
} else if (part.type === "named_imports") {
for (const spec of part.namedChildren) {
if (spec.type !== "import_specifier") continue;
const name = spec.childForFieldName("name")?.text;
const alias = spec.childForFieldName("alias")?.text;
// An un-renamed specifier already reaches the call binder as
// FileRecord.importedNames; only a rename needs recording.
if (name && alias && alias !== name) out.push({ local: alias, name: unquote(name), from });
}
}
}
}
} else if (node.type === "export_statement" && node.children.some((c) => c.type === "default")) {
const decl = node.childForFieldName("declaration");
const name = decl ? decl.childForFieldName("name")?.text : node.childForFieldName("value")?.type === "identifier" ? node.childForFieldName("value")!.text : undefined;
// An anonymous default is named after the file stem by the declaration walk;
// the binder falls back to that name, so nothing needs recording for it.
if (name) out.push({ local: "default", name });
}
}

// Python: `import a.b [as c]` binds a MODULE (`a`, or `c`); `from m import x
// as y` renames a name — or a submodule, which only resolution can tell apart.
// An un-renamed `from m import x` binds `x` under its own name: nothing to map.
function readPython(node: TSNode, out: ImportAlias[]): void {
if (node.type === "import_statement") {
for (const part of node.namedChildren) {
if (part.type === "dotted_name") {
// `import a.b.c` binds only the root `a`, to the package `a`.
const root = part.namedChildren[0]?.text;
if (root) out.push({ local: root, name: "*", from: root });
} else if (part.type === "aliased_import") {
const from = part.childForFieldName("name")?.text;
const alias = part.childForFieldName("alias")?.text;
if (from && alias) out.push({ local: alias, name: "*", from });
}
}
} else if (node.type === "import_from_statement") {
const from = node.childForFieldName("module_name")?.text;
if (!from) return;
for (const part of node.namedChildren) {
if (part.type !== "aliased_import") continue;
const name = part.childForFieldName("name")?.text;
const alias = part.childForFieldName("alias")?.text;
// `from .app import Flask as Flask` is PEP 484's re-export marker, not a
// rename (the declaration walk reports it as a `reexport` symbol).
if (name && alias && alias !== name) out.push({ local: alias, name, from });
}
} else if (PY_TOP_BLOCKS.has(node.type)) {
for (const c of node.namedChildren) readPython(c, out);
}
}

// Go: only an EXPLICIT package name (`foo "x/bar"`) is recorded — the implicit
// one is the package's own name, which the binder reads off the imported
// package. The blank (`_`) and dot (`.`) forms bind no qualifier.
function readGo(node: TSNode, out: ImportAlias[]): void {
if (node.type === "import_declaration" || node.type === "import_spec_list") {
for (const c of node.namedChildren) readGo(c, out);
} else if (node.type === "import_spec") {
const name = node.childForFieldName("name");
const path = node.childForFieldName("path");
if (name?.type === "package_identifier" && path) out.push({ local: name.text, name: "*", from: unquote(path.text) });
}
}

/** The import aliases a file's top level binds, deduped and sorted (local, name, from). */
export function readImportAliases(root: TSNode, lang: string): ImportAlias[] {
const read =
lang === "typescript" || lang === "javascript" ? readJs : lang === "python" ? readPython : lang === "go" ? readGo : undefined;
if (!read) return [];
const found: ImportAlias[] = [];
for (const node of root.namedChildren) read(node, found);
const seen = new Set<string>();
const out: ImportAlias[] = [];
for (const a of found.sort((x, y) => byStr(x.local, y.local) || byStr(x.name, y.name) || byStr(x.from ?? "", y.from ?? ""))) {
const key = `${a.local}\u0000${a.name}\u0000${a.from ?? ""}`;
if (seen.has(key)) continue;
seen.add(key);
out.push(a);
if (out.length >= MAX_ALIASES) break;
}
return out;
}
20 changes: 17 additions & 3 deletions src/ast/doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,31 @@ const DOC_WRAPPERS = new Set([
"body",
]);

// Siblings written between a doc comment and its declaration that belong to the
// declaration without being part of its node. Rust hangs every outer attribute
// off the enclosing list, not the item (`/// Doc`, `#[derive(Debug)]`, then
// `pub struct S`), and tree-sitter-typescript does the same with a decorator on
// its own line in a class body. Stopping at them lost the doc of nearly every
// documented Rust item that derives or inlines anything (457 of 1187 symbols in
// memchr). `inner_attribute_item` (`#![…]`) is deliberately absent: it belongs
// to the enclosing module, so a comment above it documents nothing below.
const ATTRIBUTE_SIBLING = /^(attribute_item|decorator)$/;

// The contiguous comment run immediately above `node`, oldest line first.
// "Contiguous" means no blank line: a comment separated from what follows
// belongs to neither, and attributing it would put a file header on whichever
// declaration happens to come first.
// declaration happens to come first. Attributes in between are stepped over
// (rustdoc joins `/// a`, `#[x]`, `/// b` into one doc), but they still count
// for contiguity.
function commentLinesAbove(node: TSNode): string[] {
const blocks: TSNode[] = [];
let prev = node.previousNamedSibling;
let nextRow = node.startPosition.row;
while (prev && COMMENT_TYPE.test(prev.type)) {
while (prev) {
const comment = COMMENT_TYPE.test(prev.type);
if (!comment && !ATTRIBUTE_SIBLING.test(prev.type)) break;
if (nextRow - prev.endPosition.row > 1) break;
blocks.push(prev);
if (comment) blocks.push(prev);
nextRow = prev.startPosition.row;
prev = prev.previousNamedSibling;
}
Expand Down
Loading
Loading