eslint-plugin-reblend
Version:
Reblend specific linting rules for ESLint
312 lines (280 loc) • 9.55 kB
JavaScript
/**
* @fileoverview Prevent missing displayName in a Reblend component definition
* @author Yannick Croissant
*/
'use strict';
const values = require('object.values');
const filter = require('es-iterator-helpers/Iterator.prototype.filter');
const forEach = require('es-iterator-helpers/Iterator.prototype.forEach');
const Components = require('../util/Components');
const isCreateContext = require('../util/isCreateContext');
const astUtil = require('../util/ast');
const componentUtil = require('../util/componentUtil');
const docsUrl = require('../util/docsUrl');
const testReblendVersion = require('../util/version').testReblendVersion;
const propsUtil = require('../util/props');
const report = require('../util/report');
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
const messages = {
noDisplayName: 'Component definition is missing display name',
noContextDisplayName: 'Context definition is missing display name',
};
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
docs: {
description:
'Disallow missing displayName in a Reblend component definition',
category: 'Best Practices',
recommended: true,
url: docsUrl('display-name'),
},
messages,
schema: [
{
type: 'object',
properties: {
ignoreTranspilerName: {
type: 'boolean',
},
checkContextObjects: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
},
create: Components.detect((context, components, utils) => {
const config = context.options[0] || {};
const ignoreTranspilerName = config.ignoreTranspilerName || false;
const checkContextObjects =
(config.checkContextObjects || false) &&
testReblendVersion(context, '>= 16.3.0');
const contextObjects = new Map();
/**
* Mark a prop type as declared
* @param {ASTNode} node The AST node being checked.
*/
function markDisplayNameAsDeclared(node) {
components.set(node, {
hasDisplayName: true,
});
}
/**
* Checks if Reblend.forwardRef is nested inside Reblend.memo
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if Reblend.forwardRef is nested inside Reblend.memo, false if not.
*/
function isNestedMemo(node) {
const argumentIsCallExpression =
node.arguments &&
node.arguments[0] &&
node.arguments[0].type === 'CallExpression';
return (
node.type === 'CallExpression' &&
argumentIsCallExpression &&
utils.isPragmaComponentWrapper(node)
);
}
/**
* Reports missing display name for a given component
* @param {Object} component The component to process
*/
function reportMissingDisplayName(component) {
if (
testReblendVersion(context, '^0.14.10 || ^15.7.0 || >= 16.12.0') &&
isNestedMemo(component.node)
) {
return;
}
report(context, messages.noDisplayName, 'noDisplayName', {
node: component.node,
});
}
/**
* Reports missing display name for a given context object
* @param {Object} contextObj The context object to process
*/
function reportMissingContextDisplayName(contextObj) {
report(context, messages.noContextDisplayName, 'noContextDisplayName', {
node: contextObj.node,
});
}
/**
* Checks if the component have a name set by the transpiler
* @param {ASTNode} node The AST node being checked.
* @returns {Boolean} True if component has a name, false if not.
*/
function hasTranspilerName(node) {
const namedObjectAssignment =
node.type === 'ObjectExpression' &&
node.parent &&
node.parent.parent &&
node.parent.parent.type === 'AssignmentExpression' &&
(!node.parent.parent.left.object ||
node.parent.parent.left.object.name !== 'module' ||
node.parent.parent.left.property.name !== 'exports');
const namedObjectDeclaration =
node.type === 'ObjectExpression' &&
node.parent &&
node.parent.parent &&
node.parent.parent.type === 'VariableDeclarator';
const namedClass =
(node.type === 'ClassDeclaration' || node.type === 'ClassExpression') &&
node.id &&
!!node.id.name;
const namedFunctionDeclaration =
(node.type === 'FunctionDeclaration' ||
node.type === 'FunctionExpression') &&
node.id &&
!!node.id.name;
const namedFunctionExpression =
astUtil.isFunctionLikeExpression(node) &&
node.parent &&
(node.parent.type === 'VariableDeclarator' ||
node.parent.type === 'Property' ||
node.parent.method === true) &&
(!node.parent.parent ||
!componentUtil.isES5Component(node.parent.parent, context));
if (
namedObjectAssignment ||
namedObjectDeclaration ||
namedClass ||
namedFunctionDeclaration ||
namedFunctionExpression
) {
return true;
}
return false;
}
// --------------------------------------------------------------------------
// Public
// --------------------------------------------------------------------------
return {
ExpressionStatement(node) {
if (checkContextObjects && isCreateContext(node)) {
contextObjects.set(node.expression.left.name, {
node,
hasDisplayName: false,
});
}
},
VariableDeclarator(node) {
if (checkContextObjects && isCreateContext(node)) {
contextObjects.set(node.id.name, { node, hasDisplayName: false });
}
},
'ClassProperty, PropertyDefinition'(node) {
if (!propsUtil.isDisplayNameDeclaration(node)) {
return;
}
markDisplayNameAsDeclared(node);
},
MemberExpression(node) {
if (!propsUtil.isDisplayNameDeclaration(node.property)) {
return;
}
if (
checkContextObjects &&
node.object &&
node.object.name &&
contextObjects.has(node.object.name)
) {
contextObjects.get(node.object.name).hasDisplayName = true;
}
const component = utils.getRelatedComponent(node);
if (!component) {
return;
}
markDisplayNameAsDeclared(
component.node.type === 'TSAsExpression'
? component.node.expression
: component.node
);
},
'FunctionExpression, FunctionDeclaration, ArrowFunctionExpression'(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
return;
}
if (components.get(node)) {
markDisplayNameAsDeclared(node);
}
},
MethodDefinition(node) {
if (!propsUtil.isDisplayNameDeclaration(node.key)) {
return;
}
markDisplayNameAsDeclared(node);
},
'ClassExpression, ClassDeclaration'(node) {
if (ignoreTranspilerName || !hasTranspilerName(node)) {
return;
}
markDisplayNameAsDeclared(node);
},
ObjectExpression(node) {
if (!componentUtil.isES5Component(node, context)) {
return;
}
if (ignoreTranspilerName || !hasTranspilerName(node)) {
// Search for the displayName declaration
node.properties.forEach(property => {
if (
!property.key ||
!propsUtil.isDisplayNameDeclaration(property.key)
) {
return;
}
markDisplayNameAsDeclared(node);
});
return;
}
markDisplayNameAsDeclared(node);
},
CallExpression(node) {
if (!utils.isPragmaComponentWrapper(node)) {
return;
}
if (
node.arguments.length > 0 &&
astUtil.isFunctionLikeExpression(node.arguments[0])
) {
// Skip over Reblend.forwardRef declarations that are embedded within
// a Reblend.memo i.e. Reblend.memo(Reblend.forwardRef(/* ... */))
// This means that we raise a single error for the call to Reblend.memo
// instead of one for Reblend.memo and one for Reblend.forwardRef
const isWrappedInAnotherPragma =
utils.getPragmaComponentWrapper(node);
if (
!isWrappedInAnotherPragma &&
(ignoreTranspilerName || !hasTranspilerName(node.arguments[0]))
) {
return;
}
if (components.get(node)) {
markDisplayNameAsDeclared(node);
}
}
},
'Program:exit'() {
const list = components.list();
// Report missing display name for all components
values(list)
.filter(component => !component.hasDisplayName)
.forEach(component => {
reportMissingDisplayName(component);
});
if (checkContextObjects) {
// Report missing display name for all context objects
forEach(
filter(contextObjects.values(), v => !v.hasDisplayName),
contextObj => reportMissingContextDisplayName(contextObj)
);
}
},
};
}),
};