1// Internal methods used by buildIdealTree.
2// Answer the question: "can I put this dep here?"
3//
4// IMPORTANT: *nothing* in this class should *ever* modify or mutate the tree
5// at all.  The contract here is strictly limited to read operations.  We call
6// this in the process of walking through the ideal tree checking many
7// different potential placement targets for a given node.  If a change is made
8// to the tree along the way, that can cause serious problems!
9//
10// In order to enforce this restriction, in debug mode, canPlaceDep() will
11// snapshot the tree at the start of the process, and then at the end, will
12// verify that it still matches the snapshot, and throw an error if any changes
13// occurred.
14//
15// The algorithm is roughly like this:
16// - check the node itself:
17//   - if there is no version present, and no conflicting edges from target,
18//     OK, provided all peers can be placed at or above the target.
19//   - if the current version matches, KEEP
20//   - if there is an older version present, which can be replaced, then
21//     - if satisfying and preferDedupe? KEEP
22//     - else: REPLACE
23//   - if there is a newer version present, and preferDedupe, REPLACE
24//   - if the version present satisfies the edge, KEEP
25//   - else: CONFLICT
26// - if the node is not in conflict, check each of its peers:
27//   - if the peer can be placed in the target, continue
28//   - else if the peer can be placed in a parent, and there is no other
29//     conflicting version shadowing it, continue
30//   - else CONFLICT
31// - If the peers are not in conflict, return the original node's value
32//
33// An exception to this logic is that if the target is the deepest location
34// that a node can be placed, and the conflicting node can be placed deeper,
35// then we will return REPLACE rather than CONFLICT, and Arborist will queue
36// the replaced node for resolution elsewhere.
37
38const localeCompare = require('@isaacs/string-locale-compare')('en')
39const semver = require('semver')
40const debug = require('./debug.js')
41const peerEntrySets = require('./peer-entry-sets.js')
42const deepestNestingTarget = require('./deepest-nesting-target.js')
43
44const CONFLICT = Symbol('CONFLICT')
45const OK = Symbol('OK')
46const REPLACE = Symbol('REPLACE')
47const KEEP = Symbol('KEEP')
48
49class CanPlaceDep {
50  // dep is a dep that we're trying to place.  it should already live in
51  // a virtual tree where its peer set is loaded as children of the root.
52  // target is the actual place where we're trying to place this dep
53  // in a node_modules folder.
54  // edge is the edge that we're trying to satisfy with this placement.
55  // parent is the CanPlaceDep object of the entry node when placing a peer.
56  constructor (options) {
57    const {
58      dep,
59      target,
60      edge,
61      preferDedupe,
62      parent = null,
63      peerPath = [],
64      explicitRequest = false,
65    } = options
66
67    debug(() => {
68      if (!dep) {
69        throw new Error('no dep provided to CanPlaceDep')
70      }
71
72      if (!target) {
73        throw new Error('no target provided to CanPlaceDep')
74      }
75
76      if (!edge) {
77        throw new Error('no edge provided to CanPlaceDep')
78      }
79
80      this._treeSnapshot = JSON.stringify([...target.root.inventory.entries()]
81        .map(([loc, { packageName, version, resolved }]) => {
82          return [loc, packageName, version, resolved]
83        }).sort(([a], [b]) => localeCompare(a, b)))
84    })
85
86    // the result of whether we can place it or not
87    this.canPlace = null
88    // if peers conflict, but this one doesn't, then that is useful info
89    this.canPlaceSelf = null
90
91    this.dep = dep
92    this.target = target
93    this.edge = edge
94    this.explicitRequest = explicitRequest
95
96    // preventing cycles when we check peer sets
97    this.peerPath = peerPath
98    // we always prefer to dedupe peers, because they are trying
99    // a bit harder to be singletons.
100    this.preferDedupe = !!preferDedupe || edge.peer
101    this.parent = parent
102    this.children = []
103
104    this.isSource = target === this.peerSetSource
105    this.name = edge.name
106    this.current = target.children.get(this.name)
107    this.targetEdge = target.edgesOut.get(this.name)
108    this.conflicts = new Map()
109
110    // check if this dep was already subject to a peerDep override while
111    // building the peerSet.
112    this.edgeOverride = !dep.satisfies(edge)
113
114    this.canPlace = this.checkCanPlace()
115    if (!this.canPlaceSelf) {
116      this.canPlaceSelf = this.canPlace
117    }
118
119    debug(() => {
120      const treeSnapshot = JSON.stringify([...target.root.inventory.entries()]
121        .map(([loc, { packageName, version, resolved }]) => {
122          return [loc, packageName, version, resolved]
123        }).sort(([a], [b]) => localeCompare(a, b)))
124      /* istanbul ignore if */
125      if (this._treeSnapshot !== treeSnapshot) {
126        throw Object.assign(new Error('tree changed in CanPlaceDep'), {
127          expect: this._treeSnapshot,
128          actual: treeSnapshot,
129        })
130      }
131    })
132  }
133
134  checkCanPlace () {
135    const { target, targetEdge, current, dep } = this
136
137    // if the dep failed to load, we're going to fail the build or
138    // prune it out anyway, so just move forward placing/replacing it.
139    if (dep.errors.length) {
140      return current ? REPLACE : OK
141    }
142
143    // cannot place peers inside their dependents, except for tops
144    if (targetEdge && targetEdge.peer && !target.isTop) {
145      return CONFLICT
146    }
147
148    // skip this test if there's a current node, because we might be able
149    // to dedupe against it anyway
150    if (!current &&
151        targetEdge &&
152        !dep.satisfies(targetEdge) &&
153        targetEdge !== this.edge) {
154      return CONFLICT
155    }
156
157    return current ? this.checkCanPlaceCurrent() : this.checkCanPlaceNoCurrent()
158  }
159
160  // we know that the target has a dep by this name in its node_modules
161  // already.  Can return KEEP, REPLACE, or CONFLICT.
162  checkCanPlaceCurrent () {
163    const { preferDedupe, explicitRequest, current, target, edge, dep } = this
164
165    if (dep.matches(current)) {
166      if (current.satisfies(edge) || this.edgeOverride) {
167        return explicitRequest ? REPLACE : KEEP
168      }
169    }
170
171    const { version: curVer } = current
172    const { version: newVer } = dep
173    const tryReplace = curVer && newVer && semver.gte(newVer, curVer)
174    if (tryReplace && dep.canReplace(current)) {
175      // It's extremely rare that a replaceable node would be a conflict, if
176      // the current one wasn't a conflict, but it is theoretically possible
177      // if peer deps are pinned.  In that case we treat it like any other
178      // conflict, and keep trying.
179      const cpp = this.canPlacePeers(REPLACE)
180      if (cpp !== CONFLICT) {
181        return cpp
182      }
183    }
184
185    // ok, can't replace the current with new one, but maybe current is ok?
186    if (current.satisfies(edge) && (!explicitRequest || preferDedupe)) {
187      return KEEP
188    }
189
190    // if we prefer deduping, then try replacing newer with older
191    if (preferDedupe && !tryReplace && dep.canReplace(current)) {
192      const cpp = this.canPlacePeers(REPLACE)
193      if (cpp !== CONFLICT) {
194        return cpp
195      }
196    }
197
198    // Check for interesting cases!
199    // First, is this the deepest place that this thing can go, and NOT the
200    // deepest place where the conflicting dep can go?  If so, replace it,
201    // and let it re-resolve deeper in the tree.
202    const myDeepest = this.deepestNestingTarget
203
204    // ok, i COULD be placed deeper, so leave the current one alone.
205    if (target !== myDeepest) {
206      return CONFLICT
207    }
208
209    // if we are not checking a peerDep, then we MUST place it here, in the
210    // target that has a non-peer dep on it.
211    if (!edge.peer && target === edge.from) {
212      return this.canPlacePeers(REPLACE)
213    }
214
215    // if we aren't placing a peer in a set, then we're done here.
216    // This is ignored because it SHOULD be redundant, as far as I can tell,
217    // with the deepest target and target===edge.from tests.  But until we
218    // can prove that isn't possible, this condition is here for safety.
219    /* istanbul ignore if - allegedly impossible */
220    if (!this.parent && !edge.peer) {
221      return CONFLICT
222    }
223
224    // check the deps in the peer group for each edge into that peer group
225    // if ALL of them can be pushed deeper, or if it's ok to replace its
226    // members with the contents of the new peer group, then we're good.
227    let canReplace = true
228    for (const [entryEdge, currentPeers] of peerEntrySets(current)) {
229      if (entryEdge === this.edge || entryEdge === this.peerEntryEdge) {
230        continue
231      }
232
233      // First, see if it's ok to just replace the peerSet entirely.
234      // we do this by walking out from the entryEdge, because in a case like
235      // this:
236      //
237      // v -> PEER(a@1||2)
238      // a@1 -> PEER(b@1)
239      // a@2 -> PEER(b@2)
240      // b@1 -> PEER(a@1)
241      // b@2 -> PEER(a@2)
242      //
243      // root
244      // +-- v
245      // +-- a@2
246      // +-- b@2
247      //
248      // Trying to place a peer group of (a@1, b@1) would fail to note that
249      // they can be replaced, if we did it by looping 1 by 1.  If we are
250      // replacing something, we don't have to check its peer deps, because
251      // the peerDeps in the placed peerSet will presumably satisfy.
252      const entryNode = entryEdge.to
253      const entryRep = dep.parent.children.get(entryNode.name)
254      if (entryRep) {
255        if (entryRep.canReplace(entryNode, dep.parent.children.keys())) {
256          continue
257        }
258      }
259
260      let canClobber = !entryRep
261      if (!entryRep) {
262        const peerReplacementWalk = new Set([entryNode])
263        OUTER: for (const currentPeer of peerReplacementWalk) {
264          for (const edge of currentPeer.edgesOut.values()) {
265            if (!edge.peer || !edge.valid) {
266              continue
267            }
268            const rep = dep.parent.children.get(edge.name)
269            if (!rep) {
270              if (edge.to) {
271                peerReplacementWalk.add(edge.to)
272              }
273              continue
274            }
275            if (!rep.satisfies(edge)) {
276              canClobber = false
277              break OUTER
278            }
279          }
280        }
281      }
282      if (canClobber) {
283        continue
284      }
285
286      // ok, we can't replace, but maybe we can nest the current set deeper?
287      let canNestCurrent = true
288      for (const currentPeer of currentPeers) {
289        if (!canNestCurrent) {
290          break
291        }
292
293        // still possible to nest this peerSet
294        const curDeep = deepestNestingTarget(entryEdge.from, currentPeer.name)
295        if (curDeep === target || target.isDescendantOf(curDeep)) {
296          canNestCurrent = false
297          canReplace = false
298        }
299        if (canNestCurrent) {
300          continue
301        }
302      }
303    }
304
305    // if we can nest or replace all the current peer groups, we can replace.
306    if (canReplace) {
307      return this.canPlacePeers(REPLACE)
308    }
309
310    return CONFLICT
311  }
312
313  checkCanPlaceNoCurrent () {
314    const { target, peerEntryEdge, dep, name } = this
315
316    // check to see what that name resolves to here, and who may depend on
317    // being able to reach it by crawling up past the parent.  we know
318    // that it's not the target's direct child node, and if it was a direct
319    // dep of the target, we would have conflicted earlier.
320    const current = target !== peerEntryEdge.from && target.resolve(name)
321    if (current) {
322      for (const edge of current.edgesIn.values()) {
323        if (edge.from.isDescendantOf(target) && edge.valid) {
324          if (!dep.satisfies(edge)) {
325            return CONFLICT
326          }
327        }
328      }
329    }
330
331    // no objections, so this is fine as long as peers are ok here.
332    return this.canPlacePeers(OK)
333  }
334
335  get deepestNestingTarget () {
336    const start = this.parent ? this.parent.deepestNestingTarget
337      : this.edge.from
338    return deepestNestingTarget(start, this.name)
339  }
340
341  get conflictChildren () {
342    return this.allChildren.filter(c => c.canPlace === CONFLICT)
343  }
344
345  get allChildren () {
346    const set = new Set(this.children)
347    for (const child of set) {
348      for (const grandchild of child.children) {
349        set.add(grandchild)
350      }
351    }
352    return [...set]
353  }
354
355  get top () {
356    return this.parent ? this.parent.top : this
357  }
358
359  // check if peers can go here.  returns state or CONFLICT
360  canPlacePeers (state) {
361    this.canPlaceSelf = state
362    if (this._canPlacePeers) {
363      return this._canPlacePeers
364    }
365
366    // TODO: represent peerPath in ERESOLVE error somehow?
367    const peerPath = [...this.peerPath, this.dep]
368    let sawConflict = false
369    for (const peerEdge of this.dep.edgesOut.values()) {
370      if (!peerEdge.peer || !peerEdge.to || peerPath.includes(peerEdge.to)) {
371        continue
372      }
373      const peer = peerEdge.to
374      // it may be the case that the *initial* dep can be nested, but a peer
375      // of that dep needs to be placed shallower, because the target has
376      // a peer dep on the peer as well.
377      const target = deepestNestingTarget(this.target, peer.name)
378      const cpp = new CanPlaceDep({
379        dep: peer,
380        target,
381        parent: this,
382        edge: peerEdge,
383        peerPath,
384        // always place peers in preferDedupe mode
385        preferDedupe: true,
386      })
387      /* istanbul ignore next */
388      debug(() => {
389        if (this.children.some(c => c.dep === cpp.dep)) {
390          throw new Error('checking same dep repeatedly')
391        }
392      })
393      this.children.push(cpp)
394
395      if (cpp.canPlace === CONFLICT) {
396        sawConflict = true
397      }
398    }
399
400    this._canPlacePeers = sawConflict ? CONFLICT : state
401    return this._canPlacePeers
402  }
403
404  // what is the node that is causing this peerSet to be placed?
405  get peerSetSource () {
406    return this.parent ? this.parent.peerSetSource : this.edge.from
407  }
408
409  get peerEntryEdge () {
410    return this.top.edge
411  }
412
413  static get CONFLICT () {
414    return CONFLICT
415  }
416
417  static get OK () {
418    return OK
419  }
420
421  static get REPLACE () {
422    return REPLACE
423  }
424
425  static get KEEP () {
426    return KEEP
427  }
428
429  get description () {
430    const { canPlace } = this
431    return canPlace && canPlace.description ||
432    /* istanbul ignore next - old node affordance */ canPlace
433  }
434}
435
436module.exports = CanPlaceDep
437