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) =>