@opetope/lint 0.15.0 → 0.16.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.
@@ -1 +1 @@
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":"mgBA0BA,MAAMA,EAAc,CAAC,QAAQ,EAEvBC,EAAc,CAAC,QAAQ,EAEvBC,EAAmB,EAEnBC,EAAmB,EAGnBC,EAAc,IAAI,IAAI,CAAC,KAAM,MAAO,IAAK,KAAM,KAAM,MAAO,IAAK,KAAM,KAAM,YAAY,CAAC,EAC1FC,EAAc,IAAI,IAAI,CAAC,IAAK,QAAQ,CAAC,EAGrCC,EAAQ,IAAI,IAAY,CAACC,EAAe,gBAAiBA,EAAe,cAAc,CAAC,EAG7F,SAASC,EAAeC,EAAoBC,EAAyB,CACnE,MAAMC,EAAQC,EAAWH,EAAQC,CAAI,EAErC,GAAIC,EAAM,OAASJ,EAAe,eAAgB,MAAO,GAEzD,MAAMM,EAAQC,EAAWL,EAAQE,EAAM,MAAM,EAE7C,OAAOE,GAAO,SAAW,SAAWE,EAAkBN,EAAQI,EAAM,EAAE,CACxE,CAGA,SAASG,EAAYP,EAAoBC,EAAmB,CAG1D,GAFIA,EAAK,OAASH,EAAe,gBAE7B,CAACU,EAAMR,EAAQC,EAAK,OAAQ,gBAAiB,SAAU,EAAI,EAAG,OAElE,KAAM,CAACQ,EAAcC,CAAQ,EAAIT,EAAK,UAEtC,GAAIQ,IAAiB,OAAW,OAEhC,MAAME,EAASC,EAAOH,CAAY,EAElC,OAAIE,EAAO,OAASb,EAAe,gBAAwB,CAAE,aAAc,CAACa,CAAM,EAAG,SAAAD,CAAQ,EAGtF,CAAE,aAAcC,EAAO,SAAS,QAAQE,GAAYA,IAAY,KAAO,CAAA,EAAK,CAACA,CAAO,CAAE,EAAG,SAAAH,CAAQ,CAC1G,CAOA,SAASI,EAAWb,EAAyB,CAC3C,KAAM,CAAE,OAAAc,CAAM,EAAKd,EAEnB,OAAIc,EAAO,OAASjB,EAAe,iBAAyBH,EAAY,IAAIoB,EAAO,QAAQ,EAEvFA,EAAO,OAASjB,EAAe,gBAAwBF,EAAY,IAAImB,EAAO,QAAQ,GAEtFA,EAAO,OAASjB,EAAe,uBAE5BiB,EAAO,OAASjB,EAAe,cAAeiB,EAAO,OAASd,CACvE,CAGA,SAASe,EACPhB,EACAU,EACAO,EAAgB,CAEhB,MAAMC,EAAKR,IAAa,OAAY,OAAYP,EAAWH,EAAQU,CAAQ,EAE3E,GAAIQ,IAAO,QAAa,CAACC,EAAWD,CAAE,EAAG,OAEzC,MAAME,EAAYF,EAAG,OAAOD,CAAQ,EAEpC,OAAOG,GAAW,OAAStB,EAAe,WAAasB,EAAY,MACrE,CAUA,SAASC,EAAQrB,EAAoBU,EAAqCO,EAAgB,CACxF,MAAMG,EAAYJ,EAAkBhB,EAAQU,EAAUO,CAAQ,EAI9D,OAHiBG,IAAc,OAAY,KAAOE,EAAWtB,EAAQoB,CAAS,IAIlE,WAAW,KACnB,CAAC,CAAE,WAAAG,CAAU,IAAOA,EAAW,OAASzB,EAAe,YAAc,CAACgB,EAAWS,CAAU,CAAC,GACzF,EAET,CAGA,SAASC,EAAQxB,EAAoByB,EAAsBxB,EAAmB,CAC5E,MAAMG,EAAQQ,EAAOX,CAAI,EAEzB,GAAIG,EAAM,OAASN,EAAe,WAAY,OAE9C,MAAM4B,EAAWJ,EAAWtB,EAAQI,CAAK,EAErCsB,IAAa,MAAQ3B,EAAeC,EAAQI,CAAK,GAAGqB,EAAM,IAAIC,CAAQ,CAC5E,CAQA,SAASC,EAAY3B,EAAoB4B,EAAmB,CAC1D,MAAMH,EAAQ,IAAI,IAGlBD,EAAQxB,EAAQyB,EAAOG,CAAI,EAE3B,MAAMC,EAAUtB,EAAYP,EAAQG,EAAWH,EAAQ4B,CAAI,CAAC,EAE5D,GAAIC,IAAY,OAAW,OAAOJ,EAElC,SAAW,CAACR,EAAUa,CAAU,IAAKD,EAAQ,aAAa,UACpDR,EAAQrB,EAAQ6B,EAAQ,SAAUZ,CAAQ,GAAGO,EAAQxB,EAAQyB,EAAOK,CAAU,EAGpF,OAAOL,CACT,CAMA,SAASM,EACP/B,EACAgC,EACAC,EACAhB,EACAiB,EAAQ,CAER,MAAMC,EAAUC,EAAepC,EAAQgC,EAAMf,EAAU,CAACiB,CAAG,EAAGD,EAAQ,WAAW,EAEjF,OAAOA,EAAQ,MACZ,OAAOI,GAAQC,EAAOD,EAAML,CAAI,GAAKO,EAAkBvC,EAAQqC,CAAI,IAAML,CAAI,EAC7E,OAAOK,GAAQG,EAAYxC,EAAQqC,EAAK,OAAQF,EAASD,CAAG,CAAC,EAC7D,KAAK,CAACO,EAAMC,IAAUD,EAAK,MAAM,CAAC,EAAIC,EAAM,MAAM,CAAC,CAAC,CACzD,CAGA,SAASC,EAAS3C,EAAoB4C,EAAmBX,EAAgB,CACvE,OAAOF,EAAa/B,EAAQ4C,EAAKX,EAASxC,EAAkBF,EAAY,CAAC,CAAC,CAC5E,CAGA,SAASsD,EAAiB7C,EAAoBqC,EAA+BZ,EAA4B,CACvG,KAAM,CAACqB,CAAM,EAAIT,EAAK,UAEtB,GAAIS,IAAW,OAAW,MAAO,GAEjC,MAAM1C,EAAQQ,EAAOkC,CAAM,EAE3B,GAAI1C,EAAM,OAASN,EAAe,WAAY,MAAO,GAErD,MAAM4B,EAAWJ,EAAWtB,EAAQI,CAAK,EAEzC,OAAOsB,IAAa,MAAQD,EAAM,IAAIC,CAAQ,CAChD,CAOA,SAASqB,EAAe/C,EAAoBC,EAAmB,CAC7D,MAAMC,EAAQC,EAAWH,EAAQY,EAAOX,CAAI,CAAC,EAE7C,GAAIC,EAAM,OAASJ,EAAe,eAAgB,OAElD,MAAMM,EAAQC,EAAWL,EAAQE,EAAM,MAAM,EAE7C,GAAIE,GAAO,SAAW,UAEtB,OAAO4C,EAAuBhD,EAAQI,EAAM,EAAE,EAAI6C,EAAYjD,EAAQE,CAAK,EAAI,MACjF,CAGA,SAASgD,EACPlD,EACAqC,EACAJ,EACAR,EAA4B,CAE5B,KAAM,CAACqB,CAAM,EAAIT,EAAK,UAChBL,EAAOc,IAAW,OAAY,OAAYC,EAAe/C,EAAQ8C,CAAM,EAE7E,OAAId,IAAS,OAAkB,GAExBD,EAAa/B,EAAQgC,EAAMC,EAASvC,EAAkBH,EAAY,CAAC,CAAC,EAAE,KAAK4D,GAChFN,EAAiB7C,EAAQmD,EAAO1B,CAAK,CAAC,CAE1C,CAGA,SAAS2B,EAAcC,EAA6B,CAClD,IAAIC,EAAUD,EAEd,KAAOC,EAAQ,OAASxD,EAAe,gBAAgB,CACrD,MAAMyD,EAAOD,EAAQ,KAAK,GAAG,EAAE,EAE/B,GAAIC,IAAS,OAAW,OAExBD,EAAUC,CACZ,CAEA,OAAOD,CACT,CAGA,SAASE,EAAeH,EAA6B,CACnD,MAAME,EAAOH,EAAcC,CAAS,EAEpC,OAAOE,IAAS,QAAa1D,EAAM,IAAI0D,EAAK,IAAI,CAClD,CAGA,SAASE,EAAMJ,EAA6B,CAC1C,MAAME,EAAOH,EAAcC,CAAS,EAEpC,OAAIE,IAAS,OAAkB,GAE3B1D,EAAM,IAAI0D,EAAK,IAAI,EAAU,GAG7BA,EAAK,OAASzD,EAAe,aAAeyD,EAAK,YAAc,KAAa,GAEzEC,EAAeD,EAAK,UAAU,GAAKC,EAAeD,EAAK,SAAS,CACzE,CAOA,SAASG,EAAc1D,EAAoB2D,EAAsBC,EAAqB,CACpF,MAAMhC,EAAO+B,EAAM,MAAM,CAAC,EACpBE,EAAKD,EAAO,MAAM,CAAC,EACnBE,EAAWT,GACfA,EAAU,MAAM,CAAC,GAAKzB,GAAQyB,EAAU,MAAM,CAAC,GAAKQ,GAAMJ,EAAMJ,CAAS,EAE3E,OAAOrD,EACJ,aAAa2D,CAAK,EAClB,KAAK1D,GAAQA,EAAK,OAASH,EAAe,gBAAkBG,EAAK,KAAK,KAAK6D,CAAO,CAAC,CACxF,CAGA,SAASC,EAAWC,EAAiCC,EAA+B,CAClF,MAAMC,EAAS,KAAK,IAAIF,EAAM,OAAQC,EAAM,MAAM,EAClD,IAAIE,EAAK,EAET,KAAOA,EAAKD,GAAUF,EAAMG,CAAE,IAAMF,EAAME,CAAE,GAAGA,GAAM,EAErD,OAAOA,CACT,CAQA,SAASC,EAAQpE,EAAoB2D,EAAsBC,EAAqB,CAC9E,MAAMI,EAAQ,CAAC,GAAGhE,EAAO,aAAa2D,CAAK,EAAGA,CAAK,EAC7CM,EAAQ,CAAC,GAAGjE,EAAO,aAAa4D,CAAM,EAAGA,CAAM,EAC/CO,EAAKJ,EAAWC,EAAOC,CAAK,EAC5BI,EAAQF,IAAO,EAAI,OAAYH,EAAMG,EAAK,CAAC,EAC3CG,EAASN,EAAMG,CAAE,EACjBI,EAAQN,EAAME,CAAE,EAEtB,OAAIE,GAAO,OAASvE,EAAe,gBAAkBwE,IAAW,QAAaC,IAAU,OAAkB,GAElGD,EAAO,MAAM,CAAC,GAAKC,EAAM,MAAM,CAAC,GAAK,CAACb,EAAc1D,EAAQ2D,EAAOC,CAAM,CAClF,CAEA,MAAMY,EAA0BC,EAA2B,CACzD,OAAOC,EAAO,CACZ,MAAM1E,EAAS0E,EAAQ,WACjBzC,EAAmB,CAAE,MAAO,CAAA,EAAI,YAAa,CAAA,CAAE,EAE/C0C,EAAY,CAAC/B,EAAmBnB,IAAsC,CAC1E,MAAMmD,EAASjC,EAAS3C,EAAQ4C,EAAKX,CAAO,EAE5C,UAAWkB,KAASyB,EACb/B,EAAiB7C,EAAQmD,EAAO1B,CAAK,GAEtCmD,EAAO,KAAKC,GAAST,EAAQpE,EAAQmD,EAAO0B,CAAK,CAAC,GACpDH,EAAQ,OAAO,CAAE,UAAW,wBAAyB,KAAMvB,EAAO,EAKtE,UAAWd,KAAQN,EAAa/B,EAAQ4C,EAAKX,EAASxC,EAAkBD,EAAY,CAAC,CAAC,EAC/E0D,EAAmBlD,EAAQqC,EAAMJ,EAASR,CAAK,GAEhDmD,EAAO,KAAKC,GAAST,EAAQpE,EAAQqC,EAAMwC,CAAK,CAAC,GACnDH,EAAQ,OAAO,CAAE,UAAW,+BAAgC,KAAMrC,EAAM,CAG9E,EAEA,MAAO,CACL,eAAepC,EAAI,CACjBgC,EAAQ,MAAM,KAAKhC,CAAI,CACzB,EACA,gBAAc,CACZ,UAAWoC,KAAQJ,EAAQ,MAAO,CAChC,MAAM6C,EAAWC,EAAe/E,EAAQqC,CAAI,EAE5C,GAAIyC,IAAa,OAAW,SAE5B,MAAMrD,EAAQE,EAAY3B,EAAQ8E,EAAS,IAAI,EAE3CrD,EAAM,KAAO,GAAGkD,EAAUG,EAAS,IAAKrD,CAAK,CACnD,CACF,EACA,mBAAmBxB,EAAI,CACrBgC,EAAQ,YAAY,KAAKhC,CAAI,CAC/B,EAEJ,EACA,KAAM,CACJ,KAAM,CACJ,YAAa,8FACd,EACD,SAAU,CAOR,6BACE,0wBAOF,sBACE,4lBACH,EACD,OAAQ,CAAA,EACR,KAAM,SACP,EACD,KAAM,6BACP,CAAA"}
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 (effect runs in `docs/reference/reactions.md`) — and a selector that only\n * classifies its dependency is that carve-out written down: a `derive` publishing `current === null ? 'none' : 'some'`\n * does not move when one key replaces another. So the selector has to be readable here and has to read this dependency\n * as a value at least once; a selector assembled elsewhere, one that drops the dependency, and one that only asks a\n * question about it all leave 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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"}
@@ -1,2 +1,2 @@
1
- import{AST_NODE_TYPES as i}from"@typescript-eslint/types";import{propertyName as u}from"../ast.js";import{isBodyCall as f}from"../feature-body-records.js";import{isApi as h,unwrap as d}from"../model-bindings.js";import{createRule as p}from"../rule.js";const m={callee:"defineFeature",known:new Set(["imports","requires","own","exports","provides","when","body"]),unknown:"unknownSection"},w={callee:"defineFeature.body",known:new Set(["own","exports","provides"]),unknown:"unknownBodySection"},k=p({create(r){const s=r.sourceCode,c=(e,t,n)=>{if(e.type===i.SpreadElement){r.report({data:{callee:t.callee},messageId:"spreadSection",node:e});return}const o=u(e);if(o===void 0||e.kind!=="init"){r.report({data:{callee:t.callee},messageId:"dynamicSection",node:e.key});return}t.known.has(o)?n.has(o)&&r.report({data:{name:o},messageId:"duplicateSection",node:e.key}):r.report({data:{name:o},messageId:t.unknown,node:e.key}),n.add(o)},a=(e,t)=>{const n=new Set;for(const o of e.properties)c(o,t,n)},l=e=>{const[,t]=e.arguments,n=t===void 0?void 0:d(t);n?.type===i.ObjectExpression&&a(n,w)};return{CallExpression(e){if(f(s,e)){l(e);return}const[t,n]=e.arguments;if(n===void 0||t?.type===i.ObjectExpression||!h(s,e.callee,"@opetope/runtime","defineFeature"))return;const o=d(n);o.type===i.ObjectExpression?a(o,m):r.report({messageId:"sectionsNotLiteral",node:n})}}},meta:{docs:{description:"The sections of `defineFeature` are one object literal written at the call, with known keys only."},messages:{duplicateSection:"`{{name}}` is written twice in the sections of this feature, and the later one replaces the first without a word. Keep one (D494).",dynamicSection:"A section of `{{callee}}` is a plain key and its value: a computed key or an accessor hides from the types and from these rules which section the feature declares. Write the key as `own:`, `provides:` and the like (D494).",sectionsNotLiteral:"The sections of `defineFeature` are one object literal written at the call. The types infer `own` for `exports` and `provides` from that literal in the order it is written, and the rules of this package read it; a record built elsewhere hides both. Write the record inline (D477, D494).",spreadSection:"A spread in the sections of `{{callee}}` hides which sections it declares from the types and from these rules. Write each section in the record itself (D494).",unknownBodySection:"`{{name}}` is not a section of `defineFeature.body`, and the runtime refuses the body when it loads: a body takes the `own`, `exports` and `provides` of its header (D186, D494).",unknownSection:"`{{name}}` is not a section of `defineFeature`, and the runtime refuses the declaration: a feature takes `imports`, `requires`, `own`, `exports`, `provides`, `when` and `body` after its id (D280, D494)."},schema:[],type:"problem"},name:"require-static-feature"});export{k as requireStaticFeature};
1
+ import{AST_NODE_TYPES as i}from"@typescript-eslint/types";import{propertyName as u}from"../ast.js";import{isBodyCall as f}from"../feature-body-records.js";import{isApi as h,unwrap as d}from"../model-bindings.js";import{createRule as p}from"../rule.js";const m={callee:"defineFeature",known:new Set(["imports","requires","own","exports","provides","when","body"]),unknown:"unknownSection"},w={callee:"defineFeature.body",known:new Set(["own","exports","provides"]),unknown:"unknownBodySection"},k=p({create(r){const s=r.sourceCode,c=(e,t,n)=>{if(e.type===i.SpreadElement){r.report({data:{callee:t.callee},messageId:"spreadSection",node:e});return}const o=u(e);if(o===void 0||e.kind!=="init"){r.report({data:{callee:t.callee},messageId:"dynamicSection",node:e.key});return}t.known.has(o)?n.has(o)&&r.report({data:{name:o},messageId:"duplicateSection",node:e.key}):r.report({data:{name:o},messageId:t.unknown,node:e.key}),n.add(o)},a=(e,t)=>{const n=new Set;for(const o of e.properties)c(o,t,n)},l=e=>{const[,t]=e.arguments,n=t===void 0?void 0:d(t);n?.type===i.ObjectExpression&&a(n,w)};return{CallExpression(e){if(f(s,e)){l(e);return}const[t,n]=e.arguments;if(n===void 0||t?.type===i.ObjectExpression||!h(s,e.callee,"@opetope/runtime","defineFeature"))return;const o=d(n);o.type===i.ObjectExpression?a(o,m):r.report({messageId:"sectionsNotLiteral",node:n})}}},meta:{docs:{description:"The sections of `defineFeature` are one object literal written at the call, with known keys only."},messages:{duplicateSection:"`{{name}}` is written twice in the sections of this feature, and the later one replaces the first without a word. Keep one (D494).",dynamicSection:"A section of `{{callee}}` is a plain key and its value: a computed key or an accessor hides from the types and from these rules which section the feature declares. Write the key as `own:`, `provides:` and the like (D494).",sectionsNotLiteral:"The sections of `defineFeature` are one object literal written at the call. The types infer `own` for `exports` and `provides` from that literal in the order it is written, and the rules of this package read it; a record built elsewhere hides both. Write the record inline (D477, D494).",spreadSection:"A spread in the sections of `{{callee}}` hides which sections it declares from the types and from these rules. Write each section in the record itself (D494).",unknownBodySection:"`{{name}}` is not a section of `defineFeature.body`, and the runtime refuses the body when it loads: a body takes the `own`, `exports` and `provides` of its header (D542, D494).",unknownSection:"`{{name}}` is not a section of `defineFeature`, and the runtime refuses the declaration: a feature takes `imports`, `requires`, `own`, `exports`, `provides`, `when` and `body` after its id (D280, D494)."},schema:[],type:"problem"},name:"require-static-feature"});export{k as requireStaticFeature};
2
2
  //# sourceMappingURL=require-static-feature.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"require-static-feature.js","sources":["../../src/rules/require-static-feature.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { propertyName } from '../ast';\nimport { isBodyCall } from '../feature-body-records';\nimport { isApi, unwrap } from '../model-bindings';\nimport { createRule } from '../rule';\n\ntype MessageIds =\n | 'duplicateSection'\n | 'dynamicSection'\n | 'sectionsNotLiteral'\n | 'spreadSection'\n | 'unknownBodySection'\n | 'unknownSection';\n\n/** What one record of sections may hold, and how an unknown key of it is refused. */\ntype Sections = { readonly callee: string; readonly known: ReadonlySet<string>; readonly unknown: MessageIds };\n\n/** The sections `defineFeature` takes after its id, in the order `define-feature-property-order` holds (D186, D280). */\nconst FEATURE: Sections = {\n callee: 'defineFeature',\n known: new Set(['imports', 'requires', 'own', 'exports', 'provides', 'when', 'body']),\n unknown: 'unknownSection',\n};\n\n/** The sections of a body file: `own`, `exports` and `provides` of the header it implements (D186). */\nconst BODY: Sections = {\n callee: 'defineFeature.body',\n known: new Set(['own', 'exports', 'provides']),\n unknown: 'unknownBodySection',\n};\n\n/**\n * D494: the sections of a feature are one record written where the declaration stands. The types read it in the order\n * it is written — `exports` and `provides` see the `own` inferred above them (D477) — and every rule of this package\n * that speaks about a feature reads that literal. A record built elsewhere, spread into it or keyed by a computed name\n * hides from both what the feature declares, and a key written twice drops the first section without a word.\n */\nconst requireStaticFeature = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n\n const checkProperty = (property: TSESTree.ObjectLiteralElement, sections: Sections, seen: Set<string>): void => {\n if (property.type === AST_NODE_TYPES.SpreadElement) {\n context.report({ data: { callee: sections.callee }, messageId: 'spreadSection', node: property });\n\n return;\n }\n\n const name = propertyName(property);\n\n if (name === undefined || property.kind !== 'init') {\n context.report({ data: { callee: sections.callee }, messageId: 'dynamicSection', node: property.key });\n\n return;\n }\n\n if (!sections.known.has(name)) {\n context.report({ data: { name }, messageId: sections.unknown, node: property.key });\n } else if (seen.has(name)) {\n context.report({ data: { name }, messageId: 'duplicateSection', node: property.key });\n }\n\n seen.add(name);\n };\n\n const checkRecord = (record: TSESTree.ObjectExpression, sections: Sections): void => {\n const seen = new Set<string>();\n\n for (const property of record.properties) checkProperty(property, sections, seen);\n };\n\n /** A body file may hold its record in a name, as the feature reference shows; a record written here is read. */\n const checkBody = (node: TSESTree.CallExpression): void => {\n const [, implementation] = node.arguments;\n const record = implementation === undefined ? undefined : unwrap(implementation);\n\n if (record?.type === AST_NODE_TYPES.ObjectExpression) checkRecord(record, BODY);\n };\n\n return {\n CallExpression(node): void {\n if (isBodyCall(source, node)) {\n checkBody(node);\n\n return;\n }\n\n const [identity, sections] = node.arguments;\n\n // The single-record form D280 retired is refused by the compiler; this rule reads the current one.\n if (sections === undefined || identity?.type === AST_NODE_TYPES.ObjectExpression) return;\n\n if (!isApi(source, node.callee, '@opetope/runtime', 'defineFeature')) return;\n\n // `satisfies` and `as const` are for the types alone: the literal under them is the record.\n const record = unwrap(sections);\n\n if (record.type === AST_NODE_TYPES.ObjectExpression) {\n checkRecord(record, FEATURE);\n } else {\n context.report({ messageId: 'sectionsNotLiteral', node: sections });\n }\n },\n };\n },\n meta: {\n docs: {\n description: 'The sections of `defineFeature` are one object literal written at the call, with known keys only.',\n },\n messages: {\n duplicateSection:\n '`{{name}}` is written twice in the sections of this feature, and the later one replaces the first without a word. Keep one (D494).',\n dynamicSection:\n 'A section of `{{callee}}` is a plain key and its value: a computed key or an accessor hides from the types and from these rules which section the feature declares. Write the key as `own:`, `provides:` and the like (D494).',\n sectionsNotLiteral:\n 'The sections of `defineFeature` are one object literal written at the call. The types infer `own` for `exports` and `provides` from that literal in the order it is written, and the rules of this package read it; a record built elsewhere hides both. Write the record inline (D477, D494).',\n spreadSection:\n 'A spread in the sections of `{{callee}}` hides which sections it declares from the types and from these rules. Write each section in the record itself (D494).',\n unknownBodySection:\n '`{{name}}` is not a section of `defineFeature.body`, and the runtime refuses the body when it loads: a body takes the `own`, `exports` and `provides` of its header (D186, D494).',\n unknownSection:\n '`{{name}}` is not a section of `defineFeature`, and the runtime refuses the declaration: a feature takes `imports`, `requires`, `own`, `exports`, `provides`, `when` and `body` after its id (D280, D494).',\n },\n schema: [],\n type: 'problem',\n },\n name: 'require-static-feature',\n});\n\nexport { requireStaticFeature };\n"],"names":["FEATURE","BODY","requireStaticFeature","createRule","context","source","checkProperty","property","sections","seen","AST_NODE_TYPES","name","propertyName","checkRecord","record","checkBody","node","implementation","unwrap","isBodyCall","identity","isApi"],"mappings":"4PAoBA,MAAMA,EAAoB,CACxB,OAAQ,gBACR,MAAO,IAAI,IAAI,CAAC,UAAW,WAAY,MAAO,UAAW,WAAY,OAAQ,MAAM,CAAC,EACpF,QAAS,kBAILC,EAAiB,CACrB,OAAQ,qBACR,MAAO,IAAI,IAAI,CAAC,MAAO,UAAW,UAAU,CAAC,EAC7C,QAAS,sBASLC,EAAuBC,EAA2B,CACtD,OAAOC,EAAO,CACZ,MAAMC,EAASD,EAAQ,WAEjBE,EAAgB,CAACC,EAAyCC,EAAoBC,IAA2B,CAC7G,GAAIF,EAAS,OAASG,EAAe,cAAe,CAClDN,EAAQ,OAAO,CAAE,KAAM,CAAE,OAAQI,EAAS,MAAM,EAAI,UAAW,gBAAiB,KAAMD,CAAQ,CAAE,EAEhG,MACF,CAEA,MAAMI,EAAOC,EAAaL,CAAQ,EAElC,GAAII,IAAS,QAAaJ,EAAS,OAAS,OAAQ,CAClDH,EAAQ,OAAO,CAAE,KAAM,CAAE,OAAQI,EAAS,MAAM,EAAI,UAAW,iBAAkB,KAAMD,EAAS,GAAG,CAAE,EAErG,MACF,CAEKC,EAAS,MAAM,IAAIG,CAAI,EAEjBF,EAAK,IAAIE,CAAI,GACtBP,EAAQ,OAAO,CAAE,KAAM,CAAE,KAAAO,CAAI,EAAI,UAAW,mBAAoB,KAAMJ,EAAS,GAAG,CAAE,EAFpFH,EAAQ,OAAO,CAAE,KAAM,CAAE,KAAAO,CAAI,EAAI,UAAWH,EAAS,QAAS,KAAMD,EAAS,GAAG,CAAE,EAKpFE,EAAK,IAAIE,CAAI,CACf,EAEME,EAAc,CAACC,EAAmCN,IAA4B,CAClF,MAAMC,EAAO,IAAI,IAEjB,UAAWF,KAAYO,EAAO,WAAYR,EAAcC,EAAUC,EAAUC,CAAI,CAClF,EAGMM,EAAaC,GAAuC,CACxD,KAAM,CAAA,CAAGC,CAAc,EAAID,EAAK,UAC1BF,EAASG,IAAmB,OAAY,OAAYC,EAAOD,CAAc,EAE3EH,GAAQ,OAASJ,EAAe,kBAAkBG,EAAYC,EAAQb,CAAI,CAChF,EAEA,MAAO,CACL,eAAee,EAAI,CACjB,GAAIG,EAAWd,EAAQW,CAAI,EAAG,CAC5BD,EAAUC,CAAI,EAEd,MACF,CAEA,KAAM,CAACI,EAAUZ,CAAQ,EAAIQ,EAAK,UAKlC,GAFIR,IAAa,QAAaY,GAAU,OAASV,EAAe,kBAE5D,CAACW,EAAMhB,EAAQW,EAAK,OAAQ,mBAAoB,eAAe,EAAG,OAGtE,MAAMF,EAASI,EAAOV,CAAQ,EAE1BM,EAAO,OAASJ,EAAe,iBACjCG,EAAYC,EAAQd,CAAO,EAE3BI,EAAQ,OAAO,CAAE,UAAW,qBAAsB,KAAMI,EAAU,CAEtE,EAEJ,EACA,KAAM,CACJ,KAAM,CACJ,YAAa,mGACd,EACD,SAAU,CACR,iBACE,qIACF,eACE,gOACF,mBACE,iSACF,cACE,iKACF,mBACE,oLACF,eACE,4MACH,EACD,OAAQ,CAAA,EACR,KAAM,SACP,EACD,KAAM,wBACP,CAAA"}
1
+ {"version":3,"file":"require-static-feature.js","sources":["../../src/rules/require-static-feature.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { propertyName } from '../ast';\nimport { isBodyCall } from '../feature-body-records';\nimport { isApi, unwrap } from '../model-bindings';\nimport { createRule } from '../rule';\n\ntype MessageIds =\n | 'duplicateSection'\n | 'dynamicSection'\n | 'sectionsNotLiteral'\n | 'spreadSection'\n | 'unknownBodySection'\n | 'unknownSection';\n\n/** What one record of sections may hold, and how an unknown key of it is refused. */\ntype Sections = { readonly callee: string; readonly known: ReadonlySet<string>; readonly unknown: MessageIds };\n\n/** The sections `defineFeature` takes after its id, in the order `define-feature-property-order` holds (D542, D280). */\nconst FEATURE: Sections = {\n callee: 'defineFeature',\n known: new Set(['imports', 'requires', 'own', 'exports', 'provides', 'when', 'body']),\n unknown: 'unknownSection',\n};\n\n/** The sections of a body file: `own`, `exports` and `provides` of the header it implements (D542). */\nconst BODY: Sections = {\n callee: 'defineFeature.body',\n known: new Set(['own', 'exports', 'provides']),\n unknown: 'unknownBodySection',\n};\n\n/**\n * D494: the sections of a feature are one record written where the declaration stands. The types read it in the order\n * it is written — `exports` and `provides` see the `own` inferred above them (D477) — and every rule of this package\n * that speaks about a feature reads that literal. A record built elsewhere, spread into it or keyed by a computed name\n * hides from both what the feature declares, and a key written twice drops the first section without a word.\n */\nconst requireStaticFeature = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n\n const checkProperty = (property: TSESTree.ObjectLiteralElement, sections: Sections, seen: Set<string>): void => {\n if (property.type === AST_NODE_TYPES.SpreadElement) {\n context.report({ data: { callee: sections.callee }, messageId: 'spreadSection', node: property });\n\n return;\n }\n\n const name = propertyName(property);\n\n if (name === undefined || property.kind !== 'init') {\n context.report({ data: { callee: sections.callee }, messageId: 'dynamicSection', node: property.key });\n\n return;\n }\n\n if (!sections.known.has(name)) {\n context.report({ data: { name }, messageId: sections.unknown, node: property.key });\n } else if (seen.has(name)) {\n context.report({ data: { name }, messageId: 'duplicateSection', node: property.key });\n }\n\n seen.add(name);\n };\n\n const checkRecord = (record: TSESTree.ObjectExpression, sections: Sections): void => {\n const seen = new Set<string>();\n\n for (const property of record.properties) checkProperty(property, sections, seen);\n };\n\n /** A body file may hold its record in a name, as the feature reference shows; a record written here is read. */\n const checkBody = (node: TSESTree.CallExpression): void => {\n const [, implementation] = node.arguments;\n const record = implementation === undefined ? undefined : unwrap(implementation);\n\n if (record?.type === AST_NODE_TYPES.ObjectExpression) checkRecord(record, BODY);\n };\n\n return {\n CallExpression(node): void {\n if (isBodyCall(source, node)) {\n checkBody(node);\n\n return;\n }\n\n const [identity, sections] = node.arguments;\n\n // The single-record form D280 retired is refused by the compiler; this rule reads the current one.\n if (sections === undefined || identity?.type === AST_NODE_TYPES.ObjectExpression) return;\n\n if (!isApi(source, node.callee, '@opetope/runtime', 'defineFeature')) return;\n\n // `satisfies` and `as const` are for the types alone: the literal under them is the record.\n const record = unwrap(sections);\n\n if (record.type === AST_NODE_TYPES.ObjectExpression) {\n checkRecord(record, FEATURE);\n } else {\n context.report({ messageId: 'sectionsNotLiteral', node: sections });\n }\n },\n };\n },\n meta: {\n docs: {\n description: 'The sections of `defineFeature` are one object literal written at the call, with known keys only.',\n },\n messages: {\n duplicateSection:\n '`{{name}}` is written twice in the sections of this feature, and the later one replaces the first without a word. Keep one (D494).',\n dynamicSection:\n 'A section of `{{callee}}` is a plain key and its value: a computed key or an accessor hides from the types and from these rules which section the feature declares. Write the key as `own:`, `provides:` and the like (D494).',\n sectionsNotLiteral:\n 'The sections of `defineFeature` are one object literal written at the call. The types infer `own` for `exports` and `provides` from that literal in the order it is written, and the rules of this package read it; a record built elsewhere hides both. Write the record inline (D477, D494).',\n spreadSection:\n 'A spread in the sections of `{{callee}}` hides which sections it declares from the types and from these rules. Write each section in the record itself (D494).',\n unknownBodySection:\n '`{{name}}` is not a section of `defineFeature.body`, and the runtime refuses the body when it loads: a body takes the `own`, `exports` and `provides` of its header (D542, D494).',\n unknownSection:\n '`{{name}}` is not a section of `defineFeature`, and the runtime refuses the declaration: a feature takes `imports`, `requires`, `own`, `exports`, `provides`, `when` and `body` after its id (D280, D494).',\n },\n schema: [],\n type: 'problem',\n },\n name: 'require-static-feature',\n});\n\nexport { requireStaticFeature };\n"],"names":["FEATURE","BODY","requireStaticFeature","createRule","context","source","checkProperty","property","sections","seen","AST_NODE_TYPES","name","propertyName","checkRecord","record","checkBody","node","implementation","unwrap","isBodyCall","identity","isApi"],"mappings":"4PAoBA,MAAMA,EAAoB,CACxB,OAAQ,gBACR,MAAO,IAAI,IAAI,CAAC,UAAW,WAAY,MAAO,UAAW,WAAY,OAAQ,MAAM,CAAC,EACpF,QAAS,kBAILC,EAAiB,CACrB,OAAQ,qBACR,MAAO,IAAI,IAAI,CAAC,MAAO,UAAW,UAAU,CAAC,EAC7C,QAAS,sBASLC,EAAuBC,EAA2B,CACtD,OAAOC,EAAO,CACZ,MAAMC,EAASD,EAAQ,WAEjBE,EAAgB,CAACC,EAAyCC,EAAoBC,IAA2B,CAC7G,GAAIF,EAAS,OAASG,EAAe,cAAe,CAClDN,EAAQ,OAAO,CAAE,KAAM,CAAE,OAAQI,EAAS,MAAM,EAAI,UAAW,gBAAiB,KAAMD,CAAQ,CAAE,EAEhG,MACF,CAEA,MAAMI,EAAOC,EAAaL,CAAQ,EAElC,GAAII,IAAS,QAAaJ,EAAS,OAAS,OAAQ,CAClDH,EAAQ,OAAO,CAAE,KAAM,CAAE,OAAQI,EAAS,MAAM,EAAI,UAAW,iBAAkB,KAAMD,EAAS,GAAG,CAAE,EAErG,MACF,CAEKC,EAAS,MAAM,IAAIG,CAAI,EAEjBF,EAAK,IAAIE,CAAI,GACtBP,EAAQ,OAAO,CAAE,KAAM,CAAE,KAAAO,CAAI,EAAI,UAAW,mBAAoB,KAAMJ,EAAS,GAAG,CAAE,EAFpFH,EAAQ,OAAO,CAAE,KAAM,CAAE,KAAAO,CAAI,EAAI,UAAWH,EAAS,QAAS,KAAMD,EAAS,GAAG,CAAE,EAKpFE,EAAK,IAAIE,CAAI,CACf,EAEME,EAAc,CAACC,EAAmCN,IAA4B,CAClF,MAAMC,EAAO,IAAI,IAEjB,UAAWF,KAAYO,EAAO,WAAYR,EAAcC,EAAUC,EAAUC,CAAI,CAClF,EAGMM,EAAaC,GAAuC,CACxD,KAAM,CAAA,CAAGC,CAAc,EAAID,EAAK,UAC1BF,EAASG,IAAmB,OAAY,OAAYC,EAAOD,CAAc,EAE3EH,GAAQ,OAASJ,EAAe,kBAAkBG,EAAYC,EAAQb,CAAI,CAChF,EAEA,MAAO,CACL,eAAee,EAAI,CACjB,GAAIG,EAAWd,EAAQW,CAAI,EAAG,CAC5BD,EAAUC,CAAI,EAEd,MACF,CAEA,KAAM,CAACI,EAAUZ,CAAQ,EAAIQ,EAAK,UAKlC,GAFIR,IAAa,QAAaY,GAAU,OAASV,EAAe,kBAE5D,CAACW,EAAMhB,EAAQW,EAAK,OAAQ,mBAAoB,eAAe,EAAG,OAGtE,MAAMF,EAASI,EAAOV,CAAQ,EAE1BM,EAAO,OAASJ,EAAe,iBACjCG,EAAYC,EAAQd,CAAO,EAE3BI,EAAQ,OAAO,CAAE,UAAW,qBAAsB,KAAMI,EAAU,CAEtE,EAEJ,EACA,KAAM,CACJ,KAAM,CACJ,YAAa,mGACd,EACD,SAAU,CACR,iBACE,qIACF,eACE,gOACF,mBACE,iSACF,cACE,iKACF,mBACE,oLACF,eACE,4MACH,EACD,OAAQ,CAAA,EACR,KAAM,SACP,EACD,KAAM,wBACP,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opetope/lint",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "engines": {
5
5
  "node": ">=20.19.0"
6
6
  },