UNPKG

@addon24/eslint-config

Version:

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

496 lines (428 loc) 21.5 kB
/** * 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, }, }); } } } } } }, }; }, };