diff --git a/README.md b/README.md index afa74d7..6cce5dc 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,10 @@ If set `true`, always use explicit return. ✅ Set in the `recommended` configuration.\ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix). -| Name | Description | ⚠️ | 🔧 | -| :----------------------------------------------------- | :---------------------------------- | :- | :- | -| [arrow-return-style](docs/rules/arrow-return-style.md) | Enforce arrow function return style | ✅ | 🔧 | +| Name                    | Description | ⚠️ | 🔧 | +| :--------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------- | :- | :- | +| [arrow-return-style](docs/rules/arrow-return-style.md) | Enforce arrow function return style | ✅ | 🔧 | +| [no-export-default-arrow](docs/rules/no-export-default-arrow.md) | Disallow export default anonymous arrow function
_**Automatically fix using the current file name.**_ | ✅ | 🔧 | diff --git a/docs/rules/no-export-default-arrow.md b/docs/rules/no-export-default-arrow.md new file mode 100644 index 0000000..fcc8ec9 --- /dev/null +++ b/docs/rules/no-export-default-arrow.md @@ -0,0 +1,23 @@ +# Disallow export default anonymous arrow function
_**Automatically fix using the current file name.**_ (`arrow-return-style/no-export-default-arrow`) + +⚠️ This rule _warns_ in the ✅ `recommended` config. + +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + + + +If the default export is an anonymous arrow function, it will automatically be converted to a named function using the current file name for exporting. + +```ts +export default () => { + // ... +}; + +// ↓↓↓↓↓↓↓↓↓↓ + +const autoFixToCurrentFileName = () => { + // ... +}; + +export default autoFixToCurrentFileName; +``` diff --git a/package.json b/package.json index b84cf60..0619b34 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ ] }, "dependencies": { - "@typescript-eslint/utils": "^6.3.0" + "@typescript-eslint/utils": "^6.3.0", + "scule": "^1.0.0" }, "devDependencies": { "@tsconfig/node18": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4402a32..b3ad47a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: '@typescript-eslint/utils': specifier: ^6.3.0 version: 6.3.0(eslint@8.46.0)(typescript@5.1.6) + scule: + specifier: ^1.0.0 + version: 1.0.0 devDependencies: '@tsconfig/node18': @@ -4989,6 +4992,10 @@ packages: regexp-ast-analysis: 0.6.0 dev: true + /scule@1.0.0: + resolution: {integrity: sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==} + dev: false + /semver@5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true diff --git a/src/configs/recommended.ts b/src/configs/recommended.ts index 8564e97..0bf0e64 100644 --- a/src/configs/recommended.ts +++ b/src/configs/recommended.ts @@ -6,5 +6,6 @@ export default defineConfig({ rules: { 'arrow-body-style': 'off', 'arrow-return-style/arrow-return-style': 'warn', + 'arrow-return-style/no-export-default-arrow': 'warn', }, }); diff --git a/src/index.ts b/src/index.ts index e5e9c48..6e2bd48 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,8 @@ import type { Rule } from 'eslint'; import { name, version } from '../package.json'; import recommended from './configs/recommended'; -import { arrowReturnStyleRule, RULE_NAME } from './rules/arrow-return-style'; +import { arrowReturnStyleRule, RULE_NAME as arrowReturnStyleRuleName } from './rules/arrow-return-style'; +import { noExportDefaultArrowRule, RULE_NAME as noExportDefaultArrowRuleName } from './rules/no-export-default-arrow'; import { definePlugin } from './utils'; export default definePlugin({ @@ -15,6 +16,7 @@ export default definePlugin({ }, rules: { - [RULE_NAME]: arrowReturnStyleRule as unknown as Rule.RuleModule, + [arrowReturnStyleRuleName]: arrowReturnStyleRule as unknown as Rule.RuleModule, + [noExportDefaultArrowRuleName]: noExportDefaultArrowRule as unknown as Rule.RuleModule, }, }); diff --git a/src/rules/no-export-default-arrow.test.ts b/src/rules/no-export-default-arrow.test.ts new file mode 100644 index 0000000..5356a1c --- /dev/null +++ b/src/rules/no-export-default-arrow.test.ts @@ -0,0 +1,113 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; +import dedent from 'dedent'; +import { afterAll, describe, it } from 'vitest'; +import { noExportDefaultArrowRule, RULE_NAME } from './no-export-default-arrow'; + +RuleTester.afterAll = afterAll; +RuleTester.describe = describe; +RuleTester.it = it; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + + parserOptions: { + ecmaFeatures: { jsx: true }, + }, +}); + +ruleTester.run(RULE_NAME, noExportDefaultArrowRule, { + invalid: [ + { + code: dedent` + import { useState } from 'react' + + export default () => { + const [, update] = useState({}) + + const forceUpdate = () => { + update({}) + } + + return forceUpdate + } + `, + + errors: [{ messageId: 'disallowExportDefaultArrow' }], + + filename: 'useForceUpdate.ts', + + output: dedent` + import { useState } from 'react' + + const useForceUpdate = () => { + const [, update] = useState({}) + + const forceUpdate = () => { + update({}) + } + + return forceUpdate + } + + export default useForceUpdate + `, + }, + + { + code: dedent` + export default () => {} + + export const foo = () => 'foo' + `, + + errors: [{ messageId: 'disallowExportDefaultArrow' }], + + filename: 'use-mouse.tsx', + + output: dedent` + const useMouse = () => {} + + export const foo = () => 'foo' + + export default useMouse + `, + }, + + { + code: dedent` + export default () => 1 + + // line comment + + /* block comment */ + `, + + errors: [{ messageId: 'disallowExportDefaultArrow' }], + + filename: 'just_for_fun.js', + + output: dedent` + const justForFun = () => 1 + + // line comment + + /* block comment */ + + export default justForFun + `, + }, + ], + + valid: [ + dedent` + const foo = () => { + return 'foo' + } + + export default foo + `, + + 'const now = () => Date.now()', + 'export const useQuery = () => {}', + ], +}); diff --git a/src/rules/no-export-default-arrow.ts b/src/rules/no-export-default-arrow.ts new file mode 100644 index 0000000..1a13fa0 --- /dev/null +++ b/src/rules/no-export-default-arrow.ts @@ -0,0 +1,64 @@ +import path from 'node:path'; +import { AST_NODE_TYPES, ASTUtils, type TSESTree } from '@typescript-eslint/utils'; +import { camelCase } from 'scule'; +import { createRule } from '../utils/create-rule'; + +export const RULE_NAME = 'no-export-default-arrow'; + +export const noExportDefaultArrowRule = createRule({ + create: (context) => { + const sourceCode = context.getSourceCode(); + let program: TSESTree.Program; + + return { + ArrowFunctionExpression: (arrowFunction) => { + const { body: arrowBody, parent: arrowFunctionParent } = arrowFunction; + + if (arrowFunctionParent.type === AST_NODE_TYPES.ExportDefaultDeclaration) { + context.report({ + fix: (fixer) => { + const fixes = []; + const lastToken = sourceCode.getLastToken(program, { includeComments: true }) || arrowFunctionParent; + const fileName = context.getPhysicalFilename?.() || context.getFilename() || 'namedFunction'; + const { name: fileNameWithoutExtension } = path.parse(fileName); + const funcName = camelCase(fileNameWithoutExtension); + + fixes.push( + fixer.replaceText(arrowFunctionParent, `const ${funcName} = ${sourceCode.getText(arrowFunction)}`), + fixer.insertTextAfter(lastToken, `\n\nexport default ${funcName}`) + ); + + return fixes; + }, + + messageId: 'disallowExportDefaultArrow', + node: arrowFunction, + }); + } + }, + + Program: (node) => (program = node), + }; + }, + + defaultOptions: [], + + meta: { + docs: { + description: + 'Disallow export default anonymous arrow function
_**Automatically fix using the current file name.**_', + }, + + fixable: 'code', + + messages: { + disallowExportDefaultArrow: 'Disallow export default anonymous arrow function', + }, + + schema: [], + + type: 'suggestion', + }, + + name: RULE_NAME, +});