• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const { resolve } = require('path')
4const { parser, arrayDelimiter } = require('@npmcli/query')
5const localeCompare = require('@isaacs/string-locale-compare')('en')
6const log = require('proc-log')
7const { minimatch } = require('minimatch')
8const npa = require('npm-package-arg')
9const pacote = require('pacote')
10const semver = require('semver')
11const fetch = require('npm-registry-fetch')
12
13// handle results for parsed query asts, results are stored in a map that has a
14// key that points to each ast selector node and stores the resulting array of
15// arborist nodes as its value, that is essential to how we handle multiple
16// query selectors, e.g: `#a, #b, #c` <- 3 diff ast selector nodes
17class Results {
18  #currentAstSelector
19  #initialItems
20  #inventory
21  #outdatedCache = new Map()
22  #vulnCache
23  #pendingCombinator
24  #results = new Map()
25  #targetNode
26
27  constructor (opts) {
28    this.#currentAstSelector = opts.rootAstNode.nodes[0]
29    this.#inventory = opts.inventory
30    this.#initialItems = opts.initialItems
31    this.#vulnCache = opts.vulnCache
32    this.#targetNode = opts.targetNode
33
34    this.currentResults = this.#initialItems
35
36    // We get this when first called and need to pass it to pacote
37    this.flatOptions = opts.flatOptions || {}
38
39    // reset by rootAstNode walker
40    this.currentAstNode = opts.rootAstNode
41  }
42
43  get currentResults () {
44    return this.#results.get(this.#currentAstSelector)
45  }
46
47  set currentResults (value) {
48    this.#results.set(this.#currentAstSelector, value)
49  }
50
51  // retrieves the initial items to which start the filtering / matching
52  // for most of the different types of recognized ast nodes, e.g: class (aka
53  // depType), id, *, etc in different contexts we need to start with the
54  // current list of filtered results, for example a query for `.workspace`
55  // actually means the same as `*.workspace` so we want to start with the full
56  // inventory if that's the first ast node we're reading but if it appears in
57  // the middle of a query it should respect the previous filtered results,
58  // combinators are a special case in which we always want to have the
59  // complete inventory list in order to use the left-hand side ast node as a
60  // filter combined with the element on its right-hand side
61  get initialItems () {
62    const firstParsed =
63      (this.currentAstNode.parent.nodes[0] === this.currentAstNode) &&
64      (this.currentAstNode.parent.parent.type === 'root')
65
66    if (firstParsed) {
67      return this.#initialItems
68    }
69
70    if (this.currentAstNode.prev().type === 'combinator') {
71      return this.#inventory
72    }
73    return this.currentResults
74  }
75
76  // combinators need information about previously filtered items along
77  // with info of the items parsed / retrieved from the selector right
78  // past the combinator, for this reason combinators are stored and
79  // only ran as the last part of each selector logic
80  processPendingCombinator (nextResults) {
81    if (this.#pendingCombinator) {
82      const res = this.#pendingCombinator(this.currentResults, nextResults)
83      this.#pendingCombinator = null
84      this.currentResults = res
85    } else {
86      this.currentResults = nextResults
87    }
88  }
89
90  // when collecting results to a root astNode, we traverse the list of child
91  // selector nodes and collect all of their resulting arborist nodes into a
92  // single/flat Set of items, this ensures we also deduplicate items
93  collect (rootAstNode) {
94    return new Set(rootAstNode.nodes.flatMap(n => this.#results.get(n)))
95  }
96
97  // selector types map to the '.type' property of the ast nodes via `${astNode.type}Type`
98  //
99  // attribute selector [name=value], etc
100  attributeType () {
101    const nextResults = this.initialItems.filter(node =>
102      attributeMatch(this.currentAstNode, node.package)
103    )
104    this.processPendingCombinator(nextResults)
105  }
106
107  // dependency type selector (i.e. .prod, .dev, etc)
108  // css calls this class, we interpret is as dependency type
109  classType () {
110    const depTypeFn = depTypes[String(this.currentAstNode)]
111    if (!depTypeFn) {
112      throw Object.assign(
113        new Error(`\`${String(this.currentAstNode)}\` is not a supported dependency type.`),
114        { code: 'EQUERYNODEPTYPE' }
115      )
116    }
117    const nextResults = depTypeFn(this.initialItems)
118    this.processPendingCombinator(nextResults)
119  }
120
121  // combinators (i.e. '>', ' ', '~')
122  combinatorType () {
123    this.#pendingCombinator = combinators[String(this.currentAstNode)]
124  }
125
126  // name selectors (i.e. #foo)
127  // css calls this id, we interpret it as name
128  idType () {
129    const name = this.currentAstNode.value
130    const nextResults = this.initialItems.filter(node =>
131      (name === node.name) || (name === node.package.name)
132    )
133    this.processPendingCombinator(nextResults)
134  }
135
136  // pseudo selectors (prefixed with :)
137  async pseudoType () {
138    const pseudoFn = `${this.currentAstNode.value.slice(1)}Pseudo`
139    if (!this[pseudoFn]) {
140      throw Object.assign(
141        new Error(`\`${this.currentAstNode.value
142        }\` is not a supported pseudo selector.`),
143        { code: 'EQUERYNOPSEUDO' }
144      )
145    }
146    const nextResults = await this[pseudoFn]()
147    this.processPendingCombinator(nextResults)
148  }
149
150  selectorType () {
151    this.#currentAstSelector = this.currentAstNode
152    // starts a new array in which resulting items
153    // can be stored for each given ast selector
154    if (!this.currentResults) {
155      this.currentResults = []
156    }
157  }
158
159  universalType () {
160    this.processPendingCombinator(this.initialItems)
161  }
162
163  // pseudo selectors map to the 'value' property of the pseudo selectors in the ast nodes
164  // via selectors via `${value.slice(1)}Pseudo`
165  attrPseudo () {
166    const { lookupProperties, attributeMatcher } = this.currentAstNode
167
168    return this.initialItems.filter(node => {
169      let objs = [node.package]
170      for (const prop of lookupProperties) {
171        // if an isArray symbol is found that means we'll need to iterate
172        // over the previous found array to basically make sure we traverse
173        // all its indexes testing for possible objects that may eventually
174        // hold more keys specified in a selector
175        if (prop === arrayDelimiter) {
176          objs = objs.flat()
177          continue
178        }
179
180        // otherwise just maps all currently found objs
181        // to the next prop from the lookup properties list,
182        // filters out any empty key lookup
183        objs = objs.flatMap(obj => obj[prop] || [])
184
185        // in case there's no property found in the lookup
186        // just filters that item out
187        const noAttr = objs.every(obj => !obj)
188        if (noAttr) {
189          return false
190        }
191      }
192
193      // if any of the potential object matches
194      // that item should be in the final result
195      return objs.some(obj => attributeMatch(attributeMatcher, obj))
196    })
197  }
198
199  emptyPseudo () {
200    return this.initialItems.filter(node => node.edgesOut.size === 0)
201  }
202
203  extraneousPseudo () {
204    return this.initialItems.filter(node => node.extraneous)
205  }
206
207  async hasPseudo () {
208    const found = []
209    for (const item of this.initialItems) {
210      // This is the one time initialItems differs from inventory
211      const res = await retrieveNodesFromParsedAst({
212        flatOptions: this.flatOptions,
213        initialItems: [item],
214        inventory: this.#inventory,
215        rootAstNode: this.currentAstNode.nestedNode,
216        targetNode: item,
217        vulnCache: this.#vulnCache,
218      })
219      if (res.size > 0) {
220        found.push(item)
221      }
222    }
223    return found
224  }
225
226  invalidPseudo () {
227    const found = []
228    for (const node of this.initialItems) {
229      for (const edge of node.edgesIn) {
230        if (edge.invalid) {
231          found.push(node)
232          break
233        }
234      }
235    }
236    return found
237  }
238
239  async isPseudo () {
240    const res = await retrieveNodesFromParsedAst({
241      flatOptions: this.flatOptions,
242      initialItems: this.initialItems,
243      inventory: this.#inventory,
244      rootAstNode: this.currentAstNode.nestedNode,
245      targetNode: this.currentAstNode,
246      vulnCache: this.#vulnCache,
247    })
248    return [...res]
249  }
250
251  linkPseudo () {
252    return this.initialItems.filter(node => node.isLink || (node.isTop && !node.isRoot))
253  }
254
255  missingPseudo () {
256    return this.#inventory.reduce((res, node) => {
257      for (const edge of node.edgesOut.values()) {
258        if (edge.missing) {
259          const pkg = { name: edge.name, version: edge.spec }
260          res.push(new this.#targetNode.constructor({ pkg }))
261        }
262      }
263      return res
264    }, [])
265  }
266
267  async notPseudo () {
268    const res = await retrieveNodesFromParsedAst({
269      flatOptions: this.flatOptions,
270      initialItems: this.initialItems,
271      inventory: this.#inventory,
272      rootAstNode: this.currentAstNode.nestedNode,
273      targetNode: this.currentAstNode,
274      vulnCache: this.#vulnCache,
275    })
276    const internalSelector = new Set(res)
277    return this.initialItems.filter(node =>
278      !internalSelector.has(node))
279  }
280
281  overriddenPseudo () {
282    return this.initialItems.filter(node => node.overridden)
283  }
284
285  pathPseudo () {
286    return this.initialItems.filter(node => {
287      if (!this.currentAstNode.pathValue) {
288        return true
289      }
290      return minimatch(
291        node.realpath.replace(/\\+/g, '/'),
292        resolve(node.root.realpath, this.currentAstNode.pathValue).replace(/\\+/g, '/')
293      )
294    })
295  }
296
297  privatePseudo () {
298    return this.initialItems.filter(node => node.package.private)
299  }
300
301  rootPseudo () {
302    return this.initialItems.filter(node => node === this.#targetNode.root)
303  }
304
305  scopePseudo () {
306    return this.initialItems.filter(node => node === this.#targetNode)
307  }
308
309  semverPseudo () {
310    const {
311      attributeMatcher,
312      lookupProperties,
313      semverFunc = 'infer',
314      semverValue,
315    } = this.currentAstNode
316    const { qualifiedAttribute } = attributeMatcher
317
318    if (!semverValue) {
319      // DEPRECATED: remove this warning and throw an error as part of @npmcli/arborist@6
320      log.warn('query', 'usage of :semver() with no parameters is deprecated')
321      return this.initialItems
322    }
323
324    if (!semver.valid(semverValue) && !semver.validRange(semverValue)) {
325      throw Object.assign(
326        new Error(`\`${semverValue}\` is not a valid semver version or range`),
327        { code: 'EQUERYINVALIDSEMVER' })
328    }
329
330    const valueIsVersion = !!semver.valid(semverValue)
331
332    const nodeMatches = (node, obj) => {
333      // if we already have an operator, the user provided some test as part of the selector
334      // we evaluate that first because if it fails we don't want this node anyway
335      if (attributeMatcher.operator) {
336        if (!attributeMatch(attributeMatcher, obj)) {
337          // if the initial operator doesn't match, we're done
338          return false
339        }
340      }
341
342      const attrValue = obj[qualifiedAttribute]
343      // both valid and validRange return null for undefined, so this will skip both nodes that
344      // do not have the attribute defined as well as those where the attribute value is invalid
345      // and those where the value from the package.json is not a string
346      if ((!semver.valid(attrValue) && !semver.validRange(attrValue)) ||
347          typeof attrValue !== 'string') {
348        return false
349      }
350
351      const attrIsVersion = !!semver.valid(attrValue)
352
353      let actualFunc = semverFunc
354
355      // if we're asked to infer, we examine outputs to make a best guess
356      if (actualFunc === 'infer') {
357        if (valueIsVersion && attrIsVersion) {
358          // two versions -> semver.eq
359          actualFunc = 'eq'
360        } else if (!valueIsVersion && !attrIsVersion) {
361          // two ranges -> semver.intersects
362          actualFunc = 'intersects'
363        } else {
364          // anything else -> semver.satisfies
365          actualFunc = 'satisfies'
366        }
367      }
368
369      if (['eq', 'neq', 'gt', 'gte', 'lt', 'lte'].includes(actualFunc)) {
370        // both sides must be versions, but one is not
371        if (!valueIsVersion || !attrIsVersion) {
372          return false
373        }
374
375        return semver[actualFunc](attrValue, semverValue)
376      } else if (['gtr', 'ltr', 'satisfies'].includes(actualFunc)) {
377        // at least one side must be a version, but neither is
378        if (!valueIsVersion && !attrIsVersion) {
379          return false
380        }
381
382        return valueIsVersion
383          ? semver[actualFunc](semverValue, attrValue)
384          : semver[actualFunc](attrValue, semverValue)
385      } else if (['intersects', 'subset'].includes(actualFunc)) {
386        // these accept two ranges and since a version is also a range, anything goes
387        return semver[actualFunc](attrValue, semverValue)
388      } else {
389        // user provided a function we don't know about, throw an error
390        throw Object.assign(new Error(`\`semver.${actualFunc}\` is not a supported operator.`),
391          { code: 'EQUERYINVALIDOPERATOR' })
392      }
393    }
394
395    return this.initialItems.filter((node) => {
396      // no lookupProperties just means its a top level property, see if it matches
397      if (!lookupProperties.length) {
398        return nodeMatches(node, node.package)
399      }
400
401      // this code is mostly duplicated from attrPseudo to traverse into the package until we get
402      // to our deepest requested object
403      let objs = [node.package]
404      for (const prop of lookupProperties) {
405        if (prop === arrayDelimiter) {
406          objs = objs.flat()
407          continue
408        }
409
410        objs = objs.flatMap(obj => obj[prop] || [])
411        const noAttr = objs.every(obj => !obj)
412        if (noAttr) {
413          return false
414        }
415
416        return objs.some(obj => nodeMatches(node, obj))
417      }
418    })
419  }
420
421  typePseudo () {
422    if (!this.currentAstNode.typeValue) {
423      return this.initialItems
424    }
425    return this.initialItems
426      .flatMap(node => {
427        const found = []
428        for (const edge of node.edgesIn) {
429          if (npa(`${edge.name}@${edge.spec}`).type === this.currentAstNode.typeValue) {
430            found.push(edge.to)
431          }
432        }
433        return found
434      })
435  }
436
437  dedupedPseudo () {
438    return this.initialItems.filter(node => node.target.edgesIn.size > 1)
439  }
440
441  async vulnPseudo () {
442    if (!this.initialItems.length) {
443      return this.initialItems
444    }
445    if (!this.#vulnCache) {
446      const packages = {}
447      // We have to map the items twice, once to get the request, and a second time to filter out the results of that request
448      this.initialItems.map((node) => {
449        if (node.isProjectRoot || node.package.private) {
450          return
451        }
452        if (!packages[node.name]) {
453          packages[node.name] = []
454        }
455        if (!packages[node.name].includes(node.version)) {
456          packages[node.name].push(node.version)
457        }
458      })
459      const res = await fetch('/-/npm/v1/security/advisories/bulk', {
460        ...this.flatOptions,
461        registry: this.flatOptions.auditRegistry || this.flatOptions.registry,
462        method: 'POST',
463        gzip: true,
464        body: packages,
465      })
466      this.#vulnCache = await res.json()
467    }
468    const advisories = this.#vulnCache
469    const { vulns } = this.currentAstNode
470    return this.initialItems.filter(item => {
471      const vulnerable = advisories[item.name]?.filter(advisory => {
472        // This could be for another version of this package elsewhere in the tree
473        if (!semver.intersects(advisory.vulnerable_versions, item.version)) {
474          return false
475        }
476        if (!vulns) {
477          return true
478        }
479        // vulns are OR with each other, if any one matches we're done
480        for (const vuln of vulns) {
481          if (vuln.severity && !vuln.severity.includes('*')) {
482            if (!vuln.severity.includes(advisory.severity)) {
483              continue
484            }
485          }
486
487          if (vuln?.cwe) {
488            // * is special, it means "has a cwe"
489            if (vuln.cwe.includes('*')) {
490              if (!advisory.cwe.length) {
491                continue
492              }
493            } else if (!vuln.cwe.every(cwe => advisory.cwe.includes(`CWE-${cwe}`))) {
494              continue
495            }
496          }
497          return true
498        }
499      })
500      if (vulnerable?.length) {
501        item.queryContext = {
502          advisories: vulnerable,
503        }
504        return true
505      }
506      return false
507    })
508  }
509
510  async outdatedPseudo () {
511    const { outdatedKind = 'any' } = this.currentAstNode
512
513    // filter the initialItems
514    // NOTE: this uses a Promise.all around a map without in-line concurrency handling
515    // since the only async action taken is retrieving the packument, which is limited
516    // based on the max-sockets config in make-fetch-happen
517    const initialResults = await Promise.all(this.initialItems.map(async (node) => {
518      // the root can't be outdated, skip it
519      if (node.isProjectRoot) {
520        return false
521      }
522
523      // private packages can't be published, skip them
524      if (node.package.private) {
525        return false
526      }
527
528      // we cache the promise representing the full versions list, this helps reduce the
529      // number of requests we send by keeping population of the cache in a single tick
530      // making it less likely that multiple requests for the same package will be inflight
531      if (!this.#outdatedCache.has(node.name)) {
532        this.#outdatedCache.set(node.name, getPackageVersions(node.name, this.flatOptions))
533      }
534      const availableVersions = await this.#outdatedCache.get(node.name)
535
536      // we attach _all_ versions to the queryContext to allow consumers to do their own
537      // filtering and comparisons
538      node.queryContext.versions = availableVersions
539
540      // next we further reduce the set to versions that are greater than the current one
541      const greaterVersions = availableVersions.filter((available) => {
542        return semver.gt(available, node.version)
543      })
544
545      // no newer versions than the current one, drop this node from the result set
546      if (!greaterVersions.length) {
547        return false
548      }
549
550      // if we got here, we know that newer versions exist, if the kind is 'any' we're done
551      if (outdatedKind === 'any') {
552        return node
553      }
554
555      // look for newer versions that differ from current by a specific part of the semver version
556      if (['major', 'minor', 'patch'].includes(outdatedKind)) {
557        // filter the versions greater than our current one based on semver.diff
558        const filteredVersions = greaterVersions.filter((version) => {
559          return semver.diff(node.version, version) === outdatedKind
560        })
561
562        // no available versions are of the correct diff type
563        if (!filteredVersions.length) {
564          return false
565        }
566
567        return node
568      }
569
570      // look for newer versions that satisfy at least one edgeIn to this node
571      if (outdatedKind === 'in-range') {
572        const inRangeContext = []
573        for (const edge of node.edgesIn) {
574          const inRangeVersions = greaterVersions.filter((version) => {
575            return semver.satisfies(version, edge.spec)
576          })
577
578          // this edge has no in-range candidates, just move on
579          if (!inRangeVersions.length) {
580            continue
581          }
582
583          inRangeContext.push({
584            from: edge.from.location,
585            versions: inRangeVersions,
586          })
587        }
588
589        // if we didn't find at least one match, drop this node
590        if (!inRangeContext.length) {
591          return false
592        }
593
594        // now add to the context each version that is in-range for each edgeIn
595        node.queryContext.outdated = {
596          ...node.queryContext.outdated,
597          inRange: inRangeContext,
598        }
599
600        return node
601      }
602
603      // look for newer versions that _do not_ satisfy at least one edgeIn
604      if (outdatedKind === 'out-of-range') {
605        const outOfRangeContext = []
606        for (const edge of node.edgesIn) {
607          const outOfRangeVersions = greaterVersions.filter((version) => {
608            return !semver.satisfies(version, edge.spec)
609          })
610
611          // this edge has no out-of-range candidates, skip it
612          if (!outOfRangeVersions.length) {
613            continue
614          }
615
616          outOfRangeContext.push({
617            from: edge.from.location,
618            versions: outOfRangeVersions,
619          })
620        }
621
622        // if we didn't add at least one thing to the context, this node is not a match
623        if (!outOfRangeContext.length) {
624          return false
625        }
626
627        // attach the out-of-range context to the node
628        node.queryContext.outdated = {
629          ...node.queryContext.outdated,
630          outOfRange: outOfRangeContext,
631        }
632
633        return node
634      }
635
636      // any other outdatedKind is unknown and will never match
637      return false
638    }))
639
640    // return an array with the holes for non-matching nodes removed
641    return initialResults.filter(Boolean)
642  }
643}
644
645// operators for attribute selectors
646const attributeOperators = {
647  // attribute value is equivalent
648  '=' ({ attr, value, insensitive }) {
649    return attr === value
650  },
651  // attribute value contains word
652  '~=' ({ attr, value, insensitive }) {
653    return (attr.match(/\w+/g) || []).includes(value)
654  },
655  // attribute value contains string
656  '*=' ({ attr, value, insensitive }) {
657    return attr.includes(value)
658  },
659  // attribute value is equal or starts with
660  '|=' ({ attr, value, insensitive }) {
661    return attr.startsWith(`${value}-`)
662  },
663  // attribute value starts with
664  '^=' ({ attr, value, insensitive }) {
665    return attr.startsWith(value)
666  },
667  // attribute value ends with
668  '$=' ({ attr, value, insensitive }) {
669    return attr.endsWith(value)
670  },
671}
672
673const attributeOperator = ({ attr, value, insensitive, operator }) => {
674  if (typeof attr === 'number') {
675    attr = String(attr)
676  }
677  if (typeof attr !== 'string') {
678    // It's an object or an array, bail
679    return false
680  }
681  if (insensitive) {
682    attr = attr.toLowerCase()
683  }
684
685  return attributeOperators[operator]({
686    attr,
687    insensitive,
688    value,
689  })
690}
691
692const attributeMatch = (matcher, obj) => {
693  const insensitive = !!matcher.insensitive
694  const operator = matcher.operator || ''
695  const attribute = matcher.qualifiedAttribute
696  let value = matcher.value || ''
697  // return early if checking existence
698  if (operator === '') {
699    return Boolean(obj[attribute])
700  }
701  if (insensitive) {
702    value = value.toLowerCase()
703  }
704  // in case the current object is an array
705  // then we try to match every item in the array
706  if (Array.isArray(obj[attribute])) {
707    return obj[attribute].find((i, index) => {
708      const attr = obj[attribute][index] || ''
709      return attributeOperator({ attr, value, insensitive, operator })
710    })
711  } else {
712    const attr = obj[attribute] || ''
713    return attributeOperator({ attr, value, insensitive, operator })
714  }
715}
716
717const edgeIsType = (node, type, seen = new Set()) => {
718  for (const edgeIn of node.edgesIn) {
719    // TODO Need a test with an infinite loop
720    if (seen.has(edgeIn)) {
721      continue
722    }
723    seen.add(edgeIn)
724    if (edgeIn.type === type || edgeIn.from[type] || edgeIsType(edgeIn.from, type, seen)) {
725      return true
726    }
727  }
728  return false
729}
730
731const filterByType = (nodes, type) => {
732  const found = []
733  for (const node of nodes) {
734    if (node[type] || edgeIsType(node, type)) {
735      found.push(node)
736    }
737  }
738  return found
739}
740
741const depTypes = {
742  // dependency
743  '.prod' (prevResults) {
744    const found = []
745    for (const node of prevResults) {
746      if (!node.dev) {
747        found.push(node)
748      }
749    }
750    return found
751  },
752  // devDependency
753  '.dev' (prevResults) {
754    return filterByType(prevResults, 'dev')
755  },
756  // optionalDependency
757  '.optional' (prevResults) {
758    return filterByType(prevResults, 'optional')
759  },
760  // peerDependency
761  '.peer' (prevResults) {
762    return filterByType(prevResults, 'peer')
763  },
764  // workspace
765  '.workspace' (prevResults) {
766    return prevResults.filter(node => node.isWorkspace)
767  },
768  // bundledDependency
769  '.bundled' (prevResults) {
770    return prevResults.filter(node => node.inBundle)
771  },
772}
773
774// checks if a given node has a direct parent in any of the nodes provided in
775// the compare nodes array
776const hasParent = (node, compareNodes) => {
777  // All it takes is one so we loop and return on the first hit
778  for (let compareNode of compareNodes) {
779    if (compareNode.isLink) {
780      compareNode = compareNode.target
781    }
782
783    // follows logical parent for link anscestors
784    if (node.isTop && (node.resolveParent === compareNode)) {
785      return true
786    }
787    // follows edges-in to check if they match a possible parent
788    for (const edge of node.edgesIn) {
789      if (edge && edge.from === compareNode) {
790        return true
791      }
792    }
793  }
794  return false
795}
796
797// checks if a given node is a descendant of any of the nodes provided in the
798// compareNodes array
799const hasAscendant = (node, compareNodes, seen = new Set()) => {
800  // TODO (future) loop over ancestry property
801  if (hasParent(node, compareNodes)) {
802    return true
803  }
804
805  if (node.isTop && node.resolveParent) {
806    /* istanbul ignore if - investigate if linksIn check obviates need for this */
807    if (hasAscendant(node.resolveParent, compareNodes)) {
808      return true
809    }
810  }
811  for (const edge of node.edgesIn) {
812    // TODO Need a test with an infinite loop
813    if (seen.has(edge)) {
814      continue
815    }
816    seen.add(edge)
817    if (edge && edge.from && hasAscendant(edge.from, compareNodes, seen)) {
818      return true
819    }
820  }
821  for (const linkNode of node.linksIn) {
822    if (hasAscendant(linkNode, compareNodes, seen)) {
823      return true
824    }
825  }
826  return false
827}
828
829const combinators = {
830  // direct descendant
831  '>' (prevResults, nextResults) {
832    return nextResults.filter(node => hasParent(node, prevResults))
833  },
834  // any descendant
835  ' ' (prevResults, nextResults) {
836    return nextResults.filter(node => hasAscendant(node, prevResults))
837  },
838  // sibling
839  '~' (prevResults, nextResults) {
840    // Return any node in nextResults that is a sibling of (aka shares a
841    // parent with) a node in prevResults
842    const parentNodes = new Set() // Parents of everything in prevResults
843    for (const node of prevResults) {
844      for (const edge of node.edgesIn) {
845        // edge.from always exists cause it's from another node's edgesIn
846        parentNodes.add(edge.from)
847      }
848    }
849    return nextResults.filter(node =>
850      !prevResults.includes(node) && hasParent(node, [...parentNodes])
851    )
852  },
853}
854
855// get a list of available versions of a package filtered to respect --before
856// NOTE: this runs over each node and should not throw
857const getPackageVersions = async (name, opts) => {
858  let packument
859  try {
860    packument = await pacote.packument(name, {
861      ...opts,
862      fullMetadata: false, // we only need the corgi
863    })
864  } catch (err) {
865    // if the fetch fails, log a warning and pretend there are no versions
866    log.warn('query', `could not retrieve packument for ${name}: ${err.message}`)
867    return []
868  }
869
870  // start with a sorted list of all versions (lowest first)
871  let candidates = Object.keys(packument.versions).sort(semver.compare)
872
873  // if the packument has a time property, and the user passed a before flag, then
874  // we filter this list down to only those versions that existed before the specified date
875  if (packument.time && opts.before) {
876    candidates = candidates.filter((version) => {
877      // this version isn't found in the times at all, drop it
878      if (!packument.time[version]) {
879        return false
880      }
881
882      return Date.parse(packument.time[version]) <= opts.before
883    })
884  }
885
886  return candidates
887}
888
889const retrieveNodesFromParsedAst = async (opts) => {
890  // when we first call this it's the parsed query.  all other times it's
891  // results.currentNode.nestedNode
892  const rootAstNode = opts.rootAstNode
893
894  if (!rootAstNode.nodes) {
895    return new Set()
896  }
897
898  const results = new Results(opts)
899
900  const astNodeQueue = new Set()
901  // walk is sync, so we have to build up our async functions and then await them later
902  rootAstNode.walk((nextAstNode) => {
903    astNodeQueue.add(nextAstNode)
904  })
905
906  for (const nextAstNode of astNodeQueue) {
907    // This is the only place we reset currentAstNode
908    results.currentAstNode = nextAstNode
909    const updateFn = `${results.currentAstNode.type}Type`
910    if (typeof results[updateFn] !== 'function') {
911      throw Object.assign(
912        new Error(`\`${results.currentAstNode.type}\` is not a supported selector.`),
913        { code: 'EQUERYNOSELECTOR' }
914      )
915    }
916    await results[updateFn]()
917  }
918
919  return results.collect(rootAstNode)
920}
921
922const querySelectorAll = async (targetNode, query, flatOptions) => {
923  // This never changes ever we just pass it around. But we can't scope it to
924  // this whole file if we ever want to support concurrent calls to this
925  // function.
926  const inventory = [...targetNode.root.inventory.values()]
927  // res is a Set of items returned for each parsed css ast selector
928  const res = await retrieveNodesFromParsedAst({
929    initialItems: inventory,
930    inventory,
931    flatOptions,
932    rootAstNode: parser(query),
933    targetNode,
934  })
935
936  // returns nodes ordered by realpath
937  return [...res].sort((a, b) => localeCompare(a.location, b.location))
938}
939
940module.exports = querySelectorAll
941