The doc-sniffing dog
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}