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,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tree-sitter views of one definition's own source, used by the automatic
|
|
3
|
+
* evidence checks.
|
|
4
|
+
*
|
|
5
|
+
* The checks read definitions from snapshot indexes rather than whole files, so
|
|
6
|
+
* every parse starts from a single definition's text. A member definition is
|
|
7
|
+
* not a valid module on its own, so a container and a binding wrapper are
|
|
8
|
+
* retried before a fragment is reported unsupported: a fragment no attempt
|
|
9
|
+
* parses contributes no body, no return type, and no shape, and the caller says
|
|
10
|
+
* which definitions were left out instead of guessing.
|
|
11
|
+
*
|
|
12
|
+
* Every helper here is syntax-only. Nothing resolves a name to a value: a
|
|
13
|
+
* return type, a declared field, or a parameter binding is what the grammar
|
|
14
|
+
* wrote, and call/argument answers stay with the extractor indexes the checks
|
|
15
|
+
* already read.
|
|
16
|
+
*/
|
|
17
|
+
import Parser from "tree-sitter";
|
|
18
|
+
import { loadGrammarPackage, resolveLanguage, } from "../languages/grammars.js";
|
|
19
|
+
import { detectLanguage } from "../languages/registry.js";
|
|
20
|
+
import { collapseWs } from "../languages/types.js";
|
|
21
|
+
const parser = new Parser();
|
|
22
|
+
/** One grammar handle, or a confirmed failure that must not be retried. */
|
|
23
|
+
const grammars = new Map();
|
|
24
|
+
function languageFor(file) {
|
|
25
|
+
const extractor = detectLanguage(file);
|
|
26
|
+
if (!extractor)
|
|
27
|
+
return null;
|
|
28
|
+
const key = extractor.grammarExport
|
|
29
|
+
? `${extractor.grammarPackage}:${extractor.grammarExport}`
|
|
30
|
+
: extractor.grammarPackage;
|
|
31
|
+
const cached = grammars.get(key);
|
|
32
|
+
if (cached !== undefined)
|
|
33
|
+
return cached;
|
|
34
|
+
let language = null;
|
|
35
|
+
try {
|
|
36
|
+
language = resolveLanguage(loadGrammarPackage(extractor.grammarPackage), extractor.grammarExport);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// A grammar that cannot be installed or loaded leaves its definitions
|
|
40
|
+
// unchecked; the checks report them as unsupported rather than failing.
|
|
41
|
+
language = null;
|
|
42
|
+
}
|
|
43
|
+
grammars.set(key, language);
|
|
44
|
+
return language;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Literal text that only parses inside a container, so a class member or a
|
|
48
|
+
* bare function value can be parsed at all. The wrapper names cannot collide
|
|
49
|
+
* with an extracted definition: no definition span contains them.
|
|
50
|
+
*/
|
|
51
|
+
const CONTAINER_WRAPPER = (source) => `class __diffninjaEvidenceContainer {\n${source}\n}`;
|
|
52
|
+
const BINDING_WRAPPER = (source) => `const __diffninjaEvidenceBinding = ${source};`;
|
|
53
|
+
/**
|
|
54
|
+
* Parse one definition fragment, trying the text as written first and then the
|
|
55
|
+
* two wrappers above. The first attempt whose whole tree is error-free wins, so
|
|
56
|
+
* the same fragment always yields the same tree.
|
|
57
|
+
*/
|
|
58
|
+
export function parseFragment(file, text) {
|
|
59
|
+
const language = languageFor(file);
|
|
60
|
+
if (language === null)
|
|
61
|
+
return null;
|
|
62
|
+
const source = text.trim();
|
|
63
|
+
if (source === "")
|
|
64
|
+
return null;
|
|
65
|
+
const attempts = [
|
|
66
|
+
{ source, lineOffset: 0 },
|
|
67
|
+
{ source: CONTAINER_WRAPPER(source), lineOffset: 1 },
|
|
68
|
+
{ source: BINDING_WRAPPER(source), lineOffset: 0 },
|
|
69
|
+
];
|
|
70
|
+
for (const attempt of attempts) {
|
|
71
|
+
let tree = null;
|
|
72
|
+
try {
|
|
73
|
+
// @ts-expect-error tree-sitter Language under-specifies grammar module exports
|
|
74
|
+
parser.setLanguage(language);
|
|
75
|
+
tree = parser.parse(attempt.source);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// A grammar that rejects this fragment leaves nothing to read here.
|
|
79
|
+
tree = null;
|
|
80
|
+
}
|
|
81
|
+
if (tree === null || tree.rootNode.hasError)
|
|
82
|
+
continue;
|
|
83
|
+
return { tree, lineOffset: attempt.lineOffset };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
/** Imports in a module prefix; an unfinished following class does not invalidate preceding imports. */
|
|
88
|
+
export function moduleImports(file, prefix) {
|
|
89
|
+
const language = languageFor(file);
|
|
90
|
+
if (language === null)
|
|
91
|
+
return null;
|
|
92
|
+
let tree;
|
|
93
|
+
try {
|
|
94
|
+
// @ts-expect-error tree-sitter Language under-specifies grammar module exports
|
|
95
|
+
parser.setLanguage(language);
|
|
96
|
+
tree = parser.parse(prefix);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const imports = new Map();
|
|
102
|
+
for (const node of tree.rootNode.namedChildren) {
|
|
103
|
+
if (node.type !== "import_statement" || node.hasError)
|
|
104
|
+
continue;
|
|
105
|
+
const clause = node.namedChildren.find(child => child.type === "import_clause");
|
|
106
|
+
const literal = node.childForFieldName("source");
|
|
107
|
+
const module = literal?.namedChildren.find(child => child.type === "string_fragment")?.text;
|
|
108
|
+
if (!clause || module === undefined)
|
|
109
|
+
continue;
|
|
110
|
+
for (const child of clause.namedChildren) {
|
|
111
|
+
if (child.type === "identifier")
|
|
112
|
+
imports.set(child.text, module);
|
|
113
|
+
if (child.type === "namespace_import") {
|
|
114
|
+
const local = child.namedChildren.find(entry => entry.type === "identifier");
|
|
115
|
+
if (local)
|
|
116
|
+
imports.set(local.text, module);
|
|
117
|
+
}
|
|
118
|
+
if (child.type !== "named_imports")
|
|
119
|
+
continue;
|
|
120
|
+
for (const specifier of child.namedChildren) {
|
|
121
|
+
if (specifier.type !== "import_specifier")
|
|
122
|
+
continue;
|
|
123
|
+
const local = specifier.childForFieldName("alias") ?? specifier.childForFieldName("name");
|
|
124
|
+
if (local)
|
|
125
|
+
imports.set(local.text, module);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return imports;
|
|
130
|
+
}
|
|
131
|
+
/* ------------------------------------------------------------------ bodies */
|
|
132
|
+
/**
|
|
133
|
+
* Bodies that belong to a container rather than to a definition. `block` is
|
|
134
|
+
* deliberately absent: it is a function body in grammars that have no
|
|
135
|
+
* `statement_block`, and a nested control-flow block is always smaller.
|
|
136
|
+
*/
|
|
137
|
+
const CONTAINER_BODIES = {
|
|
138
|
+
class_body: true,
|
|
139
|
+
interface_body: true,
|
|
140
|
+
enum_body: true,
|
|
141
|
+
declaration_list: true,
|
|
142
|
+
field_declaration_list: true,
|
|
143
|
+
struct_body: true,
|
|
144
|
+
trait_body: true,
|
|
145
|
+
impl_body: true,
|
|
146
|
+
namespace_body: true,
|
|
147
|
+
module_body: true,
|
|
148
|
+
object_type: true,
|
|
149
|
+
program: true,
|
|
150
|
+
translation_unit: true,
|
|
151
|
+
source_file: true,
|
|
152
|
+
compilation_unit: true,
|
|
153
|
+
script: true,
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* The definition's own body. The fragment holds either the definition itself or
|
|
157
|
+
* one wrapper (a class the member was written in, or a binding the function
|
|
158
|
+
* value was assigned to), so the outermost node with a callable body is the
|
|
159
|
+
* definition being read. Breadth-first order picks the shallowest one, which
|
|
160
|
+
* keeps a large nested callback inside a default parameter from being taken for
|
|
161
|
+
* the definition's body, and ties are broken by position, so the choice is
|
|
162
|
+
* stable.
|
|
163
|
+
*/
|
|
164
|
+
export function definitionSyntax(fragment) {
|
|
165
|
+
let level = [fragment.tree.rootNode];
|
|
166
|
+
while (level.length > 0) {
|
|
167
|
+
const next = [];
|
|
168
|
+
for (const node of level) {
|
|
169
|
+
const body = node.childForFieldName("body");
|
|
170
|
+
if (body && !Object.hasOwn(CONTAINER_BODIES, body.type))
|
|
171
|
+
return { definition: node, body };
|
|
172
|
+
next.push(...node.children);
|
|
173
|
+
}
|
|
174
|
+
level = next;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Comment-free token stream of a body: leaf text in source order, one space
|
|
180
|
+
* between tokens. Formatting and comments are gone; every literal and operator
|
|
181
|
+
* is kept exactly as written, so two bodies match only when their tokens do.
|
|
182
|
+
*/
|
|
183
|
+
export function bodyTokenSignature(body) {
|
|
184
|
+
const tokens = [];
|
|
185
|
+
const walk = (node) => {
|
|
186
|
+
if (node.childCount === 0) {
|
|
187
|
+
const type = node.type;
|
|
188
|
+
if (type !== "comment" && !type.endsWith("_comment"))
|
|
189
|
+
tokens.push(node.text);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const child of node.children)
|
|
193
|
+
walk(child);
|
|
194
|
+
};
|
|
195
|
+
walk(body);
|
|
196
|
+
return tokens.join(" ");
|
|
197
|
+
}
|
|
198
|
+
/* ------------------------------------------------------------- annotations */
|
|
199
|
+
/** Declared response type name of a definition's return annotation, if any. */
|
|
200
|
+
export function returnedResponseType(definition) {
|
|
201
|
+
return declaredResponseType(definition.childForFieldName("return_type"), true);
|
|
202
|
+
}
|
|
203
|
+
/** Declared fields written in one object literal type; empty when there are none. */
|
|
204
|
+
function objectFields(objectNode) {
|
|
205
|
+
const fields = [];
|
|
206
|
+
for (const member of objectNode.namedChildren) {
|
|
207
|
+
if (member.type !== "property_signature")
|
|
208
|
+
continue;
|
|
209
|
+
const name = member.childForFieldName("name");
|
|
210
|
+
const annotation = member.childForFieldName("type");
|
|
211
|
+
if (!name || !annotation)
|
|
212
|
+
continue;
|
|
213
|
+
// A property signature's `type` field is the annotation node, which still
|
|
214
|
+
// carries its colon; the declared type is the node inside it.
|
|
215
|
+
const type = annotation.type === "type_annotation"
|
|
216
|
+
? annotation.namedChildren[0] ?? annotation
|
|
217
|
+
: annotation;
|
|
218
|
+
fields.push({ name: name.text, type: collapseWs(type.text) });
|
|
219
|
+
}
|
|
220
|
+
return fields;
|
|
221
|
+
}
|
|
222
|
+
/** Wrapper type names whose single type argument is the declared response. */
|
|
223
|
+
const RESPONSE_WRAPPERS = { Promise: true, Awaited: true, Readonly: true };
|
|
224
|
+
/**
|
|
225
|
+
* The declared response type name of a type annotation, or null when the
|
|
226
|
+
* annotation declares something else. Only a plain name (`CreateResult`) or a
|
|
227
|
+
* known async/readonly wrapper of one (`Promise<CreateResult>`) is a response:
|
|
228
|
+
* an array of them (`CreateResult[]`), an arbitrary generic
|
|
229
|
+
* (`Wrapper<CreateResult>`), a union, or a function type is a different thing,
|
|
230
|
+
* and this check must not treat it as holding one response.
|
|
231
|
+
*/
|
|
232
|
+
export function declaredResponseType(annotation, awaited = false) {
|
|
233
|
+
if (!annotation)
|
|
234
|
+
return null;
|
|
235
|
+
const inner = annotation.type === "type_annotation" ? annotation.namedChildren[0] : annotation;
|
|
236
|
+
if (!inner)
|
|
237
|
+
return null;
|
|
238
|
+
if (inner.type === "type_identifier" || inner.type === "identifier")
|
|
239
|
+
return inner.text;
|
|
240
|
+
// `Promise<CreateResult>` is the response only where the code awaits it: an
|
|
241
|
+
// async definition's declared return, and a call whose result is awaited.
|
|
242
|
+
// A parameter or local declared as a Promise holds the pending value itself.
|
|
243
|
+
if (inner.type === "generic_type") {
|
|
244
|
+
if (!awaited)
|
|
245
|
+
return null;
|
|
246
|
+
const name = inner.childForFieldName("name");
|
|
247
|
+
const args = inner.childForFieldName("type_arguments");
|
|
248
|
+
if (!name || !args || !Object.hasOwn(RESPONSE_WRAPPERS, name.text))
|
|
249
|
+
return null;
|
|
250
|
+
const parameters = args.namedChildren;
|
|
251
|
+
if (parameters.length !== 1)
|
|
252
|
+
return null;
|
|
253
|
+
return declaredResponseType(parameters[0], true);
|
|
254
|
+
}
|
|
255
|
+
if (inner.type === "type_annotation")
|
|
256
|
+
return declaredResponseType(inner.namedChildren[0], awaited);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
/** Name a declaration node declares, or null when it names none. */
|
|
260
|
+
function declarationName(node) {
|
|
261
|
+
return (node.childForFieldName("name")?.text ??
|
|
262
|
+
node.namedChildren.find(child => child.type === "type_identifier")?.text ??
|
|
263
|
+
null);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Declared fields of the named interface or type alias in a fragment, or null
|
|
267
|
+
* when that declaration is not the one the fragment holds. The name is required
|
|
268
|
+
* because one line can carry several declarations: a fragment read for `B` that
|
|
269
|
+
* holds both `A` and `B` must never report `A`'s fields as `B`'s. `extends` and
|
|
270
|
+
* intersections are not followed, so this is only ever the fields written in the
|
|
271
|
+
* declaration itself; an empty array means it writes no field, which is a
|
|
272
|
+
* different answer from a declaration nobody could read.
|
|
273
|
+
*/
|
|
274
|
+
export function contractFields(fragment, name) {
|
|
275
|
+
const declarations = [];
|
|
276
|
+
walkSyntax(fragment.tree.rootNode, node => {
|
|
277
|
+
if (node.type === "interface_declaration" || node.type === "type_alias_declaration") {
|
|
278
|
+
declarations.push(node);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
const matches = declarations.filter(declaration => declarationName(declaration) === name);
|
|
282
|
+
if (matches.length !== 1)
|
|
283
|
+
return null;
|
|
284
|
+
const declaration = matches[0];
|
|
285
|
+
if (declaration.type === "interface_declaration") {
|
|
286
|
+
const body = declaration.childForFieldName("body");
|
|
287
|
+
return body ? objectFields(body) : null;
|
|
288
|
+
}
|
|
289
|
+
const value = declaration.childForFieldName("value");
|
|
290
|
+
return value && value.type === "object_type" ? objectFields(value) : null;
|
|
291
|
+
}
|
|
292
|
+
/** Nodes that declare a named value with an explicit type annotation. */
|
|
293
|
+
const TYPED_BINDING_NODES = {
|
|
294
|
+
required_parameter: true,
|
|
295
|
+
optional_parameter: true,
|
|
296
|
+
parameter: true,
|
|
297
|
+
typed_parameter: true,
|
|
298
|
+
variable_declarator: true,
|
|
299
|
+
public_field_definition: true,
|
|
300
|
+
property_declaration: true,
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Names declared with a type annotation that names `typeName`: a parameter, a
|
|
304
|
+
* local, or a class field. Only a plainly named binding is returned; a
|
|
305
|
+
* destructuring pattern is left out, because its fields are read positions
|
|
306
|
+
* rather than one value the checks can follow.
|
|
307
|
+
*/
|
|
308
|
+
export function typedBindings(definition, typeName) {
|
|
309
|
+
const bindings = [];
|
|
310
|
+
const walk = (node) => {
|
|
311
|
+
if (Object.hasOwn(TYPED_BINDING_NODES, node.type)) {
|
|
312
|
+
const annotation = node.childForFieldName("type");
|
|
313
|
+
if (declaredResponseType(annotation) === typeName) {
|
|
314
|
+
const pattern = node.childForFieldName("pattern") ??
|
|
315
|
+
node.childForFieldName("name") ??
|
|
316
|
+
node.childForFieldName("left");
|
|
317
|
+
if (pattern?.type === "identifier") {
|
|
318
|
+
bindings.push({ name: pattern.text, row: pattern.startPosition.row });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (const child of node.children)
|
|
323
|
+
walk(child);
|
|
324
|
+
};
|
|
325
|
+
walk(definition);
|
|
326
|
+
return bindings;
|
|
327
|
+
}
|
|
328
|
+
/** Every node in the fragment, in pre-order. */
|
|
329
|
+
export function walkSyntax(root, visit) {
|
|
330
|
+
visit(root);
|
|
331
|
+
for (const child of root.children)
|
|
332
|
+
walkSyntax(child, visit);
|
|
333
|
+
}
|
|
334
|
+
/** Properties whose read reports how many items a value holds. */
|
|
335
|
+
const COUNT_PROPERTIES = { length: true, size: true, count: true };
|
|
336
|
+
export function countReadOf(node) {
|
|
337
|
+
if (node.type !== "member_expression")
|
|
338
|
+
return null;
|
|
339
|
+
const property = node.childForFieldName("property");
|
|
340
|
+
if (property?.type !== "property_identifier" || !Object.hasOwn(COUNT_PROPERTIES, property.text))
|
|
341
|
+
return null;
|
|
342
|
+
const object = node.childForFieldName("object");
|
|
343
|
+
if (!object)
|
|
344
|
+
return null;
|
|
345
|
+
if (object.type === "identifier")
|
|
346
|
+
return { read: node, value: object.text, ofField: false };
|
|
347
|
+
if (object.type !== "member_expression")
|
|
348
|
+
return null;
|
|
349
|
+
const owner = object.childForFieldName("object");
|
|
350
|
+
const field = object.childForFieldName("property");
|
|
351
|
+
if (owner?.type !== "identifier" || field?.type !== "property_identifier")
|
|
352
|
+
return null;
|
|
353
|
+
return { read: node, value: field.text, ofField: true };
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* First count-named read inside a `return`, which is the number a receiver hands
|
|
357
|
+
* back to its own caller. That is the count a partial-failure question is about,
|
|
358
|
+
* more than any diagnostic log written after it.
|
|
359
|
+
*/
|
|
360
|
+
export function returnedCountRead(definition) {
|
|
361
|
+
let found = null;
|
|
362
|
+
walkSyntax(definition, node => {
|
|
363
|
+
if (found || node.type !== "return_statement")
|
|
364
|
+
return;
|
|
365
|
+
walkSyntax(node, inner => {
|
|
366
|
+
if (found)
|
|
367
|
+
return;
|
|
368
|
+
const count = countReadOf(inner);
|
|
369
|
+
if (count)
|
|
370
|
+
found = count;
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
return found;
|
|
374
|
+
}
|
|
375
|
+
/** Whole camel-case words that name a lifecycle or state value, never a part. */
|
|
376
|
+
const STATE_WORDS = { status: true, state: true, stage: true, phase: true, lifecycle: true, mode: true };
|
|
377
|
+
/** Whether one identifier or property name says the value is state. */
|
|
378
|
+
function namesState(text) {
|
|
379
|
+
// `model` must not match `mode`, so words are compared whole at camel
|
|
380
|
+
// boundaries: `leaseStatus` and `status` name state; `model` does not.
|
|
381
|
+
const words = text.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z]+|[0-9]+/g) ?? [text];
|
|
382
|
+
return words.some(word => Object.hasOwn(STATE_WORDS, word.toLowerCase()));
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* State/status properties in a call payload. A queue routing constant or a
|
|
386
|
+
* state-looking word inside a string is not by itself a state assignment.
|
|
387
|
+
*/
|
|
388
|
+
export function stateWritesAt(call) {
|
|
389
|
+
const values = new Set();
|
|
390
|
+
const record = (key, value) => {
|
|
391
|
+
if (!namesState(key.text))
|
|
392
|
+
return;
|
|
393
|
+
values.add(value ? `${key.text}: ${collapseWs(value.text)}` : key.text);
|
|
394
|
+
};
|
|
395
|
+
walkSyntax(call, node => {
|
|
396
|
+
if (node.type !== "pair")
|
|
397
|
+
return;
|
|
398
|
+
record(node.childForFieldName("key") ?? node.namedChildren[0] ?? node, node.childForFieldName("value"));
|
|
399
|
+
});
|
|
400
|
+
return [...values];
|
|
401
|
+
}
|
|
402
|
+
/** Call expressions written in one definition, in source order. */
|
|
403
|
+
export function callsIn(definition) {
|
|
404
|
+
const calls = [];
|
|
405
|
+
walkSyntax(definition, node => {
|
|
406
|
+
if (node.type === "call_expression")
|
|
407
|
+
calls.push(node);
|
|
408
|
+
});
|
|
409
|
+
return calls;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Count-named reads in a body, in source order: the numbers a receiver reports,
|
|
413
|
+
* which are what a partial-failure question compares with the failures it never
|
|
414
|
+
* read.
|
|
415
|
+
*/
|
|
416
|
+
export function countReads(definition) {
|
|
417
|
+
const reads = [];
|
|
418
|
+
walkSyntax(definition, node => {
|
|
419
|
+
const count = countReadOf(node);
|
|
420
|
+
if (count)
|
|
421
|
+
reads.push(count);
|
|
422
|
+
});
|
|
423
|
+
return reads;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Whether two node handles denote the same syntax node. Tree-sitter hands back
|
|
427
|
+
* a fresh wrapper per field access, so `===` on handles is not a stable test:
|
|
428
|
+
* two handles for one node can differ between calls. Node ids are stable within
|
|
429
|
+
* one tree, which is what every structural question here compares.
|
|
430
|
+
*/
|
|
431
|
+
export function sameNode(left, right) {
|
|
432
|
+
return left !== null && left !== undefined && right !== null && right !== undefined && left.id === right.id;
|
|
433
|
+
}
|
|
434
|
+
/** Node types that declare a name local to one definition's body. */
|
|
435
|
+
const VALUE_DECLARATIONS = {
|
|
436
|
+
variable_declarator: true,
|
|
437
|
+
required_parameter: true,
|
|
438
|
+
optional_parameter: true,
|
|
439
|
+
parameter: true,
|
|
440
|
+
typed_parameter: true,
|
|
441
|
+
rest_pattern: true,
|
|
442
|
+
function_declaration: true,
|
|
443
|
+
class_declaration: true,
|
|
444
|
+
import_specifier: true,
|
|
445
|
+
};
|
|
446
|
+
/**
|
|
447
|
+
* Names one definition declares for itself: its parameters, its locals, and any
|
|
448
|
+
* nested declaration. A call written under one of these names reaches the local
|
|
449
|
+
* value, not whatever definition elsewhere carries the same bare key, so a
|
|
450
|
+
* caller-side check must not resolve through the shadow.
|
|
451
|
+
*/
|
|
452
|
+
export function declaredValueNames(definition) {
|
|
453
|
+
const names = new Set();
|
|
454
|
+
walkSyntax(definition, node => {
|
|
455
|
+
if (!Object.hasOwn(VALUE_DECLARATIONS, node.type))
|
|
456
|
+
return;
|
|
457
|
+
const declared = node.childForFieldName("name") ??
|
|
458
|
+
node.childForFieldName("pattern") ??
|
|
459
|
+
node.childForFieldName("left") ??
|
|
460
|
+
node.childForFieldName("declarator");
|
|
461
|
+
if (declared?.type === "identifier")
|
|
462
|
+
names.add(declared.text);
|
|
463
|
+
if (node.type === "rest_pattern") {
|
|
464
|
+
const inner = node.namedChildren.find(child => child.type === "identifier");
|
|
465
|
+
if (inner)
|
|
466
|
+
names.add(inner.text);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
return names;
|
|
470
|
+
}
|
|
471
|
+
/** Value of a string literal node, or the raw text of any other node. */
|
|
472
|
+
export function staticStringValue(node) {
|
|
473
|
+
if (node.type === "string") {
|
|
474
|
+
const fragment = node.namedChildren.find((c) => c.type === "string_fragment");
|
|
475
|
+
return fragment ? fragment.text : "";
|
|
476
|
+
}
|
|
477
|
+
return node.text;
|
|
478
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ReviewContextNode } from "./types.js";
|
|
2
|
+
/** Source evidence is snapshot-bound; syntactic relationships are not runtime proof. */
|
|
3
|
+
export interface EvidenceExcerpt {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
file: string;
|
|
7
|
+
line: number;
|
|
8
|
+
endLine?: number;
|
|
9
|
+
ref: string;
|
|
10
|
+
text: string;
|
|
11
|
+
role: "change" | "caller" | "contract" | "related" | "test";
|
|
12
|
+
}
|
|
13
|
+
export interface AutomaticFinding {
|
|
14
|
+
id: string;
|
|
15
|
+
kind: "unused-error-result" | "duplicate-body" | "broken-reference";
|
|
16
|
+
title: string;
|
|
17
|
+
scope: string;
|
|
18
|
+
limitation: string;
|
|
19
|
+
unitIds: string[];
|
|
20
|
+
evidence: EvidenceExcerpt[];
|
|
21
|
+
}
|
|
22
|
+
export interface CheckCoverage {
|
|
23
|
+
kind: AutomaticFinding["kind"];
|
|
24
|
+
status: "checked" | "partial" | "not-checked";
|
|
25
|
+
detail: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ReviewAgendaEntry {
|
|
28
|
+
id: string;
|
|
29
|
+
title: string;
|
|
30
|
+
reason: string;
|
|
31
|
+
priority: number;
|
|
32
|
+
unitIds: string[];
|
|
33
|
+
findingIds: string[];
|
|
34
|
+
evidence: EvidenceExcerpt[];
|
|
35
|
+
context: ReviewContextNode[];
|
|
36
|
+
}
|
|
37
|
+
export interface PullRequestIntent {
|
|
38
|
+
title: string;
|
|
39
|
+
body: string;
|
|
40
|
+
url?: string;
|
|
41
|
+
baseRef?: string;
|
|
42
|
+
headRef?: string;
|
|
43
|
+
}
|
|
44
|
+
export interface IntentClaim {
|
|
45
|
+
text: string;
|
|
46
|
+
origin: "title" | "author" | "generated-summary";
|
|
47
|
+
status: "evidence-linked" | "not-established";
|
|
48
|
+
unitIds: string[];
|
|
49
|
+
explanation: string;
|
|
50
|
+
}
|
|
51
|
+
export interface IntentCrossCheck {
|
|
52
|
+
verdict: "not-established" | "contradicted" | "supported-within-checked-scope";
|
|
53
|
+
summary: string;
|
|
54
|
+
claims: IntentClaim[];
|
|
55
|
+
obligations: string[];
|
|
56
|
+
}
|
|
57
|
+
export interface ReviewEvidence {
|
|
58
|
+
findings: AutomaticFinding[];
|
|
59
|
+
checks: CheckCoverage[];
|
|
60
|
+
agenda: ReviewAgendaEntry[];
|
|
61
|
+
intent: IntentCrossCheck;
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type FunctionIndex } from "../extract.js";
|
|
2
|
+
import type { ContextSources } from "./call-context.js";
|
|
3
|
+
import type { AutomaticFinding, CheckCoverage, ReviewAgendaEntry } from "./evidence-types.js";
|
|
4
|
+
import type { ReviewUnit } from "./types.js";
|
|
5
|
+
export interface ReviewEvidenceOptions {
|
|
6
|
+
/** Prior-snapshot index; absent for patch-only input. */
|
|
7
|
+
before?: FunctionIndex;
|
|
8
|
+
/** Resulting-snapshot index; absent for patch-only input. */
|
|
9
|
+
after?: FunctionIndex;
|
|
10
|
+
/** Per-snapshot definition source readers; absent when nothing can be read. */
|
|
11
|
+
sources?: ContextSources;
|
|
12
|
+
/** Provenance label for prior-snapshot evidence, e.g. the base commit. */
|
|
13
|
+
baseRef?: string;
|
|
14
|
+
/** Provenance label for resulting-snapshot evidence, e.g. the head commit. */
|
|
15
|
+
headRef?: string;
|
|
16
|
+
/** Local module binding resolver over the resulting immutable snapshot. */
|
|
17
|
+
resolveImport?: (importer: string, specifier: string) => string | undefined;
|
|
18
|
+
}
|
|
19
|
+
export interface ReviewEvidenceResult {
|
|
20
|
+
findings: AutomaticFinding[];
|
|
21
|
+
checks: CheckCoverage[];
|
|
22
|
+
agenda: ReviewAgendaEntry[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Automatic findings, check coverage, and the review agenda for one diff. Main
|
|
26
|
+
* calls this inside the index callback with the same snapshots it renders, so
|
|
27
|
+
* every excerpt names the revision it came from; a call without indexes or
|
|
28
|
+
* without source readers still returns an agenda over the hunks, with each
|
|
29
|
+
* check honestly not checked.
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildReviewEvidence(units: readonly ReviewUnit[], options?: ReviewEvidenceOptions): ReviewEvidenceResult;
|