Skip to content

Commit 2875c2d

Browse files
authored
fix(require-jsdoc): check interfaces within file for comment blocks; fixes #768 (#1410)
1 parent b0faae7 commit 2875c2d

File tree

3 files changed

+200
-0
lines changed

3 files changed

+200
-0
lines changed

docs/rules/require-jsdoc.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,19 @@ type Props = {
10281028
export type { Props as ComponentProps };
10291029
// "jsdoc/require-jsdoc": ["error"|"warn", {"contexts":["VariableDeclaration","TSTypeAliasDeclaration","TSPropertySignature","TSInterfaceDeclaration","TSMethodSignature","TSEnumDeclaration"],"enableFixer":true,"publicOnly":{"esm":true},"require":{"ArrowFunctionExpression":true,"ClassDeclaration":true,"ClassExpression":true,"FunctionDeclaration":true,"FunctionExpression":true,"MethodDefinition":true}}]
10301030
// Message: Missing JSDoc comment.
1031+
1032+
export interface A {
1033+
a: string;
1034+
}
1035+
1036+
export class B implements A, B {
1037+
a = 'abc';
1038+
public f(): void {
1039+
//
1040+
}
1041+
}
1042+
// "jsdoc/require-jsdoc": ["error"|"warn", {"contexts":["MethodDefinition"]}]
1043+
// Message: Missing JSDoc comment.
10311044
````
10321045

10331046

@@ -1931,5 +1944,21 @@ export function arrayMap<Target, Source extends Array<unknown>>(data: Source, ca
19311944
export function arrayMap<Target, Source extends AnyArrayType>(data: Source, callback: MapCallback<Target, Source>): AnyArrayType<Target> {
19321945
return data.map(callback);
19331946
}
1947+
1948+
export interface A {
1949+
a: string;
1950+
/**
1951+
* Documentation.
1952+
*/
1953+
f(): void;
1954+
}
1955+
1956+
export class B implements A {
1957+
a = 'abc';
1958+
public f(): void {
1959+
//
1960+
}
1961+
}
1962+
// "jsdoc/require-jsdoc": ["error"|"warn", {"contexts":["MethodDefinition"]}]
19341963
````
19351964

src/rules/requireJsdoc.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,100 @@ const OPTIONS_SCHEMA = {
173173
type: 'object',
174174
};
175175

176+
/**
177+
* @param {string} interfaceName
178+
* @param {string} methodName
179+
* @param {import("eslint").Scope.Scope | null} scope
180+
* @returns {import('@typescript-eslint/types').TSESTree.TSMethodSignature|null}
181+
*/
182+
const getMethodOnInterface = (interfaceName, methodName, scope) => {
183+
let scp = scope;
184+
while (scp) {
185+
for (const {
186+
identifiers,
187+
name,
188+
} of scp.variables) {
189+
if (interfaceName !== name) {
190+
continue;
191+
}
192+
193+
for (const identifier of identifiers) {
194+
const interfaceDeclaration = /** @type {import('@typescript-eslint/types').TSESTree.Identifier & {parent: import('@typescript-eslint/types').TSESTree.TSInterfaceDeclaration}} */ (
195+
identifier
196+
).parent;
197+
/* c8 ignore next 3 -- TS */
198+
if (interfaceDeclaration.type !== 'TSInterfaceDeclaration') {
199+
continue;
200+
}
201+
202+
for (const bodyItem of interfaceDeclaration.body.body) {
203+
const methodSig = /** @type {import('@typescript-eslint/types').TSESTree.TSMethodSignature} */ (
204+
bodyItem
205+
);
206+
if (methodName === /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
207+
methodSig.key
208+
).name) {
209+
return methodSig;
210+
}
211+
}
212+
}
213+
}
214+
215+
scp = scp.upper;
216+
}
217+
218+
return null;
219+
};
220+
221+
/**
222+
* @param {import('eslint').Rule.Node} node
223+
* @param {import('eslint').SourceCode} sourceCode
224+
* @param {import('eslint').Rule.RuleContext} context
225+
* @param {import('../iterateJsdoc.js').Settings} settings
226+
*/
227+
const isExemptedImplementer = (node, sourceCode, context, settings) => {
228+
if (node.type === 'FunctionExpression' &&
229+
node.parent.type === 'MethodDefinition' &&
230+
node.parent.parent.type === 'ClassBody' &&
231+
node.parent.parent.parent.type === 'ClassDeclaration' &&
232+
'implements' in node.parent.parent.parent
233+
) {
234+
const implments = /** @type {import('@typescript-eslint/types').TSESTree.TSClassImplements[]} */ (
235+
node.parent.parent.parent.implements
236+
);
237+
238+
const {
239+
name: methodName,
240+
} = /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
241+
node.parent.key
242+
);
243+
244+
for (const impl of implments) {
245+
const {
246+
name: interfaceName,
247+
} = /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
248+
impl.expression
249+
);
250+
251+
const interfaceMethodNode = getMethodOnInterface(interfaceName, methodName, node && (
252+
(sourceCode.getScope &&
253+
/* c8 ignore next 2 */
254+
sourceCode.getScope(node)) ||
255+
context.getScope()
256+
));
257+
if (interfaceMethodNode) {
258+
// @ts-expect-error Ok
259+
const comment = getJSDocComment(sourceCode, interfaceMethodNode, settings);
260+
if (comment) {
261+
return true;
262+
}
263+
}
264+
}
265+
}
266+
267+
return false;
268+
};
269+
176270
/**
177271
* @param {import('eslint').Rule.RuleContext} context
178272
* @param {import('json-schema').JSONSchema4Object} baseObject
@@ -421,6 +515,10 @@ export default {
421515
}
422516
}
423517

518+
if (isExemptedImplementer(node, sourceCode, context, settings)) {
519+
return;
520+
}
521+
424522
const fix = /** @type {import('eslint').Rule.ReportFixer} */ (fixer) => {
425523
// Default to one line break if the `minLines`/`maxLines` settings allow
426524
const lines = settings.minLines === 0 && settings.maxLines >= 1 ? 1 : settings.minLines;

test/rules/assertions/requireJsdoc.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4251,6 +4251,51 @@ function quux (foo) {
42514251
export type { Props as ComponentProps };
42524252
`,
42534253
},
4254+
{
4255+
code: `
4256+
export interface A {
4257+
a: string;
4258+
}
4259+
4260+
export class B implements A, B {
4261+
a = 'abc';
4262+
public f(): void {
4263+
//
4264+
}
4265+
}
4266+
`,
4267+
errors: [
4268+
{
4269+
line: 8,
4270+
message: 'Missing JSDoc comment.',
4271+
},
4272+
],
4273+
languageOptions: {
4274+
parser: typescriptEslintParser,
4275+
},
4276+
options: [
4277+
{
4278+
contexts: [
4279+
'MethodDefinition',
4280+
],
4281+
},
4282+
],
4283+
output: `
4284+
export interface A {
4285+
a: string;
4286+
}
4287+
4288+
export class B implements A, B {
4289+
a = 'abc';
4290+
/**
4291+
*
4292+
*/
4293+
public f(): void {
4294+
//
4295+
}
4296+
}
4297+
`,
4298+
},
42544299
],
42554300
valid: [
42564301
{
@@ -6398,5 +6443,33 @@ function quux (foo) {
63986443
parser: typescriptEslintParser,
63996444
},
64006445
},
6446+
{
6447+
code: `
6448+
export interface A {
6449+
a: string;
6450+
/**
6451+
* Documentation.
6452+
*/
6453+
f(): void;
6454+
}
6455+
6456+
export class B implements A {
6457+
a = 'abc';
6458+
public f(): void {
6459+
//
6460+
}
6461+
}
6462+
`,
6463+
languageOptions: {
6464+
parser: typescriptEslintParser,
6465+
},
6466+
options: [
6467+
{
6468+
contexts: [
6469+
'MethodDefinition',
6470+
],
6471+
},
6472+
],
6473+
},
64016474
],
64026475
});

0 commit comments

Comments
 (0)