eslint-plugin-th-rules 1.15.4 → 1.15.5

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,10 @@
1
+ ## [1.15.5](https://github.com/tomerh2001/eslint-plugin-th-rules/compare/v1.15.4...v1.15.5) (2024-12-30)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * enforce naming conventions for top-level functions and improve error messages ([c50194d](https://github.com/tomerh2001/eslint-plugin-th-rules/commit/c50194d3ad4c2ef98387d3bbee8d06a30e2aa458))
7
+
1
8
  ## [1.15.4](https://github.com/tomerh2001/eslint-plugin-th-rules/compare/v1.15.3...v1.15.4) (2024-12-30)
2
9
 
3
10
 
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.5",
4
4
  "description": "A List of custom ESLint rules created by Tomer Horowitz",
5
5
  "keywords": [
6
6
  "eslint",
@@ -1,3 +1,16 @@
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
+ */
1
14
  const meta = {
2
15
  type: 'suggestion',
3
16
  docs: {
@@ -10,45 +23,127 @@ const meta = {
10
23
  schema: [],
11
24
  };
12
25
 
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*function\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
+ */
13
90
  function create(context) {
91
+ const sourceCode = context.getSourceCode();
92
+
14
93
  return {
15
94
  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) {
17
104
  return;
18
105
  }
19
106
 
20
- const sourceCode = context.getSourceCode();
107
+ const isExport
108
+ = grandParent.type === 'ExportNamedDeclaration'
109
+ || grandParent.type === 'ExportDefaultDeclaration';
21
110
 
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
+ }
25
114
 
115
+ const functionName = node.id && node.id.name;
116
+ if (!functionName) {
117
+ return;
118
+ }
119
+
120
+ if (node.init.type === 'ArrowFunctionExpression') {
26
121
  context.report({
27
122
  node: node.init,
28
- message: 'Top-level functions must be named/regular functions.',
123
+ message: 'Top-level arrow functions must be named/regular functions.',
29
124
  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);
38
133
  },
39
134
  });
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') {
46
136
  context.report({
47
137
  node: node.init,
48
- message: 'Top-level functions must be named/regular functions.',
138
+ message: 'Top-level function expressions must be named/regular functions.',
49
139
  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);
52
147
  },
53
148
  });
54
149
  }
@@ -59,20 +154,29 @@ function create(context) {
59
154
  return;
60
155
  }
61
156
 
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;
71
158
 
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;
75
165
  }
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
+ });
76
180
  },
77
181
  };
78
182
  }