@llui/compiler 0.5.4 → 0.5.5
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.
|
@@ -114,7 +114,7 @@ function findFirstLeakInAccessor(accessor, checker) {
|
|
|
114
114
|
shape = `call to an unresolvable callee \`${parent.expression.text}(s)\` (function parameter, import, or destructured binding)`;
|
|
115
115
|
if (isFunctionParam) {
|
|
116
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) => …`.';
|
|
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) => …`. For non-iterating helpers (single-value renderers, form rows, layout chrome) see the other patterns in `docs/composition-patterns.md` — accessor passthrough, pre-built Nodes, Node[] slots.';
|
|
118
118
|
}
|
|
119
119
|
else {
|
|
120
120
|
hint =
|
|
@@ -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 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"]}
|
|
1
|
+
{"version":3,"file":"opaque-state-flow.js","sourceRoot":"","sources":["../../src/modules/opaque-state-flow.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,sEAAsE;AACtE,oEAAoE;AACpE,gEAAgE;AAChE,sEAAsE;AACtE,kEAAkE;AAClE,oEAAoE;AACpE,sBAAsB;AACtB,EAAE;AACF,+DAA+D;AAC/D,4DAA4D;AAC5D,uEAAuE;AACvE,uEAAuE;AACvE,0DAA0D;AAC1D,EAAE;AACF,kDAAkD;AAClD,uCAAuC;AACvC,sEAAsE;AACtE,oEAAoE;AACpE,uDAAuD;AACvD,EAAE;AACF,6DAA6D;AAC7D,qEAAqE;AACrE,4DAA4D;AAC5D,qEAAqE;AACrE,mEAAmE;AACnE,mEAAmE;AACnE,wDAAwD;AACxD,kEAAkE;AAClE,6DAA6D;AAC7D,+CAA+C;AAC/C,sCAAsC;AACtC,gEAAgE;AAChE,8DAA8D;AAC9D,qEAAqE;AACrE,mEAAmE;AACnE,oCAAoC;AAEpC,OAAO,EAAE,MAAM,YAAY,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AAEnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AAE7D,iEAAiE;AACjE,kEAAkE;AAClE,0CAA0C;AAC1C,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,CAAC,CAAC,CAAA;AAQxF,SAAS,uBAAuB,CAC9B,QAA2E,EAC3E,OAAmC;IAEnC,IAAI,QAAQ,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACjD,MAAM,KAAK,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAE,CAAA;IACrC,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IAC7C,IAAI,CAAC,QAAQ,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IAC/B,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAA;IAElC,IAAI,IAAI,GAAoB,IAAI,CAAA;IAChC,MAAM,KAAK,GAAG,CAAC,IAAa,EAAQ,EAAE;QACpC,IAAI,IAAI;YAAE,OAAM;QAChB,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACtD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;YAC1B,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;gBACtC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;gBAC5B,OAAM;YACR,CAAC;YACD,gEAAgE;YAChE,IAAI,OAAO,GAAG,KAAK,CAAA;YACnB,IAAI,KAAK,GAAG,EAAE,CAAA;YACd,IAAI,IAAI,GAAG,EAAE,CAAA;YACb,IAAI,EAAE,CAAC,0BAA0B,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;gBACxE,OAAO,GAAG,IAAI,CAAA;YAChB,CAAC;iBAAM,IAAI,EAAE,CAAC,yBAAyB,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;gBAC9E,IACE,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,kBAAkB,CAAC;oBACjD,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC,kBAAkB,CAAC,EAC9C,CAAC;oBACD,OAAO,GAAG,IAAI,CAAA;gBAChB,CAAC;qBAAM,CAAC;oBACN,KAAK,GAAG,sCAAsC,CAAA;oBAC9C,IAAI;wBACF,6HAA6H,CAAA;gBACjI,CAAC;YACH,CAAC;iBAAM,IAAI,EAAE,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC;gBACvC,MAAM,QAAQ,GAAG,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAqB,CAAC,CAAA;gBAChE,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;oBACjB,6DAA6D;oBAC7D,wDAAwD;oBACxD,8DAA8D;oBAC9D,wDAAwD;oBACxD,8DAA8D;oBAC9D,yDAAyD;oBACzD,wDAAwD;oBACxD,6DAA6D;oBAC7D,OAAO,GAAG,IAAI,CAAA;gBAChB,CAAC;qBAAM,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;oBAC1B,IACE,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC;wBAClC,CAAC,sBAAsB,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,EACnD,CAAC;wBACD,0DAA0D;wBAC1D,yDAAyD;wBACzD,uDAAuD;wBACvD,sDAAsD;wBACtD,uDAAuD;wBACvD,uDAAuD;wBACvD,uDAAuD;wBACvD,0DAA0D;wBAC1D,MAAM,QAAQ,GAAG,mBAAmB,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;wBAChE,IAAI,QAAQ,EAAE,CAAC;4BACb,OAAO,GAAG,IAAI,CAAA;wBAChB,CAAC;6BAAM,CAAC;4BACN,MAAM,YAAY,GAAG,OAAO,EAAE,mBAAmB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;4BACpE,MAAM,eAAe,GAAG,CAAC,CAAC,YAAY,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC,CAAiB,EAAE,EAAE,CAC/E,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAClB,CAAA;4BACD,KAAK,GAAG,oCAAoC,MAAM,CAAC,UAAU,CAAC,IAAI,6DAA6D,CAAA;4BAC/H,IAAI,eAAe,EAAE,CAAC;gCACpB,IAAI;oCACF,yjBAAyjB,CAAA;4BAC7jB,CAAC;iCAAM,CAAC;gCACN,IAAI;oCACF,+KAA+K,CAAA;4BACnL,CAAC;wBACH,CAAC;oBACH,CAAC;yBAAM,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;wBAC/C,iDAAiD;wBACjD,uDAAuD;wBACvD,0DAA0D;wBAC1D,yDAAyD;wBACzD,yDAAyD;wBACzD,yDAAyD;wBACzD,wDAAwD;wBACxD,sDAAsD;wBACtD,0DAA0D;wBAC1D,oCAAoC;wBACpC,OAAO,GAAG,IAAI,CAAA;oBAChB,CAAC;gBACH,CAAC;YACH,CAAC;iBAAM,IAAI,EAAE,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC;gBACtC,KAAK,GAAG,qDAAqD,CAAA;gBAC7D,IAAI;oBACF,2HAA2H,CAAA;YAC/H,CAAC;iBAAM,IAAI,EAAE,CAAC,eAAe,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC;gBACvE,KAAK,GAAG,oCAAoC,CAAA;gBAC5C,IAAI;oBACF,mGAAmG,CAAA;YACvG,CAAC;iBAAM,IAAI,EAAE,CAAC,qBAAqB,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5C,KAAK,GAAG,sCAAsC,CAAA;gBAC9C,IAAI;oBACF,oHAAoH,CAAA;YACxH,CAAC;iBAAM,IAAI,EAAE,CAAC,uBAAuB,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC9C,KAAK,GAAG,oDAAoD,CAAA;gBAC5D,IAAI,GAAG,8EAA8E,CAAA;YACvF,CAAC;iBAAM,IAAI,EAAE,CAAC,cAAc,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,yBAAyB,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC7E,KAAK,GAAG,gDAAgD,CAAA;gBACxD,IAAI,GAAG,kEAAkE,CAAA;YAC3E,CAAC;iBAAM,IAAI,EAAE,CAAC,yBAAyB,CAAC,MAAM,CAAC,EAAE,CAAC;gBAChD,6DAA6D;gBAC7D,2BAA2B;gBAC3B,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;gBAC5B,OAAM;YACR,CAAC;iBAAM,CAAC;gBACN,KAAK,GAAG,2CAA2C,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAA;gBACtE,IAAI;oBACF,yJAAyJ,CAAA;YAC7J,CAAC;YACD,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,IAAI,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;gBAC5B,OAAM;YACR,CAAC;QACH,CAAC;QACD,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IAC9B,CAAC,CAAA;IACD,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;IACpB,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,iBAAiB,CAAC,KAA+C;IACxE,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAA;IACvB,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,oBAAoB,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAChG,OAAO,KAAK,CAAA;IACd,CAAC;IACD,MAAM,GAAG,GAAG,EAAE,CAAC,MAAM,CAAA;IACrB,IAAI,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC,yBAAyB,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAA;IAC5D,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAA;IACvB,IAAI,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,GAAG;QAAE,OAAO,KAAK,CAAA;IAClF,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,KAAK,OAAO,CAAA;IAC7E,IAAI,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACnD,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO,CAAA;IAC9C,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,QAAQ,CAAC,IAAa;IAC7B,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC,IAAI,CAAA;IAC3C,IAAI,EAAE,CAAC,0BAA0B,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IAChG,OAAO,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACjC,CAAC;AAED,MAAM,UAAU,qBAAqB;IACnC,OAAO;QACL,IAAI,EAAE,mBAAmB;QACzB,eAAe,EAAE,QAAQ;QACzB,WAAW,EAAE;YACX;gBACE,EAAE,EAAE,wBAAwB;gBAC5B,WAAW,EACT,oOAAoO;aACvO;SACF;QACD,QAAQ,EAAE;YACR,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE;gBACxC,gEAAgE;gBAChE,0DAA0D;gBAC1D,gEAAgE;gBAChE,8DAA8D;gBAC9D,0DAA0D;gBAC1D,0DAA0D;gBAC1D,2CAA2C;gBAC3C,MAAM,OAAO,GAAG,IAAqB,CAAA;gBACrC,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;gBAChE,MAAM,OAAO,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;gBACrD,MAAM,EAAE,GACN,WAAW;oBACX,EAAE,CAAC,gBAAgB,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;gBAEnF,MAAM,IAAI,GAAG,CAAC,CAAU,EAAQ,EAAE;oBAChC,IAAI,CAAC,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,IAAI,kBAAkB,CAAC,CAAC,CAAC,EAAE,CAAC;wBACnF,yDAAyD;wBACzD,6DAA6D;wBAC7D,wDAAwD;wBACxD,mDAAmD;wBACnD,yDAAyD;wBACzD,yDAAyD;wBACzD,yDAAyD;wBACzD,0DAA0D;wBAC1D,qBAAqB;wBACrB,IAAI,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC;4BACzB,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;4BACxB,OAAM;wBACR,CAAC;wBACD,MAAM,IAAI,GAAG,uBAAuB,CAAC,CAAC,EAAE,OAAO,CAAC,CAAA;wBAChD,IAAI,IAAI,EAAE,CAAC;4BACT,GAAG,CAAC,gBAAgB,CAAC;gCACnB,EAAE,EAAE,wBAAwB;gCAC5B,QAAQ,EAAE,OAAO;gCACjB,QAAQ,EAAE,MAAM;gCAChB,OAAO,EACL,4CAA4C,IAAI,CAAC,KAAK,IAAI;oCAC1D,2EAA2E;oCAC3E,8CAA8C,IAAI,CAAC,IAAI,EAAE;gCAC3D,QAAQ,EAAE;oCACR,IAAI,EAAE,EAAE,CAAC,QAAQ;oCACjB,KAAK,EAAE,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;iCAC7E;6BACF,CAAC,CAAA;wBACJ,CAAC;oBACH,CAAC;oBACD,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;gBAC1B,CAAC,CAAA;gBACD,IAAI,CAAC,EAAE,CAAC,CAAA;YACV,CAAC;SACF;KACF,CAAA;AACH,CAAC","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) => …`. For non-iterating helpers (single-value renderers, form rows, layout chrome) see the other patterns in `docs/composition-patterns.md` — accessor passthrough, pre-built Nodes, Node[] slots.'\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"]}
|