feat: enforce to use function declaration on top-level
This commit is contained in:
		| @@ -103,6 +103,8 @@ This config does NOT lint CSS. I personally use [UnoCSS](https://github.com/unoc | ||||
|  | ||||
| Sure, you can override the rules in your `.eslintrc` file. | ||||
|  | ||||
| <!-- eslint-skip --> | ||||
|  | ||||
| ```jsonc | ||||
| { | ||||
|   "extends": "@antfu", | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <script setup lang="ts"> | ||||
| const { t, availableLocales, locale } = useI18n() | ||||
|  | ||||
| const toggleLocales = () => { | ||||
| function toggleLocales() { | ||||
|   // change to some real logic | ||||
|   const locales = availableLocales | ||||
|   locale.value = locales[(locales.indexOf(locale.value) + 1) % locales.length] | ||||
|   | ||||
| @@ -3,7 +3,7 @@ const user = useUserStore() | ||||
| const name = $ref(user.savedName) | ||||
|  | ||||
| const router = useRouter() | ||||
| const go = () => { | ||||
| function go() { | ||||
|   if (name) | ||||
|     router.push(`/hi/${encodeURIComponent(name)}`) | ||||
| } | ||||
|   | ||||
| @@ -365,6 +365,7 @@ module.exports = { | ||||
|     // antfu | ||||
|     'antfu/if-newline': 'error', | ||||
|     'antfu/import-dedupe': 'error', | ||||
|     'antfu/top-level-function': 'error', | ||||
|     // 'antfu/prefer-inline-type-import': 'error', | ||||
|   }, | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import genericSpacing from './rules/generic-spacing' | ||||
| import ifNewline from './rules/if-newline' | ||||
| import importDedupe from './rules/import-dedupe' | ||||
| import preferInlineTypeImport from './rules/prefer-inline-type-import' | ||||
| import topLevelFunction from './rules/top-level-function' | ||||
|  | ||||
| export default { | ||||
|   rules: { | ||||
| @@ -9,5 +10,6 @@ export default { | ||||
|     'import-dedupe': importDedupe, | ||||
|     'prefer-inline-type-import': preferInlineTypeImport, | ||||
|     'generic-spacing': genericSpacing, | ||||
|     'top-level-function': topLevelFunction, | ||||
|   }, | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,51 @@ | ||||
| import { RuleTester } from '@typescript-eslint/utils/dist/ts-eslint' | ||||
| import { it } from 'vitest' | ||||
| import rule, { RULE_NAME } from './top-level-function' | ||||
|  | ||||
| const valids = [ | ||||
|   'function foo() {}', | ||||
|   // allow arrow function inside function | ||||
|   'function foo() { const bar = () => {} }', | ||||
|   // allow arrow function when type is specified | ||||
|   'const Foo: Bar = () => {}', | ||||
|   // allow let/var | ||||
|   'let foo = () => {}', | ||||
|   // allow arrow function in as | ||||
|   'const foo = (() => {}) as any', | ||||
|   // allow iife | ||||
|   ';(() => {})()', | ||||
| ] | ||||
|  | ||||
| const invalids = [ | ||||
|   [ | ||||
|     'const foo = (as: string, bar: number) => { return as + bar }', | ||||
|     'function foo (as: string, bar: number) { return as + bar }', | ||||
|   ], | ||||
|   [ | ||||
|     'const foo = <K, T extends Boolean>(as: string, bar: number): Omit<T, K> => as + bar', | ||||
|     'function foo <K, T extends Boolean>(as: string, bar: number): Omit<T, K> { return as + bar }', | ||||
|   ], | ||||
|   [ | ||||
|     'export const foo = () => {}', | ||||
|     'export function foo () {}', | ||||
|   ], | ||||
|   [ | ||||
|     'export const foo = () => ({})', | ||||
|     'export function foo () { return {} }', | ||||
|   ], | ||||
| ] | ||||
|  | ||||
| it('runs', () => { | ||||
|   const ruleTester: RuleTester = new RuleTester({ | ||||
|     parser: require.resolve('@typescript-eslint/parser'), | ||||
|   }) | ||||
|  | ||||
|   ruleTester.run(RULE_NAME, rule, { | ||||
|     valid: valids, | ||||
|     invalid: invalids.map(i => ({ | ||||
|       code: i[0], | ||||
|       output: i[1], | ||||
|       errors: [{ messageId: 'topLevelFunctionDeclaration' }], | ||||
|     })), | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										81
									
								
								packages/eslint-plugin-antfu/src/rules/top-level-function.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								packages/eslint-plugin-antfu/src/rules/top-level-function.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| import { createEslintRule } from '../utils' | ||||
|  | ||||
| export const RULE_NAME = 'top-level-function' | ||||
| export type MessageIds = 'topLevelFunctionDeclaration' | ||||
| export type Options = [] | ||||
|  | ||||
| export default createEslintRule<Options, MessageIds>({ | ||||
|   name: RULE_NAME, | ||||
|   meta: { | ||||
|     type: 'problem', | ||||
|     docs: { | ||||
|       description: 'Enforce top-level functions to be declared with function keyword', | ||||
|       recommended: 'error', | ||||
|     }, | ||||
|     fixable: 'code', | ||||
|     schema: [], | ||||
|     messages: { | ||||
|       topLevelFunctionDeclaration: 'Top-level functions should be declared with function keyword', | ||||
|     }, | ||||
|   }, | ||||
|   defaultOptions: [], | ||||
|   create: (context) => { | ||||
|     return { | ||||
|       VariableDeclaration(node) { | ||||
|         if (node.parent.type !== 'Program' && node.parent.type !== 'ExportNamedDeclaration') | ||||
|           return | ||||
|  | ||||
|         if (node.declarations.length !== 1) | ||||
|           return | ||||
|         if (node.kind !== 'const') | ||||
|           return | ||||
|         if (node.declare) | ||||
|           return | ||||
|  | ||||
|         const declaration = node.declarations[0] | ||||
|  | ||||
|         if (declaration.init?.type !== 'ArrowFunctionExpression') | ||||
|           return | ||||
|         if (declaration.id?.type !== 'Identifier') | ||||
|           return | ||||
|         if (declaration.id.typeAnnotation) | ||||
|           return | ||||
|  | ||||
|         const arrowFn = declaration.init | ||||
|         const body = declaration.init.body | ||||
|         const id = declaration.id | ||||
|  | ||||
|         context.report({ | ||||
|           node, | ||||
|           loc: { | ||||
|             start: node.loc.start, | ||||
|             end: node.loc.end, | ||||
|           }, | ||||
|           messageId: 'topLevelFunctionDeclaration', | ||||
|           fix(fixer) { | ||||
|             const code = context.getSourceCode().text | ||||
|             const textName = code.slice(id.range[0], id.range[1]) | ||||
|             const textArgs = arrowFn.params.length | ||||
|               ? code.slice(arrowFn.params[0].range[0], arrowFn.params[arrowFn.params.length - 1].range[1]) | ||||
|               : '' | ||||
|             const textBody = body.type === 'BlockStatement' | ||||
|               ? code.slice(body.range[0], body.range[1]) | ||||
|               : `{ return ${code.slice(body.range[0], body.range[1])} }` | ||||
|             const textGeneric = arrowFn.typeParameters | ||||
|               ? code.slice(arrowFn.typeParameters.range[0], arrowFn.typeParameters.range[1]) | ||||
|               : '' | ||||
|             const textTypeReturn = arrowFn.returnType | ||||
|               ? code.slice(arrowFn.returnType.range[0], arrowFn.returnType.range[1]) | ||||
|               : '' | ||||
|             const text = `function ${textName} ${textGeneric}(${textArgs})${textTypeReturn} ${textBody}` | ||||
|             // console.log({ | ||||
|             //   input: code.slice(node.range[0], node.range[1]), | ||||
|             //   output: text, | ||||
|             // }) | ||||
|             return fixer.replaceTextRange([node.range[0], node.range[1]], text) | ||||
|           }, | ||||
|         }) | ||||
|       }, | ||||
|     } | ||||
|   }, | ||||
| }) | ||||
		Reference in New Issue
	
	Block a user