ref-object-references.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702
  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. for (const { node, path } of utils.iterateReferencesTraceMap(tracker, {
  80. ref: {
  81. [ReferenceTracker.CALL]: true
  82. },
  83. computed: {
  84. [ReferenceTracker.CALL]: true
  85. },
  86. toRef: {
  87. [ReferenceTracker.CALL]: true
  88. },
  89. customRef: {
  90. [ReferenceTracker.CALL]: true
  91. },
  92. shallowRef: {
  93. [ReferenceTracker.CALL]: true
  94. },
  95. toRefs: {
  96. [ReferenceTracker.CALL]: true
  97. }
  98. })) {
  99. const expr = /** @type {CallExpression} */ (node)
  100. yield {
  101. node: expr,
  102. name: path[path.length - 1]
  103. }
  104. }
  105. }
  106. /**
  107. * Iterate the call expressions that defineModel() macro.
  108. * @param {import('eslint').Scope.Scope} globalScope
  109. * @returns {Iterable<{ node: CallExpression }>}
  110. */
  111. function* iterateDefineModels(globalScope) {
  112. for (const { identifier } of iterateMacroReferences()) {
  113. if (
  114. identifier.parent.type === 'CallExpression' &&
  115. identifier.parent.callee === identifier
  116. ) {
  117. yield {
  118. node: identifier.parent
  119. }
  120. }
  121. }
  122. /**
  123. * Iterate macro reference.
  124. * @returns {Iterable<Reference>}
  125. */
  126. function* iterateMacroReferences() {
  127. const variable = globalScope.set.get('defineModel')
  128. if (
  129. variable &&
  130. variable.defs.length === 0 /* It was automatically defined. */
  131. ) {
  132. yield* variable.references
  133. }
  134. for (const ref of globalScope.through) {
  135. if (ref.identifier.name === 'defineModel') {
  136. yield ref
  137. }
  138. }
  139. }
  140. }
  141. /**
  142. * Iterate the call expressions that define the reactive variables.
  143. * @param {import('eslint').Scope.Scope} globalScope
  144. * @returns {Iterable<{ node: CallExpression, name: string }>}
  145. */
  146. function* iterateDefineReactiveVariables(globalScope) {
  147. for (const { identifier } of iterateRefMacroReferences()) {
  148. if (
  149. identifier.parent.type === 'CallExpression' &&
  150. identifier.parent.callee === identifier
  151. ) {
  152. yield {
  153. node: identifier.parent,
  154. name: identifier.name
  155. }
  156. }
  157. }
  158. /**
  159. * Iterate ref macro reference.
  160. * @returns {Iterable<Reference>}
  161. */
  162. function* iterateRefMacroReferences() {
  163. yield* REF_MACROS.map((m) => globalScope.set.get(m))
  164. .filter(utils.isDef)
  165. .flatMap((v) => v.references)
  166. for (const ref of globalScope.through) {
  167. if (REF_MACROS.includes(ref.identifier.name)) {
  168. yield ref
  169. }
  170. }
  171. }
  172. }
  173. /**
  174. * Iterate the call expressions that the escape hint values.
  175. * @param {import('eslint').Scope.Scope} globalScope
  176. * @returns {Iterable<CallExpression>}
  177. */
  178. function* iterateEscapeHintValueRefs(globalScope) {
  179. for (const { identifier } of iterateEscapeHintReferences()) {
  180. if (
  181. identifier.parent.type === 'CallExpression' &&
  182. identifier.parent.callee === identifier
  183. ) {
  184. yield identifier.parent
  185. }
  186. }
  187. /**
  188. * Iterate escape hint reference.
  189. * @returns {Iterable<Reference>}
  190. */
  191. function* iterateEscapeHintReferences() {
  192. const escapeHint = globalScope.set.get('$$')
  193. if (escapeHint) {
  194. yield* escapeHint.references
  195. }
  196. for (const ref of globalScope.through) {
  197. if (ref.identifier.name === '$$') {
  198. yield ref
  199. }
  200. }
  201. }
  202. }
  203. /**
  204. * Extract identifier from given pattern node.
  205. * @param {Pattern} node
  206. * @returns {Iterable<Identifier>}
  207. */
  208. function* extractIdentifier(node) {
  209. switch (node.type) {
  210. case 'Identifier': {
  211. yield node
  212. break
  213. }
  214. case 'ObjectPattern': {
  215. for (const property of node.properties) {
  216. if (property.type === 'Property') {
  217. yield* extractIdentifier(property.value)
  218. } else if (property.type === 'RestElement') {
  219. yield* extractIdentifier(property)
  220. }
  221. }
  222. break
  223. }
  224. case 'ArrayPattern': {
  225. for (const element of node.elements) {
  226. if (element) {
  227. yield* extractIdentifier(element)
  228. }
  229. }
  230. break
  231. }
  232. case 'AssignmentPattern': {
  233. yield* extractIdentifier(node.left)
  234. break
  235. }
  236. case 'RestElement': {
  237. yield* extractIdentifier(node.argument)
  238. break
  239. }
  240. case 'MemberExpression': {
  241. // can't extract
  242. break
  243. }
  244. // No default
  245. }
  246. }
  247. /**
  248. * Iterate references of the given identifier.
  249. * @param {Identifier} id
  250. * @param {import('eslint').Scope.Scope} globalScope
  251. * @returns {Iterable<import('eslint').Scope.Reference>}
  252. */
  253. function* iterateIdentifierReferences(id, globalScope) {
  254. const variable = eslintUtils.findVariable(globalScope, id)
  255. if (!variable) {
  256. return
  257. }
  258. for (const reference of variable.references) {
  259. yield reference
  260. }
  261. }
  262. /**
  263. * @param {RuleContext} context The rule context.
  264. */
  265. function getGlobalScope(context) {
  266. const sourceCode = context.getSourceCode()
  267. return (
  268. sourceCode.scopeManager.globalScope || sourceCode.scopeManager.scopes[0]
  269. )
  270. }
  271. module.exports = {
  272. iterateDefineRefs,
  273. extractRefObjectReferences,
  274. extractReactiveVariableReferences
  275. }
  276. /**
  277. * @typedef {object} RefObjectReferenceContext
  278. * @property {string} method
  279. * @property {CallExpression} define
  280. * @property {(CallExpression | Identifier | MemberExpression)[]} defineChain Holds the initialization path for assignment of ref objects.
  281. */
  282. /**
  283. * @implements {RefObjectReferences}
  284. */
  285. class RefObjectReferenceExtractor {
  286. /**
  287. * @param {RuleContext} context The rule context.
  288. */
  289. constructor(context) {
  290. this.context = context
  291. /** @type {Map<Identifier | MemberExpression | CallExpression | ObjectPattern, RefObjectReference>} */
  292. this.references = new Map()
  293. /** @type {Set<Identifier>} */
  294. this._processedIds = new Set()
  295. }
  296. /**
  297. * @template {Identifier | Expression | Pattern | Super} T
  298. * @param {T} node
  299. * @returns {T extends Identifier ?
  300. * RefObjectReferenceForIdentifier | null :
  301. * T extends Expression ?
  302. * RefObjectReferenceForExpression | null :
  303. * T extends Pattern ?
  304. * RefObjectReferenceForPattern | null :
  305. * null}
  306. */
  307. get(node) {
  308. return /** @type {never} */ (
  309. this.references.get(/** @type {never} */ (node)) || null
  310. )
  311. }
  312. /**
  313. * @param {CallExpression} node
  314. * @param {string} method
  315. */
  316. processDefineRef(node, method) {
  317. const parent = node.parent
  318. /** @type {Pattern | null} */
  319. let pattern = null
  320. if (parent.type === 'VariableDeclarator') {
  321. pattern = parent.id
  322. } else if (
  323. parent.type === 'AssignmentExpression' &&
  324. parent.operator === '='
  325. ) {
  326. pattern = parent.left
  327. } else {
  328. if (method !== 'toRefs') {
  329. this.references.set(node, {
  330. type: 'expression',
  331. node,
  332. method,
  333. define: node,
  334. defineChain: [node]
  335. })
  336. }
  337. return
  338. }
  339. const ctx = {
  340. method,
  341. define: node,
  342. defineChain: [node]
  343. }
  344. if (method === 'toRefs') {
  345. const propertyReferenceExtractor = definePropertyReferenceExtractor(
  346. this.context
  347. )
  348. const propertyReferences =
  349. propertyReferenceExtractor.extractFromPattern(pattern)
  350. for (const name of propertyReferences.allProperties().keys()) {
  351. for (const nest of propertyReferences.getNestNodes(name)) {
  352. if (nest.type === 'expression') {
  353. this.processMemberExpression(nest.node, ctx)
  354. } else if (nest.type === 'pattern') {
  355. this.processPattern(nest.node, ctx)
  356. }
  357. }
  358. }
  359. } else {
  360. this.processPattern(pattern, ctx)
  361. }
  362. }
  363. /**
  364. * @param {CallExpression} node
  365. */
  366. processDefineModel(node) {
  367. const parent = node.parent
  368. /** @type {Pattern | null} */
  369. let pattern = null
  370. if (parent.type === 'VariableDeclarator') {
  371. pattern = parent.id
  372. } else if (
  373. parent.type === 'AssignmentExpression' &&
  374. parent.operator === '='
  375. ) {
  376. pattern = parent.left
  377. } else {
  378. return
  379. }
  380. const ctx = {
  381. method: 'defineModel',
  382. define: node,
  383. defineChain: [node]
  384. }
  385. if (pattern.type === 'ArrayPattern' && pattern.elements[0]) {
  386. pattern = pattern.elements[0]
  387. }
  388. this.processPattern(pattern, ctx)
  389. }
  390. /**
  391. * @param {MemberExpression | Identifier} node
  392. * @param {RefObjectReferenceContext} ctx
  393. */
  394. processExpression(node, ctx) {
  395. const parent = node.parent
  396. if (parent.type === 'AssignmentExpression') {
  397. if (parent.operator === '=' && parent.right === node) {
  398. // `(foo = obj.mem)`
  399. this.processPattern(parent.left, {
  400. ...ctx,
  401. defineChain: [node, ...ctx.defineChain]
  402. })
  403. return true
  404. }
  405. } else if (parent.type === 'VariableDeclarator' && parent.init === node) {
  406. // `const foo = obj.mem`
  407. this.processPattern(parent.id, {
  408. ...ctx,
  409. defineChain: [node, ...ctx.defineChain]
  410. })
  411. return true
  412. }
  413. return false
  414. }
  415. /**
  416. * @param {MemberExpression} node
  417. * @param {RefObjectReferenceContext} ctx
  418. */
  419. processMemberExpression(node, ctx) {
  420. if (this.processExpression(node, ctx)) {
  421. return
  422. }
  423. this.references.set(node, {
  424. type: 'expression',
  425. node,
  426. ...ctx
  427. })
  428. }
  429. /**
  430. * @param {Pattern} node
  431. * @param {RefObjectReferenceContext} ctx
  432. */
  433. processPattern(node, ctx) {
  434. switch (node.type) {
  435. case 'Identifier': {
  436. this.processIdentifierPattern(node, ctx)
  437. break
  438. }
  439. case 'ArrayPattern':
  440. case 'RestElement':
  441. case 'MemberExpression': {
  442. return
  443. }
  444. case 'ObjectPattern': {
  445. this.references.set(node, {
  446. type: 'pattern',
  447. node,
  448. ...ctx
  449. })
  450. return
  451. }
  452. case 'AssignmentPattern': {
  453. this.processPattern(node.left, ctx)
  454. return
  455. }
  456. // No default
  457. }
  458. }
  459. /**
  460. * @param {Identifier} node
  461. * @param {RefObjectReferenceContext} ctx
  462. */
  463. processIdentifierPattern(node, ctx) {
  464. if (this._processedIds.has(node)) {
  465. return
  466. }
  467. this._processedIds.add(node)
  468. for (const reference of iterateIdentifierReferences(
  469. node,
  470. getGlobalScope(this.context)
  471. )) {
  472. const def =
  473. reference.resolved &&
  474. reference.resolved.defs.length === 1 &&
  475. reference.resolved.defs[0].type === 'Variable'
  476. ? reference.resolved.defs[0]
  477. : null
  478. if (def && def.name === reference.identifier) {
  479. continue
  480. }
  481. if (
  482. reference.isRead() &&
  483. this.processExpression(reference.identifier, ctx)
  484. ) {
  485. continue
  486. }
  487. this.references.set(reference.identifier, {
  488. type: reference.isWrite() ? 'pattern' : 'expression',
  489. node: reference.identifier,
  490. variableDeclarator: def ? def.node : null,
  491. variableDeclaration: def ? def.parent : null,
  492. ...ctx
  493. })
  494. }
  495. }
  496. }
  497. /**
  498. * Extracts references of all ref objects.
  499. * @param {RuleContext} context The rule context.
  500. * @returns {RefObjectReferences}
  501. */
  502. function extractRefObjectReferences(context) {
  503. const sourceCode = context.getSourceCode()
  504. const cachedReferences = cacheForRefObjectReferences.get(sourceCode.ast)
  505. if (cachedReferences) {
  506. return cachedReferences
  507. }
  508. const references = new RefObjectReferenceExtractor(context)
  509. const globalScope = getGlobalScope(context)
  510. for (const { node, name } of iterateDefineRefs(globalScope)) {
  511. references.processDefineRef(node, name)
  512. }
  513. for (const { node } of iterateDefineModels(globalScope)) {
  514. references.processDefineModel(node)
  515. }
  516. cacheForRefObjectReferences.set(sourceCode.ast, references)
  517. return references
  518. }
  519. /**
  520. * @implements {ReactiveVariableReferences}
  521. */
  522. class ReactiveVariableReferenceExtractor {
  523. /**
  524. * @param {RuleContext} context The rule context.
  525. */
  526. constructor(context) {
  527. this.context = context
  528. /** @type {Map<Identifier, ReactiveVariableReference>} */
  529. this.references = new Map()
  530. /** @type {Set<Identifier>} */
  531. this._processedIds = new Set()
  532. /** @type {Set<CallExpression>} */
  533. this._escapeHintValueRefs = new Set(
  534. iterateEscapeHintValueRefs(getGlobalScope(context))
  535. )
  536. }
  537. /**
  538. * @param {Identifier} node
  539. * @returns {ReactiveVariableReference | null}
  540. */
  541. get(node) {
  542. return this.references.get(node) || null
  543. }
  544. /**
  545. * @param {CallExpression} node
  546. * @param {string} method
  547. */
  548. processDefineReactiveVariable(node, method) {
  549. const parent = node.parent
  550. if (parent.type !== 'VariableDeclarator') {
  551. return
  552. }
  553. /** @type {Pattern | null} */
  554. const pattern = parent.id
  555. if (method === '$') {
  556. for (const id of extractIdentifier(pattern)) {
  557. this.processIdentifierPattern(id, method, node)
  558. }
  559. } else {
  560. if (pattern.type === 'Identifier') {
  561. this.processIdentifierPattern(pattern, method, node)
  562. }
  563. }
  564. }
  565. /**
  566. * @param {Identifier} node
  567. * @param {string} method
  568. * @param {CallExpression} define
  569. */
  570. processIdentifierPattern(node, method, define) {
  571. if (this._processedIds.has(node)) {
  572. return
  573. }
  574. this._processedIds.add(node)
  575. for (const reference of iterateIdentifierReferences(
  576. node,
  577. getGlobalScope(this.context)
  578. )) {
  579. const def =
  580. reference.resolved &&
  581. reference.resolved.defs.length === 1 &&
  582. reference.resolved.defs[0].type === 'Variable'
  583. ? reference.resolved.defs[0]
  584. : null
  585. if (!def || def.name === reference.identifier) {
  586. continue
  587. }
  588. this.references.set(reference.identifier, {
  589. node: reference.identifier,
  590. escape: this.withinEscapeHint(reference.identifier),
  591. method,
  592. define,
  593. variableDeclaration: def.parent
  594. })
  595. }
  596. }
  597. /**
  598. * Checks whether the given identifier node within the escape hints (`$$()`) or not.
  599. * @param {Identifier} node
  600. */
  601. withinEscapeHint(node) {
  602. /** @type {Identifier | ObjectExpression | ArrayExpression | SpreadElement | Property | AssignmentProperty} */
  603. let target = node
  604. /** @type {ASTNode | null} */
  605. let parent = target.parent
  606. while (parent) {
  607. if (parent.type === 'CallExpression') {
  608. if (
  609. parent.arguments.includes(/** @type {any} */ (target)) &&
  610. this._escapeHintValueRefs.has(parent)
  611. ) {
  612. return true
  613. }
  614. return false
  615. }
  616. if (
  617. (parent.type === 'Property' && parent.value === target) ||
  618. (parent.type === 'ObjectExpression' &&
  619. parent.properties.includes(/** @type {any} */ (target))) ||
  620. parent.type === 'ArrayExpression' ||
  621. parent.type === 'SpreadElement'
  622. ) {
  623. target = parent
  624. parent = target.parent
  625. } else {
  626. return false
  627. }
  628. }
  629. return false
  630. }
  631. }
  632. /**
  633. * Extracts references of all reactive variables.
  634. * @param {RuleContext} context The rule context.
  635. * @returns {ReactiveVariableReferences}
  636. */
  637. function extractReactiveVariableReferences(context) {
  638. const sourceCode = context.getSourceCode()
  639. const cachedReferences = cacheForReactiveVariableReferences.get(
  640. sourceCode.ast
  641. )
  642. if (cachedReferences) {
  643. return cachedReferences
  644. }
  645. const references = new ReactiveVariableReferenceExtractor(context)
  646. for (const { node, name } of iterateDefineReactiveVariables(
  647. getGlobalScope(context)
  648. )) {
  649. references.processDefineReactiveVariable(node, name)
  650. }
  651. cacheForReactiveVariableReferences.set(sourceCode.ast, references)
  652. return references
  653. }