Skip to content

Commit 00c1a4f

Browse files
committed
fix(componentUtil): fix ESLint 10 getJSDocComment fallback semantics
The previous fallback used getCommentsBefore(classNode) directly, which broke in two ways identified by reviewer haimuhaimu: 1. Node remapping: ESLint 9's SourceCode#getJSDocComment redirects exported class declarations to their export parent and class expressions to parent.parent. The fallback was calling getCommentsBefore on the class node itself, returning [] for those patterns. 2. Adjacency & intervening comments: getCommentsBefore does not enforce the 'at most one blank line' constraint. It also skips over line comments (// ...) to pick an earlier JSDoc, incorrectly returning a match where ESLint 9 would return null. Fix mirrors ESLint's own ast-utils.js logic: - isExportDeclaration(): detects export parent nodes for remapping - findJSDocComment(): uses getTokenBefore(node, { includeComments: true }) with the line-adjacency check (gap <= 1) - getFunctionExpressionJSDocTarget(): extracted helper for the ArrowFunctionExpression/FunctionExpression parent-walk (also reduces getJSDocComment cognitive complexity from 21 to ~9) ESLint 5-9 users are unaffected: the getJSDocComment native path is taken whenever typeof sourceCode.getJSDocComment === 'function'. Tests: 14 contract validation cases covering plain ClassDeclaration, exported declarations (Named + Default), ClassExpression via parent.parent remapping, blank-line separation, intervening line comments, and the ESLint 9 native path (available / null / throws).
1 parent 8f873f7 commit 00c1a4f

2 files changed

Lines changed: 447 additions & 59 deletions

File tree

‎lib/util/componentUtil.js‎

Lines changed: 102 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,80 @@ function isES5Component(node, context) {
5656
return false;
5757
}
5858

59+
/**
60+
* Checks whether a node represents an ES6 export declaration.
61+
* Mirrors ESLint's own `isExportDeclaration` in ast-utils.
62+
* @param {ASTNode} node
63+
* @returns {boolean}
64+
*/
65+
function isExportDeclaration(node) {
66+
return (
67+
node.type === 'ExportDefaultDeclaration'
68+
|| node.type === 'ExportNamedDeclaration'
69+
|| node.type === 'ExportAllDeclaration'
70+
);
71+
}
72+
73+
/**
74+
* Checks that `tokenBefore` is a JSDoc block comment immediately preceding
75+
* `node` (at most one blank line between them). Mirrors ESLint's own
76+
* `findJSDocComment` in ast-utils.
77+
* @param {ASTNode} node The target AST node
78+
* @param {any} sourceCode The ESLint source code object
79+
* @returns {ASTNode | null}
80+
*/
81+
function findJSDocComment(node, sourceCode) {
82+
// NOSONAR - Optional chaining requires Node 14+, but this plugin supports Node >=6. Remove when dropping Node < 14.
83+
const tokenBefore = sourceCode.getTokenBefore(node, { includeComments: true });
84+
if (
85+
tokenBefore // NOSONAR
86+
&& tokenBefore.type === 'Block'
87+
&& tokenBefore.value.charAt(0) === '*'
88+
&& node.loc.start.line - tokenBefore.loc.end.line <= 1
89+
) {
90+
return tokenBefore;
91+
}
92+
return null;
93+
}
94+
95+
/**
96+
* Walks up the parent chain from a function expression's immediate parent to
97+
* find the appropriate ancestor node for a JSDoc look-up. Mirrors ESLint 9's
98+
* internal walk in `SourceCode#getJSDocComment`.
99+
* Returns the ancestor to search, or `null` to fall back to the function node.
100+
* @param {ASTNode} parent The immediate parent of the function expression
101+
* @param {any} sourceCode
102+
* @returns {ASTNode | null}
103+
*/
104+
function getFunctionExpressionJSDocTarget(parent, sourceCode) {
105+
if (!parent || parent.type === 'CallExpression' || parent.type === 'NewExpression') {
106+
return null;
107+
}
108+
let target = parent;
109+
while (
110+
target
111+
&& !sourceCode.getCommentsBefore(target).length
112+
&& !/Function/u.test(target.type)
113+
&& target.type !== 'MethodDefinition'
114+
&& target.type !== 'Property'
115+
) {
116+
target = target.parent;
117+
}
118+
if (target && target.type !== 'FunctionDeclaration' && target.type !== 'Program') {
119+
return target;
120+
}
121+
return null;
122+
}
123+
59124
/**
60125
* Retrieves the JSDoc comment for a given node.
126+
*
127+
* When `sourceCode.getJSDocComment` is available (ESLint ≤ 9) it is used
128+
* directly. Otherwise (ESLint 10+, where the method was removed) this
129+
* fallback mirrors the exact node-remapping and adjacency logic from
130+
* ESLint's internal `getJSDocComment` / `findJSDocComment` in ast-utils.js
131+
* so that the semantics remain identical across ESLint versions.
132+
*
61133
* @param {any} sourceCode The ESLint source code object
62134
* @param {ASTNode} node The AST node
63135
* @returns {ASTNode | null} The JSDoc comment node or null
@@ -66,22 +138,44 @@ function getJSDocComment(sourceCode, node) {
66138
if (typeof sourceCode.getJSDocComment === 'function') {
67139
// Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes.
68140
// Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27
69-
// eslint-disable-next-line no-warning-comments
70-
// FIXME: Remove try/catch when dropping support for ESLint < 8. // NOSONAR
141+
// eslint-disable-next-line no-warning-comments -- ESLint <8 is still in peerDeps (^5||^6||^7); remove when those are dropped. // NOSONAR
142+
// FIXME: Remove try/catch when dropping support for ESLint < 8.
71143
try {
72144
return sourceCode.getJSDocComment(node);
73145
} catch (e) { // NOSONAR
74146
return null;
75147
}
76148
}
77-
const comments = sourceCode.getCommentsBefore(node);
78-
for (let i = comments.length - 1; i >= 0; i--) {
79-
const comment = comments[i];
80-
if (comment.type === 'Block' && comment.value.startsWith('*')) {
81-
return comment;
149+
150+
// ESLint 10+ fallback: replicate the node-remapping logic from ESLint 9's
151+
// SourceCode#getJSDocComment so that exported declarations and class
152+
// expressions are resolved to the correct parent node before the adjacency
153+
// check is performed.
154+
const parent = node.parent;
155+
switch (node.type) {
156+
case 'ClassDeclaration':
157+
case 'FunctionDeclaration':
158+
return findJSDocComment(
159+
parent && isExportDeclaration(parent) ? parent : node,
160+
sourceCode,
161+
);
162+
163+
case 'ClassExpression': {
164+
// class expression: `const Foo = class {}` → look at VariableDeclaration
165+
// NOSONAR - Optional chaining requires Node 14+, but this plugin supports Node >=6. Remove when dropping Node < 14.
166+
const classExprTarget = parent && parent.parent ? parent.parent : node; // NOSONAR
167+
return findJSDocComment(classExprTarget, sourceCode);
168+
}
169+
170+
case 'ArrowFunctionExpression':
171+
case 'FunctionExpression': {
172+
const fnTarget = getFunctionExpressionJSDocTarget(parent, sourceCode);
173+
return findJSDocComment(fnTarget || node, sourceCode);
82174
}
175+
176+
default:
177+
return null;
83178
}
84-
return null;
85179
}
86180

87181
/**

0 commit comments

Comments
 (0)