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

227 lines 8.52 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AnemicModelDetector = void 0; const AnemicModelViolation_1 = require("../../domain/value-objects/AnemicModelViolation"); const constants_1 = require("../../shared/constants"); const rules_1 = require("../../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' * ``` */ class AnemicModelDetector { entityPatterns = [/\/entities\//, /\/aggregates\//]; excludePatterns = [ /\.test\.ts$/, /\.spec\.ts$/, /Dto\.ts$/, /Request\.ts$/, /Response\.ts$/, /Mapper\.ts$/, ]; /** * Detects anemic model violations in the given code */ detectAnemicModels(code, filePath, layer) { if (!this.shouldAnalyze(filePath, layer)) { return []; } const violations = []; const classes = this.extractClasses(code); for (const classInfo of classes) { const violation = this.analyzeClass(classInfo, filePath, layer || rules_1.LAYERS.DOMAIN); if (violation) { violations.push(violation); } } return violations; } /** * Checks if file should be analyzed */ shouldAnalyze(filePath, layer) { if (layer !== rules_1.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 */ extractClasses(code) { const classes = []; const lines = code.split("\n"); let currentClass = 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 */ extractProperties(classBody) { const properties = []; 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 */ extractMethods(classBody) { const methods = []; 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] || constants_1.CLASS_KEYWORDS.PUBLIC; const accessor = match[2]; const methodName = match[3]; if (methodName === constants_1.CLASS_KEYWORDS.CONSTRUCTOR) { continue; } const isGetter = accessor === "get" || this.isGetterMethod(methodName); const isSetter = accessor === "set" || this.isSetterMethod(methodName, classBody); const isPublic = visibility === constants_1.CLASS_KEYWORDS.PUBLIC || !visibility; methods.push({ name: methodName, isGetter, isSetter, isPublic, isBusinessLogic: !isGetter && !isSetter, }); } return methods; } /** * Analyzes class for anemic model violations */ analyzeClass(classInfo, filePath, layer) { 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_1.AnemicModelViolation.create(className, filePath, layer, lineNumber, methodCount, propertyCount, rules_1.ANEMIC_MODEL_FLAGS.HAS_ONLY_GETTERS_SETTERS_FALSE, rules_1.ANEMIC_MODEL_FLAGS.HAS_PUBLIC_SETTERS_TRUE); } if (hasOnlyGettersSetters && methodCount >= 2 && propertyCount > 0) { return AnemicModelViolation_1.AnemicModelViolation.create(className, filePath, layer, lineNumber, methodCount, propertyCount, rules_1.ANEMIC_MODEL_FLAGS.HAS_ONLY_GETTERS_SETTERS_TRUE, rules_1.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_1.AnemicModelViolation.create(className, filePath, layer, lineNumber, methodCount, propertyCount, rules_1.ANALYZER_DEFAULTS.HAS_ONLY_GETTERS_SETTERS, rules_1.ANALYZER_DEFAULTS.HAS_PUBLIC_SETTERS); } return null; } /** * Checks if method name is a getter pattern */ isGetterMethod(methodName) { return (methodName.startsWith("get") || methodName.startsWith("is") || methodName.startsWith("has")); } /** * Checks if method is a setter pattern */ isSetterMethod(methodName, _classBody) { return methodName.startsWith("set"); } /** * Checks if property declaration is actually a method signature */ isMethodSignature(propertyDeclaration) { return propertyDeclaration.includes("(") && propertyDeclaration.includes(")"); } /** * Gets line number for a position in code */ getLineNumber(code, position) { const lines = code.substring(0, position).split("\n"); return lines.length; } } exports.AnemicModelDetector = AnemicModelDetector; //# sourceMappingURL=AnemicModelDetector.js.map