Evaluability Rules (EVL)
Evaluability rules flag TypeScript patterns that the chant evaluator cannot statically resolve. All evaluability rules have severity error.
EVL001 and EVL003 share one subset definition with fold(), the static AST reducer chant build uses by default (see TypeScript as Data). A shape either rule flags is, with the environment-dependent exceptions below, exactly a shape the folder rejects, at the same line. EVL reports it in your editor and in chant lint, before a build, and fold() enforces it. A file the folder rejects still builds. chant imports and runs it instead, and EVL keeps that run inside the deterministic subset.
Where EVL and fold() diverge. Both import the same classifier (fold/subset.ts) so they can’t drift on node kinds or operators, but several things depend on the environment and can’t be decided from shape alone:
- Identifier resolution — EVL treats any bare name as valid;
fold()alone resolves it against the file’sconsts and rejects an unbound one. A false negative on EVL’s part, never the reverse. - Intrinsic tag registration — EVL has no lexicon registry at lint time, so it can’t tell a registered intrinsic tagged template from a made-up one; it treats any tagged-template interior as opaque.
fold()has the registry and rejects an unregistered tag. - Intrinsic call-form registration — an intrinsic its lexicon opted into call-form folding (
Ref(...),Concat(...)) folds, but onlyfold()is given the registry that says so. EVL has none, so it still flags such a call in a resource’s props even though--foldreduces it. This is the one place EVL is stricter than the folder. The classifier takes the registry as an optional parameter, so a tool that has one gets the exact answer; wiring it into the lint engine is not done today. - Spread-source runtime type — EVL (via EVL004) checks that a spread source is traceable to a
const;fold()additionally rejects it if that const turns out not to be an object or array at all. - A nested
new Type(...)used as a property value — shape-valid to EVL either way, butfold()rejects a nested resource constructor (as opposed to the top-level one being folded) as a value, because it can’t be represented without constructing it, so the file falls back to run. - Short-circuit laziness —
fold()evaluates&&/||/??/? :lazily, so an unfoldable untaken branch doesn’t reject. EVL has no notion of “taken” and requires every branch to be shape-valid. That gives false positives relative tofold(), never the reverse. - Composite step access (chant #1544) —
Checkout({...}).stepand the same shape from any other single-actionComposite()wrapper (a call immediately narrowed to.step) is the documented idiom for embedding one inline in aJob’ssteps:array. EVL001 treats it as shape-valid;fold()still can’t fold it (a call is not a value it can reduce) and falls the file back to the run path, as documented. The fallback was always valid; EVL001 used to flag it by mistake.
In practice EVL catches the same code. These are the edge cases where code that passes lint can still fail to fold, or EVL flags code fold() would accept.
EVL001: Non-Literal Expression
Section titled “EVL001: Non-Literal Expression”Resource property values must be statically evaluable. Function calls, method calls, and other dynamic expressions are not allowed.
// ❌ Triggers EVL001export const bucket = new _.Resource({ bucketName: getName(), tags: Object.assign({}, baseTags),});// ✅ Fixedconst name = "my-data-bucket";const tags = { ...baseTags, env: "prod" };
export const bucket = new _.Resource({ bucketName: name, tags: tags,});// ✅ Also allowed — a Composite() wrapper's `.step` output embedded inline// (chant #1544; see "Where EVL and fold() diverge" above)export const build = new Job({ "runs-on": "ubuntu-latest", steps: [Checkout({}).step, new Step({ name: "Test", run: "npm test" })],});EVL002: Control Flow Wrapping Resources
Section titled “EVL002: Control Flow Wrapping Resources”Resource instantiation cannot appear inside control flow blocks (if, for, while, switch, try). Resources must be declared at the top level.
// ❌ Triggers EVL002if (env === "prod") { export const bucket = new _.Resource({ bucketName: "prod-data", });}// ✅ Fixedexport const bucket = new _.Resource({ bucketName: "my-data",});EVL003: Dynamic Property Access
Section titled “EVL003: Dynamic Property Access”Computed property access with a non-literal key cannot be evaluated statically.
// ❌ Triggers EVL003const region = config[key];// ✅ Fixedconst region = config["production"];EVL004: Spread From Non-Const Source
Section titled “EVL004: Spread From Non-Const Source”The spread operator requires the source to be traceable to a const declaration. A binding imported from another module counts, so an exported const can be shared and spread across modules.
// ❌ Triggers EVL004export const bucket = new _.Resource({ ...getDefaults(),});// ✅ Fixedconst defaults = { versioning: true, encryption: "AES256" };
export const bucket = new _.Resource({ ...defaults,});EVL005: Block Body in resource() Callback
Section titled “EVL005: Block Body in resource() Callback”Callbacks passed to resource() must use expression body syntax. Block bodies with return statements cannot be statically evaluated.
// ❌ Triggers EVL005resource(_.Resource, (props) => { const name = props.name + "-data"; return { bucketName: name };});// ✅ Fixed — use expression bodyresource(_.Resource, (props) => ({ bucketName: props.name + "-data",}));EVL007: Invalid Siblings Access
Section titled “EVL007: Invalid Siblings Access”In composite resources, accessing siblings.key.attr where key does not exist in the resource map is an error.
// ❌ Triggers EVL007resource(_.ServiceType, (props, siblings) => ({ role: siblings.typo.arn,}));// ✅ Fixedresource(_.ServiceType, (props, siblings) => ({ role: siblings.executionRole.arn,}));