mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b953956181 | ||
|
|
af094eb54a | ||
|
|
656571860e | ||
|
|
a6b4c69b75 | ||
|
|
1d6c2a0e00 | ||
|
|
db8a97202e |
17
README.md
17
README.md
@@ -4,7 +4,7 @@ A TypeScript monorepo for code quality and analysis tools.
|
||||
|
||||
## Packages
|
||||
|
||||
- **[@puaros/guardian](./packages/guardian)** - Code quality guardian for vibe coders and enterprise teams. Detects hardcoded values, circular dependencies, and architecture violations. Perfect for AI-assisted development and enforcing Clean Architecture at scale.
|
||||
- **[@puaros/guardian](./packages/guardian)** - Research-backed code quality guardian for vibe coders and enterprise teams. Detects hardcoded values, secrets, circular dependencies, architecture violations, and anemic domain models. Every rule is based on academic research, industry standards (OWASP, SonarQube), and authoritative books (Martin Fowler, Uncle Bob, Eric Evans). Perfect for AI-assisted development and enforcing Clean Architecture at scale.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -147,6 +147,21 @@ The `@puaros/guardian` package is a code quality analyzer for both individual de
|
||||
- **CLI Tool**: Command-line interface with `guardian` command
|
||||
- **CI/CD Integration**: JSON/Markdown output for automation pipelines
|
||||
|
||||
### 📚 Research-Backed Rules
|
||||
|
||||
Guardian's detection rules are based on decades of software engineering research and industry best practices:
|
||||
|
||||
- **Academic Research**: MIT Course 6.031, ScienceDirect peer-reviewed studies (2020-2023), IEEE papers on Technical Debt
|
||||
- **Industry Standards**: SonarQube (400,000+ organizations), Google/Airbnb/Microsoft style guides, OWASP security standards
|
||||
- **Authoritative Books**:
|
||||
- Clean Architecture (Robert C. Martin, 2017)
|
||||
- Implementing Domain-Driven Design (Vaughn Vernon, 2013)
|
||||
- Domain-Driven Design (Eric Evans, 2003)
|
||||
- Patterns of Enterprise Application Architecture (Martin Fowler, 2002)
|
||||
- **Security Standards**: OWASP Secrets Management, GitHub Secret Scanning (350+ patterns)
|
||||
|
||||
**Every rule links to research citations** - see [Why Guardian's Rules Matter](./packages/guardian/docs/WHY.md) and [Full Research Citations](./packages/guardian/docs/RESEARCH_CITATIONS.md) for complete academic papers, books, and expert references.
|
||||
|
||||
### Use Cases
|
||||
|
||||
**For Vibe Coders:**
|
||||
|
||||
@@ -5,6 +5,126 @@ 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.9.1] - 2025-11-26
|
||||
|
||||
### Changed
|
||||
|
||||
- 🔄 **Refactored hardcode detector** - Migrated from regex-based to AST-based analysis:
|
||||
- Replaced regex pattern matching with tree-sitter Abstract Syntax Tree traversal
|
||||
- Improved accuracy with AST node context awareness (exports, types, tests)
|
||||
- Reduced false positives with better constant and context detection
|
||||
- Added duplicate value tracking across files for better insights
|
||||
- Implemented boolean literal detection (magic-boolean type)
|
||||
- Added value type classification (email, url, ip_address, api_key, uuid, version, color, etc.)
|
||||
- New modular architecture with specialized analyzers:
|
||||
- `AstTreeTraverser` - AST walking with "almost constants" detection
|
||||
- `DuplicateValueTracker` - Cross-file duplicate tracking
|
||||
- `AstContextChecker` - Node context analysis (reduced nesting depth)
|
||||
- `AstNumberAnalyzer`, `AstStringAnalyzer`, `AstBooleanAnalyzer` - Specialized analyzers
|
||||
- `ValuePatternMatcher` - Value type pattern detection
|
||||
|
||||
### Removed
|
||||
|
||||
- 🗑️ **Deprecated regex components** - Removed old regex-based detection strategies:
|
||||
- `BraceTracker.ts` - Replaced by AST context checking
|
||||
- `ExportConstantAnalyzer.ts` - Replaced by AstContextChecker
|
||||
- `MagicNumberMatcher.ts` - Replaced by AstNumberAnalyzer
|
||||
- `MagicStringMatcher.ts` - Replaced by AstStringAnalyzer
|
||||
|
||||
### Quality
|
||||
|
||||
- ✅ **All tests pass** - 629/629 tests passing (added 51 new tests)
|
||||
- ✅ **Test coverage** - 87.97% statements, 96.75% functions
|
||||
- ✅ **Build successful** - TypeScript compilation with no errors
|
||||
- ✅ **Linter** - 0 errors, 5 acceptable warnings (complexity, params)
|
||||
- ✅ **Code size** - Net reduction: -40 lines (more features, less code)
|
||||
|
||||
## [0.9.0] - 2025-11-26
|
||||
|
||||
### Added
|
||||
|
||||
- 🏛️ **Anemic Model Detection** - NEW feature to detect anemic domain models lacking business logic:
|
||||
- Detects entities with only getters/setters (violates DDD principles)
|
||||
- Identifies classes with public setters (breaks encapsulation)
|
||||
- Analyzes method-to-property ratio to find data-heavy, logic-light classes
|
||||
- Provides detailed suggestions: add business methods, move logic from services, encapsulate invariants
|
||||
- New `AnemicModelDetector` infrastructure component
|
||||
- New `AnemicModelViolation` value object with rich example fixes
|
||||
- New `IAnemicModelDetector` domain interface
|
||||
- Integrated into CLI with detailed violation reports
|
||||
- 12 comprehensive tests for anemic model detection
|
||||
|
||||
- 📦 **New shared constants** - Centralized constants for better code maintainability:
|
||||
- `CLASS_KEYWORDS` - TypeScript class and method keywords (constructor, public, private, protected)
|
||||
- `EXAMPLE_CODE_CONSTANTS` - Documentation example code strings (ORDER_STATUS_PENDING, ORDER_STATUS_APPROVED, CANNOT_APPROVE_ERROR)
|
||||
- `ANEMIC_MODEL_MESSAGES` - 8 suggestion messages for fixing anemic models
|
||||
|
||||
- 📚 **Example files** - Added DDD examples demonstrating anemic vs rich domain models:
|
||||
- `examples/bad/domain/entities/anemic-model-only-getters-setters.ts`
|
||||
- `examples/bad/domain/entities/anemic-model-public-setters.ts`
|
||||
- `examples/good-architecture/domain/entities/Customer.ts`
|
||||
- `examples/good-architecture/domain/entities/Order.ts`
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️ **Refactored hardcoded values** - Extracted all remaining hardcoded values to centralized constants:
|
||||
- Updated `AnemicModelDetector.ts` to use `CLASS_KEYWORDS` constants
|
||||
- Updated `AnemicModelViolation.ts` to use `EXAMPLE_CODE_CONSTANTS` for example fix strings
|
||||
- Replaced local constants with shared constants from `shared/constants`
|
||||
- Improved code maintainability and consistency
|
||||
|
||||
- 🎯 **Enhanced violation detection pipeline** - Added anemic model detection to `ExecuteDetection.ts`
|
||||
- 📊 **Updated API** - Added anemic model violations to response DTO
|
||||
- 🔧 **CLI improvements** - Added anemic model section to output formatting
|
||||
|
||||
### Quality
|
||||
|
||||
- ✅ **Guardian self-check** - 0 issues (was 5) - 100% clean codebase
|
||||
- ✅ **All tests pass** - 578/578 tests passing (added 12 new tests)
|
||||
- ✅ **Build successful** - TypeScript compilation with no errors
|
||||
- ✅ **Linter clean** - 0 errors, 3 acceptable warnings (complexity, params)
|
||||
- ✅ **Format verified** - All files properly formatted with 4-space indentation
|
||||
|
||||
## [0.8.1] - 2025-11-25
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🧹 **Code quality improvements** - Fixed all 63 hardcoded value issues detected by Guardian self-check:
|
||||
- Fixed 1 CRITICAL: Removed hardcoded Slack token from documentation examples
|
||||
- Fixed 1 HIGH: Removed aws-sdk framework leak from domain layer examples
|
||||
- Fixed 4 MEDIUM: Renamed pipeline files to follow verb-noun convention
|
||||
- Fixed 57 LOW: Extracted all magic strings to reusable constants
|
||||
|
||||
### Added
|
||||
|
||||
- 📦 **New constants file** - `domain/constants/SecretExamples.ts`:
|
||||
- 32 secret keyword constants (AWS, GitHub, NPM, SSH, Slack, etc.)
|
||||
- 15 secret type name constants
|
||||
- 7 example secret values for documentation
|
||||
- Regex patterns and encoding constants
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️ **Refactored pipeline naming** - Updated use case files to follow naming conventions:
|
||||
- `DetectionPipeline.ts` → `ExecuteDetection.ts`
|
||||
- `FileCollectionStep.ts` → `CollectFiles.ts`
|
||||
- `ParsingStep.ts` → `ParseSourceFiles.ts`
|
||||
- `ResultAggregator.ts` → `AggregateResults.ts`
|
||||
- Added `Aggregate`, `Collect`, `Parse` to `USE_CASE_VERBS` list
|
||||
- 🔧 **Updated 3 core files to use constants**:
|
||||
- `SecretViolation.ts`: All secret examples use constants, `getSeverity()` returns `typeof SEVERITY_LEVELS.CRITICAL`
|
||||
- `SecretDetector.ts`: All secret keywords use constants
|
||||
- `MagicStringMatcher.ts`: Regex patterns extracted to constants
|
||||
- 📝 **Test updates** - Updated 2 tests to match new example fix messages
|
||||
|
||||
### Quality
|
||||
|
||||
- ✅ **Guardian self-check** - 0 issues (was 63) - 100% clean codebase
|
||||
- ✅ **All tests pass** - 566/566 tests passing
|
||||
- ✅ **Build successful** - TypeScript compilation with no errors
|
||||
- ✅ **Linter clean** - 0 errors, 2 acceptable warnings (complexity, params)
|
||||
- ✅ **Format verified** - All files properly formatted with 4-space indentation
|
||||
|
||||
## [0.8.0] - 2025-11-25
|
||||
|
||||
### Added
|
||||
|
||||
@@ -79,7 +79,7 @@ Code quality guardian for vibe coders and enterprise teams - because AI writes f
|
||||
- Supports multiple folder structures (domain/aggregates/*, domain/*, domain/entities/*)
|
||||
- Filters allowed imports (value-objects, events, repositories, services)
|
||||
- Critical severity for maintaining aggregate independence
|
||||
- 📚 *Based on: Domain-Driven Design (Evans 2003), Implementing DDD (Vernon 2013)* → [Why?](./docs/WHY.md#aggregate-boundaries)
|
||||
- 📚 *Based on: Domain-Driven Design (Evans 2003), Implementing DDD (Vernon 2013)* → [Why?](./docs/WHY.md#aggregate-boundary-validation)
|
||||
|
||||
🔐 **Secret Detection** ✨ NEW in v0.8.0
|
||||
- Detects 350+ types of hardcoded secrets using industry-standard Secretlint
|
||||
@@ -88,7 +88,25 @@ Code quality guardian for vibe coders and enterprise teams - because AI writes f
|
||||
- Context-aware remediation suggestions for each secret type
|
||||
- Prevents credentials from reaching version control
|
||||
- Integrates seamlessly with existing detectors
|
||||
- 📚 *Based on: OWASP Top 10, CWE-798 (Hardcoded Credentials), NIST Security Guidelines* → [Learn more](https://owasp.org/www-community/vulnerabilities/Use_of_hard-coded_password)
|
||||
- 📚 *Based on: OWASP Secrets Management, GitHub Secret Scanning (350+ patterns), security standards* → [Why?](./docs/WHY.md#secret-detection)
|
||||
|
||||
🩺 **Anemic Domain Model Detection** ✨ NEW in v0.9.0
|
||||
- Detects entities with only getters/setters (data bags without behavior)
|
||||
- Identifies public setters anti-pattern in domain entities
|
||||
- Calculates methods-to-properties ratio for behavioral analysis
|
||||
- Enforces rich domain models over anemic models
|
||||
- Suggests moving business logic from services to entities
|
||||
- Medium severity - architectural code smell
|
||||
- 📚 *Based on: Martin Fowler's "Anemic Domain Model" (2003), DDD (Evans 2003), Transaction Script vs Domain Model patterns* → [Why?](./docs/WHY.md#anemic-domain-model-detection)
|
||||
|
||||
🎯 **Severity-Based Prioritization**
|
||||
- Automatic sorting by severity: CRITICAL → HIGH → MEDIUM → LOW
|
||||
- Filter by severity level: `--only-critical` or `--min-severity high`
|
||||
- Focus on what matters most: secrets and circular dependencies first
|
||||
- Visual severity indicators with color-coded labels (🔴🟠🟡🟢)
|
||||
- Smart categorization based on impact to production
|
||||
- Enables gradual technical debt reduction
|
||||
- 📚 *Based on: SonarQube severity classification, IEEE/ScienceDirect research on Technical Debt prioritization* → [Why?](./docs/WHY.md#severity-based-prioritization)
|
||||
|
||||
🏗️ **Clean Architecture Enforcement**
|
||||
- Built with DDD principles
|
||||
|
||||
@@ -2,7 +2,20 @@
|
||||
|
||||
This document outlines the current features and future plans for @puaros/guardian.
|
||||
|
||||
## Current Version: 0.7.5 ✅ RELEASED
|
||||
## Current Version: 0.9.0 ✅ RELEASED
|
||||
|
||||
**Released:** 2025-11-26
|
||||
|
||||
### What's New in 0.9.0
|
||||
|
||||
- 🏛️ **Anemic Model Detection** - NEW feature to detect anemic domain models lacking business logic
|
||||
- ✅ **100% clean codebase** - Guardian now passes its own self-check with 0 issues
|
||||
- 📦 **New shared constants** - Added CLASS_KEYWORDS and EXAMPLE_CODE_CONSTANTS
|
||||
- ✅ **All 578 tests passing** - Added 12 new tests for anemic model detection
|
||||
|
||||
---
|
||||
|
||||
## Previous Version: 0.8.1 ✅ RELEASED
|
||||
|
||||
**Released:** 2025-11-25
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ This document provides authoritative sources, academic papers, industry standard
|
||||
8. [General Software Quality Standards](#8-general-software-quality-standards)
|
||||
9. [Code Complexity Metrics](#9-code-complexity-metrics)
|
||||
10. [Additional Authoritative Sources](#10-additional-authoritative-sources)
|
||||
11. [Anemic Domain Model Detection](#11-anemic-domain-model-detection)
|
||||
12. [Aggregate Boundary Validation (DDD Tactical Patterns)](#12-aggregate-boundary-validation-ddd-tactical-patterns)
|
||||
13. [Secret Detection & Security](#13-secret-detection--security)
|
||||
14. [Severity-Based Prioritization & Technical Debt](#14-severity-based-prioritization--technical-debt)
|
||||
|
||||
---
|
||||
|
||||
@@ -503,22 +507,318 @@ This document provides authoritative sources, academic papers, industry standard
|
||||
|
||||
---
|
||||
|
||||
## 11. Anemic Domain Model Detection
|
||||
|
||||
### Martin Fowler's Original Blog Post (2003)
|
||||
|
||||
**Blog Post: "Anemic Domain Model"** (November 25, 2003)
|
||||
- Author: Martin Fowler
|
||||
- Published: November 25, 2003
|
||||
- Described as an anti-pattern related to domain driven design and application architecture
|
||||
- Basic symptom: domain objects have hardly any behavior, making them little more than bags of getters and setters
|
||||
- Reference: [Martin Fowler - Anemic Domain Model](https://martinfowler.com/bliki/AnemicDomainModel.html)
|
||||
|
||||
**Key Problems Identified:**
|
||||
- "The basic symptom of an Anemic Domain Model is that at first blush it looks like the real thing"
|
||||
- "There are objects, many named after the nouns in the domain space, and these objects are connected with the rich relationships and structure that true domain models have"
|
||||
- "The catch comes when you look at the behavior, and you realize that there is hardly any behavior on these objects"
|
||||
- "This is contrary to the basic idea of object-oriented design; which is to combine data and process together"
|
||||
|
||||
**Why It's an Anti-pattern:**
|
||||
- Fowler argues that anemic domain models incur all of the costs of a domain model, without yielding any of the benefits
|
||||
- The logic that should be in a domain object is domain logic - validations, calculations, business rules
|
||||
- Separating data from behavior violates core OOP principles
|
||||
- Reference: [Wikipedia - Anemic Domain Model](https://en.wikipedia.org/wiki/Anemic_domain_model)
|
||||
|
||||
### Rich Domain Model vs Transaction Script
|
||||
|
||||
**Martin Fowler: Transaction Script Pattern**
|
||||
- Transaction Script organizes business logic by procedures where each procedure handles a single request
|
||||
- Good for simple logic with not-null checks and basic calculations
|
||||
- Reference: [Martin Fowler - Transaction Script](https://martinfowler.com/eaaCatalog/transactionScript.html)
|
||||
|
||||
**When to Use Rich Domain Model:**
|
||||
- If you have complicated and everchanging business rules involving validation, calculations, and derivations
|
||||
- Object model handles complex domain logic better than procedural scripts
|
||||
- Reference: [InformIT - Domain Logic Patterns](https://www.informit.com/articles/article.aspx?p=1398617&seqNum=2)
|
||||
|
||||
**Comparison:**
|
||||
- Transaction Script is better for simple logic
|
||||
- Domain Model is better when things get complicated with complex business rules
|
||||
- You can refactor from Transaction Script to Domain Model, but it's a harder change
|
||||
- Reference: [Medium - Transaction Script vs Domain Model](https://medium.com/@vibstudio_7040/transaction-script-active-record-and-domain-model-the-good-the-bad-and-the-ugly-c5b80a733305)
|
||||
|
||||
### Domain-Driven Design Context
|
||||
|
||||
**Eric Evans: Domain-Driven Design** (2003)
|
||||
- Entities should have both identity and behavior
|
||||
- Rich domain models place business logic within domain entities
|
||||
- Anemic models violate DDD principles by separating data from behavior
|
||||
- Reference: Already covered in Section 10 - [Domain-Driven Design Book](#domain-driven-design)
|
||||
|
||||
**Community Discussion:**
|
||||
- Some argue anemic models can follow SOLID design principles
|
||||
- However, consensus among DDD practitioners aligns with Fowler's anti-pattern view
|
||||
- Reference: [Stack Overflow - Anemic Domain Model Anti-Pattern](https://stackoverflow.com/questions/6293981/concrete-examples-on-why-the-anemic-domain-model-is-considered-an-anti-pattern)
|
||||
|
||||
---
|
||||
|
||||
## 12. Aggregate Boundary Validation (DDD Tactical Patterns)
|
||||
|
||||
### Eric Evans: Domain-Driven Design (2003)
|
||||
|
||||
**Original Book Definition:**
|
||||
- Aggregate: "A cluster of associated objects that we treat as a unit for the purpose of data changes"
|
||||
- An aggregate defines a consistency boundary around one or more entities
|
||||
- Exactly one entity in an aggregate is the root
|
||||
- Reference: [Microsoft Learn - Tactical DDD](https://learn.microsoft.com/en-us/azure/architecture/microservices/model/tactical-ddd)
|
||||
|
||||
**DDD Reference Document** (2015)
|
||||
- Official Domain-Driven Design Reference by Eric Evans
|
||||
- Contains comprehensive definitions of Aggregates and boundaries
|
||||
- Reference: [Domain Language - DDD Reference PDF](https://www.domainlanguage.com/wp-content/uploads/2016/05/DDD_Reference_2015-03.pdf)
|
||||
|
||||
### Vaughn Vernon: Implementing Domain-Driven Design (2013)
|
||||
|
||||
**Chapter 10: Aggregates** (Page 347)
|
||||
- Author: Vaughn Vernon
|
||||
- Publisher: Addison-Wesley
|
||||
- ISBN: 978-0321834577
|
||||
- Available at: [Amazon - Implementing DDD](https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577)
|
||||
|
||||
**Key Rules from the Chapter:**
|
||||
- **Rule: Model True Invariants in Consistency Boundaries**
|
||||
- **Rule: Design Small Aggregates**
|
||||
- **Rule: Reference Other Aggregates by Identity**
|
||||
- **Rule: Use Eventual Consistency Outside the Boundary**
|
||||
|
||||
**Effective Aggregate Design Series:**
|
||||
- Three-part essay series by Vaughn Vernon
|
||||
- Available as downloadable PDFs
|
||||
- Licensed under Creative Commons Attribution-NoDerivs 3.0
|
||||
- Reference: [Kalele - Effective Aggregate Design](https://kalele.io/effective-aggregate-design/)
|
||||
|
||||
**Appendix A: Aggregates and Event Sourcing:**
|
||||
- Additional coverage of aggregate patterns
|
||||
- Practical implementation guidance
|
||||
- Reference: Available in the book
|
||||
|
||||
### Tactical DDD Patterns
|
||||
|
||||
**Microsoft Azure Architecture Center:**
|
||||
- "Using tactical DDD to design microservices"
|
||||
- Official Microsoft documentation on aggregate boundaries
|
||||
- Comprehensive guide for microservices architecture
|
||||
- Reference: [Microsoft Learn - Tactical DDD](https://learn.microsoft.com/en-us/azure/architecture/microservices/model/tactical-ddd)
|
||||
|
||||
**SOCADK Design Practice Repository:**
|
||||
- Summaries of artifacts, templates, and techniques for tactical DDD
|
||||
- Practical examples of aggregate boundary enforcement
|
||||
- Reference: [SOCADK - Tactical DDD](https://socadk.github.io/design-practice-repository/activities/DPR-TacticDDD.html)
|
||||
|
||||
### Why Aggregate Boundaries Matter
|
||||
|
||||
**Transactional Boundary:**
|
||||
- What makes it an aggregate is the transactional boundary
|
||||
- Changes to aggregate must be atomic
|
||||
- Ensures consistency within the boundary
|
||||
- Reference: [Medium - Mastering Aggregate Design](https://medium.com/ssense-tech/ddd-beyond-the-basics-mastering-aggregate-design-26591e218c8c)
|
||||
|
||||
**Cross-Aggregate References:**
|
||||
- Aggregates should only reference other aggregates by ID, not direct entity references
|
||||
- Prevents tight coupling between aggregates
|
||||
- Maintains clear boundaries
|
||||
- Reference: [Lev Gorodinski - Two Sides of DDD](http://gorodinski.com/blog/2013/03/11/the-two-sides-of-domain-driven-design/)
|
||||
|
||||
---
|
||||
|
||||
## 13. Secret Detection & Security
|
||||
|
||||
### OWASP Standards
|
||||
|
||||
**OWASP Secrets Management Cheat Sheet**
|
||||
- Official OWASP best practices and guidelines for secrets management
|
||||
- Comprehensive coverage of hardcoded credentials risks
|
||||
- Reference: [OWASP - Secrets Management](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
|
||||
|
||||
**OWASP DevSecOps Guideline**
|
||||
- Section on Secrets Management (v-0.2)
|
||||
- Integration with CI/CD pipelines
|
||||
- Reference: [OWASP - DevSecOps Secrets](https://owasp.org/www-project-devsecops-guideline/latest/01a-Secrets-Management)
|
||||
|
||||
**OWASP Password Management: Hardcoded Password**
|
||||
- Vulnerability documentation on hardcoded passwords
|
||||
- "It is never a good idea to hardcode a password"
|
||||
- Makes fixing the problem extremely difficult
|
||||
- Reference: [OWASP - Hardcoded Password Vulnerability](https://owasp.org/www-community/vulnerabilities/Use_of_hard-coded_password)
|
||||
|
||||
### Key Security Principles
|
||||
|
||||
**Don't Hardcode Secrets:**
|
||||
- Secrets should not be hardcoded
|
||||
- Should not be unencrypted
|
||||
- Should not be stored in source code
|
||||
- Reference: [OWASP Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
|
||||
|
||||
**Centralized Management:**
|
||||
- Growing need to centralize storage, provisioning, auditing, rotation, and management of secrets
|
||||
- Control access and prevent secrets from leaking
|
||||
- Use purpose-built tools for encryption-at-rest
|
||||
- Reference: [OWASP SAMM - Secret Management](https://owaspsamm.org/model/implementation/secure-deployment/stream-b/)
|
||||
|
||||
**Prevention Tools:**
|
||||
- Use pre-commit hooks to prevent secrets from entering codebase
|
||||
- Automated scanning in CI/CD pipelines
|
||||
- Reference: [GitHub OWASP Secrets Management](https://github.com/dominikdesmit/owasp-secrets-management)
|
||||
|
||||
### GitHub Secret Scanning
|
||||
|
||||
**Official GitHub Documentation:**
|
||||
- About Secret Scanning: Automated detection of secrets in repositories
|
||||
- Scans for patterns and heuristics matching known types of secrets
|
||||
- Reference: [GitHub Docs - Secret Scanning](https://docs.github.com/code-security/secret-scanning/about-secret-scanning)
|
||||
|
||||
**How It Works:**
|
||||
- Automatically scans repository contents for sensitive data (API keys, passwords, tokens)
|
||||
- Scans commits, issues, and pull requests continuously
|
||||
- Real-time alerts to repository administrators
|
||||
- Reference: [GitHub Docs - Keeping Secrets Secure](https://docs.github.com/en/code-security/secret-scanning)
|
||||
|
||||
**AI-Powered Detection:**
|
||||
- Copilot Secret Scanning uses large language models (LLMs)
|
||||
- Identifies unstructured secrets (generic passwords) in source code
|
||||
- Enhances detection beyond pattern matching
|
||||
- Reference: [GitHub Docs - Copilot Secret Scanning](https://docs.github.com/en/code-security/secret-scanning/copilot-secret-scanning)
|
||||
|
||||
**Supported Patterns:**
|
||||
- 350+ secret patterns detected
|
||||
- AWS, GitHub, NPM, SSH, GCP, Slack, Basic Auth, JWT tokens
|
||||
- Reference: [GitHub Docs - Supported Patterns](https://docs.github.com/en/code-security/secret-scanning/introduction/supported-secret-scanning-patterns)
|
||||
|
||||
### Mobile Security
|
||||
|
||||
**OWASP Mobile Security:**
|
||||
- "Secrets security is the most important issue for mobile applications"
|
||||
- Only safe way: keep secrets off the client side entirely
|
||||
- Move sensitive information to backend
|
||||
- Reference: [GitGuardian - OWASP Top 10 Mobile](https://blog.gitguardian.com/owasp-top-10-for-mobile-secrets/)
|
||||
|
||||
### Third-Party Tools
|
||||
|
||||
**GitGuardian:**
|
||||
- Secrets security and non-human identity governance
|
||||
- Enterprise-grade secret detection
|
||||
- Reference: [GitGuardian Official Site](https://www.gitguardian.com/)
|
||||
|
||||
**Yelp detect-secrets:**
|
||||
- Open-source enterprise-friendly secret detection
|
||||
- Prevent secrets in code
|
||||
- Reference: [GitHub - Yelp detect-secrets](https://github.com/Yelp/detect-secrets)
|
||||
|
||||
---
|
||||
|
||||
## 14. Severity-Based Prioritization & Technical Debt
|
||||
|
||||
### Academic Research on Technical Debt Prioritization
|
||||
|
||||
**Systematic Literature Review** (2020)
|
||||
- Title: "A systematic literature review on Technical Debt prioritization"
|
||||
- Analyzed 557 unique papers, included 44 primary studies
|
||||
- Finding: "Technical Debt prioritization research is preliminary and there is no consensus on what the important factors are and how to measure them"
|
||||
- Reference: [ScienceDirect - TD Prioritization](https://www.sciencedirect.com/science/article/pii/S016412122030220X)
|
||||
|
||||
**IEEE Conference Paper** (2021)
|
||||
- Title: "Technical Debt Prioritization: Taxonomy, Methods Results, and Practical Characteristics"
|
||||
- Systematic mapping review of 112 studies, resulting in 51 unique papers
|
||||
- Classified methods in two-level taxonomy with 10 categories
|
||||
- Reference: [IEEE Xplore - TD Prioritization](https://ieeexplore.ieee.org/document/9582595/)
|
||||
|
||||
**Identifying Severity of Technical Debt** (2023)
|
||||
- Journal: Software Quality Journal
|
||||
- Title: "Identifying the severity of technical debt issues based on semantic and structural information"
|
||||
- Problem: "Existing studies mainly focus on detecting TD through source code or comments but usually ignore the severity degree of TD issues"
|
||||
- Proposed approach combining semantic and structural information
|
||||
- Reference: [Springer - TD Severity](https://link.springer.com/article/10.1007/s11219-023-09651-3)
|
||||
|
||||
### SonarQube Severity Classification
|
||||
|
||||
**Current Severity Levels** (SonarQube 10.2+)
|
||||
- Severity levels: **info, low, medium, high, and blocker**
|
||||
- Reference: [SonarQube Docs - Metrics Definition](https://docs.sonarsource.com/sonarqube-server/user-guide/code-metrics/metrics-definition)
|
||||
|
||||
**High/Blocker Severity:**
|
||||
- An issue with significant probability of severe unintended consequences
|
||||
- Should be fixed immediately
|
||||
- Includes bugs leading to production crashes
|
||||
- Security flaws allowing attackers to extract sensitive data or execute malicious code
|
||||
- Reference: [SonarQube Docs - Metrics](https://docs.sonarsource.com/sonarqube-server/10.8/user-guide/code-metrics/metrics-definition)
|
||||
|
||||
**Medium Severity:**
|
||||
- Quality flaw that can highly impact developer's productivity
|
||||
- Uncovered code, duplicated blocks, unused parameters
|
||||
- Reference: [SonarQube Documentation](https://docs.sonarsource.com/sonarqube-server/10.8/user-guide/code-metrics/metrics-definition)
|
||||
|
||||
**Low Severity:**
|
||||
- Quality flaw with slight impact on developer productivity
|
||||
- Lines too long, switch statements with few cases
|
||||
- Reference: [SonarQube Documentation](https://docs.sonarsource.com/sonarqube-server/10.8/user-guide/code-metrics/metrics-definition)
|
||||
|
||||
**Info Severity:**
|
||||
- No expected impact on application
|
||||
- Informational purposes only
|
||||
- Reference: [SonarQube Documentation](https://docs.sonarsource.com/sonarqube-server/10.8/user-guide/code-metrics/metrics-definition)
|
||||
|
||||
### Legacy SonarQube Classification (pre-10.2)
|
||||
|
||||
**Five Severity Levels:**
|
||||
- **BLOCKER**: Bug with high probability to impact behavior in production (memory leak, unclosed JDBC connection)
|
||||
- **CRITICAL**: Bug with low probability to impact production behavior OR security flaw (empty catch block, SQL injection)
|
||||
- **MAJOR**: Quality flaw highly impacting developer productivity (uncovered code, duplicated blocks, unused parameters)
|
||||
- **MINOR**: Quality flaw slightly impacting developer productivity (lines too long, switch statements < 3 cases)
|
||||
- **INFO**: Informational only
|
||||
- Reference: [SonarQube Community - Severity Categories](https://community.sonarsource.com/t/sonarqube-severity-categories/115287)
|
||||
|
||||
### Research on Impact and Effectiveness
|
||||
|
||||
**Empirical Study** (2020)
|
||||
- Title: "Some SonarQube issues have a significant but small effect on faults and changes"
|
||||
- Published in: ScienceDirect (Information and Software Technology)
|
||||
- Large-scale empirical study on SonarQube issue impact
|
||||
- Reference: [ScienceDirect - SonarQube Issues](https://www.sciencedirect.com/science/article/abs/pii/S0164121220301734)
|
||||
|
||||
**Machine Learning for Prioritization** (2024)
|
||||
- Recent approaches: "Development teams could integrate models into CI/CD pipelines"
|
||||
- Automatically flag potential TD issues during code reviews
|
||||
- Prioritize based on severity
|
||||
- Reference: [arXiv - Technical Debt Management](https://arxiv.org/html/2403.06484v1)
|
||||
|
||||
### Multiple-Case Study
|
||||
|
||||
**Aligning TD with Business Objectives** (2018)
|
||||
- Title: "Aligning Technical Debt Prioritization with Business Objectives: A Multiple-Case Study"
|
||||
- Demonstrates importance of priority-based technical debt management
|
||||
- Reference: [ResearchGate - TD Business Alignment](https://www.researchgate.net/publication/328903587_Aligning_Technical_Debt_Prioritization_with_Business_Objectives_A_Multiple-Case_Study)
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The code quality detection rules implemented in Guardian are firmly grounded in:
|
||||
|
||||
1. **Academic Research**: Peer-reviewed papers on software maintainability, complexity metrics, and code quality
|
||||
2. **Industry Standards**: ISO/IEC 25010, SonarQube rules, Google and Airbnb style guides
|
||||
1. **Academic Research**: Peer-reviewed papers on software maintainability, complexity metrics, code quality, technical debt prioritization, and severity classification
|
||||
2. **Industry Standards**: ISO/IEC 25010, SonarQube rules, OWASP security guidelines, Google and Airbnb style guides
|
||||
3. **Authoritative Books**:
|
||||
- Robert C. Martin's "Clean Architecture" (2017)
|
||||
- Vaughn Vernon's "Implementing Domain-Driven Design" (2013)
|
||||
- Eric Evans' "Domain-Driven Design" (2003)
|
||||
- Martin Fowler's "Patterns of Enterprise Application Architecture" (2002)
|
||||
- Martin Fowler's "Refactoring" (1999, 2018)
|
||||
- Steve McConnell's "Code Complete" (1993, 2004)
|
||||
4. **Expert Guidance**: Martin Fowler, Robert C. Martin (Uncle Bob), Eric Evans, Alistair Cockburn, Kent Beck
|
||||
5. **Open Source Tools**: ArchUnit, SonarQube, ESLint - widely adopted in enterprise environments
|
||||
4. **Expert Guidance**: Martin Fowler, Robert C. Martin (Uncle Bob), Eric Evans, Vaughn Vernon, Alistair Cockburn, Kent Beck
|
||||
5. **Security Standards**: OWASP Secrets Management, GitHub Secret Scanning, GitGuardian best practices
|
||||
6. **Open Source Tools**: ArchUnit, SonarQube, ESLint, Secretlint - widely adopted in enterprise environments
|
||||
|
||||
These rules represent decades of software engineering wisdom, empirical research, and battle-tested practices from the world's leading software organizations and thought leaders.
|
||||
These rules represent decades of software engineering wisdom, empirical research, security best practices, and battle-tested practices from the world's leading software organizations and thought leaders.
|
||||
|
||||
---
|
||||
|
||||
@@ -545,8 +845,8 @@ These rules represent decades of software engineering wisdom, empirical research
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0
|
||||
**Last Updated**: 2025-11-24
|
||||
**Document Version**: 1.1
|
||||
**Last Updated**: 2025-11-26
|
||||
**Questions or want to contribute research?**
|
||||
- 📧 Email: fozilbek.samiyev@gmail.com
|
||||
- 🐙 GitHub: https://github.com/samiyev/puaros/issues
|
||||
|
||||
@@ -10,6 +10,10 @@ Guardian's detection rules are not invented - they're based on decades of softwa
|
||||
- [Entity Exposure](#entity-exposure)
|
||||
- [Repository Pattern](#repository-pattern)
|
||||
- [Naming Conventions](#naming-conventions)
|
||||
- [Anemic Domain Model Detection](#anemic-domain-model-detection)
|
||||
- [Aggregate Boundary Validation](#aggregate-boundary-validation)
|
||||
- [Secret Detection](#secret-detection)
|
||||
- [Severity-Based Prioritization](#severity-based-prioritization)
|
||||
- [Full Research Citations](#full-research-citations)
|
||||
|
||||
---
|
||||
@@ -319,6 +323,192 @@ Consistent naming:
|
||||
|
||||
---
|
||||
|
||||
## Anemic Domain Model Detection
|
||||
|
||||
### Why it matters
|
||||
|
||||
Anemic domain models violate core OOP principles:
|
||||
- ❌ **No behavior** - Entities become data bags with only getters/setters
|
||||
- ❌ **Logic in services** - Business logic scattered across service layers
|
||||
- ❌ **Violates OOP** - Separates data from behavior
|
||||
- ❌ **Higher complexity** - Loses benefits of domain modeling
|
||||
|
||||
### Who says so?
|
||||
|
||||
**Martin Fowler's Original Anti-Pattern:**
|
||||
- **Blog Post: "Anemic Domain Model"** (November 25, 2003)
|
||||
> "The basic symptom of an Anemic Domain Model is that at first blush it looks like the real thing. There are objects, many named after the nouns in the domain space... The catch comes when you look at the behavior, and you realize that there is hardly any behavior on these objects."
|
||||
- Published over 20 years ago, still relevant today
|
||||
- [Read Fowler's post](https://martinfowler.com/bliki/AnemicDomainModel.html)
|
||||
|
||||
**Why It's an Anti-pattern:**
|
||||
> "This is contrary to the basic idea of object-oriented design; which is to combine data and process together."
|
||||
- Incurs all costs of domain model without any benefits
|
||||
- Logic should be in domain objects: validations, calculations, business rules
|
||||
- [Wikipedia - Anemic Domain Model](https://en.wikipedia.org/wiki/Anemic_domain_model)
|
||||
|
||||
**Rich Domain Model vs Transaction Script:**
|
||||
- **Transaction Script**: Good for simple logic (Fowler, 2002)
|
||||
- **Rich Domain Model**: Better for complex, ever-changing business rules
|
||||
- Can refactor from Transaction Script to Domain Model, but it's harder than starting right
|
||||
- [Martin Fowler - Transaction Script](https://martinfowler.com/eaaCatalog/transactionScript.html)
|
||||
|
||||
**Domain-Driven Design Context:**
|
||||
- **Eric Evans (2003)**: Entities should have both identity AND behavior
|
||||
- Anemic models violate DDD by separating data from behavior
|
||||
- [Stack Overflow discussion](https://stackoverflow.com/questions/6293981/concrete-examples-on-why-the-anemic-domain-model-is-considered-an-anti-pattern)
|
||||
|
||||
[Read full research →](./RESEARCH_CITATIONS.md#11-anemic-domain-model-detection)
|
||||
|
||||
---
|
||||
|
||||
## Aggregate Boundary Validation
|
||||
|
||||
### Why it matters
|
||||
|
||||
Proper aggregate boundaries ensure:
|
||||
- ✅ **Consistency** - Atomic changes within boundaries
|
||||
- ✅ **Low coupling** - Aggregates are loosely connected
|
||||
- ✅ **Clear transactions** - One aggregate = one transaction
|
||||
- ✅ **Maintainability** - Boundaries prevent complexity spread
|
||||
|
||||
### The Rules
|
||||
|
||||
**Vaughn Vernon's Four Rules (2013):**
|
||||
1. **Model True Invariants in Consistency Boundaries**
|
||||
2. **Design Small Aggregates**
|
||||
3. **Reference Other Aggregates by Identity**
|
||||
4. **Use Eventual Consistency Outside the Boundary**
|
||||
|
||||
### Who says so?
|
||||
|
||||
**Eric Evans: Domain-Driven Design (2003)**
|
||||
- **Original Definition**:
|
||||
> "A cluster of associated objects that we treat as a unit for the purpose of data changes"
|
||||
- An aggregate defines a consistency boundary
|
||||
- Exactly one entity is the aggregate root
|
||||
- [Microsoft Learn - Tactical DDD](https://learn.microsoft.com/en-us/azure/architecture/microservices/model/tactical-ddd)
|
||||
|
||||
**Vaughn Vernon: Implementing Domain-Driven Design (2013)**
|
||||
- **Chapter 10: Aggregates** (Page 347)
|
||||
- ISBN: 978-0321834577
|
||||
- Comprehensive rules for aggregate design
|
||||
- Three-part essay series: "Effective Aggregate Design"
|
||||
- [Available at Kalele](https://kalele.io/effective-aggregate-design/)
|
||||
|
||||
**Why Boundaries Matter:**
|
||||
- **Transactional Boundary**: Changes must be atomic
|
||||
- **Reference by ID**: No direct entity references across aggregates
|
||||
- **Prevents tight coupling**: Maintains clear boundaries
|
||||
- [Medium - Mastering Aggregate Design](https://medium.com/ssense-tech/ddd-beyond-the-basics-mastering-aggregate-design-26591e218c8c)
|
||||
|
||||
**Microsoft Azure Documentation:**
|
||||
- Official guide for microservices architecture
|
||||
- Comprehensive aggregate boundary patterns
|
||||
- [Microsoft Learn - Tactical DDD](https://learn.microsoft.com/en-us/azure/architecture/microservices/model/tactical-ddd)
|
||||
|
||||
[Read full research →](./RESEARCH_CITATIONS.md#12-aggregate-boundary-validation-ddd-tactical-patterns)
|
||||
|
||||
---
|
||||
|
||||
## Secret Detection
|
||||
|
||||
### Why it matters
|
||||
|
||||
Hardcoded secrets create critical security risks:
|
||||
- 🔴 **Data breaches** - Exposed credentials lead to unauthorized access
|
||||
- 🔴 **Production incidents** - Leaked tokens cause service disruptions
|
||||
- 🔴 **Compliance violations** - GDPR, PCI-DSS, SOC 2 requirements
|
||||
- 🔴 **Impossible to rotate** - Secrets in code are difficult to change
|
||||
|
||||
### Who says so?
|
||||
|
||||
**OWASP Security Standards:**
|
||||
- **OWASP Secrets Management Cheat Sheet**
|
||||
> "Secrets should not be hardcoded, should not be unencrypted, and should not be stored in source code."
|
||||
- Official best practices from OWASP Foundation
|
||||
- [Read the cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html)
|
||||
|
||||
- **OWASP Hardcoded Password Vulnerability**
|
||||
> "It is never a good idea to hardcode a password, as it allows all of the project's developers to view the password and makes fixing the problem extremely difficult."
|
||||
- [OWASP Documentation](https://owasp.org/www-community/vulnerabilities/Use_of_hard-coded_password)
|
||||
|
||||
**GitHub Secret Scanning:**
|
||||
- **Official GitHub Documentation**
|
||||
- Automatically scans 350+ secret patterns
|
||||
- Detects AWS, GitHub, NPM, SSH, GCP, Slack tokens
|
||||
- AI-powered detection with Copilot Secret Scanning
|
||||
- [GitHub Docs](https://docs.github.com/code-security/secret-scanning/about-secret-scanning)
|
||||
|
||||
**Key Security Principles:**
|
||||
- **Centralized Management**: Use purpose-built secret management tools
|
||||
- **Prevention Tools**: Pre-commit hooks to prevent secrets entering codebase
|
||||
- **Encryption at Rest**: Never store secrets in plaintext
|
||||
- [OWASP SAMM - Secret Management](https://owaspsamm.org/model/implementation/secure-deployment/stream-b/)
|
||||
|
||||
**Mobile Security:**
|
||||
- OWASP: "Secrets security is the most important issue for mobile applications"
|
||||
- Only safe way: keep secrets off the client side entirely
|
||||
- [GitGuardian - OWASP Top 10 Mobile](https://blog.gitguardian.com/owasp-top-10-for-mobile-secrets/)
|
||||
|
||||
[Read full research →](./RESEARCH_CITATIONS.md#13-secret-detection--security)
|
||||
|
||||
---
|
||||
|
||||
## Severity-Based Prioritization
|
||||
|
||||
### Why it matters
|
||||
|
||||
Severity classification enables:
|
||||
- ✅ **Focus on critical issues** - Fix what matters most first
|
||||
- ✅ **Reduced technical debt** - Prioritize based on impact
|
||||
- ✅ **Better CI/CD integration** - Fail builds on critical issues only
|
||||
- ✅ **Team efficiency** - Don't waste time on low-impact issues
|
||||
|
||||
### Who says so?
|
||||
|
||||
**Academic Research:**
|
||||
- **Systematic Literature Review (2020)**
|
||||
- Title: "A systematic literature review on Technical Debt prioritization"
|
||||
- Analyzed 557 papers, included 44 primary studies
|
||||
- Finding: Need for consensus on severity factors
|
||||
- [ScienceDirect](https://www.sciencedirect.com/science/article/pii/S016412122030220X)
|
||||
|
||||
- **IEEE Conference Paper (2021)**
|
||||
- "Technical Debt Prioritization: Taxonomy, Methods Results"
|
||||
- Reviewed 112 studies
|
||||
- Classified methods in 10 categories
|
||||
- [IEEE Xplore](https://ieeexplore.ieee.org/document/9582595/)
|
||||
|
||||
- **Software Quality Journal (2023)**
|
||||
- "Identifying the severity of technical debt issues"
|
||||
- Problem: Most studies ignore severity degree
|
||||
- Proposed semantic + structural approach
|
||||
- [Springer](https://link.springer.com/article/10.1007/s11219-023-09651-3)
|
||||
|
||||
**SonarQube Industry Standard:**
|
||||
- **Current Classification (10.2+)**:
|
||||
- **Blocker/High**: Severe unintended consequences, fix immediately
|
||||
- **Medium**: Impacts developer productivity
|
||||
- **Low**: Slight impact on productivity
|
||||
- **Info**: No expected impact
|
||||
- [SonarQube Docs](https://docs.sonarsource.com/sonarqube-server/user-guide/code-metrics/metrics-definition)
|
||||
|
||||
**Real-World Impact:**
|
||||
- Development teams integrate models into CI/CD pipelines
|
||||
- Automatically flag potential TD issues during code reviews
|
||||
- Prioritize based on severity
|
||||
- [arXiv - Technical Debt Management](https://arxiv.org/html/2403.06484v1)
|
||||
|
||||
**Business Alignment:**
|
||||
- "Aligning Technical Debt Prioritization with Business Objectives" (2018)
|
||||
- Multiple-case study demonstrating importance
|
||||
- [ResearchGate](https://www.researchgate.net/publication/328903587_Aligning_Technical_Debt_Prioritization_with_Business_Objectives_A_Multiple-Case_Study)
|
||||
|
||||
[Read full research →](./RESEARCH_CITATIONS.md#14-severity-based-prioritization--technical-debt)
|
||||
|
||||
---
|
||||
|
||||
## Full Research Citations
|
||||
|
||||
For complete academic papers, books, and authoritative sources, see:
|
||||
@@ -354,8 +544,9 @@ Guardian's rules align with international standards:
|
||||
|
||||
Guardian's rules are backed by:
|
||||
|
||||
✅ **5 Seminal Books** (1993-2017)
|
||||
✅ **6 Seminal Books** (1993-2017)
|
||||
- Clean Architecture (Robert C. Martin, 2017)
|
||||
- Implementing Domain-Driven Design (Vaughn Vernon, 2013)
|
||||
- Domain-Driven Design (Eric Evans, 2003)
|
||||
- Patterns of Enterprise Application Architecture (Martin Fowler, 2002)
|
||||
- Refactoring (Martin Fowler, 1999)
|
||||
@@ -363,9 +554,16 @@ Guardian's rules are backed by:
|
||||
|
||||
✅ **Academic Research** (1976-2024)
|
||||
- MIT Course 6.031
|
||||
- ScienceDirect peer-reviewed studies
|
||||
- ScienceDirect peer-reviewed studies (2020-2023)
|
||||
- IEEE Conference papers on Technical Debt
|
||||
- Software Quality Journal (2023)
|
||||
- Cyclomatic Complexity (Thomas McCabe, 1976)
|
||||
|
||||
✅ **Security Standards**
|
||||
- OWASP Secrets Management Cheat Sheet
|
||||
- GitHub Secret Scanning (350+ patterns)
|
||||
- OWASP Top 10 for Mobile
|
||||
|
||||
✅ **International Standards**
|
||||
- ISO/IEC 25010:2011
|
||||
|
||||
@@ -373,10 +571,11 @@ Guardian's rules are backed by:
|
||||
- Google, Microsoft, Airbnb style guides
|
||||
- SonarQube (400,000+ organizations)
|
||||
- AWS documentation
|
||||
- GitHub security practices
|
||||
|
||||
✅ **Thought Leaders**
|
||||
- Martin Fowler, Robert C. Martin (Uncle Bob), Eric Evans
|
||||
- Alistair Cockburn, Kent Beck, Thomas McCabe
|
||||
- Vaughn Vernon, Alistair Cockburn, Kent Beck, Thomas McCabe
|
||||
|
||||
---
|
||||
|
||||
@@ -388,4 +587,4 @@ Guardian's rules are backed by:
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2025-11-24*
|
||||
*Last updated: 2025-11-26*
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* BAD EXAMPLE: Anemic Domain Model
|
||||
*
|
||||
* This Order class only has getters and setters without any business logic.
|
||||
* All business logic is likely scattered in services (procedural approach).
|
||||
*
|
||||
* This violates Domain-Driven Design principles.
|
||||
*/
|
||||
|
||||
class Order {
|
||||
private status: string
|
||||
private total: number
|
||||
private items: any[]
|
||||
|
||||
getStatus(): string {
|
||||
return this.status
|
||||
}
|
||||
|
||||
setStatus(status: string): void {
|
||||
this.status = status
|
||||
}
|
||||
|
||||
getTotal(): number {
|
||||
return this.total
|
||||
}
|
||||
|
||||
setTotal(total: number): void {
|
||||
this.total = total
|
||||
}
|
||||
|
||||
getItems(): any[] {
|
||||
return this.items
|
||||
}
|
||||
|
||||
setItems(items: any[]): void {
|
||||
this.items = items
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* BAD EXAMPLE: Anemic Domain Model with Public Setters
|
||||
*
|
||||
* This User class has public setters which is an anti-pattern in DDD.
|
||||
* Public setters allow uncontrolled state changes without validation or business rules.
|
||||
*
|
||||
* This violates Domain-Driven Design principles and encapsulation.
|
||||
*/
|
||||
|
||||
class User {
|
||||
private email: string
|
||||
private password: string
|
||||
private status: string
|
||||
|
||||
public setEmail(email: string): void {
|
||||
this.email = email
|
||||
}
|
||||
|
||||
public getEmail(): string {
|
||||
return this.email
|
||||
}
|
||||
|
||||
public setPassword(password: string): void {
|
||||
this.password = password
|
||||
}
|
||||
|
||||
public setStatus(status: string): void {
|
||||
this.status = status
|
||||
}
|
||||
|
||||
public getStatus(): string {
|
||||
return this.status
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* GOOD EXAMPLE: Rich Domain Model with Business Logic
|
||||
*
|
||||
* This Customer class encapsulates business rules and state transitions.
|
||||
* No public setters - all changes go through business methods.
|
||||
*
|
||||
* This follows Domain-Driven Design and encapsulation principles.
|
||||
*/
|
||||
|
||||
interface Address {
|
||||
street: string
|
||||
city: string
|
||||
country: string
|
||||
postalCode: string
|
||||
}
|
||||
|
||||
interface DomainEvent {
|
||||
type: string
|
||||
data: any
|
||||
}
|
||||
|
||||
class Customer {
|
||||
private readonly id: string
|
||||
private email: string
|
||||
private isActive: boolean
|
||||
private loyaltyPoints: number
|
||||
private address: Address | null
|
||||
private readonly events: DomainEvent[] = []
|
||||
|
||||
constructor(id: string, email: string) {
|
||||
this.id = id
|
||||
this.email = email
|
||||
this.isActive = true
|
||||
this.loyaltyPoints = 0
|
||||
this.address = null
|
||||
}
|
||||
|
||||
public activate(): void {
|
||||
if (this.isActive) {
|
||||
throw new Error("Customer is already active")
|
||||
}
|
||||
this.isActive = true
|
||||
this.events.push({
|
||||
type: "CustomerActivated",
|
||||
data: { customerId: this.id },
|
||||
})
|
||||
}
|
||||
|
||||
public deactivate(reason: string): void {
|
||||
if (!this.isActive) {
|
||||
throw new Error("Customer is already inactive")
|
||||
}
|
||||
this.isActive = false
|
||||
this.events.push({
|
||||
type: "CustomerDeactivated",
|
||||
data: { customerId: this.id, reason },
|
||||
})
|
||||
}
|
||||
|
||||
public changeEmail(newEmail: string): void {
|
||||
if (!this.isValidEmail(newEmail)) {
|
||||
throw new Error("Invalid email format")
|
||||
}
|
||||
if (this.email === newEmail) {
|
||||
return
|
||||
}
|
||||
const oldEmail = this.email
|
||||
this.email = newEmail
|
||||
this.events.push({
|
||||
type: "EmailChanged",
|
||||
data: { customerId: this.id, oldEmail, newEmail },
|
||||
})
|
||||
}
|
||||
|
||||
public updateAddress(address: Address): void {
|
||||
if (!this.isValidAddress(address)) {
|
||||
throw new Error("Invalid address")
|
||||
}
|
||||
this.address = address
|
||||
this.events.push({
|
||||
type: "AddressUpdated",
|
||||
data: { customerId: this.id },
|
||||
})
|
||||
}
|
||||
|
||||
public addLoyaltyPoints(points: number): void {
|
||||
if (points <= 0) {
|
||||
throw new Error("Points must be positive")
|
||||
}
|
||||
if (!this.isActive) {
|
||||
throw new Error("Cannot add points to inactive customer")
|
||||
}
|
||||
this.loyaltyPoints += points
|
||||
this.events.push({
|
||||
type: "LoyaltyPointsAdded",
|
||||
data: { customerId: this.id, points },
|
||||
})
|
||||
}
|
||||
|
||||
public redeemLoyaltyPoints(points: number): void {
|
||||
if (points <= 0) {
|
||||
throw new Error("Points must be positive")
|
||||
}
|
||||
if (this.loyaltyPoints < points) {
|
||||
throw new Error("Insufficient loyalty points")
|
||||
}
|
||||
this.loyaltyPoints -= points
|
||||
this.events.push({
|
||||
type: "LoyaltyPointsRedeemed",
|
||||
data: { customerId: this.id, points },
|
||||
})
|
||||
}
|
||||
|
||||
public getEmail(): string {
|
||||
return this.email
|
||||
}
|
||||
|
||||
public getLoyaltyPoints(): number {
|
||||
return this.loyaltyPoints
|
||||
}
|
||||
|
||||
public getAddress(): Address | null {
|
||||
return this.address ? { ...this.address } : null
|
||||
}
|
||||
|
||||
public getEvents(): DomainEvent[] {
|
||||
return [...this.events]
|
||||
}
|
||||
|
||||
private isValidEmail(email: string): boolean {
|
||||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||
}
|
||||
|
||||
private isValidAddress(address: Address): boolean {
|
||||
return !!address.street && !!address.city && !!address.country && !!address.postalCode
|
||||
}
|
||||
}
|
||||
|
||||
export { Customer }
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* GOOD EXAMPLE: Rich Domain Model
|
||||
*
|
||||
* This Order class contains business logic and enforces business rules.
|
||||
* State changes are made through business methods, not setters.
|
||||
*
|
||||
* This follows Domain-Driven Design principles.
|
||||
*/
|
||||
|
||||
type OrderStatus = "pending" | "approved" | "rejected" | "shipped"
|
||||
|
||||
interface OrderItem {
|
||||
productId: string
|
||||
quantity: number
|
||||
price: number
|
||||
}
|
||||
|
||||
interface DomainEvent {
|
||||
type: string
|
||||
data: any
|
||||
}
|
||||
|
||||
class Order {
|
||||
private readonly id: string
|
||||
private status: OrderStatus
|
||||
private items: OrderItem[]
|
||||
private readonly events: DomainEvent[] = []
|
||||
|
||||
constructor(id: string, items: OrderItem[]) {
|
||||
this.id = id
|
||||
this.status = "pending"
|
||||
this.items = items
|
||||
}
|
||||
|
||||
public approve(): void {
|
||||
if (!this.canBeApproved()) {
|
||||
throw new Error("Cannot approve order in current state")
|
||||
}
|
||||
this.status = "approved"
|
||||
this.events.push({
|
||||
type: "OrderApproved",
|
||||
data: { orderId: this.id },
|
||||
})
|
||||
}
|
||||
|
||||
public reject(reason: string): void {
|
||||
if (!this.canBeRejected()) {
|
||||
throw new Error("Cannot reject order in current state")
|
||||
}
|
||||
this.status = "rejected"
|
||||
this.events.push({
|
||||
type: "OrderRejected",
|
||||
data: { orderId: this.id, reason },
|
||||
})
|
||||
}
|
||||
|
||||
public ship(): void {
|
||||
if (!this.canBeShipped()) {
|
||||
throw new Error("Order must be approved before shipping")
|
||||
}
|
||||
this.status = "shipped"
|
||||
this.events.push({
|
||||
type: "OrderShipped",
|
||||
data: { orderId: this.id },
|
||||
})
|
||||
}
|
||||
|
||||
public addItem(item: OrderItem): void {
|
||||
if (this.status !== "pending") {
|
||||
throw new Error("Cannot modify approved or shipped order")
|
||||
}
|
||||
this.items.push(item)
|
||||
}
|
||||
|
||||
public calculateTotal(): number {
|
||||
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
||||
}
|
||||
|
||||
public getStatus(): OrderStatus {
|
||||
return this.status
|
||||
}
|
||||
|
||||
public getItems(): OrderItem[] {
|
||||
return [...this.items]
|
||||
}
|
||||
|
||||
public getEvents(): DomainEvent[] {
|
||||
return [...this.events]
|
||||
}
|
||||
|
||||
private canBeApproved(): boolean {
|
||||
return this.status === "pending" && this.items.length > 0
|
||||
}
|
||||
|
||||
private canBeRejected(): boolean {
|
||||
return this.status === "pending"
|
||||
}
|
||||
|
||||
private canBeShipped(): boolean {
|
||||
return this.status === "approved"
|
||||
}
|
||||
}
|
||||
|
||||
export { Order }
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@samiyev/guardian",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"description": "Research-backed code quality guardian for AI-assisted development. Detects hardcodes, secrets, circular deps, framework leaks, entity exposure, and 9 architecture violations. Enforces Clean Architecture/DDD principles. Works with GitHub Copilot, Cursor, Windsurf, Claude, ChatGPT, Cline, and any AI coding tool.",
|
||||
"keywords": [
|
||||
"puaros",
|
||||
|
||||
@@ -13,6 +13,8 @@ import { IDependencyDirectionDetector } from "./domain/services/IDependencyDirec
|
||||
import { IRepositoryPatternDetector } from "./domain/services/RepositoryPatternDetectorService"
|
||||
import { IAggregateBoundaryDetector } from "./domain/services/IAggregateBoundaryDetector"
|
||||
import { ISecretDetector } from "./domain/services/ISecretDetector"
|
||||
import { IAnemicModelDetector } from "./domain/services/IAnemicModelDetector"
|
||||
import { IDuplicateValueTracker } from "./domain/services/IDuplicateValueTracker"
|
||||
import { FileScanner } from "./infrastructure/scanners/FileScanner"
|
||||
import { CodeParser } from "./infrastructure/parsers/CodeParser"
|
||||
import { HardcodeDetector } from "./infrastructure/analyzers/HardcodeDetector"
|
||||
@@ -23,6 +25,8 @@ import { DependencyDirectionDetector } from "./infrastructure/analyzers/Dependen
|
||||
import { RepositoryPatternDetector } from "./infrastructure/analyzers/RepositoryPatternDetector"
|
||||
import { AggregateBoundaryDetector } from "./infrastructure/analyzers/AggregateBoundaryDetector"
|
||||
import { SecretDetector } from "./infrastructure/analyzers/SecretDetector"
|
||||
import { AnemicModelDetector } from "./infrastructure/analyzers/AnemicModelDetector"
|
||||
import { DuplicateValueTracker } from "./infrastructure/analyzers/DuplicateValueTracker"
|
||||
import { ERROR_MESSAGES } from "./shared/constants"
|
||||
|
||||
/**
|
||||
@@ -82,6 +86,8 @@ export async function analyzeProject(
|
||||
const repositoryPatternDetector: IRepositoryPatternDetector = new RepositoryPatternDetector()
|
||||
const aggregateBoundaryDetector: IAggregateBoundaryDetector = new AggregateBoundaryDetector()
|
||||
const secretDetector: ISecretDetector = new SecretDetector()
|
||||
const anemicModelDetector: IAnemicModelDetector = new AnemicModelDetector()
|
||||
const duplicateValueTracker: IDuplicateValueTracker = new DuplicateValueTracker()
|
||||
const useCase = new AnalyzeProject(
|
||||
fileScanner,
|
||||
codeParser,
|
||||
@@ -93,6 +99,8 @@ export async function analyzeProject(
|
||||
repositoryPatternDetector,
|
||||
aggregateBoundaryDetector,
|
||||
secretDetector,
|
||||
anemicModelDetector,
|
||||
duplicateValueTracker,
|
||||
)
|
||||
|
||||
const result = await useCase.execute(options)
|
||||
@@ -116,5 +124,6 @@ export type {
|
||||
DependencyDirectionViolation,
|
||||
RepositoryPatternViolation,
|
||||
AggregateBoundaryViolation,
|
||||
AnemicModelViolation,
|
||||
ProjectMetrics,
|
||||
} from "./application/use-cases/AnalyzeProject"
|
||||
|
||||
@@ -10,12 +10,14 @@ import { IDependencyDirectionDetector } from "../../domain/services/IDependencyD
|
||||
import { IRepositoryPatternDetector } from "../../domain/services/RepositoryPatternDetectorService"
|
||||
import { IAggregateBoundaryDetector } from "../../domain/services/IAggregateBoundaryDetector"
|
||||
import { ISecretDetector } from "../../domain/services/ISecretDetector"
|
||||
import { IAnemicModelDetector } from "../../domain/services/IAnemicModelDetector"
|
||||
import { IDuplicateValueTracker } from "../../domain/services/IDuplicateValueTracker"
|
||||
import { SourceFile } from "../../domain/entities/SourceFile"
|
||||
import { DependencyGraph } from "../../domain/entities/DependencyGraph"
|
||||
import { FileCollectionStep } from "./pipeline/FileCollectionStep"
|
||||
import { ParsingStep } from "./pipeline/ParsingStep"
|
||||
import { DetectionPipeline } from "./pipeline/DetectionPipeline"
|
||||
import { ResultAggregator } from "./pipeline/ResultAggregator"
|
||||
import { CollectFiles } from "./pipeline/CollectFiles"
|
||||
import { ParseSourceFiles } from "./pipeline/ParseSourceFiles"
|
||||
import { ExecuteDetection } from "./pipeline/ExecuteDetection"
|
||||
import { AggregateResults } from "./pipeline/AggregateResults"
|
||||
import {
|
||||
ERROR_MESSAGES,
|
||||
HARDCODE_TYPES,
|
||||
@@ -44,6 +46,7 @@ export interface AnalyzeProjectResponse {
|
||||
repositoryPatternViolations: RepositoryPatternViolation[]
|
||||
aggregateBoundaryViolations: AggregateBoundaryViolation[]
|
||||
secretViolations: SecretViolation[]
|
||||
anemicModelViolations: AnemicModelViolation[]
|
||||
metrics: ProjectMetrics
|
||||
}
|
||||
|
||||
@@ -60,8 +63,9 @@ export interface HardcodeViolation {
|
||||
type:
|
||||
| typeof HARDCODE_TYPES.MAGIC_NUMBER
|
||||
| typeof HARDCODE_TYPES.MAGIC_STRING
|
||||
| typeof HARDCODE_TYPES.MAGIC_BOOLEAN
|
||||
| typeof HARDCODE_TYPES.MAGIC_CONFIG
|
||||
value: string | number
|
||||
value: string | number | boolean
|
||||
file: string
|
||||
line: number
|
||||
column: number
|
||||
@@ -176,6 +180,21 @@ export interface SecretViolation {
|
||||
severity: SeverityLevel
|
||||
}
|
||||
|
||||
export interface AnemicModelViolation {
|
||||
rule: typeof RULES.ANEMIC_MODEL
|
||||
className: string
|
||||
file: string
|
||||
layer: string
|
||||
line?: number
|
||||
methodCount: number
|
||||
propertyCount: number
|
||||
hasOnlyGettersSetters: boolean
|
||||
hasPublicSetters: boolean
|
||||
message: string
|
||||
suggestion: string
|
||||
severity: SeverityLevel
|
||||
}
|
||||
|
||||
export interface ProjectMetrics {
|
||||
totalFiles: number
|
||||
totalFunctions: number
|
||||
@@ -191,10 +210,10 @@ export class AnalyzeProject extends UseCase<
|
||||
AnalyzeProjectRequest,
|
||||
ResponseDto<AnalyzeProjectResponse>
|
||||
> {
|
||||
private readonly fileCollectionStep: FileCollectionStep
|
||||
private readonly parsingStep: ParsingStep
|
||||
private readonly detectionPipeline: DetectionPipeline
|
||||
private readonly resultAggregator: ResultAggregator
|
||||
private readonly fileCollectionStep: CollectFiles
|
||||
private readonly parsingStep: ParseSourceFiles
|
||||
private readonly detectionPipeline: ExecuteDetection
|
||||
private readonly resultAggregator: AggregateResults
|
||||
|
||||
constructor(
|
||||
fileScanner: IFileScanner,
|
||||
@@ -207,11 +226,13 @@ export class AnalyzeProject extends UseCase<
|
||||
repositoryPatternDetector: IRepositoryPatternDetector,
|
||||
aggregateBoundaryDetector: IAggregateBoundaryDetector,
|
||||
secretDetector: ISecretDetector,
|
||||
anemicModelDetector: IAnemicModelDetector,
|
||||
duplicateValueTracker: IDuplicateValueTracker,
|
||||
) {
|
||||
super()
|
||||
this.fileCollectionStep = new FileCollectionStep(fileScanner)
|
||||
this.parsingStep = new ParsingStep(codeParser)
|
||||
this.detectionPipeline = new DetectionPipeline(
|
||||
this.fileCollectionStep = new CollectFiles(fileScanner)
|
||||
this.parsingStep = new ParseSourceFiles(codeParser)
|
||||
this.detectionPipeline = new ExecuteDetection(
|
||||
hardcodeDetector,
|
||||
namingConventionDetector,
|
||||
frameworkLeakDetector,
|
||||
@@ -220,8 +241,10 @@ export class AnalyzeProject extends UseCase<
|
||||
repositoryPatternDetector,
|
||||
aggregateBoundaryDetector,
|
||||
secretDetector,
|
||||
anemicModelDetector,
|
||||
duplicateValueTracker,
|
||||
)
|
||||
this.resultAggregator = new ResultAggregator()
|
||||
this.resultAggregator = new AggregateResults()
|
||||
}
|
||||
|
||||
public async execute(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DependencyGraph } from "../../../domain/entities/DependencyGraph"
|
||||
import type {
|
||||
AggregateBoundaryViolation,
|
||||
AnalyzeProjectResponse,
|
||||
AnemicModelViolation,
|
||||
ArchitectureViolation,
|
||||
CircularDependencyViolation,
|
||||
DependencyDirectionViolation,
|
||||
@@ -29,12 +30,13 @@ export interface AggregationRequest {
|
||||
repositoryPatternViolations: RepositoryPatternViolation[]
|
||||
aggregateBoundaryViolations: AggregateBoundaryViolation[]
|
||||
secretViolations: SecretViolation[]
|
||||
anemicModelViolations: AnemicModelViolation[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipeline step responsible for building final response DTO
|
||||
*/
|
||||
export class ResultAggregator {
|
||||
export class AggregateResults {
|
||||
public execute(request: AggregationRequest): AnalyzeProjectResponse {
|
||||
const metrics = this.calculateMetrics(
|
||||
request.sourceFiles,
|
||||
@@ -55,6 +57,7 @@ export class ResultAggregator {
|
||||
repositoryPatternViolations: request.repositoryPatternViolations,
|
||||
aggregateBoundaryViolations: request.aggregateBoundaryViolations,
|
||||
secretViolations: request.secretViolations,
|
||||
anemicModelViolations: request.anemicModelViolations,
|
||||
metrics,
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export interface FileCollectionResult {
|
||||
/**
|
||||
* Pipeline step responsible for file collection and basic parsing
|
||||
*/
|
||||
export class FileCollectionStep {
|
||||
export class CollectFiles {
|
||||
constructor(private readonly fileScanner: IFileScanner) {}
|
||||
|
||||
public async execute(request: FileCollectionRequest): Promise<FileCollectionResult> {
|
||||
@@ -6,8 +6,11 @@ import { IDependencyDirectionDetector } from "../../../domain/services/IDependen
|
||||
import { IRepositoryPatternDetector } from "../../../domain/services/RepositoryPatternDetectorService"
|
||||
import { IAggregateBoundaryDetector } from "../../../domain/services/IAggregateBoundaryDetector"
|
||||
import { ISecretDetector } from "../../../domain/services/ISecretDetector"
|
||||
import { IAnemicModelDetector } from "../../../domain/services/IAnemicModelDetector"
|
||||
import { IDuplicateValueTracker } from "../../../domain/services/IDuplicateValueTracker"
|
||||
import { SourceFile } from "../../../domain/entities/SourceFile"
|
||||
import { DependencyGraph } from "../../../domain/entities/DependencyGraph"
|
||||
import { HardcodedValue } from "../../../domain/value-objects/HardcodedValue"
|
||||
import {
|
||||
LAYERS,
|
||||
REPOSITORY_VIOLATION_TYPES,
|
||||
@@ -18,6 +21,7 @@ import {
|
||||
} from "../../../shared/constants"
|
||||
import type {
|
||||
AggregateBoundaryViolation,
|
||||
AnemicModelViolation,
|
||||
ArchitectureViolation,
|
||||
CircularDependencyViolation,
|
||||
DependencyDirectionViolation,
|
||||
@@ -45,12 +49,13 @@ export interface DetectionResult {
|
||||
repositoryPatternViolations: RepositoryPatternViolation[]
|
||||
aggregateBoundaryViolations: AggregateBoundaryViolation[]
|
||||
secretViolations: SecretViolation[]
|
||||
anemicModelViolations: AnemicModelViolation[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipeline step responsible for running all detectors
|
||||
*/
|
||||
export class DetectionPipeline {
|
||||
export class ExecuteDetection {
|
||||
constructor(
|
||||
private readonly hardcodeDetector: IHardcodeDetector,
|
||||
private readonly namingConventionDetector: INamingConventionDetector,
|
||||
@@ -60,6 +65,8 @@ export class DetectionPipeline {
|
||||
private readonly repositoryPatternDetector: IRepositoryPatternDetector,
|
||||
private readonly aggregateBoundaryDetector: IAggregateBoundaryDetector,
|
||||
private readonly secretDetector: ISecretDetector,
|
||||
private readonly anemicModelDetector: IAnemicModelDetector,
|
||||
private readonly duplicateValueTracker: IDuplicateValueTracker,
|
||||
) {}
|
||||
|
||||
public async execute(request: DetectionRequest): Promise<DetectionResult> {
|
||||
@@ -90,6 +97,9 @@ export class DetectionPipeline {
|
||||
this.detectAggregateBoundaryViolations(request.sourceFiles),
|
||||
),
|
||||
secretViolations: this.sortBySeverity(secretViolations),
|
||||
anemicModelViolations: this.sortBySeverity(
|
||||
this.detectAnemicModels(request.sourceFiles),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +154,10 @@ export class DetectionPipeline {
|
||||
}
|
||||
|
||||
private detectHardcode(sourceFiles: SourceFile[]): HardcodeViolation[] {
|
||||
const violations: HardcodeViolation[] = []
|
||||
const allHardcodedValues: {
|
||||
value: HardcodedValue
|
||||
file: SourceFile
|
||||
}[] = []
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const hardcodedValues = this.hardcodeDetector.detectAll(
|
||||
@@ -153,23 +166,53 @@ export class DetectionPipeline {
|
||||
)
|
||||
|
||||
for (const hardcoded of hardcodedValues) {
|
||||
violations.push({
|
||||
rule: RULES.HARDCODED_VALUE,
|
||||
type: hardcoded.type,
|
||||
value: hardcoded.value,
|
||||
file: file.path.relative,
|
||||
line: hardcoded.line,
|
||||
column: hardcoded.column,
|
||||
context: hardcoded.context,
|
||||
suggestion: {
|
||||
constantName: hardcoded.suggestConstantName(),
|
||||
location: hardcoded.suggestLocation(file.layer),
|
||||
},
|
||||
severity: VIOLATION_SEVERITY_MAP.HARDCODE,
|
||||
})
|
||||
allHardcodedValues.push({ value: hardcoded, file })
|
||||
}
|
||||
}
|
||||
|
||||
this.duplicateValueTracker.clear()
|
||||
for (const { value, file } of allHardcodedValues) {
|
||||
this.duplicateValueTracker.track(value, file.path.relative)
|
||||
}
|
||||
|
||||
const violations: HardcodeViolation[] = []
|
||||
for (const { value, file } of allHardcodedValues) {
|
||||
const duplicateLocations = this.duplicateValueTracker.getDuplicateLocations(
|
||||
value.value,
|
||||
value.type,
|
||||
)
|
||||
const enrichedValue = duplicateLocations
|
||||
? HardcodedValue.create(
|
||||
value.value,
|
||||
value.type,
|
||||
value.line,
|
||||
value.column,
|
||||
value.context,
|
||||
value.valueType,
|
||||
duplicateLocations.filter((loc) => loc.file !== file.path.relative),
|
||||
)
|
||||
: value
|
||||
|
||||
if (enrichedValue.shouldSkip(file.layer)) {
|
||||
continue
|
||||
}
|
||||
|
||||
violations.push({
|
||||
rule: RULES.HARDCODED_VALUE,
|
||||
type: enrichedValue.type,
|
||||
value: enrichedValue.value,
|
||||
file: file.path.relative,
|
||||
line: enrichedValue.line,
|
||||
column: enrichedValue.column,
|
||||
context: enrichedValue.context,
|
||||
suggestion: {
|
||||
constantName: enrichedValue.suggestConstantName(),
|
||||
location: enrichedValue.suggestLocation(file.layer),
|
||||
},
|
||||
severity: VIOLATION_SEVERITY_MAP.HARDCODE,
|
||||
})
|
||||
}
|
||||
|
||||
return violations
|
||||
}
|
||||
|
||||
@@ -398,6 +441,37 @@ export class DetectionPipeline {
|
||||
return violations
|
||||
}
|
||||
|
||||
private detectAnemicModels(sourceFiles: SourceFile[]): AnemicModelViolation[] {
|
||||
const violations: AnemicModelViolation[] = []
|
||||
|
||||
for (const file of sourceFiles) {
|
||||
const anemicModels = this.anemicModelDetector.detectAnemicModels(
|
||||
file.content,
|
||||
file.path.relative,
|
||||
file.layer,
|
||||
)
|
||||
|
||||
for (const anemicModel of anemicModels) {
|
||||
violations.push({
|
||||
rule: RULES.ANEMIC_MODEL,
|
||||
className: anemicModel.className,
|
||||
file: file.path.relative,
|
||||
layer: anemicModel.layer,
|
||||
line: anemicModel.line,
|
||||
methodCount: anemicModel.methodCount,
|
||||
propertyCount: anemicModel.propertyCount,
|
||||
hasOnlyGettersSetters: anemicModel.hasOnlyGettersSetters,
|
||||
hasPublicSetters: anemicModel.hasPublicSetters,
|
||||
message: anemicModel.getMessage(),
|
||||
suggestion: anemicModel.getSuggestion(),
|
||||
severity: VIOLATION_SEVERITY_MAP.ANEMIC_MODEL,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return violations
|
||||
}
|
||||
|
||||
private sortBySeverity<T extends { severity: SeverityLevel }>(violations: T[]): T[] {
|
||||
return violations.sort((a, b) => {
|
||||
return SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]
|
||||
@@ -15,7 +15,7 @@ export interface ParsingResult {
|
||||
/**
|
||||
* Pipeline step responsible for AST parsing and dependency graph construction
|
||||
*/
|
||||
export class ParsingStep {
|
||||
export class ParseSourceFiles {
|
||||
constructor(private readonly codeParser: ICodeParser) {}
|
||||
|
||||
public execute(request: ParsingRequest): ParsingResult {
|
||||
@@ -1,6 +1,7 @@
|
||||
import { SEVERITY_LEVELS, type SeverityLevel } from "../../shared/constants"
|
||||
import type {
|
||||
AggregateBoundaryViolation,
|
||||
AnemicModelViolation,
|
||||
ArchitectureViolation,
|
||||
CircularDependencyViolation,
|
||||
DependencyDirectionViolation,
|
||||
@@ -204,4 +205,31 @@ export class OutputFormatter {
|
||||
console.log(` 📁 Location: ${hc.suggestion.location}`)
|
||||
console.log("")
|
||||
}
|
||||
|
||||
formatAnemicModelViolation(am: AnemicModelViolation, index: number): void {
|
||||
const location = am.line ? `${am.file}:${String(am.line)}` : am.file
|
||||
console.log(`${String(index + 1)}. ${location}`)
|
||||
console.log(` Severity: ${SEVERITY_LABELS[am.severity]}`)
|
||||
console.log(` Class: ${am.className}`)
|
||||
console.log(` Layer: ${am.layer}`)
|
||||
console.log(
|
||||
` Methods: ${String(am.methodCount)} | Properties: ${String(am.propertyCount)}`,
|
||||
)
|
||||
|
||||
if (am.hasPublicSetters) {
|
||||
console.log(" ⚠️ Has public setters (DDD anti-pattern)")
|
||||
}
|
||||
if (am.hasOnlyGettersSetters) {
|
||||
console.log(" ⚠️ Only getters/setters (no business logic)")
|
||||
}
|
||||
|
||||
console.log(` ${am.message}`)
|
||||
console.log(" 💡 Suggestion:")
|
||||
am.suggestion.split("\n").forEach((line) => {
|
||||
if (line.trim()) {
|
||||
console.log(` ${line}`)
|
||||
}
|
||||
})
|
||||
console.log("")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ program
|
||||
repositoryPatternViolations,
|
||||
aggregateBoundaryViolations,
|
||||
secretViolations,
|
||||
anemicModelViolations,
|
||||
} = result
|
||||
|
||||
const minSeverity: SeverityLevel | undefined = options.onlyCritical
|
||||
@@ -134,6 +135,7 @@ program
|
||||
minSeverity,
|
||||
)
|
||||
secretViolations = grouper.filterBySeverity(secretViolations, minSeverity)
|
||||
anemicModelViolations = grouper.filterBySeverity(anemicModelViolations, minSeverity)
|
||||
|
||||
statsFormatter.displaySeverityFilterMessage(
|
||||
options.onlyCritical,
|
||||
@@ -260,6 +262,19 @@ program
|
||||
)
|
||||
}
|
||||
|
||||
if (anemicModelViolations.length > 0) {
|
||||
console.log(
|
||||
`\n🩺 Found ${String(anemicModelViolations.length)} anemic domain model(s)`,
|
||||
)
|
||||
outputFormatter.displayGroupedViolations(
|
||||
anemicModelViolations,
|
||||
(am, i) => {
|
||||
outputFormatter.formatAnemicModelViolation(am, i)
|
||||
},
|
||||
limit,
|
||||
)
|
||||
}
|
||||
|
||||
if (options.hardcode && hardcodeViolations.length > 0) {
|
||||
console.log(
|
||||
`\n${CLI_MESSAGES.HARDCODE_VIOLATIONS_HEADER} ${String(hardcodeViolations.length)} ${CLI_LABELS.HARDCODE_VIOLATIONS}`,
|
||||
@@ -283,7 +298,8 @@ program
|
||||
dependencyDirectionViolations.length +
|
||||
repositoryPatternViolations.length +
|
||||
aggregateBoundaryViolations.length +
|
||||
secretViolations.length
|
||||
secretViolations.length +
|
||||
anemicModelViolations.length
|
||||
|
||||
statsFormatter.displaySummary(totalIssues, options.verbose)
|
||||
} catch (error) {
|
||||
|
||||
@@ -69,3 +69,14 @@ export const SECRET_VIOLATION_MESSAGES = {
|
||||
ROTATE_IF_EXPOSED: "4. If secret was committed, rotate it immediately",
|
||||
USE_GITIGNORE: "5. Add secret files to .gitignore (.env, credentials.json, etc.)",
|
||||
}
|
||||
|
||||
export const ANEMIC_MODEL_MESSAGES = {
|
||||
REMOVE_PUBLIC_SETTERS: "1. Remove public setters - they allow uncontrolled state changes",
|
||||
USE_METHODS_FOR_CHANGES: "2. Use business methods instead (approve(), cancel(), addItem())",
|
||||
ENCAPSULATE_INVARIANTS: "3. Encapsulate business rules and invariants in methods",
|
||||
ADD_BUSINESS_METHODS: "1. Add business logic methods to the entity",
|
||||
MOVE_LOGIC_FROM_SERVICES:
|
||||
"2. Move business logic from services to domain entities where it belongs",
|
||||
ENCAPSULATE_BUSINESS_RULES: "3. Encapsulate business rules inside entity methods",
|
||||
USE_DOMAIN_EVENTS: "4. Use domain events to communicate state changes",
|
||||
}
|
||||
|
||||
79
packages/guardian/src/domain/constants/SecretExamples.ts
Normal file
79
packages/guardian/src/domain/constants/SecretExamples.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Secret detection constants
|
||||
* All hardcoded strings related to secret detection and examples
|
||||
*/
|
||||
|
||||
export const SECRET_KEYWORDS = {
|
||||
AWS: "aws",
|
||||
GITHUB: "github",
|
||||
NPM: "npm",
|
||||
SSH: "ssh",
|
||||
PRIVATE_KEY: "private key",
|
||||
SLACK: "slack",
|
||||
API_KEY: "api key",
|
||||
APIKEY: "apikey",
|
||||
ACCESS_KEY: "access key",
|
||||
SECRET: "secret",
|
||||
TOKEN: "token",
|
||||
PASSWORD: "password",
|
||||
USER: "user",
|
||||
BOT: "bot",
|
||||
RSA: "rsa",
|
||||
DSA: "dsa",
|
||||
ECDSA: "ecdsa",
|
||||
ED25519: "ed25519",
|
||||
BASICAUTH: "basicauth",
|
||||
GCP: "gcp",
|
||||
GOOGLE: "google",
|
||||
PRIVATEKEY: "privatekey",
|
||||
PERSONAL_ACCESS_TOKEN: "personal access token",
|
||||
OAUTH: "oauth",
|
||||
} as const
|
||||
|
||||
export const SECRET_TYPE_NAMES = {
|
||||
AWS_ACCESS_KEY: "AWS Access Key",
|
||||
AWS_SECRET_KEY: "AWS Secret Key",
|
||||
AWS_CREDENTIAL: "AWS Credential",
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN: "GitHub Personal Access Token",
|
||||
GITHUB_OAUTH_TOKEN: "GitHub OAuth Token",
|
||||
GITHUB_TOKEN: "GitHub Token",
|
||||
NPM_TOKEN: "NPM Token",
|
||||
GCP_SERVICE_ACCOUNT_KEY: "GCP Service Account Key",
|
||||
SSH_RSA_PRIVATE_KEY: "SSH RSA Private Key",
|
||||
SSH_DSA_PRIVATE_KEY: "SSH DSA Private Key",
|
||||
SSH_ECDSA_PRIVATE_KEY: "SSH ECDSA Private Key",
|
||||
SSH_ED25519_PRIVATE_KEY: "SSH Ed25519 Private Key",
|
||||
SSH_PRIVATE_KEY: "SSH Private Key",
|
||||
SLACK_BOT_TOKEN: "Slack Bot Token",
|
||||
SLACK_USER_TOKEN: "Slack User Token",
|
||||
SLACK_TOKEN: "Slack Token",
|
||||
BASIC_AUTH_CREDENTIALS: "Basic Authentication Credentials",
|
||||
API_KEY: "API Key",
|
||||
AUTHENTICATION_TOKEN: "Authentication Token",
|
||||
PASSWORD: "Password",
|
||||
SECRET: "Secret",
|
||||
SENSITIVE_DATA: "Sensitive Data",
|
||||
} as const
|
||||
|
||||
export const SECRET_EXAMPLE_VALUES = {
|
||||
AWS_ACCESS_KEY_ID: "AKIA1234567890ABCDEF",
|
||||
AWS_SECRET_ACCESS_KEY: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
GITHUB_TOKEN: "ghp_1234567890abcdefghijklmnopqrstuv",
|
||||
NPM_TOKEN: "npm_abc123xyz",
|
||||
SLACK_TOKEN: "xoxb-<token-here>",
|
||||
API_KEY: "sk_live_XXXXXXXXXXXXXXXXXXXX_example_key",
|
||||
HARDCODED_SECRET: "hardcoded-secret-value",
|
||||
} as const
|
||||
|
||||
export const FILE_ENCODING = {
|
||||
UTF8: "utf-8",
|
||||
} as const
|
||||
|
||||
export const REGEX_ESCAPE_PATTERN = {
|
||||
DOLLAR_AMPERSAND: "\\$&",
|
||||
} as const
|
||||
|
||||
export const DYNAMIC_IMPORT_PATTERN_PARTS = {
|
||||
QUOTE_START: '"`][^',
|
||||
QUOTE_END: "`]+['\"",
|
||||
} as const
|
||||
@@ -0,0 +1,29 @@
|
||||
import { AnemicModelViolation } from "../value-objects/AnemicModelViolation"
|
||||
|
||||
/**
|
||||
* Interface for detecting anemic domain model violations in the codebase
|
||||
*
|
||||
* Anemic domain models are entities that contain only getters/setters
|
||||
* without business logic. This anti-pattern violates Domain-Driven Design
|
||||
* principles and leads to procedural code scattered in services.
|
||||
*/
|
||||
export interface IAnemicModelDetector {
|
||||
/**
|
||||
* Detects anemic model violations in the given code
|
||||
*
|
||||
* Analyzes classes in domain/entities to identify:
|
||||
* - Classes with only getters and setters (no business logic)
|
||||
* - Classes with public setters (DDD anti-pattern)
|
||||
* - Classes with low method-to-property ratio
|
||||
*
|
||||
* @param code - Source code to analyze
|
||||
* @param filePath - Path to the file being analyzed
|
||||
* @param layer - The architectural layer of the file (domain, application, infrastructure, shared)
|
||||
* @returns Array of detected anemic model violations
|
||||
*/
|
||||
detectAnemicModels(
|
||||
code: string,
|
||||
filePath: string,
|
||||
layer: string | undefined,
|
||||
): AnemicModelViolation[]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { HardcodedValue } from "../value-objects/HardcodedValue"
|
||||
|
||||
export interface ValueLocation {
|
||||
file: string
|
||||
line: number
|
||||
context: string
|
||||
}
|
||||
|
||||
export interface DuplicateInfo {
|
||||
value: string | number | boolean
|
||||
locations: ValueLocation[]
|
||||
count: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for tracking duplicate hardcoded values across files
|
||||
*
|
||||
* Helps identify values that are used in multiple places
|
||||
* and should be extracted to a shared constant.
|
||||
*/
|
||||
export interface IDuplicateValueTracker {
|
||||
/**
|
||||
* Adds a hardcoded value to tracking
|
||||
*/
|
||||
track(violation: HardcodedValue, filePath: string): void
|
||||
|
||||
/**
|
||||
* Gets all duplicate values (values used in 2+ places)
|
||||
*/
|
||||
getDuplicates(): DuplicateInfo[]
|
||||
|
||||
/**
|
||||
* Gets duplicate locations for a specific value
|
||||
*/
|
||||
getDuplicateLocations(value: string | number | boolean, type: string): ValueLocation[] | null
|
||||
|
||||
/**
|
||||
* Checks if a value is duplicated
|
||||
*/
|
||||
isDuplicate(value: string | number | boolean, type: string): boolean
|
||||
|
||||
/**
|
||||
* Gets statistics about duplicates
|
||||
*/
|
||||
getStats(): {
|
||||
totalValues: number
|
||||
duplicateValues: number
|
||||
duplicatePercentage: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all tracked values
|
||||
*/
|
||||
clear(): void
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import { ValueObject } from "./ValueObject"
|
||||
import { ANEMIC_MODEL_MESSAGES } from "../constants/Messages"
|
||||
import { EXAMPLE_CODE_CONSTANTS } from "../../shared/constants"
|
||||
|
||||
interface AnemicModelViolationProps {
|
||||
readonly className: string
|
||||
readonly filePath: string
|
||||
readonly layer: string
|
||||
readonly line?: number
|
||||
readonly methodCount: number
|
||||
readonly propertyCount: number
|
||||
readonly hasOnlyGettersSetters: boolean
|
||||
readonly hasPublicSetters: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an anemic domain model violation in the codebase
|
||||
*
|
||||
* Anemic domain model occurs when entities have only getters/setters
|
||||
* without business logic. This violates Domain-Driven Design principles
|
||||
* and leads to procedural code instead of object-oriented design.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Bad: Anemic model with only getters/setters
|
||||
* const violation = AnemicModelViolation.create(
|
||||
* 'Order',
|
||||
* 'src/domain/entities/Order.ts',
|
||||
* 'domain',
|
||||
* 10,
|
||||
* 4,
|
||||
* 2,
|
||||
* true,
|
||||
* true
|
||||
* )
|
||||
*
|
||||
* console.log(violation.getMessage())
|
||||
* // "Class 'Order' is anemic: 4 methods (all getters/setters) for 2 properties"
|
||||
* ```
|
||||
*/
|
||||
export class AnemicModelViolation extends ValueObject<AnemicModelViolationProps> {
|
||||
private constructor(props: AnemicModelViolationProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public static create(
|
||||
className: string,
|
||||
filePath: string,
|
||||
layer: string,
|
||||
line: number | undefined,
|
||||
methodCount: number,
|
||||
propertyCount: number,
|
||||
hasOnlyGettersSetters: boolean,
|
||||
hasPublicSetters: boolean,
|
||||
): AnemicModelViolation {
|
||||
return new AnemicModelViolation({
|
||||
className,
|
||||
filePath,
|
||||
layer,
|
||||
line,
|
||||
methodCount,
|
||||
propertyCount,
|
||||
hasOnlyGettersSetters,
|
||||
hasPublicSetters,
|
||||
})
|
||||
}
|
||||
|
||||
public get className(): string {
|
||||
return this.props.className
|
||||
}
|
||||
|
||||
public get filePath(): string {
|
||||
return this.props.filePath
|
||||
}
|
||||
|
||||
public get layer(): string {
|
||||
return this.props.layer
|
||||
}
|
||||
|
||||
public get line(): number | undefined {
|
||||
return this.props.line
|
||||
}
|
||||
|
||||
public get methodCount(): number {
|
||||
return this.props.methodCount
|
||||
}
|
||||
|
||||
public get propertyCount(): number {
|
||||
return this.props.propertyCount
|
||||
}
|
||||
|
||||
public get hasOnlyGettersSetters(): boolean {
|
||||
return this.props.hasOnlyGettersSetters
|
||||
}
|
||||
|
||||
public get hasPublicSetters(): boolean {
|
||||
return this.props.hasPublicSetters
|
||||
}
|
||||
|
||||
public getMessage(): string {
|
||||
if (this.props.hasPublicSetters) {
|
||||
return `Class '${this.props.className}' has public setters (anti-pattern in DDD)`
|
||||
}
|
||||
|
||||
if (this.props.hasOnlyGettersSetters) {
|
||||
return `Class '${this.props.className}' is anemic: ${String(this.props.methodCount)} methods (all getters/setters) for ${String(this.props.propertyCount)} properties`
|
||||
}
|
||||
|
||||
const ratio = this.props.methodCount / Math.max(this.props.propertyCount, 1)
|
||||
return `Class '${this.props.className}' appears anemic: low method-to-property ratio (${ratio.toFixed(1)}:1)`
|
||||
}
|
||||
|
||||
public getSuggestion(): string {
|
||||
const suggestions: string[] = []
|
||||
|
||||
if (this.props.hasPublicSetters) {
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.REMOVE_PUBLIC_SETTERS)
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.USE_METHODS_FOR_CHANGES)
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.ENCAPSULATE_INVARIANTS)
|
||||
}
|
||||
|
||||
if (this.props.hasOnlyGettersSetters || this.props.methodCount < 2) {
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.ADD_BUSINESS_METHODS)
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.MOVE_LOGIC_FROM_SERVICES)
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.ENCAPSULATE_BUSINESS_RULES)
|
||||
suggestions.push(ANEMIC_MODEL_MESSAGES.USE_DOMAIN_EVENTS)
|
||||
}
|
||||
|
||||
return suggestions.join("\n")
|
||||
}
|
||||
|
||||
public getExampleFix(): string {
|
||||
if (this.props.hasPublicSetters) {
|
||||
return `
|
||||
// ❌ Bad: Public setters allow uncontrolled state changes
|
||||
class ${this.props.className} {
|
||||
private status: string
|
||||
|
||||
public setStatus(status: string): void {
|
||||
this.status = status // No validation!
|
||||
}
|
||||
|
||||
public getStatus(): string {
|
||||
return this.status
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Good: Business methods with validation
|
||||
class ${this.props.className} {
|
||||
private status: OrderStatus
|
||||
|
||||
public approve(): void {
|
||||
if (!this.canBeApproved()) {
|
||||
throw new CannotApproveOrderError()
|
||||
}
|
||||
this.status = OrderStatus.APPROVED
|
||||
this.events.push(new OrderApprovedEvent(this.id))
|
||||
}
|
||||
|
||||
public reject(reason: string): void {
|
||||
if (!this.canBeRejected()) {
|
||||
throw new CannotRejectOrderError()
|
||||
}
|
||||
this.status = OrderStatus.REJECTED
|
||||
this.rejectionReason = reason
|
||||
this.events.push(new OrderRejectedEvent(this.id, reason))
|
||||
}
|
||||
|
||||
public getStatus(): OrderStatus {
|
||||
return this.status
|
||||
}
|
||||
|
||||
private canBeApproved(): boolean {
|
||||
return this.status === OrderStatus.PENDING && this.hasItems()
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
return `
|
||||
// ❌ Bad: Anemic model (only getters/setters)
|
||||
class ${this.props.className} {
|
||||
getStatus() { return this.status }
|
||||
setStatus(status: string) { this.status = status }
|
||||
|
||||
getTotal() { return this.total }
|
||||
setTotal(total: number) { this.total = total }
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
approve(order: ${this.props.className}): void {
|
||||
if (order.getStatus() !== '${EXAMPLE_CODE_CONSTANTS.ORDER_STATUS_PENDING}') {
|
||||
throw new Error('${EXAMPLE_CODE_CONSTANTS.CANNOT_APPROVE_ERROR}')
|
||||
}
|
||||
order.setStatus('${EXAMPLE_CODE_CONSTANTS.ORDER_STATUS_APPROVED}')
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Good: Rich domain model with business logic
|
||||
class ${this.props.className} {
|
||||
private readonly id: OrderId
|
||||
private status: OrderStatus
|
||||
private items: OrderItem[]
|
||||
private events: DomainEvent[] = []
|
||||
|
||||
public approve(): void {
|
||||
if (!this.isPending()) {
|
||||
throw new CannotApproveOrderError()
|
||||
}
|
||||
this.status = OrderStatus.APPROVED
|
||||
this.events.push(new OrderApprovedEvent(this.id))
|
||||
}
|
||||
|
||||
public calculateTotal(): Money {
|
||||
return this.items.reduce(
|
||||
(sum, item) => sum.add(item.getPrice()),
|
||||
Money.zero()
|
||||
)
|
||||
}
|
||||
|
||||
public addItem(item: OrderItem): void {
|
||||
if (this.isApproved()) {
|
||||
throw new CannotModifyApprovedOrderError()
|
||||
}
|
||||
this.items.push(item)
|
||||
}
|
||||
|
||||
public getStatus(): OrderStatus {
|
||||
return this.status
|
||||
}
|
||||
|
||||
private isPending(): boolean {
|
||||
return this.status === OrderStatus.PENDING
|
||||
}
|
||||
|
||||
private isApproved(): boolean {
|
||||
return this.status === OrderStatus.APPROVED
|
||||
}
|
||||
}`
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,40 @@
|
||||
import { ValueObject } from "./ValueObject"
|
||||
import { HARDCODE_TYPES } from "../../shared/constants/rules"
|
||||
import { DETECTION_PATTERNS, HARDCODE_TYPES } from "../../shared/constants/rules"
|
||||
import { CONSTANT_NAMES, LOCATIONS, SUGGESTION_KEYWORDS } from "../constants/Suggestions"
|
||||
|
||||
export type HardcodeType = (typeof HARDCODE_TYPES)[keyof typeof HARDCODE_TYPES]
|
||||
|
||||
export type ValueType =
|
||||
| "email"
|
||||
| "url"
|
||||
| "ip_address"
|
||||
| "file_path"
|
||||
| "date"
|
||||
| "api_key"
|
||||
| "uuid"
|
||||
| "version"
|
||||
| "color"
|
||||
| "mac_address"
|
||||
| "base64"
|
||||
| "config"
|
||||
| "generic"
|
||||
|
||||
export type ValueImportance = "critical" | "high" | "medium" | "low"
|
||||
|
||||
export interface DuplicateLocation {
|
||||
file: string
|
||||
line: number
|
||||
}
|
||||
|
||||
interface HardcodedValueProps {
|
||||
readonly value: string | number
|
||||
readonly value: string | number | boolean
|
||||
readonly type: HardcodeType
|
||||
readonly valueType?: ValueType
|
||||
readonly line: number
|
||||
readonly column: number
|
||||
readonly context: string
|
||||
readonly duplicateLocations?: DuplicateLocation[]
|
||||
readonly withinFileUsageCount?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,22 +46,28 @@ export class HardcodedValue extends ValueObject<HardcodedValueProps> {
|
||||
}
|
||||
|
||||
public static create(
|
||||
value: string | number,
|
||||
value: string | number | boolean,
|
||||
type: HardcodeType,
|
||||
line: number,
|
||||
column: number,
|
||||
context: string,
|
||||
valueType?: ValueType,
|
||||
duplicateLocations?: DuplicateLocation[],
|
||||
withinFileUsageCount?: number,
|
||||
): HardcodedValue {
|
||||
return new HardcodedValue({
|
||||
value,
|
||||
type,
|
||||
valueType,
|
||||
line,
|
||||
column,
|
||||
context,
|
||||
duplicateLocations,
|
||||
withinFileUsageCount,
|
||||
})
|
||||
}
|
||||
|
||||
public get value(): string | number {
|
||||
public get value(): string | number | boolean {
|
||||
return this.props.value
|
||||
}
|
||||
|
||||
@@ -56,6 +87,28 @@ export class HardcodedValue extends ValueObject<HardcodedValueProps> {
|
||||
return this.props.context
|
||||
}
|
||||
|
||||
public get valueType(): ValueType | undefined {
|
||||
return this.props.valueType
|
||||
}
|
||||
|
||||
public get duplicateLocations(): DuplicateLocation[] | undefined {
|
||||
return this.props.duplicateLocations
|
||||
}
|
||||
|
||||
public get withinFileUsageCount(): number | undefined {
|
||||
return this.props.withinFileUsageCount
|
||||
}
|
||||
|
||||
public hasDuplicates(): boolean {
|
||||
return (
|
||||
this.props.duplicateLocations !== undefined && this.props.duplicateLocations.length > 0
|
||||
)
|
||||
}
|
||||
|
||||
public isAlmostConstant(): boolean {
|
||||
return this.props.withinFileUsageCount !== undefined && this.props.withinFileUsageCount >= 2
|
||||
}
|
||||
|
||||
public isMagicNumber(): boolean {
|
||||
return this.props.type === HARDCODE_TYPES.MAGIC_NUMBER
|
||||
}
|
||||
@@ -106,6 +159,154 @@ export class HardcodedValue extends ValueObject<HardcodedValueProps> {
|
||||
private suggestStringConstantName(): string {
|
||||
const value = String(this.props.value)
|
||||
const context = this.props.context.toLowerCase()
|
||||
const valueType = this.props.valueType
|
||||
|
||||
if (valueType === "email") {
|
||||
if (context.includes("admin")) {
|
||||
return "ADMIN_EMAIL"
|
||||
}
|
||||
if (context.includes("support")) {
|
||||
return "SUPPORT_EMAIL"
|
||||
}
|
||||
if (context.includes("noreply") || context.includes("no-reply")) {
|
||||
return "NOREPLY_EMAIL"
|
||||
}
|
||||
return "DEFAULT_EMAIL"
|
||||
}
|
||||
|
||||
if (valueType === "api_key") {
|
||||
if (context.includes("secret")) {
|
||||
return "API_SECRET_KEY"
|
||||
}
|
||||
if (context.includes("public")) {
|
||||
return "API_PUBLIC_KEY"
|
||||
}
|
||||
return "API_KEY"
|
||||
}
|
||||
|
||||
if (valueType === "url") {
|
||||
if (context.includes("api")) {
|
||||
return "API_BASE_URL"
|
||||
}
|
||||
if (context.includes("database") || context.includes("db")) {
|
||||
return "DATABASE_URL"
|
||||
}
|
||||
if (context.includes("mongo")) {
|
||||
return "MONGODB_CONNECTION_STRING"
|
||||
}
|
||||
if (context.includes("postgres") || context.includes("pg")) {
|
||||
return "POSTGRES_URL"
|
||||
}
|
||||
return "BASE_URL"
|
||||
}
|
||||
|
||||
if (valueType === "ip_address") {
|
||||
if (context.includes("server")) {
|
||||
return "SERVER_IP"
|
||||
}
|
||||
if (context.includes("database") || context.includes("db")) {
|
||||
return "DATABASE_HOST"
|
||||
}
|
||||
if (context.includes("redis")) {
|
||||
return "REDIS_HOST"
|
||||
}
|
||||
return "HOST_IP"
|
||||
}
|
||||
|
||||
if (valueType === "file_path") {
|
||||
if (context.includes("log")) {
|
||||
return "LOG_FILE_PATH"
|
||||
}
|
||||
if (context.includes("config")) {
|
||||
return "CONFIG_FILE_PATH"
|
||||
}
|
||||
if (context.includes("data")) {
|
||||
return "DATA_DIR_PATH"
|
||||
}
|
||||
if (context.includes("temp")) {
|
||||
return "TEMP_DIR_PATH"
|
||||
}
|
||||
return "FILE_PATH"
|
||||
}
|
||||
|
||||
if (valueType === "date") {
|
||||
if (context.includes("deadline")) {
|
||||
return "DEADLINE"
|
||||
}
|
||||
if (context.includes("start")) {
|
||||
return "START_DATE"
|
||||
}
|
||||
if (context.includes("end")) {
|
||||
return "END_DATE"
|
||||
}
|
||||
if (context.includes("expir")) {
|
||||
return "EXPIRATION_DATE"
|
||||
}
|
||||
return "DEFAULT_DATE"
|
||||
}
|
||||
|
||||
if (valueType === "uuid") {
|
||||
if (context.includes("id") || context.includes("identifier")) {
|
||||
return "DEFAULT_ID"
|
||||
}
|
||||
if (context.includes("request")) {
|
||||
return "REQUEST_ID"
|
||||
}
|
||||
if (context.includes("session")) {
|
||||
return "SESSION_ID"
|
||||
}
|
||||
return "UUID_CONSTANT"
|
||||
}
|
||||
|
||||
if (valueType === "version") {
|
||||
if (context.includes("api")) {
|
||||
return "API_VERSION"
|
||||
}
|
||||
if (context.includes("app")) {
|
||||
return "APP_VERSION"
|
||||
}
|
||||
return "VERSION"
|
||||
}
|
||||
|
||||
if (valueType === "color") {
|
||||
if (context.includes("primary")) {
|
||||
return "PRIMARY_COLOR"
|
||||
}
|
||||
if (context.includes("secondary")) {
|
||||
return "SECONDARY_COLOR"
|
||||
}
|
||||
if (context.includes("background")) {
|
||||
return "BACKGROUND_COLOR"
|
||||
}
|
||||
return "COLOR_CONSTANT"
|
||||
}
|
||||
|
||||
if (valueType === "mac_address") {
|
||||
return "MAC_ADDRESS"
|
||||
}
|
||||
|
||||
if (valueType === "base64") {
|
||||
if (context.includes("token")) {
|
||||
return "ENCODED_TOKEN"
|
||||
}
|
||||
if (context.includes("key")) {
|
||||
return "ENCODED_KEY"
|
||||
}
|
||||
return "BASE64_VALUE"
|
||||
}
|
||||
|
||||
if (valueType === "config") {
|
||||
if (context.includes("endpoint")) {
|
||||
return "API_ENDPOINT"
|
||||
}
|
||||
if (context.includes("route")) {
|
||||
return "ROUTE_PATH"
|
||||
}
|
||||
if (context.includes("connection")) {
|
||||
return "CONNECTION_STRING"
|
||||
}
|
||||
return "CONFIG_VALUE"
|
||||
}
|
||||
|
||||
if (value.includes(SUGGESTION_KEYWORDS.HTTP)) {
|
||||
return CONSTANT_NAMES.API_BASE_URL
|
||||
@@ -135,6 +336,23 @@ export class HardcodedValue extends ValueObject<HardcodedValueProps> {
|
||||
}
|
||||
|
||||
const context = this.props.context.toLowerCase()
|
||||
const valueType = this.props.valueType
|
||||
|
||||
if (valueType === "api_key" || valueType === "url" || valueType === "ip_address") {
|
||||
return "src/config/environment.ts"
|
||||
}
|
||||
|
||||
if (valueType === "email") {
|
||||
return "src/config/contacts.ts"
|
||||
}
|
||||
|
||||
if (valueType === "file_path") {
|
||||
return "src/config/paths.ts"
|
||||
}
|
||||
|
||||
if (valueType === "date") {
|
||||
return "src/config/dates.ts"
|
||||
}
|
||||
|
||||
if (
|
||||
context.includes(SUGGESTION_KEYWORDS.ENTITY) ||
|
||||
@@ -153,4 +371,122 @@ export class HardcodedValue extends ValueObject<HardcodedValueProps> {
|
||||
|
||||
return LOCATIONS.SHARED_CONSTANTS
|
||||
}
|
||||
|
||||
public getDetailedSuggestion(currentLayer?: string): string {
|
||||
const constantName = this.suggestConstantName()
|
||||
const location = this.suggestLocation(currentLayer)
|
||||
const valueTypeLabel = this.valueType ? ` (${this.valueType})` : ""
|
||||
|
||||
let suggestion = `Extract${valueTypeLabel} to constant ${constantName} in ${location}`
|
||||
|
||||
if (this.isAlmostConstant() && this.withinFileUsageCount) {
|
||||
suggestion += `. This value appears ${String(this.withinFileUsageCount)} times in this file`
|
||||
}
|
||||
|
||||
if (this.hasDuplicates() && this.duplicateLocations) {
|
||||
const count = this.duplicateLocations.length
|
||||
const fileList = this.duplicateLocations
|
||||
.slice(0, 3)
|
||||
.map((loc) => `${loc.file}:${String(loc.line)}`)
|
||||
.join(", ")
|
||||
|
||||
const more = count > 3 ? ` and ${String(count - 3)} more` : ""
|
||||
suggestion += `. Also duplicated in ${String(count)} other file(s): ${fileList}${more}`
|
||||
}
|
||||
|
||||
return suggestion
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes variable name and context to determine importance
|
||||
*/
|
||||
public getImportance(): ValueImportance {
|
||||
const context = this.props.context.toLowerCase()
|
||||
const valueType = this.props.valueType
|
||||
|
||||
if (valueType === "api_key") {
|
||||
return "critical"
|
||||
}
|
||||
|
||||
const criticalKeywords = [
|
||||
...DETECTION_PATTERNS.SENSITIVE_KEYWORDS,
|
||||
...DETECTION_PATTERNS.BUSINESS_KEYWORDS,
|
||||
"key",
|
||||
"age",
|
||||
]
|
||||
|
||||
if (criticalKeywords.some((keyword) => context.includes(keyword))) {
|
||||
return "critical"
|
||||
}
|
||||
|
||||
const highKeywords = [...DETECTION_PATTERNS.TECHNICAL_KEYWORDS, "db", "api"]
|
||||
|
||||
if (highKeywords.some((keyword) => context.includes(keyword))) {
|
||||
return "high"
|
||||
}
|
||||
|
||||
if (valueType === "url" || valueType === "ip_address" || valueType === "email") {
|
||||
return "high"
|
||||
}
|
||||
|
||||
const mediumKeywords = DETECTION_PATTERNS.MEDIUM_KEYWORDS
|
||||
|
||||
if (mediumKeywords.some((keyword) => context.includes(keyword))) {
|
||||
return "medium"
|
||||
}
|
||||
|
||||
const lowKeywords = DETECTION_PATTERNS.UI_KEYWORDS
|
||||
|
||||
if (lowKeywords.some((keyword) => context.includes(keyword))) {
|
||||
return "low"
|
||||
}
|
||||
|
||||
return "medium"
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this violation should be skipped based on layer strictness
|
||||
*
|
||||
* Different layers have different tolerance levels:
|
||||
* - domain: strictest (no hardcoded values allowed)
|
||||
* - application: strict (only low importance allowed)
|
||||
* - infrastructure: moderate (low and some medium allowed)
|
||||
* - cli: lenient (UI constants allowed)
|
||||
*/
|
||||
public shouldSkip(layer?: string): boolean {
|
||||
if (!layer) {
|
||||
return false
|
||||
}
|
||||
|
||||
const importance = this.getImportance()
|
||||
|
||||
if (layer === "domain") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (layer === "application") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (layer === "infrastructure") {
|
||||
return importance === "low" && this.isUIConstant()
|
||||
}
|
||||
|
||||
if (layer === "cli") {
|
||||
return importance === "low" && this.isUIConstant()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this value is a UI-related constant
|
||||
*/
|
||||
private isUIConstant(): boolean {
|
||||
const context = this.props.context.toLowerCase()
|
||||
|
||||
const uiKeywords = DETECTION_PATTERNS.UI_KEYWORDS
|
||||
|
||||
return uiKeywords.some((keyword) => context.includes(keyword))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ValueObject } from "./ValueObject"
|
||||
import { SECRET_VIOLATION_MESSAGES } from "../constants/Messages"
|
||||
import { SEVERITY_LEVELS } from "../../shared/constants"
|
||||
import { FILE_ENCODING, SECRET_EXAMPLE_VALUES, SECRET_KEYWORDS } from "../constants/SecretExamples"
|
||||
|
||||
interface SecretViolationProps {
|
||||
readonly file: string
|
||||
@@ -98,32 +100,31 @@ export class SecretViolation extends ValueObject<SecretViolationProps> {
|
||||
return this.getExampleFixForSecretType(this.props.secretType)
|
||||
}
|
||||
|
||||
public getSeverity(): "critical" {
|
||||
return "critical"
|
||||
public getSeverity(): typeof SEVERITY_LEVELS.CRITICAL {
|
||||
return SEVERITY_LEVELS.CRITICAL
|
||||
}
|
||||
|
||||
private getExampleFixForSecretType(secretType: string): string {
|
||||
const lowerType = secretType.toLowerCase()
|
||||
|
||||
if (lowerType.includes("aws")) {
|
||||
if (lowerType.includes(SECRET_KEYWORDS.AWS)) {
|
||||
return `
|
||||
// ❌ Bad: Hardcoded AWS credentials
|
||||
const AWS_ACCESS_KEY_ID = "AKIA1234567890ABCDEF"
|
||||
const AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
const AWS_ACCESS_KEY_ID = "${SECRET_EXAMPLE_VALUES.AWS_ACCESS_KEY_ID}"
|
||||
const AWS_SECRET_ACCESS_KEY = "${SECRET_EXAMPLE_VALUES.AWS_SECRET_ACCESS_KEY}"
|
||||
|
||||
// ✅ Good: Use environment variables
|
||||
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID
|
||||
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY
|
||||
|
||||
// ✅ Good: Use AWS SDK credentials provider
|
||||
import { fromEnv } from "@aws-sdk/credential-providers"
|
||||
const credentials = fromEnv()`
|
||||
// ✅ Good: Use credentials provider (in infrastructure layer)
|
||||
// Load credentials from environment or credentials file`
|
||||
}
|
||||
|
||||
if (lowerType.includes("github")) {
|
||||
if (lowerType.includes(SECRET_KEYWORDS.GITHUB)) {
|
||||
return `
|
||||
// ❌ Bad: Hardcoded GitHub token
|
||||
const GITHUB_TOKEN = "ghp_1234567890abcdefghijklmnopqrstuv"
|
||||
const GITHUB_TOKEN = "${SECRET_EXAMPLE_VALUES.GITHUB_TOKEN}"
|
||||
|
||||
// ✅ Good: Use environment variables
|
||||
const GITHUB_TOKEN = process.env.GITHUB_TOKEN
|
||||
@@ -132,10 +133,10 @@ const GITHUB_TOKEN = process.env.GITHUB_TOKEN
|
||||
// Use GitHub Apps for automated workflows instead of personal access tokens`
|
||||
}
|
||||
|
||||
if (lowerType.includes("npm")) {
|
||||
if (lowerType.includes(SECRET_KEYWORDS.NPM)) {
|
||||
return `
|
||||
// ❌ Bad: Hardcoded NPM token in code
|
||||
const NPM_TOKEN = "npm_abc123xyz"
|
||||
const NPM_TOKEN = "${SECRET_EXAMPLE_VALUES.NPM_TOKEN}"
|
||||
|
||||
// ✅ Good: Use .npmrc file (add to .gitignore)
|
||||
// .npmrc
|
||||
@@ -145,7 +146,10 @@ const NPM_TOKEN = "npm_abc123xyz"
|
||||
const NPM_TOKEN = process.env.NPM_TOKEN`
|
||||
}
|
||||
|
||||
if (lowerType.includes("ssh") || lowerType.includes("private key")) {
|
||||
if (
|
||||
lowerType.includes(SECRET_KEYWORDS.SSH) ||
|
||||
lowerType.includes(SECRET_KEYWORDS.PRIVATE_KEY)
|
||||
) {
|
||||
return `
|
||||
// ❌ Bad: Hardcoded SSH private key
|
||||
const privateKey = \`-----BEGIN RSA PRIVATE KEY-----
|
||||
@@ -153,16 +157,16 @@ MIIEpAIBAAKCAQEA...\`
|
||||
|
||||
// ✅ Good: Load from secure file (not in repository)
|
||||
import fs from "fs"
|
||||
const privateKey = fs.readFileSync(process.env.SSH_KEY_PATH, "utf-8")
|
||||
const privateKey = fs.readFileSync(process.env.SSH_KEY_PATH, "${FILE_ENCODING.UTF8}")
|
||||
|
||||
// ✅ Good: Use SSH agent
|
||||
// Configure SSH agent to handle keys securely`
|
||||
}
|
||||
|
||||
if (lowerType.includes("slack")) {
|
||||
if (lowerType.includes(SECRET_KEYWORDS.SLACK)) {
|
||||
return `
|
||||
// ❌ Bad: Hardcoded Slack token
|
||||
const SLACK_TOKEN = "xoxb-XXXX-XXXX-XXXX-example-token-here"
|
||||
const SLACK_TOKEN = "${SECRET_EXAMPLE_VALUES.SLACK_TOKEN}"
|
||||
|
||||
// ✅ Good: Use environment variables
|
||||
const SLACK_TOKEN = process.env.SLACK_BOT_TOKEN
|
||||
@@ -171,23 +175,25 @@ const SLACK_TOKEN = process.env.SLACK_BOT_TOKEN
|
||||
// Implement OAuth 2.0 flow instead of hardcoding tokens`
|
||||
}
|
||||
|
||||
if (lowerType.includes("api key") || lowerType.includes("apikey")) {
|
||||
if (
|
||||
lowerType.includes(SECRET_KEYWORDS.API_KEY) ||
|
||||
lowerType.includes(SECRET_KEYWORDS.APIKEY)
|
||||
) {
|
||||
return `
|
||||
// ❌ Bad: Hardcoded API key
|
||||
const API_KEY = "sk_live_XXXXXXXXXXXXXXXXXXXX_example_key"
|
||||
const API_KEY = "${SECRET_EXAMPLE_VALUES.API_KEY}"
|
||||
|
||||
// ✅ Good: Use environment variables
|
||||
const API_KEY = process.env.API_KEY
|
||||
|
||||
// ✅ Good: Use secret management service
|
||||
import { SecretsManager } from "aws-sdk"
|
||||
const secretsManager = new SecretsManager()
|
||||
const secret = await secretsManager.getSecretValue({ SecretId: "api-key" }).promise()`
|
||||
// ✅ Good: Use secret management service (in infrastructure layer)
|
||||
// AWS Secrets Manager, HashiCorp Vault, Azure Key Vault
|
||||
// Implement secret retrieval in infrastructure and inject via DI`
|
||||
}
|
||||
|
||||
return `
|
||||
// ❌ Bad: Hardcoded secret
|
||||
const SECRET = "hardcoded-secret-value"
|
||||
const SECRET = "${SECRET_EXAMPLE_VALUES.HARDCODED_SECRET}"
|
||||
|
||||
// ✅ Good: Use environment variables
|
||||
const SECRET = process.env.SECRET_KEY
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
import { IAnemicModelDetector } from "../../domain/services/IAnemicModelDetector"
|
||||
import { AnemicModelViolation } from "../../domain/value-objects/AnemicModelViolation"
|
||||
import { CLASS_KEYWORDS } from "../../shared/constants"
|
||||
import { ANALYZER_DEFAULTS, ANEMIC_MODEL_FLAGS, LAYERS } from "../../shared/constants/rules"
|
||||
|
||||
/**
|
||||
* Detects anemic domain model violations
|
||||
*
|
||||
* This detector identifies entities that lack business logic and contain
|
||||
* only getters/setters. Anemic models violate Domain-Driven Design principles.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const detector = new AnemicModelDetector()
|
||||
*
|
||||
* // Detect anemic models in entity file
|
||||
* const code = `
|
||||
* class Order {
|
||||
* getStatus() { return this.status }
|
||||
* setStatus(status: string) { this.status = status }
|
||||
* getTotal() { return this.total }
|
||||
* setTotal(total: number) { this.total = total }
|
||||
* }
|
||||
* `
|
||||
* const violations = detector.detectAnemicModels(
|
||||
* code,
|
||||
* 'src/domain/entities/Order.ts',
|
||||
* 'domain'
|
||||
* )
|
||||
*
|
||||
* // violations will contain anemic model violation
|
||||
* console.log(violations.length) // 1
|
||||
* console.log(violations[0].className) // 'Order'
|
||||
* ```
|
||||
*/
|
||||
export class AnemicModelDetector implements IAnemicModelDetector {
|
||||
private readonly entityPatterns = [/\/entities\//, /\/aggregates\//]
|
||||
private readonly excludePatterns = [
|
||||
/\.test\.ts$/,
|
||||
/\.spec\.ts$/,
|
||||
/Dto\.ts$/,
|
||||
/Request\.ts$/,
|
||||
/Response\.ts$/,
|
||||
/Mapper\.ts$/,
|
||||
]
|
||||
|
||||
/**
|
||||
* Detects anemic model violations in the given code
|
||||
*/
|
||||
public detectAnemicModels(
|
||||
code: string,
|
||||
filePath: string,
|
||||
layer: string | undefined,
|
||||
): AnemicModelViolation[] {
|
||||
if (!this.shouldAnalyze(filePath, layer)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const violations: AnemicModelViolation[] = []
|
||||
const classes = this.extractClasses(code)
|
||||
|
||||
for (const classInfo of classes) {
|
||||
const violation = this.analyzeClass(classInfo, filePath, layer || LAYERS.DOMAIN)
|
||||
if (violation) {
|
||||
violations.push(violation)
|
||||
}
|
||||
}
|
||||
|
||||
return violations
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if file should be analyzed
|
||||
*/
|
||||
private shouldAnalyze(filePath: string, layer: string | undefined): boolean {
|
||||
if (layer !== LAYERS.DOMAIN) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.excludePatterns.some((pattern) => pattern.test(filePath))) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.entityPatterns.some((pattern) => pattern.test(filePath))
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts class information from code
|
||||
*/
|
||||
private extractClasses(code: string): ClassInfo[] {
|
||||
const classes: ClassInfo[] = []
|
||||
const lines = code.split("\n")
|
||||
let currentClass: { name: string; startLine: number; startIndex: number } | null = null
|
||||
let braceCount = 0
|
||||
let classBody = ""
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
|
||||
if (!currentClass) {
|
||||
const classRegex = /^\s*(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/
|
||||
const classMatch = classRegex.exec(line)
|
||||
if (classMatch) {
|
||||
currentClass = {
|
||||
name: classMatch[1],
|
||||
startLine: i + 1,
|
||||
startIndex: lines.slice(0, i).join("\n").length,
|
||||
}
|
||||
braceCount = 0
|
||||
classBody = ""
|
||||
}
|
||||
}
|
||||
|
||||
if (currentClass) {
|
||||
for (const char of line) {
|
||||
if (char === "{") {
|
||||
braceCount++
|
||||
} else if (char === "}") {
|
||||
braceCount--
|
||||
}
|
||||
}
|
||||
|
||||
if (braceCount > 0) {
|
||||
classBody = `${classBody}${line}\n`
|
||||
} else if (braceCount === 0 && classBody.length > 0) {
|
||||
const properties = this.extractProperties(classBody)
|
||||
const methods = this.extractMethods(classBody)
|
||||
|
||||
classes.push({
|
||||
className: currentClass.name,
|
||||
lineNumber: currentClass.startLine,
|
||||
properties,
|
||||
methods,
|
||||
})
|
||||
|
||||
currentClass = null
|
||||
classBody = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return classes
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts properties from class body
|
||||
*/
|
||||
private extractProperties(classBody: string): PropertyInfo[] {
|
||||
const properties: PropertyInfo[] = []
|
||||
const propertyRegex = /(?:private|protected|public|readonly)*\s*(\w+)(?:\?)?:\s*\w+/g
|
||||
|
||||
let match
|
||||
while ((match = propertyRegex.exec(classBody)) !== null) {
|
||||
const propertyName = match[1]
|
||||
|
||||
if (!this.isMethodSignature(match[0])) {
|
||||
properties.push({ name: propertyName })
|
||||
}
|
||||
}
|
||||
|
||||
return properties
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts methods from class body
|
||||
*/
|
||||
private extractMethods(classBody: string): MethodInfo[] {
|
||||
const methods: MethodInfo[] = []
|
||||
const methodRegex =
|
||||
/(public|private|protected)?\s*(get|set)?\s+(\w+)\s*\([^)]*\)(?:\s*:\s*\w+)?/g
|
||||
|
||||
let match
|
||||
while ((match = methodRegex.exec(classBody)) !== null) {
|
||||
const visibility = match[1] || CLASS_KEYWORDS.PUBLIC
|
||||
const accessor = match[2]
|
||||
const methodName = match[3]
|
||||
|
||||
if (methodName === CLASS_KEYWORDS.CONSTRUCTOR) {
|
||||
continue
|
||||
}
|
||||
|
||||
const isGetter = accessor === "get" || this.isGetterMethod(methodName)
|
||||
const isSetter = accessor === "set" || this.isSetterMethod(methodName, classBody)
|
||||
const isPublic = visibility === CLASS_KEYWORDS.PUBLIC || !visibility
|
||||
|
||||
methods.push({
|
||||
name: methodName,
|
||||
isGetter,
|
||||
isSetter,
|
||||
isPublic,
|
||||
isBusinessLogic: !isGetter && !isSetter,
|
||||
})
|
||||
}
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes class for anemic model violations
|
||||
*/
|
||||
private analyzeClass(
|
||||
classInfo: ClassInfo,
|
||||
filePath: string,
|
||||
layer: string,
|
||||
): AnemicModelViolation | null {
|
||||
const { className, lineNumber, properties, methods } = classInfo
|
||||
|
||||
if (properties.length === 0 && methods.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const businessMethods = methods.filter((m) => m.isBusinessLogic)
|
||||
const hasOnlyGettersSetters = businessMethods.length === 0 && methods.length > 0
|
||||
const hasPublicSetters = methods.some((m) => m.isSetter && m.isPublic)
|
||||
|
||||
const methodCount = methods.length
|
||||
const propertyCount = properties.length
|
||||
|
||||
if (hasPublicSetters) {
|
||||
return AnemicModelViolation.create(
|
||||
className,
|
||||
filePath,
|
||||
layer,
|
||||
lineNumber,
|
||||
methodCount,
|
||||
propertyCount,
|
||||
ANEMIC_MODEL_FLAGS.HAS_ONLY_GETTERS_SETTERS_FALSE,
|
||||
ANEMIC_MODEL_FLAGS.HAS_PUBLIC_SETTERS_TRUE,
|
||||
)
|
||||
}
|
||||
|
||||
if (hasOnlyGettersSetters && methodCount >= 2 && propertyCount > 0) {
|
||||
return AnemicModelViolation.create(
|
||||
className,
|
||||
filePath,
|
||||
layer,
|
||||
lineNumber,
|
||||
methodCount,
|
||||
propertyCount,
|
||||
ANEMIC_MODEL_FLAGS.HAS_ONLY_GETTERS_SETTERS_TRUE,
|
||||
ANEMIC_MODEL_FLAGS.HAS_PUBLIC_SETTERS_FALSE,
|
||||
)
|
||||
}
|
||||
|
||||
const methodToPropertyRatio = methodCount / Math.max(propertyCount, 1)
|
||||
if (
|
||||
propertyCount > 0 &&
|
||||
businessMethods.length < 2 &&
|
||||
methodToPropertyRatio < 1.0 &&
|
||||
methodCount > 0
|
||||
) {
|
||||
return AnemicModelViolation.create(
|
||||
className,
|
||||
filePath,
|
||||
layer,
|
||||
lineNumber,
|
||||
methodCount,
|
||||
propertyCount,
|
||||
ANALYZER_DEFAULTS.HAS_ONLY_GETTERS_SETTERS,
|
||||
ANALYZER_DEFAULTS.HAS_PUBLIC_SETTERS,
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if method name is a getter pattern
|
||||
*/
|
||||
private isGetterMethod(methodName: string): boolean {
|
||||
return (
|
||||
methodName.startsWith("get") ||
|
||||
methodName.startsWith("is") ||
|
||||
methodName.startsWith("has")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if method is a setter pattern
|
||||
*/
|
||||
private isSetterMethod(methodName: string, _classBody: string): boolean {
|
||||
return methodName.startsWith("set")
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if property declaration is actually a method signature
|
||||
*/
|
||||
private isMethodSignature(propertyDeclaration: string): boolean {
|
||||
return propertyDeclaration.includes("(") && propertyDeclaration.includes(")")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets line number for a position in code
|
||||
*/
|
||||
private getLineNumber(code: string, position: number): number {
|
||||
const lines = code.substring(0, position).split("\n")
|
||||
return lines.length
|
||||
}
|
||||
}
|
||||
|
||||
interface ClassInfo {
|
||||
className: string
|
||||
lineNumber: number
|
||||
properties: PropertyInfo[]
|
||||
methods: MethodInfo[]
|
||||
}
|
||||
|
||||
interface PropertyInfo {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface MethodInfo {
|
||||
name: string
|
||||
isGetter: boolean
|
||||
isSetter: boolean
|
||||
isPublic: boolean
|
||||
isBusinessLogic: boolean
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import Parser from "tree-sitter"
|
||||
import { HardcodedValue } from "../../domain/value-objects/HardcodedValue"
|
||||
import { AstBooleanAnalyzer } from "../strategies/AstBooleanAnalyzer"
|
||||
import { AstConfigObjectAnalyzer } from "../strategies/AstConfigObjectAnalyzer"
|
||||
import { AstNumberAnalyzer } from "../strategies/AstNumberAnalyzer"
|
||||
import { AstStringAnalyzer } from "../strategies/AstStringAnalyzer"
|
||||
|
||||
/**
|
||||
* AST tree traverser for detecting hardcoded values
|
||||
*
|
||||
* Walks through the Abstract Syntax Tree and uses analyzers
|
||||
* to detect hardcoded numbers, strings, booleans, and configuration objects.
|
||||
* Also tracks value usage to identify "almost constants" - values used 2+ times.
|
||||
*/
|
||||
export class AstTreeTraverser {
|
||||
constructor(
|
||||
private readonly numberAnalyzer: AstNumberAnalyzer,
|
||||
private readonly stringAnalyzer: AstStringAnalyzer,
|
||||
private readonly booleanAnalyzer: AstBooleanAnalyzer,
|
||||
private readonly configObjectAnalyzer: AstConfigObjectAnalyzer,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Traverses the AST tree and collects hardcoded values
|
||||
*/
|
||||
public traverse(tree: Parser.Tree, sourceCode: string): HardcodedValue[] {
|
||||
const results: HardcodedValue[] = []
|
||||
const lines = sourceCode.split("\n")
|
||||
const cursor = tree.walk()
|
||||
|
||||
this.visit(cursor, lines, results)
|
||||
|
||||
this.markAlmostConstants(results)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks values that appear multiple times in the same file
|
||||
*/
|
||||
private markAlmostConstants(results: HardcodedValue[]): void {
|
||||
const valueUsage = new Map<string, number>()
|
||||
|
||||
for (const result of results) {
|
||||
const key = `${result.type}:${String(result.value)}`
|
||||
valueUsage.set(key, (valueUsage.get(key) || 0) + 1)
|
||||
}
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i]
|
||||
const key = `${result.type}:${String(result.value)}`
|
||||
const count = valueUsage.get(key) || 0
|
||||
|
||||
if (count >= 2 && !result.withinFileUsageCount) {
|
||||
results[i] = HardcodedValue.create(
|
||||
result.value,
|
||||
result.type,
|
||||
result.line,
|
||||
result.column,
|
||||
result.context,
|
||||
result.valueType,
|
||||
result.duplicateLocations,
|
||||
count,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively visits AST nodes
|
||||
*/
|
||||
private visit(cursor: Parser.TreeCursor, lines: string[], results: HardcodedValue[]): void {
|
||||
const node = cursor.currentNode
|
||||
|
||||
if (node.type === "object") {
|
||||
const violation = this.configObjectAnalyzer.analyze(node, lines)
|
||||
if (violation) {
|
||||
results.push(violation)
|
||||
}
|
||||
} else if (node.type === "number") {
|
||||
const violation = this.numberAnalyzer.analyze(node, lines)
|
||||
if (violation) {
|
||||
results.push(violation)
|
||||
}
|
||||
} else if (node.type === "string") {
|
||||
const violation = this.stringAnalyzer.analyze(node, lines)
|
||||
if (violation) {
|
||||
results.push(violation)
|
||||
}
|
||||
} else if (node.type === "true" || node.type === "false") {
|
||||
const violation = this.booleanAnalyzer.analyze(node, lines)
|
||||
if (violation) {
|
||||
results.push(violation)
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor.gotoFirstChild()) {
|
||||
do {
|
||||
this.visit(cursor, lines, results)
|
||||
} while (cursor.gotoNextSibling())
|
||||
cursor.gotoParent()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { HardcodedValue } from "../../domain/value-objects/HardcodedValue"
|
||||
import type {
|
||||
DuplicateInfo,
|
||||
IDuplicateValueTracker,
|
||||
ValueLocation,
|
||||
} from "../../domain/services/IDuplicateValueTracker"
|
||||
|
||||
/**
|
||||
* Tracks duplicate hardcoded values across files
|
||||
*
|
||||
* Helps identify values that are used in multiple places
|
||||
* and should be extracted to a shared constant.
|
||||
*/
|
||||
export class DuplicateValueTracker implements IDuplicateValueTracker {
|
||||
private readonly valueMap = new Map<string, ValueLocation[]>()
|
||||
|
||||
/**
|
||||
* Adds a hardcoded value to tracking
|
||||
*/
|
||||
public track(violation: HardcodedValue, filePath: string): void {
|
||||
const key = this.createKey(violation.value, violation.type)
|
||||
const location: ValueLocation = {
|
||||
file: filePath,
|
||||
line: violation.line,
|
||||
context: violation.context,
|
||||
}
|
||||
|
||||
const locations = this.valueMap.get(key)
|
||||
if (!locations) {
|
||||
this.valueMap.set(key, [location])
|
||||
} else {
|
||||
locations.push(location)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all duplicate values (values used in 2+ places)
|
||||
*/
|
||||
public getDuplicates(): DuplicateInfo[] {
|
||||
const duplicates: DuplicateInfo[] = []
|
||||
|
||||
for (const [key, locations] of this.valueMap.entries()) {
|
||||
if (locations.length >= 2) {
|
||||
const { value } = this.parseKey(key)
|
||||
duplicates.push({
|
||||
value,
|
||||
locations,
|
||||
count: locations.length,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return duplicates.sort((a, b) => b.count - a.count)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets duplicate locations for a specific value
|
||||
*/
|
||||
public getDuplicateLocations(
|
||||
value: string | number | boolean,
|
||||
type: string,
|
||||
): ValueLocation[] | null {
|
||||
const key = this.createKey(value, type)
|
||||
const locations = this.valueMap.get(key)
|
||||
|
||||
if (!locations || locations.length < 2) {
|
||||
return null
|
||||
}
|
||||
|
||||
return locations
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is duplicated
|
||||
*/
|
||||
public isDuplicate(value: string | number | boolean, type: string): boolean {
|
||||
const key = this.createKey(value, type)
|
||||
const locations = this.valueMap.get(key)
|
||||
return locations ? locations.length >= 2 : false
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unique key for a value
|
||||
*/
|
||||
private createKey(value: string | number | boolean, type: string): string {
|
||||
return `${type}:${String(value)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a key back to value and type
|
||||
*/
|
||||
private parseKey(key: string): { value: string; type: string } {
|
||||
const [type, ...valueParts] = key.split(":")
|
||||
return { value: valueParts.join(":"), type }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets statistics about duplicates
|
||||
*/
|
||||
public getStats(): {
|
||||
totalValues: number
|
||||
duplicateValues: number
|
||||
duplicatePercentage: number
|
||||
} {
|
||||
const totalValues = this.valueMap.size
|
||||
const duplicateValues = this.getDuplicates().length
|
||||
const duplicatePercentage = totalValues > 0 ? (duplicateValues / totalValues) * 100 : 0
|
||||
|
||||
return {
|
||||
totalValues,
|
||||
duplicateValues,
|
||||
duplicatePercentage,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all tracked values
|
||||
*/
|
||||
public clear(): void {
|
||||
this.valueMap.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,28 @@
|
||||
import Parser from "tree-sitter"
|
||||
import { IHardcodeDetector } from "../../domain/services/IHardcodeDetector"
|
||||
import { HardcodedValue } from "../../domain/value-objects/HardcodedValue"
|
||||
import { BraceTracker } from "../strategies/BraceTracker"
|
||||
import { CodeParser } from "../parsers/CodeParser"
|
||||
import { AstBooleanAnalyzer } from "../strategies/AstBooleanAnalyzer"
|
||||
import { AstConfigObjectAnalyzer } from "../strategies/AstConfigObjectAnalyzer"
|
||||
import { AstContextChecker } from "../strategies/AstContextChecker"
|
||||
import { AstNumberAnalyzer } from "../strategies/AstNumberAnalyzer"
|
||||
import { AstStringAnalyzer } from "../strategies/AstStringAnalyzer"
|
||||
import { ConstantsFileChecker } from "../strategies/ConstantsFileChecker"
|
||||
import { ExportConstantAnalyzer } from "../strategies/ExportConstantAnalyzer"
|
||||
import { MagicNumberMatcher } from "../strategies/MagicNumberMatcher"
|
||||
import { MagicStringMatcher } from "../strategies/MagicStringMatcher"
|
||||
import { AstTreeTraverser } from "./AstTreeTraverser"
|
||||
|
||||
/**
|
||||
* Detects hardcoded values (magic numbers and strings) in TypeScript/JavaScript code
|
||||
*
|
||||
* This detector identifies configuration values, URLs, timeouts, ports, and other
|
||||
* constants that should be extracted to configuration files. It uses pattern matching
|
||||
* and context analysis to reduce false positives.
|
||||
* This detector uses Abstract Syntax Tree (AST) analysis via tree-sitter to identify
|
||||
* configuration values, URLs, timeouts, ports, and other constants that should be
|
||||
* extracted to configuration files. AST-based detection provides more accurate context
|
||||
* understanding and reduces false positives compared to regex-based approaches.
|
||||
*
|
||||
* The detector uses a modular architecture with specialized components:
|
||||
* - AstContextChecker: Checks if nodes are in specific contexts (exports, types, etc.)
|
||||
* - AstNumberAnalyzer: Analyzes number literals to detect magic numbers
|
||||
* - AstStringAnalyzer: Analyzes string literals to detect magic strings
|
||||
* - AstTreeTraverser: Traverses the AST and coordinates analyzers
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -26,17 +37,25 @@ import { MagicStringMatcher } from "../strategies/MagicStringMatcher"
|
||||
*/
|
||||
export class HardcodeDetector implements IHardcodeDetector {
|
||||
private readonly constantsChecker: ConstantsFileChecker
|
||||
private readonly braceTracker: BraceTracker
|
||||
private readonly exportAnalyzer: ExportConstantAnalyzer
|
||||
private readonly numberMatcher: MagicNumberMatcher
|
||||
private readonly stringMatcher: MagicStringMatcher
|
||||
private readonly parser: CodeParser
|
||||
private readonly traverser: AstTreeTraverser
|
||||
|
||||
constructor() {
|
||||
this.constantsChecker = new ConstantsFileChecker()
|
||||
this.braceTracker = new BraceTracker()
|
||||
this.exportAnalyzer = new ExportConstantAnalyzer(this.braceTracker)
|
||||
this.numberMatcher = new MagicNumberMatcher(this.exportAnalyzer)
|
||||
this.stringMatcher = new MagicStringMatcher(this.exportAnalyzer)
|
||||
this.parser = new CodeParser()
|
||||
|
||||
const contextChecker = new AstContextChecker()
|
||||
const numberAnalyzer = new AstNumberAnalyzer(contextChecker)
|
||||
const stringAnalyzer = new AstStringAnalyzer(contextChecker)
|
||||
const booleanAnalyzer = new AstBooleanAnalyzer(contextChecker)
|
||||
const configObjectAnalyzer = new AstConfigObjectAnalyzer(contextChecker)
|
||||
|
||||
this.traverser = new AstTreeTraverser(
|
||||
numberAnalyzer,
|
||||
stringAnalyzer,
|
||||
booleanAnalyzer,
|
||||
configObjectAnalyzer,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,10 +70,8 @@ export class HardcodeDetector implements IHardcodeDetector {
|
||||
return []
|
||||
}
|
||||
|
||||
const magicNumbers = this.numberMatcher.detect(code)
|
||||
const magicStrings = this.stringMatcher.detect(code)
|
||||
|
||||
return [...magicNumbers, ...magicStrings]
|
||||
const tree = this.parseCode(code, filePath)
|
||||
return this.traverser.traverse(tree, code)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +86,9 @@ export class HardcodeDetector implements IHardcodeDetector {
|
||||
return []
|
||||
}
|
||||
|
||||
return this.numberMatcher.detect(code)
|
||||
const tree = this.parseCode(code, filePath)
|
||||
const allViolations = this.traverser.traverse(tree, code)
|
||||
return allViolations.filter((v) => v.isMagicNumber())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,6 +103,20 @@ export class HardcodeDetector implements IHardcodeDetector {
|
||||
return []
|
||||
}
|
||||
|
||||
return this.stringMatcher.detect(code)
|
||||
const tree = this.parseCode(code, filePath)
|
||||
const allViolations = this.traverser.traverse(tree, code)
|
||||
return allViolations.filter((v) => v.isMagicString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses code based on file extension
|
||||
*/
|
||||
private parseCode(code: string, filePath: string): Parser.Tree {
|
||||
if (filePath.endsWith(".tsx")) {
|
||||
return this.parser.parseTsx(code)
|
||||
} else if (filePath.endsWith(".ts")) {
|
||||
return this.parser.parseTypeScript(code)
|
||||
}
|
||||
return this.parser.parseJavaScript(code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { createEngine } from "@secretlint/node"
|
||||
import type { SecretLintConfigDescriptor } from "@secretlint/types"
|
||||
import { ISecretDetector } from "../../domain/services/ISecretDetector"
|
||||
import { SecretViolation } from "../../domain/value-objects/SecretViolation"
|
||||
import { SECRET_KEYWORDS, SECRET_TYPE_NAMES } from "../../domain/constants/SecretExamples"
|
||||
import { EXTERNAL_PACKAGES } from "../../shared/constants/rules"
|
||||
|
||||
/**
|
||||
* Detects hardcoded secrets in TypeScript/JavaScript code
|
||||
@@ -24,7 +26,7 @@ export class SecretDetector implements ISecretDetector {
|
||||
private readonly secretlintConfig: SecretLintConfigDescriptor = {
|
||||
rules: [
|
||||
{
|
||||
id: "@secretlint/secretlint-rule-preset-recommend",
|
||||
id: EXTERNAL_PACKAGES.SECRETLINT_PRESET,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -88,80 +90,80 @@ export class SecretDetector implements ISecretDetector {
|
||||
}
|
||||
|
||||
private extractSecretType(message: string, ruleId: string): string {
|
||||
if (ruleId.includes("aws")) {
|
||||
if (message.toLowerCase().includes("access key")) {
|
||||
return "AWS Access Key"
|
||||
if (ruleId.includes(SECRET_KEYWORDS.AWS)) {
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.ACCESS_KEY)) {
|
||||
return SECRET_TYPE_NAMES.AWS_ACCESS_KEY
|
||||
}
|
||||
if (message.toLowerCase().includes("secret")) {
|
||||
return "AWS Secret Key"
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.SECRET)) {
|
||||
return SECRET_TYPE_NAMES.AWS_SECRET_KEY
|
||||
}
|
||||
return "AWS Credential"
|
||||
return SECRET_TYPE_NAMES.AWS_CREDENTIAL
|
||||
}
|
||||
|
||||
if (ruleId.includes("github")) {
|
||||
if (message.toLowerCase().includes("personal access token")) {
|
||||
return "GitHub Personal Access Token"
|
||||
if (ruleId.includes(SECRET_KEYWORDS.GITHUB)) {
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.PERSONAL_ACCESS_TOKEN)) {
|
||||
return SECRET_TYPE_NAMES.GITHUB_PERSONAL_ACCESS_TOKEN
|
||||
}
|
||||
if (message.toLowerCase().includes("oauth")) {
|
||||
return "GitHub OAuth Token"
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.OAUTH)) {
|
||||
return SECRET_TYPE_NAMES.GITHUB_OAUTH_TOKEN
|
||||
}
|
||||
return "GitHub Token"
|
||||
return SECRET_TYPE_NAMES.GITHUB_TOKEN
|
||||
}
|
||||
|
||||
if (ruleId.includes("npm")) {
|
||||
return "NPM Token"
|
||||
if (ruleId.includes(SECRET_KEYWORDS.NPM)) {
|
||||
return SECRET_TYPE_NAMES.NPM_TOKEN
|
||||
}
|
||||
|
||||
if (ruleId.includes("gcp") || ruleId.includes("google")) {
|
||||
return "GCP Service Account Key"
|
||||
if (ruleId.includes(SECRET_KEYWORDS.GCP) || ruleId.includes(SECRET_KEYWORDS.GOOGLE)) {
|
||||
return SECRET_TYPE_NAMES.GCP_SERVICE_ACCOUNT_KEY
|
||||
}
|
||||
|
||||
if (ruleId.includes("privatekey") || ruleId.includes("ssh")) {
|
||||
if (message.toLowerCase().includes("rsa")) {
|
||||
return "SSH RSA Private Key"
|
||||
if (ruleId.includes(SECRET_KEYWORDS.PRIVATEKEY) || ruleId.includes(SECRET_KEYWORDS.SSH)) {
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.RSA)) {
|
||||
return SECRET_TYPE_NAMES.SSH_RSA_PRIVATE_KEY
|
||||
}
|
||||
if (message.toLowerCase().includes("dsa")) {
|
||||
return "SSH DSA Private Key"
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.DSA)) {
|
||||
return SECRET_TYPE_NAMES.SSH_DSA_PRIVATE_KEY
|
||||
}
|
||||
if (message.toLowerCase().includes("ecdsa")) {
|
||||
return "SSH ECDSA Private Key"
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.ECDSA)) {
|
||||
return SECRET_TYPE_NAMES.SSH_ECDSA_PRIVATE_KEY
|
||||
}
|
||||
if (message.toLowerCase().includes("ed25519")) {
|
||||
return "SSH Ed25519 Private Key"
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.ED25519)) {
|
||||
return SECRET_TYPE_NAMES.SSH_ED25519_PRIVATE_KEY
|
||||
}
|
||||
return "SSH Private Key"
|
||||
return SECRET_TYPE_NAMES.SSH_PRIVATE_KEY
|
||||
}
|
||||
|
||||
if (ruleId.includes("slack")) {
|
||||
if (message.toLowerCase().includes("bot")) {
|
||||
return "Slack Bot Token"
|
||||
if (ruleId.includes(SECRET_KEYWORDS.SLACK)) {
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.BOT)) {
|
||||
return SECRET_TYPE_NAMES.SLACK_BOT_TOKEN
|
||||
}
|
||||
if (message.toLowerCase().includes("user")) {
|
||||
return "Slack User Token"
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.USER)) {
|
||||
return SECRET_TYPE_NAMES.SLACK_USER_TOKEN
|
||||
}
|
||||
return "Slack Token"
|
||||
return SECRET_TYPE_NAMES.SLACK_TOKEN
|
||||
}
|
||||
|
||||
if (ruleId.includes("basicauth")) {
|
||||
return "Basic Authentication Credentials"
|
||||
if (ruleId.includes(SECRET_KEYWORDS.BASICAUTH)) {
|
||||
return SECRET_TYPE_NAMES.BASIC_AUTH_CREDENTIALS
|
||||
}
|
||||
|
||||
if (message.toLowerCase().includes("api key")) {
|
||||
return "API Key"
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.API_KEY)) {
|
||||
return SECRET_TYPE_NAMES.API_KEY
|
||||
}
|
||||
|
||||
if (message.toLowerCase().includes("token")) {
|
||||
return "Authentication Token"
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.TOKEN)) {
|
||||
return SECRET_TYPE_NAMES.AUTHENTICATION_TOKEN
|
||||
}
|
||||
|
||||
if (message.toLowerCase().includes("password")) {
|
||||
return "Password"
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.PASSWORD)) {
|
||||
return SECRET_TYPE_NAMES.PASSWORD
|
||||
}
|
||||
|
||||
if (message.toLowerCase().includes("secret")) {
|
||||
return "Secret"
|
||||
if (message.toLowerCase().includes(SECRET_KEYWORDS.SECRET)) {
|
||||
return SECRET_TYPE_NAMES.SECRET
|
||||
}
|
||||
|
||||
return "Sensitive Data"
|
||||
return SECRET_TYPE_NAMES.SENSITIVE_DATA
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import Parser from "tree-sitter"
|
||||
import { HardcodedValue, HardcodeType } from "../../domain/value-objects/HardcodedValue"
|
||||
import { DETECTION_VALUES } from "../../shared/constants/rules"
|
||||
import { AstContextChecker } from "./AstContextChecker"
|
||||
|
||||
/**
|
||||
* AST-based analyzer for detecting magic booleans
|
||||
*
|
||||
* Detects boolean literals used as arguments without clear meaning.
|
||||
* Example: doSomething(true, false, true) - hard to understand
|
||||
* Better: doSomething({ sync: true, validate: false, cache: true })
|
||||
*/
|
||||
export class AstBooleanAnalyzer {
|
||||
constructor(private readonly contextChecker: AstContextChecker) {}
|
||||
|
||||
/**
|
||||
* Analyzes a boolean node and returns a violation if it's a magic boolean
|
||||
*/
|
||||
public analyze(node: Parser.SyntaxNode, lines: string[]): HardcodedValue | null {
|
||||
if (!this.shouldDetect(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const value = node.text === DETECTION_VALUES.BOOLEAN_TRUE
|
||||
|
||||
return this.createViolation(node, value, lines)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if boolean should be detected
|
||||
*/
|
||||
private shouldDetect(node: Parser.SyntaxNode): boolean {
|
||||
if (this.contextChecker.isInExportedConstant(node)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInTypeContext(node)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInTestDescription(node)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const parent = node.parent
|
||||
if (!parent) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (parent.type === "arguments") {
|
||||
return this.isInFunctionCallWithMultipleBooleans(parent)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if function call has multiple boolean arguments
|
||||
*/
|
||||
private isInFunctionCallWithMultipleBooleans(argsNode: Parser.SyntaxNode): boolean {
|
||||
let booleanCount = 0
|
||||
|
||||
for (const child of argsNode.children) {
|
||||
if (child.type === "true" || child.type === "false") {
|
||||
booleanCount++
|
||||
}
|
||||
}
|
||||
|
||||
return booleanCount >= 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a HardcodedValue violation from a boolean node
|
||||
*/
|
||||
private createViolation(
|
||||
node: Parser.SyntaxNode,
|
||||
value: boolean,
|
||||
lines: string[],
|
||||
): HardcodedValue {
|
||||
const lineNumber = node.startPosition.row + 1
|
||||
const column = node.startPosition.column
|
||||
const context = lines[node.startPosition.row]?.trim() ?? ""
|
||||
|
||||
return HardcodedValue.create(
|
||||
value,
|
||||
"MAGIC_BOOLEAN" as HardcodeType,
|
||||
lineNumber,
|
||||
column,
|
||||
context,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import Parser from "tree-sitter"
|
||||
import { HardcodedValue, HardcodeType } from "../../domain/value-objects/HardcodedValue"
|
||||
import { HARDCODE_TYPES } from "../../shared/constants/rules"
|
||||
import { ALLOWED_NUMBERS } from "../constants/defaults"
|
||||
import { AstContextChecker } from "./AstContextChecker"
|
||||
|
||||
/**
|
||||
* AST-based analyzer for detecting configuration objects with hardcoded values
|
||||
*
|
||||
* Detects objects that contain multiple hardcoded values that should be
|
||||
* extracted to a configuration file.
|
||||
*
|
||||
* Example:
|
||||
* const config = { timeout: 5000, retries: 3, url: "http://..." }
|
||||
*/
|
||||
export class AstConfigObjectAnalyzer {
|
||||
private readonly MIN_HARDCODED_VALUES = 2
|
||||
|
||||
constructor(private readonly contextChecker: AstContextChecker) {}
|
||||
|
||||
/**
|
||||
* Analyzes an object expression and returns a violation if it contains many hardcoded values
|
||||
*/
|
||||
public analyze(node: Parser.SyntaxNode, lines: string[]): HardcodedValue | null {
|
||||
if (node.type !== "object") {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInExportedConstant(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInTypeContext(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const hardcodedCount = this.countHardcodedValues(node)
|
||||
|
||||
if (hardcodedCount < this.MIN_HARDCODED_VALUES) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.createViolation(node, hardcodedCount, lines)
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts hardcoded values in an object
|
||||
*/
|
||||
private countHardcodedValues(objectNode: Parser.SyntaxNode): number {
|
||||
let count = 0
|
||||
|
||||
for (const child of objectNode.children) {
|
||||
if (child.type === "pair") {
|
||||
const value = child.childForFieldName("value")
|
||||
if (value && this.isHardcodedValue(value)) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a node is a hardcoded value
|
||||
*/
|
||||
private isHardcodedValue(node: Parser.SyntaxNode): boolean {
|
||||
if (node.type === "number") {
|
||||
const value = parseInt(node.text, 10)
|
||||
return !ALLOWED_NUMBERS.has(value) && value >= 100
|
||||
}
|
||||
|
||||
if (node.type === "string") {
|
||||
const stringFragment = node.children.find((c) => c.type === "string_fragment")
|
||||
return stringFragment !== undefined && stringFragment.text.length > 3
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a HardcodedValue violation for a config object
|
||||
*/
|
||||
private createViolation(
|
||||
node: Parser.SyntaxNode,
|
||||
hardcodedCount: number,
|
||||
lines: string[],
|
||||
): HardcodedValue {
|
||||
const lineNumber = node.startPosition.row + 1
|
||||
const column = node.startPosition.column
|
||||
const context = lines[node.startPosition.row]?.trim() ?? ""
|
||||
|
||||
const objectPreview = this.getObjectPreview(node)
|
||||
|
||||
return HardcodedValue.create(
|
||||
`Configuration object with ${String(hardcodedCount)} hardcoded values: ${objectPreview}`,
|
||||
HARDCODE_TYPES.MAGIC_CONFIG as HardcodeType,
|
||||
lineNumber,
|
||||
column,
|
||||
context,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a preview of the object for the violation message
|
||||
*/
|
||||
private getObjectPreview(node: Parser.SyntaxNode): string {
|
||||
const text = node.text
|
||||
if (text.length <= 50) {
|
||||
return text
|
||||
}
|
||||
return `${text.substring(0, 47)}...`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import Parser from "tree-sitter"
|
||||
|
||||
/**
|
||||
* AST context checker for analyzing node contexts
|
||||
*
|
||||
* Provides reusable methods to check if a node is in specific contexts
|
||||
* like exports, type declarations, function calls, etc.
|
||||
*/
|
||||
export class AstContextChecker {
|
||||
/**
|
||||
* Checks if node is in an exported constant with "as const"
|
||||
*/
|
||||
public isInExportedConstant(node: Parser.SyntaxNode): boolean {
|
||||
let current = node.parent
|
||||
|
||||
while (current) {
|
||||
if (current.type === "export_statement") {
|
||||
if (this.checkExportedConstant(current)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check if export statement contains "as const"
|
||||
*/
|
||||
private checkExportedConstant(exportNode: Parser.SyntaxNode): boolean {
|
||||
const declaration = exportNode.childForFieldName("declaration")
|
||||
if (!declaration) {
|
||||
return false
|
||||
}
|
||||
|
||||
const declarator = this.findDescendant(declaration, "variable_declarator")
|
||||
if (!declarator) {
|
||||
return false
|
||||
}
|
||||
|
||||
const value = declarator.childForFieldName("value")
|
||||
if (value?.type !== "as_expression") {
|
||||
return false
|
||||
}
|
||||
|
||||
const asType = value.children.find((c) => c.type === "const")
|
||||
return asType !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if node is in a type context (union type, type alias, interface)
|
||||
*/
|
||||
public isInTypeContext(node: Parser.SyntaxNode): boolean {
|
||||
let current = node.parent
|
||||
|
||||
while (current) {
|
||||
if (
|
||||
current.type === "type_alias_declaration" ||
|
||||
current.type === "union_type" ||
|
||||
current.type === "literal_type" ||
|
||||
current.type === "interface_declaration" ||
|
||||
current.type === "type_annotation"
|
||||
) {
|
||||
return true
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if node is in an import statement or import() call
|
||||
*/
|
||||
public isInImportStatement(node: Parser.SyntaxNode): boolean {
|
||||
let current = node.parent
|
||||
|
||||
while (current) {
|
||||
if (current.type === "import_statement") {
|
||||
return true
|
||||
}
|
||||
|
||||
if (current.type === "call_expression") {
|
||||
const functionNode =
|
||||
current.childForFieldName("function") ||
|
||||
current.children.find((c) => c.type === "identifier" || c.type === "import")
|
||||
|
||||
if (
|
||||
functionNode &&
|
||||
(functionNode.text === "import" || functionNode.type === "import")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if node is in a test description (test(), describe(), it())
|
||||
*/
|
||||
public isInTestDescription(node: Parser.SyntaxNode): boolean {
|
||||
let current = node.parent
|
||||
|
||||
while (current) {
|
||||
if (current.type === "call_expression") {
|
||||
const callee = current.childForFieldName("function")
|
||||
if (callee?.type === "identifier") {
|
||||
const funcName = callee.text
|
||||
if (
|
||||
funcName === "test" ||
|
||||
funcName === "describe" ||
|
||||
funcName === "it" ||
|
||||
funcName === "expect"
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if node is in a console.log or console.error call
|
||||
*/
|
||||
public isInConsoleCall(node: Parser.SyntaxNode): boolean {
|
||||
let current = node.parent
|
||||
|
||||
while (current) {
|
||||
if (current.type === "call_expression") {
|
||||
const callee = current.childForFieldName("function")
|
||||
if (callee?.type === "member_expression") {
|
||||
const object = callee.childForFieldName("object")
|
||||
const property = callee.childForFieldName("property")
|
||||
|
||||
if (
|
||||
object?.text === "console" &&
|
||||
property &&
|
||||
(property.text === "log" ||
|
||||
property.text === "error" ||
|
||||
property.text === "warn")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if node is in a Symbol() call
|
||||
*/
|
||||
public isInSymbolCall(node: Parser.SyntaxNode): boolean {
|
||||
let current = node.parent
|
||||
|
||||
while (current) {
|
||||
if (current.type === "call_expression") {
|
||||
const callee = current.childForFieldName("function")
|
||||
if (callee?.type === "identifier" && callee.text === "Symbol") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if node is in a typeof check
|
||||
*/
|
||||
public isInTypeofCheck(node: Parser.SyntaxNode): boolean {
|
||||
let current = node.parent
|
||||
|
||||
while (current) {
|
||||
if (current.type === "binary_expression") {
|
||||
const left = current.childForFieldName("left")
|
||||
const right = current.childForFieldName("right")
|
||||
|
||||
if (left?.type === "unary_expression") {
|
||||
const operator = left.childForFieldName("operator")
|
||||
if (operator?.text === "typeof") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (right?.type === "unary_expression") {
|
||||
const operator = right.childForFieldName("operator")
|
||||
if (operator?.text === "typeof") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if parent is a call expression with specific function names
|
||||
*/
|
||||
public isInCallExpression(parent: Parser.SyntaxNode, functionNames: string[]): boolean {
|
||||
if (parent.type === "arguments") {
|
||||
const callExpr = parent.parent
|
||||
if (callExpr?.type === "call_expression") {
|
||||
const callee = callExpr.childForFieldName("function")
|
||||
if (callee?.type === "identifier") {
|
||||
return functionNames.includes(callee.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets context text around a node
|
||||
*/
|
||||
public getNodeContext(node: Parser.SyntaxNode): string {
|
||||
let current: Parser.SyntaxNode | null = node
|
||||
|
||||
while (current && current.type !== "lexical_declaration" && current.type !== "pair") {
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
return current ? current.text.toLowerCase() : ""
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a descendant node by type
|
||||
*/
|
||||
private findDescendant(node: Parser.SyntaxNode, type: string): Parser.SyntaxNode | null {
|
||||
if (node.type === type) {
|
||||
return node
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
const result = this.findDescendant(child, type)
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import Parser from "tree-sitter"
|
||||
import { HardcodedValue, HardcodeType } from "../../domain/value-objects/HardcodedValue"
|
||||
import { HARDCODE_TYPES } from "../../shared/constants/rules"
|
||||
import { ALLOWED_NUMBERS, DETECTION_KEYWORDS } from "../constants/defaults"
|
||||
import { AstContextChecker } from "./AstContextChecker"
|
||||
|
||||
/**
|
||||
* AST-based analyzer for detecting magic numbers
|
||||
*
|
||||
* Analyzes number literal nodes in the AST to determine if they are
|
||||
* hardcoded values that should be extracted to constants.
|
||||
*/
|
||||
export class AstNumberAnalyzer {
|
||||
constructor(private readonly contextChecker: AstContextChecker) {}
|
||||
|
||||
/**
|
||||
* Analyzes a number node and returns a violation if it's a magic number
|
||||
*/
|
||||
public analyze(node: Parser.SyntaxNode, lines: string[]): HardcodedValue | null {
|
||||
const value = parseInt(node.text, 10)
|
||||
|
||||
if (ALLOWED_NUMBERS.has(value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInExportedConstant(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!this.shouldDetect(node, value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return this.createViolation(node, value, lines)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if number should be detected based on context
|
||||
*/
|
||||
private shouldDetect(node: Parser.SyntaxNode, value: number): boolean {
|
||||
const parent = node.parent
|
||||
if (!parent) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInCallExpression(parent, ["setTimeout", "setInterval"])) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (parent.type === "variable_declarator") {
|
||||
const identifier = parent.childForFieldName("name")
|
||||
if (identifier && this.hasConfigKeyword(identifier.text.toLowerCase())) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (parent.type === "pair") {
|
||||
const key = parent.childForFieldName("key")
|
||||
if (key && this.hasConfigKeyword(key.text.toLowerCase())) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (value >= 100) {
|
||||
const context = this.contextChecker.getNodeContext(node)
|
||||
return this.looksLikeMagicNumber(context)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if name contains configuration keywords
|
||||
*/
|
||||
private hasConfigKeyword(name: string): boolean {
|
||||
const keywords = [
|
||||
DETECTION_KEYWORDS.TIMEOUT,
|
||||
DETECTION_KEYWORDS.DELAY,
|
||||
DETECTION_KEYWORDS.RETRY,
|
||||
DETECTION_KEYWORDS.LIMIT,
|
||||
DETECTION_KEYWORDS.MAX,
|
||||
DETECTION_KEYWORDS.MIN,
|
||||
DETECTION_KEYWORDS.PORT,
|
||||
DETECTION_KEYWORDS.INTERVAL,
|
||||
]
|
||||
|
||||
return (
|
||||
keywords.some((keyword) => name.includes(keyword)) ||
|
||||
name.includes("retries") ||
|
||||
name.includes("attempts")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if context suggests a magic number
|
||||
*/
|
||||
private looksLikeMagicNumber(context: string): boolean {
|
||||
const configKeywords = [
|
||||
DETECTION_KEYWORDS.TIMEOUT,
|
||||
DETECTION_KEYWORDS.DELAY,
|
||||
DETECTION_KEYWORDS.RETRY,
|
||||
DETECTION_KEYWORDS.LIMIT,
|
||||
DETECTION_KEYWORDS.MAX,
|
||||
DETECTION_KEYWORDS.MIN,
|
||||
DETECTION_KEYWORDS.PORT,
|
||||
DETECTION_KEYWORDS.INTERVAL,
|
||||
]
|
||||
|
||||
return configKeywords.some((keyword) => context.includes(keyword))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a HardcodedValue violation from a number node
|
||||
*/
|
||||
private createViolation(
|
||||
node: Parser.SyntaxNode,
|
||||
value: number,
|
||||
lines: string[],
|
||||
): HardcodedValue {
|
||||
const lineNumber = node.startPosition.row + 1
|
||||
const column = node.startPosition.column
|
||||
const context = lines[node.startPosition.row]?.trim() ?? ""
|
||||
|
||||
return HardcodedValue.create(
|
||||
value,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER as HardcodeType,
|
||||
lineNumber,
|
||||
column,
|
||||
context,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import Parser from "tree-sitter"
|
||||
import { HardcodedValue, HardcodeType } from "../../domain/value-objects/HardcodedValue"
|
||||
import { CONFIG_KEYWORDS, DETECTION_VALUES, HARDCODE_TYPES } from "../../shared/constants/rules"
|
||||
import { AstContextChecker } from "./AstContextChecker"
|
||||
import { ValuePatternMatcher } from "./ValuePatternMatcher"
|
||||
|
||||
/**
|
||||
* AST-based analyzer for detecting magic strings
|
||||
*
|
||||
* Analyzes string literal nodes in the AST to determine if they are
|
||||
* hardcoded values that should be extracted to constants.
|
||||
*
|
||||
* Detects various types of hardcoded strings:
|
||||
* - URLs and connection strings
|
||||
* - Email addresses
|
||||
* - IP addresses
|
||||
* - File paths
|
||||
* - Dates
|
||||
* - API keys
|
||||
*/
|
||||
export class AstStringAnalyzer {
|
||||
private readonly patternMatcher: ValuePatternMatcher
|
||||
|
||||
constructor(private readonly contextChecker: AstContextChecker) {
|
||||
this.patternMatcher = new ValuePatternMatcher()
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyzes a string node and returns a violation if it's a magic string
|
||||
*/
|
||||
public analyze(node: Parser.SyntaxNode, lines: string[]): HardcodedValue | null {
|
||||
const stringFragment = node.children.find((child) => child.type === "string_fragment")
|
||||
if (!stringFragment) {
|
||||
return null
|
||||
}
|
||||
|
||||
const value = stringFragment.text
|
||||
|
||||
if (value.length <= 3) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInExportedConstant(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInTypeContext(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInImportStatement(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInTestDescription(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInConsoleCall(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInSymbolCall(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.contextChecker.isInTypeofCheck(node)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (this.shouldDetect(node, value)) {
|
||||
return this.createViolation(node, value, lines)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if string value should be detected
|
||||
*/
|
||||
private shouldDetect(node: Parser.SyntaxNode, value: string): boolean {
|
||||
if (this.patternMatcher.shouldDetect(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.hasConfigurationContext(node)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if string is in a configuration-related context
|
||||
*/
|
||||
private hasConfigurationContext(node: Parser.SyntaxNode): boolean {
|
||||
const context = this.contextChecker.getNodeContext(node).toLowerCase()
|
||||
|
||||
const configKeywords = [
|
||||
"url",
|
||||
"uri",
|
||||
...CONFIG_KEYWORDS.NETWORK,
|
||||
"api",
|
||||
...CONFIG_KEYWORDS.DATABASE,
|
||||
"db",
|
||||
"env",
|
||||
...CONFIG_KEYWORDS.SECURITY,
|
||||
"key",
|
||||
...CONFIG_KEYWORDS.MESSAGES,
|
||||
"label",
|
||||
]
|
||||
|
||||
return configKeywords.some((keyword) => context.includes(keyword))
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a HardcodedValue violation from a string node
|
||||
*/
|
||||
private createViolation(
|
||||
node: Parser.SyntaxNode,
|
||||
value: string,
|
||||
lines: string[],
|
||||
): HardcodedValue {
|
||||
const lineNumber = node.startPosition.row + 1
|
||||
const column = node.startPosition.column
|
||||
const context = lines[node.startPosition.row]?.trim() ?? ""
|
||||
|
||||
const detectedType = this.patternMatcher.detectType(value)
|
||||
const valueType =
|
||||
detectedType ||
|
||||
(this.hasConfigurationContext(node)
|
||||
? DETECTION_VALUES.TYPE_CONFIG
|
||||
: DETECTION_VALUES.TYPE_GENERIC)
|
||||
|
||||
return HardcodedValue.create(
|
||||
value,
|
||||
HARDCODE_TYPES.MAGIC_STRING as HardcodeType,
|
||||
lineNumber,
|
||||
column,
|
||||
context,
|
||||
valueType,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* Tracks braces and brackets in code for context analysis
|
||||
*
|
||||
* Used to determine if a line is inside an exported constant
|
||||
* by counting unclosed braces and brackets.
|
||||
*/
|
||||
export class BraceTracker {
|
||||
/**
|
||||
* Counts unclosed braces and brackets between two line indices
|
||||
*/
|
||||
public countUnclosed(
|
||||
lines: string[],
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
): { braces: number; brackets: number } {
|
||||
let braces = 0
|
||||
let brackets = 0
|
||||
|
||||
for (let i = startLine; i <= endLine; i++) {
|
||||
const counts = this.countInLine(lines[i])
|
||||
braces += counts.braces
|
||||
brackets += counts.brackets
|
||||
}
|
||||
|
||||
return { braces, brackets }
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts braces and brackets in a single line
|
||||
*/
|
||||
private countInLine(line: string): { braces: number; brackets: number } {
|
||||
let braces = 0
|
||||
let brackets = 0
|
||||
let inString = false
|
||||
let stringChar = ""
|
||||
|
||||
for (let j = 0; j < line.length; j++) {
|
||||
const char = line[j]
|
||||
const prevChar = j > 0 ? line[j - 1] : ""
|
||||
|
||||
this.updateStringState(
|
||||
char,
|
||||
prevChar,
|
||||
inString,
|
||||
stringChar,
|
||||
(newInString, newStringChar) => {
|
||||
inString = newInString
|
||||
stringChar = newStringChar
|
||||
},
|
||||
)
|
||||
|
||||
if (!inString) {
|
||||
const counts = this.countChar(char)
|
||||
braces += counts.braces
|
||||
brackets += counts.brackets
|
||||
}
|
||||
}
|
||||
|
||||
return { braces, brackets }
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates string tracking state
|
||||
*/
|
||||
private updateStringState(
|
||||
char: string,
|
||||
prevChar: string,
|
||||
inString: boolean,
|
||||
stringChar: string,
|
||||
callback: (inString: boolean, stringChar: string) => void,
|
||||
): void {
|
||||
if ((char === "'" || char === '"' || char === "`") && prevChar !== "\\") {
|
||||
if (!inString) {
|
||||
callback(true, char)
|
||||
} else if (char === stringChar) {
|
||||
callback(false, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts a single character
|
||||
*/
|
||||
private countChar(char: string): { braces: number; brackets: number } {
|
||||
if (char === "{") {
|
||||
return { braces: 1, brackets: 0 }
|
||||
} else if (char === "}") {
|
||||
return { braces: -1, brackets: 0 }
|
||||
} else if (char === "[") {
|
||||
return { braces: 0, brackets: 1 }
|
||||
} else if (char === "]") {
|
||||
return { braces: 0, brackets: -1 }
|
||||
}
|
||||
return { braces: 0, brackets: 0 }
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import { CODE_PATTERNS } from "../constants/defaults"
|
||||
import { BraceTracker } from "./BraceTracker"
|
||||
|
||||
/**
|
||||
* Analyzes export const declarations in code
|
||||
*
|
||||
* Determines if a line is inside an exported constant declaration
|
||||
* to skip hardcode detection in constant definitions.
|
||||
*/
|
||||
export class ExportConstantAnalyzer {
|
||||
constructor(private readonly braceTracker: BraceTracker) {}
|
||||
|
||||
/**
|
||||
* Checks if a line is inside an exported constant definition
|
||||
*/
|
||||
public isInExportedConstant(lines: string[], lineIndex: number): boolean {
|
||||
const currentLineTrimmed = lines[lineIndex].trim()
|
||||
|
||||
if (this.isSingleLineExportConst(currentLineTrimmed)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const exportConstStart = this.findExportConstStart(lines, lineIndex)
|
||||
if (exportConstStart === -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { braces, brackets } = this.braceTracker.countUnclosed(
|
||||
lines,
|
||||
exportConstStart,
|
||||
lineIndex,
|
||||
)
|
||||
|
||||
return braces > 0 || brackets > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a line is a single-line export const declaration
|
||||
*/
|
||||
public isSingleLineExportConst(line: string): boolean {
|
||||
if (!line.startsWith(CODE_PATTERNS.EXPORT_CONST)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hasObjectOrArray = this.hasObjectOrArray(line)
|
||||
|
||||
if (hasObjectOrArray) {
|
||||
return this.hasAsConstEnding(line)
|
||||
}
|
||||
|
||||
return line.includes(CODE_PATTERNS.AS_CONST)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the starting line of an export const declaration
|
||||
*/
|
||||
public findExportConstStart(lines: string[], lineIndex: number): number {
|
||||
for (let currentLine = lineIndex; currentLine >= 0; currentLine--) {
|
||||
const trimmed = lines[currentLine].trim()
|
||||
|
||||
if (this.isExportConstWithStructure(trimmed)) {
|
||||
return currentLine
|
||||
}
|
||||
|
||||
if (this.isTopLevelStatement(trimmed, currentLine, lineIndex)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if line has object or array structure
|
||||
*/
|
||||
private hasObjectOrArray(line: string): boolean {
|
||||
return line.includes(CODE_PATTERNS.OBJECT_START) || line.includes(CODE_PATTERNS.ARRAY_START)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if line has 'as const' ending
|
||||
*/
|
||||
private hasAsConstEnding(line: string): boolean {
|
||||
return (
|
||||
line.includes(CODE_PATTERNS.AS_CONST_OBJECT) ||
|
||||
line.includes(CODE_PATTERNS.AS_CONST_ARRAY) ||
|
||||
line.includes(CODE_PATTERNS.AS_CONST_END_SEMICOLON_OBJECT) ||
|
||||
line.includes(CODE_PATTERNS.AS_CONST_END_SEMICOLON_ARRAY)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if line is export const with object or array
|
||||
*/
|
||||
private isExportConstWithStructure(trimmed: string): boolean {
|
||||
return (
|
||||
trimmed.startsWith(CODE_PATTERNS.EXPORT_CONST) &&
|
||||
(trimmed.includes(CODE_PATTERNS.OBJECT_START) ||
|
||||
trimmed.includes(CODE_PATTERNS.ARRAY_START))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if line is a top-level statement
|
||||
*/
|
||||
private isTopLevelStatement(trimmed: string, currentLine: number, lineIndex: number): boolean {
|
||||
return (
|
||||
currentLine < lineIndex &&
|
||||
(trimmed.startsWith(CODE_PATTERNS.EXPORT) || trimmed.startsWith(CODE_PATTERNS.IMPORT))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
import { HardcodedValue } from "../../domain/value-objects/HardcodedValue"
|
||||
import { ALLOWED_NUMBERS, DETECTION_KEYWORDS } from "../constants/defaults"
|
||||
import { HARDCODE_TYPES } from "../../shared/constants"
|
||||
import { ExportConstantAnalyzer } from "./ExportConstantAnalyzer"
|
||||
|
||||
/**
|
||||
* Detects magic numbers in code
|
||||
*
|
||||
* Identifies hardcoded numeric values that should be extracted
|
||||
* to constants, excluding allowed values and exported constants.
|
||||
*/
|
||||
export class MagicNumberMatcher {
|
||||
private readonly numberPatterns = [
|
||||
/(?:setTimeout|setInterval)\s*\(\s*[^,]+,\s*(\d+)/g,
|
||||
/(?:maxRetries|retries|attempts)\s*[=:]\s*(\d+)/gi,
|
||||
/(?:limit|max|min)\s*[=:]\s*(\d+)/gi,
|
||||
/(?:port|PORT)\s*[=:]\s*(\d+)/g,
|
||||
/(?:delay|timeout|TIMEOUT)\s*[=:]\s*(\d+)/gi,
|
||||
]
|
||||
|
||||
constructor(private readonly exportAnalyzer: ExportConstantAnalyzer) {}
|
||||
|
||||
/**
|
||||
* Detects magic numbers in code
|
||||
*/
|
||||
public detect(code: string): HardcodedValue[] {
|
||||
const results: HardcodedValue[] = []
|
||||
const lines = code.split("\n")
|
||||
|
||||
lines.forEach((line, lineIndex) => {
|
||||
if (this.shouldSkipLine(line, lines, lineIndex)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.detectInPatterns(line, lineIndex, results)
|
||||
this.detectGenericNumbers(line, lineIndex, results)
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if line should be skipped
|
||||
*/
|
||||
private shouldSkipLine(line: string, lines: string[], lineIndex: number): boolean {
|
||||
if (line.trim().startsWith("//") || line.trim().startsWith("*")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return this.exportAnalyzer.isInExportedConstant(lines, lineIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects numbers in specific patterns
|
||||
*/
|
||||
private detectInPatterns(line: string, lineIndex: number, results: HardcodedValue[]): void {
|
||||
this.numberPatterns.forEach((pattern) => {
|
||||
let match
|
||||
const regex = new RegExp(pattern)
|
||||
|
||||
while ((match = regex.exec(line)) !== null) {
|
||||
const value = parseInt(match[1], 10)
|
||||
|
||||
if (!ALLOWED_NUMBERS.has(value)) {
|
||||
results.push(
|
||||
HardcodedValue.create(
|
||||
value,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
lineIndex + 1,
|
||||
match.index,
|
||||
line.trim(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects generic 3+ digit numbers
|
||||
*/
|
||||
private detectGenericNumbers(line: string, lineIndex: number, results: HardcodedValue[]): void {
|
||||
const genericNumberRegex = /\b(\d{3,})\b/g
|
||||
let match
|
||||
|
||||
while ((match = genericNumberRegex.exec(line)) !== null) {
|
||||
const value = parseInt(match[1], 10)
|
||||
|
||||
if (this.shouldDetectNumber(value, line, match.index)) {
|
||||
results.push(
|
||||
HardcodedValue.create(
|
||||
value,
|
||||
HARDCODE_TYPES.MAGIC_NUMBER,
|
||||
lineIndex + 1,
|
||||
match.index,
|
||||
line.trim(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if number should be detected
|
||||
*/
|
||||
private shouldDetectNumber(value: number, line: string, index: number): boolean {
|
||||
if (ALLOWED_NUMBERS.has(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.isInComment(line, index)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.isInString(line, index)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const context = this.extractContext(line, index)
|
||||
return this.looksLikeMagicNumber(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if position is in a comment
|
||||
*/
|
||||
private isInComment(line: string, index: number): boolean {
|
||||
const beforeIndex = line.substring(0, index)
|
||||
return beforeIndex.includes("//") || beforeIndex.includes("/*")
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if position is in a string
|
||||
*/
|
||||
private isInString(line: string, index: number): boolean {
|
||||
const beforeIndex = line.substring(0, index)
|
||||
const singleQuotes = (beforeIndex.match(/'/g) ?? []).length
|
||||
const doubleQuotes = (beforeIndex.match(/"/g) ?? []).length
|
||||
const backticks = (beforeIndex.match(/`/g) ?? []).length
|
||||
|
||||
return singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0 || backticks % 2 !== 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts context around a position
|
||||
*/
|
||||
private extractContext(line: string, index: number): string {
|
||||
const start = Math.max(0, index - 30)
|
||||
const end = Math.min(line.length, index + 30)
|
||||
return line.substring(start, end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if context suggests a magic number
|
||||
*/
|
||||
private looksLikeMagicNumber(context: string): boolean {
|
||||
const lowerContext = context.toLowerCase()
|
||||
|
||||
const configKeywords = [
|
||||
DETECTION_KEYWORDS.TIMEOUT,
|
||||
DETECTION_KEYWORDS.DELAY,
|
||||
DETECTION_KEYWORDS.RETRY,
|
||||
DETECTION_KEYWORDS.LIMIT,
|
||||
DETECTION_KEYWORDS.MAX,
|
||||
DETECTION_KEYWORDS.MIN,
|
||||
DETECTION_KEYWORDS.PORT,
|
||||
DETECTION_KEYWORDS.INTERVAL,
|
||||
]
|
||||
|
||||
return configKeywords.some((keyword) => lowerContext.includes(keyword))
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
import { HardcodedValue } from "../../domain/value-objects/HardcodedValue"
|
||||
import { DETECTION_KEYWORDS } from "../constants/defaults"
|
||||
import { HARDCODE_TYPES } from "../../shared/constants"
|
||||
import { ExportConstantAnalyzer } from "./ExportConstantAnalyzer"
|
||||
|
||||
/**
|
||||
* Detects magic strings in code
|
||||
*
|
||||
* Identifies hardcoded string values that should be extracted
|
||||
* to constants, excluding test code, console logs, and type contexts.
|
||||
*/
|
||||
export class MagicStringMatcher {
|
||||
private readonly stringRegex = /(['"`])(?:(?!\1).)+\1/g
|
||||
|
||||
private readonly allowedPatterns = [/^[a-z]$/i, /^\/$/, /^\\$/, /^\s+$/, /^,$/, /^\.$/]
|
||||
|
||||
private readonly typeContextPatterns = [
|
||||
/^\s*type\s+\w+\s*=/i,
|
||||
/^\s*interface\s+\w+/i,
|
||||
/^\s*\w+\s*:\s*['"`]/,
|
||||
/\s+as\s+['"`]/,
|
||||
/Record<.*,\s*import\(/,
|
||||
/typeof\s+\w+\s*===\s*['"`]/,
|
||||
/['"`]\s*===\s*typeof\s+\w+/,
|
||||
]
|
||||
|
||||
constructor(private readonly exportAnalyzer: ExportConstantAnalyzer) {}
|
||||
|
||||
/**
|
||||
* Detects magic strings in code
|
||||
*/
|
||||
public detect(code: string): HardcodedValue[] {
|
||||
const results: HardcodedValue[] = []
|
||||
const lines = code.split("\n")
|
||||
|
||||
lines.forEach((line, lineIndex) => {
|
||||
if (this.shouldSkipLine(line, lines, lineIndex)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.detectStringsInLine(line, lineIndex, results)
|
||||
})
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if line should be skipped
|
||||
*/
|
||||
private shouldSkipLine(line: string, lines: string[], lineIndex: number): boolean {
|
||||
if (
|
||||
line.trim().startsWith("//") ||
|
||||
line.trim().startsWith("*") ||
|
||||
line.includes("import ") ||
|
||||
line.includes("from ")
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
return this.exportAnalyzer.isInExportedConstant(lines, lineIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects strings in a single line
|
||||
*/
|
||||
private detectStringsInLine(line: string, lineIndex: number, results: HardcodedValue[]): void {
|
||||
let match
|
||||
const regex = new RegExp(this.stringRegex)
|
||||
|
||||
while ((match = regex.exec(line)) !== null) {
|
||||
const fullMatch = match[0]
|
||||
const value = fullMatch.slice(1, -1)
|
||||
|
||||
if (this.shouldDetectString(fullMatch, value, line)) {
|
||||
results.push(
|
||||
HardcodedValue.create(
|
||||
value,
|
||||
HARDCODE_TYPES.MAGIC_STRING,
|
||||
lineIndex + 1,
|
||||
match.index,
|
||||
line.trim(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if string should be detected
|
||||
*/
|
||||
private shouldDetectString(fullMatch: string, value: string, line: string): boolean {
|
||||
if (fullMatch.startsWith("`") || value.includes("${")) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.isAllowedString(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.looksLikeMagicString(line, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if string is allowed (short strings, single chars, etc.)
|
||||
*/
|
||||
private isAllowedString(str: string): boolean {
|
||||
if (str.length <= 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
return this.allowedPatterns.some((pattern) => pattern.test(str))
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if line context suggests a magic string
|
||||
*/
|
||||
private looksLikeMagicString(line: string, value: string): boolean {
|
||||
const lowerLine = line.toLowerCase()
|
||||
|
||||
if (this.isTestCode(lowerLine)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.isConsoleLog(lowerLine)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.isInTypeContext(line)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.isInSymbolCall(line, value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.isInImportCall(line, value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (this.isUrlOrApi(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (/^\d{2,}$/.test(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return value.length > 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if line is test code
|
||||
*/
|
||||
private isTestCode(lowerLine: string): boolean {
|
||||
return (
|
||||
lowerLine.includes(DETECTION_KEYWORDS.TEST) ||
|
||||
lowerLine.includes(DETECTION_KEYWORDS.DESCRIBE)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if line is console log
|
||||
*/
|
||||
private isConsoleLog(lowerLine: string): boolean {
|
||||
return (
|
||||
lowerLine.includes(DETECTION_KEYWORDS.CONSOLE_LOG) ||
|
||||
lowerLine.includes(DETECTION_KEYWORDS.CONSOLE_ERROR)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if line is in type context
|
||||
*/
|
||||
private isInTypeContext(line: string): boolean {
|
||||
const trimmedLine = line.trim()
|
||||
|
||||
if (this.typeContextPatterns.some((pattern) => pattern.test(trimmedLine))) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (trimmedLine.includes("|") && /['"`][^'"`]+['"`]\s*\|/.test(trimmedLine)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if string is inside Symbol() call
|
||||
*/
|
||||
private isInSymbolCall(line: string, stringValue: string): boolean {
|
||||
const symbolPattern = new RegExp(
|
||||
`Symbol\\s*\\(\\s*['"\`]${stringValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"\`]\\s*\\)`,
|
||||
)
|
||||
return symbolPattern.test(line)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if string is inside import() call
|
||||
*/
|
||||
private isInImportCall(line: string, stringValue: string): boolean {
|
||||
const importPattern = /import\s*\(\s*['"`][^'"`]+['"`]\s*\)/
|
||||
return importPattern.test(line) && line.includes(stringValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if string contains URL or API reference
|
||||
*/
|
||||
private isUrlOrApi(value: string): boolean {
|
||||
return value.includes(DETECTION_KEYWORDS.HTTP) || value.includes(DETECTION_KEYWORDS.API)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Pattern matcher for detecting specific value types
|
||||
*
|
||||
* Provides pattern matching for emails, IPs, paths, dates, UUIDs, versions, and other common hardcoded values
|
||||
*/
|
||||
export class ValuePatternMatcher {
|
||||
private static readonly EMAIL_PATTERN = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||||
private static readonly IP_V4_PATTERN = /^(\d{1,3}\.){3}\d{1,3}$/
|
||||
private static readonly IP_V6_PATTERN =
|
||||
/^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4}$/
|
||||
private static readonly DATE_ISO_PATTERN = /^\d{4}-\d{2}-\d{2}$/
|
||||
private static readonly URL_PATTERN = /^https?:\/\/|^mongodb:\/\/|^postgresql:\/\//
|
||||
private static readonly UNIX_PATH_PATTERN = /^\/[a-zA-Z0-9/_-]+/
|
||||
private static readonly WINDOWS_PATH_PATTERN = /^[a-zA-Z]:\\[a-zA-Z0-9\\/_-]+/
|
||||
private static readonly API_KEY_PATTERN = /^(sk_|pk_|api_|key_)[a-zA-Z0-9_-]{20,}$/
|
||||
private static readonly UUID_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||||
private static readonly SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[\w.-]+)?(\+[\w.-]+)?$/
|
||||
private static readonly HEX_COLOR_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/
|
||||
private static readonly MAC_ADDRESS_PATTERN = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/
|
||||
private static readonly BASE64_PATTERN =
|
||||
/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
|
||||
private static readonly JWT_PATTERN = /^eyJ[A-Za-z0-9-_]+\.eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/
|
||||
|
||||
/**
|
||||
* Checks if value is an email address
|
||||
*/
|
||||
public isEmail(value: string): boolean {
|
||||
return ValuePatternMatcher.EMAIL_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if value is an IP address (v4 or v6)
|
||||
*/
|
||||
public isIpAddress(value: string): boolean {
|
||||
return (
|
||||
ValuePatternMatcher.IP_V4_PATTERN.test(value) ||
|
||||
ValuePatternMatcher.IP_V6_PATTERN.test(value)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if value is a date in ISO format
|
||||
*/
|
||||
public isDate(value: string): boolean {
|
||||
return ValuePatternMatcher.DATE_ISO_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if value is a URL
|
||||
*/
|
||||
public isUrl(value: string): boolean {
|
||||
return ValuePatternMatcher.URL_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if value is a file path (Unix or Windows)
|
||||
*/
|
||||
public isFilePath(value: string): boolean {
|
||||
return (
|
||||
ValuePatternMatcher.UNIX_PATH_PATTERN.test(value) ||
|
||||
ValuePatternMatcher.WINDOWS_PATH_PATTERN.test(value)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if value looks like an API key
|
||||
*/
|
||||
public isApiKey(value: string): boolean {
|
||||
return ValuePatternMatcher.API_KEY_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if value is a UUID
|
||||
*/
|
||||
public isUuid(value: string): boolean {
|
||||
return ValuePatternMatcher.UUID_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if value is a semantic version
|
||||
*/
|
||||
public isSemver(value: string): boolean {
|
||||
return ValuePatternMatcher.SEMVER_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if value is a hex color
|
||||
*/
|
||||
public isHexColor(value: string): boolean {
|
||||
return ValuePatternMatcher.HEX_COLOR_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if value is a MAC address
|
||||
*/
|
||||
public isMacAddress(value: string): boolean {
|
||||
return ValuePatternMatcher.MAC_ADDRESS_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if value is Base64 encoded (min length 20 to avoid false positives)
|
||||
*/
|
||||
public isBase64(value: string): boolean {
|
||||
return value.length >= 20 && ValuePatternMatcher.BASE64_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if value is a JWT token
|
||||
*/
|
||||
public isJwt(value: string): boolean {
|
||||
return ValuePatternMatcher.JWT_PATTERN.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects the type of value
|
||||
*/
|
||||
public detectType(
|
||||
value: string,
|
||||
):
|
||||
| "email"
|
||||
| "url"
|
||||
| "ip_address"
|
||||
| "file_path"
|
||||
| "date"
|
||||
| "api_key"
|
||||
| "uuid"
|
||||
| "version"
|
||||
| "color"
|
||||
| "mac_address"
|
||||
| "base64"
|
||||
| null {
|
||||
if (this.isEmail(value)) {
|
||||
return "email"
|
||||
}
|
||||
if (this.isJwt(value)) {
|
||||
return "api_key"
|
||||
}
|
||||
if (this.isApiKey(value)) {
|
||||
return "api_key"
|
||||
}
|
||||
if (this.isUrl(value)) {
|
||||
return "url"
|
||||
}
|
||||
if (this.isIpAddress(value)) {
|
||||
return "ip_address"
|
||||
}
|
||||
if (this.isFilePath(value)) {
|
||||
return "file_path"
|
||||
}
|
||||
if (this.isDate(value)) {
|
||||
return "date"
|
||||
}
|
||||
if (this.isUuid(value)) {
|
||||
return "uuid"
|
||||
}
|
||||
if (this.isSemver(value)) {
|
||||
return "version"
|
||||
}
|
||||
if (this.isHexColor(value)) {
|
||||
return "color"
|
||||
}
|
||||
if (this.isMacAddress(value)) {
|
||||
return "mac_address"
|
||||
}
|
||||
if (this.isBase64(value)) {
|
||||
return "base64"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if value should be detected as hardcoded
|
||||
*/
|
||||
public shouldDetect(value: string): boolean {
|
||||
return (
|
||||
this.isEmail(value) ||
|
||||
this.isUrl(value) ||
|
||||
this.isIpAddress(value) ||
|
||||
this.isFilePath(value) ||
|
||||
this.isDate(value) ||
|
||||
this.isApiKey(value) ||
|
||||
this.isUuid(value) ||
|
||||
this.isSemver(value) ||
|
||||
this.isHexColor(value) ||
|
||||
this.isMacAddress(value) ||
|
||||
this.isBase64(value) ||
|
||||
this.isJwt(value)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,25 @@ export const TYPE_NAMES = {
|
||||
OBJECT: "object",
|
||||
} as const
|
||||
|
||||
/**
|
||||
* TypeScript class and method keywords
|
||||
*/
|
||||
export const CLASS_KEYWORDS = {
|
||||
CONSTRUCTOR: "constructor",
|
||||
PUBLIC: "public",
|
||||
PRIVATE: "private",
|
||||
PROTECTED: "protected",
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Example code constants for documentation
|
||||
*/
|
||||
export const EXAMPLE_CODE_CONSTANTS = {
|
||||
ORDER_STATUS_PENDING: "pending",
|
||||
ORDER_STATUS_APPROVED: "approved",
|
||||
CANNOT_APPROVE_ERROR: "Cannot approve",
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Common regex patterns
|
||||
*/
|
||||
@@ -93,6 +112,7 @@ export const VIOLATION_SEVERITY_MAP = {
|
||||
DEPENDENCY_DIRECTION: SEVERITY_LEVELS.HIGH,
|
||||
FRAMEWORK_LEAK: SEVERITY_LEVELS.HIGH,
|
||||
ENTITY_EXPOSURE: SEVERITY_LEVELS.HIGH,
|
||||
ANEMIC_MODEL: SEVERITY_LEVELS.MEDIUM,
|
||||
NAMING_CONVENTION: SEVERITY_LEVELS.MEDIUM,
|
||||
ARCHITECTURE: SEVERITY_LEVELS.MEDIUM,
|
||||
HARDCODE: SEVERITY_LEVELS.LOW,
|
||||
|
||||
@@ -12,6 +12,7 @@ export const RULES = {
|
||||
REPOSITORY_PATTERN: "repository-pattern",
|
||||
AGGREGATE_BOUNDARY: "aggregate-boundary",
|
||||
SECRET_EXPOSURE: "secret-exposure",
|
||||
ANEMIC_MODEL: "anemic-model",
|
||||
} as const
|
||||
|
||||
/**
|
||||
@@ -20,6 +21,7 @@ export const RULES = {
|
||||
export const HARDCODE_TYPES = {
|
||||
MAGIC_NUMBER: "magic-number",
|
||||
MAGIC_STRING: "magic-string",
|
||||
MAGIC_BOOLEAN: "magic-boolean",
|
||||
MAGIC_CONFIG: "magic-config",
|
||||
} as const
|
||||
|
||||
@@ -103,32 +105,35 @@ export const NAMING_PATTERNS = {
|
||||
* Common verbs for use cases
|
||||
*/
|
||||
export const USE_CASE_VERBS = [
|
||||
"Aggregate",
|
||||
"Analyze",
|
||||
"Create",
|
||||
"Update",
|
||||
"Delete",
|
||||
"Get",
|
||||
"Find",
|
||||
"List",
|
||||
"Search",
|
||||
"Validate",
|
||||
"Calculate",
|
||||
"Generate",
|
||||
"Send",
|
||||
"Fetch",
|
||||
"Process",
|
||||
"Execute",
|
||||
"Handle",
|
||||
"Register",
|
||||
"Approve",
|
||||
"Authenticate",
|
||||
"Authorize",
|
||||
"Import",
|
||||
"Export",
|
||||
"Place",
|
||||
"Calculate",
|
||||
"Cancel",
|
||||
"Approve",
|
||||
"Reject",
|
||||
"Collect",
|
||||
"Confirm",
|
||||
"Create",
|
||||
"Delete",
|
||||
"Execute",
|
||||
"Export",
|
||||
"Fetch",
|
||||
"Find",
|
||||
"Generate",
|
||||
"Get",
|
||||
"Handle",
|
||||
"Import",
|
||||
"List",
|
||||
"Parse",
|
||||
"Place",
|
||||
"Process",
|
||||
"Register",
|
||||
"Reject",
|
||||
"Search",
|
||||
"Send",
|
||||
"Update",
|
||||
"Validate",
|
||||
] as const
|
||||
|
||||
/**
|
||||
@@ -412,3 +417,83 @@ export const REPOSITORY_VIOLATION_TYPES = {
|
||||
NEW_REPOSITORY_IN_USE_CASE: "new-repository-in-use-case",
|
||||
NON_DOMAIN_METHOD_NAME: "non-domain-method-name",
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Detection patterns for sensitive keywords
|
||||
*/
|
||||
export const DETECTION_PATTERNS = {
|
||||
SENSITIVE_KEYWORDS: ["password", "secret", "token", "auth", "credential"],
|
||||
BUSINESS_KEYWORDS: ["price", "salary", "balance", "amount", "limit", "threshold", "quota"],
|
||||
TECHNICAL_KEYWORDS: [
|
||||
"timeout",
|
||||
"retry",
|
||||
"attempt",
|
||||
"maxretries",
|
||||
"database",
|
||||
"connection",
|
||||
"host",
|
||||
"port",
|
||||
"endpoint",
|
||||
],
|
||||
MEDIUM_KEYWORDS: ["delay", "interval", "duration", "size", "count", "max", "min"],
|
||||
UI_KEYWORDS: [
|
||||
"padding",
|
||||
"margin",
|
||||
"width",
|
||||
"height",
|
||||
"color",
|
||||
"style",
|
||||
"label",
|
||||
"title",
|
||||
"placeholder",
|
||||
"icon",
|
||||
"text",
|
||||
"display",
|
||||
],
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Configuration detection keywords
|
||||
*/
|
||||
export const CONFIG_KEYWORDS = {
|
||||
NETWORK: ["endpoint", "host", "domain", "path", "route"],
|
||||
DATABASE: ["connection", "database"],
|
||||
SECURITY: ["config", "secret", "token", "password", "credential"],
|
||||
MESSAGES: ["message", "error", "warning", "text"],
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Detection comparison values
|
||||
*/
|
||||
export const DETECTION_VALUES = {
|
||||
BOOLEAN_TRUE: "true",
|
||||
BOOLEAN_FALSE: "false",
|
||||
TYPE_CONFIG: "config",
|
||||
TYPE_GENERIC: "generic",
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Boolean constants for analyzers
|
||||
*/
|
||||
export const ANALYZER_DEFAULTS = {
|
||||
HAS_ONLY_GETTERS_SETTERS: false,
|
||||
HAS_PUBLIC_SETTERS: false,
|
||||
HAS_BUSINESS_LOGIC: false,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Anemic model detection flags
|
||||
*/
|
||||
export const ANEMIC_MODEL_FLAGS = {
|
||||
HAS_ONLY_GETTERS_SETTERS_TRUE: true,
|
||||
HAS_ONLY_GETTERS_SETTERS_FALSE: false,
|
||||
HAS_PUBLIC_SETTERS_TRUE: true,
|
||||
HAS_PUBLIC_SETTERS_FALSE: false,
|
||||
} as const
|
||||
|
||||
/**
|
||||
* External package constants
|
||||
*/
|
||||
export const EXTERNAL_PACKAGES = {
|
||||
SECRETLINT_PRESET: "@secretlint/secretlint-rule-preset-recommend",
|
||||
} as const
|
||||
|
||||
372
packages/guardian/tests/AnemicModelDetector.test.ts
Normal file
372
packages/guardian/tests/AnemicModelDetector.test.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
import { AnemicModelDetector } from "../src/infrastructure/analyzers/AnemicModelDetector"
|
||||
|
||||
describe("AnemicModelDetector", () => {
|
||||
let detector: AnemicModelDetector
|
||||
|
||||
beforeEach(() => {
|
||||
detector = new AnemicModelDetector()
|
||||
})
|
||||
|
||||
describe("detectAnemicModels", () => {
|
||||
it("should detect class with only getters and setters", () => {
|
||||
const code = `
|
||||
class Order {
|
||||
private status: string
|
||||
private total: number
|
||||
|
||||
getStatus(): string {
|
||||
return this.status
|
||||
}
|
||||
|
||||
setStatus(status: string): void {
|
||||
this.status = status
|
||||
}
|
||||
|
||||
getTotal(): number {
|
||||
return this.total
|
||||
}
|
||||
|
||||
setTotal(total: number): void {
|
||||
this.total = total
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectAnemicModels(
|
||||
code,
|
||||
"src/domain/entities/Order.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations).toHaveLength(1)
|
||||
expect(violations[0].className).toBe("Order")
|
||||
expect(violations[0].methodCount).toBeGreaterThan(0)
|
||||
expect(violations[0].propertyCount).toBeGreaterThan(0)
|
||||
expect(violations[0].getMessage()).toContain("Order")
|
||||
})
|
||||
|
||||
it("should detect class with public setters", () => {
|
||||
const code = `
|
||||
class User {
|
||||
private email: string
|
||||
private password: string
|
||||
|
||||
public setEmail(email: string): void {
|
||||
this.email = email
|
||||
}
|
||||
|
||||
public getEmail(): string {
|
||||
return this.email
|
||||
}
|
||||
|
||||
public setPassword(password: string): void {
|
||||
this.password = password
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectAnemicModels(
|
||||
code,
|
||||
"src/domain/entities/User.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations).toHaveLength(1)
|
||||
expect(violations[0].className).toBe("User")
|
||||
expect(violations[0].hasPublicSetters).toBe(true)
|
||||
})
|
||||
|
||||
it("should not detect rich domain model with business logic", () => {
|
||||
const code = `
|
||||
class Order {
|
||||
private readonly id: string
|
||||
private status: OrderStatus
|
||||
private items: OrderItem[]
|
||||
|
||||
public approve(): void {
|
||||
if (!this.canBeApproved()) {
|
||||
throw new Error("Cannot approve")
|
||||
}
|
||||
this.status = OrderStatus.APPROVED
|
||||
}
|
||||
|
||||
public reject(reason: string): void {
|
||||
if (!this.canBeRejected()) {
|
||||
throw new Error("Cannot reject")
|
||||
}
|
||||
this.status = OrderStatus.REJECTED
|
||||
}
|
||||
|
||||
public addItem(item: OrderItem): void {
|
||||
if (this.isApproved()) {
|
||||
throw new Error("Cannot modify approved order")
|
||||
}
|
||||
this.items.push(item)
|
||||
}
|
||||
|
||||
public calculateTotal(): Money {
|
||||
return this.items.reduce((sum, item) => sum.add(item.getPrice()), Money.zero())
|
||||
}
|
||||
|
||||
public getStatus(): OrderStatus {
|
||||
return this.status
|
||||
}
|
||||
|
||||
private canBeApproved(): boolean {
|
||||
return this.status === OrderStatus.PENDING
|
||||
}
|
||||
|
||||
private canBeRejected(): boolean {
|
||||
return this.status === OrderStatus.PENDING
|
||||
}
|
||||
|
||||
private isApproved(): boolean {
|
||||
return this.status === OrderStatus.APPROVED
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectAnemicModels(
|
||||
code,
|
||||
"src/domain/entities/Order.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not analyze files outside domain layer", () => {
|
||||
const code = `
|
||||
class OrderDto {
|
||||
getStatus(): string {
|
||||
return this.status
|
||||
}
|
||||
|
||||
setStatus(status: string): void {
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectAnemicModels(
|
||||
code,
|
||||
"src/application/dtos/OrderDto.ts",
|
||||
"application",
|
||||
)
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not analyze DTO files", () => {
|
||||
const code = `
|
||||
class UserDto {
|
||||
private email: string
|
||||
|
||||
getEmail(): string {
|
||||
return this.email
|
||||
}
|
||||
|
||||
setEmail(email: string): void {
|
||||
this.email = email
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectAnemicModels(
|
||||
code,
|
||||
"src/domain/dtos/UserDto.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should not analyze test files", () => {
|
||||
const code = `
|
||||
class Order {
|
||||
getStatus(): string {
|
||||
return this.status
|
||||
}
|
||||
|
||||
setStatus(status: string): void {
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectAnemicModels(
|
||||
code,
|
||||
"src/domain/entities/Order.test.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should detect anemic model in entities folder", () => {
|
||||
const code = `
|
||||
class Product {
|
||||
private name: string
|
||||
private price: number
|
||||
|
||||
getName(): string {
|
||||
return this.name
|
||||
}
|
||||
|
||||
setName(name: string): void {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
getPrice(): number {
|
||||
return this.price
|
||||
}
|
||||
|
||||
setPrice(price: number): void {
|
||||
this.price = price
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectAnemicModels(
|
||||
code,
|
||||
"src/domain/entities/Product.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations).toHaveLength(1)
|
||||
expect(violations[0].className).toBe("Product")
|
||||
})
|
||||
|
||||
it("should detect anemic model in aggregates folder", () => {
|
||||
const code = `
|
||||
class Customer {
|
||||
private email: string
|
||||
|
||||
getEmail(): string {
|
||||
return this.email
|
||||
}
|
||||
|
||||
setEmail(email: string): void {
|
||||
this.email = email
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectAnemicModels(
|
||||
code,
|
||||
"src/domain/aggregates/customer/Customer.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations).toHaveLength(1)
|
||||
expect(violations[0].className).toBe("Customer")
|
||||
})
|
||||
|
||||
it("should not detect class with good method-to-property ratio", () => {
|
||||
const code = `
|
||||
class Account {
|
||||
private balance: number
|
||||
private isActive: boolean
|
||||
|
||||
public deposit(amount: number): void {
|
||||
if (amount <= 0) throw new Error("Invalid amount")
|
||||
this.balance += amount
|
||||
}
|
||||
|
||||
public withdraw(amount: number): void {
|
||||
if (amount > this.balance) throw new Error("Insufficient funds")
|
||||
this.balance -= amount
|
||||
}
|
||||
|
||||
public activate(): void {
|
||||
this.isActive = true
|
||||
}
|
||||
|
||||
public deactivate(): void {
|
||||
this.isActive = false
|
||||
}
|
||||
|
||||
public getBalance(): number {
|
||||
return this.balance
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectAnemicModels(
|
||||
code,
|
||||
"src/domain/entities/Account.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle class with no properties or methods", () => {
|
||||
const code = `
|
||||
class EmptyEntity {
|
||||
}
|
||||
`
|
||||
const violations = detector.detectAnemicModels(
|
||||
code,
|
||||
"src/domain/entities/EmptyEntity.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should detect multiple anemic classes in one file", () => {
|
||||
const code = `
|
||||
class Order {
|
||||
getStatus() { return this.status }
|
||||
setStatus(status: string) { this.status = status }
|
||||
}
|
||||
|
||||
class Item {
|
||||
getPrice() { return this.price }
|
||||
setPrice(price: number) { this.price = price }
|
||||
}
|
||||
`
|
||||
const violations = detector.detectAnemicModels(
|
||||
code,
|
||||
"src/domain/entities/Models.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations).toHaveLength(2)
|
||||
expect(violations[0].className).toBe("Order")
|
||||
expect(violations[1].className).toBe("Item")
|
||||
})
|
||||
|
||||
it("should provide correct violation details", () => {
|
||||
const code = `
|
||||
class Payment {
|
||||
private amount: number
|
||||
private currency: string
|
||||
|
||||
getAmount(): number {
|
||||
return this.amount
|
||||
}
|
||||
|
||||
setAmount(amount: number): void {
|
||||
this.amount = amount
|
||||
}
|
||||
|
||||
getCurrency(): string {
|
||||
return this.currency
|
||||
}
|
||||
|
||||
setCurrency(currency: string): void {
|
||||
this.currency = currency
|
||||
}
|
||||
}
|
||||
`
|
||||
const violations = detector.detectAnemicModels(
|
||||
code,
|
||||
"src/domain/entities/Payment.ts",
|
||||
"domain",
|
||||
)
|
||||
|
||||
expect(violations).toHaveLength(1)
|
||||
const violation = violations[0]
|
||||
expect(violation.className).toBe("Payment")
|
||||
expect(violation.filePath).toBe("src/domain/entities/Payment.ts")
|
||||
expect(violation.layer).toBe("domain")
|
||||
expect(violation.line).toBeGreaterThan(0)
|
||||
expect(violation.getMessage()).toContain("Payment")
|
||||
expect(violation.getSuggestion()).toContain("business")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -27,6 +27,7 @@ describe("AnalyzeProject E2E", () => {
|
||||
expect(Array.isArray(result.dependencyDirectionViolations)).toBe(true)
|
||||
expect(Array.isArray(result.repositoryPatternViolations)).toBe(true)
|
||||
expect(Array.isArray(result.aggregateBoundaryViolations)).toBe(true)
|
||||
expect(Array.isArray(result.anemicModelViolations)).toBe(true)
|
||||
})
|
||||
|
||||
it("should respect exclude patterns", async () => {
|
||||
@@ -65,7 +66,8 @@ describe("AnalyzeProject E2E", () => {
|
||||
result.entityExposureViolations.length +
|
||||
result.dependencyDirectionViolations.length +
|
||||
result.repositoryPatternViolations.length +
|
||||
result.aggregateBoundaryViolations.length
|
||||
result.aggregateBoundaryViolations.length +
|
||||
result.anemicModelViolations.length
|
||||
|
||||
expect(totalViolations).toBeGreaterThan(0)
|
||||
})
|
||||
@@ -82,6 +84,7 @@ describe("AnalyzeProject E2E", () => {
|
||||
expect(result.entityExposureViolations.length).toBe(0)
|
||||
expect(result.dependencyDirectionViolations.length).toBe(0)
|
||||
expect(result.circularDependencyViolations.length).toBe(0)
|
||||
expect(result.anemicModelViolations.length).toBe(0)
|
||||
})
|
||||
|
||||
it("should have no dependency direction violations", async () => {
|
||||
|
||||
358
packages/guardian/tests/unit/domain/EntityExposure.test.ts
Normal file
358
packages/guardian/tests/unit/domain/EntityExposure.test.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { EntityExposure } from "../../../src/domain/value-objects/EntityExposure"
|
||||
|
||||
describe("EntityExposure", () => {
|
||||
describe("create", () => {
|
||||
it("should create entity exposure with all properties", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"src/controllers/UserController.ts",
|
||||
"infrastructure",
|
||||
25,
|
||||
"getUser",
|
||||
)
|
||||
|
||||
expect(exposure.entityName).toBe("User")
|
||||
expect(exposure.returnType).toBe("User")
|
||||
expect(exposure.filePath).toBe("src/controllers/UserController.ts")
|
||||
expect(exposure.layer).toBe("infrastructure")
|
||||
expect(exposure.line).toBe(25)
|
||||
expect(exposure.methodName).toBe("getUser")
|
||||
})
|
||||
|
||||
it("should create entity exposure without optional properties", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Order",
|
||||
"Order",
|
||||
"src/controllers/OrderController.ts",
|
||||
"infrastructure",
|
||||
)
|
||||
|
||||
expect(exposure.entityName).toBe("Order")
|
||||
expect(exposure.line).toBeUndefined()
|
||||
expect(exposure.methodName).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should create entity exposure with line but without method name", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Product",
|
||||
"Product",
|
||||
"src/api/ProductApi.ts",
|
||||
"infrastructure",
|
||||
15,
|
||||
)
|
||||
|
||||
expect(exposure.line).toBe(15)
|
||||
expect(exposure.methodName).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getMessage", () => {
|
||||
it("should return message with method name", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"src/controllers/UserController.ts",
|
||||
"infrastructure",
|
||||
25,
|
||||
"getUser",
|
||||
)
|
||||
|
||||
const message = exposure.getMessage()
|
||||
|
||||
expect(message).toContain("Method 'getUser'")
|
||||
expect(message).toContain("returns domain entity 'User'")
|
||||
expect(message).toContain("instead of DTO")
|
||||
})
|
||||
|
||||
it("should return message without method name", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Order",
|
||||
"Order",
|
||||
"src/controllers/OrderController.ts",
|
||||
"infrastructure",
|
||||
30,
|
||||
)
|
||||
|
||||
const message = exposure.getMessage()
|
||||
|
||||
expect(message).toContain("returns domain entity 'Order'")
|
||||
expect(message).toContain("instead of DTO")
|
||||
expect(message).not.toContain("undefined")
|
||||
})
|
||||
|
||||
it("should handle different entity names", () => {
|
||||
const exposures = [
|
||||
EntityExposure.create(
|
||||
"Customer",
|
||||
"Customer",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
1,
|
||||
"getCustomer",
|
||||
),
|
||||
EntityExposure.create(
|
||||
"Invoice",
|
||||
"Invoice",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
2,
|
||||
"findInvoice",
|
||||
),
|
||||
EntityExposure.create(
|
||||
"Payment",
|
||||
"Payment",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
3,
|
||||
"processPayment",
|
||||
),
|
||||
]
|
||||
|
||||
exposures.forEach((exposure) => {
|
||||
const message = exposure.getMessage()
|
||||
expect(message).toContain(exposure.entityName)
|
||||
expect(message).toContain("instead of DTO")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getSuggestion", () => {
|
||||
it("should return multi-line suggestion", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"src/controllers/UserController.ts",
|
||||
"infrastructure",
|
||||
25,
|
||||
"getUser",
|
||||
)
|
||||
|
||||
const suggestion = exposure.getSuggestion()
|
||||
|
||||
expect(suggestion).toContain("Create a DTO class")
|
||||
expect(suggestion).toContain("UserResponseDto")
|
||||
expect(suggestion).toContain("Create a mapper")
|
||||
expect(suggestion).toContain("Update the method")
|
||||
})
|
||||
|
||||
it("should suggest appropriate DTO name based on entity", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Order",
|
||||
"Order",
|
||||
"src/controllers/OrderController.ts",
|
||||
"infrastructure",
|
||||
)
|
||||
|
||||
const suggestion = exposure.getSuggestion()
|
||||
|
||||
expect(suggestion).toContain("OrderResponseDto")
|
||||
expect(suggestion).toContain("convert Order to OrderResponseDto")
|
||||
})
|
||||
|
||||
it("should provide step-by-step suggestions", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Product",
|
||||
"Product",
|
||||
"src/api/ProductApi.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
)
|
||||
|
||||
const suggestion = exposure.getSuggestion()
|
||||
const lines = suggestion.split("\n")
|
||||
|
||||
expect(lines.length).toBeGreaterThan(1)
|
||||
expect(lines.some((line) => line.includes("Create a DTO"))).toBe(true)
|
||||
expect(lines.some((line) => line.includes("mapper"))).toBe(true)
|
||||
expect(lines.some((line) => line.includes("Update the method"))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getExampleFix", () => {
|
||||
it("should return example with method name", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"src/controllers/UserController.ts",
|
||||
"infrastructure",
|
||||
25,
|
||||
"getUser",
|
||||
)
|
||||
|
||||
const example = exposure.getExampleFix()
|
||||
|
||||
expect(example).toContain("Bad: Exposing domain entity")
|
||||
expect(example).toContain("Good: Using DTO")
|
||||
expect(example).toContain("getUser()")
|
||||
expect(example).toContain("Promise<User>")
|
||||
expect(example).toContain("Promise<UserResponseDto>")
|
||||
expect(example).toContain("UserMapper.toDto")
|
||||
})
|
||||
|
||||
it("should return example without method name", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Order",
|
||||
"Order",
|
||||
"src/controllers/OrderController.ts",
|
||||
"infrastructure",
|
||||
30,
|
||||
)
|
||||
|
||||
const example = exposure.getExampleFix()
|
||||
|
||||
expect(example).toContain("Promise<Order>")
|
||||
expect(example).toContain("Promise<OrderResponseDto>")
|
||||
expect(example).toContain("OrderMapper.toDto")
|
||||
expect(example).not.toContain("undefined")
|
||||
})
|
||||
|
||||
it("should show both bad and good examples", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Product",
|
||||
"Product",
|
||||
"src/api/ProductApi.ts",
|
||||
"infrastructure",
|
||||
15,
|
||||
"findProduct",
|
||||
)
|
||||
|
||||
const example = exposure.getExampleFix()
|
||||
|
||||
expect(example).toContain("❌ Bad")
|
||||
expect(example).toContain("✅ Good")
|
||||
})
|
||||
|
||||
it("should include async/await pattern", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"Customer",
|
||||
"Customer",
|
||||
"src/api/CustomerApi.ts",
|
||||
"infrastructure",
|
||||
20,
|
||||
"getCustomer",
|
||||
)
|
||||
|
||||
const example = exposure.getExampleFix()
|
||||
|
||||
expect(example).toContain("async")
|
||||
expect(example).toContain("await")
|
||||
})
|
||||
})
|
||||
|
||||
describe("value object behavior", () => {
|
||||
it("should be equal to another instance with same values", () => {
|
||||
const exposure1 = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"getUser",
|
||||
)
|
||||
const exposure2 = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"getUser",
|
||||
)
|
||||
|
||||
expect(exposure1.equals(exposure2)).toBe(true)
|
||||
})
|
||||
|
||||
it("should not be equal to instance with different values", () => {
|
||||
const exposure1 = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"getUser",
|
||||
)
|
||||
const exposure2 = EntityExposure.create(
|
||||
"Order",
|
||||
"Order",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"getUser",
|
||||
)
|
||||
|
||||
expect(exposure1.equals(exposure2)).toBe(false)
|
||||
})
|
||||
|
||||
it("should not be equal to instance with different method name", () => {
|
||||
const exposure1 = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"getUser",
|
||||
)
|
||||
const exposure2 = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"findUser",
|
||||
)
|
||||
|
||||
expect(exposure1.equals(exposure2)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle empty entity name", () => {
|
||||
const exposure = EntityExposure.create("", "", "file.ts", "infrastructure")
|
||||
|
||||
expect(exposure.entityName).toBe("")
|
||||
expect(exposure.getMessage()).toBeTruthy()
|
||||
})
|
||||
|
||||
it("should handle very long entity names", () => {
|
||||
const longName = "VeryLongEntityNameThatIsUnusuallyLong"
|
||||
const exposure = EntityExposure.create(longName, longName, "file.ts", "infrastructure")
|
||||
|
||||
expect(exposure.entityName).toBe(longName)
|
||||
const suggestion = exposure.getSuggestion()
|
||||
expect(suggestion).toContain(`${longName}ResponseDto`)
|
||||
})
|
||||
|
||||
it("should handle special characters in method name", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
10,
|
||||
"get$User",
|
||||
)
|
||||
|
||||
const message = exposure.getMessage()
|
||||
expect(message).toContain("get$User")
|
||||
})
|
||||
|
||||
it("should handle line number 0", () => {
|
||||
const exposure = EntityExposure.create("User", "User", "file.ts", "infrastructure", 0)
|
||||
|
||||
expect(exposure.line).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle very large line numbers", () => {
|
||||
const exposure = EntityExposure.create(
|
||||
"User",
|
||||
"User",
|
||||
"file.ts",
|
||||
"infrastructure",
|
||||
999999,
|
||||
)
|
||||
|
||||
expect(exposure.line).toBe(999999)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -33,13 +33,7 @@ describe("SecretViolation", () => {
|
||||
})
|
||||
|
||||
it("should create a secret violation with NPM token", () => {
|
||||
const violation = SecretViolation.create(
|
||||
".npmrc",
|
||||
1,
|
||||
1,
|
||||
"NPM Token",
|
||||
"npm_abc123xyz",
|
||||
)
|
||||
const violation = SecretViolation.create(".npmrc", 1, 1, "NPM Token", "npm_abc123xyz")
|
||||
|
||||
expect(violation.secretType).toBe("NPM Token")
|
||||
})
|
||||
@@ -133,13 +127,7 @@ describe("SecretViolation", () => {
|
||||
})
|
||||
|
||||
it("should return formatted message for NPM token", () => {
|
||||
const violation = SecretViolation.create(
|
||||
".npmrc",
|
||||
1,
|
||||
1,
|
||||
"NPM Token",
|
||||
"test",
|
||||
)
|
||||
const violation = SecretViolation.create(".npmrc", 1, 1, "NPM Token", "test")
|
||||
|
||||
expect(violation.getMessage()).toBe("Hardcoded NPM Token detected")
|
||||
})
|
||||
@@ -199,7 +187,7 @@ describe("SecretViolation", () => {
|
||||
|
||||
expect(example).toContain("AWS")
|
||||
expect(example).toContain("process.env.AWS_ACCESS_KEY_ID")
|
||||
expect(example).toContain("fromEnv")
|
||||
expect(example).toContain("credentials provider")
|
||||
})
|
||||
|
||||
it("should return GitHub-specific example for GitHub token", () => {
|
||||
@@ -219,13 +207,7 @@ describe("SecretViolation", () => {
|
||||
})
|
||||
|
||||
it("should return NPM-specific example for NPM token", () => {
|
||||
const violation = SecretViolation.create(
|
||||
".npmrc",
|
||||
1,
|
||||
1,
|
||||
"NPM Token",
|
||||
"test",
|
||||
)
|
||||
const violation = SecretViolation.create(".npmrc", 1, 1, "NPM Token", "test")
|
||||
|
||||
const example = violation.getExampleFix()
|
||||
|
||||
@@ -281,19 +263,13 @@ describe("SecretViolation", () => {
|
||||
})
|
||||
|
||||
it("should return API Key example for generic API key", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/api.ts",
|
||||
1,
|
||||
1,
|
||||
"API Key",
|
||||
"test",
|
||||
)
|
||||
const violation = SecretViolation.create("src/config/api.ts", 1, 1, "API Key", "test")
|
||||
|
||||
const example = violation.getExampleFix()
|
||||
|
||||
expect(example).toContain("API")
|
||||
expect(example).toContain("process.env.API_KEY")
|
||||
expect(example).toContain("SecretsManager")
|
||||
expect(example).toContain("secret management service")
|
||||
})
|
||||
|
||||
it("should return generic example for unknown secret type", () => {
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
import { DuplicateValueTracker } from "../../../src/infrastructure/analyzers/DuplicateValueTracker"
|
||||
import { HardcodedValue } from "../../../src/domain/value-objects/HardcodedValue"
|
||||
|
||||
describe("DuplicateValueTracker", () => {
|
||||
let tracker: DuplicateValueTracker
|
||||
|
||||
beforeEach(() => {
|
||||
tracker = new DuplicateValueTracker()
|
||||
})
|
||||
|
||||
describe("track", () => {
|
||||
it("should track a single hardcoded value", () => {
|
||||
const value = HardcodedValue.create(
|
||||
"test-value",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'test-value'",
|
||||
)
|
||||
|
||||
tracker.track(value, "file1.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should track multiple occurrences of the same value", () => {
|
||||
const value1 = HardcodedValue.create(
|
||||
"test-value",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'test-value'",
|
||||
)
|
||||
const value2 = HardcodedValue.create(
|
||||
"test-value",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'test-value'",
|
||||
)
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(1)
|
||||
expect(duplicates[0].value).toBe("test-value")
|
||||
expect(duplicates[0].count).toBe(2)
|
||||
})
|
||||
|
||||
it("should track values with different types separately", () => {
|
||||
const stringValue = HardcodedValue.create(
|
||||
"100",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = '100'",
|
||||
)
|
||||
const numberValue = HardcodedValue.create(100, "magic-number", 20, 5, "const y = 100")
|
||||
|
||||
tracker.track(stringValue, "file1.ts")
|
||||
tracker.track(numberValue, "file2.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should track boolean values", () => {
|
||||
const value1 = HardcodedValue.create(true, "MAGIC_BOOLEAN", 10, 5, "const x = true")
|
||||
const value2 = HardcodedValue.create(true, "MAGIC_BOOLEAN", 20, 5, "const y = true")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(1)
|
||||
expect(duplicates[0].value).toBe("true")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getDuplicates", () => {
|
||||
it("should return empty array when no duplicates exist", () => {
|
||||
const value1 = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'value1'",
|
||||
)
|
||||
const value2 = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'value2'",
|
||||
)
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should return duplicates sorted by count in descending order", () => {
|
||||
const value1a = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'value1'",
|
||||
)
|
||||
const value1b = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'value1'",
|
||||
)
|
||||
const value2a = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
30,
|
||||
5,
|
||||
"const z = 'value2'",
|
||||
)
|
||||
const value2b = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
40,
|
||||
5,
|
||||
"const a = 'value2'",
|
||||
)
|
||||
const value2c = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
50,
|
||||
5,
|
||||
"const b = 'value2'",
|
||||
)
|
||||
|
||||
tracker.track(value1a, "file1.ts")
|
||||
tracker.track(value1b, "file2.ts")
|
||||
tracker.track(value2a, "file3.ts")
|
||||
tracker.track(value2b, "file4.ts")
|
||||
tracker.track(value2c, "file5.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(2)
|
||||
expect(duplicates[0].value).toBe("value2")
|
||||
expect(duplicates[0].count).toBe(3)
|
||||
expect(duplicates[1].value).toBe("value1")
|
||||
expect(duplicates[1].count).toBe(2)
|
||||
})
|
||||
|
||||
it("should include location information for duplicates", () => {
|
||||
const value1 = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
const value2 = HardcodedValue.create("test", "magic-string", 20, 10, "const y = 'test'")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates[0].locations).toHaveLength(2)
|
||||
expect(duplicates[0].locations[0]).toEqual({
|
||||
file: "file1.ts",
|
||||
line: 10,
|
||||
context: "const x = 'test'",
|
||||
})
|
||||
expect(duplicates[0].locations[1]).toEqual({
|
||||
file: "file2.ts",
|
||||
line: 20,
|
||||
context: "const y = 'test'",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getDuplicateLocations", () => {
|
||||
it("should return null when value is not duplicated", () => {
|
||||
const value = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
|
||||
tracker.track(value, "file1.ts")
|
||||
|
||||
const locations = tracker.getDuplicateLocations("test", "magic-string")
|
||||
expect(locations).toBeNull()
|
||||
})
|
||||
|
||||
it("should return locations when value is duplicated", () => {
|
||||
const value1 = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
const value2 = HardcodedValue.create("test", "magic-string", 20, 10, "const y = 'test'")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const locations = tracker.getDuplicateLocations("test", "magic-string")
|
||||
expect(locations).toHaveLength(2)
|
||||
expect(locations).toEqual([
|
||||
{ file: "file1.ts", line: 10, context: "const x = 'test'" },
|
||||
{ file: "file2.ts", line: 20, context: "const y = 'test'" },
|
||||
])
|
||||
})
|
||||
|
||||
it("should return null for non-existent value", () => {
|
||||
const locations = tracker.getDuplicateLocations("non-existent", "magic-string")
|
||||
expect(locations).toBeNull()
|
||||
})
|
||||
|
||||
it("should handle numeric values", () => {
|
||||
const value1 = HardcodedValue.create(100, "magic-number", 10, 5, "const x = 100")
|
||||
const value2 = HardcodedValue.create(100, "magic-number", 20, 5, "const y = 100")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const locations = tracker.getDuplicateLocations(100, "magic-number")
|
||||
expect(locations).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe("isDuplicate", () => {
|
||||
it("should return false for non-duplicated value", () => {
|
||||
const value = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
|
||||
tracker.track(value, "file1.ts")
|
||||
|
||||
expect(tracker.isDuplicate("test", "magic-string")).toBe(false)
|
||||
})
|
||||
|
||||
it("should return true for duplicated value", () => {
|
||||
const value1 = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
const value2 = HardcodedValue.create("test", "magic-string", 20, 10, "const y = 'test'")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
expect(tracker.isDuplicate("test", "magic-string")).toBe(true)
|
||||
})
|
||||
|
||||
it("should return false for non-existent value", () => {
|
||||
expect(tracker.isDuplicate("non-existent", "magic-string")).toBe(false)
|
||||
})
|
||||
|
||||
it("should handle boolean values", () => {
|
||||
const value1 = HardcodedValue.create(true, "MAGIC_BOOLEAN", 10, 5, "const x = true")
|
||||
const value2 = HardcodedValue.create(true, "MAGIC_BOOLEAN", 20, 5, "const y = true")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
expect(tracker.isDuplicate(true, "MAGIC_BOOLEAN")).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getStats", () => {
|
||||
it("should return zero stats for empty tracker", () => {
|
||||
const stats = tracker.getStats()
|
||||
|
||||
expect(stats.totalValues).toBe(0)
|
||||
expect(stats.duplicateValues).toBe(0)
|
||||
expect(stats.duplicatePercentage).toBe(0)
|
||||
})
|
||||
|
||||
it("should calculate stats correctly with no duplicates", () => {
|
||||
const value1 = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'value1'",
|
||||
)
|
||||
const value2 = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'value2'",
|
||||
)
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const stats = tracker.getStats()
|
||||
expect(stats.totalValues).toBe(2)
|
||||
expect(stats.duplicateValues).toBe(0)
|
||||
expect(stats.duplicatePercentage).toBe(0)
|
||||
})
|
||||
|
||||
it("should calculate stats correctly with duplicates", () => {
|
||||
const value1a = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'value1'",
|
||||
)
|
||||
const value1b = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'value1'",
|
||||
)
|
||||
const value2 = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
30,
|
||||
5,
|
||||
"const z = 'value2'",
|
||||
)
|
||||
|
||||
tracker.track(value1a, "file1.ts")
|
||||
tracker.track(value1b, "file2.ts")
|
||||
tracker.track(value2, "file3.ts")
|
||||
|
||||
const stats = tracker.getStats()
|
||||
expect(stats.totalValues).toBe(2)
|
||||
expect(stats.duplicateValues).toBe(1)
|
||||
expect(stats.duplicatePercentage).toBe(50)
|
||||
})
|
||||
|
||||
it("should handle multiple duplicates", () => {
|
||||
const value1a = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'value1'",
|
||||
)
|
||||
const value1b = HardcodedValue.create(
|
||||
"value1",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'value1'",
|
||||
)
|
||||
const value2a = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
30,
|
||||
5,
|
||||
"const z = 'value2'",
|
||||
)
|
||||
const value2b = HardcodedValue.create(
|
||||
"value2",
|
||||
"magic-string",
|
||||
40,
|
||||
5,
|
||||
"const a = 'value2'",
|
||||
)
|
||||
|
||||
tracker.track(value1a, "file1.ts")
|
||||
tracker.track(value1b, "file2.ts")
|
||||
tracker.track(value2a, "file3.ts")
|
||||
tracker.track(value2b, "file4.ts")
|
||||
|
||||
const stats = tracker.getStats()
|
||||
expect(stats.totalValues).toBe(2)
|
||||
expect(stats.duplicateValues).toBe(2)
|
||||
expect(stats.duplicatePercentage).toBe(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe("clear", () => {
|
||||
it("should clear all tracked values", () => {
|
||||
const value1 = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
const value2 = HardcodedValue.create("test", "magic-string", 20, 10, "const y = 'test'")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
expect(tracker.getDuplicates()).toHaveLength(1)
|
||||
|
||||
tracker.clear()
|
||||
|
||||
expect(tracker.getDuplicates()).toHaveLength(0)
|
||||
expect(tracker.getStats().totalValues).toBe(0)
|
||||
})
|
||||
|
||||
it("should allow tracking new values after clear", () => {
|
||||
const value1 = HardcodedValue.create(
|
||||
"test1",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'test1'",
|
||||
)
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.clear()
|
||||
|
||||
const value2 = HardcodedValue.create(
|
||||
"test2",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'test2'",
|
||||
)
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const stats = tracker.getStats()
|
||||
expect(stats.totalValues).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle values with colons in them", () => {
|
||||
const value1 = HardcodedValue.create(
|
||||
"url:http://example.com",
|
||||
"magic-string",
|
||||
10,
|
||||
5,
|
||||
"const x = 'url:http://example.com'",
|
||||
)
|
||||
const value2 = HardcodedValue.create(
|
||||
"url:http://example.com",
|
||||
"magic-string",
|
||||
20,
|
||||
5,
|
||||
"const y = 'url:http://example.com'",
|
||||
)
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
const duplicates = tracker.getDuplicates()
|
||||
expect(duplicates).toHaveLength(1)
|
||||
expect(duplicates[0].value).toBe("url:http://example.com")
|
||||
})
|
||||
|
||||
it("should handle empty string values", () => {
|
||||
const value1 = HardcodedValue.create("", "magic-string", 10, 5, "const x = ''")
|
||||
const value2 = HardcodedValue.create("", "magic-string", 20, 5, "const y = ''")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
expect(tracker.isDuplicate("", "magic-string")).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle zero as a number", () => {
|
||||
const value1 = HardcodedValue.create(0, "magic-number", 10, 5, "const x = 0")
|
||||
const value2 = HardcodedValue.create(0, "magic-number", 20, 5, "const y = 0")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file2.ts")
|
||||
|
||||
expect(tracker.isDuplicate(0, "magic-number")).toBe(true)
|
||||
})
|
||||
|
||||
it("should track same file multiple times", () => {
|
||||
const value1 = HardcodedValue.create("test", "magic-string", 10, 5, "const x = 'test'")
|
||||
const value2 = HardcodedValue.create("test", "magic-string", 20, 5, "const y = 'test'")
|
||||
|
||||
tracker.track(value1, "file1.ts")
|
||||
tracker.track(value2, "file1.ts")
|
||||
|
||||
const locations = tracker.getDuplicateLocations("test", "magic-string")
|
||||
expect(locations).toHaveLength(2)
|
||||
expect(locations?.[0].file).toBe("file1.ts")
|
||||
expect(locations?.[1].file).toBe("file1.ts")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -274,4 +274,68 @@ describe("SecretDetector", () => {
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
})
|
||||
|
||||
describe("real secret detection", () => {
|
||||
it("should detect AWS access key pattern", async () => {
|
||||
const code = `const awsKey = "AKIAIOSFODNN7EXAMPLE"`
|
||||
|
||||
const violations = await detector.detectAll(code, "aws.ts")
|
||||
|
||||
if (violations.length > 0) {
|
||||
expect(violations[0].secretType).toContain("AWS")
|
||||
}
|
||||
})
|
||||
|
||||
it("should detect basic auth credentials", async () => {
|
||||
const code = `const auth = "https://user:password@example.com"`
|
||||
|
||||
const violations = await detector.detectAll(code, "auth.ts")
|
||||
|
||||
if (violations.length > 0) {
|
||||
expect(violations[0].file).toBe("auth.ts")
|
||||
expect(violations[0].line).toBeGreaterThan(0)
|
||||
expect(violations[0].column).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it("should detect private SSH key", async () => {
|
||||
const code = `
|
||||
const privateKey = \`-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIBogIBAAJBALRiMLAA...
|
||||
-----END RSA PRIVATE KEY-----\`
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "ssh.ts")
|
||||
|
||||
if (violations.length > 0) {
|
||||
expect(violations[0].secretType).toBeTruthy()
|
||||
}
|
||||
})
|
||||
|
||||
it("should return violation objects with required properties", async () => {
|
||||
const code = `const key = "AKIAIOSFODNN7EXAMPLE"`
|
||||
|
||||
const violations = await detector.detectAll(code, "test.ts")
|
||||
|
||||
violations.forEach((v) => {
|
||||
expect(v).toHaveProperty("file")
|
||||
expect(v).toHaveProperty("line")
|
||||
expect(v).toHaveProperty("column")
|
||||
expect(v).toHaveProperty("secretType")
|
||||
expect(v.getMessage).toBeDefined()
|
||||
expect(v.getSuggestion).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle files with multiple secrets", async () => {
|
||||
const code = `
|
||||
const key1 = "AKIAIOSFODNN7EXAMPLE"
|
||||
const key2 = "AKIAIOSFODNN8EXAMPLE"
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "multiple.ts")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
315
pnpm-lock.yaml
generated
315
pnpm-lock.yaml
generated
@@ -80,6 +80,18 @@ importers:
|
||||
|
||||
packages/guardian:
|
||||
dependencies:
|
||||
'@secretlint/core':
|
||||
specifier: ^11.2.5
|
||||
version: 11.2.5
|
||||
'@secretlint/node':
|
||||
specifier: ^11.2.5
|
||||
version: 11.2.5
|
||||
'@secretlint/secretlint-rule-preset-recommend':
|
||||
specifier: ^11.2.5
|
||||
version: 11.2.5
|
||||
'@secretlint/types':
|
||||
specifier: ^11.2.5
|
||||
version: 11.2.5
|
||||
commander:
|
||||
specifier: ^12.1.0
|
||||
version: 12.1.0
|
||||
@@ -154,6 +166,12 @@ packages:
|
||||
resolution: {integrity: sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==}
|
||||
engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
|
||||
|
||||
'@azu/format-text@1.0.2':
|
||||
resolution: {integrity: sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg==}
|
||||
|
||||
'@azu/style-format@1.0.1':
|
||||
resolution: {integrity: sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==}
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1040,6 +1058,40 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@secretlint/config-loader@11.2.5':
|
||||
resolution: {integrity: sha512-pUiH5xc3x8RLEDq+0dCz65v4kohtfp68I7qmYPuymTwHodzjyJ089ZbNdN1ZX8SZV4xZLQsFIrRLn1lJ55QyyQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@secretlint/core@11.2.5':
|
||||
resolution: {integrity: sha512-PZNpBd6+KVya2tA3o1oC2kTWYKju8lZG9phXyQY7geWKf+a+fInN4/HSYfCQS495oyTSjhc9qI0mNQEw83PY2Q==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@secretlint/formatter@11.2.5':
|
||||
resolution: {integrity: sha512-9XBMeveo1eKXMC9zLjA6nd2lb5JjUgjj8NUpCo1Il8jO4YJ12k7qXZk3T/QJup+Kh0ThpHO03D9C1xLDIPIEPQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@secretlint/node@11.2.5':
|
||||
resolution: {integrity: sha512-nPdtUsTzDzBJzFiKh80/H5+2ZRRogtDuHhnNiGtF7LSHp8YsQHU5piAVbESdV0AmUjbWijAjscIsWqvtU+2JUQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@secretlint/profiler@11.2.5':
|
||||
resolution: {integrity: sha512-evQ2PeO3Ub0apWIPaXJy8lMDO1OFgvgQhZd+MhYLcLHgR559EtJ9V02Sh5c10wTLkLAtJ+czlJg2kmlt0nm8fw==}
|
||||
|
||||
'@secretlint/resolver@11.2.5':
|
||||
resolution: {integrity: sha512-Zn9+Gj7cRNjEDX8d1NYZNjTG9/Wjlc8N+JvARFYYYu6JxfbtkabhFxzwxBLkRZ2ZCkPCCnuXJwepcgfVXSPsng==}
|
||||
|
||||
'@secretlint/secretlint-rule-preset-recommend@11.2.5':
|
||||
resolution: {integrity: sha512-FAnp/dPdbvHEw50aF9JMPF/OwW58ULvVXEsk+mXTtBD09VJZhG0vFum8WzxMbB98Eo4xDddGzYtE3g27pBOaQA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@secretlint/source-creator@11.2.5':
|
||||
resolution: {integrity: sha512-+ApoNDS4uIaLb2PG9PPEP9Zu1HDBWpxSd/+Qlb3MzKTwp2BG9sbUhvpGgxuIHFn7pMWQU60DhzYJJUBpbXZEHQ==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@secretlint/types@11.2.5':
|
||||
resolution: {integrity: sha512-iA7E+uXuiEydOwv8glEYM4tCHnl8C7wTgLxg+3upHhH/iSSnefWfoRqrJwVBhwxPg4MDoypVI7Oal7bX7/ne+w==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
'@sinclair/typebox@0.34.41':
|
||||
resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==}
|
||||
|
||||
@@ -1052,6 +1104,21 @@ packages:
|
||||
'@standard-schema/spec@1.0.0':
|
||||
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||
|
||||
'@textlint/ast-node-types@15.4.0':
|
||||
resolution: {integrity: sha512-IqY8i7IOGuvy05wZxISB7Me1ZyrvhaQGgx6DavfQjH3cfwpPFdDbDYmMXMuSv2xLS1kDB1kYKBV7fL2Vi16lRA==}
|
||||
|
||||
'@textlint/linter-formatter@15.4.0':
|
||||
resolution: {integrity: sha512-rfqOZmnI1Wwc/Pa4LK+vagvVPmvxf9oRsBRqIOB04DwhucingZyAIJI/TyG18DIDYbP2aFXBZ3oOvyAxHe/8PQ==}
|
||||
|
||||
'@textlint/module-interop@15.4.0':
|
||||
resolution: {integrity: sha512-uGf+SFIfzOLCbZI0gp+2NLsrkSArsvEWulPP6lJuKp7yRHadmy7Xf/YHORe46qhNyyxc8PiAfiixHJSaHGUrGg==}
|
||||
|
||||
'@textlint/resolver@15.4.0':
|
||||
resolution: {integrity: sha512-Vh/QceKZQHFJFG4GxxIsKM1Xhwv93mbtKHmFE5/ybal1mIKHdqF03Z9Guaqt6Sx/AeNUshq0hkMOEhEyEWnehQ==}
|
||||
|
||||
'@textlint/types@15.4.0':
|
||||
resolution: {integrity: sha512-ZMwJgw/xjxJufOD+IB7I2Enl9Si4Hxo04B76RwUZ5cKBKzOPcmd6WvGe2F7jqdgmTdGnfMU+Bo/joQrjPNIWqg==}
|
||||
|
||||
'@tokenizer/inflate@0.3.1':
|
||||
resolution: {integrity: sha512-4oeoZEBQdLdt5WmP/hx1KZ6D3/Oid/0cUb2nk4F0pTDAWy+KCH3/EnAkZF/bvckWo8I33EqBm01lIPgmgc8rCA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1488,6 +1555,10 @@ packages:
|
||||
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-escapes@7.2.0:
|
||||
resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1538,6 +1609,10 @@ packages:
|
||||
ast-v8-to-istanbul@0.3.8:
|
||||
resolution: {integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==}
|
||||
|
||||
astral-regex@2.0.0:
|
||||
resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
@@ -1576,9 +1651,16 @@ packages:
|
||||
resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==}
|
||||
hasBin: true
|
||||
|
||||
binaryextensions@6.11.0:
|
||||
resolution: {integrity: sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
||||
boundary@2.0.0:
|
||||
resolution: {integrity: sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA==}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||
|
||||
@@ -1638,6 +1720,10 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chalk@5.6.2:
|
||||
resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
char-regex@1.0.2:
|
||||
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1801,6 +1887,10 @@ packages:
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
editions@6.22.0:
|
||||
resolution: {integrity: sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ==}
|
||||
engines: {ecmascript: '>= es5', node: '>=4'}
|
||||
|
||||
electron-to-chromium@1.5.259:
|
||||
resolution: {integrity: sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==}
|
||||
|
||||
@@ -1818,6 +1908,10 @@ packages:
|
||||
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
environment@1.1.0:
|
||||
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
error-ex@1.3.4:
|
||||
resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
|
||||
|
||||
@@ -2249,6 +2343,10 @@ packages:
|
||||
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
istextorbinary@9.5.0:
|
||||
resolution: {integrity: sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
iterare@1.2.1:
|
||||
resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2473,6 +2571,9 @@ packages:
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
lodash.truncate@4.4.2:
|
||||
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
|
||||
|
||||
lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
@@ -2657,6 +2758,10 @@ packages:
|
||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
p-map@7.0.4:
|
||||
resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
p-try@2.2.0:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2725,6 +2830,9 @@ packages:
|
||||
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
pluralize@2.0.0:
|
||||
resolution: {integrity: sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw==}
|
||||
|
||||
pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2767,6 +2875,9 @@ packages:
|
||||
randombytes@2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
|
||||
rc-config-loader@4.1.3:
|
||||
resolution: {integrity: sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==}
|
||||
|
||||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
@@ -2894,6 +3005,10 @@ packages:
|
||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
slice-ansi@4.0.0:
|
||||
resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2972,6 +3087,9 @@ packages:
|
||||
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
structured-source@4.0.0:
|
||||
resolution: {integrity: sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA==}
|
||||
|
||||
superagent@10.2.3:
|
||||
resolution: {integrity: sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==}
|
||||
engines: {node: '>=14.18.0'}
|
||||
@@ -2988,6 +3106,10 @@ packages:
|
||||
resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
supports-hyperlinks@3.2.0:
|
||||
resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==}
|
||||
engines: {node: '>=14.18'}
|
||||
|
||||
symbol-observable@4.0.0:
|
||||
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
|
||||
engines: {node: '>=0.10'}
|
||||
@@ -2996,10 +3118,18 @@ packages:
|
||||
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
|
||||
table@6.9.0:
|
||||
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
tapable@2.3.0:
|
||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
terminal-link@4.0.0:
|
||||
resolution: {integrity: sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
terser-webpack-plugin@5.3.14:
|
||||
resolution: {integrity: sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
@@ -3025,6 +3155,13 @@ packages:
|
||||
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
||||
textextensions@6.11.0:
|
||||
resolution: {integrity: sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
tinybench@2.9.0:
|
||||
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||
|
||||
@@ -3217,6 +3354,10 @@ packages:
|
||||
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
|
||||
version-range@4.15.0:
|
||||
resolution: {integrity: sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
vite@7.2.4:
|
||||
resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -3441,6 +3582,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- chokidar
|
||||
|
||||
'@azu/format-text@1.0.2': {}
|
||||
|
||||
'@azu/style-format@1.0.1':
|
||||
dependencies:
|
||||
'@azu/format-text': 1.0.2
|
||||
|
||||
'@babel/code-frame@7.27.1':
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
@@ -4344,6 +4491,68 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.53.3':
|
||||
optional: true
|
||||
|
||||
'@secretlint/config-loader@11.2.5':
|
||||
dependencies:
|
||||
'@secretlint/profiler': 11.2.5
|
||||
'@secretlint/resolver': 11.2.5
|
||||
'@secretlint/types': 11.2.5
|
||||
ajv: 8.17.1
|
||||
debug: 4.4.3
|
||||
rc-config-loader: 4.1.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@secretlint/core@11.2.5':
|
||||
dependencies:
|
||||
'@secretlint/profiler': 11.2.5
|
||||
'@secretlint/types': 11.2.5
|
||||
debug: 4.4.3
|
||||
structured-source: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@secretlint/formatter@11.2.5':
|
||||
dependencies:
|
||||
'@secretlint/resolver': 11.2.5
|
||||
'@secretlint/types': 11.2.5
|
||||
'@textlint/linter-formatter': 15.4.0
|
||||
'@textlint/module-interop': 15.4.0
|
||||
'@textlint/types': 15.4.0
|
||||
chalk: 5.6.2
|
||||
debug: 4.4.3
|
||||
pluralize: 8.0.0
|
||||
strip-ansi: 7.1.2
|
||||
table: 6.9.0
|
||||
terminal-link: 4.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@secretlint/node@11.2.5':
|
||||
dependencies:
|
||||
'@secretlint/config-loader': 11.2.5
|
||||
'@secretlint/core': 11.2.5
|
||||
'@secretlint/formatter': 11.2.5
|
||||
'@secretlint/profiler': 11.2.5
|
||||
'@secretlint/source-creator': 11.2.5
|
||||
'@secretlint/types': 11.2.5
|
||||
debug: 4.4.3
|
||||
p-map: 7.0.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@secretlint/profiler@11.2.5': {}
|
||||
|
||||
'@secretlint/resolver@11.2.5': {}
|
||||
|
||||
'@secretlint/secretlint-rule-preset-recommend@11.2.5': {}
|
||||
|
||||
'@secretlint/source-creator@11.2.5':
|
||||
dependencies:
|
||||
'@secretlint/types': 11.2.5
|
||||
istextorbinary: 9.5.0
|
||||
|
||||
'@secretlint/types@11.2.5': {}
|
||||
|
||||
'@sinclair/typebox@0.34.41': {}
|
||||
|
||||
'@sinonjs/commons@3.0.1':
|
||||
@@ -4356,6 +4565,35 @@ snapshots:
|
||||
|
||||
'@standard-schema/spec@1.0.0': {}
|
||||
|
||||
'@textlint/ast-node-types@15.4.0': {}
|
||||
|
||||
'@textlint/linter-formatter@15.4.0':
|
||||
dependencies:
|
||||
'@azu/format-text': 1.0.2
|
||||
'@azu/style-format': 1.0.1
|
||||
'@textlint/module-interop': 15.4.0
|
||||
'@textlint/resolver': 15.4.0
|
||||
'@textlint/types': 15.4.0
|
||||
chalk: 4.1.2
|
||||
debug: 4.4.3
|
||||
js-yaml: 3.14.2
|
||||
lodash: 4.17.21
|
||||
pluralize: 2.0.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
table: 6.9.0
|
||||
text-table: 0.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@textlint/module-interop@15.4.0': {}
|
||||
|
||||
'@textlint/resolver@15.4.0': {}
|
||||
|
||||
'@textlint/types@15.4.0':
|
||||
dependencies:
|
||||
'@textlint/ast-node-types': 15.4.0
|
||||
|
||||
'@tokenizer/inflate@0.3.1':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@@ -4865,6 +5103,10 @@ snapshots:
|
||||
dependencies:
|
||||
type-fest: 0.21.3
|
||||
|
||||
ansi-escapes@7.2.0:
|
||||
dependencies:
|
||||
environment: 1.1.0
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
@@ -4904,6 +5146,8 @@ snapshots:
|
||||
estree-walker: 3.0.3
|
||||
js-tokens: 9.0.1
|
||||
|
||||
astral-regex@2.0.0: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
babel-jest@30.2.0(@babel/core@7.28.5):
|
||||
@@ -4964,12 +5208,18 @@ snapshots:
|
||||
|
||||
baseline-browser-mapping@2.8.31: {}
|
||||
|
||||
binaryextensions@6.11.0:
|
||||
dependencies:
|
||||
editions: 6.22.0
|
||||
|
||||
bl@4.1.0:
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
boundary@2.0.0: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
@@ -5031,6 +5281,8 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chalk@5.6.2: {}
|
||||
|
||||
char-regex@1.0.2: {}
|
||||
|
||||
chardet@2.1.1: {}
|
||||
@@ -5155,6 +5407,10 @@ snapshots:
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
editions@6.22.0:
|
||||
dependencies:
|
||||
version-range: 4.15.0
|
||||
|
||||
electron-to-chromium@1.5.259: {}
|
||||
|
||||
emittery@0.13.1: {}
|
||||
@@ -5168,6 +5424,8 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.3.0
|
||||
|
||||
environment@1.1.0: {}
|
||||
|
||||
error-ex@1.3.4:
|
||||
dependencies:
|
||||
is-arrayish: 0.2.1
|
||||
@@ -5647,6 +5905,12 @@ snapshots:
|
||||
html-escaper: 2.0.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
|
||||
istextorbinary@9.5.0:
|
||||
dependencies:
|
||||
binaryextensions: 6.11.0
|
||||
editions: 6.22.0
|
||||
textextensions: 6.11.0
|
||||
|
||||
iterare@1.2.1: {}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
@@ -6041,6 +6305,8 @@ snapshots:
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lodash.truncate@4.4.2: {}
|
||||
|
||||
lodash@4.17.21: {}
|
||||
|
||||
log-symbols@4.1.0:
|
||||
@@ -6204,6 +6470,8 @@ snapshots:
|
||||
dependencies:
|
||||
p-limit: 3.1.0
|
||||
|
||||
p-map@7.0.4: {}
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
@@ -6255,6 +6523,8 @@ snapshots:
|
||||
dependencies:
|
||||
find-up: 4.1.0
|
||||
|
||||
pluralize@2.0.0: {}
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
postcss@8.5.6:
|
||||
@@ -6291,6 +6561,15 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
rc-config-loader@4.1.3:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
js-yaml: 4.1.1
|
||||
json5: 2.2.3
|
||||
require-from-string: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
@@ -6441,6 +6720,12 @@ snapshots:
|
||||
|
||||
slash@3.0.0: {}
|
||||
|
||||
slice-ansi@4.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
astral-regex: 2.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map-support@0.5.13:
|
||||
@@ -6510,6 +6795,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@tokenizer/token': 0.3.0
|
||||
|
||||
structured-source@4.0.0:
|
||||
dependencies:
|
||||
boundary: 2.0.0
|
||||
|
||||
superagent@10.2.3:
|
||||
dependencies:
|
||||
component-emitter: 1.3.1
|
||||
@@ -6539,14 +6828,32 @@ snapshots:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
supports-hyperlinks@3.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
symbol-observable@4.0.0: {}
|
||||
|
||||
synckit@0.11.11:
|
||||
dependencies:
|
||||
'@pkgr/core': 0.2.9
|
||||
|
||||
table@6.9.0:
|
||||
dependencies:
|
||||
ajv: 8.17.1
|
||||
lodash.truncate: 4.4.2
|
||||
slice-ansi: 4.0.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
tapable@2.3.0: {}
|
||||
|
||||
terminal-link@4.0.0:
|
||||
dependencies:
|
||||
ansi-escapes: 7.2.0
|
||||
supports-hyperlinks: 3.2.0
|
||||
|
||||
terser-webpack-plugin@5.3.14(webpack@5.100.2):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
@@ -6569,6 +6876,12 @@ snapshots:
|
||||
glob: 7.2.3
|
||||
minimatch: 3.1.2
|
||||
|
||||
text-table@0.2.0: {}
|
||||
|
||||
textextensions@6.11.0:
|
||||
dependencies:
|
||||
editions: 6.22.0
|
||||
|
||||
tinybench@2.9.0: {}
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
@@ -6770,6 +7083,8 @@ snapshots:
|
||||
'@types/istanbul-lib-coverage': 2.0.6
|
||||
convert-source-map: 2.0.0
|
||||
|
||||
version-range@4.15.0: {}
|
||||
|
||||
vite@7.2.4(@types/node@22.19.1)(terser@5.44.1)(tsx@4.20.6):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
|
||||
Reference in New Issue
Block a user