@intentius/chant 0.19.1 → 0.21.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/dist/build-params.d.ts +108 -0
- package/dist/build-params.d.ts.map +1 -0
- package/dist/build.d.ts +79 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +31 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts +71 -0
- package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
- package/dist/cli/commands/check-lexicon.d.ts +1 -1
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts +27 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lint.d.ts.map +1 -1
- package/dist/cli/handlers/run-client.d.ts +1 -1
- package/dist/cli/handlers/run-client.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +16 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +10 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-sections.d.ts.map +1 -1
- package/dist/codegen/docs-types.d.ts +2 -0
- package/dist/codegen/docs-types.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +12 -5
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts +62 -7
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts +12 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -0
- package/dist/components/sandbox/run.d.ts +42 -0
- package/dist/components/sandbox/run.d.ts.map +1 -0
- package/dist/composite.d.ts +5 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/config.d.ts +71 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/declarable.d.ts +16 -0
- package/dist/declarable.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/entity-wire-codec.d.ts +166 -0
- package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
- package/dist/discovery/entity-wire.d.ts +50 -0
- package/dist/discovery/entity-wire.d.ts.map +1 -0
- package/dist/discovery/fold-import.d.ts +239 -0
- package/dist/discovery/fold-import.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +74 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/bundle.d.ts +18 -0
- package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
- package/dist/discovery/sandbox/child-errors.d.ts +15 -0
- package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
- package/dist/discovery/sandbox/driver.d.ts +13 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -0
- package/dist/discovery/sandbox/run.d.ts +69 -0
- package/dist/discovery/sandbox/run.d.ts.map +1 -0
- package/dist/errors.d.ts +9 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +299 -0
- package/dist/fold/fold.d.ts.map +1 -0
- package/dist/fold/foldable-helpers.d.ts +121 -0
- package/dist/fold/foldable-helpers.d.ts.map +1 -0
- package/dist/fold/subset.d.ts +134 -0
- package/dist/fold/subset.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts +7 -2
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +4 -2
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +131 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +11 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/component-checks.d.ts +7 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
- package/dist/params.d.ts +60 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime.d.ts +10 -1
- package/dist/runtime.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/build-params.test.ts +144 -0
- package/src/build-params.ts +207 -0
- package/src/build.test.ts +38 -0
- package/src/build.ts +144 -7
- package/src/cli/commands/build.test.ts +220 -2
- package/src/cli/commands/build.ts +111 -3
- package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
- package/src/cli/commands/check-lexicon-examples.ts +103 -0
- package/src/cli/commands/check-lexicon-intrinsics.test.ts +179 -0
- package/src/cli/commands/check-lexicon-intrinsics.ts +348 -0
- package/src/cli/commands/check-lexicon.test.ts +34 -0
- package/src/cli/commands/check-lexicon.ts +137 -1
- package/src/cli/commands/lint.ts +31 -3
- package/src/cli/commands/onboard.ts +1 -1
- package/src/cli/component-security-boundary.test.ts +170 -0
- package/src/cli/handlers/build.ts +24 -3
- package/src/cli/handlers/components.ts +9 -2
- package/src/cli/handlers/dev.ts +1 -1
- package/src/cli/handlers/graph.ts +7 -5
- package/src/cli/handlers/lifecycle.ts +2 -2
- package/src/cli/handlers/lint.ts +2 -0
- package/src/cli/handlers/misc.ts +2 -2
- package/src/cli/handlers/run-client.ts +1 -1
- package/src/cli/handlers/run.ts +20 -5
- package/src/cli/main.test.ts +22 -0
- package/src/cli/main.ts +39 -0
- package/src/cli/plugins.ts +20 -4
- package/src/cli/registry.ts +10 -0
- package/src/cli/security-boundary.test.ts +135 -0
- package/src/codegen/docs-sections.test.ts +67 -0
- package/src/codegen/docs-sections.ts +7 -3
- package/src/codegen/docs-types.ts +2 -0
- package/src/components/cli-support.ts +22 -10
- package/src/components/discover.ts +127 -25
- package/src/components/sandbox/driver.ts +114 -0
- package/src/components/sandbox/run.test.ts +185 -0
- package/src/components/sandbox/run.ts +177 -0
- package/src/composite.test.ts +21 -0
- package/src/composite.ts +20 -1
- package/src/config.ts +81 -0
- package/src/declarable.ts +20 -0
- package/src/discovery/collect.ts +17 -3
- package/src/discovery/entity-wire-codec.ts +487 -0
- package/src/discovery/entity-wire.test.ts +240 -0
- package/src/discovery/entity-wire.ts +67 -0
- package/src/discovery/fold-import.test.ts +1598 -0
- package/src/discovery/fold-import.ts +1998 -0
- package/src/discovery/index.test.ts +191 -1
- package/src/discovery/index.ts +242 -1
- package/src/discovery/sandbox/bundle.ts +218 -0
- package/src/discovery/sandbox/child-errors.ts +65 -0
- package/src/discovery/sandbox/driver.ts +147 -0
- package/src/discovery/sandbox/run.test.ts +179 -0
- package/src/discovery/sandbox/run.ts +196 -0
- package/src/errors.ts +9 -1
- package/src/fold/fold.test.ts +812 -0
- package/src/fold/fold.ts +805 -0
- package/src/fold/foldable-helpers.ts +171 -0
- package/src/fold/subset-doc-parity.test.ts +210 -0
- package/src/fold/subset.test.ts +352 -0
- package/src/fold/subset.ts +383 -0
- package/src/index.ts +2 -0
- package/src/lexicon-output.ts +7 -2
- package/src/lexicon-schema.test.ts +57 -0
- package/src/lexicon-schema.ts +8 -1
- package/src/lexicon.ts +132 -1
- package/src/lifecycle/git.test.ts +10 -5
- package/src/lifecycle/release-ledger.test.ts +28 -0
- package/src/lifecycle/release-ledger.ts +11 -0
- package/src/lint/component-checks.ts +8 -1
- package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
- package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
- package/src/params.test.ts +22 -0
- package/src/params.ts +66 -0
- package/src/provenance.ts +22 -0
- package/src/runtime.ts +11 -2
package/src/fold/fold.ts
ADDED
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { intrinsicCallFolds, intrinsicTagFolds, type IntrinsicDef } from "../lexicon";
|
|
3
|
+
import {
|
|
4
|
+
SUPPORTED_BINARY_OPERATORS,
|
|
5
|
+
SUPPORTED_UNARY_OPERATORS,
|
|
6
|
+
UNSUPPORTED_OBJECT_MEMBER_MESSAGE,
|
|
7
|
+
UNSUPPORTED_UNARY_MESSAGE,
|
|
8
|
+
callExpressionMessage,
|
|
9
|
+
computedPropertyNameMessage,
|
|
10
|
+
dynamicElementAccessMessage,
|
|
11
|
+
isLiteralElementKey,
|
|
12
|
+
isLiteralPropertyName,
|
|
13
|
+
unsupportedBinaryMessage,
|
|
14
|
+
unsupportedExpressionMessage,
|
|
15
|
+
type SubsetRuleId,
|
|
16
|
+
} from "./subset";
|
|
17
|
+
import { isFoldableHelperName } from "./foldable-helpers";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* fold — static AST value reducer (chant #1026/#1021/#1024, part of epic #1019)
|
|
21
|
+
*
|
|
22
|
+
* Reduces a single-file TypeScript expression AST to a value with NO
|
|
23
|
+
* module execution. The node-kind/operator/key subset it covers — literals,
|
|
24
|
+
* template interpolation, object/array literals (incl. spread), `const`
|
|
25
|
+
* identifier resolution, property and element access (incl. the
|
|
26
|
+
* cross-resource `{ __attrRef }` case, literal-key-only), unary `!`/`-`,
|
|
27
|
+
* the binary operators `+ - * / === !== > < >= <=`, short-circuit
|
|
28
|
+
* `&& || ??`, conditional expressions, `as`/`satisfies`/`!`/parenthesized
|
|
29
|
+
* unwrapping, a nested `new Type({...})` resource-as-value, and registered
|
|
30
|
+
* lexicon intrinsic tagged templates — is defined ONCE, in {@link "./subset"}
|
|
31
|
+
* ({@link findSubsetViolation}), and shared with EVL001/EVL003
|
|
32
|
+
* ({@link "../lint/rules/evl001-non-literal-expression"}), so the linted
|
|
33
|
+
* subset and the folded subset can never drift apart (#1024).
|
|
34
|
+
*
|
|
35
|
+
* A `CallExpression` has almost no case — a function call as a value is
|
|
36
|
+
* structurally unrepresentable, not merely linted against. Composite
|
|
37
|
+
* factory calls are out of scope here (epic Phase 5, #1023). There are
|
|
38
|
+
* exactly two exceptions, both closed allowlists of names declared
|
|
39
|
+
* somewhere a human had to write them down, and both reducing to a symbolic
|
|
40
|
+
* envelope that executes nothing here:
|
|
41
|
+
*
|
|
42
|
+
* - a call to a REGISTERED chant authoring helper
|
|
43
|
+
* ({@link "./foldable-helpers"}, chant #1082) → {@link FoldedHelperCall};
|
|
44
|
+
* - a call to a lexicon intrinsic whose lexicon registered it AND opted
|
|
45
|
+
* its call form in ({@link intrinsicCallFolds}, ../lexicon.ts, chant
|
|
46
|
+
* #1044) → {@link FoldedIntrinsicCall}, the same `{__intrinsic}` family
|
|
47
|
+
* the tagged-template form already reduces to.
|
|
48
|
+
*
|
|
49
|
+
* Everything else — a user's function, a method call, an array `.map`, a
|
|
50
|
+
* registered name shadowed by a local binding — still throws.
|
|
51
|
+
*
|
|
52
|
+
* Cross-file identifier resolution (chant #1020): `consts` alone is always
|
|
53
|
+
* this file's own top-level bindings — that part stays single-file, and
|
|
54
|
+
* nothing about the supported node-kind subset changes. But an identifier
|
|
55
|
+
* `fold()` can't find in `consts` isn't necessarily a dead end: the optional
|
|
56
|
+
* `externals` map (populated by ../discovery/fold-import.ts, which owns the
|
|
57
|
+
* module graph traversal) lets a caller pre-resolve an *imported* binding to
|
|
58
|
+
* its real value — a plain value for an imported `const`, or the REAL,
|
|
59
|
+
* already-constructed `Declarable`/`CompositeInstance` for a name bound to a
|
|
60
|
+
* resource/composite in the defining module — and `fold()` just returns it
|
|
61
|
+
* for the identifier, unchanged. This is why a bare object/array bracket
|
|
62
|
+
* index a few lines down (`obj[key]`) is enough to make `network.vpc.VpcId`
|
|
63
|
+
* fold correctly once `network` resolves via `externals` to the real,
|
|
64
|
+
* shared composite instance object: indexing a live class instance the same
|
|
65
|
+
* way as a plain object returns its real getter's real `AttrRef`, wired to
|
|
66
|
+
* the real shared parent — see fold-import.ts's module doc for why that
|
|
67
|
+
* shared identity is the entire hard part of #1020.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The result of folding an AST node: a plain value, a symbolic reference to
|
|
72
|
+
* a sibling resource's attribute, a folded intrinsic tagged template, an
|
|
73
|
+
* unresolved external symbol chain, or a folded resource spec.
|
|
74
|
+
*/
|
|
75
|
+
export type FoldedValue =
|
|
76
|
+
| string
|
|
77
|
+
| number
|
|
78
|
+
| boolean
|
|
79
|
+
| null
|
|
80
|
+
| undefined
|
|
81
|
+
| FoldedValue[]
|
|
82
|
+
| { [key: string]: FoldedValue }
|
|
83
|
+
| AttrRefValue
|
|
84
|
+
| FoldedIntrinsic
|
|
85
|
+
| FoldedHelperCall
|
|
86
|
+
| SymbolicValue
|
|
87
|
+
| FoldedResource;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Symbolic reference produced when a property/element access resolves to an
|
|
91
|
+
* attribute of another `const`-declared resource in the same file, e.g.
|
|
92
|
+
* `bucket.name` (or `bucket["name"]`) where `bucket` is
|
|
93
|
+
* `const bucket = new S3Bucket({...})`.
|
|
94
|
+
*
|
|
95
|
+
* This is the SAME envelope `AttrRef.prototype.toJSON()` produces at
|
|
96
|
+
* runtime ({@link "../attrref"}) — `serializer-walker.ts`'s `walkValue`
|
|
97
|
+
* already recognizes a plain `{ __attrRef }` object as an AttrRef envelope
|
|
98
|
+
* (it doesn't require a live `AttrRef` instance), so this is not an
|
|
99
|
+
* invented shape: it's the existing envelope, produced without running the
|
|
100
|
+
* module that would otherwise construct the real `AttrRef`.
|
|
101
|
+
*/
|
|
102
|
+
export interface AttrRefValue {
|
|
103
|
+
__attrRef: { entity: string; attribute: string };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The result of folding a registered lexicon intrinsic tagged template
|
|
108
|
+
* (e.g. `Sub\`${AWS.StackName}-x\``) to its node form: tag name, cooked
|
|
109
|
+
* template string parts, and folded interpolated values (in order).
|
|
110
|
+
* Mirrors the runtime call shape `Tag(strings, ...values)` so a later
|
|
111
|
+
* build path can replay it into the real intrinsic object (#1022).
|
|
112
|
+
*/
|
|
113
|
+
export type FoldedIntrinsic = FoldedIntrinsicTag | FoldedIntrinsicCall;
|
|
114
|
+
|
|
115
|
+
/** The tagged-template form: `Sub\`${x}-y\`` — see {@link FoldedIntrinsic}. */
|
|
116
|
+
export interface FoldedIntrinsicTag {
|
|
117
|
+
__intrinsic: string;
|
|
118
|
+
strings: string[];
|
|
119
|
+
values: FoldedValue[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The plain-call form of a registered, opted-in lexicon intrinsic
|
|
124
|
+
* (chant #1044) — `Ref(bucket)`, `Concat("a", b)`, `GetAtt("Fn", "Arn")`.
|
|
125
|
+
* Same `__intrinsic` key as the tagged-template form, so the same revival
|
|
126
|
+
* branch handles both and nothing downstream learns a new envelope; the
|
|
127
|
+
* payload differs because the call shape does — positional `args` mirroring
|
|
128
|
+
* `Name(...args)`, where the tag form mirrors `Name(strings, ...values)`.
|
|
129
|
+
*
|
|
130
|
+
* Symbolic, exactly like the tag form: `fold()` executes nothing, it only
|
|
131
|
+
* records that a registered intrinsic was called and with what. The real
|
|
132
|
+
* function is resolved through the folding file's own imports and invoked by
|
|
133
|
+
* `../discovery/fold-import.ts`'s `reviveFoldedValue`.
|
|
134
|
+
*/
|
|
135
|
+
export interface FoldedIntrinsicCall {
|
|
136
|
+
__intrinsic: string;
|
|
137
|
+
args: FoldedValue[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The result of folding a call to a registered chant authoring helper
|
|
142
|
+
* (chant #1082) — e.g. `phase("Apply", [...])`, `output(ref, "oX")`. Holds
|
|
143
|
+
* the helper's name and its folded arguments, in source order.
|
|
144
|
+
*
|
|
145
|
+
* Symbolic, exactly like {@link FoldedIntrinsic}: `fold()` executes nothing,
|
|
146
|
+
* it only records that a registered helper was called and with what. The real
|
|
147
|
+
* function is resolved through the folding file's own imports and invoked by
|
|
148
|
+
* `../discovery/fold-import.ts`'s `reviveFoldedValue`, which is also where
|
|
149
|
+
* the "is this name actually bound to chant's own helper" check lives. See
|
|
150
|
+
* {@link "./foldable-helpers"} for the allowlist and why it is closed.
|
|
151
|
+
*/
|
|
152
|
+
export interface FoldedHelperCall {
|
|
153
|
+
__helper: string;
|
|
154
|
+
args: FoldedValue[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A sub-expression inside a folded intrinsic that fold could not reduce to
|
|
159
|
+
* a value without resolving an identifier from outside this file — e.g. an
|
|
160
|
+
* imported pseudo-parameter namespace access like `AWS.StackName`.
|
|
161
|
+
* Cross-file import resolution is #1020; until then the raw source text is
|
|
162
|
+
* preserved verbatim (never stringified, never rejected) instead of being
|
|
163
|
+
* treated as an unresolved-identifier error. Only appears inside a folded
|
|
164
|
+
* intrinsic's `values` — see {@link foldIntrinsicValue}.
|
|
165
|
+
*/
|
|
166
|
+
export interface SymbolicValue {
|
|
167
|
+
__symbol: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The result of folding a resource constructor: `new Type({ ...props })`.
|
|
172
|
+
*/
|
|
173
|
+
export interface FoldedResource {
|
|
174
|
+
__resource: string;
|
|
175
|
+
props: { [key: string]: FoldedValue };
|
|
176
|
+
/**
|
|
177
|
+
* The constructor's optional second argument — CFN-style resource-level
|
|
178
|
+
* attributes (`DependsOn`, `Condition`, `DeletionPolicy`,
|
|
179
|
+
* `UpdateReplacePolicy`, `CreationPolicy`, `Metadata`, …) some lexicons
|
|
180
|
+
* accept alongside `props` (see `createResource`'s `attributes` param,
|
|
181
|
+
* ../runtime.ts). Present only when the source actually passed one.
|
|
182
|
+
*/
|
|
183
|
+
attributes?: { [key: string]: FoldedValue };
|
|
184
|
+
/**
|
|
185
|
+
* chant #1082 — every constructor argument, folded, in source order. Present
|
|
186
|
+
* only when the argument list is NOT the classic `(props)` / `(props,
|
|
187
|
+
* attributes)` shape — most often because the props object isn't first
|
|
188
|
+
* (`new Parameter("String", {...})`, whose signature is `(type, props)`).
|
|
189
|
+
*
|
|
190
|
+
* When present this is authoritative: the entity is constructed by spreading
|
|
191
|
+
* it, so the constructor receives exactly what the source wrote. `props`
|
|
192
|
+
* alongside it is the first object-literal argument, reported for readers,
|
|
193
|
+
* never re-passed (which would double-count it).
|
|
194
|
+
*/
|
|
195
|
+
args?: FoldedValue[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* One entry per exported `const` resource declaration in {@link foldModule}'s
|
|
200
|
+
* result. The `ok: false` case surfaces the same located, rule-id-tagged
|
|
201
|
+
* shape as {@link FoldError} (#1024) — `error` stays the formatted message
|
|
202
|
+
* string for backward-compat display, while `ruleId`/`line`/`column` let a
|
|
203
|
+
* caller cite the exact same rule id + position an EVL diagnostic for the
|
|
204
|
+
* same construct would.
|
|
205
|
+
*/
|
|
206
|
+
export type FoldModuleEntry =
|
|
207
|
+
| { ok: true; spec: FoldedResource }
|
|
208
|
+
| { ok: false; error: string; ruleId: SubsetRuleId; line: number; column: number };
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Error thrown when a node cannot be folded to a value without executing
|
|
212
|
+
* code. Carries the node's source position (1-based, matching `LintError`)
|
|
213
|
+
* so callers can report a located diagnostic, and the id of the EVL rule
|
|
214
|
+
* that flags the same construct (#1024) — "EVL001" (the general
|
|
215
|
+
* not-statically-evaluable umbrella) unless the rejection is specifically a
|
|
216
|
+
* dynamic element-access key, which is "EVL003"'s construct. A rejection
|
|
217
|
+
* with no EVL equivalent (unresolved identifier, unregistered intrinsic tag,
|
|
218
|
+
* spread of a value that turns out not to be an object/array — all
|
|
219
|
+
* environment/value-dependent, see {@link "./subset"}'s module doc) still
|
|
220
|
+
* defaults to "EVL001" since that's the closest umbrella rule, even though
|
|
221
|
+
* EVL can't actually detect it ahead of a real fold.
|
|
222
|
+
*
|
|
223
|
+
* chant #1020 hang fix — every `FoldError` is thrown for a routine, EXPECTED
|
|
224
|
+
* outcome (this node's shape isn't in the fold subset) and is ALWAYS caught
|
|
225
|
+
* a few frames up (`tryFoldFileCore`'s own top-level catch, ultimately),
|
|
226
|
+
* reduced to `.message`; `.stack` is never read anywhere on this path. V8
|
|
227
|
+
* still eagerly walks live JS frames to populate the (lazy) `.stack` getter's
|
|
228
|
+
* backing data at CONSTRUCTION time regardless of whether it's ever read —
|
|
229
|
+
* cheap for a shallow call stack, but expensive once the surrounding
|
|
230
|
+
* functions are hot enough for V8 to aggressively inline them (every corpus
|
|
231
|
+
* entry re-triggers the same call shapes across `foldFileMemoized` ->
|
|
232
|
+
* `buildExternals` -> `tryFoldFileCore` -> `resolveDeclaratorValue` ->
|
|
233
|
+
* `resolveLiveValue` -> `resolveCallExpression`/`fold`, chant #1020's
|
|
234
|
+
* cross-file resolution making that chain several layers deeper than the
|
|
235
|
+
* pre-#1020 single-file fold ever needed): capturing a stack from deep,
|
|
236
|
+
* optimized/inlined frames requires V8 to reconstruct them from deopt
|
|
237
|
+
* metadata, confirmed via `sample` to dominate CPU during the observed
|
|
238
|
+
* multi-minute stall (`Isolate::CaptureAndSetErrorStack` /
|
|
239
|
+
* `OptimizedJSFrame::Summarize` / `DeoptTranslationIterator`). Most files in
|
|
240
|
+
* the corpus (77/98 entries have at least one run-fallback file) throw one
|
|
241
|
+
* of these, so the cost compounds across a build. `Error.stackTraceLimit = 0`
|
|
242
|
+
* for the duration of `super()` makes V8 capture zero frames — free
|
|
243
|
+
* regardless of stack depth/optimization state — then the limit is restored
|
|
244
|
+
* immediately, so it doesn't suppress a real stack trace anywhere else in
|
|
245
|
+
* the process.
|
|
246
|
+
*/
|
|
247
|
+
export class FoldError extends Error {
|
|
248
|
+
readonly line: number;
|
|
249
|
+
readonly column: number;
|
|
250
|
+
readonly ruleId: SubsetRuleId;
|
|
251
|
+
|
|
252
|
+
constructor(message: string, line: number, column: number, ruleId: SubsetRuleId = "EVL001") {
|
|
253
|
+
const prevStackTraceLimit = Error.stackTraceLimit;
|
|
254
|
+
Error.stackTraceLimit = 0;
|
|
255
|
+
super(`${line}:${column} - ${message}`);
|
|
256
|
+
Error.stackTraceLimit = prevStackTraceLimit;
|
|
257
|
+
this.name = "FoldError";
|
|
258
|
+
this.line = line;
|
|
259
|
+
this.column = column;
|
|
260
|
+
this.ruleId = ruleId;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Resolve a node's 1-based line/column via its owning `SourceFile`. Exported
|
|
266
|
+
* (chant #1020) so fold-import.ts can build its own located `FoldError`s
|
|
267
|
+
* (e.g. an import-cycle diagnostic pointing at the specific `import`
|
|
268
|
+
* statement that closes the cycle) using the exact same position math
|
|
269
|
+
* `foldError` uses here, rather than a second hand-rolled implementation.
|
|
270
|
+
*/
|
|
271
|
+
export function locate(node: ts.Node): { line: number; column: number } {
|
|
272
|
+
const sourceFile = node.getSourceFile();
|
|
273
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
274
|
+
return { line: line + 1, column: character + 1 };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function foldError(node: ts.Node, message: string, ruleId: SubsetRuleId = "EVL001"): FoldError {
|
|
278
|
+
const { line, column } = locate(node);
|
|
279
|
+
return new FoldError(message, line, column, ruleId);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Collect every top-level `const x = <initializer>` in a source file into a
|
|
284
|
+
* name -> initializer map. Single-file only (cross-file is #1020).
|
|
285
|
+
*/
|
|
286
|
+
export function collectConsts(sourceFile: ts.SourceFile): Map<string, ts.Expression> {
|
|
287
|
+
const consts = new Map<string, ts.Expression>();
|
|
288
|
+
for (const statement of sourceFile.statements) {
|
|
289
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
290
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
|
291
|
+
for (const decl of statement.declarationList.declarations) {
|
|
292
|
+
if (ts.isIdentifier(decl.name) && decl.initializer) {
|
|
293
|
+
consts.set(decl.name.text, decl.initializer);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return consts;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** A property/element key foldable without execution: identifier, string, or numeric literal. */
|
|
301
|
+
function propName(node: ts.PropertyName): string {
|
|
302
|
+
if (isLiteralPropertyName(node)) return node.text;
|
|
303
|
+
throw foldError(node, computedPropertyNameMessage(node));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* An element-access key foldable without execution: a string or numeric
|
|
308
|
+
* LITERAL only (EVL003 semantics — a variable or expression key is a
|
|
309
|
+
* dynamic key and is rejected).
|
|
310
|
+
*/
|
|
311
|
+
function elementKey(node: ts.Expression): string {
|
|
312
|
+
if (isLiteralElementKey(node)) return node.text;
|
|
313
|
+
throw foldError(node, dynamicElementAccessMessage(node), "EVL003");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** True when `ident` is a `const` bound to a `new Type(...)` resource constructor. */
|
|
317
|
+
function resolvesToResource(consts: Map<string, ts.Expression>, ident: ts.Identifier): boolean {
|
|
318
|
+
const init = consts.get(ident.text);
|
|
319
|
+
return init !== undefined && ts.isNewExpression(init);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* True when `node` is an identifier, or a dotted/bracketed access chain
|
|
324
|
+
* rooted at an identifier, that neither `consts` nor `externals` can resolve
|
|
325
|
+
* — e.g. `AWS.StackName` from an imported pseudo-parameter namespace inside
|
|
326
|
+
* a lexicon package (still #1063). Nothing here can say what it refers to,
|
|
327
|
+
* so an intrinsic's interior keeps it symbolically rather than rejecting it.
|
|
328
|
+
*
|
|
329
|
+
* `externals` (chant #1020) is consulted so a root that fold CAN resolve is
|
|
330
|
+
* not treated as unresolved: `Ref(environment)`, where `environment` is a
|
|
331
|
+
* `Parameter` imported from a sibling project file, must fold to the REAL,
|
|
332
|
+
* already-constructed Declarable the fold session made for that file, not to
|
|
333
|
+
* a `{__symbol}` the bridge later re-imports — re-importing the defining
|
|
334
|
+
* module builds a second, differently-identified instance of the same
|
|
335
|
+
* resource, which is exactly the shared-identity property #1020 exists to
|
|
336
|
+
* preserve (see fold-import.ts's module doc).
|
|
337
|
+
*/
|
|
338
|
+
function isUnresolvedSymbolChain(
|
|
339
|
+
node: ts.Expression,
|
|
340
|
+
consts: Map<string, ts.Expression>,
|
|
341
|
+
externals?: ReadonlyMap<string, unknown>,
|
|
342
|
+
): boolean {
|
|
343
|
+
if (ts.isIdentifier(node)) {
|
|
344
|
+
return node.text !== "undefined" && !consts.has(node.text) && !externals?.has(node.text);
|
|
345
|
+
}
|
|
346
|
+
if (ts.isPropertyAccessExpression(node)) return isUnresolvedSymbolChain(node.expression, consts, externals);
|
|
347
|
+
if (ts.isElementAccessExpression(node)) return isUnresolvedSymbolChain(node.expression, consts, externals);
|
|
348
|
+
if (ts.isNonNullExpression(node)) return isUnresolvedSymbolChain(node.expression, consts, externals);
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Fold one sub-expression of a registered intrinsic's interior — an
|
|
354
|
+
* interpolation of its tagged-template form, or (chant #1044) an argument of
|
|
355
|
+
* its plain-call form. Identical to {@link fold}, except a symbol chain
|
|
356
|
+
* nothing can resolve (a pseudo-parameter-style access into a lexicon
|
|
357
|
+
* package, `AWS.StackName`) folds to a {@link SymbolicValue} instead of
|
|
358
|
+
* throwing — the run path resolves it once the module actually imports and
|
|
359
|
+
* runs; fold preserves it symbolically rather than stringifying or rejecting
|
|
360
|
+
* it, and fold-import.ts's `resolveSymbolicValue` resolves it for real
|
|
361
|
+
* before the intrinsic is constructed.
|
|
362
|
+
*
|
|
363
|
+
* `externals` (chant #1020) takes precedence over the symbolic path: see
|
|
364
|
+
* {@link isUnresolvedSymbolChain} for why an already-resolved cross-file
|
|
365
|
+
* binding must reach the intrinsic as the real, shared object rather than as
|
|
366
|
+
* a symbol the bridge re-imports.
|
|
367
|
+
*/
|
|
368
|
+
function foldIntrinsicValue(
|
|
369
|
+
node: ts.Expression,
|
|
370
|
+
consts: Map<string, ts.Expression>,
|
|
371
|
+
intrinsics: readonly IntrinsicDef[],
|
|
372
|
+
externals?: ReadonlyMap<string, unknown>,
|
|
373
|
+
): FoldedValue {
|
|
374
|
+
if (isUnresolvedSymbolChain(node, consts, externals)) {
|
|
375
|
+
return { __symbol: node.getText() };
|
|
376
|
+
}
|
|
377
|
+
return fold(node, consts, intrinsics, externals);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Fold a `TaggedTemplateExpression` whose tag is a registered, foldable
|
|
382
|
+
* lexicon intrinsic ({@link intrinsicTagFolds}, `../lexicon.ts`) to its node
|
|
383
|
+
* form. An unregistered — or registered-but-not-foldable — tag throws a
|
|
384
|
+
* located {@link FoldError}.
|
|
385
|
+
*
|
|
386
|
+
* Checks the TAG-form predicate specifically (chant #1044): an intrinsic
|
|
387
|
+
* whose lexicon opted its plain-call form in is not thereby usable as a
|
|
388
|
+
* tagged template, and `` Ref`...` `` stays a rejection.
|
|
389
|
+
*/
|
|
390
|
+
function foldTaggedTemplate(
|
|
391
|
+
node: ts.TaggedTemplateExpression,
|
|
392
|
+
consts: Map<string, ts.Expression>,
|
|
393
|
+
intrinsics: readonly IntrinsicDef[],
|
|
394
|
+
externals?: ReadonlyMap<string, unknown>,
|
|
395
|
+
): FoldedIntrinsic {
|
|
396
|
+
const tagName = node.tag.getText();
|
|
397
|
+
const isRegistered = intrinsics.some((i) => i.name === tagName && intrinsicTagFolds(i));
|
|
398
|
+
if (!isRegistered) {
|
|
399
|
+
throw foldError(node, `unregistered tagged template intrinsic: ${tagName}\`...\``);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const template = node.template;
|
|
403
|
+
if (ts.isNoSubstitutionTemplateLiteral(template)) {
|
|
404
|
+
return { __intrinsic: tagName, strings: [template.text], values: [] };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const strings = [template.head.text, ...template.templateSpans.map((span) => span.literal.text)];
|
|
408
|
+
const values = template.templateSpans.map((span) =>
|
|
409
|
+
foldIntrinsicValue(span.expression, consts, intrinsics, externals),
|
|
410
|
+
);
|
|
411
|
+
return { __intrinsic: tagName, strings, values };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Fold a single expression node to a value. Throws {@link FoldError} for
|
|
416
|
+
* anything outside the supported subset — including any `CallExpression`
|
|
417
|
+
* that is neither a registered chant authoring helper nor a registered,
|
|
418
|
+
* call-form-opted-in lexicon intrinsic (see the module doc).
|
|
419
|
+
*
|
|
420
|
+
* @param intrinsics - The active lexicons' registered intrinsics. A tagged
|
|
421
|
+
* template whose tag isn't in this list, or is in it without
|
|
422
|
+
* {@link intrinsicTagFolds}, is rejected; a plain call is rejected unless
|
|
423
|
+
* its callee is in this list with {@link intrinsicCallFolds} (chant
|
|
424
|
+
* #1044). Defaults to none — pass the target lexicon's manifest
|
|
425
|
+
* `intrinsics` to recognize either form.
|
|
426
|
+
* @param externals - chant #1020: pre-resolved imported bindings, consulted
|
|
427
|
+
* only when an identifier isn't in `consts`. See the module doc above.
|
|
428
|
+
* `undefined` (the default) preserves the exact pre-#1020 single-file
|
|
429
|
+
* behavior — every identifier not in `consts` is unresolved.
|
|
430
|
+
*/
|
|
431
|
+
export function fold(
|
|
432
|
+
node: ts.Expression,
|
|
433
|
+
consts: Map<string, ts.Expression>,
|
|
434
|
+
intrinsics: readonly IntrinsicDef[] = [],
|
|
435
|
+
externals?: ReadonlyMap<string, unknown>,
|
|
436
|
+
): FoldedValue {
|
|
437
|
+
if (
|
|
438
|
+
ts.isParenthesizedExpression(node) ||
|
|
439
|
+
ts.isAsExpression(node) ||
|
|
440
|
+
ts.isSatisfiesExpression(node) ||
|
|
441
|
+
ts.isNonNullExpression(node)
|
|
442
|
+
) {
|
|
443
|
+
return fold(node.expression, consts, intrinsics, externals);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
447
|
+
return node.text;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (ts.isNumericLiteral(node)) {
|
|
451
|
+
return Number(node.text);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
455
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
456
|
+
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
457
|
+
|
|
458
|
+
if (ts.isIdentifier(node) && node.text === "undefined") return undefined;
|
|
459
|
+
|
|
460
|
+
if (ts.isTaggedTemplateExpression(node)) {
|
|
461
|
+
return foldTaggedTemplate(node, consts, intrinsics, externals);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (ts.isTemplateExpression(node)) {
|
|
465
|
+
let out = node.head.text;
|
|
466
|
+
for (const span of node.templateSpans) {
|
|
467
|
+
out += String(fold(span.expression, consts, intrinsics, externals)) + span.literal.text;
|
|
468
|
+
}
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
473
|
+
const obj: { [key: string]: FoldedValue } = {};
|
|
474
|
+
for (const prop of node.properties) {
|
|
475
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
476
|
+
obj[propName(prop.name)] = fold(prop.initializer, consts, intrinsics, externals);
|
|
477
|
+
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
478
|
+
obj[prop.name.text] = fold(prop.name, consts, intrinsics, externals);
|
|
479
|
+
} else if (ts.isSpreadAssignment(prop)) {
|
|
480
|
+
const src = fold(prop.expression, consts, intrinsics, externals);
|
|
481
|
+
if (src === null || typeof src !== "object") {
|
|
482
|
+
throw foldError(prop, "spread source not an object");
|
|
483
|
+
}
|
|
484
|
+
Object.assign(obj, src);
|
|
485
|
+
} else {
|
|
486
|
+
throw foldError(prop, UNSUPPORTED_OBJECT_MEMBER_MESSAGE);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return obj;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
493
|
+
const arr: FoldedValue[] = [];
|
|
494
|
+
for (const el of node.elements) {
|
|
495
|
+
if (ts.isSpreadElement(el)) {
|
|
496
|
+
const src = fold(el.expression, consts, intrinsics, externals);
|
|
497
|
+
if (!Array.isArray(src)) {
|
|
498
|
+
throw foldError(el, "spread source not an array");
|
|
499
|
+
}
|
|
500
|
+
arr.push(...src);
|
|
501
|
+
} else {
|
|
502
|
+
arr.push(fold(el, consts, intrinsics, externals));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return arr;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (ts.isIdentifier(node)) {
|
|
509
|
+
if (!consts.has(node.text)) {
|
|
510
|
+
// chant #1020 — an imported binding fold-import.ts already resolved
|
|
511
|
+
// (to a plain value, or the real live Declarable/CompositeInstance a
|
|
512
|
+
// sibling file's own fold produced) is returned as-is: for a plain
|
|
513
|
+
// value this is exactly like resolving a local const; for a real live
|
|
514
|
+
// object, the property-access branches below just index it like any
|
|
515
|
+
// other object, which is what makes a real, correctly-identified
|
|
516
|
+
// `AttrRef` fall out of `network.vpc.VpcId` with zero special-casing
|
|
517
|
+
// here (see fold-import.ts's module doc).
|
|
518
|
+
if (externals?.has(node.text)) {
|
|
519
|
+
return externals.get(node.text) as FoldedValue;
|
|
520
|
+
}
|
|
521
|
+
// chant #1064 — a bare `process` reference is ALWAYS an ambient
|
|
522
|
+
// environment read (`process.env.X`, `process.argv`, …), never
|
|
523
|
+
// something a future resolution pass could fold: it's Node's global,
|
|
524
|
+
// not a local const or an importable module export. Point at the
|
|
525
|
+
// supported alternative instead of leaving the author to infer the fix
|
|
526
|
+
// from the generic "unresolved identifier" message — see
|
|
527
|
+
// ../build-params.ts/../params.ts.
|
|
528
|
+
if (node.text === "process") {
|
|
529
|
+
throw foldError(
|
|
530
|
+
node,
|
|
531
|
+
`ambient "process" read is not foldable — declare a build-time parameter instead ` +
|
|
532
|
+
`(chant.config.ts's buildParams + \`chant build --param name=value\`/\`--params-file\`) and reference ` +
|
|
533
|
+
`it via \`import { params } from "@intentius/chant/params"\`, rather than reading process.env directly`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
throw foldError(node, `unresolved identifier: ${node.text}`);
|
|
537
|
+
}
|
|
538
|
+
return fold(consts.get(node.text) as ts.Expression, consts, intrinsics, externals);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
542
|
+
if (ts.isIdentifier(node.expression) && resolvesToResource(consts, node.expression)) {
|
|
543
|
+
return { __attrRef: { entity: node.expression.text, attribute: node.name.text } };
|
|
544
|
+
}
|
|
545
|
+
const obj = fold(node.expression, consts, intrinsics, externals);
|
|
546
|
+
if (obj === null || obj === undefined) return undefined;
|
|
547
|
+
return (obj as { [key: string]: FoldedValue })[node.name.text];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (ts.isElementAccessExpression(node)) {
|
|
551
|
+
const key = elementKey(node.argumentExpression);
|
|
552
|
+
if (ts.isIdentifier(node.expression) && resolvesToResource(consts, node.expression)) {
|
|
553
|
+
return { __attrRef: { entity: node.expression.text, attribute: key } };
|
|
554
|
+
}
|
|
555
|
+
const obj = fold(node.expression, consts, intrinsics, externals);
|
|
556
|
+
if (obj === null || obj === undefined) return undefined;
|
|
557
|
+
return (obj as { [key: string]: FoldedValue })[key];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (ts.isPrefixUnaryExpression(node)) {
|
|
561
|
+
if (!SUPPORTED_UNARY_OPERATORS.has(node.operator)) {
|
|
562
|
+
throw foldError(node, UNSUPPORTED_UNARY_MESSAGE);
|
|
563
|
+
}
|
|
564
|
+
const value = fold(node.operand, consts, intrinsics, externals);
|
|
565
|
+
if (node.operator === ts.SyntaxKind.ExclamationToken) return !value;
|
|
566
|
+
return -(value as unknown as number); // ts.SyntaxKind.MinusToken — the only other supported operator
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (ts.isBinaryExpression(node)) {
|
|
570
|
+
const opKind = node.operatorToken.kind;
|
|
571
|
+
const S = ts.SyntaxKind;
|
|
572
|
+
|
|
573
|
+
if (opKind === S.AmpersandAmpersandToken) {
|
|
574
|
+
const left = fold(node.left, consts, intrinsics, externals);
|
|
575
|
+
return left ? fold(node.right, consts, intrinsics, externals) : left;
|
|
576
|
+
}
|
|
577
|
+
if (opKind === S.BarBarToken) {
|
|
578
|
+
const left = fold(node.left, consts, intrinsics, externals);
|
|
579
|
+
return left ? left : fold(node.right, consts, intrinsics, externals);
|
|
580
|
+
}
|
|
581
|
+
if (opKind === S.QuestionQuestionToken) {
|
|
582
|
+
const left = fold(node.left, consts, intrinsics, externals);
|
|
583
|
+
return left === null || left === undefined ? fold(node.right, consts, intrinsics, externals) : left;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (!SUPPORTED_BINARY_OPERATORS.has(opKind)) {
|
|
587
|
+
throw foldError(node, unsupportedBinaryMessage(opKind));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const left = fold(node.left, consts, intrinsics, externals);
|
|
591
|
+
const right = fold(node.right, consts, intrinsics, externals);
|
|
592
|
+
switch (opKind) {
|
|
593
|
+
case S.PlusToken:
|
|
594
|
+
return (left as unknown as string) + (right as unknown as string);
|
|
595
|
+
case S.MinusToken:
|
|
596
|
+
return (left as unknown as number) - (right as unknown as number);
|
|
597
|
+
case S.AsteriskToken:
|
|
598
|
+
return (left as unknown as number) * (right as unknown as number);
|
|
599
|
+
case S.SlashToken:
|
|
600
|
+
return (left as unknown as number) / (right as unknown as number);
|
|
601
|
+
case S.EqualsEqualsEqualsToken:
|
|
602
|
+
return left === right;
|
|
603
|
+
case S.ExclamationEqualsEqualsToken:
|
|
604
|
+
return left !== right;
|
|
605
|
+
case S.GreaterThanToken:
|
|
606
|
+
return (left as unknown as string) > (right as unknown as string);
|
|
607
|
+
case S.LessThanToken:
|
|
608
|
+
return (left as unknown as string) < (right as unknown as string);
|
|
609
|
+
case S.GreaterThanEqualsToken:
|
|
610
|
+
return (left as unknown as string) >= (right as unknown as string);
|
|
611
|
+
case S.LessThanEqualsToken:
|
|
612
|
+
return (left as unknown as string) <= (right as unknown as string);
|
|
613
|
+
default:
|
|
614
|
+
// Unreachable given the SUPPORTED_BINARY_OPERATORS guard above —
|
|
615
|
+
// kept as a defensive fallback in case that set and this switch
|
|
616
|
+
// ever fall out of sync.
|
|
617
|
+
throw foldError(node, unsupportedBinaryMessage(opKind));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (ts.isConditionalExpression(node)) {
|
|
622
|
+
return fold(node.condition, consts, intrinsics, externals)
|
|
623
|
+
? fold(node.whenTrue, consts, intrinsics, externals)
|
|
624
|
+
: fold(node.whenFalse, consts, intrinsics, externals);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (ts.isNewExpression(node)) {
|
|
628
|
+
// A nested `new Type({...})` used as a property VALUE is not leaf-foldable.
|
|
629
|
+
// fold can only produce the {__resource, props} envelope, and — unlike a
|
|
630
|
+
// TOP-LEVEL resource, which fold-import constructs into a real Declarable —
|
|
631
|
+
// a nested one is never constructed, so the envelope leaks into serialization
|
|
632
|
+
// as the wrong value (real fold-vs-run drift; the #1025 differential caught
|
|
633
|
+
// this on gitlab/multi-stage-deploy, where `new Image({...})` as a job's
|
|
634
|
+
// `image:` must serialize as `{ name }`, not `{ __resource, props }`).
|
|
635
|
+
// Reject so the file falls back to run, which constructs and serializes it
|
|
636
|
+
// correctly. EVL permits this statically — it's a documented fold/EVL
|
|
637
|
+
// divergence, like identifier resolution and spread runtime type.
|
|
638
|
+
throw foldError(node, `nested \`new ${node.expression.getText()}(...)\` as a value is not foldable — falls back to run`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (ts.isCallExpression(node)) {
|
|
642
|
+
// chant #1082 — the ONE call shape that folds: a registered chant
|
|
643
|
+
// authoring helper ({@link FOLDABLE_AUTHORING_HELPERS}), called through a
|
|
644
|
+
// bare identifier that this file hasn't shadowed with its own `const`.
|
|
645
|
+
// Nothing is executed here — the call reduces to a symbolic
|
|
646
|
+
// {@link FoldedHelperCall} envelope, exactly as a registered intrinsic
|
|
647
|
+
// tagged template reduces to a {@link FoldedIntrinsic} one, and for the
|
|
648
|
+
// same reason: the real function lives in another module, and resolving
|
|
649
|
+
// + invoking it is the async bridge's job (../discovery/fold-import.ts's
|
|
650
|
+
// `reviveFoldedValue`), which also verifies the name is actually bound to
|
|
651
|
+
// an import of chant's own before invoking anything. Every other call —
|
|
652
|
+
// a user's function, a method call, a call to something declared in this
|
|
653
|
+
// file — still has no case and throws, unchanged.
|
|
654
|
+
if (
|
|
655
|
+
ts.isIdentifier(node.expression) &&
|
|
656
|
+
isFoldableHelperName(node.expression.text) &&
|
|
657
|
+
!consts.has(node.expression.text)
|
|
658
|
+
) {
|
|
659
|
+
return {
|
|
660
|
+
__helper: node.expression.text,
|
|
661
|
+
args: node.arguments.map((arg) => fold(arg, consts, intrinsics, externals)),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// chant #1044 — the other call shape that folds: a lexicon intrinsic in
|
|
666
|
+
// PLAIN-CALL form (`Ref(bucket)`, `Concat("a", b)`), where that lexicon
|
|
667
|
+
// registered it AND opted its call form in ({@link intrinsicCallFolds},
|
|
668
|
+
// ../lexicon.ts — default off, never inferred). Reduces to the same
|
|
669
|
+
// `{__intrinsic}` envelope family the tagged-template form produces, with
|
|
670
|
+
// positional `args`; nothing is executed here, for the same reason as the
|
|
671
|
+
// tag form — the real function lives in the lexicon module, and resolving
|
|
672
|
+
// it through this file's own imports and invoking it is the async
|
|
673
|
+
// bridge's job (../discovery/fold-import.ts's `reviveFoldedValue`).
|
|
674
|
+
//
|
|
675
|
+
// Arguments fold through {@link foldIntrinsicValue}, exactly like a tag's
|
|
676
|
+
// interpolations: an intrinsic's interior is where a pseudo-parameter
|
|
677
|
+
// chain (`GetAZs(AWS.Region)`) legitimately appears, and it stays
|
|
678
|
+
// symbolic rather than rejecting.
|
|
679
|
+
//
|
|
680
|
+
// The door does not open any wider than this. A bare-identifier callee
|
|
681
|
+
// only, so `ns.Ref(...)` and `arr.map(...)` are untouched; the file's own
|
|
682
|
+
// `const` shadowing wins, so a local `Ref` is not the lexicon's; and a
|
|
683
|
+
// name absent from the active lexicons' registered set — or registered
|
|
684
|
+
// without the opt-in — falls straight through to the throw below.
|
|
685
|
+
if (ts.isIdentifier(node.expression) && !consts.has(node.expression.text)) {
|
|
686
|
+
const calleeName = node.expression.text;
|
|
687
|
+
if (intrinsics.some((i) => i.name === calleeName && intrinsicCallFolds(i))) {
|
|
688
|
+
return {
|
|
689
|
+
__intrinsic: calleeName,
|
|
690
|
+
args: node.arguments.map((arg) => foldIntrinsicValue(arg, consts, intrinsics, externals)),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
throw foldError(node, callExpressionMessage(node));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
throw foldError(node, unsupportedExpressionMessage(node));
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Fold a resource constructor call to its spec.
|
|
703
|
+
*
|
|
704
|
+
* The common `createResource` shape (../runtime.ts) is `new Type({ ...props
|
|
705
|
+
* })` or `new Type({ ...props }, { ...attributes })` — CFN-style resource
|
|
706
|
+
* attributes (`DependsOn`, `Condition`, `DeletionPolicy`, …) second — and
|
|
707
|
+
* that shape reduces to `props` (+ `attributes`) exactly as before.
|
|
708
|
+
*
|
|
709
|
+
* chant #1082 — but that is a convention, not a rule every lexicon class
|
|
710
|
+
* follows. AWS's deploy-time `Parameter` is `(type, props)`
|
|
711
|
+
* (lexicons/aws/src/parameter.ts): the props object is the SECOND argument
|
|
712
|
+
* and the first is a plain string. `foldResource` used to require argument 0
|
|
713
|
+
* to be an object literal, so no `new Parameter(...)` anywhere could ever
|
|
714
|
+
* fold, whatever surrounded it. The general case now folds every argument in
|
|
715
|
+
* source order into {@link FoldedResource.args}, which the caller constructs
|
|
716
|
+
* the entity from verbatim — no positional assumption at all. `props` is
|
|
717
|
+
* still reported (the first object-literal argument, for callers that read
|
|
718
|
+
* it) but is a VIEW onto `args`, not the thing constructed from.
|
|
719
|
+
*/
|
|
720
|
+
export function foldResource(
|
|
721
|
+
node: ts.NewExpression,
|
|
722
|
+
consts: Map<string, ts.Expression>,
|
|
723
|
+
intrinsics: readonly IntrinsicDef[] = [],
|
|
724
|
+
externals?: ReadonlyMap<string, unknown>,
|
|
725
|
+
): FoldedResource {
|
|
726
|
+
const typeName = node.expression.getText();
|
|
727
|
+
const args = node.arguments ?? ([] as unknown as ts.NodeArray<ts.Expression>);
|
|
728
|
+
const [firstArg, secondArg] = args;
|
|
729
|
+
const foldArg = (arg: ts.Expression) => fold(arg, consts, intrinsics, externals);
|
|
730
|
+
|
|
731
|
+
if (!firstArg) {
|
|
732
|
+
return { __resource: typeName, props: {} };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// The classic (props) / (props, attributes) shape — reported without an
|
|
736
|
+
// `args` list so the spec of an ordinary resource is unchanged.
|
|
737
|
+
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
738
|
+
const props = foldArg(firstArg) as { [key: string]: FoldedValue };
|
|
739
|
+
if (args.length === 1) {
|
|
740
|
+
return { __resource: typeName, props };
|
|
741
|
+
}
|
|
742
|
+
if (args.length === 2 && ts.isObjectLiteralExpression(secondArg)) {
|
|
743
|
+
return { __resource: typeName, props, attributes: foldArg(secondArg) as { [key: string]: FoldedValue } };
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Anything else: fold every argument positionally. Each one still has to be
|
|
748
|
+
// in the fold subset on its own terms — a non-foldable argument throws from
|
|
749
|
+
// `fold()` exactly as a non-foldable prop value does.
|
|
750
|
+
const folded = args.map(foldArg);
|
|
751
|
+
const propsIndex = args.findIndex((arg) => ts.isObjectLiteralExpression(arg));
|
|
752
|
+
const props = (propsIndex === -1 ? {} : folded[propsIndex]) as { [key: string]: FoldedValue };
|
|
753
|
+
return { __resource: typeName, props, args: folded };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function hasExportModifier(statement: ts.VariableStatement): boolean {
|
|
757
|
+
return statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Fold every exported `const X = new Type({...})` resource declaration in a
|
|
762
|
+
* source file to its spec, with no module execution.
|
|
763
|
+
*
|
|
764
|
+
* Non-resource `const` exports (anything whose initializer isn't a `new`
|
|
765
|
+
* expression) are left out of the result rather than folded or errored —
|
|
766
|
+
* `new`-less resource forms (composite factory calls) are epic Phase 5
|
|
767
|
+
* (#1023) and are not attempted here.
|
|
768
|
+
*
|
|
769
|
+
* @param intrinsics - Lexicon-registered intrinsic tags, forwarded to
|
|
770
|
+
* {@link fold} for every resource. See {@link fold}'s `intrinsics` param.
|
|
771
|
+
*/
|
|
772
|
+
export function foldModule(
|
|
773
|
+
source: string,
|
|
774
|
+
fileName = "module.ts",
|
|
775
|
+
intrinsics: readonly IntrinsicDef[] = [],
|
|
776
|
+
): Record<string, FoldModuleEntry> {
|
|
777
|
+
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
|
|
778
|
+
const consts = collectConsts(sourceFile);
|
|
779
|
+
const result: Record<string, FoldModuleEntry> = {};
|
|
780
|
+
|
|
781
|
+
for (const statement of sourceFile.statements) {
|
|
782
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
783
|
+
if (!hasExportModifier(statement)) continue;
|
|
784
|
+
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
|
785
|
+
|
|
786
|
+
for (const decl of statement.declarationList.declarations) {
|
|
787
|
+
if (!ts.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
788
|
+
if (!ts.isNewExpression(decl.initializer)) continue;
|
|
789
|
+
|
|
790
|
+
const name = decl.name.text;
|
|
791
|
+
try {
|
|
792
|
+
const spec = foldResource(decl.initializer, consts, intrinsics);
|
|
793
|
+
result[name] = { ok: true, spec };
|
|
794
|
+
} catch (err) {
|
|
795
|
+
if (err instanceof FoldError) {
|
|
796
|
+
result[name] = { ok: false, error: err.message, ruleId: err.ruleId, line: err.line, column: err.column };
|
|
797
|
+
} else {
|
|
798
|
+
throw err;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return result;
|
|
805
|
+
}
|