@clear-capabilities/agentic-security-scanner 0.153.2 → 0.154.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/CHANGELOG.md +34 -0
- package/bin/agentic-security.js +123 -19
- package/dist/1444.index.js +64 -19
- package/dist/1905.index.js +46 -3
- package/dist/2862.index.js +325 -0
- package/dist/3499.index.js +5 -1
- package/dist/3531.index.js +261 -0
- package/dist/3596.index.js +103 -0
- package/dist/4113.index.js +15 -3
- package/dist/4171.index.js +319 -0
- package/dist/5637.index.js +10 -4
- package/dist/6359.index.js +433 -0
- package/dist/7838.index.js +15 -3
- package/dist/8551.index.js +1164 -0
- package/dist/8587.index.js +410 -0
- package/dist/8714.index.js +98 -0
- package/dist/8798.index.js +218 -0
- package/dist/9220.index.js +35 -3
- package/dist/9560.index.js +26 -3
- package/dist/agentic-security.mjs +51 -51
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +17 -2
- package/src/dataflow/CLAUDE.md +9 -0
- package/src/dataflow/catalog-haskell.js +37 -0
- package/src/dataflow/catalog.js +4 -0
- package/src/dataflow/engine.js +33 -3
- package/src/dataflow/privacy-catalog-haskell.js +50 -0
- package/src/dataflow/privacy-catalog.js +5 -1
- package/src/discovery/CLAUDE.md +6 -0
- package/src/discovery/index.js +12 -3
- package/src/discovery/lenses.js +4 -1
- package/src/discovery/partition.js +28 -0
- package/src/egress/redact.js +8 -0
- package/src/engine.js +125 -14
- package/src/ir/index.js +39 -2
- package/src/language/CLAUDE.md +74 -0
- package/src/language/accuracy.js +90 -0
- package/src/language/aibom.js +307 -0
- package/src/language/assurance.js +193 -0
- package/src/language/bom-validate.js +227 -0
- package/src/language/bom.js +236 -0
- package/src/language/bridges.js +392 -0
- package/src/language/compliance-map.js +61 -0
- package/src/language/context.js +199 -0
- package/src/language/contracts.js +339 -0
- package/src/language/discovery.js +359 -0
- package/src/language/engine-pass.js +120 -0
- package/src/language/fix-lifecycle.js +120 -0
- package/src/language/haskell-adapter.js +51 -0
- package/src/language/haskell-disclosure.js +170 -0
- package/src/language/haskell-findings.js +44 -0
- package/src/language/haskell-fix.js +409 -0
- package/src/language/haskell-grammar.js +78 -0
- package/src/language/haskell-guards.js +382 -0
- package/src/language/haskell-ir.js +1377 -0
- package/src/language/haskell-llm.js +128 -0
- package/src/language/haskell-manifests.js +1290 -0
- package/src/language/haskell-models.js +278 -0
- package/src/language/haskell-parser.js +1073 -0
- package/src/language/haskell-resolved-graph.js +414 -0
- package/src/language/haskell-sca.js +387 -0
- package/src/language/haskell-security-rules.js +332 -0
- package/src/language/haskell-supply.js +140 -0
- package/src/language/haskell-sweep.js +85 -0
- package/src/language/haskell-syntax.js +895 -0
- package/src/language/haskell-web.js +749 -0
- package/src/language/nix-adapter.js +42 -0
- package/src/language/nix-agents.js +155 -0
- package/src/language/nix-build-trust.js +790 -0
- package/src/language/nix-closure.js +331 -0
- package/src/language/nix-eval-isolation.js +274 -0
- package/src/language/nix-fix.js +268 -0
- package/src/language/nix-grammar.js +97 -0
- package/src/language/nix-inventory.js +379 -0
- package/src/language/nix-ir.js +642 -0
- package/src/language/nix-parser.js +908 -0
- package/src/language/nix-privacy.js +114 -0
- package/src/language/nix-sca.js +358 -0
- package/src/language/nix-script-taint.js +885 -0
- package/src/language/nix-secrets.js +396 -0
- package/src/language/nixos-hardening.js +564 -0
- package/src/language/nixos-module-resolver.js +595 -0
- package/src/language/nixos-option-catalog.js +96 -0
- package/src/language/pragma.js +150 -0
- package/src/language/resolved-pass.js +146 -0
- package/src/language/secrets.js +323 -0
- package/src/language/state-artifacts.js +78 -0
- package/src/language/support-registry.js +224 -0
- package/src/language/witness.js +185 -0
- package/src/lineage/CLAUDE.md +8 -0
- package/src/lineage/coverage.js +2 -0
- package/src/lineage/cross-repo-link.js +22 -0
- package/src/lineage/graph-builder.js +5 -1
- package/src/lineage/haskell-view.js +319 -0
- package/src/lineage/index.js +8 -1
- package/src/lineage/nix-view.js +198 -0
- package/src/lineage/sink-registry.js +10 -1
- package/src/lineage/source-registry.js +3 -0
- package/src/lineage/transform-catalog.js +12 -0
- package/src/llm-validator/index.js +16 -2
- package/src/lsp/server.js +69 -9
- package/src/mcp/CLAUDE.md +7 -0
- package/src/mcp/tools.js +26 -3
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/accuracy-scorecard.js +51 -0
- package/src/posture/aibom.js +61 -3
- package/src/posture/artifact-registry.js +12 -0
- package/src/posture/auditor-walkthrough.js +41 -0
- package/src/posture/clustering.js +2 -2
- package/src/posture/evidence-bundle.js +7 -1
- package/src/posture/execution-proof.js +15 -0
- package/src/posture/falsification.js +7 -0
- package/src/posture/family-resolve.js +7 -0
- package/src/posture/fix-history.js +40 -0
- package/src/posture/fix-verify.js +15 -3
- package/src/posture/provenance/git-evidence.js +24 -0
- package/src/posture/provenance/origin-resolver.js +14 -2
- package/src/posture/provenance/predicate-replay.js +11 -2
- package/src/posture/provenance/sca-origin.js +20 -3
- package/src/posture/sbom-diff.js +7 -6
- package/src/posture/sbom.js +85 -32
- package/src/posture/sca-policy.js +10 -1
- package/src/posture/sca-verdict.js +8 -1
- package/src/posture/secret-history.js +62 -18
- package/src/posture/stable-id.js +21 -2
- package/src/posture/state-dir.js +21 -0
- package/src/posture/threat-model-auto.js +4 -4
- package/src/posture/triage.js +18 -1
- package/src/posture/watch-mode.js +34 -3
- package/src/pr-delta.js +9 -4
- package/src/report/index.js +35 -9
- package/src/runScan.js +56 -5
- package/src/sast/pipeline.js +34 -1
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
// Nix parser with original source locations (NIX-001).
|
|
2
|
+
//
|
|
3
|
+
// Static, dependency-free and bounded. It lexes and parses the Nix expression
|
|
4
|
+
// language (let/in, with, assert, if, lambdas and formals, attribute sets and
|
|
5
|
+
// paths, selects with `or` defaults, `?`, `//`, `++`, strings, indented strings
|
|
6
|
+
// and interpolation, paths, search paths and URIs) over the shipped, SHA-256
|
|
7
|
+
// pinned grammar tables in nix-grammar.js. It never runs `nix`, never evaluates
|
|
8
|
+
// anything, and never reads a file other than the source it was given.
|
|
9
|
+
//
|
|
10
|
+
// Locations. Every node carries a span {startLine, startColumn, endLine,
|
|
11
|
+
// endColumn, startByte, endByte, startOffset, endOffset}: lines are 1-based,
|
|
12
|
+
// columns are 0-based UTF-16 code units, bytes are UTF-8 offsets and end
|
|
13
|
+
// positions are exclusive. Spans always index the ORIGINAL file, including for
|
|
14
|
+
// the text parts of indented strings whose leading indentation was stripped.
|
|
15
|
+
//
|
|
16
|
+
// Literal text versus interpolation. A string or path is a list of parts, each
|
|
17
|
+
// `{kind:'text', value, span}` (escapes resolved, indentation stripped) or
|
|
18
|
+
// `{kind:'interp', expr, span}`. `literal` is the full text when there is no
|
|
19
|
+
// interpolation, else null. Comment and string content is never tokens, so a
|
|
20
|
+
// suspicious option name inside either cannot become an assignment.
|
|
21
|
+
//
|
|
22
|
+
// Recovery. A syntax error inside an attribute set or let binding is recorded,
|
|
23
|
+
// that one binding is withheld, and parsing resumes at the next `;` at the same
|
|
24
|
+
// nesting. An error elsewhere returns status `failed` with the error. Budgets
|
|
25
|
+
// (bytes, tokens, parse depth, AST depth, nodes, string size, wall clock) are
|
|
26
|
+
// bounded; a bound hit returns `budget_exceeded` with the budget named and no
|
|
27
|
+
// partial analysis, and never throws into the caller.
|
|
28
|
+
|
|
29
|
+
import { loadNixGrammar } from './nix-grammar.js';
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_PARSE_BUDGETS = Object.freeze({
|
|
32
|
+
maxBytes: 4 * 1024 * 1024,
|
|
33
|
+
maxTokens: 400_000,
|
|
34
|
+
maxDepth: 600,
|
|
35
|
+
maxAstDepth: 1500,
|
|
36
|
+
maxNodes: 500_000,
|
|
37
|
+
maxStringBytes: 1024 * 1024,
|
|
38
|
+
deadlineMs: 8000,
|
|
39
|
+
maxErrors: 100,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
class BudgetError extends Error {
|
|
43
|
+
constructor(budget, limit) {
|
|
44
|
+
super(`parser budget "${budget}" exceeded (limit ${limit})`);
|
|
45
|
+
this.budget = budget;
|
|
46
|
+
this.limit = limit;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class ParseError extends Error {
|
|
51
|
+
constructor(detail, pos, end) {
|
|
52
|
+
super(detail);
|
|
53
|
+
this.detail = detail;
|
|
54
|
+
this.pos = pos;
|
|
55
|
+
this.end = end === undefined ? pos + 1 : end;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── locations ──────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
export function makeLocator(source) {
|
|
62
|
+
const n = source.length;
|
|
63
|
+
const starts = [0];
|
|
64
|
+
for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) starts.push(i + 1);
|
|
65
|
+
let bytes = null;
|
|
66
|
+
// eslint-disable-next-line no-control-regex
|
|
67
|
+
if (/[^\x00-\x7f]/.test(source)) {
|
|
68
|
+
bytes = new Uint32Array(n + 1);
|
|
69
|
+
let acc = 0;
|
|
70
|
+
for (let i = 0; i < n; i++) {
|
|
71
|
+
bytes[i] = acc;
|
|
72
|
+
const x = source.charCodeAt(i);
|
|
73
|
+
if (x < 0x80) acc += 1;
|
|
74
|
+
else if (x < 0x800) acc += 2;
|
|
75
|
+
else if (x >= 0xd800 && x <= 0xdbff) acc += 4;
|
|
76
|
+
else if (x >= 0xdc00 && x <= 0xdfff) acc += 0;
|
|
77
|
+
else acc += 3;
|
|
78
|
+
}
|
|
79
|
+
bytes[n] = acc;
|
|
80
|
+
}
|
|
81
|
+
const lineOf = (off) => {
|
|
82
|
+
let lo = 0;
|
|
83
|
+
let hi = starts.length - 1;
|
|
84
|
+
while (lo < hi) {
|
|
85
|
+
const mid = (lo + hi + 1) >> 1;
|
|
86
|
+
if (starts[mid] <= off) lo = mid; else hi = mid - 1;
|
|
87
|
+
}
|
|
88
|
+
return lo + 1;
|
|
89
|
+
};
|
|
90
|
+
const byte = (off) => (bytes ? bytes[Math.min(off, n)] : off);
|
|
91
|
+
const span = (s, e) => {
|
|
92
|
+
const end = Math.max(s, e);
|
|
93
|
+
const sl = lineOf(s);
|
|
94
|
+
const el = lineOf(end);
|
|
95
|
+
return {
|
|
96
|
+
startLine: sl, startColumn: s - starts[sl - 1],
|
|
97
|
+
endLine: el, endColumn: end - starts[el - 1],
|
|
98
|
+
startByte: byte(s), endByte: byte(end),
|
|
99
|
+
startOffset: s, endOffset: end,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
return { span };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Text of a span, read back from the ORIGINAL source by byte range (test and tooling helper). */
|
|
106
|
+
export function spanText(source, span) {
|
|
107
|
+
return Buffer.from(source, 'utf8').subarray(span.startByte, span.endByte).toString('utf8');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const isSpace = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f' || c === '\v' || c === '';
|
|
111
|
+
const isDigit = (c) => c >= '0' && c <= '9';
|
|
112
|
+
const PATH_CHAR = /[A-Za-z0-9._+/~-]/;
|
|
113
|
+
const CLOSE = { '(': ')', '[': ']', '{': '}' };
|
|
114
|
+
|
|
115
|
+
function sticky(src) {
|
|
116
|
+
return new RegExp(src.replace(/^\^/, ''), 'y');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── parser ─────────────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
class NixParser {
|
|
122
|
+
constructor(source, grammar, budgets) {
|
|
123
|
+
this.src = source;
|
|
124
|
+
this.n = source.length;
|
|
125
|
+
this.g = grammar;
|
|
126
|
+
this.b = budgets;
|
|
127
|
+
this.loc = makeLocator(source);
|
|
128
|
+
this.pos = 0;
|
|
129
|
+
this.tok = null;
|
|
130
|
+
this.depth = 0;
|
|
131
|
+
this.tokens = 0;
|
|
132
|
+
this.nodes = 0;
|
|
133
|
+
this.errors = [];
|
|
134
|
+
this.started = Date.now();
|
|
135
|
+
this.reNumber = sticky(grammar.number);
|
|
136
|
+
this.rePath = sticky(grammar.pathStart);
|
|
137
|
+
this.reSearch = sticky(grammar.searchPath);
|
|
138
|
+
this.reUri = sticky(grammar.uri);
|
|
139
|
+
this.reIdent = sticky(grammar.identifier);
|
|
140
|
+
this.keywords = new Set(grammar.keywords);
|
|
141
|
+
this.binops = grammar.binaryOps;
|
|
142
|
+
this.puncts = [...grammar.punctuation].sort((a, b) => b.length - a.length);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// nodes
|
|
146
|
+
mk(type, s, e, props, kids) {
|
|
147
|
+
if (++this.nodes > this.b.maxNodes) throw new BudgetError('maxNodes', this.b.maxNodes);
|
|
148
|
+
let d = 1;
|
|
149
|
+
if (kids) for (const k of kids) if (k && k.d >= d) d = k.d + 1;
|
|
150
|
+
if (d > this.b.maxAstDepth) throw new BudgetError('maxAstDepth', this.b.maxAstDepth);
|
|
151
|
+
return { type, start: s, end: e, span: this.loc.span(s, e), d, ...props };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
err(detail, pos, end) {
|
|
155
|
+
return new ParseError(detail, pos, end);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// lexing
|
|
159
|
+
skipTrivia() {
|
|
160
|
+
const { src, n } = this;
|
|
161
|
+
let i = this.pos;
|
|
162
|
+
for (;;) {
|
|
163
|
+
while (i < n && isSpace(src[i])) i++;
|
|
164
|
+
if (src[i] === '#') {
|
|
165
|
+
while (i < n && src[i] !== '\n') i++;
|
|
166
|
+
} else if (src[i] === '/' && src[i + 1] === '*') {
|
|
167
|
+
const close = src.indexOf('*/', i + 2);
|
|
168
|
+
if (close === -1) throw this.err('unterminated block comment', i, n);
|
|
169
|
+
i = close + 2;
|
|
170
|
+
} else break;
|
|
171
|
+
}
|
|
172
|
+
return i;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
lex() {
|
|
176
|
+
const { src, n } = this;
|
|
177
|
+
const s = this.skipTrivia();
|
|
178
|
+
if (s >= n) return { t: 'eof', v: '', s: n, e: n };
|
|
179
|
+
const c = src[s];
|
|
180
|
+
if (c === '"') return { t: '"', v: '"', s, e: s + 1 };
|
|
181
|
+
if (c === "'" && src[s + 1] === "'") return { t: "''", v: "''", s, e: s + 2 };
|
|
182
|
+
if (c === '$' && src[s + 1] === '{') return { t: '${', v: '${', s, e: s + 2 };
|
|
183
|
+
if (c !== '-' && c !== '+' && (/[A-Za-z0-9._~/]/.test(c))) {
|
|
184
|
+
this.rePath.lastIndex = s;
|
|
185
|
+
if (this.rePath.test(src)) return { t: 'path', v: '', s, e: s };
|
|
186
|
+
}
|
|
187
|
+
if (isDigit(c) || (c === '.' && isDigit(src[s + 1] || ''))) {
|
|
188
|
+
this.reNumber.lastIndex = s;
|
|
189
|
+
const m = this.reNumber.exec(src);
|
|
190
|
+
if (m) return { t: /[.eE]/.test(m[0]) ? 'float' : 'int', v: m[0], s, e: s + m[0].length };
|
|
191
|
+
}
|
|
192
|
+
if (c === '<') {
|
|
193
|
+
this.reSearch.lastIndex = s;
|
|
194
|
+
const m = this.reSearch.exec(src);
|
|
195
|
+
if (m) return { t: 'spath', v: m[0].slice(1, -1), s, e: s + m[0].length };
|
|
196
|
+
}
|
|
197
|
+
if (/[A-Za-z_]/.test(c)) {
|
|
198
|
+
this.reUri.lastIndex = s;
|
|
199
|
+
const u = this.reUri.exec(src);
|
|
200
|
+
if (u) return { t: 'uri', v: u[0], s, e: s + u[0].length };
|
|
201
|
+
this.reIdent.lastIndex = s;
|
|
202
|
+
const m = this.reIdent.exec(src);
|
|
203
|
+
if (m) return { t: this.keywords.has(m[0]) ? 'kw' : 'id', v: m[0], s, e: s + m[0].length };
|
|
204
|
+
}
|
|
205
|
+
for (const p of this.puncts) if (src.startsWith(p, s)) return { t: p, v: p, s, e: s + p.length };
|
|
206
|
+
throw this.err(`unexpected character ${JSON.stringify(c)}`, s);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
peek() {
|
|
210
|
+
if (!this.tok) this.tok = this.lex();
|
|
211
|
+
return this.tok;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
next() {
|
|
215
|
+
const t = this.peek();
|
|
216
|
+
if (++this.tokens > this.b.maxTokens) throw new BudgetError('maxTokens', this.b.maxTokens);
|
|
217
|
+
if ((this.tokens & 255) === 0 && Date.now() - this.started > this.b.deadlineMs) throw new BudgetError('deadlineMs', this.b.deadlineMs);
|
|
218
|
+
this.pos = t.e;
|
|
219
|
+
this.tok = null;
|
|
220
|
+
return t;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Lexes `k` tokens ahead without consuming. Bounded and non-recursive. */
|
|
224
|
+
ahead(k) {
|
|
225
|
+
const sp = this.pos;
|
|
226
|
+
const st = this.tok;
|
|
227
|
+
const out = [];
|
|
228
|
+
try {
|
|
229
|
+
for (let i = 0; i < k; i++) {
|
|
230
|
+
const t = this.peek();
|
|
231
|
+
out.push(t);
|
|
232
|
+
if (t.t === 'eof' || t.t === '"' || t.t === "''" || t.t === 'path') break;
|
|
233
|
+
this.pos = t.e;
|
|
234
|
+
this.tok = null;
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// a lex error ahead is reported when the token is actually consumed
|
|
238
|
+
} finally {
|
|
239
|
+
this.pos = sp;
|
|
240
|
+
this.tok = st;
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
expect(t, why) {
|
|
246
|
+
const tk = this.peek();
|
|
247
|
+
if (tk.t !== t) throw this.err(`expected ${why || `"${t}"`} but found ${tk.t === 'eof' ? 'end of file' : `"${tk.v || tk.t}"`}`, tk.s, Math.max(tk.e, tk.s + 1));
|
|
248
|
+
return this.next();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
enter() {
|
|
252
|
+
if (++this.depth > this.b.maxDepth) throw new BudgetError('maxDepth', this.b.maxDepth);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
leave() { this.depth--; }
|
|
256
|
+
|
|
257
|
+
// ── expressions ──
|
|
258
|
+
|
|
259
|
+
parseExpr() {
|
|
260
|
+
this.enter();
|
|
261
|
+
try { return this.parseExprInner(); } finally { this.leave(); }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
parseExprInner() {
|
|
265
|
+
const t = this.peek();
|
|
266
|
+
if (t.t === 'kw') {
|
|
267
|
+
if (t.v === 'let') {
|
|
268
|
+
const nx = this.ahead(2)[1];
|
|
269
|
+
if (!(nx && nx.t === '{')) return this.parseLet();
|
|
270
|
+
} else if (t.v === 'if') return this.parseIf();
|
|
271
|
+
else if (t.v === 'with') return this.parseWith();
|
|
272
|
+
else if (t.v === 'assert') return this.parseAssert();
|
|
273
|
+
} else if (t.t === 'id') {
|
|
274
|
+
const la = this.ahead(2)[1];
|
|
275
|
+
if (la && (la.t === ':' || la.t === '@')) return this.parseLambda();
|
|
276
|
+
} else if (t.t === '{' && this.looksLikePattern()) return this.parseLambda();
|
|
277
|
+
return this.parseOp(0);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
looksLikePattern() {
|
|
281
|
+
const la = this.ahead(4);
|
|
282
|
+
const a = la[1];
|
|
283
|
+
const b = la[2];
|
|
284
|
+
const c = la[3];
|
|
285
|
+
if (!a) return false;
|
|
286
|
+
if (a.t === '...') return true;
|
|
287
|
+
if (a.t === '}') return !!b && (b.t === ':' || b.t === '@');
|
|
288
|
+
if (a.t === 'id') {
|
|
289
|
+
if (!b) return false;
|
|
290
|
+
if (b.t === ',' || b.t === '?') return true;
|
|
291
|
+
if (b.t === '}') return !!c && (c.t === ':' || c.t === '@');
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
parseLet() {
|
|
297
|
+
const kw = this.next();
|
|
298
|
+
const bindings = this.parseBindings((t) => t.t === 'kw' && t.v === 'in');
|
|
299
|
+
const inTok = this.peek();
|
|
300
|
+
if (!(inTok.t === 'kw' && inTok.v === 'in')) throw this.err('expected "in" to close let', inTok.s, Math.max(inTok.e, inTok.s + 1));
|
|
301
|
+
this.next();
|
|
302
|
+
const body = this.parseExpr();
|
|
303
|
+
return this.mk('let', kw.s, body.end, { bindings, body }, [body, ...bindings.map((x) => x.value)]);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
parseIf() {
|
|
307
|
+
const kw = this.next();
|
|
308
|
+
const cond = this.parseExpr();
|
|
309
|
+
this.expectKw('then');
|
|
310
|
+
const thenE = this.parseExpr();
|
|
311
|
+
this.expectKw('else');
|
|
312
|
+
const elseE = this.parseExpr();
|
|
313
|
+
return this.mk('if', kw.s, elseE.end, { cond, then: thenE, else: elseE }, [cond, thenE, elseE]);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
parseWith() {
|
|
317
|
+
const kw = this.next();
|
|
318
|
+
const env = this.parseExpr();
|
|
319
|
+
this.expect(';');
|
|
320
|
+
const body = this.parseExpr();
|
|
321
|
+
return this.mk('with', kw.s, body.end, { env, body }, [env, body]);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
parseAssert() {
|
|
325
|
+
const kw = this.next();
|
|
326
|
+
const cond = this.parseExpr();
|
|
327
|
+
this.expect(';');
|
|
328
|
+
const body = this.parseExpr();
|
|
329
|
+
return this.mk('assert', kw.s, body.end, { cond, body }, [cond, body]);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
expectKw(v) {
|
|
333
|
+
const t = this.peek();
|
|
334
|
+
if (!(t.t === 'kw' && t.v === v)) throw this.err(`expected "${v}"`, t.s, Math.max(t.e, t.s + 1));
|
|
335
|
+
return this.next();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
parseLambda() {
|
|
339
|
+
const start = this.peek().s;
|
|
340
|
+
const param = { kind: 'ident', name: null, formals: [], ellipsis: false, atName: null, span: null };
|
|
341
|
+
const t = this.peek();
|
|
342
|
+
if (t.t === 'id') {
|
|
343
|
+
const id = this.next();
|
|
344
|
+
if (this.peek().t === '@') {
|
|
345
|
+
this.next();
|
|
346
|
+
this.parsePattern(param);
|
|
347
|
+
param.kind = 'pattern';
|
|
348
|
+
param.atName = id.v;
|
|
349
|
+
} else {
|
|
350
|
+
param.name = id.v;
|
|
351
|
+
param.span = this.loc.span(id.s, id.e);
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
this.parsePattern(param);
|
|
355
|
+
param.kind = 'pattern';
|
|
356
|
+
if (this.peek().t === '@') {
|
|
357
|
+
this.next();
|
|
358
|
+
const id = this.expect('id', 'an identifier');
|
|
359
|
+
param.atName = id.v;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const endParam = this.pos;
|
|
363
|
+
if (!param.span) param.span = this.loc.span(start, endParam);
|
|
364
|
+
this.expect(':');
|
|
365
|
+
const body = this.parseExpr();
|
|
366
|
+
return this.mk('lambda', start, body.end, { param, body }, [body, ...param.formals.map((f) => f.default)]);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
parsePattern(param) {
|
|
370
|
+
this.expect('{');
|
|
371
|
+
for (;;) {
|
|
372
|
+
const t = this.peek();
|
|
373
|
+
if (t.t === '}') { this.next(); break; }
|
|
374
|
+
if (t.t === '...') { this.next(); param.ellipsis = true; if (this.peek().t === ',') this.next(); continue; }
|
|
375
|
+
const id = this.expect('id', 'a parameter name');
|
|
376
|
+
let def = null;
|
|
377
|
+
if (this.peek().t === '?') { this.next(); def = this.parseExpr(); }
|
|
378
|
+
param.formals.push({ name: id.v, default: def, span: this.loc.span(id.s, def ? def.end : id.e) });
|
|
379
|
+
const sep = this.peek();
|
|
380
|
+
if (sep.t === ',') this.next();
|
|
381
|
+
else if (sep.t !== '}' ) throw this.err('expected "," or "}" in function parameters', sep.s, Math.max(sep.e, sep.s + 1));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
parseOp(minPrec) {
|
|
386
|
+
this.enter();
|
|
387
|
+
try {
|
|
388
|
+
let left = this.parseUnary();
|
|
389
|
+
for (;;) {
|
|
390
|
+
const t = this.peek();
|
|
391
|
+
if (t.t === '?' && this.g.precedence.hasAttr >= minPrec) {
|
|
392
|
+
this.next();
|
|
393
|
+
const attrpath = this.parseAttrPath();
|
|
394
|
+
left = this.mk('hasattr', left.start, attrpath[attrpath.length - 1].span.endOffset, { base: left, attrpath }, [left, ...this.segKids(attrpath)]);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const op = this.binops[t.t];
|
|
398
|
+
if (!op || op.prec < minPrec) break;
|
|
399
|
+
this.next();
|
|
400
|
+
const right = this.parseOp(op.assoc === 'right' ? op.prec : op.prec + 1);
|
|
401
|
+
left = this.mk('binop', left.start, right.end, { op: t.t, left, right }, [left, right]);
|
|
402
|
+
}
|
|
403
|
+
return left;
|
|
404
|
+
} finally { this.leave(); }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
parseUnary() {
|
|
408
|
+
const t = this.peek();
|
|
409
|
+
if (t.t === '-' || t.t === '!') {
|
|
410
|
+
this.next();
|
|
411
|
+
const operand = this.parseOp(t.t === '-' ? this.g.precedence.negate : this.g.precedence.not);
|
|
412
|
+
return this.mk('unop', t.s, operand.end, { op: t.t, operand }, [operand]);
|
|
413
|
+
}
|
|
414
|
+
if (t.t === 'kw' && (t.v === 'let' || t.v === 'if' || t.v === 'with' || t.v === 'assert')) {
|
|
415
|
+
const nx = t.v === 'let' ? this.ahead(2)[1] : null;
|
|
416
|
+
if (!(nx && nx.t === '{')) return this.parseExprInner();
|
|
417
|
+
}
|
|
418
|
+
return this.parseApp();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
startsArg(t) {
|
|
422
|
+
switch (t.t) {
|
|
423
|
+
case 'id': return t.v !== 'or';
|
|
424
|
+
case 'int': case 'float': case 'path': case 'spath': case 'uri': case '"': case "''": case '(': case '[': case '{':
|
|
425
|
+
return true;
|
|
426
|
+
case 'kw': return t.v === 'rec';
|
|
427
|
+
default: return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
parseApp() {
|
|
432
|
+
let fn = this.parseSelect();
|
|
433
|
+
for (;;) {
|
|
434
|
+
const t = this.peek();
|
|
435
|
+
if (!this.startsArg(t)) break;
|
|
436
|
+
const arg = this.parseSelect();
|
|
437
|
+
fn = this.mk('app', fn.start, arg.end, { fn, arg }, [fn, arg]);
|
|
438
|
+
}
|
|
439
|
+
return fn;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
parseSelect() {
|
|
443
|
+
let base = this.parseAtom();
|
|
444
|
+
while (this.peek().t === '.') {
|
|
445
|
+
this.next();
|
|
446
|
+
const attrpath = this.parseAttrPath();
|
|
447
|
+
let def = null;
|
|
448
|
+
let end = attrpath[attrpath.length - 1].span.endOffset;
|
|
449
|
+
const o = this.peek();
|
|
450
|
+
if (o.t === 'id' && o.v === 'or') {
|
|
451
|
+
this.next();
|
|
452
|
+
def = this.parseSelect();
|
|
453
|
+
end = def.end;
|
|
454
|
+
}
|
|
455
|
+
base = this.mk('select', base.start, end, { base, attrpath, default: def }, [base, def, ...this.segKids(attrpath)]);
|
|
456
|
+
}
|
|
457
|
+
return base;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
segKids(attrpath) {
|
|
461
|
+
const out = [];
|
|
462
|
+
for (const sg of attrpath) {
|
|
463
|
+
if (sg.expr) out.push(sg.expr);
|
|
464
|
+
if (sg.parts) for (const p of sg.parts) if (p.kind === 'interp') out.push(p.expr);
|
|
465
|
+
}
|
|
466
|
+
return out;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** attrpath: id | "string" | ${expr}, joined by `.` */
|
|
470
|
+
parseAttrPath() {
|
|
471
|
+
const segs = [];
|
|
472
|
+
for (;;) {
|
|
473
|
+
const t = this.peek();
|
|
474
|
+
let seg;
|
|
475
|
+
if (t.t === 'id') {
|
|
476
|
+
this.next();
|
|
477
|
+
seg = { kind: 'static', name: t.v, span: this.loc.span(t.s, t.e), quoted: false };
|
|
478
|
+
} else if (t.t === '"') {
|
|
479
|
+
this.next();
|
|
480
|
+
const str = this.parseStringBody(t.s, false);
|
|
481
|
+
seg = str.interpolated
|
|
482
|
+
? { kind: 'dynamic', name: null, expr: null, parts: str.parts, span: str.span, quoted: true }
|
|
483
|
+
: { kind: 'static', name: str.literal, span: str.span, quoted: true };
|
|
484
|
+
} else if (t.t === '${') {
|
|
485
|
+
this.next();
|
|
486
|
+
const expr = this.parseExpr();
|
|
487
|
+
const close = this.expect('}');
|
|
488
|
+
seg = { kind: 'dynamic', name: null, expr, parts: null, span: this.loc.span(t.s, close.e), quoted: false };
|
|
489
|
+
} else {
|
|
490
|
+
throw this.err('expected an attribute name', t.s, Math.max(t.e, t.s + 1));
|
|
491
|
+
}
|
|
492
|
+
segs.push(seg);
|
|
493
|
+
if (this.peek().t !== '.') break;
|
|
494
|
+
this.next();
|
|
495
|
+
}
|
|
496
|
+
return segs;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Atoms recurse without passing through parseExpr (`[[[[...`), so they count toward the depth budget too.
|
|
500
|
+
parseAtom() {
|
|
501
|
+
this.enter();
|
|
502
|
+
try { return this.parseAtomInner(); } finally { this.leave(); }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
parseAtomInner() {
|
|
506
|
+
const t = this.peek();
|
|
507
|
+
switch (t.t) {
|
|
508
|
+
case 'id': this.next(); return this.mk('ident', t.s, t.e, { name: t.v });
|
|
509
|
+
case 'int': this.next(); return this.mk('int', t.s, t.e, { value: Number(t.v), raw: t.v });
|
|
510
|
+
case 'float': this.next(); return this.mk('float', t.s, t.e, { value: Number(t.v), raw: t.v });
|
|
511
|
+
case 'spath': this.next(); return this.mk('spath', t.s, t.e, { value: t.v });
|
|
512
|
+
case 'uri': this.next(); return this.mk('uri', t.s, t.e, { value: t.v });
|
|
513
|
+
case '"': this.next(); return this.parseStringBody(t.s, false);
|
|
514
|
+
case "''": this.next(); return this.parseStringBody(t.s, true);
|
|
515
|
+
case 'path': return this.parsePath(t.s);
|
|
516
|
+
case '(': {
|
|
517
|
+
this.next();
|
|
518
|
+
const expr = this.parseExpr();
|
|
519
|
+
const close = this.expect(')');
|
|
520
|
+
return this.mk('paren', t.s, close.e, { expr }, [expr]);
|
|
521
|
+
}
|
|
522
|
+
case '[': {
|
|
523
|
+
this.next();
|
|
524
|
+
const items = [];
|
|
525
|
+
for (;;) {
|
|
526
|
+
const k = this.peek();
|
|
527
|
+
if (k.t === ']') { this.next(); break; }
|
|
528
|
+
if (k.t === 'eof') throw this.err('unterminated list', t.s, t.s + 1);
|
|
529
|
+
items.push(this.parseSelect());
|
|
530
|
+
}
|
|
531
|
+
return this.mk('list', t.s, this.pos, { items }, items);
|
|
532
|
+
}
|
|
533
|
+
case '{': return this.parseAttrSet(false, t.s);
|
|
534
|
+
case 'kw':
|
|
535
|
+
if (t.v === 'rec') {
|
|
536
|
+
this.next();
|
|
537
|
+
const o = this.peek();
|
|
538
|
+
if (o.t !== '{') throw this.err('expected "{" after rec', o.s, Math.max(o.e, o.s + 1));
|
|
539
|
+
return this.parseAttrSet(true, t.s);
|
|
540
|
+
}
|
|
541
|
+
throw this.err(`unexpected keyword "${t.v}"`, t.s, t.e);
|
|
542
|
+
case 'eof': throw this.err('unexpected end of file', t.s, t.s);
|
|
543
|
+
default: throw this.err(`unexpected "${t.v || t.t}"`, t.s, Math.max(t.e, t.s + 1));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
parseAttrSet(rec, start) {
|
|
548
|
+
this.expect('{');
|
|
549
|
+
const bindings = this.parseBindings((t) => t.t === '}');
|
|
550
|
+
const close = this.expect('}', '"}" to close the attribute set');
|
|
551
|
+
return this.mk('attrset', start, close.e, { rec, bindings, errors: bindings.errorCount || 0 }, bindings.map((b) => b.value));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ── bindings ──
|
|
555
|
+
|
|
556
|
+
parseBindings(isCloser) {
|
|
557
|
+
const out = [];
|
|
558
|
+
out.errorCount = 0;
|
|
559
|
+
for (;;) {
|
|
560
|
+
const t = this.peek();
|
|
561
|
+
if (isCloser(t) || t.t === 'eof') break;
|
|
562
|
+
const saveDepth = this.depth;
|
|
563
|
+
try {
|
|
564
|
+
out.push(this.parseBinding());
|
|
565
|
+
} catch (e) {
|
|
566
|
+
if (!(e instanceof ParseError)) throw e;
|
|
567
|
+
this.depth = saveDepth;
|
|
568
|
+
this.recordError(e);
|
|
569
|
+
out.errorCount++;
|
|
570
|
+
this.resync(t.s, isCloser);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
recordError(e) {
|
|
577
|
+
if (this.errors.length >= this.b.maxErrors) throw new BudgetError('maxErrors', this.b.maxErrors);
|
|
578
|
+
this.errors.push({ kind: 'syntax-error', detail: e.detail, span: this.loc.span(e.pos, Math.min(Math.max(e.end, e.pos), this.n)) });
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
parseBinding() {
|
|
582
|
+
const t = this.peek();
|
|
583
|
+
if (t.t === 'kw' && t.v === 'inherit') {
|
|
584
|
+
this.next();
|
|
585
|
+
let from = null;
|
|
586
|
+
if (this.peek().t === '(') {
|
|
587
|
+
this.next();
|
|
588
|
+
from = this.parseExpr();
|
|
589
|
+
this.expect(')');
|
|
590
|
+
}
|
|
591
|
+
const names = [];
|
|
592
|
+
for (;;) {
|
|
593
|
+
const k = this.peek();
|
|
594
|
+
if (k.t === ';') break;
|
|
595
|
+
if (k.t === 'id' || (k.t === 'kw' && k.v === 'or')) {
|
|
596
|
+
this.next();
|
|
597
|
+
names.push({ kind: 'static', name: k.v, span: this.loc.span(k.s, k.e), quoted: false });
|
|
598
|
+
} else if (k.t === '"') {
|
|
599
|
+
this.next();
|
|
600
|
+
const str = this.parseStringBody(k.s, false);
|
|
601
|
+
names.push(str.interpolated
|
|
602
|
+
? { kind: 'dynamic', name: null, expr: null, parts: str.parts, span: str.span, quoted: true }
|
|
603
|
+
: { kind: 'static', name: str.literal, span: str.span, quoted: true });
|
|
604
|
+
} else if (k.t === '${') {
|
|
605
|
+
this.next();
|
|
606
|
+
const expr = this.parseExpr();
|
|
607
|
+
const close = this.expect('}');
|
|
608
|
+
names.push({ kind: 'dynamic', name: null, expr, parts: null, span: this.loc.span(k.s, close.e), quoted: false });
|
|
609
|
+
} else throw this.err('expected an attribute name in inherit', k.s, Math.max(k.e, k.s + 1));
|
|
610
|
+
}
|
|
611
|
+
const semi = this.expect(';');
|
|
612
|
+
return { kind: 'inherit', from, names, value: from, span: this.loc.span(t.s, semi.e) };
|
|
613
|
+
}
|
|
614
|
+
const path = this.parseAttrPath();
|
|
615
|
+
this.expect('=');
|
|
616
|
+
const value = this.parseExpr();
|
|
617
|
+
const semi = this.expect(';', '";" after the binding value');
|
|
618
|
+
return { kind: 'attr', path, value, span: this.loc.span(t.s, semi.e) };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Skips to the `;` that ends the failed binding, or stops before the closing token. */
|
|
622
|
+
resync(from, isCloser) {
|
|
623
|
+
this.pos = from;
|
|
624
|
+
this.tok = null;
|
|
625
|
+
const stack = [];
|
|
626
|
+
let pending = 0;
|
|
627
|
+
const wanted = { ')': ['('], ']': ['['], '}': ['{', '${'] };
|
|
628
|
+
for (;;) {
|
|
629
|
+
let t;
|
|
630
|
+
try { t = this.peek(); } catch { this.pos = Math.min(this.pos + 1, this.n); this.tok = null; if (this.pos >= this.n) return; continue; }
|
|
631
|
+
if (t.t === 'eof') return;
|
|
632
|
+
if (t.t === '"') { this.pos = this.skipPlainString(t.e); this.tok = null; continue; }
|
|
633
|
+
if (t.t === "''") { this.pos = this.skipIndentedString(t.e); this.tok = null; continue; }
|
|
634
|
+
if (t.t === 'path') { this.pos = this.skipPathChars(t.s); this.tok = null; continue; }
|
|
635
|
+
const isIn = t.t === 'kw' && t.v === 'in';
|
|
636
|
+
if (t.t === '(' || t.t === '[' || t.t === '{' || t.t === '${' || (t.t === 'kw' && t.v === 'let')) {
|
|
637
|
+
stack.push(t.t === 'kw' ? 'let' : t.t);
|
|
638
|
+
} else if (t.t === ')' || t.t === ']' || t.t === '}' || isIn) {
|
|
639
|
+
const want = isIn ? ['let'] : wanted[t.t];
|
|
640
|
+
let idx = -1;
|
|
641
|
+
for (let k = stack.length - 1; k >= 0; k--) if (want.includes(stack[k])) { idx = k; break; }
|
|
642
|
+
if (idx >= 0) stack.length = idx;
|
|
643
|
+
else if (isCloser(t)) return;
|
|
644
|
+
} else if (t.t === 'kw' && (t.v === 'with' || t.v === 'assert')) pending++;
|
|
645
|
+
else if (t.t === ';') {
|
|
646
|
+
if (pending > 0) pending--;
|
|
647
|
+
else if (!stack.length) { this.pos = t.e; this.tok = null; return; }
|
|
648
|
+
}
|
|
649
|
+
this.pos = t.e;
|
|
650
|
+
this.tok = null;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
skipPathChars(s) {
|
|
655
|
+
let i = s;
|
|
656
|
+
while (i < this.n && PATH_CHAR.test(this.src[i])) i++;
|
|
657
|
+
return Math.max(i, s + 1);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
skipPlainString(i) {
|
|
661
|
+
while (i < this.n) {
|
|
662
|
+
const c = this.src[i];
|
|
663
|
+
if (c === '\\') i += 2;
|
|
664
|
+
else if (c === '"') return i + 1;
|
|
665
|
+
else i++;
|
|
666
|
+
}
|
|
667
|
+
return this.n;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
skipIndentedString(i) {
|
|
671
|
+
while (i < this.n) {
|
|
672
|
+
const c = this.src[i];
|
|
673
|
+
if (c === "'" && this.src[i + 1] === "'") {
|
|
674
|
+
const d = this.src[i + 2];
|
|
675
|
+
if (d === "'" || d === '$') i += 3;
|
|
676
|
+
else if (d === '\\') i += 4;
|
|
677
|
+
else return i + 2;
|
|
678
|
+
} else i++;
|
|
679
|
+
}
|
|
680
|
+
return this.n;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ── strings and paths ──
|
|
684
|
+
|
|
685
|
+
/** Parses `${ expr }` whose `${` starts at `i`. Returns the part and the offset after `}`. */
|
|
686
|
+
parseInterp(i) {
|
|
687
|
+
this.pos = i + 2;
|
|
688
|
+
this.tok = null;
|
|
689
|
+
const expr = this.parseExpr();
|
|
690
|
+
const close = this.peek();
|
|
691
|
+
if (close.t !== '}') throw this.err('expected "}" to close interpolation', close.s, Math.max(close.e, close.s + 1));
|
|
692
|
+
this.next();
|
|
693
|
+
return { part: { kind: 'interp', expr, span: this.loc.span(i, close.e) }, next: close.e };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
parseStringBody(openStart, indented) {
|
|
697
|
+
const { src, n } = this;
|
|
698
|
+
const limit = this.b.maxStringBytes;
|
|
699
|
+
// The caller consumed the opening delimiter; scanning is done on raw characters from here.
|
|
700
|
+
let i = openStart + (indented ? 2 : 1);
|
|
701
|
+
const parts = [];
|
|
702
|
+
let size = 0;
|
|
703
|
+
if (!indented) {
|
|
704
|
+
let buf = '';
|
|
705
|
+
let bufStart = i;
|
|
706
|
+
const flush = (end) => {
|
|
707
|
+
if (buf.length || end > bufStart) parts.push({ kind: 'text', value: buf, span: this.loc.span(bufStart, end) });
|
|
708
|
+
buf = '';
|
|
709
|
+
};
|
|
710
|
+
for (;;) {
|
|
711
|
+
if (i >= n) throw this.err('unterminated string', openStart, n);
|
|
712
|
+
const c = src[i];
|
|
713
|
+
if (c === '"') { flush(i); i++; break; }
|
|
714
|
+
if (c === '\\') {
|
|
715
|
+
if (i + 1 >= n) throw this.err('unterminated string', openStart, n);
|
|
716
|
+
const e = src[i + 1];
|
|
717
|
+
buf += Object.prototype.hasOwnProperty.call(this.g.stringEscapes, e) ? this.g.stringEscapes[e] : e;
|
|
718
|
+
i += 2;
|
|
719
|
+
} else if (c === '$' && src[i + 1] === '{') {
|
|
720
|
+
if (buf.length || i > bufStart) flush(i);
|
|
721
|
+
const r = this.parseInterp(i);
|
|
722
|
+
parts.push(r.part);
|
|
723
|
+
i = r.next;
|
|
724
|
+
bufStart = i;
|
|
725
|
+
} else { buf += c; i++; }
|
|
726
|
+
if (++size > limit) throw new BudgetError('maxStringBytes', limit);
|
|
727
|
+
}
|
|
728
|
+
} else {
|
|
729
|
+
let j = i;
|
|
730
|
+
while (src[j] === ' ') j++;
|
|
731
|
+
if (src[j] === '\n') i = j + 1;
|
|
732
|
+
// units: text units carry resolved text and source offsets, interps are parts
|
|
733
|
+
const items = [];
|
|
734
|
+
for (;;) {
|
|
735
|
+
if (i >= n) throw this.err('unterminated indented string', openStart, n);
|
|
736
|
+
const c = src[i];
|
|
737
|
+
if (c === "'" && src[i + 1] === "'") {
|
|
738
|
+
const d = src[i + 2];
|
|
739
|
+
if (d === "'") { items.push({ c: "''", o: i, l: 3 }); i += 3; }
|
|
740
|
+
else if (d === '$') { items.push({ c: '$', o: i, l: 3 }); i += 3; }
|
|
741
|
+
else if (d === '\\') {
|
|
742
|
+
const e = src[i + 3];
|
|
743
|
+
if (e === undefined) throw this.err('unterminated indented string', openStart, n);
|
|
744
|
+
items.push({ c: Object.prototype.hasOwnProperty.call(this.g.indentedEscapes, e) ? this.g.indentedEscapes[e] : e, o: i, l: 4 });
|
|
745
|
+
i += 4;
|
|
746
|
+
} else { i += 2; break; }
|
|
747
|
+
} else if (c === '$' && src[i + 1] === '{') {
|
|
748
|
+
const r = this.parseInterp(i);
|
|
749
|
+
items.push({ part: r.part });
|
|
750
|
+
i = r.next;
|
|
751
|
+
} else { items.push({ c, o: i, l: 1 }); i++; }
|
|
752
|
+
if (items.length > limit) throw new BudgetError('maxStringBytes', limit);
|
|
753
|
+
}
|
|
754
|
+
this.dedent(items, parts);
|
|
755
|
+
}
|
|
756
|
+
this.pos = i;
|
|
757
|
+
this.tok = null;
|
|
758
|
+
const interpolated = parts.some((p) => p.kind === 'interp');
|
|
759
|
+
const literal = interpolated ? null : parts.map((p) => p.value).join('');
|
|
760
|
+
const kids = parts.filter((p) => p.kind === 'interp').map((p) => p.expr);
|
|
761
|
+
return this.mk('string', openStart, i, { indented, parts, literal, interpolated }, kids);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** Strips common indentation the way the language defines it (spaces only; blank lines do not count). */
|
|
765
|
+
dedent(items, parts) {
|
|
766
|
+
let min = Infinity;
|
|
767
|
+
let atStart = true;
|
|
768
|
+
let cur = 0;
|
|
769
|
+
for (const it of items) {
|
|
770
|
+
if (it.part) { if (atStart) { atStart = false; if (cur < min) min = cur; } continue; }
|
|
771
|
+
if (it.c === ' ') { if (atStart) cur++; } else if (it.c === '\n') { atStart = true; cur = 0; } else if (atStart) { atStart = false; if (cur < min) min = cur; }
|
|
772
|
+
}
|
|
773
|
+
if (min === Infinity) min = 0;
|
|
774
|
+
const kept = [];
|
|
775
|
+
atStart = true;
|
|
776
|
+
cur = 0;
|
|
777
|
+
for (const it of items) {
|
|
778
|
+
if (it.part) { atStart = false; kept.push(it); continue; }
|
|
779
|
+
if (atStart && it.c === ' ' && cur < min) { cur++; continue; }
|
|
780
|
+
if (it.c === '\n') { atStart = true; cur = 0; } else atStart = false;
|
|
781
|
+
kept.push(it);
|
|
782
|
+
}
|
|
783
|
+
// a final line made only of spaces is dropped
|
|
784
|
+
let k = kept.length;
|
|
785
|
+
while (k > 0 && !kept[k - 1].part && kept[k - 1].c === ' ') k--;
|
|
786
|
+
if (k === 0 || (!kept[k - 1].part && kept[k - 1].c === '\n')) kept.length = k;
|
|
787
|
+
let buf = '';
|
|
788
|
+
let first = -1;
|
|
789
|
+
let last = -1;
|
|
790
|
+
const flush = () => {
|
|
791
|
+
if (first >= 0) parts.push({ kind: 'text', value: buf, span: this.loc.span(first, last) });
|
|
792
|
+
buf = '';
|
|
793
|
+
first = -1;
|
|
794
|
+
};
|
|
795
|
+
for (const it of kept) {
|
|
796
|
+
if (it.part) { flush(); parts.push(it.part); continue; }
|
|
797
|
+
if (first < 0) first = it.o;
|
|
798
|
+
last = it.o + it.l;
|
|
799
|
+
buf += it.c;
|
|
800
|
+
}
|
|
801
|
+
flush();
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
parsePath(start) {
|
|
805
|
+
const { src, n } = this;
|
|
806
|
+
const parts = [];
|
|
807
|
+
let i = start;
|
|
808
|
+
let buf = '';
|
|
809
|
+
let bufStart = i;
|
|
810
|
+
const flush = (end) => {
|
|
811
|
+
if (end > bufStart) parts.push({ kind: 'text', value: buf, span: this.loc.span(bufStart, end) });
|
|
812
|
+
buf = '';
|
|
813
|
+
};
|
|
814
|
+
for (;;) {
|
|
815
|
+
if (i >= n) break;
|
|
816
|
+
const c = src[i];
|
|
817
|
+
if (c === '$' && src[i + 1] === '{') {
|
|
818
|
+
flush(i);
|
|
819
|
+
const r = this.parseInterp(i);
|
|
820
|
+
parts.push(r.part);
|
|
821
|
+
i = r.next;
|
|
822
|
+
bufStart = i;
|
|
823
|
+
} else if (PATH_CHAR.test(c)) { buf += c; i++; } else break;
|
|
824
|
+
if (i - start > this.b.maxStringBytes) throw new BudgetError('maxStringBytes', this.b.maxStringBytes);
|
|
825
|
+
}
|
|
826
|
+
flush(i);
|
|
827
|
+
this.pos = i;
|
|
828
|
+
this.tok = null;
|
|
829
|
+
const interpolated = parts.some((p) => p.kind === 'interp');
|
|
830
|
+
const literal = interpolated ? null : parts.map((p) => p.value).join('');
|
|
831
|
+
const first = parts[0] && parts[0].kind === 'text' ? parts[0].value : '';
|
|
832
|
+
const pathKind = first.startsWith('/') ? 'absolute' : first.startsWith('~') ? 'home' : 'relative';
|
|
833
|
+
return this.mk('path', start, i, { parts, literal, interpolated, pathKind }, parts.filter((p) => p.kind === 'interp').map((p) => p.expr));
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** Direct expression children of a node, in source order. */
|
|
838
|
+
export function childrenOf(node) {
|
|
839
|
+
const out = [];
|
|
840
|
+
const add = (x) => { if (x) out.push(x); };
|
|
841
|
+
const segs = (list) => {
|
|
842
|
+
for (const sg of list) {
|
|
843
|
+
add(sg.expr);
|
|
844
|
+
if (sg.parts) for (const p of sg.parts) if (p.kind === 'interp') add(p.expr);
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
switch (node.type) {
|
|
848
|
+
case 'app': add(node.fn); add(node.arg); break;
|
|
849
|
+
case 'binop': add(node.left); add(node.right); break;
|
|
850
|
+
case 'unop': add(node.operand); break;
|
|
851
|
+
case 'paren': add(node.expr); break;
|
|
852
|
+
case 'list': for (const x of node.items) add(x); break;
|
|
853
|
+
case 'if': add(node.cond); add(node.then); add(node.else); break;
|
|
854
|
+
case 'with': add(node.env); add(node.body); break;
|
|
855
|
+
case 'assert': add(node.cond); add(node.body); break;
|
|
856
|
+
case 'lambda': for (const f of node.param.formals) add(f.default); add(node.body); break;
|
|
857
|
+
case 'select': add(node.base); segs(node.attrpath); add(node.default); break;
|
|
858
|
+
case 'hasattr': add(node.base); segs(node.attrpath); break;
|
|
859
|
+
case 'string': case 'path': for (const p of node.parts) if (p.kind === 'interp') add(p.expr); break;
|
|
860
|
+
case 'let': case 'attrset':
|
|
861
|
+
for (const b of node.bindings) {
|
|
862
|
+
if (b.kind === 'attr') { segs(b.path); add(b.value); } else { add(b.from); segs(b.names); }
|
|
863
|
+
}
|
|
864
|
+
if (node.type === 'let') add(node.body);
|
|
865
|
+
break;
|
|
866
|
+
default: break;
|
|
867
|
+
}
|
|
868
|
+
return out;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Parses Nix source. Never throws.
|
|
873
|
+
* @param {string} source
|
|
874
|
+
* @param {{file?:string, budgets?:object, grammarSource?:Function}} [opts]
|
|
875
|
+
* @returns {{status:'ok'|'partial'|'failed'|'budget_exceeded'|'missing_grammar', ast:object|null, errors:object[], budget?:{name:string,limit:number}, gap?:object, file:string|null, grammar:object|null, usedNixBinary:false, stats:object}}
|
|
876
|
+
*/
|
|
877
|
+
export function parseNix(source, opts = {}) {
|
|
878
|
+
const file = opts.file || null;
|
|
879
|
+
const budgets = { ...DEFAULT_PARSE_BUDGETS, ...(opts.budgets || {}) };
|
|
880
|
+
const base = { file, ast: null, errors: [], grammar: null, usedNixBinary: false, stats: { bytes: 0, tokens: 0, nodes: 0 } };
|
|
881
|
+
const loaded = loadNixGrammar({ grammarSource: opts.grammarSource });
|
|
882
|
+
if (!loaded.available) return { ...base, status: 'missing_grammar', gap: loaded.gap };
|
|
883
|
+
base.grammar = { name: loaded.grammar.name, version: loaded.grammar.version, sha256: loaded.checksum };
|
|
884
|
+
if (typeof source !== 'string') return { ...base, status: 'failed', errors: [{ kind: 'malformed-input', detail: 'source is not a string', span: null }] };
|
|
885
|
+
const text = source;
|
|
886
|
+
const bytes = Buffer.byteLength(text, 'utf8');
|
|
887
|
+
base.stats.bytes = bytes;
|
|
888
|
+
if (bytes > budgets.maxBytes) return { ...base, status: 'budget_exceeded', budget: { name: 'maxBytes', limit: budgets.maxBytes } };
|
|
889
|
+
if (text.includes('\u0000')) return { ...base, status: 'failed', errors: [{ kind: 'malformed-input', detail: 'source contains NUL bytes (binary data)', span: null }] };
|
|
890
|
+
const p = new NixParser(text, loaded.grammar, budgets);
|
|
891
|
+
try {
|
|
892
|
+
const ast = p.parseExpr();
|
|
893
|
+
const tail = p.peek();
|
|
894
|
+
if (tail.t !== 'eof') throw new ParseError(`unexpected "${tail.v || tail.t}" after the expression`, tail.s, Math.max(tail.e, tail.s + 1));
|
|
895
|
+
base.stats.tokens = p.tokens;
|
|
896
|
+
base.stats.nodes = p.nodes;
|
|
897
|
+
return { ...base, status: p.errors.length ? 'partial' : 'ok', ast, errors: p.errors };
|
|
898
|
+
} catch (e) {
|
|
899
|
+
base.stats.tokens = p.tokens;
|
|
900
|
+
base.stats.nodes = p.nodes;
|
|
901
|
+
if (e instanceof BudgetError) return { ...base, status: 'budget_exceeded', budget: { name: e.budget, limit: e.limit }, errors: [] };
|
|
902
|
+
if (e instanceof ParseError) {
|
|
903
|
+
const errors = [...p.errors, { kind: 'syntax-error', detail: e.detail, span: p.loc.span(Math.min(e.pos, text.length), Math.min(Math.max(e.end, e.pos), text.length)) }];
|
|
904
|
+
return { ...base, status: 'failed', errors };
|
|
905
|
+
}
|
|
906
|
+
return { ...base, status: 'failed', errors: [{ kind: 'internal-error', detail: String((e && e.message) || e), span: null }] };
|
|
907
|
+
}
|
|
908
|
+
}
|