Add custom ESLint rule that warns when class properties or constructor parameters use primitive types (number, string, boolean) where a value object exists in src/value-objects/. The rule auto-discovers value objects from the directory and maps property names (e.g. 'health' → 'Health') to suggest the correct type. Found 4 warnings on the codebase: - MagicalObject.ts: #health: number property - MagicalObject.ts: health: number constructor param - MagicalWeapon.ts: health: number constructor param - HealingObject.ts: health: number constructor param
148 lines
4.3 KiB
JavaScript
148 lines
4.3 KiB
JavaScript
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');
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|