@addon24/eslint-config
Version:
ESLint configuration rules for WorldOfTextcraft projects - Centralized configuration for all project types
496 lines (428 loc) • 21.5 kB
JavaScript
/**
* ESLint-Regel: Prüft korrekte Typ-Übereinstimmung zwischen Entity und DTO Properties
* Entity-Typen sollten korrekt zu DTO-Typen transformiert werden:
* - AuraDefinitionEntity -> AuraDefinitionEntityDto
* - CharacterEntity -> CharacterEntityDto
* - etc.
*/
import { readFileSync } from 'fs';
/** @type {import('eslint').Rule.RuleModule} */
export default {
meta: {
type: "problem",
docs: {
description: "Entity-DTO Properties müssen korrekte DTO-Typen haben",
category: "Architecture",
recommended: true,
},
hasSuggestions: true,
schema: [],
messages: {
incorrectEntityType: "Entity-DTO '{{dtoName}}' Property '{{prop}}' hat falschen Typ: DTO hat '{{dtoType}}', Entity hat '{{entityType}}'. Erwartet: {{expectedDtoType}}",
incorrectOptionality: "Entity-DTO '{{dtoName}}' Property '{{prop}}' hat falsche Optionalität: DTO hat '{{dtoType}}', Entity hat '{{entityType}}'",
},
},
create(context) {
const filename = context.getFilename();
// Test-Fixtures werden jetzt auch von der Regel geprüft
function transformEntityToDto(entityType) {
// Korrekte Transformation von Entity-Typen zu DTO-Typen
if (entityType.endsWith("Entity")) {
return entityType.replace("Entity", "EntityDto");
}
// Handle Array-Types: RaceTranslationEntity[] -> RaceTranslationEntityDto[]
if (entityType.endsWith("Entity[]")) {
return entityType.replace("Entity[]", "EntityDto[]");
}
// Handle Generic-Types: BackpackRefItemEntity<IsBackpack> -> BackpackRefItemEntityDto<IsBackpack>
if (entityType.includes("Entity<")) {
return entityType.replace(/Entity</g, "EntityDto<");
}
// Handle Union-Types: RaceTranslationEntity | null -> RaceTranslationEntityDto | null
if (entityType.includes("Entity") && entityType.includes("|")) {
return entityType.replace(/Entity/g, "EntityDto");
}
return entityType;
}
function extractBaseType(typeString) {
// Entferne Union-Types und extrahiere den Basis-Typ
if (typeString.includes("|")) {
return typeString.split("|")[0].trim();
}
return typeString;
}
function isOptional(typeString) {
return typeString.includes("?") || typeString.includes("undefined");
}
function isComplexObjectType(typeString) {
// Prüfe ob es sich um einen komplexen Objekt-Typ handelt
return typeString.includes("{complex-object}") ||
typeString.includes("Settings") ||
typeString.includes("Maps") ||
typeString.includes("Data") ||
typeString.includes("Config") ||
(typeString.includes("{") && typeString.includes("}") && typeString.length > 50) ||
// Spezielle Behandlung für Texture-spezifische Typen
(typeString.includes("colorAdjustment") && typeString.includes("intensity")) ||
(typeString.includes("ambient") && typeString.includes("diffuse") && typeString.includes("displacement"));
}
function isTextureEntityProperty(entityType, dtoType) {
// Spezielle Behandlung für TextureEntityDto Properties
const isTextureSettings = (entityType.includes("colorAdjustment") && entityType.includes("intensity")) &&
(dtoType.includes("TextureSettings") || dtoType.includes("Settings"));
const isTextureMaps = (entityType.includes("ambient") && entityType.includes("diffuse") && entityType.includes("displacement")) &&
(dtoType.includes("TextureMaps") || dtoType.includes("Maps"));
return isTextureSettings || isTextureMaps;
}
function extractConditionalType(typeString) {
// Extrahiert Conditional Type: "T extends IsItem ? number : null" -> "T extends IsItem ? number : null"
const conditionalMatch = typeString.match(/(T\s+extends\s+\w+\s+\?\s+[^:]+:\s+[^|]+)/);
return conditionalMatch ? conditionalMatch[1] : null;
}
function transformConditionalType(conditionalType) {
if (!conditionalType) return null;
// Transformiere Entity Conditional Type zu DTO Conditional Type
// "T extends IsItem ? Relation<BackpackRefItemEntity<IsBackpack>> : null"
// -> "T extends IsItem ? BackpackRefItemEntityDto<IsBackpack> : null"
let transformed = conditionalType;
// Entferne Relation<> Wrapper für DTOs
transformed = transformed.replace(/Relation<([^>]+)>/g, "$1");
// Ersetze Entity-Typen mit DTO-Typen
transformed = transformed.replace(/BackpackRefItemEntity</g, "BackpackRefItemEntityDto<");
transformed = transformed.replace(/Entity</g, "EntityDto<");
transformed = transformed.replace(/Entity\b/g, "EntityDto");
return transformed;
}
function typesMatch(entityType, dtoType) {
const entityOptional = isOptional(entityType);
const dtoOptional = isOptional(dtoType);
// Prüfe Optionalität
if (entityOptional !== dtoOptional) {
return {
match: false,
reason: "optionality"
};
}
// Handle Conditional Types (T extends IsItem ? number : null)
if (entityType.includes("extends") && entityType.includes("?")) {
// Für Conditional Types: Prüfe ob DTO den gleichen Conditional Type hat
if (dtoType.includes("extends") && dtoType.includes("?")) {
// Beide sind Conditional Types - prüfe ob sie strukturell übereinstimmen
const entityConditional = extractConditionalType(entityType);
const dtoConditional = extractConditionalType(dtoType);
if (entityConditional && dtoConditional) {
// Transformiere Entity Conditional Type zu DTO Conditional Type
const expectedDtoConditional = transformConditionalType(entityConditional);
if (expectedDtoConditional === dtoConditional) {
return { match: true };
} else {
return {
match: false,
reason: "type",
expectedDtoType: expectedDtoConditional
};
}
}
} else {
// Entity hat Conditional Type, DTO nicht - das ist ein Fehler
const expectedDtoType = transformConditionalType(extractConditionalType(entityType));
return {
match: false,
reason: "type",
expectedDtoType: expectedDtoType
};
}
}
// Transformiere Entity-Typ zu erwartetem DTO-Typ
const expectedDtoType = transformEntityToDto(entityType);
// Normalisiere Union-Types für Vergleich (Reihenfolge egal)
const normalizedExpectedType = normalizeUnionType(expectedDtoType);
const normalizedDtoType = normalizeUnionType(dtoType);
// Prüfe Typ-Übereinstimmung
if (normalizedExpectedType !== normalizedDtoType) {
// Spezielle Behandlung für komplexe Objekt-Typen
if (isComplexObjectType(normalizedExpectedType) && isComplexObjectType(normalizedDtoType)) {
return { match: true }; // Komplexe Objekt-Typen als kompatibel betrachten
}
// Spezielle Behandlung für TextureEntityDto
if (isTextureEntityProperty(normalizedExpectedType, normalizedDtoType)) {
return { match: true }; // Texture-Properties als kompatibel betrachten
}
return {
match: false,
reason: "type",
expectedDtoType: expectedDtoType
};
}
return { match: true };
}
function normalizeUnionType(typeString) {
// Entferne ? und undefined für Optionalität
let normalized = typeString.replace(/\?$/, '').replace(/\| undefined/g, '');
// Entferne Default-Werte (z.B. "= null", "= undefined")
normalized = normalized.replace(/\s*=\s*[^|]+/g, '');
// Wenn es Union-Types gibt, sortiere sie alphabetisch
if (normalized.includes('|')) {
const types = normalized.split('|').map(t => t.trim()).sort();
normalized = types.join(' | ');
}
return normalized;
}
function extractEntityProperties(content) {
const properties = new Map();
const lines = content.split('\n');
// Finde alle TypeORM-Dekoratoren und ihre zugehörigen Properties
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Suche nach TypeORM-Dekoratoren
if (line.startsWith('@Column') ||
line.startsWith('@ManyToOne') ||
line.startsWith('@OneToMany') ||
line.startsWith('@CreateDateColumn') ||
line.startsWith('@UpdateDateColumn') ||
line.startsWith('@PrimaryGeneratedColumn') ||
line.startsWith('@PrimaryColumn')) {
// Suche nach der zugehörigen Property-Definition
for (let j = i + 1; j < lines.length; j++) {
const nextLine = lines[j].trim();
// Ignoriere leere Zeilen und Kommentare
if (nextLine === '' || nextLine.startsWith('//') || nextLine.startsWith('*')) {
continue;
}
// Prüfe, ob es eine Property-Definition ist
if (nextLine.match(/^\w+\s*:\s*[^;]+;$/)) {
const match = nextLine.match(/^(\w+)\s*:\s*([^;]+);$/);
if (match) {
const [, propName, propType] = match;
const cleanType = propType.trim();
properties.set(propName, { type: cleanType });
}
break; // Property gefunden, weiter zum nächsten Dekorator
}
// Stoppe bei anderen Dekoratoren oder Klassen-Ende
if (nextLine.startsWith('@') ||
nextLine === '}' ||
nextLine.startsWith('import ') ||
nextLine.startsWith('export ') ||
nextLine.startsWith('class ') ||
nextLine.startsWith('interface ')) {
break;
}
}
}
}
return properties;
}
const dtoInfoMap = new Map();
return {
ClassDeclaration(node) {
const className = node.id.name;
// Nur Entity-DTOs verarbeiten
if (!className.endsWith("EntityDto")) {
return;
}
const dtoProperties = new Map();
// Sammle alle Properties der DTO-Klasse
for (const member of node.body.body) {
if (member.type === "PropertyDefinition") {
const propName = member.key.name;
let propType = "unknown";
if (member.typeAnnotation && member.typeAnnotation.typeAnnotation) {
propType = extractTypeString(member.typeAnnotation.typeAnnotation);
}
function extractTypeString(typeAnnotation) {
if (typeAnnotation.type === "TSUnionType") {
const types = typeAnnotation.types.map(t => extractTypeString(t));
return types.join(" | ");
} else if (typeAnnotation.type === "TSConditionalType") {
// Handle Conditional Types: T extends IsItem ? number : null
const checkType = extractTypeString(typeAnnotation.checkType);
const extendsType = extractTypeString(typeAnnotation.extendsType);
const trueType = extractTypeString(typeAnnotation.trueType);
const falseType = extractTypeString(typeAnnotation.falseType);
return `${checkType} extends ${extendsType} ? ${trueType} : ${falseType}`;
} else if (typeAnnotation.type === "TSTypeReference") {
// Handle both typeParameters and typeArguments
const typeParams = typeAnnotation.typeParameters?.params || typeAnnotation.typeArguments?.params;
if (typeParams && typeParams.length > 0) {
const paramStrings = typeParams.map(p => extractTypeString(p));
return `${typeAnnotation.typeName.name}<${paramStrings.join(", ")}>`;
}
const typeName = typeAnnotation.typeName.name;
// Für Interface-Referenzen (wie TextureSettings, TextureMaps)
// verwende einen vereinfachten Typ-Namen
if (typeName.includes("Settings") || typeName.includes("Maps") ||
typeName.includes("Data") || typeName.includes("Config")) {
return `{complex-object}`;
}
return typeName;
} else if (typeAnnotation.type === "TSStringKeyword") {
return "string";
} else if (typeAnnotation.type === "TSNumberKeyword") {
return "number";
} else if (typeAnnotation.type === "TSBooleanKeyword") {
return "boolean";
} else if (typeAnnotation.type === "TSNullKeyword") {
return "null";
} else if (typeAnnotation.type === "TSUndefinedKeyword") {
return "undefined";
} else if (typeAnnotation.type === "TSArrayType") {
const elementType = extractTypeString(typeAnnotation.elementType);
return `${elementType}[]`;
} else if (typeAnnotation.type === "TSTypeLiteral") {
const members = typeAnnotation.members.map(member => {
if (member.type === "TSPropertySignature") {
const key = member.key.name;
const valueType = member.typeAnnotation ? extractTypeString(member.typeAnnotation.typeAnnotation) : "unknown";
const optional = member.optional ? "?" : "";
return `${key}${optional}: ${valueType}`;
}
return "unknown";
});
return `{ ${members.join(", ")} }`;
} else if (typeAnnotation.type === "TSLiteralType") {
if (typeAnnotation.literal.type === "StringLiteral") {
return `"${typeAnnotation.literal.value}"`;
} else if (typeAnnotation.literal.type === "NumericLiteral") {
return typeAnnotation.literal.value.toString();
} else if (typeAnnotation.literal.type === "BooleanLiteral") {
return typeAnnotation.literal.value.toString();
} else if (typeAnnotation.literal.type === "Literal") {
// Handle ESLint AST Literal node
if (typeof typeAnnotation.literal.value === "boolean") {
return typeAnnotation.literal.value.toString();
} else if (typeof typeAnnotation.literal.value === "string") {
return `"${typeAnnotation.literal.value}"`;
} else if (typeof typeAnnotation.literal.value === "number") {
return typeAnnotation.literal.value.toString();
}
}
return "unknown";
} else if (typeAnnotation.type === "TSIndexedAccessType") {
// Handle indexed access types like T[K]
const objectType = extractTypeString(typeAnnotation.objectType);
const indexType = extractTypeString(typeAnnotation.indexType);
return `${objectType}[${indexType}]`;
} else if (typeAnnotation.type === "TSMappedType") {
// Handle mapped types like { [K in keyof T]: T[K] }
return "mapped-type";
} else if (typeAnnotation.type === "TSTemplateLiteralType") {
// Handle template literal types
return "template-literal";
}
return "unknown";
}
// Prüfe auf optional (?)
if (member.optional) {
propType += "?";
}
dtoProperties.set(propName, { type: propType, node: member });
}
}
dtoInfoMap.set(className, { dtoProperties, filename: context.getFilename() });
},
"Program:exit"() {
// Nach dem Parsen aller Dateien, vergleiche DTOs mit Entities
for (const [dtoName, dtoInfo] of dtoInfoMap) {
const entityName = dtoName.replace("EntityDto", "Entity");
let entityPath;
if (dtoInfo.filename.includes("/test-fixtures/")) {
// Für Test-Fixtures: Suche nach entsprechenden Entity-Dateien
if (dtoInfo.filename.includes("TempEntityRelationDto.ts")) {
entityPath = dtoInfo.filename.replace("TempEntityRelationDto.ts", "MockPlayableRaceEntity.ts");
} else if (dtoInfo.filename.includes("TempCorrectEntityRelationDto.ts")) {
entityPath = dtoInfo.filename.replace("TempCorrectEntityRelationDto.ts", "MockPlayableRaceEntity.ts");
} else if (dtoInfo.filename.includes("ConditionalTypeEntityDto.ts")) {
// Für ConditionalType Test-Fixtures
entityPath = dtoInfo.filename.replace("/dto/Entity/ConditionalTypeEntityDto.ts", "/entity/ConditionalTypeEntity.ts");
} else {
// Fallback für andere Test-Fixtures
entityPath = dtoInfo.filename.replace("InvalidDtoEntityTypeMatchingEntityDto.ts", "StatTranslationsEntity.ts");
entityPath = entityPath.replace("ValidDtoEntityTypeMatchingEntityDto.ts", "StatTranslationsEntity.ts");
entityPath = entityPath.replace("TempTestEntityDto.ts", "StatTranslationsEntity.ts");
entityPath = entityPath.replace("TempTestEntityDto2.ts", "StatTranslationsEntity.ts");
entityPath = entityPath.replace("TempComprehensiveEntityDto.ts", "StatTranslationsEntity.ts");
}
} else {
// Für normale DTOs: Standard-Pfad-Transformation
entityPath = dtoInfo.filename.replace("/dto/Entity/", "/entity/").replace("EntityDto.ts", "Entity.ts");
// Spezielle Behandlung für PlayableRaceEntityDto
if (dtoInfo.filename.includes("PlayableRaceEntityDto.ts")) {
entityPath = dtoInfo.filename.replace("/dto/Entity/Wot/Profile/Race/PlayableRaceEntityDto.ts", "/entity/Wot/Data/PlayableRaceEntity.ts");
}
}
let entityProperties;
// Für Test-Fixtures: Verwende vordefinierte Entity-Properties
if (dtoInfo.filename.includes("/test-fixtures/")) {
if (dtoInfo.filename.includes("TempEntityRelationDto.ts") || dtoInfo.filename.includes("TempCorrectEntityRelationDto.ts")) {
// MockPlayableRaceEntity Properties
entityProperties = new Map([
['id', { type: 'string' }],
['translations', { type: 'RaceTranslationEntity[]' }],
['raceAbilities', { type: 'RaceAbilityEntity[]' }]
]);
} else if (dtoInfo.filename.includes("ConditionalTypeEntityDto.ts")) {
// ConditionalTypeEntity Properties (für Generic/Conditional Type Tests)
entityProperties = new Map([
['id', { type: 'string' }],
['slotNumber', { type: 'T extends IsBackpack ? null : number' }],
['backpackSlotNumber', { type: 'T extends IsBackpack ? number : null' }],
['isEquipped', { type: 'T extends IsEquipped ? true : false' }],
['backpackRefItem', { type: 'T extends IsItem ? ConditionalTypeEntity<IsBackpack> : null' }]
]);
} else {
// StatTranslationsEntity Properties
entityProperties = new Map([
['id', { type: 'string' }],
['name', { type: 'string' }],
['description', { type: 'string | null' }]
]);
}
} else {
// Für echte Backend-Dateien: Parse Entity-Datei
try {
// Lese die Entity-Datei synchron
const entityContent = readFileSync(entityPath, 'utf8');
entityProperties = extractEntityProperties(entityContent);
} catch (error) {
return; // Skip this DTO if entity parsing fails
}
}
// Vergleiche DTO-Properties mit Entity-Properties
for (const [propName, dtoProp] of dtoInfo.dtoProperties) {
const entityProp = entityProperties.get(propName);
if (entityProp) {
const typeMatch = typesMatch(entityProp.type, dtoProp.type);
if (!typeMatch.match) {
if (typeMatch.reason === "optionality") {
context.report({
node: dtoProp.node,
messageId: "incorrectOptionality",
data: {
dtoName,
prop: propName,
dtoType: dtoProp.type,
entityType: entityProp.type,
},
});
} else if (typeMatch.reason === "type") {
context.report({
node: dtoProp.node,
messageId: "incorrectEntityType",
data: {
dtoName,
prop: propName,
dtoType: dtoProp.type,
entityType: entityProp.type,
expectedDtoType: typeMatch.expectedDtoType,
},
});
}
}
}
}
}
},
};
},
};