diff --git a/packages/guardian/CHANGELOG.md b/packages/guardian/CHANGELOG.md index bc7b796..a670d4c 100644 --- a/packages/guardian/CHANGELOG.md +++ b/packages/guardian/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to @samiyev/guardian will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.6] - 2025-11-25 + +### Changed + +- ā™»ļø **Refactored CLI module** - improved maintainability and separation of concerns: + - Split 484-line `cli/index.ts` into focused modules + - Created `cli/groupers/ViolationGrouper.ts` for severity grouping and filtering (29 lines) + - Created `cli/formatters/OutputFormatter.ts` for violation formatting (190 lines) + - Created `cli/formatters/StatisticsFormatter.ts` for metrics and summary (58 lines) + - Reduced `cli/index.ts` from 484 to 260 lines (46% reduction) + - All 345 tests pass, CLI output identical to before + - No breaking changes + ## [0.7.5] - 2025-11-25 ### Changed diff --git a/packages/guardian/ROADMAP.md b/packages/guardian/ROADMAP.md index a315845..7fe80e2 100644 --- a/packages/guardian/ROADMAP.md +++ b/packages/guardian/ROADMAP.md @@ -333,32 +333,34 @@ application/use-cases/ --- -### Version 0.7.6 - Refactor CLI Module šŸ”§ +### Version 0.7.6 - Refactor CLI Module šŸ”§ āœ… RELEASED +**Released:** 2025-11-25 **Priority:** MEDIUM **Scope:** Single session (~128K tokens) -Split `cli/index.ts` (470 lines) into focused formatters. +Split `cli/index.ts` (484 lines) into focused formatters. **Problem:** -- CLI file has 470 lines +- CLI file has 484 lines - Mixing: command setup, formatting, grouping, statistics **Solution:** ``` cli/ -ā”œā”€ā”€ index.ts # Commands only (~100 lines) +ā”œā”€ā”€ index.ts # Commands only (260 lines) ā”œā”€ā”€ formatters/ -│ ā”œā”€ā”€ OutputFormatter.ts # Violation formatting -│ └── StatisticsFormatter.ts +│ ā”œā”€ā”€ OutputFormatter.ts # Violation formatting (190 lines) +│ └── StatisticsFormatter.ts # Metrics & summary (58 lines) ā”œā”€ā”€ groupers/ -│ └── ViolationGrouper.ts # Sorting & grouping +│ └── ViolationGrouper.ts # Sorting & grouping (29 lines) ``` **Deliverables:** -- [ ] Extract formatters and groupers -- [ ] Reduce `cli/index.ts` to ~100-150 lines -- [ ] CLI output identical to before +- āœ… Extract formatters and groupers +- āœ… Reduce `cli/index.ts` from 484 to 260 lines (46% reduction) +- āœ… CLI output identical to before +- āœ… All 345 tests pass, no breaking changes - [ ] Publish to npm --- diff --git a/packages/guardian/package.json b/packages/guardian/package.json index 988b431..6f2b4f8 100644 --- a/packages/guardian/package.json +++ b/packages/guardian/package.json @@ -1,6 +1,6 @@ { "name": "@samiyev/guardian", - "version": "0.7.5", + "version": "0.7.6", "description": "Research-backed code quality guardian for AI-assisted development. Detects hardcodes, circular deps, framework leaks, entity exposure, and 8 architecture violations. Enforces Clean Architecture/DDD principles. Works with GitHub Copilot, Cursor, Windsurf, Claude, ChatGPT, Cline, and any AI coding tool.", "keywords": [ "puaros", diff --git a/packages/guardian/src/cli/formatters/OutputFormatter.ts b/packages/guardian/src/cli/formatters/OutputFormatter.ts new file mode 100644 index 0000000..1a193d2 --- /dev/null +++ b/packages/guardian/src/cli/formatters/OutputFormatter.ts @@ -0,0 +1,190 @@ +import { SEVERITY_LEVELS, type SeverityLevel } from "../../shared/constants" +import type { + AggregateBoundaryViolation, + ArchitectureViolation, + CircularDependencyViolation, + DependencyDirectionViolation, + EntityExposureViolation, + FrameworkLeakViolation, + HardcodeViolation, + NamingConventionViolation, + RepositoryPatternViolation, +} from "../../application/use-cases/AnalyzeProject" +import { SEVERITY_DISPLAY_LABELS, SEVERITY_SECTION_HEADERS } from "../constants" +import { ViolationGrouper } from "../groupers/ViolationGrouper" + +const SEVERITY_LABELS: Record = { + [SEVERITY_LEVELS.CRITICAL]: SEVERITY_DISPLAY_LABELS.CRITICAL, + [SEVERITY_LEVELS.HIGH]: SEVERITY_DISPLAY_LABELS.HIGH, + [SEVERITY_LEVELS.MEDIUM]: SEVERITY_DISPLAY_LABELS.MEDIUM, + [SEVERITY_LEVELS.LOW]: SEVERITY_DISPLAY_LABELS.LOW, +} + +const SEVERITY_HEADER: Record = { + [SEVERITY_LEVELS.CRITICAL]: SEVERITY_SECTION_HEADERS.CRITICAL, + [SEVERITY_LEVELS.HIGH]: SEVERITY_SECTION_HEADERS.HIGH, + [SEVERITY_LEVELS.MEDIUM]: SEVERITY_SECTION_HEADERS.MEDIUM, + [SEVERITY_LEVELS.LOW]: SEVERITY_SECTION_HEADERS.LOW, +} + +export class OutputFormatter { + private readonly grouper = new ViolationGrouper() + + displayGroupedViolations( + violations: T[], + displayFn: (v: T, index: number) => void, + limit?: number, + ): void { + const grouped = this.grouper.groupBySeverity(violations) + const severities: SeverityLevel[] = [ + SEVERITY_LEVELS.CRITICAL, + SEVERITY_LEVELS.HIGH, + SEVERITY_LEVELS.MEDIUM, + SEVERITY_LEVELS.LOW, + ] + + let totalDisplayed = 0 + const totalAvailable = violations.length + + for (const severity of severities) { + const items = grouped.get(severity) + if (items && items.length > 0) { + console.warn(SEVERITY_HEADER[severity]) + console.warn(`Found ${String(items.length)} issue(s)\n`) + + const itemsToDisplay = + limit !== undefined ? items.slice(0, limit - totalDisplayed) : items + itemsToDisplay.forEach((item, index) => { + displayFn(item, totalDisplayed + index) + }) + totalDisplayed += itemsToDisplay.length + + if (limit !== undefined && totalDisplayed >= limit) { + break + } + } + } + + if (limit !== undefined && totalAvailable > limit) { + console.warn( + `\nāš ļø Showing first ${String(limit)} of ${String(totalAvailable)} issues (use --limit to adjust)\n`, + ) + } + } + + formatArchitectureViolation(v: ArchitectureViolation, index: number): void { + console.log(`${String(index + 1)}. ${v.file}`) + console.log(` Severity: ${SEVERITY_LABELS[v.severity]}`) + console.log(` Rule: ${v.rule}`) + console.log(` ${v.message}`) + console.log("") + } + + formatCircularDependency(cd: CircularDependencyViolation, index: number): void { + console.log(`${String(index + 1)}. ${cd.message}`) + console.log(` Severity: ${SEVERITY_LABELS[cd.severity]}`) + console.log(" Cycle path:") + cd.cycle.forEach((file, i) => { + console.log(` ${String(i + 1)}. ${file}`) + }) + console.log(` ${String(cd.cycle.length + 1)}. ${cd.cycle[0]} (back to start)`) + console.log("") + } + + formatNamingViolation(nc: NamingConventionViolation, index: number): void { + console.log(`${String(index + 1)}. ${nc.file}`) + console.log(` Severity: ${SEVERITY_LABELS[nc.severity]}`) + console.log(` File: ${nc.fileName}`) + console.log(` Layer: ${nc.layer}`) + console.log(` Type: ${nc.type}`) + console.log(` Message: ${nc.message}`) + if (nc.suggestion) { + console.log(` šŸ’” Suggestion: ${nc.suggestion}`) + } + console.log("") + } + + formatFrameworkLeak(fl: FrameworkLeakViolation, index: number): void { + console.log(`${String(index + 1)}. ${fl.file}`) + console.log(` Severity: ${SEVERITY_LABELS[fl.severity]}`) + console.log(` Package: ${fl.packageName}`) + console.log(` Category: ${fl.categoryDescription}`) + console.log(` Layer: ${fl.layer}`) + console.log(` Rule: ${fl.rule}`) + console.log(` ${fl.message}`) + console.log(` šŸ’” Suggestion: ${fl.suggestion}`) + console.log("") + } + + formatEntityExposure(ee: EntityExposureViolation, index: number): void { + const location = ee.line ? `${ee.file}:${String(ee.line)}` : ee.file + console.log(`${String(index + 1)}. ${location}`) + console.log(` Severity: ${SEVERITY_LABELS[ee.severity]}`) + console.log(` Entity: ${ee.entityName}`) + console.log(` Return Type: ${ee.returnType}`) + if (ee.methodName) { + console.log(` Method: ${ee.methodName}`) + } + console.log(` Layer: ${ee.layer}`) + console.log(` Rule: ${ee.rule}`) + console.log(` ${ee.message}`) + console.log(" šŸ’” Suggestion:") + ee.suggestion.split("\n").forEach((line) => { + if (line.trim()) { + console.log(` ${line}`) + } + }) + console.log("") + } + + formatDependencyDirection(dd: DependencyDirectionViolation, index: number): void { + console.log(`${String(index + 1)}. ${dd.file}`) + console.log(` Severity: ${SEVERITY_LABELS[dd.severity]}`) + console.log(` From Layer: ${dd.fromLayer}`) + console.log(` To Layer: ${dd.toLayer}`) + console.log(` Import: ${dd.importPath}`) + console.log(` ${dd.message}`) + console.log(` šŸ’” Suggestion: ${dd.suggestion}`) + console.log("") + } + + formatRepositoryPattern(rp: RepositoryPatternViolation, index: number): void { + console.log(`${String(index + 1)}. ${rp.file}`) + console.log(` Severity: ${SEVERITY_LABELS[rp.severity]}`) + console.log(` Layer: ${rp.layer}`) + console.log(` Type: ${rp.violationType}`) + console.log(` Details: ${rp.details}`) + console.log(` ${rp.message}`) + console.log(` šŸ’” Suggestion: ${rp.suggestion}`) + console.log("") + } + + formatAggregateBoundary(ab: AggregateBoundaryViolation, index: number): void { + const location = ab.line ? `${ab.file}:${String(ab.line)}` : ab.file + console.log(`${String(index + 1)}. ${location}`) + console.log(` Severity: ${SEVERITY_LABELS[ab.severity]}`) + console.log(` From Aggregate: ${ab.fromAggregate}`) + console.log(` To Aggregate: ${ab.toAggregate}`) + console.log(` Entity: ${ab.entityName}`) + console.log(` Import: ${ab.importPath}`) + console.log(` ${ab.message}`) + console.log(" šŸ’” Suggestion:") + ab.suggestion.split("\n").forEach((line) => { + if (line.trim()) { + console.log(` ${line}`) + } + }) + console.log("") + } + + formatHardcodeViolation(hc: HardcodeViolation, index: number): void { + console.log(`${String(index + 1)}. ${hc.file}:${String(hc.line)}:${String(hc.column)}`) + console.log(` Severity: ${SEVERITY_LABELS[hc.severity]}`) + console.log(` Type: ${hc.type}`) + console.log(` Value: ${JSON.stringify(hc.value)}`) + console.log(` Context: ${hc.context.trim()}`) + console.log(` šŸ’” Suggested: ${hc.suggestion.constantName}`) + console.log(` šŸ“ Location: ${hc.suggestion.location}`) + console.log("") + } +} diff --git a/packages/guardian/src/cli/formatters/StatisticsFormatter.ts b/packages/guardian/src/cli/formatters/StatisticsFormatter.ts new file mode 100644 index 0000000..c91ca8e --- /dev/null +++ b/packages/guardian/src/cli/formatters/StatisticsFormatter.ts @@ -0,0 +1,59 @@ +import { CLI_LABELS, CLI_MESSAGES } from "../constants" + +interface ProjectMetrics { + totalFiles: number + totalFunctions: number + totalImports: number + layerDistribution: Record +} + +export class StatisticsFormatter { + displayMetrics(metrics: ProjectMetrics): void { + console.log(CLI_MESSAGES.METRICS_HEADER) + console.log(` ${CLI_LABELS.FILES_ANALYZED} ${String(metrics.totalFiles)}`) + console.log(` ${CLI_LABELS.TOTAL_FUNCTIONS} ${String(metrics.totalFunctions)}`) + console.log(` ${CLI_LABELS.TOTAL_IMPORTS} ${String(metrics.totalImports)}`) + + if (Object.keys(metrics.layerDistribution).length > 0) { + console.log(CLI_MESSAGES.LAYER_DISTRIBUTION_HEADER) + for (const [layer, count] of Object.entries(metrics.layerDistribution)) { + console.log(` ${layer}: ${String(count)} ${CLI_LABELS.FILES}`) + } + } + } + + displaySummary(totalIssues: number, verbose: boolean): void { + if (totalIssues === 0) { + console.log(CLI_MESSAGES.NO_ISSUES) + process.exit(0) + } else { + console.log( + `${CLI_MESSAGES.ISSUES_TOTAL} ${String(totalIssues)} ${CLI_LABELS.ISSUES_TOTAL}`, + ) + console.log(CLI_MESSAGES.TIP) + + if (verbose) { + console.log(CLI_MESSAGES.HELP_FOOTER) + } + + process.exit(1) + } + } + + displaySeverityFilterMessage(onlyCritical: boolean, minSeverity?: string): void { + if (onlyCritical) { + console.log("\nšŸ”“ Filtering: Showing only CRITICAL severity issues\n") + } else if (minSeverity) { + console.log( + `\nāš ļø Filtering: Showing ${minSeverity.toUpperCase()} severity and above\n`, + ) + } + } + + displayError(message: string): void { + console.error(`\nāŒ ${CLI_MESSAGES.ERROR_PREFIX}`) + console.error(message) + console.error("") + process.exit(1) + } +} diff --git a/packages/guardian/src/cli/groupers/ViolationGrouper.ts b/packages/guardian/src/cli/groupers/ViolationGrouper.ts new file mode 100644 index 0000000..342684e --- /dev/null +++ b/packages/guardian/src/cli/groupers/ViolationGrouper.ts @@ -0,0 +1,29 @@ +import { SEVERITY_ORDER, type SeverityLevel } from "../../shared/constants" + +export class ViolationGrouper { + groupBySeverity( + violations: T[], + ): Map { + const grouped = new Map() + + for (const violation of violations) { + const existing = grouped.get(violation.severity) ?? [] + existing.push(violation) + grouped.set(violation.severity, existing) + } + + return grouped + } + + filterBySeverity( + violations: T[], + minSeverity?: SeverityLevel, + ): T[] { + if (!minSeverity) { + return violations + } + + const minSeverityOrder = SEVERITY_ORDER[minSeverity] + return violations.filter((v) => SEVERITY_ORDER[v.severity] <= minSeverityOrder) + } +} diff --git a/packages/guardian/src/cli/index.ts b/packages/guardian/src/cli/index.ts index 816ec32..f9d35e2 100644 --- a/packages/guardian/src/cli/index.ts +++ b/packages/guardian/src/cli/index.ts @@ -11,92 +11,11 @@ import { CLI_MESSAGES, CLI_OPTIONS, DEFAULT_EXCLUDES, - SEVERITY_DISPLAY_LABELS, - SEVERITY_SECTION_HEADERS, } from "./constants" -import { SEVERITY_LEVELS, SEVERITY_ORDER, type SeverityLevel } from "../shared/constants" - -const SEVERITY_LABELS: Record = { - [SEVERITY_LEVELS.CRITICAL]: SEVERITY_DISPLAY_LABELS.CRITICAL, - [SEVERITY_LEVELS.HIGH]: SEVERITY_DISPLAY_LABELS.HIGH, - [SEVERITY_LEVELS.MEDIUM]: SEVERITY_DISPLAY_LABELS.MEDIUM, - [SEVERITY_LEVELS.LOW]: SEVERITY_DISPLAY_LABELS.LOW, -} - -const SEVERITY_HEADER: Record = { - [SEVERITY_LEVELS.CRITICAL]: SEVERITY_SECTION_HEADERS.CRITICAL, - [SEVERITY_LEVELS.HIGH]: SEVERITY_SECTION_HEADERS.HIGH, - [SEVERITY_LEVELS.MEDIUM]: SEVERITY_SECTION_HEADERS.MEDIUM, - [SEVERITY_LEVELS.LOW]: SEVERITY_SECTION_HEADERS.LOW, -} - -function groupBySeverity( - violations: T[], -): Map { - const grouped = new Map() - - for (const violation of violations) { - const existing = grouped.get(violation.severity) ?? [] - existing.push(violation) - grouped.set(violation.severity, existing) - } - - return grouped -} - -function filterBySeverity( - violations: T[], - minSeverity?: SeverityLevel, -): T[] { - if (!minSeverity) { - return violations - } - - const minSeverityOrder = SEVERITY_ORDER[minSeverity] - return violations.filter((v) => SEVERITY_ORDER[v.severity] <= minSeverityOrder) -} - -function displayGroupedViolations( - violations: T[], - displayFn: (v: T, index: number) => void, - limit?: number, -): void { - const grouped = groupBySeverity(violations) - const severities: SeverityLevel[] = [ - SEVERITY_LEVELS.CRITICAL, - SEVERITY_LEVELS.HIGH, - SEVERITY_LEVELS.MEDIUM, - SEVERITY_LEVELS.LOW, - ] - - let totalDisplayed = 0 - const totalAvailable = violations.length - - for (const severity of severities) { - const items = grouped.get(severity) - if (items && items.length > 0) { - console.warn(SEVERITY_HEADER[severity]) - console.warn(`Found ${String(items.length)} issue(s)\n`) - - const itemsToDisplay = - limit !== undefined ? items.slice(0, limit - totalDisplayed) : items - itemsToDisplay.forEach((item, index) => { - displayFn(item, totalDisplayed + index) - }) - totalDisplayed += itemsToDisplay.length - - if (limit !== undefined && totalDisplayed >= limit) { - break - } - } - } - - if (limit !== undefined && totalAvailable > limit) { - console.warn( - `\nāš ļø Showing first ${String(limit)} of ${String(totalAvailable)} issues (use --limit to adjust)\n`, - ) - } -} +import { SEVERITY_LEVELS, type SeverityLevel } from "../shared/constants" +import { ViolationGrouper } from "./groupers/ViolationGrouper" +import { OutputFormatter } from "./formatters/OutputFormatter" +import { StatisticsFormatter } from "./formatters/StatisticsFormatter" const program = new Command() @@ -150,6 +69,10 @@ program .option(CLI_OPTIONS.ONLY_CRITICAL, CLI_DESCRIPTIONS.ONLY_CRITICAL_OPTION, false) .option(CLI_OPTIONS.LIMIT, CLI_DESCRIPTIONS.LIMIT_OPTION) .action(async (path: string, options) => { + const grouper = new ViolationGrouper() + const outputFormatter = new OutputFormatter() + const statsFormatter = new StatisticsFormatter() + try { console.log(CLI_MESSAGES.ANALYZING) @@ -182,270 +105,159 @@ program : undefined if (minSeverity) { - violations = filterBySeverity(violations, minSeverity) - hardcodeViolations = filterBySeverity(hardcodeViolations, minSeverity) - circularDependencyViolations = filterBySeverity( + violations = grouper.filterBySeverity(violations, minSeverity) + hardcodeViolations = grouper.filterBySeverity(hardcodeViolations, minSeverity) + circularDependencyViolations = grouper.filterBySeverity( circularDependencyViolations, minSeverity, ) - namingViolations = filterBySeverity(namingViolations, minSeverity) - frameworkLeakViolations = filterBySeverity(frameworkLeakViolations, minSeverity) - entityExposureViolations = filterBySeverity(entityExposureViolations, minSeverity) - dependencyDirectionViolations = filterBySeverity( + namingViolations = grouper.filterBySeverity(namingViolations, minSeverity) + frameworkLeakViolations = grouper.filterBySeverity( + frameworkLeakViolations, + minSeverity, + ) + entityExposureViolations = grouper.filterBySeverity( + entityExposureViolations, + minSeverity, + ) + dependencyDirectionViolations = grouper.filterBySeverity( dependencyDirectionViolations, minSeverity, ) - repositoryPatternViolations = filterBySeverity( + repositoryPatternViolations = grouper.filterBySeverity( repositoryPatternViolations, minSeverity, ) - aggregateBoundaryViolations = filterBySeverity( + aggregateBoundaryViolations = grouper.filterBySeverity( aggregateBoundaryViolations, minSeverity, ) - if (options.onlyCritical) { - console.log("\nšŸ”“ Filtering: Showing only CRITICAL severity issues\n") - } else { - console.log( - `\nāš ļø Filtering: Showing ${minSeverity.toUpperCase()} severity and above\n`, - ) - } + statsFormatter.displaySeverityFilterMessage( + options.onlyCritical, + options.minSeverity, + ) } - // Display metrics - console.log(CLI_MESSAGES.METRICS_HEADER) - console.log(` ${CLI_LABELS.FILES_ANALYZED} ${String(metrics.totalFiles)}`) - console.log(` ${CLI_LABELS.TOTAL_FUNCTIONS} ${String(metrics.totalFunctions)}`) - console.log(` ${CLI_LABELS.TOTAL_IMPORTS} ${String(metrics.totalImports)}`) + statsFormatter.displayMetrics(metrics) - if (Object.keys(metrics.layerDistribution).length > 0) { - console.log(CLI_MESSAGES.LAYER_DISTRIBUTION_HEADER) - for (const [layer, count] of Object.entries(metrics.layerDistribution)) { - console.log(` ${layer}: ${String(count)} ${CLI_LABELS.FILES}`) - } - } - - // Architecture violations if (options.architecture && violations.length > 0) { console.log( `\n${CLI_MESSAGES.VIOLATIONS_HEADER} ${String(violations.length)} ${CLI_LABELS.ARCHITECTURE_VIOLATIONS}`, ) - - displayGroupedViolations( + outputFormatter.displayGroupedViolations( violations, - (v, index) => { - console.log(`${String(index + 1)}. ${v.file}`) - console.log(` Severity: ${SEVERITY_LABELS[v.severity]}`) - console.log(` Rule: ${v.rule}`) - console.log(` ${v.message}`) - console.log("") + (v, i) => { + outputFormatter.formatArchitectureViolation(v, i) }, limit, ) } - // Circular dependency violations if (options.architecture && circularDependencyViolations.length > 0) { console.log( `\n${CLI_MESSAGES.CIRCULAR_DEPS_HEADER} ${String(circularDependencyViolations.length)} ${CLI_LABELS.CIRCULAR_DEPENDENCIES}`, ) - - displayGroupedViolations( + outputFormatter.displayGroupedViolations( circularDependencyViolations, - (cd, index) => { - console.log(`${String(index + 1)}. ${cd.message}`) - console.log(` Severity: ${SEVERITY_LABELS[cd.severity]}`) - console.log(" Cycle path:") - cd.cycle.forEach((file, i) => { - console.log(` ${String(i + 1)}. ${file}`) - }) - console.log( - ` ${String(cd.cycle.length + 1)}. ${cd.cycle[0]} (back to start)`, - ) - console.log("") + (cd, i) => { + outputFormatter.formatCircularDependency(cd, i) }, limit, ) } - // Naming convention violations if (options.architecture && namingViolations.length > 0) { console.log( `\n${CLI_MESSAGES.NAMING_VIOLATIONS_HEADER} ${String(namingViolations.length)} ${CLI_LABELS.NAMING_VIOLATIONS}`, ) - - displayGroupedViolations( + outputFormatter.displayGroupedViolations( namingViolations, - (nc, index) => { - console.log(`${String(index + 1)}. ${nc.file}`) - console.log(` Severity: ${SEVERITY_LABELS[nc.severity]}`) - console.log(` File: ${nc.fileName}`) - console.log(` Layer: ${nc.layer}`) - console.log(` Type: ${nc.type}`) - console.log(` Message: ${nc.message}`) - if (nc.suggestion) { - console.log(` šŸ’” Suggestion: ${nc.suggestion}`) - } - console.log("") + (nc, i) => { + outputFormatter.formatNamingViolation(nc, i) }, limit, ) } - // Framework leak violations if (options.architecture && frameworkLeakViolations.length > 0) { console.log( `\nšŸ—ļø Found ${String(frameworkLeakViolations.length)} framework leak(s)`, ) - - displayGroupedViolations( + outputFormatter.displayGroupedViolations( frameworkLeakViolations, - (fl, index) => { - console.log(`${String(index + 1)}. ${fl.file}`) - console.log(` Severity: ${SEVERITY_LABELS[fl.severity]}`) - console.log(` Package: ${fl.packageName}`) - console.log(` Category: ${fl.categoryDescription}`) - console.log(` Layer: ${fl.layer}`) - console.log(` Rule: ${fl.rule}`) - console.log(` ${fl.message}`) - console.log(` šŸ’” Suggestion: ${fl.suggestion}`) - console.log("") + (fl, i) => { + outputFormatter.formatFrameworkLeak(fl, i) }, limit, ) } - // Entity exposure violations if (options.architecture && entityExposureViolations.length > 0) { console.log( `\nšŸŽ­ Found ${String(entityExposureViolations.length)} entity exposure(s)`, ) - - displayGroupedViolations( + outputFormatter.displayGroupedViolations( entityExposureViolations, - (ee, index) => { - const location = ee.line ? `${ee.file}:${String(ee.line)}` : ee.file - console.log(`${String(index + 1)}. ${location}`) - console.log(` Severity: ${SEVERITY_LABELS[ee.severity]}`) - console.log(` Entity: ${ee.entityName}`) - console.log(` Return Type: ${ee.returnType}`) - if (ee.methodName) { - console.log(` Method: ${ee.methodName}`) - } - console.log(` Layer: ${ee.layer}`) - console.log(` Rule: ${ee.rule}`) - console.log(` ${ee.message}`) - console.log(" šŸ’” Suggestion:") - ee.suggestion.split("\n").forEach((line) => { - if (line.trim()) { - console.log(` ${line}`) - } - }) - console.log("") + (ee, i) => { + outputFormatter.formatEntityExposure(ee, i) }, limit, ) } - // Dependency direction violations if (options.architecture && dependencyDirectionViolations.length > 0) { console.log( `\nāš ļø Found ${String(dependencyDirectionViolations.length)} dependency direction violation(s)`, ) - - displayGroupedViolations( + outputFormatter.displayGroupedViolations( dependencyDirectionViolations, - (dd, index) => { - console.log(`${String(index + 1)}. ${dd.file}`) - console.log(` Severity: ${SEVERITY_LABELS[dd.severity]}`) - console.log(` From Layer: ${dd.fromLayer}`) - console.log(` To Layer: ${dd.toLayer}`) - console.log(` Import: ${dd.importPath}`) - console.log(` ${dd.message}`) - console.log(` šŸ’” Suggestion: ${dd.suggestion}`) - console.log("") + (dd, i) => { + outputFormatter.formatDependencyDirection(dd, i) }, limit, ) } - // Repository pattern violations if (options.architecture && repositoryPatternViolations.length > 0) { console.log( `\nšŸ“¦ Found ${String(repositoryPatternViolations.length)} repository pattern violation(s)`, ) - - displayGroupedViolations( + outputFormatter.displayGroupedViolations( repositoryPatternViolations, - (rp, index) => { - console.log(`${String(index + 1)}. ${rp.file}`) - console.log(` Severity: ${SEVERITY_LABELS[rp.severity]}`) - console.log(` Layer: ${rp.layer}`) - console.log(` Type: ${rp.violationType}`) - console.log(` Details: ${rp.details}`) - console.log(` ${rp.message}`) - console.log(` šŸ’” Suggestion: ${rp.suggestion}`) - console.log("") + (rp, i) => { + outputFormatter.formatRepositoryPattern(rp, i) }, limit, ) } - // Aggregate boundary violations if (options.architecture && aggregateBoundaryViolations.length > 0) { console.log( `\nšŸ”’ Found ${String(aggregateBoundaryViolations.length)} aggregate boundary violation(s)`, ) - - displayGroupedViolations( + outputFormatter.displayGroupedViolations( aggregateBoundaryViolations, - (ab, index) => { - const location = ab.line ? `${ab.file}:${String(ab.line)}` : ab.file - console.log(`${String(index + 1)}. ${location}`) - console.log(` Severity: ${SEVERITY_LABELS[ab.severity]}`) - console.log(` From Aggregate: ${ab.fromAggregate}`) - console.log(` To Aggregate: ${ab.toAggregate}`) - console.log(` Entity: ${ab.entityName}`) - console.log(` Import: ${ab.importPath}`) - console.log(` ${ab.message}`) - console.log(" šŸ’” Suggestion:") - ab.suggestion.split("\n").forEach((line) => { - if (line.trim()) { - console.log(` ${line}`) - } - }) - console.log("") + (ab, i) => { + outputFormatter.formatAggregateBoundary(ab, i) }, limit, ) } - // Hardcode violations if (options.hardcode && hardcodeViolations.length > 0) { console.log( `\n${CLI_MESSAGES.HARDCODE_VIOLATIONS_HEADER} ${String(hardcodeViolations.length)} ${CLI_LABELS.HARDCODE_VIOLATIONS}`, ) - - displayGroupedViolations( + outputFormatter.displayGroupedViolations( hardcodeViolations, - (hc, index) => { - console.log( - `${String(index + 1)}. ${hc.file}:${String(hc.line)}:${String(hc.column)}`, - ) - console.log(` Severity: ${SEVERITY_LABELS[hc.severity]}`) - console.log(` Type: ${hc.type}`) - console.log(` Value: ${JSON.stringify(hc.value)}`) - console.log(` Context: ${hc.context.trim()}`) - console.log(` šŸ’” Suggested: ${hc.suggestion.constantName}`) - console.log(` šŸ“ Location: ${hc.suggestion.location}`) - console.log("") + (hc, i) => { + outputFormatter.formatHardcodeViolation(hc, i) }, limit, ) } - // Summary const totalIssues = violations.length + hardcodeViolations.length + @@ -457,26 +269,9 @@ program repositoryPatternViolations.length + aggregateBoundaryViolations.length - if (totalIssues === 0) { - console.log(CLI_MESSAGES.NO_ISSUES) - process.exit(0) - } else { - console.log( - `${CLI_MESSAGES.ISSUES_TOTAL} ${String(totalIssues)} ${CLI_LABELS.ISSUES_TOTAL}`, - ) - console.log(CLI_MESSAGES.TIP) - - if (options.verbose) { - console.log(CLI_MESSAGES.HELP_FOOTER) - } - - process.exit(1) - } + statsFormatter.displaySummary(totalIssues, options.verbose) } catch (error) { - console.error(`\nāŒ ${CLI_MESSAGES.ERROR_PREFIX}`) - console.error(error instanceof Error ? error.message : String(error)) - console.error("") - process.exit(1) + statsFormatter.displayError(error instanceof Error ? error.message : String(error)) } })