@addon24/eslint-config
Version:
ESLint configuration rules for WorldOfTextcraft projects - Centralized configuration for all project types
225 lines (197 loc) • 7.43 kB
JavaScript
/**
* @fileoverview TypeORM Column Type Validation Rule
* Stellt sicher, dass @Column Decorators explizite Type-Definitionen haben
* Verhindert ColumnTypeUndefinedError zur Laufzeit
*/
/** @type {import('eslint').Rule.RuleModule} */
const typeormColumnTypeValidationRule = {
meta: {
type: "problem",
docs: {
description: "Ensure @Column decorators have explicit type definitions to prevent ColumnTypeUndefinedError",
category: "TypeORM",
recommended: true,
},
hasSuggestions: true,
schema: [],
messages: {
missingColumnType: "@Column decorator for '{{propertyName}}' must have explicit type definition. Add type parameter: @Column({ type: '{{suggestedType}}' })",
missingColumnTypeGeneric: "@Column decorator for '{{propertyName}}' must have explicit type definition. Add type parameter: @Column({ type: '{{suggestedType}}' }{{additionalOptions}})",
useCreateDateColumn: "Consider using @CreateDateColumn() instead of @Column() for '{{propertyName}}'",
useUpdateDateColumn: "Consider using @UpdateDateColumn() instead of @Column() for '{{propertyName}}'",
},
},
create(context) {
const filename = context.getFilename();
const isEntityFile = filename.includes("/entity/") && filename.endsWith(".ts") && !filename.includes("test");
if (!isEntityFile) return {};
/**
* Schlägt einen TypeORM-Typ basierend auf TypeScript-Typ vor
*/
function suggestTypeormType(tsType) {
const typeMap = {
"string": "varchar",
"number": "int",
"boolean": "boolean",
"Date": "timestamp",
"Date | null": "timestamp",
"string | null": "varchar",
"number | null": "int",
"boolean | null": "boolean",
};
return typeMap[tsType] || "varchar";
}
/**
* Prüft ob ein Property ein Datum ist
*/
function isDateProperty(propertyName, tsType) {
const datePatterns = ["createdAt", "updatedAt", "deletedAt", "modifiedAt"];
return datePatterns.some(pattern =>
propertyName.toLowerCase().includes(pattern.toLowerCase())
) || tsType === "Date" || tsType === "Date | null";
}
/**
* Schlägt den passenden Decorator für Datum-Properties vor
*/
function suggestDateDecorator(propertyName) {
if (propertyName.toLowerCase().includes("created")) {
return "CreateDateColumn";
}
if (propertyName.toLowerCase().includes("updated") || propertyName.toLowerCase().includes("modified")) {
return "UpdateDateColumn";
}
return null;
}
/**
* Extrahiert TypeScript-Typ aus Property-Definition
*/
function extractTypeScriptType(node) {
if (!node.typeAnnotation?.typeAnnotation) return "unknown";
const sourceCode = context.getSourceCode();
const typeText = sourceCode.getText(node.typeAnnotation.typeAnnotation);
// Entferne Whitespace und vereinfache
return typeText.replace(/\s+/g, " ").trim();
}
/**
* Prüft ob @Column Decorator Argumente hat
*/
function hasColumnArguments(decorator) {
return decorator.expression.arguments && decorator.expression.arguments.length > 0;
}
/**
* Prüft ob @Column Decorator einen type-Parameter hat
*/
function hasTypeParameter(decorator) {
if (!hasColumnArguments(decorator)) return false;
const args = decorator.expression.arguments[0];
if (args.type !== "ObjectExpression") return false;
return args.properties.some(prop =>
prop.type === "Property" &&
prop.key.name === "type"
);
}
return {
PropertyDefinition(node) {
if (!node.decorators || node.decorators.length === 0) return;
const columnDecorator = node.decorators.find(decorator =>
decorator.expression.callee?.name === "Column"
);
if (!columnDecorator) return;
const propertyName = node.key.name;
const tsType = extractTypeScriptType(node);
// Prüfe ob @Column() ohne Argumente verwendet wird
if (!hasColumnArguments(columnDecorator)) {
const suggestedType = suggestTypeormType(tsType);
const dateDecorator = suggestDateDecorator(propertyName);
if (dateDecorator) {
context.report({
node: columnDecorator,
messageId: dateDecorator === "CreateDateColumn" ? "useCreateDateColumn" : "useUpdateDateColumn",
data: { propertyName },
suggest: [
{
desc: `Replace with @${dateDecorator}()`,
fix(fixer) {
return fixer.replaceText(
columnDecorator.expression,
`${dateDecorator}()`
);
}
}
]
});
} else {
context.report({
node: columnDecorator,
messageId: "missingColumnType",
data: {
propertyName,
suggestedType
},
suggest: [
{
desc: `Add type parameter to @Column`,
fix(fixer) {
return fixer.replaceText(
columnDecorator.expression,
`Column({ type: '${suggestedType}' })`
);
}
}
]
});
}
return;
}
// Prüfe ob @Column({ ... }) ohne type-Parameter verwendet wird
if (!hasTypeParameter(columnDecorator)) {
const suggestedType = suggestTypeormType(tsType);
const args = columnDecorator.expression.arguments[0];
// Erstelle zusätzliche Optionen für den Fix
let additionalOptions = "";
if (args.type === "ObjectExpression" && args.properties.length > 0) {
const existingProps = args.properties
.filter(prop => prop.key.name !== "type")
.map(prop => {
const sourceCode = context.getSourceCode();
return sourceCode.getText(prop);
})
.join(", ");
if (existingProps) {
additionalOptions = `, ${existingProps}`;
}
}
context.report({
node: columnDecorator,
messageId: "missingColumnTypeGeneric",
data: {
propertyName,
suggestedType,
additionalOptions
},
suggest: [
{
desc: `Add type parameter to @Column`,
fix(fixer) {
const sourceCode = context.getSourceCode();
const decoratorText = sourceCode.getText(columnDecorator.expression);
// Füge type-Parameter am Anfang der Objekt-Argumente hinzu
const newDecoratorText = decoratorText.replace(
/Column\(\s*\{/,
`Column({ type: '${suggestedType}'`
);
return fixer.replaceText(columnDecorator.expression, newDecoratorText);
}
}
]
});
}
}
};
},
};
export default {
rules: {
"typeorm-column-type-validation": typeormColumnTypeValidationRule,
},
};