no-mutating-props.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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|CallExpression, Set<string>>} */
  27. const propsMap = new Map()
  28. /** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression } | { type: 'setup', object: CallExpression } | 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* iteratePatternProperties(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* iteratePatternProperties(param.argument, path)
  128. } else if (param.type === 'AssignmentPattern') {
  129. yield* iteratePatternProperties(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* iteratePatternProperties(prop.value, [...path, name])
  135. } else if (prop.type === 'RestElement') {
  136. yield* iteratePatternProperties(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* iteratePatternProperties(element, [...path, `${index}`])
  143. }
  144. }
  145. }
  146. /**
  147. * @param {Identifier} prop
  148. * @param {string[]} path
  149. */
  150. function verifyPropVariable(prop, path) {
  151. const variable = findVariable(context.getScope(), prop)
  152. if (!variable) {
  153. return
  154. }
  155. for (const reference of variable.references) {
  156. if (!reference.isRead()) {
  157. continue
  158. }
  159. const id = reference.identifier
  160. const invalid = utils.findMutating(id)
  161. if (!invalid) {
  162. continue
  163. }
  164. let name
  165. if (path.length === 0) {
  166. if (invalid.pathNodes.length === 0) {
  167. continue
  168. }
  169. const mem = invalid.pathNodes[0]
  170. name = getPropertyNameText(mem)
  171. } else {
  172. if (invalid.pathNodes.length === 0 && invalid.kind !== 'call') {
  173. continue
  174. }
  175. name = path[0]
  176. }
  177. report(invalid.node, name)
  178. }
  179. }
  180. return utils.compositingVisitors(
  181. {},
  182. utils.defineScriptSetupVisitor(context, {
  183. onDefinePropsEnter(node, props) {
  184. const propsSet = new Set(
  185. props.map((p) => p.propName).filter(utils.isDef)
  186. )
  187. propsMap.set(node, propsSet)
  188. vueObjectData = {
  189. type: 'setup',
  190. object: node
  191. }
  192. let target = node
  193. if (
  194. target.parent &&
  195. target.parent.type === 'CallExpression' &&
  196. target.parent.arguments[0] === target &&
  197. target.parent.callee.type === 'Identifier' &&
  198. target.parent.callee.name === 'withDefaults'
  199. ) {
  200. target = target.parent
  201. }
  202. if (
  203. !target.parent ||
  204. target.parent.type !== 'VariableDeclarator' ||
  205. target.parent.init !== target
  206. ) {
  207. return
  208. }
  209. for (const { node: prop, path } of iteratePatternProperties(
  210. target.parent.id,
  211. []
  212. )) {
  213. verifyPropVariable(prop, path)
  214. propsSet.add(prop.name)
  215. }
  216. }
  217. }),
  218. utils.defineVueVisitor(context, {
  219. onVueObjectEnter(node) {
  220. propsMap.set(
  221. node,
  222. new Set(
  223. utils
  224. .getComponentProps(node)
  225. .map((p) => p.propName)
  226. .filter(utils.isDef)
  227. )
  228. )
  229. },
  230. onVueObjectExit(node, { type }) {
  231. if (
  232. (!vueObjectData ||
  233. (vueObjectData.type !== 'export' &&
  234. vueObjectData.type !== 'setup')) &&
  235. type !== 'instance'
  236. ) {
  237. vueObjectData = {
  238. type,
  239. object: node
  240. }
  241. }
  242. },
  243. onSetupFunctionEnter(node) {
  244. const propsParam = node.params[0]
  245. if (!propsParam) {
  246. // no arguments
  247. return
  248. }
  249. if (
  250. propsParam.type === 'RestElement' ||
  251. propsParam.type === 'ArrayPattern'
  252. ) {
  253. // cannot check
  254. return
  255. }
  256. for (const { node: prop, path } of iteratePatternProperties(
  257. propsParam,
  258. []
  259. )) {
  260. verifyPropVariable(prop, path)
  261. }
  262. },
  263. /** @param {(Identifier | ThisExpression) & { parent: MemberExpression } } node */
  264. 'MemberExpression > :matches(Identifier, ThisExpression)'(
  265. node,
  266. { node: vueNode }
  267. ) {
  268. if (!utils.isThis(node, context)) {
  269. return
  270. }
  271. const mem = node.parent
  272. if (mem.object !== node) {
  273. return
  274. }
  275. const name = utils.getStaticPropertyName(mem)
  276. if (
  277. name &&
  278. /** @type {Set<string>} */ (propsMap.get(vueNode)).has(name)
  279. ) {
  280. verifyMutating(mem, name)
  281. }
  282. }
  283. }),
  284. utils.defineTemplateBodyVisitor(context, {
  285. /** @param {ThisExpression & { parent: MemberExpression } } node */
  286. 'VExpressionContainer MemberExpression > ThisExpression'(node) {
  287. if (!vueObjectData) {
  288. return
  289. }
  290. const mem = node.parent
  291. if (mem.object !== node) {
  292. return
  293. }
  294. const name = utils.getStaticPropertyName(mem)
  295. if (
  296. name &&
  297. /** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
  298. name
  299. )
  300. ) {
  301. verifyMutating(mem, name)
  302. }
  303. },
  304. /** @param {Identifier } node */
  305. 'VExpressionContainer Identifier'(node) {
  306. if (!vueObjectData) {
  307. return
  308. }
  309. if (!isVmReference(node)) {
  310. return
  311. }
  312. const name = node.name
  313. if (
  314. name &&
  315. /** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
  316. name
  317. )
  318. ) {
  319. verifyMutating(node, name)
  320. }
  321. },
  322. /** @param {ESNode} node */
  323. "VAttribute[directive=true][key.name.name='model'] VExpressionContainer > *"(
  324. node
  325. ) {
  326. if (!vueObjectData) {
  327. return
  328. }
  329. const nodes = utils.getMemberChaining(node)
  330. const first = nodes[0]
  331. let name
  332. if (isVmReference(first)) {
  333. name = first.name
  334. } else if (first.type === 'ThisExpression') {
  335. const mem = nodes[1]
  336. if (!mem) {
  337. return
  338. }
  339. name = utils.getStaticPropertyName(mem)
  340. } else {
  341. return
  342. }
  343. if (
  344. name &&
  345. /** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
  346. name
  347. )
  348. ) {
  349. report(node, name)
  350. }
  351. }
  352. })
  353. )
  354. }
  355. }