mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1663d191ee | ||
|
|
7b4cb60f13 | ||
|
|
33d763c41b |
@@ -5,6 +5,31 @@ All notable changes to @samiyev/guardian will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [0.7.4] - 2025-11-25
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🐛 **TypeScript-aware hardcode detection** - dramatically reduces false positives by 35.7%:
|
||||||
|
- Ignore strings in TypeScript union types (`type Status = 'active' | 'inactive'`)
|
||||||
|
- Ignore strings in interface property types (`interface { mode: 'development' | 'production' }`)
|
||||||
|
- Ignore strings in type assertions (`as 'read' | 'write'`)
|
||||||
|
- Ignore strings in typeof checks (`typeof x === 'string'`)
|
||||||
|
- Ignore strings in Symbol() calls for DI tokens (`Symbol('LOGGER')`)
|
||||||
|
- Ignore strings in dynamic import() calls (`import('../../module.js')`)
|
||||||
|
- Exclude tokens.ts/tokens.js files completely (DI container files)
|
||||||
|
- Tested on real-world TypeScript project: 985 → 633 issues (352 false positives eliminated)
|
||||||
|
- ✅ **Added 13 new tests** for TypeScript type context filtering
|
||||||
|
|
||||||
|
## [0.7.3] - 2025-11-25
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 🐛 **False positive: repository importing its own aggregate:**
|
||||||
|
- Added `isInternalBoundedContextImport()` method to detect internal imports
|
||||||
|
- Imports like `../aggregates/Entity` from `repositories/Repo` are now allowed
|
||||||
|
- This correctly allows `ICodeProjectRepository` to import `CodeProject` from the same bounded context
|
||||||
|
- Cross-aggregate imports (with multiple `../..`) are still detected as violations
|
||||||
|
|
||||||
## [0.7.2] - 2025-11-25
|
## [0.7.2] - 2025-11-25
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@samiyev/guardian",
|
"name": "@samiyev/guardian",
|
||||||
"version": "0.7.2",
|
"version": "0.7.3",
|
||||||
"description": "Research-backed code quality guardian for AI-assisted development. Detects hardcodes, circular deps, framework leaks, entity exposure, and 8 architecture violations. Enforces Clean Architecture/DDD principles. Works with GitHub Copilot, Cursor, Windsurf, Claude, ChatGPT, Cline, and any AI coding tool.",
|
"description": "Research-backed code quality guardian for AI-assisted development. Detects hardcodes, circular deps, framework leaks, entity exposure, and 8 architecture violations. Enforces Clean Architecture/DDD principles. Works with GitHub Copilot, Cursor, Windsurf, Claude, ChatGPT, Cline, and any AI coding tool.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"puaros",
|
"puaros",
|
||||||
|
|||||||
@@ -195,6 +195,11 @@ export class AggregateBoundaryDetector implements IAggregateBoundaryDetector {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if import stays within the same bounded context
|
||||||
|
if (this.isInternalBoundedContextImport(normalizedPath)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const targetAggregate = this.extractAggregateFromImport(normalizedPath)
|
const targetAggregate = this.extractAggregateFromImport(normalizedPath)
|
||||||
if (!targetAggregate || targetAggregate === currentAggregate) {
|
if (!targetAggregate || targetAggregate === currentAggregate) {
|
||||||
return false
|
return false
|
||||||
@@ -207,6 +212,36 @@ export class AggregateBoundaryDetector implements IAggregateBoundaryDetector {
|
|||||||
return this.seemsLikeEntityImport(normalizedPath)
|
return this.seemsLikeEntityImport(normalizedPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the import is internal to the same bounded context
|
||||||
|
*
|
||||||
|
* An import like "../aggregates/Entity" from "repositories/Repo" stays within
|
||||||
|
* the same bounded context (one level up goes to the bounded context root).
|
||||||
|
*
|
||||||
|
* An import like "../../other-context/Entity" crosses bounded context boundaries.
|
||||||
|
*/
|
||||||
|
private isInternalBoundedContextImport(normalizedPath: string): boolean {
|
||||||
|
const parts = normalizedPath.split("/")
|
||||||
|
const dotDotCount = parts.filter((p) => p === "..").length
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If only one ".." and path goes into aggregates/entities folder,
|
||||||
|
* it's likely an internal import within the same bounded context
|
||||||
|
*/
|
||||||
|
if (dotDotCount === 1) {
|
||||||
|
const nonDotParts = parts.filter((p) => p !== ".." && p !== ".")
|
||||||
|
if (nonDotParts.length >= 1) {
|
||||||
|
const firstFolder = nonDotParts[0]
|
||||||
|
// Importing from aggregates/entities within same bounded context is allowed
|
||||||
|
if (this.entityFolderNames.has(firstFolder)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the import path is from an allowed folder (value-objects, events, etc.)
|
* Checks if the import path is from an allowed folder (value-objects, events, etc.)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,6 +26,19 @@ export class HardcodeDetector implements IHardcodeDetector {
|
|||||||
|
|
||||||
private readonly ALLOWED_STRING_PATTERNS = [/^[a-z]$/i, /^\/$/, /^\\$/, /^\s+$/, /^,$/, /^\.$/]
|
private readonly ALLOWED_STRING_PATTERNS = [/^[a-z]$/i, /^\/$/, /^\\$/, /^\s+$/, /^,$/, /^\.$/]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patterns to detect TypeScript type contexts where strings should be ignored
|
||||||
|
*/
|
||||||
|
private readonly TYPE_CONTEXT_PATTERNS = [
|
||||||
|
/^\s*type\s+\w+\s*=/i, // type Foo = ...
|
||||||
|
/^\s*interface\s+\w+/i, // interface Foo { ... }
|
||||||
|
/^\s*\w+\s*:\s*['"`]/, // property: 'value' (in type or interface)
|
||||||
|
/\s+as\s+['"`]/, // ... as 'type'
|
||||||
|
/Record<.*,\s*import\(/, // Record with import type
|
||||||
|
/typeof\s+\w+\s*===\s*['"`]/, // typeof x === 'string'
|
||||||
|
/['"`]\s*===\s*typeof\s+\w+/, // 'string' === typeof x
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detects all hardcoded values (both numbers and strings) in the given code
|
* Detects all hardcoded values (both numbers and strings) in the given code
|
||||||
*
|
*
|
||||||
@@ -43,14 +56,15 @@ export class HardcodeDetector implements IHardcodeDetector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a file is a constants definition file
|
* Check if a file is a constants definition file or DI tokens file
|
||||||
*/
|
*/
|
||||||
private isConstantsFile(filePath: string): boolean {
|
private isConstantsFile(filePath: string): boolean {
|
||||||
const _fileName = filePath.split("/").pop() ?? ""
|
const _fileName = filePath.split("/").pop() ?? ""
|
||||||
const constantsPatterns = [
|
const constantsPatterns = [
|
||||||
/^constants?\.(ts|js)$/i,
|
/^constants?\.(ts|js)$/i,
|
||||||
/constants?\/.*\.(ts|js)$/i,
|
/constants?\/.*\.(ts|js)$/i,
|
||||||
/\/(constants|config|settings|defaults)\.ts$/i,
|
/\/(constants|config|settings|defaults|tokens)\.ts$/i,
|
||||||
|
/\/di\/tokens\.(ts|js)$/i,
|
||||||
]
|
]
|
||||||
return constantsPatterns.some((pattern) => pattern.test(filePath))
|
return constantsPatterns.some((pattern) => pattern.test(filePath))
|
||||||
}
|
}
|
||||||
@@ -341,6 +355,18 @@ export class HardcodeDetector implements IHardcodeDetector {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isInTypeContext(line)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isInSymbolCall(line, value)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isInImportCall(line, value)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (value.includes(DETECTION_KEYWORDS.HTTP) || value.includes(DETECTION_KEYWORDS.API)) {
|
if (value.includes(DETECTION_KEYWORDS.HTTP) || value.includes(DETECTION_KEYWORDS.API)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -388,4 +414,46 @@ export class HardcodeDetector implements IHardcodeDetector {
|
|||||||
const end = Math.min(line.length, index + 30)
|
const end = Math.min(line.length, index + 30)
|
||||||
return line.substring(start, end)
|
return line.substring(start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a line is in a TypeScript type definition context
|
||||||
|
* Examples:
|
||||||
|
* - type Foo = 'a' | 'b'
|
||||||
|
* - interface Bar { prop: 'value' }
|
||||||
|
* - Record<X, import('path')>
|
||||||
|
* - ... as 'type'
|
||||||
|
*/
|
||||||
|
private isInTypeContext(line: string): boolean {
|
||||||
|
const trimmedLine = line.trim()
|
||||||
|
|
||||||
|
if (this.TYPE_CONTEXT_PATTERNS.some((pattern) => pattern.test(trimmedLine))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmedLine.includes("|") && /['"`][^'"`]+['"`]\s*\|/.test(trimmedLine)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is inside a Symbol() call
|
||||||
|
* Example: Symbol('TOKEN_NAME')
|
||||||
|
*/
|
||||||
|
private isInSymbolCall(line: string, stringValue: string): boolean {
|
||||||
|
const symbolPattern = new RegExp(
|
||||||
|
`Symbol\\s*\\(\\s*['"\`]${stringValue.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"\`]\\s*\\)`,
|
||||||
|
)
|
||||||
|
return symbolPattern.test(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a string is inside an import() call
|
||||||
|
* Example: import('../../path/to/module.js')
|
||||||
|
*/
|
||||||
|
private isInImportCall(line: string, stringValue: string): boolean {
|
||||||
|
const importPattern = /import\s*\(\s*['"`][^'"`]+['"`]\s*\)/
|
||||||
|
return importPattern.test(line) && line.includes(stringValue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -468,4 +468,102 @@ const b = 2`
|
|||||||
expect(result[0].context).toContain("5000")
|
expect(result[0].context).toContain("5000")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("TypeScript type contexts (false positive reduction)", () => {
|
||||||
|
it("should NOT detect strings in union types", () => {
|
||||||
|
const code = `type Status = 'active' | 'inactive' | 'pending'`
|
||||||
|
const result = detector.detectMagicStrings(code, "test.ts")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should NOT detect strings in interface property types", () => {
|
||||||
|
const code = `interface Config { mode: 'development' | 'production' }`
|
||||||
|
const result = detector.detectMagicStrings(code, "test.ts")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should NOT detect strings in type aliases", () => {
|
||||||
|
const code = `type Theme = 'light' | 'dark'`
|
||||||
|
const result = detector.detectMagicStrings(code, "test.ts")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should NOT detect strings in type assertions", () => {
|
||||||
|
const code = `const mode = getMode() as 'read' | 'write'`
|
||||||
|
const result = detector.detectMagicStrings(code, "test.ts")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should NOT detect strings in Symbol() calls", () => {
|
||||||
|
const code = `const TOKEN = Symbol('MY_TOKEN')`
|
||||||
|
const result = detector.detectMagicStrings(code, "test.ts")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should NOT detect strings in multiple Symbol() calls", () => {
|
||||||
|
const code = `
|
||||||
|
export const LOGGER = Symbol('LOGGER')
|
||||||
|
export const DATABASE = Symbol('DATABASE')
|
||||||
|
export const CACHE = Symbol('CACHE')
|
||||||
|
`
|
||||||
|
const result = detector.detectMagicStrings(code, "test.ts")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should NOT detect strings in import() calls", () => {
|
||||||
|
const code = `const module = import('../../path/to/module.js')`
|
||||||
|
const result = detector.detectMagicStrings(code, "test.ts")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should NOT detect strings in typeof checks", () => {
|
||||||
|
const code = `if (typeof x === 'string') { }`
|
||||||
|
const result = detector.detectMagicStrings(code, "test.ts")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should NOT detect strings in reverse typeof checks", () => {
|
||||||
|
const code = `if ('number' === typeof count) { }`
|
||||||
|
const result = detector.detectMagicStrings(code, "test.ts")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should skip tokens.ts files completely", () => {
|
||||||
|
const code = `
|
||||||
|
export const LOGGER = Symbol('LOGGER')
|
||||||
|
export const DATABASE = Symbol('DATABASE')
|
||||||
|
const url = "http://localhost:8080"
|
||||||
|
`
|
||||||
|
const result = detector.detectAll(code, "src/di/tokens.ts")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should skip tokens.js files completely", () => {
|
||||||
|
const code = `const TOKEN = Symbol('TOKEN')`
|
||||||
|
const result = detector.detectAll(code, "src/di/tokens.js")
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should detect real magic strings even with type contexts nearby", () => {
|
||||||
|
const code = `
|
||||||
|
type Mode = 'read' | 'write'
|
||||||
|
const apiKey = "secret-key-12345"
|
||||||
|
`
|
||||||
|
const result = detector.detectMagicStrings(code, "test.ts")
|
||||||
|
|
||||||
|
expect(result.length).toBeGreaterThan(0)
|
||||||
|
expect(result.some((r) => r.value === "secret-key-12345")).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user