@fuzdev/fuz_ui 0.194.0 → 0.195.0

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.
Files changed (61) hide show
  1. package/dist/DeclarationDetail.svelte +139 -64
  2. package/dist/DeclarationDetail.svelte.d.ts +9 -0
  3. package/dist/DeclarationDetail.svelte.d.ts.map +1 -1
  4. package/dist/LibraryDetail.svelte +14 -10
  5. package/dist/LibraryDetail.svelte.d.ts +7 -0
  6. package/dist/LibraryDetail.svelte.d.ts.map +1 -1
  7. package/dist/declaration.svelte.d.ts +233 -25
  8. package/dist/declaration.svelte.d.ts.map +1 -1
  9. package/dist/declaration.svelte.js +71 -23
  10. package/dist/library.svelte.js +1 -1
  11. package/dist/library_gen.d.ts +24 -17
  12. package/dist/library_gen.d.ts.map +1 -1
  13. package/dist/library_gen.js +52 -32
  14. package/dist/library_output.d.ts +5 -4
  15. package/dist/library_output.d.ts.map +1 -1
  16. package/dist/library_output.js +11 -8
  17. package/dist/module.svelte.d.ts +21 -7
  18. package/dist/module.svelte.d.ts.map +1 -1
  19. package/dist/module.svelte.js +26 -11
  20. package/dist/tsdoc_mdz.d.ts +6 -1
  21. package/dist/tsdoc_mdz.d.ts.map +1 -1
  22. package/dist/tsdoc_mdz.js +23 -2
  23. package/package.json +10 -8
  24. package/src/lib/declaration.svelte.ts +90 -35
  25. package/src/lib/library.svelte.ts +1 -1
  26. package/src/lib/library_gen.ts +65 -42
  27. package/src/lib/library_output.ts +11 -8
  28. package/src/lib/module.svelte.ts +29 -16
  29. package/src/lib/tsdoc_mdz.ts +25 -2
  30. package/dist/analysis_context.d.ts +0 -199
  31. package/dist/analysis_context.d.ts.map +0 -1
  32. package/dist/analysis_context.js +0 -138
  33. package/dist/library_analysis.d.ts +0 -112
  34. package/dist/library_analysis.d.ts.map +0 -1
  35. package/dist/library_analysis.js +0 -106
  36. package/dist/library_generate.d.ts +0 -94
  37. package/dist/library_generate.d.ts.map +0 -1
  38. package/dist/library_generate.js +0 -147
  39. package/dist/library_pipeline.d.ts +0 -113
  40. package/dist/library_pipeline.d.ts.map +0 -1
  41. package/dist/library_pipeline.js +0 -160
  42. package/dist/module_helpers.d.ts +0 -334
  43. package/dist/module_helpers.d.ts.map +0 -1
  44. package/dist/module_helpers.js +0 -317
  45. package/dist/svelte_helpers.d.ts +0 -92
  46. package/dist/svelte_helpers.d.ts.map +0 -1
  47. package/dist/svelte_helpers.js +0 -367
  48. package/dist/ts_helpers.d.ts +0 -181
  49. package/dist/ts_helpers.d.ts.map +0 -1
  50. package/dist/ts_helpers.js +0 -674
  51. package/dist/tsdoc_helpers.d.ts +0 -119
  52. package/dist/tsdoc_helpers.d.ts.map +0 -1
  53. package/dist/tsdoc_helpers.js +0 -207
  54. package/src/lib/analysis_context.ts +0 -254
  55. package/src/lib/library_analysis.ts +0 -168
  56. package/src/lib/library_generate.ts +0 -215
  57. package/src/lib/library_pipeline.ts +0 -221
  58. package/src/lib/module_helpers.ts +0 -501
  59. package/src/lib/svelte_helpers.ts +0 -539
  60. package/src/lib/ts_helpers.ts +0 -862
  61. package/src/lib/tsdoc_helpers.ts +0 -246
@@ -1,862 +0,0 @@
1
- /**
2
- * TypeScript compiler API helpers for extracting metadata from source code.
3
- *
4
- * All functions are prefixed with `ts_` for clarity.
5
- *
6
- * @module
7
- */
8
-
9
- import ts from 'typescript';
10
- import type {
11
- DeclarationJson,
12
- GenericParamInfo,
13
- DeclarationKind,
14
- } from '@fuzdev/fuz_util/source_json.js';
15
- import type {Logger} from '@fuzdev/fuz_util/log.js';
16
-
17
- import {tsdoc_parse, tsdoc_apply_to_declaration, tsdoc_clean_comment} from './tsdoc_helpers.js';
18
- import {
19
- type ModuleSourceOptions,
20
- type SourceFileInfo,
21
- module_extract_dependencies,
22
- module_extract_path,
23
- module_is_source,
24
- } from './module_helpers.js';
25
- import type {AnalysisContext} from './analysis_context.js';
26
- // Import shared types from library_analysis (type-only import avoids circular runtime dependency)
27
- import type {DeclarationAnalysis, ReExportInfo, ModuleAnalysis} from './library_analysis.js';
28
-
29
- /**
30
- * Options for creating a TypeScript program.
31
- */
32
- export interface TsProgramOptions {
33
- /** Project root directory. @default './' */
34
- root?: string;
35
- /** Path to `tsconfig.json` (relative to root). @default 'tsconfig.json' */
36
- tsconfig?: string;
37
- /** Override compiler options. */
38
- compiler_options?: ts.CompilerOptions;
39
- }
40
-
41
- /**
42
- * Result of creating a TypeScript program.
43
- */
44
- export interface TsProgram {
45
- program: ts.Program;
46
- checker: ts.TypeChecker;
47
- }
48
-
49
- /**
50
- * Result of analyzing a module's exports.
51
- */
52
- export interface ModuleExportsAnalysis {
53
- /** Module-level documentation comment. */
54
- module_comment?: string;
55
- /** All exported declarations with nodocs flags - consumer filters based on policy. */
56
- declarations: Array<DeclarationAnalysis>;
57
- /** Same-name re-exports (for building `also_exported_from` in post-processing). */
58
- re_exports: Array<ReExportInfo>;
59
- /** Star exports (`export * from './module'`) - module paths that are fully re-exported. */
60
- star_exports: Array<string>;
61
- }
62
-
63
- /**
64
- * Create TypeScript program for analysis.
65
- *
66
- * @param options - configuration options for program creation
67
- * @param log - optional logger for info messages
68
- * @returns the program and type checker
69
- * @throws Error if `tsconfig.json` is not found
70
- */
71
- export const ts_create_program = (options?: TsProgramOptions, log?: Logger): TsProgram => {
72
- const root = options?.root ?? './';
73
- const tsconfig_name = options?.tsconfig ?? 'tsconfig.json';
74
-
75
- const config_path = ts.findConfigFile(root, ts.sys.fileExists, tsconfig_name);
76
- if (!config_path) {
77
- throw new Error(`No ${tsconfig_name} found in ${root}`);
78
- }
79
-
80
- log?.info(`using ${config_path}`);
81
-
82
- const config_file = ts.readConfigFile(config_path, ts.sys.readFile);
83
- const parsed_config = ts.parseJsonConfigFileContent(config_file.config, ts.sys, root);
84
-
85
- // Merge compiler options if provided
86
- const compiler_options = options?.compiler_options
87
- ? {...parsed_config.options, ...options.compiler_options}
88
- : parsed_config.options;
89
-
90
- const program = ts.createProgram(parsed_config.fileNames, compiler_options);
91
- return {program, checker: program.getTypeChecker()};
92
- };
93
-
94
- /**
95
- * Analyze a TypeScript file and extract module metadata.
96
- *
97
- * Wraps `ts_analyze_module_exports` and adds dependency information
98
- * from the source file info if available.
99
- *
100
- * This is a high-level function suitable for building documentation or library metadata.
101
- * For lower-level analysis, use `ts_analyze_module_exports` directly.
102
- *
103
- * @param source_file_info - the source file info (from Gro filer, file system, or other source)
104
- * @param ts_source_file - TypeScript source file from the program
105
- * @param module_path - the module path (relative to source root)
106
- * @param checker - TypeScript type checker
107
- * @param options - module source options for path extraction
108
- * @param ctx - analysis context for collecting diagnostics
109
- * @returns module metadata and re-export information
110
- */
111
- export const ts_analyze_module = (
112
- source_file_info: SourceFileInfo,
113
- ts_source_file: ts.SourceFile,
114
- module_path: string,
115
- checker: ts.TypeChecker,
116
- options: ModuleSourceOptions,
117
- ctx: AnalysisContext,
118
- ): ModuleAnalysis => {
119
- // Use the mid-level helper for core analysis
120
- const {module_comment, declarations, re_exports, star_exports} = ts_analyze_module_exports(
121
- ts_source_file,
122
- checker,
123
- options,
124
- ctx,
125
- );
126
-
127
- // Extract dependencies and dependents if provided
128
- const {dependencies, dependents} = module_extract_dependencies(source_file_info, options);
129
-
130
- return {
131
- path: module_path,
132
- module_comment,
133
- declarations,
134
- dependencies,
135
- dependents,
136
- star_exports,
137
- re_exports,
138
- };
139
- };
140
-
141
- /**
142
- * Analyze all exports from a TypeScript source file.
143
- *
144
- * Extracts the module-level comment and all exported declarations with
145
- * complete metadata. Handles re-exports by:
146
- * - Same-name re-exports: tracked in `re_exports` for `also_exported_from` building
147
- * - Renamed re-exports: included as new declarations with `alias_of` metadata
148
- * - Star exports (`export * from`): tracked in `star_exports` for namespace-level info
149
- *
150
- * This is a mid-level function (above `ts_extract_*`, below `library_gen`)
151
- * suitable for building documentation, API explorers, or analysis tools.
152
- * For standard SvelteKit library layouts, use `module_create_source_options(process.cwd())`.
153
- *
154
- * @param source_file - the TypeScript source file to analyze
155
- * @param checker - the TypeScript type checker
156
- * @param options - module source options for path extraction in re-exports
157
- * @param ctx - analysis context for collecting diagnostics
158
- * @returns module comment, declarations, re-exports, and star exports
159
- */
160
- export const ts_analyze_module_exports = (
161
- source_file: ts.SourceFile,
162
- checker: ts.TypeChecker,
163
- options: ModuleSourceOptions,
164
- ctx: AnalysisContext,
165
- ): ModuleExportsAnalysis => {
166
- const declarations: Array<DeclarationAnalysis> = [];
167
- const re_exports: Array<ReExportInfo> = [];
168
- const star_exports: Array<string> = [];
169
-
170
- // Extract module-level comment
171
- const module_comment = ts_extract_module_comment(source_file);
172
-
173
- // Extract star exports (export * from './module')
174
- for (const statement of source_file.statements) {
175
- if (
176
- ts.isExportDeclaration(statement) &&
177
- !statement.exportClause && // No exportClause means `export *`
178
- statement.moduleSpecifier &&
179
- ts.isStringLiteral(statement.moduleSpecifier)
180
- ) {
181
- // Use the type checker to resolve the module - it has already resolved all imports
182
- // during program creation, so this leverages TypeScript's full module resolution
183
- const module_symbol = checker.getSymbolAtLocation(statement.moduleSpecifier);
184
- if (module_symbol) {
185
- // Get the source file from the module symbol's declarations
186
- const module_decl = module_symbol.valueDeclaration ?? module_symbol.declarations?.[0];
187
- if (module_decl) {
188
- const resolved_source = module_decl.getSourceFile();
189
- const resolved_path = resolved_source.fileName;
190
-
191
- // Only include star exports from source modules (not node_modules)
192
- if (module_is_source(resolved_path, options)) {
193
- star_exports.push(module_extract_path(resolved_path, options));
194
- }
195
- }
196
- }
197
- // If module couldn't be resolved (external package, etc.), skip it
198
- }
199
- }
200
-
201
- // Get all exported symbols
202
- const symbol = checker.getSymbolAtLocation(source_file);
203
- if (symbol) {
204
- const exports = checker.getExportsOfModule(symbol);
205
- for (const export_symbol of exports) {
206
- // Check if this is an alias (potential re-export) using the Alias flag
207
- const is_alias = (export_symbol.flags & ts.SymbolFlags.Alias) !== 0;
208
-
209
- if (is_alias) {
210
- // This might be a re-export - use getAliasedSymbol to find the original
211
- const aliased_symbol = checker.getAliasedSymbol(export_symbol);
212
- const aliased_decl = aliased_symbol.valueDeclaration || aliased_symbol.declarations?.[0];
213
-
214
- if (aliased_decl) {
215
- const original_source = aliased_decl.getSourceFile();
216
-
217
- // Check if this is a CROSS-FILE re-export (original in different file)
218
- if (original_source.fileName !== source_file.fileName) {
219
- // Only track if the original is from a source module (not node_modules)
220
- if (module_is_source(original_source.fileName, options)) {
221
- const original_module = module_extract_path(original_source.fileName, options);
222
- const original_name = aliased_symbol.name;
223
- const is_renamed = export_symbol.name !== original_name;
224
-
225
- if (is_renamed) {
226
- // Renamed re-export (export {foo as bar}) - create new declaration with alias_of
227
- const kind = ts_infer_declaration_kind(aliased_symbol, aliased_decl);
228
- const decl: DeclarationJson = {
229
- name: export_symbol.name,
230
- kind,
231
- alias_of: {module: original_module, name: original_name, kind},
232
- };
233
- // Renamed re-exports aren't nodocs - they're new declarations pointing to the original
234
- declarations.push({declaration: decl, nodocs: false});
235
- } else {
236
- // Same-name re-export - track for also_exported_from, skip from declarations
237
- re_exports.push({
238
- name: export_symbol.name,
239
- original_module,
240
- });
241
- }
242
- continue;
243
- }
244
- // Re-export from external module (node_modules) - skip entirely
245
- continue;
246
- }
247
- // Within-file alias (export { x as y }) - fall through to normal analysis
248
- }
249
- }
250
-
251
- // Normal export or within-file alias - declared in this file
252
- const {declaration, nodocs} = ts_analyze_declaration(
253
- export_symbol,
254
- source_file,
255
- checker,
256
- ctx,
257
- );
258
- // Include all declarations with nodocs flag - consumer decides filtering policy
259
- declarations.push({declaration, nodocs});
260
- }
261
- }
262
-
263
- return {
264
- module_comment,
265
- declarations,
266
- re_exports,
267
- star_exports,
268
- };
269
- };
270
-
271
- /**
272
- * Analyze a TypeScript symbol and extract rich metadata.
273
- *
274
- * This is a high-level function that combines TSDoc parsing with TypeScript
275
- * type analysis to produce complete declaration metadata. Suitable for use
276
- * in documentation generators, IDE integrations, and other tooling.
277
- *
278
- * @param symbol - the TypeScript symbol to analyze
279
- * @param source_file - the source file containing the symbol
280
- * @param checker - the TypeScript type checker
281
- * @param ctx - optional analysis context for collecting diagnostics
282
- * @returns complete declaration metadata including docs, types, and parameters, plus nodocs flag
283
- */
284
- export const ts_analyze_declaration = (
285
- symbol: ts.Symbol,
286
- source_file: ts.SourceFile,
287
- checker: ts.TypeChecker,
288
- ctx: AnalysisContext,
289
- ): DeclarationAnalysis => {
290
- const name = symbol.name;
291
- const decl_node = symbol.valueDeclaration || symbol.declarations?.[0];
292
-
293
- // Determine kind (fallback to 'variable' if no declaration node)
294
- const kind = decl_node ? ts_infer_declaration_kind(symbol, decl_node) : 'variable';
295
-
296
- const result: DeclarationJson = {
297
- name,
298
- kind,
299
- };
300
-
301
- if (!decl_node) {
302
- return {declaration: result, nodocs: false};
303
- }
304
-
305
- // Extract TSDoc
306
- const tsdoc = tsdoc_parse(decl_node, source_file);
307
- const nodocs = tsdoc?.nodocs ?? false;
308
- tsdoc_apply_to_declaration(result, tsdoc);
309
-
310
- // Extract source line
311
- const start = decl_node.getStart(source_file);
312
- const start_pos = source_file.getLineAndCharacterOfPosition(start);
313
- result.source_line = start_pos.line + 1;
314
-
315
- // Extract type-specific info
316
- if (result.kind === 'function') {
317
- ts_extract_function_info(decl_node, symbol, checker, result, tsdoc, ctx);
318
- } else if (result.kind === 'type') {
319
- ts_extract_type_info(decl_node, symbol, checker, result, ctx);
320
- } else if (result.kind === 'class') {
321
- ts_extract_class_info(decl_node, symbol, checker, result, ctx);
322
- } else if (result.kind === 'variable') {
323
- ts_extract_variable_info(decl_node, symbol, checker, result, ctx);
324
- }
325
-
326
- return {declaration: result, nodocs};
327
- };
328
-
329
- /**
330
- * Extract module-level comment.
331
- *
332
- * Requires `@module` tag to identify module comments. The tag line is stripped
333
- * from the output. Supports optional module renaming: `@module custom-name`.
334
- *
335
- * @see {@link https://typedoc.org/documents/Tags._module.html}
336
- */
337
- export const ts_extract_module_comment = (source_file: ts.SourceFile): string | undefined => {
338
- const full_text = source_file.getFullText();
339
-
340
- // Collect all JSDoc comments in the file
341
- const all_comments: Array<{pos: number; end: number}> = [];
342
-
343
- // Check for comments at the start of the file (before any statements)
344
- const leading_comments = ts.getLeadingCommentRanges(full_text, 0);
345
- if (leading_comments?.length) {
346
- all_comments.push(...leading_comments);
347
- }
348
-
349
- // Check for comments before each statement
350
- for (const statement of source_file.statements) {
351
- const comments = ts.getLeadingCommentRanges(full_text, statement.getFullStart());
352
- if (comments?.length) {
353
- all_comments.push(...comments);
354
- }
355
- }
356
-
357
- // Find the first comment with `@module` tag
358
- for (const comment of all_comments) {
359
- const comment_text = full_text.substring(comment.pos, comment.end);
360
- if (!comment_text.trimStart().startsWith('/**')) continue;
361
-
362
- // Clean the comment first, then check for tag at start of line
363
- const cleaned = tsdoc_clean_comment(comment_text);
364
- if (!cleaned) continue;
365
-
366
- // Check for `@module` as a proper tag (at start of line, not mentioned in prose)
367
- if (/(?:^|\n)@module\b/.test(cleaned)) {
368
- const stripped = tsdoc_strip_module_tag(cleaned);
369
- return stripped || undefined;
370
- }
371
- }
372
-
373
- return undefined;
374
- };
375
-
376
- /**
377
- * Strip `@module` tag line from comment text.
378
- *
379
- * Handles formats:
380
- * - `@module` (standalone)
381
- * - `@module module-name` (with rename)
382
- */
383
- const tsdoc_strip_module_tag = (text: string): string => {
384
- // Remove lines that START with `@module` (not mentioned in prose)
385
- const lines = text.split('\n');
386
- const filtered = lines.filter((line) => !/^\s*@module\b/.test(line));
387
- return filtered.join('\n').trim();
388
- };
389
-
390
- /**
391
- * Infer declaration kind from symbol and node.
392
- *
393
- * Maps TypeScript constructs to `DeclarationKind`:
394
- * - Classes → `'class'`
395
- * - Functions (declarations, expressions, arrows) → `'function'`
396
- * - Interfaces, type aliases → `'type'`
397
- * - Enums (regular and const) → `'type'`
398
- * - Variables → `'variable'` (unless function-valued → `'function'`)
399
- */
400
- export const ts_infer_declaration_kind = (symbol: ts.Symbol, node: ts.Node): DeclarationKind => {
401
- // Check symbol flags
402
- if (symbol.flags & ts.SymbolFlags.Class) return 'class';
403
- if (symbol.flags & ts.SymbolFlags.Function) return 'function';
404
- if (symbol.flags & ts.SymbolFlags.Interface) return 'type';
405
- if (symbol.flags & ts.SymbolFlags.TypeAlias) return 'type';
406
- // Enums are treated as types (they define a named type with values)
407
- if (symbol.flags & ts.SymbolFlags.Enum) return 'type';
408
- if (symbol.flags & ts.SymbolFlags.ConstEnum) return 'type';
409
-
410
- // Check node kind
411
- if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node))
412
- return 'function';
413
- if (ts.isClassDeclaration(node)) return 'class';
414
- if (ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node)) return 'type';
415
- if (ts.isEnumDeclaration(node)) return 'type';
416
- if (ts.isVariableDeclaration(node)) {
417
- // Check if it's a function-valued variable
418
- const init = node.initializer;
419
- if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init))) {
420
- return 'function';
421
- }
422
- return 'variable';
423
- }
424
-
425
- return 'variable';
426
- };
427
-
428
- /**
429
- * Extract parameters from a TypeScript signature with TSDoc descriptions and default values.
430
- *
431
- * Shared helper for extracting parameter information from both standalone functions
432
- * and class methods/constructors.
433
- *
434
- * @param sig - the TypeScript signature to extract parameters from
435
- * @param checker - TypeScript type checker for type resolution
436
- * @param tsdoc_params - map of parameter names to TSDoc descriptions (from tsdoc.params)
437
- * @returns array of parameter info objects
438
- */
439
- export const ts_extract_signature_parameters = (
440
- sig: ts.Signature,
441
- checker: ts.TypeChecker,
442
- tsdoc_params: Map<string, string> | undefined,
443
- ): Array<{
444
- name: string;
445
- type: string;
446
- optional?: boolean;
447
- description?: string;
448
- default_value?: string;
449
- }> => {
450
- return sig.parameters.map((param) => {
451
- const param_decl = param.valueDeclaration;
452
-
453
- // Get type - use declaration location if available, otherwise get declared type
454
- let type_string = 'unknown';
455
- if (param_decl) {
456
- const param_type = checker.getTypeOfSymbolAtLocation(param, param_decl);
457
- type_string = checker.typeToString(param_type);
458
- } else {
459
- const param_type = checker.getDeclaredTypeOfSymbol(param);
460
- type_string = checker.typeToString(param_type);
461
- }
462
-
463
- // Get TSDoc description for this parameter
464
- const description = tsdoc_params?.get(param.name);
465
-
466
- // Extract default value from AST
467
- let default_value: string | undefined;
468
- if (param_decl && ts.isParameter(param_decl) && param_decl.initializer) {
469
- default_value = param_decl.initializer.getText();
470
- }
471
-
472
- const optional = !!(param_decl && ts.isParameter(param_decl) && param_decl.questionToken);
473
-
474
- return {
475
- name: param.name,
476
- type: type_string,
477
- ...(optional && {optional}),
478
- description,
479
- default_value,
480
- };
481
- });
482
- };
483
-
484
- /**
485
- * Extract function/method information including parameters
486
- * with descriptions and default values.
487
- *
488
- * @internal Use `ts_analyze_declaration` for high-level analysis.
489
- * @mutates declaration - adds type_signature, return_type, return_description, throws, since, parameters, generic_params
490
- */
491
- export const ts_extract_function_info = (
492
- node: ts.Node,
493
- symbol: ts.Symbol,
494
- checker: ts.TypeChecker,
495
- declaration: DeclarationJson,
496
- tsdoc: ReturnType<typeof tsdoc_parse>,
497
- ctx: AnalysisContext,
498
- ): void => {
499
- try {
500
- const type = checker.getTypeOfSymbolAtLocation(symbol, node);
501
- const signatures = type.getCallSignatures();
502
-
503
- if (signatures.length > 0) {
504
- const sig = signatures[0]!;
505
- declaration.type_signature = checker.signatureToString(sig);
506
-
507
- const return_type = checker.getReturnTypeOfSignature(sig);
508
- declaration.return_type = checker.typeToString(return_type);
509
-
510
- // Extract return description from TSDoc
511
- if (tsdoc?.returns) {
512
- declaration.return_description = tsdoc.returns;
513
- }
514
-
515
- // Extract throws and since from TSDoc
516
- if (tsdoc?.throws?.length) {
517
- declaration.throws = tsdoc.throws;
518
- }
519
- if (tsdoc?.since) {
520
- declaration.since = tsdoc.since;
521
- }
522
-
523
- // Extract parameters with descriptions and default values
524
- declaration.parameters = ts_extract_signature_parameters(sig, checker, tsdoc?.params);
525
- }
526
- } catch (err) {
527
- const loc = ts_get_node_location(node);
528
- ctx.add({
529
- kind: 'signature_analysis_failed',
530
- file: loc.file,
531
- line: loc.line,
532
- column: loc.column,
533
- message: `Failed to analyze signature for "${symbol.name}": ${err instanceof Error ? err.message : String(err)}`,
534
- severity: 'warning',
535
- function_name: symbol.name,
536
- });
537
- }
538
-
539
- // Extract generic type parameters
540
- if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
541
- if (node.typeParameters?.length) {
542
- declaration.generic_params = node.typeParameters.map(ts_parse_generic_param);
543
- }
544
- }
545
- };
546
-
547
- /**
548
- * Extract type/interface information with rich property metadata.
549
- *
550
- * @internal Use `ts_analyze_declaration` for high-level analysis.
551
- * @mutates declaration - adds type_signature, generic_params, extends, properties
552
- */
553
- export const ts_extract_type_info = (
554
- node: ts.Node,
555
- _symbol: ts.Symbol,
556
- checker: ts.TypeChecker,
557
- declaration: DeclarationJson,
558
- ctx: AnalysisContext,
559
- ): void => {
560
- try {
561
- const type = checker.getTypeAtLocation(node);
562
- declaration.type_signature = checker.typeToString(type);
563
- } catch (err) {
564
- const loc = ts_get_node_location(node);
565
- ctx.add({
566
- kind: 'type_extraction_failed',
567
- file: loc.file,
568
- line: loc.line,
569
- column: loc.column,
570
- message: `Failed to extract type for "${declaration.name}": ${err instanceof Error ? err.message : String(err)}`,
571
- severity: 'warning',
572
- symbol_name: declaration.name,
573
- });
574
- }
575
-
576
- if (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) {
577
- if (node.typeParameters?.length) {
578
- declaration.generic_params = node.typeParameters.map(ts_parse_generic_param);
579
- }
580
- }
581
-
582
- if (ts.isInterfaceDeclaration(node)) {
583
- if (node.heritageClauses) {
584
- declaration.extends = node.heritageClauses
585
- .filter((hc) => hc.token === ts.SyntaxKind.ExtendsKeyword)
586
- .flatMap((hc) => hc.types.map((t) => t.getText()));
587
- }
588
-
589
- // Extract properties with full metadata
590
- declaration.properties = [];
591
- for (const member of node.members) {
592
- if (ts.isPropertySignature(member) && ts.isIdentifier(member.name)) {
593
- const prop_name = member.name.text;
594
- const prop_declaration: DeclarationJson = {
595
- name: prop_name,
596
- kind: 'variable',
597
- };
598
-
599
- // Extract modifiers
600
- const modifier_flags = ts_extract_modifiers(ts.getModifiers(member));
601
- if (modifier_flags.length > 0) {
602
- prop_declaration.modifiers = modifier_flags;
603
- }
604
-
605
- // Extract type
606
- if (member.type) {
607
- prop_declaration.type_signature = member.type.getText();
608
- }
609
-
610
- // Extract TSDoc
611
- const prop_tsdoc = tsdoc_parse(member, node.getSourceFile());
612
- if (prop_tsdoc) {
613
- prop_declaration.doc_comment = prop_tsdoc.text;
614
- }
615
-
616
- declaration.properties.push(prop_declaration);
617
- }
618
- }
619
- }
620
- };
621
-
622
- /**
623
- * Extract class information with rich member metadata.
624
- *
625
- * @internal Use `ts_analyze_declaration` for high-level analysis.
626
- * @mutates declaration - adds extends, implements, generic_params, members
627
- */
628
- export const ts_extract_class_info = (
629
- node: ts.Node,
630
- _symbol: ts.Symbol,
631
- checker: ts.TypeChecker,
632
- declaration: DeclarationJson,
633
- ctx: AnalysisContext,
634
- ): void => {
635
- if (!ts.isClassDeclaration(node)) return;
636
-
637
- if (node.heritageClauses) {
638
- declaration.extends = node.heritageClauses
639
- .filter((hc) => hc.token === ts.SyntaxKind.ExtendsKeyword)
640
- .flatMap((hc) => hc.types.map((t) => t.getText()));
641
-
642
- declaration.implements = node.heritageClauses
643
- .filter((hc) => hc.token === ts.SyntaxKind.ImplementsKeyword)
644
- .flatMap((hc) => hc.types.map((t) => t.getText()));
645
- }
646
-
647
- if (node.typeParameters?.length) {
648
- declaration.generic_params = node.typeParameters.map(ts_parse_generic_param);
649
- }
650
-
651
- // Extract members with full metadata
652
- declaration.members = [];
653
- for (const member of node.members) {
654
- if (
655
- ts.isPropertyDeclaration(member) ||
656
- ts.isMethodDeclaration(member) ||
657
- ts.isConstructorDeclaration(member)
658
- ) {
659
- const is_constructor = ts.isConstructorDeclaration(member);
660
- const member_name = is_constructor
661
- ? 'constructor'
662
- : ts.isIdentifier(member.name)
663
- ? member.name.text
664
- : member.name.getText();
665
- if (!member_name) continue;
666
-
667
- // Skip private fields (those starting with #)
668
- if (member_name.startsWith('#')) continue;
669
-
670
- const member_kind: DeclarationKind = is_constructor
671
- ? 'constructor'
672
- : ts.isMethodDeclaration(member)
673
- ? 'function'
674
- : 'variable';
675
-
676
- const member_declaration: DeclarationJson = {
677
- name: member_name,
678
- kind: member_kind,
679
- };
680
-
681
- // Extract visibility and modifiers
682
- const modifier_flags = ts_extract_modifiers(ts.getModifiers(member));
683
- if (modifier_flags.length > 0) {
684
- member_declaration.modifiers = modifier_flags;
685
- }
686
-
687
- // Extract TSDoc
688
- const member_tsdoc = tsdoc_parse(member, node.getSourceFile());
689
- if (member_tsdoc) {
690
- member_declaration.doc_comment = member_tsdoc.text;
691
- }
692
-
693
- // Extract type information and parameters for methods and constructors
694
- try {
695
- if (ts.isPropertyDeclaration(member) && member.type) {
696
- member_declaration.type_signature = member.type.getText();
697
- } else if (ts.isMethodDeclaration(member) || ts.isConstructorDeclaration(member)) {
698
- let signatures: ReadonlyArray<ts.Signature> = [];
699
-
700
- if (is_constructor) {
701
- // For constructors, get construct signatures from the class symbol
702
- // Skip anonymous classes (no name)
703
- if (node.name) {
704
- const class_symbol = checker.getSymbolAtLocation(node.name);
705
- if (class_symbol) {
706
- const class_type = checker.getTypeOfSymbolAtLocation(class_symbol, node);
707
- signatures = class_type.getConstructSignatures();
708
- }
709
- }
710
- } else {
711
- // For methods, get call signatures from the method symbol
712
- const member_symbol = checker.getSymbolAtLocation(member.name);
713
- if (member_symbol) {
714
- const member_type = checker.getTypeOfSymbolAtLocation(member_symbol, member);
715
- signatures = member_type.getCallSignatures();
716
- }
717
- }
718
-
719
- if (signatures.length > 0) {
720
- const sig = signatures[0]!;
721
-
722
- // Extract type signature for both constructors and methods
723
- member_declaration.type_signature = checker.signatureToString(sig);
724
-
725
- // For methods (but not constructors), also extract return info separately
726
- if (!is_constructor) {
727
- // Extract return type for methods
728
- const return_type = checker.getReturnTypeOfSignature(sig);
729
- member_declaration.return_type = checker.typeToString(return_type);
730
-
731
- // Extract return description from TSDoc
732
- if (member_tsdoc?.returns) {
733
- member_declaration.return_description = member_tsdoc.returns;
734
- }
735
- }
736
-
737
- // Extract parameters with descriptions and default values
738
- member_declaration.parameters = ts_extract_signature_parameters(
739
- sig,
740
- checker,
741
- member_tsdoc?.params,
742
- );
743
-
744
- // Extract throws and since from TSDoc (for both methods and constructors)
745
- if (member_tsdoc?.throws?.length) {
746
- member_declaration.throws = member_tsdoc.throws;
747
- }
748
- if (member_tsdoc?.since) {
749
- member_declaration.since = member_tsdoc.since;
750
- }
751
- }
752
- }
753
- } catch (err) {
754
- const loc = ts_get_node_location(member);
755
- const class_name = node.name?.text ?? '<anonymous>';
756
- ctx.add({
757
- kind: 'class_member_failed',
758
- file: loc.file,
759
- line: loc.line,
760
- column: loc.column,
761
- message: `Failed to analyze member "${member_name}" in class "${class_name}": ${err instanceof Error ? err.message : String(err)}`,
762
- severity: 'warning',
763
- class_name,
764
- member_name,
765
- });
766
- }
767
-
768
- declaration.members.push(member_declaration);
769
- }
770
- }
771
- };
772
-
773
- /**
774
- * Extract variable information.
775
- *
776
- * @internal Use `ts_analyze_declaration` for high-level analysis.
777
- * @mutates declaration - adds type_signature
778
- */
779
- export const ts_extract_variable_info = (
780
- node: ts.Node,
781
- symbol: ts.Symbol,
782
- checker: ts.TypeChecker,
783
- declaration: DeclarationJson,
784
- ctx: AnalysisContext,
785
- ): void => {
786
- try {
787
- const type = checker.getTypeOfSymbolAtLocation(symbol, node);
788
- declaration.type_signature = checker.typeToString(type);
789
- } catch (err) {
790
- const loc = ts_get_node_location(node);
791
- ctx.add({
792
- kind: 'type_extraction_failed',
793
- file: loc.file,
794
- line: loc.line,
795
- column: loc.column,
796
- message: `Failed to extract type for variable "${symbol.name}": ${err instanceof Error ? err.message : String(err)}`,
797
- severity: 'warning',
798
- symbol_name: symbol.name,
799
- });
800
- }
801
- };
802
-
803
- /**
804
- * Extract line and column from a TypeScript node.
805
- * Returns 1-based line and column numbers.
806
- */
807
- const ts_get_node_location = (node: ts.Node): {file: string; line: number; column: number} => {
808
- const source_file = node.getSourceFile();
809
- const {line, character} = source_file.getLineAndCharacterOfPosition(node.getStart());
810
- return {
811
- file: source_file.fileName,
812
- line: line + 1, // Convert to 1-based
813
- column: character + 1, // Convert to 1-based
814
- };
815
- };
816
-
817
- const ts_parse_generic_param = (param: ts.TypeParameterDeclaration): GenericParamInfo => {
818
- const result: GenericParamInfo = {
819
- name: param.name.text,
820
- };
821
-
822
- if (param.constraint) {
823
- result.constraint = param.constraint.getText();
824
- }
825
-
826
- if (param.default) {
827
- result.default_type = param.default.getText();
828
- }
829
-
830
- return result;
831
- };
832
-
833
- /**
834
- * TypeScript modifier keywords extracted from declarations.
835
- *
836
- * These are the access modifiers and other keywords that can appear
837
- * on class members, interface properties, etc.
838
- */
839
- export type TsModifier = 'public' | 'private' | 'protected' | 'readonly' | 'static' | 'abstract';
840
-
841
- /**
842
- * Extract modifier keywords from a node's modifiers.
843
- *
844
- * Returns an array of modifier strings like `['public', 'readonly', 'static']`.
845
- */
846
- const ts_extract_modifiers = (
847
- modifiers: ReadonlyArray<ts.ModifierLike> | undefined,
848
- ): Array<TsModifier> => {
849
- const modifier_flags: Array<TsModifier> = [];
850
- if (!modifiers) return modifier_flags;
851
-
852
- for (const mod of modifiers) {
853
- if (mod.kind === ts.SyntaxKind.PublicKeyword) modifier_flags.push('public');
854
- else if (mod.kind === ts.SyntaxKind.PrivateKeyword) modifier_flags.push('private');
855
- else if (mod.kind === ts.SyntaxKind.ProtectedKeyword) modifier_flags.push('protected');
856
- else if (mod.kind === ts.SyntaxKind.ReadonlyKeyword) modifier_flags.push('readonly');
857
- else if (mod.kind === ts.SyntaxKind.StaticKeyword) modifier_flags.push('static');
858
- else if (mod.kind === ts.SyntaxKind.AbstractKeyword) modifier_flags.push('abstract');
859
- }
860
-
861
- return modifier_flags;
862
- };