agentsqripts
Version:
Comprehensive static code analysis toolkit for identifying technical debt, security vulnerabilities, performance issues, and code quality problems
281 lines (244 loc) • 8.74 kB
JavaScript
/**
* @file Comment analysis and documentation quality assessment
* @description Single responsibility: Analyze code comments and documentation coverage
*
* This module provides comprehensive comment analysis including documentation quality assessment,
* comment coverage metrics, and actionable recommendations for improving code documentation.
* It analyzes both single files and entire projects to identify areas needing better documentation.
*/
const fs = require('fs').promises;
const path = require('path');
const { getAllFiles } = require('../utils/directoryScanner');
/**
* Analyze comments in a single file
* @param {string} filePath - Path to the file to analyze
* @returns {Promise<Object>} Comment analysis results
*/
async function analyzeFileComments(filePath) {
try {
const content = await fs.readFile(filePath, 'utf8');
const lines = content.split('\n');
const analysis = {
file: filePath,
totalLines: lines.length,
commentLines: 0,
docBlocks: 0,
inlineComments: 0,
todoComments: 0,
functions: 0,
documentedFunctions: 0,
classes: 0,
documentedClasses: 0,
coverage: 0,
score: 0,
issues: []
};
let inDocBlock = false;
let currentFunction = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Count comment types
if (line.startsWith('/**')) {
analysis.docBlocks++;
analysis.commentLines++;
inDocBlock = true;
} else if (line.startsWith('*/')) {
inDocBlock = false;
analysis.commentLines++;
} else if (inDocBlock) {
analysis.commentLines++;
} else if (line.startsWith('//')) {
analysis.commentLines++;
analysis.inlineComments++;
if (line.toLowerCase().includes('todo') || line.toLowerCase().includes('fixme')) {
analysis.todoComments++;
}
}
// Detect functions and classes
if (line.includes('function ') || line.includes(') => ') || line.includes('function(')) {
analysis.functions++;
// Check if previous lines contain documentation
const prevLines = lines.slice(Math.max(0, i - 5), i);
const hasDocumentation = prevLines.some(l => l.trim().startsWith('/**') || l.trim().startsWith('*'));
if (hasDocumentation) {
analysis.documentedFunctions++;
} else {
analysis.issues.push({
type: 'missing_function_doc',
line: i + 1,
message: 'Function lacks documentation'
});
}
}
if (line.includes('class ')) {
analysis.classes++;
// Check for class documentation
const prevLines = lines.slice(Math.max(0, i - 5), i);
const hasDocumentation = prevLines.some(l => l.trim().startsWith('/**'));
if (hasDocumentation) {
analysis.documentedClasses++;
} else {
analysis.issues.push({
type: 'missing_class_doc',
line: i + 1,
message: 'Class lacks documentation'
});
}
}
}
// Calculate coverage and score
analysis.coverage = analysis.totalLines > 0 ? (analysis.commentLines / analysis.totalLines) * 100 : 0;
const functionCoverage = analysis.functions > 0 ? (analysis.documentedFunctions / analysis.functions) * 100 : 100;
const classCoverage = analysis.classes > 0 ? (analysis.documentedClasses / analysis.classes) * 100 : 100;
analysis.score = Math.round((analysis.coverage * 0.4 + functionCoverage * 0.4 + classCoverage * 0.2));
return analysis;
} catch (error) {
return {
file: filePath,
error: error.message,
score: 0
};
}
}
/**
* Analyze comments across an entire project
* @param {string} projectPath - Path to the project directory
* @param {Object} options - Analysis options
* @returns {Promise<Object>} Project-wide comment analysis
*/
async function analyzeProjectComments(projectPath, options = {}) {
const extensions = options.extensions || ['.js', '.ts', '.jsx', '.tsx'];
try {
const files = await getAllFiles(projectPath, extensions);
const results = [];
const summary = {
totalFiles: files.length,
analyzedFiles: 0,
averageScore: 0,
totalLines: 0,
totalCommentLines: 0,
totalFunctions: 0,
documentedFunctions: 0,
totalClasses: 0,
documentedClasses: 0,
issues: []
};
// Analyze each file
for (const file of files) {
const analysis = await analyzeFileComments(file);
if (!analysis.error) {
results.push(analysis);
summary.analyzedFiles++;
summary.totalLines += analysis.totalLines;
summary.totalCommentLines += analysis.commentLines;
summary.totalFunctions += analysis.functions;
summary.documentedFunctions += analysis.documentedFunctions;
summary.totalClasses += analysis.classes;
summary.documentedClasses += analysis.documentedClasses;
summary.issues.push(...analysis.issues);
}
}
// Calculate project averages
if (summary.analyzedFiles > 0) {
summary.averageScore = Math.round(
results.reduce((sum, r) => sum + r.score, 0) / summary.analyzedFiles
);
}
summary.overallCoverage = summary.totalLines > 0 ?
(summary.totalCommentLines / summary.totalLines) * 100 : 0;
summary.functionCoverage = summary.totalFunctions > 0 ?
(summary.documentedFunctions / summary.totalFunctions) * 100 : 100;
summary.classCoverage = summary.totalClasses > 0 ?
(summary.documentedClasses / summary.totalClasses) * 100 : 100;
return {
summary,
files: results
};
} catch (error) {
throw new Error(`Project comment analysis failed: ${error.message}`);
}
}
/**
* Calculate documentation score for a file or project
* @param {Object} analysis - Analysis results
* @returns {number} Documentation score (0-100)
*/
function calculateDocumentationScore(analysis) {
if (analysis.summary) {
// Project-level score
return analysis.summary.averageScore;
} else {
// File-level score
return analysis.score || 0;
}
}
/**
* Generate recommendations for improving documentation
* @param {Object} analysis - Analysis results
* @returns {Array} Array of recommendations
*/
function generateCommentRecommendations(analysis) {
const recommendations = [];
if (analysis.summary) {
// Project-level recommendations
const summary = analysis.summary;
if (summary.averageScore < 60) {
recommendations.push({
priority: 'HIGH',
type: 'overall_documentation',
message: `Project documentation score is low (${summary.averageScore}/100). Consider establishing documentation standards.`
});
}
if (summary.functionCoverage < 70) {
recommendations.push({
priority: 'HIGH',
type: 'function_documentation',
message: `Only ${Math.round(summary.functionCoverage)}% of functions are documented. Add JSDoc comments to improve maintainability.`
});
}
if (summary.classCoverage < 80) {
recommendations.push({
priority: 'MEDIUM',
type: 'class_documentation',
message: `${Math.round(summary.classCoverage)}% of classes have documentation. Consider adding class-level comments.`
});
}
if (summary.overallCoverage < 15) {
recommendations.push({
priority: 'MEDIUM',
type: 'comment_coverage',
message: `Comment coverage is ${Math.round(summary.overallCoverage)}%. Consider adding more explanatory comments.`
});
}
} else {
// File-level recommendations
if (analysis.score < 60) {
recommendations.push({
priority: 'HIGH',
type: 'file_documentation',
message: 'File needs better documentation coverage'
});
}
if (analysis.functions > analysis.documentedFunctions) {
recommendations.push({
priority: 'HIGH',
type: 'function_docs',
message: `${analysis.functions - analysis.documentedFunctions} functions need documentation`
});
}
if (analysis.todoComments > 0) {
recommendations.push({
priority: 'LOW',
type: 'todo_cleanup',
message: `${analysis.todoComments} TODO/FIXME comments should be addressed`
});
}
}
return recommendations;
}
module.exports = {
analyzeFileComments,
analyzeProjectComments,
calculateDocumentationScore,
generateCommentRecommendations
};