no-use-computed-property-like-method.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. /**
  2. * @author tyankatsu <https://github.com/tyankatsu0105>
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const eslintUtils = require('@eslint-community/eslint-utils')
  7. const utils = require('../utils')
  8. /**
  9. * @typedef {import('eslint').Scope.Scope} Scope
  10. * @typedef {import('../utils').ComponentObjectPropertyData} ComponentObjectPropertyData
  11. * @typedef {import('../utils').GroupName} GroupName
  12. */
  13. /**
  14. * @typedef {object} CallMember
  15. * @property {string} name
  16. * @property {CallExpression} node
  17. */
  18. /** @type {Set<GroupName>} */
  19. const GROUPS = new Set(['data', 'props', 'computed', 'methods'])
  20. const NATIVE_NOT_FUNCTION_TYPES = new Set([
  21. 'String',
  22. 'Number',
  23. 'BigInt',
  24. 'Boolean',
  25. 'Object',
  26. 'Array',
  27. 'Symbol'
  28. ])
  29. /**
  30. * @param {RuleContext} context
  31. * @param {Expression} node
  32. * @returns {Set<Expression>}
  33. */
  34. function resolvedExpressions(context, node) {
  35. /** @type {Map<Expression, Set<Expression>>} */
  36. const resolvedMap = new Map()
  37. return resolvedExpressionsInternal(node)
  38. /**
  39. * @param {Expression} node
  40. * @returns {Set<Expression>}
  41. */
  42. function resolvedExpressionsInternal(node) {
  43. let resolvedSet = resolvedMap.get(node)
  44. if (!resolvedSet) {
  45. resolvedSet = new Set()
  46. resolvedMap.set(node, resolvedSet)
  47. for (const e of extractResolvedExpressions(node)) {
  48. resolvedSet.add(e)
  49. }
  50. }
  51. if (resolvedSet.size === 0) {
  52. resolvedSet.add(node)
  53. }
  54. return resolvedSet
  55. }
  56. /**
  57. * @param {Expression} node
  58. * @returns {Iterable<Expression>}
  59. */
  60. function* extractResolvedExpressions(node) {
  61. switch (node.type) {
  62. case 'Identifier': {
  63. const variable = utils.findVariableByIdentifier(context, node)
  64. if (variable) {
  65. for (const ref of variable.references) {
  66. const id = ref.identifier
  67. if (id.parent.type === 'VariableDeclarator') {
  68. if (id.parent.id === id && id.parent.init) {
  69. yield* resolvedExpressionsInternal(id.parent.init)
  70. }
  71. } else if (
  72. id.parent.type === 'AssignmentExpression' &&
  73. id.parent.left === id
  74. ) {
  75. yield* resolvedExpressionsInternal(id.parent.right)
  76. }
  77. }
  78. }
  79. break
  80. }
  81. case 'ConditionalExpression': {
  82. yield* resolvedExpressionsInternal(node.consequent)
  83. yield* resolvedExpressionsInternal(node.alternate)
  84. break
  85. }
  86. case 'LogicalExpression': {
  87. yield* resolvedExpressionsInternal(node.left)
  88. yield* resolvedExpressionsInternal(node.right)
  89. break
  90. }
  91. }
  92. }
  93. }
  94. /**
  95. * Get type of props item.
  96. * Can't consider array props like: props: {propsA: [String, Number, Function]}
  97. * @param {RuleContext} context
  98. * @param {ComponentObjectPropertyData} prop
  99. * @return {string[] | null}
  100. *
  101. * @example
  102. * props: {
  103. * propA: String, // => String
  104. * propB: {
  105. * type: Number // => Number
  106. * },
  107. * }
  108. */
  109. function getComponentPropsTypes(context, prop) {
  110. const result = []
  111. for (const expr of resolvedExpressions(context, prop.property.value)) {
  112. const types = getComponentPropsTypesFromExpression(expr)
  113. if (types == null) {
  114. return null
  115. }
  116. result.push(...types)
  117. }
  118. return result
  119. /**
  120. * @param {Expression} expr
  121. */
  122. function getComponentPropsTypesFromExpression(expr) {
  123. let typeExprs
  124. /**
  125. * Check object props `props: { objectProps: {...} }`
  126. */
  127. if (expr.type === 'ObjectExpression') {
  128. const type = utils.findProperty(expr, 'type')
  129. if (type == null) return null
  130. typeExprs = resolvedExpressions(context, type.value)
  131. } else {
  132. typeExprs = [expr]
  133. }
  134. const result = []
  135. for (const typeExpr of typeExprs) {
  136. const types = getComponentPropsTypesFromTypeExpression(typeExpr)
  137. if (types == null) {
  138. return null
  139. }
  140. result.push(...types)
  141. }
  142. return result
  143. }
  144. /**
  145. * @param {Expression} typeExpr
  146. */
  147. function getComponentPropsTypesFromTypeExpression(typeExpr) {
  148. if (typeExpr.type === 'Identifier') {
  149. return [typeExpr.name]
  150. }
  151. if (typeExpr.type === 'ArrayExpression') {
  152. const types = []
  153. for (const element of typeExpr.elements) {
  154. if (!element) {
  155. continue
  156. }
  157. if (element.type === 'SpreadElement') {
  158. return null
  159. }
  160. for (const elementExpr of resolvedExpressions(context, element)) {
  161. if (elementExpr.type !== 'Identifier') {
  162. return null
  163. }
  164. types.push(elementExpr.name)
  165. }
  166. }
  167. return types
  168. }
  169. return null
  170. }
  171. }
  172. /**
  173. * Check whether given expression may be a function.
  174. * @param {RuleContext} context
  175. * @param {Expression} node
  176. * @returns {boolean}
  177. */
  178. function maybeFunction(context, node) {
  179. for (const expr of resolvedExpressions(context, node)) {
  180. if (
  181. expr.type === 'ObjectExpression' ||
  182. expr.type === 'ArrayExpression' ||
  183. expr.type === 'Literal' ||
  184. expr.type === 'TemplateLiteral' ||
  185. expr.type === 'BinaryExpression' ||
  186. expr.type === 'UnaryExpression' ||
  187. expr.type === 'UpdateExpression'
  188. ) {
  189. continue
  190. }
  191. if (
  192. expr.type === 'ConditionalExpression' &&
  193. !maybeFunction(context, expr.consequent) &&
  194. !maybeFunction(context, expr.alternate)
  195. ) {
  196. continue
  197. }
  198. const evaluated = eslintUtils.getStaticValue(
  199. expr,
  200. utils.getScope(context, expr)
  201. )
  202. if (!evaluated) {
  203. // It could be a function because we don't know what it is.
  204. return true
  205. }
  206. if (typeof evaluated.value === 'function') {
  207. return true
  208. }
  209. }
  210. return false
  211. }
  212. class FunctionData {
  213. /**
  214. * @param {string} name
  215. * @param {'methods' | 'computed'} kind
  216. * @param {FunctionExpression | ArrowFunctionExpression} node
  217. * @param {RuleContext} context
  218. */
  219. constructor(name, kind, node, context) {
  220. this.context = context
  221. this.name = name
  222. this.kind = kind
  223. this.node = node
  224. /** @type {(Expression | null)[]} */
  225. this.returnValues = []
  226. /** @type {boolean | null} */
  227. this.cacheMaybeReturnFunction = null
  228. }
  229. /**
  230. * @param {Expression | null} node
  231. */
  232. addReturnValue(node) {
  233. this.returnValues.push(node)
  234. }
  235. /**
  236. * @param {ComponentStack} component
  237. */
  238. maybeReturnFunction(component) {
  239. if (this.cacheMaybeReturnFunction != null) {
  240. return this.cacheMaybeReturnFunction
  241. }
  242. // Avoid infinite recursion.
  243. this.cacheMaybeReturnFunction = true
  244. return (this.cacheMaybeReturnFunction = this.returnValues.some(
  245. (returnValue) =>
  246. returnValue && component.maybeFunctionExpression(returnValue)
  247. ))
  248. }
  249. }
  250. /** Component information class. */
  251. class ComponentStack {
  252. /**
  253. * @param {ObjectExpression} node
  254. * @param {RuleContext} context
  255. * @param {ComponentStack | null} upper
  256. */
  257. constructor(node, context, upper) {
  258. this.node = node
  259. this.context = context
  260. /** Upper scope component */
  261. this.upper = upper
  262. /** @type {Map<string, boolean>} */
  263. const maybeFunctions = new Map()
  264. /** @type {FunctionData[]} */
  265. const functions = []
  266. // Extract properties
  267. for (const property of utils.iterateProperties(node, GROUPS)) {
  268. if (property.type === 'array') {
  269. continue
  270. }
  271. switch (property.groupName) {
  272. case 'data': {
  273. maybeFunctions.set(
  274. property.name,
  275. maybeFunction(context, property.property.value)
  276. )
  277. break
  278. }
  279. case 'props': {
  280. const types = getComponentPropsTypes(context, property)
  281. maybeFunctions.set(
  282. property.name,
  283. !types || types.some((type) => !NATIVE_NOT_FUNCTION_TYPES.has(type))
  284. )
  285. break
  286. }
  287. case 'computed': {
  288. let value = property.property.value
  289. if (value.type === 'ObjectExpression') {
  290. const getProp = utils.findProperty(value, 'get')
  291. if (getProp) {
  292. value = getProp.value
  293. }
  294. }
  295. processFunction(property.name, value, 'computed')
  296. break
  297. }
  298. case 'methods': {
  299. const value = property.property.value
  300. processFunction(property.name, value, 'methods')
  301. maybeFunctions.set(property.name, true)
  302. break
  303. }
  304. }
  305. }
  306. this.maybeFunctions = maybeFunctions
  307. this.functions = functions
  308. /** @type {CallMember[]} */
  309. this.callMembers = []
  310. /** @type {Map<Expression, boolean>} */
  311. this.cacheMaybeFunctionExpressions = new Map()
  312. /**
  313. * @param {string} name
  314. * @param {Expression} value
  315. * @param {'methods' | 'computed'} kind
  316. */
  317. function processFunction(name, value, kind) {
  318. if (value.type === 'FunctionExpression') {
  319. functions.push(new FunctionData(name, kind, value, context))
  320. } else if (value.type === 'ArrowFunctionExpression') {
  321. const data = new FunctionData(name, kind, value, context)
  322. if (value.expression) {
  323. data.addReturnValue(value.body)
  324. }
  325. functions.push(data)
  326. }
  327. }
  328. }
  329. /**
  330. * Adds the given return statement to the return value of the function.
  331. * @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} scopeFunction
  332. * @param {ReturnStatement} returnNode
  333. */
  334. addReturnStatement(scopeFunction, returnNode) {
  335. for (const data of this.functions) {
  336. if (data.node === scopeFunction) {
  337. data.addReturnValue(returnNode.argument)
  338. break
  339. }
  340. }
  341. }
  342. verifyComponent() {
  343. for (const call of this.callMembers) {
  344. this.verifyCallMember(call)
  345. }
  346. }
  347. /**
  348. * @param {CallMember} call
  349. */
  350. verifyCallMember(call) {
  351. const fnData = this.functions.find(
  352. (data) => data.name === call.name && data.kind === 'computed'
  353. )
  354. if (!fnData) {
  355. // It is not computed, or unknown.
  356. return
  357. }
  358. if (!fnData.maybeReturnFunction(this)) {
  359. const prefix = call.node.callee.type === 'MemberExpression' ? 'this.' : ''
  360. this.context.report({
  361. node: call.node,
  362. messageId: 'unexpected',
  363. data: {
  364. likeProperty: `${prefix}${call.name}`,
  365. likeMethod: `${prefix}${call.name}()`
  366. }
  367. })
  368. }
  369. }
  370. /**
  371. * Check whether given expression may be a function.
  372. * @param {Expression} node
  373. * @returns {boolean}
  374. */
  375. maybeFunctionExpression(node) {
  376. const cache = this.cacheMaybeFunctionExpressions.get(node)
  377. if (cache != null) {
  378. return cache
  379. }
  380. // Avoid infinite recursion.
  381. this.cacheMaybeFunctionExpressions.set(node, true)
  382. const result = maybeFunctionExpressionWithoutCache.call(this)
  383. this.cacheMaybeFunctionExpressions.set(node, result)
  384. return result
  385. /**
  386. * @this {ComponentStack}
  387. */
  388. function maybeFunctionExpressionWithoutCache() {
  389. for (const expr of resolvedExpressions(this.context, node)) {
  390. if (!maybeFunction(this.context, expr)) {
  391. continue
  392. }
  393. switch (expr.type) {
  394. case 'MemberExpression': {
  395. if (utils.isThis(expr.object, this.context)) {
  396. const name = utils.getStaticPropertyName(expr)
  397. if (name && !this.maybeFunctionProperty(name)) {
  398. continue
  399. }
  400. }
  401. break
  402. }
  403. case 'CallExpression': {
  404. if (
  405. expr.callee.type === 'MemberExpression' &&
  406. utils.isThis(expr.callee.object, this.context)
  407. ) {
  408. const name = utils.getStaticPropertyName(expr.callee)
  409. const fnData = this.functions.find((data) => data.name === name)
  410. if (
  411. fnData &&
  412. fnData.kind === 'methods' &&
  413. !fnData.maybeReturnFunction(this)
  414. ) {
  415. continue
  416. }
  417. }
  418. break
  419. }
  420. case 'ConditionalExpression': {
  421. if (
  422. !this.maybeFunctionExpression(expr.consequent) &&
  423. !this.maybeFunctionExpression(expr.alternate)
  424. ) {
  425. continue
  426. }
  427. break
  428. }
  429. }
  430. // It could be a function because we don't know what it is.
  431. return true
  432. }
  433. return false
  434. }
  435. }
  436. /**
  437. * Check whether given property name may be a function.
  438. * @param {string} name
  439. * @returns {boolean}
  440. */
  441. maybeFunctionProperty(name) {
  442. const cache = this.maybeFunctions.get(name)
  443. if (cache != null) {
  444. return cache
  445. }
  446. // Avoid infinite recursion.
  447. this.maybeFunctions.set(name, true)
  448. const result = maybeFunctionPropertyWithoutCache.call(this)
  449. this.maybeFunctions.set(name, result)
  450. return result
  451. /**
  452. * @this {ComponentStack}
  453. */
  454. function maybeFunctionPropertyWithoutCache() {
  455. const fnData = this.functions.find((data) => data.name === name)
  456. if (fnData && fnData.kind === 'computed') {
  457. return fnData.maybeReturnFunction(this)
  458. }
  459. // It could be a function because we don't know what it is.
  460. return true
  461. }
  462. }
  463. }
  464. module.exports = {
  465. meta: {
  466. type: 'problem',
  467. docs: {
  468. description: 'disallow use computed property like method',
  469. categories: ['vue3-essential', 'vue2-essential'],
  470. url: 'https://eslint.vuejs.org/rules/no-use-computed-property-like-method.html'
  471. },
  472. fixable: null,
  473. schema: [],
  474. messages: {
  475. unexpected: 'Use {{ likeProperty }} instead of {{ likeMethod }}.'
  476. }
  477. },
  478. /** @param {RuleContext} context */
  479. create(context) {
  480. /**
  481. * @typedef {object} ScopeStack
  482. * @property {ScopeStack | null} upper
  483. * @property {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} scopeNode
  484. */
  485. /** @type {ScopeStack | null} */
  486. let scopeStack = null
  487. /** @type {ComponentStack | null} */
  488. let componentStack = null
  489. /** @type {ComponentStack | null} */
  490. let templateComponent = null
  491. return utils.compositingVisitors(
  492. {},
  493. utils.defineVueVisitor(context, {
  494. onVueObjectEnter(node) {
  495. componentStack = new ComponentStack(node, context, componentStack)
  496. if (!templateComponent && utils.isInExportDefault(node)) {
  497. templateComponent = componentStack
  498. }
  499. },
  500. onVueObjectExit() {
  501. if (componentStack) {
  502. componentStack.verifyComponent()
  503. componentStack = componentStack.upper
  504. }
  505. },
  506. /**
  507. * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
  508. */
  509. ':function'(node) {
  510. scopeStack = {
  511. upper: scopeStack,
  512. scopeNode: node
  513. }
  514. },
  515. ReturnStatement(node) {
  516. if (scopeStack && componentStack) {
  517. componentStack.addReturnStatement(scopeStack.scopeNode, node)
  518. }
  519. },
  520. ':function:exit'() {
  521. scopeStack = scopeStack && scopeStack.upper
  522. },
  523. /**
  524. * @param {ThisExpression | Identifier} node
  525. */
  526. 'ThisExpression, Identifier'(node) {
  527. if (
  528. !componentStack ||
  529. node.parent.type !== 'MemberExpression' ||
  530. node.parent.object !== node ||
  531. node.parent.parent.type !== 'CallExpression' ||
  532. node.parent.parent.callee !== node.parent ||
  533. !utils.isThis(node, context)
  534. ) {
  535. return
  536. }
  537. const name = utils.getStaticPropertyName(node.parent)
  538. if (name) {
  539. componentStack.callMembers.push({
  540. name,
  541. node: node.parent.parent
  542. })
  543. }
  544. }
  545. }),
  546. utils.defineTemplateBodyVisitor(context, {
  547. /**
  548. * @param {VExpressionContainer} node
  549. */
  550. VExpressionContainer(node) {
  551. if (!templateComponent) {
  552. return
  553. }
  554. for (const id of node.references
  555. .filter((ref) => ref.variable == null)
  556. .map((ref) => ref.id)) {
  557. if (
  558. id.parent.type !== 'CallExpression' ||
  559. id.parent.callee !== id
  560. ) {
  561. continue
  562. }
  563. templateComponent.verifyCallMember({
  564. name: id.name,
  565. node: id.parent
  566. })
  567. }
  568. }
  569. })
  570. )
  571. }
  572. }