Skip to content

Commit 09722e4

Browse files
committed
chore: add @function tags to all arrow functions
1 parent aa94953 commit 09722e4

12 files changed

Lines changed: 579 additions & 6 deletions
Lines changed: 389 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,389 @@
1+
/**
2+
* ESLint rule to require `@function` tag in JSDoc comments for arrow functions
3+
*
4+
* @packageDocumentation
5+
*/
6+
7+
import {
8+
AST_NODE_TYPES,
9+
AST_TOKEN_TYPES,
10+
ESLintUtils,
11+
} from '@typescript-eslint/utils';
12+
13+
/**
14+
* @import {TSESTree} from "@typescript-eslint/typescript-estree"
15+
*/
16+
/**
17+
* ESLint rule to require `@function` tag in JSDoc comments for arrow functions
18+
*
19+
* This rule enforces the presence of `@function` tags in JSDoc comments for
20+
* arrow functions to improve code documentation and maintain consistency with
21+
* the project's documentation standards.
22+
*
23+
* The rule supports:
24+
*
25+
* - Named arrow functions (`const foo = () => {}`)
26+
* - Arrow functions in object properties (`{ method: () => {} }`)
27+
* - Arrow functions in assignments (`obj.method = () => {}`)
28+
* - Export declarations (`export const foo = () => {}`)
29+
*
30+
* Configuration options:
31+
*
32+
* - `requireForNamed`: Whether to require `@function` tags for named arrow
33+
* functions (default: `true`)
34+
* - `requireForAnonymous`: Whether to require `@function` tags for anonymous
35+
* arrow functions (default: `false`)
36+
*/
37+
export default ESLintUtils.RuleCreator.withoutDocs({
38+
create(context, [options]) {
39+
const sourceCode = context.sourceCode;
40+
41+
/**
42+
* Determines if an arrow function should require a @function tag based on
43+
* its context.
44+
*
45+
* This function identifies "named" arrow functions, which are arrow
46+
* functions that are assigned to a variable, property, or exported. These
47+
* are distinguished from anonymous arrow functions used in callbacks or
48+
* inline expressions.
49+
*
50+
* Supported patterns:
51+
*
52+
* - Variable declarations: `const foo = () => {}`
53+
* - Object properties: `{ method: () => {} }`
54+
* - Property assignments: `obj.method = () => {}`
55+
* - Export declarations: `export const foo = () => {}`
56+
*
57+
* @example
58+
*
59+
* ```ts
60+
* // Returns true for these patterns:
61+
* const myFunc = () => {}; // Variable declaration
62+
* obj.method = () => {}; // Property assignment
63+
* const obj = { method: () => {} }; // Object property
64+
* export const func = () => {}; // Export declaration
65+
*
66+
* // Returns false for these patterns:
67+
* [1, 2, 3].map(() => {}); // Anonymous callback
68+
* setTimeout(() => {}, 100); // Anonymous callback
69+
* ```
70+
*
71+
* @function
72+
* @param {TSESTree.ArrowFunctionExpression} node - The arrow function AST
73+
* node to check
74+
* @returns {boolean} True if this arrow function should be considered
75+
* "named" and potentially require a @function tag based on its syntactic
76+
* context
77+
*/
78+
const shouldRequireTag =
79+
/**
80+
* @function
81+
*/
82+
(node) => {
83+
const parent = node.parent;
84+
85+
if (
86+
parent?.type === AST_NODE_TYPES.VariableDeclarator &&
87+
parent.id?.type === AST_NODE_TYPES.Identifier
88+
) {
89+
return true;
90+
}
91+
92+
if (
93+
parent?.type === AST_NODE_TYPES.AssignmentExpression &&
94+
parent.left?.type === AST_NODE_TYPES.MemberExpression
95+
) {
96+
return true;
97+
}
98+
99+
if (parent?.type === AST_NODE_TYPES.Property && parent.key) {
100+
return true;
101+
}
102+
103+
if (
104+
parent?.type === AST_NODE_TYPES.VariableDeclarator &&
105+
parent.parent?.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration
106+
) {
107+
return true;
108+
}
109+
110+
return false;
111+
};
112+
113+
/**
114+
* Determines whether a specific arrow function should be flagged by this
115+
* rule based on the configured options and the function's naming context.
116+
*
117+
* This function combines the result of `shouldRequireTag()` (which
118+
* determines if an arrow function is "named") with the user's configuration
119+
* options to decide if the rule should be applied to this specific arrow
120+
* function.
121+
*
122+
* @example
123+
*
124+
* ```ts
125+
* // With options: { requireForNamed: true, requireForAnonymous: false }
126+
*
127+
* const namedFunc = () => {}; // Returns true (named + requireForNamed)
128+
* [1, 2].map(() => {}); // Returns false (anonymous + !requireForAnonymous)
129+
*
130+
* // With options: { requireForNamed: false, requireForAnonymous: true }
131+
*
132+
* const namedFunc = () => {}; // Returns false (named + !requireForNamed)
133+
* [1, 2].map(() => {}); // Returns true (anonymous + requireForAnonymous)
134+
* ```
135+
*
136+
* @function
137+
* @param {TSESTree.ArrowFunctionExpression} node - The arrow function AST
138+
* node to evaluate
139+
* @returns {boolean} True if this arrow function should be flagged for
140+
* missing `@function` tag, false if it should be ignored by this rule
141+
*/
142+
const isNamedArrowFunction =
143+
/**
144+
* @function
145+
*/
146+
(node) => {
147+
const isNamed = shouldRequireTag(node);
148+
149+
if (isNamed && options.requireForNamed) {
150+
return true;
151+
}
152+
153+
if (!isNamed && options.requireForAnonymous) {
154+
return true;
155+
}
156+
157+
return false;
158+
};
159+
160+
/**
161+
* Finds the JSDoc comment associated with an arrow function, if any exists.
162+
*
163+
* This function is crucial for the rule's functionality because arrow
164+
* functions don't have JSDoc comments directly attached to them. Instead,
165+
* the JSDoc comment is typically attached to the parent node (variable
166+
* declaration, object property, etc.).
167+
*
168+
* The function looks for JSDoc comments in the appropriate location based
169+
* on the arrow function's syntactic context:
170+
*
171+
* - For variable declarations: Looks before the entire variable declaration
172+
* - For object properties: Looks before the property definition
173+
* - For assignments: Looks before the assignment expression
174+
* - For other cases: Falls back to looking before the arrow function itself
175+
*
176+
* @example
177+
*
178+
* ```ts
179+
* // These patterns will find the JSDoc comment:
180+
*
181+
* // Pattern 1: Variable declaration
182+
* // JSDoc here
183+
* // const func = () => {}; // Finds comment before variable declaration
184+
*
185+
* // Pattern 2: Object property
186+
* // const obj = {
187+
* // // JSDoc here
188+
* // method: () => {} // Finds comment before property
189+
* // };
190+
*
191+
* // Pattern 3: Assignment expression
192+
* // // JSDoc here
193+
* // obj.method = () => {}; // Finds comment before assignment
194+
* ```
195+
*
196+
* @function
197+
* @param {TSESTree.ArrowFunctionExpression} node - The arrow function AST
198+
* node
199+
* @returns {TSESTree.Comment | undefined} The JSDoc comment node if found,
200+
* undefined otherwise
201+
*/
202+
const getJSDocComment =
203+
/**
204+
* @function
205+
*/
206+
(node) => {
207+
// For named arrow functions (variable declarations), look for JSDoc on the variable declarator
208+
const parent = node.parent;
209+
/** @type {TSESTree.Node} */
210+
let targetNode = node;
211+
212+
switch (parent?.type) {
213+
case AST_NODE_TYPES.AssignmentExpression: {
214+
// For assignments like obj.foo = () => {}, look before the assignment
215+
targetNode = parent;
216+
break;
217+
}
218+
case AST_NODE_TYPES.Property: {
219+
// For object properties, look before the property
220+
targetNode = parent;
221+
break;
222+
}
223+
case AST_NODE_TYPES.VariableDeclarator: {
224+
// Look for comments before the variable declaration
225+
const variableDeclaration = parent.parent;
226+
if (
227+
variableDeclaration?.type === AST_NODE_TYPES.VariableDeclaration
228+
) {
229+
// Check if the variable declaration is exported
230+
if (
231+
variableDeclaration.parent?.type ===
232+
AST_NODE_TYPES.ExportNamedDeclaration
233+
) {
234+
targetNode = variableDeclaration.parent;
235+
} else {
236+
targetNode = variableDeclaration;
237+
}
238+
}
239+
break;
240+
}
241+
}
242+
243+
const comments = sourceCode.getCommentsBefore(targetNode);
244+
const comment = comments.findLast(
245+
(comment) =>
246+
comment.type === AST_TOKEN_TYPES.Block &&
247+
comment.value.startsWith('*'),
248+
);
249+
250+
// If we didn't find a comment and we're dealing with an exported variable,
251+
// also try looking before the variable declarator itself
252+
if (!comment && parent?.type === AST_NODE_TYPES.VariableDeclarator) {
253+
const declaratorComments = sourceCode.getCommentsBefore(parent);
254+
return declaratorComments.findLast(
255+
(comment) =>
256+
comment.type === AST_TOKEN_TYPES.Block &&
257+
comment.value.startsWith('*'),
258+
);
259+
}
260+
261+
return comment;
262+
};
263+
return {
264+
ArrowFunctionExpression(node) {
265+
if (!isNamedArrowFunction(node)) {
266+
return;
267+
}
268+
269+
const comment = getJSDocComment(node);
270+
271+
if (!comment) {
272+
context.report({
273+
fix(fixer) {
274+
// Insert JSDoc before the appropriate node (variable declaration, assignment, etc.)
275+
const parent = node.parent;
276+
/** @type {TSESTree.Node} */
277+
let insertTarget = node;
278+
279+
switch (parent?.type) {
280+
case AST_NODE_TYPES.AssignmentExpression: {
281+
insertTarget = parent;
282+
break;
283+
}
284+
case AST_NODE_TYPES.Property: {
285+
insertTarget = parent;
286+
break;
287+
}
288+
case AST_NODE_TYPES.VariableDeclarator: {
289+
const variableDeclaration = parent.parent;
290+
if (
291+
variableDeclaration?.type ===
292+
AST_NODE_TYPES.VariableDeclaration
293+
) {
294+
// Check if the variable declaration is exported
295+
if (
296+
variableDeclaration.parent?.type ===
297+
AST_NODE_TYPES.ExportNamedDeclaration
298+
) {
299+
insertTarget = variableDeclaration.parent;
300+
} else {
301+
insertTarget = variableDeclaration;
302+
}
303+
}
304+
break;
305+
}
306+
}
307+
308+
return fixer.insertTextBefore(
309+
insertTarget,
310+
'/**\n * @function\n */\n',
311+
);
312+
},
313+
messageId: 'missingJSDoc',
314+
node,
315+
});
316+
return;
317+
}
318+
319+
if (!/^\s*\*\s*@function\s*$/m.test(comment.value)) {
320+
context.report({
321+
fix(fixer) {
322+
// Add @function tag to existing JSDoc
323+
const value = comment.value;
324+
325+
// For JSDoc comments, insert @function before the last line
326+
// The comment.value doesn't include /* and */, so we need to add them back
327+
const lines = value.split('\n');
328+
329+
// Find where to insert @function (before the closing line)
330+
// Look for the last line that contains content (not just whitespace)
331+
let insertIndex = lines.length;
332+
for (let i = lines.length - 1; i >= 0; i--) {
333+
const line = lines[i]?.trim();
334+
if (line && !line.match(/^\s*$/)) {
335+
insertIndex = i + 1;
336+
break;
337+
}
338+
}
339+
340+
// Insert @function at the appropriate position
341+
const beforeLines = lines.slice(0, insertIndex);
342+
const afterLines = lines.slice(insertIndex);
343+
344+
const newLines = [...beforeLines, ' * @function', ...afterLines];
345+
346+
const updatedValue = newLines.join('\n');
347+
return fixer.replaceText(comment, `/*${updatedValue}*/`);
348+
},
349+
messageId: 'missingFunctionTag',
350+
node,
351+
});
352+
}
353+
},
354+
};
355+
},
356+
defaultOptions: [
357+
{
358+
requireForAnonymous: false,
359+
requireForNamed: true,
360+
},
361+
],
362+
meta: {
363+
docs: {
364+
description:
365+
'Require @function tag in JSDoc comments for arrow functions',
366+
},
367+
fixable: 'code',
368+
messages: {
369+
missingFunctionTag: 'JSDoc comment should include @function tag',
370+
missingJSDoc:
371+
'Arrow function should have JSDoc comment with @function tag',
372+
},
373+
schema: [
374+
{
375+
additionalProperties: false,
376+
properties: {
377+
requireForAnonymous: {
378+
type: 'boolean',
379+
},
380+
requireForNamed: {
381+
type: 'boolean',
382+
},
383+
},
384+
type: 'object',
385+
},
386+
],
387+
type: 'suggestion',
388+
},
389+
});

0 commit comments

Comments
 (0)