@llui/compiler 0.5.2 → 0.5.4
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/accessor-resolver.d.ts +9 -1
- package/dist/accessor-resolver.d.ts.map +1 -1
- package/dist/accessor-resolver.js +68 -9
- package/dist/accessor-resolver.js.map +1 -1
- package/dist/collect-deps.d.ts.map +1 -1
- package/dist/collect-deps.js +217 -11
- package/dist/collect-deps.js.map +1 -1
- package/dist/module.d.ts +12 -1
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +2 -1
- package/dist/module.js.map +1 -1
- package/dist/modules/map-on-state-array.d.ts.map +1 -1
- package/dist/modules/map-on-state-array.js +54 -1
- package/dist/modules/map-on-state-array.js.map +1 -1
- package/dist/modules/opaque-state-flow.d.ts.map +1 -1
- package/dist/modules/opaque-state-flow.js +107 -34
- package/dist/modules/opaque-state-flow.js.map +1 -1
- package/dist/transform.d.ts +1 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +10 -2
- package/dist/transform.js.map +1 -1
- package/package.json +1 -1
package/dist/module.d.ts
CHANGED
|
@@ -56,6 +56,17 @@ export interface AnalysisContext {
|
|
|
56
56
|
sourceFile: ts.SourceFile;
|
|
57
57
|
/** TS TypeChecker, when the host adapter has built a Program. May be undefined for AST-only paths. */
|
|
58
58
|
checker: ts.TypeChecker | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* The cross-file Program the checker is bound to, when available.
|
|
61
|
+
* Modules that need to resolve identifiers across files (e.g. the
|
|
62
|
+
* opaque-state-flow lint walking through imported helpers) must walk
|
|
63
|
+
* Program-bound nodes — the file the registry hands them is a
|
|
64
|
+
* locally-reparsed copy and its identifiers won't resolve through the
|
|
65
|
+
* checker. Use `program.getSourceFile(sourceFile.fileName)` to fetch
|
|
66
|
+
* the Program-bound counterpart. Undefined when the host doesn't
|
|
67
|
+
* supply a Program (test path, lint adapters without cross-file).
|
|
68
|
+
*/
|
|
69
|
+
program: ts.Program | undefined;
|
|
59
70
|
/**
|
|
60
71
|
* Get the named module's accumulator slot (creating it lazily). The
|
|
61
72
|
* slot is whatever shape the module wrote; type-safe access is the
|
|
@@ -245,7 +256,7 @@ export declare class ModuleRegistry {
|
|
|
245
256
|
* 4. Emission: each module's `emit?` fires; the registry merges
|
|
246
257
|
* contributions, detecting (field, target) conflicts.
|
|
247
258
|
*/
|
|
248
|
-
run(sourceFile: ts.SourceFile, checker?: ts.TypeChecker, externalTypes?: ModuleExternalTypes): RegistryRunResult;
|
|
259
|
+
run(sourceFile: ts.SourceFile, checker?: ts.TypeChecker, externalTypes?: ModuleExternalTypes, program?: ts.Program): RegistryRunResult;
|
|
249
260
|
/** Module names in declaration order. Adapters surface this for debug logs / config diagnostics. */
|
|
250
261
|
listModules(): string[];
|
|
251
262
|
/** All diagnostic definitions across active modules. Used by adapters to enumerate stable IDs. */
|
package/dist/module.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"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"}
|
package/dist/module.js
CHANGED
|
@@ -79,7 +79,7 @@ export class ModuleRegistry {
|
|
|
79
79
|
* 4. Emission: each module's `emit?` fires; the registry merges
|
|
80
80
|
* contributions, detecting (field, target) conflicts.
|
|
81
81
|
*/
|
|
82
|
-
run(sourceFile, checker, externalTypes) {
|
|
82
|
+
run(sourceFile, checker, externalTypes, program) {
|
|
83
83
|
const analysis = {
|
|
84
84
|
sourceFile,
|
|
85
85
|
perModule: new Map(),
|
|
@@ -101,6 +101,7 @@ export class ModuleRegistry {
|
|
|
101
101
|
const ctx = {
|
|
102
102
|
sourceFile: currentSf,
|
|
103
103
|
checker,
|
|
104
|
+
program,
|
|
104
105
|
getSlot: (name, init) => {
|
|
105
106
|
let slot = analysis.perModule.get(name);
|
|
106
107
|
if (slot === undefined) {
|
package/dist/module.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.js","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"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","sourcesContent":["// CompilerModule + ModuleRegistry — v2c §2 visitor-registry primitive.\n//\n// Modules accumulate findings during a single AST walk per file and\n// contribute emissions after the walk completes. The walker visits each\n// node once; every module registered for that node's SyntaxKind sees it.\n//\n// This file defines the interfaces + the registry. The actual modules\n// (`compiler-core`, `compiler-agent`, `compiler-ssr`, `compiler-devtools`)\n// will consume them; for v2c-partial only the primitive lands, and one\n// proof-of-concept module exercises it.\n//\n// Design contract (v2c.md §2.1):\n// - Modules NEVER walk the AST themselves — only the registry walks.\n// This keeps the cost O(nodes), not O(modules × nodes).\n// - Visitor order for a given SyntaxKind is the declaration order in\n// `llui.config.ts`'s `modules: [...]` array. Observable to module\n// authors. Alphabetical-by-name was rejected (couples correctness\n// to package names).\n// - Emission conflicts (two modules writing to the same field) are\n// a hard error, not a silent overwrite. Each module owns disjoint\n// output fields.\n// - `runtimeImports` arrays merge by union (deduplicated). Multiple\n// modules requesting the same runtime helper collapse to one\n// import.\n\nimport ts from 'typescript'\nimport type { Diagnostic } from './diagnostic.js'\n\n// ── Module interface ────────────────────────────────────────────────\n\nexport interface DiagnosticDefinition {\n /** Stable id, e.g. `llui/opaque-view-call`. Per v2c §3 §8.2. */\n id: string\n /** One-line description; useful for adapter UIs that don't render the message. */\n description: string\n}\n\n/**\n * Per-file analysis output. Modules accumulate findings here during\n * visitor dispatch; emit consumes it. The shape is intentionally\n * open-ended — modules name their own slots and the umbrella's\n * orchestrator never inspects them, only forwards.\n */\nexport interface FileAnalysis {\n /** Source file the analysis ran over. */\n sourceFile: ts.SourceFile\n /** Per-module accumulator buckets, keyed by module name. */\n perModule: Map<string, unknown>\n /** Diagnostics emitted during the walk. */\n diagnostics: Diagnostic[]\n}\n\n/**\n * Context passed to every visitor invocation. Modules use it to record\n * findings, emit diagnostics, and consult shared state (the TS\n * Compiler-API checker, the project root, sibling-module findings if\n * dependencies allow).\n */\n/**\n * Resolved external type sources for the file under analysis. Same\n * shape as `transform.ts`'s `ExternalTypeSources`; declared here as a\n * structural minimum so the module registry doesn't import from the\n * umbrella. The host adapter (vite-plugin) supplies the values via\n * its async cross-file resolver (`findTypeSource`).\n *\n * Always undefined for test-only `transformLlui(source, fileName)`\n * invocations and for lint adapters without import resolution. Modules\n * that consume this should fall back to file-local behaviour when\n * absent.\n */\nexport interface ModuleExternalTypes {\n state?: { source: string; typeName: string }\n msg?: { source: string; typeName: string }\n effect?: { source: string; typeName: string }\n}\n\nexport interface AnalysisContext {\n sourceFile: ts.SourceFile\n /** TS TypeChecker, when the host adapter has built a Program. May be undefined for AST-only paths. */\n checker: ts.TypeChecker | undefined\n /**\n * Get the named module's accumulator slot (creating it lazily). The\n * slot is whatever shape the module wrote; type-safe access is the\n * module author's responsibility — typically via a typed `get<T>()`\n * wrapper exported alongside the module.\n */\n getSlot<T>(moduleName: string, init: () => T): T\n /** Record a diagnostic. The diagnostic's `id` should match one declared in `DiagnosticDefinition[]`. */\n reportDiagnostic(d: Diagnostic): void\n /**\n * External type sources from the host adapter's cross-file resolver.\n * Undefined when the host doesn't supply them (test path, lint-only\n * adapters without import resolution).\n */\n externalTypes?: ModuleExternalTypes\n}\n\nexport interface EmissionContribution {\n /** Module emitting this contribution — used for conflict reporting. */\n module: string\n /** Field name on the `ComponentDef` object literal (e.g. `__msgSchema`). */\n field: string\n /** AST expression to assign. The umbrella merges into the component()'s config arg. */\n value: ts.Expression\n /**\n * Optional per-call target. When set, this contribution applies only\n * to the named `component()` call expression; the umbrella's\n * emission-merger writes the field into that call's config-arg\n * object literal. When omitted, the contribution is *file-global*:\n * the merger writes the field into every `component()` call in the\n * file (the common case — `__msgSchema`, `__prefixes`, `__schemaHash`\n * are file-shape-derived).\n *\n * Per-call target is needed for `__componentMeta` (file + line vary\n * per call site) and any other field whose value depends on the\n * specific `component()` call location.\n *\n * Conflict-detection runs per-(field, target) tuple — two modules\n * may both contribute `__custom` if they target *different* call\n * expressions; same target on the same field is still an error.\n */\n target?: ts.CallExpression\n}\n\nexport interface EmissionContext {\n sourceFile: ts.SourceFile\n factory: ts.NodeFactory\n}\n\n/**\n * A compiler module declares:\n * - identification (name, compilerVersion semver against the umbrella);\n * - the diagnostics it can emit (stable IDs);\n * - per-`SyntaxKind` visitor handlers (the walker dispatches each AST\n * node once; every module with a handler for its kind sees it);\n * - optionally, an `emit` function that contributes ComponentDef fields\n * after the walk completes;\n * - optionally, `runtimeImports` declaring which `@llui/dom` symbols\n * its emissions reference.\n */\nexport interface CompilerModule {\n name: string\n /** Semver range against the compiler API. v2c §5. */\n compilerVersion: string\n /** Modules this one depends on. The registry verifies presence at activation. */\n dependsOn?: string[]\n diagnostics: DiagnosticDefinition[]\n /**\n * Optional AST pre-transform. Called once per file BEFORE the\n * visitor walk and emission phase. Returns a (possibly rewritten)\n * SourceFile; the result is threaded through subsequent modules'\n * pre-transforms (in declaration order) and then becomes the file\n * the visitor walks. Use for AST mutations the visitor model can't\n * cleanly express — adjacent statement insertion, wrapping arrow\n * expressions, etc. The agent's connect-pattern pass and the\n * universal handler-tagger are the canonical examples (MODULE-MAPPING.md\n * binding-descriptors entry).\n *\n * Most modules do NOT need this. Visitor + emit is the preferred\n * shape because it composes deterministically across modules without\n * threading a mutable SourceFile through each one. preTransform\n * exists for the cases where AST mutation is unavoidable.\n *\n * The §2.1 \"walker runs once per file\" invariant is preserved: the\n * VISITOR walk runs once. preTransform passes are additional, but\n * they're typically cheap (targeted call-site rewrites, not deep\n * recursive walks) and execute before the single visitor walk.\n */\n preTransform?(ctx: PreTransformContext, sf: ts.SourceFile): ts.SourceFile\n visitors: {\n [K in ts.SyntaxKind]?: (ctx: AnalysisContext, node: ts.Node) => void\n }\n /**\n * Optional per-call AST rewrite, BOTTOM-UP (after children visited).\n * Called once per `CallExpression` during the post-visitor transform\n * phase, AFTER analysis has accumulated findings in\n * `analysis.perModule` AND after `ts.visitEachChild` has recursively\n * rewritten the node's children. Returns either:\n * - `null` — node unchanged; chain continues with the next module's\n * transformCall (if any).\n * - a new `ts.CallExpression` — node replaced; subsequent modules'\n * transformCall hooks see the new node (composes in declaration\n * order, just like preTransform).\n *\n * Use for rewrites that depend on the rewritten children — e.g.\n * row-factory emission inspects the render body for an already-emitted\n * `elTemplate(...)` call, so element rewrites that produce\n * `elTemplate` MUST have fired first. Module authors should treat\n * transformCall as a pure function of its inputs (the node + analysis\n * findings).\n */\n transformCall?(ctx: TransformCallContext, node: ts.CallExpression): ts.CallExpression | null\n /**\n * Optional per-call AST rewrite, TOP-DOWN (before children visited).\n * Mirrors `transformCall` but fires BEFORE `ts.visitEachChild`\n * recurses into the call's children. Use when the rewrite must happen\n * before the children are visited — most commonly when the rewrite\n * changes the call's argument shape and the children's visitor would\n * misinterpret the original shape. Memo-wrapping the `items:`\n * accessor of an `each()` call is the canonical example: the wrapped\n * accessor is what subsequent passes (item-selector dedup, mask\n * injection) read.\n *\n * Both `transformCallEnter` and `transformCall` may be declared by\n * the same module; enter fires top-down before recursion, transformCall\n * fires bottom-up after. Ordering within each direction is declaration\n * order across modules; the two directions never interleave for a\n * given node.\n */\n transformCallEnter?(ctx: TransformCallContext, node: ts.CallExpression): ts.CallExpression | null\n /** Called once per file after the visitor pass completes. Returns this module's emission contributions. */\n emit?(ctx: EmissionContext, analysis: FileAnalysis): EmissionContribution[]\n /** Runtime symbol names this module's emissions reference (from `@llui/dom`). */\n runtimeImports?: string[]\n}\n\nexport interface PreTransformContext {\n factory: ts.NodeFactory\n /**\n * Shared per-file findings accumulator. preTransform passes that\n * need to communicate with their own emit step (e.g. \"this file\n * needed scope-variant registrations\") use this slot map. The same\n * `analysis.perModule` map is later passed to visitors and emit.\n */\n analysis: FileAnalysis\n}\n\n/**\n * Context passed to every `transformCall` invocation. Carries the\n * factory for building new AST nodes and a read-only view of analysis\n * findings (visitors have already completed and populated\n * `analysis.perModule` by the time transformCall fires).\n */\nexport interface TransformCallContext {\n factory: ts.NodeFactory\n /** Read-only access to visitor-phase findings. */\n analysis: FileAnalysis\n}\n\n// ── Registry ────────────────────────────────────────────────────────\n\nexport interface RegistryRunResult {\n analysis: FileAnalysis\n emissions: EmissionContribution[]\n /** Union of runtime imports from every active module. */\n runtimeImports: string[]\n}\n\n/**\n * The visitor registry. Built once per compiler boot from the user's\n * `llui.config.ts` `modules: [...]` array; the umbrella's per-file\n * pipeline calls `run(sourceFile, checker)` to drive a complete pass.\n */\nexport class ModuleRegistry {\n private readonly modules: ReadonlyArray<CompilerModule>\n /** Pre-indexed by SyntaxKind for O(1) dispatch. */\n private readonly visitorsByKind: Map<ts.SyntaxKind, Array<CompilerModule>>\n\n constructor(modules: ReadonlyArray<CompilerModule>) {\n this.modules = modules\n this.verifyDependencies()\n this.visitorsByKind = this.buildVisitorIndex()\n }\n\n private verifyDependencies(): void {\n const present = new Set(this.modules.map((m) => m.name))\n for (const m of this.modules) {\n for (const dep of m.dependsOn ?? []) {\n if (!present.has(dep)) {\n throw new Error(\n `[llui] module \"${m.name}\" depends on \"${dep}\", which is not in the active module list. ` +\n `Add ${dep}() to your llui.config.ts modules array (must appear before \"${m.name}\"). ` +\n `See docs/proposals/v2-compiler/v2c.md §2.4.`,\n )\n }\n }\n }\n }\n\n private buildVisitorIndex(): Map<ts.SyntaxKind, Array<CompilerModule>> {\n const index = new Map<ts.SyntaxKind, Array<CompilerModule>>()\n for (const m of this.modules) {\n for (const kindStr of Object.keys(m.visitors)) {\n const kind = Number(kindStr) as ts.SyntaxKind\n if (!index.has(kind)) index.set(kind, [])\n index.get(kind)!.push(m)\n }\n }\n return index\n }\n\n /**\n * Run a full analysis + emission pass over `sourceFile`. Phases:\n * 1. Pre-transform: each module's `preTransform?` fires in\n * declaration order; the (possibly rewritten) SourceFile flows\n * through subsequent passes.\n * 2. Visitor walk: a single AST walk dispatches each node to every\n * module's matching SyntaxKind handler. Read-only — visitors\n * accumulate findings in `analysis.perModule` but cannot rewrite.\n * 3. Transform: a `ts.transform`-style walk dispatches each\n * `CallExpression` to every module's `transformCallEnter?`\n * (top-down, before children recursion) and `transformCall?`\n * (bottom-up, after children recursion) hooks in declaration\n * order; each hook's return value (if non-null) feeds the next.\n * Composes call-site rewrites without each module paying a\n * whole-file walk cost.\n * 4. Emission: each module's `emit?` fires; the registry merges\n * contributions, detecting (field, target) conflicts.\n */\n run(\n sourceFile: ts.SourceFile,\n checker?: ts.TypeChecker,\n externalTypes?: ModuleExternalTypes,\n ): RegistryRunResult {\n const analysis: FileAnalysis = {\n sourceFile,\n perModule: new Map(),\n diagnostics: [],\n }\n\n // Phase 1: pre-transform passes. Threaded SourceFile flows through\n // each module's preTransform in declaration order. Modules without\n // a preTransform pass through.\n let currentSf = sourceFile\n for (const m of this.modules) {\n if (!m.preTransform) continue\n currentSf = m.preTransform(\n {\n factory: ts.factory,\n analysis,\n },\n currentSf,\n )\n }\n analysis.sourceFile = currentSf\n const ctx: AnalysisContext = {\n sourceFile: currentSf,\n checker,\n getSlot: <T>(name: string, init: () => T): T => {\n let slot = analysis.perModule.get(name) as T | undefined\n if (slot === undefined) {\n slot = init()\n analysis.perModule.set(name, slot)\n }\n return slot\n },\n reportDiagnostic: (d) => {\n analysis.diagnostics.push(d)\n },\n externalTypes,\n }\n\n // Phase 2: single-pass visitor walk over the (possibly\n // pre-transformed) SourceFile.\n const walk = (node: ts.Node): void => {\n const handlers = this.visitorsByKind.get(node.kind)\n if (handlers) {\n for (const m of handlers) {\n const handler = m.visitors[node.kind]\n if (handler) handler(ctx, node)\n }\n }\n ts.forEachChild(node, walk)\n }\n walk(currentSf)\n\n // Phase 2b: per-CallExpression transform. Modules with a\n // `transformCallEnter` (top-down) or `transformCall` (bottom-up)\n // hook get one chance to rewrite each call site per direction;\n // chained in declaration order. The phase is skipped entirely\n // when no module declares either hook (zero overhead for the\n // common case of metadata-only modules).\n //\n // Within a single CallExpression visit:\n // 1. transformCallEnter fires (declaration order) — rewrites\n // the node BEFORE children are recursed; subsequent\n // transformCallEnter hooks see the rewritten node.\n // 2. ts.visitEachChild recurses into the (possibly enter-rewritten)\n // node, visiting children.\n // 3. transformCall fires (declaration order) — rewrites the\n // now-children-rewritten node; subsequent transformCall hooks\n // see the result.\n //\n // The two directions never interleave for a given node: all enters\n // run, then all children visit, then all exits run.\n const enterModules = this.modules.filter((m) => m.transformCallEnter)\n const exitModules = this.modules.filter((m) => m.transformCall)\n if (enterModules.length > 0 || exitModules.length > 0) {\n const transformCtx: TransformCallContext = {\n factory: ts.factory,\n analysis,\n }\n const visit: ts.Visitor = (node) => {\n // Top-down (enter) — fires BEFORE children recursion. Chain\n // composes in declaration order; each enter hook sees the\n // output of the previous one.\n let current = node\n if (ts.isCallExpression(current)) {\n for (const m of enterModules) {\n const replaced = m.transformCallEnter!(transformCtx, current as ts.CallExpression)\n if (replaced) current = replaced\n }\n }\n // Recurse children of the (possibly enter-rewritten) node.\n const visited = ts.visitEachChild(current, visit, undefined!)\n // Bottom-up (exit) — fires AFTER children recursion. Chain\n // composes in declaration order; each exit hook sees the\n // output of the previous one.\n if (ts.isCallExpression(visited)) {\n let result = visited as ts.CallExpression\n for (const m of exitModules) {\n const replaced = m.transformCall!(transformCtx, result)\n if (replaced) result = replaced\n }\n return result\n }\n return visited\n }\n currentSf = ts.visitNode(currentSf, visit) as ts.SourceFile\n analysis.sourceFile = currentSf\n }\n\n // Phase 3: emission. Each module contributes after analysis\n // completes. Conflicts on (field, target) tuples are hard errors\n // per §2.1.\n const emissionCtx: EmissionContext = {\n sourceFile: currentSf,\n factory: ts.factory,\n }\n const emissions: EmissionContribution[] = []\n // Conflict detection keyed by `(field, target)` — two modules may\n // contribute distinct per-target emissions to the same field name\n // (e.g. component-meta for two different `component()` calls in\n // one file), but two emissions with the same target on the same\n // field is the hard error from §2.1.\n //\n // Targets are compared by object identity (not by `pos`/`end`) so\n // synthetic nodes (factory-created with pos=-1) compare correctly.\n // File-global emissions (target=undefined) share one bucket.\n const globalOwnerByField = new Map<string, string>()\n const targetOwnerByField = new Map<ts.CallExpression, Map<string, string>>()\n for (const m of this.modules) {\n if (!m.emit) continue\n const contributions = m.emit(emissionCtx, analysis)\n for (const c of contributions) {\n let owners: Map<string, string>\n if (c.target) {\n let existing = targetOwnerByField.get(c.target)\n if (!existing) {\n existing = new Map()\n targetOwnerByField.set(c.target, existing)\n }\n owners = existing\n } else {\n owners = globalOwnerByField\n }\n const other = owners.get(c.field)\n if (other !== undefined) {\n throw new Error(\n `[llui/module-emission-conflict] Modules \"${other}\" and \"${c.module}\" both ` +\n `contribute to ComponentDef field \"${c.field}\"${c.target ? ' for the same component() call site' : ''}. ` +\n `This is a hard error — each (field, target) pair must be owned by exactly one ` +\n `module. Either deduplicate, or move one emission to a distinct field. See ` +\n `docs/proposals/v2-compiler/v2c.md §2.1.`,\n )\n }\n owners.set(c.field, c.module)\n emissions.push(c)\n }\n }\n\n // Union runtime imports.\n const runtimeImports = new Set<string>()\n for (const m of this.modules) {\n for (const imp of m.runtimeImports ?? []) runtimeImports.add(imp)\n }\n\n return {\n analysis,\n emissions,\n runtimeImports: [...runtimeImports].sort(),\n }\n }\n\n /** Module names in declaration order. Adapters surface this for debug logs / config diagnostics. */\n listModules(): string[] {\n return this.modules.map((m) => m.name)\n }\n\n /** All diagnostic definitions across active modules. Used by adapters to enumerate stable IDs. */\n listDiagnostics(): DiagnosticDefinition[] {\n return this.modules.flatMap((m) => m.diagnostics)\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"module.js","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"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","sourcesContent":["// CompilerModule + ModuleRegistry — v2c §2 visitor-registry primitive.\n//\n// Modules accumulate findings during a single AST walk per file and\n// contribute emissions after the walk completes. The walker visits each\n// node once; every module registered for that node's SyntaxKind sees it.\n//\n// This file defines the interfaces + the registry. The actual modules\n// (`compiler-core`, `compiler-agent`, `compiler-ssr`, `compiler-devtools`)\n// will consume them; for v2c-partial only the primitive lands, and one\n// proof-of-concept module exercises it.\n//\n// Design contract (v2c.md §2.1):\n// - Modules NEVER walk the AST themselves — only the registry walks.\n// This keeps the cost O(nodes), not O(modules × nodes).\n// - Visitor order for a given SyntaxKind is the declaration order in\n// `llui.config.ts`'s `modules: [...]` array. Observable to module\n// authors. Alphabetical-by-name was rejected (couples correctness\n// to package names).\n// - Emission conflicts (two modules writing to the same field) are\n// a hard error, not a silent overwrite. Each module owns disjoint\n// output fields.\n// - `runtimeImports` arrays merge by union (deduplicated). Multiple\n// modules requesting the same runtime helper collapse to one\n// import.\n\nimport ts from 'typescript'\nimport type { Diagnostic } from './diagnostic.js'\n\n// ── Module interface ────────────────────────────────────────────────\n\nexport interface DiagnosticDefinition {\n /** Stable id, e.g. `llui/opaque-view-call`. Per v2c §3 §8.2. */\n id: string\n /** One-line description; useful for adapter UIs that don't render the message. */\n description: string\n}\n\n/**\n * Per-file analysis output. Modules accumulate findings here during\n * visitor dispatch; emit consumes it. The shape is intentionally\n * open-ended — modules name their own slots and the umbrella's\n * orchestrator never inspects them, only forwards.\n */\nexport interface FileAnalysis {\n /** Source file the analysis ran over. */\n sourceFile: ts.SourceFile\n /** Per-module accumulator buckets, keyed by module name. */\n perModule: Map<string, unknown>\n /** Diagnostics emitted during the walk. */\n diagnostics: Diagnostic[]\n}\n\n/**\n * Context passed to every visitor invocation. Modules use it to record\n * findings, emit diagnostics, and consult shared state (the TS\n * Compiler-API checker, the project root, sibling-module findings if\n * dependencies allow).\n */\n/**\n * Resolved external type sources for the file under analysis. Same\n * shape as `transform.ts`'s `ExternalTypeSources`; declared here as a\n * structural minimum so the module registry doesn't import from the\n * umbrella. The host adapter (vite-plugin) supplies the values via\n * its async cross-file resolver (`findTypeSource`).\n *\n * Always undefined for test-only `transformLlui(source, fileName)`\n * invocations and for lint adapters without import resolution. Modules\n * that consume this should fall back to file-local behaviour when\n * absent.\n */\nexport interface ModuleExternalTypes {\n state?: { source: string; typeName: string }\n msg?: { source: string; typeName: string }\n effect?: { source: string; typeName: string }\n}\n\nexport interface AnalysisContext {\n sourceFile: ts.SourceFile\n /** TS TypeChecker, when the host adapter has built a Program. May be undefined for AST-only paths. */\n checker: ts.TypeChecker | undefined\n /**\n * The cross-file Program the checker is bound to, when available.\n * Modules that need to resolve identifiers across files (e.g. the\n * opaque-state-flow lint walking through imported helpers) must walk\n * Program-bound nodes — the file the registry hands them is a\n * locally-reparsed copy and its identifiers won't resolve through the\n * checker. Use `program.getSourceFile(sourceFile.fileName)` to fetch\n * the Program-bound counterpart. Undefined when the host doesn't\n * supply a Program (test path, lint adapters without cross-file).\n */\n program: ts.Program | undefined\n /**\n * Get the named module's accumulator slot (creating it lazily). The\n * slot is whatever shape the module wrote; type-safe access is the\n * module author's responsibility — typically via a typed `get<T>()`\n * wrapper exported alongside the module.\n */\n getSlot<T>(moduleName: string, init: () => T): T\n /** Record a diagnostic. The diagnostic's `id` should match one declared in `DiagnosticDefinition[]`. */\n reportDiagnostic(d: Diagnostic): void\n /**\n * External type sources from the host adapter's cross-file resolver.\n * Undefined when the host doesn't supply them (test path, lint-only\n * adapters without import resolution).\n */\n externalTypes?: ModuleExternalTypes\n}\n\nexport interface EmissionContribution {\n /** Module emitting this contribution — used for conflict reporting. */\n module: string\n /** Field name on the `ComponentDef` object literal (e.g. `__msgSchema`). */\n field: string\n /** AST expression to assign. The umbrella merges into the component()'s config arg. */\n value: ts.Expression\n /**\n * Optional per-call target. When set, this contribution applies only\n * to the named `component()` call expression; the umbrella's\n * emission-merger writes the field into that call's config-arg\n * object literal. When omitted, the contribution is *file-global*:\n * the merger writes the field into every `component()` call in the\n * file (the common case — `__msgSchema`, `__prefixes`, `__schemaHash`\n * are file-shape-derived).\n *\n * Per-call target is needed for `__componentMeta` (file + line vary\n * per call site) and any other field whose value depends on the\n * specific `component()` call location.\n *\n * Conflict-detection runs per-(field, target) tuple — two modules\n * may both contribute `__custom` if they target *different* call\n * expressions; same target on the same field is still an error.\n */\n target?: ts.CallExpression\n}\n\nexport interface EmissionContext {\n sourceFile: ts.SourceFile\n factory: ts.NodeFactory\n}\n\n/**\n * A compiler module declares:\n * - identification (name, compilerVersion semver against the umbrella);\n * - the diagnostics it can emit (stable IDs);\n * - per-`SyntaxKind` visitor handlers (the walker dispatches each AST\n * node once; every module with a handler for its kind sees it);\n * - optionally, an `emit` function that contributes ComponentDef fields\n * after the walk completes;\n * - optionally, `runtimeImports` declaring which `@llui/dom` symbols\n * its emissions reference.\n */\nexport interface CompilerModule {\n name: string\n /** Semver range against the compiler API. v2c §5. */\n compilerVersion: string\n /** Modules this one depends on. The registry verifies presence at activation. */\n dependsOn?: string[]\n diagnostics: DiagnosticDefinition[]\n /**\n * Optional AST pre-transform. Called once per file BEFORE the\n * visitor walk and emission phase. Returns a (possibly rewritten)\n * SourceFile; the result is threaded through subsequent modules'\n * pre-transforms (in declaration order) and then becomes the file\n * the visitor walks. Use for AST mutations the visitor model can't\n * cleanly express — adjacent statement insertion, wrapping arrow\n * expressions, etc. The agent's connect-pattern pass and the\n * universal handler-tagger are the canonical examples (MODULE-MAPPING.md\n * binding-descriptors entry).\n *\n * Most modules do NOT need this. Visitor + emit is the preferred\n * shape because it composes deterministically across modules without\n * threading a mutable SourceFile through each one. preTransform\n * exists for the cases where AST mutation is unavoidable.\n *\n * The §2.1 \"walker runs once per file\" invariant is preserved: the\n * VISITOR walk runs once. preTransform passes are additional, but\n * they're typically cheap (targeted call-site rewrites, not deep\n * recursive walks) and execute before the single visitor walk.\n */\n preTransform?(ctx: PreTransformContext, sf: ts.SourceFile): ts.SourceFile\n visitors: {\n [K in ts.SyntaxKind]?: (ctx: AnalysisContext, node: ts.Node) => void\n }\n /**\n * Optional per-call AST rewrite, BOTTOM-UP (after children visited).\n * Called once per `CallExpression` during the post-visitor transform\n * phase, AFTER analysis has accumulated findings in\n * `analysis.perModule` AND after `ts.visitEachChild` has recursively\n * rewritten the node's children. Returns either:\n * - `null` — node unchanged; chain continues with the next module's\n * transformCall (if any).\n * - a new `ts.CallExpression` — node replaced; subsequent modules'\n * transformCall hooks see the new node (composes in declaration\n * order, just like preTransform).\n *\n * Use for rewrites that depend on the rewritten children — e.g.\n * row-factory emission inspects the render body for an already-emitted\n * `elTemplate(...)` call, so element rewrites that produce\n * `elTemplate` MUST have fired first. Module authors should treat\n * transformCall as a pure function of its inputs (the node + analysis\n * findings).\n */\n transformCall?(ctx: TransformCallContext, node: ts.CallExpression): ts.CallExpression | null\n /**\n * Optional per-call AST rewrite, TOP-DOWN (before children visited).\n * Mirrors `transformCall` but fires BEFORE `ts.visitEachChild`\n * recurses into the call's children. Use when the rewrite must happen\n * before the children are visited — most commonly when the rewrite\n * changes the call's argument shape and the children's visitor would\n * misinterpret the original shape. Memo-wrapping the `items:`\n * accessor of an `each()` call is the canonical example: the wrapped\n * accessor is what subsequent passes (item-selector dedup, mask\n * injection) read.\n *\n * Both `transformCallEnter` and `transformCall` may be declared by\n * the same module; enter fires top-down before recursion, transformCall\n * fires bottom-up after. Ordering within each direction is declaration\n * order across modules; the two directions never interleave for a\n * given node.\n */\n transformCallEnter?(ctx: TransformCallContext, node: ts.CallExpression): ts.CallExpression | null\n /** Called once per file after the visitor pass completes. Returns this module's emission contributions. */\n emit?(ctx: EmissionContext, analysis: FileAnalysis): EmissionContribution[]\n /** Runtime symbol names this module's emissions reference (from `@llui/dom`). */\n runtimeImports?: string[]\n}\n\nexport interface PreTransformContext {\n factory: ts.NodeFactory\n /**\n * Shared per-file findings accumulator. preTransform passes that\n * need to communicate with their own emit step (e.g. \"this file\n * needed scope-variant registrations\") use this slot map. The same\n * `analysis.perModule` map is later passed to visitors and emit.\n */\n analysis: FileAnalysis\n}\n\n/**\n * Context passed to every `transformCall` invocation. Carries the\n * factory for building new AST nodes and a read-only view of analysis\n * findings (visitors have already completed and populated\n * `analysis.perModule` by the time transformCall fires).\n */\nexport interface TransformCallContext {\n factory: ts.NodeFactory\n /** Read-only access to visitor-phase findings. */\n analysis: FileAnalysis\n}\n\n// ── Registry ────────────────────────────────────────────────────────\n\nexport interface RegistryRunResult {\n analysis: FileAnalysis\n emissions: EmissionContribution[]\n /** Union of runtime imports from every active module. */\n runtimeImports: string[]\n}\n\n/**\n * The visitor registry. Built once per compiler boot from the user's\n * `llui.config.ts` `modules: [...]` array; the umbrella's per-file\n * pipeline calls `run(sourceFile, checker)` to drive a complete pass.\n */\nexport class ModuleRegistry {\n private readonly modules: ReadonlyArray<CompilerModule>\n /** Pre-indexed by SyntaxKind for O(1) dispatch. */\n private readonly visitorsByKind: Map<ts.SyntaxKind, Array<CompilerModule>>\n\n constructor(modules: ReadonlyArray<CompilerModule>) {\n this.modules = modules\n this.verifyDependencies()\n this.visitorsByKind = this.buildVisitorIndex()\n }\n\n private verifyDependencies(): void {\n const present = new Set(this.modules.map((m) => m.name))\n for (const m of this.modules) {\n for (const dep of m.dependsOn ?? []) {\n if (!present.has(dep)) {\n throw new Error(\n `[llui] module \"${m.name}\" depends on \"${dep}\", which is not in the active module list. ` +\n `Add ${dep}() to your llui.config.ts modules array (must appear before \"${m.name}\"). ` +\n `See docs/proposals/v2-compiler/v2c.md §2.4.`,\n )\n }\n }\n }\n }\n\n private buildVisitorIndex(): Map<ts.SyntaxKind, Array<CompilerModule>> {\n const index = new Map<ts.SyntaxKind, Array<CompilerModule>>()\n for (const m of this.modules) {\n for (const kindStr of Object.keys(m.visitors)) {\n const kind = Number(kindStr) as ts.SyntaxKind\n if (!index.has(kind)) index.set(kind, [])\n index.get(kind)!.push(m)\n }\n }\n return index\n }\n\n /**\n * Run a full analysis + emission pass over `sourceFile`. Phases:\n * 1. Pre-transform: each module's `preTransform?` fires in\n * declaration order; the (possibly rewritten) SourceFile flows\n * through subsequent passes.\n * 2. Visitor walk: a single AST walk dispatches each node to every\n * module's matching SyntaxKind handler. Read-only — visitors\n * accumulate findings in `analysis.perModule` but cannot rewrite.\n * 3. Transform: a `ts.transform`-style walk dispatches each\n * `CallExpression` to every module's `transformCallEnter?`\n * (top-down, before children recursion) and `transformCall?`\n * (bottom-up, after children recursion) hooks in declaration\n * order; each hook's return value (if non-null) feeds the next.\n * Composes call-site rewrites without each module paying a\n * whole-file walk cost.\n * 4. Emission: each module's `emit?` fires; the registry merges\n * contributions, detecting (field, target) conflicts.\n */\n run(\n sourceFile: ts.SourceFile,\n checker?: ts.TypeChecker,\n externalTypes?: ModuleExternalTypes,\n program?: ts.Program,\n ): RegistryRunResult {\n const analysis: FileAnalysis = {\n sourceFile,\n perModule: new Map(),\n diagnostics: [],\n }\n\n // Phase 1: pre-transform passes. Threaded SourceFile flows through\n // each module's preTransform in declaration order. Modules without\n // a preTransform pass through.\n let currentSf = sourceFile\n for (const m of this.modules) {\n if (!m.preTransform) continue\n currentSf = m.preTransform(\n {\n factory: ts.factory,\n analysis,\n },\n currentSf,\n )\n }\n analysis.sourceFile = currentSf\n const ctx: AnalysisContext = {\n sourceFile: currentSf,\n checker,\n program,\n getSlot: <T>(name: string, init: () => T): T => {\n let slot = analysis.perModule.get(name) as T | undefined\n if (slot === undefined) {\n slot = init()\n analysis.perModule.set(name, slot)\n }\n return slot\n },\n reportDiagnostic: (d) => {\n analysis.diagnostics.push(d)\n },\n externalTypes,\n }\n\n // Phase 2: single-pass visitor walk over the (possibly\n // pre-transformed) SourceFile.\n const walk = (node: ts.Node): void => {\n const handlers = this.visitorsByKind.get(node.kind)\n if (handlers) {\n for (const m of handlers) {\n const handler = m.visitors[node.kind]\n if (handler) handler(ctx, node)\n }\n }\n ts.forEachChild(node, walk)\n }\n walk(currentSf)\n\n // Phase 2b: per-CallExpression transform. Modules with a\n // `transformCallEnter` (top-down) or `transformCall` (bottom-up)\n // hook get one chance to rewrite each call site per direction;\n // chained in declaration order. The phase is skipped entirely\n // when no module declares either hook (zero overhead for the\n // common case of metadata-only modules).\n //\n // Within a single CallExpression visit:\n // 1. transformCallEnter fires (declaration order) — rewrites\n // the node BEFORE children are recursed; subsequent\n // transformCallEnter hooks see the rewritten node.\n // 2. ts.visitEachChild recurses into the (possibly enter-rewritten)\n // node, visiting children.\n // 3. transformCall fires (declaration order) — rewrites the\n // now-children-rewritten node; subsequent transformCall hooks\n // see the result.\n //\n // The two directions never interleave for a given node: all enters\n // run, then all children visit, then all exits run.\n const enterModules = this.modules.filter((m) => m.transformCallEnter)\n const exitModules = this.modules.filter((m) => m.transformCall)\n if (enterModules.length > 0 || exitModules.length > 0) {\n const transformCtx: TransformCallContext = {\n factory: ts.factory,\n analysis,\n }\n const visit: ts.Visitor = (node) => {\n // Top-down (enter) — fires BEFORE children recursion. Chain\n // composes in declaration order; each enter hook sees the\n // output of the previous one.\n let current = node\n if (ts.isCallExpression(current)) {\n for (const m of enterModules) {\n const replaced = m.transformCallEnter!(transformCtx, current as ts.CallExpression)\n if (replaced) current = replaced\n }\n }\n // Recurse children of the (possibly enter-rewritten) node.\n const visited = ts.visitEachChild(current, visit, undefined!)\n // Bottom-up (exit) — fires AFTER children recursion. Chain\n // composes in declaration order; each exit hook sees the\n // output of the previous one.\n if (ts.isCallExpression(visited)) {\n let result = visited as ts.CallExpression\n for (const m of exitModules) {\n const replaced = m.transformCall!(transformCtx, result)\n if (replaced) result = replaced\n }\n return result\n }\n return visited\n }\n currentSf = ts.visitNode(currentSf, visit) as ts.SourceFile\n analysis.sourceFile = currentSf\n }\n\n // Phase 3: emission. Each module contributes after analysis\n // completes. Conflicts on (field, target) tuples are hard errors\n // per §2.1.\n const emissionCtx: EmissionContext = {\n sourceFile: currentSf,\n factory: ts.factory,\n }\n const emissions: EmissionContribution[] = []\n // Conflict detection keyed by `(field, target)` — two modules may\n // contribute distinct per-target emissions to the same field name\n // (e.g. component-meta for two different `component()` calls in\n // one file), but two emissions with the same target on the same\n // field is the hard error from §2.1.\n //\n // Targets are compared by object identity (not by `pos`/`end`) so\n // synthetic nodes (factory-created with pos=-1) compare correctly.\n // File-global emissions (target=undefined) share one bucket.\n const globalOwnerByField = new Map<string, string>()\n const targetOwnerByField = new Map<ts.CallExpression, Map<string, string>>()\n for (const m of this.modules) {\n if (!m.emit) continue\n const contributions = m.emit(emissionCtx, analysis)\n for (const c of contributions) {\n let owners: Map<string, string>\n if (c.target) {\n let existing = targetOwnerByField.get(c.target)\n if (!existing) {\n existing = new Map()\n targetOwnerByField.set(c.target, existing)\n }\n owners = existing\n } else {\n owners = globalOwnerByField\n }\n const other = owners.get(c.field)\n if (other !== undefined) {\n throw new Error(\n `[llui/module-emission-conflict] Modules \"${other}\" and \"${c.module}\" both ` +\n `contribute to ComponentDef field \"${c.field}\"${c.target ? ' for the same component() call site' : ''}. ` +\n `This is a hard error — each (field, target) pair must be owned by exactly one ` +\n `module. Either deduplicate, or move one emission to a distinct field. See ` +\n `docs/proposals/v2-compiler/v2c.md §2.1.`,\n )\n }\n owners.set(c.field, c.module)\n emissions.push(c)\n }\n }\n\n // Union runtime imports.\n const runtimeImports = new Set<string>()\n for (const m of this.modules) {\n for (const imp of m.runtimeImports ?? []) runtimeImports.add(imp)\n }\n\n return {\n analysis,\n emissions,\n runtimeImports: [...runtimeImports].sort(),\n }\n }\n\n /** Module names in declaration order. Adapters surface this for debug logs / config diagnostics. */\n listModules(): string[] {\n return this.modules.map((m) => m.name)\n }\n\n /** All diagnostic definitions across active modules. Used by adapters to enumerate stable IDs. */\n listDiagnostics(): DiagnosticDefinition[] {\n return this.modules.flatMap((m) => m.diagnostics)\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-on-state-array.d.ts","sourceRoot":"","sources":["../../src/modules/map-on-state-array.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"map-on-state-array.d.ts","sourceRoot":"","sources":["../../src/modules/map-on-state-array.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAmFlD,wBAAgB,qBAAqB,IAAI,cAAc,CAiDtD"}
|
|
@@ -17,6 +17,58 @@ function findViewProperty(call) {
|
|
|
17
17
|
}
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* True when the `.map()` call sits at any depth inside the `items:`
|
|
22
|
+
* accessor of an enclosing `each({ items, ... })` call. In that
|
|
23
|
+
* position the `.map()`'s array is what `items` is supposed to return —
|
|
24
|
+
* the caller already adopted the `each` pattern. Firing the rule here
|
|
25
|
+
* was a self-referential false positive: the diagnostic told the
|
|
26
|
+
* author to use the very `each` they were inside.
|
|
27
|
+
*
|
|
28
|
+
* The walker stops at function boundaries to avoid attributing nested
|
|
29
|
+
* helper definitions to their enclosing scope, but follows property
|
|
30
|
+
* assignments transparently — the canonical shape is `each({ items:
|
|
31
|
+
* (s) => s.foo.map(...) })`, and the `.map()` lives several AST levels
|
|
32
|
+
* down from the `items:` PropertyAssignment.
|
|
33
|
+
*/
|
|
34
|
+
function isInsideEachItemsAccessor(n) {
|
|
35
|
+
let cur = n.parent;
|
|
36
|
+
// The enclosing function whose body contains the `.map()`. If it
|
|
37
|
+
// turns out to be the `items:` arrow of an `each(...)`, suppress.
|
|
38
|
+
let enclosingFn;
|
|
39
|
+
while (cur) {
|
|
40
|
+
if (ts.isArrowFunction(cur) || ts.isFunctionExpression(cur)) {
|
|
41
|
+
enclosingFn = cur;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
cur = cur.parent;
|
|
45
|
+
}
|
|
46
|
+
if (!enclosingFn)
|
|
47
|
+
return false;
|
|
48
|
+
// The function must be the value of a `items:` PropertyAssignment...
|
|
49
|
+
const pa = enclosingFn.parent;
|
|
50
|
+
if (!pa ||
|
|
51
|
+
!ts.isPropertyAssignment(pa) ||
|
|
52
|
+
!ts.isIdentifier(pa.name) ||
|
|
53
|
+
pa.name.text !== 'items') {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
// ...inside an object literal passed as the first arg to a call
|
|
57
|
+
// whose callee resolves (by name) to `each`. Handles both the
|
|
58
|
+
// bare import (`each({...})`) and the View-bag form (`h.each({...})`).
|
|
59
|
+
const obj = pa.parent;
|
|
60
|
+
if (!obj || !ts.isObjectLiteralExpression(obj))
|
|
61
|
+
return false;
|
|
62
|
+
const call = obj.parent;
|
|
63
|
+
if (!call || !ts.isCallExpression(call) || call.arguments[0] !== obj)
|
|
64
|
+
return false;
|
|
65
|
+
if (ts.isIdentifier(call.expression))
|
|
66
|
+
return call.expression.text === 'each';
|
|
67
|
+
if (ts.isPropertyAccessExpression(call.expression)) {
|
|
68
|
+
return call.expression.name.text === 'each';
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
20
72
|
/**
|
|
21
73
|
* True when `expr` resolves to a state-like reference. We recognize
|
|
22
74
|
* the conventional names — `state`, `s`, `_state` — and chained
|
|
@@ -60,7 +112,8 @@ export function mapOnStateArrayModule() {
|
|
|
60
112
|
ts.isPropertyAccessExpression(n.expression) &&
|
|
61
113
|
ts.isIdentifier(n.expression.name) &&
|
|
62
114
|
n.expression.name.text === 'map' &&
|
|
63
|
-
isStateReference(n.expression.expression)
|
|
115
|
+
isStateReference(n.expression.expression) &&
|
|
116
|
+
!isInsideEachItemsAccessor(n)) {
|
|
64
117
|
ctx.reportDiagnostic({
|
|
65
118
|
id: 'llui/map-on-state-array',
|
|
66
119
|
severity: 'error',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"map-on-state-array.js","sourceRoot":"","sources":["../../src/modules/map-on-state-array.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,kEAAkE;AAClE,sEAAsE;AACtE,oEAAoE;AACpE,8DAA8D;AAE9D,OAAO,EAAE,MAAM,YAAY,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAEnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAEjD,SAAS,gBAAgB,CAAC,IAAuB;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAA;IAC7B,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,yBAAyB,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IAChE,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE,CAAC;QAClC,IAAI,EAAE,CAAC,oBAAoB,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC7F,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,gBAAgB,CAAC,IAAmB;IAC3C,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,IAAI,KAAK,OAAO,IAAI,IAAI,CAAC,IAAI,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAA;IAC7E,CAAC;IACD,IAAI,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,OAAO,gBAAgB,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IAC1C,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,qBAAqB;IACnC,OAAO;QACL,IAAI,EAAE,oBAAoB;QAC1B,eAAe,EAAE,QAAQ;QACzB,WAAW,EAAE;YACX;gBACE,EAAE,EAAE,yBAAyB;gBAC7B,WAAW,EACT,iFAAiF;aACpF;SACF;QACD,QAAQ,EAAE;YACR,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;gBACxC,MAAM,OAAO,GAAG,IAAqB,CAAA;gBACrC,MAAM,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;gBAC5F,KAAK,MAAM,IAAI,IAAI,kBAAkB,CAAC,EAAE,CAAC,EAAE,CAAC;oBAC1C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAA;oBACvC,IAAI,CAAC,QAAQ;wBAAE,SAAQ;oBACvB,MAAM,EAAE,GAAG,QAAQ,CAAC,WAAW,CAAA;oBAC/B,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,oBAAoB,CAAC,EAAE,CAAC;wBAAE,SAAQ;oBAErE,MAAM,IAAI,GAAG,CAAC,CAAU,EAAQ,EAAE;wBAChC,IACE,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC;4BACtB,EAAE,CAAC,0BAA0B,CAAC,CAAC,CAAC,UAAU,CAAC;4BAC3C,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;4BAClC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,KAAK,KAAK;4BAChC,gBAAgB,CAAC,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"map-on-state-array.js","sourceRoot":"","sources":["../../src/modules/map-on-state-array.ts"],"names":[],"mappings":"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","sourcesContent":["// `map-on-state-array` — errors when a state-derived array is iterated\n// with `.map()` inside a view function. The reactive primitive is\n// `each(...)`; `.map()` produces a static list with no per-row scope,\n// no key-based reconciliation, and no precise mask gating. Migrated\n// from `@llui/eslint-plugin/src/rules/map-on-state-array.ts`.\n\nimport ts from 'typescript'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\nimport { findComponentCalls } from './_shared.js'\n\nfunction findViewProperty(call: ts.CallExpression): ts.PropertyAssignment | undefined {\n const arg = call.arguments[0]\n if (!arg || !ts.isObjectLiteralExpression(arg)) return undefined\n for (const prop of arg.properties) {\n if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'view') {\n return prop\n }\n }\n return undefined\n}\n\n/**\n * True when the `.map()` call sits at any depth inside the `items:`\n * accessor of an enclosing `each({ items, ... })` call. In that\n * position the `.map()`'s array is what `items` is supposed to return —\n * the caller already adopted the `each` pattern. Firing the rule here\n * was a self-referential false positive: the diagnostic told the\n * author to use the very `each` they were inside.\n *\n * The walker stops at function boundaries to avoid attributing nested\n * helper definitions to their enclosing scope, but follows property\n * assignments transparently — the canonical shape is `each({ items:\n * (s) => s.foo.map(...) })`, and the `.map()` lives several AST levels\n * down from the `items:` PropertyAssignment.\n */\nfunction isInsideEachItemsAccessor(n: ts.Node): boolean {\n let cur: ts.Node | undefined = n.parent\n // The enclosing function whose body contains the `.map()`. If it\n // turns out to be the `items:` arrow of an `each(...)`, suppress.\n let enclosingFn: ts.ArrowFunction | ts.FunctionExpression | undefined\n while (cur) {\n if (ts.isArrowFunction(cur) || ts.isFunctionExpression(cur)) {\n enclosingFn = cur\n break\n }\n cur = cur.parent\n }\n if (!enclosingFn) return false\n // The function must be the value of a `items:` PropertyAssignment...\n const pa = enclosingFn.parent\n if (\n !pa ||\n !ts.isPropertyAssignment(pa) ||\n !ts.isIdentifier(pa.name) ||\n pa.name.text !== 'items'\n ) {\n return false\n }\n // ...inside an object literal passed as the first arg to a call\n // whose callee resolves (by name) to `each`. Handles both the\n // bare import (`each({...})`) and the View-bag form (`h.each({...})`).\n const obj = pa.parent\n if (!obj || !ts.isObjectLiteralExpression(obj)) return false\n const call = obj.parent\n if (!call || !ts.isCallExpression(call) || call.arguments[0] !== obj) return false\n if (ts.isIdentifier(call.expression)) return call.expression.text === 'each'\n if (ts.isPropertyAccessExpression(call.expression)) {\n return call.expression.name.text === 'each'\n }\n return false\n}\n\n/**\n * True when `expr` resolves to a state-like reference. We recognize\n * the conventional names — `state`, `s`, `_state` — and chained\n * property accesses rooted at one of them (e.g. `s.items.filtered`).\n * Type-aware resolution would be more precise; the conventions are\n * stable enough across the codebase that name matching catches the\n * intended cases without a checker.\n */\nfunction isStateReference(expr: ts.Expression): boolean {\n if (ts.isIdentifier(expr)) {\n return expr.text === 'state' || expr.text === 's' || expr.text === '_state'\n }\n if (ts.isPropertyAccessExpression(expr)) {\n return isStateReference(expr.expression)\n }\n return false\n}\n\nexport function mapOnStateArrayModule(): CompilerModule {\n return {\n name: 'map-on-state-array',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/map-on-state-array',\n description:\n 'Array .map() on a state-derived value in view(). Use each() for reactive lists.',\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const visited = node as ts.SourceFile\n const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n for (const call of findComponentCalls(sf)) {\n const viewProp = findViewProperty(call)\n if (!viewProp) continue\n const fn = viewProp.initializer\n if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn)) continue\n\n const walk = (n: ts.Node): void => {\n if (\n ts.isCallExpression(n) &&\n ts.isPropertyAccessExpression(n.expression) &&\n ts.isIdentifier(n.expression.name) &&\n n.expression.name.text === 'map' &&\n isStateReference(n.expression.expression) &&\n !isInsideEachItemsAccessor(n)\n ) {\n ctx.reportDiagnostic({\n id: 'llui/map-on-state-array',\n severity: 'error',\n category: 'reactivity',\n message:\n 'Array `.map()` on a state-derived value inside view(). `.map()` produces a static list — use `each({ items, key, render })` for reactive lists with per-row scope and key-based reconciliation.',\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, n.getStart(sf), n.getEnd()),\n },\n })\n }\n ts.forEachChild(n, walk)\n }\n if (fn.body) walk(fn.body)\n }\n },\n },\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opaque-state-flow.d.ts","sourceRoot":"","sources":["../../src/modules/opaque-state-flow.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"opaque-state-flow.d.ts","sourceRoot":"","sources":["../../src/modules/opaque-state-flow.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAkLlD,wBAAgB,qBAAqB,IAAI,cAAc,CAiEtD"}
|
|
@@ -43,7 +43,7 @@ import { resolveAccessorBody } from '../accessor-resolver.js';
|
|
|
43
43
|
// framework primitives are visited as accessor positions in their
|
|
44
44
|
// own right, so we don't double-classify.
|
|
45
45
|
const NON_DELEGATION_HELPERS = new Set(['sample', 'item', 'memo', 'text', 'unsafeHtml']);
|
|
46
|
-
function findFirstLeakInAccessor(accessor) {
|
|
46
|
+
function findFirstLeakInAccessor(accessor, checker) {
|
|
47
47
|
if (accessor.parameters.length !== 1)
|
|
48
48
|
return null;
|
|
49
49
|
const param = accessor.parameters[0];
|
|
@@ -80,40 +80,62 @@ function findFirstLeakInAccessor(accessor) {
|
|
|
80
80
|
'replace the dynamic key with a literal property (e.g. `s.foo`), or declare the read via `track({ deps: (s) => [s[key]] })`.';
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
else if (ts.isCallExpression(parent)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
83
|
+
else if (ts.isCallExpression(parent)) {
|
|
84
|
+
const argIndex = parent.arguments.indexOf(node);
|
|
85
|
+
if (argIndex > 0) {
|
|
86
|
+
// State passed as arg1+ to a call. The header documents this
|
|
87
|
+
// as NOT flagged (intentional): the existing delegation
|
|
88
|
+
// branch only attempts to trace arg0, and the mask classifier
|
|
89
|
+
// emits a whole-state sentinel into `__prefixes` so the
|
|
90
|
+
// binding stays correct. The cost is per-update re-evaluation
|
|
91
|
+
// — a property of the composition pattern, not an author
|
|
92
|
+
// mistake worth blocking. Without this branch we'd fall
|
|
93
|
+
// through to the default "outside a tracked container" leak.
|
|
94
|
+
tracked = true;
|
|
95
|
+
}
|
|
96
|
+
else if (argIndex === 0) {
|
|
97
|
+
if (ts.isIdentifier(parent.expression) &&
|
|
98
|
+
!NON_DELEGATION_HELPERS.has(parent.expression.text)) {
|
|
99
|
+
// Identifier-callee delegation. Recurse into the callee's
|
|
100
|
+
// body via the same resolver the mask walker uses. If it
|
|
101
|
+
// resolves to a local accessor, the helper's reads are
|
|
102
|
+
// walked transitively and the call is tracked. If the
|
|
103
|
+
// callee is a function parameter, import, destructured
|
|
104
|
+
// binding, or otherwise unresolvable, this IS the leak
|
|
105
|
+
// shape — flag it here so the diagnostic points at the
|
|
106
|
+
// call site rather than at some deeper unresolvable read.
|
|
107
|
+
const resolved = resolveAccessorBody(parent.expression, checker);
|
|
108
|
+
if (resolved) {
|
|
109
|
+
tracked = true;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const calleeSymbol = checker?.getSymbolAtLocation(parent.expression);
|
|
113
|
+
const isFunctionParam = !!calleeSymbol?.declarations?.some((d) => ts.isParameter(d));
|
|
114
|
+
shape = `call to an unresolvable callee \`${parent.expression.text}(s)\` (function parameter, import, or destructured binding)`;
|
|
115
|
+
if (isFunctionParam) {
|
|
116
|
+
hint =
|
|
117
|
+
'this callee is a function parameter — the closure passed at the call site is opaque to per-binding analysis. The framework expects per-row dynamic state to flow through `each` items (slot data on `item.*`) rather than through `(s) => ...` callback parameters; restructure the helper so its bindings read `item.*` and the call site builds the slot data once in `items: (s) => …`.';
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
hint =
|
|
121
|
+
'inline the read against `s` directly, refactor the callee into a same-module `const`/`function` declaration, or declare the dependencies via `track({ deps: (s) => [...] })`.';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
97
124
|
}
|
|
98
|
-
else {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
125
|
+
else if (!ts.isIdentifier(parent.expression)) {
|
|
126
|
+
// Method-call / computed callee with state arg —
|
|
127
|
+
// `obj.helper(s)`, `lib.fn(s)`. This is the documented
|
|
128
|
+
// headless-components idiom (`pr.valueText(s)` where `pr`
|
|
129
|
+
// comes from `progress.connect()`); refactoring it would
|
|
130
|
+
// defeat the API surface. The runtime sentinel keeps the
|
|
131
|
+
// binding correct — just at the cost of re-evaluating on
|
|
132
|
+
// every update. Treat as tracked from the lint's POV so
|
|
133
|
+
// legitimate composition doesn't error the build; the
|
|
134
|
+
// perf cost is a property of the composition pattern, not
|
|
135
|
+
// an author mistake worth blocking.
|
|
136
|
+
tracked = true;
|
|
102
137
|
}
|
|
103
138
|
}
|
|
104
|
-
else if (!ts.isIdentifier(parent.expression)) {
|
|
105
|
-
// Method-call / computed callee with state arg —
|
|
106
|
-
// `obj.helper(s)`, `lib.fn(s)`. This is the documented
|
|
107
|
-
// headless-components idiom (`pr.valueText(s)` where `pr`
|
|
108
|
-
// comes from `progress.connect()`); refactoring it would
|
|
109
|
-
// defeat the API surface. The runtime sentinel keeps the
|
|
110
|
-
// binding correct — just at the cost of re-evaluating on
|
|
111
|
-
// every update. Treat as tracked from the lint's POV so
|
|
112
|
-
// legitimate composition doesn't error the build; the
|
|
113
|
-
// perf cost is a property of the composition pattern, not
|
|
114
|
-
// an author mistake worth blocking.
|
|
115
|
-
tracked = true;
|
|
116
|
-
}
|
|
117
139
|
}
|
|
118
140
|
else if (ts.isNewExpression(parent)) {
|
|
119
141
|
shape = 'state passed as a constructor argument (`new X(s)`)';
|
|
@@ -159,6 +181,34 @@ function findFirstLeakInAccessor(accessor) {
|
|
|
159
181
|
visit(accessor.body);
|
|
160
182
|
return leak;
|
|
161
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* True when `arrow` is the value of a `deps:` PropertyAssignment in
|
|
186
|
+
* a `track({ ... })` call. The diagnostic is suppressed in that
|
|
187
|
+
* position because `track` is the documented escape hatch for cases
|
|
188
|
+
* the walker can't statically infer; firing the lint inside it moves
|
|
189
|
+
* the diagnostic without giving the author a path forward.
|
|
190
|
+
*
|
|
191
|
+
* Handles both forms: bare `track({...})` (import from `@llui/dom`)
|
|
192
|
+
* and the View-bag form `h.track({...})` if it ever exists.
|
|
193
|
+
*/
|
|
194
|
+
function isInsideTrackDeps(arrow) {
|
|
195
|
+
const pa = arrow.parent;
|
|
196
|
+
if (!pa || !ts.isPropertyAssignment(pa) || !ts.isIdentifier(pa.name) || pa.name.text !== 'deps') {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
const obj = pa.parent;
|
|
200
|
+
if (!obj || !ts.isObjectLiteralExpression(obj))
|
|
201
|
+
return false;
|
|
202
|
+
const call = obj.parent;
|
|
203
|
+
if (!call || !ts.isCallExpression(call) || call.arguments[0] !== obj)
|
|
204
|
+
return false;
|
|
205
|
+
if (ts.isIdentifier(call.expression))
|
|
206
|
+
return call.expression.text === 'track';
|
|
207
|
+
if (ts.isPropertyAccessExpression(call.expression)) {
|
|
208
|
+
return call.expression.name.text === 'track';
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
162
212
|
function describe(node) {
|
|
163
213
|
if (ts.isIdentifier(node))
|
|
164
214
|
return node.text;
|
|
@@ -178,11 +228,34 @@ export function opaqueStateFlowModule() {
|
|
|
178
228
|
],
|
|
179
229
|
visitors: {
|
|
180
230
|
[ts.SyntaxKind.SourceFile]: (ctx, node) => {
|
|
231
|
+
// When the host adapter has built a Program, walk the checker's
|
|
232
|
+
// own SourceFile so symbol resolution (Alias → Symbol via
|
|
233
|
+
// `getSymbolAtLocation`) actually works. The reparsed file used
|
|
234
|
+
// in the AST-only fallback is not part of any Program, so the
|
|
235
|
+
// checker can't resolve identifiers in it. Fall back to a
|
|
236
|
+
// reparse for paths without a Program (test harness, lint
|
|
237
|
+
// adapters without cross-file resolution).
|
|
181
238
|
const visited = node;
|
|
182
|
-
const
|
|
239
|
+
const fromProgram = ctx.program?.getSourceFile(visited.fileName);
|
|
240
|
+
const checker = fromProgram ? ctx.checker : undefined;
|
|
241
|
+
const sf = fromProgram ??
|
|
242
|
+
ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true);
|
|
183
243
|
const walk = (n) => {
|
|
184
244
|
if ((ts.isArrowFunction(n) || ts.isFunctionExpression(n)) && isReactiveAccessor(n)) {
|
|
185
|
-
|
|
245
|
+
// `track({ deps: (s) => [...] })` is the user's explicit
|
|
246
|
+
// opt-in for "this binding's reads can't be inferred — trust
|
|
247
|
+
// my declaration." Firing a perf lint inside the user's
|
|
248
|
+
// declaration defeats the primitive's purpose; the
|
|
249
|
+
// diagnostic moves from the original call site to inside
|
|
250
|
+
// track.deps without going away, leaving authors with no
|
|
251
|
+
// recovery path. Suppress here. The mask/path classifier
|
|
252
|
+
// still walks the body for what it can extract; this only
|
|
253
|
+
// silences the lint.
|
|
254
|
+
if (isInsideTrackDeps(n)) {
|
|
255
|
+
ts.forEachChild(n, walk);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const leak = findFirstLeakInAccessor(n, checker);
|
|
186
259
|
if (leak) {
|
|
187
260
|
ctx.reportDiagnostic({
|
|
188
261
|
id: 'llui/opaque-state-flow',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opaque-state-flow.js","sourceRoot":"","sources":["../../src/modules/opaque-state-flow.ts"],"names":[],"mappings":"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","sourcesContent":["// `opaque-state-flow` — errors when a reactive accessor's body flows\n// the state identifier into an expression the walker can't statically\n// trace. The compiler still produces a *correct* binding by forcing\n// FULL_MASK and emitting a whole-state `(s) => s` sentinel into\n// `__prefixes` (see `02 Compiler.md` § \"Opaque-flow classifier\"), but\n// the binding then re-evaluates on every state change rather than\n// only when its actual reads change. This rule surfaces the leak so\n// authors can either:\n//\n// - Rewrite the accessor as direct property access (`s.foo`,\n// `s.foo['literal']`), the form the walker can resolve.\n// - Declare the reads explicitly via `track({ deps: (s) => [...] })`\n// — the compile-time escape hatch the framework provides for cases\n// where the read genuinely can't be expressed inline.\n//\n// Detected leak shapes (mirrors the classifier in\n// `transform.ts:computeAccessorMask`):\n// - `helper(s)` with an Identifier callee that can't be resolved to\n// a local declaration (function parameter, import, destructured\n// binding) — the callee may read any field of `s`.\n//\n// NOT flagged (intentional): `obj.helper(s)` / `lib.fn(s)` —\n// PropertyAccessExpression callees. This is the documented headless-\n// components idiom (`pr.valueText(s)` where `pr` comes from\n// `progress.connect()`), and refactoring it defeats the API surface.\n// The runtime sentinel keeps such bindings correct — the cost is a\n// per-update re-evaluation, which is a property of the composition\n// pattern rather than an author mistake worth blocking.\n// - `new Wrapper(s)` — NewExpression with state as an argument.\n// - `` tag`${s}` `` — TaggedTemplate with state in a span.\n// - `{ ...s }` / `[...s]` — spread of state.\n// - `const x = s` — const aliasing.\n// - `cond ? s : other` — state in a conditional branch (state\n// reaches the binding via a path the walker can't trace).\n// - `s[expr]` — dynamic element access (literal keys are tracked).\n// - State passed as `arg1+` to any call (the existing delegation\n// branch only inspects `arg0`).\n\nimport ts from 'typescript'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\nimport { isReactiveAccessor } from '../collect-deps.js'\nimport { resolveAccessorBody } from '../accessor-resolver.js'\n\n// Mirrors the file-local list in collect-deps.ts. Calls to these\n// framework primitives are visited as accessor positions in their\n// own right, so we don't double-classify.\nconst NON_DELEGATION_HELPERS = new Set(['sample', 'item', 'memo', 'text', 'unsafeHtml'])\n\ninterface LeakSite {\n node: ts.Node\n shape: string\n hint: string\n}\n\nfunction findFirstLeakInAccessor(\n accessor: ts.ArrowFunction | ts.FunctionExpression | ts.FunctionDeclaration,\n): LeakSite | null {\n if (accessor.parameters.length !== 1) return null\n const param = accessor.parameters[0]!\n if (!ts.isIdentifier(param.name)) return null\n if (!accessor.body) return null\n const stateParam = param.name.text\n\n let leak: LeakSite | null = null\n const visit = (node: ts.Node): void => {\n if (leak) return\n if (ts.isIdentifier(node) && node.text === stateParam) {\n const parent = node.parent\n if (!parent || ts.isParameter(parent)) {\n ts.forEachChild(node, visit)\n return\n }\n // Tracked containers — the same set the mask classifier honors.\n let tracked = false\n let shape = ''\n let hint = ''\n if (ts.isPropertyAccessExpression(parent) && parent.expression === node) {\n tracked = true\n } else if (ts.isElementAccessExpression(parent) && parent.expression === node) {\n if (\n ts.isStringLiteralLike(parent.argumentExpression) ||\n ts.isNumericLiteral(parent.argumentExpression)\n ) {\n tracked = true\n } else {\n shape = `dynamic element access \\`s[<expr>]\\``\n hint =\n 'replace the dynamic key with a literal property (e.g. `s.foo`), or declare the read via `track({ deps: (s) => [s[key]] })`.'\n }\n } else if (ts.isCallExpression(parent) && parent.arguments[0] === node) {\n if (\n ts.isIdentifier(parent.expression) &&\n !NON_DELEGATION_HELPERS.has(parent.expression.text)\n ) {\n // Identifier-callee delegation. Recurse into the callee's\n // body via the same resolver the mask walker uses. If it\n // resolves to a local accessor, the helper's reads are\n // walked transitively and the call is tracked. If the\n // callee is a function parameter, import, destructured\n // binding, or otherwise unresolvable, this IS the leak\n // shape — flag it here so the diagnostic points at the\n // call site rather than at some deeper unresolvable read.\n const resolved = resolveAccessorBody(parent.expression)\n if (resolved) {\n tracked = true\n } else {\n shape = `call to an unresolvable callee \\`${parent.expression.text}(s)\\` (function parameter, import, or destructured binding)`\n hint =\n 'inline the read against `s` directly, refactor the callee into a same-module `const`/`function` declaration, or declare the dependencies via `track({ deps: (s) => [...] })`.'\n }\n } else if (!ts.isIdentifier(parent.expression)) {\n // Method-call / computed callee with state arg —\n // `obj.helper(s)`, `lib.fn(s)`. This is the documented\n // headless-components idiom (`pr.valueText(s)` where `pr`\n // comes from `progress.connect()`); refactoring it would\n // defeat the API surface. The runtime sentinel keeps the\n // binding correct — just at the cost of re-evaluating on\n // every update. Treat as tracked from the lint's POV so\n // legitimate composition doesn't error the build; the\n // perf cost is a property of the composition pattern, not\n // an author mistake worth blocking.\n tracked = true\n }\n } else if (ts.isNewExpression(parent)) {\n shape = 'state passed as a constructor argument (`new X(s)`)'\n hint =\n 'compute the derived value inline against direct state reads, or use `track({ deps: (s) => [...] })` to declare the reads.'\n } else if (ts.isSpreadElement(parent) || ts.isSpreadAssignment(parent)) {\n shape = 'state spread (`{...s}` / `[...s]`)'\n hint =\n 'spread only the fields you actually need (`{...s.user}`), or use `track({ deps: (s) => [...] })`.'\n } else if (ts.isVariableDeclaration(parent)) {\n shape = 'const alias (`const x = s; … x.foo`)'\n hint =\n 'inline the alias to `s.foo`, or split the deeper read into a separate single-assignment alias `const foo = s.foo`.'\n } else if (ts.isConditionalExpression(parent)) {\n shape = 'state in a conditional branch (`cond ? s : other`)'\n hint = 'move the conditional inside the property access: `cond ? s.foo : other.foo`.'\n } else if (ts.isAsExpression(parent) || ts.isTypeAssertionExpression(parent)) {\n shape = 'type assertion wrapping state (`(s as T).foo`)'\n hint = 'drop the assertion — the chain `s.foo` already carries the type.'\n } else if (ts.isParenthesizedExpression(parent)) {\n // Walk up through parens transparently. Don't flag here; the\n // outer parent classifies.\n ts.forEachChild(node, visit)\n return\n } else {\n shape = `state used outside a tracked container (${describe(parent)})`\n hint =\n 'restructure the expression so `s` appears only as the root of a property/element-access chain, or declare the read via `track({ deps: (s) => [...] })`.'\n }\n if (!tracked) {\n leak = { node, shape, hint }\n return\n }\n }\n ts.forEachChild(node, visit)\n }\n visit(accessor.body)\n return leak\n}\n\nfunction describe(node: ts.Node): string {\n if (ts.isIdentifier(node)) return node.text\n if (ts.isPropertyAccessExpression(node)) return `${describe(node.expression)}.${node.name.text}`\n return ts.SyntaxKind[node.kind]\n}\n\nexport function opaqueStateFlowModule(): CompilerModule {\n return {\n name: 'opaque-state-flow',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/opaque-state-flow',\n description:\n \"Reactive accessor flows state into an opaque expression the walker can't trace. The runtime stays correct via a FULL_MASK binding + whole-state sentinel in `__prefixes`, but the binding then re-evaluates on every state change.\",\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n const visited = node as ts.SourceFile\n const sf = ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n\n const walk = (n: ts.Node): void => {\n if ((ts.isArrowFunction(n) || ts.isFunctionExpression(n)) && isReactiveAccessor(n)) {\n const leak = findFirstLeakInAccessor(n)\n if (leak) {\n ctx.reportDiagnostic({\n id: 'llui/opaque-state-flow',\n severity: 'error',\n category: 'perf',\n message:\n `Reactive accessor flows state opaquely — ${leak.shape}. ` +\n `The compiler ships a correct binding (FULL_MASK + whole-state sentinel), ` +\n `but it re-evaluates on every state change. ${leak.hint}`,\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, leak.node.getStart(sf), leak.node.getEnd()),\n },\n })\n }\n }\n ts.forEachChild(n, walk)\n }\n walk(sf)\n },\n },\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"opaque-state-flow.js","sourceRoot":"","sources":["../../src/modules/opaque-state-flow.ts"],"names":[],"mappings":"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","sourcesContent":["// `opaque-state-flow` — errors when a reactive accessor's body flows\n// the state identifier into an expression the walker can't statically\n// trace. The compiler still produces a *correct* binding by forcing\n// FULL_MASK and emitting a whole-state `(s) => s` sentinel into\n// `__prefixes` (see `02 Compiler.md` § \"Opaque-flow classifier\"), but\n// the binding then re-evaluates on every state change rather than\n// only when its actual reads change. This rule surfaces the leak so\n// authors can either:\n//\n// - Rewrite the accessor as direct property access (`s.foo`,\n// `s.foo['literal']`), the form the walker can resolve.\n// - Declare the reads explicitly via `track({ deps: (s) => [...] })`\n// — the compile-time escape hatch the framework provides for cases\n// where the read genuinely can't be expressed inline.\n//\n// Detected leak shapes (mirrors the classifier in\n// `transform.ts:computeAccessorMask`):\n// - `helper(s)` with an Identifier callee that can't be resolved to\n// a local declaration (function parameter, import, destructured\n// binding) — the callee may read any field of `s`.\n//\n// NOT flagged (intentional): `obj.helper(s)` / `lib.fn(s)` —\n// PropertyAccessExpression callees. This is the documented headless-\n// components idiom (`pr.valueText(s)` where `pr` comes from\n// `progress.connect()`), and refactoring it defeats the API surface.\n// The runtime sentinel keeps such bindings correct — the cost is a\n// per-update re-evaluation, which is a property of the composition\n// pattern rather than an author mistake worth blocking.\n// - `new Wrapper(s)` — NewExpression with state as an argument.\n// - `` tag`${s}` `` — TaggedTemplate with state in a span.\n// - `{ ...s }` / `[...s]` — spread of state.\n// - `const x = s` — const aliasing.\n// - `cond ? s : other` — state in a conditional branch (state\n// reaches the binding via a path the walker can't trace).\n// - `s[expr]` — dynamic element access (literal keys are tracked).\n// - State passed as `arg1+` to any call (the existing delegation\n// branch only inspects `arg0`).\n\nimport ts from 'typescript'\nimport { rangeFromOffsets } from '../diagnostic.js'\nimport type { CompilerModule } from '../module.js'\nimport { isReactiveAccessor } from '../collect-deps.js'\nimport { resolveAccessorBody } from '../accessor-resolver.js'\n\n// Mirrors the file-local list in collect-deps.ts. Calls to these\n// framework primitives are visited as accessor positions in their\n// own right, so we don't double-classify.\nconst NON_DELEGATION_HELPERS = new Set(['sample', 'item', 'memo', 'text', 'unsafeHtml'])\n\ninterface LeakSite {\n node: ts.Node\n shape: string\n hint: string\n}\n\nfunction findFirstLeakInAccessor(\n accessor: ts.ArrowFunction | ts.FunctionExpression | ts.FunctionDeclaration,\n checker: ts.TypeChecker | undefined,\n): LeakSite | null {\n if (accessor.parameters.length !== 1) return null\n const param = accessor.parameters[0]!\n if (!ts.isIdentifier(param.name)) return null\n if (!accessor.body) return null\n const stateParam = param.name.text\n\n let leak: LeakSite | null = null\n const visit = (node: ts.Node): void => {\n if (leak) return\n if (ts.isIdentifier(node) && node.text === stateParam) {\n const parent = node.parent\n if (!parent || ts.isParameter(parent)) {\n ts.forEachChild(node, visit)\n return\n }\n // Tracked containers — the same set the mask classifier honors.\n let tracked = false\n let shape = ''\n let hint = ''\n if (ts.isPropertyAccessExpression(parent) && parent.expression === node) {\n tracked = true\n } else if (ts.isElementAccessExpression(parent) && parent.expression === node) {\n if (\n ts.isStringLiteralLike(parent.argumentExpression) ||\n ts.isNumericLiteral(parent.argumentExpression)\n ) {\n tracked = true\n } else {\n shape = `dynamic element access \\`s[<expr>]\\``\n hint =\n 'replace the dynamic key with a literal property (e.g. `s.foo`), or declare the read via `track({ deps: (s) => [s[key]] })`.'\n }\n } else if (ts.isCallExpression(parent)) {\n const argIndex = parent.arguments.indexOf(node as ts.Expression)\n if (argIndex > 0) {\n // State passed as arg1+ to a call. The header documents this\n // as NOT flagged (intentional): the existing delegation\n // branch only attempts to trace arg0, and the mask classifier\n // emits a whole-state sentinel into `__prefixes` so the\n // binding stays correct. The cost is per-update re-evaluation\n // — a property of the composition pattern, not an author\n // mistake worth blocking. Without this branch we'd fall\n // through to the default \"outside a tracked container\" leak.\n tracked = true\n } else if (argIndex === 0) {\n if (\n ts.isIdentifier(parent.expression) &&\n !NON_DELEGATION_HELPERS.has(parent.expression.text)\n ) {\n // Identifier-callee delegation. Recurse into the callee's\n // body via the same resolver the mask walker uses. If it\n // resolves to a local accessor, the helper's reads are\n // walked transitively and the call is tracked. If the\n // callee is a function parameter, import, destructured\n // binding, or otherwise unresolvable, this IS the leak\n // shape — flag it here so the diagnostic points at the\n // call site rather than at some deeper unresolvable read.\n const resolved = resolveAccessorBody(parent.expression, checker)\n if (resolved) {\n tracked = true\n } else {\n const calleeSymbol = checker?.getSymbolAtLocation(parent.expression)\n const isFunctionParam = !!calleeSymbol?.declarations?.some((d: ts.Declaration) =>\n ts.isParameter(d),\n )\n shape = `call to an unresolvable callee \\`${parent.expression.text}(s)\\` (function parameter, import, or destructured binding)`\n if (isFunctionParam) {\n hint =\n 'this callee is a function parameter — the closure passed at the call site is opaque to per-binding analysis. The framework expects per-row dynamic state to flow through `each` items (slot data on `item.*`) rather than through `(s) => ...` callback parameters; restructure the helper so its bindings read `item.*` and the call site builds the slot data once in `items: (s) => …`.'\n } else {\n hint =\n 'inline the read against `s` directly, refactor the callee into a same-module `const`/`function` declaration, or declare the dependencies via `track({ deps: (s) => [...] })`.'\n }\n }\n } else if (!ts.isIdentifier(parent.expression)) {\n // Method-call / computed callee with state arg —\n // `obj.helper(s)`, `lib.fn(s)`. This is the documented\n // headless-components idiom (`pr.valueText(s)` where `pr`\n // comes from `progress.connect()`); refactoring it would\n // defeat the API surface. The runtime sentinel keeps the\n // binding correct — just at the cost of re-evaluating on\n // every update. Treat as tracked from the lint's POV so\n // legitimate composition doesn't error the build; the\n // perf cost is a property of the composition pattern, not\n // an author mistake worth blocking.\n tracked = true\n }\n }\n } else if (ts.isNewExpression(parent)) {\n shape = 'state passed as a constructor argument (`new X(s)`)'\n hint =\n 'compute the derived value inline against direct state reads, or use `track({ deps: (s) => [...] })` to declare the reads.'\n } else if (ts.isSpreadElement(parent) || ts.isSpreadAssignment(parent)) {\n shape = 'state spread (`{...s}` / `[...s]`)'\n hint =\n 'spread only the fields you actually need (`{...s.user}`), or use `track({ deps: (s) => [...] })`.'\n } else if (ts.isVariableDeclaration(parent)) {\n shape = 'const alias (`const x = s; … x.foo`)'\n hint =\n 'inline the alias to `s.foo`, or split the deeper read into a separate single-assignment alias `const foo = s.foo`.'\n } else if (ts.isConditionalExpression(parent)) {\n shape = 'state in a conditional branch (`cond ? s : other`)'\n hint = 'move the conditional inside the property access: `cond ? s.foo : other.foo`.'\n } else if (ts.isAsExpression(parent) || ts.isTypeAssertionExpression(parent)) {\n shape = 'type assertion wrapping state (`(s as T).foo`)'\n hint = 'drop the assertion — the chain `s.foo` already carries the type.'\n } else if (ts.isParenthesizedExpression(parent)) {\n // Walk up through parens transparently. Don't flag here; the\n // outer parent classifies.\n ts.forEachChild(node, visit)\n return\n } else {\n shape = `state used outside a tracked container (${describe(parent)})`\n hint =\n 'restructure the expression so `s` appears only as the root of a property/element-access chain, or declare the read via `track({ deps: (s) => [...] })`.'\n }\n if (!tracked) {\n leak = { node, shape, hint }\n return\n }\n }\n ts.forEachChild(node, visit)\n }\n visit(accessor.body)\n return leak\n}\n\n/**\n * True when `arrow` is the value of a `deps:` PropertyAssignment in\n * a `track({ ... })` call. The diagnostic is suppressed in that\n * position because `track` is the documented escape hatch for cases\n * the walker can't statically infer; firing the lint inside it moves\n * the diagnostic without giving the author a path forward.\n *\n * Handles both forms: bare `track({...})` (import from `@llui/dom`)\n * and the View-bag form `h.track({...})` if it ever exists.\n */\nfunction isInsideTrackDeps(arrow: ts.ArrowFunction | ts.FunctionExpression): boolean {\n const pa = arrow.parent\n if (!pa || !ts.isPropertyAssignment(pa) || !ts.isIdentifier(pa.name) || pa.name.text !== 'deps') {\n return false\n }\n const obj = pa.parent\n if (!obj || !ts.isObjectLiteralExpression(obj)) return false\n const call = obj.parent\n if (!call || !ts.isCallExpression(call) || call.arguments[0] !== obj) return false\n if (ts.isIdentifier(call.expression)) return call.expression.text === 'track'\n if (ts.isPropertyAccessExpression(call.expression)) {\n return call.expression.name.text === 'track'\n }\n return false\n}\n\nfunction describe(node: ts.Node): string {\n if (ts.isIdentifier(node)) return node.text\n if (ts.isPropertyAccessExpression(node)) return `${describe(node.expression)}.${node.name.text}`\n return ts.SyntaxKind[node.kind]\n}\n\nexport function opaqueStateFlowModule(): CompilerModule {\n return {\n name: 'opaque-state-flow',\n compilerVersion: '^0.3.0',\n diagnostics: [\n {\n id: 'llui/opaque-state-flow',\n description:\n \"Reactive accessor flows state into an opaque expression the walker can't trace. The runtime stays correct via a FULL_MASK binding + whole-state sentinel in `__prefixes`, but the binding then re-evaluates on every state change.\",\n },\n ],\n visitors: {\n [ts.SyntaxKind.SourceFile]: (ctx, node) => {\n // When the host adapter has built a Program, walk the checker's\n // own SourceFile so symbol resolution (Alias → Symbol via\n // `getSymbolAtLocation`) actually works. The reparsed file used\n // in the AST-only fallback is not part of any Program, so the\n // checker can't resolve identifiers in it. Fall back to a\n // reparse for paths without a Program (test harness, lint\n // adapters without cross-file resolution).\n const visited = node as ts.SourceFile\n const fromProgram = ctx.program?.getSourceFile(visited.fileName)\n const checker = fromProgram ? ctx.checker : undefined\n const sf =\n fromProgram ??\n ts.createSourceFile(visited.fileName, visited.text, ts.ScriptTarget.Latest, true)\n\n const walk = (n: ts.Node): void => {\n if ((ts.isArrowFunction(n) || ts.isFunctionExpression(n)) && isReactiveAccessor(n)) {\n // `track({ deps: (s) => [...] })` is the user's explicit\n // opt-in for \"this binding's reads can't be inferred — trust\n // my declaration.\" Firing a perf lint inside the user's\n // declaration defeats the primitive's purpose; the\n // diagnostic moves from the original call site to inside\n // track.deps without going away, leaving authors with no\n // recovery path. Suppress here. The mask/path classifier\n // still walks the body for what it can extract; this only\n // silences the lint.\n if (isInsideTrackDeps(n)) {\n ts.forEachChild(n, walk)\n return\n }\n const leak = findFirstLeakInAccessor(n, checker)\n if (leak) {\n ctx.reportDiagnostic({\n id: 'llui/opaque-state-flow',\n severity: 'error',\n category: 'perf',\n message:\n `Reactive accessor flows state opaquely — ${leak.shape}. ` +\n `The compiler ships a correct binding (FULL_MASK + whole-state sentinel), ` +\n `but it re-evaluates on every state change. ${leak.hint}`,\n location: {\n file: sf.fileName,\n range: rangeFromOffsets(sf.text, leak.node.getStart(sf), leak.node.getEnd()),\n },\n })\n }\n }\n ts.forEachChild(n, walk)\n }\n walk(sf)\n },\n },\n }\n}\n"]}
|
package/dist/transform.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export interface PreExtractedSchemas {
|
|
|
56
56
|
stateSchema?: ReturnType<typeof extractStateSchema>;
|
|
57
57
|
effectSchema?: ReturnType<typeof extractEffectSchema>;
|
|
58
58
|
}
|
|
59
|
-
export declare function transformLlui(source: string, _filename: string, devMode?: boolean, emitAgentMetadata?: boolean, mcpPort?: number | null, verbose?: boolean, typeSources?: ExternalTypeSources, preExtracted?: PreExtractedSchemas, crossFilePaths?: ReadonlySet<string>, crossFileOpaque?: boolean): {
|
|
59
|
+
export declare function transformLlui(source: string, _filename: string, devMode?: boolean, emitAgentMetadata?: boolean, mcpPort?: number | null, verbose?: boolean, typeSources?: ExternalTypeSources, preExtracted?: PreExtractedSchemas, crossFilePaths?: ReadonlySet<string>, crossFileOpaque?: boolean, crossFileProgram?: ts.Program): {
|
|
60
60
|
output: string;
|
|
61
61
|
edits: TransformEdit[];
|
|
62
62
|
diagnostics: Diagnostic[];
|