-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Add no-mutation-props #1416
base: master
Are you sure you want to change the base?
Add no-mutation-props #1416
Conversation
Fix change requests mentioned in jsx-eslint#1145
c653ea1
to
70ecbae
Compare
Fixes jsx-eslint#1113 Now warns on: * array mutations * `Object.assign` * `Object.defineProperty` * `delete` * all of the above also works with array and object destructuring and variable assignment
70ecbae
to
d934305
Compare
Once this is in a good place, it's probably a good idea to use the same logic in the |
lib/rules/no-mutation-props.js
Outdated
assignments.forEach(assignment => { | ||
components.add(assignment); | ||
components.set(assignment, { | ||
isFromProps: true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how I feel about using the components
as a storage for non-component nodes... Maybe we can use some local collection instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, I was just trying to re-use the logic since what I need is all there. But, I'm happy to copy/paste what I need into here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like:
assignments.forEach(assignment => {
allAssignments.push(assignment.range.join(':')); // not sure about the name of the array :(
});
And where it's checked:
const currentIdentifier = allAssignments.indexOf(originalIdentifiers[originalIdentifiers.length - 1].range.join(':')) >= 0;
return currentIdentifier;
This way, at the end of the file there will only be the actual components inside the list, and we skip looping over all those assignments. For all tests, list would only contain 1 value.
Object.keys(list).forEach(key => {
const component = list[key];
reportMutations(component);
});
What do you think?
Can you also add support for On the other hand, this rule supports other cases that |
Yea, I'd love to share code between this and |
@jseminck changes made. I'd be happy to look at sharing code between this rule and |
Yeah. I agree! Looks good for me. It's a pleasure to read your code. 👍 Someone else should also still go over the code since I'm not an official maintainer though! |
var Hello = React.createClass({ | ||
render: function() { | ||
const {list} = this.props; | ||
list.push(2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't see how this can safely be considered a mutation; list
could be anything with a "push" method, there's no way to know it's an array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great point. I'd hate to loose the functionality though. Any thoughts on how we could determine it's an array?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Meanwhile, on Gitter…
@not-an-aardvark suggests that leave this rule as-is since these method names tend to imply mutation anyway (also, it would be pretty weird to have a prop
that isn't a plain object). Further, people can disable custom use-cases with an inline eslint-disable
.
If we want to be very safe, we could put this behind an option flag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think putting it behind an option is a great compromise - what about two booleans, and two lists of 1) instance methods, and 2) static methods? That way the instance method list could default to ARRAY_MUTATIONS
, and the static method list could default to ['Object.assign', 'Object.defineProperties', 'Object.defineProperty']
- and both booleans would default to false
.
That way most people wouldn't touch the lists at all, they'd just enable the booleans - but those who wanted to update the lists could do so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm with you on the array mutations, but I'm not sure I understand the danger of the static methods. Can you run that by me one more time? When is Object.assign(this.props, {…})
ever a safe operation?
docs/rules/no-mutation-props.md
Outdated
|
||
var Hello = React.createClass({ | ||
render: function() { | ||
Object.assign(this.props.foo, {bar: 'baz'}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're going to support builtins like Object.assign
and Object.defineProperty
(where's Object.defineProperties
btw), we'd need to support a way to allow this to be extensible - in other words, to allow devs to specify a custom function name, and which argument(s) are mutated as well.
I don't think we should support functions in this way in the initial version of the rule.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I follow? Object.assign
is never safe to use when this.props
is the first argument, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Object.defineProperties
is a good catch! Added.
docs/rules/no-mutation-props.md
Outdated
|
||
var Hello = React.createClass({ | ||
render: function() { | ||
Object.defineProperty(this.props, 'foo') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please ensure that all examples properly use semicolons.
lib/rules/no-mutation-props.js
Outdated
'use strict'; | ||
|
||
const Components = require('../util/Components'); | ||
const ARRAY_MUTATIONS = ['push', 'pop', 'shift', 'unshift']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
although i don't think we can safely check for any of these, you're also missing the other array mutator methods: splice
, reverse
, sort
, fill
, copyWithin
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added, but I'll hold of on adding tests until we resolve what to do here.
lib/rules/no-mutation-props.js
Outdated
*/ | ||
const addToPropsNodes = node => { | ||
const id = getId(node); | ||
if (propsNodes[id]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (!propsNodes[id]) {
propsNodes[id] = node;
}
return propsNodes[id];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sense.
* Gets a unique ID for a node | ||
* @param {Object} node The node to get an ID for | ||
*/ | ||
const getId = node => node && node.range.join(':'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what happens if the range only has one item, and that item is toString
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I know what you mean? I didn't think it was possible for range to have only one item? This logic is copied from https://github.com/yannickcr/eslint-plugin-react/blob/f1e86b5ba32ed9359eb7823b98f7a1dcc52376e3/lib/util/Components.js#L43-L45
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair, but since it's being used as an object key, without any prefix, there's still the possibility that it could conflict with an Object.prototype key.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I'm still not following you. Will the AST allow for range
to be anything other than a tuple of integers? We have full control of the object this key will be used for, so a string key composed of two joined integers should be totally safe, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'm just not that familiar with what the AST could return. If range
always contains > 1 item, then this is totally safe when node
is truthy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, the spec says range
must be an array with 2 integers: https://eslint.org/docs/developer-guide/working-with-plugins#all-nodes
* @param {Object} node The node to add | ||
*/ | ||
const addToPropsNodes = node => { | ||
const id = getId(node); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getId
can return a falsy value - that would get coerced to a string here, and probably provide undesired results.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure? It would just try to find a falsey value key which would be undefined
. I don't think there's any harm, but I agree it's safer and slightly faster to add a guard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@ljharb I appreciate you taking a look. I'm sure you're busy, but I'd like to work with you to get this merged. I believe it's a nice feature. At the very least, it would help my team quite a bit. |
var Hello = React.createClass({ | ||
render: function() { | ||
const {list} = this.props; | ||
list.push(2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think putting it behind an option is a great compromise - what about two booleans, and two lists of 1) instance methods, and 2) static methods? That way the instance method list could default to ARRAY_MUTATIONS
, and the static method list could default to ['Object.assign', 'Object.defineProperties', 'Object.defineProperty']
- and both booleans would default to false
.
That way most people wouldn't touch the lists at all, they'd just enable the booleans - but those who wanted to update the lists could do so.
* Gets a unique ID for a node | ||
* @param {Object} node The node to get an ID for | ||
*/ | ||
const getId = node => node && node.range.join(':'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'm just not that familiar with what the AST could return. If range
always contains > 1 item, then this is totally safe when node
is truthy.
|
||
let topObject = node; | ||
|
||
// when looking at an object path, get the top-most object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this might be helpful to move to a helper method, so that this function's code can be simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hear you, but since this is only used in one place, I'm not sure the abstraction is worth it.
function setPropMutation(mutation, node) { | ||
const component = components.get(utils.getParentComponent()); | ||
const propMutations = component && component.propMutations | ||
? component.propMutations.concat([mutation]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is mutation
an array? If not, the wrapping brackets aren't needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope! mutation
is not an array, it a node.
lib/rules/no-mutation-props.js
Outdated
|
||
function setPropMutation(mutation, node) { | ||
const component = components.get(utils.getParentComponent()); | ||
const propMutations = component && component.propMutations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make component.propMutations
always start out an empty array, so we don't need this check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could do, but that feels like unnecessary baggage across the rest of the rules if someone opts to not enable this rule. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't imagine that an empty array will slow things down much, compared the the speedup and clarity of not having to ever check for truthiness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Just in case you've got a prop with array mutation methods on it and they're actually safe to use in a react component.
@ljharb okay, I've added an option for |
I'm thinking a schema like this: {
"enableInstanceMethods": true,
"instanceMethods": ["push", "shift"],
"enableStaticMethods": true,
"staticMethod": ["Object.assign", "Object.defineProperty"]
} |
Interesting. A few questions:
|
The value in providing both a boolean and an array is that a shared config (like the airbnb config) can specify the array without having to enable it, and users can enable it without having to copy/paste the array.
|
I'm unsure of who else to ping, so @yannickcr @jseminck @jackyho112 what are your thoughts on #1416 (comment) |
* Removes separate settings for Object and Reflect mutators * Adds lodash mutators as a default (we don't _have_ to do this, but I think it's very low risk, and we should)
docs/rules/no-mutation-props.md
Outdated
|
||
When `true` the rule ignores [`Object` methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/object#Methods_of_the_Object_constructor) that can mutation props. | ||
By default, this is set to: `['Object.assign', 'Object.defineProperty', 'Object.defineProperties', 'Reflect.defineProperty', 'Reflect.deleteProperty', 'Reflect.set', '_.fill', '_.reverse', '_.assign', '_.extend', '_.assignIn', '_.assignInWith', '_.extendWith', '_.assignWith', '_.defaults', '_.defaultsDeep', '_.merge', '_.mergeWith', '_.set', '_.setWith']`. This contains language built-ins that can mutate objects (see: [`Object` methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/object#Methods_of_the_Object_constructor) and [`Reflect` mutation methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Reflects/Reflect#Methods)) as well as methods on [lodash](https://lodash.com) that will mutate either arrays or objects. You can override this with your own set of object/method pairs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think all the lodash/underscore methods should not be included by default; that's presuming that more people use those libs than do.
Tests should certainly ensure these would work, ofc :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I figured you'd say that, so I held off on tests ;)
Okay, I'll move them out, and just have them in the docs for easy reference.
@ljharb okay, options have been changed up and lodash methods removed. We now have just a @yannickcr @jseminck @jackyho112 The githubs tell me that you're also maintainers on this package. I'd love to get your thoughts on disabling array mutations on @ljharb makes the point that we can't do this with 100% safety. It's true, but I contend that the possibility of a We're at a bit of a deadlock, so any of you weighing in would be greatly appreciated. In general, disallowing |
I'm just a contributor 😄 I'll try to go through the whole PR tomorrow and see if I can weigh in, but honestly from following this from the side I have to admit some of the technical discussions are a bit above my technical knowledge level 😞 |
I am merely just a contributor as well. I am not sure if I will have time to review it this week, but I will try my best. 😄 |
@jseminck @jackyho112 whoops sorry about that! Opinions are very welcome though! |
@joeybaker it's been awhile, but if you're still interested in landing this, it'd be great if you rebased it and marked it as ready for review :-) |
@joeybaker @ljharb |
59af733
to
865ed16
Compare
069314a
to
181c68f
Compare
380e32c
to
51d342b
Compare
Fixes #1113
This is based on the work in #1145 but reports on many more cases.
Now warns on:
Object.assign
Object.defineProperty
delete
variable assignment