import * as ts from "typescript"; import { type IntrinsicDef } from "../lexicon.js"; /** * subset — the single canonical definition of chant's statically-foldable * expression subset (chant #1024, epic #1019). * * `fold()` ({@link "./fold"}, the enforcement layer — a construct outside * this subset simply has no case there) and EVL001/EVL003 * ({@link "../lint/rules/evl001-non-literal-expression"}, * {@link "../lint/rules/evl003-dynamic-property-access"} — the pre-flight * diagnostic layer) both import this module so the two can never drift * apart on *which node kinds, operators, and key shapes* are foldable. * Before this module existed, `fold()` and EVL001 each hand-rolled their * own recursive classifier; they agreed almost everywhere but had several * real, silent gaps (see git history of #1024 for the enumerated list — * dynamic object-literal keys, template/tagged-template interiors, and * unrestricted binary/unary operators were all accepted by EVL001 but * rejected by `fold()`). This module is the fix: one classifier, two * importers. * * Scope — this module classifies *shape* only: the syntactic kind of an * expression, its operator, and (for keys) its literal-ness. It * deliberately does NOT resolve bindings or perform any evaluation, because * three things a full fold needs are inherently environment-dependent and * cannot be recovered from shape alone: * * 1. Identifier *resolution* — is a bare name actually a local `const`, * vs. an unbound name? That needs the file's `consts` map. Both * `fold()` and this module treat a bare identifier as shape-valid; * `fold()` alone resolves it (and rejects if unresolved) once it has * that map. A lint rule has no equivalent binding-resolution pass * today, so EVL (via this module) stays permissive here — a known, * intentional asymmetry, not a bug: it can only ever be a *false * negative* on EVL's part (EVL passes something `fold()` might later * reject for being unresolved), never the reverse. * * chant #1169 adds one more resolution-dependent rejection in the same * direction: an identifier bound to a same-file `const x = new T(...)`, * used as a VALUE. It is shape-valid here; `fold()` answers it only when * its caller pre-resolved that const to the one real instance the file * built (`../discovery/fold-import.ts`), and rejects otherwise, because * re-folding the initializer would construct a duplicate of a resource * discovery has already registered. Shape cannot see the difference, and * the rejection is a fall-back-to-run, never a wrong value. * 2. Tagged-template *tag registration* — needs a lexicon's intrinsics * manifest, which isn't available to a syntax-only lint rule. `fold()` * alone checks it; this module treats any tag name as shape-valid and * only classifies the interpolated values. * 2b. Authoring-helper *provenance* (chant #1082) — a call to a registered * chant helper (`phase(...)`, `output(...)`; ./foldable-helpers.ts) * folds, but only when the name is genuinely bound to an import of * chant's own. That needs the module graph, which a syntax-only lint * rule doesn't have. This module checks the NAME only and stays * permissive; `fold()`'s bridge does the provenance check and falls the * file back to run when it fails. Same direction as every other item * here — a false negative for EVL, never a false positive. * 2c. Intrinsic *call-form* registration (chant #1044) — a plain call to a * lexicon intrinsic the lexicon opted in (`Ref(bucket)`, * `Concat(a, b)`; `IntrinsicDef.foldsAsCall`, ../lexicon.ts) folds. * Whether a given name is such an intrinsic is not knowable from shape, * so {@link findSubsetViolation} takes the registry as an OPTIONAL * parameter instead of guessing: supply it and the answer for a call is * exact (fold()'s own), omit it and every call is a violation, the * pre-#1044 answer. * * That parameter is the whole reason the call case lives here rather * than only in `fold()`. This module is a shared predicate, and the * point of a shared predicate is that a consumer can ask "will this * fold?" without running fold — a control plane deciding whether a * repository needs a sandboxed child process at all, for instance. * Keeping the case out of here would make the predicate answer "no" for * idiomatic `Ref(...)` source that `fold()` reduces cleanly: not a * permissive gap but a systematically WRONG answer in the expensive * direction, and the one direction this module is not allowed to be * wrong in (see the false-negative/false-positive rule in point 1, and * the flow-sensitivity note below — the single divergence in the other * direction, and the one this module treats as a wart). A caller with * no registry degrades to "assume it runs", which is safe and cheap to * reason about. * * chant #1106 — EVL is no longer such a caller by default. `runLint` * (../lint/engine.ts) takes the active lexicons' `IntrinsicDef[]` as a * parameter and puts it on `LintContext.intrinsics` * (../lint/rule.ts), and EVL001 (evl001-non-literal-expression.ts) * passes it straight through to `checkObjectMember`. `chant lint`'s * three CLI entry points (the `lint` command's initial pass, its * `--fix` re-lint, and the LSP's per-file diagnostics) all resolve the * project's lexicons and thread their intrinsics through, mirroring * how `discover()` has done it for the fold path since #1039/#1105 — * so `chant lint` on a real project no longer flags `Ref(...)` that * `fold()` accepts. A `LintContext` built without that plumbing (a * unit test constructing one directly, a consumer that hasn't * resolved lexicons) still gets the pre-#1044 conservative answer — * that path was never wrong, only stricter than it had to be. * 3. Runtime *type* of a folded value — e.g. spreading `const n = 5` * (`{...n}`) is shape-valid (`n` is a plain identifier) but `fold()` * rejects it once it discovers `n` folds to a number, not an object. * Only real evaluation catches this; EVL004 independently narrows * spread sources to a stricter "traceable to a const" shape, which * catches most real-world misuse without evaluating, but is not a * full substitute. * * One more inherent gap, on the *value-flow* side rather than shape: * `fold()` evaluates `&&`/`||`/`??` and `? :` lazily — it only folds the * side/branch actually taken, exactly like the JS runtime — so an * otherwise-unfoldable *untaken* branch does not reject * (`false && sideEffect()` folds cleanly to `false`). This module (and so * EVL) is flow-insensitive: it has no notion of "taken", so it requires * every operand/branch to be shape-valid. This can only make EVL *stricter* * than `fold()` (a false positive relative to fold, flagging code the * folder would actually accept) — never the reverse. Making EVL * flow-sensitive would mean re-implementing an evaluator inside a lint * rule; out of scope here. See #1024. */ /** The two EVL rule ids a shape violation can be attributed to. */ export type SubsetRuleId = "EVL001" | "EVL003"; /** A located, shape-level rejection: the offending node, the EVL rule id it maps to, and a message. */ export interface SubsetViolation { node: ts.Node; ruleId: SubsetRuleId; message: string; } /** Binary operators `fold()` implements — see `fold()`'s operator switch in ./fold.ts. */ export declare const SUPPORTED_BINARY_OPERATORS: ReadonlySet; /** Prefix unary operators `fold()` implements: logical-not and numeric negation. */ export declare const SUPPORTED_UNARY_OPERATORS: ReadonlySet; /** A property name foldable without execution: identifier, string, or numeric literal (not a computed name). */ export declare function isLiteralPropertyName(node: ts.PropertyName): node is ts.Identifier | ts.StringLiteral | ts.NumericLiteral; /** An element-access key foldable without execution: a string or numeric LITERAL only (EVL003 semantics). */ export declare function isLiteralElementKey(node: ts.Expression): node is ts.StringLiteral | ts.NumericLiteral; /** * A short, single-line rendering of `node`'s source text for embedding in a * diagnostic message — never the raw `getText()`, which reproduces the * node's ENTIRE source verbatim and can span dozens of lines for a real * composite call or object literal (chant #1054: a fold fallback reason that * embeds one of these buries the actual error after it, and breaks any * line-oriented consumer of `[fold:run]` output). Internal whitespace * (including newlines) collapses to a single space, and the result is capped * to a bounded length so one pathological node can't blow out an otherwise * one-line reason either. */ export declare function briefNodeText(node: ts.Node, maxLength?: number): string; export declare function computedPropertyNameMessage(node: ts.PropertyName): string; export declare function dynamicElementAccessMessage(keyNode: ts.Expression): string; export declare const UNSUPPORTED_OBJECT_MEMBER_MESSAGE = "unsupported object member"; export declare const UNSUPPORTED_UNARY_MESSAGE = "unsupported unary"; export declare function unsupportedBinaryMessage(opKind: ts.SyntaxKind): string; /** * chant #1054 — the ONE wording for "a bare function/method call used where * chant needs a value it can fold." Before this, `fold()` (this message) and * `../discovery/fold-import.ts`'s `resolveCallExpression` (a top-level * export's own call-as-a-value check) had each grown their own hand-written * copy — "function call as a value" here, "call expression as a value" * there — for the identical rejection, which meant a tool grouping fallback * reasons by text had to match both to avoid silently under-counting one of * them. `resolveCallExpression` now calls this function directly instead of * building its own string. */ export declare function callExpressionMessage(node: ts.CallExpression): string; export declare function unsupportedExpressionMessage(node: ts.Node): string; /** * Classify one object-literal member (`{ a: 1 }`'s `a: 1`, `{ ...x }`'s * `...x`, or a shorthand `{ a }`). Checks the key's shape before the * value's, mirroring `propName()` in fold.ts, which runs before folding * the value. Returns the first violation within this member, or * `undefined` when it's fully in the subset. */ export declare function checkObjectMember(prop: ts.ObjectLiteralElementLike, intrinsics?: readonly IntrinsicDef[]): SubsetViolation | undefined; /** * The canonical recursive shape classifier: is `node`'s expression *shape* * within the subset `fold()` can reduce? Mirrors `fold()`'s own dispatch * node-kind for node-kind (see ./fold.ts) — every branch here has a * matching branch there, and vice versa — but performs no * resolution/evaluation (see the module doc comment for the three * environment-dependent exceptions). Returns the first (deepest, * `fold()`-evaluation-order) unsupported node, or `undefined` when `node`'s * whole shape is foldable. */ export declare function findSubsetViolation(node: ts.Node, intrinsics?: readonly IntrinsicDef[]): SubsetViolation | undefined; //# sourceMappingURL=subset.d.ts.map