no-mutating-props.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. /**
  2. * @fileoverview disallow mutation component props
  3. * @author 2018 Armano
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. const { findVariable } = require('eslint-utils')
  8. // ------------------------------------------------------------------------------
  9. // Rule Definition
  10. // ------------------------------------------------------------------------------
  11. module.exports = {
  12. meta: {
  13. type: 'suggestion',
  14. docs: {
  15. description: 'disallow mutation of component props',
  16. categories: ['vue3-essential', 'essential'],
  17. url: 'https://eslint.vuejs.org/rules/no-mutating-props.html'
  18. },
  19. fixable: null, // or "code" or "whitespace"
  20. schema: [
  21. // fill in your schema
  22. ]
  23. },
  24. /** @param {RuleContext} context */
  25. create(context) {
  26. /** @type {Map<ObjectExpression, Set<string>>} */
  27. const propsMap = new Map()
  28. /** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression } | null } */
  29. let vueObjectData = null
  30. /**
  31. * @param {ASTNode} node
  32. * @param {string} name
  33. */
  34. function report(node, name) {
  35. context.report({
  36. node,
  37. message: 'Unexpected mutation of "{{key}}" prop.',
  38. data: {
  39. key: name
  40. }
  41. })
  42. }
  43. /**
  44. * @param {ASTNode} node
  45. * @returns {VExpressionContainer}
  46. */
  47. function getVExpressionContainer(node) {
  48. let n = node
  49. while (n.type !== 'VExpressionContainer') {
  50. n = /** @type {ASTNode} */ (n.parent)
  51. }
  52. return n
  53. }
  54. /**
  55. * @param {MemberExpression|AssignmentProperty} node
  56. * @returns {string}
  57. */
  58. function getPropertyNameText(node) {
  59. const name = utils.getStaticPropertyName(node)
  60. if (name) {
  61. return name
  62. }
  63. if (node.computed) {
  64. const expr = node.type === 'Property' ? node.key : node.property
  65. const str = context.getSourceCode().getText(expr)
  66. return `[${str}]`
  67. }
  68. return '?unknown?'
  69. }
  70. /**
  71. * @param {ASTNode} node
  72. * @returns {node is Identifier}
  73. */
  74. function isVmReference(node) {
  75. if (node.type !== 'Identifier') {
  76. return false
  77. }
  78. const parent = node.parent
  79. if (parent.type === 'MemberExpression') {
  80. if (parent.property === node) {
  81. // foo.id
  82. return false
  83. }
  84. } else if (parent.type === 'Property') {
  85. // {id: foo}
  86. if (parent.key === node && !parent.computed) {
  87. return false
  88. }
  89. }
  90. const exprContainer = getVExpressionContainer(node)
  91. for (const reference of exprContainer.references) {
  92. if (reference.variable != null) {
  93. // Not vm reference
  94. continue
  95. }
  96. if (reference.id === node) {
  97. return true
  98. }
  99. }
  100. return false
  101. }
  102. /**
  103. * @param {MemberExpression|Identifier} props
  104. * @param {string} name
  105. */
  106. function verifyMutating(props, name) {
  107. const invalid = utils.findMutating(props)
  108. if (invalid) {
  109. report(invalid.node, name)
  110. }
  111. }
  112. /**
  113. * @param {Pattern} param
  114. * @param {string[]} path
  115. * @returns {Generator<{ node: Identifier, path: string[] }>}
  116. */
  117. function* iterateParamProperties(param, path) {
  118. if (!param) {
  119. return
  120. }
  121. if (param.type === 'Identifier') {
  122. yield {
  123. node: param,
  124. path
  125. }
  126. } else if (param.type === 'RestElement') {
  127. yield* iterateParamProperties(param.argument, path)
  128. } else if (param.type === 'AssignmentPattern') {
  129. yield* iterateParamProperties(param.left, path)
  130. } else if (param.type === 'ObjectPattern') {
  131. for (const prop of param.properties) {
  132. if (prop.type === 'Property') {
  133. const name = getPropertyNameText(prop)
  134. yield* iterateParamProperties(prop.value, [...path, name])
  135. } else if (prop.type === 'RestElement') {
  136. yield* iterateParamProperties(prop.argument, path)
  137. }
  138. }
  139. } else if (param.type === 'ArrayPattern') {
  140. for (let index = 0; index < param.elements.length; index++) {
  141. const element = param.elements[index]
  142. yield* iterateParamProperties(element, [...path, `${index}`])
  143. }
  144. }
  145. }
  146. return Object.assign(
  147. {},
  148. utils.defineVueVisitor(context, {
  149. onVueObjectEnter(node) {
  150. propsMap.set(
  151. node,
  152. new Set(
  153. utils
  154. .getComponentProps(node)
  155. .map((p) => p.propName)
  156. .filter(utils.isDef)
  157. )
  158. )
  159. },
  160. onVueObjectExit(node, { type }) {
  161. if (
  162. (!vueObjectData || vueObjectData.type !== 'export') &&
  163. type !== 'instance'
  164. ) {
  165. vueObjectData = {
  166. type,
  167. object: node
  168. }
  169. }
  170. },
  171. onSetupFunctionEnter(node) {
  172. const propsParam = node.params[0]
  173. if (!propsParam) {
  174. // no arguments
  175. return
  176. }
  177. if (
  178. propsParam.type === 'RestElement' ||
  179. propsParam.type === 'ArrayPattern'
  180. ) {
  181. // cannot check
  182. return
  183. }
  184. for (const { node: prop, path } of iterateParamProperties(
  185. propsParam,
  186. []
  187. )) {
  188. const variable = findVariable(context.getScope(), prop)
  189. if (!variable) {
  190. continue
  191. }
  192. for (const reference of variable.references) {
  193. if (!reference.isRead()) {
  194. continue
  195. }
  196. const id = reference.identifier
  197. const invalid = utils.findMutating(id)
  198. if (!invalid) {
  199. continue
  200. }
  201. let name
  202. if (path.length === 0) {
  203. if (invalid.pathNodes.length === 0) {
  204. continue
  205. }
  206. const mem = invalid.pathNodes[0]
  207. name = getPropertyNameText(mem)
  208. } else {
  209. if (invalid.pathNodes.length === 0 && invalid.kind !== 'call') {
  210. continue
  211. }
  212. name = path[0]
  213. }
  214. report(invalid.node, name)
  215. }
  216. }
  217. },
  218. /** @param {(Identifier | ThisExpression) & { parent: MemberExpression } } node */
  219. 'MemberExpression > :matches(Identifier, ThisExpression)'(
  220. node,
  221. { node: vueNode }
  222. ) {
  223. if (!utils.isThis(node, context)) {
  224. return
  225. }
  226. const mem = node.parent
  227. if (mem.object !== node) {
  228. return
  229. }
  230. const name = utils.getStaticPropertyName(mem)
  231. if (
  232. name &&
  233. /** @type {Set<string>} */ (propsMap.get(vueNode)).has(name)
  234. ) {
  235. verifyMutating(mem, name)
  236. }
  237. }
  238. }),
  239. utils.defineTemplateBodyVisitor(context, {
  240. /** @param {ThisExpression & { parent: MemberExpression } } node */
  241. 'VExpressionContainer MemberExpression > ThisExpression'(node) {
  242. if (!vueObjectData) {
  243. return
  244. }
  245. const mem = node.parent
  246. if (mem.object !== node) {
  247. return
  248. }
  249. const name = utils.getStaticPropertyName(mem)
  250. if (
  251. name &&
  252. /** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
  253. name
  254. )
  255. ) {
  256. verifyMutating(mem, name)
  257. }
  258. },
  259. /** @param {Identifier } node */
  260. 'VExpressionContainer Identifier'(node) {
  261. if (!vueObjectData) {
  262. return
  263. }
  264. if (!isVmReference(node)) {
  265. return
  266. }
  267. const name = node.name
  268. if (
  269. name &&
  270. /** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
  271. name
  272. )
  273. ) {
  274. verifyMutating(node, name)
  275. }
  276. },
  277. /** @param {ESNode} node */
  278. "VAttribute[directive=true][key.name.name='model'] VExpressionContainer > *"(
  279. node
  280. ) {
  281. if (!vueObjectData) {
  282. return
  283. }
  284. const nodes = utils.getMemberChaining(node)
  285. const first = nodes[0]
  286. let name
  287. if (isVmReference(first)) {
  288. name = first.name
  289. } else if (first.type === 'ThisExpression') {
  290. const mem = nodes[1]
  291. if (!mem) {
  292. return
  293. }
  294. name = utils.getStaticPropertyName(mem)
  295. } else {
  296. return
  297. }
  298. if (
  299. name &&
  300. /** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
  301. name
  302. )
  303. ) {
  304. report(node, name)
  305. }
  306. }
  307. })
  308. )
  309. }
  310. }