@opetope/lint 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +460 -4
- package/README.md +312 -61
- package/README.ru.md +251 -61
- package/dist/ast.d.ts +3 -1
- package/dist/ast.js +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/command-hooks.d.ts +2 -2
- package/dist/command-hooks.js +1 -1
- package/dist/command-hooks.js.map +1 -1
- package/dist/context-members.d.ts +22 -0
- package/dist/context-members.js +2 -0
- package/dist/context-members.js.map +1 -0
- package/dist/declaration-ingress.d.ts +10 -2
- package/dist/declaration-ingress.js +1 -1
- package/dist/declaration-ingress.js.map +1 -1
- package/dist/effect-declarations.d.ts +20 -0
- package/dist/effect-declarations.js +2 -0
- package/dist/effect-declarations.js.map +1 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/retired-vocabulary.d.ts +31 -0
- package/dist/retired-vocabulary.js +2 -0
- package/dist/retired-vocabulary.js.map +1 -0
- package/dist/rules/capture-command-cleanup.js +1 -1
- package/dist/rules/capture-command-cleanup.js.map +1 -1
- package/dist/rules/define-feature-property-order.js +1 -1
- package/dist/rules/define-feature-property-order.js.map +1 -1
- package/dist/rules/enabled-predicate.d.ts +6 -0
- package/dist/rules/enabled-predicate.js +2 -0
- package/dist/rules/enabled-predicate.js.map +1 -0
- package/dist/rules/no-command-in-deps.js +1 -1
- package/dist/rules/no-command-in-deps.js.map +1 -1
- package/dist/rules/no-internal-imports.js +1 -1
- package/dist/rules/no-internal-imports.js.map +1 -1
- package/dist/rules/no-retired-vocabulary.d.ts +5 -0
- package/dist/rules/no-retired-vocabulary.js +2 -0
- package/dist/rules/no-retired-vocabulary.js.map +1 -0
- package/dist/rules/no-write-after-source-write.d.ts +6 -0
- package/dist/rules/no-write-after-source-write.js +2 -0
- package/dist/rules/no-write-after-source-write.js.map +1 -0
- package/dist/rules/prefer-effect-current.js +1 -1
- package/dist/rules/prefer-effect-current.js.map +1 -1
- package/oxlintrc.json +3 -1
- package/package.json +1 -1
- package/dist/rules/when-predicate.d.ts +0 -6
- package/dist/rules/when-predicate.js +0 -2
- package/dist/rules/when-predicate.js.map +0 -1
|
@@ -1 +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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"}
|
|
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 selection whose fields may be hooks, or one such field (D388). */\ntype HookKind = 'candidate' | 'hook' | '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(['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 = useModel(M, select)` names the record; `const { save } = useModel(M, select)` 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 // D388: `useCommands` is gone; the record a render depends on now comes from `useModel` with a selection.\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,22 @@
|
|
|
1
|
+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import type { FunctionNode } from './model-bindings.js';
|
|
3
|
+
type Variable = TSESLint.Scope.Variable;
|
|
4
|
+
/**
|
|
5
|
+
* The names one callback reads its execution context through: the parameter itself, and what was taken out of it —
|
|
6
|
+
* renamed in the parameter list, taken apart by a declaration inside the body, or given a second name by a `const`.
|
|
7
|
+
* A closure over a member, a member stored anywhere but a `const` and a context handed to a helper are not followed,
|
|
8
|
+
* and every rule that reads this says so in its own boundary.
|
|
9
|
+
*/
|
|
10
|
+
type ContextMembers<Key extends string> = {
|
|
11
|
+
readonly bound: Map<Key, Set<Variable>>;
|
|
12
|
+
readonly parameter: Variable | undefined;
|
|
13
|
+
};
|
|
14
|
+
/** A name taken from the context under this key, or the key read off the context itself. */
|
|
15
|
+
declare function fromContext<Key extends string>(source: TSESLint.SourceCode, node: TSESTree.Node, members: ContextMembers<Key>, key: Key): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* The context of one callback, whether it arrived whole under a name or taken apart where the callback receives it.
|
|
18
|
+
* The position is part of the contract: a command takes its context second and a reaction run takes it first.
|
|
19
|
+
*/
|
|
20
|
+
declare function contextMembers<Key extends string>(source: TSESLint.SourceCode, fn: FunctionNode, position: number, keys: readonly Key[], declarators: readonly TSESTree.VariableDeclarator[]): ContextMembers<Key>;
|
|
21
|
+
export type { ContextMembers };
|
|
22
|
+
export { contextMembers, fromContext };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{AST_NODE_TYPES as o}from"@typescript-eslint/utils";import{within as b,propertyName as v}from"./ast.js";import{variableOf as p,unwrap as I}from"./model-bindings.js";const O=new Set;function P(n,t){return n.bound.get(t)??O}function c(n,t,e){const r=n.bound.get(t);r===void 0?n.bound.set(t,new Set([e])):r.add(e)}function l(n,t,e){return e!==void 0&&t.type===o.Identifier&&p(n,t)===e}function w(n,t,e,r){return t.type!==o.MemberExpression||t.computed?!1:t.property.type===o.Identifier&&t.property.name===r&&l(n,t.object,e.parameter)}function m(n,t,e,r){if(t.type!==o.Identifier)return w(n,t,e,r);const i=p(n,t);return i!==null&&P(e,r).has(i)}function s(n,t,e,r){for(const i of t.properties){if(i.type!==o.Property||i.value.type!==o.Identifier)continue;const u=v(i),f=r.find(d=>d===u),a=f===void 0?null:p(n,i.value);f!==void 0&&a!==null&&c(e,f,a)}}function x(n,t,e,r){const{id:i,init:u}=t;if(u===null)return;const f=I(u);if(i.type===o.ObjectPattern){l(n,f,e.parameter)&&s(n,i,e,r);return}const a=i.type===o.Identifier?p(n,i):null,d=r.find(y=>m(n,f,e,y));a!==null&&d!==void 0&&c(e,d,a)}function E(n,t){return t?.type===o.Identifier?p(n,t)??void 0:void 0}function M(n,t,e,r,i){const u=t.params[e],f={bound:new Map,parameter:E(n,u)};u?.type===o.ObjectPattern&&s(n,u,f,r);for(const a of i)b(a,t)&&x(n,a,f,r);return f}export{M as contextMembers,m as fromContext};
|
|
2
|
+
//# sourceMappingURL=context-members.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-members.js","sources":["../src/context-members.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { propertyName, within } from './ast';\nimport type { FunctionNode } from './model-bindings';\nimport { unwrap, variableOf } from './model-bindings';\n\ntype Variable = TSESLint.Scope.Variable;\n\n/**\n * The names one callback reads its execution context through: the parameter itself, and what was taken out of it —\n * renamed in the parameter list, taken apart by a declaration inside the body, or given a second name by a `const`.\n * A closure over a member, a member stored anywhere but a `const` and a context handed to a helper are not followed,\n * and every rule that reads this says so in its own boundary.\n */\ntype ContextMembers<Key extends string> = {\n readonly bound: Map<Key, Set<Variable>>;\n readonly parameter: Variable | undefined;\n};\n\nconst NONE: ReadonlySet<Variable> = new Set();\n\nfunction boundOf<Key extends string>(members: ContextMembers<Key>, key: Key): ReadonlySet<Variable> {\n return members.bound.get(key) ?? NONE;\n}\n\nfunction add<Key extends string>(members: ContextMembers<Key>, key: Key, variable: Variable): void {\n const bound = members.bound.get(key);\n\n if (bound === undefined) members.bound.set(key, new Set([variable]));\n else bound.add(variable);\n}\n\nfunction isParameter(source: TSESLint.SourceCode, node: TSESTree.Node, parameter: Variable | undefined): boolean {\n return parameter !== undefined && node.type === AST_NODE_TYPES.Identifier && variableOf(source, node) === parameter;\n}\n\n/** `execution.<key>`, read off the context parameter itself. */\nfunction isContextMember<Key extends string>(\n source: TSESLint.SourceCode,\n node: TSESTree.Node,\n members: ContextMembers<Key>,\n key: Key,\n): boolean {\n if (node.type !== AST_NODE_TYPES.MemberExpression || node.computed) return false;\n\n return (\n node.property.type === AST_NODE_TYPES.Identifier &&\n node.property.name === key &&\n isParameter(source, node.object, members.parameter)\n );\n}\n\n/** A name taken from the context under this key, or the key read off the context itself. */\nfunction fromContext<Key extends string>(\n source: TSESLint.SourceCode,\n node: TSESTree.Node,\n members: ContextMembers<Key>,\n key: Key,\n): boolean {\n if (node.type !== AST_NODE_TYPES.Identifier) return isContextMember(source, node, members, key);\n\n const variable = variableOf(source, node);\n\n return variable !== null && boundOf(members, key).has(variable);\n}\n\n/** `({ update, signal: aborting })`: the variables a pattern bound under the keys a rule reads. */\nfunction takeFrom<Key extends string>(\n source: TSESLint.SourceCode,\n pattern: TSESTree.ObjectPattern,\n members: ContextMembers<Key>,\n keys: readonly Key[],\n): void {\n for (const property of pattern.properties) {\n if (property.type !== AST_NODE_TYPES.Property || property.value.type !== AST_NODE_TYPES.Identifier) continue;\n\n const name = propertyName(property);\n const key = keys.find(candidate => candidate === name);\n const variable = key === undefined ? null : variableOf(source, property.value);\n\n if (key !== undefined && variable !== null) add(members, key, variable);\n }\n}\n\n/**\n * `const { update } = execution` takes the context apart one line later, and `const write = update` gives a name the\n * body already holds a second one. Declarations are read in the order they are written, so a name is known before an\n * alias of it.\n */\nfunction takeFromDeclarator<Key extends string>(\n source: TSESLint.SourceCode,\n declarator: TSESTree.VariableDeclarator,\n members: ContextMembers<Key>,\n keys: readonly Key[],\n): void {\n const { id, init } = declarator;\n\n if (init === null) return;\n\n const value = unwrap(init);\n\n if (id.type === AST_NODE_TYPES.ObjectPattern) {\n if (isParameter(source, value, members.parameter)) takeFrom(source, id, members, keys);\n\n return;\n }\n\n const variable = id.type === AST_NODE_TYPES.Identifier ? variableOf(source, id) : null;\n const key = keys.find(candidate => fromContext(source, value, members, candidate));\n\n if (variable !== null && key !== undefined) add(members, key, variable);\n}\n\n/** The context parameter under the name it arrived with, or nothing where the callback took it apart at once. */\nfunction receivedParameter(\n source: TSESLint.SourceCode,\n received: TSESTree.Parameter | undefined,\n): Variable | undefined {\n return received?.type === AST_NODE_TYPES.Identifier ? (variableOf(source, received) ?? undefined) : undefined;\n}\n\n/**\n * The context of one callback, whether it arrived whole under a name or taken apart where the callback receives it.\n * The position is part of the contract: a command takes its context second and a reaction run takes it first.\n */\nfunction contextMembers<Key extends string>(\n source: TSESLint.SourceCode,\n fn: FunctionNode,\n position: number,\n keys: readonly Key[],\n declarators: readonly TSESTree.VariableDeclarator[],\n): ContextMembers<Key> {\n const received = fn.params[position];\n const members: ContextMembers<Key> = { bound: new Map(), parameter: receivedParameter(source, received) };\n\n if (received?.type === AST_NODE_TYPES.ObjectPattern) takeFrom(source, received, members, keys);\n\n for (const declarator of declarators) {\n if (within(declarator, fn)) takeFromDeclarator(source, declarator, members, keys);\n }\n\n return members;\n}\n\nexport type { ContextMembers };\nexport { contextMembers, fromContext };\n"],"names":["NONE","boundOf","members","key","add","variable","bound","isParameter","source","node","parameter","AST_NODE_TYPES","variableOf","isContextMember","fromContext","takeFrom","pattern","keys","property","name","propertyName","candidate","takeFromDeclarator","declarator","id","init","value","unwrap","receivedParameter","received","contextMembers","fn","position","declarators","within"],"mappings":"2KAoBA,MAAMA,EAA8B,IAAI,IAExC,SAASC,EAA4BC,EAA8BC,EAAQ,CACzE,OAAOD,EAAQ,MAAM,IAAIC,CAAG,GAAKH,CACnC,CAEA,SAASI,EAAwBF,EAA8BC,EAAUE,EAAkB,CACzF,MAAMC,EAAQJ,EAAQ,MAAM,IAAIC,CAAG,EAE/BG,IAAU,OAAWJ,EAAQ,MAAM,IAAIC,EAAK,IAAI,IAAI,CAACE,CAAQ,CAAC,CAAC,EAC9DC,EAAM,IAAID,CAAQ,CACzB,CAEA,SAASE,EAAYC,EAA6BC,EAAqBC,EAA+B,CACpG,OAAOA,IAAc,QAAaD,EAAK,OAASE,EAAe,YAAcC,EAAWJ,EAAQC,CAAI,IAAMC,CAC5G,CAGA,SAASG,EACPL,EACAC,EACAP,EACAC,EAAQ,CAER,OAAIM,EAAK,OAASE,EAAe,kBAAoBF,EAAK,SAAiB,GAGzEA,EAAK,SAAS,OAASE,EAAe,YACtCF,EAAK,SAAS,OAASN,GACvBI,EAAYC,EAAQC,EAAK,OAAQP,EAAQ,SAAS,CAEtD,CAGA,SAASY,EACPN,EACAC,EACAP,EACAC,EAAQ,CAER,GAAIM,EAAK,OAASE,EAAe,WAAY,OAAOE,EAAgBL,EAAQC,EAAMP,EAASC,CAAG,EAE9F,MAAME,EAAWO,EAAWJ,EAAQC,CAAI,EAExC,OAAOJ,IAAa,MAAQJ,EAAQC,EAASC,CAAG,EAAE,IAAIE,CAAQ,CAChE,CAGA,SAASU,EACPP,EACAQ,EACAd,EACAe,EAAoB,CAEpB,UAAWC,KAAYF,EAAQ,WAAY,CACzC,GAAIE,EAAS,OAASP,EAAe,UAAYO,EAAS,MAAM,OAASP,EAAe,WAAY,SAEpG,MAAMQ,EAAOC,EAAaF,CAAQ,EAC5Bf,EAAMc,EAAK,KAAKI,GAAaA,IAAcF,CAAI,EAC/Cd,EAAWF,IAAQ,OAAY,KAAOS,EAAWJ,EAAQU,EAAS,KAAK,EAEzEf,IAAQ,QAAaE,IAAa,MAAMD,EAAIF,EAASC,EAAKE,CAAQ,CACxE,CACF,CAOA,SAASiB,EACPd,EACAe,EACArB,EACAe,EAAoB,CAEpB,KAAM,CAAE,GAAAO,EAAI,KAAAC,CAAI,EAAKF,EAErB,GAAIE,IAAS,KAAM,OAEnB,MAAMC,EAAQC,EAAOF,CAAI,EAEzB,GAAID,EAAG,OAASb,EAAe,cAAe,CACxCJ,EAAYC,EAAQkB,EAAOxB,EAAQ,SAAS,GAAGa,EAASP,EAAQgB,EAAItB,EAASe,CAAI,EAErF,MACF,CAEA,MAAMZ,EAAWmB,EAAG,OAASb,EAAe,WAAaC,EAAWJ,EAAQgB,CAAE,EAAI,KAC5ErB,EAAMc,EAAK,KAAKI,GAAaP,EAAYN,EAAQkB,EAAOxB,EAASmB,CAAS,CAAC,EAE7EhB,IAAa,MAAQF,IAAQ,QAAWC,EAAIF,EAASC,EAAKE,CAAQ,CACxE,CAGA,SAASuB,EACPpB,EACAqB,EAAwC,CAExC,OAAOA,GAAU,OAASlB,EAAe,WAAcC,EAAWJ,EAAQqB,CAAQ,GAAK,OAAa,MACtG,CAMA,SAASC,EACPtB,EACAuB,EACAC,EACAf,EACAgB,EAAmD,CAEnD,MAAMJ,EAAWE,EAAG,OAAOC,CAAQ,EAC7B9B,EAA+B,CAAE,MAAO,IAAI,IAAO,UAAW0B,EAAkBpB,EAAQqB,CAAQ,CAAC,EAEnGA,GAAU,OAASlB,EAAe,eAAeI,EAASP,EAAQqB,EAAU3B,EAASe,CAAI,EAE7F,UAAWM,KAAcU,EACnBC,EAAOX,EAAYQ,CAAE,GAAGT,EAAmBd,EAAQe,EAAYrB,EAASe,CAAI,EAGlF,OAAOf,CACT"}
|
|
@@ -19,7 +19,8 @@ declare function methodPath(source: TSESLint.SourceCode, callee: TSESTree.Node):
|
|
|
19
19
|
declare function isContextFunction(source: TSESLint.SourceCode, fn: FunctionNode): boolean;
|
|
20
20
|
/**
|
|
21
21
|
* D330: the context of a model and nothing else. The `own` section of a feature is a declaring context too, but its
|
|
22
|
-
* `
|
|
22
|
+
* `command` takes the body second and hands it no writer (D139, D385), so a rule about the writer of a command asks
|
|
23
|
+
* this.
|
|
23
24
|
*/
|
|
24
25
|
declare function isModelContextFunction(source: TSESLint.SourceCode, fn: FunctionNode): boolean;
|
|
25
26
|
/**
|
|
@@ -27,8 +28,15 @@ declare function isModelContextFunction(source: TSESLint.SourceCode, fn: Functio
|
|
|
27
28
|
* all the way out, so a nested function body counts; only the resolved context and the position decide.
|
|
28
29
|
*/
|
|
29
30
|
declare function isDeclaredIngress(source: TSESLint.SourceCode, node: TSESTree.Node): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* `context.command(run, options?)`: the body a model command is declared with, inline or bound to a local name.
|
|
33
|
+
* D447: two rules read the writes of one command body — `capture-command-cleanup` where the declaration stands, and
|
|
34
|
+
* `no-write-after-source-write` where a run invokes the name that holds it — so the resolution is one function here
|
|
35
|
+
* rather than the same six lines twice.
|
|
36
|
+
*/
|
|
37
|
+
declare function commandBody(source: TSESLint.SourceCode, call: TSESTree.CallExpression): FunctionNode | undefined;
|
|
30
38
|
/**
|
|
31
39
|
* D323: a rule that rewrites code has to resolve its package exactly (D309), and this pair is that resolution: the
|
|
32
40
|
* method a callee names on a declaring context, and whether that parameter really is one.
|
|
33
41
|
*/
|
|
34
|
-
export { isContextFunction, isDeclaredIngress, isModelContextFunction, methodPath };
|
|
42
|
+
export { commandBody, isContextFunction, isDeclaredIngress, isModelContextFunction, methodPath };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as i,TSESLint as
|
|
1
|
+
import{AST_NODE_TYPES as i,TSESLint as l}from"@typescript-eslint/utils";import{propertyName as s}from"./ast.js";import{localValue as y,unwrap as g,isFunction as a,isFeatureSection as h,isApi as x,definitionOf as S}from"./model-bindings.js";const w=new Map([["event",1],["scope.each",2],["scope.switch",1],["scope.while",1]]),I=new Map([["attach",{argument:1,property:"open"}],["resource.live",{argument:0,property:"connect"}]]);function E(n){const[t]=n.params;if(t?.type===i.Identifier||t?.type===i.ObjectPattern)return t}function p(n,t){const e=E(t)?.typeAnnotation?.typeAnnotation;if(e?.type!==i.TSTypeReference)return!1;const{typeName:r}=e;return r.type===i.Identifier&&x(n,r,"@opetope/core","ModelContext",!0)}function c(n,t){const e=S(n,t);if(!(e?.type!==l.Scope.DefinitionType.Parameter||!a(e.node)))return{fn:e.node,name:e.name}}function P(n,t){const e=c(n,t);return e!==void 0&&e.name===e.fn.params[0]?e.fn:void 0}function b(n,t){const e=c(n,t),r=e?.name.parent;if(e===void 0||r?.type!==i.Property)return;const o=r.parent===e.fn.params[0]?s(r):void 0;return o===void 0?void 0:{fn:e.fn,method:o}}function C(n){const t=[];let e=n;for(;e.type===i.MemberExpression;){if(e.computed||e.property.type!==i.Identifier)return;t.unshift(e.property.name),e=e.object}return{root:e,steps:t}}function u(n,t){const e=C(t);if(e?.root.type!==i.Identifier)return;const r=e.steps.length===0?void 0:P(n,e.root);if(r!==void 0)return{fn:r,method:e.steps.join(".")};const o=b(n,e.root);return o===void 0?void 0:{fn:o.fn,method:[o.method,...e.steps].join(".")}}function m(n,t){const e=t.parent;if(e.type!==i.CallExpression||e.arguments[1]!==t)return;const r=u(n,e.callee);return r?.method==="model"?r.fn:void 0}function d(n,t){const e=new Set;let r=t;for(;r!==void 0&&!e.has(r);){if(e.add(r),h(n,r,"own",!0)||p(n,r))return!0;r=m(n,r)}return!1}function M(n,t){if(p(n,t))return!0;const e=m(n,t);return e!==void 0&&d(n,e)}function A(n,t,e){if(w.get(n)===t)return!0;const r=I.get(n);return r?.argument===t&&r.property===e}function O(n,t,e,r){const o=u(n,t.callee);return o!==void 0&&A(o.method,e,r)&&d(n,o.fn)}function T(n,t,e){return n.type!==i.ObjectExpression||t.type!==i.Property?e:s(t)}function j(n,t){let e=t,r;for(const o of n.getAncestors(t).reverse()){if(o.type===i.CallExpression){const f=o.arguments.indexOf(e);if(f!==-1&&O(n,o,f,r))return!0;r=void 0}else r=T(o,e,r);e=o}return!1}function v(n,t){const[e]=t.arguments;if(e===void 0||e.type===i.SpreadElement)return;const r=y(n,g(e));return a(r)?r:void 0}export{v as commandBody,d as isContextFunction,j as isDeclaredIngress,M as isModelContextFunction,u as methodPath};
|
|
2
2
|
//# sourceMappingURL=declaration-ingress.js.map
|
|
@@ -1 +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 — `run`, `load`,\n * `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]);\n\n/**\n * The callbacks written as a record member. `attach`'s `close` is a release, not an opening; D349 puts the connection\n * of a live Resource under `connect` in the one options record, so that is where the ingress of a subscription is.\n */\nconst recordIngress = new Map([\n ['attach', { argument: 1, property: 'open' }],\n ['resource.live', { argument: 0, property: 'connect' }],\n]);\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/** `({ resource }) => resource.live(…)`: 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\n/**\n * D330: the context of a model and nothing else. The `own` section of a feature is a declaring context too, but its\n * `call` takes the body second and hands it no writer (D139), so a rule about the writer of a command asks this.\n */\nfunction isModelContextFunction(source: TSESLint.SourceCode, fn: FunctionNode): boolean {\n if (declaresModelContext(source, fn)) return true;\n\n const declaring = declaringContext(source, fn);\n\n return declaring !== undefined && isContextFunction(source, declaring);\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\n/**\n * D323: a rule that rewrites code has to resolve its package exactly (D309), and this pair is that resolution: the\n * method a callee names on a declaring context, and whether that parameter really is one.\n */\nexport { isContextFunction, isDeclaredIngress, isModelContextFunction, methodPath };\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","isModelContextFunction","admitsIngress","position","record","isIngressArgument","memberProperty","parent","child","isDeclaredIngress"],"mappings":"oNAYA,MAAMA,EAAoB,IAAI,IAAI,CAChC,CAAC,QAAS,CAAC,EACX,CAAC,cAAe,CAAC,EACjB,CAAC,eAAgB,CAAC,EAClB,CAAC,cAAe,CAAC,CAClB,CAAA,EAMKC,EAAgB,IAAI,IAAI,CAC5B,CAAC,SAAU,CAAE,SAAU,EAAG,SAAU,MAAM,CAAE,EAC5C,CAAC,gBAAiB,CAAE,SAAU,EAAG,SAAU,SAAS,CAAE,CACvD,CAAA,EAMD,SAASC,EAAmBC,EAAgB,CAC1C,KAAM,CAACC,CAAS,EAAID,EAAG,OAEvB,GAAIC,GAAW,OAASC,EAAe,YAAcD,GAAW,OAASC,EAAe,cACtF,OAAOD,CAIX,CAGA,SAASE,EAAqBC,EAA6BJ,EAAgB,CACzE,MAAMK,EAAaN,EAAmBC,CAAE,GAAG,gBAAgB,eAE3D,GAAIK,GAAY,OAASH,EAAe,gBAAiB,MAAO,GAEhE,KAAM,CAAE,SAAAI,CAAQ,EAAKD,EAErB,OAAOC,EAAS,OAASJ,EAAe,YAAcK,EAAMH,EAAQE,EAAU,gBAAiB,eAAgB,EAAI,CACrH,CAGA,SAASE,EAAYJ,EAA6BK,EAAyB,CACzE,MAAMC,EAAaC,EAAaP,EAAQK,CAAI,EAE5C,GAAI,EAAAC,GAAY,OAASE,EAAS,MAAM,eAAe,WAAa,CAACC,EAAWH,EAAW,IAAI,GAE/F,MAAO,CAAE,GAAIA,EAAW,KAAM,KAAMA,EAAW,IAAI,CACrD,CAGA,SAASI,EAAYV,EAA6BK,EAAyB,CACzE,MAAMR,EAAYO,EAAYJ,EAAQK,CAAI,EAE1C,OAAOR,IAAc,QAAaA,EAAU,OAASA,EAAU,GAAG,OAAO,CAAC,EAAIA,EAAU,GAAK,MAC/F,CAGA,SAASc,EAAmBX,EAA6BK,EAAyB,CAChF,MAAMR,EAAYO,EAAYJ,EAAQK,CAAI,EACpCO,EAAWf,GAAW,KAAK,OAEjC,GAAIA,IAAc,QAAae,GAAU,OAASd,EAAe,SAAU,OAE3E,MAAMe,EAASD,EAAS,SAAWf,EAAU,GAAG,OAAO,CAAC,EAAIiB,EAAaF,CAAQ,EAAI,OAErF,OAAOC,IAAW,OAAY,OAAY,CAAE,GAAIhB,EAAU,GAAI,OAAAgB,CAAM,CACtE,CAGA,SAASE,EAAYC,EAAqB,CACxC,MAAMC,EAAkB,CAAA,EACxB,IAAIC,EAAyBF,EAE7B,KAAOE,EAAQ,OAASpB,EAAe,kBAAkB,CACvD,GAAIoB,EAAQ,UAAYA,EAAQ,SAAS,OAASpB,EAAe,WAAY,OAE7EmB,EAAM,QAAQC,EAAQ,SAAS,IAAI,EACnCA,EAAUA,EAAQ,MACpB,CAEA,MAAO,CAAE,KAAMA,EAAS,MAAAD,CAAK,CAC/B,CAMA,SAASE,EAAWnB,EAA6BgB,EAAqB,CACpE,MAAMI,EAAQL,EAAYC,CAAM,EAEhC,GAAII,GAAO,KAAK,OAAStB,EAAe,WAAY,OAEpD,MAAMuB,EAAQD,EAAM,MAAM,SAAW,EAAI,OAAYV,EAAYV,EAAQoB,EAAM,IAAI,EAEnF,GAAIC,IAAU,OAAW,MAAO,CAAE,GAAIA,EAAO,OAAQD,EAAM,MAAM,KAAK,GAAG,CAAC,EAE1E,MAAME,EAAeX,EAAmBX,EAAQoB,EAAM,IAAI,EAE1D,OAAOE,IAAiB,OACpB,OACA,CAAE,GAAIA,EAAa,GAAI,OAAQ,CAACA,EAAa,OAAQ,GAAGF,EAAM,KAAK,EAAE,KAAK,GAAG,CAAC,CACpF,CAGA,SAASG,EAAiBvB,EAA6BJ,EAAgB,CACrE,MAAM4B,EAAO5B,EAAG,OAEhB,GAAI4B,EAAK,OAAS1B,EAAe,gBAAkB0B,EAAK,UAAU,CAAC,IAAM5B,EAAI,OAE7E,MAAM6B,EAAYN,EAAWnB,EAAQwB,EAAK,MAAM,EAEhD,OAAOC,GAAW,SAAW,QAAUA,EAAU,GAAK,MACxD,CAQA,SAASC,EAAkB1B,EAA6BJ,EAAgB,CACtE,MAAM+B,EAAO,IAAI,IACjB,IAAIT,EAAoCtB,EAExC,KAAOsB,IAAY,QAAa,CAACS,EAAK,IAAIT,CAAO,GAAG,CAGlD,GAFAS,EAAK,IAAIT,CAAO,EAEZU,EAAiB5B,EAAQkB,EAAS,MAAO,EAAI,GAAKnB,EAAqBC,EAAQkB,CAAO,EAAG,MAAO,GAEpGA,EAAUK,EAAiBvB,EAAQkB,CAAO,CAC5C,CAEA,MAAO,EACT,CAMA,SAASW,EAAuB7B,EAA6BJ,EAAgB,CAC3E,GAAIG,EAAqBC,EAAQJ,CAAE,EAAG,MAAO,GAE7C,MAAM6B,EAAYF,EAAiBvB,EAAQJ,CAAE,EAE7C,OAAO6B,IAAc,QAAaC,EAAkB1B,EAAQyB,CAAS,CACvE,CAEA,SAASK,EAAcjB,EAAgBkB,EAAkBnB,EAA4B,CACnF,GAAInB,EAAkB,IAAIoB,CAAM,IAAMkB,EAAU,MAAO,GAEvD,MAAMC,EAAStC,EAAc,IAAImB,CAAM,EAEvC,OAAOmB,GAAQ,WAAaD,GAAYC,EAAO,WAAapB,CAC9D,CAGA,SAASqB,EACPjC,EACAwB,EACAO,EACAnB,EAA4B,CAE5B,MAAMS,EAAQF,EAAWnB,EAAQwB,EAAK,MAAM,EAE5C,OAAOH,IAAU,QAAaS,EAAcT,EAAM,OAAQU,EAAUnB,CAAQ,GAAKc,EAAkB1B,EAAQqB,EAAM,EAAE,CACrH,CAGA,SAASa,EAAeC,EAAuBC,EAAsBlB,EAA2B,CAC9F,OAAIiB,EAAO,OAASrC,EAAe,kBAAoBsC,EAAM,OAAStC,EAAe,SAAiBoB,EAE/FJ,EAAasB,CAAK,CAC3B,CAMA,SAASC,EAAkBrC,EAA6BK,EAAmB,CACzE,IAAI+B,EAAuB/B,EACvBO,EAEJ,UAAWuB,KAAUnC,EAAO,aAAaK,CAAI,EAAE,UAAW,CACxD,GAAI8B,EAAO,OAASrC,EAAe,eAAgB,CACjD,MAAMiC,EAAWI,EAAO,UAAU,QAAQC,CAAwC,EAElF,GAAIL,IAAa,IAAME,EAAkBjC,EAAQmC,EAAQJ,EAAUnB,CAAQ,EAAG,MAAO,GAErFA,EAAW,MACb,MACEA,EAAWsB,EAAeC,EAAQC,EAAOxB,CAAQ,EAGnDwB,EAAQD,CACV,CAEA,MAAO,EACT"}
|
|
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, localValue, unwrap } 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 — `run`, `load`,\n * `filter` and `key` sit in the same calls and are not ingress.\n */\nconst positionalIngress = new Map([\n ['event', 1],\n // D378: the key of `each` stands between the source and the child, so the callback that opens is the third argument.\n ['scope.each', 2],\n ['scope.switch', 1],\n ['scope.while', 1],\n]);\n\n/**\n * The callbacks written as a record member. `attach`'s `close` is a release, not an opening; D349 puts the connection\n * of a live Resource under `connect` in the one options record, so that is where the ingress of a subscription is.\n */\nconst recordIngress = new Map([\n ['attach', { argument: 1, property: 'open' }],\n ['resource.live', { argument: 0, property: 'connect' }],\n]);\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/** `({ resource }) => resource.live(…)`: 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\n/**\n * D330: the context of a model and nothing else. The `own` section of a feature is a declaring context too, but its\n * `command` takes the body second and hands it no writer (D139, D385), so a rule about the writer of a command asks\n * this.\n */\nfunction isModelContextFunction(source: TSESLint.SourceCode, fn: FunctionNode): boolean {\n if (declaresModelContext(source, fn)) return true;\n\n const declaring = declaringContext(source, fn);\n\n return declaring !== undefined && isContextFunction(source, declaring);\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\n/**\n * `context.command(run, options?)`: the body a model command is declared with, inline or bound to a local name.\n * D447: two rules read the writes of one command body — `capture-command-cleanup` where the declaration stands, and\n * `no-write-after-source-write` where a run invokes the name that holds it — so the resolution is one function here\n * rather than the same six lines twice.\n */\nfunction commandBody(source: TSESLint.SourceCode, call: TSESTree.CallExpression): FunctionNode | undefined {\n const [body] = call.arguments;\n\n if (body === undefined || body.type === AST_NODE_TYPES.SpreadElement) return undefined;\n\n const value = localValue(source, unwrap(body));\n\n return isFunction(value) ? value : undefined;\n}\n\n/**\n * D323: a rule that rewrites code has to resolve its package exactly (D309), and this pair is that resolution: the\n * method a callee names on a declaring context, and whether that parameter really is one.\n */\nexport { commandBody, isContextFunction, isDeclaredIngress, isModelContextFunction, methodPath };\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","isModelContextFunction","admitsIngress","position","record","isIngressArgument","memberProperty","parent","child","isDeclaredIngress","commandBody","body","value","localValue","unwrap"],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import type { FunctionNode } from './model-bindings.js';
|
|
3
|
+
/**
|
|
4
|
+
* One `effect(source, run, options?)` declared on a declaring context of this library. The source is the expression
|
|
5
|
+
* as written, past an `as`; a rule that needs a name of its own asks for one. `inline` says the run is the function
|
|
6
|
+
* literal the declaration takes, which is what a rule that rewrites inside it needs to know.
|
|
7
|
+
*/
|
|
8
|
+
type DeclaredEffect = {
|
|
9
|
+
readonly from: TSESTree.Node;
|
|
10
|
+
readonly inline: boolean;
|
|
11
|
+
readonly options: TSESTree.CallExpressionArgument | undefined;
|
|
12
|
+
readonly run: FunctionNode;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* D309: a rule that speaks about an effect resolves its package exactly — the method a callee names on a declaring
|
|
16
|
+
* context, and whether that parameter really is one. The shape decides first, so a call that cannot be an effect
|
|
17
|
+
* never pays for the resolution.
|
|
18
|
+
*/
|
|
19
|
+
declare function declaredEffect(source: TSESLint.SourceCode, call: TSESTree.CallExpression): DeclaredEffect | undefined;
|
|
20
|
+
export { declaredEffect };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{AST_NODE_TYPES as f}from"@typescript-eslint/utils";import{methodPath as m,isContextFunction as c}from"./declaration-ingress.js";import{localValue as a,isFunction as i,unwrap as u}from"./model-bindings.js";function p(t,r){const[n,e,o]=r.arguments;if(n===void 0||n.type===f.SpreadElement||e===void 0||e.type===f.SpreadElement)return;const d=a(t,e);if(i(d))return{from:u(n),inline:i(u(e)),options:o,run:d}}function l(t,r){const n=m(t,r.callee);if(n?.method!=="effect")return;const e=p(t,r);if(e!==void 0)return c(t,n.fn)?e:void 0}export{l as declaredEffect};
|
|
2
|
+
//# sourceMappingURL=effect-declarations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"effect-declarations.js","sources":["../src/effect-declarations.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { isContextFunction, methodPath } from './declaration-ingress';\nimport type { FunctionNode } from './model-bindings';\nimport { isFunction, localValue, unwrap } from './model-bindings';\n\n/**\n * One `effect(source, run, options?)` declared on a declaring context of this library. The source is the expression\n * as written, past an `as`; a rule that needs a name of its own asks for one. `inline` says the run is the function\n * literal the declaration takes, which is what a rule that rewrites inside it needs to know.\n */\ntype DeclaredEffect = {\n readonly from: TSESTree.Node;\n readonly inline: boolean;\n readonly options: TSESTree.CallExpressionArgument | undefined;\n readonly run: FunctionNode;\n};\n\n/** The source, the run and the modifiers of an `effect`, where the run resolves to a function of this module. */\nfunction declaredArguments(source: TSESLint.SourceCode, call: TSESTree.CallExpression): DeclaredEffect | undefined {\n const [from, run, options] = call.arguments;\n\n if (from === undefined || from.type === AST_NODE_TYPES.SpreadElement) return undefined;\n\n if (run === undefined || run.type === AST_NODE_TYPES.SpreadElement) return undefined;\n\n const body = localValue(source, run);\n\n if (!isFunction(body)) return undefined;\n\n return { from: unwrap(from), inline: isFunction(unwrap(run)), options, run: body };\n}\n\n/**\n * D309: a rule that speaks about an effect resolves its package exactly — the method a callee names on a declaring\n * context, and whether that parameter really is one. The shape decides first, so a call that cannot be an effect\n * never pays for the resolution.\n */\nfunction declaredEffect(source: TSESLint.SourceCode, call: TSESTree.CallExpression): DeclaredEffect | undefined {\n const named = methodPath(source, call.callee);\n\n if (named?.method !== 'effect') return undefined;\n\n const declared = declaredArguments(source, call);\n\n if (declared === undefined) return undefined;\n\n return isContextFunction(source, named.fn) ? declared : undefined;\n}\n\nexport { declaredEffect };\n"],"names":["declaredArguments","source","call","from","run","options","AST_NODE_TYPES","body","localValue","isFunction","unwrap","declaredEffect","named","methodPath","declared","isContextFunction"],"mappings":"oNAoBA,SAASA,EAAkBC,EAA6BC,EAA6B,CACnF,KAAM,CAACC,EAAMC,EAAKC,CAAO,EAAIH,EAAK,UAIlC,GAFIC,IAAS,QAAaA,EAAK,OAASG,EAAe,eAEnDF,IAAQ,QAAaA,EAAI,OAASE,EAAe,cAAe,OAEpE,MAAMC,EAAOC,EAAWP,EAAQG,CAAG,EAEnC,GAAKK,EAAWF,CAAI,EAEpB,MAAO,CAAE,KAAMG,EAAOP,CAAI,EAAG,OAAQM,EAAWC,EAAON,CAAG,CAAC,EAAG,QAAAC,EAAS,IAAKE,CAAI,CAClF,CAOA,SAASI,EAAeV,EAA6BC,EAA6B,CAChF,MAAMU,EAAQC,EAAWZ,EAAQC,EAAK,MAAM,EAE5C,GAAIU,GAAO,SAAW,SAAU,OAEhC,MAAME,EAAWd,EAAkBC,EAAQC,CAAI,EAE/C,GAAIY,IAAa,OAEjB,OAAOC,EAAkBd,EAAQW,EAAM,EAAE,EAAIE,EAAW,MAC1D"}
|
package/dist/index.d.ts
CHANGED
|
@@ -44,6 +44,9 @@ declare const plugin: {
|
|
|
44
44
|
'no-redundant-const-tuple': import("@typescript-eslint/utils/ts-eslint").RuleModule<"redundantConstTuple", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
45
45
|
name: string;
|
|
46
46
|
};
|
|
47
|
+
'no-retired-vocabulary': import("@typescript-eslint/utils/ts-eslint").RuleModule<"commandLane" | "commandName" | "commandOnce" | "commandOutcomeStatus" | "commandPolicy" | "commandQueueBy" | "conditionFrom" | "contextCall" | "contextCalls" | "contributionWhen" | "deliveryKey" | "deliveryKind" | "deliveryReject" | "devtoolsResourceActivity" | "effectOnDispose" | "effectSkipInitial" | "effectWhen" | "eventBackpressure" | "fromExternal" | "loadingActivity" | "navigatorPolicy" | "resourceDisposer" | "resourceInvalidate" | "runtimeInternalEntry" | "scopeKeyed" | "useCommands", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
48
|
+
name: string;
|
|
49
|
+
};
|
|
47
50
|
'no-snapshot-in-update': import("@typescript-eslint/utils/ts-eslint").RuleModule<"snapshotInUpdate" | "writeFromPrevious", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
48
51
|
name: string;
|
|
49
52
|
};
|
|
@@ -53,6 +56,9 @@ declare const plugin: {
|
|
|
53
56
|
'no-subscribe-outside-models': import("@typescript-eslint/utils/ts-eslint").RuleModule<"handWrittenSubscription", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
54
57
|
name: string;
|
|
55
58
|
};
|
|
59
|
+
'no-write-after-source-write': import("@typescript-eslint/utils/ts-eslint").RuleModule<"writeAfterInvokedSourceWrite" | "writeAfterSourceWrite", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
60
|
+
name: string;
|
|
61
|
+
};
|
|
56
62
|
'prefer-effect-current': import("@typescript-eslint/utils/ts-eslint").RuleModule<"preferEffectCurrent", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
57
63
|
name: string;
|
|
58
64
|
};
|
|
@@ -69,7 +75,7 @@ declare const plugin: {
|
|
|
69
75
|
}], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
70
76
|
name: string;
|
|
71
77
|
};
|
|
72
|
-
'
|
|
78
|
+
'enabled-predicate': import("@typescript-eslint/utils/ts-eslint").RuleModule<"enabledIsAsync" | "enabledReturnsBareSource" | "enabledReturnsSource", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
73
79
|
name: string;
|
|
74
80
|
};
|
|
75
81
|
};
|
|
@@ -77,7 +83,7 @@ declare const plugin: {
|
|
|
77
83
|
/**
|
|
78
84
|
* The laws every Opetope author writes against, with no assumption about where a host keeps its files. A rule that
|
|
79
85
|
* needs such an assumption is off here and arrives through `layers`. The namespace is `opetope`, so a rule reads
|
|
80
|
-
* `opetope/
|
|
86
|
+
* `opetope/enabled-predicate` wherever a host names it.
|
|
81
87
|
*/
|
|
82
88
|
type NativeRule = NonNullable<ESLint.Plugin['rules']>[string];
|
|
83
89
|
type NativeRules = {
|
|
@@ -106,14 +112,16 @@ declare const configuredPlugin: {
|
|
|
106
112
|
'opetope/no-command-in-deps': "error";
|
|
107
113
|
'opetope/no-internal-imports': "error";
|
|
108
114
|
'opetope/no-redundant-const-tuple': "error";
|
|
115
|
+
'opetope/no-retired-vocabulary': "error";
|
|
109
116
|
'opetope/no-snapshot-in-update': "error";
|
|
110
117
|
'opetope/no-snapshot-read-in-render': "error";
|
|
111
118
|
'opetope/no-subscribe-outside-models': "off";
|
|
119
|
+
'opetope/no-write-after-source-write': "error";
|
|
112
120
|
'opetope/prefer-effect-current': "error";
|
|
113
121
|
'opetope/prefer-model-selection': "off";
|
|
114
122
|
'opetope/require-declared-models': "error";
|
|
115
123
|
'opetope/require-literal-id': "error";
|
|
116
|
-
'opetope/
|
|
124
|
+
'opetope/enabled-predicate': "error";
|
|
117
125
|
};
|
|
118
126
|
};
|
|
119
127
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{captureCommandCleanup as p}from"./rules/capture-command-cleanup.js";import{defineFeaturePropertyOrder as s}from"./rules/define-feature-property-order.js";import{idNaming as i}from"./rules/id-naming.js";import{layerPlacement as
|
|
1
|
+
import{captureCommandCleanup as p}from"./rules/capture-command-cleanup.js";import{defineFeaturePropertyOrder as s}from"./rules/define-feature-property-order.js";import{idNaming as i}from"./rules/id-naming.js";import{layerPlacement as a}from"./rules/layer-placement.js";import{noCommandInDeps as m}from"./rules/no-command-in-deps.js";import{noInternalImports as l}from"./rules/no-internal-imports.js";import{noRedundantConstTuple as d}from"./rules/no-redundant-const-tuple.js";import{noRetiredVocabulary as c}from"./rules/no-retired-vocabulary.js";import{noSnapshotInUpdate as u}from"./rules/no-snapshot-in-update.js";import{noSnapshotReadInRender as f}from"./rules/no-snapshot-read-in-render.js";import{noSubscribeOutsideModels as b}from"./rules/no-subscribe-outside-models.js";import{noWriteAfterSourceWrite as g}from"./rules/no-write-after-source-write.js";import{preferEffectCurrent as y}from"./rules/prefer-effect-current.js";import{preferModelSelection as S}from"./rules/prefer-model-selection.js";import{requireDeclaredModels as h}from"./rules/require-declared-models.js";import{requireLiteralId as I}from"./rules/require-literal-id.js";import{enabledPredicate as q}from"./rules/enabled-predicate.js";const C=["integration","models","ui"],E=["**/*.ts","**/*.tsx"],R=["**/__tests__/**","**/*.spec.ts","**/*.spec.tsx"],_={meta:{name:"@opetope/lint",version:"0.12.0"},rules:{"capture-command-cleanup":p,"define-feature-property-order":s,"id-naming":i,"layer-placement":a,"no-command-in-deps":m,"no-internal-imports":l,"no-redundant-const-tuple":d,"no-retired-vocabulary":c,"no-snapshot-in-update":u,"no-snapshot-read-in-render":f,"no-subscribe-outside-models":b,"no-write-after-source-write":g,"prefer-effect-current":y,"prefer-model-selection":S,"require-declared-models":h,"require-literal-id":I,"enabled-predicate":q}},r=_,j={name:"opetope/recommended",plugins:{opetope:r},rules:{"opetope/capture-command-cleanup":"error","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-redundant-const-tuple":"error","opetope/no-retired-vocabulary":"error","opetope/no-snapshot-in-update":"error","opetope/no-snapshot-read-in-render":"error","opetope/no-subscribe-outside-models":"off","opetope/no-write-after-source-write":"error","opetope/prefer-effect-current":"error","opetope/prefer-model-selection":"off","opetope/require-declared-models":"error","opetope/require-literal-id":"error","opetope/enabled-predicate":"error"}},w=e=>C.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}]}}]}),x=({models:e=[],tests:o=R})=>e.length===0?[]:[{files:E,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"}}],L=e=>[...w(e),...x(e)],P=({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"}}]],v=Object.assign(r,{configs:{internalImports:P,layers:L,recommended:j}}),n=v;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 { captureCommandCleanup } from './rules/capture-command-cleanup';\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 { noRedundantConstTuple } from './rules/no-redundant-const-tuple';\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 { preferEffectCurrent } from './rules/prefer-effect-current';\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.11.0' },\n rules: {\n 'capture-command-cleanup': captureCommandCleanup,\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-redundant-const-tuple': noRedundantConstTuple,\n 'no-snapshot-in-update': noSnapshotInUpdate,\n 'no-snapshot-read-in-render': noSnapshotReadInRender,\n 'no-subscribe-outside-models': noSubscribeOutsideModels,\n 'prefer-effect-current': preferEffectCurrent,\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/capture-command-cleanup': 'error',\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-redundant-const-tuple': '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-effect-current': 'error',\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","captureCommandCleanup","defineFeaturePropertyOrder","idNaming","layerPlacement","noCommandInDeps","noInternalImports","noRedundantConstTuple","noSnapshotInUpdate","noSnapshotReadInRender","noSubscribeOutsideModels","preferEffectCurrent","preferModelSelection","requireDeclaredModels","requireLiteralId","whenPredicate","nativePlugin","recommended","placement","options","layer","files","subscriptions","models","tests","layers","internalImports","allow","configuredPlugin","opetopeLint"],"mappings":"yhCAkCA,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,QAAQ,EAChD,MAAO,CACL,0BAA2BC,EAC3B,gCAAiCC,EACjC,YAAaC,EACb,kBAAmBC,EACnB,qBAAsBC,EACtB,sBAAuBC,EACvB,2BAA4BC,EAC5B,wBAAyBC,EACzB,6BAA8BC,EAC9B,8BAA+BC,EAC/B,wBAAyBC,EACzB,yBAA0BC,EAC1B,0BAA2BC,EAC3B,qBAAsBC,EACtB,iBAAkBC,CACnB,GAcGC,EAAehB,EAEfiB,EAAc,CAClB,KAAM,sBACN,QAAS,CAAE,QAASD,CAAY,EAChC,MAAO,CACL,kCAAmC,QACnC,wCAAyC,QACzC,oBAAqB,QACrB,0BAA2B,MAC3B,6BAA8B,QAC9B,8BAA+B,QAC/B,mCAAoC,QACpC,gCAAiC,QACjC,qCAAsC,QACtC,sCAAuC,MACvC,gCAAiC,QACjC,iCAAkC,MAClC,kCAAmC,QACnC,6BAA8B,QAC9B,yBAA0B,OAC3B,GAOGE,EAAaC,GACjBtB,EAAO,QAAQuB,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,EAAQzB,CAAU,IACtDwB,EAAO,SAAW,EACd,CAAA,EACA,CACE,CACE,MAAOzB,EACP,KAAM,+BACN,QAAS,CAAE,QAASkB,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 { captureCommandCleanup } from './rules/capture-command-cleanup';\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 { noRedundantConstTuple } from './rules/no-redundant-const-tuple';\nimport { noRetiredVocabulary } from './rules/no-retired-vocabulary';\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 { noWriteAfterSourceWrite } from './rules/no-write-after-source-write';\nimport { preferEffectCurrent } from './rules/prefer-effect-current';\nimport { preferModelSelection } from './rules/prefer-model-selection';\nimport { requireDeclaredModels } from './rules/require-declared-models';\nimport { requireLiteralId } from './rules/require-literal-id';\nimport { enabledPredicate } from './rules/enabled-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.12.0' },\n rules: {\n 'capture-command-cleanup': captureCommandCleanup,\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-redundant-const-tuple': noRedundantConstTuple,\n 'no-retired-vocabulary': noRetiredVocabulary,\n 'no-snapshot-in-update': noSnapshotInUpdate,\n 'no-snapshot-read-in-render': noSnapshotReadInRender,\n 'no-subscribe-outside-models': noSubscribeOutsideModels,\n 'no-write-after-source-write': noWriteAfterSourceWrite,\n 'prefer-effect-current': preferEffectCurrent,\n 'prefer-model-selection': preferModelSelection,\n 'require-declared-models': requireDeclaredModels,\n 'require-literal-id': requireLiteralId,\n 'enabled-predicate': enabledPredicate,\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/enabled-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/capture-command-cleanup': 'error',\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-redundant-const-tuple': 'error',\n 'opetope/no-retired-vocabulary': '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/no-write-after-source-write': 'error',\n 'opetope/prefer-effect-current': 'error',\n 'opetope/prefer-model-selection': 'off',\n 'opetope/require-declared-models': 'error',\n 'opetope/require-literal-id': 'error',\n 'opetope/enabled-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","captureCommandCleanup","defineFeaturePropertyOrder","idNaming","layerPlacement","noCommandInDeps","noInternalImports","noRedundantConstTuple","noRetiredVocabulary","noSnapshotInUpdate","noSnapshotReadInRender","noSubscribeOutsideModels","noWriteAfterSourceWrite","preferEffectCurrent","preferModelSelection","requireDeclaredModels","requireLiteralId","enabledPredicate","nativePlugin","recommended","placement","options","layer","files","subscriptions","models","tests","layers","internalImports","allow","configuredPlugin","opetopeLint"],"mappings":"urCAoCA,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,QAAQ,EAChD,MAAO,CACL,0BAA2BC,EAC3B,gCAAiCC,EACjC,YAAaC,EACb,kBAAmBC,EACnB,qBAAsBC,EACtB,sBAAuBC,EACvB,2BAA4BC,EAC5B,wBAAyBC,EACzB,wBAAyBC,EACzB,6BAA8BC,EAC9B,8BAA+BC,EAC/B,8BAA+BC,EAC/B,wBAAyBC,EACzB,yBAA0BC,EAC1B,0BAA2BC,EAC3B,qBAAsBC,EACtB,oBAAqBC,CACtB,GAcGC,EAAelB,EAEfmB,EAAc,CAClB,KAAM,sBACN,QAAS,CAAE,QAASD,CAAY,EAChC,MAAO,CACL,kCAAmC,QACnC,wCAAyC,QACzC,oBAAqB,QACrB,0BAA2B,MAC3B,6BAA8B,QAC9B,8BAA+B,QAC/B,mCAAoC,QACpC,gCAAiC,QACjC,gCAAiC,QACjC,qCAAsC,QACtC,sCAAuC,MACvC,sCAAuC,QACvC,gCAAiC,QACjC,iCAAkC,MAClC,kCAAmC,QACnC,6BAA8B,QAC9B,4BAA6B,OAC9B,GAOGE,EAAaC,GACjBxB,EAAO,QAAQyB,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,EAAQ3B,CAAU,IACtD0B,EAAO,SAAW,EACd,CAAA,EACA,CACE,CACE,MAAO3B,EACP,KAAM,+BACN,QAAS,CAAE,QAASoB,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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
3
|
+
/**
|
|
4
|
+
* A value this module proved is a Resource of this library: the `resource.load` or `resource.live` that declared it,
|
|
5
|
+
* read past the name it was given. The shape decides first and the resolution second, so a call that cannot be a
|
|
6
|
+
* declaration never pays for the context it would have been declared on (D323).
|
|
7
|
+
*/
|
|
8
|
+
declare function isResourceValue(source: TSESLint.SourceCode, node: TSESTree.Node): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* `useResource(book)`: the record of hooks, under whatever local name this file gave the import. The module is
|
|
11
|
+
* `@opetope/react` exactly — a host's own `./hooks` must not pass for ours, which is what `exact` asks for (D309).
|
|
12
|
+
*/
|
|
13
|
+
declare function isResourceHook(source: TSESLint.SourceCode, node: TSESTree.Node): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* D411, D415: the surface `defineSurface` answers, under whatever local name this file gave the import. The module
|
|
16
|
+
* is `@opetope/navigation/react` exactly, for the reason `useResource` is asked exactly (D309): `policy` is a word
|
|
17
|
+
* a host writes about its own objects all day, so the rule asks the symbol and never the spelling.
|
|
18
|
+
*/
|
|
19
|
+
declare function isSurfaceValue(source: TSESLint.SourceCode, node: TSESTree.Node): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* D375: `activity` left the `loading` state and stayed on the `ready` one, so a read of it is retired only where a
|
|
22
|
+
* narrowing proves which branch it is about. Both halves are asked: the expression is a `ResourceState` of this
|
|
23
|
+
* library, and the read stands inside a guard written about that same expression.
|
|
24
|
+
*/
|
|
25
|
+
declare function isLoadingActivity(source: TSESLint.SourceCode, node: TSESTree.MemberExpression): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* D411: the settled answer a consumer keeps — `.outcome` read on a `useCommand`, or the name it was destructured
|
|
28
|
+
* into. `status` is a common word, so the rule asks the symbol: nothing but this object is reported.
|
|
29
|
+
*/
|
|
30
|
+
declare function isCommandOutcome(source: TSESLint.SourceCode, node: TSESTree.Node): boolean;
|
|
31
|
+
export { isCommandOutcome, isLoadingActivity, isResourceHook, isResourceValue, isSurfaceValue };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{AST_NODE_TYPES as r,TSESLint as b}from"@typescript-eslint/utils";import{within as g,propertyName as x}from"./ast.js";import{methodPath as S,isContextFunction as C}from"./declaration-ingress.js";import{localValue as p,isApi as a,unwrap as u,definitionOf as h}from"./model-bindings.js";const v=new Set(["resource.live","resource.load"]),I="loading";function c(e,t){const n=p(e,t);if(n.type!==r.CallExpression)return!1;const i=S(e,n.callee);return i!==void 0&&v.has(i.method)&&C(e,i.fn)}function s(e,t){const n=p(e,t);return n.type===r.CallExpression&&a(e,n.callee,"@opetope/react","useResource",!0)}function y(e,t){const n=p(e,t);return n.type===r.CallExpression&&a(e,n.callee,"@opetope/react","useCommand",!0)}function O(e,t){const n=p(e,t);return n.type===r.CallExpression&&a(e,n.callee,"@opetope/navigation/react","defineSurface",!0)}function m(e,t){if(t.type!==r.MemberExpression||t.computed||t.property.type!==r.Identifier||t.property.name!=="state")return!1;const n=u(t.object);return c(e,n)||s(e,n)}function R(e,t){return t.type!==r.MemberExpression||t.computed||t.property.type!==r.Identifier||t.property.name!=="getSnapshot"?!1:m(e,u(t.object))}function d(e,t,n){const i=h(e,t);if(i?.type!==b.Scope.DefinitionType.Variable)return;const{init:o}=i.node,f=i.name.parent;if(!(o===null||f.type!==r.Property)&&!(x(f)!==n||f.parent!==i.node.id))return o}function T(e,t){if(t.type===r.Identifier){const i=d(e,t,"state");if(i!==void 0)return s(e,i)}const n=p(e,t);return n.type===r.CallExpression?R(e,n.callee):m(e,n)}function j(e,t,n){return t.type!==r.MemberExpression||t.computed||t.property.type!==r.Identifier||t.property.name!=="kind"?!1:e.getText(t.object)===n}function E(e,t,n,i){return n.type===r.Literal&&n.value===I&&j(e,t,i)}function l(e,t,n){return t.type===r.LogicalExpression&&t.operator==="&&"?l(e,t.left,n)||l(e,t.right,n):t.type!==r.BinaryExpression||t.operator!=="==="?!1:E(e,t.left,t.right,n)||E(e,t.right,t.left,n)}function L(e){if(e.type===r.IfStatement)return{body:e.consequent,test:e.test};if(e.type===r.ConditionalExpression)return{body:e.consequent,test:e.test};if(e.type===r.LogicalExpression&&e.operator==="&&")return{body:e.right,test:e.left}}function k(e,t){if(!T(e,t.object))return!1;const n=e.getText(t.object);return e.getAncestors(t).some(i=>{const o=L(i);return o!==void 0&&g(t,o.body)&&l(e,o.test,n)})}function A(e,t){if(t.type===r.Identifier){const i=d(e,t,"outcome");if(i!==void 0)return y(e,i)}const n=p(e,t);return n.type!==r.MemberExpression||n.computed||n.property.type!==r.Identifier||n.property.name!=="outcome"?!1:y(e,u(n.object))}export{A as isCommandOutcome,k as isLoadingActivity,s as isResourceHook,c as isResourceValue,O as isSurfaceValue};
|
|
2
|
+
//# sourceMappingURL=retired-vocabulary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retired-vocabulary.js","sources":["../src/retired-vocabulary.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils';\n\nimport { propertyName, within } from './ast';\nimport { isContextFunction, methodPath } from './declaration-ingress';\nimport { definitionOf, isApi, localValue, unwrap } from './model-bindings';\n\n/** D349: the two factories of the Resource family. Both answer the object whose verbs a migration renames. */\nconst RESOURCE_FACTORIES = new Set(['resource.live', 'resource.load']);\n/** D375: the one kind of `ResourceState` that lost `activity`; a `ready` one still carries the record whole. */\nconst LOADING = 'loading';\n\n/** A narrowing as it is written: the test that proves the kind, and the half of the code it protects. */\ntype Guard = { readonly body: TSESTree.Node; readonly test: TSESTree.Node };\n\n/**\n * A value this module proved is a Resource of this library: the `resource.load` or `resource.live` that declared it,\n * read past the name it was given. The shape decides first and the resolution second, so a call that cannot be a\n * declaration never pays for the context it would have been declared on (D323).\n */\nfunction isResourceValue(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n const value = localValue(source, node);\n\n if (value.type !== AST_NODE_TYPES.CallExpression) return false;\n\n const named = methodPath(source, value.callee);\n\n return named !== undefined && RESOURCE_FACTORIES.has(named.method) && isContextFunction(source, named.fn);\n}\n\n/**\n * `useResource(book)`: the record of hooks, under whatever local name this file gave the import. The module is\n * `@opetope/react` exactly — a host's own `./hooks` must not pass for ours, which is what `exact` asks for (D309).\n */\nfunction isResourceHook(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n const value = localValue(source, node);\n\n return (\n value.type === AST_NODE_TYPES.CallExpression && isApi(source, value.callee, '@opetope/react', 'useResource', true)\n );\n}\n\n/**\n * D411: `useCommand(save)` — the one consumer of a command, under whatever local name this file gave the import.\n * The module is `@opetope/react` exactly, for the reason `useResource` is asked exactly (D309).\n */\nfunction isCommandHook(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n const value = localValue(source, node);\n\n return (\n value.type === AST_NODE_TYPES.CallExpression && isApi(source, value.callee, '@opetope/react', 'useCommand', true)\n );\n}\n\n/**\n * D411, D415: the surface `defineSurface` answers, under whatever local name this file gave the import. The module\n * is `@opetope/navigation/react` exactly, for the reason `useResource` is asked exactly (D309): `policy` is a word\n * a host writes about its own objects all day, so the rule asks the symbol and never the spelling.\n */\nfunction isSurfaceValue(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n const value = localValue(source, node);\n\n return (\n value.type === AST_NODE_TYPES.CallExpression &&\n isApi(source, value.callee, '@opetope/navigation/react', 'defineSurface', true)\n );\n}\n\n/** `.state` read on a Resource of this library or on the record `useResource` answers. */\nfunction isStateMember(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n if (node.type !== AST_NODE_TYPES.MemberExpression || node.computed) return false;\n\n if (node.property.type !== AST_NODE_TYPES.Identifier || node.property.name !== 'state') return false;\n\n const object = unwrap(node.object);\n\n return isResourceValue(source, object) || isResourceHook(source, object);\n}\n\n/** `book.state.getSnapshot()`: the state as a model reads it. */\nfunction isSnapshotRead(source: TSESLint.SourceCode, callee: TSESTree.Node): boolean {\n if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) return false;\n\n if (callee.property.type !== AST_NODE_TYPES.Identifier || callee.property.name !== 'getSnapshot') return false;\n\n return isStateMember(source, unwrap(callee.object));\n}\n\n/**\n * `const { state } = useResource(book)`: the value a name was destructured out of, under the key it was taken by.\n * `localValue` answers the whole initializer for such a name, which is the record and not the member, so the key is\n * read here instead — the way a context member is read off the pattern that bound it.\n */\nfunction takenFrom(source: TSESLint.SourceCode, node: TSESTree.Identifier, key: string): TSESTree.Node | undefined {\n const definition = definitionOf(source, node);\n\n if (definition?.type !== TSESLint.Scope.DefinitionType.Variable) return undefined;\n\n const { init } = definition.node;\n const property = definition.name.parent;\n\n if (init === null || property.type !== AST_NODE_TYPES.Property) return undefined;\n\n if (propertyName(property) !== key || property.parent !== definition.node.id) return undefined;\n\n return init;\n}\n\n/** The `ResourceState` a name holds: the member itself, one value read out of it, or the key it was taken from. */\nfunction isResourceState(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n if (node.type === AST_NODE_TYPES.Identifier) {\n const taken = takenFrom(source, node, 'state');\n\n if (taken !== undefined) return isResourceHook(source, taken);\n }\n\n const value = localValue(source, node);\n\n return value.type === AST_NODE_TYPES.CallExpression\n ? isSnapshotRead(source, value.callee)\n : isStateMember(source, value);\n}\n\n/** `state.kind` read off the same expression the `activity` was read off, compared as the author wrote it. */\nfunction isKindOf(source: TSESLint.SourceCode, node: TSESTree.Node, text: string): boolean {\n if (node.type !== AST_NODE_TYPES.MemberExpression || node.computed) return false;\n\n if (node.property.type !== AST_NODE_TYPES.Identifier || node.property.name !== 'kind') return false;\n\n return source.getText(node.object) === text;\n}\n\nfunction isLoadingTest(source: TSESLint.SourceCode, kind: TSESTree.Node, word: TSESTree.Node, text: string): boolean {\n return word.type === AST_NODE_TYPES.Literal && word.value === LOADING && isKindOf(source, kind, text);\n}\n\n/** `state.kind === 'loading'` written about this very expression, through the `&&` chain a guard may be built of. */\nfunction narrows(source: TSESLint.SourceCode, test: TSESTree.Node, text: string): boolean {\n if (test.type === AST_NODE_TYPES.LogicalExpression && test.operator === '&&') {\n return narrows(source, test.left, text) || narrows(source, test.right, text);\n }\n\n if (test.type !== AST_NODE_TYPES.BinaryExpression || test.operator !== '===') return false;\n\n return isLoadingTest(source, test.left, test.right, text) || isLoadingTest(source, test.right, test.left, text);\n}\n\n/** The three shapes a narrowing is written in, each with the half of the code it protects. */\nfunction guardOf(node: TSESTree.Node): Guard | undefined {\n if (node.type === AST_NODE_TYPES.IfStatement) return { body: node.consequent, test: node.test };\n\n if (node.type === AST_NODE_TYPES.ConditionalExpression) return { body: node.consequent, test: node.test };\n\n if (node.type === AST_NODE_TYPES.LogicalExpression && node.operator === '&&') {\n return { body: node.right, test: node.left };\n }\n\n return undefined;\n}\n\n/**\n * D375: `activity` left the `loading` state and stayed on the `ready` one, so a read of it is retired only where a\n * narrowing proves which branch it is about. Both halves are asked: the expression is a `ResourceState` of this\n * library, and the read stands inside a guard written about that same expression.\n */\nfunction isLoadingActivity(source: TSESLint.SourceCode, node: TSESTree.MemberExpression): boolean {\n if (!isResourceState(source, node.object)) return false;\n\n const text = source.getText(node.object);\n\n return source.getAncestors(node).some(ancestor => {\n const guard = guardOf(ancestor);\n\n return guard !== undefined && within(node, guard.body) && narrows(source, guard.test, text);\n });\n}\n\n/**\n * D411: the settled answer a consumer keeps — `.outcome` read on a `useCommand`, or the name it was destructured\n * into. `status` is a common word, so the rule asks the symbol: nothing but this object is reported.\n */\nfunction isCommandOutcome(source: TSESLint.SourceCode, node: TSESTree.Node): boolean {\n if (node.type === AST_NODE_TYPES.Identifier) {\n const taken = takenFrom(source, node, 'outcome');\n\n if (taken !== undefined) return isCommandHook(source, taken);\n }\n\n const value = localValue(source, node);\n\n if (value.type !== AST_NODE_TYPES.MemberExpression || value.computed) return false;\n\n if (value.property.type !== AST_NODE_TYPES.Identifier || value.property.name !== 'outcome') return false;\n\n return isCommandHook(source, unwrap(value.object));\n}\n\nexport { isCommandOutcome, isLoadingActivity, isResourceHook, isResourceValue, isSurfaceValue };\n"],"names":["RESOURCE_FACTORIES","LOADING","isResourceValue","source","node","value","localValue","AST_NODE_TYPES","named","methodPath","isContextFunction","isResourceHook","isApi","isCommandHook","isSurfaceValue","isStateMember","object","unwrap","isSnapshotRead","callee","takenFrom","key","definition","definitionOf","TSESLint","init","property","propertyName","isResourceState","taken","isKindOf","text","isLoadingTest","kind","word","narrows","test","guardOf","isLoadingActivity","ancestor","guard","within","isCommandOutcome"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as
|
|
1
|
+
import{AST_NODE_TYPES as l}from"@typescript-eslint/utils";import{within as u,findProperty as w}from"../ast.js";import{contextMembers as g,fromContext as f}from"../context-members.js";import{methodPath as C,isModelContextFunction as b,commandBody as x}from"../declaration-ingress.js";import{unwrap as d,enclosingFunction as m}from"../model-bindings.js";import{createRule as S}from"../rule.js";const E=["update","signal","rethrowIfCancelled"],T=0;function A(a){const e=a.arguments[1],n=e===void 0?void 0:d(e),r=n?.type===l.ObjectExpression?w(n,"concurrency"):void 0,t=r===void 0?void 0:d(r.value);return t?.type===l.Literal&&t.value==="parallel"}function h(a,e,n,r){return e.type!==l.MemberExpression||e.computed||e.property.type!==l.Identifier||e.property.name!==r?!1:f(a,e.object,n,"signal")}function y(a){if(a.type===l.ThrowStatement||a.type===l.ReturnStatement)return!0;if(a.type!==l.BlockStatement)return!1;const e=a.body.at(-1);return e!==void 0&&y(e)}function I(a,e,n){if(e.type===l.IfStatement)return h(a,d(e.test),n,"aborted")&&y(e.consequent);if(e.type!==l.ExpressionStatement)return!1;const r=d(e.expression);return r.type!==l.CallExpression?!1:f(a,r.callee,n,"rethrowIfCancelled")||h(a,r.callee,n,"throwIfAborted")}function v(a,e,n,r){for(const t of e.body){if(u(n,t))return!1;if(I(a,t,r))return!0}return!1}function O(a,e,n,r){return a.getAncestors(n).some(t=>t.type===l.BlockStatement&&u(t,e.body)&&v(a,t,n,r))}function k(a,e,n){const r=m(a,e);return n.barriers.some(t=>u(t,e.block)&&m(a,t)===r)}function D(a,e,n,r,t,c){if(n===e.block||!k(a,e,c))return;if(n===e.finalizer)return"finally";const{handler:o}=e;if(!(o===null||n!==o))return O(a,o,r,t)?void 0:"catch"}function P(a,e,n,r,t){let c=e;for(const o of a.getAncestors(e).reverse()){if(o===n)return;const i=o.type===l.TryStatement?D(a,o,c,e,r,t):void 0;if(i!==void 0)return i;c=o}}const B=S({create(a){const e=a.sourceCode,n={barriers:[],calls:[],declarators:[]},r=(t,c)=>{const o=g(e,t,T,E,n.declarators),i=c?"parallelCommandCleanup":"captureCommandCleanup";for(const s of n.calls){if(!u(s,t)||!f(e,s.callee,o,"update"))continue;const p=P(e,s,t,o,n);p!==void 0&&a.report({data:{clause:p},messageId:i,node:s})}};return{AwaitExpression(t){n.barriers.push(t)},CallExpression(t){n.calls.push(t)},ForOfStatement(t){t.await&&n.barriers.push(t)},"Program:exit"(){for(const t of n.calls){const c=C(e,t.callee);if(c?.method!=="command"||!b(e,c.fn))continue;const o=x(e,t);o!==void 0&&r(o,A(t))}},VariableDeclarator(t){n.declarators.push(t)}}},meta:{docs:{description:"A command writes the cleanup it owes after an `await` through a commit, not through its writer."},messages:{captureCommandCleanup:"This write is in the `{{clause}}` of a `try` that awaits, and the writer of a command lives only as long as its caller: once the call is cancelled the write is dropped without a record. A cleanup that must run for a cancelled call \u2014 a busy flag, a pending mark \u2014 goes through `const commit = execution.capture(state)` taken before the `await` and `commit.update(\u2026)` here. An answer that must not land after cancellation \u2014 a failure, a result \u2014 belongs in a `catch` after `rethrowIfCancelled(cause)` (D288, D319, D330).",parallelCommandCleanup:"This write is in the `{{clause}}` of a `try` that awaits in a `concurrency: 'parallel'` command, and once the call is cancelled it is dropped without a record. A commit does not fix it: calls of this command overlap, so the cleanup of a cancelled call would clear state another call still holds, and a flag shared by parallel calls is racy either way. Keep such state per call \u2014 keyed by its input \u2014 or leave progress to the consumer's `inFlight`. An answer that must not land after cancellation belongs in a `catch` after `rethrowIfCancelled(cause)` (D288, D330)."},schema:[],type:"problem"},name:"capture-command-cleanup"});export{B as captureCommandCleanup};
|
|
2
2
|
//# sourceMappingURL=capture-command-cleanup.js.map
|