All files analyzer.ts

80.76% Statements 42/52
43.75% Branches 14/32
100% Functions 2/2
82% Lines 41/50

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159                                    1x 1x 1x 1x   1x 1x 1x 1x   1x   1x 1x 2x 2x               2x 2x     2x 2x         2x   2x 2x       2x   2x 2x   2x 1x     1x 1x 1x     1x 1x     1x 1x     1x 2x 2x     1x 1x 1x           1x                                                                                 1x             1x                                  
import {
  scanFiles,
  calculateDocDrift,
  getFileCommitTimestamps,
  getLineRangeLastModifiedCached,
  Severity,
  IssueType,
  emitProgress,
  getParser,
  Language,
} from '@aiready/core';
import type { DocDriftOptions, DocDriftReport, DocDriftIssue } from './types';
import { readFileSync } from 'fs';
 
export async function analyzeDocDrift(
  options: DocDriftOptions
): Promise<DocDriftReport> {
  // Use core scanFiles which respects .gitignore recursively
  const files = await scanFiles(options);
  const issues: DocDriftIssue[] = [];
  const staleMonths = options.staleMonths ?? 6;
  const staleSeconds = staleMonths * 30 * 24 * 60 * 60;
 
  let uncommentedExports = 0;
  let totalExports = 0;
  let outdatedComments = 0;
  let undocumentedComplexity = 0;
 
  const now = Math.floor(Date.now() / 1000);
 
  let processed = 0;
  for (const file of files) {
    processed++;
    emitProgress(
      processed,
      files.length,
      'doc-drift',
      'analyzing files',
      options.onProgress
    );
 
    const parser = getParser(file);
    Iif (!parser) continue;
 
    let code: string;
    try {
      code = readFileSync(file, 'utf-8');
    } catch {
      continue;
    }
 
    try {
      // Initialize parser (it's a singleton in core, but ensures WASM is loaded)
      await parser.initialize();
      const parseResult = parser.parse(code, file);
 
      let fileLineStamps: Record<number, number> | undefined;
 
      for (const exp of parseResult.exports) {
        // Only analyze functions and classes for documentation drift
        Eif (exp.type === 'function' || exp.type === 'class') {
          totalExports++;
 
          if (!exp.documentation) {
            uncommentedExports++;
 
            // Complexity check (heuristic based on line count if range available)
            Eif (exp.loc) {
              const lines = exp.loc.end.line - exp.loc.start.line;
              Eif (lines > 20) undocumentedComplexity++;
            }
          } else {
            const doc = exp.documentation;
            const docContent = doc.content;
 
            // Signature mismatch detection (generalized heuristic)
            Eif (exp.type === 'function' && exp.parameters) {
              const params = exp.parameters;
              // Check if params mentioned in doc (standard @param or simple mention)
              // Use regex with word boundaries to avoid partial matches (e.g. 'b' in 'numbers')
              const missingParams = params.filter((p) => {
                const regex = new RegExp(`\\b${p}\\b`, 'i');
                return !regex.test(docContent);
              });
 
              Eif (missingParams.length > 0) {
                outdatedComments++;
                issues.push({
                  type: IssueType.DocDrift,
                  severity: Severity.Major,
                  message: `Documentation mismatch: function parameters (${missingParams.join(', ')}) are not mentioned in the docs.`,
                  location: { file, line: exp.loc?.start.line || 1 },
                });
                continue;
              }
            }
 
            // Timestamp comparison
            if (exp.loc) {
              if (!fileLineStamps) {
                fileLineStamps = getFileCommitTimestamps(file);
              }
 
              // We don't have exact lines for the doc node in ExportInfo yet,
              // but we know it precedes the export. Using export start as a proxy for drift check.
              const bodyModified = getLineRangeLastModifiedCached(
                fileLineStamps,
                exp.loc.start.line,
                exp.loc.end.line
              );
 
              if (bodyModified > 0) {
                // If body was modified much later than the "stale" threshold
                if (
                  now - bodyModified < staleSeconds / 4 &&
                  exp.documentation.isStale === true
                ) {
                  // This would require isStale to be set by the parser if it knew history
                  // For now, we compare body modification vs current time if docs look very old (heuristic)
                }
 
                // If the file itself is very old but has no issues, it's fine.
                // Doc-drift is really about implementation changing without doc updates.
              }
            }
          }
        }
      }
    } catch (error) {
      console.warn(`Doc-drift: Failed to parse ${file}: ${error}`);
      continue;
    }
  }
 
  const riskResult = calculateDocDrift({
    uncommentedExports,
    totalExports,
    outdatedComments,
    undocumentedComplexity,
  });
 
  return {
    summary: {
      filesAnalyzed: files.length,
      functionsAnalyzed: totalExports,
      score: riskResult.score,
      rating: riskResult.rating,
    },
    issues,
    rawData: {
      uncommentedExports,
      totalExports,
      outdatedComments,
      undocumentedComplexity,
    },
    recommendations: riskResult.recommendations,
  };
}