@prowise/eslint-config-linting-rules 2.4.7 → 3.0.1

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.
@@ -0,0 +1,62 @@
1
+ import nanospinner from "nanospinner";
2
+ import path from "node:path";
3
+ import pc from "picocolors";
4
+
5
+ const spinner = nanospinner.createSpinner("", {
6
+ frames: ["|", "/", "-", "\\"],
7
+ color: "cyan",
8
+ });
9
+
10
+ let bindExit = false;
11
+ let initialReportDone = false;
12
+
13
+ const defaultSettings = {
14
+ hide: false,
15
+ hideFileName: false,
16
+ successMessage: "Lint done.",
17
+ };
18
+
19
+ const exitCallback = (exitCode, settings) => {
20
+ if (exitCode === 0 && settings.hide !== true) {
21
+ spinner.success({ text: settings.successMessage });
22
+ }
23
+ };
24
+
25
+ const create = (context) => {
26
+ const settings = { ...defaultSettings, ...context.settings.progress };
27
+
28
+ if (!bindExit) {
29
+ process.on("exit", (code) => {
30
+ exitCallback(code, settings);
31
+ });
32
+ bindExit = true;
33
+ }
34
+
35
+ if (settings.hide) {
36
+ return {};
37
+ }
38
+
39
+ if (!settings.hideFileName) {
40
+ const { filename } = context;
41
+ const relativeFilePath = path.relative(context.cwd, filename);
42
+ spinner.update({ text: `Processing: ${pc.green(relativeFilePath)} \n` });
43
+ } else if (!initialReportDone) {
44
+ spinner.update({ text: "Linting...\n" });
45
+ initialReportDone = true;
46
+ }
47
+
48
+ spinner.spin();
49
+ return {};
50
+ };
51
+
52
+ export const progress = {
53
+ name: import.meta.filename,
54
+ meta: {
55
+ type: "suggestion",
56
+ messages: [],
57
+ schema: [],
58
+ },
59
+ create,
60
+ };
61
+
62
+ export default { rules: { activate: progress } };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Enforce a blank JSDoc line immediately before the @returns tag.
3
+ */
4
+ export default {
5
+ meta: {
6
+ type: "layout",
7
+ docs: {
8
+ description: "require a blank line before @returns in JSDoc blocks",
9
+ },
10
+ fixable: "whitespace",
11
+ schema: [],
12
+ },
13
+ create: (context) => {
14
+ const comments = context.sourceCode.getAllComments();
15
+ return {
16
+ Program: () => {
17
+ for (const comment of comments) {
18
+ if (comment.type !== "Block" || !comment.value.startsWith("*")) continue;
19
+ const lines = comment.value.split("\n");
20
+ for (let i = 0; i < lines.length; i++) {
21
+ if (lines[i].trim().startsWith("* @returns")) {
22
+ if (i > 0 && lines[i - 1].trim() !== "*") {
23
+ context.report({
24
+ loc: {
25
+ start: {
26
+ line: comment.loc.start.line + i,
27
+ column: 0,
28
+ },
29
+ },
30
+ message: "JSDoc blocks must have a blank line before `@returns`.",
31
+ fix: (fixer) => {
32
+ const prevLineEnd = comment.range[0] +
33
+ 2 +
34
+ lines.slice(0, i).map((l) => l.length + 1).reduce((a, b) => a + b, 0);
35
+ return fixer.insertTextAfterRange(
36
+ [prevLineEnd - 1, prevLineEnd],
37
+ " *\n",
38
+ );
39
+ },
40
+ });
41
+ }
42
+ }
43
+ }
44
+ }
45
+ },
46
+ };
47
+ },
48
+ };
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Enforce brackets around optional parameter names in JSDoc @param tags.
3
+ * When a parameter has a `?` in its TypeScript signature or has a default value,
4
+ * the JSDoc @param tag should use brackets: `@param [paramName]` instead of `@param paramName`.
5
+ */
6
+ export default {
7
+ meta: {
8
+ type: "problem",
9
+ docs: {
10
+ description: "require brackets around optional parameter names in JSDoc @param tags",
11
+ },
12
+ fixable: "code",
13
+ schema: [],
14
+ messages: {
15
+ missingBrackets: "Optional parameter '{{paramName}}' should be wrapped in brackets: @param [{{paramName}}]",
16
+ },
17
+ },
18
+ create: (context) => {
19
+ /**
20
+ * Extracts parameter name from JSDoc @param line (without brackets).
21
+ *
22
+ * @param line - The JSDoc line.
23
+ *
24
+ * @returns - The parameter name or null.
25
+ */
26
+ function extractParamName(line) {
27
+ // Match: @param {Type} paramName or @param paramName (but not [paramName])
28
+ // Also matches nested properties like paramName.property
29
+ const match = line.match(/@param\s+(?:\{[^}]+}\s+)?(\w+(?:\.\w+)*)/);
30
+ return match ? match[1] : null;
31
+ }
32
+
33
+ /**
34
+ * Checks if a parameter name already has correct brackets.
35
+ *
36
+ * @param line - The JSDoc line.
37
+ * @param paramName - The parameter name to check (including nested properties).
38
+ *
39
+ * @returns - True if brackets are correctly placed.
40
+ */
41
+ function hasBrackets(line, paramName) {
42
+ // Check if the entire parameter path is wrapped in brackets
43
+ // e.g., [paramName] or [paramName.property]
44
+ const escapedParamName = paramName.replace(/\./g, "\\.");
45
+ // eslint-disable-next-line no-useless-escape
46
+ const correctBracketPattern = new RegExp(`@param\s+(?:\{[^}]+}\s+)?\[${escapedParamName}\]`);
47
+ return correctBracketPattern.test(line);
48
+ }
49
+
50
+ /**
51
+ * Checks if a function parameter is optional.
52
+ *
53
+ * @param param - The function parameter node.
54
+ *
55
+ * @returns - True if the parameter is optional.
56
+ */
57
+ function isOptionalParam(param) {
58
+ // Check if the parameter has a question mark (optional: true)
59
+ if (param.optional) {
60
+ return true;
61
+ }
62
+
63
+ // Check if the parameter has a default value
64
+ if (param.type === "AssignmentPattern") {
65
+ return true;
66
+ }
67
+
68
+ // For TypeScript, check TSParameterProperty
69
+ if (param.type === "TSParameterProperty") {
70
+ return param.parameter.optional || param.parameter.type === "AssignmentPattern";
71
+ }
72
+
73
+ return false;
74
+ }
75
+
76
+ /**
77
+ * Gets parameter name from a parameter node.
78
+ *
79
+ * @param param - The parameter node.
80
+ *
81
+ * @returns - The parameter name or null.
82
+ */
83
+ function getParamName(param) {
84
+ if (param.type === "Identifier") {
85
+ return param.name;
86
+ }
87
+ if (param.type === "ObjectPattern") {
88
+ return null;
89
+ }
90
+ if (param.type === "AssignmentPattern" && param.left.type === "Identifier") {
91
+ return param.left.name;
92
+ }
93
+ if (param.type === "TSParameterProperty") {
94
+ if (param.parameter.type === "Identifier") {
95
+ return param.parameter.name;
96
+ }
97
+ if (param.parameter.type === "AssignmentPattern" && param.parameter.left.type === "Identifier") {
98
+ return param.parameter.left.name;
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+
104
+ /**
105
+ * Collects optional JSDoc parameter paths from a destructured object parameter.
106
+ *
107
+ * @param pattern - The object pattern node.
108
+ * @param parentName - The top-level JSDoc parameter name.
109
+ * @param typeAnnotation - The TypeScript type annotation node.
110
+ * @param optionalParams - The set to mutate with optional param paths.
111
+ */
112
+ function collectOptionalObjectPatternParams(pattern, parentName, typeAnnotation, optionalParams) {
113
+ if (!pattern || pattern.type !== "ObjectPattern") {
114
+ return;
115
+ }
116
+
117
+ const typeNode = typeAnnotation?.typeAnnotation;
118
+ if (!typeNode || typeNode.type !== "TSTypeLiteral") {
119
+ return;
120
+ }
121
+
122
+ for (const property of pattern.properties) {
123
+ if (property.type !== "Property" || property.computed) {
124
+ continue;
125
+ }
126
+
127
+ const keyName = property.key.type === "Identifier" ? property.key.name : null;
128
+
129
+ if (!keyName) {
130
+ continue;
131
+ }
132
+
133
+ const matchingMember = typeNode.members.find((member) =>
134
+ member.type === "TSPropertySignature" &&
135
+ !member.computed &&
136
+ member.key.type === "Identifier" &&
137
+ member.key.name === keyName,
138
+ );
139
+
140
+ if (matchingMember?.optional) {
141
+ optionalParams.add(`${parentName}.${keyName}`);
142
+ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Gets a TypeScript type literal for props defined on a variable annotation like FC<{ ... }>.
148
+ *
149
+ * @param node - The function node.
150
+ *
151
+ * @returns - The type literal node or null.
152
+ */
153
+ function getVariablePropsTypeLiteral(node) {
154
+ if (!node.parent || node.parent.type !== "VariableDeclarator") {
155
+ return null;
156
+ }
157
+
158
+ const typeAnnotation = node.parent.id?.typeAnnotation?.typeAnnotation;
159
+
160
+ if (!typeAnnotation || typeAnnotation.type !== "TSTypeReference") {
161
+ return null;
162
+ }
163
+
164
+ const typeParams = typeAnnotation.typeArguments?.params;
165
+
166
+ if (!typeParams || typeParams.length === 0) {
167
+ return null;
168
+ }
169
+
170
+ const [firstTypeParam] = typeParams;
171
+
172
+ return firstTypeParam?.type === "TSTypeLiteral" ? firstTypeParam : null;
173
+ }
174
+
175
+ /**
176
+ * Gets the JSDoc comment associated with a node.
177
+ *
178
+ * @param node - The AST node.
179
+ *
180
+ * @returns - The JSDoc comment or null.
181
+ */
182
+ function getJSDocComment(node) {
183
+ // For arrow/function expressions in variable declarations, get the VariableDeclaration
184
+ let targetNode = node;
185
+ if (node.parent && node.parent.type === "VariableDeclarator") {
186
+ targetNode = node.parent.parent; // VariableDeclaration
187
+ }
188
+
189
+ // For function expressions inside class methods, get the MethodDefinition
190
+ if (node.parent && node.parent.type === "MethodDefinition") {
191
+ targetNode = node.parent;
192
+ }
193
+
194
+ // For exported functions, check the parent node
195
+ if (targetNode.parent && targetNode.parent.type === "ExportNamedDeclaration") {
196
+ targetNode = targetNode.parent;
197
+ }
198
+
199
+ const comments = context.sourceCode.getCommentsBefore(targetNode);
200
+ // Look for the last block comment that looks like JSDoc
201
+ for (let i = comments.length - 1; i >= 0; i--) {
202
+ const comment = comments[i];
203
+ if (comment.type === "Block" && comment.value.startsWith("*")) {
204
+ return comment;
205
+ }
206
+ }
207
+ return null;
208
+ }
209
+
210
+ /**
211
+ * Checks a function node for optional parameters.
212
+ *
213
+ * @param node - The function node.
214
+ */
215
+ function checkFunction(node) {
216
+ if (!node.params || node.params.length === 0) return;
217
+
218
+ const jsdocComment = getJSDocComment(node);
219
+ if (!jsdocComment) return;
220
+
221
+ // Build a set of optional parameter names
222
+ const optionalParams = new Set();
223
+ for (const param of node.params) {
224
+ if (isOptionalParam(param)) {
225
+ const paramName = getParamName(param);
226
+ if (paramName) {
227
+ optionalParams.add(paramName);
228
+ }
229
+ }
230
+
231
+ if (param.type === "ObjectPattern") {
232
+ collectOptionalObjectPatternParams(param, "props", param.typeAnnotation, optionalParams);
233
+
234
+ const variablePropsTypeLiteral = getVariablePropsTypeLiteral(node);
235
+ if (variablePropsTypeLiteral) {
236
+ collectOptionalObjectPatternParams(
237
+ param,
238
+ "props",
239
+ { typeAnnotation: variablePropsTypeLiteral },
240
+ optionalParams,
241
+ );
242
+ }
243
+ }
244
+
245
+ if (param.type === "AssignmentPattern" && param.left.type === "ObjectPattern") {
246
+ collectOptionalObjectPatternParams(param.left, "props", param.left.typeAnnotation, optionalParams);
247
+ }
248
+ }
249
+
250
+ if (optionalParams.size === 0) return;
251
+
252
+ // Check JSDoc @param tags
253
+ const lines = jsdocComment.value.split("\n");
254
+ for (let i = 0; i < lines.length; i++) {
255
+ const line = lines[i];
256
+ const trimmedLine = line.trim();
257
+
258
+ if (trimmedLine.startsWith("* @param")) {
259
+ const paramName = extractParamName(trimmedLine);
260
+
261
+ if (paramName && !hasBrackets(trimmedLine, paramName)) {
262
+ // Check if this is a top-level optional param or a nested property
263
+ const isTopLevelOptional = optionalParams.has(paramName);
264
+ const isNestedProperty = paramName.includes(".");
265
+ const parentParam = isNestedProperty ? paramName.split(".")[0] : null;
266
+ const isNestedOfOptional = isNestedProperty && parentParam && optionalParams.has(parentParam);
267
+
268
+ if (isTopLevelOptional || isNestedOfOptional) {
269
+ const lineNumber = jsdocComment.loc.start.line + i;
270
+
271
+ context.report({
272
+ loc: {
273
+ start: {
274
+ line: lineNumber,
275
+ column: 0,
276
+ },
277
+ end: {
278
+ line: lineNumber,
279
+ column: line.length,
280
+ },
281
+ },
282
+ messageId: "missingBrackets",
283
+ data: {
284
+ paramName,
285
+ },
286
+ fix: (fixer) => {
287
+ // Calculate the position in the comment
288
+ const lineStart = jsdocComment.range[0] + 2 + // "/*"
289
+ lines.slice(0, i).reduce((sum, l) => sum + l.length + 1, 0);
290
+
291
+ // Find the parameter name in the line and wrap it with brackets
292
+ // Escape dots in paramName for regex
293
+ const escapedParamName = paramName.replace(/\./g, "\\.");
294
+ // Match the pattern, accounting for the * prefix and whitespace
295
+ const paramMatch = line.match(new RegExp(`(\\*\\s*@param\\s+(?:\\{[^}]+\\}\\s+)?)(${escapedParamName})(?=\\s|$)`));
296
+ if (paramMatch) {
297
+ const paramStartInLine = paramMatch.index + paramMatch[1].length;
298
+ const paramStart = lineStart + paramStartInLine;
299
+ const paramEnd = paramStart + paramName.length;
300
+
301
+ return [
302
+ fixer.insertTextBeforeRange([paramStart, paramEnd], "["),
303
+ fixer.insertTextAfterRange([paramStart, paramEnd], "]"),
304
+ ];
305
+ }
306
+ return null;
307
+ },
308
+ });
309
+ }
310
+ }
311
+ }
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Checks a variable declarator with function initialization.
317
+ *
318
+ * @param node - The VariableDeclarator node.
319
+ */
320
+ function checkVariableDeclarator(node) {
321
+ // Check if the init is a function expression or arrow function
322
+ if (node.init &&
323
+ (node.init.type === "FunctionExpression" ||
324
+ node.init.type === "ArrowFunctionExpression")) {
325
+ checkFunction(node.init);
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Checks a function declaration.
331
+ *
332
+ * @param node - The function declaration node.
333
+ */
334
+ function checkFunctionDeclaration(node) {
335
+ // Only check top-level function declarations (not nested ones)
336
+ // For arrow/function expressions, they'll be checked via VariableDeclarator
337
+ if (node.parent.type === "ExportNamedDeclaration" ||
338
+ node.parent.type === "Program") {
339
+ checkFunction(node);
340
+ }
341
+ }
342
+
343
+ return {
344
+ FunctionDeclaration: checkFunctionDeclaration,
345
+ VariableDeclarator: checkVariableDeclarator,
346
+ MethodDefinition: (node) => {
347
+ if (node.value) checkFunction(node.value);
348
+ },
349
+ };
350
+ },
351
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Enforce single-line JSDoc comments to use inline format.
3
+ */
4
+ export default {
5
+ meta: {
6
+ type: "layout",
7
+ docs: {
8
+ description: "require single-line JSDoc comments to use /** ... */ format instead of multi-line format",
9
+ },
10
+ fixable: "whitespace",
11
+ schema: [],
12
+ },
13
+ create: (context) => {
14
+ const comments = context.sourceCode.getAllComments();
15
+ return {
16
+ Program: () => {
17
+ for (const comment of comments) {
18
+ if (comment.type !== "Block" || !comment.value.startsWith("*")) continue;
19
+
20
+ const lines = comment.value.split("\n");
21
+
22
+ // Skip if it's already a single-line comment (doesn't contain newlines in the value)
23
+ if (lines.length === 1) continue;
24
+
25
+ // Check if this is a multi-line block that's actually just one line of content
26
+ const contentLines = lines
27
+ .map((line) => line.trim())
28
+ .filter((line) => line !== "*" && line !== "");
29
+
30
+ // If there's only one line of actual content (not counting empty * lines)
31
+ if (contentLines.length === 1) {
32
+ const content = contentLines[0].replace(/^\*\s*/, "").replace(/\s*$/, "");
33
+
34
+ context.report({
35
+ loc: comment.loc,
36
+ message: "Single-line JSDoc comments should use inline format: /** ... */",
37
+ fix: (fixer) => fixer.replaceTextRange(comment.range, `/** ${content} */`),
38
+ });
39
+ }
40
+ }
41
+ },
42
+ };
43
+ },
44
+ };
@@ -31,7 +31,7 @@ export const jsxElementRule = {
31
31
  }
32
32
 
33
33
  function isPascalCase(name) {
34
- return /^[A-Z][a-zA-Z0-9]*$/.test(name) &&
34
+ return (/^[A-Z][a-zA-Z0-9]*$/).test(name) &&
35
35
  // allow exceptions for the following names
36
36
  !["GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH"].includes(name);
37
37
  }