@addon24/eslint-config
Version:
ESLint configuration rules for WorldOfTextcraft projects - Centralized configuration for all project types
262 lines (226 loc) • 8.71 kB
JavaScript
export default {
rules: {
"enforce-dto-create-pattern": {
meta: {
type: "problem",
docs: {
description: "Enforce strict DTO create method pattern: no Object.assign, explicit property assignment, all properties required.",
category: "Best Practices",
recommended: true,
},
fixable: null,
schema: [],
messages: {
noObjectAssignInCreate: "Object.assign is not allowed in create methods. Use explicit property assignment instead.",
mustUseNewInstance: "Create methods must use 'const dto = new ClassName();' pattern.",
missingPropertyAssignment: "Property '{{propertyName}}' is not assigned in create method. All properties must be explicitly assigned.",
invalidCreateMethodStructure: "Create method must follow strict pattern: const dto = new ClassName(); followed by explicit property assignments.",
},
},
create(context) {
const EXEMPT_DTOS = [
"JwtOptionsDto",
];
function getClassName(context, node) {
// Find the class declaration that contains this method
let current = node;
while (current && current.type !== "ClassDeclaration") {
current = current.parent;
}
return current ? current.id.name : null;
}
function isExemptDto(className) {
return EXEMPT_DTOS.includes(className);
}
function checkObjectAssignUsage(context, body) {
for (const statement of body.body) {
if (statement.type === "VariableDeclaration") {
for (const declarator of statement.declarations) {
if (
declarator.init &&
declarator.init.type === "CallExpression" &&
declarator.init.callee &&
declarator.init.callee.type === "MemberExpression" &&
declarator.init.callee.object &&
declarator.init.callee.object.name === "Object" &&
declarator.init.callee.property &&
declarator.init.callee.property.name === "assign"
) {
context.report({
node: declarator,
messageId: "noObjectAssignInCreate",
});
}
}
}
}
}
function checkNewInstancePattern(context, body, className) {
let foundNewInstance = false;
let foundConstDto = false;
for (const statement of body.body) {
if (statement.type === "VariableDeclaration") {
for (const declarator of statement.declarations) {
if (
declarator.id.name === "dto" &&
declarator.init &&
declarator.init.type === "NewExpression" &&
declarator.init.callee &&
declarator.init.callee.name === className
) {
foundNewInstance = true;
foundConstDto = true;
break;
}
}
}
}
if (!foundConstDto) {
context.report({
node: body,
messageId: "mustUseNewInstance",
});
}
if (!foundNewInstance) {
context.report({
node: body,
messageId: "invalidCreateMethodStructure",
});
}
}
function checkPropertyAssignments(context, body, className) {
// Get all class properties from the file
const classProperties = getClassProperties(context, className);
if (classProperties.length === 0) {
return;
}
const assignedProperties = new Set();
// Find all property assignments in the create method
for (const statement of body.body) {
if (statement.type === "ExpressionStatement" && statement.expression.type === "AssignmentExpression") {
const assignment = statement.expression;
if (
assignment.left.type === "MemberExpression" &&
assignment.left.object &&
assignment.left.object.name === "dto" &&
assignment.left.property &&
assignment.left.property.type === "Identifier"
) {
assignedProperties.add(assignment.left.property.name);
}
}
}
// Check for missing property assignments
for (const property of classProperties) {
if (!assignedProperties.has(property)) {
context.report({
node: body,
messageId: "missingPropertyAssignment",
data: { propertyName: property },
});
}
}
}
function getClassProperties(context, className) {
const sourceCode = context.getSourceCode();
const classDeclaration = findClassDeclaration(sourceCode, className);
if (!classDeclaration) {
return [];
}
const properties = [];
for (const member of classDeclaration.body.body) {
if (member.type === "PropertyDefinition" && member.key && member.key.type === "Identifier") {
// Skip static methods
if (member.static) {
continue;
}
properties.push(member.key.name);
}
}
return properties;
}
function findClassDeclaration(sourceCode, className) {
const ast = sourceCode.ast;
const visited = new WeakSet();
function findClass(node) {
if (!node || typeof node !== "object" || visited.has(node)) {
return null;
}
visited.add(node);
if (node.type === "ClassDeclaration" && node.id && node.id.name === className) {
return node;
}
for (const key in node) {
if (node[key] && typeof node[key] === "object") {
if (Array.isArray(node[key])) {
for (const child of node[key]) {
const result = findClass(child);
if (result) return result;
}
} else {
const result = findClass(node[key]);
if (result) return result;
}
}
}
return null;
}
return findClass(ast);
}
return {
ClassMethod(node) {
// Check if this is a static create method
if (
node.static &&
node.key.name === "create" &&
node.body &&
node.body.type === "BlockStatement"
) {
const className = getClassName(context, node);
if (!className || !className.endsWith("Dto")) {
return;
}
// Skip exempt DTOs
if (isExemptDto(className)) {
return;
}
const body = node.body;
// Check for Object.assign usage
checkObjectAssignUsage(context, body);
// Check for proper new instance pattern
checkNewInstancePattern(context, body, className);
// Check for explicit property assignments
checkPropertyAssignments(context, body, className);
}
},
MethodDefinition(node) {
// Check if this is a static create method
if (
node.static &&
node.key.name === "create" &&
node.value &&
node.value.body &&
node.value.body.type === "BlockStatement"
) {
const className = getClassName(context, node);
if (!className || !className.endsWith("Dto")) {
return;
}
// Skip exempt DTOs
if (isExemptDto(className)) {
return;
}
const body = node.value.body;
// Check for Object.assign usage
checkObjectAssignUsage(context, body);
// Check for proper new instance pattern
checkNewInstancePattern(context, body, className);
// Check for explicit property assignments
checkPropertyAssignments(context, body, className);
}
},
};
},
},
},
};