eslint-plugin-th-rules 1.15.4 → 1.15.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## [1.15.6](https://github.com/tomerh2001/eslint-plugin-th-rules/compare/v1.15.5...v1.15.6) (2024-12-30)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * enhance top-level functions rule to support async and export keywords ([cca9ceb](https://github.com/tomerh2001/eslint-plugin-th-rules/commit/cca9cebe77fa01820048a55c444e55bd297b883c))
7
+
8
+ ## [1.15.5](https://github.com/tomerh2001/eslint-plugin-th-rules/compare/v1.15.4...v1.15.5) (2024-12-30)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * enforce naming conventions for top-level functions and improve error messages ([c50194d](https://github.com/tomerh2001/eslint-plugin-th-rules/commit/c50194d3ad4c2ef98387d3bbee8d06a30e2aa458))
14
+
1
15
  ## [1.15.4](https://github.com/tomerh2001/eslint-plugin-th-rules/compare/v1.15.3...v1.15.4) (2024-12-30)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-th-rules",
3
- "version": "1.15.4",
3
+ "version": "1.15.6",
4
4
  "description": "A List of custom ESLint rules created by Tomer Horowitz",
5
5
  "keywords": [
6
6
  "eslint",
@@ -1,3 +1,5 @@
1
+ /* eslint-disable unicorn/prefer-module */
2
+
1
3
  const meta = {
2
4
  type: 'suggestion',
3
5
  docs: {
@@ -10,45 +12,140 @@ const meta = {
10
12
  schema: [],
11
13
  };
12
14
 
15
+ /**
16
+ * Build a replacement code string for an arrow function:
17
+ *
18
+ * @param {string} funcName - The name of the new function.
19
+ * @param {ArrowFunctionExpression} arrowNode - The ArrowFunctionExpression node.
20
+ * @param {import('eslint').SourceCode} sourceCode - The ESLint SourceCode object.
21
+ * @param {boolean} isExport - Whether or not this function is exported (e.g., `export const foo = ...`).
22
+ * @returns {string} The replacement code.
23
+ */
24
+ function buildArrowFunctionReplacement(functionName, arrowNode, sourceCode, isExport) {
25
+ const asyncKeyword = arrowNode.async ? 'async ' : '';
26
+ const exportKeyword = isExport ? 'export ' : '';
27
+
28
+ const parametersText = arrowNode.params.map(parameter => sourceCode.getText(parameter)).join(', ');
29
+
30
+ let bodyText;
31
+ if (arrowNode.body.type === 'BlockStatement') {
32
+ bodyText = sourceCode.getText(arrowNode.body);
33
+ } else {
34
+ const expressionText = sourceCode.getText(arrowNode.body);
35
+ bodyText = `{ return ${expressionText}; }`;
36
+ }
37
+
38
+ return `${exportKeyword}${asyncKeyword}function ${functionName}(${parametersText}) ${bodyText}`;
39
+ }
40
+
41
+ /**
42
+ * Build a replacement code string for a function expression:
43
+ *
44
+ * @param {string} funcName - The name of the new function.
45
+ * @param {FunctionExpression} funcExprNode - The FunctionExpression node.
46
+ * @param {import('eslint').SourceCode} sourceCode - The ESLint SourceCode object.
47
+ * @param {boolean} isExport - Whether or not this function is exported.
48
+ * @returns {string} The replacement code.
49
+ */
50
+ function buildFunctionExpressionReplacement(functionName, functionExprNode, sourceCode, isExport) {
51
+ const asyncKeyword = functionExprNode.async ? 'async ' : '';
52
+ const exportKeyword = isExport ? 'export ' : '';
53
+
54
+ const parametersText = functionExprNode.params.map(parameter => sourceCode.getText(parameter)).join(', ');
55
+ const bodyText = sourceCode.getText(functionExprNode.body);
56
+
57
+ return `${exportKeyword}${asyncKeyword}function ${functionName}(${parametersText}) ${bodyText}`;
58
+ }
59
+
60
+ /**
61
+ * Build a replacement for an anonymous top-level FunctionDeclaration (including async).
62
+ *
63
+ * @param {import('eslint').SourceCode} sourceCode
64
+ * @param {import('estree').FunctionDeclaration} node
65
+ * @param {string} [funcName='defaultFunction']
66
+ * @param {boolean} [isExport=false]
67
+ */
68
+ function buildAnonymousFunctionDeclarationReplacement(sourceCode, node, functionName = 'defaultFunction', isExport = false) {
69
+ 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}(`);
78
+
79
+ if (isExport && !replaced.trimStart().startsWith('export')) {
80
+ replaced = `${exportKeyword}${replaced}`;
81
+ }
82
+
83
+ return replaced;
84
+ }
85
+
13
86
  function create(context) {
87
+ const sourceCode = context.getSourceCode();
88
+
14
89
  return {
15
90
  VariableDeclarator(node) {
16
- if (node.parent.parent.type !== 'Program') {
91
+ const declParent = node.parent;
92
+ const grandParent = declParent.parent;
93
+
94
+ const isTopLevel
95
+ = grandParent.type === 'Program'
96
+ || grandParent.type === 'ExportNamedDeclaration'
97
+ || grandParent.type === 'ExportDefaultDeclaration';
98
+
99
+ if (!isTopLevel) {
17
100
  return;
18
101
  }
19
102
 
20
- const sourceCode = context.getSourceCode();
103
+ const isExport
104
+ = grandParent.type === 'ExportNamedDeclaration'
105
+ || grandParent.type === 'ExportDefaultDeclaration';
21
106
 
22
- if (node.init && node.init.type === 'ArrowFunctionExpression') {
23
- const functionName = node.id.name;
24
- const functionText = sourceCode.getText(node.init);
107
+ if (!node.init) {
108
+ return;
109
+ }
110
+
111
+ const functionName = node.id && node.id.name;
112
+ if (!functionName) {
113
+ return;
114
+ }
25
115
 
116
+ if (node.init.type === 'ArrowFunctionExpression') {
26
117
  context.report({
27
118
  node: node.init,
28
- message: 'Top-level functions must be named/regular functions.',
119
+ message: 'Top-level arrow functions must be named/regular functions.',
29
120
  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);
121
+ const replacement = buildArrowFunctionReplacement(
122
+ functionName,
123
+ node.init,
124
+ sourceCode,
125
+ isExport,
126
+ );
127
+
128
+ return fixer.replaceText(
129
+ isExport ? grandParent : declParent,
130
+ replacement,
131
+ );
38
132
  },
39
133
  });
40
- }
41
-
42
- if (node.init && node.init.type === 'FunctionExpression') {
43
- const functionName = node.id.name;
44
- const functionText = sourceCode.getText(node.init);
45
-
134
+ } else if (node.init.type === 'FunctionExpression') {
46
135
  context.report({
47
136
  node: node.init,
48
- message: 'Top-level functions must be named/regular functions.',
137
+ message: 'Top-level function expressions must be named/regular functions.',
49
138
  fix(fixer) {
50
- const fixedCode = `function ${functionName}${functionText.slice(functionText.indexOf('('))}`;
51
- return fixer.replaceText(node.parent, fixedCode);
139
+ const replacement = buildFunctionExpressionReplacement(
140
+ functionName,
141
+ node.init,
142
+ sourceCode,
143
+ isExport,
144
+ );
145
+ return fixer.replaceText(
146
+ isExport ? grandParent : declParent,
147
+ replacement,
148
+ );
52
149
  },
53
150
  });
54
151
  }
@@ -59,20 +156,39 @@ function create(context) {
59
156
  return;
60
157
  }
61
158
 
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}(`);
159
+ const parent = node.parent;
71
160
 
72
- return fixer.replaceText(node, fixedCode);
73
- },
74
- });
161
+ const isTopLevel
162
+ = parent.type === 'Program'
163
+ || parent.type === 'ExportNamedDeclaration'
164
+ || parent.type === 'ExportDefaultDeclaration';
165
+
166
+ if (!isTopLevel) {
167
+ return;
75
168
  }
169
+
170
+ const isExport
171
+ = parent.type === 'ExportNamedDeclaration'
172
+ || parent.type === 'ExportDefaultDeclaration';
173
+
174
+ context.report({
175
+ node,
176
+ message: 'Top-level anonymous function declarations must be named.',
177
+ fix(fixer) {
178
+ const newName = 'defaultFunction';
179
+ const replacement = buildAnonymousFunctionDeclarationReplacement(
180
+ sourceCode,
181
+ node,
182
+ newName,
183
+ isExport,
184
+ );
185
+
186
+ return fixer.replaceText(
187
+ isExport ? parent : node,
188
+ replacement,
189
+ );
190
+ },
191
+ });
76
192
  },
77
193
  };
78
194
  }