property-references.js 19 KB

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