@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.
- package/CHANGELOG.md +62 -0
- package/README.md +39 -2
- package/dist/ast.d.ts +14 -2
- package/dist/ast.js +1 -1
- package/dist/ast.js.map +1 -1
- package/dist/command-hooks.d.ts +7 -6
- package/dist/command-hooks.js +1 -1
- package/dist/command-hooks.js.map +1 -1
- package/dist/context-members.d.ts +4 -4
- package/dist/context-members.js +1 -1
- package/dist/context-members.js.map +1 -1
- package/dist/declaration-ingress.d.ts +43 -11
- package/dist/declaration-ingress.js +1 -1
- package/dist/declaration-ingress.js.map +1 -1
- package/dist/declarations.d.ts +1 -1
- package/dist/declarations.js +1 -1
- package/dist/declarations.js.map +1 -1
- package/dist/effect-declarations.d.ts +3 -2
- package/dist/effect-declarations.js +1 -1
- package/dist/effect-declarations.js.map +1 -1
- package/dist/entry-move-rows.d.ts +21 -0
- package/dist/entry-move-rows.js +2 -0
- package/dist/entry-move-rows.js.map +1 -0
- package/dist/entry-moves.d.ts +25 -0
- package/dist/entry-moves.js +10 -0
- package/dist/entry-moves.js.map +1 -0
- package/dist/feature-body-records.d.ts +5 -3
- package/dist/feature-body-records.js +1 -1
- package/dist/feature-body-records.js.map +1 -1
- package/dist/index.d.ts +65 -58
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lazy-body-migration.d.ts +5 -5
- package/dist/lazy-body-migration.js +1 -1
- package/dist/lazy-body-migration.js.map +1 -1
- package/dist/model-bindings.d.ts +21 -13
- package/dist/model-bindings.js +1 -1
- package/dist/model-bindings.js.map +1 -1
- package/dist/resource-vocabulary.d.ts +17 -0
- package/dist/resource-vocabulary.js +2 -0
- package/dist/resource-vocabulary.js.map +1 -0
- package/dist/retired-vocabulary-messages.d.ts +6 -0
- package/dist/retired-vocabulary-messages.js +2 -0
- package/dist/retired-vocabulary-messages.js.map +1 -0
- package/dist/retired-vocabulary.d.ts +10 -8
- package/dist/retired-vocabulary.js +1 -1
- package/dist/retired-vocabulary.js.map +1 -1
- package/dist/rule.d.ts +94 -5
- package/dist/rule.js +1 -1
- package/dist/rule.js.map +1 -1
- package/dist/rules/capture-command-cleanup.d.ts +1 -4
- package/dist/rules/capture-command-cleanup.js +1 -1
- package/dist/rules/capture-command-cleanup.js.map +1 -1
- package/dist/rules/define-feature-property-order.d.ts +1 -4
- package/dist/rules/define-feature-property-order.js +1 -1
- package/dist/rules/define-feature-property-order.js.map +1 -1
- package/dist/rules/enabled-predicate.d.ts +1 -4
- package/dist/rules/enabled-predicate.js +1 -1
- package/dist/rules/enabled-predicate.js.map +1 -1
- package/dist/rules/id-naming.d.ts +1 -3
- package/dist/rules/layer-placement.d.ts +1 -3
- package/dist/rules/no-behavior-in-integration.d.ts +8 -0
- package/dist/rules/no-behavior-in-integration.js +2 -0
- package/dist/rules/no-behavior-in-integration.js.map +1 -0
- package/dist/rules/no-command-in-deps.d.ts +1 -4
- package/dist/rules/no-command-in-deps.js +1 -1
- package/dist/rules/no-command-in-deps.js.map +1 -1
- package/dist/rules/no-dead-abort-check.d.ts +8 -0
- package/dist/rules/no-dead-abort-check.js +2 -0
- package/dist/rules/no-dead-abort-check.js.map +1 -0
- package/dist/rules/no-event-without-emit.d.ts +10 -0
- package/dist/rules/no-event-without-emit.js +2 -0
- package/dist/rules/no-event-without-emit.js.map +1 -0
- package/dist/rules/no-inline-requires-models.d.ts +8 -0
- package/dist/rules/no-inline-requires-models.js +5 -0
- package/dist/rules/no-inline-requires-models.js.map +1 -0
- package/dist/rules/no-internal-imports.d.ts +1 -3
- package/dist/rules/no-internal-imports.js +1 -1
- package/dist/rules/no-internal-imports.js.map +1 -1
- package/dist/rules/no-redundant-const-tuple.d.ts +1 -4
- package/dist/rules/no-redundant-const-tuple.js +1 -1
- package/dist/rules/no-redundant-const-tuple.js.map +1 -1
- package/dist/rules/no-retired-vocabulary.d.ts +2 -4
- package/dist/rules/no-retired-vocabulary.js +1 -1
- package/dist/rules/no-retired-vocabulary.js.map +1 -1
- package/dist/rules/no-single-command-lane.d.ts +10 -0
- package/dist/rules/no-single-command-lane.js +2 -0
- package/dist/rules/no-single-command-lane.js.map +1 -0
- package/dist/rules/no-snapshot-in-update.d.ts +1 -4
- package/dist/rules/no-snapshot-in-update.js.map +1 -1
- package/dist/rules/no-snapshot-read-in-render.d.ts +2 -3
- package/dist/rules/no-snapshot-read-in-render.js +1 -1
- package/dist/rules/no-snapshot-read-in-render.js.map +1 -1
- package/dist/rules/no-subscribe-outside-models.d.ts +1 -3
- package/dist/rules/no-subscribe-outside-models.js +1 -1
- package/dist/rules/no-subscribe-outside-models.js.map +1 -1
- package/dist/rules/no-write-after-source-write.d.ts +1 -4
- package/dist/rules/no-write-after-source-write.js +1 -1
- package/dist/rules/no-write-after-source-write.js.map +1 -1
- package/dist/rules/prefer-derive.d.ts +7 -0
- package/dist/rules/prefer-derive.js +2 -0
- package/dist/rules/prefer-derive.js.map +1 -0
- package/dist/rules/prefer-effect-current.d.ts +1 -4
- package/dist/rules/prefer-effect-current.js +1 -1
- package/dist/rules/prefer-effect-current.js.map +1 -1
- package/dist/rules/prefer-lazy-body-record.d.ts +1 -4
- package/dist/rules/prefer-lazy-body-record.js +1 -1
- package/dist/rules/prefer-lazy-body-record.js.map +1 -1
- package/dist/rules/prefer-model-selection.d.ts +1 -3
- package/dist/rules/prefer-model-selection.js +1 -1
- package/dist/rules/prefer-model-selection.js.map +1 -1
- package/dist/rules/prefer-read.d.ts +2 -0
- package/dist/rules/prefer-read.js +2 -0
- package/dist/rules/prefer-read.js.map +1 -0
- package/dist/rules/prefer-when-over-enabled.d.ts +7 -0
- package/dist/rules/prefer-when-over-enabled.js +2 -0
- package/dist/rules/prefer-when-over-enabled.js.map +1 -0
- package/dist/rules/require-declared-models.d.ts +1 -3
- package/dist/rules/require-declared-models.js +1 -1
- package/dist/rules/require-declared-models.js.map +1 -1
- package/dist/rules/require-literal-id.d.ts +2 -3
- package/dist/rules/require-literal-id.js +1 -1
- package/dist/rules/require-literal-id.js.map +1 -1
- package/dist/rules/require-static-feature.d.ts +9 -0
- package/dist/rules/require-static-feature.js +2 -0
- package/dist/rules/require-static-feature.js.map +1 -0
- package/dist/rules/scope-builder-releases-subscriptions.d.ts +10 -0
- package/dist/rules/scope-builder-releases-subscriptions.js +2 -0
- package/dist/rules/scope-builder-releases-subscriptions.js.map +1 -0
- package/dist/snapshot-updates.d.ts +4 -3
- package/dist/snapshot-updates.js +1 -1
- package/dist/snapshot-updates.js.map +1 -1
- package/dist/static-value.d.ts +3 -2
- package/dist/static-value.js +1 -1
- package/dist/static-value.js.map +1 -1
- package/oxlintrc.json +10 -0
- package/package.json +13 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture-command-cleanup.js","sources":["../../src/rules/capture-command-cleanup.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 type { ContextMembers } from '../context-members';\nimport { contextMembers, fromContext } from '../context-members';\nimport { commandBody, isModelContextFunction, methodPath } from '../declaration-ingress';\nimport type { FunctionNode } from '../model-bindings';\nimport { enclosingFunction, unwrap } from '../model-bindings';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'captureCommandCleanup' | 'parallelCommandCleanup';\ntype Clause = 'catch' | 'finally';\n\n/** What one file wrote, collected before any write 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\ntype ContextKey = 'rethrowIfCancelled' | 'signal' | 'update';\ntype Context = ContextMembers<ContextKey>;\n\n/** D386: the three names this rule reads off the context of a command body, which is its one parameter. */\nconst CONTEXT_KEYS: readonly ContextKey[] = ['update', 'signal', 'rethrowIfCancelled'];\nconst CONTEXT_POSITION = 0;\n\n/**\n * D387: `{ concurrency: 'parallel' }` written on the declaration itself. A record assembled elsewhere is not read: a\n * discipline this rule cannot see is treated as the default queue, and the README says so.\n */\nfunction declaresParallel(command: TSESTree.CallExpression): boolean {\n const options = command.arguments[1];\n const record = options === undefined ? undefined : unwrap(options);\n const concurrency =\n record?.type === AST_NODE_TYPES.ObjectExpression ? findProperty(record, 'concurrency') : undefined;\n const value = concurrency === undefined ? undefined : unwrap(concurrency.value);\n\n return value?.type === AST_NODE_TYPES.Literal && value.value === 'parallel';\n}\n\n/** A member read on the signal of this context: `signal.aborted`, `execution.signal.throwIfAborted`. */\nfunction signalMember(source: TSESLint.SourceCode, node: TSESTree.Node, context: Context, member: string): boolean {\n if (node.type !== AST_NODE_TYPES.MemberExpression || node.computed) return false;\n if (node.property.type !== AST_NODE_TYPES.Identifier || node.property.name !== member) return false;\n\n return fromContext(source, node.object, context, 'signal');\n}\n\nfunction exits(statement: TSESTree.Statement): boolean {\n if (statement.type === AST_NODE_TYPES.ThrowStatement || statement.type === AST_NODE_TYPES.ReturnStatement)\n return true;\n if (statement.type !== AST_NODE_TYPES.BlockStatement) return false;\n\n const last = statement.body.at(-1);\n\n return last !== undefined && exits(last);\n}\n\n/**\n * A statement past which a `catch` is no longer running for a cancelled caller: `rethrowIfCancelled(cause)`, a\n * `signal.throwIfAborted()`, or an `if (signal.aborted)` that throws or returns. A write after one of them is the\n * author's statement that it must not run for a cancelled call, so it is not the cleanup this rule is about.\n */\nfunction leavesOnCancellation(source: TSESLint.SourceCode, statement: TSESTree.Statement, context: Context): boolean {\n if (statement.type === AST_NODE_TYPES.IfStatement) {\n return signalMember(source, unwrap(statement.test), context, 'aborted') && exits(statement.consequent);\n }\n\n if (statement.type !== AST_NODE_TYPES.ExpressionStatement) return false;\n\n const expression = unwrap(statement.expression);\n\n if (expression.type !== AST_NODE_TYPES.CallExpression) return false;\n\n return (\n fromContext(source, expression.callee, context, 'rethrowIfCancelled') ||\n signalMember(source, expression.callee, context, 'throwIfAborted')\n );\n}\n\n/** Whether a statement written before the write in this block already left for a cancelled caller. */\nfunction guardedBlock(\n source: TSESLint.SourceCode,\n block: TSESTree.BlockStatement,\n write: TSESTree.Node,\n context: Context,\n): boolean {\n for (const statement of block.body) {\n if (within(write, statement)) return false;\n if (leavesOnCancellation(source, statement, context)) return true;\n }\n\n return false;\n}\n\n/**\n * A guard counts where it precedes the write in the block that holds it or in any block around that one, up to the\n * `catch` itself: `if (cause instanceof Refused) { rethrowIfCancelled(cause); update(failed, true) }` is guarded, and\n * a guard inside a block the write is not in guards nothing.\n */\nfunction guardedCatch(\n source: TSESLint.SourceCode,\n handler: TSESTree.CatchClause,\n write: TSESTree.Node,\n context: Context,\n): boolean {\n return source\n .getAncestors(write)\n .some(\n ancestor =>\n ancestor.type === AST_NODE_TYPES.BlockStatement &&\n within(ancestor, handler.body) &&\n guardedBlock(source, ancestor, write, context),\n );\n}\n\n/** Whether the protected block suspends the function the `try` belongs to; an `await` in a nested callback does not. */\nfunction awaits(source: TSESLint.SourceCode, statement: TSESTree.TryStatement, written: Written): boolean {\n const owner = enclosingFunction(source, statement);\n\n return written.barriers.some(\n barrier => within(barrier, statement.block) && enclosingFunction(source, barrier) === owner,\n );\n}\n\n/** Where this `try` holds the write it was reached from: a cleanup clause of an awaited block, or nowhere. */\nfunction clauseOf(\n source: TSESLint.SourceCode,\n statement: TSESTree.TryStatement,\n child: TSESTree.Node,\n write: TSESTree.CallExpression,\n context: Context,\n written: Written,\n): Clause | undefined {\n if (child === statement.block || !awaits(source, statement, written)) return undefined;\n if (child === statement.finalizer) return 'finally';\n\n const { handler } = statement;\n\n if (handler === null || child !== handler) return undefined;\n\n return guardedCatch(source, handler, write, context) ? undefined : 'catch';\n}\n\n/** The cleanup clause of the innermost awaited `try` that holds this write, walking out no further than the body. */\nfunction cleanupClause(\n source: TSESLint.SourceCode,\n write: TSESTree.CallExpression,\n body: FunctionNode,\n context: Context,\n written: Written,\n): Clause | undefined {\n let child: TSESTree.Node = write;\n\n for (const parent of source.getAncestors(write).reverse()) {\n if (parent === body) return undefined;\n\n const clause =\n parent.type === AST_NODE_TYPES.TryStatement\n ? clauseOf(source, parent, child, write, context, written)\n : undefined;\n\n if (clause !== undefined) return clause;\n\n child = parent;\n }\n\n return undefined;\n}\n\nconst captureCommandCleanup = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const written: Written = { barriers: [], calls: [], declarators: [] };\n\n const reportWrites = (body: FunctionNode, parallel: boolean): void => {\n const received = contextMembers(source, body, CONTEXT_POSITION, CONTEXT_KEYS, written.declarators);\n const messageId: MessageIds = parallel ? 'parallelCommandCleanup' : 'captureCommandCleanup';\n\n for (const call of written.calls) {\n if (!within(call, body) || !fromContext(source, call.callee, received, 'update')) continue;\n\n const clause = cleanupClause(source, call, body, received, written);\n\n if (clause !== undefined) context.report({ data: { clause }, messageId, node: call });\n }\n };\n\n return {\n AwaitExpression(node): void {\n written.barriers.push(node);\n },\n CallExpression(node): void {\n written.calls.push(node);\n },\n ForOfStatement(node): void {\n if (node.await) written.barriers.push(node);\n },\n 'Program:exit'(): void {\n for (const call of written.calls) {\n const named = methodPath(source, call.callee);\n\n if (named?.method !== 'command' || !isModelContextFunction(source, named.fn)) continue;\n\n const body = commandBody(source, call);\n\n if (body !== undefined) reportWrites(body, declaresParallel(call));\n }\n },\n VariableDeclarator(node): void {\n written.declarators.push(node);\n },\n };\n },\n meta: {\n docs: {\n description: 'A command writes the cleanup it owes after an `await` through a commit, not through its writer.',\n },\n messages: {\n captureCommandCleanup:\n 'This write is in the `{{clause}}` of a `try` that awaits, and the writer of a command lives only as long as its caller: once the call is cancelled the write is dropped without a record. A cleanup that must run for a cancelled call — a busy flag, a pending mark — goes through `const commit = execution.capture(state)` taken before the `await` and `commit.update(…)` here. An answer that must not land after cancellation — a failure, a result — belongs in a `catch` after `rethrowIfCancelled(cause)` (D288, D319, D330).',\n parallelCommandCleanup:\n \"This write is in the `{{clause}}` of a `try` that awaits in a `concurrency: 'parallel'` command, and once the call is cancelled it is dropped without a record. A commit does not fix it: calls of this command overlap, so the cleanup of a cancelled call would clear state another call still holds, and a flag shared by parallel calls is racy either way. Keep such state per call — keyed by its input — or leave progress to the consumer's `inFlight`. An answer that must not land after cancellation belongs in a `catch` after `rethrowIfCancelled(cause)` (D288, D330).\",\n },\n schema: [],\n type: 'problem',\n },\n name: 'capture-command-cleanup',\n});\n\nexport { captureCommandCleanup };\n"],"names":["CONTEXT_KEYS","CONTEXT_POSITION","declaresParallel","command","options","record","unwrap","concurrency","AST_NODE_TYPES","findProperty","value","signalMember","source","node","context","member","fromContext","exits","statement","last","leavesOnCancellation","expression","guardedBlock","block","write","within","guardedCatch","handler","ancestor","awaits","written","owner","enclosingFunction","barrier","clauseOf","child","cleanupClause","body","parent","clause","captureCommandCleanup","createRule","reportWrites","parallel","received","contextMembers","messageId","call","named","methodPath","isModelContextFunction","commandBody"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"capture-command-cleanup.js","sources":["../../src/rules/capture-command-cleanup.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { findProperty, within } from '../ast';\nimport type { ContextMembers } from '../context-members';\nimport { contextMembers, fromContext } from '../context-members';\nimport { commandBody, isModelContextFunction, methodPath } from '../declaration-ingress';\nimport type { FunctionNode } from '../model-bindings';\nimport { enclosingFunction, unwrap } from '../model-bindings';\nimport type { SourceCode } from '../rule';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'captureCommandCleanup' | 'parallelCommandCleanup';\ntype Clause = 'catch' | 'finally';\n\n/** What one file wrote, collected before any write 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\ntype ContextKey = 'rethrowIfCancelled' | 'signal' | 'update';\ntype Context = ContextMembers<ContextKey>;\n\n/** D386: the three names this rule reads off the context of a command body, which is its one parameter. */\nconst CONTEXT_KEYS: readonly ContextKey[] = ['update', 'signal', 'rethrowIfCancelled'];\nconst CONTEXT_POSITION = 0;\n\n/**\n * D387: `{ concurrency: 'parallel' }` written on the declaration itself. A record assembled elsewhere is not read: a\n * discipline this rule cannot see is treated as the default queue, and the README says so.\n */\nfunction declaresParallel(command: TSESTree.CallExpression): boolean {\n const options = command.arguments[1];\n const record = options === undefined ? undefined : unwrap(options);\n const concurrency =\n record?.type === AST_NODE_TYPES.ObjectExpression ? findProperty(record, 'concurrency') : undefined;\n const value = concurrency === undefined ? undefined : unwrap(concurrency.value);\n\n return value?.type === AST_NODE_TYPES.Literal && value.value === 'parallel';\n}\n\n/** A member read on the signal of this context: `signal.aborted`, `execution.signal.throwIfAborted`. */\nfunction signalMember(source: SourceCode, node: TSESTree.Node, context: Context, member: string): boolean {\n if (node.type !== AST_NODE_TYPES.MemberExpression || node.computed) return false;\n if (node.property.type !== AST_NODE_TYPES.Identifier || node.property.name !== member) return false;\n\n return fromContext(source, node.object, context, 'signal');\n}\n\nfunction exits(statement: TSESTree.Statement): boolean {\n if (statement.type === AST_NODE_TYPES.ThrowStatement || statement.type === AST_NODE_TYPES.ReturnStatement)\n return true;\n if (statement.type !== AST_NODE_TYPES.BlockStatement) return false;\n\n const last = statement.body.at(-1);\n\n return last !== undefined && exits(last);\n}\n\n/**\n * A statement past which a `catch` is no longer running for a cancelled caller: `rethrowIfCancelled(cause)`, a\n * `signal.throwIfAborted()`, or an `if (signal.aborted)` that throws or returns. A write after one of them is the\n * author's statement that it must not run for a cancelled call, so it is not the cleanup this rule is about.\n */\nfunction leavesOnCancellation(source: SourceCode, statement: TSESTree.Statement, context: Context): boolean {\n if (statement.type === AST_NODE_TYPES.IfStatement) {\n return signalMember(source, unwrap(statement.test), context, 'aborted') && exits(statement.consequent);\n }\n\n if (statement.type !== AST_NODE_TYPES.ExpressionStatement) return false;\n\n const expression = unwrap(statement.expression);\n\n if (expression.type !== AST_NODE_TYPES.CallExpression) return false;\n\n return (\n fromContext(source, expression.callee, context, 'rethrowIfCancelled') ||\n signalMember(source, expression.callee, context, 'throwIfAborted')\n );\n}\n\n/** Whether a statement written before the write in this block already left for a cancelled caller. */\nfunction guardedBlock(\n source: SourceCode,\n block: TSESTree.BlockStatement,\n write: TSESTree.Node,\n context: Context,\n): boolean {\n for (const statement of block.body) {\n if (within(write, statement)) return false;\n if (leavesOnCancellation(source, statement, context)) return true;\n }\n\n return false;\n}\n\n/**\n * A guard counts where it precedes the write in the block that holds it or in any block around that one, up to the\n * `catch` itself: `if (cause instanceof Refused) { rethrowIfCancelled(cause); update(failed, true) }` is guarded, and\n * a guard inside a block the write is not in guards nothing.\n */\nfunction guardedCatch(\n source: SourceCode,\n handler: TSESTree.CatchClause,\n write: TSESTree.Node,\n context: Context,\n): boolean {\n return source\n .getAncestors(write)\n .some(\n ancestor =>\n ancestor.type === AST_NODE_TYPES.BlockStatement &&\n within(ancestor, handler.body) &&\n guardedBlock(source, ancestor, write, context),\n );\n}\n\n/** Whether the protected block suspends the function the `try` belongs to; an `await` in a nested callback does not. */\nfunction awaits(source: SourceCode, statement: TSESTree.TryStatement, written: Written): boolean {\n const owner = enclosingFunction(source, statement);\n\n return written.barriers.some(\n barrier => within(barrier, statement.block) && enclosingFunction(source, barrier) === owner,\n );\n}\n\n/** Where this `try` holds the write it was reached from: a cleanup clause of an awaited block, or nowhere. */\nfunction clauseOf(\n source: SourceCode,\n statement: TSESTree.TryStatement,\n child: TSESTree.Node,\n write: TSESTree.CallExpression,\n context: Context,\n written: Written,\n): Clause | undefined {\n if (child === statement.block || !awaits(source, statement, written)) return undefined;\n if (child === statement.finalizer) return 'finally';\n\n const { handler } = statement;\n\n if (handler === null || child !== handler) return undefined;\n\n return guardedCatch(source, handler, write, context) ? undefined : 'catch';\n}\n\n/** The cleanup clause of the innermost awaited `try` that holds this write, walking out no further than the body. */\nfunction cleanupClause(\n source: SourceCode,\n write: TSESTree.CallExpression,\n body: FunctionNode,\n context: Context,\n written: Written,\n): Clause | undefined {\n let child: TSESTree.Node = write;\n\n for (const parent of source.getAncestors(write).reverse()) {\n if (parent === body) return undefined;\n\n const clause =\n parent.type === AST_NODE_TYPES.TryStatement\n ? clauseOf(source, parent, child, write, context, written)\n : undefined;\n\n if (clause !== undefined) return clause;\n\n child = parent;\n }\n\n return undefined;\n}\n\nconst captureCommandCleanup = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const written: Written = { barriers: [], calls: [], declarators: [] };\n\n const reportWrites = (body: FunctionNode, parallel: boolean): void => {\n const received = contextMembers(source, body, CONTEXT_POSITION, CONTEXT_KEYS, written.declarators);\n const messageId: MessageIds = parallel ? 'parallelCommandCleanup' : 'captureCommandCleanup';\n\n for (const call of written.calls) {\n if (!within(call, body) || !fromContext(source, call.callee, received, 'update')) continue;\n\n const clause = cleanupClause(source, call, body, received, written);\n\n if (clause !== undefined) context.report({ data: { clause }, messageId, node: call });\n }\n };\n\n return {\n AwaitExpression(node): void {\n written.barriers.push(node);\n },\n CallExpression(node): void {\n written.calls.push(node);\n },\n ForOfStatement(node): void {\n if (node.await) written.barriers.push(node);\n },\n 'Program:exit'(): void {\n for (const call of written.calls) {\n const named = methodPath(source, call.callee);\n\n if (named?.method !== 'command' || !isModelContextFunction(source, named.fn)) continue;\n\n const body = commandBody(source, call);\n\n if (body !== undefined) reportWrites(body, declaresParallel(call));\n }\n },\n VariableDeclarator(node): void {\n written.declarators.push(node);\n },\n };\n },\n meta: {\n docs: {\n description: 'A command writes the cleanup it owes after an `await` through a commit, not through its writer.',\n },\n messages: {\n captureCommandCleanup:\n 'This write is in the `{{clause}}` of a `try` that awaits, and the writer of a command lives only as long as its caller: once the call is cancelled the write is dropped without a record. A cleanup that must run for a cancelled call — a busy flag, a pending mark — goes through `const commit = execution.capture(state)` taken before the `await` and `commit.update(…)` here. An answer that must not land after cancellation — a failure, a result — belongs in a `catch` after `rethrowIfCancelled(cause)` (D288, D319, D330).',\n parallelCommandCleanup:\n \"This write is in the `{{clause}}` of a `try` that awaits in a `concurrency: 'parallel'` command, and once the call is cancelled it is dropped without a record. A commit does not fix it: calls of this command overlap, so the cleanup of a cancelled call would clear state another call still holds, and a flag shared by parallel calls is racy either way. Keep such state per call — keyed by its input — or leave progress to the consumer's `inFlight`. An answer that must not land after cancellation belongs in a `catch` after `rethrowIfCancelled(cause)` (D288, D330).\",\n },\n schema: [],\n type: 'problem',\n },\n name: 'capture-command-cleanup',\n});\n\nexport { captureCommandCleanup };\n"],"names":["CONTEXT_KEYS","CONTEXT_POSITION","declaresParallel","command","options","record","unwrap","concurrency","AST_NODE_TYPES","findProperty","value","signalMember","source","node","context","member","fromContext","exits","statement","last","leavesOnCancellation","expression","guardedBlock","block","write","within","guardedCatch","handler","ancestor","awaits","written","owner","enclosingFunction","barrier","clauseOf","child","cleanupClause","body","parent","clause","captureCommandCleanup","createRule","reportWrites","parallel","received","contextMembers","messageId","call","named","methodPath","isModelContextFunction","commandBody"],"mappings":"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"}
|
|
@@ -1,5 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
declare const defineFeaturePropertyOrder: TSESLint.RuleModule<"sectionOrder", [], unknown, TSESLint.RuleListener> & {
|
|
3
|
-
name: string;
|
|
4
|
-
};
|
|
1
|
+
declare const defineFeaturePropertyOrder: import("../rule.js").RuleModule<"sectionOrder", []>;
|
|
5
2
|
export { defineFeaturePropertyOrder };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as
|
|
1
|
+
import{AST_NODE_TYPES as f}from"@typescript-eslint/types";import{calleeName as u,propertyName as a}from"../ast.js";import{createRule as m}from"../rule.js";const d=["imports","requires","own","exports","provides","when","body"],l=new Map([["defineFeature",d],["defineFeature.body",["own","exports","provides"]]]);function c(t){const n=a(t);return n===void 0?-1:d.indexOf(n)}function h(t,n){const e=[];for(const r of t){const o=a(r);if(o===void 0||!n.includes(o))return;e.push(d.indexOf(o))}return e}function x(t){let n=-1;for(const[e,r]of t.entries()){if(r<n)return e;n=r}return-1}function y(t,n){return t.getCommentsInside(n).some(e=>!n.properties.some(r=>r.range[0]<=e.range[0]&&e.range[1]<=r.range[1]))}function*g(t,n,e){const r=[...e].sort((o,i)=>c(o)-c(i));for(const[o,i]of e.entries()){const s=r[o];s!==void 0&&s!==i&&(yield t.replaceText(i,n.getText(s)))}}const O=m({create(t){const n=(e,r)=>{const o=h(e.properties,r);if(o===void 0)return;const i=x(o),s=e.properties[i];s!==void 0&&t.report({data:{order:r.join(", ")},fix:y(t.sourceCode,e)?null:p=>[...g(p,t.sourceCode,e.properties)],messageId:"sectionOrder",node:s})};return{CallExpression(e){const[,r]=e.arguments,o=l.get(u(e)??"");o===void 0||r?.type!==f.ObjectExpression||n(r,o)}}},meta:{docs:{description:"The sections of `defineFeature` keep the declaration order of the specification."},fixable:"code",messages:{sectionOrder:"A feature declares its sections in one order: {{order}}. Type inference holds part of it: `exports` and `provides` read the `own` that TypeScript inferred from the sections written above them (spec \xA72.1, D186, D280, D477)."},schema:[],type:"layout"},name:"define-feature-property-order"});export{O as defineFeaturePropertyOrder};
|
|
2
2
|
//# sourceMappingURL=define-feature-property-order.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-feature-property-order.js","sources":["../../src/rules/define-feature-property-order.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"define-feature-property-order.js","sources":["../../src/rules/define-feature-property-order.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { calleeName, propertyName } from '../ast';\nimport type { RuleFix, RuleFixer, SourceCode } from '../rule';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'sectionOrder';\ntype Properties = readonly TSESTree.ObjectLiteralElement[];\n\n/**\n * The declaration order of the sections: the edges, the stages, then the lifetime and the loader that replaces the\n * last three stages. The identity is no longer among them — a feature names itself with its first argument (D280).\n * The specification names the sections; the order is the one a key sorter can also be configured to hold, so a host\n * runs one convention instead of two (D186). D477: it is not only a reading order. TypeScript contextually types the\n * sections of one object in the order they are written, so `exports` and `provides` above `own` read an `own` with\n * nothing in it; the type of that empty `own` says so, and this rule says it before the compiler does.\n *\n * D434: sections are written at two callees, not one. A body file writes three of them, and its call was read as\n * some other function's, so the one order held at `defineFeature` and nowhere else. Both are read against the same\n * order — the body's three are a subsequence of it — and each against its own list, because a refusal that\n * enumerates seven where three are taken names four keys the author cannot write there (D207).\n */\nconst SECTIONS = ['imports', 'requires', 'own', 'exports', 'provides', 'when', 'body'];\nconst CALLEE_SECTIONS = new Map([\n ['defineFeature', SECTIONS],\n ['defineFeature.body', ['own', 'exports', 'provides']],\n]);\n\nfunction sectionIndex(property: TSESTree.ObjectLiteralElement): number {\n const name = propertyName(property);\n\n return name === undefined ? -1 : SECTIONS.indexOf(name);\n}\n\n/** The declared sections in source order, or `undefined` when a key is not a section this callee places. */\nfunction sectionOrder(properties: Properties, sections: readonly string[]): readonly number[] | undefined {\n const order: number[] = [];\n\n for (const property of properties) {\n const name = propertyName(property);\n\n if (name === undefined || !sections.includes(name)) return undefined;\n\n order.push(SECTIONS.indexOf(name));\n }\n\n return order;\n}\n\nfunction firstMisplaced(order: readonly number[]): number {\n let previous = -1;\n\n for (const [index, value] of order.entries()) {\n if (value < previous) return index;\n\n previous = value;\n }\n\n return -1;\n}\n\n/**\n * A comment between two sections belongs to neither of them, so reordering would move it away from the line it\n * explains. Such an object is reported without a fix; a comment inside a section travels with it.\n */\nfunction hasSeparatingComment(sourceCode: SourceCode, object: TSESTree.ObjectExpression): boolean {\n return sourceCode\n .getCommentsInside(object)\n .some(\n comment =>\n !object.properties.some(\n property => property.range[0] <= comment.range[0] && comment.range[1] <= property.range[1],\n ),\n );\n}\n\nfunction* reorder(fixer: RuleFixer, sourceCode: SourceCode, properties: Properties): Generator<RuleFix> {\n const sorted = [...properties].sort((left, right) => sectionIndex(left) - sectionIndex(right));\n\n for (const [index, property] of properties.entries()) {\n const replacement = sorted[index];\n\n if (replacement !== undefined && replacement !== property) {\n yield fixer.replaceText(property, sourceCode.getText(replacement));\n }\n }\n}\n\nconst defineFeaturePropertyOrder = createRule<[], MessageIds>({\n create(context) {\n const check = (object: TSESTree.ObjectExpression, sections: readonly string[]): void => {\n const order = sectionOrder(object.properties, sections);\n\n if (order === undefined) return;\n\n const misplaced = firstMisplaced(order);\n const property = object.properties[misplaced];\n\n if (property === undefined) return;\n\n context.report({\n data: { order: sections.join(', ') },\n fix: hasSeparatingComment(context.sourceCode, object)\n ? null\n : fixer => [...reorder(fixer, context.sourceCode, object.properties)],\n messageId: 'sectionOrder',\n node: property,\n });\n };\n\n return {\n CallExpression(node): void {\n // D280, D207: the identity is the first argument of both callees, so the sections are the second.\n const [, sections] = node.arguments;\n const declared = CALLEE_SECTIONS.get(calleeName(node) ?? '');\n\n if (declared === undefined || sections?.type !== AST_NODE_TYPES.ObjectExpression) return;\n\n check(sections, declared);\n },\n };\n },\n meta: {\n docs: {\n description: 'The sections of `defineFeature` keep the declaration order of the specification.',\n },\n fixable: 'code',\n messages: {\n sectionOrder:\n 'A feature declares its sections in one order: {{order}}. Type inference holds part of it: `exports` and ' +\n '`provides` read the `own` that TypeScript inferred from the sections written above them (spec §2.1, D186, ' +\n 'D280, D477).',\n },\n schema: [],\n type: 'layout',\n },\n name: 'define-feature-property-order',\n});\n\nexport { defineFeaturePropertyOrder };\n"],"names":["SECTIONS","CALLEE_SECTIONS","sectionIndex","property","name","propertyName","sectionOrder","properties","sections","order","firstMisplaced","previous","index","value","hasSeparatingComment","sourceCode","object","comment","reorder","fixer","sorted","left","right","replacement","defineFeaturePropertyOrder","createRule","context","check","misplaced","node","declared","calleeName","AST_NODE_TYPES"],"mappings":"2JAuBA,MAAMA,EAAW,CAAC,UAAW,WAAY,MAAO,UAAW,WAAY,OAAQ,MAAM,EAC/EC,EAAkB,IAAI,IAAI,CAC9B,CAAC,gBAAiBD,CAAQ,EAC1B,CAAC,qBAAsB,CAAC,MAAO,UAAW,UAAU,CAAC,CACtD,CAAA,EAED,SAASE,EAAaC,EAAuC,CAC3D,MAAMC,EAAOC,EAAaF,CAAQ,EAElC,OAAOC,IAAS,OAAY,GAAKJ,EAAS,QAAQI,CAAI,CACxD,CAGA,SAASE,EAAaC,EAAwBC,EAA2B,CACvE,MAAMC,EAAkB,CAAA,EAExB,UAAWN,KAAYI,EAAY,CACjC,MAAMH,EAAOC,EAAaF,CAAQ,EAElC,GAAIC,IAAS,QAAa,CAACI,EAAS,SAASJ,CAAI,EAAG,OAEpDK,EAAM,KAAKT,EAAS,QAAQI,CAAI,CAAC,CACnC,CAEA,OAAOK,CACT,CAEA,SAASC,EAAeD,EAAwB,CAC9C,IAAIE,EAAW,GAEf,SAAW,CAACC,EAAOC,CAAK,IAAKJ,EAAM,QAAO,EAAI,CAC5C,GAAII,EAAQF,EAAU,OAAOC,EAE7BD,EAAWE,CACb,CAEA,MAAO,EACT,CAMA,SAASC,EAAqBC,EAAwBC,EAAiC,CACrF,OAAOD,EACJ,kBAAkBC,CAAM,EACxB,KACCC,GACE,CAACD,EAAO,WAAW,KACjBb,GAAYA,EAAS,MAAM,CAAC,GAAKc,EAAQ,MAAM,CAAC,GAAKA,EAAQ,MAAM,CAAC,GAAKd,EAAS,MAAM,CAAC,CAAC,CAC3F,CAET,CAEA,SAAUe,EAAQC,EAAkBJ,EAAwBR,EAAsB,CAChF,MAAMa,EAAS,CAAC,GAAGb,CAAU,EAAE,KAAK,CAACc,EAAMC,IAAUpB,EAAamB,CAAI,EAAInB,EAAaoB,CAAK,CAAC,EAE7F,SAAW,CAACV,EAAOT,CAAQ,IAAKI,EAAW,QAAO,EAAI,CACpD,MAAMgB,EAAcH,EAAOR,CAAK,EAE5BW,IAAgB,QAAaA,IAAgBpB,IAC/C,MAAMgB,EAAM,YAAYhB,EAAUY,EAAW,QAAQQ,CAAW,CAAC,EAErE,CACF,CAEA,MAAMC,EAA6BC,EAA2B,CAC5D,OAAOC,EAAO,CACZ,MAAMC,EAAQ,CAACX,EAAmCR,IAAqC,CACrF,MAAMC,EAAQH,EAAaU,EAAO,WAAYR,CAAQ,EAEtD,GAAIC,IAAU,OAAW,OAEzB,MAAMmB,EAAYlB,EAAeD,CAAK,EAChCN,EAAWa,EAAO,WAAWY,CAAS,EAExCzB,IAAa,QAEjBuB,EAAQ,OAAO,CACb,KAAM,CAAE,MAAOlB,EAAS,KAAK,IAAI,CAAC,EAClC,IAAKM,EAAqBY,EAAQ,WAAYV,CAAM,EAChD,KACAG,GAAS,CAAC,GAAGD,EAAQC,EAAOO,EAAQ,WAAYV,EAAO,UAAU,CAAC,EACtE,UAAW,eACX,KAAMb,CACP,CAAA,CACH,EAEA,MAAO,CACL,eAAe0B,EAAI,CAEjB,KAAM,CAAA,CAAGrB,CAAQ,EAAIqB,EAAK,UACpBC,EAAW7B,EAAgB,IAAI8B,EAAWF,CAAI,GAAK,EAAE,EAEvDC,IAAa,QAAatB,GAAU,OAASwB,EAAe,kBAEhEL,EAAMnB,EAAUsB,CAAQ,CAC1B,EAEJ,EACA,KAAM,CACJ,KAAM,CACJ,YAAa,kFACd,EACD,QAAS,OACT,SAAU,CACR,aACE,mOAGH,EACD,OAAQ,CAAA,EACR,KAAM,QACP,EACD,KAAM,+BACP,CAAA"}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
1
|
type MessageIds = 'enabledIsAsync' | 'enabledReturnsBareSource' | 'enabledReturnsSource';
|
|
3
|
-
declare const enabledPredicate:
|
|
4
|
-
name: string;
|
|
5
|
-
};
|
|
2
|
+
declare const enabledPredicate: import("../rule.js").RuleModule<MessageIds, []>;
|
|
6
3
|
export { enabledPredicate };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as i}from"@typescript-eslint/
|
|
1
|
+
import{AST_NODE_TYPES as i}from"@typescript-eslint/types";import{bodyRecordEntries as E}from"../feature-body-records.js";import{isFunction as b,localValue as h}from"../model-bindings.js";import{propertyName as y,findProperty as I,patternKeys as f,calleeName as C,memberRoot as S,memberFirstStep as g}from"../ast.js";import{createRule as x}from"../rule.js";const R=new Set(["pipe","register","slot"]),T=new Set(["exports","imports","own","read"]),w=new Set(["exports","imports","own"]);function A(e){const{parent:n}=e;if(n.type!==i.CallExpression||n.arguments.length<2)return!1;const t=C(n);return t!==void 0&&R.has(t)&&n.arguments.at(-1)===e}function m(e){const[n]=e.params;if(!(e.params.length!==1||n?.type!==i.ObjectPattern))return[...f(n)].some(t=>T.has(t))?n:void 0}function O(e){const[n]=e.params;if(!(e.params.length!==1||n?.type!==i.Identifier))return n}function N(e,n){const t=S(n),o=g(n);return t.type!==i.Identifier||t.name!==e.name?!1:o!==void 0&&w.has(o)}function P(e,n){const t=S(n);return t.type!==i.Identifier?!1:w.has(t.name)&&f(e).has(t.name)}function B(e,n){const t=m(e);if(t!==void 0)return P(t,n)?"read":void 0;const o=O(e);if(o!==void 0)return N(o,n)?`${o.name}.read`:void 0}function D(e,n){const t=n.properties.find(u=>{const r=y(u);return r===void 0||r>"read"});if(t!==void 0)return e.insertTextBefore(t,"read, ");const o=n.properties.at(-1);return o===void 0?void 0:e.insertTextAfter(o,", read")}function v(e,n){return e.params.some(t=>t.type===i.Identifier&&t.name===n.name)}function _(e,n){const{parent:t}=e;return t.type===i.ObjectExpression&&A(t)||m(n)!==void 0}const F=x({create(e){const n=[],t=new Set,o=(r,s)=>{const d=B(r,s);d!==void 0&&e.report({fix(c){const a=m(r),p=c.replaceText(s,`${d}(${e.sourceCode.getText(s)})`);if(a===void 0||f(a).has("read"))return[p];const l=D(c,a);return l===void 0?[p]:[p,l]},messageId:"enabledReturnsSource",node:s})},u=r=>{if(t.has(r))return;if(t.add(r),r.async){e.report({messageId:"enabledIsAsync",node:r});return}if(r.type!==i.ArrowFunctionExpression)return;const{body:s}=r;if(s.type===i.MemberExpression){o(r,s);return}s.type===i.Identifier&&!v(r,s)&&e.report({messageId:"enabledReturnsBareSource",node:s})};return{CallExpression(r){n.push(r)},"Program:exit"(){for(const r of n)for(const s of E(e.sourceCode,r)){if(s.options===void 0)continue;const d=h(e.sourceCode,s.options);if(d.type!==i.ObjectExpression)continue;const c=I(d,"enabled");if(c===void 0)continue;const a=h(e.sourceCode,c.value);b(a)&&u(a)}},Property(r){y(r)!=="enabled"||!b(r.value)||_(r,r.value)&&u(r.value)}}},meta:{docs:{description:"A contribution `enabled` predicate answers the visibility fact, not the source that carries it."},fixable:"code",messages:{enabledIsAsync:"A contribution `enabled` predicate answers a boolean synchronously; an asynchronous one answers a promise (D220).",enabledReturnsBareSource:"A contribution `enabled` takes a `Readable<boolean>` as it is; a predicate must answer the fact with `read(...)` (D220).",enabledReturnsSource:"A contribution `enabled` predicate returns the source instead of the fact; read it with `read(...)` (D220)."},schema:[],type:"problem"},name:"enabled-predicate"});export{F as enabledPredicate};
|
|
2
2
|
//# sourceMappingURL=enabled-predicate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"enabled-predicate.js","sources":["../../src/rules/enabled-predicate.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { bodyRecordEntries } from '../feature-body-records';\nimport type { FunctionNode } from '../model-bindings';\nimport { isFunction, localValue } from '../model-bindings';\nimport { calleeName, findProperty, memberFirstStep, memberRoot, patternKeys, propertyName } from '../ast';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'enabledIsAsync' | 'enabledReturnsBareSource' | 'enabledReturnsSource';\n\nconst CONTRIBUTION_BUILDERS = new Set(['pipe', 'register', 'slot']);\nconst CONTEXT_KEYS = new Set(['exports', 'imports', 'own', 'read']);\nconst CONTEXT_SOURCES = new Set(['exports', 'imports', 'own']);\n\n/** The options object of `slot(target, contribution, {…})`, `pipe(target, {fold}, {…})`, `register(…)`. */\nfunction isBuilderOptions(node: TSESTree.ObjectExpression): boolean {\n const { parent } = node;\n\n if (parent.type !== AST_NODE_TYPES.CallExpression || parent.arguments.length < 2) return false;\n\n const name = calleeName(parent);\n\n return name !== undefined && CONTRIBUTION_BUILDERS.has(name) && parent.arguments.at(-1) === node;\n}\n\n/** The destructured evaluation context of a contribution predicate: `({ exports, imports, own, read })`. */\nfunction contextPattern(fn: FunctionNode): TSESTree.ObjectPattern | undefined {\n const [parameter] = fn.params;\n\n if (fn.params.length !== 1 || parameter?.type !== AST_NODE_TYPES.ObjectPattern) return undefined;\n\n return [...patternKeys(parameter)].some(key => CONTEXT_KEYS.has(key)) ? parameter : undefined;\n}\n\n/** The same context taken as one named parameter: `context => context.imports.platform.submitAllowed`. */\nfunction contextIdentifier(fn: FunctionNode): TSESTree.Identifier | undefined {\n const [parameter] = fn.params;\n\n if (fn.params.length !== 1 || parameter?.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n return parameter;\n}\n\nfunction isNamedContextAccess(parameter: TSESTree.Identifier, body: TSESTree.MemberExpression): boolean {\n const root = memberRoot(body);\n const step = memberFirstStep(body);\n\n if (root.type !== AST_NODE_TYPES.Identifier || root.name !== parameter.name) return false;\n\n return step !== undefined && CONTEXT_SOURCES.has(step);\n}\n\nfunction isDestructuredContextAccess(pattern: TSESTree.ObjectPattern, body: TSESTree.MemberExpression): boolean {\n const root = memberRoot(body);\n\n if (root.type !== AST_NODE_TYPES.Identifier) return false;\n\n return CONTEXT_SOURCES.has(root.name) && patternKeys(pattern).has(root.name);\n}\n\n/** The reader a fix would call, or `undefined` when the body reaches something other than the instance context. */\nfunction readerFor(fn: FunctionNode, body: TSESTree.MemberExpression): string | undefined {\n const pattern = contextPattern(fn);\n\n if (pattern !== undefined) return isDestructuredContextAccess(pattern, body) ? 'read' : undefined;\n\n const parameter = contextIdentifier(fn);\n\n if (parameter === undefined) return undefined;\n\n return isNamedContextAccess(parameter, body) ? `${parameter.name}.read` : undefined;\n}\n\n/** `read` joins the destructured context in place, so the fixed predicate keeps the order authors write. */\nfunction insertReadKey(fixer: TSESLint.RuleFixer, pattern: TSESTree.ObjectPattern): TSESLint.RuleFix | undefined {\n const successor = pattern.properties.find(property => {\n const name = propertyName(property);\n\n return name === undefined || name > 'read';\n });\n\n if (successor !== undefined) return fixer.insertTextBefore(successor, 'read, ');\n\n const last = pattern.properties.at(-1);\n\n return last === undefined ? undefined : fixer.insertTextAfter(last, ', read');\n}\n\nfunction isOwnParameter(fn: FunctionNode, identifier: TSESTree.Identifier): boolean {\n return fn.params.some(\n parameter => parameter.type === AST_NODE_TYPES.Identifier && parameter.name === identifier.name,\n );\n}\n\nfunction isContributionEnabled(property: TSESTree.Property, fn: FunctionNode): boolean {\n const { parent } = property;\n\n return (\n (parent.type === AST_NODE_TYPES.ObjectExpression && isBuilderOptions(parent)) || contextPattern(fn) !== undefined\n );\n}\n\nconst enabledPredicate = createRule<[], MessageIds>({\n create(context) {\n const calls: TSESTree.CallExpression[] = [];\n const checked = new Set<FunctionNode>();\n const reportSource = (fn: FunctionNode, body: TSESTree.MemberExpression): void => {\n const reader = readerFor(fn, body);\n\n if (reader === undefined) return;\n\n context.report({\n fix(fixer) {\n const pattern = contextPattern(fn);\n const wrapped = fixer.replaceText(body, `${reader}(${context.sourceCode.getText(body)})`);\n\n if (pattern === undefined || patternKeys(pattern).has('read')) return [wrapped];\n\n const inserted = insertReadKey(fixer, pattern);\n\n return inserted === undefined ? [wrapped] : [wrapped, inserted];\n },\n messageId: 'enabledReturnsSource',\n node: body,\n });\n };\n\n const check = (fn: FunctionNode): void => {\n if (checked.has(fn)) return;\n checked.add(fn);\n if (fn.async) {\n context.report({ messageId: 'enabledIsAsync', node: fn });\n\n return;\n }\n\n if (fn.type !== AST_NODE_TYPES.ArrowFunctionExpression) return;\n\n const { body } = fn;\n\n if (body.type === AST_NODE_TYPES.MemberExpression) {\n reportSource(fn, body);\n\n return;\n }\n\n if (body.type === AST_NODE_TYPES.Identifier && !isOwnParameter(fn, body)) {\n context.report({ messageId: 'enabledReturnsBareSource', node: body });\n }\n };\n\n return {\n CallExpression(node): void {\n calls.push(node);\n },\n 'Program:exit'(): void {\n for (const call of calls) {\n for (const entry of bodyRecordEntries(context.sourceCode, call)) {\n if (entry.options === undefined) continue;\n const options = localValue(context.sourceCode, entry.options);\n if (options.type !== AST_NODE_TYPES.ObjectExpression) continue;\n const enabled = findProperty(options, 'enabled');\n if (enabled === undefined) continue;\n const predicate = localValue(context.sourceCode, enabled.value);\n if (isFunction(predicate)) check(predicate);\n }\n }\n },\n Property(node): void {\n if (propertyName(node) !== 'enabled' || !isFunction(node.value)) return;\n\n if (isContributionEnabled(node, node.value)) check(node.value);\n },\n };\n },\n meta: {\n docs: {\n description: 'A contribution `enabled` predicate answers the visibility fact, not the source that carries it.',\n },\n fixable: 'code',\n messages: {\n enabledIsAsync:\n 'A contribution `enabled` predicate answers a boolean synchronously; an asynchronous one answers a promise (D220).',\n enabledReturnsBareSource:\n 'A contribution `enabled` takes a `Readable<boolean>` as it is; a predicate must answer the fact with `read(...)` (D220).',\n enabledReturnsSource:\n 'A contribution `enabled` predicate returns the source instead of the fact; read it with `read(...)` (D220).',\n },\n schema: [],\n type: 'problem',\n },\n name: 'enabled-predicate',\n});\n\nexport { enabledPredicate };\n"],"names":["CONTRIBUTION_BUILDERS","CONTEXT_KEYS","CONTEXT_SOURCES","isBuilderOptions","node","parent","AST_NODE_TYPES","name","calleeName","contextPattern","fn","parameter","patternKeys","key","contextIdentifier","isNamedContextAccess","body","root","memberRoot","step","memberFirstStep","isDestructuredContextAccess","pattern","readerFor","insertReadKey","fixer","successor","property","propertyName","last","isOwnParameter","identifier","isContributionEnabled","enabledPredicate","createRule","context","calls","checked","reportSource","reader","wrapped","inserted","check","call","entry","bodyRecordEntries","options","localValue","enabled","findProperty","predicate","isFunction"],"mappings":"oWAWA,MAAMA,EAAwB,IAAI,IAAI,CAAC,OAAQ,WAAY,MAAM,CAAC,EAC5DC,EAAe,IAAI,IAAI,CAAC,UAAW,UAAW,MAAO,MAAM,CAAC,EAC5DC,EAAkB,IAAI,IAAI,CAAC,UAAW,UAAW,KAAK,CAAC,EAG7D,SAASC,EAAiBC,EAA+B,CACvD,KAAM,CAAE,OAAAC,CAAM,EAAKD,EAEnB,GAAIC,EAAO,OAASC,EAAe,gBAAkBD,EAAO,UAAU,OAAS,EAAG,MAAO,GAEzF,MAAME,EAAOC,EAAWH,CAAM,EAE9B,OAAOE,IAAS,QAAaP,EAAsB,IAAIO,CAAI,GAAKF,EAAO,UAAU,GAAG,EAAE,IAAMD,CAC9F,CAGA,SAASK,EAAeC,EAAgB,CACtC,KAAM,CAACC,CAAS,EAAID,EAAG,OAEvB,GAAI,EAAAA,EAAG,OAAO,SAAW,GAAKC,GAAW,OAASL,EAAe,eAEjE,MAAO,CAAC,GAAGM,EAAYD,CAAS,CAAC,EAAE,KAAKE,GAAOZ,EAAa,IAAIY,CAAG,CAAC,EAAIF,EAAY,MACtF,CAGA,SAASG,EAAkBJ,EAAgB,CACzC,KAAM,CAACC,CAAS,EAAID,EAAG,OAEvB,GAAI,EAAAA,EAAG,OAAO,SAAW,GAAKC,GAAW,OAASL,EAAe,YAEjE,OAAOK,CACT,CAEA,SAASI,EAAqBJ,EAAgCK,EAA+B,CAC3F,MAAMC,EAAOC,EAAWF,CAAI,EACtBG,EAAOC,EAAgBJ,CAAI,EAEjC,OAAIC,EAAK,OAASX,EAAe,YAAcW,EAAK,OAASN,EAAU,KAAa,GAE7EQ,IAAS,QAAajB,EAAgB,IAAIiB,CAAI,CACvD,CAEA,SAASE,EAA4BC,EAAiCN,EAA+B,CACnG,MAAMC,EAAOC,EAAWF,CAAI,EAE5B,OAAIC,EAAK,OAASX,EAAe,WAAmB,GAE7CJ,EAAgB,IAAIe,EAAK,IAAI,GAAKL,EAAYU,CAAO,EAAE,IAAIL,EAAK,IAAI,CAC7E,CAGA,SAASM,EAAUb,EAAkBM,EAA+B,CAClE,MAAMM,EAAUb,EAAeC,CAAE,EAEjC,GAAIY,IAAY,OAAW,OAAOD,EAA4BC,EAASN,CAAI,EAAI,OAAS,OAExF,MAAML,EAAYG,EAAkBJ,CAAE,EAEtC,GAAIC,IAAc,OAElB,OAAOI,EAAqBJ,EAAWK,CAAI,EAAI,GAAGL,EAAU,IAAI,QAAU,MAC5E,CAGA,SAASa,EAAcC,EAA2BH,EAA+B,CAC/E,MAAMI,EAAYJ,EAAQ,WAAW,KAAKK,GAAW,CACnD,MAAMpB,EAAOqB,EAAaD,CAAQ,EAElC,OAAOpB,IAAS,QAAaA,EAAO,MACtC,CAAC,EAED,GAAImB,IAAc,OAAW,OAAOD,EAAM,iBAAiBC,EAAW,QAAQ,EAE9E,MAAMG,EAAOP,EAAQ,WAAW,GAAG,EAAE,EAErC,OAAOO,IAAS,OAAY,OAAYJ,EAAM,gBAAgBI,EAAM,QAAQ,CAC9E,CAEA,SAASC,EAAepB,EAAkBqB,EAA+B,CACvE,OAAOrB,EAAG,OAAO,KACfC,GAAaA,EAAU,OAASL,EAAe,YAAcK,EAAU,OAASoB,EAAW,IAAI,CAEnG,CAEA,SAASC,EAAsBL,EAA6BjB,EAAgB,CAC1E,KAAM,CAAE,OAAAL,CAAM,EAAKsB,EAEnB,OACGtB,EAAO,OAASC,EAAe,kBAAoBH,EAAiBE,CAAM,GAAMI,EAAeC,CAAE,IAAM,MAE5G,CAEA,MAAMuB,EAAmBC,EAA2B,CAClD,OAAOC,EAAO,CACZ,MAAMC,EAAmC,CAAA,EACnCC,EAAU,IAAI,IACdC,EAAe,CAAC5B,EAAkBM,IAAyC,CAC/E,MAAMuB,EAAShB,EAAUb,EAAIM,CAAI,EAE7BuB,IAAW,QAEfJ,EAAQ,OAAO,CACb,IAAIV,EAAK,CACP,MAAMH,EAAUb,EAAeC,CAAE,EAC3B8B,EAAUf,EAAM,YAAYT,EAAM,GAAGuB,CAAM,IAAIJ,EAAQ,WAAW,QAAQnB,CAAI,CAAC,GAAG,EAExF,GAAIM,IAAY,QAAaV,EAAYU,CAAO,EAAE,IAAI,MAAM,EAAG,MAAO,CAACkB,CAAO,EAE9E,MAAMC,EAAWjB,EAAcC,EAAOH,CAAO,EAE7C,OAAOmB,IAAa,OAAY,CAACD,CAAO,EAAI,CAACA,EAASC,CAAQ,CAChE,EACA,UAAW,uBACX,KAAMzB,CACP,CAAA,CACH,EAEM0B,EAAShC,GAA0B,CACvC,GAAI2B,EAAQ,IAAI3B,CAAE,EAAG,OAErB,GADA2B,EAAQ,IAAI3B,CAAE,EACVA,EAAG,MAAO,CACZyB,EAAQ,OAAO,CAAE,UAAW,iBAAkB,KAAMzB,EAAI,EAExD,MACF,CAEA,GAAIA,EAAG,OAASJ,EAAe,wBAAyB,OAExD,KAAM,CAAE,KAAAU,CAAI,EAAKN,EAEjB,GAAIM,EAAK,OAASV,EAAe,iBAAkB,CACjDgC,EAAa5B,EAAIM,CAAI,EAErB,MACF,CAEIA,EAAK,OAASV,EAAe,YAAc,CAACwB,EAAepB,EAAIM,CAAI,GACrEmB,EAAQ,OAAO,CAAE,UAAW,2BAA4B,KAAMnB,EAAM,CAExE,EAEA,MAAO,CACL,eAAeZ,EAAI,CACjBgC,EAAM,KAAKhC,CAAI,CACjB,EACA,gBAAc,CACZ,UAAWuC,KAAQP,EACjB,UAAWQ,KAASC,EAAkBV,EAAQ,WAAYQ,CAAI,EAAG,CAC/D,GAAIC,EAAM,UAAY,OAAW,SACjC,MAAME,EAAUC,EAAWZ,EAAQ,WAAYS,EAAM,OAAO,EAC5D,GAAIE,EAAQ,OAASxC,EAAe,iBAAkB,SACtD,MAAM0C,EAAUC,EAAaH,EAAS,SAAS,EAC/C,GAAIE,IAAY,OAAW,SAC3B,MAAME,EAAYH,EAAWZ,EAAQ,WAAYa,EAAQ,KAAK,EAC1DG,EAAWD,CAAS,GAAGR,EAAMQ,CAAS,CAC5C,CAEJ,EACA,SAAS9C,EAAI,CACPwB,EAAaxB,CAAI,IAAM,WAAa,CAAC+C,EAAW/C,EAAK,KAAK,GAE1D4B,EAAsB5B,EAAMA,EAAK,KAAK,GAAGsC,EAAMtC,EAAK,KAAK,CAC/D,EAEJ,EACA,KAAM,CACJ,KAAM,CACJ,YAAa,iGACd,EACD,QAAS,OACT,SAAU,CACR,eACE,oHACF,yBACE,2HACF,qBACE,6GACH,EACD,OAAQ,CAAA,EACR,KAAM,SACP,EACD,KAAM,mBACP,CAAA"}
|
|
1
|
+
{"version":3,"file":"enabled-predicate.js","sources":["../../src/rules/enabled-predicate.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { bodyRecordEntries } from '../feature-body-records';\nimport type { FunctionNode } from '../model-bindings';\nimport { isFunction, localValue } from '../model-bindings';\nimport { calleeName, findProperty, memberFirstStep, memberRoot, patternKeys, propertyName } from '../ast';\nimport type { RuleFix, RuleFixer } from '../rule';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'enabledIsAsync' | 'enabledReturnsBareSource' | 'enabledReturnsSource';\n\nconst CONTRIBUTION_BUILDERS = new Set(['pipe', 'register', 'slot']);\nconst CONTEXT_KEYS = new Set(['exports', 'imports', 'own', 'read']);\nconst CONTEXT_SOURCES = new Set(['exports', 'imports', 'own']);\n\n/** The options object of `slot(target, contribution, {…})`, `pipe(target, {fold}, {…})`, `register(…)`. */\nfunction isBuilderOptions(node: TSESTree.ObjectExpression): boolean {\n const { parent } = node;\n\n if (parent.type !== AST_NODE_TYPES.CallExpression || parent.arguments.length < 2) return false;\n\n const name = calleeName(parent);\n\n return name !== undefined && CONTRIBUTION_BUILDERS.has(name) && parent.arguments.at(-1) === node;\n}\n\n/** The destructured evaluation context of a contribution predicate: `({ exports, imports, own, read })`. */\nfunction contextPattern(fn: FunctionNode): TSESTree.ObjectPattern | undefined {\n const [parameter] = fn.params;\n\n if (fn.params.length !== 1 || parameter?.type !== AST_NODE_TYPES.ObjectPattern) return undefined;\n\n return [...patternKeys(parameter)].some(key => CONTEXT_KEYS.has(key)) ? parameter : undefined;\n}\n\n/** The same context taken as one named parameter: `context => context.imports.platform.submitAllowed`. */\nfunction contextIdentifier(fn: FunctionNode): TSESTree.Identifier | undefined {\n const [parameter] = fn.params;\n\n if (fn.params.length !== 1 || parameter?.type !== AST_NODE_TYPES.Identifier) return undefined;\n\n return parameter;\n}\n\nfunction isNamedContextAccess(parameter: TSESTree.Identifier, body: TSESTree.MemberExpression): boolean {\n const root = memberRoot(body);\n const step = memberFirstStep(body);\n\n if (root.type !== AST_NODE_TYPES.Identifier || root.name !== parameter.name) return false;\n\n return step !== undefined && CONTEXT_SOURCES.has(step);\n}\n\nfunction isDestructuredContextAccess(pattern: TSESTree.ObjectPattern, body: TSESTree.MemberExpression): boolean {\n const root = memberRoot(body);\n\n if (root.type !== AST_NODE_TYPES.Identifier) return false;\n\n return CONTEXT_SOURCES.has(root.name) && patternKeys(pattern).has(root.name);\n}\n\n/** The reader a fix would call, or `undefined` when the body reaches something other than the instance context. */\nfunction readerFor(fn: FunctionNode, body: TSESTree.MemberExpression): string | undefined {\n const pattern = contextPattern(fn);\n\n if (pattern !== undefined) return isDestructuredContextAccess(pattern, body) ? 'read' : undefined;\n\n const parameter = contextIdentifier(fn);\n\n if (parameter === undefined) return undefined;\n\n return isNamedContextAccess(parameter, body) ? `${parameter.name}.read` : undefined;\n}\n\n/** `read` joins the destructured context in place, so the fixed predicate keeps the order authors write. */\nfunction insertReadKey(fixer: RuleFixer, pattern: TSESTree.ObjectPattern): RuleFix | undefined {\n const successor = pattern.properties.find(property => {\n const name = propertyName(property);\n\n return name === undefined || name > 'read';\n });\n\n if (successor !== undefined) return fixer.insertTextBefore(successor, 'read, ');\n\n const last = pattern.properties.at(-1);\n\n return last === undefined ? undefined : fixer.insertTextAfter(last, ', read');\n}\n\nfunction isOwnParameter(fn: FunctionNode, identifier: TSESTree.Identifier): boolean {\n return fn.params.some(\n parameter => parameter.type === AST_NODE_TYPES.Identifier && parameter.name === identifier.name,\n );\n}\n\nfunction isContributionEnabled(property: TSESTree.Property, fn: FunctionNode): boolean {\n const { parent } = property;\n\n return (\n (parent.type === AST_NODE_TYPES.ObjectExpression && isBuilderOptions(parent)) || contextPattern(fn) !== undefined\n );\n}\n\nconst enabledPredicate = createRule<[], MessageIds>({\n create(context) {\n const calls: TSESTree.CallExpression[] = [];\n const checked = new Set<FunctionNode>();\n const reportSource = (fn: FunctionNode, body: TSESTree.MemberExpression): void => {\n const reader = readerFor(fn, body);\n\n if (reader === undefined) return;\n\n context.report({\n fix(fixer) {\n const pattern = contextPattern(fn);\n const wrapped = fixer.replaceText(body, `${reader}(${context.sourceCode.getText(body)})`);\n\n if (pattern === undefined || patternKeys(pattern).has('read')) return [wrapped];\n\n const inserted = insertReadKey(fixer, pattern);\n\n return inserted === undefined ? [wrapped] : [wrapped, inserted];\n },\n messageId: 'enabledReturnsSource',\n node: body,\n });\n };\n\n const check = (fn: FunctionNode): void => {\n if (checked.has(fn)) return;\n checked.add(fn);\n if (fn.async) {\n context.report({ messageId: 'enabledIsAsync', node: fn });\n\n return;\n }\n\n if (fn.type !== AST_NODE_TYPES.ArrowFunctionExpression) return;\n\n const { body } = fn;\n\n if (body.type === AST_NODE_TYPES.MemberExpression) {\n reportSource(fn, body);\n\n return;\n }\n\n if (body.type === AST_NODE_TYPES.Identifier && !isOwnParameter(fn, body)) {\n context.report({ messageId: 'enabledReturnsBareSource', node: body });\n }\n };\n\n return {\n CallExpression(node): void {\n calls.push(node);\n },\n 'Program:exit'(): void {\n for (const call of calls) {\n for (const entry of bodyRecordEntries(context.sourceCode, call)) {\n if (entry.options === undefined) continue;\n const options = localValue(context.sourceCode, entry.options);\n if (options.type !== AST_NODE_TYPES.ObjectExpression) continue;\n const enabled = findProperty(options, 'enabled');\n if (enabled === undefined) continue;\n const predicate = localValue(context.sourceCode, enabled.value);\n if (isFunction(predicate)) check(predicate);\n }\n }\n },\n Property(node): void {\n if (propertyName(node) !== 'enabled' || !isFunction(node.value)) return;\n\n if (isContributionEnabled(node, node.value)) check(node.value);\n },\n };\n },\n meta: {\n docs: {\n description: 'A contribution `enabled` predicate answers the visibility fact, not the source that carries it.',\n },\n fixable: 'code',\n messages: {\n enabledIsAsync:\n 'A contribution `enabled` predicate answers a boolean synchronously; an asynchronous one answers a promise (D220).',\n enabledReturnsBareSource:\n 'A contribution `enabled` takes a `Readable<boolean>` as it is; a predicate must answer the fact with `read(...)` (D220).',\n enabledReturnsSource:\n 'A contribution `enabled` predicate returns the source instead of the fact; read it with `read(...)` (D220).',\n },\n schema: [],\n type: 'problem',\n },\n name: 'enabled-predicate',\n});\n\nexport { enabledPredicate };\n"],"names":["CONTRIBUTION_BUILDERS","CONTEXT_KEYS","CONTEXT_SOURCES","isBuilderOptions","node","parent","AST_NODE_TYPES","name","calleeName","contextPattern","fn","parameter","patternKeys","key","contextIdentifier","isNamedContextAccess","body","root","memberRoot","step","memberFirstStep","isDestructuredContextAccess","pattern","readerFor","insertReadKey","fixer","successor","property","propertyName","last","isOwnParameter","identifier","isContributionEnabled","enabledPredicate","createRule","context","calls","checked","reportSource","reader","wrapped","inserted","check","call","entry","bodyRecordEntries","options","localValue","enabled","findProperty","predicate","isFunction"],"mappings":"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"}
|
|
@@ -2,7 +2,5 @@ type MessageIds = 'invalidId' | 'missingPrefix';
|
|
|
2
2
|
type Options = readonly [{
|
|
3
3
|
readonly prefix?: string;
|
|
4
4
|
}];
|
|
5
|
-
declare const idNaming: import("
|
|
6
|
-
name: string;
|
|
7
|
-
};
|
|
5
|
+
declare const idNaming: import("../rule.js").RuleModule<MessageIds, Options>;
|
|
8
6
|
export { idNaming };
|
|
@@ -2,7 +2,5 @@ type Layer = 'integration' | 'models' | 'ui';
|
|
|
2
2
|
type Options = readonly [{
|
|
3
3
|
readonly layer?: Layer;
|
|
4
4
|
}];
|
|
5
|
-
declare const layerPlacement: import("
|
|
6
|
-
name: string;
|
|
7
|
-
};
|
|
5
|
+
declare const layerPlacement: import("../rule.js").RuleModule<"misplacedDeclaration", Options>;
|
|
8
6
|
export { layerPlacement };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D494: the integration layer composes — `imports`, `requires`, `model(Declaration, factory)` with the factory imported
|
|
3
|
+
* from the model layer, `select`, `attach` and `command` — and behaviour lives in models. A call that declares model
|
|
4
|
+
* behaviour on a model's context, or hands that context to a Kit helper, is a factory written in the wrong layer. That
|
|
5
|
+
* is a convention of layers, not a law of the library, so the rule arrives with `layers({ integration })`.
|
|
6
|
+
*/
|
|
7
|
+
declare const noBehaviorInIntegration: import("../rule.js").RuleModule<"behaviorInIntegration", []>;
|
|
8
|
+
export { noBehaviorInIntegration };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{methodPath as i,isModelContextFunction as s}from"../declaration-ingress.js";import{apiName as c}from"../model-bindings.js";import{createRule as l}from"../rule.js";const m=new Set(["command","effect","event","keyedLane","lane","state"]),d=new Set(["resource","scope","timers"]),u=new Set(["debounce","once","poll"]);function a(n,e){const[t]=e.range;let o=n.getText(e);for(const r of n.getCommentsInside(e).toReversed())o=`${o.slice(0,r.range[0]-t)} ${o.slice(r.range[1]-t)}`;return o.replace(/\s+/gu," ").replace(/\s*(\?\.|\.)\s*/gu,"$1").trim()}function h(n){const[e,t]=n.split(".");return t===void 0?m.has(n):d.has(e??"")}const p=l({create(n){const e=n.sourceCode;return{CallExpression(t){const o=c(e,t.callee,"@opetope/kit",!0);if(o!==void 0&&u.has(o)){n.report({data:{call:`${a(e,t.callee)}(\u2026)`},messageId:"behaviorInIntegration",node:t.callee});return}const r=i(e,t.callee);r===void 0||!h(r.method)||!s(e,r.fn)||n.report({data:{call:a(e,t.callee)},messageId:"behaviorInIntegration",node:t.callee})}}},meta:{docs:{description:"The integration layer composes features; model behaviour is declared in the model layer."},messages:{behaviorInIntegration:"`{{call}}` declares model behaviour in the integration layer, which only composes: `imports`, `requires`, `model(Declaration, factory)`, `select`, `attach` and `command`. Move the factory into the model layer and import it here (D494)."},schema:[],type:"suggestion"},name:"no-behavior-in-integration"});export{p as noBehaviorInIntegration};
|
|
2
|
+
//# sourceMappingURL=no-behavior-in-integration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-behavior-in-integration.js","sources":["../../src/rules/no-behavior-in-integration.ts"],"sourcesContent":["import type { TSESTree } from '@typescript-eslint/types';\n\nimport { isModelContextFunction, methodPath } from '../declaration-ingress';\nimport { apiName } from '../model-bindings';\nimport type { SourceCode } from '../rule';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'behaviorInIntegration';\n\n/** The members of a model's context that declare behaviour, and the three whose methods all do. */\nconst BEHAVIOUR = new Set(['command', 'effect', 'event', 'keyedLane', 'lane', 'state']);\nconst BEHAVIOUR_NAMESPACES = new Set(['resource', 'scope', 'timers']);\n/** The Kit helpers that declare work on the model context they are handed (D468). */\nconst KIT_HELPERS = new Set(['debounce', 'once', 'poll']);\n\n/**\n * The callee as the author wrote it — `ctx.effect`, `timers.delay`, `debounce` — on one line: its comments left out,\n * its spaces kept to one and none around a member access, so `ctx\\n .effect` reads `ctx.effect` and a cast\n * `(ctx as ModelContext).effect` keeps its words apart.\n */\nfunction written(source: SourceCode, callee: TSESTree.Node): string {\n const [start] = callee.range;\n let text = source.getText(callee);\n\n for (const comment of source.getCommentsInside(callee).toReversed()) {\n text = `${text.slice(0, comment.range[0] - start)} ${text.slice(comment.range[1] - start)}`;\n }\n\n return text\n .replace(/\\s+/gu, ' ')\n .replace(/\\s*(\\?\\.|\\.)\\s*/gu, '$1')\n .trim();\n}\n\nfunction isBehaviour(method: string): boolean {\n const [namespace, member] = method.split('.');\n\n return member === undefined ? BEHAVIOUR.has(method) : BEHAVIOUR_NAMESPACES.has(namespace ?? '');\n}\n\n/**\n * D494: the integration layer composes — `imports`, `requires`, `model(Declaration, factory)` with the factory imported\n * from the model layer, `select`, `attach` and `command` — and behaviour lives in models. A call that declares model\n * behaviour on a model's context, or hands that context to a Kit helper, is a factory written in the wrong layer. That\n * is a convention of layers, not a law of the library, so the rule arrives with `layers({ integration })`.\n */\nconst noBehaviorInIntegration = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n\n return {\n CallExpression(node): void {\n const kit = apiName(source, node.callee, '@opetope/kit', true);\n\n if (kit !== undefined && KIT_HELPERS.has(kit)) {\n context.report({\n data: { call: `${written(source, node.callee)}(…)` },\n messageId: 'behaviorInIntegration',\n node: node.callee,\n });\n\n return;\n }\n\n const named = methodPath(source, node.callee);\n\n if (named === undefined || !isBehaviour(named.method) || !isModelContextFunction(source, named.fn)) return;\n\n context.report({\n data: { call: written(source, node.callee) },\n messageId: 'behaviorInIntegration',\n node: node.callee,\n });\n },\n };\n },\n meta: {\n docs: {\n description: 'The integration layer composes features; model behaviour is declared in the model layer.',\n },\n messages: {\n behaviorInIntegration:\n '`{{call}}` declares model behaviour in the integration layer, which only composes: `imports`, `requires`, `model(Declaration, factory)`, `select`, `attach` and `command`. Move the factory into the model layer and import it here (D494).',\n },\n schema: [],\n type: 'suggestion',\n },\n name: 'no-behavior-in-integration',\n});\n\nexport { noBehaviorInIntegration };\n"],"names":["BEHAVIOUR","BEHAVIOUR_NAMESPACES","KIT_HELPERS","written","source","callee","start","text","comment","isBehaviour","method","namespace","member","noBehaviorInIntegration","createRule","context","node","kit","apiName","named","methodPath","isModelContextFunction"],"mappings":"0KAUA,MAAMA,EAAY,IAAI,IAAI,CAAC,UAAW,SAAU,QAAS,YAAa,OAAQ,OAAO,CAAC,EAChFC,EAAuB,IAAI,IAAI,CAAC,WAAY,QAAS,QAAQ,CAAC,EAE9DC,EAAc,IAAI,IAAI,CAAC,WAAY,OAAQ,MAAM,CAAC,EAOxD,SAASC,EAAQC,EAAoBC,EAAqB,CACxD,KAAM,CAACC,CAAK,EAAID,EAAO,MACvB,IAAIE,EAAOH,EAAO,QAAQC,CAAM,EAEhC,UAAWG,KAAWJ,EAAO,kBAAkBC,CAAM,EAAE,aACrDE,EAAO,GAAGA,EAAK,MAAM,EAAGC,EAAQ,MAAM,CAAC,EAAIF,CAAK,CAAC,IAAIC,EAAK,MAAMC,EAAQ,MAAM,CAAC,EAAIF,CAAK,CAAC,GAG3F,OAAOC,EACJ,QAAQ,QAAS,GAAG,EACpB,QAAQ,oBAAqB,IAAI,EACjC,KAAI,CACT,CAEA,SAASE,EAAYC,EAAc,CACjC,KAAM,CAACC,EAAWC,CAAM,EAAIF,EAAO,MAAM,GAAG,EAE5C,OAAOE,IAAW,OAAYZ,EAAU,IAAIU,CAAM,EAAIT,EAAqB,IAAIU,GAAa,EAAE,CAChG,CAQA,MAAME,EAA0BC,EAA2B,CACzD,OAAOC,EAAO,CACZ,MAAMX,EAASW,EAAQ,WAEvB,MAAO,CACL,eAAeC,EAAI,CACjB,MAAMC,EAAMC,EAAQd,EAAQY,EAAK,OAAQ,eAAgB,EAAI,EAE7D,GAAIC,IAAQ,QAAaf,EAAY,IAAIe,CAAG,EAAG,CAC7CF,EAAQ,OAAO,CACb,KAAM,CAAE,KAAM,GAAGZ,EAAQC,EAAQY,EAAK,MAAM,CAAC,UAAK,EAClD,UAAW,wBACX,KAAMA,EAAK,MACZ,CAAA,EAED,MACF,CAEA,MAAMG,EAAQC,EAAWhB,EAAQY,EAAK,MAAM,EAExCG,IAAU,QAAa,CAACV,EAAYU,EAAM,MAAM,GAAK,CAACE,EAAuBjB,EAAQe,EAAM,EAAE,GAEjGJ,EAAQ,OAAO,CACb,KAAM,CAAE,KAAMZ,EAAQC,EAAQY,EAAK,MAAM,CAAC,EAC1C,UAAW,wBACX,KAAMA,EAAK,MACZ,CAAA,CACH,EAEJ,EACA,KAAM,CACJ,KAAM,CACJ,YAAa,0FACd,EACD,SAAU,CACR,sBACE,6OACH,EACD,OAAQ,CAAA,EACR,KAAM,YACP,EACD,KAAM,4BACP,CAAA"}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
1
|
type MessageIds = 'commandHookInDeps' | 'commandRecordInDeps' | 'dependOnMember';
|
|
3
|
-
declare const noCommandInDeps:
|
|
4
|
-
name: string;
|
|
5
|
-
};
|
|
2
|
+
declare const noCommandInDeps: import("../rule.js").RuleModule<MessageIds, []>;
|
|
6
3
|
export { noCommandInDeps };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as x}from"@typescript-eslint/
|
|
1
|
+
import{AST_NODE_TYPES as x}from"@typescript-eslint/types";import{isFunctionLike as k}from"../ast.js";import{classifyDeclaration as y,bindingAt as v,usageIn as D,STATUS_MEMBERS as E}from"../command-hooks.js";import{unwrap as h,apiName as I}from"../model-bindings.js";import{createRule as S}from"../rule.js";const T=new Set(["useCallback","useEffect","useImperativeHandle","useInsertionEffect","useLayoutEffect","useMemo"]),R=[...E,"run"],M=2;function A(n){const e=n.arguments.at(-1);if(!(n.arguments.length<M||e?.type!==x.ArrayExpression))return{callback:n.arguments.slice(0,-1).findLast(k),deps:e}}function w(n){const e=[];for(const t of R)for(const o of n)o.member===t&&!e.includes(o.text)&&e.push(o.text);return e}function C(n){const[e]=n.accesses;if(!(n.opaque||e?.member!=="run"||e.optional))return n.accesses.every(t=>t.text===e.text)?e.text:void 0}function O(n,e,t){if(e.opaque)return[];const o=w(e.accesses);return n.proved&&!e.accesses.some(c=>c.member==="run")?[...o,`${t}.run`]:o}function P(n,e){const t=n.elements.findIndex(r=>r===e),o=n.elements[t+1]??null,c=n.elements[t-1]??null;return o!==null?[e.range[0],o.range[0]]:c===null?e.range:[c.range[1],e.range[1]]}function g(n,e,t,o){const c=e.elements.some(r=>r!==null&&r!==t&&n.getText(h(r))===o);return r=>c?r.removeRange(P(e,t)):r.replaceText(t,o)}function _(n,e){const t=I(n,e.callee,"react");return t!==void 0&&T.has(t)?A(e):void 0}const j=S({create(n){const e=n.sourceCode,t=new Map,o=[],c=[],r=(s,a,d)=>{const i=e.getText(h(a)),f=D(e,d,s.callback),u=d.record?"commandRecordInDeps":"commandHookInDeps",m=C(f);if(m!==void 0){n.report({data:{name:i},fix:g(e,s.deps,a,m),messageId:u,node:a});return}const l=O(d,f,i).map(p=>({data:{path:p},fix:g(e,s.deps,a,p),messageId:"dependOnMember"}));l.length===0?n.report({data:{name:i},messageId:u,node:a}):n.report({data:{name:i},messageId:u,node:a,suggest:l})},b=s=>{const a=_(e,s);if(a!==void 0)for(const d of a.deps.elements){if(d===null)continue;const i=v(e,t,d);i!==void 0&&r(a,d,i)}};return{CallExpression(s){c.push(s)},VariableDeclarator(s){o.push(s)},"Program:exit"(){for(const s of o)y(e,s,t);for(const s of c)b(s)}}},meta:{docs:{description:"A React dependency array names the stable `run` of a command hook, not the hook object."},fixable:"code",hasSuggestions:!0,messages:{commandHookInDeps:"The hook object of a command is rebuilt on every `inFlight`/`outcome` change, so this dependency re-runs the effect or voids the memo on each of them, and an effect that calls `run` from it loops. Depend on `{{name}}.run` \u2014 stable for the life of the binding \u2014 or on the status field the callback reads (D290).",commandRecordInDeps:"`{{name}}` is the record `useModel` returns for a selection, and it is a new object on every render, so this dependency changes on every render: the effect re-runs, the memo is voided, and an effect that calls `run` from it never settles. Depend on the fields the callback uses, such as `hooks.save.run` (D290).",dependOnMember:"Depend on `{{path}}` instead of the object this element names."},schema:[],type:"problem"},name:"no-command-in-deps"});export{j as noCommandInDeps};
|
|
2
2
|
//# sourceMappingURL=no-command-in-deps.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-command-in-deps.js","sources":["../../src/rules/no-command-in-deps.ts"],"sourcesContent":["import type { TSESLint, TSESTree } from '@typescript-eslint/utils';\nimport { AST_NODE_TYPES } from '@typescript-eslint/utils';\n\nimport { isFunctionLike } from '../ast';\nimport type { Access, Binding, HookKinds, Usage } from '../command-hooks';\nimport { bindingAt, classifyDeclaration, STATUS_MEMBERS, usageIn } from '../command-hooks';\nimport { apiName, unwrap } from '../model-bindings';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'commandHookInDeps' | 'commandRecordInDeps' | 'dependOnMember';\ntype Site = { readonly callback: TSESTree.Node | undefined; readonly deps: TSESTree.ArrayExpression };\n\nconst REACT_HOOKS = new Set([\n 'useCallback',\n 'useEffect',\n 'useImperativeHandle',\n 'useInsertionEffect',\n 'useLayoutEffect',\n 'useMemo',\n]);\n/** The order a suggestion list is read in: what the callback already watches, and the stable member last. */\nconst MEMBERS = [...STATUS_MEMBERS, 'run'];\n/** A dependency array is an argument, and the callback that reads it is the argument before. */\nconst SITE_ARITY = 2;\n\n/** `useEffect(fn, deps)` and `useImperativeHandle(ref, create, deps)`: the array last, the callback before it. */\nfunction dependencySite(node: TSESTree.CallExpression): Site | undefined {\n const deps = node.arguments.at(-1);\n\n if (node.arguments.length < SITE_ARITY || deps?.type !== AST_NODE_TYPES.ArrayExpression) return undefined;\n\n return { callback: node.arguments.slice(0, -1).findLast(isFunctionLike), deps };\n}\n\n/** Each path the callback wrote once, statuses before `run`: what the author already reads comes first. */\nfunction readPaths(accesses: readonly Access[]): readonly string[] {\n const paths: string[] = [];\n\n for (const member of MEMBERS) {\n for (const access of accesses) {\n if (access.member === member && !paths.includes(access.text)) paths.push(access.text);\n }\n }\n\n return paths;\n}\n\n/** One answer and no other: every read of the binding went through the same `run`, written without a `?.`. */\nfunction fixedPath(usage: Usage): string | undefined {\n const [first] = usage.accesses;\n\n if (usage.opaque || first?.member !== 'run' || first.optional) return undefined;\n\n return usage.accesses.every(access => access.text === first.text) ? first.text : undefined;\n}\n\n/**\n * Oxlint applies the first suggestion unattended, so a use no member replaces — the object passed on, destructured,\n * or read past the members named here — carries none: the report says what is wrong, and the author says what the\n * dependency meant. A selection field proved by `run` keeps that offer even where this callback only reads a status.\n */\nfunction suggestedPaths(binding: Binding, usage: Usage, base: string): readonly string[] {\n if (usage.opaque) return [];\n\n const paths = readPaths(usage.accesses);\n\n return binding.proved && !usage.accesses.some(access => access.member === 'run') ? [...paths, `${base}.run`] : paths;\n}\n\n/** The element and one comma with it, for an array that already names the dependency the fix would write. */\nfunction removalRange(deps: TSESTree.ArrayExpression, element: TSESTree.Node): TSESTree.Range {\n const index = deps.elements.findIndex(other => other === element);\n const next = deps.elements[index + 1] ?? null;\n const previous = deps.elements[index - 1] ?? null;\n\n if (next !== null) return [element.range[0], next.range[0]];\n\n return previous === null ? element.range : [previous.range[1], element.range[1]];\n}\n\n/** A dependency already written stays the one written: the element is removed rather than doubled. */\nfunction rewrite(\n source: TSESLint.SourceCode,\n deps: TSESTree.ArrayExpression,\n element: TSESTree.Node,\n path: string,\n): TSESLint.ReportFixFunction {\n const duplicate = deps.elements.some(\n other => other !== null && other !== element && source.getText(unwrap(other)) === path,\n );\n\n return fixer => (duplicate ? fixer.removeRange(removalRange(deps, element)) : fixer.replaceText(element, path));\n}\n\n/** The dependency site of one of React's own hooks, under whatever local name this file gave the import. */\nfunction reactSite(source: TSESLint.SourceCode, node: TSESTree.CallExpression): Site | undefined {\n const name = apiName(source, node.callee, 'react');\n\n return name !== undefined && REACT_HOOKS.has(name) ? dependencySite(node) : undefined;\n}\n\nconst noCommandInDeps = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const kinds: HookKinds = new Map();\n const declarators: TSESTree.VariableDeclarator[] = [];\n const calls: TSESTree.CallExpression[] = [];\n\n /** The name is read off the unwrapped element, so an `as`, a `satisfies` or a `!` never reaches a fix. */\n const report = (site: Site, element: TSESTree.Node, binding: Binding): void => {\n const base = source.getText(unwrap(element));\n const usage = usageIn(source, binding, site.callback);\n const messageId = binding.record ? 'commandRecordInDeps' : 'commandHookInDeps';\n const path = fixedPath(usage);\n\n if (path !== undefined) {\n context.report({\n data: { name: base },\n fix: rewrite(source, site.deps, element, path),\n messageId,\n node: element,\n });\n\n return;\n }\n\n const suggest = suggestedPaths(binding, usage, base).map(suggestion => ({\n data: { path: suggestion },\n fix: rewrite(source, site.deps, element, suggestion),\n messageId: 'dependOnMember' as const,\n }));\n\n if (suggest.length === 0) context.report({ data: { name: base }, messageId, node: element });\n else context.report({ data: { name: base }, messageId, node: element, suggest });\n };\n\n const check = (call: TSESTree.CallExpression): void => {\n const site = reactSite(source, call);\n\n if (site === undefined) return;\n\n for (const element of site.deps.elements) {\n if (element === null) continue;\n\n const binding = bindingAt(source, kinds, element);\n\n if (binding !== undefined) report(site, element, binding);\n }\n };\n\n return {\n CallExpression(node): void {\n calls.push(node);\n },\n VariableDeclarator(node): void {\n declarators.push(node);\n },\n 'Program:exit'(): void {\n for (const declarator of declarators) classifyDeclaration(source, declarator, kinds);\n for (const call of calls) check(call);\n },\n };\n },\n meta: {\n docs: {\n description: 'A React dependency array names the stable `run` of a command hook, not the hook object.',\n },\n fixable: 'code',\n hasSuggestions: true,\n messages: {\n commandHookInDeps:\n 'The hook object of a command is rebuilt on every `inFlight`/`outcome` change, so this dependency re-runs the effect or voids the memo on each of them, and an effect that calls `run` from it loops. Depend on `{{name}}.run` — stable for the life of the binding — or on the status field the callback reads (D290).',\n commandRecordInDeps:\n '`{{name}}` is the record `useModel` returns for a selection, and it is a new object on every render, so this dependency changes on every render: the effect re-runs, the memo is voided, and an effect that calls `run` from it never settles. Depend on the fields the callback uses, such as `hooks.save.run` (D290).',\n dependOnMember: 'Depend on `{{path}}` instead of the object this element names.',\n },\n schema: [],\n type: 'problem',\n },\n name: 'no-command-in-deps',\n});\n\nexport { noCommandInDeps };\n"],"names":["REACT_HOOKS","MEMBERS","STATUS_MEMBERS","SITE_ARITY","dependencySite","node","deps","AST_NODE_TYPES","isFunctionLike","readPaths","accesses","paths","member","access","fixedPath","usage","first","suggestedPaths","binding","base","removalRange","element","index","other","next","previous","rewrite","source","path","duplicate","unwrap","fixer","reactSite","name","apiName","noCommandInDeps","createRule","context","kinds","declarators","calls","report","site","usageIn","messageId","suggest","suggestion","check","call","bindingAt","declarator","classifyDeclaration"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"no-command-in-deps.js","sources":["../../src/rules/no-command-in-deps.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { isFunctionLike } from '../ast';\nimport type { Access, Binding, HookKinds, Usage } from '../command-hooks';\nimport { bindingAt, classifyDeclaration, STATUS_MEMBERS, usageIn } from '../command-hooks';\nimport { apiName, unwrap } from '../model-bindings';\nimport type { ReportFixFunction, SourceCode } from '../rule';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'commandHookInDeps' | 'commandRecordInDeps' | 'dependOnMember';\ntype Site = { readonly callback: TSESTree.Node | undefined; readonly deps: TSESTree.ArrayExpression };\n\nconst REACT_HOOKS = new Set([\n 'useCallback',\n 'useEffect',\n 'useImperativeHandle',\n 'useInsertionEffect',\n 'useLayoutEffect',\n 'useMemo',\n]);\n/** The order a suggestion list is read in: what the callback already watches, and the stable member last. */\nconst MEMBERS = [...STATUS_MEMBERS, 'run'];\n/** A dependency array is an argument, and the callback that reads it is the argument before. */\nconst SITE_ARITY = 2;\n\n/** `useEffect(fn, deps)` and `useImperativeHandle(ref, create, deps)`: the array last, the callback before it. */\nfunction dependencySite(node: TSESTree.CallExpression): Site | undefined {\n const deps = node.arguments.at(-1);\n\n if (node.arguments.length < SITE_ARITY || deps?.type !== AST_NODE_TYPES.ArrayExpression) return undefined;\n\n return { callback: node.arguments.slice(0, -1).findLast(isFunctionLike), deps };\n}\n\n/** Each path the callback wrote once, statuses before `run`: what the author already reads comes first. */\nfunction readPaths(accesses: readonly Access[]): readonly string[] {\n const paths: string[] = [];\n\n for (const member of MEMBERS) {\n for (const access of accesses) {\n if (access.member === member && !paths.includes(access.text)) paths.push(access.text);\n }\n }\n\n return paths;\n}\n\n/** One answer and no other: every read of the binding went through the same `run`, written without a `?.`. */\nfunction fixedPath(usage: Usage): string | undefined {\n const [first] = usage.accesses;\n\n if (usage.opaque || first?.member !== 'run' || first.optional) return undefined;\n\n return usage.accesses.every(access => access.text === first.text) ? first.text : undefined;\n}\n\n/**\n * Oxlint applies the first suggestion unattended, so a use no member replaces — the object passed on, destructured,\n * or read past the members named here — carries none: the report says what is wrong, and the author says what the\n * dependency meant. A selection field proved by `run` keeps that offer even where this callback only reads a status.\n */\nfunction suggestedPaths(binding: Binding, usage: Usage, base: string): readonly string[] {\n if (usage.opaque) return [];\n\n const paths = readPaths(usage.accesses);\n\n return binding.proved && !usage.accesses.some(access => access.member === 'run') ? [...paths, `${base}.run`] : paths;\n}\n\n/** The element and one comma with it, for an array that already names the dependency the fix would write. */\nfunction removalRange(deps: TSESTree.ArrayExpression, element: TSESTree.Node): TSESTree.Range {\n const index = deps.elements.findIndex(other => other === element);\n const next = deps.elements[index + 1] ?? null;\n const previous = deps.elements[index - 1] ?? null;\n\n if (next !== null) return [element.range[0], next.range[0]];\n\n return previous === null ? element.range : [previous.range[1], element.range[1]];\n}\n\n/** A dependency already written stays the one written: the element is removed rather than doubled. */\nfunction rewrite(\n source: SourceCode,\n deps: TSESTree.ArrayExpression,\n element: TSESTree.Node,\n path: string,\n): ReportFixFunction {\n const duplicate = deps.elements.some(\n other => other !== null && other !== element && source.getText(unwrap(other)) === path,\n );\n\n return fixer => (duplicate ? fixer.removeRange(removalRange(deps, element)) : fixer.replaceText(element, path));\n}\n\n/** The dependency site of one of React's own hooks, under whatever local name this file gave the import. */\nfunction reactSite(source: SourceCode, node: TSESTree.CallExpression): Site | undefined {\n const name = apiName(source, node.callee, 'react');\n\n return name !== undefined && REACT_HOOKS.has(name) ? dependencySite(node) : undefined;\n}\n\nconst noCommandInDeps = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const kinds: HookKinds = new Map();\n const declarators: TSESTree.VariableDeclarator[] = [];\n const calls: TSESTree.CallExpression[] = [];\n\n /** The name is read off the unwrapped element, so an `as`, a `satisfies` or a `!` never reaches a fix. */\n const report = (site: Site, element: TSESTree.Node, binding: Binding): void => {\n const base = source.getText(unwrap(element));\n const usage = usageIn(source, binding, site.callback);\n const messageId = binding.record ? 'commandRecordInDeps' : 'commandHookInDeps';\n const path = fixedPath(usage);\n\n if (path !== undefined) {\n context.report({\n data: { name: base },\n fix: rewrite(source, site.deps, element, path),\n messageId,\n node: element,\n });\n\n return;\n }\n\n const suggest = suggestedPaths(binding, usage, base).map(suggestion => ({\n data: { path: suggestion },\n fix: rewrite(source, site.deps, element, suggestion),\n messageId: 'dependOnMember' as const,\n }));\n\n if (suggest.length === 0) context.report({ data: { name: base }, messageId, node: element });\n else context.report({ data: { name: base }, messageId, node: element, suggest });\n };\n\n const check = (call: TSESTree.CallExpression): void => {\n const site = reactSite(source, call);\n\n if (site === undefined) return;\n\n for (const element of site.deps.elements) {\n if (element === null) continue;\n\n const binding = bindingAt(source, kinds, element);\n\n if (binding !== undefined) report(site, element, binding);\n }\n };\n\n return {\n CallExpression(node): void {\n calls.push(node);\n },\n VariableDeclarator(node): void {\n declarators.push(node);\n },\n 'Program:exit'(): void {\n for (const declarator of declarators) classifyDeclaration(source, declarator, kinds);\n for (const call of calls) check(call);\n },\n };\n },\n meta: {\n docs: {\n description: 'A React dependency array names the stable `run` of a command hook, not the hook object.',\n },\n fixable: 'code',\n hasSuggestions: true,\n messages: {\n commandHookInDeps:\n 'The hook object of a command is rebuilt on every `inFlight`/`outcome` change, so this dependency re-runs the effect or voids the memo on each of them, and an effect that calls `run` from it loops. Depend on `{{name}}.run` — stable for the life of the binding — or on the status field the callback reads (D290).',\n commandRecordInDeps:\n '`{{name}}` is the record `useModel` returns for a selection, and it is a new object on every render, so this dependency changes on every render: the effect re-runs, the memo is voided, and an effect that calls `run` from it never settles. Depend on the fields the callback uses, such as `hooks.save.run` (D290).',\n dependOnMember: 'Depend on `{{path}}` instead of the object this element names.',\n },\n schema: [],\n type: 'problem',\n },\n name: 'no-command-in-deps',\n});\n\nexport { noCommandInDeps };\n"],"names":["REACT_HOOKS","MEMBERS","STATUS_MEMBERS","SITE_ARITY","dependencySite","node","deps","AST_NODE_TYPES","isFunctionLike","readPaths","accesses","paths","member","access","fixedPath","usage","first","suggestedPaths","binding","base","removalRange","element","index","other","next","previous","rewrite","source","path","duplicate","unwrap","fixer","reactSite","name","apiName","noCommandInDeps","createRule","context","kinds","declarators","calls","report","site","usageIn","messageId","suggest","suggestion","check","call","bindingAt","declarator","classifyDeclaration"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D494: the reporter hears a failed run of an effect or an event unless that run was aborted, whatever the run threw,
|
|
3
|
+
* and a cancellation is never heard. `signal.throwIfAborted()` right before a `throw` therefore changes nothing: on an
|
|
4
|
+
* aborted run it replaces one error nobody hears by another, and on a live run it does nothing at all. The check that
|
|
5
|
+
* means something stands before work a cancelled run must not do, not before its last word.
|
|
6
|
+
*/
|
|
7
|
+
declare const noDeadAbortCheck: import("../rule.js").RuleModule<"deadAbortCheck", []>;
|
|
8
|
+
export { noDeadAbortCheck };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{AST_NODE_TYPES as i}from"@typescript-eslint/types";import{fromContext as f,contextMembers as l}from"../context-members.js";import{methodPath as d,isModelContextFunction as h,isContextFunction as m}from"../declaration-ingress.js";import{declaredEffect as p}from"../effect-declarations.js";import{enclosingFunction as b,unwrap as g,localValue as y,isFunction as w}from"../model-bindings.js";import{createRule as k}from"../rule.js";function C(n){if(n.type!==i.CallExpression||n.arguments.length>0)return;const{callee:e}=n;return e.type===i.MemberExpression&&!e.computed&&e.property.type===i.Identifier&&e.property.name==="throwIfAborted"?e:void 0}function x(n){const e=C(n.expression);return e===void 0?void 0:g(e.object)}function S(n){const{parent:e}=n;return e.type===i.BlockStatement?e.body:e.type===i.SwitchCase?e.consequent:void 0}function E(n){const e=S(n)??[],a=e.indexOf(n);return a===-1?void 0:e[a+1]}function A(n,e){for(let a=n,t=n.parent;t!==void 0&&t!==e;){if(t.type===i.TryStatement&&t.handler!==null&&t.block===a)return!0;a=t,t=t.parent}return!1}function v(n,e){const a=d(n,e.callee),t=e.arguments[1];if(a?.method!=="event"||t===void 0||t.type===i.SpreadElement)return;const c=y(n,t);return w(c)&&m(n,a.fn)?c:void 0}const D=k({create(n){const e=n.sourceCode,a=new Set,t=[],c=[],u=(r,o)=>f(e,o,l(e,r,1,["signal"],c),"signal");return{CallExpression(r){const o=p(e,r)?.run??v(e,r),s=o===void 0?void 0:d(e,r.callee);o!==void 0&&s!==void 0&&h(e,s.fn)&&a.add(o)},ExpressionStatement(r){const o=x(r);o!==void 0&&E(r)?.type===i.ThrowStatement&&t.push({signal:o,statement:r})},"Program:exit"(){for(const{signal:r,statement:o}of t){const s=b(e,o);s===void 0||!a.has(s)||!u(s,r)||A(o,s)||n.report({messageId:"deadAbortCheck",node:o})}},VariableDeclarator(r){c.push(r)}}},meta:{docs:{description:"An aborted run of an effect or an event is not reported, so an abort check right before a throw is dead."},messages:{deadAbortCheck:"The reporter does not hear a run of an effect or an event that was aborted, whatever it throws, so this check right before a `throw` changes nothing: it only swaps one unheard error for another. Delete it; an abort check belongs before work a cancelled run must not do (D494)."},schema:[],type:"suggestion"},name:"no-dead-abort-check"});export{D as noDeadAbortCheck};
|
|
2
|
+
//# sourceMappingURL=no-dead-abort-check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-dead-abort-check.js","sources":["../../src/rules/no-dead-abort-check.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { contextMembers, fromContext } from '../context-members';\nimport { isContextFunction, isModelContextFunction, methodPath } from '../declaration-ingress';\nimport { declaredEffect } from '../effect-declarations';\nimport type { FunctionNode } from '../model-bindings';\nimport { enclosingFunction, isFunction, localValue, unwrap } from '../model-bindings';\nimport type { SourceCode } from '../rule';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'deadAbortCheck';\n\n/** `object.throwIfAborted()` with no argument: the member it calls, where the call is that one. */\nfunction throwIfAbortedCallee(expression: TSESTree.Expression): TSESTree.MemberExpression | undefined {\n if (expression.type !== AST_NODE_TYPES.CallExpression || expression.arguments.length > 0) return undefined;\n\n const { callee } = expression;\n\n return callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.property.type === AST_NODE_TYPES.Identifier &&\n callee.property.name === 'throwIfAborted'\n ? callee\n : undefined;\n}\n\n/** `<signal>.throwIfAborted()` written as a statement of its own, and the signal it asks. */\nfunction abortCheck(statement: TSESTree.ExpressionStatement): TSESTree.Node | undefined {\n const callee = throwIfAbortedCallee(statement.expression);\n\n return callee === undefined ? undefined : unwrap(callee.object);\n}\n\n/** The statements of the block, or of the `case`, that holds this one. */\nfunction siblingsOf(statement: TSESTree.Statement): readonly TSESTree.Statement[] | undefined {\n const { parent } = statement;\n\n if (parent.type === AST_NODE_TYPES.BlockStatement) return parent.body;\n\n return parent.type === AST_NODE_TYPES.SwitchCase ? parent.consequent : undefined;\n}\n\n/** The statement written right after this one in the same block, where there is one. */\nfunction nextStatement(statement: TSESTree.Statement): TSESTree.Statement | undefined {\n const siblings = siblingsOf(statement) ?? [];\n const index = siblings.indexOf(statement);\n\n return index === -1 ? undefined : siblings[index + 1];\n}\n\n/**\n * Whether a `try` with a `catch` between the check and its run holds the check in its block: then the run itself\n * catches what the check throws, and the check decides what the run does next.\n */\nfunction caughtInRun(statement: TSESTree.Node, run: FunctionNode): boolean {\n for (let child = statement, current = statement.parent; current !== undefined && current !== run;) {\n if (current.type === AST_NODE_TYPES.TryStatement && current.handler !== null && current.block === child)\n return true;\n\n child = current;\n current = current.parent;\n }\n\n return false;\n}\n\n/** The run of `event(source, run, subscribe)` on a declaring context, where it is a function of this module. */\nfunction eventRun(source: SourceCode, call: TSESTree.CallExpression): FunctionNode | undefined {\n const named = methodPath(source, call.callee);\n const run = call.arguments[1];\n\n if (named?.method !== 'event' || run === undefined || run.type === AST_NODE_TYPES.SpreadElement) return undefined;\n\n const value = localValue(source, run);\n\n return isFunction(value) && isContextFunction(source, named.fn) ? value : undefined;\n}\n\n/**\n * D494: the reporter hears a failed run of an effect or an event unless that run was aborted, whatever the run threw,\n * and a cancellation is never heard. `signal.throwIfAborted()` right before a `throw` therefore changes nothing: on an\n * aborted run it replaces one error nobody hears by another, and on a live run it does nothing at all. The check that\n * means something stands before work a cancelled run must not do, not before its last word.\n */\nconst noDeadAbortCheck = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n const runs = new Set<FunctionNode>();\n const checks: { readonly signal: TSESTree.Node; readonly statement: TSESTree.ExpressionStatement }[] = [];\n const declarators: TSESTree.VariableDeclarator[] = [];\n\n /** The run's own signal: the second parameter of an effect run or an event run, however it was taken apart. */\n const isRunSignal = (run: FunctionNode, signal: TSESTree.Node): boolean =>\n fromContext(source, signal, contextMembers(source, run, 1, ['signal'], declarators), 'signal');\n\n return {\n CallExpression(node): void {\n const run = declaredEffect(source, node)?.run ?? eventRun(source, node);\n const named = run === undefined ? undefined : methodPath(source, node.callee);\n\n if (run !== undefined && named !== undefined && isModelContextFunction(source, named.fn)) runs.add(run);\n },\n ExpressionStatement(node): void {\n const signal = abortCheck(node);\n\n if (signal !== undefined && nextStatement(node)?.type === AST_NODE_TYPES.ThrowStatement) {\n checks.push({ signal, statement: node });\n }\n },\n 'Program:exit'(): void {\n for (const { signal, statement } of checks) {\n const run = enclosingFunction(source, statement);\n\n if (run === undefined || !runs.has(run) || !isRunSignal(run, signal) || caughtInRun(statement, run)) continue;\n\n context.report({ messageId: 'deadAbortCheck', node: statement });\n }\n },\n VariableDeclarator(node): void {\n declarators.push(node);\n },\n };\n },\n meta: {\n docs: {\n description:\n 'An aborted run of an effect or an event is not reported, so an abort check right before a throw is dead.',\n },\n messages: {\n deadAbortCheck:\n 'The reporter does not hear a run of an effect or an event that was aborted, whatever it throws, so this check right before a `throw` changes nothing: it only swaps one unheard error for another. Delete it; an abort check belongs before work a cancelled run must not do (D494).',\n },\n schema: [],\n type: 'suggestion',\n },\n name: 'no-dead-abort-check',\n});\n\nexport { noDeadAbortCheck };\n"],"names":["throwIfAbortedCallee","expression","AST_NODE_TYPES","callee","abortCheck","statement","unwrap","siblingsOf","parent","nextStatement","siblings","index","caughtInRun","run","child","current","eventRun","source","call","named","methodPath","value","localValue","isFunction","isContextFunction","noDeadAbortCheck","createRule","context","runs","checks","declarators","isRunSignal","signal","fromContext","contextMembers","node","declaredEffect","isModelContextFunction","enclosingFunction"],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D494: an event runs its `run` for a payload `emit` admitted, and nothing else admits one (D298, D319). A `subscribe`
|
|
3
|
+
* that never calls `emit` — nor hands it to the source it opens — declares a handler that never runs. When it does
|
|
4
|
+
* not write through its own `update` either, what is left neither admits nor writes, and that is reported. One that
|
|
5
|
+
* writes is a supported form, not a mistake: its writes land inside every notification, which an `effect` over the
|
|
6
|
+
* same source does not reproduce — the effect runs after the notification, merges a burst into its newest value and
|
|
7
|
+
* skips a value equal to the last one.
|
|
8
|
+
*/
|
|
9
|
+
declare const noEventWithoutEmit: import("../rule.js").RuleModule<"eventWithoutEmit", []>;
|
|
10
|
+
export { noEventWithoutEmit };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{AST_NODE_TYPES as r}from"@typescript-eslint/types";import{propertyName as f}from"../ast.js";import{methodPath as h,isContextFunction as p}from"../declaration-ingress.js";import{localValue as m,isFunction as l,variableOf as a}from"../model-bindings.js";import{createRule as w}from"../rule.js";const u=new Set(["emit","update"]);function c(t){return t.includes("used")?"used":t.includes("unknown")?"unknown":"none"}function y(t,n){return(a(t,n)?.references??[]).some(o=>o.identifier!==n)?"used":"none"}function b(t){return t.properties.some(n=>n.type===r.RestElement||n.computed)}function v(t,n){return n.type===r.Property&&n.value.type===r.Identifier?y(t,n.value):"unknown"}function d(t,n){const e=[],o=new Set;for(const i of n.properties){const s=f(i);s!==void 0&&u.has(s)&&(o.add(s),e.push(v(t,i)))}return o.size<u.size&&b(n)&&e.push("unknown"),c(e)}function k(t){return t.computed?"unknown":t.property.type===r.Identifier&&u.has(t.property.name)?"used":"none"}function E(t,n){const{parent:e}=n;return e.type===r.MemberExpression&&e.object===n?k(e):e.type!==r.VariableDeclarator||e.init!==n?"unknown":e.id.type===r.ObjectPattern?d(t,e.id):"unknown"}function R(t,n){return c((a(t,n)?.references??[]).filter(e=>e.identifier!==n).map(({identifier:e})=>e.type===r.Identifier?E(t,e):"unknown"))}function g(t,n){const[e]=n.params;return e===void 0?"none":e.type===r.ObjectPattern?d(t,e):e.type===r.Identifier?R(t,e):"unknown"}function D(t,n){const e=n.arguments[2];if(e===void 0||e.type===r.SpreadElement)return;const o=h(t,n.callee);if(o?.method!=="event"||!p(t,o.fn))return;const i=m(t,e);return l(i)?i:void 0}const I=w({create(t){const n=t.sourceCode;return{CallExpression(e){const o=D(n,e);o===void 0||g(n,o)!=="none"||t.report({messageId:"eventWithoutEmit",node:e.callee})}}},meta:{docs:{description:"The `subscribe` of an event admits payloads with `emit` or writes with its own `update`; one that does neither never starts `run` and writes nothing."},messages:{eventWithoutEmit:"The `subscribe` of this event never calls `emit` or its own `update`, nor hands either on, so its `run` never runs: only a payload `emit` admitted starts it. Emit what the handler is for, or write through the subscription's `update` to answer every notification inside it. `effect(source, run)` is not the same: it runs after the notification, merges a burst of changes into the newest value and skips a value equal to the last, so choose it when only the value the source settles on matters (D298, D319, D494)."},schema:[],type:"problem"},name:"no-event-without-emit"});export{I as noEventWithoutEmit};
|
|
2
|
+
//# sourceMappingURL=no-event-without-emit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-event-without-emit.js","sources":["../../src/rules/no-event-without-emit.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { propertyName } from '../ast';\nimport { isContextFunction, methodPath } from '../declaration-ingress';\nimport type { FunctionNode } from '../model-bindings';\nimport { isFunction, localValue, variableOf } from '../model-bindings';\nimport type { SourceCode } from '../rule';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'eventWithoutEmit';\n\n/** What the subscription's context says about its outlets: one of them used, none reachable, or out of sight. */\ntype Reach = 'none' | 'unknown' | 'used';\n\n/**\n * The members through which a subscription acts on its model: `emit` admits a payload for `run`, and `update` writes a\n * cell from inside the subscription (D494).\n */\nconst outlets: ReadonlySet<string> = new Set(['emit', 'update']);\n\n/** Readings of the context together: one outlet used is enough, one out of sight keeps the rule silent. */\nfunction joined(reaches: readonly Reach[]): Reach {\n if (reaches.includes('used')) return 'used';\n\n return reaches.includes('unknown') ? 'unknown' : 'none';\n}\n\n/** Whether reading this name reaches its outlet: a binding of it that anything reads. */\nfunction readReach(source: SourceCode, name: TSESTree.Identifier): Reach {\n const references = variableOf(source, name)?.references ?? [];\n\n return references.some(reference => reference.identifier !== name) ? 'used' : 'none';\n}\n\n/** Whether a pattern hides what it takes: a rest element keeps an outlet, a computed key may be one. */\nfunction hides(pattern: TSESTree.ObjectPattern): boolean {\n return pattern.properties.some(property => property.type === AST_NODE_TYPES.RestElement || property.computed);\n}\n\n/** An outlet a pattern takes: whether a reader of the name it is bound to reads it, or a default or nested pattern. */\nfunction takenReach(source: SourceCode, property: TSESTree.ObjectPattern['properties'][number]): Reach {\n return property.type === AST_NODE_TYPES.Property && property.value.type === AST_NODE_TYPES.Identifier\n ? readReach(source, property.value)\n : 'unknown';\n}\n\n/**\n * `({ emit, update })`: whether the pattern took an outlet and a reader of that name reads it, or the pattern hides an\n * outlet it did not name.\n */\nfunction patternReach(source: SourceCode, pattern: TSESTree.ObjectPattern): Reach {\n const reaches: Reach[] = [];\n const named = new Set<string>();\n\n for (const property of pattern.properties) {\n const name = propertyName(property);\n\n if (name !== undefined && outlets.has(name)) {\n named.add(name);\n reaches.push(takenReach(source, property));\n }\n }\n\n if (named.size < outlets.size && hides(pattern)) reaches.push('unknown');\n\n return joined(reaches);\n}\n\n/** One member read off the context taken whole: an outlet, another member, or a key this rule cannot read. */\nfunction memberReach(member: TSESTree.MemberExpression): Reach {\n if (member.computed) return 'unknown';\n\n return member.property.type === AST_NODE_TYPES.Identifier && outlets.has(member.property.name) ? 'used' : 'none';\n}\n\n/** One use of the context taken whole: an outlet read off it, another member, or the context handed on. */\nfunction referenceReach(source: SourceCode, identifier: TSESTree.Identifier): Reach {\n const { parent } = identifier;\n\n if (parent.type === AST_NODE_TYPES.MemberExpression && parent.object === identifier) return memberReach(parent);\n\n if (parent.type !== AST_NODE_TYPES.VariableDeclarator || parent.init !== identifier) return 'unknown';\n\n return parent.id.type === AST_NODE_TYPES.ObjectPattern ? patternReach(source, parent.id) : 'unknown';\n}\n\n/** Every use of the context taken whole, and what they say together. */\nfunction wholeReach(source: SourceCode, parameter: TSESTree.Identifier): Reach {\n return joined(\n (variableOf(source, parameter)?.references ?? [])\n .filter(reference => reference.identifier !== parameter)\n .map(({ identifier }) =>\n identifier.type === AST_NODE_TYPES.Identifier ? referenceReach(source, identifier) : 'unknown',\n ),\n );\n}\n\n/** Whether an outlet is reachable from the context `subscribe` receives, and whether that is visible from here. */\nfunction outletReach(source: SourceCode, subscribe: FunctionNode): Reach {\n const [parameter] = subscribe.params;\n\n if (parameter === undefined) return 'none';\n\n if (parameter.type === AST_NODE_TYPES.ObjectPattern) return patternReach(source, parameter);\n\n return parameter.type === AST_NODE_TYPES.Identifier ? wholeReach(source, parameter) : 'unknown';\n}\n\n/** The `subscribe` of an `event` on a declaring context, where it is a function of this module. */\nfunction subscribeOf(source: SourceCode, call: TSESTree.CallExpression): FunctionNode | undefined {\n const subscribe = call.arguments[2];\n\n if (subscribe === undefined || subscribe.type === AST_NODE_TYPES.SpreadElement) return undefined;\n\n const named = methodPath(source, call.callee);\n\n if (named?.method !== 'event' || !isContextFunction(source, named.fn)) return undefined;\n\n const value = localValue(source, subscribe);\n\n return isFunction(value) ? value : undefined;\n}\n\n/**\n * D494: an event runs its `run` for a payload `emit` admitted, and nothing else admits one (D298, D319). A `subscribe`\n * that never calls `emit` — nor hands it to the source it opens — declares a handler that never runs. When it does\n * not write through its own `update` either, what is left neither admits nor writes, and that is reported. One that\n * writes is a supported form, not a mistake: its writes land inside every notification, which an `effect` over the\n * same source does not reproduce — the effect runs after the notification, merges a burst into its newest value and\n * skips a value equal to the last one.\n */\nconst noEventWithoutEmit = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n\n return {\n CallExpression(node): void {\n const subscribe = subscribeOf(source, node);\n\n if (subscribe === undefined || outletReach(source, subscribe) !== 'none') return;\n\n context.report({ messageId: 'eventWithoutEmit', node: node.callee });\n },\n };\n },\n meta: {\n docs: {\n description:\n 'The `subscribe` of an event admits payloads with `emit` or writes with its own `update`; one that does neither never starts `run` and writes nothing.',\n },\n messages: {\n eventWithoutEmit:\n \"The `subscribe` of this event never calls `emit` or its own `update`, nor hands either on, so its `run` never runs: only a payload `emit` admitted starts it. Emit what the handler is for, or write through the subscription's `update` to answer every notification inside it. `effect(source, run)` is not the same: it runs after the notification, merges a burst of changes into the newest value and skips a value equal to the last, so choose it when only the value the source settles on matters (D298, D319, D494).\",\n },\n schema: [],\n type: 'problem',\n },\n name: 'no-event-without-emit',\n});\n\nexport { noEventWithoutEmit };\n"],"names":["outlets","joined","reaches","readReach","source","name","variableOf","reference","hides","pattern","property","AST_NODE_TYPES","takenReach","patternReach","named","propertyName","memberReach","member","referenceReach","identifier","parent","wholeReach","parameter","outletReach","subscribe","subscribeOf","call","methodPath","isContextFunction","value","localValue","isFunction","noEventWithoutEmit","createRule","context","node"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D494: React Compiler's inference mode compiles a component it can name — a function declared at the top of a
|
|
3
|
+
* module under a capitalized name — and so do the compiler rules of oxlint. A component written inline inside
|
|
4
|
+
* `requiresModels([...])(…)` is an anonymous argument, invisible to both, so the React reference declares it first
|
|
5
|
+
* and wraps it after (D158).
|
|
6
|
+
*/
|
|
7
|
+
declare const noInlineRequiresModels: import("../rule.js").RuleModule<"inlineComponent", []>;
|
|
8
|
+
export { noInlineRequiresModels };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{AST_NODE_TYPES as i}from"@typescript-eslint/types";import{isFunctionLike as c}from"../ast.js";import{isApi as l}from"../model-bindings.js";import{createRule as m}from"../rule.js";function g(e,n){const{callee:r}=n,[t]=n.arguments;if(!(r.type!==i.CallExpression||t===void 0)&&l(e,r.callee,"@opetope/react","requiresModels"))return{component:t,exact:l(e,r.callee,"@opetope/react","requiresModels",!0)}}const y=/@ts-(?:ignore|expect-error)/u;function d(e){return`${e.charAt(0).toUpperCase()}${e.slice(1)}View`}function x(e){const n=e.type===i.AssignmentPattern?e.left:e;return"typeAnnotation"in n&&!!n.typeAnnotation}function C(e,n){return e.typeArguments||!c(n)?!1:n.params.every(x)}function w(e){const n=e.parent.type===i.ExportNamedDeclaration?e.parent:e;return e.kind==="const"&&n.parent?.type===i.Program?n:void 0}function D(e){const n=e.parent;if(n.type!==i.VariableDeclarator||n.init!==e)return;const{id:r,parent:t}=n;if(r.type!==i.Identifier||t.type!==i.VariableDeclaration)return;const o=w(t);return o===void 0?void 0:{name:r.name,statement:o}}function h(e,n){const r=n.replaceAll("$","\\$");return new RegExp(`(?<![\\w$])${r}(?![\\w$])`,"u").test(e.getText())}function A(e,n){const r=e.body.indexOf(n);return r>0?e.body[r-1]:void 0}function p(e,n){const r=n.parent;if(r?.type!==i.Program)return[];const t=A(r,n)?.range[1],o=e.getText();return e.getCommentsInside(r).filter(a=>a.range[1]<=n.range[0]&&(t===void 0||a.range[0]>=t&&o.slice(t,a.range[0]).includes(`
|
|
2
|
+
`)))}function T(e,n,r){const t=e.getCommentsInside(n).filter(o=>o.range[1]<=r.range[0]);return[...p(e,n),...t].some(o=>y.test(o.value))}function E(e,n,r){return e.body.some(t=>{const o=t.type===i.ExportNamedDeclaration?t.declaration:t;return o?.type!==i.VariableDeclaration?!1:o.declarations.some(({id:a})=>a.type===i.Identifier&&a.name!==n&&d(a.name)===r)})}function $(e,n,r){const t=d(r);if(!/^[A-Z]/u.test(t)||h(e,t))return;const o=n.parent;return o?.type===i.Program&&!E(o,r,t)?t:void 0}function b(e,n,r){const t=D(n);if(t===void 0||!C(n,r))return null;const o=$(e,t.statement,t.name);if(o===void 0||T(e,t.statement,r))return null;const[a]=p(e,t.statement),s=a?.range[0]??t.statement.range[0],f=`const ${o} = ${e.getText(r)};
|
|
3
|
+
|
|
4
|
+
`;return u=>[u.replaceTextRange([s,s],f),u.replaceText(r,o)]}const I=m({create(e){const n=e.sourceCode;return{CallExpression(r){const t=g(n,r);if(t===void 0||!c(t.component))return;const o=t.exact?b(n,r,t.component):null;e.report({fix:o,messageId:"inlineComponent",node:t.component})}}},meta:{docs:{description:"The component `requiresModels` wraps is declared at the top of the module, not inline."},fixable:"code",messages:{inlineComponent:"A component written inline inside `requiresModels([...])(\u2026)` is invisible to React Compiler's inference mode and to the compiler rules of oxlint. Declare it at the top of the module under a capitalized name and wrap it after: `const CountView = () => \u2026;` then `export const Count = requiresModels([CountModel])(CountView);` (D158, D494)."},schema:[],type:"suggestion"},name:"no-inline-requires-models"});export{I as noInlineRequiresModels};
|
|
5
|
+
//# sourceMappingURL=no-inline-requires-models.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-inline-requires-models.js","sources":["../../src/rules/no-inline-requires-models.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { isFunctionLike } from '../ast';\nimport { isApi } from '../model-bindings';\nimport type { ReportFixFunction, SourceCode } from '../rule';\nimport { createRule } from '../rule';\n\ntype MessageIds = 'inlineComponent';\n\n/** `requiresModels([...])(component)`: the component, and whether `requiresModels` is exactly `@opetope/react`'s. */\nfunction wrappedComponent(\n source: SourceCode,\n node: TSESTree.CallExpression,\n): { readonly component: TSESTree.Node; readonly exact: boolean } | undefined {\n const { callee } = node;\n const [component] = node.arguments;\n\n if (callee.type !== AST_NODE_TYPES.CallExpression || component === undefined) return undefined;\n\n if (!isApi(source, callee.callee, '@opetope/react', 'requiresModels')) return undefined;\n\n return { component, exact: isApi(source, callee.callee, '@opetope/react', 'requiresModels', true) };\n}\n\n/** A directive that silences the line below it: moved away from its component, it would silence the wrong line. */\nconst DIRECTIVE = /@ts-(?:ignore|expect-error)/u;\n\n/** The name of the `const` the component moves into: the wrapped name, capitalized, with `View` after it. */\nfunction viewName(name: string): string {\n return `${name.charAt(0).toUpperCase()}${name.slice(1)}View`;\n}\n\n/**\n * Whether a parameter says its own type: once the component leaves the call, nothing else will. The Oxlint bridge\n * gives an absent annotation as `null` and ESLint leaves it out, so both read as falsy.\n */\nfunction annotated(parameter: TSESTree.Parameter): boolean {\n const pattern = parameter.type === AST_NODE_TYPES.AssignmentPattern ? parameter.left : parameter;\n\n return 'typeAnnotation' in pattern && Boolean(pattern.typeAnnotation);\n}\n\n/**\n * Whether the component keeps its types once it leaves the call. An inline component takes the type of its props from\n * the wrapper — from what `requiresModels([...])` infers, or from type arguments written at the call — and a `const`\n * of its own has no such context: an unannotated parameter would become an implicit `any` (TS7006, TS7031).\n */\nfunction keepsTypes(node: TSESTree.CallExpression, component: TSESTree.Node): boolean {\n if (node.typeArguments || !isFunctionLike(component)) return false;\n\n return component.params.every(annotated);\n}\n\n/** The statement a `const` declaration stands as at the top of the module, `export` included. */\nfunction moduleStatement(declaration: TSESTree.VariableDeclaration): TSESTree.Node | undefined {\n const statement =\n declaration.parent.type === AST_NODE_TYPES.ExportNamedDeclaration ? declaration.parent : declaration;\n\n return declaration.kind === 'const' && statement.parent?.type === AST_NODE_TYPES.Program ? statement : undefined;\n}\n\n/**\n * The statement that declares the wrapped component at the top of the module — `const Count = …` or\n * `export const Count = …` — and the name it declares, or `undefined` anywhere else: a component written inside a\n * function or handed on without a name has no place at the top of the module that a rewrite could take for it.\n */\nfunction topLevelDeclaration(\n node: TSESTree.CallExpression,\n): { readonly name: string; readonly statement: TSESTree.Node } | undefined {\n const declarator = node.parent;\n\n if (declarator.type !== AST_NODE_TYPES.VariableDeclarator || declarator.init !== node) return undefined;\n\n const { id, parent } = declarator;\n\n if (id.type !== AST_NODE_TYPES.Identifier || parent.type !== AST_NODE_TYPES.VariableDeclaration) return undefined;\n\n const statement = moduleStatement(parent);\n\n return statement === undefined ? undefined : { name: id.name, statement };\n}\n\n/** Whether the module spells this name anywhere, as a whole identifier. */\nfunction spells(source: SourceCode, name: string): boolean {\n const escaped = name.replaceAll('$', '\\\\$');\n\n return new RegExp(`(?<![\\\\w$])${escaped}(?![\\\\w$])`, 'u').test(source.getText());\n}\n\n/** The statement written before this one at the top of the module, where there is one. */\nfunction previousStatement(program: TSESTree.Program, statement: TSESTree.Node): TSESTree.Node | undefined {\n const index = program.body.indexOf(statement as TSESTree.ProgramStatement);\n\n return index > 0 ? program.body[index - 1] : undefined;\n}\n\n/**\n * The comments that lead the statement: those after the one before it, less a comment that trails the last line of\n * that statement, which belongs to it.\n */\nfunction leadingComments(source: SourceCode, statement: TSESTree.Node): readonly TSESTree.Comment[] {\n const program = statement.parent;\n\n if (program?.type !== AST_NODE_TYPES.Program) return [];\n\n const end = previousStatement(program, statement)?.range[1];\n const text = source.getText();\n\n // Ranges and text, not `loc`: both linters give every node and comment the first two.\n return source\n .getCommentsInside(program)\n .filter(\n comment =>\n comment.range[1] <= statement.range[0] &&\n (end === undefined || (comment.range[0] >= end && text.slice(end, comment.range[0]).includes('\\n'))),\n );\n}\n\n/** Whether a directive stands anywhere above the component within its declaration, leading comments included. */\nfunction silenced(source: SourceCode, statement: TSESTree.Node, component: TSESTree.Node): boolean {\n const inside = source.getCommentsInside(statement).filter(comment => comment.range[1] <= component.range[0]);\n\n return [...leadingComments(source, statement), ...inside].some(comment => DIRECTIVE.test(comment.value));\n}\n\n/** Whether another `const` at the top of the module would take the same name — `Badge` and `badge` both give `BadgeView`. */\nfunction collides(program: TSESTree.Program, declared: string, name: string): boolean {\n return program.body.some(statement => {\n const declaration = statement.type === AST_NODE_TYPES.ExportNamedDeclaration ? statement.declaration : statement;\n\n if (declaration?.type !== AST_NODE_TYPES.VariableDeclaration) return false;\n\n return declaration.declarations.some(\n ({ id }) => id.type === AST_NODE_TYPES.Identifier && id.name !== declared && viewName(id.name) === name,\n );\n });\n}\n\n/**\n * The name the fix may write: capitalized in Latin, as React Compiler reads a component, spelled nowhere in the\n * module and taken by no other rewrite of it.\n */\nfunction freeName(source: SourceCode, statement: TSESTree.Node, declared: string): string | undefined {\n const name = viewName(declared);\n\n if (!/^[A-Z]/u.test(name) || spells(source, name)) return undefined;\n\n const program = statement.parent;\n\n return program?.type === AST_NODE_TYPES.Program && !collides(program, declared, name) ? name : undefined;\n}\n\n/**\n * The rewrite the report asks for: the component moves, as written, into a `const` of its own right above the\n * declaration and the comments that lead it, and the wrapper takes it by that name. Whatever the move would change\n * leaves the rewrite to the author: a component whose types come from the call, a name that is not a free component\n * name, and a directive that would stay behind to silence the wrapper instead of the component.\n */\nfunction hoist(source: SourceCode, node: TSESTree.CallExpression, component: TSESTree.Node): ReportFixFunction | null {\n const declared = topLevelDeclaration(node);\n\n if (declared === undefined || !keepsTypes(node, component)) return null;\n\n const name = freeName(source, declared.statement, declared.name);\n\n if (name === undefined || silenced(source, declared.statement, component)) return null;\n\n const [first] = leadingComments(source, declared.statement);\n const at = first?.range[0] ?? declared.statement.range[0];\n const text = `const ${name} = ${source.getText(component)};\\n\\n`;\n\n return fixer => [fixer.replaceTextRange([at, at], text), fixer.replaceText(component, name)];\n}\n\n/**\n * D494: React Compiler's inference mode compiles a component it can name — a function declared at the top of a\n * module under a capitalized name — and so do the compiler rules of oxlint. A component written inline inside\n * `requiresModels([...])(…)` is an anonymous argument, invisible to both, so the React reference declares it first\n * and wraps it after (D158).\n */\nconst noInlineRequiresModels = createRule<[], MessageIds>({\n create(context) {\n const source = context.sourceCode;\n\n return {\n CallExpression(node): void {\n const wrapped = wrappedComponent(source, node);\n\n if (wrapped === undefined || !isFunctionLike(wrapped.component)) return;\n\n // D309: a rewrite resolves its package exactly; a host's own wrapper is reported and left as written.\n const fix = wrapped.exact ? hoist(source, node, wrapped.component) : null;\n\n context.report({ fix, messageId: 'inlineComponent', node: wrapped.component });\n },\n };\n },\n meta: {\n docs: {\n description: 'The component `requiresModels` wraps is declared at the top of the module, not inline.',\n },\n fixable: 'code',\n messages: {\n inlineComponent:\n \"A component written inline inside `requiresModels([...])(…)` is invisible to React Compiler's inference mode and to the compiler rules of oxlint. Declare it at the top of the module under a capitalized name and wrap it after: `const CountView = () => …;` then `export const Count = requiresModels([CountModel])(CountView);` (D158, D494).\",\n },\n schema: [],\n type: 'suggestion',\n },\n name: 'no-inline-requires-models',\n});\n\nexport { noInlineRequiresModels };\n"],"names":["wrappedComponent","source","node","callee","component","AST_NODE_TYPES","isApi","DIRECTIVE","viewName","name","annotated","parameter","pattern","keepsTypes","isFunctionLike","moduleStatement","declaration","statement","topLevelDeclaration","declarator","id","parent","spells","escaped","previousStatement","program","index","leadingComments","end","text","comment","silenced","inside","collides","declared","freeName","hoist","first","at","fixer","noInlineRequiresModels","createRule","context","wrapped","fix"],"mappings":"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"}
|
|
@@ -1,4 +1,2 @@
|
|
|
1
|
-
declare const noInternalImports: import("
|
|
2
|
-
name: string;
|
|
3
|
-
};
|
|
1
|
+
declare const noInternalImports: import("../rule.js").RuleModule<"internalImport", []>;
|
|
4
2
|
export { noInternalImports };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{AST_NODE_TYPES as
|
|
1
|
+
import{AST_NODE_TYPES as n}from"@typescript-eslint/types";import{importTypeSource as o,sourceText as i}from"../ast.js";import{createRule as a,findVariable as s}from"../rule.js";const l=/^@opetope\/[^/]+\/internal(?:\/|$)/,p=a({create(t){const r=e=>{e!==null&&l.test(i(e)??"")&&t.report({messageId:"internalImport",node:e})};return{CallExpression(e){e.callee.type!==n.Identifier||e.callee.name!=="require"||(s(t.sourceCode.getScope(e),e.callee)?.defs.length??0)>0||r(e.arguments[0]??null)},ExportAllDeclaration:e=>r(e.source),ExportNamedDeclaration:e=>r(e.source),ImportDeclaration:e=>r(e.source),ImportExpression:e=>r(e.source),TSExternalModuleReference:e=>r(e.expression),TSImportType:e=>r(o(e))}},meta:{docs:{description:"Application code and tests use public Opetope entries instead of the internal ABI."},messages:{internalImport:"Opetope internal entries are reserved for library implementation and explicit host integration. Use public entries; tests run real Commands with runCommand from @opetope/runtime/testing."},schema:[],type:"problem"},name:"no-internal-imports"});export{p as noInternalImports};
|
|
2
2
|
//# sourceMappingURL=no-internal-imports.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-internal-imports.js","sources":["../../src/rules/no-internal-imports.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"no-internal-imports.js","sources":["../../src/rules/no-internal-imports.ts"],"sourcesContent":["import { AST_NODE_TYPES } from '@typescript-eslint/types';\nimport type { TSESTree } from '@typescript-eslint/types';\n\nimport { importTypeSource, sourceText } from '../ast';\nimport { createRule, findVariable } from '../rule';\n\n/** Match the package boundary, including deep paths, without claiming unrelated packages or `internals`. */\nconst internalEntry = /^@opetope\\/[^/]+\\/internal(?:\\/|$)/;\n\nconst noInternalImports = createRule<[], 'internalImport'>({\n create(context) {\n const inspect = (node: TSESTree.Node | null): void => {\n if (node !== null && internalEntry.test(sourceText(node) ?? ''))\n context.report({ messageId: 'internalImport', node });\n };\n return {\n CallExpression(node): void {\n if (node.callee.type !== AST_NODE_TYPES.Identifier || node.callee.name !== 'require') return;\n const variable = findVariable(context.sourceCode.getScope(node), node.callee);\n if ((variable?.defs.length ?? 0) > 0) return;\n inspect(node.arguments[0] ?? null);\n },\n ExportAllDeclaration: node => inspect(node.source),\n ExportNamedDeclaration: node => inspect(node.source),\n ImportDeclaration: node => inspect(node.source),\n ImportExpression: node => inspect(node.source),\n TSExternalModuleReference: node => inspect(node.expression),\n TSImportType: node => inspect(importTypeSource(node)),\n };\n },\n meta: {\n docs: { description: 'Application code and tests use public Opetope entries instead of the internal ABI.' },\n messages: {\n internalImport:\n 'Opetope internal entries are reserved for library implementation and explicit host integration. Use public entries; tests run real Commands with runCommand from @opetope/runtime/testing.',\n },\n schema: [],\n type: 'problem',\n },\n name: 'no-internal-imports',\n});\n\nexport { noInternalImports };\n"],"names":["internalEntry","noInternalImports","createRule","context","inspect","node","sourceText","AST_NODE_TYPES","findVariable","importTypeSource"],"mappings":"iLAOA,MAAMA,EAAgB,qCAEhBC,EAAoBC,EAAiC,CACzD,OAAOC,EAAO,CACZ,MAAMC,EAAWC,GAAoC,CAC/CA,IAAS,MAAQL,EAAc,KAAKM,EAAWD,CAAI,GAAK,EAAE,GAC5DF,EAAQ,OAAO,CAAE,UAAW,iBAAkB,KAAAE,CAAI,CAAE,CACxD,EACA,MAAO,CACL,eAAeA,EAAI,CACbA,EAAK,OAAO,OAASE,EAAe,YAAcF,EAAK,OAAO,OAAS,YAC1DG,EAAaL,EAAQ,WAAW,SAASE,CAAI,EAAGA,EAAK,MAAM,GAC7D,KAAK,QAAU,GAAK,GACnCD,EAAQC,EAAK,UAAU,CAAC,GAAK,IAAI,CACnC,EACA,qBAAsBA,GAAQD,EAAQC,EAAK,MAAM,EACjD,uBAAwBA,GAAQD,EAAQC,EAAK,MAAM,EACnD,kBAAmBA,GAAQD,EAAQC,EAAK,MAAM,EAC9C,iBAAkBA,GAAQD,EAAQC,EAAK,MAAM,EAC7C,0BAA2BA,GAAQD,EAAQC,EAAK,UAAU,EAC1D,aAAcA,GAAQD,EAAQK,EAAiBJ,CAAI,CAAC,EAExD,EACA,KAAM,CACJ,KAAM,CAAE,YAAa,oFAAoF,EACzG,SAAU,CACR,eACE,4LACH,EACD,OAAQ,CAAA,EACR,KAAM,SACP,EACD,KAAM,qBACP,CAAA"}
|