feat(eslint): add no-primitive-value-properties rule to enforce value objects
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
This commit is contained in:
parent
dea77d463f
commit
0540e5ff5b
@ -1,6 +1,7 @@
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
import noPrimitiveValue from './eslint/no-primitive-value-properties.js';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist', 'node_modules', 'coverage', 'allium-main'] },
|
||||
@ -17,9 +18,17 @@ export default tseslint.config(
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
rpg: {
|
||||
rules: {
|
||||
'no-primitive-value-properties': noPrimitiveValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/switch-exhaustiveness-check': 'warn',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'warn',
|
||||
'rpg/no-primitive-value-properties': 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
147
eslint/no-primitive-value-properties.js
Normal file
147
eslint/no-primitive-value-properties.js
Normal file
@ -0,0 +1,147 @@
|
||||
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');
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user