eslint-plugin-executable-stories-vitest 2.1.1 → 2.1.3

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/dist/index.cjs CHANGED
@@ -40,7 +40,9 @@ var STEP_NAMES = /* @__PURE__ */ new Set([
40
40
  "context",
41
41
  "execute",
42
42
  "action",
43
- "verify"
43
+ "verify",
44
+ "fn",
45
+ "expect"
44
46
  ]);
45
47
  function isFunction(node) {
46
48
  return node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/rules/require-init-before-steps.ts","../src/rules/require-task-for-story-init.ts","../src/rules/require-test-context-for-story-init.ts"],"sourcesContent":["import type { ESLint, Linter } from 'eslint';\nimport requireInitBeforeSteps from './rules/require-init-before-steps.js';\nimport requireTaskForStoryInit from './rules/require-task-for-story-init.js';\nimport requireTestContextForStoryInit from './rules/require-test-context-for-story-init.js';\n\nconst rules = {\n 'require-task-for-story-init': requireTaskForStoryInit,\n 'require-test-context-for-story-init': requireTestContextForStoryInit,\n 'require-init-before-steps': requireInitBeforeSteps,\n};\n\nconst configs: Record<string, Linter.Config[]> = {\n recommended: [\n {\n plugins: {\n 'executable-stories-vitest': { rules },\n },\n rules: {\n 'executable-stories-vitest/require-task-for-story-init': 'error',\n 'executable-stories-vitest/require-test-context-for-story-init':\n 'error',\n 'executable-stories-vitest/require-init-before-steps': 'error',\n },\n },\n ],\n};\n\nconst plugin: ESLint.Plugin = {\n meta: {\n name: 'eslint-plugin-executable-stories-vitest',\n version: '0.2.0',\n },\n rules,\n configs,\n};\n\nexport default plugin;\nexport { rules, configs };\n","import type { Rule } from 'eslint';\nimport type { CallExpression, MemberExpression, Node } from 'estree';\n\n/**\n * Rule: require-init-before-steps\n *\n * Ensures story.init(task) is called before any step markers (story.given/when/then/etc).\n * This is a best-effort static check - it flags step markers that appear before any story.init()\n * call in the same function scope.\n *\n * BAD:\n * story.given('something'); // No story.init() before this\n *\n * GOOD:\n * story.init(task);\n * story.given('something');\n */\n\nconst STEP_NAMES = new Set([\n 'given',\n 'when',\n 'then',\n 'and',\n 'but',\n 'arrange',\n 'act',\n 'assert',\n 'setup',\n 'context',\n 'execute',\n 'action',\n 'verify',\n]);\n\nfunction isFunction(node: Node): boolean {\n return (\n node.type === 'FunctionExpression' ||\n node.type === 'ArrowFunctionExpression' ||\n node.type === 'FunctionDeclaration'\n );\n}\n\nfunction isStoryInitCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n property.name === 'init'\n );\n}\n\nfunction isStoryStepCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n STEP_NAMES.has(property.name)\n );\n}\n\nfunction getContainingFunction(\n node: Node,\n context: Rule.RuleContext,\n): Node | null {\n const ancestors = context.sourceCode.getAncestors(node);\n for (let i = ancestors.length - 1; i >= 0; i--) {\n if (isFunction(ancestors[i])) {\n return ancestors[i];\n }\n }\n return null;\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require story.init(task) to be called before any step markers (story.given/when/then/etc) in Vitest.',\n recommended: true,\n },\n schema: [],\n messages: {\n requireInit:\n 'story.init(task) must be called before using step markers like story.given(), story.when(), story.then().',\n },\n },\n create(context) {\n let hasRelevantImport = false;\n // Track functions that have story.init() calls\n const functionsWithInit = new WeakSet<Node>();\n // Track step calls and their containing functions for later verification\n const pendingStepCalls: Array<{\n node: CallExpression;\n containingFunction: Node | null;\n }> = [];\n\n return {\n ImportDeclaration(node) {\n if (node.source.value === 'executable-stories-vitest') {\n hasRelevantImport = true;\n }\n },\n CallExpression(node: CallExpression) {\n if (!hasRelevantImport) return;\n\n if (isStoryInitCall(node)) {\n const containingFunction = getContainingFunction(node, context);\n if (containingFunction) {\n functionsWithInit.add(containingFunction);\n }\n return;\n }\n\n if (isStoryStepCall(node)) {\n const containingFunction = getContainingFunction(node, context);\n pendingStepCalls.push({ node, containingFunction });\n }\n },\n 'Program:exit'() {\n for (const { node, containingFunction } of pendingStepCalls) {\n // If no containing function, or the function doesn't have story.init(), report\n if (\n !containingFunction ||\n !functionsWithInit.has(containingFunction)\n ) {\n context.report({ node, messageId: 'requireInit' });\n }\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type { CallExpression, MemberExpression } from 'estree';\n\n/**\n * Rule: require-task-for-story-init\n *\n * In Vitest, story.init() requires the task argument.\n * Use: it('...', ({ task }) => { story.init(task); ... })\n *\n * BAD: story.init();\n * GOOD: story.init(task);\n */\n\nfunction isStoryInitCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n property.name === 'init'\n );\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n \"Require the task argument for story.init() in Vitest. Use it('...', ({ task }) => { story.init(task); ... }).\",\n recommended: true,\n },\n schema: [],\n messages: {\n requireTask:\n \"story.init(task) requires the task argument. Use it('...', ({ task }) => { story.init(task); ... }).\",\n },\n },\n\n create(context) {\n let hasRelevantImport = false;\n\n return {\n ImportDeclaration(node) {\n if (node.source.value === 'executable-stories-vitest') {\n hasRelevantImport = true;\n }\n },\n CallExpression(node: CallExpression) {\n if (!hasRelevantImport) return;\n if (!isStoryInitCall(node)) return;\n if (node.arguments.length >= 1) return;\n context.report({\n node,\n messageId: 'requireTask',\n });\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type {\n ArrowFunctionExpression,\n CallExpression,\n FunctionDeclaration,\n FunctionExpression,\n MemberExpression,\n Node,\n Property,\n VariableDeclarator,\n} from 'estree';\n\n/**\n * Rule: require-test-context-for-story-init\n *\n * Ensures story.init(task) is called inside a test/it callback.\n *\n * BAD: story.init(task); // at top level\n * GOOD: it('test', ({ task }) => { story.init(task); ... });\n */\n\nconst TEST_MODIFIERS = new Set(['only', 'skip', 'todo', 'concurrent', 'fails']);\n\nfunction isFunction(node: Node): boolean {\n return (\n node.type === 'FunctionExpression' ||\n node.type === 'ArrowFunctionExpression'\n );\n}\n\nfunction isFunctionNode(\n node: Node,\n): node is FunctionDeclaration | ArrowFunctionExpression | FunctionExpression {\n return (\n node.type === 'FunctionDeclaration' ||\n node.type === 'FunctionExpression' ||\n node.type === 'ArrowFunctionExpression'\n );\n}\n\nfunction isStoryInitCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n property.name === 'init'\n );\n}\n\nfunction isTestCallExpression(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type === 'Identifier') {\n return callee.name === 'test' || callee.name === 'it';\n }\n if (callee.type === 'MemberExpression') {\n if (callee.object.type !== 'Identifier') return false;\n if (callee.object.name !== 'test' && callee.object.name !== 'it')\n return false;\n if (callee.property.type !== 'Identifier') return false;\n return TEST_MODIFIERS.has(callee.property.name);\n }\n if (callee.type === 'CallExpression') {\n const inner = callee.callee;\n if (inner.type !== 'MemberExpression') return false;\n if (inner.object.type !== 'Identifier') return false;\n if (inner.object.name !== 'test' && inner.object.name !== 'it')\n return false;\n return (\n inner.property.type === 'Identifier' && inner.property.name === 'each'\n );\n }\n return false;\n}\n\nfunction insideTestCallback(\n node: CallExpression,\n context: Rule.RuleContext,\n): boolean {\n const ancestors = context.sourceCode.getAncestors(node);\n const functionAncestors = new Set(ancestors.filter(isFunction));\n\n for (const ancestor of ancestors) {\n if (ancestor.type !== 'CallExpression') continue;\n if (!isTestCallExpression(ancestor)) continue;\n for (const arg of ancestor.arguments) {\n if (arg && typeof arg === 'object' && functionAncestors.has(arg)) {\n return true;\n }\n }\n }\n return false;\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require story.init(task) to be called inside a test/it callback in Vitest.',\n recommended: true,\n },\n schema: [],\n messages: {\n requireTest:\n \"story.init(task) must be called inside a test/it callback (e.g. it('...', ({ task }) => { story.init(task); ... })).\",\n },\n },\n create(context) {\n let hasRelevantImport = false;\n const namedFunctions = new Map<string, Node>();\n const testCallbackNames = new Set<string>();\n const pendingStoryInitCalls: Array<{\n node: CallExpression;\n containingFunctionName: string | null;\n }> = [];\n\n function getContainingFunctionName(node: CallExpression): string | null {\n const ancestors = context.sourceCode.getAncestors(node);\n for (let i = ancestors.length - 1; i >= 0; i--) {\n const ancestor = ancestors[i];\n if (ancestor.type === 'FunctionDeclaration' && ancestor.id) {\n return ancestor.id.name;\n }\n if (ancestor.type === 'VariableDeclarator') {\n const declarator = ancestor as VariableDeclarator;\n if (\n declarator.id.type === 'Identifier' &&\n declarator.init &&\n isFunctionNode(declarator.init)\n ) {\n return declarator.id.name;\n }\n }\n if (ancestor.type === 'Property') {\n const prop = ancestor as Property;\n if (\n prop.key.type === 'Identifier' &&\n prop.value &&\n isFunctionNode(prop.value)\n ) {\n return prop.key.name;\n }\n if (prop.key.type === 'Identifier' && prop.method) {\n return prop.key.name;\n }\n }\n }\n return null;\n }\n\n return {\n ImportDeclaration(node) {\n if (node.source.value === 'executable-stories-vitest') {\n hasRelevantImport = true;\n }\n },\n FunctionDeclaration(node: FunctionDeclaration) {\n if (node.id) {\n namedFunctions.set(node.id.name, node);\n }\n },\n VariableDeclarator(node: VariableDeclarator) {\n if (\n node.id.type === 'Identifier' &&\n node.init &&\n isFunctionNode(node.init)\n ) {\n namedFunctions.set(node.id.name, node.init);\n }\n },\n CallExpression(node: CallExpression) {\n if (!hasRelevantImport) return;\n\n if (isTestCallExpression(node)) {\n for (const arg of node.arguments) {\n if (arg.type === 'Identifier') {\n testCallbackNames.add(arg.name);\n }\n if (\n arg.type === 'MemberExpression' &&\n arg.property.type === 'Identifier'\n ) {\n testCallbackNames.add(arg.property.name);\n }\n }\n return;\n }\n\n if (!isStoryInitCall(node)) return;\n if (insideTestCallback(node, context)) return;\n\n const containingFunctionName = getContainingFunctionName(node);\n pendingStoryInitCalls.push({ node, containingFunctionName });\n },\n 'Program:exit'() {\n for (const { node, containingFunctionName } of pendingStoryInitCalls) {\n if (\n containingFunctionName &&\n testCallbackNames.has(containingFunctionName)\n ) {\n continue;\n }\n context.report({ node, messageId: 'requireTest' });\n }\n },\n };\n },\n};\n\nexport default rule;\n"],"mappings":"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","names":["isStoryInitCall","rule","isFunction","isStoryInitCall","rule"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/rules/require-init-before-steps.ts","../src/rules/require-task-for-story-init.ts","../src/rules/require-test-context-for-story-init.ts"],"sourcesContent":["import type { ESLint, Linter } from 'eslint';\nimport requireInitBeforeSteps from './rules/require-init-before-steps.js';\nimport requireTaskForStoryInit from './rules/require-task-for-story-init.js';\nimport requireTestContextForStoryInit from './rules/require-test-context-for-story-init.js';\n\nconst rules = {\n 'require-task-for-story-init': requireTaskForStoryInit,\n 'require-test-context-for-story-init': requireTestContextForStoryInit,\n 'require-init-before-steps': requireInitBeforeSteps,\n};\n\nconst configs: Record<string, Linter.Config[]> = {\n recommended: [\n {\n plugins: {\n 'executable-stories-vitest': { rules },\n },\n rules: {\n 'executable-stories-vitest/require-task-for-story-init': 'error',\n 'executable-stories-vitest/require-test-context-for-story-init':\n 'error',\n 'executable-stories-vitest/require-init-before-steps': 'error',\n },\n },\n ],\n};\n\nconst plugin: ESLint.Plugin = {\n meta: {\n name: 'eslint-plugin-executable-stories-vitest',\n version: '0.2.0',\n },\n rules,\n configs,\n};\n\nexport default plugin;\nexport { rules, configs };\n","import type { Rule } from 'eslint';\nimport type { CallExpression, MemberExpression, Node } from 'estree';\n\n/**\n * Rule: require-init-before-steps\n *\n * Ensures story.init(task) is called before any step markers (story.given/when/then/etc).\n * This is a best-effort static check - it flags step markers that appear before any story.init()\n * call in the same function scope.\n *\n * BAD:\n * story.given('something'); // No story.init() before this\n *\n * GOOD:\n * story.init(task);\n * story.given('something');\n */\n\nconst STEP_NAMES = new Set([\n 'given',\n 'when',\n 'then',\n 'and',\n 'but',\n 'arrange',\n 'act',\n 'assert',\n 'setup',\n 'context',\n 'execute',\n 'action',\n 'verify',\n 'fn',\n 'expect',\n]);\n\nfunction isFunction(node: Node): boolean {\n return (\n node.type === 'FunctionExpression' ||\n node.type === 'ArrowFunctionExpression' ||\n node.type === 'FunctionDeclaration'\n );\n}\n\nfunction isStoryInitCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n property.name === 'init'\n );\n}\n\nfunction isStoryStepCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n STEP_NAMES.has(property.name)\n );\n}\n\nfunction getContainingFunction(\n node: Node,\n context: Rule.RuleContext,\n): Node | null {\n const ancestors = context.sourceCode.getAncestors(node);\n for (let i = ancestors.length - 1; i >= 0; i--) {\n if (isFunction(ancestors[i])) {\n return ancestors[i];\n }\n }\n return null;\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require story.init(task) to be called before any step markers (story.given/when/then/etc) in Vitest.',\n recommended: true,\n },\n schema: [],\n messages: {\n requireInit:\n 'story.init(task) must be called before using step markers like story.given(), story.when(), story.then().',\n },\n },\n create(context) {\n let hasRelevantImport = false;\n // Track functions that have story.init() calls\n const functionsWithInit = new WeakSet<Node>();\n // Track step calls and their containing functions for later verification\n const pendingStepCalls: Array<{\n node: CallExpression;\n containingFunction: Node | null;\n }> = [];\n\n return {\n ImportDeclaration(node) {\n if (node.source.value === 'executable-stories-vitest') {\n hasRelevantImport = true;\n }\n },\n CallExpression(node: CallExpression) {\n if (!hasRelevantImport) return;\n\n if (isStoryInitCall(node)) {\n const containingFunction = getContainingFunction(node, context);\n if (containingFunction) {\n functionsWithInit.add(containingFunction);\n }\n return;\n }\n\n if (isStoryStepCall(node)) {\n const containingFunction = getContainingFunction(node, context);\n pendingStepCalls.push({ node, containingFunction });\n }\n },\n 'Program:exit'() {\n for (const { node, containingFunction } of pendingStepCalls) {\n // If no containing function, or the function doesn't have story.init(), report\n if (\n !containingFunction ||\n !functionsWithInit.has(containingFunction)\n ) {\n context.report({ node, messageId: 'requireInit' });\n }\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type { CallExpression, MemberExpression } from 'estree';\n\n/**\n * Rule: require-task-for-story-init\n *\n * In Vitest, story.init() requires the task argument.\n * Use: it('...', ({ task }) => { story.init(task); ... })\n *\n * BAD: story.init();\n * GOOD: story.init(task);\n */\n\nfunction isStoryInitCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n property.name === 'init'\n );\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n \"Require the task argument for story.init() in Vitest. Use it('...', ({ task }) => { story.init(task); ... }).\",\n recommended: true,\n },\n schema: [],\n messages: {\n requireTask:\n \"story.init(task) requires the task argument. Use it('...', ({ task }) => { story.init(task); ... }).\",\n },\n },\n\n create(context) {\n let hasRelevantImport = false;\n\n return {\n ImportDeclaration(node) {\n if (node.source.value === 'executable-stories-vitest') {\n hasRelevantImport = true;\n }\n },\n CallExpression(node: CallExpression) {\n if (!hasRelevantImport) return;\n if (!isStoryInitCall(node)) return;\n if (node.arguments.length >= 1) return;\n context.report({\n node,\n messageId: 'requireTask',\n });\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type {\n ArrowFunctionExpression,\n CallExpression,\n FunctionDeclaration,\n FunctionExpression,\n MemberExpression,\n Node,\n Property,\n VariableDeclarator,\n} from 'estree';\n\n/**\n * Rule: require-test-context-for-story-init\n *\n * Ensures story.init(task) is called inside a test/it callback.\n *\n * BAD: story.init(task); // at top level\n * GOOD: it('test', ({ task }) => { story.init(task); ... });\n */\n\nconst TEST_MODIFIERS = new Set(['only', 'skip', 'todo', 'concurrent', 'fails']);\n\nfunction isFunction(node: Node): boolean {\n return (\n node.type === 'FunctionExpression' ||\n node.type === 'ArrowFunctionExpression'\n );\n}\n\nfunction isFunctionNode(\n node: Node,\n): node is FunctionDeclaration | ArrowFunctionExpression | FunctionExpression {\n return (\n node.type === 'FunctionDeclaration' ||\n node.type === 'FunctionExpression' ||\n node.type === 'ArrowFunctionExpression'\n );\n}\n\nfunction isStoryInitCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n property.name === 'init'\n );\n}\n\nfunction isTestCallExpression(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type === 'Identifier') {\n return callee.name === 'test' || callee.name === 'it';\n }\n if (callee.type === 'MemberExpression') {\n if (callee.object.type !== 'Identifier') return false;\n if (callee.object.name !== 'test' && callee.object.name !== 'it')\n return false;\n if (callee.property.type !== 'Identifier') return false;\n return TEST_MODIFIERS.has(callee.property.name);\n }\n if (callee.type === 'CallExpression') {\n const inner = callee.callee;\n if (inner.type !== 'MemberExpression') return false;\n if (inner.object.type !== 'Identifier') return false;\n if (inner.object.name !== 'test' && inner.object.name !== 'it')\n return false;\n return (\n inner.property.type === 'Identifier' && inner.property.name === 'each'\n );\n }\n return false;\n}\n\nfunction insideTestCallback(\n node: CallExpression,\n context: Rule.RuleContext,\n): boolean {\n const ancestors = context.sourceCode.getAncestors(node);\n const functionAncestors = new Set(ancestors.filter(isFunction));\n\n for (const ancestor of ancestors) {\n if (ancestor.type !== 'CallExpression') continue;\n if (!isTestCallExpression(ancestor)) continue;\n for (const arg of ancestor.arguments) {\n if (arg && typeof arg === 'object' && functionAncestors.has(arg)) {\n return true;\n }\n }\n }\n return false;\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require story.init(task) to be called inside a test/it callback in Vitest.',\n recommended: true,\n },\n schema: [],\n messages: {\n requireTest:\n \"story.init(task) must be called inside a test/it callback (e.g. it('...', ({ task }) => { story.init(task); ... })).\",\n },\n },\n create(context) {\n let hasRelevantImport = false;\n const namedFunctions = new Map<string, Node>();\n const testCallbackNames = new Set<string>();\n const pendingStoryInitCalls: Array<{\n node: CallExpression;\n containingFunctionName: string | null;\n }> = [];\n\n function getContainingFunctionName(node: CallExpression): string | null {\n const ancestors = context.sourceCode.getAncestors(node);\n for (let i = ancestors.length - 1; i >= 0; i--) {\n const ancestor = ancestors[i];\n if (ancestor.type === 'FunctionDeclaration' && ancestor.id) {\n return ancestor.id.name;\n }\n if (ancestor.type === 'VariableDeclarator') {\n const declarator = ancestor as VariableDeclarator;\n if (\n declarator.id.type === 'Identifier' &&\n declarator.init &&\n isFunctionNode(declarator.init)\n ) {\n return declarator.id.name;\n }\n }\n if (ancestor.type === 'Property') {\n const prop = ancestor as Property;\n if (\n prop.key.type === 'Identifier' &&\n prop.value &&\n isFunctionNode(prop.value)\n ) {\n return prop.key.name;\n }\n if (prop.key.type === 'Identifier' && prop.method) {\n return prop.key.name;\n }\n }\n }\n return null;\n }\n\n return {\n ImportDeclaration(node) {\n if (node.source.value === 'executable-stories-vitest') {\n hasRelevantImport = true;\n }\n },\n FunctionDeclaration(node: FunctionDeclaration) {\n if (node.id) {\n namedFunctions.set(node.id.name, node);\n }\n },\n VariableDeclarator(node: VariableDeclarator) {\n if (\n node.id.type === 'Identifier' &&\n node.init &&\n isFunctionNode(node.init)\n ) {\n namedFunctions.set(node.id.name, node.init);\n }\n },\n CallExpression(node: CallExpression) {\n if (!hasRelevantImport) return;\n\n if (isTestCallExpression(node)) {\n for (const arg of node.arguments) {\n if (arg.type === 'Identifier') {\n testCallbackNames.add(arg.name);\n }\n if (\n arg.type === 'MemberExpression' &&\n arg.property.type === 'Identifier'\n ) {\n testCallbackNames.add(arg.property.name);\n }\n }\n return;\n }\n\n if (!isStoryInitCall(node)) return;\n if (insideTestCallback(node, context)) return;\n\n const containingFunctionName = getContainingFunctionName(node);\n pendingStoryInitCalls.push({ node, containingFunctionName });\n },\n 'Program:exit'() {\n for (const { node, containingFunctionName } of pendingStoryInitCalls) {\n if (\n containingFunctionName &&\n testCallbackNames.has(containingFunctionName)\n ) {\n continue;\n }\n context.report({ node, messageId: 'requireTest' });\n }\n },\n };\n },\n};\n\nexport default rule;\n"],"mappings":"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","names":["isStoryInitCall","rule","isFunction","isStoryInitCall","rule"]}
package/dist/index.js CHANGED
@@ -12,7 +12,9 @@ var STEP_NAMES = /* @__PURE__ */ new Set([
12
12
  "context",
13
13
  "execute",
14
14
  "action",
15
- "verify"
15
+ "verify",
16
+ "fn",
17
+ "expect"
16
18
  ]);
17
19
  function isFunction(node) {
18
20
  return node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "FunctionDeclaration";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/rules/require-init-before-steps.ts","../src/rules/require-task-for-story-init.ts","../src/rules/require-test-context-for-story-init.ts","../src/index.ts"],"sourcesContent":["import type { Rule } from 'eslint';\nimport type { CallExpression, MemberExpression, Node } from 'estree';\n\n/**\n * Rule: require-init-before-steps\n *\n * Ensures story.init(task) is called before any step markers (story.given/when/then/etc).\n * This is a best-effort static check - it flags step markers that appear before any story.init()\n * call in the same function scope.\n *\n * BAD:\n * story.given('something'); // No story.init() before this\n *\n * GOOD:\n * story.init(task);\n * story.given('something');\n */\n\nconst STEP_NAMES = new Set([\n 'given',\n 'when',\n 'then',\n 'and',\n 'but',\n 'arrange',\n 'act',\n 'assert',\n 'setup',\n 'context',\n 'execute',\n 'action',\n 'verify',\n]);\n\nfunction isFunction(node: Node): boolean {\n return (\n node.type === 'FunctionExpression' ||\n node.type === 'ArrowFunctionExpression' ||\n node.type === 'FunctionDeclaration'\n );\n}\n\nfunction isStoryInitCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n property.name === 'init'\n );\n}\n\nfunction isStoryStepCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n STEP_NAMES.has(property.name)\n );\n}\n\nfunction getContainingFunction(\n node: Node,\n context: Rule.RuleContext,\n): Node | null {\n const ancestors = context.sourceCode.getAncestors(node);\n for (let i = ancestors.length - 1; i >= 0; i--) {\n if (isFunction(ancestors[i])) {\n return ancestors[i];\n }\n }\n return null;\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require story.init(task) to be called before any step markers (story.given/when/then/etc) in Vitest.',\n recommended: true,\n },\n schema: [],\n messages: {\n requireInit:\n 'story.init(task) must be called before using step markers like story.given(), story.when(), story.then().',\n },\n },\n create(context) {\n let hasRelevantImport = false;\n // Track functions that have story.init() calls\n const functionsWithInit = new WeakSet<Node>();\n // Track step calls and their containing functions for later verification\n const pendingStepCalls: Array<{\n node: CallExpression;\n containingFunction: Node | null;\n }> = [];\n\n return {\n ImportDeclaration(node) {\n if (node.source.value === 'executable-stories-vitest') {\n hasRelevantImport = true;\n }\n },\n CallExpression(node: CallExpression) {\n if (!hasRelevantImport) return;\n\n if (isStoryInitCall(node)) {\n const containingFunction = getContainingFunction(node, context);\n if (containingFunction) {\n functionsWithInit.add(containingFunction);\n }\n return;\n }\n\n if (isStoryStepCall(node)) {\n const containingFunction = getContainingFunction(node, context);\n pendingStepCalls.push({ node, containingFunction });\n }\n },\n 'Program:exit'() {\n for (const { node, containingFunction } of pendingStepCalls) {\n // If no containing function, or the function doesn't have story.init(), report\n if (\n !containingFunction ||\n !functionsWithInit.has(containingFunction)\n ) {\n context.report({ node, messageId: 'requireInit' });\n }\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type { CallExpression, MemberExpression } from 'estree';\n\n/**\n * Rule: require-task-for-story-init\n *\n * In Vitest, story.init() requires the task argument.\n * Use: it('...', ({ task }) => { story.init(task); ... })\n *\n * BAD: story.init();\n * GOOD: story.init(task);\n */\n\nfunction isStoryInitCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n property.name === 'init'\n );\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n \"Require the task argument for story.init() in Vitest. Use it('...', ({ task }) => { story.init(task); ... }).\",\n recommended: true,\n },\n schema: [],\n messages: {\n requireTask:\n \"story.init(task) requires the task argument. Use it('...', ({ task }) => { story.init(task); ... }).\",\n },\n },\n\n create(context) {\n let hasRelevantImport = false;\n\n return {\n ImportDeclaration(node) {\n if (node.source.value === 'executable-stories-vitest') {\n hasRelevantImport = true;\n }\n },\n CallExpression(node: CallExpression) {\n if (!hasRelevantImport) return;\n if (!isStoryInitCall(node)) return;\n if (node.arguments.length >= 1) return;\n context.report({\n node,\n messageId: 'requireTask',\n });\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type {\n ArrowFunctionExpression,\n CallExpression,\n FunctionDeclaration,\n FunctionExpression,\n MemberExpression,\n Node,\n Property,\n VariableDeclarator,\n} from 'estree';\n\n/**\n * Rule: require-test-context-for-story-init\n *\n * Ensures story.init(task) is called inside a test/it callback.\n *\n * BAD: story.init(task); // at top level\n * GOOD: it('test', ({ task }) => { story.init(task); ... });\n */\n\nconst TEST_MODIFIERS = new Set(['only', 'skip', 'todo', 'concurrent', 'fails']);\n\nfunction isFunction(node: Node): boolean {\n return (\n node.type === 'FunctionExpression' ||\n node.type === 'ArrowFunctionExpression'\n );\n}\n\nfunction isFunctionNode(\n node: Node,\n): node is FunctionDeclaration | ArrowFunctionExpression | FunctionExpression {\n return (\n node.type === 'FunctionDeclaration' ||\n node.type === 'FunctionExpression' ||\n node.type === 'ArrowFunctionExpression'\n );\n}\n\nfunction isStoryInitCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n property.name === 'init'\n );\n}\n\nfunction isTestCallExpression(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type === 'Identifier') {\n return callee.name === 'test' || callee.name === 'it';\n }\n if (callee.type === 'MemberExpression') {\n if (callee.object.type !== 'Identifier') return false;\n if (callee.object.name !== 'test' && callee.object.name !== 'it')\n return false;\n if (callee.property.type !== 'Identifier') return false;\n return TEST_MODIFIERS.has(callee.property.name);\n }\n if (callee.type === 'CallExpression') {\n const inner = callee.callee;\n if (inner.type !== 'MemberExpression') return false;\n if (inner.object.type !== 'Identifier') return false;\n if (inner.object.name !== 'test' && inner.object.name !== 'it')\n return false;\n return (\n inner.property.type === 'Identifier' && inner.property.name === 'each'\n );\n }\n return false;\n}\n\nfunction insideTestCallback(\n node: CallExpression,\n context: Rule.RuleContext,\n): boolean {\n const ancestors = context.sourceCode.getAncestors(node);\n const functionAncestors = new Set(ancestors.filter(isFunction));\n\n for (const ancestor of ancestors) {\n if (ancestor.type !== 'CallExpression') continue;\n if (!isTestCallExpression(ancestor)) continue;\n for (const arg of ancestor.arguments) {\n if (arg && typeof arg === 'object' && functionAncestors.has(arg)) {\n return true;\n }\n }\n }\n return false;\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require story.init(task) to be called inside a test/it callback in Vitest.',\n recommended: true,\n },\n schema: [],\n messages: {\n requireTest:\n \"story.init(task) must be called inside a test/it callback (e.g. it('...', ({ task }) => { story.init(task); ... })).\",\n },\n },\n create(context) {\n let hasRelevantImport = false;\n const namedFunctions = new Map<string, Node>();\n const testCallbackNames = new Set<string>();\n const pendingStoryInitCalls: Array<{\n node: CallExpression;\n containingFunctionName: string | null;\n }> = [];\n\n function getContainingFunctionName(node: CallExpression): string | null {\n const ancestors = context.sourceCode.getAncestors(node);\n for (let i = ancestors.length - 1; i >= 0; i--) {\n const ancestor = ancestors[i];\n if (ancestor.type === 'FunctionDeclaration' && ancestor.id) {\n return ancestor.id.name;\n }\n if (ancestor.type === 'VariableDeclarator') {\n const declarator = ancestor as VariableDeclarator;\n if (\n declarator.id.type === 'Identifier' &&\n declarator.init &&\n isFunctionNode(declarator.init)\n ) {\n return declarator.id.name;\n }\n }\n if (ancestor.type === 'Property') {\n const prop = ancestor as Property;\n if (\n prop.key.type === 'Identifier' &&\n prop.value &&\n isFunctionNode(prop.value)\n ) {\n return prop.key.name;\n }\n if (prop.key.type === 'Identifier' && prop.method) {\n return prop.key.name;\n }\n }\n }\n return null;\n }\n\n return {\n ImportDeclaration(node) {\n if (node.source.value === 'executable-stories-vitest') {\n hasRelevantImport = true;\n }\n },\n FunctionDeclaration(node: FunctionDeclaration) {\n if (node.id) {\n namedFunctions.set(node.id.name, node);\n }\n },\n VariableDeclarator(node: VariableDeclarator) {\n if (\n node.id.type === 'Identifier' &&\n node.init &&\n isFunctionNode(node.init)\n ) {\n namedFunctions.set(node.id.name, node.init);\n }\n },\n CallExpression(node: CallExpression) {\n if (!hasRelevantImport) return;\n\n if (isTestCallExpression(node)) {\n for (const arg of node.arguments) {\n if (arg.type === 'Identifier') {\n testCallbackNames.add(arg.name);\n }\n if (\n arg.type === 'MemberExpression' &&\n arg.property.type === 'Identifier'\n ) {\n testCallbackNames.add(arg.property.name);\n }\n }\n return;\n }\n\n if (!isStoryInitCall(node)) return;\n if (insideTestCallback(node, context)) return;\n\n const containingFunctionName = getContainingFunctionName(node);\n pendingStoryInitCalls.push({ node, containingFunctionName });\n },\n 'Program:exit'() {\n for (const { node, containingFunctionName } of pendingStoryInitCalls) {\n if (\n containingFunctionName &&\n testCallbackNames.has(containingFunctionName)\n ) {\n continue;\n }\n context.report({ node, messageId: 'requireTest' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { ESLint, Linter } from 'eslint';\nimport requireInitBeforeSteps from './rules/require-init-before-steps.js';\nimport requireTaskForStoryInit from './rules/require-task-for-story-init.js';\nimport requireTestContextForStoryInit from './rules/require-test-context-for-story-init.js';\n\nconst rules = {\n 'require-task-for-story-init': requireTaskForStoryInit,\n 'require-test-context-for-story-init': requireTestContextForStoryInit,\n 'require-init-before-steps': requireInitBeforeSteps,\n};\n\nconst configs: Record<string, Linter.Config[]> = {\n recommended: [\n {\n plugins: {\n 'executable-stories-vitest': { rules },\n },\n rules: {\n 'executable-stories-vitest/require-task-for-story-init': 'error',\n 'executable-stories-vitest/require-test-context-for-story-init':\n 'error',\n 'executable-stories-vitest/require-init-before-steps': 'error',\n },\n },\n ],\n};\n\nconst plugin: ESLint.Plugin = {\n meta: {\n name: 'eslint-plugin-executable-stories-vitest',\n version: '0.2.0',\n },\n rules,\n configs,\n};\n\nexport default plugin;\nexport { rules, configs };\n"],"mappings":"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","names":["isStoryInitCall","rule","isFunction","isStoryInitCall","rule"]}
1
+ {"version":3,"sources":["../src/rules/require-init-before-steps.ts","../src/rules/require-task-for-story-init.ts","../src/rules/require-test-context-for-story-init.ts","../src/index.ts"],"sourcesContent":["import type { Rule } from 'eslint';\nimport type { CallExpression, MemberExpression, Node } from 'estree';\n\n/**\n * Rule: require-init-before-steps\n *\n * Ensures story.init(task) is called before any step markers (story.given/when/then/etc).\n * This is a best-effort static check - it flags step markers that appear before any story.init()\n * call in the same function scope.\n *\n * BAD:\n * story.given('something'); // No story.init() before this\n *\n * GOOD:\n * story.init(task);\n * story.given('something');\n */\n\nconst STEP_NAMES = new Set([\n 'given',\n 'when',\n 'then',\n 'and',\n 'but',\n 'arrange',\n 'act',\n 'assert',\n 'setup',\n 'context',\n 'execute',\n 'action',\n 'verify',\n 'fn',\n 'expect',\n]);\n\nfunction isFunction(node: Node): boolean {\n return (\n node.type === 'FunctionExpression' ||\n node.type === 'ArrowFunctionExpression' ||\n node.type === 'FunctionDeclaration'\n );\n}\n\nfunction isStoryInitCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n property.name === 'init'\n );\n}\n\nfunction isStoryStepCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n STEP_NAMES.has(property.name)\n );\n}\n\nfunction getContainingFunction(\n node: Node,\n context: Rule.RuleContext,\n): Node | null {\n const ancestors = context.sourceCode.getAncestors(node);\n for (let i = ancestors.length - 1; i >= 0; i--) {\n if (isFunction(ancestors[i])) {\n return ancestors[i];\n }\n }\n return null;\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require story.init(task) to be called before any step markers (story.given/when/then/etc) in Vitest.',\n recommended: true,\n },\n schema: [],\n messages: {\n requireInit:\n 'story.init(task) must be called before using step markers like story.given(), story.when(), story.then().',\n },\n },\n create(context) {\n let hasRelevantImport = false;\n // Track functions that have story.init() calls\n const functionsWithInit = new WeakSet<Node>();\n // Track step calls and their containing functions for later verification\n const pendingStepCalls: Array<{\n node: CallExpression;\n containingFunction: Node | null;\n }> = [];\n\n return {\n ImportDeclaration(node) {\n if (node.source.value === 'executable-stories-vitest') {\n hasRelevantImport = true;\n }\n },\n CallExpression(node: CallExpression) {\n if (!hasRelevantImport) return;\n\n if (isStoryInitCall(node)) {\n const containingFunction = getContainingFunction(node, context);\n if (containingFunction) {\n functionsWithInit.add(containingFunction);\n }\n return;\n }\n\n if (isStoryStepCall(node)) {\n const containingFunction = getContainingFunction(node, context);\n pendingStepCalls.push({ node, containingFunction });\n }\n },\n 'Program:exit'() {\n for (const { node, containingFunction } of pendingStepCalls) {\n // If no containing function, or the function doesn't have story.init(), report\n if (\n !containingFunction ||\n !functionsWithInit.has(containingFunction)\n ) {\n context.report({ node, messageId: 'requireInit' });\n }\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type { CallExpression, MemberExpression } from 'estree';\n\n/**\n * Rule: require-task-for-story-init\n *\n * In Vitest, story.init() requires the task argument.\n * Use: it('...', ({ task }) => { story.init(task); ... })\n *\n * BAD: story.init();\n * GOOD: story.init(task);\n */\n\nfunction isStoryInitCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n property.name === 'init'\n );\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n \"Require the task argument for story.init() in Vitest. Use it('...', ({ task }) => { story.init(task); ... }).\",\n recommended: true,\n },\n schema: [],\n messages: {\n requireTask:\n \"story.init(task) requires the task argument. Use it('...', ({ task }) => { story.init(task); ... }).\",\n },\n },\n\n create(context) {\n let hasRelevantImport = false;\n\n return {\n ImportDeclaration(node) {\n if (node.source.value === 'executable-stories-vitest') {\n hasRelevantImport = true;\n }\n },\n CallExpression(node: CallExpression) {\n if (!hasRelevantImport) return;\n if (!isStoryInitCall(node)) return;\n if (node.arguments.length >= 1) return;\n context.report({\n node,\n messageId: 'requireTask',\n });\n },\n };\n },\n};\n\nexport default rule;\n","import type { Rule } from 'eslint';\nimport type {\n ArrowFunctionExpression,\n CallExpression,\n FunctionDeclaration,\n FunctionExpression,\n MemberExpression,\n Node,\n Property,\n VariableDeclarator,\n} from 'estree';\n\n/**\n * Rule: require-test-context-for-story-init\n *\n * Ensures story.init(task) is called inside a test/it callback.\n *\n * BAD: story.init(task); // at top level\n * GOOD: it('test', ({ task }) => { story.init(task); ... });\n */\n\nconst TEST_MODIFIERS = new Set(['only', 'skip', 'todo', 'concurrent', 'fails']);\n\nfunction isFunction(node: Node): boolean {\n return (\n node.type === 'FunctionExpression' ||\n node.type === 'ArrowFunctionExpression'\n );\n}\n\nfunction isFunctionNode(\n node: Node,\n): node is FunctionDeclaration | ArrowFunctionExpression | FunctionExpression {\n return (\n node.type === 'FunctionDeclaration' ||\n node.type === 'FunctionExpression' ||\n node.type === 'ArrowFunctionExpression'\n );\n}\n\nfunction isStoryInitCall(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type !== 'MemberExpression') return false;\n const member = callee as MemberExpression;\n const { object, property } = member;\n return (\n object.type === 'Identifier' &&\n object.name === 'story' &&\n property.type === 'Identifier' &&\n property.name === 'init'\n );\n}\n\nfunction isTestCallExpression(node: CallExpression): boolean {\n const { callee } = node;\n if (callee.type === 'Identifier') {\n return callee.name === 'test' || callee.name === 'it';\n }\n if (callee.type === 'MemberExpression') {\n if (callee.object.type !== 'Identifier') return false;\n if (callee.object.name !== 'test' && callee.object.name !== 'it')\n return false;\n if (callee.property.type !== 'Identifier') return false;\n return TEST_MODIFIERS.has(callee.property.name);\n }\n if (callee.type === 'CallExpression') {\n const inner = callee.callee;\n if (inner.type !== 'MemberExpression') return false;\n if (inner.object.type !== 'Identifier') return false;\n if (inner.object.name !== 'test' && inner.object.name !== 'it')\n return false;\n return (\n inner.property.type === 'Identifier' && inner.property.name === 'each'\n );\n }\n return false;\n}\n\nfunction insideTestCallback(\n node: CallExpression,\n context: Rule.RuleContext,\n): boolean {\n const ancestors = context.sourceCode.getAncestors(node);\n const functionAncestors = new Set(ancestors.filter(isFunction));\n\n for (const ancestor of ancestors) {\n if (ancestor.type !== 'CallExpression') continue;\n if (!isTestCallExpression(ancestor)) continue;\n for (const arg of ancestor.arguments) {\n if (arg && typeof arg === 'object' && functionAncestors.has(arg)) {\n return true;\n }\n }\n }\n return false;\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require story.init(task) to be called inside a test/it callback in Vitest.',\n recommended: true,\n },\n schema: [],\n messages: {\n requireTest:\n \"story.init(task) must be called inside a test/it callback (e.g. it('...', ({ task }) => { story.init(task); ... })).\",\n },\n },\n create(context) {\n let hasRelevantImport = false;\n const namedFunctions = new Map<string, Node>();\n const testCallbackNames = new Set<string>();\n const pendingStoryInitCalls: Array<{\n node: CallExpression;\n containingFunctionName: string | null;\n }> = [];\n\n function getContainingFunctionName(node: CallExpression): string | null {\n const ancestors = context.sourceCode.getAncestors(node);\n for (let i = ancestors.length - 1; i >= 0; i--) {\n const ancestor = ancestors[i];\n if (ancestor.type === 'FunctionDeclaration' && ancestor.id) {\n return ancestor.id.name;\n }\n if (ancestor.type === 'VariableDeclarator') {\n const declarator = ancestor as VariableDeclarator;\n if (\n declarator.id.type === 'Identifier' &&\n declarator.init &&\n isFunctionNode(declarator.init)\n ) {\n return declarator.id.name;\n }\n }\n if (ancestor.type === 'Property') {\n const prop = ancestor as Property;\n if (\n prop.key.type === 'Identifier' &&\n prop.value &&\n isFunctionNode(prop.value)\n ) {\n return prop.key.name;\n }\n if (prop.key.type === 'Identifier' && prop.method) {\n return prop.key.name;\n }\n }\n }\n return null;\n }\n\n return {\n ImportDeclaration(node) {\n if (node.source.value === 'executable-stories-vitest') {\n hasRelevantImport = true;\n }\n },\n FunctionDeclaration(node: FunctionDeclaration) {\n if (node.id) {\n namedFunctions.set(node.id.name, node);\n }\n },\n VariableDeclarator(node: VariableDeclarator) {\n if (\n node.id.type === 'Identifier' &&\n node.init &&\n isFunctionNode(node.init)\n ) {\n namedFunctions.set(node.id.name, node.init);\n }\n },\n CallExpression(node: CallExpression) {\n if (!hasRelevantImport) return;\n\n if (isTestCallExpression(node)) {\n for (const arg of node.arguments) {\n if (arg.type === 'Identifier') {\n testCallbackNames.add(arg.name);\n }\n if (\n arg.type === 'MemberExpression' &&\n arg.property.type === 'Identifier'\n ) {\n testCallbackNames.add(arg.property.name);\n }\n }\n return;\n }\n\n if (!isStoryInitCall(node)) return;\n if (insideTestCallback(node, context)) return;\n\n const containingFunctionName = getContainingFunctionName(node);\n pendingStoryInitCalls.push({ node, containingFunctionName });\n },\n 'Program:exit'() {\n for (const { node, containingFunctionName } of pendingStoryInitCalls) {\n if (\n containingFunctionName &&\n testCallbackNames.has(containingFunctionName)\n ) {\n continue;\n }\n context.report({ node, messageId: 'requireTest' });\n }\n },\n };\n },\n};\n\nexport default rule;\n","import type { ESLint, Linter } from 'eslint';\nimport requireInitBeforeSteps from './rules/require-init-before-steps.js';\nimport requireTaskForStoryInit from './rules/require-task-for-story-init.js';\nimport requireTestContextForStoryInit from './rules/require-test-context-for-story-init.js';\n\nconst rules = {\n 'require-task-for-story-init': requireTaskForStoryInit,\n 'require-test-context-for-story-init': requireTestContextForStoryInit,\n 'require-init-before-steps': requireInitBeforeSteps,\n};\n\nconst configs: Record<string, Linter.Config[]> = {\n recommended: [\n {\n plugins: {\n 'executable-stories-vitest': { rules },\n },\n rules: {\n 'executable-stories-vitest/require-task-for-story-init': 'error',\n 'executable-stories-vitest/require-test-context-for-story-init':\n 'error',\n 'executable-stories-vitest/require-init-before-steps': 'error',\n },\n },\n ],\n};\n\nconst plugin: ESLint.Plugin = {\n meta: {\n name: 'eslint-plugin-executable-stories-vitest',\n version: '0.2.0',\n },\n rules,\n configs,\n};\n\nexport default plugin;\nexport { rules, configs };\n"],"mappings":"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","names":["isStoryInitCall","rule","isFunction","isStoryInitCall","rule"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-executable-stories-vitest",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "type": "module",
5
5
  "description": "ESLint rules for executable-stories-vitest: step context, doc.story in test/it, require task for doc.story(title)",
6
6
  "main": "./dist/index.cjs",
@@ -16,7 +16,9 @@
16
16
  },
17
17
  "files": [
18
18
  "dist",
19
- "README.md"
19
+ "skills",
20
+ "README.md",
21
+ "bin"
20
22
  ],
21
23
  "repository": {
22
24
  "type": "git",
@@ -38,16 +40,20 @@
38
40
  "devDependencies": {
39
41
  "@types/eslint": "^9.6.1",
40
42
  "@types/estree": "^1.0.8",
41
- "@types/node": "^25.3.2",
43
+ "@types/node": "^25.5.0",
42
44
  "eslint": "^10.0.1",
43
45
  "tsup": "^8.5.1",
44
46
  "typescript": "^5.9.3",
45
- "vitest": "^4.0.18",
46
- "eslint-config-executable-stories": "0.2.0"
47
+ "vitest": "^4.1.0",
48
+ "eslint-config-executable-stories": "0.2.0",
49
+ "executable-stories-vitest": "8.1.3"
47
50
  },
48
51
  "engines": {
49
52
  "node": ">=22"
50
53
  },
54
+ "bin": {
55
+ "intent": "./bin/intent.js"
56
+ },
51
57
  "scripts": {
52
58
  "build": "tsup",
53
59
  "type-check": "node -e \"const fs=require('fs');const {execSync}=require('child_process');const p=require('path').resolve(process.cwd(),'../executable-stories-vitest/package.json');if(!fs.existsSync(p)){console.log('Skipping type-check: executable-stories-vitest not in workspace');process.exit(0);}execSync('pnpm --filter executable-stories-vitest run build && tsc --noEmit',{stdio:'inherit'});\"",
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: eslint-vitest-rules
3
+ description: >
4
+ ESLint flat config plugin for executable-stories-vitest. Three rules:
5
+ require-task-for-story-init (story.init must have task argument),
6
+ require-test-context-for-story-init (must be inside it/test callback),
7
+ require-init-before-steps (init before given/when/then). Recommended
8
+ config enables all at error level.
9
+ type: core
10
+ library: eslint-plugin-executable-stories-vitest
11
+ library_version: "0.2.0"
12
+ sources:
13
+ - "jagreehal/executable-stories:packages/eslint-plugin-executable-stories-vitest/src/index.ts"
14
+ ---
15
+
16
+ # ESLint Plugin: executable-stories-vitest
17
+
18
+ ## Setup
19
+
20
+ ```typescript
21
+ // eslint.config.mjs
22
+ import vitestStories from "eslint-plugin-executable-stories-vitest";
23
+
24
+ export default [
25
+ // Option A: Use recommended config (enables all rules at error)
26
+ ...vitestStories.configs.recommended,
27
+
28
+ // Option B: Manual rule configuration
29
+ {
30
+ plugins: {
31
+ "executable-stories-vitest": vitestStories,
32
+ },
33
+ rules: {
34
+ "executable-stories-vitest/require-task-for-story-init": "error",
35
+ "executable-stories-vitest/require-test-context-for-story-init": "error",
36
+ "executable-stories-vitest/require-init-before-steps": "error",
37
+ },
38
+ },
39
+ ];
40
+ ```
41
+
42
+ ## Core Patterns
43
+
44
+ ### Rule: require-task-for-story-init
45
+
46
+ Ensures `story.init(task)` is called with the `task` argument.
47
+
48
+ ```typescript
49
+ // Fails lint
50
+ it("my test", ({ task }) => {
51
+ story.init(); // Error: story.init(task) requires the task argument
52
+ });
53
+
54
+ // Passes lint
55
+ it("my test", ({ task }) => {
56
+ story.init(task);
57
+ });
58
+ ```
59
+
60
+ ### Rule: require-test-context-for-story-init
61
+
62
+ Ensures `story.init(task)` is called inside a `test()` or `it()` callback.
63
+
64
+ ```typescript
65
+ // Fails lint
66
+ function helper() {
67
+ story.init(task); // Error: must be inside a test/it callback
68
+ }
69
+
70
+ // Passes lint
71
+ it("my test", ({ task }) => {
72
+ story.init(task);
73
+ });
74
+
75
+ // Also detects modifiers: it.only, it.skip, it.todo, it.concurrent, it.fails
76
+ it.only("focused test", ({ task }) => {
77
+ story.init(task); // Passes lint
78
+ });
79
+ ```
80
+
81
+ ### Rule: require-init-before-steps
82
+
83
+ Ensures `story.init(task)` is called before any step markers.
84
+
85
+ ```typescript
86
+ // Fails lint
87
+ it("my test", ({ task }) => {
88
+ story.given("something"); // Error: story.init(task) must be called first
89
+ story.init(task);
90
+ });
91
+
92
+ // Passes lint
93
+ it("my test", ({ task }) => {
94
+ story.init(task);
95
+ story.given("something");
96
+ });
97
+ ```
98
+
99
+ Detects all step methods: `given`, `when`, `then`, `and`, `but`, `arrange`, `act`, `assert`, `setup`, `context`, `execute`, `action`, `verify`, `fn`, `expect`.
100
+
101
+ ## Common Mistakes
102
+
103
+ ### HIGH Using legacy .eslintrc instead of flat config
104
+
105
+ Wrong:
106
+
107
+ ```json
108
+ {
109
+ "plugins": ["executable-stories-vitest"],
110
+ "rules": {
111
+ "executable-stories-vitest/require-task-for-story-init": "error"
112
+ }
113
+ }
114
+ ```
115
+
116
+ Correct:
117
+
118
+ ```typescript
119
+ // eslint.config.mjs (flat config)
120
+ import vitestStories from "eslint-plugin-executable-stories-vitest";
121
+
122
+ export default [...vitestStories.configs.recommended];
123
+ ```
124
+
125
+ This plugin only supports ESLint 9 flat config. Legacy `.eslintrc` format is not supported.
126
+
127
+ Source: packages/eslint-plugin-executable-stories-vitest/src/index.ts
128
+
129
+ ### MEDIUM Not scoping rules to story test files
130
+
131
+ ```typescript
132
+ // eslint.config.mjs
133
+ import vitestStories from "eslint-plugin-executable-stories-vitest";
134
+
135
+ export default [
136
+ {
137
+ // Scope to story test files only
138
+ files: ["**/*.story.test.ts", "**/*.story.spec.ts"],
139
+ plugins: {
140
+ "executable-stories-vitest": vitestStories,
141
+ },
142
+ rules: {
143
+ "executable-stories-vitest/require-task-for-story-init": "error",
144
+ "executable-stories-vitest/require-test-context-for-story-init": "error",
145
+ "executable-stories-vitest/require-init-before-steps": "error",
146
+ },
147
+ },
148
+ ];
149
+ ```
150
+
151
+ Scoping avoids false positives on non-story test files that don't use the story API.
152
+
153
+ Source: packages/eslint-plugin-executable-stories-vitest/src/index.ts