125 lines
3.8 KiB
JavaScript
125 lines
3.8 KiB
JavaScript
/**
|
|
* ESLint rule to enforce Presenter contract
|
|
*
|
|
* Enforces that classes ending with "Presenter" must:
|
|
* 1. Implement Presenter<TInput, TOutput> interface
|
|
* 2. Have a present(input) method
|
|
* 3. Have 'use client' directive
|
|
*/
|
|
|
|
module.exports = {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Enforce Presenter contract implementation',
|
|
category: 'Best Practices',
|
|
recommended: true,
|
|
},
|
|
fixable: null,
|
|
schema: [],
|
|
messages: {
|
|
missingImplements: 'Presenter class must implement Presenter<TInput, TOutput> interface',
|
|
missingPresentMethod: 'Presenter class must have present(input) method',
|
|
missingUseClient: 'Presenter must have \'use client\' directive at top-level',
|
|
},
|
|
},
|
|
|
|
create(context) {
|
|
const sourceCode = context.getSourceCode();
|
|
let hasUseClient = false;
|
|
let presenterClassNode = null;
|
|
let hasPresentMethod = false;
|
|
let hasImplements = false;
|
|
|
|
return {
|
|
// Check for 'use client' directive
|
|
Program(node) {
|
|
// Check comments at the top
|
|
const comments = sourceCode.getAllComments();
|
|
if (comments.length > 0) {
|
|
const firstComment = comments[0];
|
|
if (firstComment.type === 'Line' && firstComment.value.trim() === 'use client') {
|
|
hasUseClient = true;
|
|
} else if (firstComment.type === 'Block' && firstComment.value.includes('use client')) {
|
|
hasUseClient = true;
|
|
}
|
|
}
|
|
|
|
// Also check for 'use client' string literal as first statement
|
|
if (node.body.length > 0) {
|
|
const firstStmt = node.body[0];
|
|
if (firstStmt &&
|
|
firstStmt.type === 'ExpressionStatement' &&
|
|
firstStmt.expression.type === 'Literal' &&
|
|
firstStmt.expression.value === 'use client') {
|
|
hasUseClient = true;
|
|
}
|
|
}
|
|
},
|
|
|
|
// Find Presenter classes
|
|
ClassDeclaration(node) {
|
|
const className = node.id?.name;
|
|
|
|
// Check if this is a Presenter class
|
|
if (className && className.endsWith('Presenter')) {
|
|
presenterClassNode = node;
|
|
|
|
// Check if it implements any interface
|
|
if (node.implements && node.implements.length > 0) {
|
|
for (const impl of node.implements) {
|
|
// Handle GenericTypeAnnotation for Presenter<TInput, TOutput>
|
|
if (impl.expression.type === 'TSInstantiationExpression') {
|
|
const expr = impl.expression.expression;
|
|
if (expr.type === 'Identifier' && expr.name === 'Presenter') {
|
|
hasImplements = true;
|
|
}
|
|
} else if (impl.expression.type === 'Identifier') {
|
|
// Handle simple Presenter (without generics)
|
|
if (impl.expression.name === 'Presenter') {
|
|
hasImplements = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// Check for present method in classes
|
|
MethodDefinition(node) {
|
|
if (presenterClassNode &&
|
|
node.key.type === 'Identifier' &&
|
|
node.key.name === 'present' &&
|
|
node.parent === presenterClassNode) {
|
|
hasPresentMethod = true;
|
|
}
|
|
},
|
|
|
|
// Report violations at the end
|
|
'Program:exit'() {
|
|
if (!presenterClassNode) return;
|
|
|
|
if (!hasImplements) {
|
|
context.report({
|
|
node: presenterClassNode,
|
|
messageId: 'missingImplements',
|
|
});
|
|
}
|
|
|
|
if (!hasPresentMethod) {
|
|
context.report({
|
|
node: presenterClassNode,
|
|
messageId: 'missingPresentMethod',
|
|
});
|
|
}
|
|
|
|
if (!hasUseClient) {
|
|
context.report({
|
|
node: presenterClassNode,
|
|
messageId: 'missingUseClient',
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
}; |