mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88876a258b | ||
|
|
a34ca85241 |
21
CHANGELOG.md
21
CHANGELOG.md
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.4.0] - 2025-11-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Dependency direction enforcement - validate that dependencies flow in the correct direction according to Clean Architecture principles
|
||||||
|
- Architecture layer violation detection for domain, application, and infrastructure layers
|
||||||
|
|
||||||
|
## [0.3.0] - 2025-11-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Entity exposure detection - identify when domain entities are exposed outside their module boundaries
|
||||||
|
- Enhanced architecture violation reporting
|
||||||
|
|
||||||
|
## [0.2.0] - 2025-11-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Framework leak detection - detect when domain layer imports framework code
|
||||||
|
- Framework leak reporting in CLI
|
||||||
|
- Framework leak examples and documentation
|
||||||
|
|
||||||
|
## [0.1.0] - 2025-11-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Initial monorepo setup with pnpm workspaces
|
- Initial monorepo setup with pnpm workspaces
|
||||||
- `@puaros/guardian` package - code quality guardian for vibe coders and enterprise teams
|
- `@puaros/guardian` package - code quality guardian for vibe coders and enterprise teams
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default tseslint.config(
|
|||||||
// ========================================
|
// ========================================
|
||||||
// Code Style (handled by Prettier mostly)
|
// Code Style (handled by Prettier mostly)
|
||||||
// ========================================
|
// ========================================
|
||||||
indent: ['error', 4, { SwitchCase: 1 }],
|
indent: 'off', // Let Prettier handle this
|
||||||
'@typescript-eslint/indent': 'off', // Let Prettier handle this
|
'@typescript-eslint/indent': 'off', // Let Prettier handle this
|
||||||
quotes: ['error', 'double', { avoidEscape: true }],
|
quotes: ['error', 'double', { avoidEscape: true }],
|
||||||
semi: ['error', 'never'],
|
semi: ['error', 'never'],
|
||||||
|
|||||||
@@ -5,6 +5,232 @@ 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/),
|
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).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.5.2] - 2025-11-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
**🎯 Severity-Based Prioritization**
|
||||||
|
|
||||||
|
Guardian now intelligently prioritizes violations by severity, helping teams focus on critical issues first!
|
||||||
|
|
||||||
|
- ✅ **Severity Levels**
|
||||||
|
- 🔴 **CRITICAL**: Circular dependencies, Repository pattern violations
|
||||||
|
- 🟠 **HIGH**: Dependency direction violations, Framework leaks, Entity exposures
|
||||||
|
- 🟡 **MEDIUM**: Naming violations, Architecture violations
|
||||||
|
- 🟢 **LOW**: Hardcoded values
|
||||||
|
|
||||||
|
- ✅ **Automatic Sorting**
|
||||||
|
- All violations automatically sorted by severity (most critical first)
|
||||||
|
- Applied in AnalyzeProject use case before returning results
|
||||||
|
- Consistent ordering across all detection types
|
||||||
|
|
||||||
|
- ✅ **CLI Filtering Options**
|
||||||
|
- `--min-severity <level>` - Show only violations at specified level and above
|
||||||
|
- `--only-critical` - Quick filter for critical issues only
|
||||||
|
- Examples:
|
||||||
|
- `guardian check src --only-critical`
|
||||||
|
- `guardian check src --min-severity high`
|
||||||
|
|
||||||
|
- ✅ **Enhanced CLI Output**
|
||||||
|
- Color-coded severity labels (🔴🟠🟡🟢)
|
||||||
|
- Visual severity group headers with separators
|
||||||
|
- Severity displayed for each violation
|
||||||
|
- Clear filtering messages when filters active
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated all violation interfaces to include `severity: SeverityLevel` field
|
||||||
|
- Improved CLI presentation with grouped severity display
|
||||||
|
- Enhanced developer experience with visual prioritization
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- All 292 tests passing (100% pass rate)
|
||||||
|
- Coverage: 90.63% statements, 82.19% branches, 83.51% functions
|
||||||
|
- No breaking changes - fully backwards compatible
|
||||||
|
- Clean Architecture principles maintained
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.5.1] - 2025-11-24
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
**🧹 Code Quality Refactoring**
|
||||||
|
|
||||||
|
Major internal refactoring to eliminate hardcoded values and improve maintainability - Guardian now fully passes its own quality checks!
|
||||||
|
|
||||||
|
- ✅ **Extracted Constants**
|
||||||
|
- All RepositoryViolation messages moved to domain constants (Messages.ts)
|
||||||
|
- All framework leak template strings centralized
|
||||||
|
- All layer paths moved to infrastructure constants (paths.ts)
|
||||||
|
- All regex patterns extracted to IMPORT_PATTERNS constant
|
||||||
|
- 30+ new constants added for better maintainability
|
||||||
|
|
||||||
|
- ✅ **New Constants Files**
|
||||||
|
- `src/infrastructure/constants/paths.ts` - Layer paths, CLI paths, import patterns
|
||||||
|
- Extended `src/domain/constants/Messages.ts` - 25+ repository pattern messages
|
||||||
|
- Extended `src/shared/constants/rules.ts` - Package placeholder constant
|
||||||
|
|
||||||
|
- ✅ **Self-Validation Achievement**
|
||||||
|
- Reduced hardcoded values from 37 to 1 (97% improvement)
|
||||||
|
- Guardian now passes its own `src/` directory checks with 0 violations
|
||||||
|
- Only acceptable hardcode remaining: bin/guardian.js entry point path
|
||||||
|
- All 292 tests still passing (100% pass rate)
|
||||||
|
|
||||||
|
- ✅ **Improved Code Organization**
|
||||||
|
- Better separation of concerns
|
||||||
|
- More maintainable codebase
|
||||||
|
- Easier to extend with new features
|
||||||
|
- Follows DRY principle throughout
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
- No breaking changes - fully backwards compatible
|
||||||
|
- All functionality preserved
|
||||||
|
- Test suite: 292 tests passing
|
||||||
|
- Coverage: 96.77% statements, 83.82% branches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.5.0] - 2025-11-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
**📚 Repository Pattern Validation**
|
||||||
|
|
||||||
|
Validate proper implementation of the Repository Pattern to ensure domain remains decoupled from infrastructure.
|
||||||
|
|
||||||
|
- ✅ **ORM Type Detection in Interfaces**
|
||||||
|
- Detects ORM-specific types (Prisma, TypeORM, Mongoose) in domain repository interfaces
|
||||||
|
- Ensures repository interfaces remain persistence-agnostic
|
||||||
|
- Supports detection of 25+ ORM type patterns
|
||||||
|
- Provides fix suggestions with clean domain examples
|
||||||
|
|
||||||
|
- ✅ **Concrete Repository Usage Detection**
|
||||||
|
- Identifies use cases depending on concrete repository implementations
|
||||||
|
- Enforces Dependency Inversion Principle
|
||||||
|
- Validates constructor and field dependency types
|
||||||
|
- Suggests using repository interfaces instead
|
||||||
|
|
||||||
|
- ✅ **Repository Instantiation Detection**
|
||||||
|
- Detects `new Repository()` in use cases
|
||||||
|
- Enforces Dependency Injection pattern
|
||||||
|
- Identifies hidden dependencies
|
||||||
|
- Provides DI container setup guidance
|
||||||
|
|
||||||
|
- ✅ **Domain Language Validation**
|
||||||
|
- Checks repository methods use domain terminology
|
||||||
|
- Rejects technical database terms (findOne, insert, query, execute)
|
||||||
|
- Promotes ubiquitous language across codebase
|
||||||
|
- Suggests business-oriented method names
|
||||||
|
|
||||||
|
- ✅ **Smart Violation Reporting**
|
||||||
|
- RepositoryViolation value object with detailed context
|
||||||
|
- Four violation types: ORM types, concrete repos, new instances, technical names
|
||||||
|
- Provides actionable fix suggestions
|
||||||
|
- Shows before/after code examples
|
||||||
|
|
||||||
|
- ✅ **Comprehensive Test Coverage**
|
||||||
|
- 31 new tests for repository pattern detection
|
||||||
|
- 292 total tests passing (100% pass rate)
|
||||||
|
- Integration tests for multiple violation types
|
||||||
|
- 96.77% statement coverage, 83.82% branch coverage
|
||||||
|
|
||||||
|
- ✅ **Documentation & Examples**
|
||||||
|
- 6 example files (3 bad patterns, 3 good patterns)
|
||||||
|
- Comprehensive README with patterns and principles
|
||||||
|
- Examples for ORM types, concrete repos, DI, and domain language
|
||||||
|
- Demonstrates Clean Architecture and SOLID principles
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated test count: 261 → 292 tests
|
||||||
|
- Added REPOSITORY_PATTERN rule to constants
|
||||||
|
- Extended AnalyzeProject use case with repository pattern detection
|
||||||
|
- Added REPOSITORY_VIOLATION_TYPES constant with 4 violation types
|
||||||
|
- ROADMAP updated with completed repository pattern validation (v0.5.0)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.4.0] - 2025-11-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
**🔀 Dependency Direction Enforcement**
|
||||||
|
|
||||||
|
Enforce Clean Architecture dependency rules to prevent architectural violations across layers.
|
||||||
|
|
||||||
|
- ✅ **Dependency Direction Detector**
|
||||||
|
- Validates that dependencies flow in the correct direction
|
||||||
|
- Domain layer can only import from Domain and Shared
|
||||||
|
- Application layer can only import from Domain, Application, and Shared
|
||||||
|
- Infrastructure layer can import from all layers
|
||||||
|
- Shared layer can be imported by all layers
|
||||||
|
|
||||||
|
- ✅ **Smart Violation Reporting**
|
||||||
|
- DependencyViolation value object with detailed context
|
||||||
|
- Provides fix suggestions with concrete examples
|
||||||
|
- Shows both violating import and suggested layer placement
|
||||||
|
- CLI output with severity indicators
|
||||||
|
|
||||||
|
- ✅ **Comprehensive Test Coverage**
|
||||||
|
- 43 new tests for dependency direction detection
|
||||||
|
- 100% test pass rate (261 total tests)
|
||||||
|
- Examples for both good and bad architecture patterns
|
||||||
|
|
||||||
|
- ✅ **Documentation & Examples**
|
||||||
|
- Good architecture examples for all layers
|
||||||
|
- Bad architecture examples showing common violations
|
||||||
|
- Demonstrates proper layer separation
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated test count: 218 → 261 tests
|
||||||
|
- Optimized extractLayerFromImport method to reduce complexity
|
||||||
|
- Updated getExampleFix to avoid false positives
|
||||||
|
- ROADMAP updated with completed dependency direction feature
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [0.3.0] - 2025-11-24
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
**🚫 Entity Exposure Detection**
|
||||||
|
|
||||||
|
Prevent domain entities from leaking to API responses, enforcing proper DTO usage at boundaries.
|
||||||
|
|
||||||
|
- ✅ **Entity Exposure Detector**
|
||||||
|
- Detects when controllers/routes return domain entities instead of DTOs
|
||||||
|
- Scans infrastructure layer (controllers, routes, handlers, resolvers, gateways)
|
||||||
|
- Identifies PascalCase entities without Dto/Request/Response suffixes
|
||||||
|
- Parses async methods with Promise<T> return types
|
||||||
|
|
||||||
|
- ✅ **Smart Remediation Suggestions**
|
||||||
|
- EntityExposure value object with step-by-step fix guidance
|
||||||
|
- Suggests creating DTOs with proper naming
|
||||||
|
- Provides mapper implementation examples
|
||||||
|
- Shows how to separate domain from presentation concerns
|
||||||
|
|
||||||
|
- ✅ **Comprehensive Test Coverage**
|
||||||
|
- 24 new tests for entity exposure detection (98% coverage)
|
||||||
|
- EntityExposureDetector: 98.07% coverage
|
||||||
|
- Overall project: 90.6% statements, 83.97% branches
|
||||||
|
|
||||||
|
- ✅ **Documentation & Examples**
|
||||||
|
- BadUserController and BadOrderController examples
|
||||||
|
- GoodUserController showing proper DTO usage
|
||||||
|
- Integration with CLI for helpful output
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Updated test count: 194 → 218 tests
|
||||||
|
- Added entity exposure to violation pipeline
|
||||||
|
- ROADMAP updated with completed entity exposure feature
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [0.2.0] - 2025-11-24
|
## [0.2.0] - 2025-11-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -233,7 +459,6 @@ Code quality guardian for vibe coders and enterprise teams - your AI coding comp
|
|||||||
## Future Releases
|
## Future Releases
|
||||||
|
|
||||||
Planned features for upcoming versions:
|
Planned features for upcoming versions:
|
||||||
- Entity exposure detection (domain entities in presentation layer)
|
|
||||||
- Configuration file support (.guardianrc)
|
- Configuration file support (.guardianrc)
|
||||||
- Custom rule definitions
|
- Custom rule definitions
|
||||||
- Plugin system
|
- Plugin system
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
This document outlines the current features and future plans for @puaros/guardian.
|
This document outlines the current features and future plans for @puaros/guardian.
|
||||||
|
|
||||||
## Current Version: 0.4.0 ✅ RELEASED
|
## Current Version: 0.5.2 ✅ RELEASED
|
||||||
|
|
||||||
**Released:** 2025-11-24
|
**Released:** 2025-11-24
|
||||||
|
|
||||||
@@ -114,10 +114,9 @@ import { User } from '../../domain/entities/User' // OK
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Future Roadmap
|
## Version 0.5.0 - Repository Pattern Validation 📚 ✅ RELEASED
|
||||||
|
|
||||||
### Version 0.5.0 - Repository Pattern Validation 📚
|
**Released:** 2025-11-24
|
||||||
**Target:** Q1 2026
|
|
||||||
**Priority:** HIGH
|
**Priority:** HIGH
|
||||||
|
|
||||||
Validate correct implementation of Repository Pattern:
|
Validate correct implementation of Repository Pattern:
|
||||||
@@ -148,15 +147,76 @@ class CreateUser {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Planned Features:**
|
**Implemented Features:**
|
||||||
- Check repository interfaces for ORM-specific types
|
- ✅ Check repository interfaces for ORM-specific types (Prisma, TypeORM, Mongoose, Sequelize, etc.)
|
||||||
- Detect concrete repository usage in use cases
|
- ✅ Detect concrete repository usage in use cases
|
||||||
- Detect `new Repository()` in use cases (should use DI)
|
- ✅ Detect `new Repository()` in use cases (should use DI)
|
||||||
- Validate repository methods follow domain language
|
- ✅ Validate repository methods follow domain language
|
||||||
- Check for data mapper pattern usage
|
- ✅ 31 tests covering all repository pattern scenarios
|
||||||
|
- ✅ 96.77% statement coverage, 83.82% branch coverage
|
||||||
|
- ✅ Examples for both good and bad patterns
|
||||||
|
- ✅ Comprehensive README with patterns and principles
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Version 0.5.2 - Severity-Based Prioritization 🎯 ✅ RELEASED
|
||||||
|
|
||||||
|
**Released:** 2025-11-24
|
||||||
|
**Priority:** HIGH
|
||||||
|
|
||||||
|
Intelligently prioritize violations by severity to help teams focus on critical issues first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Show only critical issues
|
||||||
|
guardian check src --only-critical
|
||||||
|
|
||||||
|
# Show high severity and above
|
||||||
|
guardian check src --min-severity high
|
||||||
|
```
|
||||||
|
|
||||||
|
**Severity Levels:**
|
||||||
|
- 🔴 **CRITICAL**: Circular dependencies, Repository pattern violations
|
||||||
|
- 🟠 **HIGH**: Dependency direction violations, Framework leaks, Entity exposures
|
||||||
|
- 🟡 **MEDIUM**: Naming violations, Architecture violations
|
||||||
|
- 🟢 **LOW**: Hardcoded values
|
||||||
|
|
||||||
|
**Implemented Features:**
|
||||||
|
- ✅ Automatic sorting by severity (most critical first)
|
||||||
|
- ✅ CLI flags: `--min-severity <level>` and `--only-critical`
|
||||||
|
- ✅ Color-coded severity labels in output (🔴🟠🟡🟢)
|
||||||
|
- ✅ Visual severity group headers with separators
|
||||||
|
- ✅ Filtering messages when filters active
|
||||||
|
- ✅ All violation interfaces include severity field
|
||||||
|
- ✅ 292 tests passing with 90%+ coverage
|
||||||
|
- ✅ Backwards compatible - no breaking changes
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- Focus on critical architectural violations first
|
||||||
|
- Gradual technical debt reduction
|
||||||
|
- Better CI/CD integration (fail on critical only)
|
||||||
|
- Improved developer experience with visual prioritization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version 0.5.1 - Code Quality Refactoring 🧹 ✅ RELEASED
|
||||||
|
|
||||||
|
**Released:** 2025-11-24
|
||||||
|
**Priority:** MEDIUM
|
||||||
|
|
||||||
|
Internal refactoring to eliminate hardcoded values and improve maintainability:
|
||||||
|
|
||||||
|
**Implemented Features:**
|
||||||
|
- ✅ Extracted 30+ constants from hardcoded strings
|
||||||
|
- ✅ New constants files: paths.ts, extended Messages.ts
|
||||||
|
- ✅ Reduced hardcoded values from 37 to 1 (97% improvement)
|
||||||
|
- ✅ Guardian passes its own checks (0 violations in src/)
|
||||||
|
- ✅ All 292 tests passing
|
||||||
|
- ✅ No breaking changes - fully backwards compatible
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Roadmap
|
||||||
|
|
||||||
### Version 0.6.0 - Aggregate Boundary Validation 🔒
|
### Version 0.6.0 - Aggregate Boundary Validation 🔒
|
||||||
**Target:** Q1 2026
|
**Target:** Q1 2026
|
||||||
**Priority:** MEDIUM
|
**Priority:** MEDIUM
|
||||||
@@ -1751,4 +1811,4 @@ Until we reach 1.0.0, minor version bumps (0.x.0) may include breaking changes a
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated:** 2025-11-24
|
**Last Updated:** 2025-11-24
|
||||||
**Current Version:** 0.4.0
|
**Current Version:** 0.5.0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@samiyev/guardian",
|
"name": "@samiyev/guardian",
|
||||||
"version": "0.5.0",
|
"version": "0.5.2",
|
||||||
"description": "Code quality guardian for vibe coders and enterprise teams - catch hardcodes, architecture violations, and circular deps. Enforce Clean Architecture at scale. Works with Claude, GPT, Copilot.",
|
"description": "Code quality guardian for vibe coders and enterprise teams - catch hardcodes, architecture violations, and circular deps. Enforce Clean Architecture at scale. Works with Claude, GPT, Copilot.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"puaros",
|
"puaros",
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ import {
|
|||||||
REPOSITORY_VIOLATION_TYPES,
|
REPOSITORY_VIOLATION_TYPES,
|
||||||
RULES,
|
RULES,
|
||||||
SEVERITY_LEVELS,
|
SEVERITY_LEVELS,
|
||||||
|
SEVERITY_ORDER,
|
||||||
|
type SeverityLevel,
|
||||||
|
VIOLATION_SEVERITY_MAP,
|
||||||
} from "../../shared/constants"
|
} from "../../shared/constants"
|
||||||
|
|
||||||
export interface AnalyzeProjectRequest {
|
export interface AnalyzeProjectRequest {
|
||||||
@@ -47,6 +50,7 @@ export interface ArchitectureViolation {
|
|||||||
message: string
|
message: string
|
||||||
file: string
|
file: string
|
||||||
line?: number
|
line?: number
|
||||||
|
severity: SeverityLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HardcodeViolation {
|
export interface HardcodeViolation {
|
||||||
@@ -64,13 +68,14 @@ export interface HardcodeViolation {
|
|||||||
constantName: string
|
constantName: string
|
||||||
location: string
|
location: string
|
||||||
}
|
}
|
||||||
|
severity: SeverityLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CircularDependencyViolation {
|
export interface CircularDependencyViolation {
|
||||||
rule: typeof RULES.CIRCULAR_DEPENDENCY
|
rule: typeof RULES.CIRCULAR_DEPENDENCY
|
||||||
message: string
|
message: string
|
||||||
cycle: string[]
|
cycle: string[]
|
||||||
severity: typeof SEVERITY_LEVELS.ERROR
|
severity: SeverityLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NamingConventionViolation {
|
export interface NamingConventionViolation {
|
||||||
@@ -88,6 +93,7 @@ export interface NamingConventionViolation {
|
|||||||
actual: string
|
actual: string
|
||||||
message: string
|
message: string
|
||||||
suggestion?: string
|
suggestion?: string
|
||||||
|
severity: SeverityLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FrameworkLeakViolation {
|
export interface FrameworkLeakViolation {
|
||||||
@@ -100,6 +106,7 @@ export interface FrameworkLeakViolation {
|
|||||||
line?: number
|
line?: number
|
||||||
message: string
|
message: string
|
||||||
suggestion: string
|
suggestion: string
|
||||||
|
severity: SeverityLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityExposureViolation {
|
export interface EntityExposureViolation {
|
||||||
@@ -112,6 +119,7 @@ export interface EntityExposureViolation {
|
|||||||
methodName?: string
|
methodName?: string
|
||||||
message: string
|
message: string
|
||||||
suggestion: string
|
suggestion: string
|
||||||
|
severity: SeverityLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DependencyDirectionViolation {
|
export interface DependencyDirectionViolation {
|
||||||
@@ -123,6 +131,7 @@ export interface DependencyDirectionViolation {
|
|||||||
line?: number
|
line?: number
|
||||||
message: string
|
message: string
|
||||||
suggestion: string
|
suggestion: string
|
||||||
|
severity: SeverityLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RepositoryPatternViolation {
|
export interface RepositoryPatternViolation {
|
||||||
@@ -138,6 +147,7 @@ export interface RepositoryPatternViolation {
|
|||||||
details: string
|
details: string
|
||||||
message: string
|
message: string
|
||||||
suggestion: string
|
suggestion: string
|
||||||
|
severity: SeverityLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectMetrics {
|
export interface ProjectMetrics {
|
||||||
@@ -207,14 +217,24 @@ export class AnalyzeProject extends UseCase<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const violations = this.detectViolations(sourceFiles)
|
const violations = this.sortBySeverity(this.detectViolations(sourceFiles))
|
||||||
const hardcodeViolations = this.detectHardcode(sourceFiles)
|
const hardcodeViolations = this.sortBySeverity(this.detectHardcode(sourceFiles))
|
||||||
const circularDependencyViolations = this.detectCircularDependencies(dependencyGraph)
|
const circularDependencyViolations = this.sortBySeverity(
|
||||||
const namingViolations = this.detectNamingConventions(sourceFiles)
|
this.detectCircularDependencies(dependencyGraph),
|
||||||
const frameworkLeakViolations = this.detectFrameworkLeaks(sourceFiles)
|
)
|
||||||
const entityExposureViolations = this.detectEntityExposures(sourceFiles)
|
const namingViolations = this.sortBySeverity(this.detectNamingConventions(sourceFiles))
|
||||||
const dependencyDirectionViolations = this.detectDependencyDirections(sourceFiles)
|
const frameworkLeakViolations = this.sortBySeverity(
|
||||||
const repositoryPatternViolations = this.detectRepositoryPatternViolations(sourceFiles)
|
this.detectFrameworkLeaks(sourceFiles),
|
||||||
|
)
|
||||||
|
const entityExposureViolations = this.sortBySeverity(
|
||||||
|
this.detectEntityExposures(sourceFiles),
|
||||||
|
)
|
||||||
|
const dependencyDirectionViolations = this.sortBySeverity(
|
||||||
|
this.detectDependencyDirections(sourceFiles),
|
||||||
|
)
|
||||||
|
const repositoryPatternViolations = this.sortBySeverity(
|
||||||
|
this.detectRepositoryPatternViolations(sourceFiles),
|
||||||
|
)
|
||||||
const metrics = this.calculateMetrics(sourceFiles, totalFunctions, dependencyGraph)
|
const metrics = this.calculateMetrics(sourceFiles, totalFunctions, dependencyGraph)
|
||||||
|
|
||||||
return ResponseDto.ok({
|
return ResponseDto.ok({
|
||||||
@@ -294,6 +314,7 @@ export class AnalyzeProject extends UseCase<
|
|||||||
rule: RULES.CLEAN_ARCHITECTURE,
|
rule: RULES.CLEAN_ARCHITECTURE,
|
||||||
message: `Layer "${file.layer}" cannot import from "${importedLayer}"`,
|
message: `Layer "${file.layer}" cannot import from "${importedLayer}"`,
|
||||||
file: file.path.relative,
|
file: file.path.relative,
|
||||||
|
severity: VIOLATION_SEVERITY_MAP.ARCHITECTURE,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,6 +357,7 @@ export class AnalyzeProject extends UseCase<
|
|||||||
constantName: hardcoded.suggestConstantName(),
|
constantName: hardcoded.suggestConstantName(),
|
||||||
location: hardcoded.suggestLocation(file.layer),
|
location: hardcoded.suggestLocation(file.layer),
|
||||||
},
|
},
|
||||||
|
severity: VIOLATION_SEVERITY_MAP.HARDCODE,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,7 +377,7 @@ export class AnalyzeProject extends UseCase<
|
|||||||
rule: RULES.CIRCULAR_DEPENDENCY,
|
rule: RULES.CIRCULAR_DEPENDENCY,
|
||||||
message: `Circular dependency detected: ${cycleChain}`,
|
message: `Circular dependency detected: ${cycleChain}`,
|
||||||
cycle,
|
cycle,
|
||||||
severity: SEVERITY_LEVELS.ERROR,
|
severity: VIOLATION_SEVERITY_MAP.CIRCULAR_DEPENDENCY,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,6 +405,7 @@ export class AnalyzeProject extends UseCase<
|
|||||||
actual: violation.actual,
|
actual: violation.actual,
|
||||||
message: violation.getMessage(),
|
message: violation.getMessage(),
|
||||||
suggestion: violation.suggestion,
|
suggestion: violation.suggestion,
|
||||||
|
severity: VIOLATION_SEVERITY_MAP.NAMING_CONVENTION,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,6 +434,7 @@ export class AnalyzeProject extends UseCase<
|
|||||||
line: leak.line,
|
line: leak.line,
|
||||||
message: leak.getMessage(),
|
message: leak.getMessage(),
|
||||||
suggestion: leak.getSuggestion(),
|
suggestion: leak.getSuggestion(),
|
||||||
|
severity: VIOLATION_SEVERITY_MAP.FRAMEWORK_LEAK,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,6 +463,7 @@ export class AnalyzeProject extends UseCase<
|
|||||||
methodName: exposure.methodName,
|
methodName: exposure.methodName,
|
||||||
message: exposure.getMessage(),
|
message: exposure.getMessage(),
|
||||||
suggestion: exposure.getSuggestion(),
|
suggestion: exposure.getSuggestion(),
|
||||||
|
severity: VIOLATION_SEVERITY_MAP.ENTITY_EXPOSURE,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,6 +491,7 @@ export class AnalyzeProject extends UseCase<
|
|||||||
line: violation.line,
|
line: violation.line,
|
||||||
message: violation.getMessage(),
|
message: violation.getMessage(),
|
||||||
suggestion: violation.getSuggestion(),
|
suggestion: violation.getSuggestion(),
|
||||||
|
severity: VIOLATION_SEVERITY_MAP.DEPENDENCY_DIRECTION,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -499,6 +525,7 @@ export class AnalyzeProject extends UseCase<
|
|||||||
details: violation.details,
|
details: violation.details,
|
||||||
message: violation.getMessage(),
|
message: violation.getMessage(),
|
||||||
suggestion: violation.getSuggestion(),
|
suggestion: violation.getSuggestion(),
|
||||||
|
severity: VIOLATION_SEVERITY_MAP.REPOSITORY_PATTERN,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -528,4 +555,10 @@ export class AnalyzeProject extends UseCase<
|
|||||||
layerDistribution,
|
layerDistribution,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sortBySeverity<T extends { severity: SeverityLevel }>(violations: T[]): T[] {
|
||||||
|
return violations.sort((a, b) => {
|
||||||
|
return SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export const CLI_DESCRIPTIONS = {
|
|||||||
VERBOSE_OPTION: "Verbose output",
|
VERBOSE_OPTION: "Verbose output",
|
||||||
NO_HARDCODE_OPTION: "Skip hardcode detection",
|
NO_HARDCODE_OPTION: "Skip hardcode detection",
|
||||||
NO_ARCHITECTURE_OPTION: "Skip architecture checks",
|
NO_ARCHITECTURE_OPTION: "Skip architecture checks",
|
||||||
|
MIN_SEVERITY_OPTION: "Minimum severity level (critical, high, medium, low)",
|
||||||
|
ONLY_CRITICAL_OPTION: "Show only critical severity issues",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const CLI_OPTIONS = {
|
export const CLI_OPTIONS = {
|
||||||
@@ -27,6 +29,8 @@ export const CLI_OPTIONS = {
|
|||||||
VERBOSE: "-v, --verbose",
|
VERBOSE: "-v, --verbose",
|
||||||
NO_HARDCODE: "--no-hardcode",
|
NO_HARDCODE: "--no-hardcode",
|
||||||
NO_ARCHITECTURE: "--no-architecture",
|
NO_ARCHITECTURE: "--no-architecture",
|
||||||
|
MIN_SEVERITY: "--min-severity <level>",
|
||||||
|
ONLY_CRITICAL: "--only-critical",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const CLI_ARGUMENTS = {
|
export const CLI_ARGUMENTS = {
|
||||||
|
|||||||
@@ -11,6 +11,73 @@ import {
|
|||||||
CLI_OPTIONS,
|
CLI_OPTIONS,
|
||||||
DEFAULT_EXCLUDES,
|
DEFAULT_EXCLUDES,
|
||||||
} from "./constants"
|
} from "./constants"
|
||||||
|
import { SEVERITY_LEVELS, SEVERITY_ORDER, type SeverityLevel } from "../shared/constants"
|
||||||
|
|
||||||
|
const SEVERITY_LABELS: Record<SeverityLevel, string> = {
|
||||||
|
[SEVERITY_LEVELS.CRITICAL]: "🔴 CRITICAL",
|
||||||
|
[SEVERITY_LEVELS.HIGH]: "🟠 HIGH",
|
||||||
|
[SEVERITY_LEVELS.MEDIUM]: "🟡 MEDIUM",
|
||||||
|
[SEVERITY_LEVELS.LOW]: "🟢 LOW",
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEVERITY_HEADER: Record<SeverityLevel, string> = {
|
||||||
|
[SEVERITY_LEVELS.CRITICAL]:
|
||||||
|
"\n═══════════════════════════════════════════\n🔴 CRITICAL SEVERITY\n═══════════════════════════════════════════",
|
||||||
|
[SEVERITY_LEVELS.HIGH]:
|
||||||
|
"\n═══════════════════════════════════════════\n🟠 HIGH SEVERITY\n═══════════════════════════════════════════",
|
||||||
|
[SEVERITY_LEVELS.MEDIUM]:
|
||||||
|
"\n═══════════════════════════════════════════\n🟡 MEDIUM SEVERITY\n═══════════════════════════════════════════",
|
||||||
|
[SEVERITY_LEVELS.LOW]:
|
||||||
|
"\n═══════════════════════════════════════════\n🟢 LOW SEVERITY\n═══════════════════════════════════════════",
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupBySeverity<T extends { severity: SeverityLevel }>(
|
||||||
|
violations: T[],
|
||||||
|
): Map<SeverityLevel, T[]> {
|
||||||
|
const grouped = new Map<SeverityLevel, T[]>()
|
||||||
|
|
||||||
|
for (const violation of violations) {
|
||||||
|
const existing = grouped.get(violation.severity) || []
|
||||||
|
existing.push(violation)
|
||||||
|
grouped.set(violation.severity, existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
return grouped
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterBySeverity<T extends { severity: SeverityLevel }>(
|
||||||
|
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<T extends { severity: SeverityLevel }>(
|
||||||
|
violations: T[],
|
||||||
|
displayFn: (v: T, index: number) => void,
|
||||||
|
): void {
|
||||||
|
const grouped = groupBySeverity(violations)
|
||||||
|
const severities: SeverityLevel[] = [
|
||||||
|
SEVERITY_LEVELS.CRITICAL,
|
||||||
|
SEVERITY_LEVELS.HIGH,
|
||||||
|
SEVERITY_LEVELS.MEDIUM,
|
||||||
|
SEVERITY_LEVELS.LOW,
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const severity of severities) {
|
||||||
|
const items = grouped.get(severity)
|
||||||
|
if (items && items.length > 0) {
|
||||||
|
console.log(SEVERITY_HEADER[severity])
|
||||||
|
console.log(`Found ${String(items.length)} issue(s)\n`)
|
||||||
|
items.forEach(displayFn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
|
|
||||||
@@ -24,6 +91,8 @@ program
|
|||||||
.option(CLI_OPTIONS.VERBOSE, CLI_DESCRIPTIONS.VERBOSE_OPTION, false)
|
.option(CLI_OPTIONS.VERBOSE, CLI_DESCRIPTIONS.VERBOSE_OPTION, false)
|
||||||
.option(CLI_OPTIONS.NO_HARDCODE, CLI_DESCRIPTIONS.NO_HARDCODE_OPTION)
|
.option(CLI_OPTIONS.NO_HARDCODE, CLI_DESCRIPTIONS.NO_HARDCODE_OPTION)
|
||||||
.option(CLI_OPTIONS.NO_ARCHITECTURE, CLI_DESCRIPTIONS.NO_ARCHITECTURE_OPTION)
|
.option(CLI_OPTIONS.NO_ARCHITECTURE, CLI_DESCRIPTIONS.NO_ARCHITECTURE_OPTION)
|
||||||
|
.option(CLI_OPTIONS.MIN_SEVERITY, CLI_DESCRIPTIONS.MIN_SEVERITY_OPTION)
|
||||||
|
.option(CLI_OPTIONS.ONLY_CRITICAL, CLI_DESCRIPTIONS.ONLY_CRITICAL_OPTION, false)
|
||||||
.action(async (path: string, options) => {
|
.action(async (path: string, options) => {
|
||||||
try {
|
try {
|
||||||
console.log(CLI_MESSAGES.ANALYZING)
|
console.log(CLI_MESSAGES.ANALYZING)
|
||||||
@@ -33,16 +102,52 @@ program
|
|||||||
exclude: options.exclude,
|
exclude: options.exclude,
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
let {
|
||||||
hardcodeViolations,
|
hardcodeViolations,
|
||||||
violations,
|
violations,
|
||||||
circularDependencyViolations,
|
circularDependencyViolations,
|
||||||
namingViolations,
|
namingViolations,
|
||||||
frameworkLeakViolations,
|
frameworkLeakViolations,
|
||||||
entityExposureViolations,
|
entityExposureViolations,
|
||||||
|
dependencyDirectionViolations,
|
||||||
|
repositoryPatternViolations,
|
||||||
metrics,
|
metrics,
|
||||||
} = result
|
} = result
|
||||||
|
|
||||||
|
const minSeverity: SeverityLevel | undefined = options.onlyCritical
|
||||||
|
? SEVERITY_LEVELS.CRITICAL
|
||||||
|
: options.minSeverity
|
||||||
|
? (options.minSeverity.toLowerCase() as SeverityLevel)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
if (minSeverity) {
|
||||||
|
violations = filterBySeverity(violations, minSeverity)
|
||||||
|
hardcodeViolations = filterBySeverity(hardcodeViolations, minSeverity)
|
||||||
|
circularDependencyViolations = filterBySeverity(
|
||||||
|
circularDependencyViolations,
|
||||||
|
minSeverity,
|
||||||
|
)
|
||||||
|
namingViolations = filterBySeverity(namingViolations, minSeverity)
|
||||||
|
frameworkLeakViolations = filterBySeverity(frameworkLeakViolations, minSeverity)
|
||||||
|
entityExposureViolations = filterBySeverity(entityExposureViolations, minSeverity)
|
||||||
|
dependencyDirectionViolations = filterBySeverity(
|
||||||
|
dependencyDirectionViolations,
|
||||||
|
minSeverity,
|
||||||
|
)
|
||||||
|
repositoryPatternViolations = filterBySeverity(
|
||||||
|
repositoryPatternViolations,
|
||||||
|
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`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Display metrics
|
// Display metrics
|
||||||
console.log(CLI_MESSAGES.METRICS_HEADER)
|
console.log(CLI_MESSAGES.METRICS_HEADER)
|
||||||
console.log(` ${CLI_LABELS.FILES_ANALYZED} ${String(metrics.totalFiles)}`)
|
console.log(` ${CLI_LABELS.FILES_ANALYZED} ${String(metrics.totalFiles)}`)
|
||||||
@@ -59,11 +164,12 @@ program
|
|||||||
// Architecture violations
|
// Architecture violations
|
||||||
if (options.architecture && violations.length > 0) {
|
if (options.architecture && violations.length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`${CLI_MESSAGES.VIOLATIONS_HEADER} ${String(violations.length)} ${CLI_LABELS.ARCHITECTURE_VIOLATIONS}\n`,
|
`\n${CLI_MESSAGES.VIOLATIONS_HEADER} ${String(violations.length)} ${CLI_LABELS.ARCHITECTURE_VIOLATIONS}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
violations.forEach((v, index) => {
|
displayGroupedViolations(violations, (v, index) => {
|
||||||
console.log(`${String(index + 1)}. ${v.file}`)
|
console.log(`${String(index + 1)}. ${v.file}`)
|
||||||
|
console.log(` Severity: ${SEVERITY_LABELS[v.severity]}`)
|
||||||
console.log(` Rule: ${v.rule}`)
|
console.log(` Rule: ${v.rule}`)
|
||||||
console.log(` ${v.message}`)
|
console.log(` ${v.message}`)
|
||||||
console.log("")
|
console.log("")
|
||||||
@@ -73,12 +179,12 @@ program
|
|||||||
// Circular dependency violations
|
// Circular dependency violations
|
||||||
if (options.architecture && circularDependencyViolations.length > 0) {
|
if (options.architecture && circularDependencyViolations.length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`${CLI_MESSAGES.CIRCULAR_DEPS_HEADER} ${String(circularDependencyViolations.length)} ${CLI_LABELS.CIRCULAR_DEPENDENCIES}\n`,
|
`\n${CLI_MESSAGES.CIRCULAR_DEPS_HEADER} ${String(circularDependencyViolations.length)} ${CLI_LABELS.CIRCULAR_DEPENDENCIES}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
circularDependencyViolations.forEach((cd, index) => {
|
displayGroupedViolations(circularDependencyViolations, (cd, index) => {
|
||||||
console.log(`${String(index + 1)}. ${cd.message}`)
|
console.log(`${String(index + 1)}. ${cd.message}`)
|
||||||
console.log(` Severity: ${cd.severity}`)
|
console.log(` Severity: ${SEVERITY_LABELS[cd.severity]}`)
|
||||||
console.log(" Cycle path:")
|
console.log(" Cycle path:")
|
||||||
cd.cycle.forEach((file, i) => {
|
cd.cycle.forEach((file, i) => {
|
||||||
console.log(` ${String(i + 1)}. ${file}`)
|
console.log(` ${String(i + 1)}. ${file}`)
|
||||||
@@ -93,11 +199,12 @@ program
|
|||||||
// Naming convention violations
|
// Naming convention violations
|
||||||
if (options.architecture && namingViolations.length > 0) {
|
if (options.architecture && namingViolations.length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`${CLI_MESSAGES.NAMING_VIOLATIONS_HEADER} ${String(namingViolations.length)} ${CLI_LABELS.NAMING_VIOLATIONS}\n`,
|
`\n${CLI_MESSAGES.NAMING_VIOLATIONS_HEADER} ${String(namingViolations.length)} ${CLI_LABELS.NAMING_VIOLATIONS}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
namingViolations.forEach((nc, index) => {
|
displayGroupedViolations(namingViolations, (nc, index) => {
|
||||||
console.log(`${String(index + 1)}. ${nc.file}`)
|
console.log(`${String(index + 1)}. ${nc.file}`)
|
||||||
|
console.log(` Severity: ${SEVERITY_LABELS[nc.severity]}`)
|
||||||
console.log(` File: ${nc.fileName}`)
|
console.log(` File: ${nc.fileName}`)
|
||||||
console.log(` Layer: ${nc.layer}`)
|
console.log(` Layer: ${nc.layer}`)
|
||||||
console.log(` Type: ${nc.type}`)
|
console.log(` Type: ${nc.type}`)
|
||||||
@@ -112,11 +219,12 @@ program
|
|||||||
// Framework leak violations
|
// Framework leak violations
|
||||||
if (options.architecture && frameworkLeakViolations.length > 0) {
|
if (options.architecture && frameworkLeakViolations.length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`\n🏗️ Found ${String(frameworkLeakViolations.length)} framework leak(s):\n`,
|
`\n🏗️ Found ${String(frameworkLeakViolations.length)} framework leak(s)`,
|
||||||
)
|
)
|
||||||
|
|
||||||
frameworkLeakViolations.forEach((fl, index) => {
|
displayGroupedViolations(frameworkLeakViolations, (fl, index) => {
|
||||||
console.log(`${String(index + 1)}. ${fl.file}`)
|
console.log(`${String(index + 1)}. ${fl.file}`)
|
||||||
|
console.log(` Severity: ${SEVERITY_LABELS[fl.severity]}`)
|
||||||
console.log(` Package: ${fl.packageName}`)
|
console.log(` Package: ${fl.packageName}`)
|
||||||
console.log(` Category: ${fl.categoryDescription}`)
|
console.log(` Category: ${fl.categoryDescription}`)
|
||||||
console.log(` Layer: ${fl.layer}`)
|
console.log(` Layer: ${fl.layer}`)
|
||||||
@@ -130,12 +238,13 @@ program
|
|||||||
// Entity exposure violations
|
// Entity exposure violations
|
||||||
if (options.architecture && entityExposureViolations.length > 0) {
|
if (options.architecture && entityExposureViolations.length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`\n🎭 Found ${String(entityExposureViolations.length)} entity exposure(s):\n`,
|
`\n🎭 Found ${String(entityExposureViolations.length)} entity exposure(s)`,
|
||||||
)
|
)
|
||||||
|
|
||||||
entityExposureViolations.forEach((ee, index) => {
|
displayGroupedViolations(entityExposureViolations, (ee, index) => {
|
||||||
const location = ee.line ? `${ee.file}:${String(ee.line)}` : ee.file
|
const location = ee.line ? `${ee.file}:${String(ee.line)}` : ee.file
|
||||||
console.log(`${String(index + 1)}. ${location}`)
|
console.log(`${String(index + 1)}. ${location}`)
|
||||||
|
console.log(` Severity: ${SEVERITY_LABELS[ee.severity]}`)
|
||||||
console.log(` Entity: ${ee.entityName}`)
|
console.log(` Entity: ${ee.entityName}`)
|
||||||
console.log(` Return Type: ${ee.returnType}`)
|
console.log(` Return Type: ${ee.returnType}`)
|
||||||
if (ee.methodName) {
|
if (ee.methodName) {
|
||||||
@@ -154,16 +263,53 @@ program
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dependency direction violations
|
||||||
|
if (options.architecture && dependencyDirectionViolations.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`\n⚠️ Found ${String(dependencyDirectionViolations.length)} dependency direction violation(s)`,
|
||||||
|
)
|
||||||
|
|
||||||
|
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("")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository pattern violations
|
||||||
|
if (options.architecture && repositoryPatternViolations.length > 0) {
|
||||||
|
console.log(
|
||||||
|
`\n📦 Found ${String(repositoryPatternViolations.length)} repository pattern violation(s)`,
|
||||||
|
)
|
||||||
|
|
||||||
|
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("")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Hardcode violations
|
// Hardcode violations
|
||||||
if (options.hardcode && hardcodeViolations.length > 0) {
|
if (options.hardcode && hardcodeViolations.length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`${CLI_MESSAGES.HARDCODE_VIOLATIONS_HEADER} ${String(hardcodeViolations.length)} ${CLI_LABELS.HARDCODE_VIOLATIONS}\n`,
|
`\n${CLI_MESSAGES.HARDCODE_VIOLATIONS_HEADER} ${String(hardcodeViolations.length)} ${CLI_LABELS.HARDCODE_VIOLATIONS}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
hardcodeViolations.forEach((hc, index) => {
|
displayGroupedViolations(hardcodeViolations, (hc, index) => {
|
||||||
console.log(
|
console.log(
|
||||||
`${String(index + 1)}. ${hc.file}:${String(hc.line)}:${String(hc.column)}`,
|
`${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(` Type: ${hc.type}`)
|
||||||
console.log(` Value: ${JSON.stringify(hc.value)}`)
|
console.log(` Value: ${JSON.stringify(hc.value)}`)
|
||||||
console.log(` Context: ${hc.context.trim()}`)
|
console.log(` Context: ${hc.context.trim()}`)
|
||||||
@@ -180,7 +326,9 @@ program
|
|||||||
circularDependencyViolations.length +
|
circularDependencyViolations.length +
|
||||||
namingViolations.length +
|
namingViolations.length +
|
||||||
frameworkLeakViolations.length +
|
frameworkLeakViolations.length +
|
||||||
entityExposureViolations.length
|
entityExposureViolations.length +
|
||||||
|
dependencyDirectionViolations.length +
|
||||||
|
repositoryPatternViolations.length
|
||||||
|
|
||||||
if (totalIssues === 0) {
|
if (totalIssues === 0) {
|
||||||
console.log(CLI_MESSAGES.NO_ISSUES)
|
console.log(CLI_MESSAGES.NO_ISSUES)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export const FRAMEWORK_CATEGORY_NAMES = {
|
||||||
|
ORM: "ORM",
|
||||||
|
WEB_FRAMEWORK: "WEB_FRAMEWORK",
|
||||||
|
HTTP_CLIENT: "HTTP_CLIENT",
|
||||||
|
VALIDATION: "VALIDATION",
|
||||||
|
DI_CONTAINER: "DI_CONTAINER",
|
||||||
|
LOGGER: "LOGGER",
|
||||||
|
CACHE: "CACHE",
|
||||||
|
MESSAGE_QUEUE: "MESSAGE_QUEUE",
|
||||||
|
EMAIL: "EMAIL",
|
||||||
|
STORAGE: "STORAGE",
|
||||||
|
TESTING: "TESTING",
|
||||||
|
TEMPLATE_ENGINE: "TEMPLATE_ENGINE",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const FRAMEWORK_CATEGORY_DESCRIPTIONS = {
|
||||||
|
[FRAMEWORK_CATEGORY_NAMES.ORM]: "Database ORM/ODM",
|
||||||
|
[FRAMEWORK_CATEGORY_NAMES.WEB_FRAMEWORK]: "Web Framework",
|
||||||
|
[FRAMEWORK_CATEGORY_NAMES.HTTP_CLIENT]: "HTTP Client",
|
||||||
|
[FRAMEWORK_CATEGORY_NAMES.VALIDATION]: "Validation Library",
|
||||||
|
[FRAMEWORK_CATEGORY_NAMES.DI_CONTAINER]: "DI Container",
|
||||||
|
[FRAMEWORK_CATEGORY_NAMES.LOGGER]: "Logger",
|
||||||
|
[FRAMEWORK_CATEGORY_NAMES.CACHE]: "Cache",
|
||||||
|
[FRAMEWORK_CATEGORY_NAMES.MESSAGE_QUEUE]: "Message Queue",
|
||||||
|
[FRAMEWORK_CATEGORY_NAMES.EMAIL]: "Email Service",
|
||||||
|
[FRAMEWORK_CATEGORY_NAMES.STORAGE]: "Storage Service",
|
||||||
|
[FRAMEWORK_CATEGORY_NAMES.TESTING]: "Testing Framework",
|
||||||
|
[FRAMEWORK_CATEGORY_NAMES.TEMPLATE_ENGINE]: "Template Engine",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const DEFAULT_FRAMEWORK_CATEGORY_DESCRIPTION = "Framework Package"
|
||||||
50
packages/guardian/src/domain/constants/Messages.ts
Normal file
50
packages/guardian/src/domain/constants/Messages.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
export const DEPENDENCY_VIOLATION_MESSAGES = {
|
||||||
|
DOMAIN_INDEPENDENCE: "Domain layer should be independent and not depend on other layers",
|
||||||
|
DOMAIN_MOVE_TO_DOMAIN:
|
||||||
|
"Move the imported code to the domain layer if it contains business logic",
|
||||||
|
DOMAIN_USE_DI:
|
||||||
|
"Use dependency inversion: define an interface in domain and implement it in infrastructure",
|
||||||
|
APPLICATION_NO_INFRA: "Application layer should not depend on infrastructure",
|
||||||
|
APPLICATION_DEFINE_PORT: "Define an interface (Port) in application layer",
|
||||||
|
APPLICATION_IMPLEMENT_ADAPTER: "Implement the interface (Adapter) in infrastructure layer",
|
||||||
|
APPLICATION_USE_DI: "Use dependency injection to provide the implementation",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ENTITY_EXPOSURE_MESSAGES = {
|
||||||
|
METHOD_DEFAULT: "Method",
|
||||||
|
METHOD_DEFAULT_NAME: "getEntity",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FRAMEWORK_LEAK_MESSAGES = {
|
||||||
|
DEFAULT_MESSAGE: "Domain layer should not depend on external frameworks",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const REPOSITORY_PATTERN_MESSAGES = {
|
||||||
|
UNKNOWN_TYPE: "Unknown",
|
||||||
|
CONSTRUCTOR: "constructor",
|
||||||
|
DEFAULT_SUGGESTION: "Follow Repository Pattern best practices",
|
||||||
|
NO_EXAMPLE: "// No example available",
|
||||||
|
STEP_REMOVE_ORM_TYPES: "1. Remove ORM-specific types from repository interface",
|
||||||
|
STEP_USE_DOMAIN_TYPES: "2. Use domain types (entities, value objects) instead",
|
||||||
|
STEP_KEEP_CLEAN: "3. Keep repository interface clean and persistence-agnostic",
|
||||||
|
STEP_DEPEND_ON_INTERFACE: "1. Depend on repository interface (IUserRepository) in constructor",
|
||||||
|
STEP_MOVE_TO_INFRASTRUCTURE: "2. Move concrete implementation to infrastructure layer",
|
||||||
|
STEP_USE_DI: "3. Use dependency injection to provide implementation",
|
||||||
|
STEP_REMOVE_NEW: "1. Remove 'new Repository()' from use case",
|
||||||
|
STEP_INJECT_CONSTRUCTOR: "2. Inject repository through constructor",
|
||||||
|
STEP_CONFIGURE_DI: "3. Configure dependency injection container",
|
||||||
|
STEP_RENAME_METHOD: "1. Rename method to use domain language",
|
||||||
|
STEP_REFLECT_BUSINESS: "2. Method names should reflect business operations",
|
||||||
|
STEP_AVOID_TECHNICAL: "3. Avoid technical database terms (query, insert, select)",
|
||||||
|
EXAMPLE_PREFIX: "Example:",
|
||||||
|
BAD_ORM_EXAMPLE: "❌ Bad: findOne(query: Prisma.UserWhereInput)",
|
||||||
|
GOOD_DOMAIN_EXAMPLE: "✅ Good: findById(id: UserId): Promise<User | null>",
|
||||||
|
BAD_NEW_REPO: "❌ Bad: const repo = new UserRepository()",
|
||||||
|
GOOD_INJECT_REPO: "✅ Good: constructor(private readonly userRepo: IUserRepository) {}",
|
||||||
|
SUGGESTION_FINDONE: "findById",
|
||||||
|
SUGGESTION_FINDMANY: "findAll or findByFilter",
|
||||||
|
SUGGESTION_INSERT: "save or create",
|
||||||
|
SUGGESTION_UPDATE: "save",
|
||||||
|
SUGGESTION_DELETE: "remove or delete",
|
||||||
|
SUGGESTION_QUERY: "find or search",
|
||||||
|
}
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
import { ValueObject } from "./ValueObject"
|
import { ValueObject } from "./ValueObject"
|
||||||
|
import {
|
||||||
|
LAYER_APPLICATION,
|
||||||
|
LAYER_DOMAIN,
|
||||||
|
LAYER_INFRASTRUCTURE,
|
||||||
|
} from "../../shared/constants/layers"
|
||||||
|
import { DEPENDENCY_VIOLATION_MESSAGES } from "../constants/Messages"
|
||||||
|
|
||||||
interface DependencyViolationProps {
|
interface DependencyViolationProps {
|
||||||
readonly fromLayer: string
|
readonly fromLayer: string
|
||||||
@@ -81,18 +87,18 @@ export class DependencyViolation extends ValueObject<DependencyViolationProps> {
|
|||||||
public getSuggestion(): string {
|
public getSuggestion(): string {
|
||||||
const suggestions: string[] = []
|
const suggestions: string[] = []
|
||||||
|
|
||||||
if (this.props.fromLayer === "domain") {
|
if (this.props.fromLayer === LAYER_DOMAIN) {
|
||||||
suggestions.push(
|
suggestions.push(
|
||||||
"Domain layer should be independent and not depend on other layers",
|
DEPENDENCY_VIOLATION_MESSAGES.DOMAIN_INDEPENDENCE,
|
||||||
"Move the imported code to the domain layer if it contains business logic",
|
DEPENDENCY_VIOLATION_MESSAGES.DOMAIN_MOVE_TO_DOMAIN,
|
||||||
"Use dependency inversion: define an interface in domain and implement it in infrastructure",
|
DEPENDENCY_VIOLATION_MESSAGES.DOMAIN_USE_DI,
|
||||||
)
|
)
|
||||||
} else if (this.props.fromLayer === "application") {
|
} else if (this.props.fromLayer === LAYER_APPLICATION) {
|
||||||
suggestions.push(
|
suggestions.push(
|
||||||
"Application layer should not depend on infrastructure",
|
DEPENDENCY_VIOLATION_MESSAGES.APPLICATION_NO_INFRA,
|
||||||
"Define an interface (Port) in application layer",
|
DEPENDENCY_VIOLATION_MESSAGES.APPLICATION_DEFINE_PORT,
|
||||||
"Implement the interface (Adapter) in infrastructure layer",
|
DEPENDENCY_VIOLATION_MESSAGES.APPLICATION_IMPLEMENT_ADAPTER,
|
||||||
"Use dependency injection to provide the implementation",
|
DEPENDENCY_VIOLATION_MESSAGES.APPLICATION_USE_DI,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +106,7 @@ export class DependencyViolation extends ValueObject<DependencyViolationProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getExampleFix(): string {
|
public getExampleFix(): string {
|
||||||
if (this.props.fromLayer === "domain" && this.props.toLayer === "infrastructure") {
|
if (this.props.fromLayer === LAYER_DOMAIN && this.props.toLayer === LAYER_INFRASTRUCTURE) {
|
||||||
return `
|
return `
|
||||||
// ❌ Bad: Domain depends on Infrastructure (PrismaClient)
|
// ❌ Bad: Domain depends on Infrastructure (PrismaClient)
|
||||||
// domain/services/UserService.ts
|
// domain/services/UserService.ts
|
||||||
@@ -128,7 +134,10 @@ class PrismaUserRepository implements IUserRepository {
|
|||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.fromLayer === "application" && this.props.toLayer === "infrastructure") {
|
if (
|
||||||
|
this.props.fromLayer === LAYER_APPLICATION &&
|
||||||
|
this.props.toLayer === LAYER_INFRASTRUCTURE
|
||||||
|
) {
|
||||||
return `
|
return `
|
||||||
// ❌ Bad: Application depends on Infrastructure (SmtpEmailService)
|
// ❌ Bad: Application depends on Infrastructure (SmtpEmailService)
|
||||||
// application/use-cases/SendEmail.ts
|
// application/use-cases/SendEmail.ts
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ValueObject } from "./ValueObject"
|
import { ValueObject } from "./ValueObject"
|
||||||
|
import { ENTITY_EXPOSURE_MESSAGES } from "../constants/Messages"
|
||||||
|
|
||||||
interface EntityExposureProps {
|
interface EntityExposureProps {
|
||||||
readonly entityName: string
|
readonly entityName: string
|
||||||
@@ -80,7 +81,9 @@ export class EntityExposure extends ValueObject<EntityExposureProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getMessage(): string {
|
public getMessage(): string {
|
||||||
const method = this.props.methodName ? `Method '${this.props.methodName}'` : "Method"
|
const method = this.props.methodName
|
||||||
|
? `Method '${this.props.methodName}'`
|
||||||
|
: ENTITY_EXPOSURE_MESSAGES.METHOD_DEFAULT
|
||||||
return `${method} returns domain entity '${this.props.entityName}' instead of DTO`
|
return `${method} returns domain entity '${this.props.entityName}' instead of DTO`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +99,12 @@ export class EntityExposure extends ValueObject<EntityExposureProps> {
|
|||||||
public getExampleFix(): string {
|
public getExampleFix(): string {
|
||||||
return `
|
return `
|
||||||
// ❌ Bad: Exposing domain entity
|
// ❌ Bad: Exposing domain entity
|
||||||
async ${this.props.methodName || "getEntity"}(): Promise<${this.props.entityName}> {
|
async ${this.props.methodName || ENTITY_EXPOSURE_MESSAGES.METHOD_DEFAULT_NAME}(): Promise<${this.props.entityName}> {
|
||||||
return await this.service.find()
|
return await this.service.find()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Good: Using DTO
|
// ✅ Good: Using DTO
|
||||||
async ${this.props.methodName || "getEntity"}(): Promise<${this.props.entityName}ResponseDto> {
|
async ${this.props.methodName || ENTITY_EXPOSURE_MESSAGES.METHOD_DEFAULT_NAME}(): Promise<${this.props.entityName}ResponseDto> {
|
||||||
const entity = await this.service.find()
|
const entity = await this.service.find()
|
||||||
return ${this.props.entityName}Mapper.toDto(entity)
|
return ${this.props.entityName}Mapper.toDto(entity)
|
||||||
}`
|
}`
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { ValueObject } from "./ValueObject"
|
import { ValueObject } from "./ValueObject"
|
||||||
import { FRAMEWORK_LEAK_MESSAGES } from "../../shared/constants/rules"
|
import { FRAMEWORK_LEAK_MESSAGES } from "../../shared/constants/rules"
|
||||||
|
import {
|
||||||
|
DEFAULT_FRAMEWORK_CATEGORY_DESCRIPTION,
|
||||||
|
FRAMEWORK_CATEGORY_DESCRIPTIONS,
|
||||||
|
} from "../constants/FrameworkCategories"
|
||||||
|
|
||||||
interface FrameworkLeakProps {
|
interface FrameworkLeakProps {
|
||||||
readonly packageName: string
|
readonly packageName: string
|
||||||
@@ -72,7 +76,10 @@ export class FrameworkLeak extends ValueObject<FrameworkLeakProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getMessage(): string {
|
public getMessage(): string {
|
||||||
return FRAMEWORK_LEAK_MESSAGES.DOMAIN_IMPORT.replace("{package}", this.props.packageName)
|
return FRAMEWORK_LEAK_MESSAGES.DOMAIN_IMPORT.replace(
|
||||||
|
FRAMEWORK_LEAK_MESSAGES.PACKAGE_PLACEHOLDER,
|
||||||
|
this.props.packageName,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSuggestion(): string {
|
public getSuggestion(): string {
|
||||||
@@ -80,33 +87,10 @@ export class FrameworkLeak extends ValueObject<FrameworkLeakProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getCategoryDescription(): string {
|
public getCategoryDescription(): string {
|
||||||
switch (this.props.category) {
|
return (
|
||||||
case "ORM":
|
FRAMEWORK_CATEGORY_DESCRIPTIONS[
|
||||||
return "Database ORM/ODM"
|
this.props.category as keyof typeof FRAMEWORK_CATEGORY_DESCRIPTIONS
|
||||||
case "WEB_FRAMEWORK":
|
] || DEFAULT_FRAMEWORK_CATEGORY_DESCRIPTION
|
||||||
return "Web Framework"
|
)
|
||||||
case "HTTP_CLIENT":
|
|
||||||
return "HTTP Client"
|
|
||||||
case "VALIDATION":
|
|
||||||
return "Validation Library"
|
|
||||||
case "DI_CONTAINER":
|
|
||||||
return "DI Container"
|
|
||||||
case "LOGGER":
|
|
||||||
return "Logger"
|
|
||||||
case "CACHE":
|
|
||||||
return "Cache"
|
|
||||||
case "MESSAGE_QUEUE":
|
|
||||||
return "Message Queue"
|
|
||||||
case "EMAIL":
|
|
||||||
return "Email Service"
|
|
||||||
case "STORAGE":
|
|
||||||
return "Storage Service"
|
|
||||||
case "TESTING":
|
|
||||||
return "Testing Framework"
|
|
||||||
case "TEMPLATE_ENGINE":
|
|
||||||
return "Template Engine"
|
|
||||||
default:
|
|
||||||
return "Framework Package"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IDependencyDirectionDetector } from "../../domain/services/IDependencyDirectionDetector"
|
import { IDependencyDirectionDetector } from "../../domain/services/IDependencyDirectionDetector"
|
||||||
import { DependencyViolation } from "../../domain/value-objects/DependencyViolation"
|
import { DependencyViolation } from "../../domain/value-objects/DependencyViolation"
|
||||||
import { LAYERS } from "../../shared/constants/rules"
|
import { LAYERS } from "../../shared/constants/rules"
|
||||||
|
import { IMPORT_PATTERNS, LAYER_PATHS } from "../constants/paths"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects dependency direction violations between architectural layers
|
* Detects dependency direction violations between architectural layers
|
||||||
@@ -118,13 +119,13 @@ export class DependencyDirectionDetector implements IDependencyDirectionDetector
|
|||||||
* @returns The layer name if detected, undefined otherwise
|
* @returns The layer name if detected, undefined otherwise
|
||||||
*/
|
*/
|
||||||
public extractLayerFromImport(importPath: string): string | undefined {
|
public extractLayerFromImport(importPath: string): string | undefined {
|
||||||
const normalizedPath = importPath.replace(/['"]/g, "").toLowerCase()
|
const normalizedPath = importPath.replace(IMPORT_PATTERNS.QUOTE, "").toLowerCase()
|
||||||
|
|
||||||
const layerPatterns: Array<[string, string]> = [
|
const layerPatterns: [string, string][] = [
|
||||||
[LAYERS.DOMAIN, "/domain/"],
|
[LAYERS.DOMAIN, LAYER_PATHS.DOMAIN],
|
||||||
[LAYERS.APPLICATION, "/application/"],
|
[LAYERS.APPLICATION, LAYER_PATHS.APPLICATION],
|
||||||
[LAYERS.INFRASTRUCTURE, "/infrastructure/"],
|
[LAYERS.INFRASTRUCTURE, LAYER_PATHS.INFRASTRUCTURE],
|
||||||
[LAYERS.SHARED, "/shared/"],
|
[LAYERS.SHARED, LAYER_PATHS.SHARED],
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const [layer, pattern] of layerPatterns) {
|
for (const [layer, pattern] of layerPatterns) {
|
||||||
@@ -163,19 +164,16 @@ export class DependencyDirectionDetector implements IDependencyDirectionDetector
|
|||||||
private extractImports(line: string): string[] {
|
private extractImports(line: string): string[] {
|
||||||
const imports: string[] = []
|
const imports: string[] = []
|
||||||
|
|
||||||
const esImportRegex =
|
let match = IMPORT_PATTERNS.ES_IMPORT.exec(line)
|
||||||
/import\s+(?:{[^}]*}|[\w*]+(?:\s+as\s+\w+)?|\*\s+as\s+\w+)\s+from\s+['"]([^'"]+)['"]/g
|
|
||||||
let match = esImportRegex.exec(line)
|
|
||||||
while (match) {
|
while (match) {
|
||||||
imports.push(match[1])
|
imports.push(match[1])
|
||||||
match = esImportRegex.exec(line)
|
match = IMPORT_PATTERNS.ES_IMPORT.exec(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
|
match = IMPORT_PATTERNS.REQUIRE.exec(line)
|
||||||
match = requireRegex.exec(line)
|
|
||||||
while (match) {
|
while (match) {
|
||||||
imports.push(match[1])
|
imports.push(match[1])
|
||||||
match = requireRegex.exec(line)
|
match = IMPORT_PATTERNS.REQUIRE.exec(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
return imports
|
return imports
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { IEntityExposureDetector } from "../../domain/services/IEntityExposureDetector"
|
import { IEntityExposureDetector } from "../../domain/services/IEntityExposureDetector"
|
||||||
import { EntityExposure } from "../../domain/value-objects/EntityExposure"
|
import { EntityExposure } from "../../domain/value-objects/EntityExposure"
|
||||||
import { LAYERS } from "../../shared/constants/rules"
|
import { LAYERS } from "../../shared/constants/rules"
|
||||||
|
import { DTO_SUFFIXES, NULLABLE_TYPES, PRIMITIVE_TYPES } from "../constants/type-patterns"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects domain entity exposure in controller/route return types
|
* Detects domain entity exposure in controller/route return types
|
||||||
@@ -29,15 +30,7 @@ import { LAYERS } from "../../shared/constants/rules"
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export class EntityExposureDetector implements IEntityExposureDetector {
|
export class EntityExposureDetector implements IEntityExposureDetector {
|
||||||
private readonly dtoSuffixes = [
|
private readonly dtoSuffixes = DTO_SUFFIXES
|
||||||
"Dto",
|
|
||||||
"DTO",
|
|
||||||
"Request",
|
|
||||||
"Response",
|
|
||||||
"Command",
|
|
||||||
"Query",
|
|
||||||
"Result",
|
|
||||||
]
|
|
||||||
private readonly controllerPatterns = [
|
private readonly controllerPatterns = [
|
||||||
/Controller/i,
|
/Controller/i,
|
||||||
/Route/i,
|
/Route/i,
|
||||||
@@ -167,7 +160,9 @@ export class EntityExposureDetector implements IEntityExposureDetector {
|
|||||||
|
|
||||||
if (cleanType.includes("|")) {
|
if (cleanType.includes("|")) {
|
||||||
const types = cleanType.split("|").map((t) => t.trim())
|
const types = cleanType.split("|").map((t) => t.trim())
|
||||||
const nonNullTypes = types.filter((t) => t !== "null" && t !== "undefined")
|
const nonNullTypes = types.filter(
|
||||||
|
(t) => !(NULLABLE_TYPES as readonly string[]).includes(t),
|
||||||
|
)
|
||||||
if (nonNullTypes.length > 0) {
|
if (nonNullTypes.length > 0) {
|
||||||
cleanType = nonNullTypes[0]
|
cleanType = nonNullTypes[0]
|
||||||
}
|
}
|
||||||
@@ -180,19 +175,7 @@ export class EntityExposureDetector implements IEntityExposureDetector {
|
|||||||
* Checks if a type is a primitive type
|
* Checks if a type is a primitive type
|
||||||
*/
|
*/
|
||||||
private isPrimitiveType(type: string): boolean {
|
private isPrimitiveType(type: string): boolean {
|
||||||
const primitives = [
|
return (PRIMITIVE_TYPES as readonly string[]).includes(type.toLowerCase())
|
||||||
"string",
|
|
||||||
"number",
|
|
||||||
"boolean",
|
|
||||||
"void",
|
|
||||||
"any",
|
|
||||||
"unknown",
|
|
||||||
"null",
|
|
||||||
"undefined",
|
|
||||||
"object",
|
|
||||||
"never",
|
|
||||||
]
|
|
||||||
return primitives.includes(type.toLowerCase())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -34,11 +34,27 @@ export class HardcodeDetector implements IHardcodeDetector {
|
|||||||
* @returns Array of detected hardcoded values with suggestions
|
* @returns Array of detected hardcoded values with suggestions
|
||||||
*/
|
*/
|
||||||
public detectAll(code: string, filePath: string): HardcodedValue[] {
|
public detectAll(code: string, filePath: string): HardcodedValue[] {
|
||||||
|
if (this.isConstantsFile(filePath)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
const magicNumbers = this.detectMagicNumbers(code, filePath)
|
const magicNumbers = this.detectMagicNumbers(code, filePath)
|
||||||
const magicStrings = this.detectMagicStrings(code, filePath)
|
const magicStrings = this.detectMagicStrings(code, filePath)
|
||||||
return [...magicNumbers, ...magicStrings]
|
return [...magicNumbers, ...magicStrings]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file is a constants definition file
|
||||||
|
*/
|
||||||
|
private isConstantsFile(filePath: string): boolean {
|
||||||
|
const fileName = filePath.split("/").pop() || ""
|
||||||
|
const constantsPatterns = [
|
||||||
|
/^constants?\.(ts|js)$/i,
|
||||||
|
/constants?\/.*\.(ts|js)$/i,
|
||||||
|
/\/(constants|config|settings|defaults)\.ts$/i,
|
||||||
|
]
|
||||||
|
return constantsPatterns.some((pattern) => pattern.test(filePath))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a line is inside an exported constant definition
|
* Check if a line is inside an exported constant definition
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
PATH_PATTERNS,
|
PATH_PATTERNS,
|
||||||
PATTERN_WORDS,
|
PATTERN_WORDS,
|
||||||
} from "../constants/detectorPatterns"
|
} from "../constants/detectorPatterns"
|
||||||
|
import { NAMING_SUGGESTION_DEFAULT } from "../constants/naming-patterns"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects naming convention violations based on Clean Architecture layers
|
* Detects naming convention violations based on Clean Architecture layers
|
||||||
@@ -72,7 +73,7 @@ export class NamingConventionDetector implements INamingConventionDetector {
|
|||||||
filePath,
|
filePath,
|
||||||
NAMING_ERROR_MESSAGES.DOMAIN_FORBIDDEN,
|
NAMING_ERROR_MESSAGES.DOMAIN_FORBIDDEN,
|
||||||
fileName,
|
fileName,
|
||||||
"Move to application or infrastructure layer, or rename to follow domain patterns",
|
NAMING_SUGGESTION_DEFAULT,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return violations
|
return violations
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ export const DEFAULT_EXCLUDES = [
|
|||||||
"coverage",
|
"coverage",
|
||||||
".git",
|
".git",
|
||||||
".puaros",
|
".puaros",
|
||||||
|
"tests",
|
||||||
|
"test",
|
||||||
|
"__tests__",
|
||||||
|
"examples",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"] as const
|
export const DEFAULT_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"] as const
|
||||||
|
|||||||
17
packages/guardian/src/infrastructure/constants/paths.ts
Normal file
17
packages/guardian/src/infrastructure/constants/paths.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export const LAYER_PATHS = {
|
||||||
|
DOMAIN: "/domain/",
|
||||||
|
APPLICATION: "/application/",
|
||||||
|
INFRASTRUCTURE: "/infrastructure/",
|
||||||
|
SHARED: "/shared/",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const CLI_PATHS = {
|
||||||
|
DIST_CLI_INDEX: "../dist/cli/index.js",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const IMPORT_PATTERNS = {
|
||||||
|
ES_IMPORT:
|
||||||
|
/import\s+(?:{[^}]*}|[\w*]+(?:\s+as\s+\w+)?|\*\s+as\s+\w+)\s+from\s+['"]([^'"]+)['"]/g,
|
||||||
|
REQUIRE: /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
||||||
|
QUOTE: /['"]/g,
|
||||||
|
} as const
|
||||||
@@ -3,6 +3,7 @@ import * as path from "path"
|
|||||||
import { FileScanOptions, IFileScanner } from "../../domain/services/IFileScanner"
|
import { FileScanOptions, IFileScanner } from "../../domain/services/IFileScanner"
|
||||||
import { DEFAULT_EXCLUDES, DEFAULT_EXTENSIONS, FILE_ENCODING } from "../constants/defaults"
|
import { DEFAULT_EXCLUDES, DEFAULT_EXTENSIONS, FILE_ENCODING } from "../constants/defaults"
|
||||||
import { ERROR_MESSAGES } from "../../shared/constants"
|
import { ERROR_MESSAGES } from "../../shared/constants"
|
||||||
|
import { TEST_FILE_EXTENSIONS, TEST_FILE_SUFFIXES } from "../constants/type-patterns"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scans project directory for source files
|
* Scans project directory for source files
|
||||||
@@ -56,7 +57,12 @@ export class FileScanner implements IFileScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private shouldExclude(name: string, excludePatterns: string[]): boolean {
|
private shouldExclude(name: string, excludePatterns: string[]): boolean {
|
||||||
return excludePatterns.some((pattern) => name.includes(pattern))
|
const isExcludedDirectory = excludePatterns.some((pattern) => name.includes(pattern))
|
||||||
|
const isTestFile =
|
||||||
|
(TEST_FILE_EXTENSIONS as readonly string[]).some((ext) => name.includes(ext)) ||
|
||||||
|
(TEST_FILE_SUFFIXES as readonly string[]).some((suffix) => name.endsWith(suffix))
|
||||||
|
|
||||||
|
return isExcludedDirectory || isTestFile
|
||||||
}
|
}
|
||||||
|
|
||||||
public async readFile(filePath: string): Promise<string> {
|
public async readFile(filePath: string): Promise<string> {
|
||||||
|
|||||||
@@ -64,9 +64,36 @@ export const PLACEHOLDERS = {
|
|||||||
* Violation severity levels
|
* Violation severity levels
|
||||||
*/
|
*/
|
||||||
export const SEVERITY_LEVELS = {
|
export const SEVERITY_LEVELS = {
|
||||||
ERROR: "error",
|
CRITICAL: "critical",
|
||||||
WARNING: "warning",
|
HIGH: "high",
|
||||||
INFO: "info",
|
MEDIUM: "medium",
|
||||||
|
LOW: "low",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type SeverityLevel = (typeof SEVERITY_LEVELS)[keyof typeof SEVERITY_LEVELS]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Severity order for sorting (lower number = more critical)
|
||||||
|
*/
|
||||||
|
export const SEVERITY_ORDER: Record<SeverityLevel, number> = {
|
||||||
|
[SEVERITY_LEVELS.CRITICAL]: 0,
|
||||||
|
[SEVERITY_LEVELS.HIGH]: 1,
|
||||||
|
[SEVERITY_LEVELS.MEDIUM]: 2,
|
||||||
|
[SEVERITY_LEVELS.LOW]: 3,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Violation type to severity mapping
|
||||||
|
*/
|
||||||
|
export const VIOLATION_SEVERITY_MAP = {
|
||||||
|
CIRCULAR_DEPENDENCY: SEVERITY_LEVELS.CRITICAL,
|
||||||
|
REPOSITORY_PATTERN: SEVERITY_LEVELS.CRITICAL,
|
||||||
|
DEPENDENCY_DIRECTION: SEVERITY_LEVELS.HIGH,
|
||||||
|
FRAMEWORK_LEAK: SEVERITY_LEVELS.HIGH,
|
||||||
|
ENTITY_EXPOSURE: SEVERITY_LEVELS.HIGH,
|
||||||
|
NAMING_CONVENTION: SEVERITY_LEVELS.MEDIUM,
|
||||||
|
ARCHITECTURE: SEVERITY_LEVELS.MEDIUM,
|
||||||
|
HARDCODE: SEVERITY_LEVELS.LOW,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export * from "./rules"
|
export * from "./rules"
|
||||||
|
|||||||
15
packages/guardian/src/shared/constants/layers.ts
Normal file
15
packages/guardian/src/shared/constants/layers.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export const LAYER_DOMAIN = "domain"
|
||||||
|
export const LAYER_APPLICATION = "application"
|
||||||
|
export const LAYER_INFRASTRUCTURE = "infrastructure"
|
||||||
|
export const LAYER_SHARED = "shared"
|
||||||
|
export const LAYER_CLI = "cli"
|
||||||
|
|
||||||
|
export const LAYERS = [
|
||||||
|
LAYER_DOMAIN,
|
||||||
|
LAYER_APPLICATION,
|
||||||
|
LAYER_INFRASTRUCTURE,
|
||||||
|
LAYER_SHARED,
|
||||||
|
LAYER_CLI,
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type Layer = (typeof LAYERS)[number]
|
||||||
Reference in New Issue
Block a user