no-mutating-props.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  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-community/eslint-utils')
  8. // https://github.com/vuejs/vue-next/blob/7c11c58faf8840ab97b6449c98da0296a60dddd8/packages/shared/src/globalsWhitelist.ts
  9. const GLOBALS_WHITE_LISTED = new Set([
  10. 'Infinity',
  11. 'undefined',
  12. 'NaN',
  13. 'isFinite',
  14. 'isNaN',
  15. 'parseFloat',
  16. 'parseInt',
  17. 'decodeURI',
  18. 'decodeURIComponent',
  19. 'encodeURI',
  20. 'encodeURIComponent',
  21. 'Math',
  22. 'Number',
  23. 'Date',
  24. 'Array',
  25. 'Object',
  26. 'Boolean',
  27. 'String',
  28. 'RegExp',
  29. 'Map',
  30. 'Set',
  31. 'JSON',
  32. 'Intl',
  33. 'BigInt'
  34. ])
  35. /**
  36. * @param {ASTNode} node
  37. * @returns {VExpressionContainer}
  38. */
  39. function getVExpressionContainer(node) {
  40. let n = node
  41. while (n.type !== 'VExpressionContainer') {
  42. n = /** @type {ASTNode} */ (n.parent)
  43. }
  44. return n
  45. }
  46. /**
  47. * @param {ASTNode} node
  48. * @returns {node is Identifier}
  49. */
  50. function isVmReference(node) {
  51. if (node.type !== 'Identifier') {
  52. return false
  53. }
  54. const parent = node.parent
  55. if (parent.type === 'MemberExpression') {
  56. if (parent.property === node) {
  57. // foo.id
  58. return false
  59. }
  60. } else if (
  61. parent.type === 'Property' &&
  62. parent.key === node &&
  63. !parent.computed
  64. ) {
  65. // {id: foo}
  66. return false
  67. }
  68. const exprContainer = getVExpressionContainer(node)
  69. for (const reference of exprContainer.references) {
  70. if (reference.variable != null) {
  71. // Not vm reference
  72. continue
  73. }
  74. if (reference.id === node) {
  75. return true
  76. }
  77. }
  78. return false
  79. }
  80. module.exports = {
  81. meta: {
  82. type: 'suggestion',
  83. docs: {
  84. description: 'disallow mutation of component props',
  85. categories: ['vue3-essential', 'essential'],
  86. url: 'https://eslint.vuejs.org/rules/no-mutating-props.html'
  87. },
  88. fixable: null, // or "code" or "whitespace"
  89. schema: [
  90. // fill in your schema
  91. ]
  92. },
  93. /** @param {RuleContext} context */
  94. create(context) {
  95. /** @type {Map<ObjectExpression|CallExpression, Set<string>>} */
  96. const propsMap = new Map()
  97. /** @type { { type: 'export' | 'mark' | 'definition', object: ObjectExpression } | { type: 'setup', object: CallExpression } | null } */
  98. let vueObjectData = null
  99. /**
  100. * @param {ASTNode} node
  101. * @param {string} name
  102. */
  103. function report(node, name) {
  104. context.report({
  105. node,
  106. message: 'Unexpected mutation of "{{key}}" prop.',
  107. data: {
  108. key: name
  109. }
  110. })
  111. }
  112. /**
  113. * @param {MemberExpression|AssignmentProperty} node
  114. * @returns {string}
  115. */
  116. function getPropertyNameText(node) {
  117. const name = utils.getStaticPropertyName(node)
  118. if (name) {
  119. return name
  120. }
  121. if (node.computed) {
  122. const expr = node.type === 'Property' ? node.key : node.property
  123. const str = context.getSourceCode().getText(expr)
  124. return `[${str}]`
  125. }
  126. return '?unknown?'
  127. }
  128. /**
  129. * @param {MemberExpression|Identifier} props
  130. * @param {string} name
  131. */
  132. function verifyMutating(props, name) {
  133. const invalid = utils.findMutating(props)
  134. if (invalid) {
  135. report(invalid.node, name)
  136. }
  137. }
  138. /**
  139. * @param {Pattern} param
  140. * @param {string[]} path
  141. * @returns {Generator<{ node: Identifier, path: string[] }>}
  142. */
  143. function* iteratePatternProperties(param, path) {
  144. if (!param) {
  145. return
  146. }
  147. switch (param.type) {
  148. case 'Identifier': {
  149. yield { node: param, path }
  150. break
  151. }
  152. case 'RestElement': {
  153. yield* iteratePatternProperties(param.argument, path)
  154. break
  155. }
  156. case 'AssignmentPattern': {
  157. yield* iteratePatternProperties(param.left, path)
  158. break
  159. }
  160. case 'ObjectPattern': {
  161. for (const prop of param.properties) {
  162. if (prop.type === 'Property') {
  163. const name = getPropertyNameText(prop)
  164. yield* iteratePatternProperties(prop.value, [...path, name])
  165. } else if (prop.type === 'RestElement') {
  166. yield* iteratePatternProperties(prop.argument, path)
  167. }
  168. }
  169. break
  170. }
  171. case 'ArrayPattern': {
  172. for (let index = 0; index < param.elements.length; index++) {
  173. const element = param.elements[index]
  174. yield* iteratePatternProperties(element, [...path, `${index}`])
  175. }
  176. break
  177. }
  178. }
  179. }
  180. /**
  181. * @param {Identifier} prop
  182. * @param {string[]} path
  183. */
  184. function verifyPropVariable(prop, path) {
  185. const variable = findVariable(context.getScope(), prop)
  186. if (!variable) {
  187. return
  188. }
  189. for (const reference of variable.references) {
  190. if (!reference.isRead()) {
  191. continue
  192. }
  193. const id = reference.identifier
  194. const invalid = utils.findMutating(id)
  195. if (!invalid) {
  196. continue
  197. }
  198. let name
  199. if (path.length === 0) {
  200. if (invalid.pathNodes.length === 0) {
  201. continue
  202. }
  203. const mem = invalid.pathNodes[0]
  204. name = getPropertyNameText(mem)
  205. } else {
  206. if (invalid.pathNodes.length === 0 && invalid.kind !== 'call') {
  207. continue
  208. }
  209. name = path[0]
  210. }
  211. report(invalid.node, name)
  212. }
  213. }
  214. function* extractDefineVariableNames() {
  215. const globalScope = context.getSourceCode().scopeManager.globalScope
  216. if (globalScope) {
  217. for (const variable of globalScope.variables) {
  218. if (variable.defs.length > 0) {
  219. yield variable.name
  220. }
  221. }
  222. const moduleScope = globalScope.childScopes.find(
  223. (scope) => scope.type === 'module'
  224. )
  225. for (const variable of (moduleScope && moduleScope.variables) || []) {
  226. if (variable.defs.length > 0) {
  227. yield variable.name
  228. }
  229. }
  230. }
  231. }
  232. return utils.compositingVisitors(
  233. {},
  234. utils.defineScriptSetupVisitor(context, {
  235. onDefinePropsEnter(node, props) {
  236. const defineVariableNames = new Set(extractDefineVariableNames())
  237. const propsSet = new Set(
  238. props
  239. .map((p) => p.propName)
  240. .filter(
  241. /**
  242. * @returns {propName is string}
  243. */
  244. (propName) =>
  245. utils.isDef(propName) &&
  246. !GLOBALS_WHITE_LISTED.has(propName) &&
  247. !defineVariableNames.has(propName)
  248. )
  249. )
  250. propsMap.set(node, propsSet)
  251. vueObjectData = {
  252. type: 'setup',
  253. object: node
  254. }
  255. let target = node
  256. if (
  257. target.parent &&
  258. target.parent.type === 'CallExpression' &&
  259. target.parent.arguments[0] === target &&
  260. target.parent.callee.type === 'Identifier' &&
  261. target.parent.callee.name === 'withDefaults'
  262. ) {
  263. target = target.parent
  264. }
  265. if (
  266. !target.parent ||
  267. target.parent.type !== 'VariableDeclarator' ||
  268. target.parent.init !== target
  269. ) {
  270. return
  271. }
  272. for (const { node: prop, path } of iteratePatternProperties(
  273. target.parent.id,
  274. []
  275. )) {
  276. verifyPropVariable(prop, path)
  277. propsSet.add(prop.name)
  278. }
  279. }
  280. }),
  281. utils.defineVueVisitor(context, {
  282. onVueObjectEnter(node) {
  283. propsMap.set(
  284. node,
  285. new Set(
  286. utils
  287. .getComponentPropsFromOptions(node)
  288. .map((p) => p.propName)
  289. .filter(utils.isDef)
  290. )
  291. )
  292. },
  293. onVueObjectExit(node, { type }) {
  294. if (
  295. (!vueObjectData ||
  296. (vueObjectData.type !== 'export' &&
  297. vueObjectData.type !== 'setup')) &&
  298. type !== 'instance'
  299. ) {
  300. vueObjectData = {
  301. type,
  302. object: node
  303. }
  304. }
  305. },
  306. onSetupFunctionEnter(node) {
  307. const propsParam = node.params[0]
  308. if (!propsParam) {
  309. // no arguments
  310. return
  311. }
  312. if (
  313. propsParam.type === 'RestElement' ||
  314. propsParam.type === 'ArrayPattern'
  315. ) {
  316. // cannot check
  317. return
  318. }
  319. for (const { node: prop, path } of iteratePatternProperties(
  320. propsParam,
  321. []
  322. )) {
  323. verifyPropVariable(prop, path)
  324. }
  325. },
  326. /** @param {(Identifier | ThisExpression) & { parent: MemberExpression } } node */
  327. 'MemberExpression > :matches(Identifier, ThisExpression)'(
  328. node,
  329. { node: vueNode }
  330. ) {
  331. if (!utils.isThis(node, context)) {
  332. return
  333. }
  334. const mem = node.parent
  335. if (mem.object !== node) {
  336. return
  337. }
  338. const name = utils.getStaticPropertyName(mem)
  339. if (
  340. name &&
  341. /** @type {Set<string>} */ (propsMap.get(vueNode)).has(name)
  342. ) {
  343. verifyMutating(mem, name)
  344. }
  345. }
  346. }),
  347. utils.defineTemplateBodyVisitor(context, {
  348. /** @param {ThisExpression & { parent: MemberExpression } } node */
  349. 'VExpressionContainer MemberExpression > ThisExpression'(node) {
  350. if (!vueObjectData) {
  351. return
  352. }
  353. const mem = node.parent
  354. if (mem.object !== node) {
  355. return
  356. }
  357. const name = utils.getStaticPropertyName(mem)
  358. if (
  359. name &&
  360. /** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
  361. name
  362. )
  363. ) {
  364. verifyMutating(mem, name)
  365. }
  366. },
  367. /** @param {Identifier } node */
  368. 'VExpressionContainer Identifier'(node) {
  369. if (!vueObjectData) {
  370. return
  371. }
  372. if (!isVmReference(node)) {
  373. return
  374. }
  375. const name = node.name
  376. if (
  377. name &&
  378. /** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
  379. name
  380. )
  381. ) {
  382. verifyMutating(node, name)
  383. }
  384. },
  385. /** @param {ESNode} node */
  386. "VAttribute[directive=true]:matches([key.name.name='model'], [key.name.name='bind']) VExpressionContainer > *"(
  387. node
  388. ) {
  389. if (!vueObjectData) {
  390. return
  391. }
  392. let attr = node.parent
  393. while (attr && attr.type !== 'VAttribute') {
  394. attr = attr.parent
  395. }
  396. if (
  397. attr &&
  398. attr.directive &&
  399. attr.key.name.name === 'bind' &&
  400. !attr.key.modifiers.some((mod) => mod.name === 'sync')
  401. ) {
  402. return
  403. }
  404. const nodes = utils.getMemberChaining(node)
  405. const first = nodes[0]
  406. let name
  407. if (isVmReference(first)) {
  408. name = first.name
  409. } else if (first.type === 'ThisExpression') {
  410. const mem = nodes[1]
  411. if (!mem) {
  412. return
  413. }
  414. name = utils.getStaticPropertyName(mem)
  415. } else {
  416. return
  417. }
  418. if (
  419. name &&
  420. /** @type {Set<string>} */ (propsMap.get(vueObjectData.object)).has(
  421. name
  422. )
  423. ) {
  424. report(node, name)
  425. }
  426. }
  427. })
  428. )
  429. }
  430. }