Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New] checkContextObjects option in display-name #3529

Merged
merged 1 commit into from
Feb 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange

## Unreleased

### Added
* [`display-name`]: add `checkContextObjects` option ([#3529][] @JulesBlm)

### Fixed
* [`no-array-index-key`]: consider flatMap ([#3530][] @k-yle)

[#3530]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3530
[#3529]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3529

## [7.32.2] - 2023.01.28

Expand Down
29 changes: 28 additions & 1 deletion docs/rules/display-name.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const Hello = React.memo(function Hello({ a }) {

```js
...
"react/display-name": [<enabled>, { "ignoreTranspilerName": <boolean> }]
"react/display-name": [<enabled>, { "ignoreTranspilerName": <boolean>, "checkContextObjects": <boolean> }]
...
```

Expand Down Expand Up @@ -128,6 +128,33 @@ function HelloComponent() {
module.exports = HelloComponent();
```

### checkContextObjects (default: `false`)

`displayName` allows you to [name your context](https://reactjs.org/docs/context.html#contextdisplayname) object. This name is used in the React dev tools for the context's `Provider` and `Consumer`.
When `true` this rule will warn on context objects without a `displayName`.

Examples of **incorrect** code for this rule:

```jsx
const Hello = React.createContext();
```

```jsx
const Hello = createContext();
```

Examples of **correct** code for this rule:

```jsx
const Hello = React.createContext();
Hello.displayName = "HelloContext";
```

```jsx
const Hello = createContext();
Hello.displayName = "HelloContext";
```

## About component detection

For this rule to work we need to detect React components, this could be very hard since components could be declared in a lot of ways.
Expand Down
41 changes: 41 additions & 0 deletions lib/rules/display-name.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
const values = require('object.values');

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');
Expand All @@ -21,6 +22,7 @@ const report = require('../util/report');

const messages = {
noDisplayName: 'Component definition is missing display name',
noContextDisplayName: 'Context definition is missing display name',
};

module.exports = {
Expand All @@ -40,6 +42,9 @@ module.exports = {
ignoreTranspilerName: {
type: 'boolean',
},
checkContextObjects: {
type: 'boolean',
},
},
additionalProperties: false,
}],
Expand All @@ -48,6 +53,9 @@ module.exports = {
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
Expand Down Expand Up @@ -87,6 +95,16 @@ module.exports = {
});
}

/**
* 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.
Expand Down Expand Up @@ -144,6 +162,16 @@ module.exports = {
// --------------------------------------------------------------------------

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;
Expand All @@ -155,6 +183,14 @@ module.exports = {
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;
Expand Down Expand Up @@ -258,6 +294,11 @@ module.exports = {
values(list).filter((component) => !component.hasDisplayName).forEach((component) => {
reportMissingDisplayName(component);
});
if (checkContextObjects) {
// Report missing display name for all context objects
const contextsList = Array.from(contextObjects.values()).filter((v) => !v.hasDisplayName);
contextsList.forEach((contextObj) => reportMissingContextDisplayName(contextObj));
}
},
};
}),
Expand Down
53 changes: 53 additions & 0 deletions lib/util/isCreateContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

/**
* Checks if the node is a React.createContext call
* @param {ASTNode} node - The AST node being checked.
* @returns {Boolean} - True if node is a React.createContext call, false if not.
*/
module.exports = function isCreateContext(node) {
if (
node.init
&& node.init.type === 'CallExpression'
&& node.init.callee
&& node.init.callee.name === 'createContext'
) {
return true;
}

if (
node.init
&& node.init.callee
&& node.init.callee.type === 'MemberExpression'
&& node.init.callee.property
&& node.init.callee.property.name === 'createContext'
) {
return true;
}

if (
node.expression
&& node.expression.type === 'AssignmentExpression'
&& node.expression.operator === '='
&& node.expression.right.type === 'CallExpression'
&& node.expression.right.callee
&& node.expression.right.callee.name === 'createContext'
) {
return true;
}

if (
node.expression
&& node.expression.type === 'AssignmentExpression'
&& node.expression.operator === '='
&& node.expression.right.type === 'CallExpression'
&& node.expression.right.callee
&& node.expression.right.callee.type === 'MemberExpression'
&& node.expression.right.callee.property
&& node.expression.right.callee.property.name === 'createContext'
) {
return true;
}

return false;
};