From 183ca3c13db8b64ca4008ce00dc84ab31e0f3af1 Mon Sep 17 00:00:00 2001 From: yinm Date: Thu, 28 Dec 2023 17:54:18 +0900 Subject: [PATCH 1/5] yarn generate-rule --- docs/rules/no-empty-args.md | 36 +++++++++++++ lib/rules/no-empty-args.ts | 78 +++++++++++++++++++++++++++ tests/lib/rules/no-empty-args.test.ts | 34 ++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 docs/rules/no-empty-args.md create mode 100644 lib/rules/no-empty-args.ts create mode 100644 tests/lib/rules/no-empty-args.test.ts diff --git a/docs/rules/no-empty-args.md b/docs/rules/no-empty-args.md new file mode 100644 index 0000000..167c8e2 --- /dev/null +++ b/docs/rules/no-empty-args.md @@ -0,0 +1,36 @@ +# no-empty-args + + + + +## Rule Details + +Empty args is meaningless and should not be used. + +Examples of **incorrect** code for this rule: + +```js + +// fill me in + +``` + +Examples of **correct** code for this rule: + +```js + +// fill me in + +``` + +### Options + +If there are any options, describe them here. Otherwise, delete this section. + +## When Not To Use It + +Give a short description of when it would be appropriate to turn off this rule. If not applicable, delete this section. + +## Further Reading + +If there are other links that describe the issue this rule addresses, please include them here in a bulleted list. Otherwise, delete this section. \ No newline at end of file diff --git a/lib/rules/no-empty-args.ts b/lib/rules/no-empty-args.ts new file mode 100644 index 0000000..79a2424 --- /dev/null +++ b/lib/rules/no-empty-args.ts @@ -0,0 +1,78 @@ +/** + * @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, isVariableDeclaration } from '../utils/ast' + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +export = createStorybookRule({ + name: 'no-empty-args', + defaultOptions: [], + meta: { + type: 'problem', // `problem`, `suggestion`, or `layout` + docs: { + description: 'Fill me in', + // Add the categories that suit this rule. + categories: [CategoryId.RECOMMENDED], + recommended: 'warn', // `warn` or `error` + }, + messages: { + anyMessageIdHere: 'Fill me in', + }, + fixable: 'code', + hasSuggestions: true, + schema: [], // Add a schema if the rule has options. Otherwise remove this + }, + + create(context) { + // variables should be defined here + + //---------------------------------------------------------------------- + // Helpers + //---------------------------------------------------------------------- + + // any helper functions should go here or else delete this section + + //---------------------------------------------------------------------- + // Public + //---------------------------------------------------------------------- + + return { + /** + * 👉 Please read this and then delete this entire comment block. + * This is an example rule that reports an error in case a named export is called 'wrong'. + * Hopefully this will guide you to write your own rules. Make sure to always use the AST utilities and account for all possible cases. + * + * Keep in mind that sometimes AST nodes change when in javascript or typescript format. For example, the type of "declaration" from "export default {}" is ObjectExpression but in "export default {} as SomeType" is TSAsExpression. + * + * Use https://eslint.org/docs/developer-guide/working-with-rules for Eslint API reference + * And check https://astexplorer.net/ to help write rules + * Working with AST is fun. Good luck! + */ + ExportNamedDeclaration: function (node: TSESTree.ExportNamedDeclaration) { + const declaration = node.declaration + if (!declaration) return + // use AST helpers to make sure the nodes are of the right type + if (isVariableDeclaration(declaration)) { + const identifier = declaration.declarations[0]?.id + if (isIdentifier(identifier)) { + const { name } = identifier + if (name === 'wrong') { + context.report({ + node, + messageId: 'anyMessageIdHere', + }) + } + } + } + }, + } + }, +}) \ No newline at end of file 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..abe2afc --- /dev/null +++ b/tests/lib/rules/no-empty-args.test.ts @@ -0,0 +1,34 @@ +/** + * @fileoverview Empty args is meaningless and should not be used + * @author yinm + */ + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +import rule from '../../../lib/rules/no-empty-args' +import ruleTester from '../../utils/rule-tester' + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +ruleTester.run('no-empty-args', rule, { + /** + * 👉 Please read this and delete this entire comment block. + * This is an example test for a rule that reports an error in case a named export is called 'wrong' + * Use https://eslint.org/docs/developer-guide/working-with-rules for Eslint API reference + */ + valid: ['export const correct = {}'], + invalid: [ + { + code: 'export const wrong = {}', + errors: [ + { + messageId: 'anyMessageIdHere', // comes from the rule file + }, + ], + }, + ], +}) \ No newline at end of file From 7045028ffc16e8146267252f21c9585b43bbf716 Mon Sep 17 00:00:00 2001 From: yinm Date: Thu, 28 Dec 2023 18:05:58 +0900 Subject: [PATCH 2/5] Add CSF3 rule --- lib/rules/no-empty-args.ts | 93 ++++++++++++++++----------- tests/lib/rules/no-empty-args.test.ts | 61 +++++++++++++++--- 2 files changed, 106 insertions(+), 48 deletions(-) diff --git a/lib/rules/no-empty-args.ts b/lib/rules/no-empty-args.ts index 79a2424..462b7bd 100644 --- a/lib/rules/no-empty-args.ts +++ b/lib/rules/no-empty-args.ts @@ -6,7 +6,13 @@ import { TSESTree } from '@typescript-eslint/utils' import { createStorybookRule } from '../utils/create-storybook-rule' import { CategoryId } from '../utils/constants' -import { isIdentifier, isVariableDeclaration } from '../utils/ast' +import { + isIdentifier, + isObjectExpression, + isProperty, + isSpreadElement, + isVariableDeclaration, +} from '../utils/ast' //------------------------------------------------------------------------------ // Rule Definition @@ -16,63 +22,72 @@ export = createStorybookRule({ name: 'no-empty-args', defaultOptions: [], meta: { - type: 'problem', // `problem`, `suggestion`, or `layout` + type: 'suggestion', docs: { - description: 'Fill me in', - // Add the categories that suit this rule. - categories: [CategoryId.RECOMMENDED], - recommended: 'warn', // `warn` or `error` + description: 'A story should not have an empty args property', + categories: [CategoryId.RECOMMENDED, CategoryId.CSF], + recommended: 'error', }, messages: { - anyMessageIdHere: 'Fill me in', + detectEmptyArgs: 'Empty args should be removed as it is meaningless', + removeEmptyArgs: 'Remove empty args', }, fixable: 'code', hasSuggestions: true, - schema: [], // Add a schema if the rule has options. Otherwise remove this + schema: [], }, create(context) { - // variables should be defined here - //---------------------------------------------------------------------- // 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 - // any helper functions should go here or else delete this section + 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 { - /** - * 👉 Please read this and then delete this entire comment block. - * This is an example rule that reports an error in case a named export is called 'wrong'. - * Hopefully this will guide you to write your own rules. Make sure to always use the AST utilities and account for all possible cases. - * - * Keep in mind that sometimes AST nodes change when in javascript or typescript format. For example, the type of "declaration" from "export default {}" is ObjectExpression but in "export default {} as SomeType" is TSAsExpression. - * - * Use https://eslint.org/docs/developer-guide/working-with-rules for Eslint API reference - * And check https://astexplorer.net/ to help write rules - * Working with AST is fun. Good luck! - */ - ExportNamedDeclaration: function (node: TSESTree.ExportNamedDeclaration) { + // CSF3 + ExportDefaultDeclaration(node) { const declaration = node.declaration - if (!declaration) return - // use AST helpers to make sure the nodes are of the right type - if (isVariableDeclaration(declaration)) { - const identifier = declaration.declarations[0]?.id - if (isIdentifier(identifier)) { - const { name } = identifier - if (name === 'wrong') { - context.report({ - node, - messageId: 'anyMessageIdHere', - }) - } - } - } + 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) }, } }, -}) \ No newline at end of file +}) diff --git a/tests/lib/rules/no-empty-args.test.ts b/tests/lib/rules/no-empty-args.test.ts index abe2afc..20ac491 100644 --- a/tests/lib/rules/no-empty-args.test.ts +++ b/tests/lib/rules/no-empty-args.test.ts @@ -7,6 +7,8 @@ // 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' @@ -15,20 +17,61 @@ import ruleTester from '../../utils/rule-tester' //------------------------------------------------------------------------------ ruleTester.run('no-empty-args', rule, { - /** - * 👉 Please read this and delete this entire comment block. - * This is an example test for a rule that reports an error in case a named export is called 'wrong' - * Use https://eslint.org/docs/developer-guide/working-with-rules for Eslint API reference - */ - valid: ['export const correct = {}'], + 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' } } + `, + ], 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 wrong = {}', + code: 'export const PrimaryButton = { args: {} }', errors: [ { - messageId: 'anyMessageIdHere', // comes from the rule file + messageId: 'detectEmptyArgs', + type: AST_NODE_TYPES.Property, + suggestions: [ + { + messageId: 'removeEmptyArgs', + output: 'export const PrimaryButton = { }', + }, + ], }, ], }, ], -}) \ No newline at end of file +}) From 0f9e6a4e7052491b403758f2c77e9d3344838f3c Mon Sep 17 00:00:00 2001 From: yinm Date: Thu, 28 Dec 2023 18:10:36 +0900 Subject: [PATCH 3/5] Add CSF2 rule --- lib/rules/no-empty-args.ts | 32 ++++++++++++++++++++++++++ tests/lib/rules/no-empty-args.test.ts | 33 +++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/lib/rules/no-empty-args.ts b/lib/rules/no-empty-args.ts index 462b7bd..fe462fe 100644 --- a/lib/rules/no-empty-args.ts +++ b/lib/rules/no-empty-args.ts @@ -8,6 +8,7 @@ import { createStorybookRule } from '../utils/create-storybook-rule' import { CategoryId } from '../utils/constants' import { isIdentifier, + isMetaProperty, isObjectExpression, isProperty, isSpreadElement, @@ -88,6 +89,37 @@ export = createStorybookRule({ 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 index 20ac491..f8c1230 100644 --- a/tests/lib/rules/no-empty-args.test.ts +++ b/tests/lib/rules/no-empty-args.test.ts @@ -30,6 +30,16 @@ ruleTester.run('no-empty-args', rule, { const Default = {} export const PrimaryButton = { ...Default, args: { foo: 'bar' } } `, + + // CSF2 + ` + export const PrimaryButton = (args) =>