1+ /* eslint-disable unicorn/prefer-module */
2+
3+ /**
4+ * @type {Object }
5+ * @property {string } type - The type of the rule, in this case, 'suggestion'.
6+ * @property {Object } docs - Documentation related to the rule.
7+ * @property {string } docs.description - A brief description of the rule.
8+ * @property {string } docs.category - The category of the rule, 'Stylistic Issues'.
9+ * @property {boolean } docs.recommended - Indicates if the rule is recommended.
10+ * @property {string } docs.url - The URL to the documentation of the rule.
11+ * @property {string } fixable - Indicates if the rule is fixable, 'code'.
12+ * @property {Array } schema - The schema for the rule options.
13+ */
114const meta = {
215 type : 'suggestion' ,
316 docs : {
@@ -10,45 +23,127 @@ const meta = {
1023 schema : [ ] ,
1124} ;
1225
26+ /**
27+ * Build a replacement code string for an arrow function:
28+ *
29+ * @param {string } funcName - The name of the new function.
30+ * @param {ArrowFunctionExpression } arrowNode - The ArrowFunctionExpression node.
31+ * @param {import('eslint').SourceCode } sourceCode - The ESLint SourceCode object.
32+ * @param {boolean } isExport - Whether or not this function is exported.
33+ * @returns {string } The replacement code.
34+ */
35+ function buildArrowFunctionReplacement ( functionName , arrowNode , sourceCode , isExport ) {
36+ const parametersText = arrowNode . params . map ( parameter => sourceCode . getText ( parameter ) ) . join ( ', ' ) ;
37+
38+ let bodyText ;
39+ if ( arrowNode . body . type === 'BlockStatement' ) {
40+ bodyText = sourceCode . getText ( arrowNode . body ) ;
41+ } else {
42+ const expressionText = sourceCode . getText ( arrowNode . body ) ;
43+ bodyText = `{ return ${ expressionText } ; }` ;
44+ }
45+
46+ const exportKeyword = isExport ? 'export ' : '' ;
47+ return `${ exportKeyword } function ${ functionName } (${ parametersText } ) ${ bodyText } ` ;
48+ }
49+
50+ /**
51+ * Build a replacement code string for a function expression:
52+ *
53+ * @param {string } funcName - The name of the new function.
54+ * @param {FunctionExpression } funcExprNode - The FunctionExpression node.
55+ * @param {import('eslint').SourceCode } sourceCode - The ESLint SourceCode object.
56+ * @param {boolean } isExport - Whether or not this function is exported.
57+ * @returns {string } The replacement code.
58+ */
59+ function buildFunctionExpressionReplacement ( functionName , functionExprNode , sourceCode , isExport ) {
60+ const parametersText = functionExprNode . params . map ( parameter => sourceCode . getText ( parameter ) ) . join ( ', ' ) ;
61+ const bodyText = sourceCode . getText ( functionExprNode . body ) ;
62+
63+ const exportKeyword = isExport ? 'export ' : '' ;
64+ return `${ exportKeyword } function ${ functionName } (${ parametersText } ) ${ bodyText } ` ;
65+ }
66+
67+ /**
68+ * Build a replacement for an anonymous top-level FunctionDeclaration.
69+ *
70+ * @param {import('eslint').SourceCode } sourceCode
71+ * @param {import('estree').FunctionDeclaration } node
72+ * @param {string } [funcName='defaultFunction']
73+ */
74+ function buildAnonymousFunctionDeclarationReplacement ( sourceCode , node , functionName = 'defaultFunction' ) {
75+ const originalText = sourceCode . getText ( node ) ;
76+
77+ const fixedText = originalText . replace (
78+ / ^ ( \s * f u n c t i o n \s * ) \( / ,
79+ `$1${ functionName } (` ,
80+ ) ;
81+ return fixedText ;
82+ }
83+
84+ /**
85+ * ESLint rule to enforce naming conventions for top-level functions.
86+ *
87+ * @param {Object } context - The rule context provided by ESLint.
88+ * @returns {Object } An object containing visitor methods for AST nodes.
89+ */
1390function create ( context ) {
91+ const sourceCode = context . getSourceCode ( ) ;
92+
1493 return {
1594 VariableDeclarator ( node ) {
16- if ( node . parent . parent . type !== 'Program' ) {
95+ const declParent = node . parent ;
96+ const grandParent = declParent . parent ;
97+
98+ const isTopLevel
99+ = grandParent . type === 'Program'
100+ || grandParent . type === 'ExportNamedDeclaration'
101+ || grandParent . type === 'ExportDefaultDeclaration' ;
102+
103+ if ( ! isTopLevel ) {
17104 return ;
18105 }
19106
20- const sourceCode = context . getSourceCode ( ) ;
107+ const isExport
108+ = grandParent . type === 'ExportNamedDeclaration'
109+ || grandParent . type === 'ExportDefaultDeclaration' ;
21110
22- if ( node . init && node . init . type === 'ArrowFunctionExpression' ) {
23- const functionName = node . id . name ;
24- const functionText = sourceCode . getText ( node . init ) ;
111+ if ( ! node . init ) {
112+ return ;
113+ }
25114
115+ const functionName = node . id && node . id . name ;
116+ if ( ! functionName ) {
117+ return ;
118+ }
119+
120+ if ( node . init . type === 'ArrowFunctionExpression' ) {
26121 context . report ( {
27122 node : node . init ,
28- message : 'Top-level functions must be named/regular functions.' ,
123+ message : 'Top-level arrow functions must be named/regular functions.' ,
29124 fix ( fixer ) {
30- const isSingleExpression = node . init . body . type !== 'BlockStatement' ;
31- const functionBody = isSingleExpression
32- ? `{ return ${ functionText . slice ( functionText . indexOf ( '=>' ) + 3 ) } ; }`
33- : functionText . slice ( functionText . indexOf ( '{' ) ) ;
34- const functionParameters = functionText . slice ( 0 , functionText . indexOf ( '=>' ) ) . trim ( ) ;
35-
36- const fixedCode = `function ${ functionName } ${ functionParameters } ${ functionBody } ` ;
37- return fixer . replaceText ( node . parent , fixedCode ) ;
125+ const replacement = buildArrowFunctionReplacement (
126+ functionName ,
127+ node . init ,
128+ sourceCode ,
129+ isExport ,
130+ ) ;
131+
132+ return fixer . replaceText ( grandParent . type === 'Program' ? declParent : grandParent , replacement ) ;
38133 } ,
39134 } ) ;
40- }
41-
42- if ( node . init && node . init . type === 'FunctionExpression' ) {
43- const functionName = node . id . name ;
44- const functionText = sourceCode . getText ( node . init ) ;
45-
135+ } else if ( node . init . type === 'FunctionExpression' ) {
46136 context . report ( {
47137 node : node . init ,
48- message : 'Top-level functions must be named/regular functions.' ,
138+ message : 'Top-level function expressions must be named/regular functions.' ,
49139 fix ( fixer ) {
50- const fixedCode = `function ${ functionName } ${ functionText . slice ( functionText . indexOf ( '(' ) ) } ` ;
51- return fixer . replaceText ( node . parent , fixedCode ) ;
140+ const replacement = buildFunctionExpressionReplacement (
141+ functionName ,
142+ node . init ,
143+ sourceCode ,
144+ isExport ,
145+ ) ;
146+ return fixer . replaceText ( grandParent . type === 'Program' ? declParent : grandParent , replacement ) ;
52147 } ,
53148 } ) ;
54149 }
@@ -59,20 +154,29 @@ function create(context) {
59154 return ;
60155 }
61156
62- if ( node . parent . type === 'Program' ) {
63- context . report ( {
64- node,
65- message : 'Top-level functions must be named.' ,
66- fix ( fixer ) {
67- const functionName = 'defaultFunction' ;
68- const sourceCode = context . getSourceCode ( ) ;
69- const functionText = sourceCode . getText ( node ) ;
70- const fixedCode = functionText . replace ( 'function (' , `function ${ functionName } (` ) ;
157+ const parent = node . parent ;
71158
72- return fixer . replaceText ( node , fixedCode ) ;
73- } ,
74- } ) ;
159+ const isTopLevel = parent . type === 'Program'
160+ || parent . type === 'ExportNamedDeclaration'
161+ || parent . type === 'ExportDefaultDeclaration' ;
162+
163+ if ( ! isTopLevel ) {
164+ return ;
75165 }
166+
167+ context . report ( {
168+ node,
169+ message : 'Top-level anonymous function declarations must be named.' ,
170+ fix ( fixer ) {
171+ const newName = 'defaultFunction' ;
172+ const replacement = buildAnonymousFunctionDeclarationReplacement ( sourceCode , node , newName ) ;
173+
174+ return fixer . replaceText (
175+ parent . type === 'Program' ? node : parent ,
176+ replacement ,
177+ ) ;
178+ } ,
179+ } ) ;
76180 } ,
77181 } ;
78182}
0 commit comments