@opetope/lint 0.9.0 → 0.9.2

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.
@@ -0,0 +1,2 @@
1
+ import{AST_NODE_TYPES as o}from"@typescript-eslint/utils";import{apiName as h,localValue as I,variableOf as d,unwrap as p}from"./model-bindings.js";const O=new Set(["inFlight","outcome","run"]),S=["inFlight","outcome"],l=new Set(["hooks","selection"]),g=2,y={member:void 0,optional:!1,text:""},R={accesses:[],opaque:!0};function s(n){const{parent:e}=n;if(!(e.type!==o.MemberExpression||e.object!==n||e.computed)&&e.property.type===o.Identifier)return{key:e.property.name,node:e,optional:e.optional}}function c(n,e,i){return e===void 0||!O.has(e.key)?y:{member:e.key,optional:i||e.optional,text:n.getText(e.node)}}function m(n,e,i){const t=s(e);return!i.record&&i.key===void 0?c(n,t,!1):t===void 0?y:i.key===void 0||t.key===i.key?c(n,s(t.node),t.optional):void 0}function x(n,e){return n.range[0]>=e.range[0]&&n.range[1]<=e.range[1]}function A(n,e,i){if(i===void 0)return R;const t=[];let r=!1;for(const{identifier:f}of e.variable.references){if(f.type!==o.Identifier||!x(f,i))continue;const u=m(n,f,e);u!==void 0&&(u.member===void 0?r=!0:t.push(u))}return{accesses:t,opaque:r}}function M(n,e){for(const{identifier:i}of e.variable.references)if(i.type===o.Identifier&&m(n,i,e)?.member==="run")return!0;return!1}function k(n,e){return e===null?"":n.get(e)??""}function E(n,e){return!e.proved||M(n,e)?e:void 0}function T(n,e,i){const t=d(n,i),r=k(e,t);if(t===null||r==="")return;const f=l.has(r);return E(n,{key:void 0,proved:r==="candidate",record:f,variable:t})}function C(n){const e=p(n.object);if(!(n.computed||n.property.type!==o.Identifier))return e.type===o.Identifier?{key:n.property.name,object:e}:void 0}function _(n,e,i){const t=C(i);if(t===void 0)return;const r=d(n,t.object),f=k(e,r);if(!(r===null||!l.has(f)))return E(n,{key:t.key,proved:f==="selection",record:!1,variable:r})}function j(n,e,i){const t=p(i);return t.type===o.Identifier?T(n,e,t):t.type===o.MemberExpression?_(n,e,t):void 0}function a(n,e,i,t){if(e.type!==o.Identifier)return;const r=d(n,e);r!==null&&i.set(r,t)}function v(n,e,i,t,r){if(e.type!==o.ObjectPattern){a(n,e,i,t);return}for(const f of e.properties)f.type===o.Property&&a(n,f.value,i,r)}function B(n,e){if(e.parent.kind!=="const"||e.init===null)return;const i=I(n,e.init);return i.type===o.CallExpression?i:void 0}function D(n,e,i){const t=B(n,e);if(t===void 0)return;const r=h(n,t.callee,"@opetope/react");r==="useCommand"?a(n,e.id,i,"hook"):r==="useCommands"?v(n,e.id,i,"hooks","hook"):r==="useModel"&&t.arguments.length>=g&&v(n,e.id,i,"selection","candidate")}export{S as STATUS_MEMBERS,j as bindingAt,D as classifyDeclaration,A as usageIn};
2
+ //# sourceMappingURL=command-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-hooks.js","sources":["../src/command-hooks.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { apiName, localValue, unwrap, variableOf } from './model-bindings';\n\n/** What a name holds: one hook, a record of them, a selection whose fields may be hooks, or one such field. */\ntype HookKind = 'candidate' | 'hook' | 'hooks' | 'selection';\ntype HookKinds = Map<TSESLint.Scope.Variable, HookKind>;\n/**\n * What a dependency element names: one hook (no key, not a record), one key of a record, or the record itself,\n * which is a fresh object on every render.\n */\ntype Binding = {\n readonly key: string | undefined;\n readonly proved: boolean;\n readonly record: boolean;\n readonly variable: TSESLint.Scope.Variable;\n};\n/** One read of the binding: the member it ends in, and the text the author wrote to reach it. */\ntype Access = { readonly member: string | undefined; readonly optional: boolean; readonly text: string };\n/** `opaque` is a use no member replaces: the object passed on, destructured, or read past a member we know. */\ntype Usage = { readonly accesses: readonly Access[]; readonly opaque: boolean };\ntype Step = { readonly key: string; readonly node: TSESTree.MemberExpression; readonly optional: boolean };\ntype Occurrence = TSESTree.Identifier | TSESTree.MemberExpression;\n\nconst HOOK_MEMBERS = new Set(['inFlight', 'outcome', 'run']);\nconst STATUS_MEMBERS = ['inFlight', 'outcome'];\nconst RECORD_KINDS = new Set(['hooks', 'selection']);\nconst SELECTION_ARITY = 2;\nconst OPAQUE: Access = { member: undefined, optional: false, text: '' };\n/** A callback this module cannot read is not proof of anything, so its binding takes no fix and no suggestion. */\nconst UNREAD: Usage = { accesses: [], opaque: true };\n\n/** One written step off an occurrence: `.save`, with the `?.` the author may have put in front of it. */\nfunction step(node: Occurrence): Step | undefined {\n const { parent } = node;\n\n if (parent.type !== AST_NODE_TYPES.MemberExpression || parent.object !== node || parent.computed) return undefined;\n\n if (parent.property.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n return { key: parent.property.name, node: parent, optional: parent.optional };\n}\n\n/** A step that lands on a member this rule knows; anything else is a use it must not rewrite. */\nfunction accessAt(source: TSESLint.SourceCode, member: Step | undefined, over: boolean): Access {\n if (member === undefined || !HOOK_MEMBERS.has(member.key)) return OPAQUE;\n\n return { member: member.key, optional: over || member.optional, text: source.getText(member.node) };\n}\n\n/** What this occurrence reads on the binding: one step for a hook, two for a record — key, then member. */\nfunction accessOf(source: TSESLint.SourceCode, node: TSESTree.Identifier, binding: Binding): Access | undefined {\n const first = step(node);\n\n if (!binding.record && binding.key === undefined) return accessAt(source, first, false);\n\n if (first === undefined) return OPAQUE;\n\n if (binding.key === undefined) return accessAt(source, step(first.node), first.optional);\n\n return first.key === binding.key ? accessAt(source, step(first.node), first.optional) : undefined;\n}\n\nfunction within(node: TSESTree.Node, callback: TSESTree.Node): boolean {\n return node.range[0] >= callback.range[0] && node.range[1] <= callback.range[1];\n}\n\n/** Every read of the binding inside one callback, and whether any of them takes the object as a whole. */\nfunction usageIn(source: TSESLint.SourceCode, binding: Binding, callback: TSESTree.Node | undefined): Usage {\n if (callback === undefined) return UNREAD;\n\n const accesses: Access[] = [];\n let opaque = false;\n\n for (const { identifier } of binding.variable.references) {\n if (identifier.type !== AST_NODE_TYPES.Identifier || !within(identifier, callback)) continue;\n\n const access = accessOf(source, identifier, binding);\n\n if (access === undefined) continue;\n if (access.member === undefined) opaque = true;\n else accesses.push(access);\n }\n\n return { accesses, opaque };\n}\n\n/**\n * A selected field is a command hook where the code calls or reads `run` on it. A status name alone proves\n * nothing: plain data may carry one, and this package reads shapes rather than types.\n */\nfunction provesRun(source: TSESLint.SourceCode, binding: Binding): boolean {\n for (const { identifier } of binding.variable.references) {\n if (identifier.type !== AST_NODE_TYPES.Identifier) continue;\n\n if (accessOf(source, identifier, binding)?.member === 'run') return true;\n }\n\n return false;\n}\n\nfunction kindOf(kinds: HookKinds, variable: TSESLint.Scope.Variable | null): string {\n return variable === null ? '' : (kinds.get(variable) ?? '');\n}\n\n/** A kind that is a claim about a selected field keeps the binding only where the code proves `run` on it. */\nfunction provedBinding(source: TSESLint.SourceCode, binding: Binding): Binding | undefined {\n return !binding.proved || provesRun(source, binding) ? binding : undefined;\n}\n\nfunction namedBinding(source: TSESLint.SourceCode, kinds: HookKinds, node: TSESTree.Identifier): Binding | undefined {\n const variable = variableOf(source, node);\n const kind = kindOf(kinds, variable);\n\n if (variable === null || kind === '') return undefined;\n\n const record = RECORD_KINDS.has(kind);\n\n return provedBinding(source, { key: undefined, proved: kind === 'candidate', record, variable });\n}\n\n/** `hooks.save`: the plain name it is read from and the key it reads, when both of them are written as names. */\nfunction keyRead(node: TSESTree.MemberExpression): { key: string; object: TSESTree.Identifier } | undefined {\n const object = unwrap(node.object);\n\n if (node.computed || node.property.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n return object.type === AST_NODE_TYPES.Identifier ? { key: node.property.name, object } : undefined;\n}\n\nfunction keyedBinding(\n source: TSESLint.SourceCode,\n kinds: HookKinds,\n node: TSESTree.MemberExpression,\n): Binding | undefined {\n const read = keyRead(node);\n\n if (read === undefined) return undefined;\n\n const variable = variableOf(source, read.object);\n const kind = kindOf(kinds, variable);\n\n if (variable === null || !RECORD_KINDS.has(kind)) return undefined;\n\n return provedBinding(source, { key: read.key, proved: kind === 'selection', record: false, variable });\n}\n\n/** The binding a dependency element names as a whole; a member of one is a member, and this returns nothing. */\nfunction bindingAt(source: TSESLint.SourceCode, kinds: HookKinds, element: TSESTree.Node): Binding | undefined {\n const value = unwrap(element);\n\n if (value.type === AST_NODE_TYPES.Identifier) return namedBinding(source, kinds, value);\n\n return value.type === AST_NODE_TYPES.MemberExpression ? keyedBinding(source, kinds, value) : undefined;\n}\n\nfunction mark(source: TSESLint.SourceCode, node: TSESTree.Node, kinds: HookKinds, kind: HookKind): void {\n if (node.type !== AST_NODE_TYPES.Identifier) return;\n\n const variable = variableOf(source, node);\n\n if (variable !== null) kinds.set(variable, kind);\n}\n\n/** `const hooks = useCommands(...)` names the record; `const { save } = useCommands(...)` names each key. */\nfunction markPattern(\n source: TSESLint.SourceCode,\n node: TSESTree.Node,\n kinds: HookKinds,\n whole: HookKind,\n each: HookKind,\n): void {\n if (node.type !== AST_NODE_TYPES.ObjectPattern) {\n mark(source, node, kinds, whole);\n\n return;\n }\n\n for (const property of node.properties) {\n if (property.type === AST_NODE_TYPES.Property) mark(source, property.value, kinds, each);\n }\n}\n\n/** A reassignable name holds whatever was last written to it; only a `const` holds the hook it was opened with. */\nfunction constantCall(\n source: TSESLint.SourceCode,\n declarator: TSESTree.VariableDeclarator,\n): TSESTree.CallExpression | undefined {\n if (declarator.parent.kind !== 'const' || declarator.init === null) return undefined;\n\n const value = localValue(source, declarator.init);\n\n return value.type === AST_NODE_TYPES.CallExpression ? value : undefined;\n}\n\n/** Records what one declaration binds, so a later dependency array is read against names and not against spelling. */\nfunction classifyDeclaration(\n source: TSESLint.SourceCode,\n declarator: TSESTree.VariableDeclarator,\n kinds: HookKinds,\n): void {\n const call = constantCall(source, declarator);\n\n if (call === undefined) return;\n\n const name = apiName(source, call.callee, '@opetope/react');\n\n if (name === 'useCommand') mark(source, declarator.id, kinds, 'hook');\n else if (name === 'useCommands') markPattern(source, declarator.id, kinds, 'hooks', 'hook');\n // A selection field is a hook only where the code proves it; without a selector the granted model is stable.\n else if (name === 'useModel' && call.arguments.length >= SELECTION_ARITY)\n markPattern(source, declarator.id, kinds, 'selection', 'candidate');\n}\n\nexport type { Access, Binding, HookKinds, Usage };\nexport { bindingAt, classifyDeclaration, STATUS_MEMBERS, usageIn };\n"],"names":["HOOK_MEMBERS","STATUS_MEMBERS","RECORD_KINDS","SELECTION_ARITY","OPAQUE","UNREAD","step","node","parent","AST_NODE_TYPES","accessAt","source","member","over","accessOf","binding","first","within","callback","usageIn","accesses","opaque","identifier","access","provesRun","kindOf","kinds","variable","provedBinding","namedBinding","variableOf","kind","record","keyRead","object","unwrap","keyedBinding","read","bindingAt","element","value","mark","markPattern","whole","each","property","constantCall","declarator","localValue","classifyDeclaration","call","name","apiName"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { TSESTree } from '@typescript-eslint/utils';
2
+ import { TSESLint } from '@typescript-eslint/utils';
3
+ /**
4
+ * Whether this node stands lexically inside a callback whose opened work the declared node releases. The walk goes
5
+ * all the way out, so a nested function body counts; only the resolved context and the position decide.
6
+ */
7
+ declare function isDeclaredIngress(source: TSESLint.SourceCode, node: TSESTree.Node): boolean;
8
+ export { isDeclaredIngress };
@@ -0,0 +1,2 @@
1
+ import{AST_NODE_TYPES as i,TSESLint as p}from"@typescript-eslint/utils";import{propertyName as f}from"./ast.js";import{isFeatureSection as a,isApi as c,definitionOf as m,isFunction as l}from"./model-bindings.js";const y=new Map([["event",1],["scope.keyed",1],["scope.switch",1],["scope.while",1],["stream",2]]),h=new Map([["attach",{argument:1,property:"open"}]]);function g(t){const[n]=t.params;if(n?.type===i.Identifier||n?.type===i.ObjectPattern)return n}function x(t,n){const e=g(n)?.typeAnnotation?.typeAnnotation;if(e?.type!==i.TSTypeReference)return!1;const{typeName:r}=e;return r.type===i.Identifier&&c(t,r,"@opetope/core","ModelContext",!0)}function s(t,n){const e=m(t,n);if(!(e?.type!==p.Scope.DefinitionType.Parameter||!l(e.node)))return{fn:e.node,name:e.name}}function I(t,n){const e=s(t,n);return e!==void 0&&e.name===e.fn.params[0]?e.fn:void 0}function S(t,n){const e=s(t,n),r=e?.name.parent;if(e===void 0||r?.type!==i.Property)return;const o=r.parent===e.fn.params[0]?f(r):void 0;return o===void 0?void 0:{fn:e.fn,method:o}}function w(t){const n=[];let e=t;for(;e.type===i.MemberExpression;){if(e.computed||e.property.type!==i.Identifier)return;n.unshift(e.property.name),e=e.object}return{root:e,steps:n}}function u(t,n){const e=w(n);if(e?.root.type!==i.Identifier)return;const r=e.steps.length===0?void 0:I(t,e.root);if(r!==void 0)return{fn:r,method:e.steps.join(".")};const o=S(t,e.root);return o===void 0?void 0:{fn:o.fn,method:[o.method,...e.steps].join(".")}}function P(t,n){const e=n.parent;if(e.type!==i.CallExpression||e.arguments[1]!==n)return;const r=u(t,e.callee);return r?.method==="model"?r.fn:void 0}function E(t,n){const e=new Set;let r=n;for(;r!==void 0&&!e.has(r);){if(e.add(r),a(t,r,"own",!0)||x(t,r))return!0;r=P(t,r)}return!1}function b(t,n,e){if(y.get(t)===n)return!0;const r=h.get(t);return r?.argument===n&&r.property===e}function A(t,n,e,r){const o=u(t,n.callee);return o!==void 0&&b(o.method,e,r)&&E(t,o.fn)}function C(t,n,e){return t.type!==i.ObjectExpression||n.type!==i.Property?e:f(n)}function M(t,n){let e=n,r;for(const o of t.getAncestors(n).reverse()){if(o.type===i.CallExpression){const d=o.arguments.indexOf(e);if(d!==-1&&A(t,o,d,r))return!0;r=void 0}else r=C(o,e,r);e=o}return!1}export{M as isDeclaredIngress};
2
+ //# sourceMappingURL=declaration-ingress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"declaration-ingress.js","sources":["../src/declaration-ingress.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils';\n\nimport { propertyName } from './ast';\nimport type { FunctionNode } from './model-bindings';\nimport { definitionOf, isApi, isFeatureSection, isFunction } from './model-bindings';\n\n/**\n * The callbacks a declared node opens its work in and owns the release of: the disposer they return is run by the\n * node, in the drain of the generation that opened it. The argument index is part of the contract — `consume`,\n * `run`, `load`, `when` and `key` sit in the same calls and are not ingress.\n */\nconst positionalIngress = new Map([\n ['event', 1],\n ['scope.keyed', 1],\n ['scope.switch', 1],\n ['scope.while', 1],\n ['stream', 2],\n]);\n\n/** The one such callback written as a record member. `attach`'s `close` is a release, not an opening. */\nconst recordIngress = new Map([['attach', { argument: 1, property: 'open' }]]);\n\ntype Annotated = TSESTree.Identifier | TSESTree.ObjectPattern;\ntype NamedMethod = { readonly fn: FunctionNode; readonly method: string };\ntype ParameterOf = { readonly fn: FunctionNode; readonly name: TSESTree.BindingName };\n\nfunction annotatedParameter(fn: FunctionNode): Annotated | undefined {\n const [parameter] = fn.params;\n\n if (parameter?.type === AST_NODE_TYPES.Identifier || parameter?.type === AST_NODE_TYPES.ObjectPattern) {\n return parameter;\n }\n\n return undefined;\n}\n\n/** The factory of a model says so in the parameter it takes: the name `ModelContext` carries where it is exported. */\nfunction declaresModelContext(source: TSESLint.SourceCode, fn: FunctionNode): boolean {\n const annotation = annotatedParameter(fn)?.typeAnnotation?.typeAnnotation;\n\n if (annotation?.type !== AST_NODE_TYPES.TSTypeReference) return false;\n\n const { typeName } = annotation;\n\n return typeName.type === AST_NODE_TYPES.Identifier && isApi(source, typeName, '@opetope/core', 'ModelContext', true);\n}\n\n/** The function this name is a parameter of, and the pattern node that bound it there. */\nfunction parameterOf(source: TSESLint.SourceCode, node: TSESTree.Identifier): ParameterOf | undefined {\n const definition = definitionOf(source, node);\n\n if (definition?.type !== TSESLint.Scope.DefinitionType.Parameter || !isFunction(definition.node)) return undefined;\n\n return { fn: definition.node, name: definition.name };\n}\n\n/** `own` itself: the plain name a context parameter was given, whichever function that parameter belongs to. */\nfunction contextName(source: TSESLint.SourceCode, node: TSESTree.Identifier): FunctionNode | undefined {\n const parameter = parameterOf(source, node);\n\n return parameter !== undefined && parameter.name === parameter.fn.params[0] ? parameter.fn : undefined;\n}\n\n/** `({ stream }) => stream(…)`: the method under the key the destructuring took it from, alias included. */\nfunction destructuredMethod(source: TSESLint.SourceCode, node: TSESTree.Identifier): NamedMethod | undefined {\n const parameter = parameterOf(source, node);\n const property = parameter?.name.parent;\n\n if (parameter === undefined || property?.type !== AST_NODE_TYPES.Property) return undefined;\n\n const method = property.parent === parameter.fn.params[0] ? propertyName(property) : undefined;\n\n return method === undefined ? undefined : { fn: parameter.fn, method };\n}\n\n/** The dotted steps of a member chain and the name it starts from: `own.scope.while` is `own` and `scope.while`. */\nfunction memberSteps(callee: TSESTree.Node): { root: TSESTree.Node; steps: readonly string[] } | undefined {\n const steps: string[] = [];\n let current: TSESTree.Node = callee;\n\n while (current.type === AST_NODE_TYPES.MemberExpression) {\n if (current.computed || current.property.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n steps.unshift(current.property.name);\n current = current.object;\n }\n\n return { root: current, steps };\n}\n\n/**\n * The method this callee names on the first parameter of some function, and that function. Whether the parameter\n * really is a declaring context is a separate question: answering it here would make the two questions circular.\n */\nfunction methodPath(source: TSESLint.SourceCode, callee: TSESTree.Node): NamedMethod | undefined {\n const chain = memberSteps(callee);\n\n if (chain?.root.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n const named = chain.steps.length === 0 ? undefined : contextName(source, chain.root);\n\n if (named !== undefined) return { fn: named, method: chain.steps.join('.') };\n\n const destructured = destructuredMethod(source, chain.root);\n\n return destructured === undefined\n ? undefined\n : { fn: destructured.fn, method: [destructured.method, ...chain.steps].join('.') };\n}\n\n/** One step outward: the context whose `model(…)` call declares this factory, where that is how it was written. */\nfunction declaringContext(source: TSESLint.SourceCode, fn: FunctionNode): FunctionNode | undefined {\n const call = fn.parent;\n\n if (call.type !== AST_NODE_TYPES.CallExpression || call.arguments[1] !== fn) return undefined;\n\n const declaring = methodPath(source, call.callee);\n\n return declaring?.method === 'model' ? declaring.fn : undefined;\n}\n\n/**\n * The two functions that hand an author a declaring context: the `own` section written right where `defineFeature`\n * takes it, and the factory of a model — which is either annotated as one or written where a feature declares it,\n * as the second argument of `model(Declaration, factory)`. That second form is proved by walking outward one\n * `model(…)` at a time, and the set of functions already visited ends a chain that would come back to its start.\n */\nfunction isContextFunction(source: TSESLint.SourceCode, fn: FunctionNode): boolean {\n const seen = new Set<FunctionNode>();\n let current: FunctionNode | undefined = fn;\n\n while (current !== undefined && !seen.has(current)) {\n seen.add(current);\n\n if (isFeatureSection(source, current, 'own', true) || declaresModelContext(source, current)) return true;\n\n current = declaringContext(source, current);\n }\n\n return false;\n}\n\nfunction admitsIngress(method: string, position: number, property: string | undefined): boolean {\n if (positionalIngress.get(method) === position) return true;\n\n const record = recordIngress.get(method);\n\n return record?.argument === position && record.property === property;\n}\n\n/** The shape decides first and the import second: a call that cannot be ingress never pays for the resolution. */\nfunction isIngressArgument(\n source: TSESLint.SourceCode,\n call: TSESTree.CallExpression,\n position: number,\n property: string | undefined,\n): boolean {\n const named = methodPath(source, call.callee);\n\n return named !== undefined && admitsIngress(named.method, position, property) && isContextFunction(source, named.fn);\n}\n\n/** The record member the walk came through since the last call boundary: the `{ open }` of an `attach`. */\nfunction memberProperty(parent: TSESTree.Node, child: TSESTree.Node, current: string | undefined): string | undefined {\n if (parent.type !== AST_NODE_TYPES.ObjectExpression || child.type !== AST_NODE_TYPES.Property) return current;\n\n return propertyName(child);\n}\n\n/**\n * Whether this node stands lexically inside a callback whose opened work the declared node releases. The walk goes\n * all the way out, so a nested function body counts; only the resolved context and the position decide.\n */\nfunction isDeclaredIngress(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n let child: TSESTree.Node = node;\n let property: string | undefined;\n\n for (const parent of source.getAncestors(node).reverse()) {\n if (parent.type === AST_NODE_TYPES.CallExpression) {\n const position = parent.arguments.indexOf(child as TSESTree.CallExpressionArgument);\n\n if (position !== -1 && isIngressArgument(source, parent, position, property)) return true;\n\n property = undefined;\n } else {\n property = memberProperty(parent, child, property);\n }\n\n child = parent;\n }\n\n return false;\n}\n\nexport { isDeclaredIngress };\n"],"names":["positionalIngress","recordIngress","annotatedParameter","fn","parameter","AST_NODE_TYPES","declaresModelContext","source","annotation","typeName","isApi","parameterOf","node","definition","definitionOf","TSESLint","isFunction","contextName","destructuredMethod","property","method","propertyName","memberSteps","callee","steps","current","methodPath","chain","named","destructured","declaringContext","call","declaring","isContextFunction","seen","isFeatureSection","admitsIngress","position","record","isIngressArgument","memberProperty","parent","child","isDeclaredIngress"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -32,9 +32,15 @@ declare const plugin: {
32
32
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
33
33
  name: string;
34
34
  };
35
+ 'no-command-in-deps': import("@typescript-eslint/utils/ts-eslint").RuleModule<"commandHookInDeps" | "commandRecordInDeps" | "dependOnMember", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
36
+ name: string;
37
+ };
35
38
  'no-internal-imports': import("@typescript-eslint/utils/ts-eslint").RuleModule<"internalImport", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
36
39
  name: string;
37
40
  };
41
+ 'no-snapshot-in-update': import("@typescript-eslint/utils/ts-eslint").RuleModule<"snapshotInUpdate" | "writeFromPrevious", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
42
+ name: string;
43
+ };
38
44
  'no-snapshot-read-in-render': import("@typescript-eslint/utils/ts-eslint").RuleModule<"snapshotInRender", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
39
45
  name: string;
40
46
  };
@@ -87,7 +93,9 @@ declare const configuredPlugin: {
87
93
  'opetope/define-feature-property-order': "error";
88
94
  'opetope/id-naming': "error";
89
95
  'opetope/layer-placement': "off";
96
+ 'opetope/no-command-in-deps': "error";
90
97
  'opetope/no-internal-imports': "error";
98
+ 'opetope/no-snapshot-in-update': "error";
91
99
  'opetope/no-snapshot-read-in-render': "error";
92
100
  'opetope/no-subscribe-outside-models': "off";
93
101
  'opetope/prefer-model-selection': "off";
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import{defineFeaturePropertyOrder as p}from"./rules/define-feature-property-order.js";import{idNaming as s}from"./rules/id-naming.js";import{layerPlacement as i}from"./rules/layer-placement.js";import{noInternalImports as l}from"./rules/no-internal-imports.js";import{noSnapshotReadInRender as m}from"./rules/no-snapshot-read-in-render.js";import{noSubscribeOutsideModels as a}from"./rules/no-subscribe-outside-models.js";import{preferModelSelection as d}from"./rules/prefer-model-selection.js";import{requireDeclaredModels as c}from"./rules/require-declared-models.js";import{requireLiteralId as u}from"./rules/require-literal-id.js";import{whenPredicate as f}from"./rules/when-predicate.js";const g=["integration","models","ui"],b=["**/*.ts","**/*.tsx"],h=["**/__tests__/**","**/*.spec.ts","**/*.spec.tsx"],y={meta:{name:"@opetope/lint",version:"0.9.0"},rules:{"define-feature-property-order":p,"id-naming":s,"layer-placement":i,"no-internal-imports":l,"no-snapshot-read-in-render":m,"no-subscribe-outside-models":a,"prefer-model-selection":d,"require-declared-models":c,"require-literal-id":u,"when-predicate":f}},r=y,S={name:"opetope/recommended",plugins:{opetope:r},rules:{"opetope/define-feature-property-order":"error","opetope/id-naming":"error","opetope/layer-placement":"off","opetope/no-internal-imports":"error","opetope/no-snapshot-read-in-render":"error","opetope/no-subscribe-outside-models":"off","opetope/prefer-model-selection":"off","opetope/require-declared-models":"error","opetope/require-literal-id":"error","opetope/when-predicate":"error"}},I=e=>g.flatMap(o=>{const t=e[o]??[];return t.length===0?[]:[{files:[...t],name:`opetope/layers-${o}`,plugins:{opetope:r},rules:{"opetope/layer-placement":["error",{layer:o}]}}]}),q=({models:e=[],tests:o=h})=>e.length===0?[]:[{files:b,name:"opetope/layers-subscriptions",plugins:{opetope:r},rules:{"opetope/no-subscribe-outside-models":"error"}},{files:[...e,...o],name:"opetope/layers-subscriptions-allowed",rules:{"opetope/no-subscribe-outside-models":"off"}}],_=e=>[...I(e),...q(e)],j=({allow:e=[],files:o=["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"]}={})=>[{files:[...o],name:"opetope/internal-imports",plugins:{opetope:r},rules:{"opetope/no-internal-imports":"error"}},...e.length===0?[]:[{files:[...e],name:"opetope/internal-imports-allowed",rules:{"opetope/no-internal-imports":"off"}}]],x=Object.assign(r,{configs:{internalImports:j,layers:_,recommended:S}}),n=x;export{n as default,n as opetopeLint};
1
+ import{defineFeaturePropertyOrder as p}from"./rules/define-feature-property-order.js";import{idNaming as s}from"./rules/id-naming.js";import{layerPlacement as i}from"./rules/layer-placement.js";import{noCommandInDeps as l}from"./rules/no-command-in-deps.js";import{noInternalImports as m}from"./rules/no-internal-imports.js";import{noSnapshotInUpdate as a}from"./rules/no-snapshot-in-update.js";import{noSnapshotReadInRender as d}from"./rules/no-snapshot-read-in-render.js";import{noSubscribeOutsideModels as c}from"./rules/no-subscribe-outside-models.js";import{preferModelSelection as u}from"./rules/prefer-model-selection.js";import{requireDeclaredModels as f}from"./rules/require-declared-models.js";import{requireLiteralId as g}from"./rules/require-literal-id.js";import{whenPredicate as b}from"./rules/when-predicate.js";const h=["integration","models","ui"],y=["**/*.ts","**/*.tsx"],I=["**/__tests__/**","**/*.spec.ts","**/*.spec.tsx"],S={meta:{name:"@opetope/lint",version:"0.9.2"},rules:{"define-feature-property-order":p,"id-naming":s,"layer-placement":i,"no-command-in-deps":l,"no-internal-imports":m,"no-snapshot-in-update":a,"no-snapshot-read-in-render":d,"no-subscribe-outside-models":c,"prefer-model-selection":u,"require-declared-models":f,"require-literal-id":g,"when-predicate":b}},r=S,q={name:"opetope/recommended",plugins:{opetope:r},rules:{"opetope/define-feature-property-order":"error","opetope/id-naming":"error","opetope/layer-placement":"off","opetope/no-command-in-deps":"error","opetope/no-internal-imports":"error","opetope/no-snapshot-in-update":"error","opetope/no-snapshot-read-in-render":"error","opetope/no-subscribe-outside-models":"off","opetope/prefer-model-selection":"off","opetope/require-declared-models":"error","opetope/require-literal-id":"error","opetope/when-predicate":"error"}},_=e=>h.flatMap(o=>{const t=e[o]??[];return t.length===0?[]:[{files:[...t],name:`opetope/layers-${o}`,plugins:{opetope:r},rules:{"opetope/layer-placement":["error",{layer:o}]}}]}),j=({models:e=[],tests:o=I})=>e.length===0?[]:[{files:y,name:"opetope/layers-subscriptions",plugins:{opetope:r},rules:{"opetope/no-subscribe-outside-models":"error"}},{files:[...e,...o],name:"opetope/layers-subscriptions-allowed",rules:{"opetope/no-subscribe-outside-models":"off"}}],x=e=>[..._(e),...j(e)],E=({allow:e=[],files:o=["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"]}={})=>[{files:[...o],name:"opetope/internal-imports",plugins:{opetope:r},rules:{"opetope/no-internal-imports":"error"}},...e.length===0?[]:[{files:[...e],name:"opetope/internal-imports-allowed",rules:{"opetope/no-internal-imports":"off"}}]],L=Object.assign(r,{configs:{internalImports:E,layers:x,recommended:q}}),n=L;export{n as default,n as opetopeLint};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import type { ESLint, Linter } from 'eslint';\n\nimport { defineFeaturePropertyOrder } from './rules/define-feature-property-order';\nimport { idNaming } from './rules/id-naming';\nimport { layerPlacement } from './rules/layer-placement';\nimport { noInternalImports } from './rules/no-internal-imports';\nimport { noSnapshotReadInRender } from './rules/no-snapshot-read-in-render';\nimport { noSubscribeOutsideModels } from './rules/no-subscribe-outside-models';\nimport { preferModelSelection } from './rules/prefer-model-selection';\nimport { requireDeclaredModels } from './rules/require-declared-models';\nimport { requireLiteralId } from './rules/require-literal-id';\nimport { whenPredicate } from './rules/when-predicate';\n\n/**\n * The layers a host names for itself, as globs of the files that hold each one, with the tests that are allowed\n * to reach past a layer. `tests` defaults to the usual two shapes of a test path.\n */\ntype LayerOptions = {\n readonly integration?: readonly string[];\n readonly models?: readonly string[];\n readonly tests?: readonly string[];\n readonly ui?: readonly string[];\n};\n\ntype InternalImportOptions = {\n readonly allow?: readonly string[];\n readonly files?: readonly string[];\n};\n\nconst LAYERS = ['integration', 'models', 'ui'] as const;\nconst SOURCE_FILES = ['**/*.ts', '**/*.tsx'];\nconst TEST_FILES = ['**/__tests__/**', '**/*.spec.ts', '**/*.spec.tsx'];\n\nconst plugin = {\n meta: { name: '@opetope/lint', version: '0.9.0' },\n rules: {\n 'define-feature-property-order': defineFeaturePropertyOrder,\n 'id-naming': idNaming,\n 'layer-placement': layerPlacement,\n 'no-internal-imports': noInternalImports,\n 'no-snapshot-read-in-render': noSnapshotReadInRender,\n 'no-subscribe-outside-models': noSubscribeOutsideModels,\n 'prefer-model-selection': preferModelSelection,\n 'require-declared-models': requireDeclaredModels,\n 'require-literal-id': requireLiteralId,\n 'when-predicate': whenPredicate,\n },\n};\n\n/**\n * The laws every Opetope author writes against, with no assumption about where a host keeps its files. A rule that\n * needs such an assumption is off here and arrives through `layers`. The namespace is `opetope`, so a rule reads\n * `opetope/when-predicate` wherever a host names it.\n */\n// Rules use the parser's typed AST internally. The public bridge exposes the\n// supported ESLint rule API; package smoke tests execute it under both linters.\ntype NativeRule = NonNullable<ESLint.Plugin['rules']>[string];\ntype NativeRules = {\n [Name in keyof typeof plugin.rules]: Omit<(typeof plugin.rules)[Name], 'create'> & NativeRule;\n};\nconst nativePlugin = plugin as unknown as { meta: typeof plugin.meta; rules: NativeRules };\n\nconst recommended = {\n name: 'opetope/recommended',\n plugins: { opetope: nativePlugin },\n rules: {\n 'opetope/define-feature-property-order': 'error',\n 'opetope/id-naming': 'error',\n 'opetope/layer-placement': 'off',\n 'opetope/no-internal-imports': 'error',\n 'opetope/no-snapshot-read-in-render': 'error',\n 'opetope/no-subscribe-outside-models': 'off',\n 'opetope/prefer-model-selection': 'off',\n 'opetope/require-declared-models': 'error',\n 'opetope/require-literal-id': 'error',\n 'opetope/when-predicate': 'error',\n },\n} satisfies Linter.Config;\n\n/**\n * Placement is a convention of the project that names its layers, not a law of the framework: a layer without a\n * glob is a layer this host does not have, and it produces no configuration.\n */\nconst placement = (options: LayerOptions): readonly Linter.Config[] =>\n LAYERS.flatMap(layer => {\n const files = options[layer] ?? [];\n\n if (files.length === 0) return [];\n\n return [\n {\n files: [...files],\n name: `opetope/layers-${layer}`,\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/layer-placement': ['error', { layer }] },\n } satisfies Linter.Config,\n ];\n });\n\n/**\n * A subscription is written where the state lives, so this pair arrives only with the model layer that owns it. The\n * restriction covers every source file and steps aside in that layer and in tests: a file no layer claims is\n * covered rather than forgotten.\n */\nconst subscriptions = ({ models = [], tests = TEST_FILES }: LayerOptions): readonly Linter.Config[] =>\n models.length === 0\n ? []\n : [\n {\n files: SOURCE_FILES,\n name: 'opetope/layers-subscriptions',\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/no-subscribe-outside-models': 'error' },\n },\n {\n files: [...models, ...tests],\n name: 'opetope/layers-subscriptions-allowed',\n rules: { 'opetope/no-subscribe-outside-models': 'off' },\n },\n ];\n\nconst layers = (options: LayerOptions): readonly Linter.Config[] => [...placement(options), ...subscriptions(options)];\n\n/**\n * Only library implementation and explicitly named host integration can cross the internal ABI. A dedicated rule\n * leaves the project's `no-restricted-imports` untouched and also covers literal dynamic imports in JS tests.\n * Allowed files explicitly override `recommended`, which enables this rule as well.\n */\nconst internalImports = ({\n allow = [],\n files = ['**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}'],\n}: InternalImportOptions = {}): readonly Linter.Config[] => [\n {\n files: [...files],\n name: 'opetope/internal-imports',\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/no-internal-imports': 'error' },\n },\n ...(allow.length === 0\n ? []\n : [\n {\n files: [...allow],\n name: 'opetope/internal-imports-allowed',\n rules: { 'opetope/no-internal-imports': 'off' as const },\n },\n ]),\n];\n\nconst configuredPlugin = Object.assign(nativePlugin, {\n configs: { internalImports, layers, recommended },\n});\n\n// ESLint consumes the rule map. Config factories are additional plugin helpers;\n// expose both contracts without requiring casts at native flat-config call sites.\n// oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- Required by native ESLint config typetests.\nconst opetopeLint = configuredPlugin as unknown as typeof configuredPlugin & Pick<ESLint.Plugin, 'configs'>;\n\n/*\n * Two shapes of one object. The default is what a tool reads when it loads a plugin by path — Oxlint's `jsPlugins`\n * takes `(await import(path)).default` — and what an ESLint flat config imports by convention; the named export is\n * for a config that prefers to say which binding it takes.\n */\n// oxlint-disable-next-line import/no-default-export\n// eslint-disable-next-line import-x/no-default-export\nexport default opetopeLint;\nexport { opetopeLint };\n"],"names":["LAYERS","SOURCE_FILES","TEST_FILES","plugin","defineFeaturePropertyOrder","idNaming","layerPlacement","noInternalImports","noSnapshotReadInRender","noSubscribeOutsideModels","preferModelSelection","requireDeclaredModels","requireLiteralId","whenPredicate","nativePlugin","recommended","placement","options","layer","files","subscriptions","models","tests","layers","internalImports","allow","configuredPlugin","opetopeLint"],"mappings":"qrBA6BA,MAAMA,EAAS,CAAC,cAAe,SAAU,IAAI,EACvCC,EAAe,CAAC,UAAW,UAAU,EACrCC,EAAa,CAAC,kBAAmB,eAAgB,eAAe,EAEhEC,EAAS,CACb,KAAM,CAAE,KAAM,gBAAiB,QAAS,OAAO,EAC/C,MAAO,CACL,gCAAiCC,EACjC,YAAaC,EACb,kBAAmBC,EACnB,sBAAuBC,EACvB,6BAA8BC,EAC9B,8BAA+BC,EAC/B,yBAA0BC,EAC1B,0BAA2BC,EAC3B,qBAAsBC,EACtB,iBAAkBC,CACnB,GAcGC,EAAeX,EAEfY,EAAc,CAClB,KAAM,sBACN,QAAS,CAAE,QAASD,CAAY,EAChC,MAAO,CACL,wCAAyC,QACzC,oBAAqB,QACrB,0BAA2B,MAC3B,8BAA+B,QAC/B,qCAAsC,QACtC,sCAAuC,MACvC,iCAAkC,MAClC,kCAAmC,QACnC,6BAA8B,QAC9B,yBAA0B,OAC3B,GAOGE,EAAaC,GACjBjB,EAAO,QAAQkB,GAAQ,CACrB,MAAMC,EAAQF,EAAQC,CAAK,GAAK,CAAA,EAEhC,OAAIC,EAAM,SAAW,EAAU,CAAA,EAExB,CACL,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,kBAAkBD,CAAK,GAC7B,QAAS,CAAE,QAASJ,CAAY,EAChC,MAAO,CAAE,0BAA2B,CAAC,QAAS,CAAE,MAAAI,CAAK,CAAE,CAAC,CACjC,EAE7B,CAAC,EAOGE,EAAgB,CAAC,CAAE,OAAAC,EAAS,CAAA,EAAI,MAAAC,EAAQpB,CAAU,IACtDmB,EAAO,SAAW,EACd,CAAA,EACA,CACE,CACE,MAAOpB,EACP,KAAM,+BACN,QAAS,CAAE,QAASa,CAAY,EAChC,MAAO,CAAE,sCAAuC,OAAO,CACxD,EACD,CACE,MAAO,CAAC,GAAGO,EAAQ,GAAGC,CAAK,EAC3B,KAAM,uCACN,MAAO,CAAE,sCAAuC,KAAK,CACtD,GAGHC,EAAUN,GAAoD,CAAC,GAAGD,EAAUC,CAAO,EAAG,GAAGG,EAAcH,CAAO,CAAC,EAO/GO,EAAkB,CAAC,CACvB,MAAAC,EAAQ,CAAA,EACR,MAAAN,EAAQ,CAAC,sCAAsC,CAAC,EACvB,KAAiC,CAC1D,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,2BACN,QAAS,CAAE,QAASL,CAAY,EAChC,MAAO,CAAE,8BAA+B,OAAO,CAChD,EACD,GAAIW,EAAM,SAAW,EACjB,CAAA,EACA,CACE,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,mCACN,MAAO,CAAE,8BAA+B,KAAc,CACvD,IAIHC,EAAmB,OAAO,OAAOZ,EAAc,CACnD,QAAS,CAAE,gBAAAU,EAAiB,OAAAD,EAAQ,YAAAR,CAAW,CAChD,CAAA,EAKKY,EAAcD"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import type { ESLint, Linter } from 'eslint';\n\nimport { defineFeaturePropertyOrder } from './rules/define-feature-property-order';\nimport { idNaming } from './rules/id-naming';\nimport { layerPlacement } from './rules/layer-placement';\nimport { noCommandInDeps } from './rules/no-command-in-deps';\nimport { noInternalImports } from './rules/no-internal-imports';\nimport { noSnapshotInUpdate } from './rules/no-snapshot-in-update';\nimport { noSnapshotReadInRender } from './rules/no-snapshot-read-in-render';\nimport { noSubscribeOutsideModels } from './rules/no-subscribe-outside-models';\nimport { preferModelSelection } from './rules/prefer-model-selection';\nimport { requireDeclaredModels } from './rules/require-declared-models';\nimport { requireLiteralId } from './rules/require-literal-id';\nimport { whenPredicate } from './rules/when-predicate';\n\n/**\n * The layers a host names for itself, as globs of the files that hold each one, with the tests that are allowed\n * to reach past a layer. `tests` defaults to the usual two shapes of a test path.\n */\ntype LayerOptions = {\n readonly integration?: readonly string[];\n readonly models?: readonly string[];\n readonly tests?: readonly string[];\n readonly ui?: readonly string[];\n};\n\ntype InternalImportOptions = {\n readonly allow?: readonly string[];\n readonly files?: readonly string[];\n};\n\nconst LAYERS = ['integration', 'models', 'ui'] as const;\nconst SOURCE_FILES = ['**/*.ts', '**/*.tsx'];\nconst TEST_FILES = ['**/__tests__/**', '**/*.spec.ts', '**/*.spec.tsx'];\n\nconst plugin = {\n meta: { name: '@opetope/lint', version: '0.9.2' },\n rules: {\n 'define-feature-property-order': defineFeaturePropertyOrder,\n 'id-naming': idNaming,\n 'layer-placement': layerPlacement,\n 'no-command-in-deps': noCommandInDeps,\n 'no-internal-imports': noInternalImports,\n 'no-snapshot-in-update': noSnapshotInUpdate,\n 'no-snapshot-read-in-render': noSnapshotReadInRender,\n 'no-subscribe-outside-models': noSubscribeOutsideModels,\n 'prefer-model-selection': preferModelSelection,\n 'require-declared-models': requireDeclaredModels,\n 'require-literal-id': requireLiteralId,\n 'when-predicate': whenPredicate,\n },\n};\n\n/**\n * The laws every Opetope author writes against, with no assumption about where a host keeps its files. A rule that\n * needs such an assumption is off here and arrives through `layers`. The namespace is `opetope`, so a rule reads\n * `opetope/when-predicate` wherever a host names it.\n */\n// Rules use the parser's typed AST internally. The public bridge exposes the\n// supported ESLint rule API; package smoke tests execute it under both linters.\ntype NativeRule = NonNullable<ESLint.Plugin['rules']>[string];\ntype NativeRules = {\n [Name in keyof typeof plugin.rules]: Omit<(typeof plugin.rules)[Name], 'create'> & NativeRule;\n};\nconst nativePlugin = plugin as unknown as { meta: typeof plugin.meta; rules: NativeRules };\n\nconst recommended = {\n name: 'opetope/recommended',\n plugins: { opetope: nativePlugin },\n rules: {\n 'opetope/define-feature-property-order': 'error',\n 'opetope/id-naming': 'error',\n 'opetope/layer-placement': 'off',\n 'opetope/no-command-in-deps': 'error',\n 'opetope/no-internal-imports': 'error',\n 'opetope/no-snapshot-in-update': 'error',\n 'opetope/no-snapshot-read-in-render': 'error',\n 'opetope/no-subscribe-outside-models': 'off',\n 'opetope/prefer-model-selection': 'off',\n 'opetope/require-declared-models': 'error',\n 'opetope/require-literal-id': 'error',\n 'opetope/when-predicate': 'error',\n },\n} satisfies Linter.Config;\n\n/**\n * Placement is a convention of the project that names its layers, not a law of the framework: a layer without a\n * glob is a layer this host does not have, and it produces no configuration.\n */\nconst placement = (options: LayerOptions): readonly Linter.Config[] =>\n LAYERS.flatMap(layer => {\n const files = options[layer] ?? [];\n\n if (files.length === 0) return [];\n\n return [\n {\n files: [...files],\n name: `opetope/layers-${layer}`,\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/layer-placement': ['error', { layer }] },\n } satisfies Linter.Config,\n ];\n });\n\n/**\n * A subscription is written where the state lives, so this pair arrives only with the model layer that owns it. The\n * restriction covers every source file and steps aside in that layer and in tests: a file no layer claims is\n * covered rather than forgotten.\n */\nconst subscriptions = ({ models = [], tests = TEST_FILES }: LayerOptions): readonly Linter.Config[] =>\n models.length === 0\n ? []\n : [\n {\n files: SOURCE_FILES,\n name: 'opetope/layers-subscriptions',\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/no-subscribe-outside-models': 'error' },\n },\n {\n files: [...models, ...tests],\n name: 'opetope/layers-subscriptions-allowed',\n rules: { 'opetope/no-subscribe-outside-models': 'off' },\n },\n ];\n\nconst layers = (options: LayerOptions): readonly Linter.Config[] => [...placement(options), ...subscriptions(options)];\n\n/**\n * Only library implementation and explicitly named host integration can cross the internal ABI. A dedicated rule\n * leaves the project's `no-restricted-imports` untouched and also covers literal dynamic imports in JS tests.\n * Allowed files explicitly override `recommended`, which enables this rule as well.\n */\nconst internalImports = ({\n allow = [],\n files = ['**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}'],\n}: InternalImportOptions = {}): readonly Linter.Config[] => [\n {\n files: [...files],\n name: 'opetope/internal-imports',\n plugins: { opetope: nativePlugin },\n rules: { 'opetope/no-internal-imports': 'error' },\n },\n ...(allow.length === 0\n ? []\n : [\n {\n files: [...allow],\n name: 'opetope/internal-imports-allowed',\n rules: { 'opetope/no-internal-imports': 'off' as const },\n },\n ]),\n];\n\nconst configuredPlugin = Object.assign(nativePlugin, {\n configs: { internalImports, layers, recommended },\n});\n\n// ESLint consumes the rule map. Config factories are additional plugin helpers;\n// expose both contracts without requiring casts at native flat-config call sites.\n// oxlint-disable-next-line typescript/no-unnecessary-type-assertion -- Required by native ESLint config typetests.\nconst opetopeLint = configuredPlugin as unknown as typeof configuredPlugin & Pick<ESLint.Plugin, 'configs'>;\n\n/*\n * Two shapes of one object. The default is what a tool reads when it loads a plugin by path — Oxlint's `jsPlugins`\n * takes `(await import(path)).default` — and what an ESLint flat config imports by convention; the named export is\n * for a config that prefers to say which binding it takes.\n */\n// oxlint-disable-next-line import/no-default-export\n// eslint-disable-next-line import-x/no-default-export\nexport default opetopeLint;\nexport { opetopeLint };\n"],"names":["LAYERS","SOURCE_FILES","TEST_FILES","plugin","defineFeaturePropertyOrder","idNaming","layerPlacement","noCommandInDeps","noInternalImports","noSnapshotInUpdate","noSnapshotReadInRender","noSubscribeOutsideModels","preferModelSelection","requireDeclaredModels","requireLiteralId","whenPredicate","nativePlugin","recommended","placement","options","layer","files","subscriptions","models","tests","layers","internalImports","allow","configuredPlugin","opetopeLint"],"mappings":"2zBA+BA,MAAMA,EAAS,CAAC,cAAe,SAAU,IAAI,EACvCC,EAAe,CAAC,UAAW,UAAU,EACrCC,EAAa,CAAC,kBAAmB,eAAgB,eAAe,EAEhEC,EAAS,CACb,KAAM,CAAE,KAAM,gBAAiB,QAAS,OAAO,EAC/C,MAAO,CACL,gCAAiCC,EACjC,YAAaC,EACb,kBAAmBC,EACnB,qBAAsBC,EACtB,sBAAuBC,EACvB,wBAAyBC,EACzB,6BAA8BC,EAC9B,8BAA+BC,EAC/B,yBAA0BC,EAC1B,0BAA2BC,EAC3B,qBAAsBC,EACtB,iBAAkBC,CACnB,GAcGC,EAAeb,EAEfc,EAAc,CAClB,KAAM,sBACN,QAAS,CAAE,QAASD,CAAY,EAChC,MAAO,CACL,wCAAyC,QACzC,oBAAqB,QACrB,0BAA2B,MAC3B,6BAA8B,QAC9B,8BAA+B,QAC/B,gCAAiC,QACjC,qCAAsC,QACtC,sCAAuC,MACvC,iCAAkC,MAClC,kCAAmC,QACnC,6BAA8B,QAC9B,yBAA0B,OAC3B,GAOGE,EAAaC,GACjBnB,EAAO,QAAQoB,GAAQ,CACrB,MAAMC,EAAQF,EAAQC,CAAK,GAAK,CAAA,EAEhC,OAAIC,EAAM,SAAW,EAAU,CAAA,EAExB,CACL,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,kBAAkBD,CAAK,GAC7B,QAAS,CAAE,QAASJ,CAAY,EAChC,MAAO,CAAE,0BAA2B,CAAC,QAAS,CAAE,MAAAI,CAAK,CAAE,CAAC,CACjC,EAE7B,CAAC,EAOGE,EAAgB,CAAC,CAAE,OAAAC,EAAS,CAAA,EAAI,MAAAC,EAAQtB,CAAU,IACtDqB,EAAO,SAAW,EACd,CAAA,EACA,CACE,CACE,MAAOtB,EACP,KAAM,+BACN,QAAS,CAAE,QAASe,CAAY,EAChC,MAAO,CAAE,sCAAuC,OAAO,CACxD,EACD,CACE,MAAO,CAAC,GAAGO,EAAQ,GAAGC,CAAK,EAC3B,KAAM,uCACN,MAAO,CAAE,sCAAuC,KAAK,CACtD,GAGHC,EAAUN,GAAoD,CAAC,GAAGD,EAAUC,CAAO,EAAG,GAAGG,EAAcH,CAAO,CAAC,EAO/GO,EAAkB,CAAC,CACvB,MAAAC,EAAQ,CAAA,EACR,MAAAN,EAAQ,CAAC,sCAAsC,CAAC,EACvB,KAAiC,CAC1D,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,2BACN,QAAS,CAAE,QAASL,CAAY,EAChC,MAAO,CAAE,8BAA+B,OAAO,CAChD,EACD,GAAIW,EAAM,SAAW,EACjB,CAAA,EACA,CACE,CACE,MAAO,CAAC,GAAGA,CAAK,EAChB,KAAM,mCACN,MAAO,CAAE,8BAA+B,KAAc,CACvD,IAIHC,EAAmB,OAAO,OAAOZ,EAAc,CACnD,QAAS,CAAE,gBAAAU,EAAiB,OAAAD,EAAQ,YAAAR,CAAW,CAChD,CAAA,EAKKY,EAAcD"}
@@ -4,11 +4,17 @@ type FunctionNode = TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclarat
4
4
  type ModelBinding = TSESLint.Scope.Variable | string;
5
5
  declare function isFunction(node: TSESTree.Node): node is FunctionNode;
6
6
  declare function unwrap(node: TSESTree.Node): TSESTree.Node;
7
+ declare function variableOf(source: TSESLint.SourceCode, node: TSESTree.Identifier): TSESLint.Scope.Variable | null;
8
+ declare function definitionOf(source: TSESLint.SourceCode, node: TSESTree.Identifier): TSESLint.Scope.Definition | undefined;
7
9
  /** Local values only: an imported component or a parameter has no inspectable implementation here. */
8
10
  declare function localValue(source: TSESLint.SourceCode, node: TSESTree.Node, seen?: Set<TSESTree.Node>): TSESTree.Node;
9
- declare function isApi(source: TSESLint.SourceCode, node: TSESTree.Node, module: string, name: string): boolean;
11
+ /** The name an API carries where it is exported, whichever way this file took it in. */
12
+ declare function apiName(source: TSESLint.SourceCode, node: TSESTree.Node, module: string, exact?: boolean): string | undefined;
13
+ declare function isApi(source: TSESLint.SourceCode, node: TSESTree.Node, module: string, name: string, exact?: boolean): boolean;
10
14
  declare function modelBinding(source: TSESLint.SourceCode, node: TSESTree.Node, seen?: Set<TSESTree.Node>): ModelBinding | undefined;
15
+ /** A section written as a callback right where `defineFeature` takes it: `provides: (…) => …`, `own: (…) => …`. */
16
+ declare function isFeatureSection(source: TSESLint.SourceCode, fn: FunctionNode, section: string, exact?: boolean): boolean;
11
17
  declare function isSlot(source: TSESLint.SourceCode, node: TSESTree.Node): boolean;
12
18
  declare function enclosingFunction(source: TSESLint.SourceCode, node: TSESTree.Node): FunctionNode | undefined;
13
19
  export type { FunctionNode, ModelBinding };
14
- export { enclosingFunction, isApi, isFunction, isSlot, localValue, modelBinding, unwrap };
20
+ export { apiName, definitionOf, enclosingFunction, isApi, isFeatureSection, isFunction, isSlot, localValue, modelBinding, unwrap, variableOf, };
@@ -1,2 +1,2 @@
1
- import{AST_NODE_TYPES as r,TSESLint as u,ASTUtils as E}from"@typescript-eslint/utils";import{propertyName as d}from"./ast.js";function s(e){return e.type===r.ArrowFunctionExpression||e.type===r.FunctionDeclaration||e.type===r.FunctionExpression}function a(e){return e.type===r.TSAsExpression||e.type===r.TSSatisfiesExpression||e.type===r.TSNonNullExpression?a(e.expression):e}function m(e,n){return E.findVariable(e.getScope(n),n)}function f(e,n){return m(e,n)?.defs[0]}function y(e){if(e?.type===u.Scope.DefinitionType.FunctionName)return e.node;if(e?.type===u.Scope.DefinitionType.Variable)return e.node.init??void 0}function c(e,n,t=new Set){const i=a(n);if(i.type!==r.Identifier||t.has(i))return i;t.add(i);const p=y(f(e,i));return p===void 0?i:c(e,p,t)}function l(e){return e.imported.type===r.Identifier?e.imported.name:e.imported.value}function S(e,n){return e.type===r.ImportDeclaration&&(e.source.value===n||e.source.value.startsWith("."))}function x(e,n,t,i){if(n.type!==r.Identifier)return!1;const p=f(e,n);return p?.type!==u.Scope.DefinitionType.ImportBinding||!S(p.parent,t)?!1:p.node.type===r.ImportSpecifier&&l(p.node)===i}function I(e,n){if(!(e.type!==r.MemberExpression||e.computed||e.property.type!==r.Identifier||e.property.name!==n))return e.object.type===r.Identifier?e.object:void 0}function D(e,n,t,i){const p=I(n,i);if(p===void 0)return!1;const o=f(e,p);return o?.type===u.Scope.DefinitionType.ImportBinding&&S(o.parent,t)&&o.node.type===r.ImportNamespaceSpecifier}function b(e,n,t,i){const p=c(e,n);return x(e,p,t,i)||D(e,p,t,i)}function F(e){if(!(e?.type!==u.Scope.DefinitionType.ImportBinding||e.node.type!==r.ImportSpecifier||e.parent.type!==r.ImportDeclaration))return`import:${e.parent.source.value}:${l(e.node)}`}function g(e,n){return F(f(e,n))??m(e,n)??`unbound:${n.name}`}function v(e,n,t=new Set){const i=a(n);if(i.type!==r.Identifier)return;const p=f(e,i),o=y(p);return o?.type===r.Identifier&&!t.has(i)?(t.add(i),v(e,o,t)):g(e,i)}function j(e,n){if(n.type!==r.CallExpression)return!1;let t=n.callee;return t.type===r.MemberExpression&&!t.computed&&t.property.type===r.Identifier&&t.property.name==="body"&&(t=t.object),b(e,t,"@opetope/runtime","defineFeature")}function A(e,n){const t=n.parent;return t.type===r.Property&&d(t)==="provides"&&t.parent.type===r.ObjectExpression&&j(e,t.parent.parent)}function T(e,n){const t=f(e,n);if(!(t?.type!==u.Scope.DefinitionType.Parameter||!s(t.node)||!A(e,t.node)))return t.node}function P(e,n){const t=T(e,n)?.params[0];return t?.type!==r.ObjectPattern?!1:t.properties.some(i=>i.type===r.Property&&d(i)==="slot"&&i.value.type===r.Identifier&&i.value.name===n.name)}function N(e,n){const t=I(n,"slot");if(t===void 0)return!1;const p=T(e,t)?.params[0];return p?.type===r.Identifier&&p.name===t.name}function O(e,n){const t=c(e,n);return t.type===r.Identifier?P(e,t):N(e,t)}function B(e,n){return e.getAncestors(n).reverse().find(s)}export{B as enclosingFunction,b as isApi,s as isFunction,O as isSlot,c as localValue,v as modelBinding,a as unwrap};
1
+ import{AST_NODE_TYPES as i,TSESLint as f,ASTUtils as j}from"@typescript-eslint/utils";import{propertyName as m}from"./ast.js";function s(e){return e.type===i.ArrowFunctionExpression||e.type===i.FunctionDeclaration||e.type===i.FunctionExpression}function a(e){return e.type===i.TSAsExpression||e.type===i.TSSatisfiesExpression||e.type===i.TSNonNullExpression?a(e.expression):e}function d(e,t){return j.findVariable(e.getScope(t),t)}function o(e,t){return d(e,t)?.defs[0]}function l(e){if(e?.type===f.Scope.DefinitionType.FunctionName)return e.node;if(e?.type===f.Scope.DefinitionType.Variable)return e.node.init??void 0}function c(e,t,r=new Set){const n=a(t);if(n.type!==i.Identifier||r.has(n))return n;r.add(n);const p=l(o(e,n));return p===void 0?n:c(e,p,r)}function y(e){return e.imported.type===i.Identifier?e.imported.name:e.imported.value}function S(e,t,r){return e.type!==i.ImportDeclaration?!1:e.source.value===t?!0:!r&&t.startsWith("@opetope/")&&e.source.value.startsWith(".")}function x(e,t,r,n){if(t.type!==i.Identifier)return;const p=o(e,t);if(!(p?.type!==f.Scope.DefinitionType.ImportBinding||!S(p.parent,r,n)))return p.node.type===i.ImportSpecifier?y(p.node):void 0}function b(e){if(!(e.type!==i.MemberExpression||e.computed)&&!(e.property.type!==i.Identifier||e.object.type!==i.Identifier))return{name:e.property.name,object:e.object}}function F(e,t){const r=b(e);return r?.name===t?r.object:void 0}function N(e,t,r,n){const p=o(e,t);return p?.type!==f.Scope.DefinitionType.ImportBinding||!S(p.parent,r,n)?!1:p.node.type===i.ImportNamespaceSpecifier?!0:r==="react"&&p.node.type===i.ImportDefaultSpecifier}function I(e,t,r,n=!1){const p=c(e,t),u=b(p);return u===void 0?x(e,p,r,n):N(e,u.object,r,n)?u.name:void 0}function v(e,t,r,n,p=!1){return I(e,t,r,p)===n}function g(e){if(!(e?.type!==f.Scope.DefinitionType.ImportBinding||e.node.type!==i.ImportSpecifier||e.parent.type!==i.ImportDeclaration))return`import:${e.parent.source.value}:${y(e.node)}`}function A(e,t){return g(o(e,t))??d(e,t)??`unbound:${t.name}`}function T(e,t,r=new Set){const n=a(t);if(n.type!==i.Identifier)return;const p=o(e,n),u=l(p);return u?.type===i.Identifier&&!r.has(n)?(r.add(n),T(e,u,r)):A(e,n)}function P(e,t,r){if(t.type!==i.CallExpression)return!1;let n=t.callee;return n.type===i.MemberExpression&&!n.computed&&n.property.type===i.Identifier&&n.property.name==="body"&&(n=n.object),v(e,n,"@opetope/runtime","defineFeature",r)}function E(e,t,r,n=!1){const p=t.parent;return p.type===i.Property&&m(p)===r&&p.parent.type===i.ObjectExpression&&P(e,p.parent.parent,n)}function D(e,t,r){const n=o(e,t);if(!(n?.type!==f.Scope.DefinitionType.Parameter||!s(n.node)||!E(e,n.node,r)))return n.node}function M(e,t){const r=D(e,t,"provides")?.params[0];return r?.type!==i.ObjectPattern?!1:r.properties.some(n=>n.type===i.Property&&m(n)==="slot"&&n.value.type===i.Identifier&&n.value.name===t.name)}function O(e,t){const r=F(t,"slot");if(r===void 0)return!1;const p=D(e,r,"provides")?.params[0];return p?.type===i.Identifier&&p.name===r.name}function h(e,t){const r=c(e,t);return r.type===i.Identifier?M(e,r):O(e,r)}function B(e,t){return e.getAncestors(t).reverse().find(s)}export{I as apiName,o as definitionOf,B as enclosingFunction,v as isApi,E as isFeatureSection,s as isFunction,h as isSlot,c as localValue,T as modelBinding,a as unwrap,d as variableOf};
2
2
  //# sourceMappingURL=model-bindings.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-bindings.js","sources":["../src/model-bindings.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES, ASTUtils, TSESLint } from '@typescript-eslint/utils';\n\nimport { propertyName } from './ast';\n\ntype FunctionNode = TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression;\ntype ModelBinding = TSESLint.Scope.Variable | string;\n\nfunction isFunction(node: TSESTree.Node): node is FunctionNode {\n return (\n node.type === AST_NODE_TYPES.ArrowFunctionExpression ||\n node.type === AST_NODE_TYPES.FunctionDeclaration ||\n node.type === AST_NODE_TYPES.FunctionExpression\n );\n}\n\nfunction unwrap(node: TSESTree.Node): TSESTree.Node {\n if (\n node.type === AST_NODE_TYPES.TSAsExpression ||\n node.type === AST_NODE_TYPES.TSSatisfiesExpression ||\n node.type === AST_NODE_TYPES.TSNonNullExpression\n )\n return unwrap(node.expression);\n\n return node;\n}\n\nfunction variableOf(source: TSESLint.SourceCode, node: TSESTree.Identifier): TSESLint.Scope.Variable | null {\n return ASTUtils.findVariable(source.getScope(node), node);\n}\n\nfunction definitionOf(source: TSESLint.SourceCode, node: TSESTree.Identifier): TSESLint.Scope.Definition | undefined {\n return variableOf(source, node)?.defs[0];\n}\n\nfunction definedValue(definition: TSESLint.Scope.Definition | undefined): TSESTree.Node | undefined {\n if (definition?.type === TSESLint.Scope.DefinitionType.FunctionName) return definition.node;\n if (definition?.type === TSESLint.Scope.DefinitionType.Variable) return definition.node.init ?? undefined;\n return undefined;\n}\n\n/** Local values only: an imported component or a parameter has no inspectable implementation here. */\nfunction localValue(source: TSESLint.SourceCode, node: TSESTree.Node, seen = new Set<TSESTree.Node>()): TSESTree.Node {\n const value = unwrap(node);\n\n if (value.type !== AST_NODE_TYPES.Identifier || seen.has(value)) return value;\n\n seen.add(value);\n const defined = definedValue(definitionOf(source, value));\n\n return defined === undefined ? value : localValue(source, defined, seen);\n}\n\nfunction importName(node: TSESTree.ImportSpecifier): string {\n return node.imported.type === AST_NODE_TYPES.Identifier ? node.imported.name : node.imported.value;\n}\n\n/** Relative re-exports are useful inside a package; unrelated external imports never match. */\nfunction importsFrom(node: TSESTree.Node, module: string): boolean {\n return (\n node.type === AST_NODE_TYPES.ImportDeclaration &&\n (node.source.value === module || node.source.value.startsWith('.'))\n );\n}\n\nfunction importedApi(source: TSESLint.SourceCode, node: TSESTree.Node, module: string, name: string): boolean {\n if (node.type !== AST_NODE_TYPES.Identifier) return false;\n\n const definition = definitionOf(source, node);\n\n if (definition?.type !== TSESLint.Scope.DefinitionType.ImportBinding || !importsFrom(definition.parent, module))\n return false;\n\n return definition.node.type === AST_NODE_TYPES.ImportSpecifier && importName(definition.node) === name;\n}\n\nfunction memberObject(node: TSESTree.Node, name: string): TSESTree.Identifier | undefined {\n if (\n node.type !== AST_NODE_TYPES.MemberExpression ||\n node.computed ||\n node.property.type !== AST_NODE_TYPES.Identifier ||\n node.property.name !== name\n )\n return undefined;\n return node.object.type === AST_NODE_TYPES.Identifier ? node.object : undefined;\n}\n\nfunction namespaceApi(source: TSESLint.SourceCode, node: TSESTree.Node, module: string, name: string): boolean {\n const object = memberObject(node, name);\n if (object === undefined) return false;\n const definition = definitionOf(source, object);\n return (\n definition?.type === TSESLint.Scope.DefinitionType.ImportBinding &&\n importsFrom(definition.parent, module) &&\n definition.node.type === AST_NODE_TYPES.ImportNamespaceSpecifier\n );\n}\n\nfunction isApi(source: TSESLint.SourceCode, node: TSESTree.Node, module: string, name: string): boolean {\n const value = localValue(source, node);\n\n return importedApi(source, value, module, name) || namespaceApi(source, value, module, name);\n}\n\nfunction importedModel(definition: TSESLint.Scope.Definition | undefined): string | undefined {\n if (\n definition?.type !== TSESLint.Scope.DefinitionType.ImportBinding ||\n definition.node.type !== AST_NODE_TYPES.ImportSpecifier ||\n definition.parent.type !== AST_NODE_TYPES.ImportDeclaration\n )\n return undefined;\n return `import:${definition.parent.source.value}:${importName(definition.node)}`;\n}\n\n/** Identity, not spelling: equal names in different lexical scopes are different model declarations. */\nfunction directModelBinding(source: TSESLint.SourceCode, identity: TSESTree.Identifier): ModelBinding {\n return importedModel(definitionOf(source, identity)) ?? variableOf(source, identity) ?? `unbound:${identity.name}`;\n}\n\nfunction modelBinding(\n source: TSESLint.SourceCode,\n node: TSESTree.Node,\n seen = new Set<TSESTree.Node>(),\n): ModelBinding | undefined {\n const identity = unwrap(node);\n if (identity.type !== AST_NODE_TYPES.Identifier) return undefined;\n const definition = definitionOf(source, identity);\n const defined = definedValue(definition);\n if (defined?.type === AST_NODE_TYPES.Identifier && !seen.has(identity)) {\n seen.add(identity);\n return modelBinding(source, defined, seen);\n }\n return directModelBinding(source, identity);\n}\n\nfunction isFeatureCall(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n if (node.type !== AST_NODE_TYPES.CallExpression) return false;\n\n let callee: TSESTree.Node = node.callee;\n\n if (\n callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.property.type === AST_NODE_TYPES.Identifier &&\n callee.property.name === 'body'\n )\n callee = callee.object;\n\n return isApi(source, callee, '@opetope/runtime', 'defineFeature');\n}\n\nfunction isProvides(source: TSESLint.SourceCode, fn: FunctionNode): boolean {\n const property = fn.parent;\n\n return (\n property.type === AST_NODE_TYPES.Property &&\n propertyName(property) === 'provides' &&\n property.parent.type === AST_NODE_TYPES.ObjectExpression &&\n isFeatureCall(source, property.parent.parent)\n );\n}\n\nfunction providesParameter(source: TSESLint.SourceCode, node: TSESTree.Identifier): FunctionNode | undefined {\n const definition = definitionOf(source, node);\n if (\n definition?.type !== TSESLint.Scope.DefinitionType.Parameter ||\n !isFunction(definition.node) ||\n !isProvides(source, definition.node)\n )\n return undefined;\n return definition.node;\n}\n\nfunction slotParameter(source: TSESLint.SourceCode, node: TSESTree.Identifier): boolean {\n const pattern = providesParameter(source, node)?.params[0];\n if (pattern?.type !== AST_NODE_TYPES.ObjectPattern) return false;\n return pattern.properties.some(\n property =>\n property.type === AST_NODE_TYPES.Property &&\n propertyName(property) === 'slot' &&\n property.value.type === AST_NODE_TYPES.Identifier &&\n property.value.name === node.name,\n );\n}\n\nfunction namedSlot(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n const object = memberObject(node, 'slot');\n if (object === undefined) return false;\n const fn = providesParameter(source, object);\n const parameter = fn?.params[0];\n return parameter?.type === AST_NODE_TYPES.Identifier && parameter.name === object.name;\n}\n\nfunction isSlot(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n const value = localValue(source, node);\n\n return value.type === AST_NODE_TYPES.Identifier ? slotParameter(source, value) : namedSlot(source, value);\n}\n\nfunction enclosingFunction(source: TSESLint.SourceCode, node: TSESTree.Node): FunctionNode | undefined {\n return source.getAncestors(node).reverse().find(isFunction);\n}\n\nexport type { FunctionNode, ModelBinding };\nexport { enclosingFunction, isApi, isFunction, isSlot, localValue, modelBinding, unwrap };\n"],"names":["isFunction","node","AST_NODE_TYPES","unwrap","variableOf","source","ASTUtils","definitionOf","definedValue","definition","TSESLint","localValue","seen","value","defined","importName","importsFrom","module","importedApi","name","memberObject","namespaceApi","object","isApi","importedModel","directModelBinding","identity","modelBinding","isFeatureCall","callee","isProvides","fn","property","propertyName","providesParameter","slotParameter","pattern","namedSlot","parameter","isSlot","enclosingFunction"],"mappings":"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"}
1
+ {"version":3,"file":"model-bindings.js","sources":["../src/model-bindings.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES, ASTUtils, TSESLint } from '@typescript-eslint/utils';\n\nimport { propertyName } from './ast';\n\ntype FunctionNode = TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression;\ntype ModelBinding = TSESLint.Scope.Variable | string;\n\nfunction isFunction(node: TSESTree.Node): node is FunctionNode {\n return (\n node.type === AST_NODE_TYPES.ArrowFunctionExpression ||\n node.type === AST_NODE_TYPES.FunctionDeclaration ||\n node.type === AST_NODE_TYPES.FunctionExpression\n );\n}\n\nfunction unwrap(node: TSESTree.Node): TSESTree.Node {\n if (\n node.type === AST_NODE_TYPES.TSAsExpression ||\n node.type === AST_NODE_TYPES.TSSatisfiesExpression ||\n node.type === AST_NODE_TYPES.TSNonNullExpression\n )\n return unwrap(node.expression);\n\n return node;\n}\n\nfunction variableOf(source: TSESLint.SourceCode, node: TSESTree.Identifier): TSESLint.Scope.Variable | null {\n return ASTUtils.findVariable(source.getScope(node), node);\n}\n\nfunction definitionOf(source: TSESLint.SourceCode, node: TSESTree.Identifier): TSESLint.Scope.Definition | undefined {\n return variableOf(source, node)?.defs[0];\n}\n\nfunction definedValue(definition: TSESLint.Scope.Definition | undefined): TSESTree.Node | undefined {\n if (definition?.type === TSESLint.Scope.DefinitionType.FunctionName) return definition.node;\n if (definition?.type === TSESLint.Scope.DefinitionType.Variable) return definition.node.init ?? undefined;\n return undefined;\n}\n\n/** Local values only: an imported component or a parameter has no inspectable implementation here. */\nfunction localValue(source: TSESLint.SourceCode, node: TSESTree.Node, seen = new Set<TSESTree.Node>()): TSESTree.Node {\n const value = unwrap(node);\n\n if (value.type !== AST_NODE_TYPES.Identifier || seen.has(value)) return value;\n\n seen.add(value);\n const defined = definedValue(definitionOf(source, value));\n\n return defined === undefined ? value : localValue(source, defined, seen);\n}\n\nfunction importName(node: TSESTree.ImportSpecifier): string {\n return node.imported.type === AST_NODE_TYPES.Identifier ? node.imported.name : node.imported.value;\n}\n\n/**\n * A relative re-export is useful inside an Opetope package; React is the module it names and nothing else. That\n * allowance widens what a rule reports, so a check that uses the import to stay *silent* asks for `exact` instead:\n * there a host's own `./my-context` must not pass for ours.\n */\nfunction importsFrom(node: TSESTree.Node, module: string, exact: boolean): boolean {\n if (node.type !== AST_NODE_TYPES.ImportDeclaration) return false;\n\n if (node.source.value === module) return true;\n\n return !exact && module.startsWith('@opetope/') && node.source.value.startsWith('.');\n}\n\n/** The name an import carries in the module that exports it, past the local alias a file gave it. */\nfunction importedApiName(\n source: TSESLint.SourceCode,\n node: TSESTree.Node,\n module: string,\n exact: boolean,\n): string | undefined {\n if (node.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n const definition = definitionOf(source, node);\n\n if (\n definition?.type !== TSESLint.Scope.DefinitionType.ImportBinding ||\n !importsFrom(definition.parent, module, exact)\n )\n return undefined;\n\n return definition.node.type === AST_NODE_TYPES.ImportSpecifier ? importName(definition.node) : undefined;\n}\n\n/** `React.useEffect`: the name a member expression reads and the plain name it reads it from. */\nfunction namespaceMember(node: TSESTree.Node): { name: string; object: TSESTree.Identifier } | undefined {\n if (node.type !== AST_NODE_TYPES.MemberExpression || node.computed) return undefined;\n\n if (node.property.type !== AST_NODE_TYPES.Identifier || node.object.type !== AST_NODE_TYPES.Identifier)\n return undefined;\n\n return { name: node.property.name, object: node.object };\n}\n\nfunction memberObject(node: TSESTree.Node, name: string): TSESTree.Identifier | undefined {\n const member = namespaceMember(node);\n\n return member?.name === name ? member.object : undefined;\n}\n\n/**\n * The module taken whole and read through. React's own hooks arrive in the default binding as often as in a\n * namespace one, so `react` accepts both; an `@opetope/*` entry has no default export, and its resolution is\n * unchanged by that allowance.\n */\nfunction isWholeModule(\n source: TSESLint.SourceCode,\n object: TSESTree.Identifier,\n module: string,\n exact: boolean,\n): boolean {\n const definition = definitionOf(source, object);\n\n if (\n definition?.type !== TSESLint.Scope.DefinitionType.ImportBinding ||\n !importsFrom(definition.parent, module, exact)\n )\n return false;\n\n if (definition.node.type === AST_NODE_TYPES.ImportNamespaceSpecifier) return true;\n\n return module === 'react' && definition.node.type === AST_NODE_TYPES.ImportDefaultSpecifier;\n}\n\n/** The name an API carries where it is exported, whichever way this file took it in. */\nfunction apiName(source: TSESLint.SourceCode, node: TSESTree.Node, module: string, exact = false): string | undefined {\n const value = localValue(source, node);\n const member = namespaceMember(value);\n\n if (member === undefined) return importedApiName(source, value, module, exact);\n\n return isWholeModule(source, member.object, module, exact) ? member.name : undefined;\n}\n\nfunction isApi(source: TSESLint.SourceCode, node: TSESTree.Node, module: string, name: string, exact = false): boolean {\n return apiName(source, node, module, exact) === name;\n}\n\nfunction importedModel(definition: TSESLint.Scope.Definition | undefined): string | undefined {\n if (\n definition?.type !== TSESLint.Scope.DefinitionType.ImportBinding ||\n definition.node.type !== AST_NODE_TYPES.ImportSpecifier ||\n definition.parent.type !== AST_NODE_TYPES.ImportDeclaration\n )\n return undefined;\n return `import:${definition.parent.source.value}:${importName(definition.node)}`;\n}\n\n/** Identity, not spelling: equal names in different lexical scopes are different model declarations. */\nfunction directModelBinding(source: TSESLint.SourceCode, identity: TSESTree.Identifier): ModelBinding {\n return importedModel(definitionOf(source, identity)) ?? variableOf(source, identity) ?? `unbound:${identity.name}`;\n}\n\nfunction modelBinding(\n source: TSESLint.SourceCode,\n node: TSESTree.Node,\n seen = new Set<TSESTree.Node>(),\n): ModelBinding | undefined {\n const identity = unwrap(node);\n if (identity.type !== AST_NODE_TYPES.Identifier) return undefined;\n const definition = definitionOf(source, identity);\n const defined = definedValue(definition);\n if (defined?.type === AST_NODE_TYPES.Identifier && !seen.has(identity)) {\n seen.add(identity);\n return modelBinding(source, defined, seen);\n }\n return directModelBinding(source, identity);\n}\n\nfunction isFeatureCall(source: TSESLint.SourceCode, node: TSESTree.Node, exact: boolean): boolean {\n if (node.type !== AST_NODE_TYPES.CallExpression) return false;\n\n let callee: TSESTree.Node = node.callee;\n\n if (\n callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.property.type === AST_NODE_TYPES.Identifier &&\n callee.property.name === 'body'\n )\n callee = callee.object;\n\n return isApi(source, callee, '@opetope/runtime', 'defineFeature', exact);\n}\n\n/** A section written as a callback right where `defineFeature` takes it: `provides: (…) => …`, `own: (…) => …`. */\nfunction isFeatureSection(source: TSESLint.SourceCode, fn: FunctionNode, section: string, exact = false): boolean {\n const property = fn.parent;\n\n return (\n property.type === AST_NODE_TYPES.Property &&\n propertyName(property) === section &&\n property.parent.type === AST_NODE_TYPES.ObjectExpression &&\n isFeatureCall(source, property.parent.parent, exact)\n );\n}\n\nfunction sectionParameter(\n source: TSESLint.SourceCode,\n node: TSESTree.Identifier,\n section: string,\n): FunctionNode | undefined {\n const definition = definitionOf(source, node);\n if (\n definition?.type !== TSESLint.Scope.DefinitionType.Parameter ||\n !isFunction(definition.node) ||\n !isFeatureSection(source, definition.node, section)\n )\n return undefined;\n return definition.node;\n}\n\nfunction slotParameter(source: TSESLint.SourceCode, node: TSESTree.Identifier): boolean {\n const pattern = sectionParameter(source, node, 'provides')?.params[0];\n if (pattern?.type !== AST_NODE_TYPES.ObjectPattern) return false;\n return pattern.properties.some(\n property =>\n property.type === AST_NODE_TYPES.Property &&\n propertyName(property) === 'slot' &&\n property.value.type === AST_NODE_TYPES.Identifier &&\n property.value.name === node.name,\n );\n}\n\nfunction namedSlot(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n const object = memberObject(node, 'slot');\n if (object === undefined) return false;\n const fn = sectionParameter(source, object, 'provides');\n const parameter = fn?.params[0];\n return parameter?.type === AST_NODE_TYPES.Identifier && parameter.name === object.name;\n}\n\nfunction isSlot(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n const value = localValue(source, node);\n\n return value.type === AST_NODE_TYPES.Identifier ? slotParameter(source, value) : namedSlot(source, value);\n}\n\nfunction enclosingFunction(source: TSESLint.SourceCode, node: TSESTree.Node): FunctionNode | undefined {\n return source.getAncestors(node).reverse().find(isFunction);\n}\n\nexport type { FunctionNode, ModelBinding };\nexport {\n apiName,\n definitionOf,\n enclosingFunction,\n isApi,\n isFeatureSection,\n isFunction,\n isSlot,\n localValue,\n modelBinding,\n unwrap,\n variableOf,\n};\n"],"names":["isFunction","node","AST_NODE_TYPES","unwrap","variableOf","source","ASTUtils","definitionOf","definedValue","definition","TSESLint","localValue","seen","value","defined","importName","importsFrom","module","exact","importedApiName","namespaceMember","memberObject","name","member","isWholeModule","object","apiName","isApi","importedModel","directModelBinding","identity","modelBinding","isFeatureCall","callee","isFeatureSection","fn","section","property","propertyName","sectionParameter","slotParameter","pattern","namedSlot","parameter","isSlot","enclosingFunction"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'commandHookInDeps' | 'commandRecordInDeps' | 'dependOnMember';
3
+ declare const noCommandInDeps: TSESLint.RuleModule<MessageIds, [], unknown, TSESLint.RuleListener> & {
4
+ name: string;
5
+ };
6
+ export { noCommandInDeps };
@@ -0,0 +1,2 @@
1
+ import{AST_NODE_TYPES as x}from"@typescript-eslint/utils";import{isFunctionLike as k}from"../ast.js";import{classifyDeclaration as y,bindingAt as v,usageIn as D,STATUS_MEMBERS as E}from"../command-hooks.js";import{apiName as I,unwrap as h}from"../model-bindings.js";import{createRule as S}from"../rule.js";const T=new Set(["useCallback","useEffect","useImperativeHandle","useInsertionEffect","useLayoutEffect","useMemo"]),R=[...E,"run"],M=2;function A(n){const e=n.arguments.at(-1);if(!(n.arguments.length<M||e?.type!==x.ArrayExpression))return{callback:n.arguments.slice(0,-1).findLast(k),deps:e}}function w(n){const e=[];for(const t of R)for(const o of n)o.member===t&&!e.includes(o.text)&&e.push(o.text);return e}function C(n){const[e]=n.accesses;if(!(n.opaque||e?.member!=="run"||e.optional))return n.accesses.every(t=>t.text===e.text)?e.text:void 0}function O(n,e,t){if(e.opaque)return[];const o=w(e.accesses);return n.proved&&!e.accesses.some(c=>c.member==="run")?[...o,`${t}.run`]:o}function P(n,e){const t=n.elements.findIndex(r=>r===e),o=n.elements[t+1]??null,c=n.elements[t-1]??null;return o!==null?[e.range[0],o.range[0]]:c===null?e.range:[c.range[1],e.range[1]]}function g(n,e,t,o){const c=e.elements.some(r=>r!==null&&r!==t&&n.getText(h(r))===o);return r=>c?r.removeRange(P(e,t)):r.replaceText(t,o)}function _(n,e){const t=I(n,e.callee,"react");return t!==void 0&&T.has(t)?A(e):void 0}const j=S({create(n){const e=n.sourceCode,t=new Map,o=[],c=[],r=(s,a,d)=>{const i=e.getText(h(a)),f=D(e,d,s.callback),u=d.record?"commandRecordInDeps":"commandHookInDeps",m=C(f);if(m!==void 0){n.report({data:{name:i},fix:g(e,s.deps,a,m),messageId:u,node:a});return}const l=O(d,f,i).map(p=>({data:{path:p},fix:g(e,s.deps,a,p),messageId:"dependOnMember"}));l.length===0?n.report({data:{name:i},messageId:u,node:a}):n.report({data:{name:i},messageId:u,node:a,suggest:l})},b=s=>{const a=_(e,s);if(a!==void 0)for(const d of a.deps.elements){if(d===null)continue;const i=v(e,t,d);i!==void 0&&r(a,d,i)}};return{CallExpression(s){c.push(s)},VariableDeclarator(s){o.push(s)},"Program:exit"(){for(const s of o)y(e,s,t);for(const s of c)b(s)}}},meta:{docs:{description:"A React dependency array names the stable `run` of a command hook, not the hook object."},fixable:"code",hasSuggestions:!0,messages:{commandHookInDeps:"The hook object of a command is rebuilt on every `inFlight`/`outcome` change, so this dependency re-runs the effect or voids the memo on each of them, and an effect that calls `run` from it loops. Depend on `{{name}}.run` \u2014 stable for the life of the binding \u2014 or on the status field the callback reads (D290).",commandRecordInDeps:"`{{name}}` is the record `useCommands` returns \u2014 as does `useModel` with a selection \u2014 and it is a new object on every render, so this dependency changes on every render: the effect re-runs, the memo is voided, and an effect that calls `run` from it never settles. Depend on the fields the callback uses, such as `hooks.save.run` (D290).",dependOnMember:"Depend on `{{path}}` instead of the object this element names."},schema:[],type:"problem"},name:"no-command-in-deps"});export{j as noCommandInDeps};
2
+ //# sourceMappingURL=no-command-in-deps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-command-in-deps.js","sources":["../../src/rules/no-command-in-deps.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { isFunctionLike } from '../ast';\nimport type { Access, Binding, HookKinds, Usage } from '../command-hooks';\nimport { bindingAt, classifyDeclaration, STATUS_MEMBERS, usageIn } from '../command-hooks';\nimport { apiName, unwrap } from '../model-bindings';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'commandHookInDeps' | 'commandRecordInDeps' | 'dependOnMember';\ntype Site = { readonly callback: TSESTree.Node | undefined; readonly deps: TSESTree.ArrayExpression };\n\nconst REACT_HOOKS = new Set([\n 'useCallback',\n 'useEffect',\n 'useImperativeHandle',\n 'useInsertionEffect',\n 'useLayoutEffect',\n 'useMemo',\n]);\n/** The order a suggestion list is read in: what the callback already watches, and the stable member last. */\nconst MEMBERS = [...STATUS_MEMBERS, 'run'];\n/** A dependency array is an argument, and the callback that reads it is the argument before. */\nconst SITE_ARITY = 2;\n\n/** `useEffect(fn, deps)` and `useImperativeHandle(ref, create, deps)`: the array last, the callback before it. */\nfunction dependencySite(node: TSESTree.CallExpression): Site | undefined {\n const deps = node.arguments.at(-1);\n\n if (node.arguments.length < SITE_ARITY || deps?.type !== AST_NODE_TYPES.ArrayExpression) return undefined;\n\n return { callback: node.arguments.slice(0, -1).findLast(isFunctionLike), deps };\n}\n\n/** Each path the callback wrote once, statuses before `run`: what the author already reads comes first. */\nfunction readPaths(accesses: readonly Access[]): readonly string[] {\n const paths: string[] = [];\n\n for (const member of MEMBERS) {\n for (const access of accesses) {\n if (access.member === member && !paths.includes(access.text)) paths.push(access.text);\n }\n }\n\n return paths;\n}\n\n/** One answer and no other: every read of the binding went through the same `run`, written without a `?.`. */\nfunction fixedPath(usage: Usage): string | undefined {\n const [first] = usage.accesses;\n\n if (usage.opaque || first?.member !== 'run' || first.optional) return undefined;\n\n return usage.accesses.every(access => access.text === first.text) ? first.text : undefined;\n}\n\n/**\n * Oxlint applies the first suggestion unattended, so a use no member replaces — the object passed on, destructured,\n * or read past the members named here — carries none: the report says what is wrong, and the author says what the\n * dependency meant. A selection field proved by `run` keeps that offer even where this callback only reads a status.\n */\nfunction suggestedPaths(binding: Binding, usage: Usage, base: string): readonly string[] {\n if (usage.opaque) return [];\n\n const paths = readPaths(usage.accesses);\n\n return binding.proved && !usage.accesses.some(access => access.member === 'run') ? [...paths, `${base}.run`] : paths;\n}\n\n/** The element and one comma with it, for an array that already names the dependency the fix would write. */\nfunction removalRange(deps: TSESTree.ArrayExpression, element: TSESTree.Node): TSESTree.Range {\n const index = deps.elements.findIndex(other => other === element);\n const next = deps.elements[index + 1] ?? null;\n const previous = deps.elements[index - 1] ?? null;\n\n if (next !== null) return [element.range[0], next.range[0]];\n\n return previous === null ? element.range : [previous.range[1], element.range[1]];\n}\n\n/** A dependency already written stays the one written: the element is removed rather than doubled. */\nfunction rewrite(\n source: TSESLint.SourceCode,\n deps: TSESTree.ArrayExpression,\n element: TSESTree.Node,\n path: string,\n): TSESLint.ReportFixFunction {\n const duplicate = deps.elements.some(\n other => other !== null && other !== element && source.getText(unwrap(other)) === path,\n );\n\n return fixer => (duplicate ? fixer.removeRange(removalRange(deps, element)) : fixer.replaceText(element, path));\n}\n\n/** The dependency site of one of React's own hooks, under whatever local name this file gave the import. */\nfunction reactSite(source: TSESLint.SourceCode, node: TSESTree.CallExpression): Site | undefined {\n const name = apiName(source, node.callee, 'react');\n\n return name !== undefined && REACT_HOOKS.has(name) ? dependencySite(node) : undefined;\n}\n\nconst noCommandInDeps = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const kinds: HookKinds = new Map();\n const declarators: TSESTree.VariableDeclarator[] = [];\n const calls: TSESTree.CallExpression[] = [];\n\n /** The name is read off the unwrapped element, so an `as`, a `satisfies` or a `!` never reaches a fix. */\n const report = (site: Site, element: TSESTree.Node, binding: Binding): void => {\n const base = source.getText(unwrap(element));\n const usage = usageIn(source, binding, site.callback);\n const messageId = binding.record ? 'commandRecordInDeps' : 'commandHookInDeps';\n const path = fixedPath(usage);\n\n if (path !== undefined) {\n context.report({\n data: { name: base },\n fix: rewrite(source, site.deps, element, path),\n messageId,\n node: element,\n });\n\n return;\n }\n\n const suggest = suggestedPaths(binding, usage, base).map(suggestion => ({\n data: { path: suggestion },\n fix: rewrite(source, site.deps, element, suggestion),\n messageId: 'dependOnMember' as const,\n }));\n\n if (suggest.length === 0) context.report({ data: { name: base }, messageId, node: element });\n else context.report({ data: { name: base }, messageId, node: element, suggest });\n };\n\n const check = (call: TSESTree.CallExpression): void => {\n const site = reactSite(source, call);\n\n if (site === undefined) return;\n\n for (const element of site.deps.elements) {\n if (element === null) continue;\n\n const binding = bindingAt(source, kinds, element);\n\n if (binding !== undefined) report(site, element, binding);\n }\n };\n\n return {\n CallExpression(node): void {\n calls.push(node);\n },\n VariableDeclarator(node): void {\n declarators.push(node);\n },\n 'Program:exit'(): void {\n for (const declarator of declarators) classifyDeclaration(source, declarator, kinds);\n for (const call of calls) check(call);\n },\n };\n },\n meta: {\n docs: {\n description: 'A React dependency array names the stable `run` of a command hook, not the hook object.',\n },\n fixable: 'code',\n hasSuggestions: true,\n messages: {\n commandHookInDeps:\n 'The hook object of a command is rebuilt on every `inFlight`/`outcome` change, so this dependency re-runs the effect or voids the memo on each of them, and an effect that calls `run` from it loops. Depend on `{{name}}.run` — stable for the life of the binding — or on the status field the callback reads (D290).',\n commandRecordInDeps:\n '`{{name}}` is the record `useCommands` returns — as does `useModel` with a selection — and it is a new object on every render, so this dependency changes on every render: the effect re-runs, the memo is voided, and an effect that calls `run` from it never settles. Depend on the fields the callback uses, such as `hooks.save.run` (D290).',\n dependOnMember: 'Depend on `{{path}}` instead of the object this element names.',\n },\n schema: [],\n type: 'problem',\n },\n name: 'no-command-in-deps',\n});\n\nexport { noCommandInDeps };\n"],"names":["REACT_HOOKS","MEMBERS","STATUS_MEMBERS","SITE_ARITY","dependencySite","node","deps","AST_NODE_TYPES","isFunctionLike","readPaths","accesses","paths","member","access","fixedPath","usage","first","suggestedPaths","binding","base","removalRange","element","index","other","next","previous","rewrite","source","path","duplicate","unwrap","fixer","reactSite","name","apiName","noCommandInDeps","createRule","context","kinds","declarators","calls","report","site","usageIn","messageId","suggest","suggestion","check","call","bindingAt","declarator","classifyDeclaration"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'snapshotInUpdate' | 'writeFromPrevious';
3
+ declare const noSnapshotInUpdate: TSESLint.RuleModule<MessageIds, [], unknown, TSESLint.RuleListener> & {
4
+ name: string;
5
+ };
6
+ export { noSnapshotInUpdate };
@@ -0,0 +1,2 @@
1
+ import{createRule as l}from"../rule.js";import{siteAt as c,editsOf as m}from"../snapshot-updates.js";const h=l({create(o){const n=o.sourceCode,t={barriers:[],calls:[],declarators:[],functions:[]},p=(e,r)=>a=>m(n,e,r).map(s=>a.replaceTextRange(s.range,s.text)),u=e=>{const[r]=e.reads;if(r===void 0)return;const{node:a}=r,{name:s}=e;if(s===void 0||!e.rewritable){o.report({messageId:"snapshotInUpdate",node:a});return}const i=p(e,s);if(e.free&&e.aliases.every(d=>d.removable)){o.report({fix:i,messageId:"snapshotInUpdate",node:a});return}o.report({messageId:"snapshotInUpdate",node:a,suggest:[{data:{name:s},fix:i,messageId:"writeFromPrevious"}]})};return{":function"(e){t.functions.push(e)},"AssignmentExpression, AwaitExpression, UpdateExpression, YieldExpression"(e){t.barriers.push(e)},UnaryExpression(e){e.operator==="delete"&&t.barriers.push(e)},CallExpression(e){t.calls.push(e)},VariableDeclarator(e){t.declarators.push(e)},"Program:exit"(){for(const e of t.calls){const r=c(n,e,t);r!==void 0&&u(r)}}}},meta:{docs:{description:"A write computes its value from the previous one, not from a snapshot of the state it replaces."},fixable:"code",hasSuggestions:!0,messages:{snapshotInUpdate:"This value reads the state it is about to replace: `getSnapshot()` runs before the write, on a state that may already be closed, and the write then lands on a value read earlier. Compute it from the previous value \u2014 `update(state, previous => ({ ...previous, ... }))` \u2014 which is read at write time and is not computed at all for a write the runtime drops (D282, D288).",writeFromPrevious:"Compute the value from `{{name}}`, the value this write replaces."},schema:[],type:"problem"},name:"no-snapshot-in-update"});export{h as noSnapshotInUpdate};
2
+ //# sourceMappingURL=no-snapshot-in-update.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-snapshot-in-update.js","sources":["../../src/rules/no-snapshot-in-update.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\n\nimport { createRule } from '../rule';\nimport type { Site, Written } from '../snapshot-updates';\nimport { editsOf, siteAt } from '../snapshot-updates';\n\ntype MessageIds = 'snapshotInUpdate' | 'writeFromPrevious';\n\nconst noSnapshotInUpdate = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const written: Written = { barriers: [], calls: [], declarators: [], functions: [] };\n\n const rewrite =\n (site: Site, name: string): TSESLint.ReportFixFunction =>\n fixer =>\n editsOf(source, site, name).map(edit => fixer.replaceTextRange(edit.range, edit.text));\n\n /**\n * Oxlint applies the first suggestion unattended, so a value this rule cannot move into an updater carries\n * none: the write is still wrong, and how to compute it from the previous value is the author's to write.\n */\n const report = (site: Site): void => {\n const [read] = site.reads;\n\n if (read === undefined) return;\n\n const { node } = read;\n const { name } = site;\n\n if (name === undefined || !site.rewritable) {\n context.report({ messageId: 'snapshotInUpdate', node });\n\n return;\n }\n\n const fix = rewrite(site, name);\n\n // A name of its own and no reader left behind: only then does the rewrite need nobody to look at it.\n if (site.free && site.aliases.every(alias => alias.removable)) {\n context.report({ fix, messageId: 'snapshotInUpdate', node });\n\n return;\n }\n\n context.report({\n messageId: 'snapshotInUpdate',\n node,\n suggest: [{ data: { name }, fix, messageId: 'writeFromPrevious' }],\n });\n };\n\n return {\n ':function'(node: TSESTree.Node): void {\n written.functions.push(node);\n },\n 'AssignmentExpression, AwaitExpression, UpdateExpression, YieldExpression'(node: TSESTree.Node): void {\n written.barriers.push(node);\n },\n // `delete` is the one unary operator that writes; the rest compute, and computing travels with the value.\n UnaryExpression(node): void {\n if (node.operator === 'delete') written.barriers.push(node);\n },\n CallExpression(node): void {\n written.calls.push(node);\n },\n VariableDeclarator(node): void {\n written.declarators.push(node);\n },\n 'Program:exit'(): void {\n for (const call of written.calls) {\n const site = siteAt(source, call, written);\n\n if (site !== undefined) report(site);\n }\n },\n };\n },\n meta: {\n docs: {\n description: 'A write computes its value from the previous one, not from a snapshot of the state it replaces.',\n },\n fixable: 'code',\n hasSuggestions: true,\n messages: {\n snapshotInUpdate:\n 'This value reads the state it is about to replace: `getSnapshot()` runs before the write, on a state that may already be closed, and the write then lands on a value read earlier. Compute it from the previous value — `update(state, previous => ({ ...previous, ... }))` — which is read at write time and is not computed at all for a write the runtime drops (D282, D288).',\n writeFromPrevious: 'Compute the value from `{{name}}`, the value this write replaces.',\n },\n schema: [],\n type: 'problem',\n },\n name: 'no-snapshot-in-update',\n});\n\nexport { noSnapshotInUpdate };\n"],"names":["noSnapshotInUpdate","createRule","context","source","written","rewrite","site","name","fixer","editsOf","edit","report","read","node","fix","alias","call","siteAt"],"mappings":"qGAQA,MAAMA,EAAqBC,EAA2B,CACpD,OAAOC,EAAO,CACZ,MAAMC,EAASD,EAAQ,WACjBE,EAAmB,CAAE,SAAU,GAAI,MAAO,CAAA,EAAI,YAAa,CAAA,EAAI,UAAW,EAAE,EAE5EC,EACJ,CAACC,EAAYC,IACbC,GACEC,EAAQN,EAAQG,EAAMC,CAAI,EAAE,IAAIG,GAAQF,EAAM,iBAAiBE,EAAK,MAAOA,EAAK,IAAI,CAAC,EAMnFC,EAAUL,GAAoB,CAClC,KAAM,CAACM,CAAI,EAAIN,EAAK,MAEpB,GAAIM,IAAS,OAAW,OAExB,KAAM,CAAE,KAAAC,CAAI,EAAKD,EACX,CAAE,KAAAL,CAAI,EAAKD,EAEjB,GAAIC,IAAS,QAAa,CAACD,EAAK,WAAY,CAC1CJ,EAAQ,OAAO,CAAE,UAAW,mBAAoB,KAAAW,CAAI,CAAE,EAEtD,MACF,CAEA,MAAMC,EAAMT,EAAQC,EAAMC,CAAI,EAG9B,GAAID,EAAK,MAAQA,EAAK,QAAQ,MAAMS,GAASA,EAAM,SAAS,EAAG,CAC7Db,EAAQ,OAAO,CAAE,IAAAY,EAAK,UAAW,mBAAoB,KAAAD,EAAM,EAE3D,MACF,CAEAX,EAAQ,OAAO,CACb,UAAW,mBACX,KAAAW,EACA,QAAS,CAAC,CAAE,KAAM,CAAE,KAAAN,CAAI,EAAI,IAAAO,EAAK,UAAW,oBAAqB,CAClE,CAAA,CACH,EAEA,MAAO,CACL,YAAYD,EAAmB,CAC7BT,EAAQ,UAAU,KAAKS,CAAI,CAC7B,EACA,2EAA2EA,EAAmB,CAC5FT,EAAQ,SAAS,KAAKS,CAAI,CAC5B,EAEA,gBAAgBA,EAAI,CACdA,EAAK,WAAa,UAAUT,EAAQ,SAAS,KAAKS,CAAI,CAC5D,EACA,eAAeA,EAAI,CACjBT,EAAQ,MAAM,KAAKS,CAAI,CACzB,EACA,mBAAmBA,EAAI,CACrBT,EAAQ,YAAY,KAAKS,CAAI,CAC/B,EACA,gBAAc,CACZ,UAAWG,KAAQZ,EAAQ,MAAO,CAChC,MAAME,EAAOW,EAAOd,EAAQa,EAAMZ,CAAO,EAErCE,IAAS,QAAWK,EAAOL,CAAI,CACrC,CACF,EAEJ,EACA,KAAM,CACJ,KAAM,CACJ,YAAa,iGACd,EACD,QAAS,OACT,eAAgB,GAChB,SAAU,CACR,iBACE,6XACF,kBAAmB,mEACpB,EACD,OAAQ,CAAA,EACR,KAAM,SACP,EACD,KAAM,uBACP,CAAA"}
@@ -1,2 +1,2 @@
1
- import{AST_NODE_TYPES as r}from"@typescript-eslint/utils";import{createRule as s}from"../rule.js";function n(t){const{callee:e}=t;return e.type!==r.MemberExpression||e.computed||t.arguments.length===0?!1:e.property.type===r.Identifier&&e.property.name==="subscribe"}const o=s({create(t){return{CallExpression(e){n(e)&&t.report({messageId:"handWrittenSubscription",node:e.callee})}}},meta:{docs:{description:"A hand-written subscription stays in the layer that owns the state it observes."},messages:{handWrittenSubscription:"A subscription written by hand owns a cleanup nobody here can see. In a component read with `useReadable` or a model selection; in a feature or a model declare `effect`, `event` or `stream`."},schema:[],type:"problem"},name:"no-subscribe-outside-models"});export{o as noSubscribeOutsideModels};
1
+ import{AST_NODE_TYPES as t}from"@typescript-eslint/utils";import{isDeclaredIngress as s}from"../declaration-ingress.js";import{createRule as n}from"../rule.js";function o(r){const{callee:e}=r;return e.type!==t.MemberExpression||e.computed||r.arguments.length===0?!1:e.property.type===t.Identifier&&e.property.name==="subscribe"}const i=n({create(r){return{CallExpression(e){!o(e)||s(r.sourceCode,e)||r.report({messageId:"handWrittenSubscription",node:e.callee})}}},meta:{docs:{description:"A hand-written subscription stays in the layer that owns the state it observes."},messages:{handWrittenSubscription:"A subscription written by hand owns a cleanup nobody here can see. In a component read with `useReadable` or a model selection; in a feature or a model declare `effect`, `event` or `stream`."},schema:[],type:"problem"},name:"no-subscribe-outside-models"});export{i as noSubscribeOutsideModels};
2
2
  //# sourceMappingURL=no-subscribe-outside-models.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"no-subscribe-outside-models.js","sources":["../../src/rules/no-subscribe-outside-models.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { createRule } from '../rule';\n\ntype MessageIds = 'handWrittenSubscription';\n\n/** A call, not a reference: `useSyncExternalStore(source.subscribe, …)` hands the function over without calling it. */\nfunction isSubscribeCall(node: TSESTree.CallExpression): boolean {\n const { callee } = node;\n\n if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed || node.arguments.length === 0) return false;\n\n return callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === 'subscribe';\n}\n\nconst noSubscribeOutsideModels = createRule<[], MessageIds>({\n create(context) {\n return {\n CallExpression(node): void {\n if (isSubscribeCall(node)) {\n context.report({ messageId: 'handWrittenSubscription', node: node.callee });\n }\n },\n };\n },\n meta: {\n docs: {\n description: 'A hand-written subscription stays in the layer that owns the state it observes.',\n },\n messages: {\n handWrittenSubscription:\n 'A subscription written by hand owns a cleanup nobody here can see. In a component read with `useReadable` or a model selection; in a feature or a model declare `effect`, `event` or `stream`.',\n },\n schema: [],\n type: 'problem',\n },\n name: 'no-subscribe-outside-models',\n});\n\nexport { noSubscribeOutsideModels };\n"],"names":["isSubscribeCall","node","callee","AST_NODE_TYPES","noSubscribeOutsideModels","createRule","context"],"mappings":"kGAQA,SAASA,EAAgBC,EAA6B,CACpD,KAAM,CAAE,OAAAC,CAAM,EAAKD,EAEnB,OAAIC,EAAO,OAASC,EAAe,kBAAoBD,EAAO,UAAYD,EAAK,UAAU,SAAW,EAAU,GAEvGC,EAAO,SAAS,OAASC,EAAe,YAAcD,EAAO,SAAS,OAAS,WACxF,CAEA,MAAME,EAA2BC,EAA2B,CAC1D,OAAOC,EAAO,CACZ,MAAO,CACL,eAAeL,EAAI,CACbD,EAAgBC,CAAI,GACtBK,EAAQ,OAAO,CAAE,UAAW,0BAA2B,KAAML,EAAK,OAAQ,CAE9E,EAEJ,EACA,KAAM,CACJ,KAAM,CACJ,YAAa,iFACd,EACD,SAAU,CACR,wBACE,gMACH,EACD,OAAQ,CAAA,EACR,KAAM,SACP,EACD,KAAM,6BACP,CAAA"}
1
+ {"version":3,"file":"no-subscribe-outside-models.js","sources":["../../src/rules/no-subscribe-outside-models.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { isDeclaredIngress } from '../declaration-ingress';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'handWrittenSubscription';\n\n/** A call, not a reference: `useSyncExternalStore(source.subscribe, …)` hands the function over without calling it. */\nfunction isSubscribeCall(node: TSESTree.CallExpression): boolean {\n const { callee } = node;\n\n if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed || node.arguments.length === 0) return false;\n\n return callee.property.type === AST_NODE_TYPES.Identifier && callee.property.name === 'subscribe';\n}\n\nconst noSubscribeOutsideModels = createRule<[], MessageIds>({\n create(context) {\n return {\n CallExpression(node): void {\n if (!isSubscribeCall(node) || isDeclaredIngress(context.sourceCode, node)) return;\n\n context.report({ messageId: 'handWrittenSubscription', node: node.callee });\n },\n };\n },\n meta: {\n docs: {\n description: 'A hand-written subscription stays in the layer that owns the state it observes.',\n },\n messages: {\n handWrittenSubscription:\n 'A subscription written by hand owns a cleanup nobody here can see. In a component read with `useReadable` or a model selection; in a feature or a model declare `effect`, `event` or `stream`.',\n },\n schema: [],\n type: 'problem',\n },\n name: 'no-subscribe-outside-models',\n});\n\nexport { noSubscribeOutsideModels };\n"],"names":["isSubscribeCall","node","callee","AST_NODE_TYPES","noSubscribeOutsideModels","createRule","context","isDeclaredIngress"],"mappings":"gKASA,SAASA,EAAgBC,EAA6B,CACpD,KAAM,CAAE,OAAAC,CAAM,EAAKD,EAEnB,OAAIC,EAAO,OAASC,EAAe,kBAAoBD,EAAO,UAAYD,EAAK,UAAU,SAAW,EAAU,GAEvGC,EAAO,SAAS,OAASC,EAAe,YAAcD,EAAO,SAAS,OAAS,WACxF,CAEA,MAAME,EAA2BC,EAA2B,CAC1D,OAAOC,EAAO,CACZ,MAAO,CACL,eAAeL,EAAI,CACb,CAACD,EAAgBC,CAAI,GAAKM,EAAkBD,EAAQ,WAAYL,CAAI,GAExEK,EAAQ,OAAO,CAAE,UAAW,0BAA2B,KAAML,EAAK,OAAQ,CAC5E,EAEJ,EACA,KAAM,CACJ,KAAM,CACJ,YAAa,iFACd,EACD,SAAU,CACR,wBACE,gMACH,EACD,OAAQ,CAAA,EACR,KAAM,SACP,EACD,KAAM,6BACP,CAAA"}
@@ -0,0 +1,39 @@
1
+ import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
+ /** What one file wrote, collected before any write is judged: a rule answers on `Program:exit` (D290). */
3
+ type Written = {
4
+ readonly barriers: TSESTree.Node[];
5
+ readonly calls: TSESTree.CallExpression[];
6
+ readonly declarators: TSESTree.VariableDeclarator[];
7
+ readonly functions: TSESTree.Node[];
8
+ };
9
+ /** One node the rewrite replaces: the parameter goes where the whole read was, a field of it where a field was. */
10
+ type Read = {
11
+ readonly key: string | undefined;
12
+ readonly node: TSESTree.Node;
13
+ };
14
+ /** A name given to the read before the write: the references inside the value, and whether the name has no others. */
15
+ type Alias = {
16
+ readonly declaration: TSESTree.VariableDeclaration;
17
+ readonly references: readonly Read[];
18
+ readonly removable: boolean;
19
+ };
20
+ /** One `update(state, value)` whose value reads the state it replaces, with everything a rewrite of it needs. */
21
+ type Site = {
22
+ readonly aliases: readonly Alias[];
23
+ readonly free: boolean;
24
+ readonly name: string | undefined;
25
+ readonly reads: readonly Read[];
26
+ readonly rewritable: boolean;
27
+ readonly value: TSESTree.Node;
28
+ };
29
+ /** One replacement the rewrite writes, so the rule hands the fixer ranges and never a node of its own. */
30
+ type Edit = {
31
+ readonly range: TSESTree.Range;
32
+ readonly text: string;
33
+ };
34
+ /** The write this rule reports, or nothing: a value that reads another readable replaces no state of its own. */
35
+ declare function siteAt(source: TSESLint.SourceCode, node: TSESTree.CallExpression, written: Written): Site | undefined;
36
+ /** One rewrite, whether it is offered as a fix or as a suggestion: the value, and the names it leaves unused. */
37
+ declare function editsOf(source: TSESLint.SourceCode, site: Site, name: string): readonly Edit[];
38
+ export type { Site, Written };
39
+ export { editsOf, siteAt };
@@ -0,0 +1,2 @@
1
+ import{AST_NODE_TYPES as s,AST_TOKEN_TYPES as g}from"@typescript-eslint/utils";import{isFunctionLike as T,propertyName as I}from"./ast.js";import{enclosingFunction as y,unwrap as l,variableOf as m}from"./model-bindings.js";const A=["previous","snapshot"],S=2;function R(e){return e.type===s.Identifier?e.name==="update":e.type!==s.MemberExpression||e.computed?!1:e.property.type===s.Identifier&&e.property.name==="update"}function O(e){if(e===void 0)return;const n=l(e);return n.type===s.Identifier||n.type===s.MemberExpression?n:void 0}function $(e){if(!R(e.callee)||e.arguments.length!==S)return;const[n,t]=e.arguments,r=O(n);if(!(r===void 0||t===void 0||t.type===s.SpreadElement))return T(l(t))?void 0:{state:r,value:t}}function h(e){const{callee:n}=e;if(!(n.type!==s.MemberExpression||n.computed)&&!(n.property.type!==s.Identifier||n.property.name!=="getSnapshot"))return e.arguments.length===0?l(n.object):void 0}function v(e,n,t){if(n.type!==s.Identifier)return t.type===s.MemberExpression&&e.getText(t)===e.getText(n);if(t.type!==s.Identifier)return!1;const r=m(e,n),i=m(e,t);return r===null&&i===null?n.name===t.name:r===i}function a(e,n){return e.range[0]>=n.range[0]&&e.range[1]<=n.range[1]}function k(e,n,t){return t.functions.some(r=>a(r,n)&&a(e,r))}function w(e,n,t,r){const i=[];for(const u of r.calls){if(!a(u,t))continue;const f=h(u);f!==void 0&&v(e,n,f)&&i.push(u)}return i}function M(e){const n=[];for(const t of e.properties){if(t.type!==s.Property||t.value.type!==s.Identifier)return;const r=I(t);if(r===void 0)return;n.push({id:t.value,key:r})}return n}function N(e){return e.type===s.Identifier?[{id:e,key:void 0}]:e.type===s.ObjectPattern?M(e):void 0}function C(e){const n=N(e.id);if(n===void 0||e.init===null)return;const t=l(e.init),r=t.type===s.CallExpression?h(t):void 0;return r===void 0?void 0:{bindings:n,receiver:r}}function P(e){const{parent:n}=e;return n===void 0?!1:n.type===s.AssignmentExpression?n.left===e:n.type===s.UpdateExpression}function F(e,n){return e.references.some(({identifier:t})=>t!==n.id&&P(t))}function _(e,n){return!a(e,n)&&!a(n,e)}function Y(e,n,t,r){return t.key===void 0&&n.kind==="const"&&r===0&&n.declarations.length===1&&_(e,n)}function j(e,n,t,r){const i=[];let u=0;for(const{identifier:o}of r.references)o!==t.id&&(a(o,e)?i.push({key:t.key,node:o}):u+=1);const f=n.parent;if(i.length!==0&&!(f.kind!=="const"&&F(r,t)))return{declaration:f,references:i,removable:Y(e,f,t,u)}}function L(e,n,t,r,i){const u=[];for(const f of r){if(y(e,f.id)!==i)continue;const o=m(e,f.id),d=o===null?void 0:j(n,t,f,o);d!==void 0&&u.push(d)}return u}function U(e,n,t,r,i){const u=[];for(const f of i.declarators){const o=C(f);o===void 0||!v(e,n,o.receiver)||u.push(...L(e,t,f,o.bindings,r))}return u}function q(e,n){const t=new Set(e.getTokens(n).map(r=>r.value));return A.filter(r=>!t.has(r))}function z(e,n,t){let r=e.getScope(n);for(;r!==null;){if(r.variables.some(i=>i.name===t))return!1;r=r.upper}return!0}function B(e,n,t){const r=q(e,t),i=r.find(u=>z(e,n,u));return{free:i!==void 0,name:i??r[0]}}function D(e,n,t){return t.barriers.some(r=>a(r,n)&&!k(r,n,t))?!1:!e.some(r=>k(r.node,n,t))}function K(e,n,t){const r=$(n);if(r===void 0)return;const{state:i,value:u}=r,f=w(e,i,u,t).map(c=>({key:void 0,node:c})),o=U(e,i,u,y(e,n),t),d=o.flatMap(c=>c.references),p=[...f,...d].sort((c,E)=>c.node.range[0]-E.node.range[0]);if(p.length===0)return;const{free:b,name:x}=B(e,n,u);return{aliases:o,free:b,name:x,reads:p,rewritable:D(p,u,t),value:u}}function W(e,n){return e.getFirstToken(n)?.value==="{"||n.type===s.SequenceExpression}function G(e,n,t){const r=n.key===void 0?t:`${t}.${n.key}`,{parent:i}=n.node;return i?.type!==s.Property||!i.shorthand||i.value!==n.node?r:`${e.getText(i.key)}: ${r}`}function H(e,n,t){const r=n.value.range[0],i=e.getText(n.value);let u="",f=0;for(const o of n.reads)u+=i.slice(f,o.node.range[0]-r)+G(e,o,t),f=o.node.range[1]-r;return u+=i.slice(f),`${t} => ${W(e,n.value)?`(${u})`:u}`}function J(e,n,t){return!(t.type===g.Line||t.type===g.Block)||t.loc.start.line!==n.loc.end.line?t:e.getTokenAfter(t,{includeComments:!0})}function Q(e,n){const t=e.getTokenAfter(n,{includeComments:!0}),r=t===null?null:J(e,n,t);return r===null?n.range:[n.range[0],r.range[0]]}function V(e,n,t){const r=n.aliases.filter(i=>i.removable).map(i=>({range:Q(e,i.declaration),text:""}));return[{range:n.value.range,text:H(e,n,t)},...r]}export{V as editsOf,K as siteAt};
2
+ //# sourceMappingURL=snapshot-updates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-updates.js","sources":["../src/snapshot-updates.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES, AST_TOKEN_TYPES } from '@typescript-eslint/utils';\n\nimport { isFunctionLike, propertyName } from './ast';\nimport { enclosingFunction, unwrap, variableOf } from './model-bindings';\n\n/** What one file wrote, collected before any write is judged: a rule answers on `Program:exit` (D290). */\ntype Written = {\n readonly barriers: TSESTree.Node[];\n readonly calls: TSESTree.CallExpression[];\n readonly declarators: TSESTree.VariableDeclarator[];\n readonly functions: TSESTree.Node[];\n};\n\n/** One node the rewrite replaces: the parameter goes where the whole read was, a field of it where a field was. */\ntype Read = { readonly key: string | undefined; readonly node: TSESTree.Node };\n\n/** What a declaration gave the read: the value under one name, or fields of it under names of their own. */\ntype Bound = { readonly id: TSESTree.Identifier; readonly key: string | undefined };\n\n/** A name given to the read before the write: the references inside the value, and whether the name has no others. */\ntype Alias = {\n readonly declaration: TSESTree.VariableDeclaration;\n readonly references: readonly Read[];\n readonly removable: boolean;\n};\n\n/** One `update(state, value)` whose value reads the state it replaces, with everything a rewrite of it needs. */\ntype Site = {\n readonly aliases: readonly Alias[];\n readonly free: boolean;\n readonly name: string | undefined;\n readonly reads: readonly Read[];\n readonly rewritable: boolean;\n readonly value: TSESTree.Node;\n};\n\n/** One replacement the rewrite writes, so the rule hands the fixer ranges and never a node of its own. */\ntype Edit = { readonly range: TSESTree.Range; readonly text: string };\n\n/** The parameter the updater is written with, in the order a reader expects to meet it. */\nconst NAMES = ['previous', 'snapshot'];\nconst ARITY = 2;\n\n/** `update(state, value)` and `ctx.update(state, value)` alike: the writer is called by that name (D288). */\nfunction isUpdateCallee(node: TSESTree.Node): boolean {\n if (node.type === AST_NODE_TYPES.Identifier) return node.name === 'update';\n\n if (node.type !== AST_NODE_TYPES.MemberExpression || node.computed) return false;\n\n return node.property.type === AST_NODE_TYPES.Identifier && node.property.name === 'update';\n}\n\n/** The readable a write names first, where it is named at all: a call in that position is somebody else's value. */\nfunction stateArgument(node: TSESTree.Node | undefined): TSESTree.Node | undefined {\n if (node === undefined) return undefined;\n\n const state = unwrap(node);\n\n return state.type === AST_NODE_TYPES.Identifier || state.type === AST_NODE_TYPES.MemberExpression ? state : undefined;\n}\n\n/**\n * The shape this rule reads: two arguments, a readable named first, and a value that is not already an updater.\n * A spread in the value position is not the value — the arguments are assembled elsewhere, and what arrives in\n * this position is not written here.\n */\nfunction writeAt(node: TSESTree.CallExpression): { state: TSESTree.Node; value: TSESTree.Node } | undefined {\n if (!isUpdateCallee(node.callee) || node.arguments.length !== ARITY) return undefined;\n\n const [first, second] = node.arguments;\n const state = stateArgument(first);\n\n if (state === undefined || second === undefined || second.type === AST_NODE_TYPES.SpreadElement) return undefined;\n\n return isFunctionLike(unwrap(second)) ? undefined : { state, value: second };\n}\n\n/** `s.getSnapshot()`, and the receiver it was read on; a reference that is not called reads nothing. */\nfunction snapshotReceiver(node: TSESTree.CallExpression): TSESTree.Node | undefined {\n const { callee } = node;\n\n if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) return undefined;\n\n if (callee.property.type !== AST_NODE_TYPES.Identifier || callee.property.name !== 'getSnapshot') return undefined;\n\n return node.arguments.length === 0 ? unwrap(callee.object) : undefined;\n}\n\n/**\n * The same readable: a name is the binding it resolves to, and a member expression is the path it spells. Two\n * different objects written the same way are one receiver here, which is what reading shapes rather than types buys.\n */\nfunction sameReceiver(source: TSESLint.SourceCode, state: TSESTree.Node, candidate: TSESTree.Node): boolean {\n if (state.type !== AST_NODE_TYPES.Identifier) {\n return candidate.type === AST_NODE_TYPES.MemberExpression && source.getText(candidate) === source.getText(state);\n }\n\n if (candidate.type !== AST_NODE_TYPES.Identifier) return false;\n\n const named = variableOf(source, state);\n const read = variableOf(source, candidate);\n\n return named === null && read === null ? state.name === candidate.name : named === read;\n}\n\nfunction within(node: TSESTree.Node, outer: TSESTree.Node): boolean {\n return node.range[0] >= outer.range[0] && node.range[1] <= outer.range[1];\n}\n\n/** A function written inside the value runs later than the value does, so what it holds does not travel with it. */\nfunction nested(node: TSESTree.Node, value: TSESTree.Node, written: Written): boolean {\n return written.functions.some(fn => within(fn, value) && within(node, fn));\n}\n\n/** Every read of this state written into the value itself. */\nfunction snapshotsIn(\n source: TSESLint.SourceCode,\n state: TSESTree.Node,\n value: TSESTree.Node,\n written: Written,\n): readonly TSESTree.CallExpression[] {\n const reads: TSESTree.CallExpression[] = [];\n\n for (const call of written.calls) {\n if (!within(call, value)) continue;\n\n const receiver = snapshotReceiver(call);\n\n if (receiver !== undefined && sameReceiver(source, state, receiver)) reads.push(call);\n }\n\n return reads;\n}\n\n/** `const { items } = s.getSnapshot()`: a field taken apart is still that field of the read, under a name. */\nfunction patternNames(pattern: TSESTree.ObjectPattern): readonly Bound[] | undefined {\n const names: Bound[] = [];\n\n for (const property of pattern.properties) {\n if (property.type !== AST_NODE_TYPES.Property || property.value.type !== AST_NODE_TYPES.Identifier)\n return undefined;\n\n const key = propertyName(property);\n\n if (key === undefined) return undefined;\n\n names.push({ id: property.value, key });\n }\n\n return names;\n}\n\n/** Every name a declaration gave the read; a shape this rule cannot write back through gives none. */\nfunction boundNames(id: TSESTree.Node): readonly Bound[] | undefined {\n if (id.type === AST_NODE_TYPES.Identifier) return [{ id, key: undefined }];\n\n return id.type === AST_NODE_TYPES.ObjectPattern ? patternNames(id) : undefined;\n}\n\n/** `const current = s.getSnapshot()`: the names it binds and the receiver it read, where it is written that plainly. */\nfunction namedRead(\n declarator: TSESTree.VariableDeclarator,\n): { bindings: readonly Bound[]; receiver: TSESTree.Node } | undefined {\n const bindings = boundNames(declarator.id);\n\n if (bindings === undefined || declarator.init === null) return undefined;\n\n const init = unwrap(declarator.init);\n const receiver = init.type === AST_NODE_TYPES.CallExpression ? snapshotReceiver(init) : undefined;\n\n return receiver === undefined ? undefined : { bindings, receiver };\n}\n\n/** A name written again does not hold the read it was opened with, so the rewrite has nothing to say about it. */\nfunction isWritten(node: TSESTree.Node): boolean {\n const { parent } = node;\n\n if (parent === undefined) return false;\n\n if (parent.type === AST_NODE_TYPES.AssignmentExpression) return parent.left === node;\n\n return parent.type === AST_NODE_TYPES.UpdateExpression;\n}\n\nfunction isReassigned(variable: TSESLint.Scope.Variable, binding: Bound): boolean {\n return variable.references.some(({ identifier }) => identifier !== binding.id && isWritten(identifier));\n}\n\n/** Code where the two overlap is broken before this rule reads it, and a rewrite of it must not overlap either. */\nfunction separate(value: TSESTree.Node, declaration: TSESTree.Node): boolean {\n return !within(value, declaration) && !within(declaration, value);\n}\n\n/** Only a `const` that held the whole read, for this value and nobody else, leaves the file with it. */\nfunction isRemovable(\n value: TSESTree.Node,\n declaration: TSESTree.VariableDeclaration,\n binding: Bound,\n elsewhere: number,\n): boolean {\n return (\n binding.key === undefined &&\n declaration.kind === 'const' &&\n elsewhere === 0 &&\n declaration.declarations.length === 1 &&\n separate(value, declaration)\n );\n}\n\n/** The references this value makes of one name, and whether the declaration behind it goes with them. */\nfunction aliasOf(\n value: TSESTree.Node,\n declarator: TSESTree.VariableDeclarator,\n binding: Bound,\n variable: TSESLint.Scope.Variable,\n): Alias | undefined {\n const references: Read[] = [];\n let elsewhere = 0;\n\n for (const { identifier } of variable.references) {\n if (identifier === binding.id) continue;\n\n if (within(identifier, value)) references.push({ key: binding.key, node: identifier });\n else elsewhere += 1;\n }\n\n const declaration = declarator.parent;\n\n if (references.length === 0) return undefined;\n if (declaration.kind !== 'const' && isReassigned(variable, binding)) return undefined;\n\n return { declaration, references, removable: isRemovable(value, declaration, binding, elsewhere) };\n}\n\n/** Each name one declaration gave this read and this value reads back. */\nfunction aliasesIn(\n source: TSESLint.SourceCode,\n value: TSESTree.Node,\n declarator: TSESTree.VariableDeclarator,\n bindings: readonly Bound[],\n owner: TSESTree.Node | undefined,\n): readonly Alias[] {\n const aliases: Alias[] = [];\n\n for (const binding of bindings) {\n if (enclosingFunction(source, binding.id) !== owner) continue;\n\n const variable = variableOf(source, binding.id);\n const alias = variable === null ? undefined : aliasOf(value, declarator, binding, variable);\n\n if (alias !== undefined) aliases.push(alias);\n }\n\n return aliases;\n}\n\n/** Each name of this file that holds a read of this state and is read back inside the value. */\nfunction aliasesOf(\n source: TSESLint.SourceCode,\n state: TSESTree.Node,\n value: TSESTree.Node,\n owner: TSESTree.Node | undefined,\n written: Written,\n): readonly Alias[] {\n const aliases: Alias[] = [];\n\n for (const declarator of written.declarators) {\n const read = namedRead(declarator);\n\n if (read === undefined || !sameReceiver(source, state, read.receiver)) continue;\n\n aliases.push(...aliasesIn(source, value, declarator, read.bindings, owner));\n }\n\n return aliases;\n}\n\n/** A name the value never spells, so the parameter a rewrite introduces cannot take one the value reads. */\nfunction candidates(source: TSESLint.SourceCode, value: TSESTree.Node): readonly string[] {\n const spelled = new Set(source.getTokens(value).map(token => token.value));\n\n return NAMES.filter(name => !spelled.has(name));\n}\n\n/** A name no enclosing scope holds: only such a name is written without shadowing something the file named. */\nfunction isFree(source: TSESLint.SourceCode, node: TSESTree.Node, name: string): boolean {\n let scope: TSESLint.Scope.Scope | null = source.getScope(node);\n\n while (scope !== null) {\n if (scope.variables.some(variable => variable.name === name)) return false;\n\n scope = scope.upper;\n }\n\n return true;\n}\n\nfunction naming(\n source: TSESLint.SourceCode,\n node: TSESTree.Node,\n value: TSESTree.Node,\n): { free: boolean; name: string | undefined } {\n const usable = candidates(source, value);\n const free = usable.find(name => isFree(source, node, name));\n\n return { free: free !== undefined, name: free ?? usable[0] };\n}\n\n/**\n * The rewrite moves the value into the body of a function, so what the value does outside one does not move with\n * it: an `await` or a `yield` written there no longer parses, an assignment would run at write time instead of\n * here, and a read kept for a later call would freeze on the value of this moment.\n */\nfunction isRewritable(reads: readonly Read[], value: TSESTree.Node, written: Written): boolean {\n if (written.barriers.some(node => within(node, value) && !nested(node, value, written))) return false;\n\n return !reads.some(read => nested(read.node, value, written));\n}\n\n/** The write this rule reports, or nothing: a value that reads another readable replaces no state of its own. */\nfunction siteAt(source: TSESLint.SourceCode, node: TSESTree.CallExpression, written: Written): Site | undefined {\n const write = writeAt(node);\n\n if (write === undefined) return undefined;\n\n const { state, value } = write;\n const direct = snapshotsIn(source, state, value, written).map(call => ({ key: undefined, node: call }));\n const aliases = aliasesOf(source, state, value, enclosingFunction(source, node), written);\n const named = aliases.flatMap(alias => alias.references);\n const reads = [...direct, ...named].sort((left, right) => left.node.range[0] - right.node.range[0]);\n\n if (reads.length === 0) return undefined;\n\n const { free, name } = naming(source, node, value);\n\n return { aliases, free, name, reads, rewritable: isRewritable(reads, value, written), value };\n}\n\n/** An object literal opens with the token that opens a function body, so the updater returns it in parentheses. */\nfunction parenthesized(source: TSESLint.SourceCode, value: TSESTree.Node): boolean {\n return source.getFirstToken(value)?.value === '{' || value.type === AST_NODE_TYPES.SequenceExpression;\n}\n\n/** A shorthand property spells the name once for the key and once for the value; only the value is replaced. */\nfunction replacement(source: TSESLint.SourceCode, read: Read, name: string): string {\n const text = read.key === undefined ? name : `${name}.${read.key}`;\n const { parent } = read.node;\n\n if (parent?.type !== AST_NODE_TYPES.Property || !parent.shorthand || parent.value !== read.node) return text;\n\n return `${source.getText(parent.key)}: ${text}`;\n}\n\n/** The value as its author wrote it, with every read of the state it replaces reading the previous value instead. */\nfunction updater(source: TSESLint.SourceCode, site: Site, name: string): string {\n const start = site.value.range[0];\n const text = source.getText(site.value);\n let rewritten = '';\n let at = 0;\n\n for (const read of site.reads) {\n rewritten += text.slice(at, read.node.range[0] - start) + replacement(source, read, name);\n at = read.node.range[1] - start;\n }\n\n rewritten += text.slice(at);\n\n return `${name} => ${parenthesized(source, site.value) ? `(${rewritten})` : rewritten}`;\n}\n\n/** A comment on the line the declaration ends explains that declaration, and it has nothing to explain without it. */\nfunction trailingComment(\n source: TSESLint.SourceCode,\n declaration: TSESTree.VariableDeclaration,\n next: TSESTree.Token,\n): TSESTree.Token | null {\n const comment = next.type === AST_TOKEN_TYPES.Line || next.type === AST_TOKEN_TYPES.Block;\n\n if (!comment || next.loc.start.line !== declaration.loc.end.line) return next;\n\n return source.getTokenAfter(next, { includeComments: true });\n}\n\n/** The declaration and the space up to the line that follows it, so the statement leaves no blank of its own. */\nfunction removalRange(source: TSESLint.SourceCode, declaration: TSESTree.VariableDeclaration): TSESTree.Range {\n const next = source.getTokenAfter(declaration, { includeComments: true });\n const after = next === null ? null : trailingComment(source, declaration, next);\n\n return after === null ? declaration.range : [declaration.range[0], after.range[0]];\n}\n\n/** One rewrite, whether it is offered as a fix or as a suggestion: the value, and the names it leaves unused. */\nfunction editsOf(source: TSESLint.SourceCode, site: Site, name: string): readonly Edit[] {\n const removals = site.aliases\n .filter(alias => alias.removable)\n .map(alias => ({ range: removalRange(source, alias.declaration), text: '' }));\n\n return [{ range: site.value.range, text: updater(source, site, name) }, ...removals];\n}\n\nexport type { Site, Written };\nexport { editsOf, siteAt };\n"],"names":["NAMES","ARITY","isUpdateCallee","node","AST_NODE_TYPES","stateArgument","state","unwrap","writeAt","first","second","isFunctionLike","snapshotReceiver","callee","sameReceiver","source","candidate","named","variableOf","read","within","outer","nested","value","written","fn","snapshotsIn","reads","call","receiver","patternNames","pattern","names","property","key","propertyName","boundNames","id","namedRead","declarator","bindings","init","isWritten","parent","isReassigned","variable","binding","identifier","separate","declaration","isRemovable","elsewhere","aliasOf","references","aliasesIn","owner","aliases","enclosingFunction","alias","aliasesOf","candidates","spelled","token","name","isFree","scope","naming","usable","free","isRewritable","siteAt","write","direct","left","right","parenthesized","replacement","text","updater","site","start","rewritten","at","trailingComment","next","AST_TOKEN_TYPES","removalRange","after","editsOf","removals"],"mappings":"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"}
package/oxlintrc.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "rules": {
3
+ "opetope/define-feature-property-order": "error",
4
+ "opetope/id-naming": "error",
5
+ "opetope/layer-placement": "off",
6
+ "opetope/no-command-in-deps": "error",
7
+ "opetope/no-internal-imports": "error",
8
+ "opetope/no-snapshot-in-update": "error",
9
+ "opetope/no-snapshot-read-in-render": "error",
10
+ "opetope/no-subscribe-outside-models": "off",
11
+ "opetope/prefer-model-selection": "off",
12
+ "opetope/require-declared-models": "error",
13
+ "opetope/require-literal-id": "error",
14
+ "opetope/when-predicate": "error"
15
+ }
16
+ }