import { readdirSync } from 'node:fs'; import { join, parse } from 'node:path'; const VALUE_OBJECTS_DIR = join(import.meta.dirname, '..', 'src', 'value-objects'); function discoverValueObjects() { const files = readdirSync(VALUE_OBJECTS_DIR).filter((f) => f.endsWith('.ts') && f !== 'index.ts'); return files.map((f) => parse(f).name); } const VALUE_OBJECTS = discoverValueObjects(); function buildPropertyMap() { const map = {}; for (const name of VALUE_OBJECTS) { const propertyName = name.charAt(0).toLowerCase() + name.slice(1); map[propertyName] = name; } return map; } const PROPERTY_MAP = buildPropertyMap(); /** Map ESLint AST type names to the primitive keyword string. */ const PRIMITIVE_TYPE_MAP = { TSNumberKeyword: 'number', TSStringKeyword: 'string', TSBooleanKeyword: 'boolean', }; function isPrimitiveType(typeNode) { if (!typeNode) return false; const keyword = PRIMITIVE_TYPE_MAP[typeNode.type]; return keyword !== undefined; } function getPrimitiveKeyword(typeNode) { if (!typeNode) return null; return PRIMITIVE_TYPE_MAP[typeNode.type] || null; } function getParamName(param) { if (param.type === 'TSParameterProperty') { return getParamName(param.parameter); } if (param.type === 'Identifier') { return param.name; } return null; } export default { meta: { type: 'suggestion', docs: { description: 'Warn when primitive types are used in class properties or constructor parameters that should be value objects', recommended: false, }, schema: [ { type: 'object', properties: { includeConstructorParams: { type: 'boolean', default: true }, includeProperties: { type: 'boolean', default: true }, }, additionalProperties: false, }, ], messages: { primitiveProperty: 'Use value object "{{valueObject}}" instead of primitive "{{primitive}}" for property "{{propertyName}}". Available: {{available}}', primitiveParam: 'Use value object "{{valueObject}}" instead of primitive "{{primitive}}" for parameter "{{paramName}}". Available: {{available}}', }, }, create(context) { const options = context.options[0] || {}; const includeConstructorParams = options.includeConstructorParams !== false; const includeProperties = options.includeProperties !== false; function reportPrimitive(node, propName, primitiveType, messageId) { const valueObject = PROPERTY_MAP[propName]; if (valueObject) { context.report({ node, messageId, data: { valueObject, primitive: primitiveType, propertyName: propName, paramName: propName, available: VALUE_OBJECTS.join(', '), }, }); } } return { ClassProperty(node) { if (!includeProperties) return; if (!node.typeAnnotation) return; const typeNode = node.typeAnnotation.typeAnnotation; if (!isPrimitiveType(typeNode)) return; reportPrimitive(node, node.key.name, getPrimitiveKeyword(typeNode), 'primitiveProperty'); }, PropertyDefinition(node) { if (!includeProperties) return; if (!node.typeAnnotation) return; const typeNode = node.typeAnnotation.typeAnnotation; if (!isPrimitiveType(typeNode)) return; reportPrimitive(node, node.key.name, getPrimitiveKeyword(typeNode), 'primitiveProperty'); }, MethodDefinition(node) { if (!includeConstructorParams) return; if (node.key.name !== 'constructor') return; if (!node.value.params) return; for (const param of node.value.params) { let paramName = null; let typeNode = null; if (param.type === 'TSParameterProperty') { paramName = getParamName(param.parameter); typeNode = param.parameter.typeAnnotation?.typeAnnotation; } else if (param.type === 'Identifier') { paramName = param.name; typeNode = param.typeAnnotation?.typeAnnotation; } if (!paramName || !typeNode || !isPrimitiveType(typeNode)) continue; reportPrimitive(param, paramName, getPrimitiveKeyword(typeNode), 'primitiveParam'); } }, }; }, };