'use strict';
const { isJsdoc, parseTags } = require('./jsdoc-utils.js');
module.exports = {
meta: {
schema: [],
},
create: requireGraphqlPublicApiDocs,
};
function requireGraphqlPublicApiDocs(context) {
const sourceCode = context.getSourceCode();
function report(node, message) {
context.report({ node, message });
}
function commentFor(node) {
const target =
node.parent?.type === 'ExportNamedDeclaration' &&
node.parent.declaration === node
? node.parent
: node;
const comments = sourceCode.getCommentsBefore(target).filter(isJsdoc);
const comment = comments[comments.length - 1];
if (comment == null || hasCodeBetween(comment, target)) {
return null;
}
return comment;
}
function hasCodeBetween(comment, node) {
return sourceCode
.getTokensBetween(comment, node, { includeComments: true })
.some((token) => token.type !== 'Block' && token.type !== 'Line');
}
function parsedCommentFor(node) {
const comment =
node.type === 'Program' ? topComment(node) : commentFor(node);
return comment == null ? null : { tags: parseTags(comment) };
}
function topComment(program) {
const first = program.body[0];
const comments =
first == null
? sourceCode.getAllComments()
: sourceCode.getCommentsBefore(first);
return comments.find(isJsdoc) ?? null;
}
function checkPublicDoc(node, label, fileCategory, options = {}) {
const comment = parsedCommentFor(node);
if (comment == null || comment.tags.has('internal')) {
return;
}
if (
options.requireCategory !== false &&
!comment.tags.has('category') &&
fileCategory == null
) {
report(node, `${label} is missing @category.`);
}
requireNamedTags(
node,
comment,
'typeParam',
typeParameterNames(node),
label,
);
}
function checkPublicMemberDocs(declaration, ownerName) {
for (const member of publicMembers(declaration)) {
checkPublicDoc(member, memberLabel(ownerName, member), null, {
requireCategory: false,
});
}
}
function requireNamedTags(node, comment, tag, requiredNames, label) {
const documented = comment.tags.get(tag) ?? new Map();
for (const name of requiredNames) {
if (!documented.has(name) || documented.get(name) === '') {
report(node, `${label} is missing @${tag} ${name}.`);
}
}
}
return {
'Program:exit'(program) {
const moduleComment = parsedCommentFor(program);
const fileCategory =
moduleComment?.tags.get('category')?.get('*') ?? null;
for (const statement of program.body) {
const namespaceName = namespaceExportName(statement);
if (namespaceName != null) {
checkPublicDoc(statement, namespaceName, fileCategory);
}
const declaration = unwrapExportedDeclaration(statement);
if (!isDocumentableDeclaration(declaration)) {
continue;
}
for (const name of declarationNames(declaration)) {
checkPublicDoc(declaration, name, fileCategory);
checkPublicMemberDocs(declaration, name);
}
}
},
};
}
function unwrapExportedDeclaration(statement) {
return statement.type === 'ExportNamedDeclaration'
? statement.declaration
: statement;
}
function namespaceExportName(statement) {
return statement.type === 'ExportAllDeclaration'
? statement.exported?.name
: null;
}
function typeParameterNames(node) {
const typeParameters = node.value?.typeParameters ?? node.typeParameters;
return (typeParameters?.params ?? [])
.map((param) => param.name?.name)
.filter(Boolean);
}
function typeLiteralMembers(typeAnnotation) {
if (typeAnnotation == null) {
return [];
}
if (typeAnnotation.type === 'TSTypeLiteral') {
return typeAnnotation.members;
}
if (
typeAnnotation.type === 'TSIntersectionType' ||
typeAnnotation.type === 'TSUnionType'
) {
return typeAnnotation.types.flatMap(typeLiteralMembers);
}
return [];
}
function publicMembers(declaration) {
if (declaration.type === 'ClassDeclaration') {
return declaration.body.body.filter(
(member) => member.accessibility !== 'private',
);
}
if (declaration.type === 'TSInterfaceDeclaration') {
return declaration.body.body.filter(isDocumentableTypeMember);
}
if (declaration.type === 'TSTypeAliasDeclaration') {
return typeLiteralMembers(declaration.typeAnnotation).filter(
isDocumentableTypeMember,
);
}
return [];
}
function isDocumentableTypeMember(member) {
return member.type !== 'TSIndexSignature';
}
function memberLabel(ownerName, member) {
const key = member.key ?? member.id;
if (member.kind === 'constructor') {
return `${ownerName}.constructor`;
}
if (key?.type === 'Identifier') {
return `${ownerName}.${key.name}`;
}
if (key?.type === 'Literal') {
return `${ownerName}.${String(key.value)}`;
}
return `${ownerName}.<computed>`;
}
function isDocumentableDeclaration(node) {
return (
node?.type === 'ClassDeclaration' ||
node?.type === 'FunctionDeclaration' ||
node?.type === 'TSDeclareFunction' ||
node?.type === 'TSInterfaceDeclaration' ||
node?.type === 'TSTypeAliasDeclaration' ||
node?.type === 'TSEnumDeclaration' ||
node?.type === 'VariableDeclaration'
);
}
function declarationNames(node) {
if (node.type === 'VariableDeclaration') {
return node.declarations
.map((declaration) =>
declaration.id.type === 'Identifier' ? declaration.id.name : null,
)
.filter(Boolean);
}
return node.id?.name == null ? [] : [node.id.name];
}