diff --git a/.changeset/early-lies-sell.md b/.changeset/early-lies-sell.md new file mode 100644 index 00000000..7e58d62f --- /dev/null +++ b/.changeset/early-lies-sell.md @@ -0,0 +1,5 @@ +--- +'@devup-ui/eslint-plugin': patch +--- + +Implement style order rule diff --git a/packages/eslint-plugin/src/configs/__tests__/__snapshots__/recommended.test.ts.snap b/packages/eslint-plugin/src/configs/__tests__/__snapshots__/recommended.test.ts.snap index bd5c0b2d..f1467256 100644 --- a/packages/eslint-plugin/src/configs/__tests__/__snapshots__/recommended.test.ts.snap +++ b/packages/eslint-plugin/src/configs/__tests__/__snapshots__/recommended.test.ts.snap @@ -73,6 +73,22 @@ exports[`recommended > export recommended config 1`] = ` }, "name": "no-useless-tailing-nulls", }, + "style-order-range": { + "create": [Function], + "defaultOptions": [], + "meta": { + "docs": { + "description": "Ensures styleOrder prop is within valid range (0 < value < 255).", + "url": "https://github.com/dev-five-git/devup-ui/tree/main/packages/eslint-plugin/src/rules/style-order-range", + }, + "messages": { + "styleOrderRange": "styleOrder prop must be a number greater than 0 and less than 255.", + }, + "schema": [], + "type": "problem", + }, + "name": "style-order-range", + }, }, }, }, @@ -81,6 +97,7 @@ exports[`recommended > export recommended config 1`] = ` "@devup-ui/no-duplicate-value": "error", "@devup-ui/no-useless-responsive": "error", "@devup-ui/no-useless-tailing-nulls": "error", + "@devup-ui/style-order-range": "error", }, }, ] diff --git a/packages/eslint-plugin/src/configs/recommended.ts b/packages/eslint-plugin/src/configs/recommended.ts index 7bfef860..3827b73a 100644 --- a/packages/eslint-plugin/src/configs/recommended.ts +++ b/packages/eslint-plugin/src/configs/recommended.ts @@ -3,6 +3,7 @@ import { noDuplicateValue, noUselessResponsive, noUselessTailingNulls, + styleOrderRange, } from '../rules' export default [ @@ -14,6 +15,7 @@ export default [ 'css-utils-literal-only': cssUtilsLiteralOnly, 'no-duplicate-value': noDuplicateValue, 'no-useless-responsive': noUselessResponsive, + 'style-order-range': styleOrderRange, }, }, }, @@ -22,6 +24,7 @@ export default [ '@devup-ui/css-utils-literal-only': 'error', '@devup-ui/no-duplicate-value': 'error', '@devup-ui/no-useless-responsive': 'error', + '@devup-ui/style-order-range': 'error', }, }, ] diff --git a/packages/eslint-plugin/src/rules/__tests__/index.test.ts b/packages/eslint-plugin/src/rules/__tests__/index.test.ts index 787c17c4..6c6900b3 100644 --- a/packages/eslint-plugin/src/rules/__tests__/index.test.ts +++ b/packages/eslint-plugin/src/rules/__tests__/index.test.ts @@ -6,6 +6,7 @@ describe('export index', () => { cssUtilsLiteralOnly: expect.any(Object), noDuplicateValue: expect.any(Object), noUselessResponsive: expect.any(Object), + styleOrderRange: expect.any(Object), }) }) }) diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 2c7e3eb8..67184778 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -2,3 +2,4 @@ export * from './css-utils-literal-only' export * from './no-duplicate-value' export * from './no-useless-responsive' export * from './no-useless-tailing-nulls' +export * from './style-order-range' diff --git a/packages/eslint-plugin/src/rules/style-order-range/README.md b/packages/eslint-plugin/src/rules/style-order-range/README.md new file mode 100644 index 00000000..72c2da96 --- /dev/null +++ b/packages/eslint-plugin/src/rules/style-order-range/README.md @@ -0,0 +1,41 @@ +# style-order-range + +Ensures `styleOrder` prop is within valid range (0 < value < 255). + +## Rule Details + +This rule enforces that the `styleOrder` prop must be a number greater than 0 and less than 255. + +### Examples of **incorrect** code for this rule: + +```jsx +// Zero and negative values +
+
+
+ +// Values greater than or equal to 255 +
+
+
+ +// Non-numeric values +
+
+``` + +### Examples of **correct** code for this rule: + +```jsx +// Valid range values (0 < value < 255) +
+
+
+
+
+
+``` + +## When Not To Use It + +If you don't use `styleOrder` props or want to allow any value range, you can disable this rule. diff --git a/packages/eslint-plugin/src/rules/style-order-range/__tests__/index.test.ts b/packages/eslint-plugin/src/rules/style-order-range/__tests__/index.test.ts new file mode 100644 index 00000000..2ebc5e97 --- /dev/null +++ b/packages/eslint-plugin/src/rules/style-order-range/__tests__/index.test.ts @@ -0,0 +1,349 @@ +import { RuleTester } from '@typescript-eslint/rule-tester' + +import { styleOrderRange } from '../index' + +describe('style-order-range rule', () => { + const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 'latest', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + }) + + ruleTester.run('style-order-range rule', styleOrderRange, { + valid: [ + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: 1})', + filename: 'src/app/page.tsx', + }, + { + code: 'css({styleOrder: 1})', + filename: 'src/app/page.tsx', + }, + { + code: '', + filename: 'src/app/page.tsx', + }, + ], + invalid: [ + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { Box } from "@devup-ui/react";\n', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: someVariable})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: `someVariable`})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: 1000})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: -100})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: "1000"})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: `1000`})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: `${someVariable}`})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: +someVariable})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: -someVariable})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: typeof `100`})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: void `100`})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: delete `100`})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: ~ `100`})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: `100` + 100})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + { + code: 'import { css } from "@devup-ui/react";\ncss({styleOrder: ~10})', + filename: 'src/app/page.tsx', + errors: [ + { + messageId: 'styleOrderRange', + }, + ], + }, + ], + }) +}) diff --git a/packages/eslint-plugin/src/rules/style-order-range/index.ts b/packages/eslint-plugin/src/rules/style-order-range/index.ts new file mode 100644 index 00000000..978d1136 --- /dev/null +++ b/packages/eslint-plugin/src/rules/style-order-range/index.ts @@ -0,0 +1,163 @@ +import { + AST_NODE_TYPES, + ESLintUtils, + type TSESTree, +} from '@typescript-eslint/utils' +import type { RuleContext } from '@typescript-eslint/utils/ts-eslint' + +import { ImportStorage } from '../../utils/import-storage' + +const createRule = ESLintUtils.RuleCreator( + (name) => + `https://github.com/dev-five-git/devup-ui/tree/main/packages/eslint-plugin/src/rules/${name}`, +) + +function checkStyleOrderRange>( + expression: TSESTree.Expression, + context: T, +) { + let value: number | null = null + + if (expression.type === AST_NODE_TYPES.Literal) { + if (typeof expression.value === 'number') { + value = expression.value + } else if (typeof expression.value === 'string') { + const parsed = parseInt(expression.value, 10) + if (!isNaN(parsed)) { + value = parsed + } + } + } else if (expression.type === AST_NODE_TYPES.UnaryExpression) { + if ( + expression.argument.type === AST_NODE_TYPES.Literal && + typeof expression.argument.value === 'number' && + (expression.operator === '-' || expression.operator === '+') + ) { + value = + expression.operator === '-' + ? -expression.argument.value + : expression.argument.value + } else { + context.report({ + node: expression, + messageId: 'styleOrderRange', + }) + return + } + } else if (expression.type === AST_NODE_TYPES.TemplateLiteral) { + if (expression.expressions.length > 0) { + // error report + context.report({ + node: expression, + messageId: 'styleOrderRange', + }) + return + } else { + value = parseInt(expression.quasis[0].value.raw, 10) + if (isNaN(value)) { + // error report + context.report({ + node: expression, + messageId: 'styleOrderRange', + }) + return + } + } + } + + if (value === null || value < 1 || value > 254) { + context.report({ + node: expression, + messageId: 'styleOrderRange', + }) + } +} + +export const styleOrderRange = createRule({ + name: 'style-order-range', + defaultOptions: [], + meta: { + schema: [], + messages: { + styleOrderRange: + 'styleOrder prop must be a number greater than 0 and less than 255.', + }, + type: 'problem', + docs: { + description: + 'Ensures styleOrder prop is within valid range (0 < value < 255).', + }, + }, + create(context) { + const importStorage = new ImportStorage() + let devupContext: + | TSESTree.CallExpression + | TSESTree.JSXOpeningElement + | null = null + + return { + ImportDeclaration(node) { + importStorage.addImportByDeclaration(node) + }, + CallExpression(node) { + if ( + importStorage.checkContextType(node) === 'UTIL' && + node.arguments.length === 1 && + node.arguments[0].type === AST_NODE_TYPES.ObjectExpression + ) { + devupContext = node + } + }, + 'CallExpression:exit'(node) { + if (devupContext === node) { + devupContext = null + } + }, + Property(node) { + if (!devupContext) return + if ( + node.key.type === AST_NODE_TYPES.Identifier && + node.key.name === 'styleOrder' + ) { + const value = node.value + if ( + value.type !== AST_NODE_TYPES.AssignmentPattern && + value.type !== AST_NODE_TYPES.TSEmptyBodyFunctionExpression + ) { + checkStyleOrderRange(value, context) + } + } + }, + JSXOpeningElement(node) { + if (importStorage.checkContextType(node) === 'COMPONENT') { + devupContext = node + } + }, + 'JSXOpeningElement:exit'(node) { + if (devupContext === node) { + devupContext = null + } + }, + JSXAttribute(node) { + if (!devupContext) return + // styleOrder prop만 체크 + if ( + node.name.type !== AST_NODE_TYPES.JSXIdentifier || + node.name.name !== 'styleOrder' || + !node.value + ) { + return + } + + if (node.value.type === AST_NODE_TYPES.JSXExpressionContainer) { + const expression = node.value.expression + if (expression.type !== AST_NODE_TYPES.JSXEmptyExpression) { + checkStyleOrderRange(expression, context) + } + } else if (node.value.type === AST_NODE_TYPES.Literal) { + checkStyleOrderRange(node.value, context) + } + }, + } + }, +})