Add documentation comments everywhere

This commit is contained in:
David Payr
2025-03-15 17:12:42 +01:00
parent 6eb5c02591
commit 3863ffe4d8
30 changed files with 1607 additions and 188 deletions
+206
View File
@@ -0,0 +1,206 @@
import ts from 'typescript';
import * as fs from 'node:fs';
function checkDocblocks(filePath: string): {
success: boolean;
missingDocblocks: { name: string; kind: string; line: number; reason: string }[];
} {
const program = ts.createProgram([filePath], {});
const sourceFile = program.getSourceFile(filePath);
const checker = program.getTypeChecker();
if (!sourceFile) {
throw new Error(`Could not find source file: ${filePath}`);
}
const missingDocblocks: { name: string; kind: string; line: number; reason: string }[] = [];
function checkNode(node: ts.Node) {
if (
ts.isInterfaceDeclaration(node)
|| ts.isClassDeclaration(node)
|| ts.isMethodDeclaration(node)
|| ts.isPropertyDeclaration(node)
|| ts.isFunctionDeclaration(node)
|| ts.isTypeAliasDeclaration(node)
|| ts.isEnumDeclaration(node)
|| ts.isPropertySignature(node)
|| ts.isMethodSignature(node)
|| ts.isVariableStatement(node)
|| ts.isVariableDeclaration(node)
|| (ts.isParameter(node) && ts.isPropertyDeclaration(node.parent))
) {
let symbol: ts.Symbol | undefined;
try {
if (ts.isVariableStatement(node)) {
node.declarationList.declarations.forEach((declaration) => {
const declSymbol = checker.getSymbolAtLocation(declaration.name);
if (declSymbol) {
const docStatus = checkDocumentationContent(declSymbol, declaration);
if (docStatus.hasProblem) {
const name = declaration.name.getText(sourceFile);
const line = sourceFile!.getLineAndCharacterOfPosition(declaration.getStart()).line + 1;
missingDocblocks.push({
name,
kind: 'variable',
line,
reason: docStatus.reason,
});
}
}
});
return;
} else if ('name' in node && node.name) {
symbol = checker.getSymbolAtLocation(node.name);
}
} catch {
symbol = undefined;
}
let name = 'anonymous';
const kind = ts.SyntaxKind[node.kind].replace(/Declaration|Statement/g, '').toLowerCase();
if ('name' in node && node.name) {
if (ts.isIdentifier(node.name)) {
name = node.name.text;
} else if ('getText' in node.name) {
name = node.name.getText(sourceFile);
}
}
const line = sourceFile!.getLineAndCharacterOfPosition(node.getStart()).line + 1;
if (!symbol) {
const jsDocNodes = ts.getJSDocCommentsAndTags(node);
if (jsDocNodes.length === 0) {
missingDocblocks.push({
name,
kind,
line,
reason: 'No docblock found',
});
} else {
const docStatus = checkJSDocContent(jsDocNodes);
if (docStatus.hasProblem) {
missingDocblocks.push({
name,
kind,
line,
reason: docStatus.reason,
});
}
}
} else {
const docStatus = checkDocumentationContent(symbol, node);
if (docStatus.hasProblem) {
missingDocblocks.push({
name,
kind,
line,
reason: docStatus.reason,
});
}
}
}
ts.forEachChild(node, checkNode);
}
function checkDocumentationContent(symbol: ts.Symbol, node: ts.Node): { hasProblem: boolean; reason: string } {
const docComments = symbol.getDocumentationComment(checker);
if (docComments.length === 0) {
const jsDocNodes = ts.getJSDocCommentsAndTags(node);
if (jsDocNodes.length === 0) {
return { hasProblem: true, reason: 'No docblock found' };
}
return checkJSDocContent(jsDocNodes);
}
// Get the raw text of the comment
const docText = docComments.map(comment => comment.text).join('').trim();
// Remove all @tags with regex
const cleanedText = docText.replace(/@\S+/g, '').trim();
// If nothing remains after removing tags, it's just modifiers
if (cleanedText.length === 0) {
return { hasProblem: true, reason: 'Docblock contains only modifiers' };
}
return { hasProblem: false, reason: '' };
}
function checkJSDocContent(jsDocNodes: readonly ts.Node[]): { hasProblem: boolean; reason: string } {
if (jsDocNodes.length === 0) {
return { hasProblem: true, reason: 'No docblock found' };
}
for (const node of jsDocNodes) {
if (ts.isJSDoc(node)) {
const commentText = node.comment ?? '';
if (typeof commentText !== 'string') {
throw new Error('Can\'t handle this yet!');
}
// Remove all @tags with regex
const cleanedText = commentText.replace(/@\S+/g, '').trim();
// If there's content after removing tags, it's a meaningful docblock
if (cleanedText.length > 0) {
return { hasProblem: false, reason: '' };
}
}
}
return { hasProblem: true, reason: 'Docblock contains only modifiers' };
}
if (sourceFile) {
checkNode(sourceFile);
}
return {
success: missingDocblocks.length === 0,
missingDocblocks,
};
}
function main() {
const args = process.argv.slice(2);
if (args.length !== 1) {
console.error('Missing file argument.');
process.exit(1);
}
const filePath = args[0]!;
if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`);
process.exit(1);
}
try {
const result = checkDocblocks(filePath);
if (result.success) {
console.log(`✅ All symbols in ${filePath} have meaningful docblocks.`);
} else {
console.log(
`❌ Found ${result.missingDocblocks.length} symbols with insufficient docblocks in ${filePath}:`,
);
result.missingDocblocks.forEach((item) => {
console.log(` - ${item.kind} '${item.name}' at line ${item.line}: ${item.reason}`);
});
process.exit(1);
}
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
}
main();