@addon24/eslint-config
Version:
ESLint configuration rules for WorldOfTextcraft projects - Centralized configuration for all project types
322 lines (266 loc) • 10.9 kB
JavaScript
/**
* @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,
},
};