UNPKG

@addon24/eslint-config

Version:

ESLint configuration rules for WorldOfTextcraft projects - Centralized configuration for all project types

322 lines (266 loc) 10.9 kB
/** * @fileoverview Stellt sicher, dass alle required Properties einer Entity beim Erstellen einer Instanz gesetzt werden * Diese Regel analysiert Entity-Klassen und prüft, ob bei `new EntityName()` alle nicht-optionalen Properties zugewiesen werden */ import fs from "fs"; import path from "path"; /** @type {import('eslint').Rule.RuleModule} */ const entityRequiredPropertiesRule = { meta: { type: "problem", docs: { description: "Stellt sicher, dass alle required Properties einer Entity beim Erstellen einer Instanz gesetzt werden", category: "TypeORM", recommended: true, }, hasSuggestions: true, schema: [], messages: { missingRequiredProperties: "Entity '{{entityName}}' Instanz fehlen required Properties: {{missingProps}}. Diese müssen nach 'new {{entityName}}()' gesetzt werden.", optionalPropertyInfo: "Optional properties für '{{entityName}}': {{optionalProps}}", }, }, create(context) { const sourceCode = context.getSourceCode(); const filename = context.getFilename(); // Nur Service-Dateien und Controller prüfen (dort werden meist Entities erstellt) if (!filename.includes("/service/") && !filename.includes("/controller/")) { return {}; } /** * Analysiert Entity-Klasse und extrahiert required/optional Properties (String-basiert) */ function analyzeEntityClass(entityFilePath) { const required = []; const optional = []; try { const content = fs.readFileSync(entityFilePath, "utf8"); // Finde Klassen-Definition const classMatch = content.match(/export\s+class\s+(\w+Entity)/); if (!classMatch) { return { required, optional }; } const className = classMatch[1]; // Teile Content in Zeilen und analysiere Properties const lines = content.split('\n'); let insideClass = false; let currentDecorators = []; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Starte bei Klassendefinition if (line.includes(`class ${className}`)) { insideClass = true; continue; } if (!insideClass) continue; // Ende der Klasse if (line === '}') { break; } // Decorator erkennen (kann mehrzeilig sein) if (line.startsWith('@')) { const decoratorMatch = line.match(/@(\w+)/); if (decoratorMatch) { let decoratorContent = line; // Wenn Decorator mehrzeilig ist, sammle alle Zeilen bis zur schließenden Klammer let j = i + 1; let openBraces = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length; let openParens = (line.match(/\(/g) || []).length - (line.match(/\)/g) || []).length; while (j < lines.length && (openBraces > 0 || openParens > 0)) { const nextLine = lines[j].trim(); decoratorContent += " " + nextLine; openBraces += (nextLine.match(/\{/g) || []).length - (nextLine.match(/\}/g) || []).length; openParens += (nextLine.match(/\(/g) || []).length - (nextLine.match(/\)/g) || []).length; j++; } currentDecorators.push({ name: decoratorMatch[1], full: decoratorContent }); // Springe über verarbeitete Zeilen i = j - 1; } continue; } // Property erkennen - nur echte Klassenmember const propertyMatch = line.match(/^(\w+)(\?)?\s*:\s*([^;{]+);?\s*$/); if (propertyMatch && !line.includes('type:') && !line.includes('length:')) { const propertyName = propertyMatch[1]; const isOptional = !!propertyMatch[2]; // hat ? const propertyType = propertyMatch[3]; const isRequired = analyzePropertyRequirement(propertyName, isOptional, propertyType, currentDecorators); if (isRequired) { required.push(propertyName); } else { optional.push(propertyName); } // Reset für nächstes Property currentDecorators = []; } } } catch (error) { // Bei Fehlern keine Warnungen } return { required, optional }; } /** * Analysiert ob ein Property required ist (String-basiert) */ function analyzePropertyRequirement(propertyName, isOptional, propertyType, decorators) { // TypeScript optional marker if (isOptional) { return false; } // Nur Properties mit Decorators überwachen if (decorators.length === 0) { return false; } // Automatische Properties const autoGeneratedDecorators = ["PrimaryGeneratedColumn", "CreateDateColumn", "UpdateDateColumn"]; if (decorators.some(d => autoGeneratedDecorators.includes(d.name))) { return false; } // @Column mit nullable oder default const columnDecorator = decorators.find(d => d.name === "Column"); if (columnDecorator) { const hasNullable = columnDecorator.full.includes("nullable: true"); const hasDefault = columnDecorator.full.includes("default:") || columnDecorator.full.includes("default {"); if (hasNullable || hasDefault) { return false; } return true; // Column ohne nullable/default ist required } // Relationen const relationDecorators = ["ManyToOne", "OneToOne", "OneToMany", "ManyToMany"]; const relationDecorator = decorators.find(d => relationDecorators.includes(d.name)); if (relationDecorator) { const hasNullable = relationDecorator.full.includes("nullable: true"); const hasNullType = propertyType.includes("| null"); if (hasNullable || hasNullType) { return false; } return true; // Relation ohne nullable ist required } // Andere Decorators (JoinColumn, etc.) sind optional return false; } /** * Findet Entity-Datei basierend auf Import */ function findEntityFile(entityName) { const currentDir = path.dirname(filename); const projectRoot = path.resolve(currentDir, "../../../.."); // Rekursive Dateisuche ohne externe Dependencies function findFileRecursively(dir, fileName) { try { const files = fs.readdirSync(dir); for (const file of files) { const fullPath = path.join(dir, file); const stat = fs.statSync(fullPath); if (stat.isFile() && file === fileName) { return fullPath; } else if (stat.isDirectory()) { const found = findFileRecursively(fullPath, fileName); if (found) return found; } } } catch { // Verzeichnis nicht zugänglich, weiter } return null; } // Suche in entity-Verzeichnissen const entitySearchDirs = [ path.resolve(projectRoot, "src", "entity"), path.resolve(currentDir, "../../../entity"), path.resolve(currentDir, "../../../../entity"), ]; for (const searchDir of entitySearchDirs) { const found = findFileRecursively(searchDir, `${entityName}.ts`); if (found) { return found; } } return null; } /** * Sammelt alle Property-Zuweisungen nach einer Entity-Instanziierung */ function collectPropertyAssignments(entityVariableName, startNode) { const assignments = new Set(); const scope = sourceCode.getScope(startNode); // Durchsuche nachfolgende Statements für Property-Zuweisungen let parent = startNode.parent; while (parent && parent.type !== "BlockStatement") { parent = parent.parent; } if (parent && parent.body) { const startIndex = parent.body.indexOf(startNode.parent); const followingStatements = parent.body.slice(startIndex + 1); followingStatements.forEach(statement => { if (statement.type === "ExpressionStatement" && statement.expression.type === "AssignmentExpression") { const left = statement.expression.left; if (left.type === "MemberExpression" && left.object?.name === entityVariableName) { assignments.add(left.property?.name); } } }); } return assignments; } return { VariableDeclarator(node) { // Prüfe auf `new EntityName()` Pattern if (node.init?.type === "NewExpression") { const entityName = node.init.callee?.name; // Prüfe ob es eine Entity ist (Name endet mit "Entity") if (entityName && entityName.endsWith("Entity")) { const variableName = node.id?.name; if (variableName) { // Finde Entity-Datei und analysiere required Properties const entityFile = findEntityFile(entityName); if (entityFile) { const { required, optional } = analyzeEntityClass(entityFile); if (required.length > 0) { // Sammle Property-Zuweisungen nach der Instanziierung const assignments = collectPropertyAssignments(variableName, node); // Finde fehlende required Properties const missingProperties = required.filter(prop => !assignments.has(prop)); if (missingProperties.length > 0) { context.report({ node: node.init, messageId: "missingRequiredProperties", data: { entityName, missingProps: missingProperties.join(", ") }, suggest: [ { desc: `Add missing required properties for ${entityName}`, fix(fixer) { // Erstelle Vorschlag für fehlende Properties const suggestions = missingProperties.map(prop => `${variableName}.${prop} = /* TODO: set value */;` ).join("\n"); return fixer.insertTextAfter(node.parent, `\n${suggestions}`); } } ] }); } } } } } } } }; }, }; export default { rules: { "entity-required-properties": entityRequiredPropertiesRule, }, };