@0xcraft/powershot 1.0.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.
Files changed (87) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +306 -0
  3. package/dist/agents.js +82 -0
  4. package/dist/bench.js +179 -0
  5. package/dist/budget.js +59 -0
  6. package/dist/bundle.js +173 -0
  7. package/dist/cache.js +155 -0
  8. package/dist/cli/agent-command.js +27 -0
  9. package/dist/cli/app.js +31 -0
  10. package/dist/cli/args.js +76 -0
  11. package/dist/cli/bench-command.js +89 -0
  12. package/dist/cli/dismiss-command.js +42 -0
  13. package/dist/cli/environment.js +32 -0
  14. package/dist/cli/reports.js +64 -0
  15. package/dist/cli/review-command.js +268 -0
  16. package/dist/cli/session-command.js +62 -0
  17. package/dist/cli.js +7 -0
  18. package/dist/config.js +130 -0
  19. package/dist/delegate.js +84 -0
  20. package/dist/dismissed.js +130 -0
  21. package/dist/fspolicy.js +62 -0
  22. package/dist/git.js +238 -0
  23. package/dist/ground.js +286 -0
  24. package/dist/judges/judge.js +85 -0
  25. package/dist/judges/llm.js +234 -0
  26. package/dist/judges/prompts.js +86 -0
  27. package/dist/judges/tools.js +125 -0
  28. package/dist/lang/packs.js +557 -0
  29. package/dist/lang/pyright.js +108 -0
  30. package/dist/lang/python-deps.js +174 -0
  31. package/dist/lang/ruby-deps.js +77 -0
  32. package/dist/langtest.js +248 -0
  33. package/dist/manifest.js +209 -0
  34. package/dist/otel.js +75 -0
  35. package/dist/package-meta.js +13 -0
  36. package/dist/package-smoke.js +110 -0
  37. package/dist/plan.js +134 -0
  38. package/dist/position.js +94 -0
  39. package/dist/report/ansi.js +18 -0
  40. package/dist/report/codequality.js +19 -0
  41. package/dist/report/compact.js +15 -0
  42. package/dist/report/highlight.js +54 -0
  43. package/dist/report/markdown.js +113 -0
  44. package/dist/report/sarif.js +66 -0
  45. package/dist/report/terminal.js +170 -0
  46. package/dist/report/viewer.js +148 -0
  47. package/dist/review.js +355 -0
  48. package/dist/scan.js +67 -0
  49. package/dist/selftest.js +1928 -0
  50. package/dist/session.js +140 -0
  51. package/dist/snapshot.js +101 -0
  52. package/dist/text.js +50 -0
  53. package/dist/types.js +2 -0
  54. package/dist/verifiers/assertion-drift.js +137 -0
  55. package/dist/verifiers/contract-drift.js +140 -0
  56. package/dist/verifiers/copy-paste-drift.js +106 -0
  57. package/dist/verifiers/dead-on-arrival.js +92 -0
  58. package/dist/verifiers/dropped-guard.js +144 -0
  59. package/dist/verifiers/foreign-contract-drift.js +114 -0
  60. package/dist/verifiers/foreign-copy-paste-drift.js +83 -0
  61. package/dist/verifiers/foreign-dropped-guard.js +78 -0
  62. package/dist/verifiers/foreign-phantom-api.js +36 -0
  63. package/dist/verifiers/foreign-phantom-config.js +40 -0
  64. package/dist/verifiers/foreign-phantom-dep.js +82 -0
  65. package/dist/verifiers/foreign-reinvented.js +65 -0
  66. package/dist/verifiers/foreign-scope-creep.js +42 -0
  67. package/dist/verifiers/foreign-swallowed-error.js +36 -0
  68. package/dist/verifiers/foreign-tests.js +143 -0
  69. package/dist/verifiers/foreign-tokens.js +94 -0
  70. package/dist/verifiers/foreign.js +16 -0
  71. package/dist/verifiers/index.js +38 -0
  72. package/dist/verifiers/lying-comment.js +90 -0
  73. package/dist/verifiers/phantom-api.js +88 -0
  74. package/dist/verifiers/phantom-config.js +93 -0
  75. package/dist/verifiers/phantom-dep.js +110 -0
  76. package/dist/verifiers/reinvented.js +74 -0
  77. package/dist/verifiers/scope-creep.js +77 -0
  78. package/dist/verifiers/swallowed-error.js +110 -0
  79. package/dist/verifiers/vacuous-test.js +138 -0
  80. package/docs/architecture.md +191 -0
  81. package/docs/assets/cli-preview.svg +68 -0
  82. package/docs/assets/powershot-logo.png +0 -0
  83. package/docs/ci.md +151 -0
  84. package/examples/github-actions/action.yml +23 -0
  85. package/examples/github-actions/cli.yml +43 -0
  86. package/examples/gitlab/.gitlab-ci.yml +21 -0
  87. package/package.json +65 -0
@@ -0,0 +1,557 @@
1
+ import { createRequire } from 'node:module';
2
+ import { dirname, join } from 'node:path';
3
+ /** Shared defaults; a pack overrides only what its grammar spells differently. */
4
+ const COMMON_NODES = {
5
+ identifier: ['identifier'],
6
+ comment: ['comment', 'line_comment', 'block_comment'],
7
+ ifCondition: 'condition',
8
+ ifBody: 'consequence',
9
+ declarationName: 'name',
10
+ block: ['block'],
11
+ };
12
+ /** Pull the key out of `getenv("HOME")` / `environ["HOME"]` style reads. */
13
+ function envKeysFrom(root, callPattern, callTypes) {
14
+ const out = [];
15
+ for (const call of nodesOfType(root, callTypes)) {
16
+ if (!callPattern.test(call.text))
17
+ continue;
18
+ const key = /["'`]([A-Z_][A-Z0-9_]*)["'`]/.exec(call.text);
19
+ if (key?.[1])
20
+ out.push({ name: key[1], node: call });
21
+ }
22
+ return out;
23
+ }
24
+ /** depth-first walk, used by every pack */
25
+ export function walk(node, visit) {
26
+ visit(node);
27
+ for (let i = 0; i < node.childCount; i++) {
28
+ const child = node.child(i);
29
+ if (child)
30
+ walk(child, visit);
31
+ }
32
+ }
33
+ export function nodesOfType(root, types) {
34
+ const out = [];
35
+ walk(root, (n) => {
36
+ if (types.includes(n.type))
37
+ out.push(n);
38
+ });
39
+ return out;
40
+ }
41
+ /**
42
+ * A comment inside a handler is the author saying "I meant this", in every language.
43
+ * Deliberate silence is not a defect, so it suppresses the finding — the same rule
44
+ * the TypeScript checks already follow.
45
+ */
46
+ function hasComment(node) {
47
+ if (!node)
48
+ return false;
49
+ let found = false;
50
+ walk(node, (n) => {
51
+ if (n.type === 'comment' || n.type === 'line_comment' || n.type === 'block_comment')
52
+ found = true;
53
+ });
54
+ return found;
55
+ }
56
+ /** A body is meaningless when it does nothing at all, or only says "pass". */
57
+ function isInertBlock(block, inertTypes) {
58
+ if (!block)
59
+ return false;
60
+ if (hasComment(block))
61
+ return false;
62
+ const statements = block.namedChildren.filter((c) => c.type !== 'comment');
63
+ if (statements.length === 0)
64
+ return true;
65
+ return statements.every((s) => inertTypes.includes(s.type));
66
+ }
67
+ /**
68
+ * The try/catch family. Java, C++, C#, PHP, Kotlin, Swift and Scala all discard a
69
+ * failure the same way — a handler whose body does nothing, or only logs — so the
70
+ * rule is written once and each pack supplies its grammar's spelling.
71
+ */
72
+ function catchBased(catchTypes, bodyField, logPattern) {
73
+ return (root) => {
74
+ const out = [];
75
+ for (const clause of nodesOfType(root, catchTypes)) {
76
+ const body = (bodyField ? clause.childForFieldName(bodyField) : undefined) ??
77
+ clause.namedChildren.find((c) => /block|compound_statement|statements/.test(c.type)) ??
78
+ null;
79
+ // Kotlin and Swift put the braces straight on the catch clause, so an empty
80
+ // handler has no body node at all to inspect — only a `{}` at the end
81
+ if (!body) {
82
+ if (/\{\s*\}\s*$/.test(clause.text) && !hasComment(clause)) {
83
+ out.push({ node: clause, what: 'catch block does nothing' });
84
+ }
85
+ continue;
86
+ }
87
+ if (isInertBlock(body, [])) {
88
+ out.push({ node: clause, what: 'catch block does nothing' });
89
+ continue;
90
+ }
91
+ if (hasComment(body))
92
+ continue;
93
+ const statements = (body?.namedChildren ?? []).filter((c) => !COMMON_NODES.comment.includes(c.type));
94
+ if (statements.length > 0 && statements.every((st) => logPattern.test(st.text.trim()))) {
95
+ out.push({ node: clause, what: 'catch block only logs' });
96
+ }
97
+ }
98
+ return out;
99
+ };
100
+ }
101
+ const PYTHON = {
102
+ name: 'python',
103
+ extensions: ['.py', '.pyi'],
104
+ grammar: 'tree-sitter-python',
105
+ nodes: {
106
+ ...COMMON_NODES,
107
+ ifStatement: ['if_statement'],
108
+ ifBody: 'consequence',
109
+ bail: ['return_statement', 'raise_statement', 'continue_statement', 'break_statement'],
110
+ declaration: ['function_definition', 'class_definition'],
111
+ },
112
+ envReads(root) {
113
+ return envKeysFrom(root, /os\.environ|getenv/, ['call', 'subscript']);
114
+ },
115
+ imports(root) {
116
+ const out = [];
117
+ for (const stmt of nodesOfType(root, ['import_statement', 'import_from_statement'])) {
118
+ // `import a.b`, `from a.b import c`, `import a as x` — the module is the first
119
+ // dotted name, and a relative `from . import x` has none
120
+ const first = stmt.namedChildren.find((c) => c.type === 'dotted_name' || c.type === 'aliased_import');
121
+ const dotted = first?.type === 'aliased_import' ? first.namedChildren[0] : first;
122
+ if (!dotted || stmt.text.includes('from .'))
123
+ continue;
124
+ out.push({ name: dotted.text, node: stmt });
125
+ }
126
+ return out;
127
+ },
128
+ signatures(root) {
129
+ const out = new Map();
130
+ for (const fn of nodesOfType(root, ['function_definition'])) {
131
+ const name = fn.childForFieldName('name');
132
+ const params = fn.childForFieldName('parameters');
133
+ if (!name || !params)
134
+ continue;
135
+ const listed = [];
136
+ let required = 0;
137
+ for (const p of params.namedChildren) {
138
+ // *args and **kwargs demand nothing of a caller, and self/cls are bound
139
+ if (p.type === 'list_splat_pattern' || p.type === 'dictionary_splat_pattern')
140
+ continue;
141
+ const label = p.text;
142
+ if (/^(self|cls)\b/.test(label))
143
+ continue;
144
+ listed.push(label);
145
+ // a default makes it optional; a bare or annotated name does not
146
+ if (p.type === 'identifier' || p.type === 'typed_parameter')
147
+ required++;
148
+ }
149
+ out.set(name.text, { required, params: listed, node: fn });
150
+ }
151
+ return out;
152
+ },
153
+ tests(root) {
154
+ const out = [];
155
+ for (const fn of nodesOfType(root, ['function_definition'])) {
156
+ const name = fn.childForFieldName('name')?.text;
157
+ if (!name || !name.startsWith('test'))
158
+ continue;
159
+ const body = fn.childForFieldName('body');
160
+ if (!body)
161
+ continue;
162
+ const assertions = [];
163
+ for (const stmt of nodesOfType(body, ['assert_statement'])) {
164
+ // `assert subject == expected` is what can drift; a bare `assert x` cannot
165
+ const comparison = stmt.namedChildren.find((c) => c.type === 'comparison_operator');
166
+ const left = comparison?.namedChildren[0];
167
+ const right = comparison?.namedChildren[1];
168
+ if (comparison && left && right) {
169
+ assertions.push({ subject: left.text, expected: right.text, node: stmt, expectedNode: right });
170
+ }
171
+ }
172
+ // unittest style: self.assertEqual(subject, expected)
173
+ for (const call of nodesOfType(body, ['call'])) {
174
+ const fnName = call.childForFieldName('function')?.text ?? '';
175
+ if (!/(^|\.)assert[A-Z_]/.test(fnName))
176
+ continue;
177
+ const args = call.childForFieldName('arguments')?.namedChildren ?? [];
178
+ if (args.length >= 2 && args[0] && args[1]) {
179
+ assertions.push({ subject: args[0].text, expected: args[1].text, node: call, expectedNode: args[1] });
180
+ }
181
+ }
182
+ // includes mock's family — `publish.assert_not_awaited()` is how most async
183
+ // suites assert, and missing it called 25 good tests vacuous on a real repo
184
+ const asserts = nodesOfType(body, ['assert_statement']).length > 0 ||
185
+ nodesOfType(body, ['call']).some((c) => /(^|\.)(assert[A-Z_a-z]|fail$|raises$)/.test(c.childForFieldName('function')?.text ?? '')) ||
186
+ nodesOfType(body, ['with_statement']).some((w) => /raises|assertRaises|pytest\.warns/.test(w.text));
187
+ const runsSomething = nodesOfType(body, ['call']).length > 0;
188
+ // a body that is only `pass` or a docstring is a placeholder, not a lie
189
+ const isPlaceholder = body.namedChildren.every((c) => c.type === 'pass_statement' || c.type === 'expression_statement' && /^["']/.test(c.text.trim()));
190
+ out.push({ name, node: fn, assertions, provesNothing: runsSomething && !asserts && !isPlaceholder });
191
+ }
192
+ return out;
193
+ },
194
+ documentedParams(root) {
195
+ const out = [];
196
+ for (const fn of nodesOfType(root, ['function_definition'])) {
197
+ const name = fn.childForFieldName('name')?.text;
198
+ const params = fn.childForFieldName('parameters');
199
+ const body = fn.childForFieldName('body');
200
+ if (!name || !params || !body)
201
+ continue;
202
+ // the docstring is the first statement, if it is a bare string
203
+ const first = body.namedChildren[0];
204
+ if (!first || first.type !== 'expression_statement' || !/^[ru]?["']/.test(first.text.trim()))
205
+ continue;
206
+ const declared = params.namedChildren
207
+ .filter((p) => p.type !== 'list_splat_pattern' && p.type !== 'dictionary_splat_pattern')
208
+ .map((p) => (p.text.split(/[:=]/)[0] ?? '').trim())
209
+ .filter((n) => n !== 'self' && n !== 'cls' && n !== '');
210
+ const documented = [];
211
+ const text = first.text;
212
+ // reST `:param name:`. A line at a time: across the whole string the optional
213
+ // type group swallows the name and matches the *next* directive's colon.
214
+ for (const line of text.split('\n')) {
215
+ const m = /^\s*:param\s+(?:[^\s:]+\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*:/.exec(line);
216
+ if (m?.[1])
217
+ documented.push({ name: m[1], node: first });
218
+ }
219
+ // Google: an `Args:` block, one `name:` or `name (type):` per line
220
+ const args = /\n\s*Args:\s*\n([\s\S]*?)(\n\s*(?:Returns|Raises|Yields|Examples?|Notes?):|["']{3})/.exec(text);
221
+ if (args?.[1]) {
222
+ for (const line of args[1].split('\n')) {
223
+ const m = /^\s{2,}([A-Za-z_][A-Za-z0-9_]*)\s*(?:\([^)]*\))?\s*:/.exec(line);
224
+ if (m?.[1])
225
+ documented.push({ name: m[1], node: first });
226
+ }
227
+ }
228
+ if (documented.length > 0)
229
+ out.push({ fn: name, declared, documented });
230
+ }
231
+ return out;
232
+ },
233
+ swallowedError(root) {
234
+ const out = [];
235
+ for (const clause of nodesOfType(root, ['except_clause'])) {
236
+ const block = clause.namedChildren.find((c) => c.type === 'block') ?? null;
237
+ // `except: pass` is the canonical Python spelling of discarding a failure
238
+ if (isInertBlock(block, ['pass_statement', 'ellipsis'])) {
239
+ out.push({ node: clause, what: 'except block does nothing' });
240
+ continue;
241
+ }
242
+ if (hasComment(block))
243
+ continue;
244
+ const statements = (block?.namedChildren ?? []).filter((c) => c.type !== 'comment');
245
+ const onlyLogs = statements.length > 0 &&
246
+ statements.every((s) => s.type === 'expression_statement' &&
247
+ /^(print|log|logger|logging)\s*[.(]/.test(s.text.trim()));
248
+ if (onlyLogs)
249
+ out.push({ node: clause, what: 'except block only logs' });
250
+ }
251
+ return out;
252
+ },
253
+ };
254
+ const GO = {
255
+ name: 'go',
256
+ extensions: ['.go'],
257
+ grammar: 'tree-sitter-go',
258
+ nodes: {
259
+ ...COMMON_NODES,
260
+ ifStatement: ['if_statement'],
261
+ bail: ['return_statement', 'continue_statement', 'break_statement', 'goto_statement'],
262
+ declaration: ['function_declaration', 'method_declaration', 'type_declaration'],
263
+ },
264
+ envReads(root) {
265
+ return envKeysFrom(root, /os\.Getenv|os\.LookupEnv/, ['call_expression']);
266
+ },
267
+ swallowedError(root) {
268
+ const out = [];
269
+ // Go has no catch: the idiom is `if err != nil { ... }`, and the failure is
270
+ // discarded when that body does nothing
271
+ for (const stmt of nodesOfType(root, ['if_statement'])) {
272
+ const cond = stmt.childForFieldName('condition');
273
+ if (!cond || !/\berr\b\s*!=\s*nil/.test(cond.text))
274
+ continue;
275
+ const body = stmt.childForFieldName('consequence');
276
+ if (isInertBlock(body, []))
277
+ out.push({ node: stmt, what: 'error is checked and then ignored' });
278
+ }
279
+ // `_ = f()` deliberately NOT reported: Go compiles without assigning at all, so
280
+ // the blank identifier is someone saying "I know". An empty `if err != nil` isn't.
281
+ return out;
282
+ },
283
+ };
284
+ const JAVA = {
285
+ name: 'java',
286
+ extensions: ['.java'],
287
+ grammar: 'tree-sitter-java',
288
+ nodes: {
289
+ ...COMMON_NODES,
290
+ ifStatement: ['if_statement'],
291
+ bail: ['return_statement', 'throw_statement', 'continue_statement', 'break_statement'],
292
+ declaration: ['method_declaration', 'class_declaration', 'interface_declaration', 'record_declaration'],
293
+ },
294
+ envReads(root) {
295
+ return envKeysFrom(root, /System\.getenv/, ['method_invocation']);
296
+ },
297
+ swallowedError: catchBased(['catch_clause'], 'body', /^(System\.(out|err)|log|logger|LOG|LOGGER)\s*\./),
298
+ };
299
+ const RUST = {
300
+ name: 'rust',
301
+ extensions: ['.rs'],
302
+ grammar: 'tree-sitter-rust',
303
+ nodes: {
304
+ ...COMMON_NODES,
305
+ ifStatement: ['if_expression'],
306
+ ifCondition: 'condition',
307
+ ifBody: 'consequence',
308
+ bail: ['return_expression', 'break_expression', 'continue_expression'],
309
+ declaration: ['function_item', 'struct_item', 'enum_item', 'trait_item'],
310
+ },
311
+ envReads(root) {
312
+ return envKeysFrom(root, /env::var|env::var_os/, ['call_expression']);
313
+ },
314
+ swallowedError(root) {
315
+ const out = [];
316
+ // `let _ = fallible()` deliberately NOT reported: it is Rust's own "on purpose",
317
+ // written to silence #[must_use]. Measured, 59 findings over 19 commits, all
318
+ // idiomatic. An empty `Err(..)` arm has no such marker.
319
+ for (const arm of nodesOfType(root, ['match_arm'])) {
320
+ const pattern = arm.childForFieldName('pattern');
321
+ if (!pattern || !/^Err\b/.test(pattern.text.trim()))
322
+ continue;
323
+ const value = arm.childForFieldName('value');
324
+ if (value && (value.text.trim() === '{}' || isInertBlock(value, []))) {
325
+ out.push({ node: arm, what: 'Err arm does nothing' });
326
+ }
327
+ }
328
+ return out;
329
+ },
330
+ };
331
+ const CLIKE_LOG = /^(std::(cout|cerr)|printf|fprintf|Console\.|Log|log|logger|error_log|print_r|var_dump)/;
332
+ const CPP = {
333
+ name: 'cpp',
334
+ extensions: ['.cpp', '.cc', '.cxx', '.hpp', '.hh', '.h'],
335
+ grammar: 'tree-sitter-cpp',
336
+ nodes: {
337
+ ...COMMON_NODES,
338
+ ifStatement: ['if_statement'],
339
+ bail: ['return_statement', 'throw_statement', 'break_statement', 'continue_statement', 'goto_statement'],
340
+ declaration: ['function_definition'],
341
+ declarationName: 'declarator',
342
+ block: ['compound_statement'],
343
+ },
344
+ envReads: (root) => envKeysFrom(root, /getenv|GetEnvironmentVariable/, ['call_expression']),
345
+ swallowedError: catchBased(['catch_clause'], 'body', CLIKE_LOG),
346
+ };
347
+ const C = {
348
+ name: 'c',
349
+ extensions: ['.c'],
350
+ grammar: 'tree-sitter-c',
351
+ nodes: {
352
+ ...COMMON_NODES,
353
+ ifStatement: ['if_statement'],
354
+ bail: ['return_statement', 'break_statement', 'continue_statement', 'goto_statement'],
355
+ declaration: ['function_definition'],
356
+ declarationName: 'declarator',
357
+ block: ['compound_statement'],
358
+ },
359
+ envReads: (root) => envKeysFrom(root, /getenv/, ['call_expression']),
360
+ // C has no exceptions; its error handling is return codes, which cannot be told
361
+ // from ordinary control flow without types. The other checks still apply.
362
+ };
363
+ const CSHARP = {
364
+ name: 'c#',
365
+ extensions: ['.cs'],
366
+ grammar: 'tree-sitter-c_sharp',
367
+ nodes: {
368
+ ...COMMON_NODES,
369
+ ifStatement: ['if_statement'],
370
+ bail: ['return_statement', 'throw_statement', 'break_statement', 'continue_statement'],
371
+ declaration: ['method_declaration', 'class_declaration', 'interface_declaration', 'record_declaration'],
372
+ block: ['block', 'declaration_list'],
373
+ },
374
+ envReads: (root) => envKeysFrom(root, /GetEnvironmentVariable/, ['invocation_expression']),
375
+ swallowedError: catchBased(['catch_clause'], 'body', CLIKE_LOG),
376
+ };
377
+ const PHP = {
378
+ name: 'php',
379
+ extensions: ['.php'],
380
+ grammar: 'tree-sitter-php',
381
+ nodes: {
382
+ ...COMMON_NODES,
383
+ ifStatement: ['if_statement'],
384
+ bail: ['return_statement', 'throw_expression', 'break_statement', 'continue_statement'],
385
+ declaration: ['function_definition', 'method_declaration', 'class_declaration'],
386
+ block: ['compound_statement'],
387
+ },
388
+ envReads: (root) => envKeysFrom(root, /getenv|\$_ENV/, ['function_call_expression', 'subscript_expression']),
389
+ swallowedError: catchBased(['catch_clause'], 'body', CLIKE_LOG),
390
+ };
391
+ const KOTLIN = {
392
+ name: 'kotlin',
393
+ extensions: ['.kt', '.kts'],
394
+ grammar: 'tree-sitter-kotlin',
395
+ nodes: {
396
+ ...COMMON_NODES,
397
+ identifier: ['simple_identifier'],
398
+ ifStatement: ['if_expression'],
399
+ bail: ['jump_expression'],
400
+ declaration: ['function_declaration', 'class_declaration', 'object_declaration'],
401
+ block: ['statements', 'block'],
402
+ },
403
+ envReads: (root) => envKeysFrom(root, /System\.getenv|getenv/, ['call_expression']),
404
+ swallowedError: catchBased(['catch_block'], undefined, /^(println|print|log|logger|Log)\b/),
405
+ };
406
+ /*
407
+ * Swift is deliberately absent.
408
+ *
409
+ * The pack was written and all of its checks passed — and then the process died
410
+ * every time, inside V8's background compilation of that grammar, with a signal no
411
+ * `try` can catch. It is not a bug in the rule: the grammar reliably takes the
412
+ * runtime down after the work is done.
413
+ *
414
+ * The rule this project holds to is that an unsupported language goes unreviewed and
415
+ * never crashes the run, so shipping a pack that kills the process would break the
416
+ * guarantee it exists to serve. Restore it when the grammar or the runtime changes;
417
+ * the definition is kept here so nobody rewrites it from scratch.
418
+ */
419
+ const SWIFT_DISABLED = {
420
+ name: 'swift',
421
+ extensions: ['.swift'],
422
+ grammar: 'tree-sitter-swift',
423
+ nodes: {
424
+ ...COMMON_NODES,
425
+ identifier: ['simple_identifier'],
426
+ ifStatement: ['if_statement'],
427
+ bail: ['control_transfer_statement'],
428
+ declaration: ['function_declaration', 'class_declaration', 'protocol_declaration'],
429
+ block: ['statements', 'function_body'],
430
+ },
431
+ envReads: (root) => envKeysFrom(root, /ProcessInfo|environment/, ['call_expression', 'subscript_expression']),
432
+ swallowedError: catchBased(['catch_block'], undefined, /^(print|NSLog|os_log|logger)\b/),
433
+ };
434
+ const RUBY = {
435
+ name: 'ruby',
436
+ extensions: ['.rb', '.rake'],
437
+ grammar: 'tree-sitter-ruby',
438
+ nodes: {
439
+ ...COMMON_NODES,
440
+ ifStatement: ['if', 'if_modifier'],
441
+ ifCondition: 'condition',
442
+ ifBody: 'consequence',
443
+ bail: ['return', 'break', 'next'],
444
+ declaration: ['method', 'singleton_method', 'class', 'module'],
445
+ block: ['body_statement', 'do_block', 'block'],
446
+ },
447
+ envReads: (root) => envKeysFrom(root, /ENV/, ['element_reference', 'call']),
448
+ imports(root) {
449
+ const out = [];
450
+ for (const call of nodesOfType(root, ['call'])) {
451
+ const method = call.childForFieldName('method')?.text;
452
+ // require_relative always names this repository's own files
453
+ if (method !== 'require')
454
+ continue;
455
+ const literal = /["']([^"']+)["']/.exec(call.text);
456
+ if (literal?.[1])
457
+ out.push({ name: literal[1], node: call });
458
+ }
459
+ return out;
460
+ },
461
+ swallowedError(root) {
462
+ const out = [];
463
+ for (const rescue of nodesOfType(root, ['rescue'])) {
464
+ // a rescue holds its handler inline, so an empty one has no body_statement
465
+ if (hasComment(rescue))
466
+ continue;
467
+ const body = rescue.namedChildren.find((c) => c.type === 'body_statement' || c.type === 'then');
468
+ const statements = (body?.namedChildren ?? []).filter((c) => !COMMON_NODES.comment.includes(c.type));
469
+ if (statements.length === 0) {
470
+ out.push({ node: rescue, what: 'rescue block does nothing' });
471
+ continue;
472
+ }
473
+ if (statements.every((st) => /^(puts|print|p|log|logger|Rails\.logger)\b/.test(st.text.trim()))) {
474
+ out.push({ node: rescue, what: 'rescue block only logs' });
475
+ }
476
+ }
477
+ return out;
478
+ },
479
+ };
480
+ void SWIFT_DISABLED;
481
+ const SOLIDITY = {
482
+ name: 'solidity',
483
+ extensions: ['.sol'],
484
+ grammar: 'tree-sitter-solidity',
485
+ nodes: {
486
+ ...COMMON_NODES,
487
+ ifStatement: ['if_statement'],
488
+ // revert and require are how a contract refuses, alongside plain return
489
+ bail: ['return_statement', 'revert_statement', 'break_statement', 'continue_statement'],
490
+ declaration: ['function_definition', 'contract_declaration', 'modifier_definition'],
491
+ block: ['block_statement', 'function_body', 'contract_body'],
492
+ },
493
+ swallowedError: catchBased(['catch_clause'], 'body', /^(emit|console\.log)/),
494
+ };
495
+ export const PACKS = [PYTHON, GO, JAVA, RUST, CPP, C, CSHARP, PHP, KOTLIN, RUBY, SOLIDITY];
496
+ export function packFor(path) {
497
+ return PACKS.find((p) => p.extensions.some((e) => path.endsWith(e)));
498
+ }
499
+ let ready;
500
+ const parsers = new Map();
501
+ /**
502
+ * Measured, not guessed, and the measurement is worth writing down because the naive
503
+ * one is misleading. Loading grammars is cheap — all eleven load for ~143MB. Parsing
504
+ * with them is not: V8 tiers up each wasm module in the background, and RSS climbed
505
+ * 63 → 690MB across eleven before the process died inside that compilation. Six
506
+ * grammars sat at ~131MB and were comfortable.
507
+ */
508
+ const MAX_GRAMMARS = 6;
509
+ export const skippedLanguages = [];
510
+ /**
511
+ * Grammars load lazily and once. A repository with no Python pays nothing for
512
+ * Python, and the wasm runtime is only initialised when a foreign file appears.
513
+ */
514
+ async function parserFor(pack) {
515
+ const cached = parsers.get(pack.name);
516
+ if (cached)
517
+ return cached;
518
+ if (parsers.size >= MAX_GRAMMARS) {
519
+ if (!skippedLanguages.includes(pack.name))
520
+ skippedLanguages.push(pack.name);
521
+ return undefined;
522
+ }
523
+ try {
524
+ if (!ready) {
525
+ ready = (async () => {
526
+ const mod = (await import('web-tree-sitter'));
527
+ const Parser = (mod.default ?? mod);
528
+ await Parser.init();
529
+ return Parser;
530
+ })();
531
+ }
532
+ const Parser = await ready;
533
+ const require_ = createRequire(import.meta.url);
534
+ const wasmDir = join(dirname(require_.resolve('tree-sitter-wasms/package.json')), 'out');
535
+ const language = await Parser.Language.load(join(wasmDir, pack.grammar + '.wasm'));
536
+ const parser = new Parser();
537
+ parser.setLanguage(language);
538
+ parsers.set(pack.name, parser);
539
+ return parser;
540
+ }
541
+ catch {
542
+ // a missing grammar means this language is simply not reviewed — never a crash
543
+ return undefined;
544
+ }
545
+ }
546
+ export async function parse(pack, source) {
547
+ const parser = await parserFor(pack);
548
+ if (!parser)
549
+ return undefined;
550
+ try {
551
+ return parser.parse(source);
552
+ }
553
+ catch {
554
+ return undefined;
555
+ }
556
+ }
557
+ //# sourceMappingURL=packs.js.map
@@ -0,0 +1,108 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { decode } from '#app/text.js';
3
+ const HALLUCINATION_RULES = new Map([
4
+ ['reportAttributeAccessIssue', 'attribute does not exist on the type'],
5
+ ['reportCallIssue', 'call does not match the signature'],
6
+ ['reportArgumentType', 'argument type does not match the parameter'],
7
+ ['reportIndexIssue', 'type does not support this indexing'],
8
+ ['reportNoOverloadImplementation', 'no overload matches this call'],
9
+ ['reportRedeclaration', 'redeclared with a different type'],
10
+ ['reportUndefinedVariable', 'name is not defined'],
11
+ ]);
12
+ /**
13
+ * Where pyright might be — on PATH, put there by whoever runs this.
14
+ *
15
+ * Deliberately not the reviewed repository's `node_modules`. Resolving it there let a
16
+ * repository supply the binary that reviews it, which is arbitrary code execution
17
+ * dressed as a dev dependency, and it happens before a single finding is reported.
18
+ */
19
+ const CANDIDATES = [
20
+ { cmd: 'pyright', prefix: [] },
21
+ { cmd: 'basedpyright', prefix: [] },
22
+ ];
23
+ let resolved;
24
+ export function pyrightCommand(root) {
25
+ if (resolved !== undefined)
26
+ return resolved ?? undefined;
27
+ for (const candidate of CANDIDATES) {
28
+ try {
29
+ execFileSync(candidate.cmd, [...candidate.prefix, '--version'], {
30
+ cwd: root,
31
+ stdio: ['ignore', 'ignore', 'ignore'],
32
+ timeout: 30_000,
33
+ });
34
+ resolved = candidate;
35
+ return candidate;
36
+ }
37
+ catch {
38
+ // try the next place it could live
39
+ }
40
+ }
41
+ resolved = null;
42
+ return undefined;
43
+ }
44
+ export function pyrightAvailable(root) {
45
+ return pyrightCommand(root) !== undefined;
46
+ }
47
+ /**
48
+ * Run pyright over the given files and keep only the diagnostics that mean something
49
+ * was invented. Type-strictness complaints are deliberately not reported: a review
50
+ * that argues about optional-ness on every line is a review nobody reads.
51
+ */
52
+ export function pyrightDiagnostics(root, files) {
53
+ if (files.length === 0)
54
+ return [];
55
+ const command = pyrightCommand(root);
56
+ if (!command)
57
+ return [];
58
+ let raw;
59
+ try {
60
+ raw = decode(execFileSync(command.cmd, [...command.prefix, '--outputjson', ...files], {
61
+ cwd: root,
62
+ maxBuffer: 64 * 1024 * 1024,
63
+ // pyright exits non-zero when it finds anything, which is the normal case
64
+ stdio: ['ignore', 'pipe', 'ignore'],
65
+ timeout: 180_000,
66
+ }));
67
+ }
68
+ catch (e) {
69
+ const err = e;
70
+ // pyright exits non-zero whenever it finds something, which is the normal case and
71
+ // still writes its report. What is not normal is dying without one: killed by a
72
+ // signal, or out of time. Returning [] there would report "nothing found" about a
73
+ // checker that never looked, so it is raised and becomes a recorded failure.
74
+ // an empty Buffer is truthy, which is how the first attempt at this check missed
75
+ // the very case it was written for
76
+ if (!err.stdout || err.stdout.length === 0) {
77
+ throw new Error(err.signal
78
+ ? 'pyright was killed by ' + err.signal
79
+ : 'pyright could not run (' + (err.status ?? 'no exit code') + ')');
80
+ }
81
+ raw = decode(err.stdout);
82
+ }
83
+ let parsed;
84
+ try {
85
+ parsed = JSON.parse(raw);
86
+ }
87
+ catch {
88
+ return [];
89
+ }
90
+ const out = [];
91
+ for (const d of parsed.generalDiagnostics ?? []) {
92
+ if (d.severity !== 'error')
93
+ continue;
94
+ const meaning = d.rule ? HALLUCINATION_RULES.get(d.rule) : undefined;
95
+ if (!meaning || !d.file)
96
+ continue;
97
+ out.push({
98
+ file: d.file,
99
+ line: (d.range?.start?.line ?? 0) + 1,
100
+ column: (d.range?.start?.character ?? 0) + 1,
101
+ rule: d.rule ?? '',
102
+ message: (d.message ?? '').split('\n')[0] ?? '',
103
+ meaning,
104
+ });
105
+ }
106
+ return out;
107
+ }
108
+ //# sourceMappingURL=pyright.js.map