The doc-sniffing dog
at main 8.9 kB view raw
1/** 2 * 🐶 Doggo - JSDoc Coverage Analyzer for Deno 3 * 4 * A loyal companion for your documentation journey! Doggo sniffs out undocumented 5 * exports in your Deno packages and helps you achieve 100% JSDoc coverage. 6 * 7 * @module 8 * 9 * @example Install Doggo 10 * ```bash 11 * deno install --global --allow-read jsr:@knotbin/doggo --name doggo 12 * ``` 13 * 14 * @example Usage 15 * ```bash 16 * # Run Doggo in the current directory 17 * doggo 18 * # Run Doggo in a specific directory 19 * doggo /path/to/directory 20 * # Run Doggo for a specific file 21 * doggo /path/to/file.ts 22 * ``` 23 */ 24 25import { parseArgs } from "@std/cli"; 26import { join, resolve } from "@std/path"; 27import { bold, cyan, gray, green, red, yellow } from "@std/fmt/colors"; 28import { 29 analyzeDirectory, 30 type DocumentationStats, 31 type ExportedSymbol, 32} from "./core.ts"; 33 34/** 35 * Analyzer of JSDoc coverage in files and directories 36 */ 37class JSDocAnalyzer { 38 private rootPath: string; 39 40 constructor(rootPath: string) { 41 this.rootPath = resolve(rootPath); 42 } 43 44 async analyze(): Promise<void> { 45 console.log( 46 cyan(bold("\n🐶 Doggo is analyzing your documentation coverage!\n")), 47 ); 48 console.log(gray(`Path: ${this.rootPath}\n`)); 49 50 // Use core module for analysis 51 const result = await analyzeDirectory(this.rootPath); 52 53 if (result.hasDenoJson && result.hasExports) { 54 console.log(gray(`Found deno.json with exports: ${result.exportPath}\n`)); 55 console.log( 56 cyan("Sniffing out public API from export entry points...\n"), 57 ); 58 } 59 60 // Output results 61 this.outputResults(result.symbols, result.stats); 62 } 63 64 private outputResults( 65 symbols: ExportedSymbol[], 66 stats: DocumentationStats, 67 ): void { 68 if (symbols.length === 0) { 69 console.log(yellow("Woof! No exported symbols found. 🦴")); 70 return; 71 } 72 73 // Output undocumented symbols 74 const undocumented = symbols.filter((s) => !s.hasJSDoc); 75 76 if (undocumented.length > 0) { 77 console.log(red(bold("📝 Undocumented Exports (Doggo found these!):\n"))); 78 79 // Group by file 80 const byFile = new Map<string, ExportedSymbol[]>(); 81 for (const symbol of undocumented) { 82 if (!byFile.has(symbol.file)) { 83 byFile.set(symbol.file, []); 84 } 85 byFile.get(symbol.file)!.push(symbol); 86 } 87 88 // Output by file 89 for (const [file, symbols] of byFile.entries()) { 90 console.log(yellow(` ${file}:`)); 91 for (const symbol of symbols.sort((a, b) => a.line - b.line)) { 92 const typeLabel = gray(`[${symbol.type}]`); 93 const lineNum = gray(`:${symbol.line}`); 94 console.log(` ${red("✗")} ${symbol.name} ${typeLabel}${lineNum}`); 95 } 96 console.log(); 97 } 98 } else { 99 console.log( 100 green(bold("✨ Good boy! All exported symbols are documented!\n")), 101 ); 102 } 103 104 // Output summary 105 this.outputSummary(stats); 106 } 107 108 private outputSummary(stats: DocumentationStats): void { 109 console.log(bold(cyan("📊 Documentation Coverage Summary\n"))); 110 console.log(gray("─".repeat(50))); 111 112 // Overall stats 113 const percentageColor = stats.percentage >= 80 114 ? green 115 : stats.percentage >= 60 116 ? yellow 117 : red; 118 119 console.log(` Total Exports: ${bold(stats.total.toString())}`); 120 console.log(` Documented: ${green(stats.documented.toString())}`); 121 console.log(` Undocumented: ${red(stats.undocumented.toString())}`); 122 console.log( 123 ` Coverage: ${percentageColor(bold(`${stats.percentage}%`))}`, 124 ); 125 126 // Stats by type 127 if (Object.keys(stats.byType).length > 0) { 128 console.log(gray("─".repeat(50))); 129 console.log(bold("\n Coverage by Type:\n")); 130 131 for (const [type, typeStats] of Object.entries(stats.byType)) { 132 const percentage = typeStats.total > 0 133 ? Math.round((typeStats.documented / typeStats.total) * 100) 134 : 100; 135 const percentageColor = percentage >= 80 136 ? green 137 : percentage >= 60 138 ? yellow 139 : red; 140 141 const typeLabel = type.padEnd(12); 142 const statsStr = `${typeStats.documented}/${typeStats.total}`.padEnd(7); 143 console.log( 144 ` ${typeLabel} ${statsStr} ${percentageColor(`${percentage}%`)}`, 145 ); 146 } 147 } 148 149 console.log(gray("─".repeat(50))); 150 151 // Coverage indicator 152 const indicator = this.getCoverageIndicator(stats.percentage); 153 console.log(`\n ${indicator}`); 154 } 155 156 private getCoverageIndicator(percentage: number): string { 157 if (percentage === 100) { 158 return green("🏆 Perfect! Doggo is doing zoomies!"); 159 } else if (percentage >= 90) { 160 return green("✨ Excellent! Tail wagging intensifies!"); 161 } else if (percentage >= 80) { 162 return green("👍 Good boy! Happy tail wags"); 163 } else if (percentage >= 60) { 164 return yellow("📈 Doggo needs more treats (documentation)"); 165 } else if (percentage >= 40) { 166 return red("⚠️ Poor coverage (Sad puppy eyes)"); 167 } else { 168 return red("🚨 Critical! Doggo is hiding under the bed"); 169 } 170 } 171} 172 173// Main CLI 174async function main() { 175 const args = parseArgs(Deno.args, { 176 string: ["path"], 177 boolean: ["help", "version", "workspace"], 178 alias: { 179 h: "help", 180 v: "version", 181 p: "path", 182 w: "workspace", 183 }, 184 default: { 185 path: ".", 186 }, 187 }); 188 189 if (args.help) { 190 console.log(` 191 ${bold("🐶 Doggo - The Doc Sniffing Dog")} 192 193 A loyal companion for your documentation journey! Doggo sniffs out undocumented 194 exports in your Deno packages and helps you achieve 100% JSDoc coverage. 195 196 __ 197 (___()'${"`"}; 198 /,___ /${"`"} 199 ${"\\\\"} ${"\\\\"} Woof! Let's document that code! 200 201 ${bold("Usage:")} 202 doggo [options] [path] 203 204 ${bold("Options:")} 205 -h, --help Show help (Doggo does tricks!) 206 -v, --version Show version (Doggo's age in dog years) 207 -p, --path Path to analyze (where should Doggo sniff?) 208 -w, --workspace Force workspace mode (analyze the whole pack!) 209 210 ${bold("Examples:")} 211 doggo # Analyze current directory 212 doggo ./src # Analyze specific directory 213 doggo ./src/module.ts # Analyze single file 214 doggo --workspace # Analyze all workspace members 215 216 ${bold("Notes:")} 217 🦴 If a workspace configuration is found, analyzes all pack members 218 🎯 If a deno.json 'exports' field is found, analyzes only the public API 219 🔍 Otherwise, sniffs out all exported symbols in the codebase 220 221 ${bold("Remember:")} A well-documented codebase is like a well-trained dog - 222 everyone loves working with it! 223 `); 224 Deno.exit(0); 225 } 226 227 if (args.version) { 228 console.log("🐶 Doggo - The goodest documentation boy!"); 229 Deno.exit(0); 230 } 231 232 // Get path from positional argument or --path flag 233 const targetPath = args._[0]?.toString() || args.path; 234 235 try { 236 // Check if we should try workspace mode 237 const workspaceConfigPath = join(resolve(targetPath), "deno.json"); 238 const workspaceConfigPathJsonc = join(resolve(targetPath), "deno.jsonc"); 239 240 let hasWorkspace = false; 241 242 // Check for workspace configuration 243 try { 244 const content = await Deno.readTextFile(workspaceConfigPath); 245 const config = JSON.parse(content); 246 hasWorkspace = !!config.workspace; 247 } catch { 248 // Try deno.jsonc 249 try { 250 const content = await Deno.readTextFile(workspaceConfigPathJsonc); 251 // Simple JSONC parsing 252 const jsonContent = content 253 .split("\n") 254 .map((line) => { 255 const commentIndex = line.indexOf("//"); 256 return commentIndex > -1 ? line.slice(0, commentIndex) : line; 257 }) 258 .join("\n") 259 .replace(/\/\*[\s\S]*?\*\//g, ""); 260 const config = JSON.parse(jsonContent); 261 hasWorkspace = !!config.workspace; 262 } catch { 263 // No workspace config found 264 } 265 } 266 267 // Use workspace analyzer if workspace found or forced 268 if (hasWorkspace || args.workspace) { 269 const { WorkspaceAnalyzer } = await import("./workspace.ts"); 270 const workspaceAnalyzer = new WorkspaceAnalyzer(targetPath); 271 const report = await workspaceAnalyzer.analyze(); 272 273 if (!report && args.workspace) { 274 console.log(yellow("\n⚠️ Woof! No workspace configuration found.")); 275 console.log( 276 gray( 277 "Doggo was looking for 'workspace' field in deno.json or deno.jsonc\n", 278 ), 279 ); 280 Deno.exit(1); 281 } 282 } else { 283 // Regular single-package analysis 284 const analyzer = new JSDocAnalyzer(targetPath); 285 await analyzer.analyze(); 286 } 287 } catch (error) { 288 console.error(red(`Error: ${error}`)); 289 Deno.exit(1); 290 } 291} 292 293// Export for use in workspace.ts 294export { JSDocAnalyzer }; 295 296if (import.meta.main) { 297 await main(); 298}