123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- /**
- * @fileoverview Prevent missing displayName in a React 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 testReactVersion = require('../util/version').testReactVersion;
- 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',
- };
- module.exports = {
- meta: {
- docs: {
- description: 'Disallow missing displayName in a React 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) && testReactVersion(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 React.forwardRef is nested inside React.memo
- * @param {ASTNode} node The AST node being checked.
- * @returns {Boolean} True if React.forwardRef is nested inside React.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 (
- testReactVersion(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 React.forwardRef declarations that are embeded within
- // a React.memo i.e. React.memo(React.forwardRef(/* ... */))
- // This means that we raise a single error for the call to React.memo
- // instead of one for React.memo and one for React.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)
- );
- }
- },
- };
- }),
- };
|