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
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { escapeHtml } from "./escape-html.js";
|
|
2
|
+
/**
|
|
3
|
+
* The opening view of a review report: what the pull request says it does, what
|
|
4
|
+
* the gathered evidence actually established, the short reading agenda, and the
|
|
5
|
+
* automatic findings with the source behind them.
|
|
6
|
+
*
|
|
7
|
+
* Every string is a fixed template or escaped report data. The pull request
|
|
8
|
+
* title, description and evidence text are untrusted input: they are escaped
|
|
9
|
+
* into text nodes and never reach the page script. No numeric priority, model
|
|
10
|
+
* probability or judgment value is printed here, and no check is described as
|
|
11
|
+
* passing: a check states the scope it ran over and where its certainty ends,
|
|
12
|
+
* and a finding carries its own limitation.
|
|
13
|
+
*
|
|
14
|
+
* The order is the reading order: outcome first, then the checks that ran and
|
|
15
|
+
* what they did not cover, then the agenda, then the findings the agenda points
|
|
16
|
+
* at. One piece of evidence is rendered once — an excerpt a finding already
|
|
17
|
+
* carries is linked from the agenda instead of repeated, and a definition cited
|
|
18
|
+
* by two agenda entries is shown in the first and linked from the second.
|
|
19
|
+
*/
|
|
20
|
+
export function renderBrief(report) {
|
|
21
|
+
const evidence = report.evidence;
|
|
22
|
+
const ranks = hunkRanks(report.items);
|
|
23
|
+
const claimed = new Set();
|
|
24
|
+
// Finding id → the card that carries it, so an agenda entry links to the
|
|
25
|
+
// finding it cites instead of repeating its evidence.
|
|
26
|
+
const findingsAt = new Map();
|
|
27
|
+
if (evidence !== undefined) {
|
|
28
|
+
for (const [index, finding] of evidence.findings.entries()) {
|
|
29
|
+
findingsAt.set(finding.id, index + 1);
|
|
30
|
+
for (const excerpt of finding.evidence)
|
|
31
|
+
claimed.add(excerptKey(excerpt));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [
|
|
35
|
+
'<h2 id="brief-outcome">Expected outcome</h2>',
|
|
36
|
+
'<nav class="ev-links" aria-label="Evidence navigation"><a href="#brief-agenda-h">Read these first</a> · <a href="#brief-findings-h">Automatic findings</a> · <a href="#view-diff" data-view="diff">All hunks</a></nav>',
|
|
37
|
+
report.pr === undefined
|
|
38
|
+
? '<p class="ev-note">No pull request metadata was recorded for this report.</p>'
|
|
39
|
+
: renderPullRequest(report.pr),
|
|
40
|
+
evidence === undefined
|
|
41
|
+
? '<p class="ev-note">No intent cross-check was recorded for this report.</p>'
|
|
42
|
+
: renderIntent(evidence.intent, ranks),
|
|
43
|
+
report.project === undefined ? "" : renderProject(report.project),
|
|
44
|
+
evidence === undefined ? "" : renderChecks(evidence.checks),
|
|
45
|
+
evidence === undefined ? "" : renderAgenda(evidence.agenda, ranks, claimed, findingsAt),
|
|
46
|
+
evidence === undefined ? "" : renderFindings(evidence.findings, ranks),
|
|
47
|
+
]
|
|
48
|
+
.filter((part) => part !== "")
|
|
49
|
+
.join("\n");
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Repository context the diff does not show: related reverts, contributor
|
|
53
|
+
* guidelines, and sibling-file conventions. Commit subjects and paths are
|
|
54
|
+
* repository text, printed escaped; they are pointers to read, not verdicts.
|
|
55
|
+
*/
|
|
56
|
+
function renderProject(project) {
|
|
57
|
+
const rows = [];
|
|
58
|
+
for (const revert of project.reverts) {
|
|
59
|
+
const why = revert.reason.kind === "file" ? `touched ${revert.reason.file}` : `shares the word “${revert.reason.term}”`;
|
|
60
|
+
rows.push(`<li><span class="mono">${escapeHtml(revert.commit)}</span> ${escapeHtml(revert.date)} ${escapeHtml(revert.subject)} <span class="ev-note">(revert; ${escapeHtml(why)})</span></li>`);
|
|
61
|
+
}
|
|
62
|
+
for (const path of project.guidelines)
|
|
63
|
+
rows.push(`<li>Guideline: <span class="mono">${escapeHtml(path)}</span></li>`);
|
|
64
|
+
for (const convention of project.conventions) {
|
|
65
|
+
const names = convention.common.map((entry) => `${entry.name} (${entry.peers}/${convention.peers})`).join(", ");
|
|
66
|
+
rows.push(`<li>New <span class="mono">${escapeHtml(convention.file)}</span> uses none of what most <span class="mono">${escapeHtml(convention.pattern)}</span> files use: <span class="mono">${escapeHtml(names)}</span></li>`);
|
|
67
|
+
}
|
|
68
|
+
const shallow = project.history === "shallow"
|
|
69
|
+
? '<p class="ev-note">This clone is shallow: line history and reverts before its boundary are missing.</p>'
|
|
70
|
+
: "";
|
|
71
|
+
return [
|
|
72
|
+
'<h2 id="brief-project">Project context</h2>',
|
|
73
|
+
shallow,
|
|
74
|
+
rows.length === 0 ? '<p class="ev-note">No related reverts, guidelines, or sibling conventions were found.</p>' : `<ul>${rows.join("")}</ul>`,
|
|
75
|
+
].filter((part) => part !== "").join("\n");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The only external URL the report will ever emit: a github.com pull request
|
|
79
|
+
* link. The URL arrives from whatever API caller produced the report, so it is
|
|
80
|
+
* validated against this exact shape before it becomes an `href`; anything else
|
|
81
|
+
* is printed as escaped text, so a `javascript:`, `data:`, userinfo or
|
|
82
|
+
* off-host URL cannot become a link even if the report is opened locally.
|
|
83
|
+
*/
|
|
84
|
+
const PULL_REQUEST_URL = /^https:\/\/github\.com\/[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+\/pull\/\d+$/;
|
|
85
|
+
/** The exact pull request metadata, as untrusted text under its own labels. */
|
|
86
|
+
function renderPullRequest(pr) {
|
|
87
|
+
const range = pr.baseRef === undefined || pr.headRef === undefined
|
|
88
|
+
? ""
|
|
89
|
+
: `<dt>Range</dt><dd class="mono">${escapeHtml(`${pr.baseRef} → ${pr.headRef}`)}</dd>`;
|
|
90
|
+
const url = pr.url ?? "";
|
|
91
|
+
const source = url === ""
|
|
92
|
+
? ""
|
|
93
|
+
: `<dt>Source</dt><dd class="mono">${PULL_REQUEST_URL.test(url)
|
|
94
|
+
? `<a href="${escapeHtml(url)}" rel="noreferrer noopener">${escapeHtml(url)}</a>`
|
|
95
|
+
: escapeHtml(url)}</dd>`;
|
|
96
|
+
return [
|
|
97
|
+
'<h3 id="brief-pr">Pull request</h3>',
|
|
98
|
+
'<dl class="pr-facts">',
|
|
99
|
+
'<dt>Title</dt>',
|
|
100
|
+
`<dd class="pr-title">${pr.title === "" ? '<span class="muted">The pull request has no title.</span>' : escapeHtml(pr.title)}</dd>`,
|
|
101
|
+
source,
|
|
102
|
+
range,
|
|
103
|
+
"</dl>",
|
|
104
|
+
pr.body === ""
|
|
105
|
+
? '<p class="ev-note">The pull request has no description. Nothing in it states an expected outcome.</p>'
|
|
106
|
+
: [
|
|
107
|
+
'<details class="pr-body">',
|
|
108
|
+
'<summary>PR description — untrusted claims, including any generated notes</summary>',
|
|
109
|
+
`<pre class="pr-text">${escapeHtml(pr.body)}</pre>`,
|
|
110
|
+
"</details>",
|
|
111
|
+
].join("\n"),
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
/** Verdict, the statements it is about, and the requirements nothing answered. */
|
|
115
|
+
function renderIntent(intent, ranks) {
|
|
116
|
+
const verdict = VERDICT[intent.verdict];
|
|
117
|
+
const claims = intent.claims.map((claim) => renderClaim(claim, ranks)).join("\n");
|
|
118
|
+
return [
|
|
119
|
+
'<h3 id="brief-intent-h">Intent cross-check</h3>',
|
|
120
|
+
`<div class="intent intent-${verdict.tone}">`,
|
|
121
|
+
`<p class="intent-verdict">${badge(verdict.label, verdict.tone, verdict.hint)}</p>`,
|
|
122
|
+
`<p class="intent-summary">${escapeHtml(intent.summary)}</p>`,
|
|
123
|
+
claims === "" ? '<p class="ev-note">No intent statements were extracted; inspect the exact description above.</p>' : `<details class="intent-claims"><summary>Compare ${intent.claims.length} intent statements with changed code</summary><ul class="claims">${claims}</ul></details>`,
|
|
124
|
+
renderObligations(intent.obligations),
|
|
125
|
+
"</div>",
|
|
126
|
+
]
|
|
127
|
+
.filter((part) => part !== "")
|
|
128
|
+
.join("\n");
|
|
129
|
+
}
|
|
130
|
+
function renderClaim(claim, ranks) {
|
|
131
|
+
const status = CLAIM_STATUS[claim.status];
|
|
132
|
+
const links = claim.unitIds.length === 0 ? "" : `<p class="claim-links">${hunkLinks(claim.unitIds, "", ranks)}</p>`;
|
|
133
|
+
return [
|
|
134
|
+
'<li class="claim">',
|
|
135
|
+
'<p class="claim-head">',
|
|
136
|
+
`<span class="claim-origin">${escapeHtml(ORIGIN_LABEL[claim.origin])}</span>`,
|
|
137
|
+
badge(status.label, status.tone, status.hint),
|
|
138
|
+
"</p>",
|
|
139
|
+
`<p class="claim-text">${escapeHtml(claim.text)}</p>`,
|
|
140
|
+
claim.explanation === "" ? "" : `<p class="claim-note">${escapeHtml(claim.explanation)}</p>`,
|
|
141
|
+
links,
|
|
142
|
+
"</li>",
|
|
143
|
+
]
|
|
144
|
+
.filter((part) => part !== "")
|
|
145
|
+
.join("");
|
|
146
|
+
}
|
|
147
|
+
/** Review decisions and proof obligations still open after static inspection. */
|
|
148
|
+
function renderObligations(obligations) {
|
|
149
|
+
if (obligations.length === 0)
|
|
150
|
+
return "";
|
|
151
|
+
return [
|
|
152
|
+
'<details class="obligations">',
|
|
153
|
+
`<summary>${obligations.length} open review decision${obligations.length === 1 ? "" : "s"} and proof obligations</summary>`,
|
|
154
|
+
`<ul>${obligations.map((text) => `<li>${escapeHtml(text)}</li>`).join("")}</ul>`,
|
|
155
|
+
"</details>",
|
|
156
|
+
].join("");
|
|
157
|
+
}
|
|
158
|
+
/** What each check ran over, in the words of the check itself. */
|
|
159
|
+
function renderChecks(checks) {
|
|
160
|
+
const rows = checks
|
|
161
|
+
.map((check) => {
|
|
162
|
+
const status = CHECK_STATUS[check.status];
|
|
163
|
+
return [
|
|
164
|
+
'<li class="check">',
|
|
165
|
+
'<details><summary class="check-head">',
|
|
166
|
+
`<span class="check-kind">${escapeHtml(KIND_LABEL[check.kind])}</span>`,
|
|
167
|
+
badge(status.label, status.tone, status.hint),
|
|
168
|
+
"</summary>",
|
|
169
|
+
`<p class="check-detail">${escapeHtml(check.detail)}</p>`,
|
|
170
|
+
"</details></li>",
|
|
171
|
+
].join("");
|
|
172
|
+
})
|
|
173
|
+
.join("\n");
|
|
174
|
+
return [
|
|
175
|
+
'<h3 id="brief-checks-h">Checks and their limits</h3>',
|
|
176
|
+
rows === "" ? '<p class="ev-note">No check reported a result for this pull request.</p>' : `<ul class="checks">${rows}</ul>`,
|
|
177
|
+
// Silence is not a result: a kind that ran is listed above, whatever it found.
|
|
178
|
+
'<p class="ev-note">Each line states the scope its check ran over. A check that is not listed here produced no result for this pull request.</p>',
|
|
179
|
+
].join("\n");
|
|
180
|
+
}
|
|
181
|
+
/** The reading agenda, in the order the report ranked it. */
|
|
182
|
+
function renderAgenda(entries, ranks, claimed, findingsAt) {
|
|
183
|
+
if (entries.length === 0) {
|
|
184
|
+
return [
|
|
185
|
+
'<h3 id="brief-agenda-h">Read these first</h3>',
|
|
186
|
+
'<p class="ev-note">No reading agenda was produced for this pull request.</p>',
|
|
187
|
+
].join("\n");
|
|
188
|
+
}
|
|
189
|
+
// A definition two agenda entries cite is shown once and linked after that.
|
|
190
|
+
const contexts = new Map();
|
|
191
|
+
const cards = entries.map((entry, index) => renderAgendaEntry(entry, index + 1, ranks, claimed, findingsAt, contexts));
|
|
192
|
+
return [
|
|
193
|
+
'<h3 id="brief-agenda-h">Read these first</h3>',
|
|
194
|
+
`<ol class="agenda">${cards.slice(0, 5).join("\n")}</ol>`,
|
|
195
|
+
cards.length > 5 ? `<details class="supporting-agenda"><summary>${cards.length - 5} more review entries and supporting changes</summary><ol class="agenda" start="6">${cards.slice(5).join("\n")}</ol></details>` : "",
|
|
196
|
+
'<p class="ev-note">Evidence cards are places to read — changed code, direct caller, contract, related code or test — not results.</p>',
|
|
197
|
+
].join("\n");
|
|
198
|
+
}
|
|
199
|
+
function renderAgendaEntry(entry, at, ranks, claimed, findingsAt, contexts) {
|
|
200
|
+
// One excerpt is rendered once; what a finding already carries is linked.
|
|
201
|
+
const shown = entry.evidence.filter((excerpt) => !claimed.has(excerptKey(excerpt)));
|
|
202
|
+
const file = entry.evidence[0]?.file ?? "";
|
|
203
|
+
const links = entry.unitIds.length === 0 ? "" : `<p class="ag-links">${hunkLinks(entry.unitIds, file, ranks)}</p>`;
|
|
204
|
+
const findings = entry.findingIds
|
|
205
|
+
.map((id) => {
|
|
206
|
+
const where = findingsAt.get(id);
|
|
207
|
+
return where === undefined ? "" : `<a class="ag-finding" href="#finding-${where}">Finding ${where}</a>`;
|
|
208
|
+
})
|
|
209
|
+
.filter((link) => link !== "")
|
|
210
|
+
.join("");
|
|
211
|
+
const context = entry.context.map((node) => renderContextNode(node, contexts)).filter((part) => part !== "").join("");
|
|
212
|
+
const linked = linkedRanks(entry.unitIds, file, ranks);
|
|
213
|
+
const evidence = shown.map((excerpt) => renderEvidence(excerpt, ranks, linked)).join("");
|
|
214
|
+
return [
|
|
215
|
+
`<li class="ag-card" id="agenda-${at}">`,
|
|
216
|
+
'<p class="ag-head">',
|
|
217
|
+
`<span class="ag-rank mono" aria-hidden="true">${at}</span>`,
|
|
218
|
+
`<span class="ag-title">${escapeHtml(entry.title)}</span>`,
|
|
219
|
+
findings,
|
|
220
|
+
"</p>",
|
|
221
|
+
entry.reason === "" ? "" : `<p class="ag-reason">${escapeHtml(entry.reason)}</p>`,
|
|
222
|
+
links,
|
|
223
|
+
context === "" ? "" : `<div class="ag-ctx-list">${context}</div>`,
|
|
224
|
+
evidence === "" ? "" : `<ul class="evidence">${evidence}</ul>`,
|
|
225
|
+
"</li>",
|
|
226
|
+
]
|
|
227
|
+
.filter((part) => part !== "")
|
|
228
|
+
.join("");
|
|
229
|
+
}
|
|
230
|
+
/** One definition an agenda entry reads: shown once, linked when cited again. */
|
|
231
|
+
function renderContextNode(node, seen) {
|
|
232
|
+
const key = `${node.key}\u0000${node.file}\u0000${node.line}`;
|
|
233
|
+
const shown = seen.get(key);
|
|
234
|
+
if (shown !== undefined) {
|
|
235
|
+
return `<a class="ag-ctx-again" href="#ctx-${shown}">${escapeHtml(locationOf(node.file, node.line))} shown above</a>`;
|
|
236
|
+
}
|
|
237
|
+
const at = seen.size + 1;
|
|
238
|
+
seen.set(key, at);
|
|
239
|
+
return [
|
|
240
|
+
`<details class="ag-ctx" id="ctx-${at}">`,
|
|
241
|
+
`<summary class="ag-ctx-sum"><span class="mono">${escapeHtml(locationOf(node.file, node.line))}</span> ${escapeHtml(node.label)}</summary>`,
|
|
242
|
+
node.detail === ""
|
|
243
|
+
? '<p class="ev-note">No text was captured for this definition.</p>'
|
|
244
|
+
: `<pre class="ev-code ag-ctx-code">${escapeHtml(node.detail)}</pre>`,
|
|
245
|
+
"</details>",
|
|
246
|
+
].join("");
|
|
247
|
+
}
|
|
248
|
+
/** The automatic findings, each with the scope it ran over and its own limit. */
|
|
249
|
+
function renderFindings(findings, ranks) {
|
|
250
|
+
if (findings.length === 0) {
|
|
251
|
+
return [
|
|
252
|
+
'<h3 id="brief-findings-h">Automatic findings</h3>',
|
|
253
|
+
'<p class="ev-note">No automatic finding was reported. The checks above state the scope that ran, and an empty list is not a statement about the code.</p>',
|
|
254
|
+
].join("\n");
|
|
255
|
+
}
|
|
256
|
+
const rendered = new Set();
|
|
257
|
+
const cards = findings
|
|
258
|
+
.map((finding, index) => {
|
|
259
|
+
const linked = linkedRanks(finding.unitIds, finding.evidence[0]?.file ?? "", ranks);
|
|
260
|
+
const evidence = finding.evidence
|
|
261
|
+
.filter((excerpt) => {
|
|
262
|
+
const key = excerptKey(excerpt);
|
|
263
|
+
if (rendered.has(key))
|
|
264
|
+
return false;
|
|
265
|
+
rendered.add(key);
|
|
266
|
+
return true;
|
|
267
|
+
})
|
|
268
|
+
.map((excerpt) => renderEvidence(excerpt, ranks, linked))
|
|
269
|
+
.join("");
|
|
270
|
+
return [
|
|
271
|
+
`<li class="finding" id="finding-${index + 1}">`,
|
|
272
|
+
'<p class="finding-head">',
|
|
273
|
+
`<span class="finding-kind">${escapeHtml(KIND_LABEL[finding.kind])}</span>`,
|
|
274
|
+
`<span class="finding-title">${escapeHtml(finding.title)}</span>`,
|
|
275
|
+
"</p>",
|
|
276
|
+
`<p class="finding-scope"><span class="finding-label">Checked</span> ${escapeHtml(finding.scope)}</p>`,
|
|
277
|
+
`<p class="finding-limit"><span class="finding-label">Limit</span> ${escapeHtml(finding.limitation)}</p>`,
|
|
278
|
+
finding.unitIds.length === 0 ? "" : `<p class="finding-hunks">${hunkLinks(finding.unitIds, finding.evidence[0]?.file ?? "", ranks)}</p>`,
|
|
279
|
+
evidence === "" ? "" : `<ul class="evidence">${evidence}</ul>`,
|
|
280
|
+
"</li>",
|
|
281
|
+
]
|
|
282
|
+
.filter((part) => part !== "")
|
|
283
|
+
.join("");
|
|
284
|
+
})
|
|
285
|
+
.join("\n");
|
|
286
|
+
return ['<h3 id="brief-findings-h">Automatic findings</h3>', `<ul class="findings">${cards}</ul>`].join("\n");
|
|
287
|
+
}
|
|
288
|
+
/** One change/caller/contract card: where it is, how to reach it, and its text. */
|
|
289
|
+
function renderEvidence(excerpt, ranks, linked) {
|
|
290
|
+
return [
|
|
291
|
+
'<li class="ev-card"><details>',
|
|
292
|
+
'<summary class="ev-head">',
|
|
293
|
+
`<span class="ev-role">${escapeHtml(ROLE_LABEL[excerpt.role])}</span>`,
|
|
294
|
+
`<span class="ev-where mono">${escapeHtml(locationOf(excerpt.file, excerpt.line, excerpt.endLine))}</span>`,
|
|
295
|
+
excerpt.ref === "" ? "" : `<span class="ev-ref mono">from ${escapeHtml(/^[0-9a-f]{40,64}$/iu.test(excerpt.ref) ? excerpt.ref.slice(0, 12) : excerpt.ref)}</span>`,
|
|
296
|
+
hunkLinks([], excerpt.file, ranks, linked),
|
|
297
|
+
"</summary>",
|
|
298
|
+
excerpt.label === "" ? "" : `<p class="ev-label">${escapeHtml(excerpt.label)}</p>`,
|
|
299
|
+
excerpt.text === ""
|
|
300
|
+
? '<p class="ev-note">No source text was captured for this excerpt.</p>'
|
|
301
|
+
: `<pre class="ev-code">${escapeHtml(excerpt.text)}</pre>`,
|
|
302
|
+
"</details></li>",
|
|
303
|
+
]
|
|
304
|
+
.filter((part) => part !== "")
|
|
305
|
+
.join("");
|
|
306
|
+
}
|
|
307
|
+
function hunkRanks(items) {
|
|
308
|
+
const unit = new Map();
|
|
309
|
+
const file = new Map();
|
|
310
|
+
for (const [index, item] of items.entries()) {
|
|
311
|
+
const rank = index + 1;
|
|
312
|
+
if (!unit.has(item.id))
|
|
313
|
+
unit.set(item.id, rank);
|
|
314
|
+
if (!file.has(item.file))
|
|
315
|
+
file.set(item.file, rank);
|
|
316
|
+
}
|
|
317
|
+
return { unit, file };
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The report ranks a card's links resolve to, in order: the hunks it names by
|
|
321
|
+
* unit id, or the first hunk of its file when it names none. A rank another link
|
|
322
|
+
* on the same card already points at is left out, so one card never offers the
|
|
323
|
+
* same hunk twice.
|
|
324
|
+
*/
|
|
325
|
+
function linksToRanks(unitIds, file, ranks, skip = new Set()) {
|
|
326
|
+
const found = [];
|
|
327
|
+
for (const id of unitIds) {
|
|
328
|
+
const rank = ranks.unit.get(id);
|
|
329
|
+
if (rank !== undefined && !found.includes(rank))
|
|
330
|
+
found.push(rank);
|
|
331
|
+
}
|
|
332
|
+
if (found.length === 0) {
|
|
333
|
+
const rank = ranks.file.get(file);
|
|
334
|
+
if (rank !== undefined)
|
|
335
|
+
found.push(rank);
|
|
336
|
+
}
|
|
337
|
+
return found.filter((rank) => !skip.has(rank));
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Links that open a hunk in the diff. The link carries a flag the page script
|
|
341
|
+
* acts on: it clears any hiding filter and focus mode first, so a hunk the
|
|
342
|
+
* current view hides is still reachable.
|
|
343
|
+
*/
|
|
344
|
+
function hunkLinks(unitIds, file, ranks, skip = new Set()) {
|
|
345
|
+
return linksToRanks(unitIds, file, ranks, skip)
|
|
346
|
+
.map((rank) => `<a class="ev-hunk" data-open-hunk href="#item-${rank}">Open hunk #${rank}</a>`)
|
|
347
|
+
.join(" ");
|
|
348
|
+
}
|
|
349
|
+
/** The ranks a card's own hunk links resolve to, for the evidence inside it. */
|
|
350
|
+
function linkedRanks(unitIds, file, ranks) {
|
|
351
|
+
return new Set(linksToRanks(unitIds, file, ranks));
|
|
352
|
+
}
|
|
353
|
+
/** Evidence is identified by its own id, or by where it was read from. */
|
|
354
|
+
function excerptKey(excerpt) {
|
|
355
|
+
return excerpt.id === "" ? `${excerpt.file}:${excerpt.line}:${excerpt.ref}` : excerpt.id;
|
|
356
|
+
}
|
|
357
|
+
function locationOf(file, line, endLine) {
|
|
358
|
+
if (!Number.isFinite(line) || line <= 0)
|
|
359
|
+
return file;
|
|
360
|
+
if (endLine === undefined || !Number.isFinite(endLine) || endLine <= line)
|
|
361
|
+
return `${file}:${line}`;
|
|
362
|
+
return `${file}:${line}-${endLine}`;
|
|
363
|
+
}
|
|
364
|
+
/** One status word, with the reason for it as the tooltip and no color alone. */
|
|
365
|
+
function badge(label, tone, hint) {
|
|
366
|
+
return `<span class="ev-badge ev-badge-${tone}" title="${escapeHtml(hint)}">${escapeHtml(label)}</span>`;
|
|
367
|
+
}
|
|
368
|
+
const ROLE_LABEL = {
|
|
369
|
+
change: "Changed code",
|
|
370
|
+
caller: "Direct caller",
|
|
371
|
+
contract: "Contract",
|
|
372
|
+
related: "Related code",
|
|
373
|
+
test: "Test",
|
|
374
|
+
};
|
|
375
|
+
/** Kind names, shared by a check's coverage line and the finding it produces. */
|
|
376
|
+
const KIND_LABEL = {
|
|
377
|
+
"unused-error-result": "Unused failure result",
|
|
378
|
+
"duplicate-body": "Duplicate function body",
|
|
379
|
+
"broken-reference": "Broken reference",
|
|
380
|
+
};
|
|
381
|
+
const CHECK_STATUS = {
|
|
382
|
+
checked: { label: "Checked", tone: "done", hint: "the check ran over the whole change" },
|
|
383
|
+
partial: { label: "Partly checked", tone: "warn", hint: "the check ran over part of the change" },
|
|
384
|
+
"not-checked": { label: "Not checked", tone: "warn", hint: "no trusted result is available" },
|
|
385
|
+
};
|
|
386
|
+
const VERDICT = {
|
|
387
|
+
"supported-within-checked-scope": {
|
|
388
|
+
label: "Supported within the checked scope",
|
|
389
|
+
tone: "done",
|
|
390
|
+
hint: "the changed code carries the statement, within the scope the checks covered",
|
|
391
|
+
},
|
|
392
|
+
"not-established": {
|
|
393
|
+
label: "Not established",
|
|
394
|
+
tone: "warn",
|
|
395
|
+
hint: "the gathered evidence neither carries nor contradicts the statement",
|
|
396
|
+
},
|
|
397
|
+
contradicted: {
|
|
398
|
+
label: "Contradicted by a check",
|
|
399
|
+
tone: "warn",
|
|
400
|
+
hint: "a check reports the opposite of the statement",
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
const CLAIM_STATUS = {
|
|
404
|
+
"evidence-linked": {
|
|
405
|
+
label: "Evidence linked",
|
|
406
|
+
tone: "done",
|
|
407
|
+
hint: "changed code carries this statement",
|
|
408
|
+
},
|
|
409
|
+
"not-established": {
|
|
410
|
+
label: "Not established",
|
|
411
|
+
tone: "warn",
|
|
412
|
+
hint: "no changed code was linked to this statement",
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
const ORIGIN_LABEL = {
|
|
416
|
+
title: "PR title",
|
|
417
|
+
author: "Author description",
|
|
418
|
+
"generated-summary": "Generated summary",
|
|
419
|
+
};
|
|
420
|
+
export const BRIEF_STYLES = `
|
|
421
|
+
.brief { display: flex; flex-direction: column; gap: 7px; }
|
|
422
|
+
.brief > h2 { font-size: 13.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent); margin-top: 7px; }
|
|
423
|
+
.brief > h2:first-child { margin-top: 0; }
|
|
424
|
+
.brief h3 { font-size: 13px; color: var(--ink-soft); font-weight: 600; }
|
|
425
|
+
.pr-facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 12px; margin: 0; }
|
|
426
|
+
.pr-facts dt { font-size: 12px; color: var(--ink-soft); }
|
|
427
|
+
.pr-facts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
|
428
|
+
.pr-title { font-size: 17px; font-weight: 700; }
|
|
429
|
+
.pr-body > summary { cursor: pointer; font-size: 12.5px; color: var(--ink-soft); }
|
|
430
|
+
.pr-text {
|
|
431
|
+
margin: 6px 0 0;
|
|
432
|
+
padding: 9px 11px;
|
|
433
|
+
border: 1px solid var(--line);
|
|
434
|
+
border-radius: 6px;
|
|
435
|
+
background: var(--sunken);
|
|
436
|
+
font-family: var(--mono);
|
|
437
|
+
font-size: 12.5px;
|
|
438
|
+
line-height: 1.5;
|
|
439
|
+
white-space: pre-wrap;
|
|
440
|
+
overflow-wrap: anywhere;
|
|
441
|
+
max-height: 40vh;
|
|
442
|
+
overflow: auto;
|
|
443
|
+
}
|
|
444
|
+
.intent { border: 1px solid var(--line); border-left: 5px solid var(--line-strong); border-radius: 6px; padding: 10px 14px; background: var(--panel); }
|
|
445
|
+
.intent-done { border-left-color: var(--teal); }
|
|
446
|
+
.intent-warn { border-left-color: var(--warn); }
|
|
447
|
+
.intent-summary { font-size: 14.5px; overflow-wrap: anywhere; }
|
|
448
|
+
.intent-verdict { margin-bottom: 4px; }
|
|
449
|
+
.ev-badge {
|
|
450
|
+
display: inline-flex;
|
|
451
|
+
align-items: center;
|
|
452
|
+
gap: 6px;
|
|
453
|
+
border: 1px solid var(--line-strong);
|
|
454
|
+
border-radius: 999px;
|
|
455
|
+
padding: 2px 10px;
|
|
456
|
+
font-size: 11.5px;
|
|
457
|
+
font-weight: 600;
|
|
458
|
+
white-space: nowrap;
|
|
459
|
+
}
|
|
460
|
+
.ev-badge-done { border-color: var(--teal); color: var(--teal); background: var(--teal-bg); }
|
|
461
|
+
.ev-badge-warn { border-color: var(--warn); color: var(--warn); background: var(--warn-bg); }
|
|
462
|
+
.claims, .checks, .findings, .evidence { list-style: none; margin: 5px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
463
|
+
.claim, .check, .finding, .ev-card { border: 1px solid var(--line); border-radius: 6px; padding: 6px 9px; background: var(--panel); }
|
|
464
|
+
.claim-head, .check-head, .finding-head, .ev-head, .ag-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
465
|
+
summary.ev-head, summary.check-head { cursor: pointer; }
|
|
466
|
+
summary.ev-head::before, summary.check-head::before { content: "+"; font-family: var(--mono); }
|
|
467
|
+
details[open] > summary.ev-head::before, details[open] > summary.check-head::before { content: "-"; font-family: var(--mono); }
|
|
468
|
+
.claim-origin, .ev-role, .finding-kind, .check-kind { font-size: 11.5px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-soft); }
|
|
469
|
+
.claim-text, .ag-title, .finding-title { font-size: 14.5px; font-weight: 600; overflow-wrap: anywhere; }
|
|
470
|
+
.claim-note, .ag-reason { font-size: 13px; color: var(--ink-soft); overflow-wrap: anywhere; }
|
|
471
|
+
.check-detail, .finding-scope, .finding-limit, .ev-label, .ev-note, .ag-links, .finding-hunks, .claim-links {
|
|
472
|
+
font-size: 12.5px;
|
|
473
|
+
color: var(--ink-soft);
|
|
474
|
+
overflow-wrap: anywhere;
|
|
475
|
+
margin-top: 3px;
|
|
476
|
+
}
|
|
477
|
+
.finding-label { font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; font-size: 11px; }
|
|
478
|
+
.ev-where, .ev-ref { font-size: 12px; color: var(--ink-soft); overflow-wrap: anywhere; }
|
|
479
|
+
.ev-card { background: var(--sunken); }
|
|
480
|
+
.ev-code {
|
|
481
|
+
margin: 6px 0 0;
|
|
482
|
+
padding: 7px 9px;
|
|
483
|
+
border: 1px solid var(--line);
|
|
484
|
+
border-radius: 6px;
|
|
485
|
+
background: var(--panel);
|
|
486
|
+
font-family: var(--mono);
|
|
487
|
+
font-size: 12px;
|
|
488
|
+
line-height: 1.5;
|
|
489
|
+
max-height: 26vh;
|
|
490
|
+
overflow: auto;
|
|
491
|
+
tab-size: 4;
|
|
492
|
+
}
|
|
493
|
+
.ev-hunk { font-size: 12px; white-space: nowrap; }
|
|
494
|
+
.agenda { list-style: none; counter-reset: agenda; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
495
|
+
.ag-card { border: 1px solid var(--line); border-left: 5px solid var(--line-strong); border-radius: 6px; padding: 9px 12px; background: var(--panel); }
|
|
496
|
+
.ag-rank {
|
|
497
|
+
display: inline-flex;
|
|
498
|
+
align-items: center;
|
|
499
|
+
justify-content: center;
|
|
500
|
+
min-width: 20px;
|
|
501
|
+
height: 20px;
|
|
502
|
+
border-radius: 999px;
|
|
503
|
+
background: var(--sunken);
|
|
504
|
+
border: 1px solid var(--line);
|
|
505
|
+
font-size: 11.5px;
|
|
506
|
+
font-weight: 700;
|
|
507
|
+
color: var(--ink-soft);
|
|
508
|
+
}
|
|
509
|
+
.ag-ctx-list { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
|
|
510
|
+
.ag-ctx > summary { cursor: pointer; font-size: 12.5px; color: var(--ink-soft); overflow-wrap: anywhere; }
|
|
511
|
+
.ag-ctx { border: 1px dashed var(--line-strong); border-radius: 6px; padding: 6px 9px; }
|
|
512
|
+
.ag-ctx-again { font-size: 12.5px; }
|
|
513
|
+
.obligations { margin-top: 8px; }
|
|
514
|
+
.obligations > summary { cursor: pointer; font-size: 12.5px; color: var(--ink-soft); }
|
|
515
|
+
.obligations ul { margin: 6px 0 0; padding-left: 20px; font-size: 13px; }
|
|
516
|
+
.obligations li { overflow-wrap: anywhere; }
|
|
517
|
+
@media (max-width: 680px) {
|
|
518
|
+
.pr-facts { grid-template-columns: minmax(0, 1fr); gap: 0 0; }
|
|
519
|
+
.pr-facts dd { margin-bottom: 6px; }
|
|
520
|
+
}
|
|
521
|
+
`;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { type SyntaxNode, type Tree } from "../languages/types.js";
|
|
2
|
+
/** A fragment parsed without syntax errors, and the row shift a wrapper added. */
|
|
3
|
+
export interface FragmentParse {
|
|
4
|
+
tree: Tree;
|
|
5
|
+
/**
|
|
6
|
+
* Rows of the parse above the fragment's first line: 1 after the container
|
|
7
|
+
* wrapper, 0 for the text as written. Callers convert a node row to a source
|
|
8
|
+
* line by subtracting this from the row and adding the definition's line.
|
|
9
|
+
*/
|
|
10
|
+
lineOffset: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parse one definition fragment, trying the text as written first and then the
|
|
14
|
+
* two wrappers above. The first attempt whose whole tree is error-free wins, so
|
|
15
|
+
* the same fragment always yields the same tree.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseFragment(file: string, text: string): FragmentParse | null;
|
|
18
|
+
/** Imports in a module prefix; an unfinished following class does not invalidate preceding imports. */
|
|
19
|
+
export declare function moduleImports(file: string, prefix: string): Map<string, string> | null;
|
|
20
|
+
/** A function-like node and the body its grammar gives it. */
|
|
21
|
+
export interface DefinitionSyntax {
|
|
22
|
+
definition: SyntaxNode;
|
|
23
|
+
body: SyntaxNode;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The definition's own body. The fragment holds either the definition itself or
|
|
27
|
+
* one wrapper (a class the member was written in, or a binding the function
|
|
28
|
+
* value was assigned to), so the outermost node with a callable body is the
|
|
29
|
+
* definition being read. Breadth-first order picks the shallowest one, which
|
|
30
|
+
* keeps a large nested callback inside a default parameter from being taken for
|
|
31
|
+
* the definition's body, and ties are broken by position, so the choice is
|
|
32
|
+
* stable.
|
|
33
|
+
*/
|
|
34
|
+
export declare function definitionSyntax(fragment: FragmentParse): DefinitionSyntax | null;
|
|
35
|
+
/**
|
|
36
|
+
* Comment-free token stream of a body: leaf text in source order, one space
|
|
37
|
+
* between tokens. Formatting and comments are gone; every literal and operator
|
|
38
|
+
* is kept exactly as written, so two bodies match only when their tokens do.
|
|
39
|
+
*/
|
|
40
|
+
export declare function bodyTokenSignature(body: SyntaxNode): string;
|
|
41
|
+
/** Declared response type name of a definition's return annotation, if any. */
|
|
42
|
+
export declare function returnedResponseType(definition: SyntaxNode): string | null;
|
|
43
|
+
/** One field declared in an interface or type-alias object. */
|
|
44
|
+
export interface ContractField {
|
|
45
|
+
name: string;
|
|
46
|
+
type: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The declared response type name of a type annotation, or null when the
|
|
50
|
+
* annotation declares something else. Only a plain name (`CreateResult`) or a
|
|
51
|
+
* known async/readonly wrapper of one (`Promise<CreateResult>`) is a response:
|
|
52
|
+
* an array of them (`CreateResult[]`), an arbitrary generic
|
|
53
|
+
* (`Wrapper<CreateResult>`), a union, or a function type is a different thing,
|
|
54
|
+
* and this check must not treat it as holding one response.
|
|
55
|
+
*/
|
|
56
|
+
export declare function declaredResponseType(annotation: SyntaxNode | null | undefined, awaited?: boolean): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Declared fields of the named interface or type alias in a fragment, or null
|
|
59
|
+
* when that declaration is not the one the fragment holds. The name is required
|
|
60
|
+
* because one line can carry several declarations: a fragment read for `B` that
|
|
61
|
+
* holds both `A` and `B` must never report `A`'s fields as `B`'s. `extends` and
|
|
62
|
+
* intersections are not followed, so this is only ever the fields written in the
|
|
63
|
+
* declaration itself; an empty array means it writes no field, which is a
|
|
64
|
+
* different answer from a declaration nobody could read.
|
|
65
|
+
*/
|
|
66
|
+
export declare function contractFields(fragment: FragmentParse, name: string): ContractField[] | null;
|
|
67
|
+
/** A name a response value is bound to, and where the binding was written. */
|
|
68
|
+
export interface TypedBinding {
|
|
69
|
+
name: string;
|
|
70
|
+
/** 0-based row inside the parsed fragment. */
|
|
71
|
+
row: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Names declared with a type annotation that names `typeName`: a parameter, a
|
|
75
|
+
* local, or a class field. Only a plainly named binding is returned; a
|
|
76
|
+
* destructuring pattern is left out, because its fields are read positions
|
|
77
|
+
* rather than one value the checks can follow.
|
|
78
|
+
*/
|
|
79
|
+
export declare function typedBindings(definition: SyntaxNode, typeName: string): TypedBinding[];
|
|
80
|
+
/** Every node in the fragment, in pre-order. */
|
|
81
|
+
export declare function walkSyntax(root: SyntaxNode, visit: (node: SyntaxNode) => void): void;
|
|
82
|
+
/**
|
|
83
|
+
* The value one expression counts, when the expression is a count-named read.
|
|
84
|
+
* `users.length` counts the binding `users`; `result.users.length` counts the
|
|
85
|
+
* field `users` read off the binding `result`. Both shapes report a number, and
|
|
86
|
+
* nothing else does: a plain `.length` on a call or an element access is not
|
|
87
|
+
* attributed to a binding, because the checks only follow named values.
|
|
88
|
+
*/
|
|
89
|
+
export interface CountRead {
|
|
90
|
+
/** Expression that reports the count, e.g. `result.users.length`. */
|
|
91
|
+
read: SyntaxNode;
|
|
92
|
+
/** Name of the value counted, e.g. `users`. */
|
|
93
|
+
value: string;
|
|
94
|
+
/** Whether the value is a field read off another name rather than a binding. */
|
|
95
|
+
ofField: boolean;
|
|
96
|
+
}
|
|
97
|
+
export declare function countReadOf(node: SyntaxNode): CountRead | null;
|
|
98
|
+
/**
|
|
99
|
+
* First count-named read inside a `return`, which is the number a receiver hands
|
|
100
|
+
* back to its own caller. That is the count a partial-failure question is about,
|
|
101
|
+
* more than any diagnostic log written after it.
|
|
102
|
+
*/
|
|
103
|
+
export declare function returnedCountRead(definition: SyntaxNode): CountRead | null;
|
|
104
|
+
/**
|
|
105
|
+
* State/status properties in a call payload. A queue routing constant or a
|
|
106
|
+
* state-looking word inside a string is not by itself a state assignment.
|
|
107
|
+
*/
|
|
108
|
+
export declare function stateWritesAt(call: SyntaxNode): string[];
|
|
109
|
+
/** Call expressions written in one definition, in source order. */
|
|
110
|
+
export declare function callsIn(definition: SyntaxNode): SyntaxNode[];
|
|
111
|
+
/**
|
|
112
|
+
* Count-named reads in a body, in source order: the numbers a receiver reports,
|
|
113
|
+
* which are what a partial-failure question compares with the failures it never
|
|
114
|
+
* read.
|
|
115
|
+
*/
|
|
116
|
+
export declare function countReads(definition: SyntaxNode): CountRead[];
|
|
117
|
+
/**
|
|
118
|
+
* Whether two node handles denote the same syntax node. Tree-sitter hands back
|
|
119
|
+
* a fresh wrapper per field access, so `===` on handles is not a stable test:
|
|
120
|
+
* two handles for one node can differ between calls. Node ids are stable within
|
|
121
|
+
* one tree, which is what every structural question here compares.
|
|
122
|
+
*/
|
|
123
|
+
export declare function sameNode(left: SyntaxNode | null | undefined, right: SyntaxNode | null | undefined): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Names one definition declares for itself: its parameters, its locals, and any
|
|
126
|
+
* nested declaration. A call written under one of these names reaches the local
|
|
127
|
+
* value, not whatever definition elsewhere carries the same bare key, so a
|
|
128
|
+
* caller-side check must not resolve through the shadow.
|
|
129
|
+
*/
|
|
130
|
+
export declare function declaredValueNames(definition: SyntaxNode): Set<string>;
|
|
131
|
+
/** Value of a string literal node, or the raw text of any other node. */
|
|
132
|
+
export declare function staticStringValue(node: SyntaxNode): string;
|