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

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