@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
JavaScript
;
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