Skip to content

Commit

Permalink
Add no-mutating-props rule.
Browse files Browse the repository at this point in the history
  • Loading branch information
armano2 committed Nov 8, 2018
1 parent 1b5a799 commit 9c6a762
Show file tree
Hide file tree
Showing 5 changed files with 519 additions and 8 deletions.
139 changes: 139 additions & 0 deletions lib/rules/no-mutating-props.js
@@ -0,0 +1,139 @@
/**
* @fileoverview Check if component props are not mutated
* @author 2018 Armano
*/
'use strict'

const utils = require('../utils')

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

module.exports = {
meta: {
docs: {
description: 'disallow mutation of props',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.3/docs/rules/no-mutating-props.md'
},
fixable: null, // or "code" or "whitespace"
schema: [
// fill in your schema
]
},

create (context) {
let mutatedNodes = []
let props = []

function checkForMutations () {
for (const prop of props) {
const propName = utils.getStaticPropertyName(prop.key)

for (const node of mutatedNodes) {
if (propName === node.name) {
context.report({
node: node.node,
message: 'Unexpected mutation of "{{key}}" prop.',
data: { key: node.name }
})
}
}
}
mutatedNodes = []
}

function checkTemplateProperty (node) {
if (node.type === 'MemberExpression') {
const expression = utils.parseMemberExpression(node)
mutatedNodes.push({
name: expression[0] === 'this' ? expression[1] : expression[0],
node
})
} else if (node.type === 'Identifier') {
mutatedNodes.push({
name: node.name,
node
})
}
}

return Object.assign({},
{
// this.xxx <=|+=|-=>
'AssignmentExpression' (node) {
if (node.left.type !== 'MemberExpression') return
const expression = utils.parseMemberExpression(node.left)
if (expression[0] === 'this') {
mutatedNodes.push({
name: expression[1],
node
})
}
},
// this.xxx <++|-->
'UpdateExpression > MemberExpression' (node) {
const expression = utils.parseMemberExpression(node)
if (expression[0] === 'this') {
mutatedNodes.push({
name: expression[1],
node
})
}
},
// this.xxx.func()
'CallExpression' (node) {
const expression = utils.parseMemberOrCallExpression(node)
const code = expression.join('.').replace(/\.\[/g, '[')
const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g

if (MUTATION_REGEX.test(code)) {
if (expression[0] === 'this') {
mutatedNodes.push({
name: expression[1],
node
})
}
}
}
},
utils.executeOnVue(context, (obj) => {
props = utils.getComponentProps(obj)
.filter(cp => cp.key)
checkForMutations()
}),

utils.defineTemplateBodyVisitor(context, {
'VExpressionContainer AssignmentExpression' (node) {
checkTemplateProperty(node.left)
},
// this.xxx <++|-->
'VExpressionContainer UpdateExpression' (node) {
checkTemplateProperty(node.argument)
},
// this.xxx.func()
'VExpressionContainer CallExpression' (node) {
const expression = utils.parseMemberOrCallExpression(node)
const code = expression.join('.').replace(/\.\[/g, '[')
const MUTATION_REGEX = /(this.)?((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g

if (MUTATION_REGEX.test(code)) {
mutatedNodes.push({
name: expression[0] === 'this' ? expression[1] : expression[0],
node
})
}
},

"VAttribute[directive=true][key.name='model'] VExpressionContainer" (node) {
checkTemplateProperty(node.expression)
},

"VElement[name='template']:exit" () {
checkForMutations()
}
})
)
}
}
11 changes: 7 additions & 4 deletions lib/rules/no-side-effects-in-computed-properties.js
Expand Up @@ -42,6 +42,9 @@ module.exports = {
// this.xxx.func()
'CallExpression' (node) {
const code = utils.parseMemberOrCallExpression(node)
.join('.')
.replace(/\.\[/g, '[')

const MUTATION_REGEX = /(this.)((?!(concat|slice|map|filter)\().)[^\)]*((push|pop|shift|unshift|reverse|splice|sort|copyWithin|fill)\()/g

if (MUTATION_REGEX.test(code)) {
Expand All @@ -52,8 +55,8 @@ module.exports = {
utils.executeOnVue(context, (obj) => {
const computedProperties = utils.getComputedProperties(obj)

computedProperties.forEach(cp => {
forbiddenNodes.forEach(node => {
for (const cp of computedProperties) {
for (const node of forbiddenNodes) {
if (
cp.value &&
node.loc.start.line >= cp.value.loc.start.line &&
Expand All @@ -65,8 +68,8 @@ module.exports = {
data: { key: cp.key }
})
}
})
})
}
}
})
)
}
Expand Down
41 changes: 39 additions & 2 deletions lib/utils/index.js
Expand Up @@ -365,9 +365,46 @@ module.exports = {
return null
},

/**
* Get all props by looking at all component's properties
* @param {ObjectExpression} componentObject Object with component definition
* @return {Array} Array of component props in format: [{key?: String, value?: ASTNode, node: ASTNod}]
*/
getComponentProps (componentObject) {
const propsNode = componentObject.properties
.find(p =>
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'props' &&
(p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression')
)

if (!propsNode) {
return []
}

let props

if (propsNode.value.type === 'ObjectExpression') {
props = propsNode.value.properties
.filter(cp => cp.type === 'Property')
.map(cp => {
return { key: cp.key, value: this.unwrapTypes(cp.value), node: cp }
})
} else {
props = propsNode.value.elements
.map(cp => {
const key = cp.type === 'Literal' && typeof cp.value === 'string' ? cp : null
return { key, value: null, node: cp }
})
}

return props
},

/**
* Get all computed properties by looking at all component's properties
* @param {ObjectExpression} Object with component definition
* @param {ObjectExpression} componentObject Object with component definition
* @return {Array} Array of computed properties in format: [{key: String, value: ASTNode}]
*/
getComputedProperties (componentObject) {
Expand Down Expand Up @@ -710,7 +747,7 @@ module.exports = {
parsedCallee.push('this')
}

return parsedCallee.reverse().join('.').replace(/\.\[/g, '[')
return parsedCallee.reverse()
},

/**
Expand Down

0 comments on commit 9c6a762

Please sign in to comment.