@opetope/lint 0.14.0 → 0.15.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +39 -2
  3. package/dist/ast.d.ts +14 -2
  4. package/dist/ast.js +1 -1
  5. package/dist/ast.js.map +1 -1
  6. package/dist/command-hooks.d.ts +7 -6
  7. package/dist/command-hooks.js +1 -1
  8. package/dist/command-hooks.js.map +1 -1
  9. package/dist/context-members.d.ts +4 -4
  10. package/dist/context-members.js +1 -1
  11. package/dist/context-members.js.map +1 -1
  12. package/dist/declaration-ingress.d.ts +43 -11
  13. package/dist/declaration-ingress.js +1 -1
  14. package/dist/declaration-ingress.js.map +1 -1
  15. package/dist/declarations.d.ts +1 -1
  16. package/dist/declarations.js +1 -1
  17. package/dist/declarations.js.map +1 -1
  18. package/dist/effect-declarations.d.ts +3 -2
  19. package/dist/effect-declarations.js +1 -1
  20. package/dist/effect-declarations.js.map +1 -1
  21. package/dist/entry-move-rows.d.ts +21 -0
  22. package/dist/entry-move-rows.js +2 -0
  23. package/dist/entry-move-rows.js.map +1 -0
  24. package/dist/entry-moves.d.ts +25 -0
  25. package/dist/entry-moves.js +10 -0
  26. package/dist/entry-moves.js.map +1 -0
  27. package/dist/feature-body-records.d.ts +5 -3
  28. package/dist/feature-body-records.js +1 -1
  29. package/dist/feature-body-records.js.map +1 -1
  30. package/dist/index.d.ts +65 -58
  31. package/dist/index.js +1 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/lazy-body-migration.d.ts +5 -5
  34. package/dist/lazy-body-migration.js +1 -1
  35. package/dist/lazy-body-migration.js.map +1 -1
  36. package/dist/model-bindings.d.ts +21 -13
  37. package/dist/model-bindings.js +1 -1
  38. package/dist/model-bindings.js.map +1 -1
  39. package/dist/resource-vocabulary.d.ts +17 -0
  40. package/dist/resource-vocabulary.js +2 -0
  41. package/dist/resource-vocabulary.js.map +1 -0
  42. package/dist/retired-vocabulary-messages.d.ts +6 -0
  43. package/dist/retired-vocabulary-messages.js +2 -0
  44. package/dist/retired-vocabulary-messages.js.map +1 -0
  45. package/dist/retired-vocabulary.d.ts +10 -8
  46. package/dist/retired-vocabulary.js +1 -1
  47. package/dist/retired-vocabulary.js.map +1 -1
  48. package/dist/rule.d.ts +94 -5
  49. package/dist/rule.js +1 -1
  50. package/dist/rule.js.map +1 -1
  51. package/dist/rules/capture-command-cleanup.d.ts +1 -4
  52. package/dist/rules/capture-command-cleanup.js +1 -1
  53. package/dist/rules/capture-command-cleanup.js.map +1 -1
  54. package/dist/rules/define-feature-property-order.d.ts +1 -4
  55. package/dist/rules/define-feature-property-order.js +1 -1
  56. package/dist/rules/define-feature-property-order.js.map +1 -1
  57. package/dist/rules/enabled-predicate.d.ts +1 -4
  58. package/dist/rules/enabled-predicate.js +1 -1
  59. package/dist/rules/enabled-predicate.js.map +1 -1
  60. package/dist/rules/id-naming.d.ts +1 -3
  61. package/dist/rules/layer-placement.d.ts +1 -3
  62. package/dist/rules/no-behavior-in-integration.d.ts +8 -0
  63. package/dist/rules/no-behavior-in-integration.js +2 -0
  64. package/dist/rules/no-behavior-in-integration.js.map +1 -0
  65. package/dist/rules/no-command-in-deps.d.ts +1 -4
  66. package/dist/rules/no-command-in-deps.js +1 -1
  67. package/dist/rules/no-command-in-deps.js.map +1 -1
  68. package/dist/rules/no-dead-abort-check.d.ts +8 -0
  69. package/dist/rules/no-dead-abort-check.js +2 -0
  70. package/dist/rules/no-dead-abort-check.js.map +1 -0
  71. package/dist/rules/no-event-without-emit.d.ts +10 -0
  72. package/dist/rules/no-event-without-emit.js +2 -0
  73. package/dist/rules/no-event-without-emit.js.map +1 -0
  74. package/dist/rules/no-inline-requires-models.d.ts +8 -0
  75. package/dist/rules/no-inline-requires-models.js +5 -0
  76. package/dist/rules/no-inline-requires-models.js.map +1 -0
  77. package/dist/rules/no-internal-imports.d.ts +1 -3
  78. package/dist/rules/no-internal-imports.js +1 -1
  79. package/dist/rules/no-internal-imports.js.map +1 -1
  80. package/dist/rules/no-redundant-const-tuple.d.ts +1 -4
  81. package/dist/rules/no-redundant-const-tuple.js +1 -1
  82. package/dist/rules/no-redundant-const-tuple.js.map +1 -1
  83. package/dist/rules/no-retired-vocabulary.d.ts +2 -4
  84. package/dist/rules/no-retired-vocabulary.js +1 -1
  85. package/dist/rules/no-retired-vocabulary.js.map +1 -1
  86. package/dist/rules/no-single-command-lane.d.ts +10 -0
  87. package/dist/rules/no-single-command-lane.js +2 -0
  88. package/dist/rules/no-single-command-lane.js.map +1 -0
  89. package/dist/rules/no-snapshot-in-update.d.ts +1 -4
  90. package/dist/rules/no-snapshot-in-update.js.map +1 -1
  91. package/dist/rules/no-snapshot-read-in-render.d.ts +2 -3
  92. package/dist/rules/no-snapshot-read-in-render.js +1 -1
  93. package/dist/rules/no-snapshot-read-in-render.js.map +1 -1
  94. package/dist/rules/no-subscribe-outside-models.d.ts +1 -3
  95. package/dist/rules/no-subscribe-outside-models.js +1 -1
  96. package/dist/rules/no-subscribe-outside-models.js.map +1 -1
  97. package/dist/rules/no-write-after-source-write.d.ts +1 -4
  98. package/dist/rules/no-write-after-source-write.js +1 -1
  99. package/dist/rules/no-write-after-source-write.js.map +1 -1
  100. package/dist/rules/prefer-derive.d.ts +7 -0
  101. package/dist/rules/prefer-derive.js +2 -0
  102. package/dist/rules/prefer-derive.js.map +1 -0
  103. package/dist/rules/prefer-effect-current.d.ts +1 -4
  104. package/dist/rules/prefer-effect-current.js +1 -1
  105. package/dist/rules/prefer-effect-current.js.map +1 -1
  106. package/dist/rules/prefer-lazy-body-record.d.ts +1 -4
  107. package/dist/rules/prefer-lazy-body-record.js +1 -1
  108. package/dist/rules/prefer-lazy-body-record.js.map +1 -1
  109. package/dist/rules/prefer-model-selection.d.ts +1 -3
  110. package/dist/rules/prefer-model-selection.js +1 -1
  111. package/dist/rules/prefer-model-selection.js.map +1 -1
  112. package/dist/rules/prefer-read.d.ts +2 -0
  113. package/dist/rules/prefer-read.js +2 -0
  114. package/dist/rules/prefer-read.js.map +1 -0
  115. package/dist/rules/prefer-when-over-enabled.d.ts +7 -0
  116. package/dist/rules/prefer-when-over-enabled.js +2 -0
  117. package/dist/rules/prefer-when-over-enabled.js.map +1 -0
  118. package/dist/rules/require-declared-models.d.ts +1 -3
  119. package/dist/rules/require-declared-models.js +1 -1
  120. package/dist/rules/require-declared-models.js.map +1 -1
  121. package/dist/rules/require-literal-id.d.ts +2 -3
  122. package/dist/rules/require-literal-id.js +1 -1
  123. package/dist/rules/require-literal-id.js.map +1 -1
  124. package/dist/rules/require-static-feature.d.ts +9 -0
  125. package/dist/rules/require-static-feature.js +2 -0
  126. package/dist/rules/require-static-feature.js.map +1 -0
  127. package/dist/rules/scope-builder-releases-subscriptions.d.ts +10 -0
  128. package/dist/rules/scope-builder-releases-subscriptions.js +2 -0
  129. package/dist/rules/scope-builder-releases-subscriptions.js.map +1 -0
  130. package/dist/snapshot-updates.d.ts +4 -3
  131. package/dist/snapshot-updates.js +1 -1
  132. package/dist/snapshot-updates.js.map +1 -1
  133. package/dist/static-value.d.ts +3 -2
  134. package/dist/static-value.js +1 -1
  135. package/dist/static-value.js.map +1 -1
  136. package/oxlintrc.json +10 -0
  137. package/package.json +13 -3
@@ -1 +1 @@
1
- {"version":3,"file":"no-write-after-source-write.js","sources":["../../src/rules/no-write-after-source-write.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { within } from '../ast';\nimport { contextMembers, fromContext } from '../context-members';\nimport { commandBody, isContextFunction, isModelContextFunction, methodPath } from '../declaration-ingress';\nimport { declaredEffect } from '../effect-declarations';\nimport type { FunctionNode } from '../model-bindings';\nimport { enclosingFunction, isApi, isFunction, localValue, unwrap, variableOf } from '../model-bindings';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'writeAfterInvokedSourceWrite' | 'writeAfterSourceWrite';\ntype Variable = TSESLint.Scope.Variable;\n\n/** What one file wrote, collected before any write is judged: the rule answers on `Program:exit` (D290). */\ntype Written = {\n readonly calls: TSESTree.CallExpression[];\n readonly declarators: TSESTree.VariableDeclarator[];\n};\n\n/** The dependencies of a `derive` and the selector that publishes a value from them. */\ntype Derived = {\n readonly dependencies: readonly TSESTree.Node[];\n readonly selector: TSESTree.CallExpressionArgument | undefined;\n};\n\n/** The one name this rule reads off the context of a run — its writer — and the place a run receives that context. */\nconst WRITER_KEYS = ['update'] as const;\n/** D447: the other name it reads off a run — the word a run starts an already declared command with. */\nconst INVOKE_KEYS = ['invoke'] as const;\n/** D379: position 1 — the value the run answers is its first parameter, and the context follows it. */\nconst CONTEXT_POSITION = 1;\n/** D386: a command body takes one argument, its context, so the same writer is read one position earlier. */\nconst COMMAND_POSITION = 0;\n\n/** A read of a cell that answers a question about it instead of publishing it. */\nconst COMPARISONS = new Set(['!=', '!==', '<', '<=', '==', '===', '>', '>=', 'in', 'instanceof']);\nconst CLASSIFIERS = new Set(['!', 'typeof']);\n\n/** The two statements that end a run for good. */\nconst EXITS = new Set<string>([AST_NODE_TYPES.ReturnStatement, AST_NODE_TYPES.ThrowStatement]);\n\n/** A cell of this model, written where the declaration can be read: `context.state(…)` on a declaring context. */\nfunction isDeclaredCell(source: TSESLint.SourceCode, node: TSESTree.Identifier): 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?.method === 'state' && isContextFunction(source, named.fn);\n}\n\n/** The dependencies of `derive([a, cell], …)` and of `derive(cell, …)` alike, as the declaration spells them. */\nfunction derivedFrom(source: TSESLint.SourceCode, node: TSESTree.Node): Derived | undefined {\n if (node.type !== AST_NODE_TYPES.CallExpression) return undefined;\n\n if (!isApi(source, node.callee, '@opetope/core', 'derive', true)) return undefined;\n\n const [dependencies, selector] = node.arguments;\n\n if (dependencies === undefined) return undefined;\n\n const listed = unwrap(dependencies);\n\n if (listed.type !== AST_NODE_TYPES.ArrayExpression) return { dependencies: [listed], selector };\n\n // A hole in the literal is not a dependency, and a spread is one this rule cannot read; both leave the list here.\n return { dependencies: listed.elements.flatMap(element => (element === null ? [] : [element])), selector };\n}\n\n/**\n * Whether this read carries the value of the cell into what the selector publishes. A read that only asks a question\n * about it — `current === null`, `!current`, `typeof current`, `current ? a : b` — classifies the cell rather than\n * publishing it, and a write that keeps the answer keeps the published value.\n */\nfunction classified(node: TSESTree.Identifier): boolean {\n const { parent } = node;\n\n if (parent.type === AST_NODE_TYPES.BinaryExpression) return COMPARISONS.has(parent.operator);\n\n if (parent.type === AST_NODE_TYPES.UnaryExpression) return CLASSIFIERS.has(parent.operator);\n\n if (parent.type === AST_NODE_TYPES.ConditionalExpression) return parent.test === node;\n\n return parent.type === AST_NODE_TYPES.IfStatement && parent.test === node;\n}\n\n/** The parameter of the selector that stands for this dependency, where the selector is one this file can read. */\nfunction selectorParameter(\n source: TSESLint.SourceCode,\n selector: TSESTree.Node | undefined,\n position: number,\n): TSESTree.Identifier | undefined {\n const fn = selector === undefined ? undefined : localValue(source, selector);\n\n if (fn === undefined || !isFunction(fn)) return undefined;\n\n const parameter = fn.params[position];\n\n return parameter?.type === AST_NODE_TYPES.Identifier ? parameter : undefined;\n}\n\n/**\n * Whether the value the source publishes travels with this dependency. The law has a carve-out — a write that leaves\n * the published value equal ends nothing (spec §2.11) — and a selector that only classifies its dependency is that\n * carve-out written down: a `derive` publishing `current === null ? 'none' : 'some'` does not move when one key\n * replaces another. So the selector has to be readable here and has to read this dependency as a value at least once;\n * a selector assembled elsewhere, one that drops the dependency, and one that only asks a question about it all leave\n * the rule silent.\n */\nfunction travels(source: TSESLint.SourceCode, selector: TSESTree.Node | undefined, position: number): boolean {\n const parameter = selectorParameter(source, selector, position);\n const variable = parameter === undefined ? null : variableOf(source, parameter);\n\n // A JSX tag name is not a read of this value, and neither is anything else this rule cannot place.\n return (\n variable?.references.some(\n ({ identifier }) => identifier.type === AST_NODE_TYPES.Identifier && !classified(identifier),\n ) ?? false\n );\n}\n\n/** This name, where it is a cell of this model, joins the set the source republishes. */\nfunction addCell(source: TSESLint.SourceCode, cells: Set<Variable>, node: TSESTree.Node): void {\n const named = unwrap(node);\n\n if (named.type !== AST_NODE_TYPES.Identifier) return;\n\n const variable = variableOf(source, named);\n\n if (variable !== null && isDeclaredCell(source, named)) cells.add(variable);\n}\n\n/**\n * The cells of this model whose value the effect's source republishes: the source itself where a cell is the source,\n * and every dependency of a `derive` this scope holds whose value the selector publishes. A source assembled in\n * another function, a cell that arrived as a parameter and a `derive` over a `derive` name nothing here — the rule\n * then stays silent, because it cannot prove a write moves the source.\n */\nfunction sourceCells(source: TSESLint.SourceCode, from: TSESTree.Node): ReadonlySet<Variable> {\n const cells = new Set<Variable>();\n\n // A cell as the source itself publishes its own value, so it travels by construction.\n addCell(source, cells, from);\n\n const derived = derivedFrom(source, localValue(source, from));\n\n if (derived === undefined) return cells;\n\n for (const [position, dependency] of derived.dependencies.entries()) {\n if (travels(source, derived.selector, position)) addCell(source, cells, dependency);\n }\n\n return cells;\n}\n\n/**\n * The calls one body makes through one name of its own context, in its own body and in the order they are written.\n * The position is part of the contract: a reaction run takes its context second and a command body first (D386).\n */\nfunction callsThrough<Key extends string>(\n source: TSESLint.SourceCode,\n body: FunctionNode,\n written: Written,\n position: number,\n key: Key,\n): readonly TSESTree.CallExpression[] {\n const members = contextMembers(source, body, position, [key], written.declarators);\n\n return written.calls\n .filter(call => within(call, body) && enclosingFunction(source, call) === body)\n .filter(call => fromContext(source, call.callee, members, key))\n .sort((left, right) => left.range[0] - right.range[0]);\n}\n\n/** The writes of one run through the writer it was given, in its own body and in the order they are written. */\nfunction writesOf(\n source: TSESLint.SourceCode,\n run: FunctionNode,\n written: Written,\n): readonly TSESTree.CallExpression[] {\n return callsThrough(source, run, written, CONTEXT_POSITION, WRITER_KEYS[0]);\n}\n\n/** `update(cell, …)` over a cell of this source: the readable a write names first, resolved to its declaration. */\nfunction writesSourceCell(\n source: TSESLint.SourceCode,\n call: TSESTree.CallExpression,\n cells: ReadonlySet<Variable>,\n): boolean {\n const [target] = call.arguments;\n\n if (target === undefined) return false;\n\n const named = unwrap(target);\n\n if (named.type !== AST_NODE_TYPES.Identifier) return false;\n\n const variable = variableOf(source, named);\n\n return variable !== null && cells.has(variable);\n}\n\n/**\n * D447: the body of the command this name holds, where it is declared on a model's own context in this module. The\n * run and the commands it invokes are one authority — `invoke` hands the command the cancellation of the run — so a\n * write of such a command into a cell the run's source reads ends the run exactly as the run's own write does.\n */\nfunction invokedCommand(source: TSESLint.SourceCode, node: TSESTree.Node): FunctionNode | undefined {\n const value = localValue(source, unwrap(node));\n\n if (value.type !== AST_NODE_TYPES.CallExpression) return undefined;\n\n const named = methodPath(source, value.callee);\n\n if (named?.method !== 'command') return undefined;\n\n return isModelContextFunction(source, named.fn) ? commandBody(source, value) : undefined;\n}\n\n/** `invoke(release)` where the body of `release` writes a cell of this source: the moving write is the command's. */\nfunction invokesSourceWrite(\n source: TSESLint.SourceCode,\n call: TSESTree.CallExpression,\n written: Written,\n cells: ReadonlySet<Variable>,\n): boolean {\n const [target] = call.arguments;\n const body = target === undefined ? undefined : invokedCommand(source, target);\n\n if (body === undefined) return false;\n\n return callsThrough(source, body, written, COMMAND_POSITION, WRITER_KEYS[0]).some(write =>\n writesSourceCell(source, write, cells),\n );\n}\n\n/** The statement a block ends on, looking inward: `{ … { return } }` ends on that `return`. */\nfunction lastStatement(statement: TSESTree.Statement): TSESTree.Statement | undefined {\n let current = statement;\n\n while (current.type === AST_NODE_TYPES.BlockStatement) {\n const last = current.body.at(-1);\n\n if (last === undefined) return undefined;\n\n current = last;\n }\n\n return current;\n}\n\n/** A branch that ends on a `return` or a `throw`. */\nfunction leavesDirectly(statement: TSESTree.Statement): boolean {\n const last = lastStatement(statement);\n\n return last !== undefined && EXITS.has(last.type);\n}\n\n/** A statement past which the run has left for good; a `break` or a `continue` leaves the code below it reachable. */\nfunction exits(statement: TSESTree.Statement): boolean {\n const last = lastStatement(statement);\n\n if (last === undefined) return false;\n\n if (EXITS.has(last.type)) return true;\n\n // `if (a) { return } else { throw }` leaves by either branch; an `if` nested deeper is not read.\n if (last.type !== AST_NODE_TYPES.IfStatement || last.alternate === null) return false;\n\n return leavesDirectly(last.consequent) && leavesDirectly(last.alternate);\n}\n\n/**\n * Whether the run leaves for good between the two writes. Only a block that holds the first write is read, and only\n * the statements of it that stand after that write and before the second one, so the `return` of\n * `if (key === null) { update(key, null); return; }` is found and the write below the `if` is left alone.\n */\nfunction leavesBetween(source: TSESLint.SourceCode, first: TSESTree.Node, second: TSESTree.Node): boolean {\n const from = first.range[1];\n const to = second.range[0];\n const between = (statement: TSESTree.Statement): boolean =>\n statement.range[0] >= from && statement.range[1] <= to && exits(statement);\n\n return source\n .getAncestors(first)\n .some(node => node.type === AST_NODE_TYPES.BlockStatement && node.body.some(between));\n}\n\n/** The step at which two paths from the same root part company. */\nfunction divergence(outer: readonly TSESTree.Node[], inner: readonly TSESTree.Node[]): number {\n const shared = Math.min(outer.length, inner.length);\n let at = 0;\n\n while (at < shared && outer[at] === inner[at]) at += 1;\n\n return at;\n}\n\n/**\n * Whether the second write can run after the first: the two stand in one block, in different statements, in this\n * order, with no unconditional exit of the run between them. Two writes of one statement — the branches of an `if`,\n * the arms of a ternary, the halves of a `try`, the sides of a comma — are not a sequence, and a rule that must not\n * cry wolf says nothing about them.\n */\nfunction follows(source: TSESLint.SourceCode, first: TSESTree.Node, second: TSESTree.Node): boolean {\n const outer = [...source.getAncestors(first), first];\n const inner = [...source.getAncestors(second), second];\n const at = divergence(outer, inner);\n const block = at === 0 ? undefined : outer[at - 1];\n const before = outer[at];\n const after = inner[at];\n\n if (block?.type !== AST_NODE_TYPES.BlockStatement || before === undefined || after === undefined) return false;\n\n return before.range[1] <= after.range[0] && !leavesBetween(source, first, second);\n}\n\nconst noWriteAfterSourceWrite = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const written: Written = { calls: [], declarators: [] };\n\n const reportRun = (run: FunctionNode, cells: ReadonlySet<Variable>): void => {\n const writes = writesOf(source, run, written);\n\n for (const write of writes) {\n if (!writesSourceCell(source, write, cells)) continue;\n\n if (writes.some(later => follows(source, write, later))) {\n context.report({ messageId: 'writeAfterSourceWrite', node: write });\n }\n }\n\n // D447: the same question of a call, because a command the run invokes writes under the authority of this run.\n for (const call of callsThrough(source, run, written, CONTEXT_POSITION, INVOKE_KEYS[0])) {\n if (!invokesSourceWrite(source, call, written, cells)) continue;\n\n if (writes.some(later => follows(source, call, later))) {\n context.report({ messageId: 'writeAfterInvokedSourceWrite', node: call });\n }\n }\n };\n\n return {\n CallExpression(node): void {\n written.calls.push(node);\n },\n 'Program:exit'(): void {\n for (const call of written.calls) {\n const declared = declaredEffect(source, call);\n\n if (declared === undefined) continue;\n\n const cells = sourceCells(source, declared.from);\n\n if (cells.size > 0) reportRun(declared.run, cells);\n }\n },\n VariableDeclarator(node): void {\n written.declarators.push(node);\n },\n };\n },\n meta: {\n docs: {\n description: 'A write into the own source of an effect, its own or a command it invokes, is that run last.',\n },\n messages: {\n /**\n * D447: the moving write is the command's, and the loss is the run's. This form is the one the runtime often\n * cannot report — the run is awaiting that call when its own command ends it, so the body never reaches a\n * write there is anything to drop — which is why the editor has to name it. The remedies are the same three,\n * in the same words, for the same reason as below.\n */\n writeAfterInvokedSourceWrite:\n \"The command this call starts writes into a cell this run's own source reads, so unless that write leaves the published value equal it ends this run inside the command: `invoke` gives the command the cancellation of the run, and one abort ends them both. Everything of the run after this call is then lost — a write it reaches is dropped and reaches the reporter as `LostWrite`, and a body still awaiting this call is cut short with a cancellation and reaches no write at all. Make the write into the source the last write of the run, or keep what the run accumulates out of its source and read it with `getSnapshot()`, or take `execution.capture(state)` before the moving write — a commit outlives the run and answers `applied` or `cancelled` (D288, D319, D365, D432, D447).\",\n /**\n * D442: the remedies are the three the runtime names in `LostWrite`, and the sentence below is that record's\n * own sentence word for word. The author reads this one first — the editor answers before the run does — so\n * the one remedy that saves the write whole, a `capture` taken before the moving write, cannot be the one\n * only the runtime names (D319, D432).\n */\n writeAfterSourceWrite:\n \"This write goes into a cell this run's own source reads, so unless it leaves the published value equal it ends this run inside the write itself, and every write of the run after it is dropped. The first of those dropped writes reaches the reporter as `LostWrite`, once per run, saying the rest went with it. Make the write into the source the last write of the run, or keep what the run accumulates out of its source and read it with `getSnapshot()`, or take `execution.capture(state)` before the moving write — a commit outlives the run and answers `applied` or `cancelled` (D288, D319, D365, D432, D447).\",\n },\n schema: [],\n type: 'problem',\n },\n name: 'no-write-after-source-write',\n});\n\nexport { noWriteAfterSourceWrite };\n"],"names":["WRITER_KEYS","INVOKE_KEYS","CONTEXT_POSITION","COMMAND_POSITION","COMPARISONS","CLASSIFIERS","EXITS","AST_NODE_TYPES","isDeclaredCell","source","node","value","localValue","named","methodPath","isContextFunction","derivedFrom","isApi","dependencies","selector","listed","unwrap","element","classified","parent","selectorParameter","position","fn","isFunction","parameter","travels","variableOf","identifier","addCell","cells","variable","sourceCells","from","derived","dependency","callsThrough","body","written","key","members","contextMembers","call","within","enclosingFunction","fromContext","left","right","writesOf","run","writesSourceCell","target","invokedCommand","isModelContextFunction","commandBody","invokesSourceWrite","write","lastStatement","statement","current","last","leavesDirectly","exits","leavesBetween","first","second","to","between","divergence","outer","inner","shared","at","follows","block","before","after","noWriteAfterSourceWrite","createRule","context","reportRun","writes","later","declared","declaredEffect"],"mappings":"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"}
1
+ {"version":3,"file":"no-write-after-source-write.js","sources":["../../src/rules/no-write-after-source-write.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { within } from '../ast';\nimport { contextMembers, fromContext } from '../context-members';\nimport { commandBody, isContextFunction, isModelContextFunction, methodPath } from '../declaration-ingress';\nimport { declaredEffect } from '../effect-declarations';\nimport type { FunctionNode } from '../model-bindings';\nimport { enclosingFunction, isApi, isFunction, localValue, unwrap, variableOf } from '../model-bindings';\nimport type { SourceCode, Variable } from '../rule';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'writeAfterInvokedSourceWrite' | 'writeAfterSourceWrite';\n/** What one file wrote, collected before any write is judged: the rule answers on `Program:exit` (D290). */\ntype Written = {\n readonly calls: TSESTree.CallExpression[];\n readonly declarators: TSESTree.VariableDeclarator[];\n};\n\n/** The dependencies of a `derive` and the selector that publishes a value from them. */\ntype Derived = {\n readonly dependencies: readonly TSESTree.Node[];\n readonly selector: TSESTree.CallExpressionArgument | undefined;\n};\n\n/** The one name this rule reads off the context of a run — its writer — and the place a run receives that context. */\nconst WRITER_KEYS = ['update'] as const;\n/** D447: the other name it reads off a run — the word a run starts an already declared command with. */\nconst INVOKE_KEYS = ['invoke'] as const;\n/** D379: position 1 — the value the run answers is its first parameter, and the context follows it. */\nconst CONTEXT_POSITION = 1;\n/** D386: a command body takes one argument, its context, so the same writer is read one position earlier. */\nconst COMMAND_POSITION = 0;\n\n/** A read of a cell that answers a question about it instead of publishing it. */\nconst COMPARISONS = new Set(['!=', '!==', '<', '<=', '==', '===', '>', '>=', 'in', 'instanceof']);\nconst CLASSIFIERS = new Set(['!', 'typeof']);\n\n/** The two statements that end a run for good. */\nconst EXITS = new Set<string>([AST_NODE_TYPES.ReturnStatement, AST_NODE_TYPES.ThrowStatement]);\n\n/** A cell of this model, written where the declaration can be read: `context.state(…)` on a declaring context. */\nfunction isDeclaredCell(source: SourceCode, node: TSESTree.Identifier): 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?.method === 'state' && isContextFunction(source, named.fn);\n}\n\n/** The dependencies of `derive([a, cell], …)` and of `derive(cell, …)` alike, as the declaration spells them. */\nfunction derivedFrom(source: SourceCode, node: TSESTree.Node): Derived | undefined {\n if (node.type !== AST_NODE_TYPES.CallExpression) return undefined;\n\n if (!isApi(source, node.callee, '@opetope/core', 'derive', true)) return undefined;\n\n const [dependencies, selector] = node.arguments;\n\n if (dependencies === undefined) return undefined;\n\n const listed = unwrap(dependencies);\n\n if (listed.type !== AST_NODE_TYPES.ArrayExpression) return { dependencies: [listed], selector };\n\n // A hole in the literal is not a dependency, and a spread is one this rule cannot read; both leave the list here.\n return { dependencies: listed.elements.flatMap(element => (element === null ? [] : [element])), selector };\n}\n\n/**\n * Whether this read carries the value of the cell into what the selector publishes. A read that only asks a question\n * about it — `current === null`, `!current`, `typeof current`, `current ? a : b` — classifies the cell rather than\n * publishing it, and a write that keeps the answer keeps the published value.\n */\nfunction classified(node: TSESTree.Identifier): boolean {\n const { parent } = node;\n\n if (parent.type === AST_NODE_TYPES.BinaryExpression) return COMPARISONS.has(parent.operator);\n\n if (parent.type === AST_NODE_TYPES.UnaryExpression) return CLASSIFIERS.has(parent.operator);\n\n if (parent.type === AST_NODE_TYPES.ConditionalExpression) return parent.test === node;\n\n return parent.type === AST_NODE_TYPES.IfStatement && parent.test === node;\n}\n\n/** The parameter of the selector that stands for this dependency, where the selector is one this file can read. */\nfunction selectorParameter(\n source: SourceCode,\n selector: TSESTree.Node | undefined,\n position: number,\n): TSESTree.Identifier | undefined {\n const fn = selector === undefined ? undefined : localValue(source, selector);\n\n if (fn === undefined || !isFunction(fn)) return undefined;\n\n const parameter = fn.params[position];\n\n return parameter?.type === AST_NODE_TYPES.Identifier ? parameter : undefined;\n}\n\n/**\n * Whether the value the source publishes travels with this dependency. The law has a carve-out — a write that leaves\n * the published value equal ends nothing (spec §2.11) — and a selector that only classifies its dependency is that\n * carve-out written down: a `derive` publishing `current === null ? 'none' : 'some'` does not move when one key\n * replaces another. So the selector has to be readable here and has to read this dependency as a value at least once;\n * a selector assembled elsewhere, one that drops the dependency, and one that only asks a question about it all leave\n * the rule silent.\n */\nfunction travels(source: SourceCode, selector: TSESTree.Node | undefined, position: number): boolean {\n const parameter = selectorParameter(source, selector, position);\n const variable = parameter === undefined ? null : variableOf(source, parameter);\n\n // A JSX tag name is not a read of this value, and neither is anything else this rule cannot place.\n return (\n variable?.references.some(\n ({ identifier }) => identifier.type === AST_NODE_TYPES.Identifier && !classified(identifier),\n ) ?? false\n );\n}\n\n/** This name, where it is a cell of this model, joins the set the source republishes. */\nfunction addCell(source: SourceCode, cells: Set<Variable>, node: TSESTree.Node): void {\n const named = unwrap(node);\n\n if (named.type !== AST_NODE_TYPES.Identifier) return;\n\n const variable = variableOf(source, named);\n\n if (variable !== null && isDeclaredCell(source, named)) cells.add(variable);\n}\n\n/**\n * The cells of this model whose value the effect's source republishes: the source itself where a cell is the source,\n * and every dependency of a `derive` this scope holds whose value the selector publishes. A source assembled in\n * another function, a cell that arrived as a parameter and a `derive` over a `derive` name nothing here — the rule\n * then stays silent, because it cannot prove a write moves the source.\n */\nfunction sourceCells(source: SourceCode, from: TSESTree.Node): ReadonlySet<Variable> {\n const cells = new Set<Variable>();\n\n // A cell as the source itself publishes its own value, so it travels by construction.\n addCell(source, cells, from);\n\n const derived = derivedFrom(source, localValue(source, from));\n\n if (derived === undefined) return cells;\n\n for (const [position, dependency] of derived.dependencies.entries()) {\n if (travels(source, derived.selector, position)) addCell(source, cells, dependency);\n }\n\n return cells;\n}\n\n/**\n * The calls one body makes through one name of its own context, in its own body and in the order they are written.\n * The position is part of the contract: a reaction run takes its context second and a command body first (D386).\n */\nfunction callsThrough<Key extends string>(\n source: SourceCode,\n body: FunctionNode,\n written: Written,\n position: number,\n key: Key,\n): readonly TSESTree.CallExpression[] {\n const members = contextMembers(source, body, position, [key], written.declarators);\n\n return written.calls\n .filter(call => within(call, body) && enclosingFunction(source, call) === body)\n .filter(call => fromContext(source, call.callee, members, key))\n .sort((left, right) => left.range[0] - right.range[0]);\n}\n\n/** The writes of one run through the writer it was given, in its own body and in the order they are written. */\nfunction writesOf(source: SourceCode, run: FunctionNode, written: Written): readonly TSESTree.CallExpression[] {\n return callsThrough(source, run, written, CONTEXT_POSITION, WRITER_KEYS[0]);\n}\n\n/** `update(cell, …)` over a cell of this source: the readable a write names first, resolved to its declaration. */\nfunction writesSourceCell(source: SourceCode, call: TSESTree.CallExpression, cells: ReadonlySet<Variable>): boolean {\n const [target] = call.arguments;\n\n if (target === undefined) return false;\n\n const named = unwrap(target);\n\n if (named.type !== AST_NODE_TYPES.Identifier) return false;\n\n const variable = variableOf(source, named);\n\n return variable !== null && cells.has(variable);\n}\n\n/**\n * D447: the body of the command this name holds, where it is declared on a model's own context in this module. The\n * run and the commands it invokes are one authority — `invoke` hands the command the cancellation of the run — so a\n * write of such a command into a cell the run's source reads ends the run exactly as the run's own write does.\n */\nfunction invokedCommand(source: SourceCode, node: TSESTree.Node): FunctionNode | undefined {\n const value = localValue(source, unwrap(node));\n\n if (value.type !== AST_NODE_TYPES.CallExpression) return undefined;\n\n const named = methodPath(source, value.callee);\n\n if (named?.method !== 'command') return undefined;\n\n return isModelContextFunction(source, named.fn) ? commandBody(source, value) : undefined;\n}\n\n/** `invoke(release)` where the body of `release` writes a cell of this source: the moving write is the command's. */\nfunction invokesSourceWrite(\n source: SourceCode,\n call: TSESTree.CallExpression,\n written: Written,\n cells: ReadonlySet<Variable>,\n): boolean {\n const [target] = call.arguments;\n const body = target === undefined ? undefined : invokedCommand(source, target);\n\n if (body === undefined) return false;\n\n return callsThrough(source, body, written, COMMAND_POSITION, WRITER_KEYS[0]).some(write =>\n writesSourceCell(source, write, cells),\n );\n}\n\n/** The statement a block ends on, looking inward: `{ … { return } }` ends on that `return`. */\nfunction lastStatement(statement: TSESTree.Statement): TSESTree.Statement | undefined {\n let current = statement;\n\n while (current.type === AST_NODE_TYPES.BlockStatement) {\n const last = current.body.at(-1);\n\n if (last === undefined) return undefined;\n\n current = last;\n }\n\n return current;\n}\n\n/** A branch that ends on a `return` or a `throw`. */\nfunction leavesDirectly(statement: TSESTree.Statement): boolean {\n const last = lastStatement(statement);\n\n return last !== undefined && EXITS.has(last.type);\n}\n\n/** A statement past which the run has left for good; a `break` or a `continue` leaves the code below it reachable. */\nfunction exits(statement: TSESTree.Statement): boolean {\n const last = lastStatement(statement);\n\n if (last === undefined) return false;\n\n if (EXITS.has(last.type)) return true;\n\n // `if (a) { return } else { throw }` leaves by either branch; an `if` nested deeper is not read.\n if (last.type !== AST_NODE_TYPES.IfStatement || last.alternate === null) return false;\n\n return leavesDirectly(last.consequent) && leavesDirectly(last.alternate);\n}\n\n/**\n * Whether the run leaves for good between the two writes. Only a block that holds the first write is read, and only\n * the statements of it that stand after that write and before the second one, so the `return` of\n * `if (key === null) { update(key, null); return; }` is found and the write below the `if` is left alone.\n */\nfunction leavesBetween(source: SourceCode, first: TSESTree.Node, second: TSESTree.Node): boolean {\n const from = first.range[1];\n const to = second.range[0];\n const between = (statement: TSESTree.Statement): boolean =>\n statement.range[0] >= from && statement.range[1] <= to && exits(statement);\n\n return source\n .getAncestors(first)\n .some(node => node.type === AST_NODE_TYPES.BlockStatement && node.body.some(between));\n}\n\n/** The step at which two paths from the same root part company. */\nfunction divergence(outer: readonly TSESTree.Node[], inner: readonly TSESTree.Node[]): number {\n const shared = Math.min(outer.length, inner.length);\n let at = 0;\n\n while (at < shared && outer[at] === inner[at]) at += 1;\n\n return at;\n}\n\n/**\n * Whether the second write can run after the first: the two stand in one block, in different statements, in this\n * order, with no unconditional exit of the run between them. Two writes of one statement — the branches of an `if`,\n * the arms of a ternary, the halves of a `try`, the sides of a comma — are not a sequence, and a rule that must not\n * cry wolf says nothing about them.\n */\nfunction follows(source: SourceCode, first: TSESTree.Node, second: TSESTree.Node): boolean {\n const outer = [...source.getAncestors(first), first];\n const inner = [...source.getAncestors(second), second];\n const at = divergence(outer, inner);\n const block = at === 0 ? undefined : outer[at - 1];\n const before = outer[at];\n const after = inner[at];\n\n if (block?.type !== AST_NODE_TYPES.BlockStatement || before === undefined || after === undefined) return false;\n\n return before.range[1] <= after.range[0] && !leavesBetween(source, first, second);\n}\n\nconst noWriteAfterSourceWrite = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const written: Written = { calls: [], declarators: [] };\n\n const reportRun = (run: FunctionNode, cells: ReadonlySet<Variable>): void => {\n const writes = writesOf(source, run, written);\n\n for (const write of writes) {\n if (!writesSourceCell(source, write, cells)) continue;\n\n if (writes.some(later => follows(source, write, later))) {\n context.report({ messageId: 'writeAfterSourceWrite', node: write });\n }\n }\n\n // D447: the same question of a call, because a command the run invokes writes under the authority of this run.\n for (const call of callsThrough(source, run, written, CONTEXT_POSITION, INVOKE_KEYS[0])) {\n if (!invokesSourceWrite(source, call, written, cells)) continue;\n\n if (writes.some(later => follows(source, call, later))) {\n context.report({ messageId: 'writeAfterInvokedSourceWrite', node: call });\n }\n }\n };\n\n return {\n CallExpression(node): void {\n written.calls.push(node);\n },\n 'Program:exit'(): void {\n for (const call of written.calls) {\n const declared = declaredEffect(source, call);\n\n if (declared === undefined) continue;\n\n const cells = sourceCells(source, declared.from);\n\n if (cells.size > 0) reportRun(declared.run, cells);\n }\n },\n VariableDeclarator(node): void {\n written.declarators.push(node);\n },\n };\n },\n meta: {\n docs: {\n description: 'A write into the own source of an effect, its own or a command it invokes, is that run last.',\n },\n messages: {\n /**\n * D447: the moving write is the command's, and the loss is the run's. This form is the one the runtime often\n * cannot report — the run is awaiting that call when its own command ends it, so the body never reaches a\n * write there is anything to drop — which is why the editor has to name it. The remedies are the same three,\n * in the same words, for the same reason as below.\n */\n writeAfterInvokedSourceWrite:\n \"The command this call starts writes into a cell this run's own source reads, so unless that write leaves the published value equal it ends this run inside the command: `invoke` gives the command the cancellation of the run, and one abort ends them both. Everything of the run after this call is then lost — a write it reaches is dropped and reaches the reporter as `LostWrite`, and a body still awaiting this call is cut short with a cancellation and reaches no write at all. Make the write into the source the last write of the run, or keep what the run accumulates out of its source and read it with `read`, or take `execution.capture(state)` before the moving write — a commit outlives the run and answers `applied` or `cancelled` (D288, D319, D365, D432, D447).\",\n /**\n * D442: the remedies are the three the runtime names in `LostWrite`, and the sentence below is that record's\n * own sentence word for word. The author reads this one first — the editor answers before the run does — so\n * the one remedy that saves the write whole, a `capture` taken before the moving write, cannot be the one\n * only the runtime names (D319, D432).\n */\n writeAfterSourceWrite:\n \"This write goes into a cell this run's own source reads, so unless it leaves the published value equal it ends this run inside the write itself, and every write of the run after it is dropped. The first of those dropped writes reaches the reporter as `LostWrite`, once per run, saying the rest went with it. Make the write into the source the last write of the run, or keep what the run accumulates out of its source and read it with `read`, or take `execution.capture(state)` before the moving write — a commit outlives the run and answers `applied` or `cancelled` (D288, D319, D365, D432, D447).\",\n },\n schema: [],\n type: 'problem',\n },\n name: 'no-write-after-source-write',\n});\n\nexport { noWriteAfterSourceWrite };\n"],"names":["WRITER_KEYS","INVOKE_KEYS","CONTEXT_POSITION","COMMAND_POSITION","COMPARISONS","CLASSIFIERS","EXITS","AST_NODE_TYPES","isDeclaredCell","source","node","value","localValue","named","methodPath","isContextFunction","derivedFrom","isApi","dependencies","selector","listed","unwrap","element","classified","parent","selectorParameter","position","fn","isFunction","parameter","travels","variableOf","identifier","addCell","cells","variable","sourceCells","from","derived","dependency","callsThrough","body","written","key","members","contextMembers","call","within","enclosingFunction","fromContext","left","right","writesOf","run","writesSourceCell","target","invokedCommand","isModelContextFunction","commandBody","invokesSourceWrite","write","lastStatement","statement","current","last","leavesDirectly","exits","leavesBetween","first","second","to","between","divergence","outer","inner","shared","at","follows","block","before","after","noWriteAfterSourceWrite","createRule","context","reportRun","writes","later","declared","declaredEffect"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * D494: an effect whose run only writes a projection of its value into a cell nothing else writes stores a second
3
+ * copy of what its source already says, a microtask late. `derive(source, project)` is that value with no cell, no
4
+ * run and no lag, and it is what the authoring checklist asks for.
5
+ */
6
+ declare const preferDerive: import("../rule.js").RuleModule<"preferDerive", []>;
7
+ export { preferDerive };
@@ -0,0 +1,2 @@
1
+ import{DefinitionType as w}from"@typescript-eslint/scope-manager";import{AST_NODE_TYPES as a}from"@typescript-eslint/types";import{within as m,boundNames as E}from"../ast.js";import{contextMembers as g,fromContext as v}from"../context-members.js";import{methodPath as u,isModelContextFunction as h}from"../declaration-ingress.js";import{declaredEffect as C}from"../effect-declarations.js";import{variableOf as l,localValue as S,referencesWithin as D,unwrap as A,isApi as O,isFunction as I}from"../model-bindings.js";import{createRule as M}from"../rule.js";function T(t){const{body:e}=t;if(e.type===a.CallExpression)return e;if(e.type!==a.BlockStatement||e.body.length!==1)return;const[n]=e.body;if(n?.type===a.ExpressionStatement)return n.expression.type===a.CallExpression?n.expression:void 0}function y(t,e){const n=new Set;for(const r of e.flatMap(o=>E(o))){const o=l(t,r);o!==null&&n.add(o)}return n}function V(t,e){if(e.has(t))return!0;const[n]=t.defs;return n?.type===w.Variable&&n.parent.kind!=="const"}function k(t,e,n){return D(t,e,n).flatMap(r=>l(t,r)??[])}function F(t,e,n){const{run:r,value:o}=e,i=y(t,r.params.slice(0,1)),s=y(t,r.params.slice(1)).add(n),d=k(t,r,o);return d.some(f=>i.has(f))&&!d.some(f=>V(f,s))}function x(t){const{callee:e}=t;return e.type===a.Identifier?e.name:e.type===a.MemberExpression&&e.property.type===a.Identifier?e.property.name:void 0}function N(t){let e=t;for(;e.parent!==void 0&&A(e.parent)!==e.parent;)e=e.parent;return e}const P=new Set(["derive","effect","event","select"]);function R(t,e){if(x(e)==="read"||O(t,e.callee,"@opetope/core","derive"))return!0;const n=u(t,e.callee);return n!==void 0&&P.has(n.method)&&h(t,n.fn)}function _(t,e,n){return e.references.some(({identifier:r})=>{const o=N(r),i=o.parent;if(i?.type!==a.CallExpression||i===n)return!1;const s=i.arguments.findIndex(d=>d===o);return s!==-1&&!(s===0&&R(t,i))})}function j(t,e){const n=C(t,e);if(!(n?.inline!==!0||n.options!==void 0))return n.run.async||n.run.generator?void 0:n.run}function W(t){return t!==void 0&&t.type!==a.SpreadElement&&!I(t)}function Y(t){const e=T(t),[n,r]=e?.arguments??[];if(!(e===void 0||n?.type!==a.Identifier))return W(r)?{target:n,value:r,write:e}:void 0}function B(t,e){const n=j(t,e),r=n===void 0?void 0:Y(n),o=r===void 0?void 0:u(t,e.callee);if(!(n===void 0||r===void 0||o===void 0))return h(t,o.fn)?{fn:o.fn,run:n,...r}:void 0}function G(t,e,n){const r=S(t,e);if(r.type!==a.CallExpression)return!1;const o=u(t,r.callee);return o?.method==="state"&&o.fn===n}const H=M({create(t){const e=t.sourceCode,n={barriers:[],calls:[],declarators:[]},r=i=>{const{fn:s,run:d,target:f,write:p}=i,b=g(e,d,1,["update"],n.declarators);if(!v(e,p.callee,b,"update")||!G(e,f,s))return;const c=l(e,f);return c===null||_(e,c,p)?void 0:c},o=i=>!n.barriers.some(s=>m(s,i))&&!n.calls.some(s=>m(s,i)&&x(s)==="getSnapshot");return{"AssignmentExpression, AwaitExpression, UpdateExpression, YieldExpression"(i){n.barriers.push(i)},CallExpression(i){n.calls.push(i)},"Program:exit"(){for(const i of n.calls){const s=B(e,i),d=s===void 0||!o(s.value)?void 0:r(s);s===void 0||d===void 0||!F(e,s,d)||t.report({data:{cell:d.name},messageId:"preferDerive",node:i})}},VariableDeclarator(i){n.declarators.push(i)}}},meta:{docs:{description:"A value computed from one source is a `derive`, not an effect that copies it into a cell."},messages:{preferDerive:"This effect only writes a projection of its source into `{{cell}}`, and nothing else writes that cell: it keeps a second copy of what the source already says, a microtask behind it. `derive(source, value => \u2026)` is the same value with no cell, no run and no lag; replace the cell and the effect with it (D494)."},schema:[],type:"suggestion"},name:"prefer-derive"});export{H as preferDerive};
2
+ //# sourceMappingURL=prefer-derive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-derive.js","sources":["../../src/rules/prefer-derive.ts"],"sourcesContent":["import { DefinitionType } from '@typescript-eslint/scope-manager';\nimport { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { boundNames, within } from '../ast';\nimport { contextMembers, fromContext } from '../context-members';\nimport { isModelContextFunction, methodPath } from '../declaration-ingress';\nimport { declaredEffect } from '../effect-declarations';\nimport type { FunctionNode } from '../model-bindings';\nimport { isApi, isFunction, localValue, referencesWithin, unwrap, variableOf } from '../model-bindings';\nimport type { SourceCode, Variable } from '../rule';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'preferDerive';\n\n/** What one file wrote, collected before any effect is judged: the rule answers on `Program:exit` (D290). */\ntype Written = {\n readonly barriers: TSESTree.Node[];\n readonly calls: TSESTree.CallExpression[];\n readonly declarators: TSESTree.VariableDeclarator[];\n};\n\n/** An effect of a model whose synchronous run is one call, and the model context that declared it. */\ntype Candidate = {\n readonly fn: FunctionNode;\n readonly run: FunctionNode;\n readonly target: TSESTree.Identifier;\n readonly value: TSESTree.Expression;\n readonly write: TSESTree.CallExpression;\n};\n\n/** The one statement of the run: `update(cell, value)`, as an expression body or the only statement of a block. */\nfunction onlyCall(run: FunctionNode): TSESTree.CallExpression | undefined {\n const { body } = run;\n\n if (body.type === AST_NODE_TYPES.CallExpression) return body;\n\n if (body.type !== AST_NODE_TYPES.BlockStatement || body.body.length !== 1) return undefined;\n\n const [statement] = body.body;\n\n if (statement?.type !== AST_NODE_TYPES.ExpressionStatement) return undefined;\n\n return statement.expression.type === AST_NODE_TYPES.CallExpression ? statement.expression : undefined;\n}\n\n/** The variables a list of parameters binds. */\nfunction parameterVariables(source: SourceCode, parameters: readonly TSESTree.Parameter[]): Set<Variable> {\n const variables = new Set<Variable>();\n\n for (const name of parameters.flatMap(parameter => boundNames(parameter))) {\n const variable = variableOf(source, name);\n\n if (variable !== null) variables.add(variable);\n }\n\n return variables;\n}\n\n/** A name the value may not read: the run's context, the cell it writes, or a binding that can change under it. */\nfunction isForbidden(variable: Variable, forbidden: ReadonlySet<Variable>): boolean {\n if (forbidden.has(variable)) return true;\n\n const [definition] = variable.defs;\n\n return definition?.type === DefinitionType.Variable && definition.parent.kind !== 'const';\n}\n\n/** The variables the value reads, in the run's scope and every scope under it. */\nfunction readsOf(source: SourceCode, run: FunctionNode, value: TSESTree.Node): Variable[] {\n return referencesWithin(source, run, value).flatMap(identifier => variableOf(source, identifier) ?? []);\n}\n\n/**\n * D494: a value that is a projection of the run's value alone — it reads that value, and nothing the run's context,\n * the written cell or a mutable binding would add. `derive(source, project)` computes exactly that.\n */\nfunction projectsCurrent(source: SourceCode, candidate: Candidate, cell: Variable): boolean {\n const { run, value } = candidate;\n const current = parameterVariables(source, run.params.slice(0, 1));\n const forbidden = parameterVariables(source, run.params.slice(1)).add(cell);\n const reads = readsOf(source, run, value);\n\n return reads.some(variable => current.has(variable)) && !reads.some(variable => isForbidden(variable, forbidden));\n}\n\n/** The name a call is made through: `update(…)` and `commit.update(…)` both answer `update`. */\nfunction calledName(call: TSESTree.CallExpression): string | undefined {\n const { callee } = call;\n\n if (callee.type === AST_NODE_TYPES.Identifier) return callee.name;\n\n return callee.type === AST_NODE_TYPES.MemberExpression && callee.property.type === AST_NODE_TYPES.Identifier\n ? callee.property.name\n : undefined;\n}\n\n/** The expression a reference stands as where it is written, past the type-only wrappers around it. */\nfunction writtenAs(identifier: TSESTree.Node): TSESTree.Node {\n let current = identifier;\n\n while (current.parent !== undefined && unwrap(current.parent) !== current.parent) current = current.parent;\n\n return current;\n}\n\n/** The methods of a model's context whose first argument is a source they read, never a cell they write. */\nconst READING_METHODS: ReadonlySet<string> = new Set(['derive', 'effect', 'event', 'select']);\n\n/**\n * Whether the call only reads what it is handed first: the source of a model's `derive`, `effect`, `event` or\n * `select`, the source of Core's `derive`, or `read(cell)` of a selection.\n */\nfunction readsFirst(source: SourceCode, call: TSESTree.CallExpression): boolean {\n if (calledName(call) === 'read' || isApi(source, call.callee, '@opetope/core', 'derive')) return true;\n\n const named = methodPath(source, call.callee);\n\n return named !== undefined && READING_METHODS.has(named.method) && isModelContextFunction(source, named.fn);\n}\n\n/**\n * Whether the cell is handed to any call but this write, other than one that reads it: `update(cell, …)` or\n * `capture(cell)` of any context, a writer taken under another name, or a helper that may write it. The rule cannot\n * tell which of them writes, and a cell that is written anywhere else is a cell of its own, so any of them keeps it\n * silent; a reading position — the source of a `derive` or an `effect`, `read(cell)` — is not a write.\n */\nfunction writtenElsewhere(source: SourceCode, cell: Variable, write: TSESTree.CallExpression): boolean {\n return cell.references.some(({ identifier }) => {\n const argument = writtenAs(identifier);\n const call = argument.parent;\n\n if (call?.type !== AST_NODE_TYPES.CallExpression || call === write) return false;\n\n const index = call.arguments.findIndex(item => item === argument);\n\n return index !== -1 && !(index === 0 && readsFirst(source, call));\n });\n}\n\n/** The run of an effect written inline, synchronous, with no modifiers. */\nfunction inlineRun(source: SourceCode, call: TSESTree.CallExpression): FunctionNode | undefined {\n const declared = declaredEffect(source, call);\n\n if (declared?.inline !== true || declared.options !== undefined) return undefined;\n\n return declared.run.async || declared.run.generator ? undefined : declared.run;\n}\n\n/** D288: a function written into a cell is an updater of its previous value, which is a fold and not a copy. */\nfunction isPlainValue(value: TSESTree.CallExpressionArgument | undefined): value is TSESTree.Expression {\n return value !== undefined && value.type !== AST_NODE_TYPES.SpreadElement && !isFunction(value);\n}\n\n/** The one call of the run, `write(cell, value)`, with a named cell and a plain value. */\nfunction onlyWrite(run: FunctionNode): Omit<Candidate, 'fn' | 'run'> | undefined {\n const write = onlyCall(run);\n const [target, value] = write?.arguments ?? [];\n\n if (write === undefined || target?.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n return isPlainValue(value) ? { target, value, write } : undefined;\n}\n\n/** An effect of a model whose inline, synchronous run is one write, and the context that declared it. */\nfunction candidateOf(source: SourceCode, call: TSESTree.CallExpression): Candidate | undefined {\n const run = inlineRun(source, call);\n const written = run === undefined ? undefined : onlyWrite(run);\n const named = written === undefined ? undefined : methodPath(source, call.callee);\n\n if (run === undefined || written === undefined || named === undefined) return undefined;\n\n return isModelContextFunction(source, named.fn) ? { fn: named.fn, run, ...written } : undefined;\n}\n\n/** Whether the cell is the `state(…)` of this very model. */\nfunction isStateOf(source: SourceCode, target: TSESTree.Identifier, fn: FunctionNode): boolean {\n const declared = localValue(source, target);\n\n if (declared.type !== AST_NODE_TYPES.CallExpression) return false;\n\n const named = methodPath(source, declared.callee);\n\n return named?.method === 'state' && named.fn === fn;\n}\n\n/**\n * D494: an effect whose run only writes a projection of its value into a cell nothing else writes stores a second\n * copy of what its source already says, a microtask late. `derive(source, project)` is that value with no cell, no\n * run and no lag, and it is what the authoring checklist asks for.\n */\nconst preferDerive = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const written: Written = { barriers: [], calls: [], declarators: [] };\n\n /** A cell of this very model, written through the run's own writer, which nothing else in the file writes. */\n const cellOf = (candidate: Candidate): Variable | undefined => {\n const { fn, run, target, write } = candidate;\n const members = contextMembers(source, run, 1, ['update'], written.declarators);\n\n if (!fromContext(source, write.callee, members, 'update') || !isStateOf(source, target, fn)) return undefined;\n\n const cell = variableOf(source, target);\n\n return cell === null || writtenElsewhere(source, cell, write) ? undefined : cell;\n };\n /** Past a write, an `await` or a read of another source, the value is no longer a projection of the run's own. */\n const settles = (value: TSESTree.Node): boolean =>\n !written.barriers.some(node => within(node, value)) &&\n !written.calls.some(call => within(call, value) && calledName(call) === 'getSnapshot');\n\n return {\n 'AssignmentExpression, AwaitExpression, UpdateExpression, YieldExpression'(node: TSESTree.Node): void {\n written.barriers.push(node);\n },\n CallExpression(node): void {\n written.calls.push(node);\n },\n 'Program:exit'(): void {\n for (const call of written.calls) {\n const candidate = candidateOf(source, call);\n const cell = candidate === undefined || !settles(candidate.value) ? undefined : cellOf(candidate);\n\n if (candidate === undefined || cell === undefined || !projectsCurrent(source, candidate, cell)) continue;\n\n context.report({ data: { cell: cell.name }, messageId: 'preferDerive', node: call });\n }\n },\n VariableDeclarator(node): void {\n written.declarators.push(node);\n },\n };\n },\n meta: {\n docs: {\n description: 'A value computed from one source is a `derive`, not an effect that copies it into a cell.',\n },\n messages: {\n preferDerive:\n 'This effect only writes a projection of its source into `{{cell}}`, and nothing else writes that cell: it keeps a second copy of what the source already says, a microtask behind it. `derive(source, value => …)` is the same value with no cell, no run and no lag; replace the cell and the effect with it (D494).',\n },\n schema: [],\n type: 'suggestion',\n },\n name: 'prefer-derive',\n});\n\nexport { preferDerive };\n"],"names":["onlyCall","run","body","AST_NODE_TYPES","statement","parameterVariables","source","parameters","variables","name","parameter","boundNames","variable","variableOf","isForbidden","forbidden","definition","DefinitionType","readsOf","value","referencesWithin","identifier","projectsCurrent","candidate","cell","current","reads","calledName","call","callee","writtenAs","unwrap","READING_METHODS","readsFirst","isApi","named","methodPath","isModelContextFunction","writtenElsewhere","write","argument","index","item","inlineRun","declared","declaredEffect","isPlainValue","isFunction","onlyWrite","target","candidateOf","written","isStateOf","fn","localValue","preferDerive","createRule","context","cellOf","members","contextMembers","fromContext","settles","node","within"],"mappings":"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"}
@@ -1,5 +1,2 @@
1
- import type { TSESLint } from '@typescript-eslint/utils';
2
- declare const preferEffectCurrent: TSESLint.RuleModule<"preferEffectCurrent", [], unknown, TSESLint.RuleListener> & {
3
- name: string;
4
- };
1
+ declare const preferEffectCurrent: import("../rule.js").RuleModule<"preferEffectCurrent", []>;
5
2
  export { preferEffectCurrent };
@@ -1,2 +1,2 @@
1
- import{AST_NODE_TYPES as o}from"@typescript-eslint/utils";import{within as u,findProperty as g}from"../ast.js";import{declaredEffect as E}from"../effect-declarations.js";import{isFunction as b,unwrap as c,variableOf as p}from"../model-bindings.js";import{createRule as y}from"../rule.js";function x(t){const{callee:n}=t;if(!(n.type!==o.MemberExpression||n.computed)&&!(n.property.type!==o.Identifier||n.property.name!=="getSnapshot"))return t.arguments.length===0?c(n.object):void 0}function w(t,n,s){if(s.type!==o.Identifier)return!1;const f=p(t,n),a=p(t,s);return f===null&&a===null?n.name===s.name:f===a}function v(t){const n=t.params[0];return n?.type===o.Identifier?n.name:void 0}function C(t){if(t===void 0)return!1;const n=c(t);return n.type!==o.ObjectExpression?!0:g(n,"filter")!==void 0}const I=y({create(t){const n=t.sourceCode,s={barriers:[],calls:[],functions:[]},f=r=>{const e=E(n,r);if(!(e===void 0||!e.inline||e.from.type!==o.Identifier))return{fixable:!C(e.options),from:e.from,run:e.run}},a=(r,e)=>s.functions.some(i=>i!==e&&u(i,e)&&u(r,i)),d=(r,e)=>s.barriers.some(i=>u(i,e)&&i.range[0]<r.range[0]),l=(r,e)=>{const i=v(r.run);if(i===void 0||!r.fixable||d(e,r.run)){t.report({messageId:"preferEffectCurrent",node:e});return}t.report({fix:h=>h.replaceTextRange(e.range,i),messageId:"preferEffectCurrent",node:e})},m=r=>{for(const e of s.calls){if(!u(e,r.run)||a(e,r.run))continue;const i=x(e);i===void 0||!w(n,r.from,i)||l(r,e)}};return{":function"(r){b(r)&&s.functions.push(r)},"AwaitExpression, YieldExpression"(r){s.barriers.push(r)},CallExpression(r){s.calls.push(r)},"Program:exit"(){for(const r of s.calls){const e=f(r);e!==void 0&&m(e)}}}},meta:{docs:{description:"The run of an effect answers a value it was given; it does not read that value back."},fixable:"code",messages:{preferEffectCurrent:"This reads the source of the effect it is already inside. The run receives that value as its first parameter, which is the value this run was started for and the one `filter` and `initial` are written against; `getSnapshot()` here reads the source a second time and says nothing about which run this is (D296, D323, D379)."},schema:[],type:"problem"},name:"prefer-effect-current"});export{I as preferEffectCurrent};
1
+ import{AST_NODE_TYPES as o}from"@typescript-eslint/types";import{within as u,findProperty as g}from"../ast.js";import{declaredEffect as E}from"../effect-declarations.js";import{isFunction as b,unwrap as c,variableOf as p}from"../model-bindings.js";import{createRule as y}from"../rule.js";function x(t){const{callee:n}=t;if(!(n.type!==o.MemberExpression||n.computed)&&!(n.property.type!==o.Identifier||n.property.name!=="getSnapshot"))return t.arguments.length===0?c(n.object):void 0}function v(t,n,s){if(s.type!==o.Identifier)return!1;const f=p(t,n),a=p(t,s);return f===null&&a===null?n.name===s.name:f===a}function w(t){const n=t.params[0];return n?.type===o.Identifier?n.name:void 0}function C(t){if(t===void 0)return!1;const n=c(t);return n.type!==o.ObjectExpression?!0:g(n,"filter")!==void 0}const I=y({create(t){const n=t.sourceCode,s={barriers:[],calls:[],functions:[]},f=r=>{const e=E(n,r);if(!(e===void 0||!e.inline||e.from.type!==o.Identifier))return{fixable:!C(e.options),from:e.from,run:e.run}},a=(r,e)=>s.functions.some(i=>i!==e&&u(i,e)&&u(r,i)),d=(r,e)=>s.barriers.some(i=>u(i,e)&&i.range[0]<r.range[0]),l=(r,e)=>{const i=w(r.run);if(i===void 0||!r.fixable||d(e,r.run)){t.report({messageId:"preferEffectCurrent",node:e});return}t.report({fix:h=>h.replaceTextRange(e.range,i),messageId:"preferEffectCurrent",node:e})},m=r=>{for(const e of s.calls){if(!u(e,r.run)||a(e,r.run))continue;const i=x(e);i===void 0||!v(n,r.from,i)||l(r,e)}};return{":function"(r){b(r)&&s.functions.push(r)},"AwaitExpression, YieldExpression"(r){s.barriers.push(r)},CallExpression(r){s.calls.push(r)},"Program:exit"(){for(const r of s.calls){const e=f(r);e!==void 0&&m(e)}}}},meta:{docs:{description:"The run of an effect answers the value it is given; it does not read that value back."},fixable:"code",messages:{preferEffectCurrent:"This reads the source of the effect it is already inside. The run receives that value as its first parameter, which is the value this run was started for and the one `filter` and `initial` are written against; `getSnapshot()` here reads the source a second time and says nothing about which run this is (D296, D323, D379)."},schema:[],type:"problem"},name:"prefer-effect-current"});export{I as preferEffectCurrent};
2
2
  //# sourceMappingURL=prefer-effect-current.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-effect-current.js","sources":["../../src/rules/prefer-effect-current.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { findProperty, within } from '../ast';\nimport { declaredEffect } from '../effect-declarations';\nimport type { FunctionNode } from '../model-bindings';\nimport { isFunction, unwrap, variableOf } from '../model-bindings';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'preferEffectCurrent';\n\n/** What one file wrote, collected before any read is judged: the rule answers on `Program:exit` (D290). */\ntype Written = {\n readonly barriers: TSESTree.Node[];\n readonly calls: TSESTree.CallExpression[];\n readonly functions: FunctionNode[];\n};\n\n/** One `effect(source, run, options?)` declared on a context of this library, with what a rewrite of it needs. */\ntype Declared = { readonly fixable: boolean; readonly from: TSESTree.Identifier; readonly run: FunctionNode };\n\n/** `s.getSnapshot()`, and the receiver it was read on; a reference that is not called reads nothing. */\nfunction snapshotReceiver(node: TSESTree.CallExpression): TSESTree.Node | undefined {\n const { callee } = node;\n\n if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) return undefined;\n\n if (callee.property.type !== AST_NODE_TYPES.Identifier || callee.property.name !== 'getSnapshot') return undefined;\n\n return node.arguments.length === 0 ? unwrap(callee.object) : undefined;\n}\n\n/** The same source: a name is the binding it resolves to, and two unresolved names are the same spelling. */\nfunction sameSource(source: TSESLint.SourceCode, from: TSESTree.Identifier, candidate: TSESTree.Node): boolean {\n if (candidate.type !== AST_NODE_TYPES.Identifier) return false;\n\n const declared = variableOf(source, from);\n const read = variableOf(source, candidate);\n\n return declared === null && read === null ? from.name === candidate.name : declared === read;\n}\n\n/**\n * D379: the value is the first parameter of the run, so the word the fix writes is the name the author bound it\n * under. A run that destructured the value itself, or took no parameter at all, has no word for the whole of it, and\n * the report then carries no fix.\n */\nfunction currentExpression(run: FunctionNode): string | undefined {\n const parameter = run.params[0];\n\n return parameter?.type === AST_NODE_TYPES.Identifier ? parameter.name : undefined;\n}\n\n/**\n * D296, D379: `filter` selects values without restarting the run, so under it the source moves while the run keeps\n * going. The value the run was given is then the one it was started for and the snapshot is the newer one — two\n * different facts, and a silent rewrite between them is exactly what a fixable rule may not do.\n */\nfunction declaresFilter(options: TSESTree.CallExpressionArgument | undefined): boolean {\n if (options === undefined) return false;\n\n const record = unwrap(options);\n\n // A record assembled elsewhere may carry the predicate, and a rule that rewrites code assumes it does.\n if (record.type !== AST_NODE_TYPES.ObjectExpression) return true;\n\n return findProperty(record, 'filter') !== undefined;\n}\n\nconst preferEffectCurrent = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const written: Written = { barriers: [], calls: [], functions: [] };\n\n /**\n * This rule rewrites a read inside the run, so it asks for the two shapes a rewrite needs and nothing wider: a\n * source named by an identifier, to compare the read against, and the run written where the declaration takes it.\n */\n const effectRun = (call: TSESTree.CallExpression): Declared | undefined => {\n const declared = declaredEffect(source, call);\n\n if (declared === undefined || !declared.inline || declared.from.type !== AST_NODE_TYPES.Identifier) {\n return undefined;\n }\n\n return { fixable: !declaresFilter(declared.options), from: declared.from, run: declared.run };\n };\n /** A function written inside the run runs later than the run does, and a snapshot read there is its own. */\n const nested = (node: TSESTree.Node, run: FunctionNode): boolean =>\n written.functions.some(fn => fn !== run && within(fn, run) && within(node, fn));\n /**\n * After an `await` the two are no longer the same value: the parameter is what this run started with, the\n * snapshot is what the source holds now. The run usually restarts on a change, so they usually agree — and\n * «usually» is not a licence to rewrite, so past the first barrier the rule only says so.\n */\n const settled = (node: TSESTree.Node, run: FunctionNode): boolean =>\n written.barriers.some(barrier => within(barrier, run) && barrier.range[0] < node.range[0]);\n const report = (declared: Declared, read: TSESTree.CallExpression): void => {\n const replacement = currentExpression(declared.run);\n\n if (replacement === undefined || !declared.fixable || settled(read, declared.run)) {\n context.report({ messageId: 'preferEffectCurrent', node: read });\n\n return;\n }\n\n context.report({\n fix: fixer => fixer.replaceTextRange(read.range, replacement),\n messageId: 'preferEffectCurrent',\n node: read,\n });\n };\n const reportReads = (declared: Declared): void => {\n for (const read of written.calls) {\n if (!within(read, declared.run) || nested(read, declared.run)) continue;\n\n const receiver = snapshotReceiver(read);\n\n if (receiver === undefined || !sameSource(source, declared.from, receiver)) continue;\n\n report(declared, read);\n }\n };\n\n return {\n ':function'(node: TSESTree.Node): void {\n if (isFunction(node)) written.functions.push(node);\n },\n 'AwaitExpression, YieldExpression'(node: TSESTree.Node): void {\n written.barriers.push(node);\n },\n CallExpression(node): void {\n written.calls.push(node);\n },\n 'Program:exit'(): void {\n for (const call of written.calls) {\n const declared = effectRun(call);\n\n if (declared !== undefined) reportReads(declared);\n }\n },\n };\n },\n meta: {\n docs: {\n description: 'The run of an effect answers a value it was given; it does not read that value back.',\n },\n fixable: 'code',\n messages: {\n preferEffectCurrent:\n 'This reads the source of the effect it is already inside. The run receives that value as its first parameter, which is the value this run was started for and the one `filter` and `initial` are written against; `getSnapshot()` here reads the source a second time and says nothing about which run this is (D296, D323, D379).',\n },\n schema: [],\n type: 'problem',\n },\n name: 'prefer-effect-current',\n});\n\nexport { preferEffectCurrent };\n"],"names":["snapshotReceiver","node","callee","AST_NODE_TYPES","unwrap","sameSource","source","from","candidate","declared","variableOf","read","currentExpression","run","parameter","declaresFilter","options","record","findProperty","preferEffectCurrent","createRule","context","written","effectRun","call","declaredEffect","nested","fn","within","settled","barrier","report","replacement","fixer","reportReads","receiver","isFunction"],"mappings":"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"}
1
+ {"version":3,"file":"prefer-effect-current.js","sources":["../../src/rules/prefer-effect-current.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { findProperty, within } from '../ast';\nimport { declaredEffect } from '../effect-declarations';\nimport type { FunctionNode } from '../model-bindings';\nimport { isFunction, unwrap, variableOf } from '../model-bindings';\nimport type { SourceCode } from '../rule';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'preferEffectCurrent';\n\n/** What one file wrote, collected before any read is judged: the rule answers on `Program:exit` (D290). */\ntype Written = {\n readonly barriers: TSESTree.Node[];\n readonly calls: TSESTree.CallExpression[];\n readonly functions: FunctionNode[];\n};\n\n/** One `effect(source, run, options?)` declared on a context of this library, with what a rewrite of it needs. */\ntype Declared = { readonly fixable: boolean; readonly from: TSESTree.Identifier; readonly run: FunctionNode };\n\n/** `s.getSnapshot()`, and the receiver it was read on; a reference that is not called reads nothing. */\nfunction snapshotReceiver(node: TSESTree.CallExpression): TSESTree.Node | undefined {\n const { callee } = node;\n\n if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed) return undefined;\n\n if (callee.property.type !== AST_NODE_TYPES.Identifier || callee.property.name !== 'getSnapshot') return undefined;\n\n return node.arguments.length === 0 ? unwrap(callee.object) : undefined;\n}\n\n/** The same source: a name is the binding it resolves to, and two unresolved names are the same spelling. */\nfunction sameSource(source: SourceCode, from: TSESTree.Identifier, candidate: TSESTree.Node): boolean {\n if (candidate.type !== AST_NODE_TYPES.Identifier) return false;\n\n const declared = variableOf(source, from);\n const read = variableOf(source, candidate);\n\n return declared === null && read === null ? from.name === candidate.name : declared === read;\n}\n\n/**\n * D379: the value is the first parameter of the run, so the word the fix writes is the name the author bound it\n * under. A run that destructured the value itself, or took no parameter at all, has no word for the whole of it, and\n * the report then carries no fix.\n */\nfunction currentExpression(run: FunctionNode): string | undefined {\n const parameter = run.params[0];\n\n return parameter?.type === AST_NODE_TYPES.Identifier ? parameter.name : undefined;\n}\n\n/**\n * D296, D379: `filter` selects values without restarting the run, so under it the source moves while the run keeps\n * going. The value the run was given is then the one it was started for and the snapshot is the newer one — two\n * different facts, and a silent rewrite between them is exactly what a fixable rule may not do.\n */\nfunction declaresFilter(options: TSESTree.CallExpressionArgument | undefined): boolean {\n if (options === undefined) return false;\n\n const record = unwrap(options);\n\n // A record assembled elsewhere may carry the predicate, and a rule that rewrites code assumes it does.\n if (record.type !== AST_NODE_TYPES.ObjectExpression) return true;\n\n return findProperty(record, 'filter') !== undefined;\n}\n\nconst preferEffectCurrent = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const written: Written = { barriers: [], calls: [], functions: [] };\n\n /**\n * This rule rewrites a read inside the run, so it asks for the two shapes a rewrite needs and nothing wider: a\n * source named by an identifier, to compare the read against, and the run written where the declaration takes it.\n */\n const effectRun = (call: TSESTree.CallExpression): Declared | undefined => {\n const declared = declaredEffect(source, call);\n\n if (declared === undefined || !declared.inline || declared.from.type !== AST_NODE_TYPES.Identifier) {\n return undefined;\n }\n\n return { fixable: !declaresFilter(declared.options), from: declared.from, run: declared.run };\n };\n /** A function written inside the run runs later than the run does, and a snapshot read there is its own. */\n const nested = (node: TSESTree.Node, run: FunctionNode): boolean =>\n written.functions.some(fn => fn !== run && within(fn, run) && within(node, fn));\n /**\n * After an `await` the two are no longer the same value: the parameter is what this run started with, the\n * snapshot is what the source holds now. The run usually restarts on a change, so they usually agree — and\n * «usually» is not a licence to rewrite, so past the first barrier the rule only says so.\n */\n const settled = (node: TSESTree.Node, run: FunctionNode): boolean =>\n written.barriers.some(barrier => within(barrier, run) && barrier.range[0] < node.range[0]);\n const report = (declared: Declared, read: TSESTree.CallExpression): void => {\n const replacement = currentExpression(declared.run);\n\n if (replacement === undefined || !declared.fixable || settled(read, declared.run)) {\n context.report({ messageId: 'preferEffectCurrent', node: read });\n\n return;\n }\n\n context.report({\n fix: fixer => fixer.replaceTextRange(read.range, replacement),\n messageId: 'preferEffectCurrent',\n node: read,\n });\n };\n const reportReads = (declared: Declared): void => {\n for (const read of written.calls) {\n if (!within(read, declared.run) || nested(read, declared.run)) continue;\n\n const receiver = snapshotReceiver(read);\n\n if (receiver === undefined || !sameSource(source, declared.from, receiver)) continue;\n\n report(declared, read);\n }\n };\n\n return {\n ':function'(node: TSESTree.Node): void {\n if (isFunction(node)) written.functions.push(node);\n },\n 'AwaitExpression, YieldExpression'(node: TSESTree.Node): void {\n written.barriers.push(node);\n },\n CallExpression(node): void {\n written.calls.push(node);\n },\n 'Program:exit'(): void {\n for (const call of written.calls) {\n const declared = effectRun(call);\n\n if (declared !== undefined) reportReads(declared);\n }\n },\n };\n },\n meta: {\n docs: {\n description: 'The run of an effect answers the value it is given; it does not read that value back.',\n },\n fixable: 'code',\n messages: {\n preferEffectCurrent:\n 'This reads the source of the effect it is already inside. The run receives that value as its first parameter, which is the value this run was started for and the one `filter` and `initial` are written against; `getSnapshot()` here reads the source a second time and says nothing about which run this is (D296, D323, D379).',\n },\n schema: [],\n type: 'problem',\n },\n name: 'prefer-effect-current',\n});\n\nexport { preferEffectCurrent };\n"],"names":["snapshotReceiver","node","callee","AST_NODE_TYPES","unwrap","sameSource","source","from","candidate","declared","variableOf","read","currentExpression","run","parameter","declaresFilter","options","record","findProperty","preferEffectCurrent","createRule","context","written","effectRun","call","declaredEffect","nested","fn","within","settled","barrier","report","replacement","fixer","reportReads","receiver","isFunction"],"mappings":"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"}
@@ -1,5 +1,2 @@
1
- import { TSESLint } from '@typescript-eslint/utils';
2
- declare const preferLazyBodyRecord: TSESLint.RuleModule<"preferRecord", [], unknown, TSESLint.RuleListener> & {
3
- name: string;
4
- };
1
+ declare const preferLazyBodyRecord: import("../rule.js").RuleModule<"preferRecord", []>;
5
2
  export { preferLazyBodyRecord };
@@ -1,2 +1,2 @@
1
- import{AST_NODE_TYPES as d,TSESLint as p}from"@typescript-eslint/utils";import{definitionOf as s}from"../model-bindings.js";import{bodyCallback as c,headerRecord as f,recordReplacement as m}from"../lazy-body-migration.js";import{createRule as y}from"../rule.js";function l(t,e){return e?.type===d.Identifier&&s(t,e)?.type===p.Scope.DefinitionType.ImportBinding}const u=y({create(t){const e=t.sourceCode;return{CallExpression(o){const r=c(e,o);if(r===void 0)return;const i=o.arguments[0];if(!l(e,i)&&f(e,i)===void 0)return;const n=m(e,o,r.fn,r.entries);t.report({node:r.fn,messageId:"preferRecord",fix:n===void 0?null:a=>a.replaceText(r.fn,n)})}}},meta:{docs:{description:"Prefer implementations keyed by a lazy header over repeated targets and priorities."},fixable:"code",messages:{preferRecord:"For a lazy header with fixed target keys, use `provides: { key: implementation }`; target and priority already belong to the header. Keep the callback when the header is dynamic or its target types require it (D464)."},schema:[],type:"suggestion"},name:"prefer-lazy-body-record"});export{u as preferLazyBodyRecord};
1
+ import{DefinitionType as d}from"@typescript-eslint/scope-manager";import{AST_NODE_TYPES as p}from"@typescript-eslint/types";import{definitionOf as s}from"../model-bindings.js";import{bodyCallback as c,headerRecord as m,recordReplacement as f}from"../lazy-body-migration.js";import{createRule as y}from"../rule.js";function l(t,e){return e?.type===p.Identifier&&s(t,e)?.type===d.ImportBinding}const u=y({create(t){const e=t.sourceCode;return{CallExpression(o){const r=c(e,o);if(r===void 0)return;const i=o.arguments[0];if(!l(e,i)&&m(e,i)===void 0)return;const n=f(e,o,r.fn,r.entries);t.report({node:r.fn,messageId:"preferRecord",fix:n===void 0?null:a=>a.replaceText(r.fn,n)})}}},meta:{docs:{description:"Prefer implementations keyed by a lazy header over repeated targets and priorities."},fixable:"code",messages:{preferRecord:"For a lazy header with fixed target keys, use `provides: { key: implementation }`; target and priority already belong to the header. Keep the callback when the header is dynamic or its target types require it (D464)."},schema:[],type:"suggestion"},name:"prefer-lazy-body-record"});export{u as preferLazyBodyRecord};
2
2
  //# sourceMappingURL=prefer-lazy-body-record.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-lazy-body-record.js","sources":["../../src/rules/prefer-lazy-body-record.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES, TSESLint } from '@typescript-eslint/utils';\n\nimport { definitionOf } from '../model-bindings';\nimport { bodyCallback, headerRecord, recordReplacement } from '../lazy-body-migration';\nimport { createRule } from '../rule';\n\nfunction importedHeader(source: TSESLint.SourceCode, header: TSESTree.Node | undefined): boolean {\n return (\n header?.type === AST_NODE_TYPES.Identifier &&\n definitionOf(source, header)?.type === TSESLint.Scope.DefinitionType.ImportBinding\n );\n}\n\nconst preferLazyBodyRecord = createRule<[], 'preferRecord'>({\n create(context) {\n const source = context.sourceCode;\n return {\n CallExpression(call): void {\n const callback = bodyCallback(source, call);\n if (callback === undefined) return;\n const header = call.arguments[0];\n const imported = importedHeader(source, header);\n if (!imported && headerRecord(source, header) === undefined) return;\n const replacement = recordReplacement(source, call, callback.fn, callback.entries);\n context.report({\n node: callback.fn,\n messageId: 'preferRecord',\n fix: replacement === undefined ? null : fixer => fixer.replaceText(callback.fn, replacement),\n });\n },\n };\n },\n meta: {\n docs: { description: 'Prefer implementations keyed by a lazy header over repeated targets and priorities.' },\n fixable: 'code',\n messages: {\n preferRecord:\n 'For a lazy header with fixed target keys, use `provides: { key: implementation }`; target and priority already belong to the header. Keep the callback when the header is dynamic or its target types require it (D464).',\n },\n schema: [],\n type: 'suggestion',\n },\n name: 'prefer-lazy-body-record',\n});\n\nexport { preferLazyBodyRecord };\n"],"names":["importedHeader","source","header","AST_NODE_TYPES","definitionOf","TSESLint","preferLazyBodyRecord","createRule","context","call","callback","bodyCallback","headerRecord","replacement","recordReplacement","fixer"],"mappings":"sQAOA,SAASA,EAAeC,EAA6BC,EAAiC,CACpF,OACEA,GAAQ,OAASC,EAAe,YAChCC,EAAaH,EAAQC,CAAM,GAAG,OAASG,EAAS,MAAM,eAAe,aAEzE,CAEA,MAAMC,EAAuBC,EAA+B,CAC1D,OAAOC,EAAO,CACZ,MAAMP,EAASO,EAAQ,WACvB,MAAO,CACL,eAAeC,EAAI,CACjB,MAAMC,EAAWC,EAAaV,EAAQQ,CAAI,EAC1C,GAAIC,IAAa,OAAW,OAC5B,MAAMR,EAASO,EAAK,UAAU,CAAC,EAE/B,GAAI,CADaT,EAAeC,EAAQC,CAAM,GAC7BU,EAAaX,EAAQC,CAAM,IAAM,OAAW,OAC7D,MAAMW,EAAcC,EAAkBb,EAAQQ,EAAMC,EAAS,GAAIA,EAAS,OAAO,EACjFF,EAAQ,OAAO,CACb,KAAME,EAAS,GACf,UAAW,eACX,IAAKG,IAAgB,OAAY,KAAOE,GAASA,EAAM,YAAYL,EAAS,GAAIG,CAAW,CAC5F,CAAA,CACH,EAEJ,EACA,KAAM,CACJ,KAAM,CAAE,YAAa,qFAAqF,EAC1G,QAAS,OACT,SAAU,CACR,aACE,0NACH,EACD,OAAQ,CAAA,EACR,KAAM,YACP,EACD,KAAM,yBACP,CAAA"}
1
+ {"version":3,"file":"prefer-lazy-body-record.js","sources":["../../src/rules/prefer-lazy-body-record.ts"],"sourcesContent":["import { DefinitionType } from '@typescript-eslint/scope-manager';\nimport { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { definitionOf } from '../model-bindings';\nimport { bodyCallback, headerRecord, recordReplacement } from '../lazy-body-migration';\nimport type { SourceCode } from '../rule';\nimport { createRule } from '../rule';\n\nfunction importedHeader(source: SourceCode, header: TSESTree.Node | undefined): boolean {\n return (\n header?.type === AST_NODE_TYPES.Identifier && definitionOf(source, header)?.type === DefinitionType.ImportBinding\n );\n}\n\nconst preferLazyBodyRecord = createRule<[], 'preferRecord'>({\n create(context) {\n const source = context.sourceCode;\n return {\n CallExpression(call): void {\n const callback = bodyCallback(source, call);\n if (callback === undefined) return;\n const header = call.arguments[0];\n const imported = importedHeader(source, header);\n if (!imported && headerRecord(source, header) === undefined) return;\n const replacement = recordReplacement(source, call, callback.fn, callback.entries);\n context.report({\n node: callback.fn,\n messageId: 'preferRecord',\n fix: replacement === undefined ? null : fixer => fixer.replaceText(callback.fn, replacement),\n });\n },\n };\n },\n meta: {\n docs: { description: 'Prefer implementations keyed by a lazy header over repeated targets and priorities.' },\n fixable: 'code',\n messages: {\n preferRecord:\n 'For a lazy header with fixed target keys, use `provides: { key: implementation }`; target and priority already belong to the header. Keep the callback when the header is dynamic or its target types require it (D464).',\n },\n schema: [],\n type: 'suggestion',\n },\n name: 'prefer-lazy-body-record',\n});\n\nexport { preferLazyBodyRecord };\n"],"names":["importedHeader","source","header","AST_NODE_TYPES","definitionOf","DefinitionType","preferLazyBodyRecord","createRule","context","call","callback","bodyCallback","headerRecord","replacement","recordReplacement","fixer"],"mappings":"0TASA,SAASA,EAAeC,EAAoBC,EAAiC,CAC3E,OACEA,GAAQ,OAASC,EAAe,YAAcC,EAAaH,EAAQC,CAAM,GAAG,OAASG,EAAe,aAExG,CAEA,MAAMC,EAAuBC,EAA+B,CAC1D,OAAOC,EAAO,CACZ,MAAMP,EAASO,EAAQ,WACvB,MAAO,CACL,eAAeC,EAAI,CACjB,MAAMC,EAAWC,EAAaV,EAAQQ,CAAI,EAC1C,GAAIC,IAAa,OAAW,OAC5B,MAAMR,EAASO,EAAK,UAAU,CAAC,EAE/B,GAAI,CADaT,EAAeC,EAAQC,CAAM,GAC7BU,EAAaX,EAAQC,CAAM,IAAM,OAAW,OAC7D,MAAMW,EAAcC,EAAkBb,EAAQQ,EAAMC,EAAS,GAAIA,EAAS,OAAO,EACjFF,EAAQ,OAAO,CACb,KAAME,EAAS,GACf,UAAW,eACX,IAAKG,IAAgB,OAAY,KAAOE,GAASA,EAAM,YAAYL,EAAS,GAAIG,CAAW,CAC5F,CAAA,CACH,EAEJ,EACA,KAAM,CACJ,KAAM,CAAE,YAAa,qFAAqF,EAC1G,QAAS,OACT,SAAU,CACR,aACE,0NACH,EACD,OAAQ,CAAA,EACR,KAAM,YACP,EACD,KAAM,yBACP,CAAA"}
@@ -1,7 +1,5 @@
1
1
  type Options = readonly [{
2
2
  readonly threshold: number;
3
3
  }];
4
- declare const preferModelSelection: import("@typescript-eslint/utils/ts-eslint").RuleModule<"separateHooks", Options, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
5
- name: string;
6
- };
4
+ declare const preferModelSelection: import("../rule.js").RuleModule<"separateHooks", Options>;
7
5
  export { preferModelSelection };
@@ -1,2 +1,2 @@
1
- import{AST_NODE_TYPES as a,ASTUtils as c}from"@typescript-eslint/utils";import{calleeName as u,memberRoot as f}from"../ast.js";import{createRule as l}from"../rule.js";const m=new Set(["useCommand","useReadable"]);function p(e){const{parent:n}=e;if(!(u(e)!=="useModel"||e.arguments.length!==1)&&!(n.type!==a.VariableDeclarator||n.id.type!==a.Identifier))return n}function h(e){const n=u(e),[o]=e.arguments;if(n===void 0||!m.has(n)||o?.type!==a.MemberExpression)return;const s=f(o);return s.type===a.Identifier?s:void 0}const g=l({create(e,[{threshold:n}]){const o=new Map,s=t=>{const r=h(t);if(r===void 0)return;const[i]=c.findVariable(e.sourceCode.getScope(r),r)?.defs??[],d=i===void 0?void 0:o.get(i.node);d!==void 0&&(d.count+=1)};return{CallExpression(t){const r=p(t);r===void 0?s(t):o.set(r,{count:0,node:t})},"Program:exit"(){for(const t of o.values())t.count<n||e.report({data:{count:t.count},messageId:"separateHooks",node:t.node})}}},meta:{defaultOptions:[{threshold:3}],docs:{description:"One model is read through one selection instead of a hook for each of its fields."},messages:{separateHooks:"This model is read through {{count}} separate hooks: `useModel(Declaration, (model, { read }) => ...)` selects data and commands in one call, with the same authority and the same statuses (D205, D214)."},schema:[{additionalProperties:!1,properties:{threshold:{minimum:2,type:"integer"}},type:"object"}],type:"suggestion"},name:"prefer-model-selection"});export{g as preferModelSelection};
1
+ import{AST_NODE_TYPES as a}from"@typescript-eslint/types";import{calleeName as u,memberRoot as c}from"../ast.js";import{createRule as f,findVariable as l}from"../rule.js";const m=new Set(["useCommand","useReadable"]);function p(e){const{parent:n}=e;if(!(u(e)!=="useModel"||e.arguments.length!==1)&&!(n.type!==a.VariableDeclarator||n.id.type!==a.Identifier))return n}function h(e){const n=u(e),[o]=e.arguments;if(n===void 0||!m.has(n)||o?.type!==a.MemberExpression)return;const s=c(o);return s.type===a.Identifier?s:void 0}const g=f({create(e,[{threshold:n}]){const o=new Map,s=t=>{const r=h(t);if(r===void 0)return;const[i]=l(e.sourceCode.getScope(r),r)?.defs??[],d=i===void 0?void 0:o.get(i.node);d!==void 0&&(d.count+=1)};return{CallExpression(t){const r=p(t);r===void 0?s(t):o.set(r,{count:0,node:t})},"Program:exit"(){for(const t of o.values())t.count<n||e.report({data:{count:t.count},messageId:"separateHooks",node:t.node})}}},meta:{defaultOptions:[{threshold:3}],docs:{description:"One model is read through one selection instead of a hook for each of its fields."},messages:{separateHooks:"This model is read through {{count}} separate hooks: `useModel(Declaration, (model, { read }) => ...)` selects data and commands in one call, with the same authority and the same statuses (D205, D214)."},schema:[{additionalProperties:!1,properties:{threshold:{minimum:2,type:"integer"}},type:"object"}],type:"suggestion"},name:"prefer-model-selection"});export{g as preferModelSelection};
2
2
  //# sourceMappingURL=prefer-model-selection.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-model-selection.js","sources":["../../src/rules/prefer-model-selection.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES, ASTUtils } from '@typescript-eslint/utils';\n\nimport { calleeName, memberRoot } from '../ast';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'separateHooks';\ntype Options = readonly [{ readonly threshold: number }];\n\ntype Reader = {\n count: number;\n readonly node: TSESTree.CallExpression;\n};\n\nconst FIELD_HOOKS = new Set(['useCommand', 'useReadable']);\n\n/** `const order = useModel(OrderModel)` — the granted model, without a selection of its own. */\nfunction grantedModelDeclarator(node: TSESTree.CallExpression): TSESTree.VariableDeclarator | undefined {\n const { parent } = node;\n\n if (calleeName(node) !== 'useModel' || node.arguments.length !== 1) return undefined;\n\n if (parent.type !== AST_NODE_TYPES.VariableDeclarator || parent.id.type !== AST_NODE_TYPES.Identifier) {\n return undefined;\n }\n\n return parent;\n}\n\n/** The identifier a field hook reads its argument from: `useReadable(order.amount)` names `order`. */\nfunction readIdentifier(node: TSESTree.CallExpression): TSESTree.Identifier | undefined {\n const name = calleeName(node);\n const [argument] = node.arguments;\n\n if (name === undefined || !FIELD_HOOKS.has(name) || argument?.type !== AST_NODE_TYPES.MemberExpression) {\n return undefined;\n }\n\n const root = memberRoot(argument);\n\n return root.type === AST_NODE_TYPES.Identifier ? root : undefined;\n}\n\nconst preferModelSelection = createRule<Options, MessageIds>({\n create(context, [{ threshold }]) {\n // Keyed by the declaration a name resolves to, so two components with the same variable name stay apart.\n const readers = new Map<TSESTree.Node, Reader>();\n\n const count = (node: TSESTree.CallExpression): void => {\n const identifier = readIdentifier(node);\n\n if (identifier === undefined) return;\n\n const [definition] = ASTUtils.findVariable(context.sourceCode.getScope(identifier), identifier)?.defs ?? [];\n const reader = definition === undefined ? undefined : readers.get(definition.node);\n\n if (reader !== undefined) reader.count += 1;\n };\n\n return {\n CallExpression(node): void {\n const declarator = grantedModelDeclarator(node);\n\n if (declarator === undefined) count(node);\n else readers.set(declarator, { count: 0, node });\n },\n 'Program:exit'(): void {\n for (const reader of readers.values()) {\n if (reader.count < threshold) continue;\n\n context.report({\n data: { count: reader.count },\n messageId: 'separateHooks',\n node: reader.node,\n });\n }\n },\n };\n },\n meta: {\n defaultOptions: [{ threshold: 3 }],\n docs: {\n description: 'One model is read through one selection instead of a hook for each of its fields.',\n },\n messages: {\n separateHooks:\n 'This model is read through {{count}} separate hooks: `useModel(Declaration, (model, { read }) => ...)` selects data and commands in one call, with the same authority and the same statuses (D205, D214).',\n },\n schema: [\n {\n additionalProperties: false,\n properties: {\n threshold: { minimum: 2, type: 'integer' },\n },\n type: 'object',\n },\n ],\n type: 'suggestion',\n },\n name: 'prefer-model-selection',\n});\n\nexport { preferModelSelection };\n"],"names":["FIELD_HOOKS","grantedModelDeclarator","node","parent","calleeName","AST_NODE_TYPES","readIdentifier","name","argument","root","memberRoot","preferModelSelection","createRule","context","threshold","readers","count","identifier","definition","ASTUtils","reader","declarator"],"mappings":"uKAcA,MAAMA,EAAc,IAAI,IAAI,CAAC,aAAc,aAAa,CAAC,EAGzD,SAASC,EAAuBC,EAA6B,CAC3D,KAAM,CAAE,OAAAC,CAAM,EAAKD,EAEnB,GAAI,EAAAE,EAAWF,CAAI,IAAM,YAAcA,EAAK,UAAU,SAAW,IAE7D,EAAAC,EAAO,OAASE,EAAe,oBAAsBF,EAAO,GAAG,OAASE,EAAe,YAI3F,OAAOF,CACT,CAGA,SAASG,EAAeJ,EAA6B,CACnD,MAAMK,EAAOH,EAAWF,CAAI,EACtB,CAACM,CAAQ,EAAIN,EAAK,UAExB,GAAIK,IAAS,QAAa,CAACP,EAAY,IAAIO,CAAI,GAAKC,GAAU,OAASH,EAAe,iBACpF,OAGF,MAAMI,EAAOC,EAAWF,CAAQ,EAEhC,OAAOC,EAAK,OAASJ,EAAe,WAAaI,EAAO,MAC1D,CAEA,MAAME,EAAuBC,EAAgC,CAC3D,OAAOC,EAAS,CAAC,CAAE,UAAAC,CAAS,CAAE,EAAC,CAE7B,MAAMC,EAAU,IAAI,IAEdC,EAASd,GAAuC,CACpD,MAAMe,EAAaX,EAAeJ,CAAI,EAEtC,GAAIe,IAAe,OAAW,OAE9B,KAAM,CAACC,CAAU,EAAIC,EAAS,aAAaN,EAAQ,WAAW,SAASI,CAAU,EAAGA,CAAU,GAAG,MAAQ,CAAA,EACnGG,EAASF,IAAe,OAAY,OAAYH,EAAQ,IAAIG,EAAW,IAAI,EAE7EE,IAAW,SAAWA,EAAO,OAAS,EAC5C,EAEA,MAAO,CACL,eAAelB,EAAI,CACjB,MAAMmB,EAAapB,EAAuBC,CAAI,EAE1CmB,IAAe,OAAWL,EAAMd,CAAI,EACnCa,EAAQ,IAAIM,EAAY,CAAE,MAAO,EAAG,KAAAnB,EAAM,CACjD,EACA,gBAAc,CACZ,UAAWkB,KAAUL,EAAQ,SACvBK,EAAO,MAAQN,GAEnBD,EAAQ,OAAO,CACb,KAAM,CAAE,MAAOO,EAAO,KAAK,EAC3B,UAAW,gBACX,KAAMA,EAAO,IACd,CAAA,CAEL,EAEJ,EACA,KAAM,CACJ,eAAgB,CAAC,CAAE,UAAW,EAAG,EACjC,KAAM,CACJ,YAAa,mFACd,EACD,SAAU,CACR,cACE,2MACH,EACD,OAAQ,CACN,CACE,qBAAsB,GACtB,WAAY,CACV,UAAW,CAAE,QAAS,EAAG,KAAM,SAAS,CACzC,EACD,KAAM,QACP,CACF,EACD,KAAM,YACP,EACD,KAAM,wBACP,CAAA"}
1
+ {"version":3,"file":"prefer-model-selection.js","sources":["../../src/rules/prefer-model-selection.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { calleeName, memberRoot } from '../ast';\nimport { createRule, findVariable } from '../rule';\n\ntype MessageIds = 'separateHooks';\ntype Options = readonly [{ readonly threshold: number }];\n\ntype Reader = {\n count: number;\n readonly node: TSESTree.CallExpression;\n};\n\nconst FIELD_HOOKS = new Set(['useCommand', 'useReadable']);\n\n/** `const order = useModel(OrderModel)` — the granted model, without a selection of its own. */\nfunction grantedModelDeclarator(node: TSESTree.CallExpression): TSESTree.VariableDeclarator | undefined {\n const { parent } = node;\n\n if (calleeName(node) !== 'useModel' || node.arguments.length !== 1) return undefined;\n\n if (parent.type !== AST_NODE_TYPES.VariableDeclarator || parent.id.type !== AST_NODE_TYPES.Identifier) {\n return undefined;\n }\n\n return parent;\n}\n\n/** The identifier a field hook reads its argument from: `useReadable(order.amount)` names `order`. */\nfunction readIdentifier(node: TSESTree.CallExpression): TSESTree.Identifier | undefined {\n const name = calleeName(node);\n const [argument] = node.arguments;\n\n if (name === undefined || !FIELD_HOOKS.has(name) || argument?.type !== AST_NODE_TYPES.MemberExpression) {\n return undefined;\n }\n\n const root = memberRoot(argument);\n\n return root.type === AST_NODE_TYPES.Identifier ? root : undefined;\n}\n\nconst preferModelSelection = createRule<Options, MessageIds>({\n create(context, [{ threshold }]) {\n // Keyed by the declaration a name resolves to, so two components with the same variable name stay apart.\n const readers = new Map<TSESTree.Node, Reader>();\n\n const count = (node: TSESTree.CallExpression): void => {\n const identifier = readIdentifier(node);\n\n if (identifier === undefined) return;\n\n const [definition] = findVariable(context.sourceCode.getScope(identifier), identifier)?.defs ?? [];\n const reader = definition === undefined ? undefined : readers.get(definition.node);\n\n if (reader !== undefined) reader.count += 1;\n };\n\n return {\n CallExpression(node): void {\n const declarator = grantedModelDeclarator(node);\n\n if (declarator === undefined) count(node);\n else readers.set(declarator, { count: 0, node });\n },\n 'Program:exit'(): void {\n for (const reader of readers.values()) {\n if (reader.count < threshold) continue;\n\n context.report({\n data: { count: reader.count },\n messageId: 'separateHooks',\n node: reader.node,\n });\n }\n },\n };\n },\n meta: {\n defaultOptions: [{ threshold: 3 }],\n docs: {\n description: 'One model is read through one selection instead of a hook for each of its fields.',\n },\n messages: {\n separateHooks:\n 'This model is read through {{count}} separate hooks: `useModel(Declaration, (model, { read }) => ...)` selects data and commands in one call, with the same authority and the same statuses (D205, D214).',\n },\n schema: [\n {\n additionalProperties: false,\n properties: {\n threshold: { minimum: 2, type: 'integer' },\n },\n type: 'object',\n },\n ],\n type: 'suggestion',\n },\n name: 'prefer-model-selection',\n});\n\nexport { preferModelSelection };\n"],"names":["FIELD_HOOKS","grantedModelDeclarator","node","parent","calleeName","AST_NODE_TYPES","readIdentifier","name","argument","root","memberRoot","preferModelSelection","createRule","context","threshold","readers","count","identifier","definition","findVariable","reader","declarator"],"mappings":"2KAcA,MAAMA,EAAc,IAAI,IAAI,CAAC,aAAc,aAAa,CAAC,EAGzD,SAASC,EAAuBC,EAA6B,CAC3D,KAAM,CAAE,OAAAC,CAAM,EAAKD,EAEnB,GAAI,EAAAE,EAAWF,CAAI,IAAM,YAAcA,EAAK,UAAU,SAAW,IAE7D,EAAAC,EAAO,OAASE,EAAe,oBAAsBF,EAAO,GAAG,OAASE,EAAe,YAI3F,OAAOF,CACT,CAGA,SAASG,EAAeJ,EAA6B,CACnD,MAAMK,EAAOH,EAAWF,CAAI,EACtB,CAACM,CAAQ,EAAIN,EAAK,UAExB,GAAIK,IAAS,QAAa,CAACP,EAAY,IAAIO,CAAI,GAAKC,GAAU,OAASH,EAAe,iBACpF,OAGF,MAAMI,EAAOC,EAAWF,CAAQ,EAEhC,OAAOC,EAAK,OAASJ,EAAe,WAAaI,EAAO,MAC1D,CAEA,MAAME,EAAuBC,EAAgC,CAC3D,OAAOC,EAAS,CAAC,CAAE,UAAAC,CAAS,CAAE,EAAC,CAE7B,MAAMC,EAAU,IAAI,IAEdC,EAASd,GAAuC,CACpD,MAAMe,EAAaX,EAAeJ,CAAI,EAEtC,GAAIe,IAAe,OAAW,OAE9B,KAAM,CAACC,CAAU,EAAIC,EAAaN,EAAQ,WAAW,SAASI,CAAU,EAAGA,CAAU,GAAG,MAAQ,CAAA,EAC1FG,EAASF,IAAe,OAAY,OAAYH,EAAQ,IAAIG,EAAW,IAAI,EAE7EE,IAAW,SAAWA,EAAO,OAAS,EAC5C,EAEA,MAAO,CACL,eAAelB,EAAI,CACjB,MAAMmB,EAAapB,EAAuBC,CAAI,EAE1CmB,IAAe,OAAWL,EAAMd,CAAI,EACnCa,EAAQ,IAAIM,EAAY,CAAE,MAAO,EAAG,KAAAnB,EAAM,CACjD,EACA,gBAAc,CACZ,UAAWkB,KAAUL,EAAQ,SACvBK,EAAO,MAAQN,GAEnBD,EAAQ,OAAO,CACb,KAAM,CAAE,MAAOO,EAAO,KAAK,EAC3B,UAAW,gBACX,KAAMA,EAAO,IACd,CAAA,CAEL,EAEJ,EACA,KAAM,CACJ,eAAgB,CAAC,CAAE,UAAW,EAAG,EACjC,KAAM,CACJ,YAAa,mFACd,EACD,SAAU,CACR,cACE,2MACH,EACD,OAAQ,CACN,CACE,qBAAsB,GACtB,WAAY,CACV,UAAW,CAAE,QAAS,EAAG,KAAM,SAAS,CACzC,EACD,KAAM,QACP,CACF,EACD,KAAM,YACP,EACD,KAAM,wBACP,CAAA"}
@@ -0,0 +1,2 @@
1
+ declare const preferRead: import("../rule.js").RuleModule<"preferRead", []>;
2
+ export { preferRead };
@@ -0,0 +1,2 @@
1
+ import{AST_NODE_TYPES as a}from"@typescript-eslint/types";import{DefinitionType as u}from"@typescript-eslint/scope-manager";import{isFunctionLike as C}from"../ast.js";import{methodPath as m,isModelContextFunction as c,contextName as k}from"../declaration-ingress.js";import{declaredEffect as R}from"../effect-declarations.js";import{unwrap as s,isFunction as l,variableOf as o,hasWritesBeyondDefinition as j,isApi as y,definitionOf as x}from"../model-bindings.js";import{createRule as w,findVariable as g}from"../rule.js";function M(n,e,t,r){const i=e.params[t];if(i?.type===a.Identifier){const f=o(n,i);return f===null||g(n.getScope(r),i.name)!==f||f.references.some(p=>p.isWrite()&&p.identifier!==i)?void 0:`${i.name}.read`}if(i?.type===a.ObjectPattern)for(const f of i.properties){if(f.type!==a.Property||f.computed||f.key.type!==a.Identifier||f.key.name!=="read"||f.value.type!==a.Identifier)continue;const p=o(n,f.value);if(p!==null&&g(n.getScope(r),f.value.name)===p&&!p.references.some(d=>d.isWrite()&&d.identifier!==f.value))return f.value.name}}function b(n,e){return e?.type===a.TSIntersectionType||e?.type===a.TSUnionType?e.types.some(t=>b(n,t)):e?.type===a.TSTypeReference&&y(n,e.typeName,"@opetope/core","ModelStateWriter",!0)}function O(n,e){const t=e.params.findIndex(r=>r.type!==a.Identifier&&r.type!==a.ObjectPattern?!1:b(n,r.typeAnnotation?.typeAnnotation));return t===-1?void 0:t}function I(n){return!n.computed&&n.key.type===a.Identifier&&n.key.name==="timers"}function h(n,e,t){const r=e.parent;if(r.type!==a.CallExpression)return;const i=m(n,r.callee);if(i===void 0||!c(n,i.fn))return;if(i.method==="event"&&r.arguments[2]===e)return 0;const f=i.method==="command"?0:i.method==="effect"||i.method==="event"||t&&(i.method==="timers.delay"||i.method==="timers.interval")?1:void 0;return f!==void 0&&r.arguments[f]===e?i.method==="effect"||i.method==="event"?1:0:void 0}function v(n,e){const t=e.parent;if(t===void 0||!l(t))return!1;const r=h(n,t,!1);return r!==void 0&&t.params[r]===e}function E(n,e){const t=k(n,e);if(t!==void 0&&c(n,t))return!0;const r=x(n,e);return r?.type===u.Parameter&&v(n,r.name)}function T(n,e){if(e.type===a.MemberExpression)return!e.computed&&e.property.type===a.Identifier&&e.property.name==="timers"&&e.object.type===a.Identifier&&E(n,e.object);if(e.type!==a.Identifier)return!1;const t=x(n,e),r=t?.name.parent;if(t?.type===u.Parameter)return r?.type===a.Property&&I(r)&&v(n,r.parent);if(t?.type!==u.Variable||t.parent.kind!=="const")return!1;const i=t.node.init===null?void 0:s(t.node.init);return t.node.id===t.name?i?.type===a.MemberExpression&&T(n,i):r?.type===a.Property&&r.parent===t.node.id&&I(r)&&i?.type===a.Identifier&&E(n,i)}function W(n,e,t){const{callee:r}=e;return e.arguments[1]===t&&r.type===a.MemberExpression&&!r.computed&&r.property.type===a.Identifier&&(r.property.name==="delay"||r.property.name==="interval")&&T(n,r.object)}function U(n,e){const t=h(n,e,!0);return t!==void 0?t:e.parent.type===a.CallExpression&&W(n,e.parent,e)?0:void 0}function D(n,e){const t=n.getAncestors(e).reverse().find(l);if(t===void 0)return;const r=O(n,t)??U(n,t);return r===void 0?void 0:{fn:t,index:r,parameter:t.params[r]}}function F(n,e,t){const{fn:r,index:i,parameter:f}=e;return(f?.type===a.Identifier||f?.type===a.ObjectPattern)&&(f.optional||f.typeAnnotation?.typeAnnotation.type===a.TSUnionType)?null:M(n,r,i,t)??null}function V(n,e){return n.range[0]>=e.range[0]&&n.range[1]<=e.range[1]}function S(n){const{callee:e}=n,t=e.type===a.Identifier?e.name:e.type===a.MemberExpression&&!e.computed&&e.property.type===a.Identifier?e.property.name:void 0,[r,i]=n.arguments;if(t!=="update"||n.arguments.length!==2||r===void 0||i===void 0||i.type===a.SpreadElement||C(s(i)))return;const f=s(r);return f.type===a.Identifier||f.type===a.MemberExpression?f:void 0}function P(n,e,t){if(e.type!==a.Identifier)return t.type===a.MemberExpression&&n.getText(t)===n.getText(e);if(t.type!==a.Identifier)return!1;const r=o(n,e),i=o(n,t);return r===null&&i===null?e.name===t.name:r===i}function N(n,e,t){let r=e;for(const i of n.getAncestors(e).reverse()){if(i.type===a.CallExpression&&i.arguments[1]===r){const f=S(i);if(f!==void 0&&P(n,f,t))return!0}r=i}return!1}function $(n,e,t){return e.references.some(({identifier:r})=>n.getAncestors(r).some(i=>{if(i.type!==a.CallExpression)return!1;const f=S(i),p=i.arguments[1];return f!==void 0&&p!==void 0&&V(r,p)&&P(n,f,t)}))}function _(n,e,t){let r=e;for(;r.parent!==void 0&&s(r.parent)===e;)r=r.parent;const i=r.parent;return i?.type!==a.VariableDeclarator||i.init!==r?!0:(i.id.type===a.Identifier?[i.id]:i.id.type===a.ObjectPattern?i.id.properties.map(p=>p.type===a.Property?p.value:p.argument):[i.id]).every(p=>{const d=p.type===a.Identifier?o(n,p):null;return d!==null&&!$(n,d,t)})}function z(n,e){if(e.type!==a.Identifier)return!1;const t=o(n,e);if(t?.defs.length!==1)return!1;const r=t.defs[0];if(r?.type!==u.Variable||r.parent.kind!=="const"||j(t,r))return!1;const i=r.node.init===null?void 0:s(r.node.init);if(i?.type!==a.CallExpression)return!1;const f=m(n,i.callee);return f?.method==="state"&&c(n,f.fn)?!0:y(n,i.callee,"@opetope/core","derive",!0)}function B(n,e,t){const r=n.getAncestors(e).reverse().find(l);if(r?.parent.type!==a.CallExpression||t.type!==a.Identifier)return!1;const i=R(n,r.parent);return i?.run===r&&i.from.type===a.Identifier&&o(n,i.from)===o(n,t)&&i.from.name===t.name}const L=w({create(n){const e=n.sourceCode;return{CallExpression(t){if(t.arguments.length!==0||t.callee.type!==a.MemberExpression)return;const r=t.callee;if(r.computed||r.property.type!==a.Identifier||r.property.name!=="getSnapshot"||B(e,t,r.object))return;const i=D(e,t);if(i===void 0)return;const f=r.object;if(N(e,t,s(f)))return;const p=F(e,i,t),d=p!==null&&!t.optional&&!r.optional&&f.type===a.Identifier&&z(e,f)&&_(e,t,f)&&e.getCommentsInside(t).length===0&&e.getText(t).slice(f.range[1]-t.range[0],r.property.range[0]-t.range[0]).trim()===".";n.report({messageId:"preferRead",node:t,...d?{fix:A=>A.replaceText(t,`${p}(${e.getText(f)})`)}:{}})}}},meta:{docs:{description:"Use the passive execution reader for a snapshot read in an execution context."},fixable:"code",messages:{preferRead:"For a Readable, use the execution context `read(source)` here. A non-Readable host API may keep `getSnapshot()`; suppress this warning for that call."},schema:[],type:"suggestion"},name:"prefer-read"});export{L as preferRead};
2
+ //# sourceMappingURL=prefer-read.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-read.js","sources":["../../src/rules/prefer-read.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\nimport { DefinitionType } from '@typescript-eslint/scope-manager';\n\nimport { isFunctionLike } from '../ast';\nimport { contextName, isModelContextFunction, methodPath } from '../declaration-ingress';\nimport { declaredEffect } from '../effect-declarations';\nimport { definitionOf, hasWritesBeyondDefinition, isApi, isFunction, unwrap, variableOf } from '../model-bindings';\nimport type { FunctionNode } from '../model-bindings';\nimport type { SourceCode, Variable } from '../rule';\nimport { createRule, findVariable } from '../rule';\n\ntype MessageIds = 'preferRead';\n\n/** The writer is usable by name, or its `read` member was bound in the parameter pattern. */\n// eslint-disable-next-line complexity -- Every binding and write check is needed before proposing a fix.\nfunction reader(source: SourceCode, fn: FunctionNode, index: number, at: TSESTree.Node): string | undefined {\n const parameter = fn.params[index];\n if (parameter?.type === AST_NODE_TYPES.Identifier) {\n const variable = variableOf(source, parameter);\n if (\n variable === null ||\n findVariable(source.getScope(at), parameter.name) !== variable ||\n variable.references.some(ref => ref.isWrite() && ref.identifier !== parameter)\n )\n return undefined;\n return `${parameter.name}.read`;\n }\n\n if (parameter?.type !== AST_NODE_TYPES.ObjectPattern) return undefined;\n for (const property of parameter.properties) {\n if (\n property.type !== AST_NODE_TYPES.Property ||\n property.computed ||\n property.key.type !== AST_NODE_TYPES.Identifier ||\n property.key.name !== 'read' ||\n property.value.type !== AST_NODE_TYPES.Identifier\n )\n continue;\n\n const binding = variableOf(source, property.value);\n if (\n binding !== null &&\n findVariable(source.getScope(at), property.value.name) === binding &&\n !binding.references.some(ref => ref.isWrite() && ref.identifier !== property.value)\n )\n return property.value.name;\n }\n return undefined;\n}\n\nfunction containsWriter(source: SourceCode, annotation: TSESTree.TypeNode | undefined): boolean {\n if (annotation?.type === AST_NODE_TYPES.TSIntersectionType || annotation?.type === AST_NODE_TYPES.TSUnionType)\n return annotation.types.some(type => containsWriter(source, type));\n return (\n annotation?.type === AST_NODE_TYPES.TSTypeReference &&\n isApi(source, annotation.typeName, '@opetope/core', 'ModelStateWriter', true)\n );\n}\n\nfunction typedWriter(source: SourceCode, fn: FunctionNode): number | undefined {\n const index = fn.params.findIndex(parameter => {\n if (parameter.type !== AST_NODE_TYPES.Identifier && parameter.type !== AST_NODE_TYPES.ObjectPattern) return false;\n return containsWriter(source, parameter.typeAnnotation?.typeAnnotation);\n });\n return index === -1 ? undefined : index;\n}\n\nfunction timersOf(property: TSESTree.Property): boolean {\n return !property.computed && property.key.type === AST_NODE_TYPES.Identifier && property.key.name === 'timers';\n}\n\n/** A callback a model context method takes, and the position of its writer; model timers only when asked. */\n// eslint-disable-next-line complexity -- The method and callback position must both prove the writer.\nfunction contextCallback(source: SourceCode, fn: FunctionNode, timers: boolean): number | undefined {\n const parent = fn.parent;\n if (parent.type !== AST_NODE_TYPES.CallExpression) return undefined;\n const named = methodPath(source, parent.callee);\n if (named === undefined || !isModelContextFunction(source, named.fn)) return undefined;\n if (named.method === 'event' && parent.arguments[2] === fn) return 0;\n const index =\n named.method === 'command'\n ? 0\n : named.method === 'effect' ||\n named.method === 'event' ||\n (timers && (named.method === 'timers.delay' || named.method === 'timers.interval'))\n ? 1\n : undefined;\n return index !== undefined && parent.arguments[index] === fn\n ? named.method === 'effect' || named.method === 'event'\n ? 1\n : 0\n : undefined;\n}\n\n/** The parameter, or its pattern, at the context position of a command, effect or event callback. */\nfunction executionPattern(source: SourceCode, parameter: TSESTree.Node): boolean {\n const fn = parameter.parent;\n if (fn === undefined || !isFunction(fn)) return false;\n const index = contextCallback(source, fn, false);\n return index !== undefined && fn.params[index] === parameter;\n}\n\n/** A model context, or the context an execution callback receives: every one of them carries model timers. */\nfunction ownsTimers(source: SourceCode, name: TSESTree.Identifier): boolean {\n const context = contextName(source, name);\n if (context !== undefined && isModelContextFunction(source, context)) return true;\n const definition = definitionOf(source, name);\n return definition?.type === DefinitionType.Parameter && executionPattern(source, definition.name);\n}\n\n/** `timers` bound by a parameter pattern or a `const` to a context that owns timers, or read off one. */\n// eslint-disable-next-line complexity -- A parameter pattern, a `const` and a member read are the three bindings.\nfunction ownedTimers(source: SourceCode, holder: TSESTree.Node): boolean {\n if (holder.type === AST_NODE_TYPES.MemberExpression)\n return (\n !holder.computed &&\n holder.property.type === AST_NODE_TYPES.Identifier &&\n holder.property.name === 'timers' &&\n holder.object.type === AST_NODE_TYPES.Identifier &&\n ownsTimers(source, holder.object)\n );\n if (holder.type !== AST_NODE_TYPES.Identifier) return false;\n const definition = definitionOf(source, holder);\n const property = definition?.name.parent;\n if (definition?.type === DefinitionType.Parameter)\n return (\n property?.type === AST_NODE_TYPES.Property && timersOf(property) && executionPattern(source, property.parent)\n );\n if (definition?.type !== DefinitionType.Variable || definition.parent.kind !== 'const') return false;\n const init = definition.node.init === null ? undefined : unwrap(definition.node.init);\n if (definition.node.id === definition.name)\n return init?.type === AST_NODE_TYPES.MemberExpression && ownedTimers(source, init);\n return (\n property?.type === AST_NODE_TYPES.Property &&\n property.parent === definition.node.id &&\n timersOf(property) &&\n init?.type === AST_NODE_TYPES.Identifier &&\n ownsTimers(source, init)\n );\n}\n\n/**\n * The run callback of `timers.delay` or `timers.interval` on timers a model context or an execution context owns,\n * however the timers were reached: it is handed a writer as well (D518).\n */\nfunction timerCallback(source: SourceCode, call: TSESTree.CallExpression, fn: FunctionNode): boolean {\n const { callee } = call;\n return (\n call.arguments[1] === fn &&\n callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.property.type === AST_NODE_TYPES.Identifier &&\n (callee.property.name === 'delay' || callee.property.name === 'interval') &&\n ownedTimers(source, callee.object)\n );\n}\n\nfunction callbackWriter(source: SourceCode, fn: FunctionNode): number | undefined {\n const index = contextCallback(source, fn, true);\n if (index !== undefined) return index;\n return fn.parent.type === AST_NODE_TYPES.CallExpression && timerCallback(source, fn.parent, fn) ? 0 : undefined;\n}\n\ntype Execution = {\n readonly fn: FunctionNode;\n readonly index: number;\n readonly parameter: TSESTree.Parameter | undefined;\n};\n\n/** An execution callback is the nearest function; a later callback cannot borrow its writer. */\nfunction executionOf(source: SourceCode, node: TSESTree.Node): Execution | undefined {\n const fn = source.getAncestors(node).reverse().find(isFunction);\n if (fn === undefined) return undefined;\n const index = typedWriter(source, fn) ?? callbackWriter(source, fn);\n return index === undefined ? undefined : { fn, index, parameter: fn.params[index] };\n}\n\n/** The reader a fix may call, or `null` when the writer is optional, a union or not bound by a usable name. */\n// eslint-disable-next-line complexity -- Optional and union writer annotations need a diagnostic without a fix.\nfunction executionReader(source: SourceCode, execution: Execution, node: TSESTree.Node): string | null {\n const { fn, index, parameter } = execution;\n if (\n (parameter?.type === AST_NODE_TYPES.Identifier || parameter?.type === AST_NODE_TYPES.ObjectPattern) &&\n (parameter.optional || parameter.typeAnnotation?.typeAnnotation.type === AST_NODE_TYPES.TSUnionType)\n )\n return null;\n return reader(source, fn, index, node) ?? null;\n}\n\nfunction within(node: TSESTree.Node, outer: TSESTree.Node): boolean {\n return node.range[0] >= outer.range[0] && node.range[1] <= outer.range[1];\n}\n\n/**\n * The state `update(state, value)` or `x.update(state, value)` writes, in the shape `no-snapshot-in-update` reads:\n * any writer, two arguments, a name or a member path first, and a value that is not already an updater.\n */\n// eslint-disable-next-line complexity -- Each condition mirrors one refusal of the write rule's own reading.\nfunction writtenState(call: TSESTree.CallExpression): TSESTree.Node | undefined {\n const { callee } = call;\n const named =\n callee.type === AST_NODE_TYPES.Identifier\n ? callee.name\n : callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.property.type === AST_NODE_TYPES.Identifier\n ? callee.property.name\n : undefined;\n const [first, value] = call.arguments;\n if (named !== 'update' || call.arguments.length !== 2 || first === undefined || value === undefined) return undefined;\n if (value.type === AST_NODE_TYPES.SpreadElement || isFunctionLike(unwrap(value))) return undefined;\n const state = unwrap(first);\n return state.type === AST_NODE_TYPES.Identifier || state.type === AST_NODE_TYPES.MemberExpression ? state : undefined;\n}\n\n/** The same readable as the write rule compares it: a name by its binding, a member path by its spelling. */\nfunction sameState(source: SourceCode, state: TSESTree.Node, receiver: TSESTree.Node): boolean {\n if (state.type !== AST_NODE_TYPES.Identifier)\n return receiver.type === AST_NODE_TYPES.MemberExpression && source.getText(receiver) === source.getText(state);\n if (receiver.type !== AST_NODE_TYPES.Identifier) return false;\n const named = variableOf(source, state);\n const read = variableOf(source, receiver);\n return named === null && read === null ? state.name === receiver.name : named === read;\n}\n\n/**\n * A read written into the value of an update of the same state is the write `no-snapshot-in-update` reports, whoever\n * the writer is — this execution's, a timer's or a foreign one. That rule looks for `getSnapshot()`, so rewriting the\n * read to `read(state)` would hide the write from it for good; the whole case is left to it.\n */\nfunction ownedUpdate(source: SourceCode, node: TSESTree.Node, receiver: TSESTree.Node): boolean {\n let child = node;\n for (const ancestor of source.getAncestors(node).reverse()) {\n if (ancestor.type === AST_NODE_TYPES.CallExpression && ancestor.arguments[1] === child) {\n const state = writtenState(ancestor);\n if (state !== undefined && sameState(source, state, receiver)) return true;\n }\n child = ancestor;\n }\n return false;\n}\n\n/** A name this read was given is later written into an update of the same state, which `no-snapshot-in-update` needs. */\nfunction feedsUpdate(source: SourceCode, variable: Variable, receiver: TSESTree.Identifier): boolean {\n return variable.references.some(({ identifier }) =>\n source.getAncestors(identifier).some(ancestor => {\n if (ancestor.type !== AST_NODE_TYPES.CallExpression) return false;\n const state = writtenState(ancestor);\n const value = ancestor.arguments[1];\n return (\n state !== undefined && value !== undefined && within(identifier, value) && sameState(source, state, receiver)\n );\n }),\n );\n}\n\n/**\n * An initializer is rewritten only when none of the names it gives the read reaches an update of the same state:\n * that alias is how `no-snapshot-in-update` sees a stale write, and `read(state)` would hide it.\n */\n// eslint-disable-next-line complexity -- Every name a declarator can bind must be proved not to feed a write.\nfunction safeInitializer(source: SourceCode, node: TSESTree.Node, receiver: TSESTree.Identifier): boolean {\n let value: TSESTree.Node = node;\n while (value.parent !== undefined && unwrap(value.parent) === node) value = value.parent;\n const declarator = value.parent;\n if (declarator?.type !== AST_NODE_TYPES.VariableDeclarator || declarator.init !== value) return true;\n const names =\n declarator.id.type === AST_NODE_TYPES.Identifier\n ? [declarator.id]\n : declarator.id.type === AST_NODE_TYPES.ObjectPattern\n ? declarator.id.properties.map(property =>\n property.type === AST_NODE_TYPES.Property ? property.value : property.argument,\n )\n : [declarator.id];\n return names.every(name => {\n const variable = name.type === AST_NODE_TYPES.Identifier ? variableOf(source, name) : null;\n return variable !== null && !feedsUpdate(source, variable, receiver);\n });\n}\n\n/** Only a stable local result of a known plain-readable creator licenses a semantic autofix. */\n// eslint-disable-next-line complexity -- Every declaration and creator condition is part of the autofix proof.\nfunction plainReadable(source: SourceCode, receiver: TSESTree.Node): boolean {\n if (receiver.type !== AST_NODE_TYPES.Identifier) return false;\n const variable = variableOf(source, receiver);\n if (variable?.defs.length !== 1) return false;\n const definition = variable.defs[0];\n if (\n definition?.type !== DefinitionType.Variable ||\n definition.parent.kind !== 'const' ||\n hasWritesBeyondDefinition(variable, definition)\n )\n return false;\n const init = definition.node.init === null ? undefined : unwrap(definition.node.init);\n if (init?.type !== AST_NODE_TYPES.CallExpression) return false;\n const named = methodPath(source, init.callee);\n if (named?.method === 'state' && isModelContextFunction(source, named.fn)) return true;\n return isApi(source, init.callee, '@opetope/core', 'derive', true);\n}\n\n// eslint-disable-next-line complexity -- Same-source effect reads belong to prefer-effect-current.\nfunction effectSourceRead(source: SourceCode, node: TSESTree.CallExpression, receiver: TSESTree.Node): boolean {\n const fn = source.getAncestors(node).reverse().find(isFunction);\n if (fn?.parent.type !== AST_NODE_TYPES.CallExpression || receiver.type !== AST_NODE_TYPES.Identifier) return false;\n const effect = declaredEffect(source, fn.parent);\n return (\n effect?.run === fn &&\n effect.from.type === AST_NODE_TYPES.Identifier &&\n variableOf(source, effect.from) === variableOf(source, receiver) &&\n effect.from.name === receiver.name\n );\n}\n\nconst preferRead = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n return {\n // eslint-disable-next-line complexity -- Report broadly, but fix only the syntax proven safe above.\n CallExpression(node): void {\n if (node.arguments.length !== 0 || node.callee.type !== AST_NODE_TYPES.MemberExpression) return;\n const callee = node.callee;\n if (\n callee.computed ||\n callee.property.type !== AST_NODE_TYPES.Identifier ||\n callee.property.name !== 'getSnapshot'\n )\n return;\n if (effectSourceRead(source, node, callee.object)) return;\n const execution = executionOf(source, node);\n if (execution === undefined) return;\n const receiver = callee.object;\n if (ownedUpdate(source, node, unwrap(receiver))) return;\n const writer = executionReader(source, execution, node);\n const fixable =\n writer !== null &&\n !node.optional &&\n !callee.optional &&\n receiver.type === AST_NODE_TYPES.Identifier &&\n plainReadable(source, receiver) &&\n safeInitializer(source, node, receiver) &&\n source.getCommentsInside(node).length === 0 &&\n source\n .getText(node)\n .slice(receiver.range[1] - node.range[0], callee.property.range[0] - node.range[0])\n .trim() === '.';\n context.report({\n messageId: 'preferRead',\n node,\n ...(fixable ? { fix: fixer => fixer.replaceText(node, `${writer}(${source.getText(receiver)})`) } : {}),\n });\n },\n };\n },\n meta: {\n docs: { description: 'Use the passive execution reader for a snapshot read in an execution context.' },\n fixable: 'code',\n messages: {\n preferRead:\n 'For a Readable, use the execution context `read(source)` here. A non-Readable host API may keep `getSnapshot()`; suppress this warning for that call.',\n },\n schema: [],\n type: 'suggestion',\n },\n name: 'prefer-read',\n});\n\nexport { preferRead };\n"],"names":["reader","source","fn","index","at","parameter","AST_NODE_TYPES","variable","variableOf","findVariable","ref","property","binding","containsWriter","annotation","type","isApi","typedWriter","timersOf","contextCallback","timers","parent","named","methodPath","isModelContextFunction","executionPattern","isFunction","ownsTimers","name","context","contextName","definition","definitionOf","DefinitionType","ownedTimers","holder","init","unwrap","timerCallback","call","callee","callbackWriter","executionOf","node","executionReader","execution","within","outer","writtenState","first","value","isFunctionLike","state","sameState","receiver","read","ownedUpdate","child","ancestor","feedsUpdate","identifier","safeInitializer","declarator","plainReadable","hasWritesBeyondDefinition","effectSourceRead","effect","declaredEffect","preferRead","createRule","writer","fixable","fixer"],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * D494: `enabled` hides a contribution while its feature stays open — its models, their work and their Resources
3
+ * keep running (D220) — and `when` closes the feature itself. A feature that hands nothing to other features and
4
+ * hides every contribution behind one and the same `enabled` keeps all of that running while nothing of it is shown.
5
+ */
6
+ declare const preferWhenOverEnabled: import("../rule.js").RuleModule<"preferWhen", []>;
7
+ export { preferWhenOverEnabled };
@@ -0,0 +1,2 @@
1
+ import{AST_NODE_TYPES as i}from"@typescript-eslint/types";import{propertyName as a,findProperty as c,boundNames as w}from"../ast.js";import{isApi as v,isFunction as l,variableOf as f,localValue as m,referencesWithin as g}from"../model-bindings.js";import{createRule as O}from"../rule.js";const E=new Set(["pipe","register","slot"]);function x(t){const{body:n}=t;let e=n;if(n.type===i.BlockStatement){const[r]=n.body;e=n.body.length===1&&r?.type===i.ReturnStatement?r.argument:void 0}return e?.type===i.ObjectExpression?e:void 0}function I(t,n){const e=t.properties.find(r=>r.type===i.Property&&r.value.type===i.Identifier&&r.value.name===n);return e===void 0?void 0:a(e)}function S(t,n){const{object:e,property:r}=n;if(!(n.computed||e.type!==i.Identifier||e.name!==t.name))return r.type===i.Identifier?r.name:void 0}function j(t,n){const[e]=t.params;if(n.type===i.Identifier&&e?.type===i.ObjectPattern)return I(e,n.name);if(n.type===i.MemberExpression&&e?.type===i.Identifier)return S(e,n)}function k(t,n){const e=n.type===i.Property?n.value:void 0;if(e?.type===i.CallExpression)return E.has(j(t,e.callee)??"")?e:void 0}function P(t,n){const e=n?.arguments[2],r=e===void 0?void 0:m(t,e);return r?.type===i.ObjectExpression?c(r,"enabled"):void 0}function C(t,n){const e=n.properties.flatMap(r=>r.type===i.Property&&a(r)==="own"?w(r.value):[]);return new Set(e.flatMap(r=>f(t,r)??[]))}function h(t,n){const[e]=n.params;return e?.type===i.Identifier?{own:new Set,whole:f(t,e)}:{own:e?.type===i.ObjectPattern?C(t,e):new Set,whole:null}}function R(t){const{parent:n}=t;return n.type===i.MemberExpression&&n.object===t&&!n.computed&&n.property.type===i.Identifier&&n.property.name==="own"}function B(t,n,e){const r=m(t,e),d=r===e?[e]:[e,r],s=[n,...d.filter(l).map(o=>h(t,o))];return d.some(o=>g(t,o,o).some(p=>{const u=f(t,p);return u===null?!1:s.some(({own:y,whole:b})=>y.has(u)||b===u&&R(p))}))}function M(t,n,e,r){const d=P(t,k(n,e));if(d===void 0)return;const s=t.getTokens(d.value).map(o=>o.value);return s.length===0||B(t,r,d.value)?void 0:s.join(" ")}function N(t,n,e){const r=h(t,n),d=e.properties.map(o=>M(t,n,o,r)),[s]=d;return s!==void 0&&d.every(o=>o===s)?s:void 0}function T(t){const n=t.properties.map(e=>a(e));if(!(n.includes(void 0)||n.includes("exports")||n.includes("body")))return c(t,"provides")}function W(t){const n=t.arguments[1];if(n?.type!==i.ObjectExpression)return;const e=T(n);if(e===void 0||!l(e.value))return;const r=x(e.value);return r===void 0?void 0:{fn:e.value,key:e.key,record:r}}const A=O({create(t){const n=t.sourceCode;return{CallExpression(e){const r=W(e);r===void 0||N(n,r.fn,r.record)===void 0||v(n,e.callee,"@opetope/runtime","defineFeature")&&t.report({messageId:"preferWhen",node:r.key})}}},meta:{docs:{description:"A feature that exports nothing and hides every contribution behind one `enabled` is gated by `when`."},messages:{preferWhen:"Every contribution of this feature is hidden by the same `enabled`, and the feature exports nothing, so its models, their work and their Resources stay open while nothing of it is shown. Gate the feature itself: move the fact into a Condition and list it in `when`, which closes the feature while it is false; if its models must work while nothing is shown, move the contributions into a feature of their own that `when` gates (D220, D494)."},schema:[],type:"suggestion"},name:"prefer-when-over-enabled"});export{A as preferWhenOverEnabled};
2
+ //# sourceMappingURL=prefer-when-over-enabled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-when-over-enabled.js","sources":["../../src/rules/prefer-when-over-enabled.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { boundNames, findProperty, propertyName } from '../ast';\nimport type { FunctionNode } from '../model-bindings';\nimport { isApi, isFunction, localValue, referencesWithin, variableOf } from '../model-bindings';\nimport type { SourceCode, Variable } from '../rule';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'preferWhen';\n\n/** The builders of `provides` whose entries are contributions a host shows, and so carry `enabled` (D220, D411). */\nconst CONTRIBUTIONS = new Set(['pipe', 'register', 'slot']);\n\n/** The record a `provides` callback answers: its expression body, or the one `return` of its block. */\nfunction returnedRecord(fn: FunctionNode): TSESTree.ObjectExpression | undefined {\n const { body } = fn;\n let returned: TSESTree.Node | null | undefined = body;\n\n if (body.type === AST_NODE_TYPES.BlockStatement) {\n const [only] = body.body;\n\n returned = body.body.length === 1 && only?.type === AST_NODE_TYPES.ReturnStatement ? only.argument : undefined;\n }\n\n return returned?.type === AST_NODE_TYPES.ObjectExpression ? returned : undefined;\n}\n\n/** `({ slot, register: add }) => …`: the builder a name was taken from the context under. */\nfunction destructuredBuilder(pattern: TSESTree.ObjectPattern, name: string): string | undefined {\n const property = pattern.properties.find(\n entry =>\n entry.type === AST_NODE_TYPES.Property &&\n entry.value.type === AST_NODE_TYPES.Identifier &&\n entry.value.name === name,\n );\n\n return property === undefined ? undefined : propertyName(property);\n}\n\n/** `context => context.slot(…)`: the builder read off the context taken whole. */\nfunction namedBuilder(parameter: TSESTree.Identifier, callee: TSESTree.MemberExpression): string | undefined {\n const { object, property } = callee;\n\n if (callee.computed || object.type !== AST_NODE_TYPES.Identifier || object.name !== parameter.name) return undefined;\n\n return property.type === AST_NODE_TYPES.Identifier ? property.name : undefined;\n}\n\n/** The contribution builder a callee names: `slot` taken apart from the context, or `context.slot` read off it. */\nfunction builderOf(fn: FunctionNode, callee: TSESTree.Node): string | undefined {\n const [parameter] = fn.params;\n\n if (callee.type === AST_NODE_TYPES.Identifier && parameter?.type === AST_NODE_TYPES.ObjectPattern) {\n return destructuredBuilder(parameter, callee.name);\n }\n\n if (callee.type === AST_NODE_TYPES.MemberExpression && parameter?.type === AST_NODE_TYPES.Identifier) {\n return namedBuilder(parameter, callee);\n }\n\n return undefined;\n}\n\n/** The entry as a call of `slot`, `pipe` or `register`, or `undefined` for anything else. */\nfunction contributionCall(fn: FunctionNode, entry: TSESTree.ObjectLiteralElement): TSESTree.CallExpression | undefined {\n const call = entry.type === AST_NODE_TYPES.Property ? entry.value : undefined;\n\n if (call?.type !== AST_NODE_TYPES.CallExpression) return undefined;\n\n return CONTRIBUTIONS.has(builderOf(fn, call.callee) ?? '') ? call : undefined;\n}\n\n/** The `enabled` of a contribution's options, written inline or as a record of this module. */\nfunction enabledOf(source: SourceCode, call: TSESTree.CallExpression | undefined): TSESTree.Property | undefined {\n const written = call?.arguments[2];\n const options = written === undefined ? undefined : localValue(source, written);\n\n return options?.type === AST_NODE_TYPES.ObjectExpression ? findProperty(options, 'enabled') : undefined;\n}\n\n/**\n * How a function reaches the feature's own models through the context it takes: the variables its parameter takes\n * apart of `own` — `({ own: { auth } })` binds `auth`, `({ own })` binds `own` — or the context itself, taken whole,\n * whose `.own` it reads.\n */\ntype OwnBindings = { readonly own: ReadonlySet<Variable>; readonly whole: Variable | null };\n\n/** The variables a pattern of the context takes apart of its `own`. */\nfunction ownVariables(source: SourceCode, pattern: TSESTree.ObjectPattern): Set<Variable> {\n const names = pattern.properties.flatMap(property =>\n property.type === AST_NODE_TYPES.Property && propertyName(property) === 'own' ? boundNames(property.value) : [],\n );\n\n return new Set(names.flatMap(name => variableOf(source, name) ?? []));\n}\n\nfunction ownBindings(source: SourceCode, fn: FunctionNode): OwnBindings {\n const [parameter] = fn.params;\n\n if (parameter?.type === AST_NODE_TYPES.Identifier) return { own: new Set(), whole: variableOf(source, parameter) };\n\n const own = parameter?.type === AST_NODE_TYPES.ObjectPattern ? ownVariables(source, parameter) : new Set<Variable>();\n\n return { own, whole: null };\n}\n\n/** `context.own` read off a context taken whole: the identifier is the object of a plain `.own`. */\nfunction readsOwnMember(identifier: TSESTree.Identifier): boolean {\n const { parent } = identifier;\n\n return (\n parent.type === AST_NODE_TYPES.MemberExpression &&\n parent.object === identifier &&\n !parent.computed &&\n parent.property.type === AST_NODE_TYPES.Identifier &&\n parent.property.name === 'own'\n );\n}\n\n/**\n * Whether a gate reads the feature's own models. The references of the gate — and of the function of this module a\n * name of it holds — are resolved to their variables and compared with what the `provides` callback and the gate\n * itself take of `own`; a key, a member after `.` and a name that only spells the same are not references.\n */\nfunction readsOwn(source: SourceCode, provides: OwnBindings, gate: TSESTree.Node): boolean {\n const held = localValue(source, gate);\n const nodes = held === gate ? [gate] : [gate, held];\n const bindings = [provides, ...nodes.filter(isFunction).map(fn => ownBindings(source, fn))];\n\n return nodes.some(node =>\n referencesWithin(source, node, node).some(identifier => {\n const variable = variableOf(source, identifier);\n\n if (variable === null) return false;\n\n return bindings.some(({ own, whole }) => own.has(variable) || (whole === variable && readsOwnMember(identifier)));\n }),\n );\n}\n\n/**\n * The `enabled` of one entry, spelled as tokens so that two copies of one predicate compare equal however they were\n * formatted — or `undefined` when the entry is not a contribution, or is shown whatever happens. A gate that reads\n * the feature's own models, there or in the name of this module it is held in, cannot become its `when`: they exist\n * only while the feature is open.\n */\nfunction gateOf(\n source: SourceCode,\n fn: FunctionNode,\n entry: TSESTree.ObjectLiteralElement,\n provides: OwnBindings,\n): string | undefined {\n const enabled = enabledOf(source, contributionCall(fn, entry));\n\n if (enabled === undefined) return undefined;\n\n const written = source.getTokens(enabled.value).map(token => token.value);\n\n return written.length === 0 || readsOwn(source, provides, enabled.value) ? undefined : written.join(' ');\n}\n\n/** The one gate every entry of the record shares, or `undefined` when an entry has another or none. */\nfunction sharedGate(source: SourceCode, fn: FunctionNode, record: TSESTree.ObjectExpression): string | undefined {\n const provides = ownBindings(source, fn);\n const gates = record.properties.map(entry => gateOf(source, fn, entry, provides));\n const [first] = gates;\n\n return first !== undefined && gates.every(gate => gate === first) ? first : undefined;\n}\n\n/** The `provides` of a feature that hands nothing to other features and loads no body of its own. */\nfunction providesOf(sections: TSESTree.ObjectExpression): TSESTree.Property | undefined {\n const names = sections.properties.map(property => propertyName(property));\n\n if (names.includes(undefined) || names.includes('exports') || names.includes('body')) return undefined;\n\n return findProperty(sections, 'provides');\n}\n\ntype Provided = { readonly fn: FunctionNode; readonly key: TSESTree.Node; readonly record: TSESTree.ObjectExpression };\n\n/** The `provides` callback of this call's sections and the record it answers. */\nfunction providedRecord(call: TSESTree.CallExpression): Provided | undefined {\n const sections = call.arguments[1];\n\n if (sections?.type !== AST_NODE_TYPES.ObjectExpression) return undefined;\n\n const provides = providesOf(sections);\n\n if (provides === undefined || !isFunction(provides.value)) return undefined;\n\n const record = returnedRecord(provides.value);\n\n return record === undefined ? undefined : { fn: provides.value, key: provides.key, record };\n}\n\n/**\n * D494: `enabled` hides a contribution while its feature stays open — its models, their work and their Resources\n * keep running (D220) — and `when` closes the feature itself. A feature that hands nothing to other features and\n * hides every contribution behind one and the same `enabled` keeps all of that running while nothing of it is shown.\n */\nconst preferWhenOverEnabled = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n\n return {\n CallExpression(node): void {\n const provided = providedRecord(node);\n\n if (provided === undefined || sharedGate(source, provided.fn, provided.record) === undefined) return;\n\n if (!isApi(source, node.callee, '@opetope/runtime', 'defineFeature')) return;\n\n context.report({ messageId: 'preferWhen', node: provided.key });\n },\n };\n },\n meta: {\n docs: {\n description:\n 'A feature that exports nothing and hides every contribution behind one `enabled` is gated by `when`.',\n },\n messages: {\n preferWhen:\n 'Every contribution of this feature is hidden by the same `enabled`, and the feature exports nothing, so its models, their work and their Resources stay open while nothing of it is shown. Gate the feature itself: move the fact into a Condition and list it in `when`, which closes the feature while it is false; if its models must work while nothing is shown, move the contributions into a feature of their own that `when` gates (D220, D494).',\n },\n schema: [],\n type: 'suggestion',\n },\n name: 'prefer-when-over-enabled',\n});\n\nexport { preferWhenOverEnabled };\n"],"names":["CONTRIBUTIONS","returnedRecord","fn","body","returned","AST_NODE_TYPES","only","destructuredBuilder","pattern","name","property","entry","propertyName","namedBuilder","parameter","callee","object","builderOf","contributionCall","call","enabledOf","source","written","options","localValue","findProperty","ownVariables","names","boundNames","variableOf","ownBindings","readsOwnMember","identifier","parent","readsOwn","provides","gate","held","nodes","bindings","isFunction","node","referencesWithin","variable","own","whole","gateOf","enabled","token","sharedGate","record","gates","first","providesOf","sections","providedRecord","preferWhenOverEnabled","createRule","context","provided","isApi"],"mappings":"gSAYA,MAAMA,EAAgB,IAAI,IAAI,CAAC,OAAQ,WAAY,MAAM,CAAC,EAG1D,SAASC,EAAeC,EAAgB,CACtC,KAAM,CAAE,KAAAC,CAAI,EAAKD,EACjB,IAAIE,EAA6CD,EAEjD,GAAIA,EAAK,OAASE,EAAe,eAAgB,CAC/C,KAAM,CAACC,CAAI,EAAIH,EAAK,KAEpBC,EAAWD,EAAK,KAAK,SAAW,GAAKG,GAAM,OAASD,EAAe,gBAAkBC,EAAK,SAAW,MACvG,CAEA,OAAOF,GAAU,OAASC,EAAe,iBAAmBD,EAAW,MACzE,CAGA,SAASG,EAAoBC,EAAiCC,EAAY,CACxE,MAAMC,EAAWF,EAAQ,WAAW,KAClCG,GACEA,EAAM,OAASN,EAAe,UAC9BM,EAAM,MAAM,OAASN,EAAe,YACpCM,EAAM,MAAM,OAASF,CAAI,EAG7B,OAAOC,IAAa,OAAY,OAAYE,EAAaF,CAAQ,CACnE,CAGA,SAASG,EAAaC,EAAgCC,EAAiC,CACrF,KAAM,CAAE,OAAAC,EAAQ,SAAAN,CAAQ,EAAKK,EAE7B,GAAI,EAAAA,EAAO,UAAYC,EAAO,OAASX,EAAe,YAAcW,EAAO,OAASF,EAAU,MAE9F,OAAOJ,EAAS,OAASL,EAAe,WAAaK,EAAS,KAAO,MACvE,CAGA,SAASO,EAAUf,EAAkBa,EAAqB,CACxD,KAAM,CAACD,CAAS,EAAIZ,EAAG,OAEvB,GAAIa,EAAO,OAASV,EAAe,YAAcS,GAAW,OAAST,EAAe,cAClF,OAAOE,EAAoBO,EAAWC,EAAO,IAAI,EAGnD,GAAIA,EAAO,OAASV,EAAe,kBAAoBS,GAAW,OAAST,EAAe,WACxF,OAAOQ,EAAaC,EAAWC,CAAM,CAIzC,CAGA,SAASG,EAAiBhB,EAAkBS,EAAoC,CAC9E,MAAMQ,EAAOR,EAAM,OAASN,EAAe,SAAWM,EAAM,MAAQ,OAEpE,GAAIQ,GAAM,OAASd,EAAe,eAElC,OAAOL,EAAc,IAAIiB,EAAUf,EAAIiB,EAAK,MAAM,GAAK,EAAE,EAAIA,EAAO,MACtE,CAGA,SAASC,EAAUC,EAAoBF,EAAyC,CAC9E,MAAMG,EAAUH,GAAM,UAAU,CAAC,EAC3BI,EAAUD,IAAY,OAAY,OAAYE,EAAWH,EAAQC,CAAO,EAE9E,OAAOC,GAAS,OAASlB,EAAe,iBAAmBoB,EAAaF,EAAS,SAAS,EAAI,MAChG,CAUA,SAASG,EAAaL,EAAoBb,EAA+B,CACvE,MAAMmB,EAAQnB,EAAQ,WAAW,QAAQE,GACvCA,EAAS,OAASL,EAAe,UAAYO,EAAaF,CAAQ,IAAM,MAAQkB,EAAWlB,EAAS,KAAK,EAAI,EAAE,EAGjH,OAAO,IAAI,IAAIiB,EAAM,QAAQlB,GAAQoB,EAAWR,EAAQZ,CAAI,GAAK,CAAA,CAAE,CAAC,CACtE,CAEA,SAASqB,EAAYT,EAAoBnB,EAAgB,CACvD,KAAM,CAACY,CAAS,EAAIZ,EAAG,OAEvB,OAAIY,GAAW,OAAST,EAAe,WAAmB,CAAE,IAAK,IAAI,IAAO,MAAOwB,EAAWR,EAAQP,CAAS,CAAC,EAIzG,CAAE,IAFGA,GAAW,OAAST,EAAe,cAAgBqB,EAAaL,EAAQP,CAAS,EAAI,IAAI,IAEvF,MAAO,IAAI,CAC3B,CAGA,SAASiB,EAAeC,EAA+B,CACrD,KAAM,CAAE,OAAAC,CAAM,EAAKD,EAEnB,OACEC,EAAO,OAAS5B,EAAe,kBAC/B4B,EAAO,SAAWD,GAClB,CAACC,EAAO,UACRA,EAAO,SAAS,OAAS5B,EAAe,YACxC4B,EAAO,SAAS,OAAS,KAE7B,CAOA,SAASC,EAASb,EAAoBc,EAAuBC,EAAmB,CAC9E,MAAMC,EAAOb,EAAWH,EAAQe,CAAI,EAC9BE,EAAQD,IAASD,EAAO,CAACA,CAAI,EAAI,CAACA,EAAMC,CAAI,EAC5CE,EAAW,CAACJ,EAAU,GAAGG,EAAM,OAAOE,CAAU,EAAE,IAAItC,GAAM4B,EAAYT,EAAQnB,CAAE,CAAC,CAAC,EAE1F,OAAOoC,EAAM,KAAKG,GAChBC,EAAiBrB,EAAQoB,EAAMA,CAAI,EAAE,KAAKT,GAAa,CACrD,MAAMW,EAAWd,EAAWR,EAAQW,CAAU,EAE9C,OAAIW,IAAa,KAAa,GAEvBJ,EAAS,KAAK,CAAC,CAAE,IAAAK,EAAK,MAAAC,CAAK,IAAOD,EAAI,IAAID,CAAQ,GAAME,IAAUF,GAAYZ,EAAeC,CAAU,CAAE,CAClH,CAAC,CAAC,CAEN,CAQA,SAASc,EACPzB,EACAnB,EACAS,EACAwB,EAAqB,CAErB,MAAMY,EAAU3B,EAAUC,EAAQH,EAAiBhB,EAAIS,CAAK,CAAC,EAE7D,GAAIoC,IAAY,OAAW,OAE3B,MAAMzB,EAAUD,EAAO,UAAU0B,EAAQ,KAAK,EAAE,IAAIC,GAASA,EAAM,KAAK,EAExE,OAAO1B,EAAQ,SAAW,GAAKY,EAASb,EAAQc,EAAUY,EAAQ,KAAK,EAAI,OAAYzB,EAAQ,KAAK,GAAG,CACzG,CAGA,SAAS2B,EAAW5B,EAAoBnB,EAAkBgD,EAAiC,CACzF,MAAMf,EAAWL,EAAYT,EAAQnB,CAAE,EACjCiD,EAAQD,EAAO,WAAW,IAAIvC,GAASmC,EAAOzB,EAAQnB,EAAIS,EAAOwB,CAAQ,CAAC,EAC1E,CAACiB,CAAK,EAAID,EAEhB,OAAOC,IAAU,QAAaD,EAAM,MAAMf,GAAQA,IAASgB,CAAK,EAAIA,EAAQ,MAC9E,CAGA,SAASC,EAAWC,EAAmC,CACrD,MAAM3B,EAAQ2B,EAAS,WAAW,IAAI5C,GAAYE,EAAaF,CAAQ,CAAC,EAExE,GAAI,EAAAiB,EAAM,SAAS,MAAS,GAAKA,EAAM,SAAS,SAAS,GAAKA,EAAM,SAAS,MAAM,GAEnF,OAAOF,EAAa6B,EAAU,UAAU,CAC1C,CAKA,SAASC,EAAepC,EAA6B,CACnD,MAAMmC,EAAWnC,EAAK,UAAU,CAAC,EAEjC,GAAImC,GAAU,OAASjD,EAAe,iBAAkB,OAExD,MAAM8B,EAAWkB,EAAWC,CAAQ,EAEpC,GAAInB,IAAa,QAAa,CAACK,EAAWL,EAAS,KAAK,EAAG,OAE3D,MAAMe,EAASjD,EAAekC,EAAS,KAAK,EAE5C,OAAOe,IAAW,OAAY,OAAY,CAAE,GAAIf,EAAS,MAAO,IAAKA,EAAS,IAAK,OAAAe,CAAM,CAC3F,CAOA,MAAMM,EAAwBC,EAA2B,CACvD,OAAOC,EAAO,CACZ,MAAMrC,EAASqC,EAAQ,WAEvB,MAAO,CACL,eAAejB,EAAI,CACjB,MAAMkB,EAAWJ,EAAed,CAAI,EAEhCkB,IAAa,QAAaV,EAAW5B,EAAQsC,EAAS,GAAIA,EAAS,MAAM,IAAM,QAE9EC,EAAMvC,EAAQoB,EAAK,OAAQ,mBAAoB,eAAe,GAEnEiB,EAAQ,OAAO,CAAE,UAAW,aAAc,KAAMC,EAAS,IAAK,CAChE,EAEJ,EACA,KAAM,CACJ,KAAM,CACJ,YACE,sGACH,EACD,SAAU,CACR,WACE,0bACH,EACD,OAAQ,CAAA,EACR,KAAM,YACP,EACD,KAAM,0BACP,CAAA"}
@@ -1,5 +1,3 @@
1
1
  type MessageIds = 'missingWrapper' | 'undeclaredUiModel';
2
- declare const requireDeclaredModels: import("@typescript-eslint/utils/ts-eslint").RuleModule<MessageIds, readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
3
- name: string;
4
- };
2
+ declare const requireDeclaredModels: import("../rule.js").RuleModule<MessageIds, readonly []>;
5
3
  export { requireDeclaredModels };
@@ -1,2 +1,2 @@
1
- import{AST_NODE_TYPES as s}from"@typescript-eslint/utils";import{findProperty as b}from"../ast.js";import{bodyRecordEntries as T}from"../feature-body-records.js";import{isSlot as B,enclosingFunction as M,unwrap as P,localValue as c,isFunction as m,modelBinding as p,isApi as U}from"../model-bindings.js";import{createRule as V}from"../rule.js";function W(i){if(i.type===s.CallExpression)return i.arguments[0];if(i.type===s.ArrayExpression)return i.elements[0]??void 0}const D=V({create(i){const o=i.sourceCode,a=[],l=new Map,g=new Map,u=new Map,h=new Set,y=(e,n)=>U(o,e,"@opetope/react",n),E=e=>{if(e===void 0)return;const n=c(o,e);if(n.type!==s.ArrayExpression)return;const t=new Set;for(const r of n.elements){if(r===null)continue;const d=p(o,r);if(d===void 0)return;t.add(d)}return t},w=e=>{const n=P(e.callee);if(n.type!==s.CallExpression||!y(n.callee,"requiresModels"))return;const t=E(n.arguments[0]);g.set(e,t);const[r]=e.arguments;if(r===void 0)return;const d=c(o,r);m(d)&&u.set(d,t)},x=e=>{for(const n of e.elements){if(n===null)continue;const t=c(o,n),r=W(t),d=r===void 0?void 0:p(o,r);d!==void 0&&h.add(d)}},C=e=>g.has(e)||m(e)&&u.has(e),S=e=>{const n=c(o,e.value);C(n)||n.type===s.Identifier||n.type===s.MemberExpression||i.report({messageId:"missingWrapper",node:e})},q=e=>{const n=b(e,"Component"),t=b(e,"models");if(n===void 0||t===void 0)return;const r=c(o,t.value);r.type!==s.ArrayExpression||r.elements.length===0||(x(r),S(n))},k=e=>e.body===void 0?[]:e.body.type===s.BlockStatement?l.get(e)??[]:[e.body],f=(e,n=new Set)=>{const t=c(o,e);if(!n.has(t)){if(n.add(t),t.type===s.ObjectExpression)q(t);else if(m(t))for(const r of k(t))f(r,n)}},A=e=>{for(const n of T(o,e))n.implementation!==void 0&&f(n.implementation)},R=(e,n)=>{const t=M(o,e);if(t===void 0||!u.has(t))return!1;const r=u.get(t);return r===void 0||r.has(n)},I=e=>{if(!y(e.callee,"useModel"))return;const[n]=e.arguments,t=n===void 0?void 0:p(o,n);t===void 0||!h.has(t)||R(e,t)||i.report({data:{name:o.getText(n)},messageId:"undeclaredUiModel",node:e})};return{CallExpression(e){a.push(e)},ReturnStatement(e){const n=M(o,e);if(n===void 0||e.argument===null)return;const t=l.get(n)??[];t.push(e.argument),l.set(n,t)},"Program:exit"(){for(const e of a)w(e);for(const e of a){const n=e.arguments[1];n!==void 0&&B(o,e.callee)&&f(n),A(e)}for(const e of a)I(e)}}},meta:{docs:{description:"Each visible reader of a per-mount UI model declares it with requiresModels."},messages:{missingWrapper:"This contribution grants models, so its Component must come from requiresModels([...]) of @opetope/react.",undeclaredUiModel:"A per-mount model is read by a component or hook that declares it: wrap this reader in requiresModels([{{ name }}]), or read a model the feature owns."},schema:[],type:"problem"},name:"require-declared-models"});export{D as requireDeclaredModels};
1
+ import{AST_NODE_TYPES as s}from"@typescript-eslint/types";import{findProperty as b}from"../ast.js";import{bodyRecordEntries as T}from"../feature-body-records.js";import{isSlot as B,enclosingFunction as M,unwrap as P,localValue as c,isFunction as m,modelBinding as p,isApi as U}from"../model-bindings.js";import{createRule as V}from"../rule.js";function W(i){if(i.type===s.CallExpression)return i.arguments[0];if(i.type===s.ArrayExpression)return i.elements[0]??void 0}const D=V({create(i){const o=i.sourceCode,a=[],l=new Map,g=new Map,u=new Map,h=new Set,y=(e,n)=>U(o,e,"@opetope/react",n),E=e=>{if(e===void 0)return;const n=c(o,e);if(n.type!==s.ArrayExpression)return;const t=new Set;for(const r of n.elements){if(r===null)continue;const d=p(o,r);if(d===void 0)return;t.add(d)}return t},w=e=>{const n=P(e.callee);if(n.type!==s.CallExpression||!y(n.callee,"requiresModels"))return;const t=E(n.arguments[0]);g.set(e,t);const[r]=e.arguments;if(r===void 0)return;const d=c(o,r);m(d)&&u.set(d,t)},x=e=>{for(const n of e.elements){if(n===null)continue;const t=c(o,n),r=W(t),d=r===void 0?void 0:p(o,r);d!==void 0&&h.add(d)}},C=e=>g.has(e)||m(e)&&u.has(e),S=e=>{const n=c(o,e.value);C(n)||n.type===s.Identifier||n.type===s.MemberExpression||i.report({messageId:"missingWrapper",node:e})},q=e=>{const n=b(e,"Component"),t=b(e,"models");if(n===void 0||t===void 0)return;const r=c(o,t.value);r.type!==s.ArrayExpression||r.elements.length===0||(x(r),S(n))},k=e=>e.body===void 0?[]:e.body.type===s.BlockStatement?l.get(e)??[]:[e.body],f=(e,n=new Set)=>{const t=c(o,e);if(!n.has(t)){if(n.add(t),t.type===s.ObjectExpression)q(t);else if(m(t))for(const r of k(t))f(r,n)}},A=e=>{for(const n of T(o,e))n.implementation!==void 0&&f(n.implementation)},R=(e,n)=>{const t=M(o,e);if(t===void 0||!u.has(t))return!1;const r=u.get(t);return r===void 0||r.has(n)},I=e=>{if(!y(e.callee,"useModel"))return;const[n]=e.arguments,t=n===void 0?void 0:p(o,n);t===void 0||!h.has(t)||R(e,t)||i.report({data:{name:o.getText(n)},messageId:"undeclaredUiModel",node:e})};return{CallExpression(e){a.push(e)},ReturnStatement(e){const n=M(o,e);if(n===void 0||e.argument===null)return;const t=l.get(n)??[];t.push(e.argument),l.set(n,t)},"Program:exit"(){for(const e of a)w(e);for(const e of a){const n=e.arguments[1];n!==void 0&&B(o,e.callee)&&f(n),A(e)}for(const e of a)I(e)}}},meta:{docs:{description:"Each visible reader of a per-mount UI model declares it with requiresModels."},messages:{missingWrapper:"This contribution grants models, so its Component must come from requiresModels([...]) of @opetope/react.",undeclaredUiModel:"A per-mount model is read by a component or hook that declares it: wrap this reader in requiresModels([{{ name }}]), or read a model the feature owns."},schema:[],type:"problem"},name:"require-declared-models"});export{D as requireDeclaredModels};
2
2
  //# sourceMappingURL=require-declared-models.js.map