@bfra.me/workspace-analyzer 0.1.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.
- package/README.md +402 -0
- package/lib/chunk-4LSFAAZW.js +1 -0
- package/lib/chunk-JDF7DQ4V.js +27 -0
- package/lib/chunk-WOJ4C7N7.js +7122 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +318 -0
- package/lib/index.d.ts +3701 -0
- package/lib/index.js +1262 -0
- package/lib/types/index.d.ts +146 -0
- package/lib/types/index.js +28 -0
- package/package.json +89 -0
- package/src/analyzers/analyzer.ts +201 -0
- package/src/analyzers/architectural-analyzer.ts +304 -0
- package/src/analyzers/build-config-analyzer.ts +334 -0
- package/src/analyzers/circular-import-analyzer.ts +463 -0
- package/src/analyzers/config-consistency-analyzer.ts +335 -0
- package/src/analyzers/dead-code-analyzer.ts +565 -0
- package/src/analyzers/duplicate-code-analyzer.ts +626 -0
- package/src/analyzers/duplicate-dependency-analyzer.ts +381 -0
- package/src/analyzers/eslint-config-analyzer.ts +281 -0
- package/src/analyzers/exports-field-analyzer.ts +324 -0
- package/src/analyzers/index.ts +388 -0
- package/src/analyzers/large-dependency-analyzer.ts +535 -0
- package/src/analyzers/package-json-analyzer.ts +349 -0
- package/src/analyzers/peer-dependency-analyzer.ts +275 -0
- package/src/analyzers/tree-shaking-analyzer.ts +623 -0
- package/src/analyzers/tsconfig-analyzer.ts +382 -0
- package/src/analyzers/unused-dependency-analyzer.ts +356 -0
- package/src/analyzers/version-alignment-analyzer.ts +308 -0
- package/src/api/analyze-workspace.ts +245 -0
- package/src/api/index.ts +11 -0
- package/src/cache/cache-manager.ts +495 -0
- package/src/cache/cache-schema.ts +247 -0
- package/src/cache/change-detector.ts +169 -0
- package/src/cache/file-hasher.ts +65 -0
- package/src/cache/index.ts +47 -0
- package/src/cli/commands/analyze.ts +240 -0
- package/src/cli/commands/index.ts +5 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/types.ts +65 -0
- package/src/cli/ui.ts +213 -0
- package/src/cli.ts +9 -0
- package/src/config/defaults.ts +183 -0
- package/src/config/index.ts +81 -0
- package/src/config/loader.ts +270 -0
- package/src/config/merger.ts +229 -0
- package/src/config/schema.ts +263 -0
- package/src/core/incremental-analyzer.ts +462 -0
- package/src/core/index.ts +34 -0
- package/src/core/orchestrator.ts +416 -0
- package/src/graph/dependency-graph.ts +408 -0
- package/src/graph/index.ts +19 -0
- package/src/index.ts +417 -0
- package/src/parser/config-parser.ts +491 -0
- package/src/parser/import-extractor.ts +340 -0
- package/src/parser/index.ts +54 -0
- package/src/parser/typescript-parser.ts +95 -0
- package/src/performance/bundle-estimator.ts +444 -0
- package/src/performance/index.ts +27 -0
- package/src/reporters/console-reporter.ts +355 -0
- package/src/reporters/index.ts +49 -0
- package/src/reporters/json-reporter.ts +273 -0
- package/src/reporters/markdown-reporter.ts +349 -0
- package/src/reporters/reporter.ts +399 -0
- package/src/rules/builtin-rules.ts +709 -0
- package/src/rules/index.ts +52 -0
- package/src/rules/rule-engine.ts +409 -0
- package/src/scanner/index.ts +18 -0
- package/src/scanner/workspace-scanner.ts +403 -0
- package/src/types/index.ts +176 -0
- package/src/types/result.ts +19 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/pattern-matcher.ts +48 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console reporter for terminal output with colors and formatting.
|
|
3
|
+
*
|
|
4
|
+
* Generates colorful terminal output using consola and @clack/prompts
|
|
5
|
+
* for interactive analysis feedback and CI/CD integration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {AnalysisResult, Issue, Severity} from '../types/index'
|
|
9
|
+
import type {GroupedIssues, Reporter, ReportOptions, ReportSummary} from './reporter'
|
|
10
|
+
|
|
11
|
+
import * as p from '@clack/prompts'
|
|
12
|
+
import {consola} from 'consola'
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
calculateSummary,
|
|
16
|
+
CATEGORY_CONFIG,
|
|
17
|
+
DEFAULT_REPORT_OPTIONS,
|
|
18
|
+
filterIssuesForReport,
|
|
19
|
+
formatDuration,
|
|
20
|
+
getRelativePath,
|
|
21
|
+
groupIssues,
|
|
22
|
+
SEVERITY_CONFIG,
|
|
23
|
+
truncateText,
|
|
24
|
+
} from './reporter'
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Options specific to console reporter.
|
|
28
|
+
*/
|
|
29
|
+
export interface ConsoleReporterOptions extends ReportOptions {
|
|
30
|
+
/** Whether to use colors (auto-detected by default) */
|
|
31
|
+
readonly colors?: boolean
|
|
32
|
+
/** Whether to show verbose output with full descriptions */
|
|
33
|
+
readonly verbose?: boolean
|
|
34
|
+
/** Whether to show compact output (one line per issue) */
|
|
35
|
+
readonly compact?: boolean
|
|
36
|
+
/** Maximum width for text wrapping */
|
|
37
|
+
readonly maxWidth?: number
|
|
38
|
+
/** Whether to use @clack/prompts styling */
|
|
39
|
+
readonly useClack?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a console reporter instance.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const reporter = createConsoleReporter({verbose: true})
|
|
48
|
+
* reporter.generate(analysisResult) // Outputs to console
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function createConsoleReporter(defaultOptions?: ConsoleReporterOptions): Reporter {
|
|
52
|
+
return {
|
|
53
|
+
id: 'console',
|
|
54
|
+
name: 'Console Reporter',
|
|
55
|
+
generate(result: AnalysisResult, options?: ReportOptions): string {
|
|
56
|
+
const mergedOptions: ConsoleReporterOptions = {
|
|
57
|
+
...DEFAULT_REPORT_OPTIONS,
|
|
58
|
+
...defaultOptions,
|
|
59
|
+
...options,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const output = generateConsoleOutput(result, mergedOptions)
|
|
63
|
+
printToConsole(output, mergedOptions)
|
|
64
|
+
return output.join('\n')
|
|
65
|
+
},
|
|
66
|
+
stream(result: AnalysisResult, write: (chunk: string) => void, options?: ReportOptions): void {
|
|
67
|
+
const output = this.generate(result, options)
|
|
68
|
+
write(output)
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generate console output lines.
|
|
75
|
+
*/
|
|
76
|
+
function generateConsoleOutput(result: AnalysisResult, options: ConsoleReporterOptions): string[] {
|
|
77
|
+
const lines: string[] = []
|
|
78
|
+
const filteredIssues = filterIssuesForReport(result.issues, options)
|
|
79
|
+
const summary = calculateSummary({...result, issues: filteredIssues})
|
|
80
|
+
const grouped = groupIssues(filteredIssues, options.groupBy)
|
|
81
|
+
|
|
82
|
+
lines.push(...generateHeader(summary))
|
|
83
|
+
lines.push('')
|
|
84
|
+
|
|
85
|
+
if (options.includeSummary) {
|
|
86
|
+
lines.push(...generateSummary(summary))
|
|
87
|
+
lines.push('')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
lines.push(...generateIssuesList(grouped, result.workspacePath, options))
|
|
91
|
+
|
|
92
|
+
lines.push('')
|
|
93
|
+
lines.push(...generateFooter(summary, result))
|
|
94
|
+
|
|
95
|
+
return lines
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Generate header section.
|
|
100
|
+
*/
|
|
101
|
+
function generateHeader(summary: ReportSummary): string[] {
|
|
102
|
+
const lines: string[] = []
|
|
103
|
+
const status = getStatusIcon(summary.highestSeverity)
|
|
104
|
+
const statusText =
|
|
105
|
+
summary.totalIssues === 0 ? 'No issues found' : `${summary.totalIssues} issue(s) found`
|
|
106
|
+
|
|
107
|
+
lines.push('─'.repeat(60))
|
|
108
|
+
lines.push(`${status} Workspace Analysis Report`)
|
|
109
|
+
lines.push(` ${statusText}`)
|
|
110
|
+
lines.push('─'.repeat(60))
|
|
111
|
+
|
|
112
|
+
return lines
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generate summary section.
|
|
117
|
+
*/
|
|
118
|
+
function generateSummary(summary: ReportSummary): string[] {
|
|
119
|
+
const lines: string[] = []
|
|
120
|
+
|
|
121
|
+
lines.push('📊 Summary')
|
|
122
|
+
lines.push('')
|
|
123
|
+
|
|
124
|
+
const severityCounts: string[] = []
|
|
125
|
+
for (const severity of ['critical', 'error', 'warning', 'info'] as const) {
|
|
126
|
+
const count = summary.bySeverity[severity]
|
|
127
|
+
if (count > 0) {
|
|
128
|
+
const icon = SEVERITY_CONFIG[severity].emoji
|
|
129
|
+
severityCounts.push(`${icon} ${count} ${severity}`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (severityCounts.length > 0) {
|
|
134
|
+
lines.push(` ${severityCounts.join(' | ')}`)
|
|
135
|
+
} else {
|
|
136
|
+
lines.push(' ✅ No issues!')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
lines.push('')
|
|
140
|
+
lines.push(
|
|
141
|
+
` 📦 ${summary.packagesAnalyzed} packages | 📄 ${summary.filesAnalyzed} files | ⏱️ ${formatDuration(summary.durationMs)}`,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return lines
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Generate issues list.
|
|
149
|
+
*/
|
|
150
|
+
function generateIssuesList(
|
|
151
|
+
groups: readonly GroupedIssues[],
|
|
152
|
+
workspacePath: string,
|
|
153
|
+
options: ConsoleReporterOptions,
|
|
154
|
+
): string[] {
|
|
155
|
+
const lines: string[] = []
|
|
156
|
+
|
|
157
|
+
if (groups.length === 0 || groups.every(g => g.count === 0)) {
|
|
158
|
+
lines.push('✅ No issues to report!')
|
|
159
|
+
return lines
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const group of groups) {
|
|
163
|
+
if (group.count === 0) {
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
lines.push('')
|
|
168
|
+
lines.push(...generateGroupSection(group, workspacePath, options))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return lines
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Generate a group section.
|
|
176
|
+
*/
|
|
177
|
+
function generateGroupSection(
|
|
178
|
+
group: GroupedIssues,
|
|
179
|
+
workspacePath: string,
|
|
180
|
+
options: ConsoleReporterOptions,
|
|
181
|
+
): string[] {
|
|
182
|
+
const lines: string[] = []
|
|
183
|
+
|
|
184
|
+
const groupIcon = getGroupIcon(group.key, options.groupBy)
|
|
185
|
+
lines.push(`${groupIcon} ${group.label} (${group.count})`)
|
|
186
|
+
lines.push('')
|
|
187
|
+
|
|
188
|
+
const maxIssues = options.maxIssuesPerGroup ?? 50
|
|
189
|
+
const issuesToShow = group.issues.slice(0, maxIssues)
|
|
190
|
+
const remainingCount = group.count - issuesToShow.length
|
|
191
|
+
|
|
192
|
+
for (const issue of issuesToShow) {
|
|
193
|
+
if (options.compact) {
|
|
194
|
+
lines.push(formatCompactIssue(issue, workspacePath))
|
|
195
|
+
} else {
|
|
196
|
+
lines.push(...formatDetailedIssue(issue, workspacePath, options))
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (remainingCount > 0) {
|
|
201
|
+
lines.push(` ... and ${remainingCount} more issue(s)`)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return lines
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Format an issue in compact mode (single line).
|
|
209
|
+
*/
|
|
210
|
+
function formatCompactIssue(issue: Issue, workspacePath: string): string {
|
|
211
|
+
const icon = SEVERITY_CONFIG[issue.severity].emoji
|
|
212
|
+
const relativePath = getRelativePath(issue.location.filePath, workspacePath)
|
|
213
|
+
const location =
|
|
214
|
+
issue.location.line === undefined ? relativePath : `${relativePath}:${issue.location.line}`
|
|
215
|
+
const title = truncateText(issue.title, 50)
|
|
216
|
+
|
|
217
|
+
return ` ${icon} ${location}: ${title}`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Format an issue in detailed mode.
|
|
222
|
+
*/
|
|
223
|
+
function formatDetailedIssue(
|
|
224
|
+
issue: Issue,
|
|
225
|
+
workspacePath: string,
|
|
226
|
+
options: ConsoleReporterOptions,
|
|
227
|
+
): string[] {
|
|
228
|
+
const lines: string[] = []
|
|
229
|
+
const icon = SEVERITY_CONFIG[issue.severity].emoji
|
|
230
|
+
const severityLabel = SEVERITY_CONFIG[issue.severity].label.toUpperCase()
|
|
231
|
+
|
|
232
|
+
lines.push(` ${icon} [${severityLabel}] ${issue.title}`)
|
|
233
|
+
|
|
234
|
+
const relativePath = getRelativePath(issue.location.filePath, workspacePath)
|
|
235
|
+
let location = ` 📍 ${relativePath}`
|
|
236
|
+
if (issue.location.line !== undefined) {
|
|
237
|
+
location += `:${issue.location.line}`
|
|
238
|
+
if (issue.location.column !== undefined) {
|
|
239
|
+
location += `:${issue.location.column}`
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
lines.push(location)
|
|
243
|
+
|
|
244
|
+
if (options.verbose) {
|
|
245
|
+
const maxWidth = options.maxWidth ?? 80
|
|
246
|
+
const wrappedDesc = wrapText(issue.description, maxWidth - 9)
|
|
247
|
+
for (const line of wrappedDesc) {
|
|
248
|
+
lines.push(` ${line}`)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (options.includeSuggestions && issue.suggestion !== undefined) {
|
|
253
|
+
lines.push(` 💡 ${issue.suggestion}`)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
lines.push('')
|
|
257
|
+
|
|
258
|
+
return lines
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Generate footer section.
|
|
263
|
+
*/
|
|
264
|
+
function generateFooter(summary: ReportSummary, result: AnalysisResult): string[] {
|
|
265
|
+
const lines: string[] = []
|
|
266
|
+
|
|
267
|
+
lines.push('─'.repeat(60))
|
|
268
|
+
|
|
269
|
+
if (summary.totalIssues === 0) {
|
|
270
|
+
lines.push('✅ Your workspace looks great!')
|
|
271
|
+
} else {
|
|
272
|
+
const recommendations: string[] = []
|
|
273
|
+
if (summary.bySeverity.critical > 0 || summary.bySeverity.error > 0) {
|
|
274
|
+
recommendations.push('Fix critical and error issues first')
|
|
275
|
+
}
|
|
276
|
+
if (summary.bySeverity.warning > 0) {
|
|
277
|
+
recommendations.push('Review warnings for potential improvements')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (recommendations.length > 0) {
|
|
281
|
+
lines.push('💡 Recommendations:')
|
|
282
|
+
for (const rec of recommendations) {
|
|
283
|
+
lines.push(` • ${rec}`)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const duration = formatDuration(result.completedAt.getTime() - result.startedAt.getTime())
|
|
289
|
+
lines.push('')
|
|
290
|
+
lines.push(`Analysis completed in ${duration}`)
|
|
291
|
+
|
|
292
|
+
return lines
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Print output to console using appropriate method.
|
|
297
|
+
*/
|
|
298
|
+
function printToConsole(lines: string[], options: ConsoleReporterOptions): void {
|
|
299
|
+
if (options.useClack) {
|
|
300
|
+
p.log.message(lines.join('\n'))
|
|
301
|
+
} else {
|
|
302
|
+
for (const line of lines) {
|
|
303
|
+
consola.log(line)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Get status icon based on severity.
|
|
310
|
+
*/
|
|
311
|
+
function getStatusIcon(severity: Severity | null): string {
|
|
312
|
+
if (severity === null) {
|
|
313
|
+
return '✅'
|
|
314
|
+
}
|
|
315
|
+
return SEVERITY_CONFIG[severity].emoji
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get group icon based on groupBy option.
|
|
320
|
+
*/
|
|
321
|
+
function getGroupIcon(key: string, groupBy: ReportOptions['groupBy']): string {
|
|
322
|
+
if (groupBy === 'severity') {
|
|
323
|
+
return SEVERITY_CONFIG[key as Severity]?.emoji ?? '📋'
|
|
324
|
+
}
|
|
325
|
+
if (groupBy === 'category') {
|
|
326
|
+
return CATEGORY_CONFIG[key as keyof typeof CATEGORY_CONFIG]?.emoji ?? '📋'
|
|
327
|
+
}
|
|
328
|
+
return '📄'
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Wrap text to specified width.
|
|
333
|
+
*/
|
|
334
|
+
function wrapText(text: string, maxWidth: number): string[] {
|
|
335
|
+
const words = text.split(/\s+/)
|
|
336
|
+
const lines: string[] = []
|
|
337
|
+
let currentLine = ''
|
|
338
|
+
|
|
339
|
+
for (const word of words) {
|
|
340
|
+
if (currentLine.length + word.length + 1 <= maxWidth) {
|
|
341
|
+
currentLine += (currentLine.length > 0 ? ' ' : '') + word
|
|
342
|
+
} else {
|
|
343
|
+
if (currentLine.length > 0) {
|
|
344
|
+
lines.push(currentLine)
|
|
345
|
+
}
|
|
346
|
+
currentLine = word
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (currentLine.length > 0) {
|
|
351
|
+
lines.push(currentLine)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return lines
|
|
355
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reporter exports for workspace analysis output.
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified API for generating analysis reports in various formats:
|
|
5
|
+
* - JSON: Machine-readable output for CI/CD integration
|
|
6
|
+
* - Markdown: Human-readable reports for documentation and review
|
|
7
|
+
* - Console: Terminal output with colors for interactive feedback
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Console reporter exports
|
|
11
|
+
export type {ConsoleReporterOptions} from './console-reporter'
|
|
12
|
+
export {createConsoleReporter} from './console-reporter'
|
|
13
|
+
|
|
14
|
+
// JSON reporter exports
|
|
15
|
+
export type {
|
|
16
|
+
JsonReport,
|
|
17
|
+
JsonReporterOptions,
|
|
18
|
+
JsonReportGroup,
|
|
19
|
+
JsonReportIssue,
|
|
20
|
+
JsonReportLocation,
|
|
21
|
+
JsonReportMetadata,
|
|
22
|
+
} from './json-reporter'
|
|
23
|
+
export {createJsonReporter} from './json-reporter'
|
|
24
|
+
|
|
25
|
+
// Markdown reporter exports
|
|
26
|
+
export type {MarkdownReporterOptions} from './markdown-reporter'
|
|
27
|
+
export {createMarkdownReporter} from './markdown-reporter'
|
|
28
|
+
|
|
29
|
+
// Core reporter types and utilities
|
|
30
|
+
export type {
|
|
31
|
+
GroupedIssues,
|
|
32
|
+
Reporter,
|
|
33
|
+
ReporterFactory,
|
|
34
|
+
ReportFormat,
|
|
35
|
+
ReportOptions,
|
|
36
|
+
ReportSummary,
|
|
37
|
+
} from './reporter'
|
|
38
|
+
export {
|
|
39
|
+
calculateSummary,
|
|
40
|
+
CATEGORY_CONFIG,
|
|
41
|
+
DEFAULT_REPORT_OPTIONS,
|
|
42
|
+
filterIssuesForReport,
|
|
43
|
+
formatDuration,
|
|
44
|
+
formatLocation,
|
|
45
|
+
getRelativePath,
|
|
46
|
+
groupIssues,
|
|
47
|
+
SEVERITY_CONFIG,
|
|
48
|
+
truncateText,
|
|
49
|
+
} from './reporter'
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON reporter for machine-readable analysis output.
|
|
3
|
+
*
|
|
4
|
+
* Generates structured JSON reports suitable for CI/CD integration,
|
|
5
|
+
* programmatic processing, and data analysis tools.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {AnalysisResult, Issue} from '../types/index'
|
|
9
|
+
import type {Reporter, ReportOptions, ReportSummary} from './reporter'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
calculateSummary,
|
|
13
|
+
DEFAULT_REPORT_OPTIONS,
|
|
14
|
+
filterIssuesForReport,
|
|
15
|
+
getRelativePath,
|
|
16
|
+
groupIssues,
|
|
17
|
+
} from './reporter'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* JSON report structure for serialization.
|
|
21
|
+
*/
|
|
22
|
+
export interface JsonReport {
|
|
23
|
+
/** Schema version for compatibility checking */
|
|
24
|
+
readonly $schema?: string
|
|
25
|
+
/** Report metadata */
|
|
26
|
+
readonly metadata: JsonReportMetadata
|
|
27
|
+
/** Summary statistics */
|
|
28
|
+
readonly summary: ReportSummary
|
|
29
|
+
/** All issues (filtered based on options) */
|
|
30
|
+
readonly issues: readonly JsonReportIssue[]
|
|
31
|
+
/** Issues grouped by the specified key */
|
|
32
|
+
readonly groupedIssues?: readonly JsonReportGroup[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Report metadata for provenance tracking.
|
|
37
|
+
*/
|
|
38
|
+
export interface JsonReportMetadata {
|
|
39
|
+
/** Report generation timestamp (ISO 8601) */
|
|
40
|
+
readonly generatedAt: string
|
|
41
|
+
/** Analysis start timestamp (ISO 8601) */
|
|
42
|
+
readonly analysisStartedAt: string
|
|
43
|
+
/** Analysis completion timestamp (ISO 8601) */
|
|
44
|
+
readonly analysisCompletedAt: string
|
|
45
|
+
/** Workspace path that was analyzed */
|
|
46
|
+
readonly workspacePath: string
|
|
47
|
+
/** Report options used for generation */
|
|
48
|
+
readonly reportOptions: Partial<ReportOptions>
|
|
49
|
+
/** Analyzer version (if available) */
|
|
50
|
+
readonly version?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Issue representation in JSON report.
|
|
55
|
+
*/
|
|
56
|
+
export interface JsonReportIssue {
|
|
57
|
+
/** Issue identifier */
|
|
58
|
+
readonly id: string
|
|
59
|
+
/** Issue title */
|
|
60
|
+
readonly title: string
|
|
61
|
+
/** Issue description */
|
|
62
|
+
readonly description: string
|
|
63
|
+
/** Severity level */
|
|
64
|
+
readonly severity: Issue['severity']
|
|
65
|
+
/** Issue category */
|
|
66
|
+
readonly category: Issue['category']
|
|
67
|
+
/** File location */
|
|
68
|
+
readonly location: JsonReportLocation
|
|
69
|
+
/** Related locations (for multi-file issues like circular imports) */
|
|
70
|
+
readonly relatedLocations?: readonly JsonReportLocation[]
|
|
71
|
+
/** Suggested fix */
|
|
72
|
+
readonly suggestion?: string
|
|
73
|
+
/** Additional metadata */
|
|
74
|
+
readonly metadata?: Readonly<Record<string, unknown>>
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Location representation in JSON report.
|
|
79
|
+
*/
|
|
80
|
+
export interface JsonReportLocation {
|
|
81
|
+
/** File path (relative to workspace root) */
|
|
82
|
+
readonly file: string
|
|
83
|
+
/** Absolute file path */
|
|
84
|
+
readonly absolutePath: string
|
|
85
|
+
/** Line number (1-indexed) */
|
|
86
|
+
readonly line?: number
|
|
87
|
+
/** Column number (1-indexed) */
|
|
88
|
+
readonly column?: number
|
|
89
|
+
/** End line number (1-indexed) */
|
|
90
|
+
readonly endLine?: number
|
|
91
|
+
/** End column number (1-indexed) */
|
|
92
|
+
readonly endColumn?: number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Grouped issues in JSON report.
|
|
97
|
+
*/
|
|
98
|
+
export interface JsonReportGroup {
|
|
99
|
+
/** Group key */
|
|
100
|
+
readonly key: string
|
|
101
|
+
/** Group label */
|
|
102
|
+
readonly label: string
|
|
103
|
+
/** Number of issues in group */
|
|
104
|
+
readonly count: number
|
|
105
|
+
/** Issues in this group */
|
|
106
|
+
readonly issues: readonly JsonReportIssue[]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Options specific to JSON reporter.
|
|
111
|
+
*/
|
|
112
|
+
export interface JsonReporterOptions extends ReportOptions {
|
|
113
|
+
/** Whether to pretty-print JSON output */
|
|
114
|
+
readonly prettyPrint?: boolean
|
|
115
|
+
/** Indentation size for pretty-printing (default: 2) */
|
|
116
|
+
readonly indentation?: number
|
|
117
|
+
/** Whether to include grouped issues section */
|
|
118
|
+
readonly includeGroupedIssues?: boolean
|
|
119
|
+
/** JSON schema URL for validation */
|
|
120
|
+
readonly schemaUrl?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Create a JSON reporter instance.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* const reporter = createJsonReporter({prettyPrint: true})
|
|
129
|
+
* const json = reporter.generate(analysisResult)
|
|
130
|
+
* await fs.writeFile('report.json', json)
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export function createJsonReporter(defaultOptions?: JsonReporterOptions): Reporter {
|
|
134
|
+
return {
|
|
135
|
+
id: 'json',
|
|
136
|
+
name: 'JSON Reporter',
|
|
137
|
+
generate(result: AnalysisResult, options?: ReportOptions): string {
|
|
138
|
+
const mergedOptions: JsonReporterOptions = {
|
|
139
|
+
...DEFAULT_REPORT_OPTIONS,
|
|
140
|
+
...defaultOptions,
|
|
141
|
+
...options,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const report = generateJsonReport(result, mergedOptions)
|
|
145
|
+
const prettyPrint = mergedOptions.prettyPrint ?? true
|
|
146
|
+
const indentation = mergedOptions.indentation ?? 2
|
|
147
|
+
|
|
148
|
+
return prettyPrint ? JSON.stringify(report, null, indentation) : JSON.stringify(report)
|
|
149
|
+
},
|
|
150
|
+
stream(result: AnalysisResult, write: (chunk: string) => void, options?: ReportOptions): void {
|
|
151
|
+
const output = this.generate(result, options)
|
|
152
|
+
write(output)
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Generate the complete JSON report structure.
|
|
159
|
+
*/
|
|
160
|
+
function generateJsonReport(result: AnalysisResult, options: JsonReporterOptions): JsonReport {
|
|
161
|
+
const filteredIssues = filterIssuesForReport(result.issues, options)
|
|
162
|
+
const summary = calculateSummary({...result, issues: filteredIssues})
|
|
163
|
+
|
|
164
|
+
const baseReport: JsonReport = {
|
|
165
|
+
metadata: generateMetadata(result, options),
|
|
166
|
+
summary,
|
|
167
|
+
issues: filteredIssues.map(issue => formatIssueForJson(issue, result.workspacePath, options)),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const report: JsonReport =
|
|
171
|
+
options.schemaUrl === undefined ? baseReport : {$schema: options.schemaUrl, ...baseReport}
|
|
172
|
+
|
|
173
|
+
if (options.includeGroupedIssues ?? options.groupBy !== 'none') {
|
|
174
|
+
const grouped = groupIssues(filteredIssues, options.groupBy)
|
|
175
|
+
return {
|
|
176
|
+
...report,
|
|
177
|
+
groupedIssues: grouped.map(group => ({
|
|
178
|
+
key: group.key,
|
|
179
|
+
label: group.label,
|
|
180
|
+
count: group.count,
|
|
181
|
+
issues: group.issues.map(issue => formatIssueForJson(issue, result.workspacePath, options)),
|
|
182
|
+
})),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return report
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Generate report metadata.
|
|
191
|
+
*/
|
|
192
|
+
function generateMetadata(
|
|
193
|
+
result: AnalysisResult,
|
|
194
|
+
options: JsonReporterOptions,
|
|
195
|
+
): JsonReportMetadata {
|
|
196
|
+
const {schemaUrl, prettyPrint, indentation, includeGroupedIssues, ...reportOptions} = options
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
generatedAt: new Date().toISOString(),
|
|
200
|
+
analysisStartedAt: result.startedAt.toISOString(),
|
|
201
|
+
analysisCompletedAt: result.completedAt.toISOString(),
|
|
202
|
+
workspacePath: result.workspacePath,
|
|
203
|
+
reportOptions,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Format a single issue for JSON output.
|
|
209
|
+
*
|
|
210
|
+
* Accumulates optional properties (relatedLocations, suggestion, metadata)
|
|
211
|
+
* based on issue data and report options.
|
|
212
|
+
*/
|
|
213
|
+
function formatIssueForJson(
|
|
214
|
+
issue: Issue,
|
|
215
|
+
workspacePath: string,
|
|
216
|
+
options: JsonReporterOptions,
|
|
217
|
+
): JsonReportIssue {
|
|
218
|
+
let jsonIssue: JsonReportIssue = {
|
|
219
|
+
id: issue.id,
|
|
220
|
+
title: issue.title,
|
|
221
|
+
description: issue.description,
|
|
222
|
+
severity: issue.severity,
|
|
223
|
+
category: issue.category,
|
|
224
|
+
location: formatLocationForJson(issue.location, workspacePath, options),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (issue.relatedLocations !== undefined && issue.relatedLocations.length > 0) {
|
|
228
|
+
jsonIssue = {
|
|
229
|
+
...jsonIssue,
|
|
230
|
+
relatedLocations: issue.relatedLocations.map(loc =>
|
|
231
|
+
formatLocationForJson(loc, workspacePath, options),
|
|
232
|
+
),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (options.includeSuggestions === true && issue.suggestion !== undefined) {
|
|
237
|
+
jsonIssue = {...jsonIssue, suggestion: issue.suggestion}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (options.includeMetadata === true && issue.metadata !== undefined) {
|
|
241
|
+
jsonIssue = {...jsonIssue, metadata: issue.metadata}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return jsonIssue
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Format a location for JSON output.
|
|
249
|
+
*/
|
|
250
|
+
function formatLocationForJson(
|
|
251
|
+
location: Issue['location'],
|
|
252
|
+
workspacePath: string,
|
|
253
|
+
options: JsonReporterOptions,
|
|
254
|
+
): JsonReportLocation {
|
|
255
|
+
const baseLocation: JsonReportLocation = {
|
|
256
|
+
file: getRelativePath(location.filePath, workspacePath),
|
|
257
|
+
absolutePath: location.filePath,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!options.includeLocation) {
|
|
261
|
+
return baseLocation
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const result: JsonReportLocation = {
|
|
265
|
+
...baseLocation,
|
|
266
|
+
...(location.line === undefined ? {} : {line: location.line}),
|
|
267
|
+
...(location.column === undefined ? {} : {column: location.column}),
|
|
268
|
+
...(location.endLine === undefined ? {} : {endLine: location.endLine}),
|
|
269
|
+
...(location.endColumn === undefined ? {} : {endColumn: location.endColumn}),
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return result
|
|
273
|
+
}
|