UNPKG

@samiyev/guardian

Version:

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, W

159 lines 5.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EntityExposureDetector = void 0; const EntityExposure_1 = require("../../domain/value-objects/EntityExposure"); const rules_1 = require("../../shared/constants/rules"); const type_patterns_1 = require("../constants/type-patterns"); /** * Detects domain entity exposure in controller/route return types * * This detector identifies violations where controllers or route handlers * directly return domain entities instead of using DTOs (Data Transfer Objects). * This violates separation of concerns and can expose internal domain logic. * * @example * ```typescript * const detector = new EntityExposureDetector() * * // Detect exposures in a controller file * const code = ` * class UserController { * async getUser(id: string): Promise<User> { * return this.userService.findById(id) * } * } * ` * const exposures = detector.detectExposures(code, 'src/infrastructure/controllers/UserController.ts', 'infrastructure') * * // exposures will contain violation for returning User entity * console.log(exposures.length) // 1 * console.log(exposures[0].entityName) // 'User' * ``` */ class EntityExposureDetector { dtoSuffixes = type_patterns_1.DTO_SUFFIXES; controllerPatterns = [ /Controller/i, /Route/i, /Handler/i, /Resolver/i, /Gateway/i, ]; /** * Detects entity exposure violations in the given code * * Analyzes method return types in controllers/routes to identify * domain entities being directly exposed to external clients. * * @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 entity exposure violations */ detectExposures(code, filePath, layer) { if (layer !== rules_1.LAYERS.INFRASTRUCTURE || !this.isControllerFile(filePath)) { return []; } const exposures = []; const lines = code.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNumber = i + 1; const methodMatches = this.findMethodReturnTypes(line); for (const match of methodMatches) { const { methodName, returnType } = match; if (this.isDomainEntity(returnType)) { exposures.push(EntityExposure_1.EntityExposure.create(returnType, returnType, filePath, layer, lineNumber, methodName)); } } } return exposures; } /** * Checks if a return type is a domain entity * * Domain entities are typically PascalCase nouns without Dto/Request/Response suffixes * and are defined in the domain layer. * * @param returnType - The return type to check * @returns True if the return type appears to be a domain entity */ isDomainEntity(returnType) { if (!returnType || returnType.trim() === "") { return false; } const cleanType = this.extractCoreType(returnType); if (this.isPrimitiveType(cleanType)) { return false; } if (this.hasAllowedSuffix(cleanType)) { return false; } return this.isPascalCase(cleanType); } /** * Checks if the file is a controller/route file */ isControllerFile(filePath) { return this.controllerPatterns.some((pattern) => pattern.test(filePath)); } /** * Finds method return types in a line of code */ findMethodReturnTypes(line) { const matches = []; const methodRegex = /(?:async\s+)?(\w+)\s*\([^)]*\)\s*:\s*Promise<([^>]+)>|(?:async\s+)?(\w+)\s*\([^)]*\)\s*:\s*([A-Z]\w+)/g; let match; while ((match = methodRegex.exec(line)) !== null) { const methodName = match[1] || match[3]; const returnType = match[2] || match[4]; if (methodName && returnType) { matches.push({ methodName, returnType }); } } return matches; } /** * Extracts core type from complex type annotations * Examples: * - "Promise<User>" -> "User" * - "User[]" -> "User" * - "User | null" -> "User" */ extractCoreType(returnType) { let cleanType = returnType.trim(); cleanType = cleanType.replace(/Promise<([^>]+)>/, "$1"); cleanType = cleanType.replace(/\[\]$/, ""); if (cleanType.includes("|")) { const types = cleanType.split("|").map((t) => t.trim()); const nonNullTypes = types.filter((t) => !type_patterns_1.NULLABLE_TYPES.includes(t)); if (nonNullTypes.length > 0) { cleanType = nonNullTypes[0]; } } return cleanType.trim(); } /** * Checks if a type is a primitive type */ isPrimitiveType(type) { return type_patterns_1.PRIMITIVE_TYPES.includes(type.toLowerCase()); } /** * Checks if a type has an allowed DTO/Response suffix */ hasAllowedSuffix(type) { return this.dtoSuffixes.some((suffix) => type.endsWith(suffix)); } /** * Checks if a string is in PascalCase */ isPascalCase(str) { if (!str || str.length === 0) { return false; } return /^[A-Z]([a-z0-9]+[A-Z]?)*[a-z0-9]*$/.test(str) && /[a-z]/.test(str); } } exports.EntityExposureDetector = EntityExposureDetector; //# sourceMappingURL=EntityExposureDetector.js.map