ref-object-references.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. /**
  2. * @author Yosuke Ota
  3. * @copyright 2022 Yosuke Ota. All rights reserved.
  4. * See LICENSE file in root directory for full license.
  5. */
  6. 'use strict'
  7. const utils = require('./index')
  8. const eslintUtils = require('@eslint-community/eslint-utils')
  9. const { definePropertyReferenceExtractor } = require('./property-references')
  10. const { ReferenceTracker } = eslintUtils
  11. /**
  12. * @typedef {object} RefObjectReferenceForExpression
  13. * @property {'expression'} type
  14. * @property {MemberExpression | CallExpression} node
  15. * @property {string} method
  16. * @property {CallExpression} define
  17. * @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
  18. *
  19. * @typedef {object} RefObjectReferenceForPattern
  20. * @property {'pattern'} type
  21. * @property {ObjectPattern} node
  22. * @property {string} method
  23. * @property {CallExpression} define
  24. * @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
  25. *
  26. * @typedef {object} RefObjectReferenceForIdentifier
  27. * @property {'expression' | 'pattern'} type
  28. * @property {Identifier} node
  29. * @property {VariableDeclarator | null} variableDeclarator
  30. * @property {VariableDeclaration | null} variableDeclaration
  31. * @property {string} method
  32. * @property {CallExpression} define
  33. * @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
  34. *
  35. * @typedef {RefObjectReferenceForIdentifier | RefObjectReferenceForExpression | RefObjectReferenceForPattern} RefObjectReference
  36. */
  37. /**
  38. * @typedef {object} ReactiveVariableReference
  39. * @property {Identifier} node
  40. * @property {boolean} escape Within escape hint (`$$()`)
  41. * @property {VariableDeclaration} variableDeclaration
  42. * @property {string} method
  43. * @property {CallExpression} define
  44. */
  45. /**
  46. * @typedef {object} RefObjectReferences
  47. * @property {<T extends Identifier | Expression | Pattern | Super> (node: T) =>
  48. * T extends Identifier ?
  49. * RefObjectReferenceForIdentifier | null :
  50. * T extends Expression ?
  51. * RefObjectReferenceForExpression | null :
  52. * T extends Pattern ?
  53. * RefObjectReferenceForPattern | null :
  54. * null} get
  55. */
  56. /**
  57. * @typedef {object} ReactiveVariableReferences
  58. * @property {(node: Identifier) => ReactiveVariableReference | null} get
  59. */
  60. const REF_MACROS = [
  61. '$ref',
  62. '$computed',
  63. '$shallowRef',
  64. '$customRef',
  65. '$toRef',
  66. '$'
  67. ]
  68. /** @type {WeakMap<Program, RefObjectReferences>} */
  69. const cacheForRefObjectReferences = new WeakMap()
  70. /** @type {WeakMap<Program, ReactiveVariableReferences>} */
  71. const cacheForReactiveVariableReferences = new WeakMap()
  72. /**
  73. * Iterate the call expressions that define the ref object.
  74. * @param {import('eslint').Scope.Scope} globalScope
  75. * @returns {Iterable<{ node: CallExpression, name: string }>}
  76. */
  77. function* iterateDefineRefs(globalScope) {
  78. const tracker = new ReferenceTracker(globalScope)
  79. const traceMap = utils.createCompositionApiTraceMap({
  80. [ReferenceTracker.ESM]: true,
  81. ref: {
  82. [ReferenceTracker.CALL]: true
  83. },
  84. computed: {
  85. [ReferenceTracker.CALL]: true
  86. },
  87. toRef: {
  88. [ReferenceTracker.CALL]: true
  89. },
  90. customRef: {
  91. [ReferenceTracker.CALL]: true
  92. },
  93. shallowRef: {
  94. [ReferenceTracker.CALL]: true
  95. },
  96. toRefs: {
  97. [ReferenceTracker.CALL]: true
  98. }
  99. })
  100. for (const { node, path } of tracker.iterateEsmReferences(traceMap)) {
  101. const expr = /** @type {CallExpression} */ (node)
  102. yield {
  103. node: expr,
  104. name: path[path.length - 1]
  105. }
  106. }
  107. }
  108. /**
  109. * Iterate the call expressions that define the reactive variables.
  110. * @param {import('eslint').Scope.Scope} globalScope
  111. * @returns {Iterable<{ node: CallExpression, name: string }>}
  112. */
  113. function* iterateDefineReactiveVariables(globalScope) {
  114. for (const { identifier } of iterateRefMacroReferences()) {
  115. if (
  116. identifier.parent.type === 'CallExpression' &&
  117. identifier.parent.callee === identifier
  118. ) {
  119. yield {
  120. node: identifier.parent,
  121. name: identifier.name
  122. }
  123. }
  124. }
  125. /**
  126. * Iterate ref macro reference.
  127. * @returns {Iterable<Reference>}
  128. */
  129. function* iterateRefMacroReferences() {
  130. yield* REF_MACROS.map((m) => globalScope.set.get(m))
  131. .filter(utils.isDef)
  132. .flatMap((v) => v.references)
  133. for (const ref of globalScope.through) {
  134. if (REF_MACROS.includes(ref.identifier.name)) {
  135. yield ref
  136. }
  137. }
  138. }
  139. }
  140. /**
  141. * Iterate the call expressions that the escape hint values.
  142. * @param {import('eslint').Scope.Scope} globalScope
  143. * @returns {Iterable<CallExpression>}
  144. */
  145. function* iterateEscapeHintValueRefs(globalScope) {
  146. for (const { identifier } of iterateEscapeHintReferences()) {
  147. if (
  148. identifier.parent.type === 'CallExpression' &&
  149. identifier.parent.callee === identifier
  150. ) {
  151. yield identifier.parent
  152. }
  153. }
  154. /**
  155. * Iterate escape hint reference.
  156. * @returns {Iterable<Reference>}
  157. */
  158. function* iterateEscapeHintReferences() {
  159. const escapeHint = globalScope.set.get('$$')
  160. if (escapeHint) {
  161. yield* escapeHint.references
  162. }
  163. for (const ref of globalScope.through) {
  164. if (ref.identifier.name === '$$') {
  165. yield ref
  166. }
  167. }
  168. }
  169. }
  170. /**
  171. * Extract identifier from given pattern node.
  172. * @param {Pattern} node
  173. * @returns {Iterable<Identifier>}
  174. */
  175. function* extractIdentifier(node) {
  176. switch (node.type) {
  177. case 'Identifier': {
  178. yield node
  179. break
  180. }
  181. case 'ObjectPattern': {
  182. for (const property of node.properties) {
  183. if (property.type === 'Property') {
  184. yield* extractIdentifier(property.value)
  185. } else if (property.type === 'RestElement') {
  186. yield* extractIdentifier(property)
  187. }
  188. }
  189. break
  190. }
  191. case 'ArrayPattern': {
  192. for (const element of node.elements) {
  193. if (element) {
  194. yield* extractIdentifier(element)
  195. }
  196. }
  197. break
  198. }
  199. case 'AssignmentPattern': {
  200. yield* extractIdentifier(node.left)
  201. break
  202. }
  203. case 'RestElement': {
  204. yield* extractIdentifier(node.argument)
  205. break
  206. }
  207. case 'MemberExpression': {
  208. // can't extract
  209. break
  210. }
  211. // No default
  212. }
  213. }
  214. /**
  215. * Iterate references of the given identifier.
  216. * @param {Identifier} id
  217. * @param {import('eslint').Scope.Scope} globalScope
  218. * @returns {Iterable<import('eslint').Scope.Reference>}
  219. */
  220. function* iterateIdentifierReferences(id, globalScope) {
  221. const variable = eslintUtils.findVariable(globalScope, id)
  222. if (!variable) {
  223. return
  224. }
  225. for (const reference of variable.references) {
  226. yield reference
  227. }
  228. }
  229. /**
  230. * @param {RuleContext} context The rule context.
  231. */
  232. function getGlobalScope(context) {
  233. const sourceCode = context.getSourceCode()
  234. return (
  235. sourceCode.scopeManager.globalScope || sourceCode.scopeManager.scopes[0]
  236. )
  237. }
  238. module.exports = {
  239. extractRefObjectReferences,
  240. extractReactiveVariableReferences
  241. }
  242. /**
  243. * @typedef {object} RefObjectReferenceContext
  244. * @property {string} method
  245. * @property {CallExpression} define
  246. * @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
  247. */
  248. /**
  249. * @implements {RefObjectReferences}
  250. */
  251. class RefObjectReferenceExtractor {
  252. /**
  253. * @param {RuleContext} context The rule context.
  254. */
  255. constructor(context) {
  256. this.context = context
  257. /** @type {Map<Identifier | MemberExpression | CallExpression | ObjectPattern, RefObjectReference>} */
  258. this.references = new Map()
  259. /** @type {Set<Identifier>} */
  260. this._processedIds = new Set()
  261. }
  262. /**
  263. * @template {Identifier | Expression | Pattern | Super} T
  264. * @param {T} node
  265. * @returns {T extends Identifier ?
  266. * RefObjectReferenceForIdentifier | null :
  267. * T extends Expression ?
  268. * RefObjectReferenceForExpression | null :
  269. * T extends Pattern ?
  270. * RefObjectReferenceForPattern | null :
  271. * null}
  272. */
  273. get(node) {
  274. return /** @type {never} */ (
  275. this.references.get(/** @type {never} */ (node)) || null
  276. )
  277. }
  278. /**
  279. * @param {CallExpression} node
  280. * @param {string} method
  281. */
  282. processDefineRef(node, method) {
  283. const parent = node.parent
  284. /** @type {Pattern | null} */
  285. let pattern = null
  286. if (parent.type === 'VariableDeclarator') {
  287. pattern = parent.id
  288. } else if (
  289. parent.type === 'AssignmentExpression' &&
  290. parent.operator === '='
  291. ) {
  292. pattern = parent.left
  293. } else {
  294. if (method !== 'toRefs') {
  295. this.references.set(node, {
  296. type: 'expression',
  297. node,
  298. method,
  299. define: node,
  300. defineChain: [node]
  301. })
  302. }
  303. return
  304. }
  305. const ctx = {
  306. method,
  307. define: node,
  308. defineChain: [node]
  309. }
  310. if (method === 'toRefs') {
  311. const propertyReferenceExtractor = definePropertyReferenceExtractor(
  312. this.context
  313. )
  314. const propertyReferences =
  315. propertyReferenceExtractor.extractFromPattern(pattern)
  316. for (const name of propertyReferences.allProperties().keys()) {
  317. for (const nest of propertyReferences.getNestNodes(name)) {
  318. if (nest.type === 'expression') {
  319. this.processMemberExpression(nest.node, ctx)
  320. } else if (nest.type === 'pattern') {
  321. this.processPattern(nest.node, ctx)
  322. }
  323. }
  324. }
  325. } else {
  326. this.processPattern(pattern, ctx)
  327. }
  328. }
  329. /**
  330. * @param {MemberExpression | Identifier} node
  331. * @param {RefObjectReferenceContext} ctx
  332. */
  333. processExpression(node, ctx) {
  334. const parent = node.parent
  335. if (parent.type === 'AssignmentExpression') {
  336. if (parent.operator === '=' && parent.right === node) {
  337. // `(foo = obj.mem)`
  338. this.processPattern(parent.left, {
  339. ...ctx,
  340. defineChain: [node, ...ctx.defineChain]
  341. })
  342. return true
  343. }
  344. } else if (parent.type === 'VariableDeclarator' && parent.init === node) {
  345. // `const foo = obj.mem`
  346. this.processPattern(parent.id, {
  347. ...ctx,
  348. defineChain: [node, ...ctx.defineChain]
  349. })
  350. return true
  351. }
  352. return false
  353. }
  354. /**
  355. * @param {MemberExpression} node
  356. * @param {RefObjectReferenceContext} ctx
  357. */
  358. processMemberExpression(node, ctx) {
  359. if (this.processExpression(node, ctx)) {
  360. return
  361. }
  362. this.references.set(node, {
  363. type: 'expression',
  364. node,
  365. ...ctx
  366. })
  367. }
  368. /**
  369. * @param {Pattern} node
  370. * @param {RefObjectReferenceContext} ctx
  371. */
  372. processPattern(node, ctx) {
  373. switch (node.type) {
  374. case 'Identifier': {
  375. this.processIdentifierPattern(node, ctx)
  376. break
  377. }
  378. case 'ArrayPattern':
  379. case 'RestElement':
  380. case 'MemberExpression': {
  381. return
  382. }
  383. case 'ObjectPattern': {
  384. this.references.set(node, {
  385. type: 'pattern',
  386. node,
  387. ...ctx
  388. })
  389. return
  390. }
  391. case 'AssignmentPattern': {
  392. this.processPattern(node.left, ctx)
  393. return
  394. }
  395. // No default
  396. }
  397. }
  398. /**
  399. * @param {Identifier} node
  400. * @param {RefObjectReferenceContext} ctx
  401. */
  402. processIdentifierPattern(node, ctx) {
  403. if (this._processedIds.has(node)) {
  404. return
  405. }
  406. this._processedIds.add(node)
  407. for (const reference of iterateIdentifierReferences(
  408. node,
  409. getGlobalScope(this.context)
  410. )) {
  411. const def =
  412. reference.resolved &&
  413. reference.resolved.defs.length === 1 &&
  414. reference.resolved.defs[0].type === 'Variable'
  415. ? reference.resolved.defs[0]
  416. : null
  417. if (def && def.name === reference.identifier) {
  418. continue
  419. }
  420. if (
  421. reference.isRead() &&
  422. this.processExpression(reference.identifier, ctx)
  423. ) {
  424. continue
  425. }
  426. this.references.set(reference.identifier, {
  427. type: reference.isWrite() ? 'pattern' : 'expression',
  428. node: reference.identifier,
  429. variableDeclarator: def ? def.node : null,
  430. variableDeclaration: def ? def.parent : null,
  431. ...ctx
  432. })
  433. }
  434. }
  435. }
  436. /**
  437. * Extracts references of all ref objects.
  438. * @param {RuleContext} context The rule context.
  439. * @returns {RefObjectReferences}
  440. */
  441. function extractRefObjectReferences(context) {
  442. const sourceCode = context.getSourceCode()
  443. const cachedReferences = cacheForRefObjectReferences.get(sourceCode.ast)
  444. if (cachedReferences) {
  445. return cachedReferences
  446. }
  447. const references = new RefObjectReferenceExtractor(context)
  448. for (const { node, name } of iterateDefineRefs(getGlobalScope(context))) {
  449. references.processDefineRef(node, name)
  450. }
  451. cacheForRefObjectReferences.set(sourceCode.ast, references)
  452. return references
  453. }
  454. /**
  455. * @implements {ReactiveVariableReferences}
  456. */
  457. class ReactiveVariableReferenceExtractor {
  458. /**
  459. * @param {RuleContext} context The rule context.
  460. */
  461. constructor(context) {
  462. this.context = context
  463. /** @type {Map<Identifier, ReactiveVariableReference>} */
  464. this.references = new Map()
  465. /** @type {Set<Identifier>} */
  466. this._processedIds = new Set()
  467. /** @type {Set<CallExpression>} */
  468. this._escapeHintValueRefs = new Set(
  469. iterateEscapeHintValueRefs(getGlobalScope(context))
  470. )
  471. }
  472. /**
  473. * @param {Identifier} node
  474. * @returns {ReactiveVariableReference | null}
  475. */
  476. get(node) {
  477. return this.references.get(node) || null
  478. }
  479. /**
  480. * @param {CallExpression} node
  481. * @param {string} method
  482. */
  483. processDefineReactiveVariable(node, method) {
  484. const parent = node.parent
  485. if (parent.type !== 'VariableDeclarator') {
  486. return
  487. }
  488. /** @type {Pattern | null} */
  489. const pattern = parent.id
  490. if (method === '$') {
  491. for (const id of extractIdentifier(pattern)) {
  492. this.processIdentifierPattern(id, method, node)
  493. }
  494. } else {
  495. if (pattern.type === 'Identifier') {
  496. this.processIdentifierPattern(pattern, method, node)
  497. }
  498. }
  499. }
  500. /**
  501. * @param {Identifier} node
  502. * @param {string} method
  503. * @param {CallExpression} define
  504. */
  505. processIdentifierPattern(node, method, define) {
  506. if (this._processedIds.has(node)) {
  507. return
  508. }
  509. this._processedIds.add(node)
  510. for (const reference of iterateIdentifierReferences(
  511. node,
  512. getGlobalScope(this.context)
  513. )) {
  514. const def =
  515. reference.resolved &&
  516. reference.resolved.defs.length === 1 &&
  517. reference.resolved.defs[0].type === 'Variable'
  518. ? reference.resolved.defs[0]
  519. : null
  520. if (!def || def.name === reference.identifier) {
  521. continue
  522. }
  523. this.references.set(reference.identifier, {
  524. node: reference.identifier,
  525. escape: this.withinEscapeHint(reference.identifier),
  526. method,
  527. define,
  528. variableDeclaration: def.parent
  529. })
  530. }
  531. }
  532. /**
  533. * Checks whether the given identifier node within the escape hints (`$$()`) or not.
  534. * @param {Identifier} node
  535. */
  536. withinEscapeHint(node) {
  537. /** @type {Identifier | ObjectExpression | ArrayExpression | SpreadElement | Property | AssignmentProperty} */
  538. let target = node
  539. /** @type {ASTNode | null} */
  540. let parent = target.parent
  541. while (parent) {
  542. if (parent.type === 'CallExpression') {
  543. if (
  544. parent.arguments.includes(/** @type {any} */ (target)) &&
  545. this._escapeHintValueRefs.has(parent)
  546. ) {
  547. return true
  548. }
  549. return false
  550. }
  551. if (
  552. (parent.type === 'Property' && parent.value === target) ||
  553. (parent.type === 'ObjectExpression' &&
  554. parent.properties.includes(/** @type {any} */ (target))) ||
  555. parent.type === 'ArrayExpression' ||
  556. parent.type === 'SpreadElement'
  557. ) {
  558. target = parent
  559. parent = target.parent
  560. } else {
  561. return false
  562. }
  563. }
  564. return false
  565. }
  566. }
  567. /**
  568. * Extracts references of all reactive variables.
  569. * @param {RuleContext} context The rule context.
  570. * @returns {ReactiveVariableReferences}
  571. */
  572. function extractReactiveVariableReferences(context) {
  573. const sourceCode = context.getSourceCode()
  574. const cachedReferences = cacheForReactiveVariableReferences.get(
  575. sourceCode.ast
  576. )
  577. if (cachedReferences) {
  578. return cachedReferences
  579. }
  580. const references = new ReactiveVariableReferenceExtractor(context)
  581. for (const { node, name } of iterateDefineReactiveVariables(
  582. getGlobalScope(context)
  583. )) {
  584. references.processDefineReactiveVariable(node, name)
  585. }
  586. cacheForReactiveVariableReferences.set(sourceCode.ast, references)
  587. return references
  588. }