Skip to content

Commit cca9ceb

Browse files
committed
fix: enhance top-level functions rule to support async and export keywords
1 parent 926c6d7 commit cca9ceb

File tree

1 file changed

+46
-34
lines changed

1 file changed

+46
-34
lines changed

src/rules/top-level-functions.js

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,5 @@
11
/* eslint-disable unicorn/prefer-module */
22

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-
*/
143
const meta = {
154
type: 'suggestion',
165
docs: {
@@ -29,10 +18,13 @@ const meta = {
2918
* @param {string} funcName - The name of the new function.
3019
* @param {ArrowFunctionExpression} arrowNode - The ArrowFunctionExpression node.
3120
* @param {import('eslint').SourceCode} sourceCode - The ESLint SourceCode object.
32-
* @param {boolean} isExport - Whether or not this function is exported.
21+
* @param {boolean} isExport - Whether or not this function is exported (e.g., `export const foo = ...`).
3322
* @returns {string} The replacement code.
3423
*/
3524
function buildArrowFunctionReplacement(functionName, arrowNode, sourceCode, isExport) {
25+
const asyncKeyword = arrowNode.async ? 'async ' : '';
26+
const exportKeyword = isExport ? 'export ' : '';
27+
3628
const parametersText = arrowNode.params.map(parameter => sourceCode.getText(parameter)).join(', ');
3729

3830
let bodyText;
@@ -43,8 +35,7 @@ function buildArrowFunctionReplacement(functionName, arrowNode, sourceCode, isEx
4335
bodyText = `{ return ${expressionText}; }`;
4436
}
4537

46-
const exportKeyword = isExport ? 'export ' : '';
47-
return `${exportKeyword}function ${functionName}(${parametersText}) ${bodyText}`;
38+
return `${exportKeyword}${asyncKeyword}function ${functionName}(${parametersText}) ${bodyText}`;
4839
}
4940

5041
/**
@@ -57,36 +48,41 @@ function buildArrowFunctionReplacement(functionName, arrowNode, sourceCode, isEx
5748
* @returns {string} The replacement code.
5849
*/
5950
function buildFunctionExpressionReplacement(functionName, functionExprNode, sourceCode, isExport) {
51+
const asyncKeyword = functionExprNode.async ? 'async ' : '';
52+
const exportKeyword = isExport ? 'export ' : '';
53+
6054
const parametersText = functionExprNode.params.map(parameter => sourceCode.getText(parameter)).join(', ');
6155
const bodyText = sourceCode.getText(functionExprNode.body);
6256

63-
const exportKeyword = isExport ? 'export ' : '';
64-
return `${exportKeyword}function ${functionName}(${parametersText}) ${bodyText}`;
57+
return `${exportKeyword}${asyncKeyword}function ${functionName}(${parametersText}) ${bodyText}`;
6558
}
6659

6760
/**
68-
* Build a replacement for an anonymous top-level FunctionDeclaration.
61+
* Build a replacement for an anonymous top-level FunctionDeclaration (including async).
6962
*
7063
* @param {import('eslint').SourceCode} sourceCode
7164
* @param {import('estree').FunctionDeclaration} node
7265
* @param {string} [funcName='defaultFunction']
66+
* @param {boolean} [isExport=false]
7367
*/
74-
function buildAnonymousFunctionDeclarationReplacement(sourceCode, node, functionName = 'defaultFunction') {
68+
function buildAnonymousFunctionDeclarationReplacement(sourceCode, node, functionName = 'defaultFunction', isExport = false) {
7569
const originalText = sourceCode.getText(node);
70+
const asyncKeyword = node.async ? 'async ' : '';
71+
const exportKeyword = isExport ? 'export ' : '';
72+
73+
let replaced = originalText;
74+
const asyncFunctionRegex = /^\s*async\s+function\s*\(/;
75+
const functionRegex = /^\s*function\s*\(/;
76+
77+
replaced = asyncFunctionRegex.test(replaced) ? replaced.replace(asyncFunctionRegex, `async function ${functionName}(`) : replaced.replace(functionRegex, `function ${functionName}(`);
7678

77-
const fixedText = originalText.replace(
78-
/^(\s*function\s*)\(/,
79-
`$1${functionName}(`,
80-
);
81-
return fixedText;
79+
if (isExport && !replaced.trimStart().startsWith('export')) {
80+
replaced = `${exportKeyword}${replaced}`;
81+
}
82+
83+
return replaced;
8284
}
8385

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-
*/
9086
function create(context) {
9187
const sourceCode = context.getSourceCode();
9288

@@ -129,7 +125,10 @@ function create(context) {
129125
isExport,
130126
);
131127

132-
return fixer.replaceText(grandParent.type === 'Program' ? declParent : grandParent, replacement);
128+
return fixer.replaceText(
129+
isExport ? grandParent : declParent,
130+
replacement,
131+
);
133132
},
134133
});
135134
} else if (node.init.type === 'FunctionExpression') {
@@ -143,7 +142,10 @@ function create(context) {
143142
sourceCode,
144143
isExport,
145144
);
146-
return fixer.replaceText(grandParent.type === 'Program' ? declParent : grandParent, replacement);
145+
return fixer.replaceText(
146+
isExport ? grandParent : declParent,
147+
replacement,
148+
);
147149
},
148150
});
149151
}
@@ -156,23 +158,33 @@ function create(context) {
156158

157159
const parent = node.parent;
158160

159-
const isTopLevel = parent.type === 'Program'
161+
const isTopLevel
162+
= parent.type === 'Program'
160163
|| parent.type === 'ExportNamedDeclaration'
161164
|| parent.type === 'ExportDefaultDeclaration';
162165

163166
if (!isTopLevel) {
164167
return;
165168
}
166169

170+
const isExport
171+
= parent.type === 'ExportNamedDeclaration'
172+
|| parent.type === 'ExportDefaultDeclaration';
173+
167174
context.report({
168175
node,
169176
message: 'Top-level anonymous function declarations must be named.',
170177
fix(fixer) {
171178
const newName = 'defaultFunction';
172-
const replacement = buildAnonymousFunctionDeclarationReplacement(sourceCode, node, newName);
179+
const replacement = buildAnonymousFunctionDeclarationReplacement(
180+
sourceCode,
181+
node,
182+
newName,
183+
isExport,
184+
);
173185

174186
return fixer.replaceText(
175-
parent.type === 'Program' ? node : parent,
187+
isExport ? parent : node,
176188
replacement,
177189
);
178190
},

0 commit comments

Comments
 (0)