Skip to content

Commit

Permalink
Add new rule: forbid-elements
Browse files Browse the repository at this point in the history
May specify a list of forbidden elements and a custom reporting message
  • Loading branch information
kentor committed Jan 31, 2017
1 parent 30e0206 commit d884a98
Show file tree
Hide file tree
Showing 5 changed files with 402 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -82,6 +82,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](#

* [react/display-name](docs/rules/display-name.md): Prevent missing `displayName` in a React component definition
* [react/forbid-component-props](docs/rules/forbid-component-props.md): Forbid certain props on Components
* [react/forbid-elements](docs/rules/forbid-elements.md): Forbid certain elements
* [react/forbid-prop-types](docs/rules/forbid-prop-types.md): Forbid certain propTypes
* [react/no-array-index-key](docs/rules/no-array-index-key.md): Prevent using Array index in `key` props
* [react/no-children-prop](docs/rules/no-children-prop.md): Prevent passing children as props
Expand Down
58 changes: 58 additions & 0 deletions docs/rules/forbid-elements.md
@@ -0,0 +1,58 @@
# Forbid certain elements (forbid-elements)

You may want to forbid usage of certain elements in favor of others, (e.g. forbid all `<div />` and use `<Box />` instead). This rule allows you to configure a list of forbidden elements and to specify their desired replacements.

## Rule Details

This rule checks all JSX elements and `React.createElement` calls and verifies that no forbidden elements are used. This rule is off by default. If on, no elements are forbidden by default.

## Rule Options

```js
...
"forbid-elements": [<enabled>, { "forbid": [<string|object>] }]
...
```

### `forbid`

An array of strings and/or objects. An object in this array may have the following properties:

* `element` (required): the name of the forbidden element (e.g. `'button'`, `'Modal'`)
* `message`: additional message that gets reported

A string item in the array is a shorthand for `{ element: string }`.

The following patterns are not considered warnings:

```jsx
// [1, { "forbid": ["button"] }]
<Button />

// [1, { "forbid": [{ "element": "button" }] }]
<Button />
```

The following patterns are considered warnings:

```jsx
// [1, { "forbid": ["button"] }]
<button />
React.createElement('button');

// [1, { "forbid": ["Modal"] }]
<Modal />
React.createElement(Modal);

// [1, { "forbid": ["Namespaced.Element"] }]
<Namespaced.Element />
React.createElement(Namespaced.Element);

// [1, { "forbid": [{ "element": "button", "message": "use <Button> instead" }, "input"] }]
<div><button /><input /></div>
React.createElement('div', {}, React.createElemet('button', {}, React.createElement('input')));
```

## When not to use

If you don't want to forbid any elements.
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -42,6 +42,7 @@ var allRules = {
'jsx-space-before-closing': require('./lib/rules/jsx-space-before-closing'),
'no-direct-mutation-state': require('./lib/rules/no-direct-mutation-state'),
'forbid-component-props': require('./lib/rules/forbid-component-props'),
'forbid-elements': require('./lib/rules/forbid-elements'),
'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
'prefer-es6-class': require('./lib/rules/prefer-es6-class'),
'jsx-key': require('./lib/rules/jsx-key'),
Expand Down
112 changes: 112 additions & 0 deletions lib/rules/forbid-elements.js
@@ -0,0 +1,112 @@
/**
* @fileoverview Forbid certain elements
* @author Kenneth Chung
*/
'use strict';

var has = require('has');

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
docs: {
description: 'Forbid certain elements',
category: 'Best Practices',
recommended: false
},

schema: [{
type: 'object',
properties: {
forbid: {
type: 'array',
items: {
anyOf: [
{type: 'string'},
{
type: 'object',
properties: {
element: {type: 'string'},
message: {type: 'string'}
},
required: ['element'],
additionalProperties: false
}
]
}
}
},
additionalProperties: false
}]
},

create: function(context) {
var sourceCode = context.getSourceCode();
var configuration = context.options[0] || {};
var forbidConfiguration = configuration.forbid || [];

var indexedForbidConfigs = {};

forbidConfiguration.forEach(function(item) {
if (typeof item === 'string') {
indexedForbidConfigs[item] = {element: item};
} else {
indexedForbidConfigs[item.element] = item;
}
});

function errorMessageForElement(name) {
var message = '<' + name + '> is forbidden';
var additionalMessage = indexedForbidConfigs[name].message;

if (additionalMessage) {
message = message + ', ' + additionalMessage;
}

return message;
}

function isValidCreateElement(node) {
return node.callee
&& node.callee.type === 'MemberExpression'
&& node.callee.object.name === 'React'
&& node.callee.property.name === 'createElement'
&& node.arguments.length > 0;
}

function reportIfForbidden(element, node) {
if (has(indexedForbidConfigs, element)) {
context.report({
node: node,
message: errorMessageForElement(element)
});
}
}

return {
JSXOpeningElement: function(node) {
reportIfForbidden(sourceCode.getText(node.name), node.name);
},

CallExpression: function(node) {
if (!isValidCreateElement(node)) {
return;
}

var argument = node.arguments[0];
var argType = argument.type;

if (argType === 'Identifier' && /^[A-Z_]/.test(argument.name)) {
reportIfForbidden(argument.name, argument);
} else if (argType === 'Literal' && /^[a-z][^\.]*$/.test(argument.value)) {
reportIfForbidden(argument.value, argument);
} else if (argType === 'MemberExpression') {
reportIfForbidden(sourceCode.getText(argument), argument);
}
}
};
}
};

0 comments on commit d884a98

Please sign in to comment.