@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.
- package/LICENSE +202 -0
- package/README.md +306 -0
- package/dist/agents.js +82 -0
- package/dist/bench.js +179 -0
- package/dist/budget.js +59 -0
- package/dist/bundle.js +173 -0
- package/dist/cache.js +155 -0
- package/dist/cli/agent-command.js +27 -0
- package/dist/cli/app.js +31 -0
- package/dist/cli/args.js +76 -0
- package/dist/cli/bench-command.js +89 -0
- package/dist/cli/dismiss-command.js +42 -0
- package/dist/cli/environment.js +32 -0
- package/dist/cli/reports.js +64 -0
- package/dist/cli/review-command.js +268 -0
- package/dist/cli/session-command.js +62 -0
- package/dist/cli.js +7 -0
- package/dist/config.js +130 -0
- package/dist/delegate.js +84 -0
- package/dist/dismissed.js +130 -0
- package/dist/fspolicy.js +62 -0
- package/dist/git.js +238 -0
- package/dist/ground.js +286 -0
- package/dist/judges/judge.js +85 -0
- package/dist/judges/llm.js +234 -0
- package/dist/judges/prompts.js +86 -0
- package/dist/judges/tools.js +125 -0
- package/dist/lang/packs.js +557 -0
- package/dist/lang/pyright.js +108 -0
- package/dist/lang/python-deps.js +174 -0
- package/dist/lang/ruby-deps.js +77 -0
- package/dist/langtest.js +248 -0
- package/dist/manifest.js +209 -0
- package/dist/otel.js +75 -0
- package/dist/package-meta.js +13 -0
- package/dist/package-smoke.js +110 -0
- package/dist/plan.js +134 -0
- package/dist/position.js +94 -0
- package/dist/report/ansi.js +18 -0
- package/dist/report/codequality.js +19 -0
- package/dist/report/compact.js +15 -0
- package/dist/report/highlight.js +54 -0
- package/dist/report/markdown.js +113 -0
- package/dist/report/sarif.js +66 -0
- package/dist/report/terminal.js +170 -0
- package/dist/report/viewer.js +148 -0
- package/dist/review.js +355 -0
- package/dist/scan.js +67 -0
- package/dist/selftest.js +1928 -0
- package/dist/session.js +140 -0
- package/dist/snapshot.js +101 -0
- package/dist/text.js +50 -0
- package/dist/types.js +2 -0
- package/dist/verifiers/assertion-drift.js +137 -0
- package/dist/verifiers/contract-drift.js +140 -0
- package/dist/verifiers/copy-paste-drift.js +106 -0
- package/dist/verifiers/dead-on-arrival.js +92 -0
- package/dist/verifiers/dropped-guard.js +144 -0
- package/dist/verifiers/foreign-contract-drift.js +114 -0
- package/dist/verifiers/foreign-copy-paste-drift.js +83 -0
- package/dist/verifiers/foreign-dropped-guard.js +78 -0
- package/dist/verifiers/foreign-phantom-api.js +36 -0
- package/dist/verifiers/foreign-phantom-config.js +40 -0
- package/dist/verifiers/foreign-phantom-dep.js +82 -0
- package/dist/verifiers/foreign-reinvented.js +65 -0
- package/dist/verifiers/foreign-scope-creep.js +42 -0
- package/dist/verifiers/foreign-swallowed-error.js +36 -0
- package/dist/verifiers/foreign-tests.js +143 -0
- package/dist/verifiers/foreign-tokens.js +94 -0
- package/dist/verifiers/foreign.js +16 -0
- package/dist/verifiers/index.js +38 -0
- package/dist/verifiers/lying-comment.js +90 -0
- package/dist/verifiers/phantom-api.js +88 -0
- package/dist/verifiers/phantom-config.js +93 -0
- package/dist/verifiers/phantom-dep.js +110 -0
- package/dist/verifiers/reinvented.js +74 -0
- package/dist/verifiers/scope-creep.js +77 -0
- package/dist/verifiers/swallowed-error.js +110 -0
- package/dist/verifiers/vacuous-test.js +138 -0
- package/docs/architecture.md +191 -0
- package/docs/assets/cli-preview.svg +68 -0
- package/docs/assets/powershot-logo.png +0 -0
- package/docs/ci.md +151 -0
- package/examples/github-actions/action.yml +23 -0
- package/examples/github-actions/cli.yml +43 -0
- package/examples/gitlab/.gitlab-ci.yml +21 -0
- package/package.json +65 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { decode } from '#app/text.js';
|
|
4
|
+
/**
|
|
5
|
+
* Python's top-level standard library. A name that is here needs no dependency, and a
|
|
6
|
+
* name that is missing from here would be reported — so an incomplete list produces
|
|
7
|
+
* false positives, which is why this is the full 3.12 set rather than a sample.
|
|
8
|
+
*/
|
|
9
|
+
const STDLIB = new Set(('abc aifc argparse array ast asynchat asyncio asyncore atexit audioop base64 bdb binascii bisect builtins bz2 ' +
|
|
10
|
+
'calendar cgi cgitb chunk cmath cmd code codecs codeop collections colorsys compileall concurrent configparser ' +
|
|
11
|
+
'contextlib contextvars copy copyreg crypt csv ctypes curses dataclasses datetime dbm decimal difflib dis ' +
|
|
12
|
+
'distutils doctest email encodings ensurepip enum errno faulthandler fcntl filecmp fileinput fnmatch fractions ' +
|
|
13
|
+
'ftplib functools gc getopt getpass gettext glob graphlib grp gzip hashlib heapq hmac html http idlelib imaplib ' +
|
|
14
|
+
'imghdr imp importlib inspect io ipaddress itertools json keyword lib2to3 linecache locale logging lzma ' +
|
|
15
|
+
'mailbox mailcap marshal math mimetypes mmap modulefinder msilib msvcrt multiprocessing netrc nis nntplib ' +
|
|
16
|
+
'ntpath numbers operator optparse os ossaudiodev pathlib pdb pickle pickletools pipes pkgutil platform plistlib ' +
|
|
17
|
+
'poplib posix posixpath pprint profile pstats pty pwd py_compile pyclbr pydoc queue quopri random re readline ' +
|
|
18
|
+
'reprlib resource rlcompleter runpy sched secrets select selectors shelve shlex shutil signal site smtplib ' +
|
|
19
|
+
'sndhdr socket socketserver spwd sqlite3 sre_compile sre_constants sre_parse ssl stat statistics string ' +
|
|
20
|
+
'stringprep struct subprocess sunau symtable sys sysconfig syslog tabnanny tarfile telnetlib tempfile termios ' +
|
|
21
|
+
'test textwrap threading time timeit tkinter token tokenize tomllib trace traceback tracemalloc tty turtle ' +
|
|
22
|
+
'types typing unicodedata unittest urllib uu uuid venv warnings wave weakref webbrowser winreg winsound wsgiref ' +
|
|
23
|
+
'xdrlib xml xmlrpc zipapp zipfile zipimport zlib zoneinfo __future__ _thread').split(' '));
|
|
24
|
+
/**
|
|
25
|
+
* Import names that differ from the distribution that provides them. Without this a
|
|
26
|
+
* project depending on PyYAML looks like it forgot `yaml`, which is the kind of
|
|
27
|
+
* confident wrong answer this tool exists to avoid.
|
|
28
|
+
*/
|
|
29
|
+
const DISTRIBUTION = {
|
|
30
|
+
yaml: 'pyyaml', cv2: 'opencv-python', PIL: 'pillow', sklearn: 'scikit-learn',
|
|
31
|
+
bs4: 'beautifulsoup4', dateutil: 'python-dateutil', jwt: 'pyjwt', dotenv: 'python-dotenv',
|
|
32
|
+
serial: 'pyserial', attr: 'attrs', google: 'protobuf', OpenSSL: 'pyopenssl',
|
|
33
|
+
pkg_resources: 'setuptools', setuptools: 'setuptools', magic: 'python-magic',
|
|
34
|
+
psycopg2: 'psycopg2-binary', redis: 'redis', mysql: 'mysql-connector-python',
|
|
35
|
+
docx: 'python-docx', pptx: 'python-pptx', fitz: 'pymupdf', win32api: 'pywin32',
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The distribution named by one requirement.
|
|
39
|
+
*
|
|
40
|
+
* A PEP 508 requirement carries its version and extras inside the quotes —
|
|
41
|
+
* `sqlalchemy[asyncio]>=2.0.48` — so the name is the leading run of name characters,
|
|
42
|
+
* not the whole string. Requiring the whole quoted string to be a bare name is what
|
|
43
|
+
* made 91 declared dependencies look missing on one real repository.
|
|
44
|
+
*/
|
|
45
|
+
function requirementName(entry) {
|
|
46
|
+
const m = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)/.exec(entry);
|
|
47
|
+
return m?.[1];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Dependency names from a manifest, read section by section rather than by scanning
|
|
51
|
+
* every quoted string — a pyproject.toml is full of quoted words that are not
|
|
52
|
+
* packages, and treating them all as declared would hide real findings.
|
|
53
|
+
*/
|
|
54
|
+
function dependencyNames(text, file) {
|
|
55
|
+
const out = [];
|
|
56
|
+
if (file.endsWith('.txt')) {
|
|
57
|
+
for (const line of text.split(/\r?\n/)) {
|
|
58
|
+
if (line.trimStart().startsWith('#'))
|
|
59
|
+
continue;
|
|
60
|
+
const name = requirementName(line);
|
|
61
|
+
if (name)
|
|
62
|
+
out.push(normalize(name));
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
// PEP 621 and setuptools: `dependencies = [...]`, plus optional-dependency groups.
|
|
67
|
+
// The array ends at a bracket that starts a line: an extras marker such as
|
|
68
|
+
// `uvicorn[standard]>=0.42` carries a `]` of its own, and a lazy match to any
|
|
69
|
+
// bracket stops there, silently dropping every requirement after it.
|
|
70
|
+
for (const block of text.matchAll(/dependencies\s*=\s*\[([\s\S]*?)^\s*\]/gm)) {
|
|
71
|
+
for (const entry of (block[1] ?? '').matchAll(/["']([^"']+)["']/g)) {
|
|
72
|
+
const name = requirementName(entry[1] ?? '');
|
|
73
|
+
if (name)
|
|
74
|
+
out.push(normalize(name));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const section of text.split(/^\[/m)) {
|
|
78
|
+
// Poetry and Pipfile list one package per line: `name = "^1.0"`
|
|
79
|
+
if (/^(tool\.poetry[a-z.]*dependencies|packages|dev-packages)\]/.test(section)) {
|
|
80
|
+
for (const line of section.split(/\r?\n/)) {
|
|
81
|
+
const m = /^\s*([A-Za-z0-9][A-Za-z0-9._-]*)\s*=\s*["'{]/.exec(line);
|
|
82
|
+
if (m?.[1])
|
|
83
|
+
out.push(normalize(m[1]));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Optional-dependency and PEP 735 groups nest arrays under a group name, so the
|
|
87
|
+
// key is `dev` rather than `dependencies` and the packages are inside the array
|
|
88
|
+
if (/^(project\.optional-dependencies|dependency-groups|tool\.uv)\]/.test(section)) {
|
|
89
|
+
for (const entry of section.matchAll(/["']([^"']+)["']/g)) {
|
|
90
|
+
const name = requirementName(entry[1] ?? '');
|
|
91
|
+
if (name)
|
|
92
|
+
out.push(normalize(name));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
/** PyPI treats `-`, `_` and `.` alike, and is case-insensitive. */
|
|
99
|
+
function normalize(name) {
|
|
100
|
+
return name.toLowerCase().replace(/[-_.]+/g, '-');
|
|
101
|
+
}
|
|
102
|
+
const MANIFESTS = ['requirements.txt', 'pyproject.toml', 'Pipfile', 'setup.py', 'requirements-dev.txt'];
|
|
103
|
+
/**
|
|
104
|
+
* Declared dependencies visible to one file.
|
|
105
|
+
*
|
|
106
|
+
* A Python monorepo declares pydantic in backend/classifications/pyproject.toml and
|
|
107
|
+
* nowhere else, so reading only the repository root calls every real dependency
|
|
108
|
+
* phantom — measured at 270 findings on one real repository. Every manifest from the
|
|
109
|
+
* file's own directory up to the root counts.
|
|
110
|
+
*/
|
|
111
|
+
export function pythonManifest(root, from = root) {
|
|
112
|
+
const found = [];
|
|
113
|
+
const names = new Set();
|
|
114
|
+
const dirs = [];
|
|
115
|
+
for (let dir = from;; dir = dirname(dir)) {
|
|
116
|
+
dirs.push(dir);
|
|
117
|
+
if (dir === root || dirname(dir) === dir)
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
for (const dir of dirs) {
|
|
121
|
+
for (const file of MANIFESTS) {
|
|
122
|
+
const path = join(dir, file);
|
|
123
|
+
if (!existsSync(path))
|
|
124
|
+
continue;
|
|
125
|
+
if (!found.includes(file))
|
|
126
|
+
found.push(file);
|
|
127
|
+
const text = decode(readFileSync(path));
|
|
128
|
+
for (const name of dependencyNames(text, file))
|
|
129
|
+
names.add(name);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return found.length > 0 ? { names, file: found.join(', ') } : undefined;
|
|
133
|
+
}
|
|
134
|
+
const SKIP_DIRS = new Set([
|
|
135
|
+
'node_modules', '.git', '.venv', 'venv', 'env', '__pycache__', 'dist', 'build',
|
|
136
|
+
'.mypy_cache', '.pytest_cache', '.tox', 'site-packages', 'target', '.next',
|
|
137
|
+
]);
|
|
138
|
+
export function localModules(root) {
|
|
139
|
+
const local = new Set();
|
|
140
|
+
const walk = (dir, depth) => {
|
|
141
|
+
if (depth > 6 || local.size > 4000)
|
|
142
|
+
return;
|
|
143
|
+
let entries;
|
|
144
|
+
try {
|
|
145
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const isPackage = entries.some((e) => e.isFile() && e.name === '__init__.py');
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (entry.name.startsWith('.') || SKIP_DIRS.has(entry.name))
|
|
153
|
+
continue;
|
|
154
|
+
if (entry.isDirectory()) {
|
|
155
|
+
// a package directory is importable by name; so is a plain source folder
|
|
156
|
+
local.add(entry.name);
|
|
157
|
+
walk(join(dir, entry.name), depth + 1);
|
|
158
|
+
}
|
|
159
|
+
else if (entry.name.endsWith('.py') && (isPackage || depth <= 2)) {
|
|
160
|
+
local.add(entry.name.slice(0, -3));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
walk(root, 0);
|
|
165
|
+
return local;
|
|
166
|
+
}
|
|
167
|
+
export function isPhantom(importName, manifest, local) {
|
|
168
|
+
const top = importName.split('.')[0] ?? importName;
|
|
169
|
+
if (STDLIB.has(top) || local.has(top))
|
|
170
|
+
return false;
|
|
171
|
+
const candidates = [normalize(top), normalize(DISTRIBUTION[top] ?? top)];
|
|
172
|
+
return !candidates.some((c) => manifest.names.has(c));
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=python-deps.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { decode } from '#app/text.js';
|
|
4
|
+
/** Ruby's standard library and default gems, by the name you require. */
|
|
5
|
+
const STDLIB = new Set(('abbrev base64 benchmark bigdecimal cgi coverage csv date delegate digest drb english erb etc expect fcntl ' +
|
|
6
|
+
'fiddle fileutils find forwardable getoptlong io ipaddr irb json logger matrix minitest monitor mutex_m net ' +
|
|
7
|
+
'nkf objspace observer open-uri open3 openssl optparse ostruct pathname pp prettyprint prime pstore psych ' +
|
|
8
|
+
'racc rake rdoc readline reline resolv rexml rinda ripper rss rubygems securerandom set shellwords singleton ' +
|
|
9
|
+
'socket stringio strscan syslog tempfile test time timeout tmpdir tsort un uri weakref yaml zlib ' +
|
|
10
|
+
'English Set').split(' '));
|
|
11
|
+
/**
|
|
12
|
+
* Requires whose name differs from the gem that provides them. Rails is the reason
|
|
13
|
+
* this table exists: a Gemfile says `rails`, and the code requires `active_record`.
|
|
14
|
+
*/
|
|
15
|
+
const GEM_FOR = {
|
|
16
|
+
active_record: 'rails', active_support: 'rails', action_pack: 'rails', action_view: 'rails',
|
|
17
|
+
action_mailer: 'rails', active_job: 'rails', active_storage: 'rails', action_cable: 'rails',
|
|
18
|
+
rails_helper: 'rails', sinatra: 'sinatra', sequel: 'sequel', nokogiri: 'nokogiri',
|
|
19
|
+
httparty: 'httparty', rspec: 'rspec', sidekiq: 'sidekiq', pry: 'pry', puma: 'puma',
|
|
20
|
+
jwt: 'jwt', redis: 'redis', pg: 'pg', mysql2: 'mysql2', dotenv: 'dotenv',
|
|
21
|
+
};
|
|
22
|
+
function normalize(name) {
|
|
23
|
+
return name.toLowerCase().replace(/[-_]+/g, '-');
|
|
24
|
+
}
|
|
25
|
+
/** Gems declared in the Gemfile or a gemspec. */
|
|
26
|
+
export function rubyManifest(root) {
|
|
27
|
+
const names = new Set();
|
|
28
|
+
const found = [];
|
|
29
|
+
const files = ['Gemfile', ...(existsSync(root) ? readdirSync(root).filter((f) => f.endsWith('.gemspec')) : [])];
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
const path = join(root, file);
|
|
32
|
+
if (!existsSync(path))
|
|
33
|
+
continue;
|
|
34
|
+
found.push(file);
|
|
35
|
+
const text = decode(readFileSync(path));
|
|
36
|
+
// `gem "name"` in a Gemfile, `add_dependency "name"` in a gemspec
|
|
37
|
+
for (const m of text.matchAll(/(?:^\s*gem|add(?:_runtime|_development)?_dependency)\s*[( ]\s*["']([^"']+)["']/gm)) {
|
|
38
|
+
if (m[1])
|
|
39
|
+
names.add(normalize(m[1]));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return found.length > 0 ? { names, file: found.join(', ') } : undefined;
|
|
43
|
+
}
|
|
44
|
+
/** Files this repository provides, so a require of its own code is not a dependency. */
|
|
45
|
+
export function rubyLocal(root) {
|
|
46
|
+
const local = new Set();
|
|
47
|
+
for (const dir of ['lib', 'app', '.', 'config']) {
|
|
48
|
+
const base = join(root, dir);
|
|
49
|
+
if (!existsSync(base))
|
|
50
|
+
continue;
|
|
51
|
+
try {
|
|
52
|
+
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
53
|
+
if (entry.isDirectory())
|
|
54
|
+
local.add(entry.name);
|
|
55
|
+
else if (entry.name.endsWith('.rb'))
|
|
56
|
+
local.add(entry.name.slice(0, -3));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// unreadable directory contributes nothing
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return local;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* `require "foo/bar"` is provided by the gem `foo`, which holds for nearly every gem.
|
|
67
|
+
* Reported as `firm`: the require-to-gem relationship is a convention, not a rule, and
|
|
68
|
+
* a gem can also arrive through a path or git source this cannot see.
|
|
69
|
+
*/
|
|
70
|
+
export function isPhantomGem(required, manifest, local) {
|
|
71
|
+
const top = required.split('/')[0] ?? required;
|
|
72
|
+
if (STDLIB.has(top) || local.has(top) || local.has(required))
|
|
73
|
+
return false;
|
|
74
|
+
const candidates = [normalize(top), normalize(GEM_FOR[top] ?? top), normalize(top.replace(/_/g, ''))];
|
|
75
|
+
return !candidates.some((c) => manifest.names.has(c));
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=ruby-deps.js.map
|
package/dist/langtest.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language pack checks, one process per language.
|
|
3
|
+
*
|
|
4
|
+
* They cannot share a process: V8 tiers up each wasm grammar in the background, and
|
|
5
|
+
* eleven of them together drove RSS past 690MB and killed the run. A child per
|
|
6
|
+
* language costs a second and keeps every pack genuinely covered rather than
|
|
7
|
+
* shrinking the suite to fit a limit.
|
|
8
|
+
*/
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { execFileSync } from 'node:child_process';
|
|
11
|
+
import { PACKS, parse } from './lang/packs.js';
|
|
12
|
+
import { tokensFor } from './verifiers/foreign.js';
|
|
13
|
+
/** each fixture: a handler that discards, one that genuinely handles, one explained */
|
|
14
|
+
const FIXTURES = {
|
|
15
|
+
python: 'def f():\n try:\n a()\n except Exception:\n pass\n try:\n b()\n except Exception as e:\n raise RuntimeError("x") from e\n try:\n c()\n except Exception:\n pass # deliberate\n',
|
|
16
|
+
go: 'package m\nfunc F() error {\n\tif err := a(); err != nil {\n\t}\n\tif err := b(); err != nil {\n\t\treturn err\n\t}\n\tif err := c(); err != nil {\n\t\t// deliberate\n\t}\n\treturn nil\n}\n',
|
|
17
|
+
java: 'class A {\n void f() {\n try { a(); } catch (Exception e) { }\n try { b(); } catch (Exception e) { throw new RuntimeException(e); }\n try { c(); } catch (Exception e) { /* deliberate */ }\n }\n}\n',
|
|
18
|
+
// `let _ = a()` is Rust's own "on purpose" marker and must NOT be reported; the
|
|
19
|
+
// empty Err arm is the one with no such marker
|
|
20
|
+
rust: 'fn f() {\n let _ = a();\n match x() { Err(_) => {}, Ok(v) => v };\n match b() { Err(e) => return Err(e), Ok(v) => v };\n}\n',
|
|
21
|
+
cpp: 'void f() {\n try { a(); } catch (...) { }\n try { b(); } catch (const std::exception& e) { throw; }\n try { c(); } catch (...) { /* deliberate */ }\n}\n',
|
|
22
|
+
c: 'int f(int n) {\n if (n < 0) { return 0; }\n return n;\n}\n',
|
|
23
|
+
'c#': 'class A {\n void F() {\n try { A(); } catch (Exception e) { }\n try { B(); } catch (Exception e) { throw; }\n try { C(); } catch (Exception e) { /* deliberate */ }\n }\n}\n',
|
|
24
|
+
php: '<?php\nfunction f() {\n try { a(); } catch (Exception $e) { }\n try { b(); } catch (Exception $e) { throw $e; }\n try { c(); } catch (Exception $e) { /* deliberate */ }\n}\n',
|
|
25
|
+
kotlin: 'fun f() {\n try { a() } catch (e: Exception) {}\n try { b() } catch (e: Exception) { throw e }\n try { c() } catch (e: Exception) { /* deliberate */ }\n}\n',
|
|
26
|
+
solidity: 'contract A {\n function f() public {\n try a() { } catch { }\n try b() { } catch { revert("x"); }\n try c() { } catch { /* deliberate */ }\n }\n}\n',
|
|
27
|
+
ruby: 'def f\n begin\n a\n rescue => e\n end\n begin\n b\n rescue => e\n raise\n end\n begin\n c\n rescue => e\n # deliberate\n end\nend\n',
|
|
28
|
+
};
|
|
29
|
+
/** the handler in each fixture that genuinely handles, and must never be reported */
|
|
30
|
+
const HANDLED = {
|
|
31
|
+
python: 'raise RuntimeError', go: 'return err', java: 'throw new RuntimeException',
|
|
32
|
+
rust: 'return Err(e)', cpp: 'throw;', 'c#': 'throw;', php: 'throw $e',
|
|
33
|
+
kotlin: 'throw e', ruby: 'raise', solidity: 'revert("x")',
|
|
34
|
+
};
|
|
35
|
+
async function one(name) {
|
|
36
|
+
const pack = PACKS.find((p) => p.name === name);
|
|
37
|
+
if (!pack) {
|
|
38
|
+
console.error('no pack named ' + name);
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
let failed = 0;
|
|
42
|
+
const check = (label, fn) => {
|
|
43
|
+
try {
|
|
44
|
+
fn();
|
|
45
|
+
console.log(' ok ' + name + ': ' + label);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
failed++;
|
|
49
|
+
console.log(' FAIL ' + name + ': ' + label + '\n ' + e.message);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const source = FIXTURES[name];
|
|
53
|
+
const tree = source === undefined ? undefined : await parse(pack, source);
|
|
54
|
+
check('has a fixture, so it is never asserted against another language', () => {
|
|
55
|
+
assert.ok(source !== undefined, 'no fixture written');
|
|
56
|
+
});
|
|
57
|
+
check('the grammar loads and parses', () => {
|
|
58
|
+
assert.ok(tree, 'failed to parse — grammar missing or ABI mismatch');
|
|
59
|
+
});
|
|
60
|
+
check('declares the node names the generic checks need', () => {
|
|
61
|
+
for (const key of ['identifier', 'comment', 'ifStatement', 'bail', 'declaration', 'block']) {
|
|
62
|
+
assert.ok(pack.nodes[key].length > 0, 'no ' + key);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const hits = tree ? pack.swallowedError?.(tree.rootNode) ?? [] : [];
|
|
66
|
+
if (name === 'c') {
|
|
67
|
+
check('does not advertise an exception oracle the language cannot supply', () => {
|
|
68
|
+
assert.equal(pack.swallowedError, undefined);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
check('finds the idiom that discards a failure', () => {
|
|
73
|
+
assert.ok(hits.length >= 1, 'found none');
|
|
74
|
+
});
|
|
75
|
+
check('stays silent where a comment states the intent', () => {
|
|
76
|
+
assert.equal(hits.some((h) => /deliberate/.test(h.node.text)), false, 'reported an explained handler');
|
|
77
|
+
});
|
|
78
|
+
check('leaves a properly handled error alone', () => {
|
|
79
|
+
assert.equal(hits.some((h) => h.node.text.includes(HANDLED[name])), false, 'reported a real handler');
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return failed;
|
|
83
|
+
}
|
|
84
|
+
/** Signature comparison is Python-only for now, and is what decides a contract break. */
|
|
85
|
+
async function pythonSignatures() {
|
|
86
|
+
const pack = PACKS.find((p) => p.name === 'python');
|
|
87
|
+
let failed = 0;
|
|
88
|
+
const check = (label, fn) => {
|
|
89
|
+
try {
|
|
90
|
+
fn();
|
|
91
|
+
console.log(' ok python signatures: ' + label);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
failed++;
|
|
95
|
+
console.log(' FAIL python signatures: ' + label + '\n ' + e.message);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const of = async (src) => {
|
|
99
|
+
const tree = await parse(pack, src);
|
|
100
|
+
return pack.signatures(tree.rootNode);
|
|
101
|
+
};
|
|
102
|
+
const one = await of('def send(to: str) -> bool:\n return True\n');
|
|
103
|
+
const two = await of('def send(to: str, subject: str) -> bool:\n return True\n');
|
|
104
|
+
const defaulted = await of('def send(to: str, subject: str = "hi") -> bool:\n return True\n');
|
|
105
|
+
const splat = await of('def send(to: str, *args, **kwargs) -> bool:\n return True\n');
|
|
106
|
+
const method = await of('class A:\n def send(self, to: str) -> bool:\n return True\n');
|
|
107
|
+
check('counts what a caller must supply', () => {
|
|
108
|
+
assert.equal(one.get('send').required, 1);
|
|
109
|
+
assert.equal(two.get('send').required, 2);
|
|
110
|
+
});
|
|
111
|
+
check('a default does not demand anything of a caller', () => {
|
|
112
|
+
assert.equal(defaulted.get('send').required, 1);
|
|
113
|
+
});
|
|
114
|
+
check('*args and **kwargs demand nothing', () => {
|
|
115
|
+
assert.equal(splat.get('send').required, 1);
|
|
116
|
+
});
|
|
117
|
+
check('self is bound, not passed', () => {
|
|
118
|
+
assert.equal(method.get('send').required, 1);
|
|
119
|
+
});
|
|
120
|
+
return failed;
|
|
121
|
+
}
|
|
122
|
+
/** Test and docstring conventions are per-language data; these are Python's. */
|
|
123
|
+
async function pythonConventions() {
|
|
124
|
+
const pack = PACKS.find((p) => p.name === 'python');
|
|
125
|
+
let failed = 0;
|
|
126
|
+
const check = (label, fn) => {
|
|
127
|
+
try {
|
|
128
|
+
fn();
|
|
129
|
+
console.log(' ok python conventions: ' + label);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
failed++;
|
|
133
|
+
console.log(' FAIL python conventions: ' + label + '\n ' + e.message);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const tests = async (src) => pack.tests((await parse(pack, src)).rootNode);
|
|
137
|
+
const docs = async (src) => pack.documentedParams((await parse(pack, src)).rootNode);
|
|
138
|
+
const Q = '"' + '"' + '"';
|
|
139
|
+
const bare = await tests('def test_a():\n do_work()\n');
|
|
140
|
+
const asserted = await tests('def test_a():\n assert do_work() == 1\n');
|
|
141
|
+
const unittest = await tests('class T:\n def test_a(self):\n self.assertEqual(do_work(), 1)\n');
|
|
142
|
+
const raises = await tests('def test_a():\n with pytest.raises(ValueError):\n do_work()\n');
|
|
143
|
+
const placeholder = await tests('def test_a():\n pass\n');
|
|
144
|
+
check('a test that runs code and proves nothing is caught', () => {
|
|
145
|
+
assert.equal(bare[0].provesNothing, true);
|
|
146
|
+
});
|
|
147
|
+
check('a plain assert counts as proof', () => {
|
|
148
|
+
assert.equal(asserted[0].provesNothing, false);
|
|
149
|
+
});
|
|
150
|
+
check('unittest assertion methods count as proof', () => {
|
|
151
|
+
assert.equal(unittest[0].provesNothing, false);
|
|
152
|
+
});
|
|
153
|
+
check('a raises block counts as proof', () => {
|
|
154
|
+
assert.equal(raises[0].provesNothing, false);
|
|
155
|
+
});
|
|
156
|
+
check('a placeholder body is not a claim that came out empty', () => {
|
|
157
|
+
assert.equal(placeholder[0].provesNothing, false);
|
|
158
|
+
});
|
|
159
|
+
check('an assertion exposes what it asserts about, for drift', () => {
|
|
160
|
+
assert.equal(asserted[0].assertions[0].subject, 'do_work()');
|
|
161
|
+
assert.equal(asserted[0].assertions[0].expected, '1');
|
|
162
|
+
assert.equal(unittest[0].assertions[0].expected, '1');
|
|
163
|
+
});
|
|
164
|
+
const google = await docs('def f(a, b):\n ' + Q + 'Do it.\n\n Args:\n a: first\n c: nope\n ' + Q + '\n return a\n');
|
|
165
|
+
const rest = await docs('def f(a):\n ' + Q + 'Do it.\n\n :param a: first\n :param z: nope\n ' + Q + '\n return a\n');
|
|
166
|
+
const undocumented = await docs('def f(a):\n return a\n');
|
|
167
|
+
check('a Google-style Args block is read', () => {
|
|
168
|
+
assert.deepEqual(google[0].documented.map((d) => d.name), ['a', 'c']);
|
|
169
|
+
assert.deepEqual(google[0].declared, ['a', 'b']);
|
|
170
|
+
});
|
|
171
|
+
check('a reST :param: block is read', () => {
|
|
172
|
+
assert.deepEqual(rest[0].documented.map((d) => d.name), ['a', 'z']);
|
|
173
|
+
});
|
|
174
|
+
check('a function with no docstring makes no claim to contradict', () => {
|
|
175
|
+
assert.equal(undocumented.length, 0);
|
|
176
|
+
});
|
|
177
|
+
return failed;
|
|
178
|
+
}
|
|
179
|
+
/** A change inside a string literal must never read as "formatting only". */
|
|
180
|
+
async function literalsAreVisible() {
|
|
181
|
+
let failed = 0;
|
|
182
|
+
const check = (label, fn) => {
|
|
183
|
+
try {
|
|
184
|
+
fn();
|
|
185
|
+
console.log(' ok literals: ' + label);
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
failed++;
|
|
189
|
+
console.log(' FAIL literals: ' + label + '\n ' + e.message);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const sample = {
|
|
193
|
+
rust: (v) => 'fn f() { let x = "' + v + '"; }',
|
|
194
|
+
go: (v) => 'package m\nvar x = "' + v + '"\n',
|
|
195
|
+
java: (v) => 'class A { String x = "' + v + '"; }',
|
|
196
|
+
cpp: (v) => 'auto x = "' + v + '";',
|
|
197
|
+
python: (v) => 'x = "' + v + '"\n',
|
|
198
|
+
ruby: (v) => 'x = "' + v + '"\n',
|
|
199
|
+
};
|
|
200
|
+
for (const [name, build] of Object.entries(sample)) {
|
|
201
|
+
const pack = PACKS.find((p) => p.name === name);
|
|
202
|
+
if (!pack)
|
|
203
|
+
continue;
|
|
204
|
+
const a = await parse(pack, build('alpha_e_gguf'));
|
|
205
|
+
const b = await parse(pack, build('alpha_d_gguf'));
|
|
206
|
+
check(name + ': a changed string literal is part of the token stream', () => {
|
|
207
|
+
assert.ok(a && b);
|
|
208
|
+
assert.notEqual(JSON.stringify(tokensFor(a.rootNode, pack)), JSON.stringify(tokensFor(b.rootNode, pack)), name + ' tokenizes two different strings identically');
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return failed;
|
|
212
|
+
}
|
|
213
|
+
async function main() {
|
|
214
|
+
const target = process.argv[2];
|
|
215
|
+
if (target)
|
|
216
|
+
return one(target);
|
|
217
|
+
console.log('\nlanguage packs (one process each)');
|
|
218
|
+
let failed = (await pythonSignatures()) + (await pythonConventions()) + (await literalsAreVisible());
|
|
219
|
+
for (const pack of PACKS) {
|
|
220
|
+
try {
|
|
221
|
+
process.stdout.write(execFileSync(process.execPath, [process.argv[1], pack.name], { encoding: 'utf8' }));
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
const err = e;
|
|
225
|
+
process.stdout.write(err.stdout ?? '');
|
|
226
|
+
// a child killed by the runtime rather than by a failed assertion
|
|
227
|
+
if (err.status === null || err.signal) {
|
|
228
|
+
console.log(' FAIL ' + pack.name + ': the process died (' + (err.signal ?? 'no exit code') + ')');
|
|
229
|
+
}
|
|
230
|
+
failed++;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return failed;
|
|
234
|
+
}
|
|
235
|
+
main().then((failed) => {
|
|
236
|
+
// only the parent summarises; a child reports just its own language
|
|
237
|
+
if (!process.argv[2]) {
|
|
238
|
+
if (failed > 0)
|
|
239
|
+
console.error('\n' + failed + ' language pack(s) failed');
|
|
240
|
+
else
|
|
241
|
+
console.log('\nall ' + PACKS.length + ' language packs pass');
|
|
242
|
+
}
|
|
243
|
+
// Deliberately not process.exit(): V8 is still compiling the wasm grammar in the
|
|
244
|
+
// background, and tearing the process down underneath that work crashes it. Setting
|
|
245
|
+
// the code and letting the loop drain costs nothing and ends cleanly.
|
|
246
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
|
247
|
+
});
|
|
248
|
+
//# sourceMappingURL=langtest.js.map
|