Skip to content

Commit

Permalink
Add rule: Forbid Elements (version 2)
Browse files Browse the repository at this point in the history
  • Loading branch information
kentor committed Oct 7, 2016
1 parent 2978484 commit 5efb73e
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 110 deletions.
27 changes: 18 additions & 9 deletions docs/rules/forbid-elements.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ You may want to forbid usage of certain elements in favor of others, (e.g. forbi

## Rule Details

This rule checks all JSX components and verifies that no forbidden elements are used. When it detects a forbidden element, the error message will contain the specified elements that you should use instead. This rule is off by default. If on, no elements are forbidden by default.
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

Expand All @@ -16,15 +16,20 @@ This rule checks all JSX components and verifies that no forbidden elements are

### `forbid`

An array of strings, objects, or a mixture of string and objects. If an item is a string, then an element matching the string will be forbidden and no replacement will be suggested. If an item is an object, then the keys will be the forbidden element and their values can be either a string or an array of strings of desired replacement elements.
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: [{button: 'Button']}]
// [1, {forbid: [{element: 'button']}]
<Button />
```

Expand All @@ -33,15 +38,19 @@ The following patterns are considered warnings:
```jsx
// [1, {forbid: ['button']}]
<button />
React.createElement('button');

// [1, {forbid: ['button', 'input']}]
<div><button /><input /></div>
// [1, {forbid: ['Modal']}]
<Modal />
React.createElement(Modal);

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

// [1, {forbid: [{div: ['Box', 'View']}, 'button']]
<div><button /></div>
// [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
Expand Down
93 changes: 47 additions & 46 deletions lib/rules/forbid-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ module.exports = {
{type: 'string'},
{
type: 'object',
additionalProperties: {
anyOf: [
{type: 'string'},
{type: 'array', items: {type: 'string'}}
]
}
properties: {
element: {type: 'string'},
message: {type: 'string'},
},
required: ['element'],
additionalProperties: false
}
]
}
Expand All @@ -45,63 +45,64 @@ module.exports = {
var sourceCode = context.getSourceCode();
var configuration = context.options[0] || {};
var forbidConfiguration = configuration.forbid || [];
var forbiddenElements = {};

function formatReplacements(replacements) {
if (!replacements || replacements.length === 0) {
return null;
}

if (typeof replacements === 'string') {
replacements = [replacements];
}

replacements = replacements.map(function(replacement) {
return '<' + replacement + '>';
});

if (replacements.length === 1) {
return replacements[0];
}
var indexedForbidConfigs = {};

if (replacements.length === 2) {
return replacements.join(' or ');
forbidConfiguration.forEach(function(item) {
if (typeof item === 'string') {
indexedForbidConfigs[item] = {element: item};
} else {
indexedForbidConfigs[item.element] = item;
}
});

var last = replacements.pop();
return replacements.join(', ') + ', or ' + last;
}

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

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

return message;
}

forbidConfiguration.forEach(function(item) {
if (typeof item === 'string') {
forbiddenElements[item] = null;
} else {
Object.keys(item).forEach(function(key) {
forbiddenElements[key] = formatReplacements(item[key]);
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 (indexedForbidConfigs.hasOwnProperty(element)) {
context.report({
node: node,
message: errorMessageForElement(element)
});
}
});
}

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

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

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

if (forbiddenElements.hasOwnProperty(name)) {
context.report({
node: node,
message: errorMessageForName(name)
});
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);
}
}
};
Expand Down
Loading

0 comments on commit 5efb73e

Please sign in to comment.