jsdoczoom 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/barrel.js +37 -36
- package/dist/cli.js +248 -163
- package/dist/drilldown.js +173 -163
- package/dist/errors.js +14 -14
- package/dist/eslint-engine.js +214 -96
- package/dist/eslint-plugin.js +316 -159
- package/dist/file-discovery.js +44 -42
- package/dist/index.js +1 -1
- package/dist/jsdoc-parser.js +157 -157
- package/dist/lint.js +44 -29
- package/dist/selector.js +24 -21
- package/dist/skill-text.js +488 -7
- package/dist/type-declarations.js +83 -69
- package/dist/types.js +6 -6
- package/dist/validate.js +84 -69
- package/package.json +7 -1
- package/types/barrel.d.ts +4 -1
- package/types/drilldown.d.ts +12 -2
- package/types/errors.d.ts +8 -8
- package/types/eslint-engine.d.ts +23 -11
- package/types/eslint-plugin.d.ts +6 -5
- package/types/file-discovery.d.ts +5 -1
- package/types/index.d.ts +18 -1
- package/types/lint.d.ts +11 -2
- package/types/skill-text.d.ts +4 -1
- package/types/types.d.ts +59 -43
- package/types/validate.d.ts +11 -2
package/dist/jsdoc-parser.js
CHANGED
|
@@ -17,65 +17,73 @@ import { JsdocError } from "./errors.js";
|
|
|
17
17
|
* or null if no file-level JSDoc is found.
|
|
18
18
|
*/
|
|
19
19
|
export function extractFileJsdoc(sourceText) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
20
|
+
const sourceFile = ts.createSourceFile(
|
|
21
|
+
"input.ts",
|
|
22
|
+
sourceText,
|
|
23
|
+
ts.ScriptTarget.Latest,
|
|
24
|
+
true,
|
|
25
|
+
);
|
|
26
|
+
// Check for syntax errors — if the file has diagnostics, throw PARSE_ERROR
|
|
27
|
+
const diagnostics = getDiagnostics(sourceFile);
|
|
28
|
+
if (diagnostics.length > 0) {
|
|
29
|
+
throw new JsdocError(
|
|
30
|
+
"PARSE_ERROR",
|
|
31
|
+
`TypeScript parse error: ${diagnostics[0]}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
// Find the first non-import top-level statement
|
|
35
|
+
let firstNonImportStatement;
|
|
36
|
+
for (const statement of sourceFile.statements) {
|
|
37
|
+
if (ts.isImportDeclaration(statement)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
firstNonImportStatement = statement;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
// If there are no non-import statements, check for comments at file start
|
|
44
|
+
if (firstNonImportStatement === undefined) {
|
|
45
|
+
return findFirstJsdocBlock(sourceText, 0, sourceText.length);
|
|
46
|
+
}
|
|
47
|
+
// The JSDoc block sits in the leading trivia of the first non-import statement.
|
|
48
|
+
const fullStart = firstNonImportStatement.getFullStart();
|
|
49
|
+
const nodeStart = firstNonImportStatement.getStart(sourceFile);
|
|
50
|
+
return findFirstJsdocBlock(sourceText, fullStart, nodeStart);
|
|
43
51
|
}
|
|
44
52
|
/**
|
|
45
53
|
* Find the first `/** ... */` block comment in the given text range.
|
|
46
54
|
*/
|
|
47
55
|
function findFirstJsdocBlock(sourceText, start, end) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
const commentRanges = getAllBlockComments(sourceText, start, end);
|
|
57
|
+
for (const range of commentRanges) {
|
|
58
|
+
const commentText = sourceText.slice(range.start, range.end);
|
|
59
|
+
if (commentText.startsWith("/**") && !commentText.startsWith("/***")) {
|
|
60
|
+
// Strip /** and */ delimiters
|
|
61
|
+
const inner = commentText.slice(3, -2);
|
|
62
|
+
return inner;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
58
66
|
}
|
|
59
67
|
/**
|
|
60
68
|
* Find the end position of a block comment starting at `pos` (after the `/*`).
|
|
61
69
|
*/
|
|
62
70
|
function findBlockCommentEnd(sourceText, pos) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
while (pos < sourceText.length) {
|
|
72
|
+
if (sourceText[pos] === "*" && sourceText[pos + 1] === "/") {
|
|
73
|
+
return pos + 2;
|
|
74
|
+
}
|
|
75
|
+
pos++;
|
|
76
|
+
}
|
|
77
|
+
return pos;
|
|
70
78
|
}
|
|
71
79
|
/**
|
|
72
80
|
* Skip past a line comment starting at `pos` (after the `//`).
|
|
73
81
|
*/
|
|
74
82
|
function findLineCommentEnd(sourceText, pos) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
while (pos < sourceText.length && sourceText[pos] !== "\n") {
|
|
84
|
+
pos++;
|
|
85
|
+
}
|
|
86
|
+
return pos;
|
|
79
87
|
}
|
|
80
88
|
const WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
|
|
81
89
|
/**
|
|
@@ -83,56 +91,52 @@ const WHITESPACE = new Set([" ", "\t", "\n", "\r"]);
|
|
|
83
91
|
* Stops at the first non-whitespace, non-comment character (code).
|
|
84
92
|
*/
|
|
85
93
|
function getAllBlockComments(sourceText, start, end) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return results;
|
|
94
|
+
const results = [];
|
|
95
|
+
let pos = start;
|
|
96
|
+
while (pos < end) {
|
|
97
|
+
if (WHITESPACE.has(sourceText[pos])) {
|
|
98
|
+
pos++;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const nextChar = sourceText[pos + 1];
|
|
102
|
+
if (sourceText[pos] !== "/" || pos + 1 >= end) break;
|
|
103
|
+
if (nextChar === "*") {
|
|
104
|
+
const commentStart = pos;
|
|
105
|
+
pos = findBlockCommentEnd(sourceText, pos + 2);
|
|
106
|
+
results.push({ start: commentStart, end: pos });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (nextChar === "/") {
|
|
110
|
+
pos = findLineCommentEnd(sourceText, pos + 2);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
return results;
|
|
109
116
|
}
|
|
110
117
|
/**
|
|
111
118
|
* Get syntax error diagnostics from a parsed source file.
|
|
112
119
|
*/
|
|
113
120
|
function getDiagnostics(sourceFile) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return diags.map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"));
|
|
121
|
+
// parseDiagnostics is available on the internal SourceFile
|
|
122
|
+
const diags = sourceFile.parseDiagnostics;
|
|
123
|
+
if (!diags || diags.length === 0) return [];
|
|
124
|
+
return diags.map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"));
|
|
119
125
|
}
|
|
120
126
|
/**
|
|
121
127
|
* Append non-empty text to an accumulator with space separation.
|
|
122
128
|
*/
|
|
123
129
|
function appendText(existing, addition) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return addition;
|
|
128
|
-
return `${existing} ${addition}`;
|
|
130
|
+
if (addition.length === 0) return existing;
|
|
131
|
+
if (existing.length === 0) return addition;
|
|
132
|
+
return `${existing} ${addition}`;
|
|
129
133
|
}
|
|
130
134
|
/** Tags whose content is treated as description (free-text). */
|
|
131
135
|
const DESCRIPTION_TAGS = new Set([
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
"desc",
|
|
137
|
+
"description",
|
|
138
|
+
"file",
|
|
139
|
+
"fileoverview",
|
|
136
140
|
]);
|
|
137
141
|
/**
|
|
138
142
|
* Parse @summary tag and free-text description from raw JSDoc inner text.
|
|
@@ -151,57 +155,54 @@ const DESCRIPTION_TAGS = new Set([
|
|
|
151
155
|
* Whitespace-only @summary tags are skipped.
|
|
152
156
|
*/
|
|
153
157
|
function parseJsdocContent(jsdocText) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
description: trimmedFreeText.length > 0 ? trimmedFreeText : null,
|
|
203
|
-
summaryCount,
|
|
204
|
-
};
|
|
158
|
+
const lines = jsdocText.split("\n");
|
|
159
|
+
let freeText = "";
|
|
160
|
+
let firstSummary = null;
|
|
161
|
+
let summaryCount = 0;
|
|
162
|
+
let currentTag = null;
|
|
163
|
+
let currentContent = "";
|
|
164
|
+
/** Flush a pending @summary tag, counting all non-empty occurrences. */
|
|
165
|
+
function flushSummary() {
|
|
166
|
+
if (currentTag !== "summary") return;
|
|
167
|
+
const trimmed = currentContent.trim();
|
|
168
|
+
if (trimmed.length > 0) {
|
|
169
|
+
summaryCount++;
|
|
170
|
+
if (firstSummary === null) {
|
|
171
|
+
firstSummary = trimmed;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const rawLine of lines) {
|
|
176
|
+
const stripped = rawLine.replace(/^\s*\*?\s?/, "");
|
|
177
|
+
const tagMatch = stripped.match(/^@([a-zA-Z]+)(?:\s|$)/);
|
|
178
|
+
if (tagMatch) {
|
|
179
|
+
flushSummary();
|
|
180
|
+
const rawTagName = tagMatch[1];
|
|
181
|
+
const tagContent = stripped.slice(tagMatch[0].length);
|
|
182
|
+
if (DESCRIPTION_TAGS.has(rawTagName.toLowerCase())) {
|
|
183
|
+
freeText = appendText(freeText, tagContent);
|
|
184
|
+
currentTag = null;
|
|
185
|
+
currentContent = "";
|
|
186
|
+
} else {
|
|
187
|
+
currentTag = rawTagName;
|
|
188
|
+
currentContent = tagContent;
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// Continuation line
|
|
193
|
+
if (currentTag === null) {
|
|
194
|
+
freeText = appendText(freeText, stripped);
|
|
195
|
+
} else if (stripped.length > 0) {
|
|
196
|
+
currentContent += ` ${stripped}`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
flushSummary();
|
|
200
|
+
const trimmedFreeText = freeText.trim();
|
|
201
|
+
return {
|
|
202
|
+
summary: firstSummary,
|
|
203
|
+
description: trimmedFreeText.length > 0 ? trimmedFreeText : null,
|
|
204
|
+
summaryCount,
|
|
205
|
+
};
|
|
205
206
|
}
|
|
206
207
|
/**
|
|
207
208
|
* Parse a TypeScript file and extract its summary and description from file-level JSDoc.
|
|
@@ -210,29 +211,28 @@ function parseJsdocContent(jsdocText) {
|
|
|
210
211
|
* @summary tag and free-text description.
|
|
211
212
|
*/
|
|
212
213
|
export function parseFileSummaries(filePath) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
};
|
|
214
|
+
let sourceText;
|
|
215
|
+
try {
|
|
216
|
+
sourceText = readFileSync(filePath, "utf-8");
|
|
217
|
+
} catch {
|
|
218
|
+
throw new JsdocError("FILE_NOT_FOUND", `File not found: ${filePath}`);
|
|
219
|
+
}
|
|
220
|
+
const jsdocText = extractFileJsdoc(sourceText);
|
|
221
|
+
if (jsdocText === null) {
|
|
222
|
+
return {
|
|
223
|
+
path: filePath,
|
|
224
|
+
summary: null,
|
|
225
|
+
description: null,
|
|
226
|
+
summaryCount: 0,
|
|
227
|
+
hasFileJsdoc: false,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const { summary, description, summaryCount } = parseJsdocContent(jsdocText);
|
|
231
|
+
return {
|
|
232
|
+
path: filePath,
|
|
233
|
+
summary,
|
|
234
|
+
description,
|
|
235
|
+
summaryCount,
|
|
236
|
+
hasFileJsdoc: true,
|
|
237
|
+
};
|
|
238
238
|
}
|
package/dist/lint.js
CHANGED
|
@@ -13,6 +13,7 @@ import { relative } from "node:path";
|
|
|
13
13
|
import { JsdocError } from "./errors.js";
|
|
14
14
|
import { createLintLinter, lintFileForLint } from "./eslint-engine.js";
|
|
15
15
|
import { discoverFiles } from "./file-discovery.js";
|
|
16
|
+
|
|
16
17
|
/**
|
|
17
18
|
* Lint a single file and return per-file diagnostics.
|
|
18
19
|
*
|
|
@@ -22,12 +23,12 @@ import { discoverFiles } from "./file-discovery.js";
|
|
|
22
23
|
* @returns File result with relative path and diagnostics array
|
|
23
24
|
*/
|
|
24
25
|
async function lintSingleFile(eslint, filePath, cwd) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
const sourceText = readFileSync(filePath, "utf-8");
|
|
27
|
+
const diagnostics = await lintFileForLint(eslint, sourceText, filePath);
|
|
28
|
+
return {
|
|
29
|
+
filePath: relative(cwd, filePath),
|
|
30
|
+
diagnostics,
|
|
31
|
+
};
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* Build a LintResult from per-file results, applying a limit to files with issues.
|
|
@@ -38,17 +39,22 @@ async function lintSingleFile(eslint, filePath, cwd) {
|
|
|
38
39
|
* @returns Aggregated lint result with summary statistics
|
|
39
40
|
*/
|
|
40
41
|
function buildLintResult(fileResults, totalFiles, limit) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
const filesWithIssues = fileResults.filter((f) => f.diagnostics.length > 0);
|
|
43
|
+
const totalDiagnostics = filesWithIssues.reduce(
|
|
44
|
+
(sum, f) => sum + f.diagnostics.length,
|
|
45
|
+
0,
|
|
46
|
+
);
|
|
47
|
+
const cappedFiles = filesWithIssues.slice(0, limit);
|
|
48
|
+
const truncated = filesWithIssues.length > limit;
|
|
49
|
+
return {
|
|
50
|
+
files: cappedFiles,
|
|
51
|
+
summary: {
|
|
52
|
+
totalFiles,
|
|
53
|
+
filesWithIssues: filesWithIssues.length,
|
|
54
|
+
totalDiagnostics,
|
|
55
|
+
...(truncated ? { truncated: true } : {}),
|
|
56
|
+
},
|
|
57
|
+
};
|
|
52
58
|
}
|
|
53
59
|
/**
|
|
54
60
|
* Lint files matching a selector pattern for comprehensive JSDoc quality.
|
|
@@ -65,14 +71,19 @@ function buildLintResult(fileResults, totalFiles, limit) {
|
|
|
65
71
|
* @throws {JsdocError} NO_FILES_MATCHED if glob selector matches no files
|
|
66
72
|
*/
|
|
67
73
|
export async function lint(selector, cwd, limit = 100, gitignore = true) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
const files = discoverFiles(selector.pattern, cwd, gitignore);
|
|
75
|
+
if (files.length === 0 && selector.type === "glob") {
|
|
76
|
+
throw new JsdocError(
|
|
77
|
+
"NO_FILES_MATCHED",
|
|
78
|
+
`No files matched: ${selector.pattern}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const tsFiles = files.filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
|
|
82
|
+
const eslint = createLintLinter(cwd);
|
|
83
|
+
const fileResults = await Promise.all(
|
|
84
|
+
tsFiles.map((f) => lintSingleFile(eslint, f, cwd)),
|
|
85
|
+
);
|
|
86
|
+
return buildLintResult(fileResults, tsFiles.length, limit);
|
|
76
87
|
}
|
|
77
88
|
/**
|
|
78
89
|
* Lint an explicit list of file paths for comprehensive JSDoc quality.
|
|
@@ -86,8 +97,12 @@ export async function lint(selector, cwd, limit = 100, gitignore = true) {
|
|
|
86
97
|
* @returns Lint result with per-file diagnostics and summary
|
|
87
98
|
*/
|
|
88
99
|
export async function lintFiles(filePaths, cwd, limit = 100) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
const tsFiles = filePaths.filter(
|
|
101
|
+
(f) => f.endsWith(".ts") || f.endsWith(".tsx"),
|
|
102
|
+
);
|
|
103
|
+
const eslint = createLintLinter(cwd);
|
|
104
|
+
const fileResults = await Promise.all(
|
|
105
|
+
tsFiles.map((f) => lintSingleFile(eslint, f, cwd)),
|
|
106
|
+
);
|
|
107
|
+
return buildLintResult(fileResults, tsFiles.length, limit);
|
|
93
108
|
}
|
package/dist/selector.js
CHANGED
|
@@ -16,25 +16,28 @@ import { JsdocError } from "./errors.js";
|
|
|
16
16
|
* @throws JsdocError INVALID_DEPTH if depth is negative, non-integer, or float
|
|
17
17
|
*/
|
|
18
18
|
export function parseSelector(input) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
19
|
+
if (!input || input.trim() === "") {
|
|
20
|
+
throw new JsdocError("INVALID_SELECTOR", "Selector cannot be empty");
|
|
21
|
+
}
|
|
22
|
+
// Reject float depths like @2.5 before extracting integer depth
|
|
23
|
+
if (/@\d+\.\d+$/.test(input)) {
|
|
24
|
+
throw new JsdocError(
|
|
25
|
+
"INVALID_DEPTH",
|
|
26
|
+
"Depth must be an integer, not a float",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
let pattern = input;
|
|
30
|
+
let depth;
|
|
31
|
+
// Match @<digits> at the end of the string
|
|
32
|
+
const depthMatch = input.match(/@(\d+)$/);
|
|
33
|
+
if (depthMatch) {
|
|
34
|
+
depth = parseInt(depthMatch[1], 10);
|
|
35
|
+
pattern = input.substring(0, depthMatch.index);
|
|
36
|
+
}
|
|
37
|
+
const hasGlobChars = /[*?[\]{]/.test(pattern);
|
|
38
|
+
return {
|
|
39
|
+
type: hasGlobChars ? "glob" : "path",
|
|
40
|
+
pattern,
|
|
41
|
+
depth,
|
|
42
|
+
};
|
|
40
43
|
}
|