do to formatters
This commit is contained in:
138
apps/website/eslint-rules/formatter-rules.js
Normal file
138
apps/website/eslint-rules/formatter-rules.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* ESLint rules for Formatter/Display Guardrails
|
||||
*
|
||||
* Enforces boundaries and purity for Formatters and Display Objects
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
// Rule 1: No IO in formatters/displays
|
||||
'no-io-in-display-objects': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid IO imports in formatters and displays',
|
||||
category: 'Formatters',
|
||||
},
|
||||
messages: {
|
||||
message: 'Formatters/Displays cannot import from api, services, page-queries, or view-models - see apps/website/lib/contracts/formatters/Formatter.ts',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const forbiddenPaths = [
|
||||
'@/lib/api/',
|
||||
'@/lib/services/',
|
||||
'@/lib/page-queries/',
|
||||
'@/lib/view-models/',
|
||||
'@/lib/presenters/',
|
||||
];
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
if (forbiddenPaths.some(path => importPath.includes(path)) &&
|
||||
!isInComment(node)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 2: No non-class display exports
|
||||
'no-non-class-display-exports': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Forbid non-class exports in formatters and displays',
|
||||
category: 'Formatters',
|
||||
},
|
||||
messages: {
|
||||
message: 'Formatters and Displays must be class-based and export only classes - see apps/website/lib/contracts/formatters/Formatter.ts',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ExportNamedDeclaration(node) {
|
||||
if (node.declaration &&
|
||||
(node.declaration.type === 'FunctionDeclaration' ||
|
||||
(node.declaration.type === 'VariableDeclaration' &&
|
||||
!node.declaration.declarations.some(d => d.init && d.init.type === 'ClassExpression')))) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
ExportDefaultDeclaration(node) {
|
||||
if (node.declaration &&
|
||||
node.declaration.type !== 'ClassDeclaration' &&
|
||||
node.declaration.type !== 'ClassExpression') {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
// Rule 3: Formatters must return primitives
|
||||
'formatters-must-return-primitives': {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce that Formatters return primitive values for ViewData compatibility',
|
||||
category: 'Formatters',
|
||||
},
|
||||
messages: {
|
||||
message: 'Formatters used in ViewDataBuilders must return primitive values (string, number, boolean, null) - see apps/website/lib/contracts/formatters/Formatter.ts',
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
const isViewDataBuilder = filename.includes('/lib/builders/view-data/');
|
||||
|
||||
if (!isViewDataBuilder) return {};
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
// Check if calling a Formatter/Display method
|
||||
if (node.callee.type === 'MemberExpression' &&
|
||||
node.callee.object.name &&
|
||||
(node.callee.object.name.endsWith('Formatter') || node.callee.object.name.endsWith('Display'))) {
|
||||
|
||||
// If it's inside a ViewData object literal, it must be a primitive return
|
||||
let parent = node.parent;
|
||||
while (parent) {
|
||||
if (parent.type === 'Property' && parent.parent.type === 'ObjectExpression') {
|
||||
// This is a property in an object literal (likely ViewData)
|
||||
// We can't easily check the return type of the method at lint time without type info,
|
||||
// but we can enforce that it's not the whole object being assigned.
|
||||
if (node.callee.property.name === 'format' || node.callee.property.name.startsWith('format')) {
|
||||
// Good: calling a format method
|
||||
return;
|
||||
}
|
||||
|
||||
// If they are assigning the result of a non-format method, warn
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'message',
|
||||
});
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Helper functions
|
||||
function isInComment(node) {
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user