Skip to content

Commit 587d808

Browse files
committed
feat: check return type of data props methods computed
1 parent c61acae commit 587d808

File tree

3 files changed

+639
-62
lines changed

3 files changed

+639
-62
lines changed

docs/rules/no-use-computed-property-like-method.md

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,83 @@ This rule disallows to use computed property like method.
2121
2222
<script>
2323
export default {
24+
data() {
25+
return {
26+
dataString: 'dataString',
27+
dataNumber: 10,
28+
dataObject: {
29+
inside: "inside"
30+
},
31+
dataArray: [1,2,3,4,5],
32+
dataBoolean: true,
33+
dataFunction() {
34+
alert('dataFunction')
35+
}
36+
}
37+
},
2438
props: {
25-
name: {
39+
propsString: {
2640
type: String
2741
},
42+
propsNumber: {
43+
type: Number
44+
},
45+
propsObject: {
46+
type: Object
47+
},
48+
propsArray: {
49+
type: Array
50+
},
51+
propsBoolean: {
52+
type: Boolean
53+
},
54+
propsFunction: {
55+
type: Function
56+
},
2857
},
2958
computed: {
30-
isExpectedName() {
31-
return this.name === 'name';
59+
computedReturnString() {
60+
return 'computedReturnString'
61+
},
62+
computedReturnNumber() {
63+
return 10
64+
},
65+
computedReturnObject() {
66+
return {
67+
inside: "inside"
68+
}
69+
},
70+
computedReturnArray() {
71+
return [1,2,3,4,5]
72+
},
73+
computedReturnBoolean() {
74+
return true
75+
},
76+
computedReturnFunction() {
77+
const fn = () => alert('computedReturnFunction')
78+
return fn
3279
}
3380
},
3481
methods: {
35-
getName() {
36-
return this.isExpectedName
82+
methodsReturnString() {
83+
return 'methodsReturnString'
84+
},
85+
methodsReturnNumber() {
86+
return 'methodsReturnNumber'
87+
},
88+
methodsReturnObject() {
89+
return {
90+
inside: "inside"
91+
}
92+
},
93+
methodsReturnArray() {
94+
return [1,2,3,4,5]
95+
},
96+
methodsReturnBoolean() {
97+
return true
3798
},
38-
getNameCallLikeMethod() {
39-
return this.isExpectedName()
99+
methodsReturnFunction() {
100+
console.log(this.dataObject.inside);
40101
}
41102
}
42103
}

lib/rules/no-use-computed-property-like-method.js

Lines changed: 232 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,198 @@
77
// ------------------------------------------------------------------------------
88
// Requirements
99
// ------------------------------------------------------------------------------
10-
11-
const { defineVueVisitor, getComputedProperties } = require('../utils')
10+
const eslitUtils = require('eslint-utils')
11+
const utils = require('../utils')
1212

1313
/**
1414
* @typedef {import('../utils').ComponentComputedProperty} ComponentComputedProperty
1515
* @typedef {import('../utils').ComponentObjectProp} ComponentObjectProp
16+
* @typedef {import('../utils').ComponentPropertyData} ComponentPropertyData
17+
* @typedef {import('../utils').ComponentObjectPropertyData} ComponentObjectPropertyData
18+
*
19+
* @typedef {{[key: string]: ComponentPropertyData & { valueType: { type: string } }}} ProprtyMap
1620
*/
17-
1821
// ------------------------------------------------------------------------------
1922
// Rule Definition
2023
// ------------------------------------------------------------------------------
2124

25+
function replacer(key, value) {
26+
if (key === 'parent') {
27+
return undefined
28+
}
29+
if (key === 'errors' && Array.isArray(value)) {
30+
return value.map((e) => ({
31+
message: e.message,
32+
index: e.index,
33+
lineNumber: e.lineNumber,
34+
column: e.column
35+
}))
36+
}
37+
return value
38+
}
39+
40+
/**
41+
*
42+
* @param {ComponentObjectPropertyData} property
43+
* @return {string | null}
44+
*/
45+
const getComponetPropsType = (property) => {
46+
if (property.property.value.type === 'ObjectExpression') {
47+
const propsTypeProperty = property.property.value.properties.find(
48+
(property) =>
49+
property.type === 'Property' &&
50+
property.key.type === 'Identifier' &&
51+
property.key.name === 'type' &&
52+
property.value.type === 'Identifier'
53+
)
54+
55+
if (propsTypeProperty === undefined) return null
56+
57+
if (
58+
propsTypeProperty.type === 'Property' &&
59+
propsTypeProperty.value.type === 'Identifier'
60+
)
61+
return propsTypeProperty.value.name
62+
}
63+
return null
64+
}
65+
66+
/**
67+
*
68+
* @param {any} obj
69+
*/
70+
const getPrototypeType = (obj) =>
71+
Object.prototype.toString.call(obj).slice(8, -1)
72+
73+
/**
74+
*
75+
* @param {Expression | Super} objectOrCallee
76+
* @returns {string | null}
77+
*/
78+
// const getThisMember = (objectOrCallee) => {
79+
// if (objectOrCallee.type === 'MemberExpression') {
80+
// if (objectOrCallee.object.type === 'Identifier') return null
81+
82+
// if (
83+
// objectOrCallee.object.type === 'ThisExpression' &&
84+
// objectOrCallee.property.type === 'Identifier'
85+
// )
86+
// return objectOrCallee.property.name
87+
88+
// if (objectOrCallee.object.type === 'MemberExpression')
89+
// getThisMember(objectOrCallee.object)
90+
// }
91+
92+
// if (objectOrCallee.type === 'CallExpression') {
93+
// if (objectOrCallee.callee.type === 'Identifier') return null
94+
95+
// getThisMember(objectOrCallee.callee)
96+
// }
97+
// }
98+
99+
/**
100+
* Get return type of property.
101+
* @param {{ property: ComponentPropertyData, propertyMap: ProprtyMap }} args
102+
*/
103+
const getValueType = ({ property, propertyMap }) => {
104+
if (property.type === 'array') {
105+
return {
106+
type: null
107+
}
108+
}
109+
110+
if (property.type === 'object') {
111+
if (property.groupName === 'props') {
112+
return {
113+
type: getComponetPropsType(property)
114+
}
115+
}
116+
117+
if (property.groupName === 'computed' || property.groupName === 'methods') {
118+
if (
119+
property.property.value.type === 'FunctionExpression' &&
120+
property.property.value.body.type === 'BlockStatement'
121+
) {
122+
const blockStatement = property.property.value.body
123+
124+
/**
125+
* Only check return statement inside computed and methods
126+
*/
127+
const returnStatement = blockStatement.body.find(
128+
(b) => b.type === 'ReturnStatement'
129+
)
130+
if (!returnStatement || returnStatement.type !== 'ReturnStatement')
131+
return
132+
133+
// if (property.groupName === 'computed') {
134+
// if (
135+
// propertyMap &&
136+
// propertyMap[property.name] &&
137+
// returnStatement.argument
138+
// ) {
139+
// const thisMember = getThisMember(returnStatement.argument)
140+
// return {
141+
// type: propertyMap[thisMember].valueType.type
142+
// }
143+
// }
144+
// }
145+
146+
/**
147+
* TODO: consider this.xxx.xxx().xxx.xxx().xxx().....
148+
*/
149+
if (property.groupName === 'computed') {
150+
if (
151+
propertyMap &&
152+
propertyMap[property.name] &&
153+
returnStatement.argument &&
154+
returnStatement.argument.type === 'MemberExpression' &&
155+
returnStatement.argument.property.type === 'Identifier'
156+
)
157+
return {
158+
type:
159+
propertyMap[returnStatement.argument.property.name].valueType
160+
.type
161+
}
162+
163+
if (
164+
propertyMap &&
165+
propertyMap[property.name] &&
166+
returnStatement.argument &&
167+
returnStatement.argument.type === 'CallExpression' &&
168+
returnStatement.argument.callee.type === 'MemberExpression' &&
169+
returnStatement.argument.callee.property.type === 'Identifier'
170+
)
171+
return {
172+
type:
173+
propertyMap[returnStatement.argument.callee.property.name]
174+
.valueType.type
175+
}
176+
}
177+
178+
const evaluated = eslitUtils.getStaticValue(returnStatement.argument)
179+
180+
if (evaluated) {
181+
return {
182+
type: getPrototypeType(evaluated.value)
183+
}
184+
}
185+
}
186+
}
187+
188+
const evaluated = eslitUtils.getStaticValue(property.property.value)
189+
190+
if (evaluated) {
191+
return {
192+
type: getPrototypeType(evaluated.value)
193+
}
194+
}
195+
196+
return {
197+
type: null
198+
}
199+
}
200+
}
201+
22202
module.exports = {
23203
meta: {
24204
type: 'problem',
@@ -31,43 +211,67 @@ module.exports = {
31211
fixable: null,
32212
schema: [],
33213
messages: {
34-
unexpected: 'Does not allow to use computed with this expression.'
214+
unexpected: 'Use {{ likeProperty }} instead of {{ likeMethod }}.'
35215
}
36216
},
37217
/** @param {RuleContext} context */
38218
create(context) {
39-
/**
40-
* @typedef {object} ScopeStack
41-
* @property {ScopeStack | null} upper
42-
* @property {BlockStatement | Expression} body
43-
*/
44-
/** @type {Map<ObjectExpression, ComponentComputedProperty[]>} */
45-
const computedPropertiesMap = new Map()
46-
47-
return defineVueVisitor(context, {
219+
const GROUP_NAMES = ['data', 'props', 'computed', 'methods']
220+
const groups = new Set(GROUP_NAMES)
221+
222+
/** @type ProprtyMap */
223+
const propertyMap = {}
224+
225+
/**@type ObjectExpression */
226+
let nodeMap = {}
227+
228+
return utils.defineVueVisitor(context, {
48229
onVueObjectEnter(node) {
49-
computedPropertiesMap.set(node, getComputedProperties(node))
230+
nodeMap = node
231+
const properties = utils.iterateProperties(node, groups)
232+
233+
for (const property of properties) {
234+
propertyMap[property.name] = {
235+
...propertyMap[property.name],
236+
...property,
237+
valueType: getValueType({ property })
238+
}
239+
}
50240
},
51241

52242
/** @param {MemberExpression} node */
53-
'MemberExpression[object.type="ThisExpression"]'(
54-
node,
55-
{ node: vueNode }
56-
) {
57-
if (node.property.type !== 'Identifier') return
243+
'MemberExpression[object.type="ThisExpression"]'(node) {
58244
if (node.parent.type !== 'CallExpression') return
245+
if (node.property.type !== 'Identifier') return
246+
247+
const properties = utils.iterateProperties(nodeMap, groups)
248+
249+
for (const property of properties) {
250+
propertyMap[property.name] = {
251+
...propertyMap[property.name],
252+
...property,
253+
valueType: getValueType({ property, propertyMap })
254+
}
255+
}
59256

60-
const computedProperties = computedPropertiesMap
61-
.get(vueNode)
62-
.map((item) => item.key)
257+
const thisMember = node.property.name
63258

64-
if (!computedProperties.includes(node.property.name)) return
259+
if (!propertyMap[thisMember].valueType.type) return
65260

66-
context.report({
67-
node: node.property,
68-
loc: node.property.loc,
69-
messageId: 'unexpected'
70-
})
261+
if (
262+
propertyMap[thisMember].groupName === 'computed' &&
263+
propertyMap[thisMember].valueType.type !== 'Function'
264+
) {
265+
context.report({
266+
node,
267+
loc: node.loc,
268+
messageId: 'unexpected',
269+
data: {
270+
likeProperty: `this.${thisMember}`,
271+
likeMethod: `this.${thisMember}()`
272+
}
273+
})
274+
}
71275
}
72276
})
73277
}

0 commit comments

Comments
 (0)