Skip to content

Commit a8b31c2

Browse files
committed
feat: no-unused-prop-types
BREAKING CHANGE: adding new recommended rule.
1 parent 26424cd commit a8b31c2

File tree

9 files changed

+532
-5
lines changed

9 files changed

+532
-5
lines changed

.babelrc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"plugins": [
3+
"transform-object-rest-spread"
4+
],
5+
"presets": [
6+
[
7+
"env",
8+
{
9+
"targets": {
10+
"node": 4
11+
}
12+
}
13+
]
14+
]
15+
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules
22
package-lock.json
3+
dist

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,5 @@ To configure individual rules:
5353
* [react-redux/mapStateToProps-no-store](docs/rules/mapStateToProps-no-store.md) Prohibits binding a whole store object to a component.
5454
* [react-redux/mapStateToProps-prefer-hoisted](docs/rules/mapStateToProps-prefer-hoisted.md) Flags generation of copies of same-by-value but different-by-reference props.
5555
* [react-redux/mapStateToProps-prefer-parameters-names](docs/rules/mapStateToProps-prefer-parameters-names.md) Enforces that all mapStateToProps parameters have specific names.
56+
* [react-redux/no-unused-prop-tyoes](docs/rules/no-unused-prop-types.md) Extension of a react's no-unused-prop-types rule filtering out false positive used in redux context.
5657
* [react-redux/prefer-separate-component-file](docs/rules/prefer-separate-component-file.md) Enforces that all connected components are defined in a separate file.

docs/rules/no-unused-prop-types.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Extension of a react's no-unused-prop-types rule filtering out false positive used in redux context. (react-redux/no-unused-prop-types)
2+
3+
[react/no-unused-prop-types](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/no-unused-prop-types.md)
4+
5+
# Rule details
6+
7+
This rule fixes some of the false positive reported by the react rule
8+
9+
In below example `react/no-unused-prop-types` would report `myProp PropType is defined but prop is never used` while `react-redux/no-unused-prop-types` would correctly detect the usage of this prop within `mapStateToProps`.
10+
11+
```js
12+
export const mapStateToProps = (state, ownProps) => ({
13+
myData: getMyData(state, ownProps.myProp),
14+
});
15+
16+
export class MyComponent extends Component {
17+
render() {
18+
return <div>{this.props.myData}</div>;
19+
}
20+
}
21+
22+
MyComponent.propTypes = {
23+
myProp: PropTypes.string.isRequired
24+
};
25+
26+
export default connect(mapStateToProps)(MyComponent);
27+
```
28+
29+
# Implementation details and Limitations
30+
31+
The rule actually runs `react/no-unused-prop-types` rule and then filters out the reports of props that are used within redux's `mapStateToProps` or `mapDispatchToProps`.
32+
The rule only works within a context of a single file. So it would only work properly if compoent and container (react connect fucntion) are defined within the same file.
33+
34+
# Configuration
35+
36+
You'd want to disable `react/no-unused-prop-types` if you using this rule.

index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const rules = {
77
'mapStateToProps-no-store': require('./lib/rules/mapStateToProps-no-store'),
88
'mapStateToProps-prefer-hoisted': require('./lib/rules/mapStateToProps-prefer-hoisted'),
99
'mapStateToProps-prefer-parameters-names': require('./lib/rules/mapStateToProps-prefer-parameters-names'),
10+
'no-unused-prop-types': require('./lib/rules/no-unused-prop-types'),
1011
'prefer-separate-component-file': require('./lib/rules/prefer-separate-component-file'),
1112
};
1213

@@ -34,6 +35,7 @@ module.exports = {
3435
'react-redux/mapStateToProps-no-store': 2,
3536
'react-redux/mapStateToProps-prefer-hoisted': 2,
3637
'react-redux/mapStateToProps-prefer-parameters-names': 2,
38+
'react-redux/no-unused-prop-types': 2,
3739
'react-redux/prefer-separate-component-file': 1,
3840
},
3941
},

lib/filterReports.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
'use strict';
2+
3+
const ruleComposer = require('eslint-rule-composer');
4+
5+
/* eslint-disable */
6+
7+
// ---- Start copy ---- //
8+
// from https://github.com/not-an-aardvark/eslint-rule-composer/blob/master/lib/rule-composer.js#L3-L125
9+
10+
/**
11+
* Translates a multi-argument context.report() call into a single object argument call
12+
* @param {...*} arguments A list of arguments passed to `context.report`
13+
* @returns {MessageDescriptor} A normalized object containing report information
14+
*/
15+
function normalizeMultiArgReportCall() {
16+
// If there is one argument, it is considered to be a new-style call already.
17+
if (arguments.length === 1) {
18+
return arguments[0];
19+
}
20+
21+
// If the second argument is a string, the arguments are interpreted as [node, message, data, fix].
22+
if (typeof arguments[1] === 'string') {
23+
return {
24+
node: arguments[0],
25+
message: arguments[1],
26+
data: arguments[2],
27+
fix: arguments[3],
28+
};
29+
}
30+
31+
// Otherwise, the arguments are interpreted as [node, loc, message, data, fix].
32+
return {
33+
node: arguments[0],
34+
loc: arguments[1],
35+
message: arguments[2],
36+
data: arguments[3],
37+
fix: arguments[4],
38+
};
39+
}
40+
41+
/**
42+
* Normalizes a MessageDescriptor to always have a `loc` with `start` and `end` properties
43+
* @param {MessageDescriptor} descriptor A descriptor for the report from a rule.
44+
* @returns {{start: Location, end: (Location|null)}} An updated location that infers the `start` and `end` properties
45+
* from the `node` of the original descriptor, or infers the `start` from the `loc` of the original descriptor.
46+
*/
47+
function normalizeReportLoc(descriptor) {
48+
if (descriptor.loc) {
49+
if (descriptor.loc.start) {
50+
return descriptor.loc;
51+
}
52+
return { start: descriptor.loc, end: null };
53+
}
54+
return descriptor.node.loc;
55+
}
56+
57+
58+
/**
59+
* Interpolates data placeholders in report messages
60+
* @param {MessageDescriptor} descriptor The report message descriptor.
61+
* @param {Object} messageIds Message IDs from rule metadata
62+
* @returns {{message: string, data: Object}} The interpolated message and data for the descriptor
63+
*/
64+
function normalizeMessagePlaceholders(descriptor, messageIds) {
65+
const message = typeof descriptor.messageId === 'string' ? messageIds[descriptor.messageId] : descriptor.message;
66+
if (!descriptor.data) {
67+
return {
68+
message,
69+
data: typeof descriptor.messageId === 'string' ? {} : null,
70+
};
71+
}
72+
73+
const normalizedData = Object.create(null);
74+
const interpolatedMessage = message.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (fullMatch, term) => {
75+
if (term in descriptor.data) {
76+
normalizedData[term] = descriptor.data[term];
77+
return descriptor.data[term];
78+
}
79+
80+
return fullMatch;
81+
});
82+
83+
return {
84+
message: interpolatedMessage,
85+
data: Object.freeze(normalizedData),
86+
};
87+
}
88+
89+
function getRuleMeta(rule) {
90+
return typeof rule === 'object' && rule.meta && typeof rule.meta === 'object'
91+
? rule.meta
92+
: {};
93+
}
94+
95+
function getMessageIds(rule) {
96+
const meta = getRuleMeta(rule);
97+
return meta.messages && typeof rule.meta.messages === 'object'
98+
? meta.messages
99+
: {};
100+
}
101+
102+
function getReportNormalizer(rule) {
103+
const messageIds = getMessageIds(rule);
104+
105+
return function normalizeReport() {
106+
const descriptor = normalizeMultiArgReportCall(...arguments);
107+
const interpolatedMessageAndData = normalizeMessagePlaceholders(descriptor, messageIds);
108+
109+
return {
110+
node: descriptor.node,
111+
message: interpolatedMessageAndData.message,
112+
messageId: typeof descriptor.messageId === 'string' ? descriptor.messageId : null,
113+
data: typeof descriptor.messageId === 'string' ? interpolatedMessageAndData.data : null,
114+
loc: normalizeReportLoc(descriptor),
115+
fix: descriptor.fix,
116+
};
117+
};
118+
}
119+
120+
function getRuleCreateFunc(rule) {
121+
return typeof rule === 'function' ? rule : rule.create;
122+
}
123+
124+
function removeMessageIfMessageIdPresent(reportDescriptor) {
125+
const newDescriptor = Object.assign({}, reportDescriptor);
126+
127+
if (typeof reportDescriptor.messageId === 'string' && typeof reportDescriptor.message === 'string') {
128+
delete newDescriptor.message;
129+
}
130+
131+
return newDescriptor;
132+
}
133+
134+
// ---- End copy ---- //
135+
136+
const filterReports = (rule, getPropNameFromReactRuleMessage, getPropNameFromReduxRuleMessage) => Object.freeze({
137+
create(context) {
138+
const removeProps = [];
139+
return getRuleCreateFunc(rule)(Object.freeze(Object.create(
140+
context,
141+
{
142+
report: {
143+
enumerable: true,
144+
value() {
145+
const reportDescriptor = getReportNormalizer(rule)(...arguments);
146+
if (reportDescriptor.message.indexOf('exclude:') > -1) {
147+
removeProps.push(getPropNameFromReduxRuleMessage(reportDescriptor.message));
148+
} else {
149+
const propName = getPropNameFromReactRuleMessage(reportDescriptor.message);
150+
if (removeProps.indexOf(propName) === -1) {
151+
context.report(removeMessageIfMessageIdPresent(reportDescriptor));
152+
}
153+
}
154+
155+
},
156+
},
157+
}
158+
)));
159+
},
160+
schema: rule.schema,
161+
meta: getRuleMeta(rule),
162+
});
163+
164+
module.exports = (rules, getPropNameFromReactRuleMessage, getPropNameFromReduxRuleMessage) => filterReports(ruleComposer.joinReports(rules), getPropNameFromReactRuleMessage, getPropNameFromReduxRuleMessage);

lib/rules/no-unused-prop-types.js

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
'use strict';
2+
3+
const filterReports = require('../filterReports');
4+
const isReactReduxConnect = require('../isReactReduxConnect');
5+
6+
const noUnusedPropTypesReact = require('eslint-plugin-react').rules['no-unused-prop-types'];
7+
8+
const belongsToReduxReact = (node, objectName, destrArg) => {
9+
const checkProp = (secondArgument) => {
10+
const secondArgumentName = secondArgument && secondArgument.type === 'Identifier' && secondArgument.name;
11+
return (secondArgumentName === objectName // ownProps.myProp
12+
|| destrArg === secondArgument // {myProp} in fn argument
13+
|| (destrArg && destrArg.parent.type === 'VariableDeclarator' && destrArg.parent.init.name === secondArgumentName) // const {myProp} = ownProps;
14+
);
15+
};
16+
let isReactRedux = false;
17+
if (node.type === 'VariableDeclaration') {
18+
node.declarations.forEach((decl) => {
19+
const name = decl.id && decl.id.name;
20+
if (name === 'mapStateToProps' || name === 'mapDispatchToProps') {
21+
const secondArgument = decl.init.params && decl.init.params[1];
22+
if (checkProp(secondArgument)) {
23+
isReactRedux = true;
24+
}
25+
}
26+
});
27+
} else if (node.type === 'FunctionDeclaration') {
28+
const name = node.id && node.id.name;
29+
if (name === 'mapStateToProps' || name === 'mapDispatchToProps') {
30+
const secondArgument = node.params && node.params[1];
31+
if (checkProp(secondArgument)) {
32+
isReactRedux = true;
33+
}
34+
}
35+
} else if (node.type === 'CallExpression') {
36+
if (isReactReduxConnect(node)) {
37+
const check = (mapToProps) => {
38+
if (mapToProps && mapToProps.body) {
39+
const secondArgument = mapToProps.params && mapToProps.params[1];
40+
if (checkProp(secondArgument)) {
41+
isReactRedux = true;
42+
}
43+
}
44+
};
45+
const mapStateToProps = node.arguments && node.arguments[0];
46+
const mapDispatchToProps = node.arguments && node.arguments[1];
47+
if (mapStateToProps) check(mapStateToProps);
48+
if (mapDispatchToProps) check(mapDispatchToProps);
49+
}
50+
}
51+
return isReactRedux;
52+
};
53+
54+
55+
const propsUsedInRedux = function (context) {
56+
return {
57+
MemberExpression(node) {
58+
const nodeName = node.object.name;
59+
const usedInReactRedux = context.getAncestors()
60+
.some(ancestor => belongsToReduxReact(ancestor, nodeName));
61+
if (usedInReactRedux) {
62+
context.report(node, `exclude:${node.property.name}`);
63+
}
64+
},
65+
ObjectPattern(node) {
66+
const usedInReactRedux = context.getAncestors()
67+
.some(ancestor => belongsToReduxReact(ancestor, null, node));
68+
if (usedInReactRedux) {
69+
node.properties.forEach(prop => context.report(node, `exclude:${prop.key.name}`));
70+
}
71+
},
72+
};
73+
};
74+
75+
const getPropNameFromReactRuleMessage = message => message.replace(' PropType is defined but prop is never used', '').replace("'", '').replace("'", '');
76+
const getPropNameFromReduxRuleMessage = message => message.replace('exclude:', '');
77+
78+
module.exports = filterReports([
79+
propsUsedInRedux,
80+
noUnusedPropTypesReact,
81+
], getPropNameFromReactRuleMessage, getPropNameFromReduxRuleMessage);

package.json

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@
99
"react-redux"
1010
],
1111
"author": "[email protected]",
12-
"main": "lib/index.js",
12+
"main": "dist/index.js",
1313
"scripts": {
14-
"lint": "eslint ./",
15-
"test": "npm run lint && mocha tests --recursive",
14+
"lint": "eslint ./lib ./tests",
15+
"test": "npm run lint && mocha --compilers js:babel-register tests --recursive",
1616
"semantic-release": "semantic-release",
17-
"commitmsg": "npm run test && commitlint -e $GIT_PARAMS"
17+
"commitmsg": "npm run test && commitlint -e $GIT_PARAMS",
18+
"build": "rm -fr ./dist && babel ./src --out-dir ./dist --copy-files"
1819
},
1920
"repository": {
2021
"type": "git",
@@ -23,25 +24,35 @@
2324
"devDependencies": {
2425
"@commitlint/cli": "^6.0.2",
2526
"@commitlint/config-conventional": "^6.0.2",
27+
"babel-cli": "^6.26.0",
2628
"babel-eslint": "^8.1.0",
29+
"babel-plugin-transform-object-rest-spread": "^6.26.0",
30+
"babel-preset-env": "^1.7.0",
31+
"babel-register": "^6.26.0",
2732
"eslint": "^4.14.0",
2833
"eslint-config-airbnb": "^16.1.0",
2934
"eslint-config-standard": "^11.0.0-beta.0",
3035
"eslint-plugin-import": "^2.8.0",
3136
"eslint-plugin-jsx-a11y": "^6.0.3",
3237
"eslint-plugin-node": "^5.2.1",
3338
"eslint-plugin-promise": "^3.6.0",
34-
"eslint-plugin-react": "^7.5.1",
3539
"eslint-plugin-standard": "^3.0.1",
3640
"husky": "^0.14.3",
3741
"mocha": "^4.0.1",
3842
"semantic-release": "^12.4.1"
3943
},
44+
"peerDependencies": {
45+
"eslint-plugin-react": "^7.11.1"
46+
},
4047
"engines": {
4148
"node": ">=6.10.0"
4249
},
4350
"license": "ISC",
4451
"directories": {
4552
"test": "tests"
53+
},
54+
"dependencies": {
55+
"eslint-rule-composer": "^0.3.0",
56+
"eslint-plugin-react": "^7.11.1"
4657
}
4758
}

0 commit comments

Comments
 (0)