@addon24/eslint-config
Version:
ESLint configuration rules for WorldOfTextcraft projects - Centralized configuration for all project types
112 lines (100 loc) • 4.5 kB
JavaScript
export default {
rules: {
"dto-annotation-property-consistency": {
meta: {
type: "problem",
docs: {
description: "Ensure consistency between DTO annotations and property optionality",
category: "Best Practices",
recommended: true,
},
messages: {
inconsistentOptionality: "Property '{{propName}}' has inconsistent optionality: {{annotation}} but property is {{propertyType}}",
inconsistentRequired: "Property '{{propName}}' has inconsistent required setting: {{annotation}} but property is {{propertyType}}",
},
schema: [],
},
create(context) {
return {
ClassDeclaration(node) {
const rawFilename = context.getFilename();
const filename = typeof rawFilename === 'string' ? rawFilename.replace(/\\/g, '/') : '';
// Nur für DTO-Dateien anwenden (auch in Test-Fixtures)
const isDtoPath = filename.toLowerCase().includes('/dto/');
const isDtoName = filename.endsWith('Dto.ts');
const isFixtureDto = filename.includes('/__tests__/fixtures/');
if (!(isDtoPath || isDtoName || isFixtureDto)) {
return;
}
// Durchlaufe alle Properties der Klasse
for (const member of node.body.body) {
if (member.type === 'PropertyDefinition') {
const propName = member.key.name;
const decorators = member.decorators || [];
// Debug-Logging entfernt für saubere JSON-Ausgabe
// Finde @IsOptional und @ApiProperty Decorators
let hasIsOptional = false;
let apiPropertyRequired = null;
for (const decorator of decorators) {
if (decorator.expression.type === 'CallExpression') {
const callee = decorator.expression.callee;
// Prüfe @IsOptional
if (callee.type === 'Identifier' && callee.name === 'IsOptional') {
hasIsOptional = true;
}
// Prüfe @ApiProperty
if (callee.type === 'Identifier' && callee.name === 'ApiProperty') {
const args = decorator.expression.arguments;
if (args.length > 0 && args[0].type === 'ObjectExpression') {
for (const prop of args[0].properties) {
if (prop.key.name === 'required' && prop.value.type === 'Literal') {
apiPropertyRequired = prop.value.value;
}
}
}
}
} else if (decorator.expression.type === 'Identifier') {
// Prüfe @IsOptional ohne Argumente
if (decorator.expression.name === 'IsOptional') {
hasIsOptional = true;
}
}
}
// Prüfe Property-Optionalität
const isPropertyOptional = member.optional || false;
const propertyType = isPropertyOptional ? 'optional (?)' : 'required';
// Prüfe Konsistenz zwischen @IsOptional und Property-Optionalität
if (hasIsOptional && !isPropertyOptional) {
context.report({
node: member,
messageId: 'inconsistentOptionality',
data: {
propName,
annotation: '@IsOptional()',
propertyType,
},
});
}
// Prüfe Konsistenz zwischen required: false/true und Property-Optionalität
if (apiPropertyRequired !== null) {
const expectedOptionality = !apiPropertyRequired;
if (expectedOptionality !== isPropertyOptional) {
context.report({
node: member,
messageId: 'inconsistentRequired',
data: {
propName,
annotation: `required: ${apiPropertyRequired}`,
propertyType,
},
});
}
}
}
}
},
};
},
},
},
};