diffninja 0.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/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/calltree.d.ts +47 -0
- package/dist/calltree.js +296 -0
- package/dist/cli.d.ts +57 -0
- package/dist/cli.js +340 -0
- package/dist/diff.d.ts +7 -0
- package/dist/diff.js +114 -0
- package/dist/extract.d.ts +26 -0
- package/dist/extract.js +152 -0
- package/dist/git.d.ts +40 -0
- package/dist/git.js +288 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/infer.d.ts +21 -0
- package/dist/infer.js +189 -0
- package/dist/languages/bash.d.ts +2 -0
- package/dist/languages/bash.js +208 -0
- package/dist/languages/c.d.ts +2 -0
- package/dist/languages/c.js +218 -0
- package/dist/languages/call-syntax.d.ts +125 -0
- package/dist/languages/call-syntax.js +997 -0
- package/dist/languages/cpp.d.ts +2 -0
- package/dist/languages/cpp.js +321 -0
- package/dist/languages/csharp.d.ts +2 -0
- package/dist/languages/csharp.js +324 -0
- package/dist/languages/elixir.d.ts +2 -0
- package/dist/languages/elixir.js +331 -0
- package/dist/languages/go.d.ts +2 -0
- package/dist/languages/go.js +299 -0
- package/dist/languages/grammars.d.ts +50 -0
- package/dist/languages/grammars.js +351 -0
- package/dist/languages/haskell.d.ts +2 -0
- package/dist/languages/haskell.js +250 -0
- package/dist/languages/java.d.ts +2 -0
- package/dist/languages/java.js +351 -0
- package/dist/languages/javascript.d.ts +4 -0
- package/dist/languages/javascript.js +648 -0
- package/dist/languages/kotlin.d.ts +2 -0
- package/dist/languages/kotlin.js +368 -0
- package/dist/languages/lua.d.ts +2 -0
- package/dist/languages/lua.js +212 -0
- package/dist/languages/ocaml.d.ts +2 -0
- package/dist/languages/ocaml.js +291 -0
- package/dist/languages/perl.d.ts +2 -0
- package/dist/languages/perl.js +418 -0
- package/dist/languages/php.d.ts +2 -0
- package/dist/languages/php.js +397 -0
- package/dist/languages/python.d.ts +2 -0
- package/dist/languages/python.js +376 -0
- package/dist/languages/registry.d.ts +7 -0
- package/dist/languages/registry.js +69 -0
- package/dist/languages/ruby.d.ts +2 -0
- package/dist/languages/ruby.js +391 -0
- package/dist/languages/rust.d.ts +2 -0
- package/dist/languages/rust.js +261 -0
- package/dist/languages/scala.d.ts +2 -0
- package/dist/languages/scala.js +307 -0
- package/dist/languages/solidity.d.ts +2 -0
- package/dist/languages/solidity.js +240 -0
- package/dist/languages/swift.d.ts +2 -0
- package/dist/languages/swift.js +268 -0
- package/dist/languages/types.d.ts +36 -0
- package/dist/languages/types.js +74 -0
- package/dist/languages/typescript-contracts.d.ts +57 -0
- package/dist/languages/typescript-contracts.js +528 -0
- package/dist/languages/typescript-dispatch.d.ts +68 -0
- package/dist/languages/typescript-dispatch.js +710 -0
- package/dist/languages/typescript.d.ts +4 -0
- package/dist/languages/typescript.js +722 -0
- package/dist/languages/zig.d.ts +2 -0
- package/dist/languages/zig.js +243 -0
- package/dist/loc.d.ts +17 -0
- package/dist/loc.js +34 -0
- package/dist/reach.d.ts +17 -0
- package/dist/reach.js +65 -0
- package/dist/render.d.ts +18 -0
- package/dist/render.js +83 -0
- package/dist/review/brand.d.ts +8 -0
- package/dist/review/brand.js +25 -0
- package/dist/review/call-context.d.ts +27 -0
- package/dist/review/call-context.js +446 -0
- package/dist/review/call-flow-html.d.ts +32 -0
- package/dist/review/call-flow-html.js +1870 -0
- package/dist/review/call-flow-nav.d.ts +151 -0
- package/dist/review/call-flow-nav.js +317 -0
- package/dist/review/call-flow.d.ts +47 -0
- package/dist/review/call-flow.js +229 -0
- package/dist/review/change-facts.d.ts +69 -0
- package/dist/review/change-facts.js +729 -0
- package/dist/review/cli.d.ts +2 -0
- package/dist/review/cli.js +50 -0
- package/dist/review/connected-analysis.d.ts +100 -0
- package/dist/review/connected-analysis.js +163 -0
- package/dist/review/connected-html.d.ts +17 -0
- package/dist/review/connected-html.js +2853 -0
- package/dist/review/connected.d.ts +23 -0
- package/dist/review/connected.js +141 -0
- package/dist/review/escape-html.d.ts +2 -0
- package/dist/review/escape-html.js +9 -0
- package/dist/review/evidence-html.d.ts +21 -0
- package/dist/review/evidence-html.js +521 -0
- package/dist/review/evidence-syntax.d.ts +132 -0
- package/dist/review/evidence-syntax.js +478 -0
- package/dist/review/evidence-types.d.ts +62 -0
- package/dist/review/evidence-types.js +1 -0
- package/dist/review/evidence.d.ts +31 -0
- package/dist/review/evidence.js +1603 -0
- package/dist/review/file-role.d.ts +9 -0
- package/dist/review/file-role.js +29 -0
- package/dist/review/github.d.ts +204 -0
- package/dist/review/github.js +1245 -0
- package/dist/review/history.d.ts +101 -0
- package/dist/review/history.js +412 -0
- package/dist/review/html.d.ts +34 -0
- package/dist/review/html.js +1104 -0
- package/dist/review/input.d.ts +10 -0
- package/dist/review/input.js +113 -0
- package/dist/review/intent.d.ts +4 -0
- package/dist/review/intent.js +75 -0
- package/dist/review/mcp-cli.d.ts +2 -0
- package/dist/review/mcp-cli.js +25 -0
- package/dist/review/mcp.d.ts +12 -0
- package/dist/review/mcp.js +414 -0
- package/dist/review/module-resolution.d.ts +2 -0
- package/dist/review/module-resolution.js +86 -0
- package/dist/review/palette.d.ts +7 -0
- package/dist/review/palette.js +104 -0
- package/dist/review/pipeline.d.ts +77 -0
- package/dist/review/pipeline.js +227 -0
- package/dist/review/pr-input.d.ts +19 -0
- package/dist/review/pr-input.js +130 -0
- package/dist/review/questions.d.ts +201 -0
- package/dist/review/questions.js +174 -0
- package/dist/review/reference-check.d.ts +7 -0
- package/dist/review/reference-check.js +733 -0
- package/dist/review/report-pages.d.ts +109 -0
- package/dist/review/report-pages.js +328 -0
- package/dist/review/service.d.ts +23 -0
- package/dist/review/service.js +198 -0
- package/dist/review/setup.d.ts +112 -0
- package/dist/review/setup.js +549 -0
- package/dist/review/source.d.ts +26 -0
- package/dist/review/source.js +276 -0
- package/dist/review/toml.d.ts +38 -0
- package/dist/review/toml.js +565 -0
- package/dist/review/types.d.ts +179 -0
- package/dist/review/types.js +1 -0
- package/dist/run.d.ts +49 -0
- package/dist/run.js +311 -0
- package/dist/types.d.ts +366 -0
- package/dist/types.js +83 -0
- package/package.json +88 -0
- package/scripts/ensure-native-grammar.mjs +188 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tanishq Kancharla
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# diffninja
|
|
2
|
+
|
|
3
|
+
PR reviews for humans, not prose from a chatbot, run from inside your coding
|
|
4
|
+
agent. Ask Claude Code, Codex, pi, or another MCP-capable agent CLI to review a
|
|
5
|
+
GitHub PR link and diffninja returns a review workspace where you read the diff,
|
|
6
|
+
write inline comments, and submit the review yourself. Give the agent a diff or
|
|
7
|
+
a git range and diffninja returns an evidence-backed reading agenda alongside
|
|
8
|
+
the complete diff, as structured data for the agent and as a page for you.
|
|
9
|
+
|
|
10
|
+
diffninja is an MCP server (`review_diff`) plus a `diffninja setup` command that
|
|
11
|
+
registers it. It has no terminal review mode.
|
|
12
|
+
|
|
13
|
+
Analysis is local and deterministic: no model is called, no source leaves your
|
|
14
|
+
machine, and the same input always gives the same report. Each hunk gets change
|
|
15
|
+
facts — did a comparison, a limit, or an input check change; is a failure handed
|
|
16
|
+
to the caller, deferred, or discarded — each pointing at the changed line it
|
|
17
|
+
rests on. Interpreting what the change means is left to you and, if you ask it,
|
|
18
|
+
to the agent you already use. diffninja writes no review prose. You stay the
|
|
19
|
+
reviewer.
|
|
20
|
+
|
|
21
|
+
## What you need
|
|
22
|
+
|
|
23
|
+
- **Node.js 22.18 or newer.**
|
|
24
|
+
- **An MCP-capable agent CLI:** Claude Code, Codex, OMP, pi, or any client that
|
|
25
|
+
can launch a stdio MCP server.
|
|
26
|
+
- **GitHub CLI (`gh`) 2.45.0+, authenticated** (`gh auth login`) — only for
|
|
27
|
+
reviewing pull requests. diffninja never asks for a token; it reuses your
|
|
28
|
+
`gh` session.
|
|
29
|
+
- No API key. Git ranges may install missing parsing grammars through npm the
|
|
30
|
+
first time a language is seen.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
Register the MCP server on every agent CLI you use (Claude Code, Codex, OMP,
|
|
35
|
+
pi) with one command:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx -y diffninja setup
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
It installs the package globally first, then registers the server in each
|
|
42
|
+
detected CLI. `diffninja setup --help` lists the options (`--cli`,
|
|
43
|
+
`--uninstall`, `--dry-run`, `--no-install`); `docs/mcp-setup.md` has the manual
|
|
44
|
+
entries.
|
|
45
|
+
|
|
46
|
+
The package ships `diffninja` (setup only) and `diffninja-mcp` (the MCP server).
|
|
47
|
+
`npx` fetches the latest published version on first run; if a cached copy feels
|
|
48
|
+
stale, pin it explicitly (`npx -y diffninja@latest setup`).
|
|
49
|
+
|
|
50
|
+
On npm 12 and later, which block dependency install scripts by default, setup
|
|
51
|
+
names the ones diffninja needs (`--allow-scripts=diffninja,tree-sitter,...`), so
|
|
52
|
+
nothing extra is required. If you install by hand, pass the same flag:
|
|
53
|
+
`npm install -g --allow-scripts=diffninja,tree-sitter,tree-sitter-javascript,tree-sitter-typescript diffninja`.
|
|
54
|
+
|
|
55
|
+
## Review a pull request
|
|
56
|
+
|
|
57
|
+
Ask your agent to review `https://github.com/OWNER/REPO/pull/123`. It calls
|
|
58
|
+
`review_diff` with the link, reads the pull request, sends its reading with
|
|
59
|
+
`finish_review`, and then gives you a loopback review page, loaded from the
|
|
60
|
+
canonical GitHub patch through your `gh` authentication. The link arrives
|
|
61
|
+
once the agent has read the change, so the page opens complete: the agent's
|
|
62
|
+
reading order, its answers, and its suggested comments. Select diff lines,
|
|
63
|
+
write single-line inline comments and a review body, pick Comment, Approve, or
|
|
64
|
+
Request changes, preview the exact payload, and submit. Comments your agent
|
|
65
|
+
suggests wait under their lines until you add them; every word you submit is one
|
|
66
|
+
you chose, and diffninja only carries it to GitHub. It never approves, blocks, or merges
|
|
67
|
+
anything on its own. The page belongs to the agent's MCP connection and closes
|
|
68
|
+
when the agent exits.
|
|
69
|
+
|
|
70
|
+
The diff opens in the **reading order**: each hunk is a numbered change,
|
|
71
|
+
most important first, with your agent's answers (does this change behavior,
|
|
72
|
+
does a test exercise it, does it serve the stated goal) and the facts to look
|
|
73
|
+
at above its code. Hunks outside the order follow under **Other changes**.
|
|
74
|
+
Press `j` and `k` to step through the changes. A rail beside the diff lists
|
|
75
|
+
them, marks the one you are reading, and fills in as you read. **By file**
|
|
76
|
+
switches to one block per file, with each change's number where it starts,
|
|
77
|
+
and keeps you on the change you were reading. When the agent works inside your local
|
|
78
|
+
clone it passes it as `repo`; once the clone has the pull request's commits,
|
|
79
|
+
the analysis adds definitions and call flows, and the page opens a file's call
|
|
80
|
+
flow in a drawer beside the diff. diffninja never fetches or writes in the
|
|
81
|
+
clone; when it lacks the commits, the page says so and names the `git fetch`
|
|
82
|
+
that would add them.
|
|
83
|
+
|
|
84
|
+
## Review a diff or a git range
|
|
85
|
+
|
|
86
|
+
Ask your agent to review a patch, the working tree, or a range such as
|
|
87
|
+
`main..HEAD` in a repository. It calls `review_diff` with `diff` text or with
|
|
88
|
+
`repo`, `from`, and `to`, and receives the report as the tool result: the exact
|
|
89
|
+
expected outcome when supplied (`expectedOutcome`), a short reading agenda,
|
|
90
|
+
bounded automatic findings, explicit check coverage, and every hunk, ranked as
|
|
91
|
+
**attention**, **uncertain**, **low**, or **passed**; git ranges add call flows
|
|
92
|
+
and snapshot-bound source. Claims in a description are not proof that the code
|
|
93
|
+
fulfills them. Once the agent has sent its reading (below), it gets `reportUrl`:
|
|
94
|
+
a read-only page on `127.0.0.1` with the same report for you to read (the
|
|
95
|
+
agenda, call-flow graphs, and every hunk), served from memory for as long as
|
|
96
|
+
the agent's session lasts.
|
|
97
|
+
The tool writes no report files. Arguments and examples:
|
|
98
|
+
[docs/mcp-setup.md](docs/mcp-setup.md). Trying it on real reviews:
|
|
99
|
+
[docs/pilot.md](docs/pilot.md).
|
|
100
|
+
|
|
101
|
+
## How static analysis works
|
|
102
|
+
|
|
103
|
+
1. **Evidence first.** No-op hunks and blank-only document changes pass.
|
|
104
|
+
Repository snapshots support bounded checks for duplicate function bodies and
|
|
105
|
+
unread `errors` response fields, plus caller and contract source cards.
|
|
106
|
+
Optional TypeScript reference checking compares before/after diagnostics,
|
|
107
|
+
including unchanged consumers, using an explicitly trusted installed compiler.
|
|
108
|
+
Unsupported or incomplete checks say so.
|
|
109
|
+
2. **Change facts per hunk.** The added and removed lines are read lexically —
|
|
110
|
+
strings and comments set aside, moved lines cancelling out. Code gets six facts:
|
|
111
|
+
comparison changed, limit changed (a numeric bound, or `<` turned into `<=`),
|
|
112
|
+
input check changed (type/shape checks, or a changed guard in front of a
|
|
113
|
+
raise), failure handed to the caller, failure deferred or retried, failure
|
|
114
|
+
discarded (an empty or defaulting `catch`, `except: pass`, …). Documentation
|
|
115
|
+
(`.md`, `.rst`, `.txt`, …) is asked whether an instruction to readers changed
|
|
116
|
+
(must, never, only, at most, …), a link target changed, or a numeric limit
|
|
117
|
+
changed. Configuration (`.yml`, `.json`, `.toml`, Dockerfiles, `.env`, …) is
|
|
118
|
+
asked whether a CI gate was weakened (`continue-on-error`, `|| true`, a
|
|
119
|
+
failure turned into a warning, a check step removed), permissions or secret
|
|
120
|
+
access changed, a version pin changed, or a limit changed. Each `yes` cites
|
|
121
|
+
its changed line. `no` speaks only about the lines the hunk shows. A change
|
|
122
|
+
that only touches formatting, comments, or line breaks is recognized as such.
|
|
123
|
+
JS/TS, Java, C#, Go, Rust, C/C++, Kotlin, Swift, PHP, Python, Ruby,
|
|
124
|
+
documentation and configuration are read; any other file type says that no
|
|
125
|
+
facts were established instead of claiming none.
|
|
126
|
+
3. **Status and order.** A code or configuration change outside a test file
|
|
127
|
+
reads **attention**; documentation reads **attention** when it changes an
|
|
128
|
+
instruction, a link, or a limit, **low** otherwise; a test-file change reads
|
|
129
|
+
**attention** only when it changes a limit, discards a failure, or weakens a
|
|
130
|
+
gate, **low** otherwise; a formatting-only change **passed**;
|
|
131
|
+
an unread file type **uncertain**, for a person to read. Priority orders hunks:
|
|
132
|
+
a fixed base, plus 10 for a real change, plus the heaviest fact of the
|
|
133
|
+
boundary group (what the change says or bounds) and of the failure group
|
|
134
|
+
(failures, CI gates, permissions) — each group counts once, never summed. The report lists
|
|
135
|
+
manual work first (binary and other metadata-only units), then the read hunks
|
|
136
|
+
by priority, then passes; among equal priorities the hunk that changes more
|
|
137
|
+
lines comes first. Hunks in test files (by path convention: `test/`,
|
|
138
|
+
`*.test.ts`, `test_*.py`, `*_test.go`, …, including snapshots diffninja does
|
|
139
|
+
not read) come after the other hunks, still ordered by their own priority: a
|
|
140
|
+
regression test changes as much as its fix.
|
|
141
|
+
Documentation is not demoted, because prose can be normative. Status is a
|
|
142
|
+
label for filtering and never reorders the report.
|
|
143
|
+
4. **Questions for your agent.** Where a judgment needs meaning rather than
|
|
144
|
+
syntax, the report asks the agent that requested it — does this hunk change
|
|
145
|
+
what callers observe, does a test exercise it, does a test change weaken it,
|
|
146
|
+
do the docs match the code, does the hunk serve the stated goal. Questions
|
|
147
|
+
are fixed templates with closed options (always including `cannot-tell`).
|
|
148
|
+
diffninja itself still calls no model.
|
|
149
|
+
The agent sends its whole reading in one `finish_review` call: an answer to
|
|
150
|
+
every question, the reading order of every hunk (most important first),
|
|
151
|
+
and the line comments it would leave (or none). diffninja checks all of it
|
|
152
|
+
and only then hands out the page link, so every page you open already
|
|
153
|
+
carries the agent's answers, its order, and its comment decision; an agent
|
|
154
|
+
cannot give you a half-read page. The pages list every hunk in the agent's
|
|
155
|
+
order, attributed to it; diffninja's own order stays available one click
|
|
156
|
+
away, and statuses stay diffninja's. `record_answers`, `record_order`, and
|
|
157
|
+
`suggest_comments` update a review afterwards. On 159 held-out open-source
|
|
158
|
+
pull requests, weighted by the severity of maintainers' actual review
|
|
159
|
+
comments, a host model that read diffninja's report put the serious
|
|
160
|
+
comments earlier than diffninja's deterministic order did.
|
|
161
|
+
On a pull request, the suggested line comments are short, in the reviewer's
|
|
162
|
+
own voice, with no "Finding 1:" scaffolding. The page shows each under its line; you add one
|
|
163
|
+
or all of them to your draft with a click, edit or dismiss them, and submit
|
|
164
|
+
the review yourself. Nothing is posted without you.
|
|
165
|
+
5. **Project context (git ranges only).** What a diff does not show is often
|
|
166
|
+
the project around it. From the local clone alone — nothing is fetched —
|
|
167
|
+
the report names the commits that last changed each hunk's removed lines
|
|
168
|
+
(`git blame` at the base), earlier revert commits that touched a changed
|
|
169
|
+
file or share a rare word with the goal or the changed file names,
|
|
170
|
+
contributor guidelines that apply (`CONTRIBUTING`, `AGENTS.md`, `.github/`,
|
|
171
|
+
docs policy pages such as versioning or preview rules), and, for a new
|
|
172
|
+
file, identifiers most of its same-named siblings use and it does not
|
|
173
|
+
(`components/*/select.py`). They add questions for your agent: does a hunk
|
|
174
|
+
undo a fix it removes, does the change reintroduce something reverted, does
|
|
175
|
+
it follow the guidelines and the sibling pattern. These are pointers, not
|
|
176
|
+
verdicts, and never change status or order. A shallow clone says so: lines
|
|
177
|
+
whose origin lies past its boundary count as unknown.
|
|
178
|
+
|
|
179
|
+
Intent cross-checks keep author claims and generated summaries separate.
|
|
180
|
+
Source matches are navigation evidence, not proof of fulfillment. Broad goals,
|
|
181
|
+
missing metadata, and behavior not established by the available code remain
|
|
182
|
+
explicitly unestablished. Findings likewise state their scope: duplicated syntax
|
|
183
|
+
is not necessarily duplicated responsibility, and an unread field alone does not
|
|
184
|
+
prove that a real failure was mishandled.
|
|
185
|
+
|
|
186
|
+
Git-range analysis supplies selected call-site blocks from both snapshots,
|
|
187
|
+
including written arguments, declared parameters, locations, and explicit target
|
|
188
|
+
and binding uncertainty. Unambiguous same-file JS/TS and Python calls support
|
|
189
|
+
positional binding; Python also supports named arguments. Imports, member
|
|
190
|
+
dispatch, dynamic targets, and other grammars do not get guessed mappings.
|
|
191
|
+
These are static source expressions, not runtime values or data-flow analysis.
|
|
192
|
+
TypeScript/TSX extraction includes methods of decorated exported classes,
|
|
193
|
+
including stacked and custom decorators; method source spans retain their
|
|
194
|
+
decorators so decorator-only changes still select the method body. Simple explicit class-field and
|
|
195
|
+
constructor-property types identify candidate dependency methods; unsupported
|
|
196
|
+
receiver types stay unresolved rather than borrowing the containing class's
|
|
197
|
+
method. These are not type-checked or proven runtime bindings.
|
|
198
|
+
|
|
199
|
+
Review context also follows candidate event and queue relations: static
|
|
200
|
+
`emit`/`emitAsync` keys match `@On*Event` handlers; injected queue `.add` keys
|
|
201
|
+
match `@Processor` consumers' `job.name` cases or `@Process` methods only on a
|
|
202
|
+
matching queue channel. String enum values can connect member keys to literal
|
|
203
|
+
cases. These are source-derived relations, not runtime calls or proof of
|
|
204
|
+
delivery; dynamic keys, aliases, and unrecognized framework syntax can be absent.
|
|
205
|
+
Constant resolution is snapshot-local, including when extraction is cached.
|
|
206
|
+
|
|
207
|
+
Module-level TypeScript/TSX interfaces, type aliases, and enums are addressable
|
|
208
|
+
non-callable context nodes. Signature, body, and generic type references connect
|
|
209
|
+
them to reviewed methods and other contracts. Relative import bindings take
|
|
210
|
+
precedence; unique module-path suffixes and unimported names are candidate
|
|
211
|
+
matches, not type checking. Unresolved imports do not borrow same-named types.
|
|
212
|
+
Barrel re-exports, nested declarations, and class-field contracts may be absent.
|
|
213
|
+
|
|
214
|
+
Context prioritizes calls adjacent to the hunk, with depth limited to four,
|
|
215
|
+
at most eight arguments per call, and 120 characters per argument excerpt.
|
|
216
|
+
Snapshot-bound changed definitions, callers, callees, dispatch endpoints, and
|
|
217
|
+
type contracts carry complete source plus selected relation evidence. Bodies not
|
|
218
|
+
already shown in the hunk take precedence over duplicate source. Unseen type
|
|
219
|
+
declarations and dispatch endpoints precede ordinary caller chains, nearest
|
|
220
|
+
first, with resulting-snapshot evidence ahead of prior-snapshot duplicates.
|
|
221
|
+
Unresolved own-call expressions remain verbatim in a whole source body rather
|
|
222
|
+
than repeating unknown-binding boilerplate. At most eight distinct definition
|
|
223
|
+
nodes are kept per hunk, each carried whole, never shortened.
|
|
224
|
+
|
|
225
|
+
Automatic response/caller evidence is narrower than the candidate call graph:
|
|
226
|
+
it requires a supported lexical or typed-constructor binding. Relative modules
|
|
227
|
+
and simple single-target `paths` aliases from snapshot-local, standalone JSON
|
|
228
|
+
`tsconfig.json` files are supported. JSONC, inherited configurations, package or
|
|
229
|
+
barrel resolution, and complex receivers remain unproven rather than borrowing
|
|
230
|
+
an unrelated same-named function. `referenceProject: "path/to/tsconfig.json"`
|
|
231
|
+
opts into the separate TypeScript diagnostic comparison; absent dependencies,
|
|
232
|
+
unsupported project layouts, and exceeded bounds are reported as not checked.
|
|
233
|
+
|
|
234
|
+
## Good to know
|
|
235
|
+
|
|
236
|
+
- Tool results embed source code, including unchanged code, and stay in your
|
|
237
|
+
agent's session. Keep transcripts that contain them private.
|
|
238
|
+
- Connected PR reviews need authenticated `gh`. Nothing else leaves your
|
|
239
|
+
machine: no model is called and no API key is needed.
|
|
240
|
+
|
|
241
|
+
## Dev
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
npm run build # tsc -> dist/
|
|
245
|
+
npm run lint # oxlint
|
|
246
|
+
npm test # vitest run
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Run the built server directly with `node dist/review/mcp-cli.js` (it speaks MCP
|
|
250
|
+
over stdio and prints nothing else to stdout).
|
|
251
|
+
|
|
252
|
+
Releases and the npm publishing setup: [docs/npm-release.md](docs/npm-release.md).
|
|
253
|
+
|
|
254
|
+
## Credits
|
|
255
|
+
|
|
256
|
+
The call-flow engine is a fork of
|
|
257
|
+
[calldiff](https://github.com/tanishqkancharla/calldiff) by Tanishq Kancharla
|
|
258
|
+
(MIT, see LICENSE). diffninja uses its call graphs to show which flows each
|
|
259
|
+
hunk touches.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type FunctionIndex } from "./extract.js";
|
|
2
|
+
import type { CallNode, FunctionInfo } from "./types.js";
|
|
3
|
+
/** Normalize user-facing paths for entry matching (`\` → `/`, strip `./`). */
|
|
4
|
+
export declare function normalizeEntryPath(entry: string): string;
|
|
5
|
+
/** Unique source paths present in an index. */
|
|
6
|
+
export declare function indexedFiles(index: FunctionIndex): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Resolve which indexed source files match a `--file` argument.
|
|
9
|
+
* Exact path wins; otherwise a unique suffix match (`routes.ts` → `src/routes.ts`).
|
|
10
|
+
*/
|
|
11
|
+
export declare function matchEntrypointFiles(entry: string, files: Iterable<string>): string[];
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a `--file` argument to a single indexed source path.
|
|
14
|
+
* Throws when missing or ambiguous.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveEntrypointFile(entry: string, files: Iterable<string>): string;
|
|
17
|
+
/** Exported definitions in a concrete source path. */
|
|
18
|
+
export declare function exportsInFile(file: string, index: FunctionIndex): FunctionInfo[];
|
|
19
|
+
/**
|
|
20
|
+
* Exported definitions for a `--file` argument against one index.
|
|
21
|
+
* Throws when the path is missing/ambiguous; returns [] when the file has no exports.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveFileEntrypoints(entry: string, index: FunctionIndex): FunctionInfo[];
|
|
24
|
+
/**
|
|
25
|
+
* Expand a function into a nested call tree by following known definitions.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildCallTree(entryKey: string, index: FunctionIndex, maxDepth: number): CallNode;
|
|
28
|
+
/**
|
|
29
|
+
* Expand a specific definition. Used by `reach` when several functions share a
|
|
30
|
+
* bare key and first-wins indexing would otherwise hide all but one body.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildCallTreeFromInfo(info: FunctionInfo, index: FunctionIndex, maxDepth: number): CallNode;
|
|
33
|
+
/**
|
|
34
|
+
* Every call site written in `info`'s own body as a one-level node: the callee's
|
|
35
|
+
* definition is attached, but nothing is expanded below it. Branch nesting and
|
|
36
|
+
* inline calls (arguments, callbacks, JSX) are flattened into the same list, so
|
|
37
|
+
* a consumer that walks definitions itself sees each site exactly once and can
|
|
38
|
+
* order them by source position.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildCallSitesFromInfo(info: FunctionInfo, index: FunctionIndex): CallNode[];
|
|
41
|
+
export declare function resolveEntry(entry: string, index: FunctionIndex): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Every definition that matches `entry` (including those shadowed in the
|
|
44
|
+
* bare-key map). Prefer exact bare-key hits; otherwise Class.method / `new X`.
|
|
45
|
+
* Order is stable by label, then file — independent of extract order.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveAllEntries(entry: string, index: FunctionIndex): FunctionInfo[];
|
package/dist/calltree.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { allFunctions, definitionsInFile, fileScopedKey, } from "./extract.js";
|
|
2
|
+
import { callContextFromSyntax } from "./languages/call-syntax.js";
|
|
3
|
+
import { pickLoc } from "./loc.js";
|
|
4
|
+
/** Normalize user-facing paths for entry matching (`\` → `/`, strip `./`). */
|
|
5
|
+
export function normalizeEntryPath(entry) {
|
|
6
|
+
return entry.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
7
|
+
}
|
|
8
|
+
/** Unique source paths present in an index. */
|
|
9
|
+
export function indexedFiles(index) {
|
|
10
|
+
return [...new Set(allFunctions(index).map((fn) => fn.file))].sort();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Resolve which indexed source files match a `--file` argument.
|
|
14
|
+
* Exact path wins; otherwise a unique suffix match (`routes.ts` → `src/routes.ts`).
|
|
15
|
+
*/
|
|
16
|
+
export function matchEntrypointFiles(entry, files) {
|
|
17
|
+
const normalized = normalizeEntryPath(entry);
|
|
18
|
+
const unique = [...new Set(files)];
|
|
19
|
+
const exact = unique.filter((file) => file === normalized);
|
|
20
|
+
if (exact.length > 0)
|
|
21
|
+
return exact.sort();
|
|
22
|
+
return unique
|
|
23
|
+
.filter((file) => file === normalized || file.endsWith(`/${normalized}`))
|
|
24
|
+
.sort();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a `--file` argument to a single indexed source path.
|
|
28
|
+
* Throws when missing or ambiguous.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveEntrypointFile(entry, files) {
|
|
31
|
+
const matched = matchEntrypointFiles(entry, files);
|
|
32
|
+
if (matched.length === 0) {
|
|
33
|
+
throw new Error(`Entrypoint file not found: ${entry}`);
|
|
34
|
+
}
|
|
35
|
+
if (matched.length > 1) {
|
|
36
|
+
throw new Error(`Ambiguous entrypoint file: ${entry} matches ${matched.join(", ")}. Use a more specific path.`);
|
|
37
|
+
}
|
|
38
|
+
return matched[0];
|
|
39
|
+
}
|
|
40
|
+
/** Exported definitions in a concrete source path. */
|
|
41
|
+
export function exportsInFile(file, index) {
|
|
42
|
+
return sortDefinitions(allFunctions(index).filter((fn) => fn.file === file && fn.exported));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Exported definitions for a `--file` argument against one index.
|
|
46
|
+
* Throws when the path is missing/ambiguous; returns [] when the file has no exports.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveFileEntrypoints(entry, index) {
|
|
49
|
+
const file = resolveEntrypointFile(entry, indexedFiles(index));
|
|
50
|
+
return exportsInFile(file, index);
|
|
51
|
+
}
|
|
52
|
+
/** Is `fn` declared inside `owner`'s source span? */
|
|
53
|
+
function declaredInside(fn, owner) {
|
|
54
|
+
if (!owner || fn.line == null || owner.line == null)
|
|
55
|
+
return false;
|
|
56
|
+
return (fn.line >= owner.line &&
|
|
57
|
+
(fn.endLine ?? fn.line) <= (owner.endLine ?? owner.line));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve one call to a definition.
|
|
61
|
+
*
|
|
62
|
+
* A definition in the file the call was written in always wins: the bare-key
|
|
63
|
+
* map is global and first-wins, so without this a call resolves to whichever
|
|
64
|
+
* same-named function happened to be indexed first, grafting an unrelated body
|
|
65
|
+
* into the tree. See #19.
|
|
66
|
+
*
|
|
67
|
+
* Falls back to the global map, which is what callers across file boundaries
|
|
68
|
+
* (the common case) rely on.
|
|
69
|
+
*/
|
|
70
|
+
function resolveCall(key, index, callSite, owner) {
|
|
71
|
+
const file = callSite?.file ?? owner?.file;
|
|
72
|
+
if (file) {
|
|
73
|
+
const candidates = definitionsInFile(index, file, key);
|
|
74
|
+
if (candidates.length === 1)
|
|
75
|
+
return candidates[0];
|
|
76
|
+
if (candidates.length > 1) {
|
|
77
|
+
// One file declaring the name twice: a helper declared inside the calling
|
|
78
|
+
// function shadows that file's top-level definition.
|
|
79
|
+
const shadowing = candidates.find((fn) => fn.local && declaredInside(fn, owner));
|
|
80
|
+
return shadowing ?? candidates[0];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return index.get(key);
|
|
84
|
+
}
|
|
85
|
+
/** Never borrow a same-named definition's parameters for a lexical binding. */
|
|
86
|
+
function callResolution(info, step) {
|
|
87
|
+
const declared = step?.syntax?.params;
|
|
88
|
+
return {
|
|
89
|
+
resolved: info !== undefined,
|
|
90
|
+
lexical: info !== undefined && step?.file === info.file && declared?.start !== undefined &&
|
|
91
|
+
declared.start === info.params?.start && declared.end === info.params?.end,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Context for one call node: the target expression and arguments as written,
|
|
96
|
+
* paired with the callee's declared parameters, plus what this expansion left
|
|
97
|
+
* unvisited.
|
|
98
|
+
*/
|
|
99
|
+
function callContextFor(step, info, resolution, omittedChildren, omittedInlineChildren) {
|
|
100
|
+
if (step?.type !== "call" || !step.syntax)
|
|
101
|
+
return undefined;
|
|
102
|
+
const context = callContextFromSyntax(step.syntax, info?.params, resolution);
|
|
103
|
+
// Counts are written only when something was actually left unexpanded.
|
|
104
|
+
if (omittedChildren)
|
|
105
|
+
context.omittedChildren = omittedChildren;
|
|
106
|
+
if (omittedInlineChildren)
|
|
107
|
+
context.omittedInlineChildren = omittedInlineChildren;
|
|
108
|
+
return context;
|
|
109
|
+
}
|
|
110
|
+
/** Attach an extracted context without an empty spread. */
|
|
111
|
+
function withContext(node, context) {
|
|
112
|
+
if (context)
|
|
113
|
+
node.context = context;
|
|
114
|
+
return node;
|
|
115
|
+
}
|
|
116
|
+
function displayCallLabel(key, index, info) {
|
|
117
|
+
if (info)
|
|
118
|
+
return info.label;
|
|
119
|
+
const fromIndex = index.get(key);
|
|
120
|
+
if (fromIndex)
|
|
121
|
+
return fromIndex.label;
|
|
122
|
+
return key.includes("(") ? key : `${key}()`;
|
|
123
|
+
}
|
|
124
|
+
function expandSteps(steps, index, depth, maxDepth, visiting,
|
|
125
|
+
/** Definition these steps were read from, used to scope call resolution. */
|
|
126
|
+
owner) {
|
|
127
|
+
return steps.map((step) => {
|
|
128
|
+
if (step.type === "branch") {
|
|
129
|
+
return {
|
|
130
|
+
key: step.key,
|
|
131
|
+
label: step.label,
|
|
132
|
+
kind: "branch",
|
|
133
|
+
...pickLoc(step),
|
|
134
|
+
children: expandSteps(step.children, index, depth, maxDepth, visiting, owner),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return expandCall(step.key, index, depth, maxDepth, visiting, step.children, step, undefined, owner);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** Definition location for a resolved call, or nothing when unresolved. */
|
|
141
|
+
function definitionLoc(info) {
|
|
142
|
+
if (!info || info.line == null)
|
|
143
|
+
return undefined;
|
|
144
|
+
const loc = { file: info.file, line: info.line };
|
|
145
|
+
// Only a multi-line definition carries an end: a single line says so once.
|
|
146
|
+
if (info.endLine != null && info.endLine !== info.line)
|
|
147
|
+
loc.endLine = info.endLine;
|
|
148
|
+
return loc;
|
|
149
|
+
}
|
|
150
|
+
/** Attach the resolved definition to a node without an empty spread. */
|
|
151
|
+
function withDefinition(node, definition) {
|
|
152
|
+
if (definition)
|
|
153
|
+
node.definition = definition;
|
|
154
|
+
return node;
|
|
155
|
+
}
|
|
156
|
+
function expandCall(key, index, depth, maxDepth, visiting, inlineChildren, callSite,
|
|
157
|
+
/** When set, expand this body even if another definition owns the bare key. */
|
|
158
|
+
infoOverride,
|
|
159
|
+
/** Definition this call was read from, used to scope call resolution. */
|
|
160
|
+
owner) {
|
|
161
|
+
const info = infoOverride ?? resolveCall(key, index, callSite, owner);
|
|
162
|
+
const label = displayCallLabel(key, index, info);
|
|
163
|
+
const definition = definitionLoc(info);
|
|
164
|
+
// Recursion is per definition, not per name: two same-named functions in
|
|
165
|
+
// different files calling each other is not a cycle.
|
|
166
|
+
const token = info ? fileScopedKey(info.file, info.key) : key;
|
|
167
|
+
// Root uses the definition start line; every other node uses the call-site in the parent.
|
|
168
|
+
const loc = depth === 0 && info?.line != null
|
|
169
|
+
? pickLoc({ file: info.file, line: info.line })
|
|
170
|
+
: pickLoc(callSite);
|
|
171
|
+
// Body steps a further expansion would have produced; the caller can tell a
|
|
172
|
+
// depth cut from an unresolvable callee by their presence.
|
|
173
|
+
const bodySteps = info?.steps.length;
|
|
174
|
+
if (depth >= maxDepth) {
|
|
175
|
+
return withContext(withDefinition({ key, label, kind: "call", ...loc, children: [] }, definition), callContextFor(callSite, info, callResolution(info, callSite), bodySteps, inlineChildren?.length));
|
|
176
|
+
}
|
|
177
|
+
if (!info && !inlineChildren?.length) {
|
|
178
|
+
return withContext(withDefinition({ key, label, kind: "call", ...loc, children: [] }, definition), callContextFor(callSite, info, callResolution(info, callSite)));
|
|
179
|
+
}
|
|
180
|
+
if (info && visiting.has(token)) {
|
|
181
|
+
// Still expand call-site JSX children; they are not a re-entry into `key`'s body.
|
|
182
|
+
const callSiteChildren = inlineChildren?.length
|
|
183
|
+
? expandSteps(inlineChildren, index, depth + 1, maxDepth, visiting, owner)
|
|
184
|
+
: [];
|
|
185
|
+
return withContext(withDefinition({
|
|
186
|
+
key,
|
|
187
|
+
label: `${label} ⇄`,
|
|
188
|
+
kind: "call",
|
|
189
|
+
...loc,
|
|
190
|
+
children: callSiteChildren,
|
|
191
|
+
}, definition), callContextFor(callSite, info, callResolution(info, callSite), bodySteps, inlineChildren?.length));
|
|
192
|
+
}
|
|
193
|
+
if (info)
|
|
194
|
+
visiting.add(token);
|
|
195
|
+
const bodyChildren = info
|
|
196
|
+
? expandSteps(info.steps, index, depth + 1, maxDepth, visiting, info)
|
|
197
|
+
: [];
|
|
198
|
+
const callSiteChildren = inlineChildren?.length
|
|
199
|
+
? expandSteps(inlineChildren, index, depth + 1, maxDepth, visiting, owner)
|
|
200
|
+
: [];
|
|
201
|
+
if (info)
|
|
202
|
+
visiting.delete(token);
|
|
203
|
+
return withContext(withDefinition({
|
|
204
|
+
key,
|
|
205
|
+
label,
|
|
206
|
+
kind: "call",
|
|
207
|
+
...loc,
|
|
208
|
+
children: [...bodyChildren, ...callSiteChildren],
|
|
209
|
+
}, definition), callContextFor(callSite, info, callResolution(info, callSite)));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Expand a function into a nested call tree by following known definitions.
|
|
213
|
+
*/
|
|
214
|
+
export function buildCallTree(entryKey, index, maxDepth) {
|
|
215
|
+
const resolved = resolveEntry(entryKey, index) ?? entryKey;
|
|
216
|
+
return expandCall(resolved, index, 0, maxDepth, new Set());
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Expand a specific definition. Used by `reach` when several functions share a
|
|
220
|
+
* bare key and first-wins indexing would otherwise hide all but one body.
|
|
221
|
+
*/
|
|
222
|
+
export function buildCallTreeFromInfo(info, index, maxDepth) {
|
|
223
|
+
return expandCall(info.key, index, 0, maxDepth, new Set(), undefined, undefined, info);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Every call site written in `info`'s own body as a one-level node: the callee's
|
|
227
|
+
* definition is attached, but nothing is expanded below it. Branch nesting and
|
|
228
|
+
* inline calls (arguments, callbacks, JSX) are flattened into the same list, so
|
|
229
|
+
* a consumer that walks definitions itself sees each site exactly once and can
|
|
230
|
+
* order them by source position.
|
|
231
|
+
*/
|
|
232
|
+
export function buildCallSitesFromInfo(info, index) {
|
|
233
|
+
const sites = [];
|
|
234
|
+
const visiting = new Set([fileScopedKey(info.file, info.key)]);
|
|
235
|
+
const walk = (steps) => {
|
|
236
|
+
for (const step of steps) {
|
|
237
|
+
if (step.type === "branch") {
|
|
238
|
+
walk(step.children);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
sites.push(expandCall(step.key, index, 1, 1, visiting, undefined, step, undefined, info));
|
|
242
|
+
// Inline calls belong to the same body; emit them as sites too.
|
|
243
|
+
if (step.children?.length)
|
|
244
|
+
walk(step.children);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
walk(info.steps);
|
|
248
|
+
return sites;
|
|
249
|
+
}
|
|
250
|
+
export function resolveEntry(entry, index) {
|
|
251
|
+
if (index.has(entry))
|
|
252
|
+
return entry;
|
|
253
|
+
const stripped = entry.replace(/\(\)$/, "");
|
|
254
|
+
if (index.has(stripped))
|
|
255
|
+
return stripped;
|
|
256
|
+
const matches = [...index.keys()].filter((key) => key === entry ||
|
|
257
|
+
key.endsWith(`.${entry}`) ||
|
|
258
|
+
key === `new ${entry}`);
|
|
259
|
+
if (matches.length === 1)
|
|
260
|
+
return matches[0];
|
|
261
|
+
if (matches.length > 1) {
|
|
262
|
+
const exported = matches.filter((key) => index.get(key)?.exported);
|
|
263
|
+
if (exported.length === 1)
|
|
264
|
+
return exported[0];
|
|
265
|
+
return matches.sort()[0];
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
function entryNameMatches(key, entry) {
|
|
270
|
+
return (key === entry ||
|
|
271
|
+
key.endsWith(`.${entry}`) ||
|
|
272
|
+
key === `new ${entry}`);
|
|
273
|
+
}
|
|
274
|
+
function sortDefinitions(fns) {
|
|
275
|
+
return [...fns].sort((a, b) => {
|
|
276
|
+
if (a.label !== b.label)
|
|
277
|
+
return a.label < b.label ? -1 : 1;
|
|
278
|
+
if (a.file !== b.file)
|
|
279
|
+
return a.file < b.file ? -1 : 1;
|
|
280
|
+
return (a.line ?? 0) - (b.line ?? 0);
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Every definition that matches `entry` (including those shadowed in the
|
|
285
|
+
* bare-key map). Prefer exact bare-key hits; otherwise Class.method / `new X`.
|
|
286
|
+
* Order is stable by label, then file — independent of extract order.
|
|
287
|
+
*/
|
|
288
|
+
export function resolveAllEntries(entry, index) {
|
|
289
|
+
const stripped = entry.replace(/\(\)$/, "");
|
|
290
|
+
const all = allFunctions(index);
|
|
291
|
+
const exact = all.filter((fn) => fn.key === entry || fn.key === stripped);
|
|
292
|
+
if (exact.length > 0)
|
|
293
|
+
return sortDefinitions(exact);
|
|
294
|
+
const matches = all.filter((fn) => entryNameMatches(fn.key, entry) || entryNameMatches(fn.key, stripped));
|
|
295
|
+
return sortDefinitions(matches);
|
|
296
|
+
}
|