property-references.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. /**
  2. * @author Yosuke Ota
  3. * @copyright 2021 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-utils')
  9. /**
  10. * @typedef {import('./style-variables').StyleVariablesContext} StyleVariablesContext
  11. */
  12. /**
  13. * @typedef {object} IHasPropertyOption
  14. * @property {boolean} [unknownCallAsAny]
  15. */
  16. /**
  17. * @typedef {object} IPropertyReferences
  18. * @property { (name: string, option?: IHasPropertyOption) => boolean } hasProperty
  19. * @property { () => Map<string, {nodes:ASTNode[]}> } allProperties
  20. * @property { (name: string) => IPropertyReferences } getNest
  21. */
  22. // ------------------------------------------------------------------------------
  23. // Helpers
  24. // ------------------------------------------------------------------------------
  25. /** @type {IPropertyReferences} */
  26. const ANY = {
  27. hasProperty: () => true,
  28. allProperties: () => new Map(),
  29. getNest: () => ANY
  30. }
  31. /** @type {IPropertyReferences} */
  32. const NEVER = {
  33. hasProperty: () => false,
  34. allProperties: () => new Map(),
  35. getNest: () => NEVER
  36. }
  37. /**
  38. * @param {RuleContext} context
  39. * @param {Identifier} id
  40. * @returns {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration | null}
  41. */
  42. function findFunction(context, id) {
  43. const calleeVariable = utils.findVariableByIdentifier(context, id)
  44. if (!calleeVariable) {
  45. return null
  46. }
  47. if (calleeVariable.defs.length === 1) {
  48. const def = calleeVariable.defs[0]
  49. if (def.node.type === 'FunctionDeclaration') {
  50. return def.node
  51. }
  52. if (
  53. def.type === 'Variable' &&
  54. def.parent.kind === 'const' &&
  55. def.node.init
  56. ) {
  57. if (
  58. def.node.init.type === 'FunctionExpression' ||
  59. def.node.init.type === 'ArrowFunctionExpression'
  60. ) {
  61. return def.node.init
  62. }
  63. if (def.node.init.type === 'Identifier') {
  64. return findFunction(context, def.node.init)
  65. }
  66. }
  67. }
  68. return null
  69. }
  70. // ------------------------------------------------------------------------------
  71. // Public
  72. // ------------------------------------------------------------------------------
  73. module.exports = {
  74. definePropertyReferenceExtractor,
  75. mergePropertyReferences
  76. }
  77. /**
  78. * @param {RuleContext} context The rule context.
  79. */
  80. function definePropertyReferenceExtractor(context) {
  81. /** @type {Map<Expression, IPropertyReferences>} */
  82. const cacheForExpression = new Map()
  83. /** @type {Map<Pattern, IPropertyReferences>} */
  84. const cacheForPattern = new Map()
  85. /** @type {Map<FunctionExpression | ArrowFunctionExpression | FunctionDeclaration, Map<number, IPropertyReferences>>} */
  86. const cacheForFunction = new Map()
  87. /** @type {{ toRefNodes: Set<ESNode>, toRefsNodes: Set<ESNode>} | null} */
  88. let toRefSet = null
  89. function getToRefSet() {
  90. if (toRefSet) {
  91. return toRefSet
  92. }
  93. const tracker = new eslintUtils.ReferenceTracker(
  94. context.getSourceCode().scopeManager.scopes[0]
  95. )
  96. const toRefNodes = new Set()
  97. for (const { node } of tracker.iterateEsmReferences(
  98. utils.createCompositionApiTraceMap({
  99. [eslintUtils.ReferenceTracker.ESM]: true,
  100. toRef: {
  101. [eslintUtils.ReferenceTracker.CALL]: true
  102. }
  103. })
  104. )) {
  105. toRefNodes.add(node)
  106. }
  107. const toRefsNodes = new Set()
  108. for (const { node } of tracker.iterateEsmReferences(
  109. utils.createCompositionApiTraceMap({
  110. [eslintUtils.ReferenceTracker.ESM]: true,
  111. toRefs: {
  112. [eslintUtils.ReferenceTracker.CALL]: true
  113. }
  114. })
  115. )) {
  116. toRefsNodes.add(node)
  117. }
  118. return (toRefSet = { toRefNodes, toRefsNodes })
  119. }
  120. /**
  121. * Collects the property references for member expr.
  122. * @implements IPropertyReferences
  123. */
  124. class PropertyReferencesForMember {
  125. /**
  126. *
  127. * @param {MemberExpression} node
  128. * @param {string} name
  129. * @param {boolean} withInTemplate
  130. */
  131. constructor(node, name, withInTemplate) {
  132. this.node = node
  133. this.name = name
  134. this.withInTemplate = withInTemplate
  135. }
  136. /**
  137. * @param {string} name
  138. */
  139. hasProperty(name) {
  140. return name === this.name
  141. }
  142. allProperties() {
  143. return new Map([[this.name, { nodes: [this.node.property] }]])
  144. }
  145. /**
  146. * @param {string} name
  147. * @returns {IPropertyReferences}
  148. */
  149. getNest(name) {
  150. return name === this.name
  151. ? extractFromExpression(this.node, this.withInTemplate)
  152. : NEVER
  153. }
  154. }
  155. /**
  156. * Collects the property references for object.
  157. * @implements IPropertyReferences
  158. */
  159. class PropertyReferencesForObject {
  160. constructor() {
  161. /** @type {Record<string, AssignmentProperty[]>} */
  162. this.properties = Object.create(null)
  163. }
  164. /**
  165. * @param {string} name
  166. */
  167. hasProperty(name) {
  168. return Boolean(this.properties[name])
  169. }
  170. allProperties() {
  171. const result = new Map()
  172. for (const [name, nodes] of Object.entries(this.properties)) {
  173. result.set(name, { nodes: nodes.map((node) => node.key) })
  174. }
  175. return result
  176. }
  177. /**
  178. * @param {string} name
  179. * @returns {IPropertyReferences}
  180. */
  181. getNest(name) {
  182. const properties = this.properties[name]
  183. return properties
  184. ? mergePropertyReferences(
  185. properties.map((property) => getNestFromPattern(property.value))
  186. )
  187. : NEVER
  188. /**
  189. * @param {Pattern} pattern
  190. * @returns {IPropertyReferences}
  191. */
  192. function getNestFromPattern(pattern) {
  193. if (pattern.type === 'ObjectPattern') {
  194. return extractFromObjectPattern(pattern)
  195. }
  196. if (pattern.type === 'Identifier') {
  197. return extractFromIdentifier(pattern)
  198. } else if (pattern.type === 'AssignmentPattern') {
  199. return getNestFromPattern(pattern.left)
  200. }
  201. return ANY
  202. }
  203. }
  204. }
  205. /**
  206. * Extract the property references from Expression.
  207. * @param {Identifier | MemberExpression | ChainExpression | ThisExpression | CallExpression} node
  208. * @param {boolean} withInTemplate
  209. * @returns {IPropertyReferences}
  210. */
  211. function extractFromExpression(node, withInTemplate) {
  212. const ref = cacheForExpression.get(node)
  213. if (ref) {
  214. return ref
  215. }
  216. cacheForExpression.set(node, ANY)
  217. const result = extractWithoutCache()
  218. cacheForExpression.set(node, result)
  219. return result
  220. function extractWithoutCache() {
  221. const parent = node.parent
  222. if (parent.type === 'AssignmentExpression') {
  223. if (withInTemplate) {
  224. return NEVER
  225. }
  226. if (parent.right === node) {
  227. // `({foo} = arg)`
  228. return extractFromPattern(parent.left)
  229. }
  230. return NEVER
  231. } else if (parent.type === 'VariableDeclarator') {
  232. if (withInTemplate) {
  233. return NEVER
  234. }
  235. if (parent.init === node) {
  236. // `const {foo} = arg`
  237. // `const foo = arg`
  238. return extractFromPattern(parent.id)
  239. }
  240. return NEVER
  241. } else if (parent.type === 'MemberExpression') {
  242. if (parent.object === node) {
  243. // `arg.foo`
  244. const name = utils.getStaticPropertyName(parent)
  245. if (name) {
  246. return new PropertyReferencesForMember(parent, name, withInTemplate)
  247. } else {
  248. return ANY
  249. }
  250. }
  251. return NEVER
  252. } else if (parent.type === 'CallExpression') {
  253. if (withInTemplate) {
  254. return NEVER
  255. }
  256. const argIndex = parent.arguments.indexOf(node)
  257. if (argIndex > -1) {
  258. // `foo(arg)`
  259. return extractFromCall(parent, argIndex)
  260. }
  261. } else if (parent.type === 'ChainExpression') {
  262. return extractFromExpression(parent, withInTemplate)
  263. } else if (
  264. parent.type === 'ArrowFunctionExpression' ||
  265. parent.type === 'ReturnStatement' ||
  266. parent.type === 'VExpressionContainer' ||
  267. parent.type === 'Property' ||
  268. parent.type === 'ArrayExpression'
  269. ) {
  270. // Maybe used externally.
  271. if (maybeExternalUsed(parent)) {
  272. return ANY
  273. }
  274. }
  275. return NEVER
  276. }
  277. /**
  278. * @param {ASTNode} parentTarget
  279. * @returns {boolean}
  280. */
  281. function maybeExternalUsed(parentTarget) {
  282. if (
  283. parentTarget.type === 'ReturnStatement' ||
  284. parentTarget.type === 'VExpressionContainer'
  285. ) {
  286. return true
  287. }
  288. if (parentTarget.type === 'ArrayExpression') {
  289. return maybeExternalUsed(parentTarget.parent)
  290. }
  291. if (parentTarget.type === 'Property') {
  292. return maybeExternalUsed(parentTarget.parent.parent)
  293. }
  294. if (parentTarget.type === 'ArrowFunctionExpression') {
  295. return parentTarget.body === node
  296. }
  297. return false
  298. }
  299. }
  300. /**
  301. * Extract the property references from one parameter of the function.
  302. * @param {Pattern} node
  303. * @returns {IPropertyReferences}
  304. */
  305. function extractFromPattern(node) {
  306. const ref = cacheForPattern.get(node)
  307. if (ref) {
  308. return ref
  309. }
  310. cacheForPattern.set(node, ANY)
  311. const result = extractWithoutCache()
  312. cacheForPattern.set(node, result)
  313. return result
  314. function extractWithoutCache() {
  315. while (node.type === 'AssignmentPattern') {
  316. node = node.left
  317. }
  318. if (node.type === 'RestElement' || node.type === 'ArrayPattern') {
  319. // cannot check
  320. return NEVER
  321. }
  322. if (node.type === 'ObjectPattern') {
  323. return extractFromObjectPattern(node)
  324. }
  325. if (node.type === 'Identifier') {
  326. return extractFromIdentifier(node)
  327. }
  328. return NEVER
  329. }
  330. }
  331. /**
  332. * Extract the property references from ObjectPattern.
  333. * @param {ObjectPattern} node
  334. * @returns {IPropertyReferences}
  335. */
  336. function extractFromObjectPattern(node) {
  337. const refs = new PropertyReferencesForObject()
  338. for (const prop of node.properties) {
  339. if (prop.type === 'Property') {
  340. const name = utils.getStaticPropertyName(prop)
  341. if (name) {
  342. const list = refs.properties[name] || (refs.properties[name] = [])
  343. list.push(prop)
  344. } else {
  345. // If cannot trace name, everything is used!
  346. return ANY
  347. }
  348. } else {
  349. // If use RestElement, everything is used!
  350. return ANY
  351. }
  352. }
  353. return refs
  354. }
  355. /**
  356. * Extract the property references from id.
  357. * @param {Identifier} node
  358. * @returns {IPropertyReferences}
  359. */
  360. function extractFromIdentifier(node) {
  361. const variable = utils.findVariableByIdentifier(context, node)
  362. if (!variable) {
  363. return NEVER
  364. }
  365. return mergePropertyReferences(
  366. variable.references.map((reference) => {
  367. const id = reference.identifier
  368. return extractFromExpression(id, false)
  369. })
  370. )
  371. }
  372. /**
  373. * Extract the property references from call.
  374. * @param {CallExpression} node
  375. * @param {number} argIndex
  376. * @returns {IPropertyReferences}
  377. */
  378. function extractFromCall(node, argIndex) {
  379. if (node.callee.type !== 'Identifier') {
  380. return {
  381. hasProperty(_name, options) {
  382. return Boolean(options && options.unknownCallAsAny)
  383. },
  384. allProperties: () => new Map(),
  385. getNest: () => ANY
  386. }
  387. }
  388. const fnNode = findFunction(context, node.callee)
  389. if (!fnNode) {
  390. if (argIndex === 0) {
  391. if (getToRefSet().toRefNodes.has(node)) {
  392. return extractFromToRef(node)
  393. } else if (getToRefSet().toRefsNodes.has(node)) {
  394. return extractFromToRefs(node)
  395. }
  396. }
  397. return {
  398. hasProperty(_name, options) {
  399. return Boolean(options && options.unknownCallAsAny)
  400. },
  401. allProperties: () => new Map(),
  402. getNest: () => ANY
  403. }
  404. }
  405. return extractFromFunctionParam(fnNode, argIndex)
  406. }
  407. /**
  408. * Extract the property references from function param.
  409. * @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node
  410. * @param {number} argIndex
  411. * @returns {IPropertyReferences}
  412. */
  413. function extractFromFunctionParam(node, argIndex) {
  414. let cacheForIndexes = cacheForFunction.get(node)
  415. if (!cacheForIndexes) {
  416. cacheForIndexes = new Map()
  417. cacheForFunction.set(node, cacheForIndexes)
  418. }
  419. const ref = cacheForIndexes.get(argIndex)
  420. if (ref) {
  421. return ref
  422. }
  423. cacheForIndexes.set(argIndex, NEVER)
  424. const arg = node.params[argIndex]
  425. if (!arg) {
  426. return NEVER
  427. }
  428. const result = extractFromPattern(arg)
  429. cacheForIndexes.set(argIndex, result)
  430. return result
  431. }
  432. /**
  433. * Extract the property references from path.
  434. * @param {string} pathString
  435. * @param {Identifier | Literal | TemplateLiteral} node
  436. * @returns {IPropertyReferences}
  437. */
  438. function extractFromPath(pathString, node) {
  439. return extractFromSegments(pathString.split('.'))
  440. /**
  441. * @param {string[]} segments
  442. * @returns {IPropertyReferences}
  443. */
  444. function extractFromSegments(segments) {
  445. if (!segments.length) {
  446. return ANY
  447. }
  448. const segmentName = segments[0]
  449. return {
  450. hasProperty: (name) => name === segmentName,
  451. allProperties: () => new Map([[segmentName, { nodes: [node] }]]),
  452. getNest: (name) =>
  453. name === segmentName ? extractFromSegments(segments.slice(1)) : NEVER
  454. }
  455. }
  456. }
  457. /**
  458. * Extract the property references from name literal.
  459. * @param {Expression} node
  460. * @returns {IPropertyReferences}
  461. */
  462. function extractFromNameLiteral(node) {
  463. const referenceName =
  464. node.type === 'Literal' || node.type === 'TemplateLiteral'
  465. ? utils.getStringLiteralValue(node)
  466. : null
  467. if (referenceName) {
  468. return {
  469. hasProperty: (name) => name === referenceName,
  470. allProperties: () => new Map([[referenceName, { nodes: [node] }]]),
  471. getNest: (name) => (name === referenceName ? ANY : NEVER)
  472. }
  473. } else {
  474. return NEVER
  475. }
  476. }
  477. /**
  478. * Extract the property references from name.
  479. * @param {string} referenceName
  480. * @param {Expression|SpreadElement} nameNode
  481. * @param { () => IPropertyReferences } [getNest]
  482. * @returns {IPropertyReferences}
  483. */
  484. function extractFromName(referenceName, nameNode, getNest) {
  485. return {
  486. hasProperty: (name) => name === referenceName,
  487. allProperties: () => new Map([[referenceName, { nodes: [nameNode] }]]),
  488. getNest: (name) =>
  489. name === referenceName ? (getNest ? getNest() : ANY) : NEVER
  490. }
  491. }
  492. /**
  493. * Extract the property references from toRef call.
  494. * @param {CallExpression} node
  495. * @returns {IPropertyReferences}
  496. */
  497. function extractFromToRef(node) {
  498. const nameNode = node.arguments[1]
  499. const refName =
  500. nameNode &&
  501. (nameNode.type === 'Literal' || nameNode.type === 'TemplateLiteral')
  502. ? utils.getStringLiteralValue(nameNode)
  503. : null
  504. if (!refName) {
  505. // unknown name
  506. return ANY
  507. }
  508. return extractFromName(refName, nameNode, () => {
  509. return extractFromExpression(node, false).getNest('value')
  510. })
  511. }
  512. /**
  513. * Extract the property references from toRefs call.
  514. * @param {CallExpression} node
  515. * @returns {IPropertyReferences}
  516. */
  517. function extractFromToRefs(node) {
  518. const base = extractFromExpression(node, false)
  519. return {
  520. hasProperty: (name, option) => base.hasProperty(name, option),
  521. allProperties: () => base.allProperties(),
  522. getNest: (name) => base.getNest(name).getNest('value')
  523. }
  524. }
  525. /**
  526. * Extract the property references from VExpressionContainer.
  527. * @param {VExpressionContainer} node
  528. * @param {object} [options]
  529. * @param {boolean} [options.ignoreGlobals]
  530. * @returns {IPropertyReferences}
  531. */
  532. function extractFromVExpressionContainer(node, options) {
  533. const ignoreGlobals = options && options.ignoreGlobals
  534. /** @type { (name:string)=>boolean } */
  535. let ignoreRef = () => false
  536. if (ignoreGlobals) {
  537. const globalScope =
  538. context.getSourceCode().scopeManager.globalScope ||
  539. context.getSourceCode().scopeManager.scopes[0]
  540. ignoreRef = (name) => globalScope.set.has(name)
  541. }
  542. const references = []
  543. for (const id of node.references
  544. .filter((ref) => ref.variable == null)
  545. .map((ref) => ref.id)) {
  546. if (ignoreRef(id.name)) {
  547. continue
  548. }
  549. references.push(
  550. extractFromName(id.name, id, () => extractFromExpression(id, true))
  551. )
  552. }
  553. return mergePropertyReferences(references)
  554. }
  555. /**
  556. * Extract the property references from StyleVariablesContext.
  557. * @param {StyleVariablesContext} ctx
  558. * @returns {IPropertyReferences}
  559. */
  560. function extractFromStyleVariablesContext(ctx) {
  561. const references = []
  562. for (const { id } of ctx.references) {
  563. references.push(
  564. extractFromName(id.name, id, () => extractFromExpression(id, true))
  565. )
  566. }
  567. return mergePropertyReferences(references)
  568. }
  569. return {
  570. extractFromExpression,
  571. extractFromPattern,
  572. extractFromFunctionParam,
  573. extractFromPath,
  574. extractFromName,
  575. extractFromNameLiteral,
  576. extractFromVExpressionContainer,
  577. extractFromStyleVariablesContext
  578. }
  579. }
  580. /**
  581. * @param {IPropertyReferences[]} references
  582. * @returns {IPropertyReferences}
  583. */
  584. function mergePropertyReferences(references) {
  585. if (references.length === 0) {
  586. return NEVER
  587. }
  588. if (references.length === 1) {
  589. return references[0]
  590. }
  591. return new PropertyReferencesForMerge(references)
  592. }
  593. /**
  594. * Collects the property references for merge.
  595. * @implements IPropertyReferences
  596. */
  597. class PropertyReferencesForMerge {
  598. /**
  599. * @param {IPropertyReferences[]} references
  600. */
  601. constructor(references) {
  602. this.references = references
  603. }
  604. /**
  605. * @param {string} name
  606. * @param {IHasPropertyOption} [option]
  607. */
  608. hasProperty(name, option) {
  609. return this.references.some((ref) => ref.hasProperty(name, option))
  610. }
  611. allProperties() {
  612. const result = new Map()
  613. for (const reference of this.references) {
  614. for (const [name, { nodes }] of reference.allProperties()) {
  615. const r = result.get(name)
  616. if (r) {
  617. r.nodes = [...new Set([...r.nodes, ...nodes])]
  618. } else {
  619. result.set(name, { nodes: [...nodes] })
  620. }
  621. }
  622. }
  623. return result
  624. }
  625. /**
  626. * @param {string} name
  627. * @returns {IPropertyReferences}
  628. */
  629. getNest(name) {
  630. /** @type {IPropertyReferences[]} */
  631. const nest = []
  632. for (const ref of this.references) {
  633. if (ref.hasProperty(name)) {
  634. nest.push(ref.getNest(name))
  635. }
  636. }
  637. return mergePropertyReferences(nest)
  638. }
  639. }