@invinite-org/chartlang-compiler 1.3.0 → 1.4.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 +64 -0
- package/dist/analysis/extractDependencyGraph.d.ts.map +1 -1
- package/dist/analysis/extractDependencyGraph.js +9 -1
- package/dist/analysis/extractDependencyGraph.js.map +1 -1
- package/dist/analysis/extractInputs.d.ts.map +1 -1
- package/dist/analysis/extractInputs.js +2 -0
- package/dist/analysis/extractInputs.js.map +1 -1
- package/dist/analysis/extractRequestedIntervals.d.ts +29 -9
- package/dist/analysis/extractRequestedIntervals.d.ts.map +1 -1
- package/dist/analysis/extractRequestedIntervals.js +173 -42
- package/dist/analysis/extractRequestedIntervals.js.map +1 -1
- package/dist/analysis/index.d.ts +1 -0
- package/dist/analysis/index.d.ts.map +1 -1
- package/dist/analysis/index.js +1 -0
- package/dist/analysis/index.js.map +1 -1
- package/dist/analysis/stateArrayCapacity.d.ts +58 -0
- package/dist/analysis/stateArrayCapacity.d.ts.map +1 -0
- package/dist/analysis/stateArrayCapacity.js +108 -0
- package/dist/analysis/stateArrayCapacity.js.map +1 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +12 -3
- package/dist/api.js.map +1 -1
- package/dist/diagnostics.d.ts +6 -2
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/manifest.d.ts +2 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +4 -0
- package/dist/manifest.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +57 -1
- package/dist/program.js.map +1 -1
- package/dist/transformers/callsiteIdInjection.d.ts.map +1 -1
- package/dist/transformers/callsiteIdInjection.js +8 -1
- package/dist/transformers/callsiteIdInjection.js.map +1 -1
- package/dist/transformers/plotKindFromCallsite.d.ts +3 -0
- package/dist/transformers/plotKindFromCallsite.d.ts.map +1 -1
- package/dist/transformers/plotKindFromCallsite.js +7 -0
- package/dist/transformers/plotKindFromCallsite.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extractRequestedIntervals.d.ts","sourceRoot":"","sources":["../../src/analysis/extractRequestedIntervals.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"extractRequestedIntervals.d.ts","sourceRoot":"","sources":["../../src/analysis/extractRequestedIntervals.ts"],"names":[],"mappings":"AAGA,OAAO,EAEH,KAAK,aAAa,EAClB,KAAK,4BAA4B,EACpC,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAG7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAG9D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACnC,SAAS,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACjC,KAAK,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IACpC,mBAAmB,EAAE,aAAa,CAAC,4BAA4B,CAAC,CAAC;CACpE,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,sBAAsB,CAClC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,OAAO,EAAE,EAAE,CAAC,WAAW,EACvB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,EACrD,WAAW,EAAE,iBAAiB,EAAE,EAChC,UAAU,GAAE,MAA4B,EACxC,mBAAmB,UAAQ,GAC5B,eAAe,CAgDjB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,yBAAyB,CACrC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,OAAO,EAAE,EAAE,CAAC,WAAW,EACvB,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,EACrD,WAAW,EAAE,iBAAiB,EAAE,EAChC,UAAU,GAAE,MAA4B,GACzC,aAAa,CAAC,MAAM,CAAC,CAEvB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Copyright (c) 2026 Invinite. Licensed under the MIT License.
|
|
2
2
|
// See the LICENSE file in the repo root for full license text.
|
|
3
|
+
import { feedKey, } from "@invinite-org/chartlang-core";
|
|
3
4
|
import ts from "typescript";
|
|
4
5
|
import { createDiagnostic } from "../diagnostics.js";
|
|
5
6
|
import { callsiteIdFor } from "../transformers/callsiteIdInjection.js";
|
|
@@ -7,11 +8,22 @@ import { resolveCalleeName } from "../transformers/resolveCallee.js";
|
|
|
7
8
|
import { validateSecurityExpr } from "./validateSecurityExpr.js";
|
|
8
9
|
/**
|
|
9
10
|
* Walk a script's AST and collect every static `interval` argument to
|
|
10
|
-
* `request.security({ interval: ... })` and `request.lowerTf(...)`,
|
|
11
|
-
* `request.security`
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `request.
|
|
11
|
+
* `request.security({ interval: ... })` and `request.lowerTf(...)`, every
|
|
12
|
+
* distinct requested `(symbol?, interval)` feed (`request.security` only —
|
|
13
|
+
* `request.lowerTf` has no symbol), plus every `request.security` *expression*
|
|
14
|
+
* callsite (a second arrow/function argument). Dynamic intervals emit
|
|
15
|
+
* `request-security-interval-not-literal` (for `request.security`) or
|
|
16
|
+
* `request-lower-tf-interval-not-literal` (for `request.lowerTf`); a dynamic
|
|
17
|
+
* `request.security` symbol emits `request-security-symbol-not-literal`. Either
|
|
18
|
+
* dynamic axis is excluded.
|
|
19
|
+
*
|
|
20
|
+
* The `symbol` opt is read the same three ways `interval` is — a string literal,
|
|
21
|
+
* an `inputs.<enum>` access (expanded to all options), or an `inputs.<name>`
|
|
22
|
+
* `input.symbol` default literal — and the cartesian product of resolved
|
|
23
|
+
* symbols × intervals is deduped into `feeds` via the shared
|
|
24
|
+
* `feedKey(symbol, interval)`. A symbol-omitted (or empty-literal) feed keeps its
|
|
25
|
+
* interval in `intervals` (the main-symbol projection); a present-symbol feed
|
|
26
|
+
* does not.
|
|
15
27
|
*
|
|
16
28
|
* Each expression callsite is recorded as a {@link SecurityExpressionDescriptor}
|
|
17
29
|
* keyed by the same `slotId` the callsite-id transformer injects (via the
|
|
@@ -23,30 +35,37 @@ import { validateSecurityExpr } from "./validateSecurityExpr.js";
|
|
|
23
35
|
* @since 0.7
|
|
24
36
|
* @stable
|
|
25
37
|
* @example
|
|
26
|
-
* // const { intervals, securityExpressions } =
|
|
38
|
+
* // const { intervals, feeds, securityExpressions } =
|
|
27
39
|
* // extractRequestAnalysis(sf, checker, inputs, diagnostics, path, true);
|
|
28
40
|
* const fn: typeof extractRequestAnalysis = extractRequestAnalysis;
|
|
29
41
|
* void fn;
|
|
30
42
|
*/
|
|
31
43
|
export function extractRequestAnalysis(sourceFile, checker, inputs, diagnostics, sourcePath = sourceFile.fileName, validateExpressions = false) {
|
|
32
44
|
const intervals = new Set();
|
|
45
|
+
// Keyed by `feedKey(symbol, interval)` so the dedup format matches the
|
|
46
|
+
// runtime/host stream key exactly and the sort below is byte-stable.
|
|
47
|
+
const feeds = new Map();
|
|
33
48
|
const securityExpressions = [];
|
|
34
49
|
const visit = (node) => {
|
|
35
50
|
if (ts.isCallExpression(node)) {
|
|
36
51
|
const calleeName = resolveCalleeName(node, checker);
|
|
37
52
|
if (calleeName === "request.security" || calleeName === "request.lowerTf") {
|
|
38
|
-
readRequestInterval(node, calleeName, sourceFile, sourcePath, inputs, diagnostics, intervals);
|
|
53
|
+
readRequestInterval(node, calleeName, sourceFile, sourcePath, inputs, diagnostics, intervals, feeds);
|
|
39
54
|
}
|
|
40
55
|
if (calleeName === "request.security") {
|
|
41
|
-
readSecurityExpression(node, sourceFile, sourcePath, checker, diagnostics, validateExpressions, securityExpressions);
|
|
56
|
+
readSecurityExpression(node, sourceFile, sourcePath, checker, diagnostics, validateExpressions, inputs, securityExpressions);
|
|
42
57
|
}
|
|
43
58
|
}
|
|
44
59
|
ts.forEachChild(node, visit);
|
|
45
60
|
};
|
|
46
61
|
ts.forEachChild(sourceFile, visit);
|
|
47
62
|
securityExpressions.sort((a, b) => a.slotId.localeCompare(b.slotId));
|
|
63
|
+
const sortedFeeds = Array.from(feeds.entries())
|
|
64
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
65
|
+
.map(([, feed]) => feed);
|
|
48
66
|
return Object.freeze({
|
|
49
67
|
intervals: Object.freeze(Array.from(intervals).sort()),
|
|
68
|
+
feeds: Object.freeze(sortedFeeds),
|
|
50
69
|
securityExpressions: Object.freeze(securityExpressions.slice()),
|
|
51
70
|
});
|
|
52
71
|
}
|
|
@@ -72,12 +91,15 @@ export function extractRequestedIntervals(sourceFile, checker, inputs, diagnosti
|
|
|
72
91
|
* Detect and record a `request.security` expression callsite — a second
|
|
73
92
|
* argument that is an arrow or function expression. Mints the descriptor's
|
|
74
93
|
* `slotId` via `callsiteIdFor` (lockstep with the injector), reads the literal
|
|
75
|
-
* `interval
|
|
76
|
-
* `
|
|
77
|
-
*
|
|
78
|
-
*
|
|
94
|
+
* `interval`, the literal `symbol` (string literal or `input.symbol` default —
|
|
95
|
+
* an `input.enum`/dynamic symbol can't anchor a single expression clock, so it
|
|
96
|
+
* is omitted, mirroring how an `input.enum` interval can't anchor one), and the
|
|
97
|
+
* callback's single parameter name, and — when `validate` — runs the capture
|
|
98
|
+
* check. A callsite whose interval is not a compile-time literal already emitted
|
|
99
|
+
* `request-security-interval-not-literal` via `readRequestInterval`; it is
|
|
100
|
+
* skipped here (no descriptor).
|
|
79
101
|
*/
|
|
80
|
-
function readSecurityExpression(call, sourceFile, sourcePath, checker, diagnostics, validate, out) {
|
|
102
|
+
function readSecurityExpression(call, sourceFile, sourcePath, checker, diagnostics, validate, inputs, out) {
|
|
81
103
|
const callback = call.arguments[1];
|
|
82
104
|
if (callback === undefined ||
|
|
83
105
|
!(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
|
|
@@ -86,28 +108,30 @@ function readSecurityExpression(call, sourceFile, sourcePath, checker, diagnosti
|
|
|
86
108
|
if (validate) {
|
|
87
109
|
validateSecurityExpr(callback, checker, diagnostics, sourcePath);
|
|
88
110
|
}
|
|
89
|
-
const
|
|
111
|
+
const opts = call.arguments[0];
|
|
112
|
+
if (opts === undefined || !ts.isObjectLiteralExpression(opts))
|
|
113
|
+
return;
|
|
114
|
+
const interval = readLiteralInterval(opts);
|
|
90
115
|
if (interval === null)
|
|
91
116
|
return;
|
|
117
|
+
const symbol = readLiteralSymbol(opts, inputs);
|
|
92
118
|
const firstParam = callback.parameters[0];
|
|
93
119
|
const paramName = firstParam !== undefined && ts.isIdentifier(firstParam.name) ? firstParam.name.text : "";
|
|
94
120
|
out.push(Object.freeze({
|
|
95
121
|
slotId: callsiteIdFor(sourceFile, call, sourcePath),
|
|
122
|
+
...(symbol === undefined ? {} : { symbol }),
|
|
96
123
|
interval,
|
|
97
124
|
paramName,
|
|
98
125
|
}));
|
|
99
126
|
}
|
|
100
127
|
/**
|
|
101
|
-
* Read the literal `interval` string off a `request.security`
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
128
|
+
* Read the literal `interval` string off a `request.security` opts object, or
|
|
129
|
+
* `null` when it is absent or non-literal. Only string-literal intervals key an
|
|
130
|
+
* expression unit; an `input.enum` interval expands to multiple intervals for
|
|
131
|
+
* the requested-interval list but cannot anchor a single expression clock, so it
|
|
132
|
+
* is treated as non-literal here.
|
|
106
133
|
*/
|
|
107
|
-
function readLiteralInterval(
|
|
108
|
-
const opts = call.arguments[0];
|
|
109
|
-
if (opts === undefined || !ts.isObjectLiteralExpression(opts))
|
|
110
|
-
return null;
|
|
134
|
+
function readLiteralInterval(opts) {
|
|
111
135
|
const intervalProperty = opts.properties
|
|
112
136
|
.filter(ts.isPropertyAssignment)
|
|
113
137
|
.find((property) => ts.isIdentifier(property.name) && property.name.text === "interval");
|
|
@@ -116,7 +140,39 @@ function readLiteralInterval(call) {
|
|
|
116
140
|
const initializer = intervalProperty.initializer;
|
|
117
141
|
return ts.isStringLiteral(initializer) ? initializer.text : null;
|
|
118
142
|
}
|
|
119
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Read the literal `symbol` off a `request.security` opts object for the
|
|
145
|
+
* expression-descriptor anchor: a string literal or an `input.symbol` default
|
|
146
|
+
* resolves to a concrete symbol; an empty literal, an `input.enum`/dynamic
|
|
147
|
+
* symbol, or an absent property resolves to `undefined` (the chart symbol —
|
|
148
|
+
* an enum/dynamic symbol can't anchor a single expression clock). Never pushes
|
|
149
|
+
* a diagnostic; `readRequestInterval` already reported any dynamic symbol.
|
|
150
|
+
*/
|
|
151
|
+
function readLiteralSymbol(opts, inputs) {
|
|
152
|
+
const resolved = resolveOptString(opts, "symbol", inputs);
|
|
153
|
+
if (resolved.kind === "literal" || resolved.kind === "input-default") {
|
|
154
|
+
return resolved.value === "" ? undefined : resolved.value;
|
|
155
|
+
}
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
function resolveOptString(opts, propName, inputs) {
|
|
159
|
+
const property = opts.properties
|
|
160
|
+
.filter(ts.isPropertyAssignment)
|
|
161
|
+
.find((p) => ts.isIdentifier(p.name) && p.name.text === propName);
|
|
162
|
+
if (property === undefined)
|
|
163
|
+
return { kind: "absent" };
|
|
164
|
+
const initializer = property.initializer;
|
|
165
|
+
if (ts.isStringLiteral(initializer))
|
|
166
|
+
return { kind: "literal", value: initializer.text };
|
|
167
|
+
const enumOptions = getInputsEnumOptions(initializer, inputs);
|
|
168
|
+
if (enumOptions !== null)
|
|
169
|
+
return { kind: "enum", values: enumOptions };
|
|
170
|
+
const symbolDefault = getInputSymbolDefault(initializer, inputs);
|
|
171
|
+
if (symbolDefault !== null)
|
|
172
|
+
return { kind: "input-default", value: symbolDefault };
|
|
173
|
+
return { kind: "dynamic", node: initializer };
|
|
174
|
+
}
|
|
175
|
+
function readRequestInterval(call, calleeName, sourceFile, sourcePath, inputs, diagnostics, intervals, feeds) {
|
|
120
176
|
const opts = call.arguments[0];
|
|
121
177
|
if (opts === undefined || !ts.isObjectLiteralExpression(opts))
|
|
122
178
|
return;
|
|
@@ -125,27 +181,102 @@ function readRequestInterval(call, calleeName, sourceFile, sourcePath, inputs, d
|
|
|
125
181
|
.find((property) => ts.isIdentifier(property.name) && property.name.text === "interval");
|
|
126
182
|
if (intervalProperty === undefined)
|
|
127
183
|
return;
|
|
128
|
-
const
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
184
|
+
const resolvedIntervals = resolveIntervals(intervalProperty.initializer, inputs);
|
|
185
|
+
if (resolvedIntervals === null) {
|
|
186
|
+
diagnostics.push(createDiagnostic({
|
|
187
|
+
severity: "error",
|
|
188
|
+
code: calleeName === "request.lowerTf"
|
|
189
|
+
? "request-lower-tf-interval-not-literal"
|
|
190
|
+
: "request-security-interval-not-literal",
|
|
191
|
+
message: `${calleeName}({ interval }) must be a string literal or input.enum value`,
|
|
192
|
+
file: sourcePath,
|
|
193
|
+
node: intervalProperty.initializer,
|
|
194
|
+
sourceFile,
|
|
195
|
+
}));
|
|
132
196
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
197
|
+
// `request.lowerTf` has no symbol dimension: it only ever feeds intervals
|
|
198
|
+
// (the chart-symbol HTF projection), never `feeds`. Preserve its existing
|
|
199
|
+
// interval-only behavior exactly.
|
|
200
|
+
if (calleeName === "request.lowerTf") {
|
|
201
|
+
for (const interval of resolvedIntervals ?? [])
|
|
202
|
+
intervals.add(interval);
|
|
137
203
|
return;
|
|
138
204
|
}
|
|
139
|
-
diagnostics
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
205
|
+
const resolvedSymbols = resolveSymbols(opts, inputs, sourceFile, sourcePath, diagnostics);
|
|
206
|
+
for (const symbol of resolvedSymbols) {
|
|
207
|
+
for (const interval of resolvedIntervals ?? []) {
|
|
208
|
+
// A symbol-omitted (chart-symbol) feed keeps its interval in the
|
|
209
|
+
// main-symbol projection; a present-symbol feed does not.
|
|
210
|
+
if (symbol === undefined)
|
|
211
|
+
intervals.add(interval);
|
|
212
|
+
feeds.set(feedKey(symbol, interval), {
|
|
213
|
+
...(symbol === undefined ? {} : { symbol }),
|
|
214
|
+
interval,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Resolve a `request.*` `interval` initializer to its concrete interval list —
|
|
221
|
+
* a single-element list for a string literal, all options for an `inputs.<enum>`
|
|
222
|
+
* access — or `null` for a genuinely-dynamic interval (the caller pushes the
|
|
223
|
+
* appropriate diagnostic). `interval` never uses the `input.symbol`-default path:
|
|
224
|
+
* `input.interval` is the main-chart interval, not a feed interval.
|
|
225
|
+
*/
|
|
226
|
+
function resolveIntervals(initializer, inputs) {
|
|
227
|
+
if (ts.isStringLiteral(initializer))
|
|
228
|
+
return [initializer.text];
|
|
229
|
+
return getInputsEnumOptions(initializer, inputs);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Resolve a `request.security` opts object's `symbol` axis to the list of
|
|
233
|
+
* requested symbols (`undefined` ⇒ the chart's own symbol): `[undefined]` when
|
|
234
|
+
* absent or an empty literal, `[value]` for a string literal or `input.symbol`
|
|
235
|
+
* default, all options for an `inputs.<enum>` access, or `[]` (excluded, after
|
|
236
|
+
* pushing `request-security-symbol-not-literal`) for a dynamic symbol.
|
|
237
|
+
*/
|
|
238
|
+
function resolveSymbols(opts, inputs, sourceFile, sourcePath, diagnostics) {
|
|
239
|
+
const resolved = resolveOptString(opts, "symbol", inputs);
|
|
240
|
+
switch (resolved.kind) {
|
|
241
|
+
case "absent":
|
|
242
|
+
return [undefined];
|
|
243
|
+
case "literal":
|
|
244
|
+
// An empty-literal symbol collapses to the chart symbol, matching
|
|
245
|
+
// `feedKey`'s empty-collapse.
|
|
246
|
+
return [resolved.value === "" ? undefined : resolved.value];
|
|
247
|
+
case "input-default":
|
|
248
|
+
return [resolved.value];
|
|
249
|
+
case "enum":
|
|
250
|
+
return resolved.values;
|
|
251
|
+
default:
|
|
252
|
+
diagnostics.push(createDiagnostic({
|
|
253
|
+
severity: "error",
|
|
254
|
+
code: "request-security-symbol-not-literal",
|
|
255
|
+
message: "request.security({ symbol }) must be a string literal, an input.symbol default, or an input.enum value",
|
|
256
|
+
file: sourcePath,
|
|
257
|
+
node: resolved.node,
|
|
258
|
+
sourceFile,
|
|
259
|
+
}));
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Resolve an `inputs.<name>` access whose descriptor is an `input.symbol` to its
|
|
265
|
+
* `defaultValue` string, or `null` when the access is not an `inputs.<name>`
|
|
266
|
+
* property access, the descriptor is missing / not a `symbol` kind, or its
|
|
267
|
+
* `defaultValue` is not a string.
|
|
268
|
+
*/
|
|
269
|
+
function getInputSymbolDefault(expr, inputs) {
|
|
270
|
+
if (!ts.isPropertyAccessExpression(expr) ||
|
|
271
|
+
!ts.isIdentifier(expr.expression) ||
|
|
272
|
+
expr.expression.text !== "inputs") {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const descriptor = inputs[expr.name.text];
|
|
276
|
+
if (descriptor === undefined || descriptor.kind !== "symbol")
|
|
277
|
+
return null;
|
|
278
|
+
const defaultValue = descriptor.defaultValue;
|
|
279
|
+
return typeof defaultValue === "string" ? defaultValue : null;
|
|
149
280
|
}
|
|
150
281
|
function getInputsEnumOptions(expr, inputs) {
|
|
151
282
|
if (!ts.isPropertyAccessExpression(expr) ||
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extractRequestedIntervals.js","sourceRoot":"","sources":["../../src/analysis/extractRequestedIntervals.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nimport type { SecurityExpressionDescriptor } from \"@invinite-org/chartlang-core\";\nimport ts from \"typescript\";\n\nimport { type CompileDiagnostic, createDiagnostic } from \"../diagnostics.js\";\nimport { callsiteIdFor } from \"../transformers/callsiteIdInjection.js\";\nimport { resolveCalleeName } from \"../transformers/resolveCallee.js\";\nimport type { ExtractedDescriptor } from \"./extractInputs.js\";\nimport { validateSecurityExpr } from \"./validateSecurityExpr.js\";\n\n/**\n * Combined result of the `request.*` analysis pass: the sorted, deduped list\n * of requested intervals plus one {@link SecurityExpressionDescriptor} per\n * `request.security({ interval }, (bar) => …)` expression callsite (sorted by\n * `slotId`).\n *\n * @since 0.7\n * @stable\n * @example\n * const r: RequestAnalysis = { intervals: [\"1W\"], securityExpressions: [] };\n * void r;\n */\nexport type RequestAnalysis = Readonly<{\n intervals: ReadonlyArray<string>;\n securityExpressions: ReadonlyArray<SecurityExpressionDescriptor>;\n}>;\n\n/**\n * Walk a script's AST and collect every static `interval` argument to\n * `request.security({ interval: ... })` and `request.lowerTf(...)`, plus every\n * `request.security` *expression* callsite (a second arrow/function argument).\n * Dynamic intervals emit `request-security-interval-not-literal` (for\n * `request.security`) or `request-lower-tf-interval-not-literal` (for\n * `request.lowerTf`) and are excluded.\n *\n * Each expression callsite is recorded as a {@link SecurityExpressionDescriptor}\n * keyed by the same `slotId` the callsite-id transformer injects (via the\n * shared `callsiteIdFor` helper) so the runtime can match the manifest entry\n * to the inlined callback. When `validateExpressions` is `true`, each callback\n * is also run through {@link validateSecurityExpr}, pushing\n * `request-security-expr-captures-local` for any out-of-subset reference.\n *\n * @since 0.7\n * @stable\n * @example\n * // const { intervals, securityExpressions } =\n * // extractRequestAnalysis(sf, checker, inputs, diagnostics, path, true);\n * const fn: typeof extractRequestAnalysis = extractRequestAnalysis;\n * void fn;\n */\nexport function extractRequestAnalysis(\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n diagnostics: CompileDiagnostic[],\n sourcePath: string = sourceFile.fileName,\n validateExpressions = false,\n): RequestAnalysis {\n const intervals = new Set<string>();\n const securityExpressions: SecurityExpressionDescriptor[] = [];\n\n const visit = (node: ts.Node): void => {\n if (ts.isCallExpression(node)) {\n const calleeName = resolveCalleeName(node, checker);\n if (calleeName === \"request.security\" || calleeName === \"request.lowerTf\") {\n readRequestInterval(\n node,\n calleeName,\n sourceFile,\n sourcePath,\n inputs,\n diagnostics,\n intervals,\n );\n }\n if (calleeName === \"request.security\") {\n readSecurityExpression(\n node,\n sourceFile,\n sourcePath,\n checker,\n diagnostics,\n validateExpressions,\n securityExpressions,\n );\n }\n }\n ts.forEachChild(node, visit);\n };\n\n ts.forEachChild(sourceFile, visit);\n securityExpressions.sort((a, b) => a.slotId.localeCompare(b.slotId));\n return Object.freeze({\n intervals: Object.freeze(Array.from(intervals).sort()),\n securityExpressions: Object.freeze(securityExpressions.slice()),\n });\n}\n\n/**\n * Walk a script's AST and collect every static `interval` argument to\n * `request.security({ interval: ... })` and `request.lowerTf(...)`. Dynamic\n * arguments emit `request-security-interval-not-literal` (for `request.security`)\n * or `request-lower-tf-interval-not-literal` (for `request.lowerTf`) and are\n * excluded. Thin delegate over {@link extractRequestAnalysis} kept for callers\n * that only need the interval list.\n *\n * @since 0.4\n * @example\n * // const intervals = extractRequestedIntervals(sf, checker, inputs, diagnostics);\n * // intervals === [\"1D\", \"5m\"];\n * const fn: typeof extractRequestedIntervals = extractRequestedIntervals;\n * void fn;\n */\nexport function extractRequestedIntervals(\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n diagnostics: CompileDiagnostic[],\n sourcePath: string = sourceFile.fileName,\n): ReadonlyArray<string> {\n return extractRequestAnalysis(sourceFile, checker, inputs, diagnostics, sourcePath).intervals;\n}\n\n/**\n * Detect and record a `request.security` expression callsite — a second\n * argument that is an arrow or function expression. Mints the descriptor's\n * `slotId` via `callsiteIdFor` (lockstep with the injector), reads the literal\n * `interval` and the callback's single parameter name, and — when\n * `validate` — runs the capture check. A callsite whose interval is not a\n * compile-time literal already emitted `request-security-interval-not-literal`\n * via `readRequestInterval`; it is skipped here (no descriptor).\n */\nfunction readSecurityExpression(\n call: ts.CallExpression,\n sourceFile: ts.SourceFile,\n sourcePath: string,\n checker: ts.TypeChecker,\n diagnostics: CompileDiagnostic[],\n validate: boolean,\n out: SecurityExpressionDescriptor[],\n): void {\n const callback = call.arguments[1];\n if (\n callback === undefined ||\n !(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))\n ) {\n return;\n }\n if (validate) {\n validateSecurityExpr(callback, checker, diagnostics, sourcePath);\n }\n const interval = readLiteralInterval(call);\n if (interval === null) return;\n const firstParam = callback.parameters[0];\n const paramName =\n firstParam !== undefined && ts.isIdentifier(firstParam.name) ? firstParam.name.text : \"\";\n out.push(\n Object.freeze({\n slotId: callsiteIdFor(sourceFile, call, sourcePath),\n interval,\n paramName,\n }),\n );\n}\n\n/**\n * Read the literal `interval` string off a `request.security` call's opts\n * object, or `null` when it is absent or non-literal. Only string-literal\n * intervals key an expression unit; an `input.enum` interval expands to\n * multiple intervals for the requested-interval list but cannot anchor a\n * single expression clock, so it is treated as non-literal here.\n */\nfunction readLiteralInterval(call: ts.CallExpression): string | null {\n const opts = call.arguments[0];\n if (opts === undefined || !ts.isObjectLiteralExpression(opts)) return null;\n const intervalProperty = opts.properties\n .filter(ts.isPropertyAssignment)\n .find((property) => ts.isIdentifier(property.name) && property.name.text === \"interval\");\n if (intervalProperty === undefined) return null;\n const initializer = intervalProperty.initializer;\n return ts.isStringLiteral(initializer) ? initializer.text : null;\n}\n\nfunction readRequestInterval(\n call: ts.CallExpression,\n calleeName: \"request.security\" | \"request.lowerTf\",\n sourceFile: ts.SourceFile,\n sourcePath: string,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n diagnostics: CompileDiagnostic[],\n intervals: Set<string>,\n): void {\n const opts = call.arguments[0];\n if (opts === undefined || !ts.isObjectLiteralExpression(opts)) return;\n const intervalProperty = opts.properties\n .filter(ts.isPropertyAssignment)\n .find((property) => ts.isIdentifier(property.name) && property.name.text === \"interval\");\n if (intervalProperty === undefined) return;\n\n const initializer = intervalProperty.initializer;\n if (ts.isStringLiteral(initializer)) {\n intervals.add(initializer.text);\n return;\n }\n\n const enumOptions = getInputsEnumOptions(initializer, inputs);\n if (enumOptions !== null) {\n for (const option of enumOptions) intervals.add(option);\n return;\n }\n\n diagnostics.push(\n createDiagnostic({\n severity: \"error\",\n code:\n calleeName === \"request.lowerTf\"\n ? \"request-lower-tf-interval-not-literal\"\n : \"request-security-interval-not-literal\",\n message: `${calleeName}({ interval }) must be a string literal or input.enum value`,\n file: sourcePath,\n node: initializer,\n sourceFile,\n }),\n );\n}\n\nfunction getInputsEnumOptions(\n expr: ts.Expression,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n): ReadonlyArray<string> | null {\n if (\n !ts.isPropertyAccessExpression(expr) ||\n !ts.isIdentifier(expr.expression) ||\n expr.expression.text !== \"inputs\"\n ) {\n return null;\n }\n const descriptor = inputs[expr.name.text];\n if (descriptor === undefined || descriptor.kind !== \"enum\") return null;\n const options = descriptor.options;\n if (!Array.isArray(options)) return null;\n const strings: string[] = [];\n for (const option of options) {\n if (typeof option !== \"string\") return null;\n strings.push(option);\n }\n return Object.freeze(strings);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"extractRequestedIntervals.js","sourceRoot":"","sources":["../../src/analysis/extractRequestedIntervals.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nimport {\n feedKey,\n type RequestedFeed,\n type SecurityExpressionDescriptor,\n} from \"@invinite-org/chartlang-core\";\nimport ts from \"typescript\";\n\nimport { type CompileDiagnostic, createDiagnostic } from \"../diagnostics.js\";\nimport { callsiteIdFor } from \"../transformers/callsiteIdInjection.js\";\nimport { resolveCalleeName } from \"../transformers/resolveCallee.js\";\nimport type { ExtractedDescriptor } from \"./extractInputs.js\";\nimport { validateSecurityExpr } from \"./validateSecurityExpr.js\";\n\n/**\n * Combined result of the `request.*` analysis pass: the sorted, deduped list\n * of requested intervals (the **main-symbol** projection), the sorted, deduped\n * list of requested `(symbol?, interval)` {@link RequestedFeed | feeds} (the\n * superset), plus one {@link SecurityExpressionDescriptor} per\n * `request.security({ interval }, (bar) => …)` expression callsite (sorted by\n * `slotId`).\n *\n * `intervals` keeps its exact existing meaning — the symbol-omitted\n * (chart-symbol) higher-timeframe intervals — so existing manifests stay\n * byte-identical. `feeds` adds the symbol dimension: one entry per distinct\n * `(symbol, interval)` pair, deduped + ordered by the shared\n * `feedKey(symbol, interval)` so the printed manifest is byte-stable.\n *\n * @since 0.7\n * @stable\n * @example\n * const r: RequestAnalysis = { intervals: [\"1W\"], feeds: [], securityExpressions: [] };\n * void r;\n */\nexport type RequestAnalysis = Readonly<{\n intervals: ReadonlyArray<string>;\n feeds: ReadonlyArray<RequestedFeed>;\n securityExpressions: ReadonlyArray<SecurityExpressionDescriptor>;\n}>;\n\n/**\n * Walk a script's AST and collect every static `interval` argument to\n * `request.security({ interval: ... })` and `request.lowerTf(...)`, every\n * distinct requested `(symbol?, interval)` feed (`request.security` only —\n * `request.lowerTf` has no symbol), plus every `request.security` *expression*\n * callsite (a second arrow/function argument). Dynamic intervals emit\n * `request-security-interval-not-literal` (for `request.security`) or\n * `request-lower-tf-interval-not-literal` (for `request.lowerTf`); a dynamic\n * `request.security` symbol emits `request-security-symbol-not-literal`. Either\n * dynamic axis is excluded.\n *\n * The `symbol` opt is read the same three ways `interval` is — a string literal,\n * an `inputs.<enum>` access (expanded to all options), or an `inputs.<name>`\n * `input.symbol` default literal — and the cartesian product of resolved\n * symbols × intervals is deduped into `feeds` via the shared\n * `feedKey(symbol, interval)`. A symbol-omitted (or empty-literal) feed keeps its\n * interval in `intervals` (the main-symbol projection); a present-symbol feed\n * does not.\n *\n * Each expression callsite is recorded as a {@link SecurityExpressionDescriptor}\n * keyed by the same `slotId` the callsite-id transformer injects (via the\n * shared `callsiteIdFor` helper) so the runtime can match the manifest entry\n * to the inlined callback. When `validateExpressions` is `true`, each callback\n * is also run through {@link validateSecurityExpr}, pushing\n * `request-security-expr-captures-local` for any out-of-subset reference.\n *\n * @since 0.7\n * @stable\n * @example\n * // const { intervals, feeds, securityExpressions } =\n * // extractRequestAnalysis(sf, checker, inputs, diagnostics, path, true);\n * const fn: typeof extractRequestAnalysis = extractRequestAnalysis;\n * void fn;\n */\nexport function extractRequestAnalysis(\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n diagnostics: CompileDiagnostic[],\n sourcePath: string = sourceFile.fileName,\n validateExpressions = false,\n): RequestAnalysis {\n const intervals = new Set<string>();\n // Keyed by `feedKey(symbol, interval)` so the dedup format matches the\n // runtime/host stream key exactly and the sort below is byte-stable.\n const feeds = new Map<string, RequestedFeed>();\n const securityExpressions: SecurityExpressionDescriptor[] = [];\n\n const visit = (node: ts.Node): void => {\n if (ts.isCallExpression(node)) {\n const calleeName = resolveCalleeName(node, checker);\n if (calleeName === \"request.security\" || calleeName === \"request.lowerTf\") {\n readRequestInterval(\n node,\n calleeName,\n sourceFile,\n sourcePath,\n inputs,\n diagnostics,\n intervals,\n feeds,\n );\n }\n if (calleeName === \"request.security\") {\n readSecurityExpression(\n node,\n sourceFile,\n sourcePath,\n checker,\n diagnostics,\n validateExpressions,\n inputs,\n securityExpressions,\n );\n }\n }\n ts.forEachChild(node, visit);\n };\n\n ts.forEachChild(sourceFile, visit);\n securityExpressions.sort((a, b) => a.slotId.localeCompare(b.slotId));\n const sortedFeeds = Array.from(feeds.entries())\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([, feed]) => feed);\n return Object.freeze({\n intervals: Object.freeze(Array.from(intervals).sort()),\n feeds: Object.freeze(sortedFeeds),\n securityExpressions: Object.freeze(securityExpressions.slice()),\n });\n}\n\n/**\n * Walk a script's AST and collect every static `interval` argument to\n * `request.security({ interval: ... })` and `request.lowerTf(...)`. Dynamic\n * arguments emit `request-security-interval-not-literal` (for `request.security`)\n * or `request-lower-tf-interval-not-literal` (for `request.lowerTf`) and are\n * excluded. Thin delegate over {@link extractRequestAnalysis} kept for callers\n * that only need the interval list.\n *\n * @since 0.4\n * @example\n * // const intervals = extractRequestedIntervals(sf, checker, inputs, diagnostics);\n * // intervals === [\"1D\", \"5m\"];\n * const fn: typeof extractRequestedIntervals = extractRequestedIntervals;\n * void fn;\n */\nexport function extractRequestedIntervals(\n sourceFile: ts.SourceFile,\n checker: ts.TypeChecker,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n diagnostics: CompileDiagnostic[],\n sourcePath: string = sourceFile.fileName,\n): ReadonlyArray<string> {\n return extractRequestAnalysis(sourceFile, checker, inputs, diagnostics, sourcePath).intervals;\n}\n\n/**\n * Detect and record a `request.security` expression callsite — a second\n * argument that is an arrow or function expression. Mints the descriptor's\n * `slotId` via `callsiteIdFor` (lockstep with the injector), reads the literal\n * `interval`, the literal `symbol` (string literal or `input.symbol` default —\n * an `input.enum`/dynamic symbol can't anchor a single expression clock, so it\n * is omitted, mirroring how an `input.enum` interval can't anchor one), and the\n * callback's single parameter name, and — when `validate` — runs the capture\n * check. A callsite whose interval is not a compile-time literal already emitted\n * `request-security-interval-not-literal` via `readRequestInterval`; it is\n * skipped here (no descriptor).\n */\nfunction readSecurityExpression(\n call: ts.CallExpression,\n sourceFile: ts.SourceFile,\n sourcePath: string,\n checker: ts.TypeChecker,\n diagnostics: CompileDiagnostic[],\n validate: boolean,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n out: SecurityExpressionDescriptor[],\n): void {\n const callback = call.arguments[1];\n if (\n callback === undefined ||\n !(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))\n ) {\n return;\n }\n if (validate) {\n validateSecurityExpr(callback, checker, diagnostics, sourcePath);\n }\n const opts = call.arguments[0];\n if (opts === undefined || !ts.isObjectLiteralExpression(opts)) return;\n const interval = readLiteralInterval(opts);\n if (interval === null) return;\n const symbol = readLiteralSymbol(opts, inputs);\n const firstParam = callback.parameters[0];\n const paramName =\n firstParam !== undefined && ts.isIdentifier(firstParam.name) ? firstParam.name.text : \"\";\n out.push(\n Object.freeze({\n slotId: callsiteIdFor(sourceFile, call, sourcePath),\n ...(symbol === undefined ? {} : { symbol }),\n interval,\n paramName,\n }),\n );\n}\n\n/**\n * Read the literal `interval` string off a `request.security` opts object, or\n * `null` when it is absent or non-literal. Only string-literal intervals key an\n * expression unit; an `input.enum` interval expands to multiple intervals for\n * the requested-interval list but cannot anchor a single expression clock, so it\n * is treated as non-literal here.\n */\nfunction readLiteralInterval(opts: ts.ObjectLiteralExpression): string | null {\n const intervalProperty = opts.properties\n .filter(ts.isPropertyAssignment)\n .find((property) => ts.isIdentifier(property.name) && property.name.text === \"interval\");\n if (intervalProperty === undefined) return null;\n const initializer = intervalProperty.initializer;\n return ts.isStringLiteral(initializer) ? initializer.text : null;\n}\n\n/**\n * Read the literal `symbol` off a `request.security` opts object for the\n * expression-descriptor anchor: a string literal or an `input.symbol` default\n * resolves to a concrete symbol; an empty literal, an `input.enum`/dynamic\n * symbol, or an absent property resolves to `undefined` (the chart symbol —\n * an enum/dynamic symbol can't anchor a single expression clock). Never pushes\n * a diagnostic; `readRequestInterval` already reported any dynamic symbol.\n */\nfunction readLiteralSymbol(\n opts: ts.ObjectLiteralExpression,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n): string | undefined {\n const resolved = resolveOptString(opts, \"symbol\", inputs);\n if (resolved.kind === \"literal\" || resolved.kind === \"input-default\") {\n return resolved.value === \"\" ? undefined : resolved.value;\n }\n return undefined;\n}\n\n/**\n * Resolution of an opts string property read three ways (mirroring `interval`,\n * plus the `input.symbol`-default path symbols need): a string literal, the\n * options of an `inputs.<enum>` access, the default of an `inputs.<name>`\n * `input.symbol` access, an absent property, or a genuinely-dynamic expression.\n */\ntype ResolvedOptString =\n | Readonly<{ kind: \"literal\"; value: string }>\n | Readonly<{ kind: \"enum\"; values: ReadonlyArray<string> }>\n | Readonly<{ kind: \"input-default\"; value: string }>\n | Readonly<{ kind: \"absent\" }>\n | Readonly<{ kind: \"dynamic\"; node: ts.Expression }>;\n\nfunction resolveOptString(\n opts: ts.ObjectLiteralExpression,\n propName: string,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n): ResolvedOptString {\n const property = opts.properties\n .filter(ts.isPropertyAssignment)\n .find((p) => ts.isIdentifier(p.name) && p.name.text === propName);\n if (property === undefined) return { kind: \"absent\" };\n\n const initializer = property.initializer;\n if (ts.isStringLiteral(initializer)) return { kind: \"literal\", value: initializer.text };\n\n const enumOptions = getInputsEnumOptions(initializer, inputs);\n if (enumOptions !== null) return { kind: \"enum\", values: enumOptions };\n\n const symbolDefault = getInputSymbolDefault(initializer, inputs);\n if (symbolDefault !== null) return { kind: \"input-default\", value: symbolDefault };\n\n return { kind: \"dynamic\", node: initializer };\n}\n\nfunction readRequestInterval(\n call: ts.CallExpression,\n calleeName: \"request.security\" | \"request.lowerTf\",\n sourceFile: ts.SourceFile,\n sourcePath: string,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n diagnostics: CompileDiagnostic[],\n intervals: Set<string>,\n feeds: Map<string, RequestedFeed>,\n): void {\n const opts = call.arguments[0];\n if (opts === undefined || !ts.isObjectLiteralExpression(opts)) return;\n const intervalProperty = opts.properties\n .filter(ts.isPropertyAssignment)\n .find((property) => ts.isIdentifier(property.name) && property.name.text === \"interval\");\n if (intervalProperty === undefined) return;\n\n const resolvedIntervals = resolveIntervals(intervalProperty.initializer, inputs);\n if (resolvedIntervals === null) {\n diagnostics.push(\n createDiagnostic({\n severity: \"error\",\n code:\n calleeName === \"request.lowerTf\"\n ? \"request-lower-tf-interval-not-literal\"\n : \"request-security-interval-not-literal\",\n message: `${calleeName}({ interval }) must be a string literal or input.enum value`,\n file: sourcePath,\n node: intervalProperty.initializer,\n sourceFile,\n }),\n );\n }\n\n // `request.lowerTf` has no symbol dimension: it only ever feeds intervals\n // (the chart-symbol HTF projection), never `feeds`. Preserve its existing\n // interval-only behavior exactly.\n if (calleeName === \"request.lowerTf\") {\n for (const interval of resolvedIntervals ?? []) intervals.add(interval);\n return;\n }\n\n const resolvedSymbols = resolveSymbols(opts, inputs, sourceFile, sourcePath, diagnostics);\n for (const symbol of resolvedSymbols) {\n for (const interval of resolvedIntervals ?? []) {\n // A symbol-omitted (chart-symbol) feed keeps its interval in the\n // main-symbol projection; a present-symbol feed does not.\n if (symbol === undefined) intervals.add(interval);\n feeds.set(feedKey(symbol, interval), {\n ...(symbol === undefined ? {} : { symbol }),\n interval,\n });\n }\n }\n}\n\n/**\n * Resolve a `request.*` `interval` initializer to its concrete interval list —\n * a single-element list for a string literal, all options for an `inputs.<enum>`\n * access — or `null` for a genuinely-dynamic interval (the caller pushes the\n * appropriate diagnostic). `interval` never uses the `input.symbol`-default path:\n * `input.interval` is the main-chart interval, not a feed interval.\n */\nfunction resolveIntervals(\n initializer: ts.Expression,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n): ReadonlyArray<string> | null {\n if (ts.isStringLiteral(initializer)) return [initializer.text];\n return getInputsEnumOptions(initializer, inputs);\n}\n\n/**\n * Resolve a `request.security` opts object's `symbol` axis to the list of\n * requested symbols (`undefined` ⇒ the chart's own symbol): `[undefined]` when\n * absent or an empty literal, `[value]` for a string literal or `input.symbol`\n * default, all options for an `inputs.<enum>` access, or `[]` (excluded, after\n * pushing `request-security-symbol-not-literal`) for a dynamic symbol.\n */\nfunction resolveSymbols(\n opts: ts.ObjectLiteralExpression,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n sourceFile: ts.SourceFile,\n sourcePath: string,\n diagnostics: CompileDiagnostic[],\n): ReadonlyArray<string | undefined> {\n const resolved = resolveOptString(opts, \"symbol\", inputs);\n switch (resolved.kind) {\n case \"absent\":\n return [undefined];\n case \"literal\":\n // An empty-literal symbol collapses to the chart symbol, matching\n // `feedKey`'s empty-collapse.\n return [resolved.value === \"\" ? undefined : resolved.value];\n case \"input-default\":\n return [resolved.value];\n case \"enum\":\n return resolved.values;\n default:\n diagnostics.push(\n createDiagnostic({\n severity: \"error\",\n code: \"request-security-symbol-not-literal\",\n message:\n \"request.security({ symbol }) must be a string literal, an input.symbol default, or an input.enum value\",\n file: sourcePath,\n node: resolved.node,\n sourceFile,\n }),\n );\n return [];\n }\n}\n\n/**\n * Resolve an `inputs.<name>` access whose descriptor is an `input.symbol` to its\n * `defaultValue` string, or `null` when the access is not an `inputs.<name>`\n * property access, the descriptor is missing / not a `symbol` kind, or its\n * `defaultValue` is not a string.\n */\nfunction getInputSymbolDefault(\n expr: ts.Expression,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n): string | null {\n if (\n !ts.isPropertyAccessExpression(expr) ||\n !ts.isIdentifier(expr.expression) ||\n expr.expression.text !== \"inputs\"\n ) {\n return null;\n }\n const descriptor = inputs[expr.name.text];\n if (descriptor === undefined || descriptor.kind !== \"symbol\") return null;\n const defaultValue = descriptor.defaultValue;\n return typeof defaultValue === \"string\" ? defaultValue : null;\n}\n\nfunction getInputsEnumOptions(\n expr: ts.Expression,\n inputs: Readonly<Record<string, ExtractedDescriptor>>,\n): ReadonlyArray<string> | null {\n if (\n !ts.isPropertyAccessExpression(expr) ||\n !ts.isIdentifier(expr.expression) ||\n expr.expression.text !== \"inputs\"\n ) {\n return null;\n }\n const descriptor = inputs[expr.name.text];\n if (descriptor === undefined || descriptor.kind !== \"enum\") return null;\n const options = descriptor.options;\n if (!Array.isArray(options)) return null;\n const strings: string[] = [];\n for (const option of options) {\n if (typeof option !== \"string\") return null;\n strings.push(option);\n }\n return Object.freeze(strings);\n}\n"]}
|
package/dist/analysis/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { runStructuralChecks } from "./structuralChecks.js";
|
|
|
2
2
|
export type { StructuralBindingInfo, StructuralCheckResult } from "./structuralChecks.js";
|
|
3
3
|
export { runForbiddenConstructs } from "./forbiddenConstructs.js";
|
|
4
4
|
export { runStatefulCallInLoop } from "./statefulCallInLoop.js";
|
|
5
|
+
export { MAX_STATE_ARRAY_CAPACITY, runStateArrayCapacity } from "./stateArrayCapacity.js";
|
|
5
6
|
export { extractCapabilities } from "./extractCapabilities.js";
|
|
6
7
|
export { extractMaxLookback } from "./extractMaxLookback.js";
|
|
7
8
|
export type { ExtractMaxLookbackResult } from "./extractMaxLookback.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analysis/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC1F,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AACnG,YAAY,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EACR,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,eAAe,GAClB,MAAM,6BAA6B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analysis/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC1F,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,YAAY,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,YAAY,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AACnG,YAAY,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EAAE,4BAA4B,EAAE,MAAM,6BAA6B,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,YAAY,EACR,gBAAgB,EAChB,QAAQ,EACR,WAAW,EACX,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,eAAe,GAClB,MAAM,6BAA6B,CAAC"}
|
package/dist/analysis/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
export { runStructuralChecks } from "./structuralChecks.js";
|
|
4
4
|
export { runForbiddenConstructs } from "./forbiddenConstructs.js";
|
|
5
5
|
export { runStatefulCallInLoop } from "./statefulCallInLoop.js";
|
|
6
|
+
export { MAX_STATE_ARRAY_CAPACITY, runStateArrayCapacity } from "./stateArrayCapacity.js";
|
|
6
7
|
export { extractCapabilities } from "./extractCapabilities.js";
|
|
7
8
|
export { extractMaxLookback } from "./extractMaxLookback.js";
|
|
8
9
|
export { extractInputs } from "./extractInputs.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/analysis/index.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+DAA+D;AAE/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAEnG,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nexport { runStructuralChecks } from \"./structuralChecks.js\";\nexport type { StructuralBindingInfo, StructuralCheckResult } from \"./structuralChecks.js\";\nexport { runForbiddenConstructs } from \"./forbiddenConstructs.js\";\nexport { runStatefulCallInLoop } from \"./statefulCallInLoop.js\";\nexport { extractCapabilities } from \"./extractCapabilities.js\";\nexport { extractMaxLookback } from \"./extractMaxLookback.js\";\nexport type { ExtractMaxLookbackResult } from \"./extractMaxLookback.js\";\nexport { extractInputs } from \"./extractInputs.js\";\nexport type { ExtractedDescriptor, ExtractInputsResult } from \"./extractInputs.js\";\nexport { extractRequestAnalysis, extractRequestedIntervals } from \"./extractRequestedIntervals.js\";\nexport type { RequestAnalysis } from \"./extractRequestedIntervals.js\";\nexport { validateSecurityExpr } from \"./validateSecurityExpr.js\";\nexport { validateLowerTfIntervals } from \"./validateLowerTfIntervals.js\";\nexport { extractRequiresIntervals } from \"./extractRequiresIntervals.js\";\nexport { extractAlertConditions } from \"./extractAlertConditions.js\";\nexport type { ExtractAlertConditionsResult } from \"./extractAlertConditions.js\";\nexport { extractDependencyGraph } from \"./extractDependencyGraph.js\";\nexport type {\n DepConsumesEntry,\n DepGraph,\n DrawnScript,\n PrivateDep,\n ProducerRef,\n ProducerSnapshot,\n ResolveProducer,\n} from \"./extractDependencyGraph.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/analysis/index.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+DAA+D;AAE/D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAC1F,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,sBAAsB,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAEnG,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nexport { runStructuralChecks } from \"./structuralChecks.js\";\nexport type { StructuralBindingInfo, StructuralCheckResult } from \"./structuralChecks.js\";\nexport { runForbiddenConstructs } from \"./forbiddenConstructs.js\";\nexport { runStatefulCallInLoop } from \"./statefulCallInLoop.js\";\nexport { MAX_STATE_ARRAY_CAPACITY, runStateArrayCapacity } from \"./stateArrayCapacity.js\";\nexport { extractCapabilities } from \"./extractCapabilities.js\";\nexport { extractMaxLookback } from \"./extractMaxLookback.js\";\nexport type { ExtractMaxLookbackResult } from \"./extractMaxLookback.js\";\nexport { extractInputs } from \"./extractInputs.js\";\nexport type { ExtractedDescriptor, ExtractInputsResult } from \"./extractInputs.js\";\nexport { extractRequestAnalysis, extractRequestedIntervals } from \"./extractRequestedIntervals.js\";\nexport type { RequestAnalysis } from \"./extractRequestedIntervals.js\";\nexport { validateSecurityExpr } from \"./validateSecurityExpr.js\";\nexport { validateLowerTfIntervals } from \"./validateLowerTfIntervals.js\";\nexport { extractRequiresIntervals } from \"./extractRequiresIntervals.js\";\nexport { extractAlertConditions } from \"./extractAlertConditions.js\";\nexport type { ExtractAlertConditionsResult } from \"./extractAlertConditions.js\";\nexport { extractDependencyGraph } from \"./extractDependencyGraph.js\";\nexport type {\n DepConsumesEntry,\n DepGraph,\n DrawnScript,\n PrivateDep,\n ProducerRef,\n ProducerSnapshot,\n ResolveProducer,\n} from \"./extractDependencyGraph.js\";\n"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { type CompileDiagnostic } from "../diagnostics.js";
|
|
3
|
+
/**
|
|
4
|
+
* The largest `capacity` a `state.array<T>(capacity)` allocation may request.
|
|
5
|
+
* The collection is bounded-execution-safe only because its size is fixed at
|
|
6
|
+
* compile time: a hard ceiling caps memory, caps the JSON snapshot size, and —
|
|
7
|
+
* crucially — caps the per-tick two-ring `Float64Array` copy the runtime does
|
|
8
|
+
* on every tick rollback. `100_000` is generous (the dominant rolling-window /
|
|
9
|
+
* event-log cases are ≤ a few hundred) while still keeping the snapshot and
|
|
10
|
+
* the per-tick copy bounded.
|
|
11
|
+
*
|
|
12
|
+
* @since 1.3
|
|
13
|
+
* @stable
|
|
14
|
+
* @example
|
|
15
|
+
* // state.array<number>(20) → 20 <= MAX_STATE_ARRAY_CAPACITY (OK)
|
|
16
|
+
* // state.array<number>(1_000_000) → exceeds the cap (error)
|
|
17
|
+
* const cap: number = MAX_STATE_ARRAY_CAPACITY;
|
|
18
|
+
* void cap;
|
|
19
|
+
*/
|
|
20
|
+
export declare const MAX_STATE_ARRAY_CAPACITY = 100000;
|
|
21
|
+
/**
|
|
22
|
+
* Walk the source file and flag every `state.array<T>(capacity)` allocation
|
|
23
|
+
* whose `capacity` argument is not a compile-time-resolvable positive integer
|
|
24
|
+
* within `MAX_STATE_ARRAY_CAPACITY`. This pins the bounded-execution +
|
|
25
|
+
* bounded-snapshot invariant at the compiler boundary: a non-literal capacity
|
|
26
|
+
* would make the backing ring's size — and therefore its snapshot size and
|
|
27
|
+
* per-tick rollback cost — non-deterministic.
|
|
28
|
+
*
|
|
29
|
+
* Capacity resolution reuses `resolveIndexUpperBound` + `collectConstNumberEnv`
|
|
30
|
+
* (the same machinery that sizes a series index), so a bare numeric literal,
|
|
31
|
+
* a parenthesised / unary-`±` literal, and a `const` numeric-literal binding
|
|
32
|
+
* (`const K = 20; state.array(K)`) are all accepted; a `let`, an input, or any
|
|
33
|
+
* runtime expression resolves to `null` and is rejected.
|
|
34
|
+
*
|
|
35
|
+
* Two error codes:
|
|
36
|
+
* - `state-array-capacity-not-literal` — the capacity is missing or does not
|
|
37
|
+
* resolve to a compile-time number.
|
|
38
|
+
* - `state-array-capacity-exceeds-max` — the capacity resolves but is `<= 0`,
|
|
39
|
+
* non-integer, or `> MAX_STATE_ARRAY_CAPACITY`.
|
|
40
|
+
*
|
|
41
|
+
* The walk runs on the **original** AST (positions match the user's source,
|
|
42
|
+
* and — running pre-injection — the capacity is `node.arguments[0]`, before
|
|
43
|
+
* the slot-id literal is injected as the leading argument). The element-access
|
|
44
|
+
* form `state["array"](cap)` is rejected upstream as
|
|
45
|
+
* `stateful-call-element-access` and never matches `"state.array"` here, so it
|
|
46
|
+
* is not double-reported. A `state.array(...)` inside a loop additionally
|
|
47
|
+
* errors `stateful-call-inside-loop`; both passes are independent.
|
|
48
|
+
*
|
|
49
|
+
* @since 1.3
|
|
50
|
+
* @example
|
|
51
|
+
* // const diagnostics = runStateArrayCapacity(
|
|
52
|
+
* // sourceFile, checker, "demo.chart.ts",
|
|
53
|
+
* // );
|
|
54
|
+
* const fn: typeof runStateArrayCapacity = runStateArrayCapacity;
|
|
55
|
+
* void fn;
|
|
56
|
+
*/
|
|
57
|
+
export declare function runStateArrayCapacity(sourceFile: ts.SourceFile, checker: ts.TypeChecker, sourcePath: string): ReadonlyArray<CompileDiagnostic>;
|
|
58
|
+
//# sourceMappingURL=stateArrayCapacity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stateArrayCapacity.d.ts","sourceRoot":"","sources":["../../src/analysis/stateArrayCapacity.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,YAAY,CAAC;AAE5B,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,mBAAmB,CAAC;AAI7E;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,wBAAwB,SAAU,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,qBAAqB,CACjC,UAAU,EAAE,EAAE,CAAC,UAAU,EACzB,OAAO,EAAE,EAAE,CAAC,WAAW,EACvB,UAAU,EAAE,MAAM,GACnB,aAAa,CAAC,iBAAiB,CAAC,CAwDlC"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Copyright (c) 2026 Invinite. Licensed under the MIT License.
|
|
2
|
+
// See the LICENSE file in the repo root for full license text.
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { createDiagnostic } from "../diagnostics.js";
|
|
5
|
+
import { resolveCalleeName } from "../transformers/resolveCallee.js";
|
|
6
|
+
import { collectConstNumberEnv, resolveIndexUpperBound } from "./resolveIndexBound.js";
|
|
7
|
+
/**
|
|
8
|
+
* The largest `capacity` a `state.array<T>(capacity)` allocation may request.
|
|
9
|
+
* The collection is bounded-execution-safe only because its size is fixed at
|
|
10
|
+
* compile time: a hard ceiling caps memory, caps the JSON snapshot size, and —
|
|
11
|
+
* crucially — caps the per-tick two-ring `Float64Array` copy the runtime does
|
|
12
|
+
* on every tick rollback. `100_000` is generous (the dominant rolling-window /
|
|
13
|
+
* event-log cases are ≤ a few hundred) while still keeping the snapshot and
|
|
14
|
+
* the per-tick copy bounded.
|
|
15
|
+
*
|
|
16
|
+
* @since 1.3
|
|
17
|
+
* @stable
|
|
18
|
+
* @example
|
|
19
|
+
* // state.array<number>(20) → 20 <= MAX_STATE_ARRAY_CAPACITY (OK)
|
|
20
|
+
* // state.array<number>(1_000_000) → exceeds the cap (error)
|
|
21
|
+
* const cap: number = MAX_STATE_ARRAY_CAPACITY;
|
|
22
|
+
* void cap;
|
|
23
|
+
*/
|
|
24
|
+
export const MAX_STATE_ARRAY_CAPACITY = 100_000;
|
|
25
|
+
/**
|
|
26
|
+
* Walk the source file and flag every `state.array<T>(capacity)` allocation
|
|
27
|
+
* whose `capacity` argument is not a compile-time-resolvable positive integer
|
|
28
|
+
* within `MAX_STATE_ARRAY_CAPACITY`. This pins the bounded-execution +
|
|
29
|
+
* bounded-snapshot invariant at the compiler boundary: a non-literal capacity
|
|
30
|
+
* would make the backing ring's size — and therefore its snapshot size and
|
|
31
|
+
* per-tick rollback cost — non-deterministic.
|
|
32
|
+
*
|
|
33
|
+
* Capacity resolution reuses `resolveIndexUpperBound` + `collectConstNumberEnv`
|
|
34
|
+
* (the same machinery that sizes a series index), so a bare numeric literal,
|
|
35
|
+
* a parenthesised / unary-`±` literal, and a `const` numeric-literal binding
|
|
36
|
+
* (`const K = 20; state.array(K)`) are all accepted; a `let`, an input, or any
|
|
37
|
+
* runtime expression resolves to `null` and is rejected.
|
|
38
|
+
*
|
|
39
|
+
* Two error codes:
|
|
40
|
+
* - `state-array-capacity-not-literal` — the capacity is missing or does not
|
|
41
|
+
* resolve to a compile-time number.
|
|
42
|
+
* - `state-array-capacity-exceeds-max` — the capacity resolves but is `<= 0`,
|
|
43
|
+
* non-integer, or `> MAX_STATE_ARRAY_CAPACITY`.
|
|
44
|
+
*
|
|
45
|
+
* The walk runs on the **original** AST (positions match the user's source,
|
|
46
|
+
* and — running pre-injection — the capacity is `node.arguments[0]`, before
|
|
47
|
+
* the slot-id literal is injected as the leading argument). The element-access
|
|
48
|
+
* form `state["array"](cap)` is rejected upstream as
|
|
49
|
+
* `stateful-call-element-access` and never matches `"state.array"` here, so it
|
|
50
|
+
* is not double-reported. A `state.array(...)` inside a loop additionally
|
|
51
|
+
* errors `stateful-call-inside-loop`; both passes are independent.
|
|
52
|
+
*
|
|
53
|
+
* @since 1.3
|
|
54
|
+
* @example
|
|
55
|
+
* // const diagnostics = runStateArrayCapacity(
|
|
56
|
+
* // sourceFile, checker, "demo.chart.ts",
|
|
57
|
+
* // );
|
|
58
|
+
* const fn: typeof runStateArrayCapacity = runStateArrayCapacity;
|
|
59
|
+
* void fn;
|
|
60
|
+
*/
|
|
61
|
+
export function runStateArrayCapacity(sourceFile, checker, sourcePath) {
|
|
62
|
+
const diagnostics = [];
|
|
63
|
+
const visit = (node) => {
|
|
64
|
+
if (ts.isCallExpression(node) && resolveCalleeName(node, checker) === "state.array") {
|
|
65
|
+
const capacity = node.arguments[0];
|
|
66
|
+
if (capacity === undefined) {
|
|
67
|
+
diagnostics.push(createDiagnostic({
|
|
68
|
+
severity: "error",
|
|
69
|
+
code: "state-array-capacity-not-literal",
|
|
70
|
+
message: "`state.array` requires a numeric-literal capacity (a `const` numeric binding is accepted).",
|
|
71
|
+
file: sourcePath,
|
|
72
|
+
node,
|
|
73
|
+
sourceFile,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const constEnv = collectConstNumberEnv(capacity, sourceFile);
|
|
78
|
+
const bound = resolveIndexUpperBound(capacity, node, { constEnv, checker });
|
|
79
|
+
if (bound === null) {
|
|
80
|
+
diagnostics.push(createDiagnostic({
|
|
81
|
+
severity: "error",
|
|
82
|
+
code: "state-array-capacity-not-literal",
|
|
83
|
+
message: "`state.array` capacity must be a numeric literal (a `const` numeric binding is accepted), not a runtime value.",
|
|
84
|
+
file: sourcePath,
|
|
85
|
+
node: capacity,
|
|
86
|
+
sourceFile,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
else if (bound <= 0 ||
|
|
90
|
+
!Number.isInteger(bound) ||
|
|
91
|
+
bound > MAX_STATE_ARRAY_CAPACITY) {
|
|
92
|
+
diagnostics.push(createDiagnostic({
|
|
93
|
+
severity: "error",
|
|
94
|
+
code: "state-array-capacity-exceeds-max",
|
|
95
|
+
message: `\`state.array\` capacity must be a positive integer in 1..${MAX_STATE_ARRAY_CAPACITY}; got ${bound}.`,
|
|
96
|
+
file: sourcePath,
|
|
97
|
+
node: capacity,
|
|
98
|
+
sourceFile,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
ts.forEachChild(node, visit);
|
|
104
|
+
};
|
|
105
|
+
ts.forEachChild(sourceFile, visit);
|
|
106
|
+
return Object.freeze(diagnostics.slice());
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=stateArrayCapacity.js.map
|