import * as ts from "typescript"; import { type IntrinsicDef } from "../lexicon.js"; import { type SubsetRuleId } from "./subset.js"; /** * fold — static AST value reducer (chant #1026/#1021/#1024, part of epic #1019) * * Reduces a single-file TypeScript expression AST to a value with NO * module execution. The node-kind/operator/key subset it covers — literals, * template interpolation, object/array literals (incl. spread), `const` * identifier resolution, property and element access (incl. the * cross-resource `{ __attrRef }` case, literal-key-only), unary `!`/`-`, * the binary operators `+ - * / === !== > < >= <=`, short-circuit * `&& || ??`, conditional expressions, `as`/`satisfies`/`!`/parenthesized * unwrapping, a nested `new Type({...})` resource-as-value (chant #1169), and * registered lexicon intrinsic tagged templates — is defined ONCE, in * {@link "./subset"} ({@link findSubsetViolation}), and shared with EVL001/EVL003 * ({@link "../lint/rules/evl001-non-literal-expression"}), so the linted * subset and the folded subset can never drift apart (#1024). * * chant #1169 closed the largest of the documented fold/EVL divergences in the * process: a nested `new Type(...)` used as a value was shape-valid for * `./subset` and rejected by `fold()`, because `fold()` could only produce the * `{__resource, props}` envelope and nothing constructed it. Now it folds to * that envelope and ../discovery/fold-import.ts constructs the REAL instance * from it. The remaining divergences are the resolution-dependent ones * ./subset's own module doc enumerates, plus one this change adds in the same * safe direction: a BARE identifier bound to a same-file `new` is shape-valid * there and rejected here, because folding it would build a duplicate of a * resource discovery already registered — see {@link fold}'s identifier branch. * * A `CallExpression` has almost no case — a function call as a value is * structurally unrepresentable, not merely linted against. Composite * factory calls are out of scope here (epic Phase 5, #1023). There are * exactly two exceptions, both closed allowlists of names declared * somewhere a human had to write them down, and both reducing to a symbolic * envelope that executes nothing here: * * - a call to a REGISTERED chant authoring helper * ({@link "./foldable-helpers"}, chant #1082) → {@link FoldedHelperCall}; * - a call to a lexicon intrinsic whose lexicon registered it AND opted * its call form in ({@link intrinsicCallFolds}, ../lexicon.ts, chant * #1044) → {@link FoldedIntrinsicCall}, the same `{__intrinsic}` family * the tagged-template form already reduces to. * * chant #1373 adds a third, the only one that evaluates rather than * enveloping: a call to a PROJECT-LOCAL function — declared in this file or * imported from a sibling project file — whose body is itself inside the * fold subset. ../discovery/fold-import.ts hands such a function in through * `externals` as a {@link FoldableFunction}, and {@link callFoldableFunction} * folds its body against the defining module's scope with the folded * arguments bound. Still nothing is imported or run. * * Everything else — a package's function, a method call, an array `.map`, a * registered name shadowed by a local binding — still throws. * * Cross-file identifier resolution (chant #1020): `consts` alone is always * this file's own top-level bindings — that part stays single-file, and * nothing about the supported node-kind subset changes. But an identifier * `fold()` can't find in `consts` isn't necessarily a dead end: the optional * `externals` map (populated by ../discovery/fold-import.ts, which owns the * module graph traversal) lets a caller pre-resolve an *imported* binding to * its real value — a plain value for an imported `const`, or the REAL, * already-constructed `Declarable`/`CompositeInstance` for a name bound to a * resource/composite in the defining module — and `fold()` just returns it * for the identifier, unchanged. This is why a bare object/array bracket * index a few lines down (`obj[key]`) is enough to make `network.vpc.VpcId` * fold correctly once `network` resolves via `externals` to the real, * shared composite instance object: indexing a live class instance the same * way as a plain object returns its real getter's real `AttrRef`, wired to * the real shared parent — see fold-import.ts's module doc for why that * shared identity is the entire hard part of #1020. */ /** * The result of folding an AST node: a plain value, a symbolic reference to * a sibling resource's attribute, a folded intrinsic tagged template, an * unresolved external symbol chain, or a folded resource spec. */ export type FoldedValue = string | number | boolean | null | undefined | FoldedValue[] | { [key: string]: FoldedValue; } | AttrRefValue | FoldedIntrinsic | FoldedHelperCall | SymbolicValue | FoldedResource; /** * Symbolic reference produced when a property/element access resolves to an * attribute of another `const`-declared resource in the same file, e.g. * `bucket.name` (or `bucket["name"]`) where `bucket` is * `const bucket = new S3Bucket({...})`. * * This is the SAME envelope `AttrRef.prototype.toJSON()` produces at * runtime ({@link "../attrref"}) — `serializer-walker.ts`'s `walkValue` * already recognizes a plain `{ __attrRef }` object as an AttrRef envelope * (it doesn't require a live `AttrRef` instance), so this is not an * invented shape: it's the existing envelope, produced without running the * module that would otherwise construct the real `AttrRef`. */ export interface AttrRefValue { __attrRef: { entity: string; attribute: string; }; } /** * The result of folding a registered lexicon intrinsic tagged template * (e.g. `Sub\`${AWS.StackName}-x\``) to its node form: tag name, cooked * template string parts, and folded interpolated values (in order). * Mirrors the runtime call shape `Tag(strings, ...values)` so a later * build path can replay it into the real intrinsic object (#1022). */ export type FoldedIntrinsic = FoldedIntrinsicTag | FoldedIntrinsicCall; /** The tagged-template form: `Sub\`${x}-y\`` — see {@link FoldedIntrinsic}. */ export interface FoldedIntrinsicTag { __intrinsic: string; strings: string[]; values: FoldedValue[]; } /** * The plain-call form of a registered, opted-in lexicon intrinsic * (chant #1044) — `Ref(bucket)`, `Concat("a", b)`, `GetAtt("Fn", "Arn")`. * Same `__intrinsic` key as the tagged-template form, so the same revival * branch handles both and nothing downstream learns a new envelope; the * payload differs because the call shape does — positional `args` mirroring * `Name(...args)`, where the tag form mirrors `Name(strings, ...values)`. * * Symbolic, exactly like the tag form: `fold()` executes nothing, it only * records that a registered intrinsic was called and with what. The real * function is resolved through the folding file's own imports and invoked by * `../discovery/fold-import.ts`'s `reviveFoldedValue`. */ export interface FoldedIntrinsicCall { __intrinsic: string; args: FoldedValue[]; } /** * The result of folding a call to a registered chant authoring helper * (chant #1082) — e.g. `phase("Apply", [...])`, `output(ref, "oX")`. Holds * the helper's name and its folded arguments, in source order. * * Symbolic, exactly like {@link FoldedIntrinsic}: `fold()` executes nothing, * it only records that a registered helper was called and with what. The real * function is resolved through the folding file's own imports and invoked by * `../discovery/fold-import.ts`'s `reviveFoldedValue`, which is also where * the "is this name actually bound to chant's own helper" check lives. See * {@link "./foldable-helpers"} for the allowlist and why it is closed. */ export interface FoldedHelperCall { __helper: string; args: FoldedValue[]; } /** * A sub-expression inside a folded intrinsic that fold could not reduce to * a value without resolving an identifier from outside this file — e.g. an * imported pseudo-parameter namespace access like `AWS.StackName`. * Cross-file import resolution is #1020; until then the raw source text is * preserved verbatim (never stringified, never rejected) instead of being * treated as an unresolved-identifier error. Only appears inside a folded * intrinsic's `values` — see {@link foldIntrinsicValue}. */ export interface SymbolicValue { __symbol: string; } /** * The result of folding a resource constructor: `new Type({ ...props })`. * * chant #1169 — produced for a NESTED `new Type(...)` used as a value too, not * just for a file's own top-level resource declaration. It is symbolic in * exactly the sense {@link FoldedIntrinsic} and {@link FoldedHelperCall} are: * `fold()` executes nothing, it records which constructor the source named and * with what arguments. ../discovery/fold-import.ts resolves that name through * the folding file's own imports and calls the real class, so a folded * construction and a run construction are the same construction. An envelope * must never reach a serializer — see the `new` branch of {@link fold}. */ export interface FoldedResource { __resource: string; props: { [key: string]: FoldedValue; }; /** * The constructor's optional second argument — CFN-style resource-level * attributes (`DependsOn`, `Condition`, `DeletionPolicy`, * `UpdateReplacePolicy`, `CreationPolicy`, `Metadata`, …) some lexicons * accept alongside `props` (see `createResource`'s `attributes` param, * ../runtime.ts). Present only when the source actually passed one. */ attributes?: { [key: string]: FoldedValue; }; /** * chant #1082 — every constructor argument, folded, in source order. Present * only when the argument list is NOT the classic `(props)` / `(props, * attributes)` shape — most often because the props object isn't first * (`new Parameter("String", {...})`, whose signature is `(type, props)`). * * When present this is authoritative: the entity is constructed by spreading * it, so the constructor receives exactly what the source wrote. `props` * alongside it is the first object-literal argument, reported for readers, * never re-passed (which would double-count it). */ args?: FoldedValue[]; } /** * One entry per exported `const` resource declaration in {@link foldModule}'s * result. The `ok: false` case surfaces the same located, rule-id-tagged * shape as {@link FoldError} (#1024) — `error` stays the formatted message * string for backward-compat display, while `ruleId`/`line`/`column` let a * caller cite the exact same rule id + position an EVL diagnostic for the * same construct would. */ export type FoldModuleEntry = { ok: true; spec: FoldedResource; } | { ok: false; error: string; ruleId: SubsetRuleId; line: number; column: number; }; /** * Error thrown when a node cannot be folded to a value without executing * code. Carries the node's source position (1-based, matching `LintError`) * so callers can report a located diagnostic, and the id of the EVL rule * that flags the same construct (#1024) — "EVL001" (the general * not-statically-evaluable umbrella) unless the rejection is specifically a * dynamic element-access key, which is "EVL003"'s construct. A rejection * with no EVL equivalent (unresolved identifier, unregistered intrinsic tag, * spread of a value that turns out not to be an object/array — all * environment/value-dependent, see {@link "./subset"}'s module doc) still * defaults to "EVL001" since that's the closest umbrella rule, even though * EVL can't actually detect it ahead of a real fold. * * chant #1020 hang fix — every `FoldError` is thrown for a routine, EXPECTED * outcome (this node's shape isn't in the fold subset) and is ALWAYS caught * a few frames up (`tryFoldFileCore`'s own top-level catch, ultimately), * reduced to `.message`; `.stack` is never read anywhere on this path. V8 * still eagerly walks live JS frames to populate the (lazy) `.stack` getter's * backing data at CONSTRUCTION time regardless of whether it's ever read — * cheap for a shallow call stack, but expensive once the surrounding * functions are hot enough for V8 to aggressively inline them (every corpus * entry re-triggers the same call shapes across `foldFileMemoized` -> * `buildExternals` -> `tryFoldFileCore` -> `resolveDeclaratorValue` -> * `resolveLiveValue` -> `resolveCallExpression`/`fold`, chant #1020's * cross-file resolution making that chain several layers deeper than the * pre-#1020 single-file fold ever needed): capturing a stack from deep, * optimized/inlined frames requires V8 to reconstruct them from deopt * metadata, confirmed via `sample` to dominate CPU during the observed * multi-minute stall (`Isolate::CaptureAndSetErrorStack` / * `OptimizedJSFrame::Summarize` / `DeoptTranslationIterator`). Most files in * the corpus (77/98 entries have at least one run-fallback file) throw one * of these, so the cost compounds across a build. `Error.stackTraceLimit = 0` * for the duration of `super()` makes V8 capture zero frames — free * regardless of stack depth/optimization state — then the limit is restored * immediately, so it doesn't suppress a real stack trace anywhere else in * the process. */ export declare class FoldError extends Error { readonly line: number; readonly column: number; readonly ruleId: SubsetRuleId; constructor(message: string, line: number, column: number, ruleId?: SubsetRuleId); } /** * A project-local function `fold()` can CALL — chant #1373. * * Produced by ../discovery/fold-import.ts for every top-level function a * project file declares (`export function f(...) {...}`, `export const f = * (...) => ...`, and their non-exported siblings) and placed in the folding * file's `externals` under the function's name, so a call through a bare * identifier bound to one evaluates STATICALLY: the arguments fold in the * caller's scope, the parameters are bound, and the body folds in the * DEFINING module's scope (`consts`/`externals` here are that module's, not * the caller's). Nothing is imported and nothing runs — this is the same * "evaluate the source instead of the module" move #1023 makes for a * composite factory body, applied to a plain function. * * Why this exists: the advice every chant project gets is to read build * parameters through a small helper (`optionalAccountId(params.accountId)`) * so defaulting and validation live in one place. Before #1373 a call to * that helper was "a function call as a value" and unfoldable, and because * fallback is per file and taint propagates along imports, ONE helper call * at the top of a parameter file demoted every stack that imported it. * * The value is a marker, never a real function: a folded file's export * namespace may carry one (an importer's `buildExternals` picks it up by * name), and `fold()` refuses it anywhere a VALUE is expected — a function * object cannot be serialized, and the run path's export namespace holds the * real function the collector ignores, so ignoring the marker matches. * * Not a `FoldedValue`: it never appears inside a folded tree. */ export declare class FoldableFunction { /** The binding name, for diagnostics. */ readonly name: string; readonly fn: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression; /** Absolute path of the defining module, for diagnostics. */ readonly file: string; /** * The defining module's top-level `const` initializers, with every * `new`-bound one already REMOVED — a body that mentions one reads the * live instance out of `externals` instead (the same object the module's * own fold registered), never a by-name `{__attrRef}` that would name an * entity of the wrong file once revived in the caller. */ readonly consts: Map; /** The defining module's resolved imports, its own pre-built resources, and its own sibling functions. Read live, never copied, so a function declared before a const it reads still sees that const's value. */ readonly externals: ReadonlyMap; /** Why an import of the defining module did NOT resolve, by local name — enriches an "unresolved identifier" inside the body. */ readonly failures?: ReadonlyMap | undefined; constructor( /** The binding name, for diagnostics. */ name: string, fn: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression, /** Absolute path of the defining module, for diagnostics. */ file: string, /** * The defining module's top-level `const` initializers, with every * `new`-bound one already REMOVED — a body that mentions one reads the * live instance out of `externals` instead (the same object the module's * own fold registered), never a by-name `{__attrRef}` that would name an * entity of the wrong file once revived in the caller. */ consts: Map, /** The defining module's resolved imports, its own pre-built resources, and its own sibling functions. Read live, never copied, so a function declared before a const it reads still sees that const's value. */ externals: ReadonlyMap, /** Why an import of the defining module did NOT resolve, by local name — enriches an "unresolved identifier" inside the body. */ failures?: ReadonlyMap | undefined); /** * Set once a call to this function has RETURNED a live object (a value * with a prototype — a pre-built resource instance of the defining module, * say) into a caller. The caller then shares that object's identity with * the defining module exactly as an imported resource binding would, and * fold-import.ts records the same `liveSources` edge for it so the two * files fold or run together. A function that returns only plain data never * sets this, which is what keeps a parameter helper from tainting anything. */ leakedIdentity: boolean; } export declare function isFoldableFunction(value: unknown): value is FoldableFunction; /** * The SHAPE half of what makes a project-local function foldable (chant * #1373) — a reason string, or `undefined` when the function is admissible. * Deliberately the same statement-level contract #1023 gives a composite * factory body: parameters bound plainly (an identifier, optionally * defaulted, or a flat object pattern), and a body that is either a single * expression or `const` declarations followed by one `return`. Every * expression inside is then folded by {@link fold} itself, so the expression * subset is defined exactly once — with the additions {@link fold} refuses * INSIDE a function body (a `new`, a tagged template, a registered helper or * intrinsic call), because each of those reduces to an envelope revived * against the CALLER's imports, which are not the scope the body was written * in. */ export declare function findFunctionSubsetViolation(fn: ts.FunctionDeclaration | ts.ArrowFunction | ts.FunctionExpression): string | undefined; /** * Resolve a node's 1-based line/column via its owning `SourceFile`. Exported * (chant #1020) so fold-import.ts can build its own located `FoldError`s * (e.g. an import-cycle diagnostic pointing at the specific `import` * statement that closes the cycle) using the exact same position math * `foldError` uses here, rather than a second hand-rolled implementation. */ export declare function locate(node: ts.Node): { line: number; column: number; }; /** * Collect every top-level `const x = ` in a source file into a * name -> initializer map. Single-file only (cross-file is #1020). */ export declare function collectConsts(sourceFile: ts.SourceFile): Map; /** * A property/element key foldable without execution: identifier, string, or * numeric literal. * * Exported for chant #1023's composite-factory interpreter * (../discovery/fold-import.ts), which walks object literals itself — a * factory body may construct a resource inside one, which {@link fold} has no * case for — and must reject a computed key with the identical message * {@link fold} would, rather than growing a second, silently divergent copy of * this rule. */ export declare function propName(node: ts.PropertyName): string; /** * Fold a single expression node to a value. Throws {@link FoldError} for * anything outside the supported subset — including any `CallExpression` * that is neither a registered chant authoring helper nor a registered, * call-form-opted-in lexicon intrinsic (see the module doc). * * @param intrinsics - The active lexicons' registered intrinsics. A tagged * template whose tag isn't in this list, or is in it without * {@link intrinsicTagFolds}, is rejected; a plain call is rejected unless * its callee is in this list with {@link intrinsicCallFolds} (chant * #1044). Defaults to none — pass the target lexicon's manifest * `intrinsics` to recognize either form. * @param externals - chant #1020: pre-resolved imported bindings, consulted * only when an identifier isn't in `consts`. See the module doc above. * `undefined` (the default) preserves the exact pre-#1020 single-file * behavior — every identifier not in `consts` is unresolved. */ export declare function fold(node: ts.Expression, consts: Map, intrinsics?: readonly IntrinsicDef[], externals?: ReadonlyMap): FoldedValue; /** * Fold a resource constructor call to its spec. * * The common `createResource` shape (../runtime.ts) is `new Type({ ...props * })` or `new Type({ ...props }, { ...attributes })` — CFN-style resource * attributes (`DependsOn`, `Condition`, `DeletionPolicy`, …) second — and * that shape reduces to `props` (+ `attributes`) exactly as before. * * chant #1082 — but that is a convention, not a rule every lexicon class * follows. AWS's deploy-time `Parameter` is `(type, props)` * (lexicons/aws/src/parameter.ts): the props object is the SECOND argument * and the first is a plain string. `foldResource` used to require argument 0 * to be an object literal, so no `new Parameter(...)` anywhere could ever * fold, whatever surrounded it. The general case now folds every argument in * source order into {@link FoldedResource.args}, which the caller constructs * the entity from verbatim — no positional assumption at all. `props` is * still reported (the first object-literal argument, for callers that read * it) but is a VIEW onto `args`, not the thing constructed from. */ export declare function foldResource(node: ts.NewExpression, consts: Map, intrinsics?: readonly IntrinsicDef[], externals?: ReadonlyMap): FoldedResource; /** * Fold every exported `const X = new Type({...})` resource declaration in a * source file to its spec, with no module execution. * * Non-resource `const` exports (anything whose initializer isn't a `new` * expression) are left out of the result rather than folded or errored — * `new`-less resource forms (composite factory calls) are epic Phase 5 * (#1023) and are not attempted here. * * @param intrinsics - Lexicon-registered intrinsic tags, forwarded to * {@link fold} for every resource. See {@link fold}'s `intrinsics` param. */ export declare function foldModule(source: string, fileName?: string, intrinsics?: readonly IntrinsicDef[]): Record; //# sourceMappingURL=fold.d.ts.map