diff --git a/README.md b/README.md
index 626cb28..8d6d2b7 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,7 @@ This plugin does not support MDX files.
| [`storybook/csf-component`](./docs/rules/csf-component.md) | The component property should be set | |
|
| [`storybook/default-exports`](./docs/rules/default-exports.md) | Story files should have a default export | 🔧 | |
| [`storybook/hierarchy-separator`](./docs/rules/hierarchy-separator.md) | Deprecated hierarchy separator in title property | 🔧 | |
+| [`storybook/no-empty-args`](./docs/rules/no-empty-args.md) | A story should not have an empty args property | 🔧 | |
| [`storybook/no-redundant-story-name`](./docs/rules/no-redundant-story-name.md) | A story should not have a redundant name property | 🔧 | |
| [`storybook/no-stories-of`](./docs/rules/no-stories-of.md) | storiesOf is deprecated and should not be used | | |
| [`storybook/no-title-property-in-meta`](./docs/rules/no-title-property-in-meta.md) | Do not define a title in meta | 🔧 | |
diff --git a/docs/rules/no-empty-args.md b/docs/rules/no-empty-args.md
new file mode 100644
index 0000000..d25edf3
--- /dev/null
+++ b/docs/rules/no-empty-args.md
@@ -0,0 +1,32 @@
+# no-empty-args
+
+
+
+**Included in these configurations**:
+
+
+
+## Rule Details
+
+Empty args is meaningless and should not be used.
+
+Examples of **incorrect** code for this rule:
+
+```js
+export default {
+ component: Button,
+ args: {},
+}
+```
+
+Examples of **correct** code for this rule:
+
+```js
+export default {
+ component: Button,
+}
+```
+
+## When Not To Use It
+
+If you're not strictly enforcing this rule in your codebase (thus allowing empty args), you should turn this rule off.
diff --git a/lib/configs/csf.ts b/lib/configs/csf.ts
index ba33ab6..d072f03 100644
--- a/lib/configs/csf.ts
+++ b/lib/configs/csf.ts
@@ -13,6 +13,7 @@ export = {
'storybook/csf-component': 'warn',
'storybook/default-exports': 'error',
'storybook/hierarchy-separator': 'warn',
+ 'storybook/no-empty-args': 'error',
'storybook/no-redundant-story-name': 'warn',
'storybook/story-exports': 'error',
},
diff --git a/lib/configs/recommended.ts b/lib/configs/recommended.ts
index 9996f33..9c919f1 100644
--- a/lib/configs/recommended.ts
+++ b/lib/configs/recommended.ts
@@ -14,6 +14,7 @@ export = {
'storybook/context-in-play-function': 'error',
'storybook/default-exports': 'error',
'storybook/hierarchy-separator': 'warn',
+ 'storybook/no-empty-args': 'error',
'storybook/no-redundant-story-name': 'warn',
'storybook/prefer-pascal-case': 'warn',
'storybook/story-exports': 'error',
diff --git a/lib/rules/no-empty-args.ts b/lib/rules/no-empty-args.ts
new file mode 100644
index 0000000..fe462fe
--- /dev/null
+++ b/lib/rules/no-empty-args.ts
@@ -0,0 +1,125 @@
+/**
+ * @fileoverview Empty args is meaningless and should not be used
+ * @author yinm
+ */
+
+import { TSESTree } from '@typescript-eslint/utils'
+import { createStorybookRule } from '../utils/create-storybook-rule'
+import { CategoryId } from '../utils/constants'
+import {
+ isIdentifier,
+ isMetaProperty,
+ isObjectExpression,
+ isProperty,
+ isSpreadElement,
+ isVariableDeclaration,
+} from '../utils/ast'
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+export = createStorybookRule({
+ name: 'no-empty-args',
+ defaultOptions: [],
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'A story should not have an empty args property',
+ categories: [CategoryId.RECOMMENDED, CategoryId.CSF],
+ recommended: 'error',
+ },
+ messages: {
+ detectEmptyArgs: 'Empty args should be removed as it is meaningless',
+ removeEmptyArgs: 'Remove empty args',
+ },
+ fixable: 'code',
+ hasSuggestions: true,
+ schema: [],
+ },
+
+ create(context) {
+ //----------------------------------------------------------------------
+ // Helpers
+ //----------------------------------------------------------------------
+ const validateObjectExpression = (node: TSESTree.ObjectExpression) => {
+ const argsNode = node.properties.find(
+ (prop) => isProperty(prop) && isIdentifier(prop.key) && prop.key.name === 'args'
+ )
+ if (typeof argsNode === 'undefined') return
+
+ if (
+ !isSpreadElement(argsNode) &&
+ isObjectExpression(argsNode.value) &&
+ argsNode.value.properties.length === 0
+ ) {
+ context.report({
+ node: argsNode,
+ messageId: 'detectEmptyArgs',
+ suggest: [
+ {
+ messageId: 'removeEmptyArgs',
+ fix(fixer) {
+ return fixer.remove(argsNode)
+ },
+ },
+ ],
+ })
+ }
+ }
+
+ //----------------------------------------------------------------------
+ // Public
+ //----------------------------------------------------------------------
+ return {
+ // CSF3
+ ExportDefaultDeclaration(node) {
+ const declaration = node.declaration
+ if (!isObjectExpression(declaration)) return
+
+ validateObjectExpression(declaration)
+ },
+
+ ExportNamedDeclaration(node) {
+ const declaration = node.declaration
+ if (!isVariableDeclaration(declaration)) return
+
+ const init = declaration.declarations[0]?.init
+ if (!isObjectExpression(init)) return
+
+ validateObjectExpression(init)
+ },
+
+ // CSF2
+ AssignmentExpression(node) {
+ const { left, right } = node
+
+ if (
+ 'property' in left &&
+ isIdentifier(left.property) &&
+ !isMetaProperty(left) &&
+ left.property.name === 'args'
+ ) {
+ if (
+ !isSpreadElement(right) &&
+ isObjectExpression(right) &&
+ right.properties.length === 0
+ ) {
+ context.report({
+ node,
+ messageId: 'detectEmptyArgs',
+ suggest: [
+ {
+ messageId: 'removeEmptyArgs',
+ fix(fixer) {
+ return fixer.remove(node)
+ },
+ },
+ ],
+ })
+ }
+ }
+ },
+ }
+ },
+})
diff --git a/tests/lib/rules/no-empty-args.test.ts b/tests/lib/rules/no-empty-args.test.ts
new file mode 100644
index 0000000..f8c1230
--- /dev/null
+++ b/tests/lib/rules/no-empty-args.test.ts
@@ -0,0 +1,110 @@
+/**
+ * @fileoverview Empty args is meaningless and should not be used
+ * @author yinm
+ */
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+import { AST_NODE_TYPES } from '@typescript-eslint/utils'
+import dedent from 'ts-dedent'
+import rule from '../../../lib/rules/no-empty-args'
+import ruleTester from '../../utils/rule-tester'
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+ruleTester.run('no-empty-args', rule, {
+ valid: [
+ // CSF3
+ `
+ export default {
+ component: Button,
+ }
+ `,
+ "export const PrimaryButton = { args: { foo: 'bar' } }",
+ "export const PrimaryButton: Story = { args: { foo: 'bar' } }",
+ `
+ const Default = {}
+ export const PrimaryButton = { ...Default, args: { foo: 'bar' } }
+ `,
+
+ // CSF2
+ `
+ export const PrimaryButton = (args) =>
+ PrimaryButton.args = { primary: true }
+ `,
+ `
+ export const PrimaryButton = Template.bind({})
+ PrimaryButton.storyName = 'The Primary Button'
+ `,
+ ],
+ invalid: [
+ // CSF3
+ {
+ code: dedent`
+ export default {
+ component: Button,
+ args: {}
+ }
+ `,
+ errors: [
+ {
+ messageId: 'detectEmptyArgs',
+ type: AST_NODE_TYPES.Property,
+ suggestions: [
+ {
+ messageId: 'removeEmptyArgs',
+ output: dedent`
+ export default {
+ component: Button,
+
+ }
+ `,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ code: 'export const PrimaryButton = { args: {} }',
+ errors: [
+ {
+ messageId: 'detectEmptyArgs',
+ type: AST_NODE_TYPES.Property,
+ suggestions: [
+ {
+ messageId: 'removeEmptyArgs',
+ output: 'export const PrimaryButton = { }',
+ },
+ ],
+ },
+ ],
+ },
+
+ // CSF2
+ {
+ code: dedent`
+ export const PrimaryButton = (args) =>
+ PrimaryButton.args = {}
+ `,
+ errors: [
+ {
+ messageId: 'detectEmptyArgs',
+ type: AST_NODE_TYPES.AssignmentExpression,
+ suggestions: [
+ {
+ messageId: 'removeEmptyArgs',
+ output: dedent`
+ export const PrimaryButton = (args) =>
+
+ `,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+})