jsdoczoom 0.4.11 → 0.4.13
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/{src/drilldown.js → drilldown.js} +9 -6
- package/package.json +2 -2
- package/dist/test/barrel.test.js +0 -77
- package/dist/test/cache.test.js +0 -222
- package/dist/test/cli.test.js +0 -479
- package/dist/test/drilldown-barrel.test.js +0 -383
- package/dist/test/drilldown.test.js +0 -469
- package/dist/test/errors.test.js +0 -26
- package/dist/test/eslint-engine.test.js +0 -130
- package/dist/test/eslint-plugin.test.js +0 -291
- package/dist/test/file-discovery.test.js +0 -72
- package/dist/test/jsdoc-parser.test.js +0 -353
- package/dist/test/lint.test.js +0 -413
- package/dist/test/selector.test.js +0 -93
- package/dist/test/type-declarations.test.js +0 -321
- package/dist/test/validate.test.js +0 -361
- package/types/test/barrel.test.d.ts +0 -1
- package/types/test/cache.test.d.ts +0 -8
- package/types/test/cli.test.d.ts +0 -1
- package/types/test/drilldown-barrel.test.d.ts +0 -1
- package/types/test/drilldown.test.d.ts +0 -1
- package/types/test/errors.test.d.ts +0 -1
- package/types/test/eslint-engine.test.d.ts +0 -6
- package/types/test/eslint-plugin.test.d.ts +0 -1
- package/types/test/file-discovery.test.d.ts +0 -1
- package/types/test/jsdoc-parser.test.d.ts +0 -1
- package/types/test/lint.test.d.ts +0 -9
- package/types/test/selector.test.d.ts +0 -1
- package/types/test/type-declarations.test.d.ts +0 -1
- package/types/test/validate.test.d.ts +0 -1
- /package/dist/{src/barrel.js → barrel.js} +0 -0
- /package/dist/{src/cache.js → cache.js} +0 -0
- /package/dist/{src/cli.js → cli.js} +0 -0
- /package/dist/{src/errors.js → errors.js} +0 -0
- /package/dist/{src/eslint-engine.js → eslint-engine.js} +0 -0
- /package/dist/{src/eslint-plugin.js → eslint-plugin.js} +0 -0
- /package/dist/{src/file-discovery.js → file-discovery.js} +0 -0
- /package/dist/{src/index.js → index.js} +0 -0
- /package/dist/{src/jsdoc-parser.js → jsdoc-parser.js} +0 -0
- /package/dist/{src/lint.js → lint.js} +0 -0
- /package/dist/{src/selector.js → selector.js} +0 -0
- /package/dist/{src/skill-text.js → skill-text.js} +0 -0
- /package/dist/{src/type-declarations.js → type-declarations.js} +0 -0
- /package/dist/{src/types.js → types.js} +0 -0
- /package/dist/{src/validate.js → validate.js} +0 -0
- /package/types/{src/barrel.d.ts → barrel.d.ts} +0 -0
- /package/types/{src/cache.d.ts → cache.d.ts} +0 -0
- /package/types/{src/cli.d.ts → cli.d.ts} +0 -0
- /package/types/{src/drilldown.d.ts → drilldown.d.ts} +0 -0
- /package/types/{src/errors.d.ts → errors.d.ts} +0 -0
- /package/types/{src/eslint-engine.d.ts → eslint-engine.d.ts} +0 -0
- /package/types/{src/eslint-plugin.d.ts → eslint-plugin.d.ts} +0 -0
- /package/types/{src/file-discovery.d.ts → file-discovery.d.ts} +0 -0
- /package/types/{src/index.d.ts → index.d.ts} +0 -0
- /package/types/{src/jsdoc-parser.d.ts → jsdoc-parser.d.ts} +0 -0
- /package/types/{src/lint.d.ts → lint.d.ts} +0 -0
- /package/types/{src/selector.d.ts → selector.d.ts} +0 -0
- /package/types/{src/skill-text.d.ts → skill-text.d.ts} +0 -0
- /package/types/{src/type-declarations.d.ts → type-declarations.d.ts} +0 -0
- /package/types/{src/types.d.ts → types.d.ts} +0 -0
- /package/types/{src/validate.d.ts → validate.d.ts} +0 -0
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
import { dirname, join } from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import ts from "typescript";
|
|
4
|
-
import { beforeEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { JsdocError } from "../src/errors.js";
|
|
6
|
-
import {
|
|
7
|
-
generateTypeDeclarations,
|
|
8
|
-
getLanguageService,
|
|
9
|
-
resetCache,
|
|
10
|
-
resolveCompilerOptions,
|
|
11
|
-
} from "../src/type-declarations.js";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Verifies that generateTypeDeclarations produces .d.ts-like output
|
|
15
|
-
* including exported types, interfaces, function signatures, const
|
|
16
|
-
* declarations, and class signatures while excluding imports, non-exported
|
|
17
|
-
* internals, and implementation bodies. JSDoc comments are preserved.
|
|
18
|
-
*
|
|
19
|
-
* @summary Tests for TypeScript declaration output generation
|
|
20
|
-
*/
|
|
21
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
const fixturesDir = join(__dirname, "fixtures", "leaf-files");
|
|
23
|
-
describe("generateTypeDeclarations", () => {
|
|
24
|
-
const exportedTypesPath = join(fixturesDir, "exported-types.ts");
|
|
25
|
-
beforeEach(() => {
|
|
26
|
-
resetCache();
|
|
27
|
-
});
|
|
28
|
-
it("includes exported type aliases", () => {
|
|
29
|
-
const declarations = generateTypeDeclarations(exportedTypesPath);
|
|
30
|
-
expect(declarations).toContain("export type Name");
|
|
31
|
-
expect(declarations).toContain("string");
|
|
32
|
-
});
|
|
33
|
-
it("includes exported interfaces", () => {
|
|
34
|
-
const declarations = generateTypeDeclarations(exportedTypesPath);
|
|
35
|
-
expect(declarations).toContain("export interface User");
|
|
36
|
-
expect(declarations).toContain("id: number");
|
|
37
|
-
expect(declarations).toContain("name: string");
|
|
38
|
-
expect(declarations).toContain("email?: string");
|
|
39
|
-
});
|
|
40
|
-
it("includes exported function signatures (no bodies)", () => {
|
|
41
|
-
const declarations = generateTypeDeclarations(exportedTypesPath);
|
|
42
|
-
// Should include the function signature (TypeScript uses 'declare' keyword)
|
|
43
|
-
expect(declarations).toContain("export declare function getUser");
|
|
44
|
-
expect(declarations).toContain("id: number");
|
|
45
|
-
expect(declarations).toContain(": User");
|
|
46
|
-
// Should NOT include the implementation body
|
|
47
|
-
expect(declarations).not.toContain("return { id, name: 'test' }");
|
|
48
|
-
});
|
|
49
|
-
it("includes exported const signatures", () => {
|
|
50
|
-
const declarations = generateTypeDeclarations(exportedTypesPath);
|
|
51
|
-
expect(declarations).toContain("export declare const DEFAULT_TIMEOUT");
|
|
52
|
-
// TypeScript declaration emit includes the literal value for const declarations
|
|
53
|
-
expect(declarations).toContain("5000");
|
|
54
|
-
});
|
|
55
|
-
it("includes exported class declarations (signatures only, no method bodies)", () => {
|
|
56
|
-
const declarations = generateTypeDeclarations(exportedTypesPath);
|
|
57
|
-
// Should include the class declaration
|
|
58
|
-
expect(declarations).toContain("export declare class UserService");
|
|
59
|
-
// Should include method signatures
|
|
60
|
-
expect(declarations).toContain("add(user: User): void");
|
|
61
|
-
expect(declarations).toContain("getAll(): User[]");
|
|
62
|
-
// Should NOT include method bodies
|
|
63
|
-
expect(declarations).not.toContain("this.users.push(user)");
|
|
64
|
-
expect(declarations).not.toContain("return this.users");
|
|
65
|
-
// Should NOT include private field initializers
|
|
66
|
-
expect(declarations).not.toContain("private users: User[] = []");
|
|
67
|
-
// But should include the private field declaration
|
|
68
|
-
expect(declarations).toContain("private users");
|
|
69
|
-
});
|
|
70
|
-
it("preserves all JSDoc comments (file-level and symbol-level)", () => {
|
|
71
|
-
const declarations = generateTypeDeclarations(exportedTypesPath);
|
|
72
|
-
// File-level JSDoc
|
|
73
|
-
expect(declarations).toContain(
|
|
74
|
-
"Module for testing type declarations generation",
|
|
75
|
-
);
|
|
76
|
-
expect(declarations).toContain("Type declarations test module");
|
|
77
|
-
// Symbol-level JSDoc
|
|
78
|
-
expect(declarations).toContain("A string type alias");
|
|
79
|
-
expect(declarations).toContain("A user interface");
|
|
80
|
-
expect(declarations).toContain("Gets a user by ID");
|
|
81
|
-
expect(declarations).toContain("@param id - The user ID");
|
|
82
|
-
expect(declarations).toContain("@returns The user object");
|
|
83
|
-
expect(declarations).toContain("The default timeout value");
|
|
84
|
-
expect(declarations).toContain("A simple utility class");
|
|
85
|
-
expect(declarations).toContain("Add a user");
|
|
86
|
-
expect(declarations).toContain("Get all users");
|
|
87
|
-
});
|
|
88
|
-
it("excludes import statements", () => {
|
|
89
|
-
const declarations = generateTypeDeclarations(exportedTypesPath);
|
|
90
|
-
// The fixture doesn't have imports, but we can verify no import statements appear
|
|
91
|
-
expect(declarations).not.toContain("import ");
|
|
92
|
-
expect(declarations).not.toContain("from ");
|
|
93
|
-
});
|
|
94
|
-
it("excludes non-exported internals", () => {
|
|
95
|
-
const declarations = generateTypeDeclarations(exportedTypesPath);
|
|
96
|
-
// Should NOT include internal helper function
|
|
97
|
-
expect(declarations).not.toContain("internalHelper");
|
|
98
|
-
// Should NOT include private constant
|
|
99
|
-
expect(declarations).not.toContain("privateConst");
|
|
100
|
-
});
|
|
101
|
-
it("maintains source order of declarations", () => {
|
|
102
|
-
const declarations = generateTypeDeclarations(exportedTypesPath);
|
|
103
|
-
// Get positions of each export in the declarations
|
|
104
|
-
const namePos = declarations.indexOf("export type Name");
|
|
105
|
-
const userPos = declarations.indexOf("export interface User");
|
|
106
|
-
const getUserPos = declarations.indexOf("export declare function getUser");
|
|
107
|
-
const timeoutPos = declarations.indexOf(
|
|
108
|
-
"export declare const DEFAULT_TIMEOUT",
|
|
109
|
-
);
|
|
110
|
-
const classPos = declarations.indexOf("export declare class UserService");
|
|
111
|
-
// Verify they appear in the same order as the source
|
|
112
|
-
expect(namePos).toBeGreaterThan(-1);
|
|
113
|
-
expect(userPos).toBeGreaterThan(namePos);
|
|
114
|
-
expect(getUserPos).toBeGreaterThan(userPos);
|
|
115
|
-
expect(timeoutPos).toBeGreaterThan(getUserPos);
|
|
116
|
-
expect(classPos).toBeGreaterThan(timeoutPos);
|
|
117
|
-
});
|
|
118
|
-
it("reuses language service across calls for files sharing the same tsconfig", () => {
|
|
119
|
-
// Get the resolved compiler options and language service for the first call
|
|
120
|
-
const { tsconfigPath, options } = resolveCompilerOptions(exportedTypesPath);
|
|
121
|
-
const firstService = getLanguageService(tsconfigPath, options);
|
|
122
|
-
// First call
|
|
123
|
-
generateTypeDeclarations(exportedTypesPath);
|
|
124
|
-
// Second call should return the same service instance
|
|
125
|
-
const secondService = getLanguageService(tsconfigPath, options);
|
|
126
|
-
expect(secondService).toBe(firstService);
|
|
127
|
-
expect(secondService.service).toBe(firstService.service);
|
|
128
|
-
// Second call to generateTypeDeclarations should not create a new service
|
|
129
|
-
generateTypeDeclarations(exportedTypesPath);
|
|
130
|
-
const thirdService = getLanguageService(tsconfigPath, options);
|
|
131
|
-
expect(thirdService).toBe(firstService);
|
|
132
|
-
});
|
|
133
|
-
it("returns empty string for files with no exports", () => {
|
|
134
|
-
const noExportsPath = join(fixturesDir, "no-exports.ts");
|
|
135
|
-
const result = generateTypeDeclarations(noExportsPath);
|
|
136
|
-
expect(result).toBe("");
|
|
137
|
-
});
|
|
138
|
-
it("throws PARSE_ERROR for files with syntax errors", () => {
|
|
139
|
-
const syntaxErrorPath = join(fixturesDir, "syntax-error.ts");
|
|
140
|
-
expect(() => generateTypeDeclarations(syntaxErrorPath)).toThrow(JsdocError);
|
|
141
|
-
try {
|
|
142
|
-
generateTypeDeclarations(syntaxErrorPath);
|
|
143
|
-
expect.fail("Should have thrown JsdocError");
|
|
144
|
-
} catch (e) {
|
|
145
|
-
expect(e).toBeInstanceOf(JsdocError);
|
|
146
|
-
expect(e.code).toBe("PARSE_ERROR");
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
describe("resolveCompilerOptions", () => {
|
|
151
|
-
const tsconfigFixturesDir = join(__dirname, "fixtures");
|
|
152
|
-
const tsconfigProjectDir = join(tsconfigFixturesDir, "tsconfig-project");
|
|
153
|
-
const sampleTsPath = join(tsconfigProjectDir, "sample.ts");
|
|
154
|
-
const malformedTsconfigPath = join(
|
|
155
|
-
tsconfigFixturesDir,
|
|
156
|
-
"tsconfig-malformed",
|
|
157
|
-
"sample.ts",
|
|
158
|
-
);
|
|
159
|
-
beforeEach(() => {
|
|
160
|
-
resetCache();
|
|
161
|
-
});
|
|
162
|
-
it("returns options with declaration: true and removeComments: false regardless of tsconfig", () => {
|
|
163
|
-
const result = resolveCompilerOptions(sampleTsPath);
|
|
164
|
-
expect(result.options.declaration).toBe(true);
|
|
165
|
-
expect(result.options.emitDeclarationOnly).toBe(true);
|
|
166
|
-
expect(result.options.removeComments).toBe(false);
|
|
167
|
-
expect(result.options.skipLibCheck).toBe(true);
|
|
168
|
-
});
|
|
169
|
-
it("uses fallback defaults when no tsconfig.json is found", () => {
|
|
170
|
-
// Use a path that won't find any tsconfig.json (root directory)
|
|
171
|
-
// This assumes there's no tsconfig.json in the root filesystem
|
|
172
|
-
const noTsconfigPath = "/file.ts";
|
|
173
|
-
const result = resolveCompilerOptions(noTsconfigPath);
|
|
174
|
-
expect(result.tsconfigPath).toBe(null);
|
|
175
|
-
expect(result.options).toEqual({
|
|
176
|
-
declaration: true,
|
|
177
|
-
emitDeclarationOnly: true,
|
|
178
|
-
target: ts.ScriptTarget.Latest,
|
|
179
|
-
module: ts.ModuleKind.NodeNext,
|
|
180
|
-
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
181
|
-
skipLibCheck: true,
|
|
182
|
-
removeComments: false,
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
it("merges tsconfig settings with required overrides", () => {
|
|
186
|
-
const result = resolveCompilerOptions(sampleTsPath);
|
|
187
|
-
// Should include settings from the tsconfig
|
|
188
|
-
expect(result.options.strict).toBe(true);
|
|
189
|
-
expect(result.options.target).toBe(ts.ScriptTarget.ES2020);
|
|
190
|
-
// Should also include required overrides
|
|
191
|
-
expect(result.options.declaration).toBe(true);
|
|
192
|
-
expect(result.options.emitDeclarationOnly).toBe(true);
|
|
193
|
-
expect(result.options.removeComments).toBe(false);
|
|
194
|
-
expect(result.options.skipLibCheck).toBe(true);
|
|
195
|
-
// Should have found the tsconfig
|
|
196
|
-
expect(result.tsconfigPath).toContain("tsconfig-project");
|
|
197
|
-
});
|
|
198
|
-
it("returns the same result object for the same tsconfig path (caching)", () => {
|
|
199
|
-
const firstResult = resolveCompilerOptions(sampleTsPath);
|
|
200
|
-
const secondResult = resolveCompilerOptions(sampleTsPath);
|
|
201
|
-
// Should return the exact same object reference (not just equal values)
|
|
202
|
-
expect(firstResult).toBe(secondResult);
|
|
203
|
-
expect(firstResult.options).toBe(secondResult.options);
|
|
204
|
-
});
|
|
205
|
-
it("handles malformed tsconfig.json gracefully (falls back to defaults)", () => {
|
|
206
|
-
const result = resolveCompilerOptions(malformedTsconfigPath);
|
|
207
|
-
// Should fall back to defaults when tsconfig is malformed
|
|
208
|
-
expect(result.tsconfigPath).toBe(null);
|
|
209
|
-
expect(result.options).toEqual({
|
|
210
|
-
declaration: true,
|
|
211
|
-
emitDeclarationOnly: true,
|
|
212
|
-
target: ts.ScriptTarget.Latest,
|
|
213
|
-
module: ts.ModuleKind.NodeNext,
|
|
214
|
-
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
215
|
-
skipLibCheck: true,
|
|
216
|
-
removeComments: false,
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
describe("getLanguageService", () => {
|
|
221
|
-
const tsconfigFixturesDir = join(__dirname, "fixtures");
|
|
222
|
-
const tsconfigProjectDir = join(tsconfigFixturesDir, "tsconfig-project");
|
|
223
|
-
const sampleTsPath = join(tsconfigProjectDir, "sample.ts");
|
|
224
|
-
beforeEach(() => {
|
|
225
|
-
resetCache();
|
|
226
|
-
});
|
|
227
|
-
it("returns a language service that can emit declarations for a registered file", () => {
|
|
228
|
-
const { tsconfigPath, options } = resolveCompilerOptions(sampleTsPath);
|
|
229
|
-
const { service, files } = getLanguageService(tsconfigPath, options);
|
|
230
|
-
// Add the file to the service's file set
|
|
231
|
-
files.add(sampleTsPath);
|
|
232
|
-
// Get emit output for the file
|
|
233
|
-
const emitOutput = service.getEmitOutput(sampleTsPath, true); // true = emitOnlyDtsFiles
|
|
234
|
-
// Should have declaration output
|
|
235
|
-
expect(emitOutput.outputFiles).toBeDefined();
|
|
236
|
-
expect(emitOutput.outputFiles.length).toBeGreaterThan(0);
|
|
237
|
-
// Find the .d.ts output
|
|
238
|
-
const dtsFile = emitOutput.outputFiles.find((file) =>
|
|
239
|
-
file.name.endsWith(".d.ts"),
|
|
240
|
-
);
|
|
241
|
-
expect(dtsFile).toBeDefined();
|
|
242
|
-
expect(dtsFile?.text).toContain("export");
|
|
243
|
-
});
|
|
244
|
-
it("reuses the same language service instance for files sharing a tsconfig", () => {
|
|
245
|
-
const { tsconfigPath, options } = resolveCompilerOptions(sampleTsPath);
|
|
246
|
-
// Get the service twice
|
|
247
|
-
const firstCall = getLanguageService(tsconfigPath, options);
|
|
248
|
-
const secondCall = getLanguageService(tsconfigPath, options);
|
|
249
|
-
// Should return the exact same object references
|
|
250
|
-
expect(firstCall).toBe(secondCall);
|
|
251
|
-
expect(firstCall.service).toBe(secondCall.service);
|
|
252
|
-
expect(firstCall.files).toBe(secondCall.files);
|
|
253
|
-
});
|
|
254
|
-
it("creates separate service instances for different tsconfigs", () => {
|
|
255
|
-
// Get service for tsconfig-project
|
|
256
|
-
const { tsconfigPath: tsconfigPath1, options: options1 } =
|
|
257
|
-
resolveCompilerOptions(sampleTsPath);
|
|
258
|
-
const service1 = getLanguageService(tsconfigPath1, options1);
|
|
259
|
-
// Get service for a file with no tsconfig (uses fallback)
|
|
260
|
-
const noTsconfigPath = "/file.ts";
|
|
261
|
-
const { tsconfigPath: tsconfigPath2, options: options2 } =
|
|
262
|
-
resolveCompilerOptions(noTsconfigPath);
|
|
263
|
-
const service2 = getLanguageService(tsconfigPath2, options2);
|
|
264
|
-
// Should be different service instances
|
|
265
|
-
expect(service1).not.toBe(service2);
|
|
266
|
-
expect(service1.service).not.toBe(service2.service);
|
|
267
|
-
expect(service1.files).not.toBe(service2.files);
|
|
268
|
-
});
|
|
269
|
-
it("adding a new file to the files Set makes it visible to the existing service's getScriptFileNames()", () => {
|
|
270
|
-
const { tsconfigPath, options } = resolveCompilerOptions(sampleTsPath);
|
|
271
|
-
const { service, files } = getLanguageService(tsconfigPath, options);
|
|
272
|
-
// Initially no files
|
|
273
|
-
expect(files.size).toBe(0);
|
|
274
|
-
// Add a file
|
|
275
|
-
files.add(sampleTsPath);
|
|
276
|
-
// Service should now see the file via getProgram().getSourceFiles()
|
|
277
|
-
const program = service.getProgram();
|
|
278
|
-
expect(program).toBeDefined();
|
|
279
|
-
const sourceFiles = program?.getSourceFiles() ?? [];
|
|
280
|
-
const hasOurFile = sourceFiles.some((sf) => sf.fileName === sampleTsPath);
|
|
281
|
-
expect(hasOurFile).toBe(true);
|
|
282
|
-
// Add another file
|
|
283
|
-
const anotherFilePath = join(tsconfigProjectDir, "another-file.ts");
|
|
284
|
-
files.add(anotherFilePath);
|
|
285
|
-
// Should now have 2 files in the set
|
|
286
|
-
expect(files.size).toBe(2);
|
|
287
|
-
expect(files.has(sampleTsPath)).toBe(true);
|
|
288
|
-
expect(files.has(anotherFilePath)).toBe(true);
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
describe("resetCache", () => {
|
|
292
|
-
const tsconfigFixturesDir = join(__dirname, "fixtures");
|
|
293
|
-
const tsconfigProjectDir = join(tsconfigFixturesDir, "tsconfig-project");
|
|
294
|
-
const sampleTsPath = join(tsconfigProjectDir, "sample.ts");
|
|
295
|
-
it("clears the compiler options cache", () => {
|
|
296
|
-
// Populate the cache
|
|
297
|
-
const firstResult = resolveCompilerOptions(sampleTsPath);
|
|
298
|
-
// Reset
|
|
299
|
-
resetCache();
|
|
300
|
-
// Get options again
|
|
301
|
-
const secondResult = resolveCompilerOptions(sampleTsPath);
|
|
302
|
-
// Should be different object instances (cache was cleared)
|
|
303
|
-
expect(firstResult).not.toBe(secondResult);
|
|
304
|
-
// But should have the same values
|
|
305
|
-
expect(firstResult.tsconfigPath).toBe(secondResult.tsconfigPath);
|
|
306
|
-
});
|
|
307
|
-
it("clears the language service cache", () => {
|
|
308
|
-
// Populate the cache
|
|
309
|
-
const { tsconfigPath, options } = resolveCompilerOptions(sampleTsPath);
|
|
310
|
-
const firstService = getLanguageService(tsconfigPath, options);
|
|
311
|
-
// Reset
|
|
312
|
-
resetCache();
|
|
313
|
-
// Get service again (need to resolve options again too since that cache was also cleared)
|
|
314
|
-
const { tsconfigPath: newTsconfigPath, options: newOptions } =
|
|
315
|
-
resolveCompilerOptions(sampleTsPath);
|
|
316
|
-
const secondService = getLanguageService(newTsconfigPath, newOptions);
|
|
317
|
-
// Should be different service instances (cache was cleared)
|
|
318
|
-
expect(firstService).not.toBe(secondService);
|
|
319
|
-
expect(firstService.service).not.toBe(secondService.service);
|
|
320
|
-
});
|
|
321
|
-
});
|
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
import { JsdocError } from "../src/errors.js";
|
|
5
|
-
import { validate, validateFiles } from "../src/validate.js";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Verifies status classification (valid, syntax_error, missing_jsdoc,
|
|
9
|
-
* missing_summary, missing_description), priority-order group filling
|
|
10
|
-
* under limits, and error handling for missing files and empty globs.
|
|
11
|
-
*
|
|
12
|
-
* @summary Tests for file-level JSDoc validation and grouped result output
|
|
13
|
-
*/
|
|
14
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
-
const fixturesDir = path.resolve(__dirname, "fixtures", "leaf-files");
|
|
16
|
-
function fixture(name) {
|
|
17
|
-
return path.join(fixturesDir, name);
|
|
18
|
-
}
|
|
19
|
-
describe("validate", () => {
|
|
20
|
-
it("valid file produces empty result", async () => {
|
|
21
|
-
const selector = {
|
|
22
|
-
type: "path",
|
|
23
|
-
pattern: fixture("two-summaries.ts"),
|
|
24
|
-
depth: undefined,
|
|
25
|
-
};
|
|
26
|
-
const result = await validate(selector, fixturesDir);
|
|
27
|
-
expect(result.syntax_error).toBeUndefined();
|
|
28
|
-
expect(result.missing_jsdoc).toBeUndefined();
|
|
29
|
-
expect(result.missing_summary).toBeUndefined();
|
|
30
|
-
expect(result.missing_description).toBeUndefined();
|
|
31
|
-
});
|
|
32
|
-
it("file with @summary but no description is missing_description", async () => {
|
|
33
|
-
const selector = {
|
|
34
|
-
type: "path",
|
|
35
|
-
pattern: fixture("summary-only.ts"),
|
|
36
|
-
depth: undefined,
|
|
37
|
-
};
|
|
38
|
-
const result = await validate(selector, fixturesDir);
|
|
39
|
-
expect(result.missing_description?.files).toEqual(["summary-only.ts"]);
|
|
40
|
-
expect(result.missing_jsdoc).toBeUndefined();
|
|
41
|
-
expect(result.missing_summary).toBeUndefined();
|
|
42
|
-
});
|
|
43
|
-
it("file with no @summary is missing_summary", async () => {
|
|
44
|
-
const selector = {
|
|
45
|
-
type: "path",
|
|
46
|
-
pattern: fixture("description-only.ts"),
|
|
47
|
-
depth: undefined,
|
|
48
|
-
};
|
|
49
|
-
const result = await validate(selector, fixturesDir);
|
|
50
|
-
expect(result.missing_summary?.files).toEqual(["description-only.ts"]);
|
|
51
|
-
});
|
|
52
|
-
it("file with no file-level JSDoc is missing_jsdoc", async () => {
|
|
53
|
-
const selector = {
|
|
54
|
-
type: "path",
|
|
55
|
-
pattern: fixture("no-jsdoc.ts"),
|
|
56
|
-
depth: undefined,
|
|
57
|
-
};
|
|
58
|
-
const result = await validate(selector, fixturesDir);
|
|
59
|
-
expect(result.missing_jsdoc?.files).toEqual(["no-jsdoc.ts"]);
|
|
60
|
-
});
|
|
61
|
-
it("file with syntax error is syntax_error", async () => {
|
|
62
|
-
const selector = {
|
|
63
|
-
type: "path",
|
|
64
|
-
pattern: fixture("syntax-error.ts"),
|
|
65
|
-
depth: undefined,
|
|
66
|
-
};
|
|
67
|
-
const result = await validate(selector, fixturesDir);
|
|
68
|
-
expect(result.syntax_error?.files).toEqual(["syntax-error.ts"]);
|
|
69
|
-
});
|
|
70
|
-
it("whitespace-only @summary is skipped, uses next non-empty one", async () => {
|
|
71
|
-
const selector = {
|
|
72
|
-
type: "path",
|
|
73
|
-
pattern: fixture("whitespace-summary.ts"),
|
|
74
|
-
depth: undefined,
|
|
75
|
-
};
|
|
76
|
-
const result = await validate(selector, fixturesDir);
|
|
77
|
-
expect(result.missing_summary).toBeUndefined();
|
|
78
|
-
});
|
|
79
|
-
it("flags files with multiple @summary tags", async () => {
|
|
80
|
-
const selector = {
|
|
81
|
-
type: "path",
|
|
82
|
-
pattern: "multiple-summaries.ts",
|
|
83
|
-
depth: undefined,
|
|
84
|
-
};
|
|
85
|
-
const result = await validate(selector, fixturesDir);
|
|
86
|
-
expect(result.multiple_summary).toBeDefined();
|
|
87
|
-
expect(result.multiple_summary?.files).toEqual(["multiple-summaries.ts"]);
|
|
88
|
-
});
|
|
89
|
-
it("silently strips @depth suffix", async () => {
|
|
90
|
-
const selector = {
|
|
91
|
-
type: "glob",
|
|
92
|
-
pattern: "**/*.ts",
|
|
93
|
-
depth: 1,
|
|
94
|
-
};
|
|
95
|
-
const result = await validate(selector, fixturesDir);
|
|
96
|
-
// Should succeed without throwing — depth is ignored
|
|
97
|
-
expect(typeof result).toBe("object");
|
|
98
|
-
});
|
|
99
|
-
it("output groups only contain invalid files, empty groups omitted", async () => {
|
|
100
|
-
const selector = {
|
|
101
|
-
type: "glob",
|
|
102
|
-
pattern: "*.ts",
|
|
103
|
-
depth: undefined,
|
|
104
|
-
};
|
|
105
|
-
const result = await validate(selector, fixturesDir);
|
|
106
|
-
// Every group key that exists should have { guidance, files } shape
|
|
107
|
-
for (const key of [
|
|
108
|
-
"syntax_error",
|
|
109
|
-
"missing_jsdoc",
|
|
110
|
-
"missing_summary",
|
|
111
|
-
"multiple_summary",
|
|
112
|
-
"missing_description",
|
|
113
|
-
"missing_barrel",
|
|
114
|
-
]) {
|
|
115
|
-
const group = result[key];
|
|
116
|
-
if (group !== undefined) {
|
|
117
|
-
expect(typeof group.guidance).toBe("string");
|
|
118
|
-
expect(group.guidance.length).toBeGreaterThan(0);
|
|
119
|
-
expect(Array.isArray(group.files)).toBe(true);
|
|
120
|
-
expect(group.files.length).toBeGreaterThan(0);
|
|
121
|
-
for (const p of group.files) {
|
|
122
|
-
expect(typeof p).toBe("string");
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
it("each group includes guidance text", async () => {
|
|
128
|
-
const selector = {
|
|
129
|
-
type: "glob",
|
|
130
|
-
pattern: "*.ts",
|
|
131
|
-
depth: undefined,
|
|
132
|
-
};
|
|
133
|
-
const result = await validate(selector, fixturesDir);
|
|
134
|
-
if (result.syntax_error) {
|
|
135
|
-
expect(result.syntax_error.guidance).toContain("### Syntax error");
|
|
136
|
-
}
|
|
137
|
-
if (result.missing_jsdoc) {
|
|
138
|
-
expect(result.missing_jsdoc.guidance).toContain(
|
|
139
|
-
"### Missing file-level JSDoc",
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
if (result.missing_summary) {
|
|
143
|
-
expect(result.missing_summary.guidance).toContain("### The @summary tag");
|
|
144
|
-
}
|
|
145
|
-
if (result.missing_description) {
|
|
146
|
-
expect(result.missing_description.guidance).toContain(
|
|
147
|
-
"### The description paragraph",
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
it("works with glob selectors", async () => {
|
|
152
|
-
const selector = {
|
|
153
|
-
type: "glob",
|
|
154
|
-
pattern: "two-*.ts",
|
|
155
|
-
depth: undefined,
|
|
156
|
-
};
|
|
157
|
-
const result = await validate(selector, fixturesDir);
|
|
158
|
-
// two-summaries.ts is valid, so no groups should appear
|
|
159
|
-
expect(result.syntax_error).toBeUndefined();
|
|
160
|
-
expect(result.missing_jsdoc).toBeUndefined();
|
|
161
|
-
expect(result.missing_summary).toBeUndefined();
|
|
162
|
-
expect(result.missing_description).toBeUndefined();
|
|
163
|
-
});
|
|
164
|
-
it("works with path selectors", async () => {
|
|
165
|
-
const selector = {
|
|
166
|
-
type: "path",
|
|
167
|
-
pattern: fixture("two-summaries.ts"),
|
|
168
|
-
depth: undefined,
|
|
169
|
-
};
|
|
170
|
-
const result = await validate(selector, fixturesDir);
|
|
171
|
-
expect(result.syntax_error).toBeUndefined();
|
|
172
|
-
expect(result.missing_jsdoc).toBeUndefined();
|
|
173
|
-
expect(result.missing_summary).toBeUndefined();
|
|
174
|
-
expect(result.missing_description).toBeUndefined();
|
|
175
|
-
});
|
|
176
|
-
it("path targeting nonexistent file throws FILE_NOT_FOUND", async () => {
|
|
177
|
-
const selector = {
|
|
178
|
-
type: "path",
|
|
179
|
-
pattern: fixture("nonexistent.ts"),
|
|
180
|
-
depth: undefined,
|
|
181
|
-
};
|
|
182
|
-
await expect(validate(selector, fixturesDir)).rejects.toThrow(JsdocError);
|
|
183
|
-
try {
|
|
184
|
-
await validate(selector, fixturesDir);
|
|
185
|
-
} catch (e) {
|
|
186
|
-
expect(e).toBeInstanceOf(JsdocError);
|
|
187
|
-
expect(e.code).toBe("FILE_NOT_FOUND");
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
it("glob with no matches throws NO_FILES_MATCHED", async () => {
|
|
191
|
-
const selector = {
|
|
192
|
-
type: "glob",
|
|
193
|
-
pattern: "nonexistent-*.ts",
|
|
194
|
-
depth: undefined,
|
|
195
|
-
};
|
|
196
|
-
await expect(validate(selector, fixturesDir)).rejects.toThrow(JsdocError);
|
|
197
|
-
try {
|
|
198
|
-
await validate(selector, fixturesDir);
|
|
199
|
-
} catch (e) {
|
|
200
|
-
expect(e).toBeInstanceOf(JsdocError);
|
|
201
|
-
expect(e.code).toBe("NO_FILES_MATCHED");
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
it("flags directories with >3 files and no barrel as missing_barrel", async () => {
|
|
205
|
-
// leaf-files has 12+ .ts files and no index.ts
|
|
206
|
-
const selector = {
|
|
207
|
-
type: "glob",
|
|
208
|
-
pattern: "*.ts",
|
|
209
|
-
depth: undefined,
|
|
210
|
-
};
|
|
211
|
-
const result = await validate(selector, fixturesDir);
|
|
212
|
-
expect(result.missing_barrel).toBeDefined();
|
|
213
|
-
expect(result.missing_barrel?.files).toContain(".");
|
|
214
|
-
});
|
|
215
|
-
it("does not flag directories with <=3 files as missing_barrel", async () => {
|
|
216
|
-
// depth-advancement has only 2 files
|
|
217
|
-
const depthDir = path.resolve(__dirname, "fixtures", "depth-advancement");
|
|
218
|
-
const selector = {
|
|
219
|
-
type: "glob",
|
|
220
|
-
pattern: "*.ts",
|
|
221
|
-
depth: undefined,
|
|
222
|
-
};
|
|
223
|
-
const result = await validate(selector, depthDir);
|
|
224
|
-
expect(result.missing_barrel).toBeUndefined();
|
|
225
|
-
});
|
|
226
|
-
it("does not flag directories that have a barrel as missing_barrel", async () => {
|
|
227
|
-
// barrel-basic has index.ts + helper.ts + utils.ts
|
|
228
|
-
const barrelDir = path.resolve(__dirname, "fixtures", "barrel-basic");
|
|
229
|
-
const selector = {
|
|
230
|
-
type: "glob",
|
|
231
|
-
pattern: "**/*.ts",
|
|
232
|
-
depth: undefined,
|
|
233
|
-
};
|
|
234
|
-
const result = await validate(selector, barrelDir);
|
|
235
|
-
expect(result.missing_barrel).toBeUndefined();
|
|
236
|
-
});
|
|
237
|
-
describe("limit", () => {
|
|
238
|
-
it("all invalid files shown when within limit", async () => {
|
|
239
|
-
const selector = {
|
|
240
|
-
type: "glob",
|
|
241
|
-
pattern: "*.ts",
|
|
242
|
-
depth: undefined,
|
|
243
|
-
};
|
|
244
|
-
const result = await validate(selector, fixturesDir, 100);
|
|
245
|
-
// With a high limit, all invalid files should be present
|
|
246
|
-
const totalShown =
|
|
247
|
-
(result.syntax_error?.files.length ?? 0) +
|
|
248
|
-
(result.missing_jsdoc?.files.length ?? 0) +
|
|
249
|
-
(result.missing_summary?.files.length ?? 0) +
|
|
250
|
-
(result.missing_description?.files.length ?? 0);
|
|
251
|
-
expect(totalShown).toBeGreaterThan(0);
|
|
252
|
-
});
|
|
253
|
-
it("total shown paths capped at limit", async () => {
|
|
254
|
-
const selector = {
|
|
255
|
-
type: "glob",
|
|
256
|
-
pattern: "*.ts",
|
|
257
|
-
depth: undefined,
|
|
258
|
-
};
|
|
259
|
-
const result = await validate(selector, fixturesDir, 1);
|
|
260
|
-
// Total shown paths should equal limit
|
|
261
|
-
const shownPaths =
|
|
262
|
-
(result.syntax_error?.files.length ?? 0) +
|
|
263
|
-
(result.missing_jsdoc?.files.length ?? 0) +
|
|
264
|
-
(result.missing_summary?.files.length ?? 0) +
|
|
265
|
-
(result.missing_description?.files.length ?? 0);
|
|
266
|
-
expect(shownPaths).toBe(1);
|
|
267
|
-
});
|
|
268
|
-
it("limit of 0 shows no file paths", async () => {
|
|
269
|
-
const selector = {
|
|
270
|
-
type: "glob",
|
|
271
|
-
pattern: "*.ts",
|
|
272
|
-
depth: undefined,
|
|
273
|
-
};
|
|
274
|
-
const result = await validate(selector, fixturesDir, 0);
|
|
275
|
-
expect(result.syntax_error).toBeUndefined();
|
|
276
|
-
expect(result.missing_jsdoc).toBeUndefined();
|
|
277
|
-
expect(result.missing_summary).toBeUndefined();
|
|
278
|
-
expect(result.missing_description).toBeUndefined();
|
|
279
|
-
});
|
|
280
|
-
it("groups are filled in priority order (syntax_error first)", async () => {
|
|
281
|
-
const files = [
|
|
282
|
-
fixture("syntax-error.ts"),
|
|
283
|
-
fixture("no-jsdoc.ts"),
|
|
284
|
-
fixture("description-only.ts"),
|
|
285
|
-
fixture("summary-only.ts"),
|
|
286
|
-
];
|
|
287
|
-
const result = await validateFiles(files, fixturesDir, 2);
|
|
288
|
-
// syntax_error should be filled first
|
|
289
|
-
expect(result.syntax_error?.files).toEqual(["syntax-error.ts"]);
|
|
290
|
-
// Then missing_jsdoc
|
|
291
|
-
expect(result.missing_jsdoc?.files).toEqual(["no-jsdoc.ts"]);
|
|
292
|
-
// Remaining groups should not appear (limit reached)
|
|
293
|
-
expect(result.missing_summary).toBeUndefined();
|
|
294
|
-
expect(result.missing_description).toBeUndefined();
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
describe("validateFiles", () => {
|
|
299
|
-
it("validates explicit file list", async () => {
|
|
300
|
-
const files = [
|
|
301
|
-
fixture("two-summaries.ts"),
|
|
302
|
-
fixture("one-summary.ts"),
|
|
303
|
-
fixture("no-jsdoc.ts"),
|
|
304
|
-
];
|
|
305
|
-
const result = await validateFiles(files, fixturesDir);
|
|
306
|
-
expect(result.missing_jsdoc?.files).toEqual(["no-jsdoc.ts"]);
|
|
307
|
-
});
|
|
308
|
-
it("filters to .ts/.tsx only", async () => {
|
|
309
|
-
const files = [
|
|
310
|
-
fixture("two-summaries.ts"),
|
|
311
|
-
"/some/path/file.js",
|
|
312
|
-
"/some/path/file.jsx",
|
|
313
|
-
fixture("one-summary.ts"),
|
|
314
|
-
];
|
|
315
|
-
const result = await validateFiles(files, fixturesDir);
|
|
316
|
-
// Should only validate .ts/.tsx files (filters out .js/.jsx) — all valid
|
|
317
|
-
expect(result.syntax_error).toBeUndefined();
|
|
318
|
-
expect(result.missing_jsdoc).toBeUndefined();
|
|
319
|
-
expect(result.missing_summary).toBeUndefined();
|
|
320
|
-
expect(result.missing_description).toBeUndefined();
|
|
321
|
-
});
|
|
322
|
-
it("produces grouped output format with ValidationGroup shape", async () => {
|
|
323
|
-
const files = [fixture("no-jsdoc.ts")];
|
|
324
|
-
const result = await validateFiles(files, fixturesDir);
|
|
325
|
-
expect(result.missing_jsdoc).toBeDefined();
|
|
326
|
-
expect(typeof result.missing_jsdoc?.guidance).toBe("string");
|
|
327
|
-
expect(Array.isArray(result.missing_jsdoc?.files)).toBe(true);
|
|
328
|
-
});
|
|
329
|
-
it("handles empty file list", async () => {
|
|
330
|
-
const result = await validateFiles([], fixturesDir);
|
|
331
|
-
expect(result.syntax_error).toBeUndefined();
|
|
332
|
-
expect(result.missing_jsdoc).toBeUndefined();
|
|
333
|
-
expect(result.missing_summary).toBeUndefined();
|
|
334
|
-
expect(result.missing_description).toBeUndefined();
|
|
335
|
-
});
|
|
336
|
-
it("handles mixed pass/fail results", async () => {
|
|
337
|
-
const files = [
|
|
338
|
-
fixture("one-summary.ts"), // Pass
|
|
339
|
-
fixture("description-only.ts"), // Fail — missing_summary
|
|
340
|
-
];
|
|
341
|
-
const result = await validateFiles(files, fixturesDir);
|
|
342
|
-
expect(result.missing_summary?.files).toEqual(["description-only.ts"]);
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
describe("cache integration", () => {
|
|
346
|
-
it("validate returns cached classification for unchanged files", async () => {
|
|
347
|
-
// Run validate twice on same file, second time should be cached
|
|
348
|
-
// (we can't directly verify caching without mocking, but verify correctness)
|
|
349
|
-
const files = [fixture("two-summaries.ts")];
|
|
350
|
-
const result1 = await validateFiles(files, fixturesDir);
|
|
351
|
-
const result2 = await validateFiles(files, fixturesDir);
|
|
352
|
-
expect(result1).toEqual(result2);
|
|
353
|
-
});
|
|
354
|
-
it("validate works with cache disabled", async () => {
|
|
355
|
-
const files = [fixture("two-summaries.ts")];
|
|
356
|
-
const disabledConfig = { enabled: false, directory: "" };
|
|
357
|
-
const result = await validateFiles(files, fixturesDir, 100, disabledConfig);
|
|
358
|
-
expect(result.syntax_error).toBeUndefined();
|
|
359
|
-
expect(result.missing_jsdoc).toBeUndefined();
|
|
360
|
-
});
|
|
361
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the content-hash-based disk cache module. Verifies content hashing,
|
|
3
|
-
* cache read/write operations, directory creation, graceful error handling,
|
|
4
|
-
* concurrency safety, and mode namespacing using temporary test directories.
|
|
5
|
-
*
|
|
6
|
-
* @summary Unit and integration tests for cache operations
|
|
7
|
-
*/
|
|
8
|
-
export {};
|