Skip to content

Commit b9225e5

Browse files
authored
fix: resolve override conflicts and apply correct versions (npm#8089)
A first step in fixing the overrides feature. In this PR I'm aiming to fix 3 bugs: 1. When we add an edge going into a node we update the node's overrides, but we don't update the overrides of that node's outgoing edges, and so forth. We need the up-to-date overrides to filter through. 2. When we remove an edge going into a node we don't update the overrides at all (and we don't update the outgoing edges like in the previous bug). 3. When we add an edge going in, and we already have a different override set for the node, we just ignore the existing override set and overwrite it with that of the new edge. Instead, this PR chooses the most specific override set. This still isn't the absolutely correct logic, since different override sets can have implications down the line of the dependency chain, but it has the advantage of being **consistent** (instead of just going with the last edge in). Also, it raises an error if it encounters a real conflict, meaning two incoming edges with override sets that aren't just a subset of one another. So if we have dependency chains A->B->C and A->C, and we override C under B, then C will be overridden. ## References Fixes some of the override [issues](npm#5850). Primary author: @AlonNavo Co-author: @owlstronaut
1 parent e345cc5 commit b9225e5

File tree

8 files changed

+884
-40
lines changed

8 files changed

+884
-40
lines changed

workspaces/arborist/lib/dep-valid.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const depValid = (child, requested, requestor) => {
101101
})
102102
}
103103

104-
default: // unpossible, just being cautious
104+
default: // impossible, just being cautious
105105
break
106106
}
107107

workspaces/arborist/lib/edge.js

+57-14
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
const util = require('node:util')
55
const npa = require('npm-package-arg')
66
const depValid = require('./dep-valid.js')
7+
const OverrideSet = require('./override-set.js')
78

89
class ArboristEdge {
910
constructor (edge) {
@@ -103,7 +104,7 @@ class Edge {
103104
}
104105

105106
satisfiedBy (node) {
106-
if (node.name !== this.#name) {
107+
if (node.name !== this.#name || !this.#from) {
107108
return false
108109
}
109110

@@ -112,7 +113,31 @@ class Edge {
112113
if (node.hasShrinkwrap || node.inShrinkwrap || node.inBundle) {
113114
return depValid(node, this.rawSpec, this.#accept, this.#from)
114115
}
115-
return depValid(node, this.spec, this.#accept, this.#from)
116+
117+
// If there's no override we just use the spec.
118+
if (!this.overrides?.keySpec) {
119+
return depValid(node, this.spec, this.#accept, this.#from)
120+
}
121+
// There's some override. If the target node satisfies the overriding spec
122+
// then it's okay.
123+
if (depValid(node, this.spec, this.#accept, this.#from)) {
124+
return true
125+
}
126+
// If it doesn't, then it should at least satisfy the original spec.
127+
if (!depValid(node, this.rawSpec, this.#accept, this.#from)) {
128+
return false
129+
}
130+
// It satisfies the original spec, not the overriding spec. We need to make
131+
// sure it doesn't use the overridden spec.
132+
// For example:
133+
// we might have an ^8.0.0 rawSpec, and an override that makes
134+
// keySpec=8.23.0 and the override value spec=9.0.0.
135+
// If the node is 9.0.0, then it's okay because it's consistent with spec.
136+
// If the node is 8.24.0, then it's okay because it's consistent with the rawSpec.
137+
// If the node is 8.23.0, then it's not okay because even though it's consistent
138+
// with the rawSpec, it's also consistent with the keySpec.
139+
// So we're looking for ^8.0.0 or 9.0.0 and not 8.23.0.
140+
return !depValid(node, this.overrides.keySpec, this.#accept, this.#from)
116141
}
117142

118143
// return the edge data, and an explanation of how that edge came to be here
@@ -181,11 +206,9 @@ class Edge {
181206
if (this.overrides?.value && this.overrides.value !== '*' && this.overrides.name === this.#name) {
182207
if (this.overrides.value.startsWith('$')) {
183208
const ref = this.overrides.value.slice(1)
184-
// we may be a virtual root, if we are we want to resolve reference overrides
185-
// from the real root, not the virtual one
186-
const pkg = this.#from.sourceReference
187-
? this.#from.sourceReference.root.package
188-
: this.#from.root.package
209+
const pkg = this.#from?.sourceReference
210+
? this.#from?.sourceReference.root.package
211+
: this.#from?.root?.package
189212
if (pkg.devDependencies?.[ref]) {
190213
return pkg.devDependencies[ref]
191214
}
@@ -234,10 +257,15 @@ class Edge {
234257
} else {
235258
this.#error = 'MISSING'
236259
}
237-
} else if (this.peer && this.#from === this.#to.parent && !this.#from.isTop) {
260+
} else if (this.peer && this.#from === this.#to.parent && !this.#from?.isTop) {
238261
this.#error = 'PEER LOCAL'
239262
} else if (!this.satisfiedBy(this.#to)) {
240263
this.#error = 'INVALID'
264+
} else if (this.overrides && this.#to.edgesOut.size && OverrideSet.doOverrideSetsConflict(this.overrides, this.#to.overrides)) {
265+
// Any inconsistency between the edge's override set and the target's override set is potentially problematic.
266+
// But we only say the edge is in error if the override sets are plainly conflicting.
267+
// Note that if the target doesn't have any dependencies of their own, then this inconsistency is irrelevant.
268+
this.#error = 'INVALID'
241269
} else {
242270
this.#error = 'OK'
243271
}
@@ -250,15 +278,26 @@ class Edge {
250278

251279
reload (hard = false) {
252280
this.#explanation = null
253-
if (this.#from.overrides) {
254-
this.overrides = this.#from.overrides.getEdgeRule(this)
281+
282+
let needToUpdateOverrideSet = false
283+
let newOverrideSet
284+
let oldOverrideSet
285+
if (this.#from?.overrides) {
286+
newOverrideSet = this.#from.overrides.getEdgeRule(this)
287+
if (newOverrideSet && !newOverrideSet.isEqual(this.overrides)) {
288+
// If there's a new different override set we need to propagate it to the nodes.
289+
// If we're deleting the override set then there's no point propagating it right now since it will be filled with another value later.
290+
needToUpdateOverrideSet = true
291+
oldOverrideSet = this.overrides
292+
this.overrides = newOverrideSet
293+
}
255294
} else {
256295
delete this.overrides
257296
}
258-
const newTo = this.#from.resolve(this.#name)
297+
const newTo = this.#from?.resolve(this.#name)
259298
if (newTo !== this.#to) {
260299
if (this.#to) {
261-
this.#to.edgesIn.delete(this)
300+
this.#to.deleteEdgeIn(this)
262301
}
263302
this.#to = newTo
264303
this.#error = null
@@ -267,15 +306,19 @@ class Edge {
267306
}
268307
} else if (hard) {
269308
this.#error = null
309+
} else if (needToUpdateOverrideSet && this.#to) {
310+
// Propagate the new override set to the target node.
311+
this.#to.updateOverridesEdgeInRemoved(oldOverrideSet)
312+
this.#to.updateOverridesEdgeInAdded(newOverrideSet)
270313
}
271314
}
272315

273316
detach () {
274317
this.#explanation = null
275318
if (this.#to) {
276-
this.#to.edgesIn.delete(this)
319+
this.#to.deleteEdgeIn(this)
277320
}
278-
this.#from.edgesOut.delete(this.#name)
321+
this.#from?.edgesOut.delete(this.#name)
279322
this.#to = null
280323
this.#error = 'DETACHED'
281324
this.#from = null

workspaces/arborist/lib/node.js

+124-15
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const debug = require('./debug.js')
4040
const gatherDepSet = require('./gather-dep-set.js')
4141
const treeCheck = require('./tree-check.js')
4242
const { walkUp } = require('walk-up-path')
43+
const { log } = require('proc-log')
4344

4445
const { resolve, relative, dirname, basename } = require('node:path')
4546
const util = require('node:util')
@@ -344,7 +345,28 @@ class Node {
344345
}
345346

346347
get overridden () {
347-
return !!(this.overrides && this.overrides.value && this.overrides.name === this.name)
348+
if (!this.overrides) {
349+
return false
350+
}
351+
if (!this.overrides.value) {
352+
return false
353+
}
354+
if (this.overrides.name !== this.name) {
355+
return false
356+
}
357+
358+
// The overrides rule is for a package with this name, but some override rules only apply to specific
359+
// versions. To make sure this package was actually overridden, we check whether any edge going in
360+
// had the rule applied to it, in which case its overrides set is different than its source node.
361+
for (const edge of this.edgesIn) {
362+
if (edge.overrides && edge.overrides.name === this.name && edge.overrides.value === this.version) {
363+
if (!edge.overrides.isEqual(edge.from.overrides)) {
364+
return true
365+
}
366+
}
367+
}
368+
369+
return false
348370
}
349371

350372
get package () {
@@ -822,9 +844,6 @@ class Node {
822844
target.root = root
823845
}
824846

825-
if (!this.overrides && this.parent && this.parent.overrides) {
826-
this.overrides = this.parent.overrides.getNodeRule(this)
827-
}
828847
// tree should always be valid upon root setter completion.
829848
treeCheck(this)
830849
if (this !== root) {
@@ -1006,10 +1025,21 @@ class Node {
10061025
return false
10071026
}
10081027

1009-
// XXX need to check for two root nodes?
1010-
if (node.overrides !== this.overrides) {
1011-
return false
1028+
// If this node has no dependencies, then it's irrelevant to check the override
1029+
// rules of the replacement node.
1030+
if (this.edgesOut.size) {
1031+
// XXX need to check for two root nodes?
1032+
if (node.overrides) {
1033+
if (!node.overrides.isEqual(this.overrides)) {
1034+
return false
1035+
}
1036+
} else {
1037+
if (this.overrides) {
1038+
return false
1039+
}
1040+
}
10121041
}
1042+
10131043
ignorePeers = new Set(ignorePeers)
10141044

10151045
// gather up all the deps of this node and that are only depended
@@ -1077,8 +1107,13 @@ class Node {
10771107
return false
10781108
}
10791109

1080-
// if we prefer dedupe, or if the version is greater/equal, take the other
1081-
if (preferDedupe || semver.gte(other.version, this.version)) {
1110+
// if we prefer dedupe, or if the version is equal, take the other
1111+
if (preferDedupe || semver.eq(other.version, this.version)) {
1112+
return true
1113+
}
1114+
1115+
// if our current version isn't the result of an override, then prefer to take the greater version
1116+
if (!this.overridden && semver.gt(other.version, this.version)) {
10821117
return true
10831118
}
10841119

@@ -1249,10 +1284,6 @@ class Node {
12491284
this[_changePath](newPath)
12501285
}
12511286

1252-
if (parent.overrides) {
1253-
this.overrides = parent.overrides.getNodeRule(this)
1254-
}
1255-
12561287
// clobbers anything at that path, resets all appropriate references
12571288
this.root = parent.root
12581289
}
@@ -1346,9 +1377,87 @@ class Node {
13461377
this.edgesOut.set(edge.name, edge)
13471378
}
13481379

1349-
addEdgeIn (edge) {
1380+
recalculateOutEdgesOverrides () {
1381+
// For each edge out propogate the new overrides through.
1382+
for (const edge of this.edgesOut.values()) {
1383+
edge.reload(true)
1384+
if (edge.to) {
1385+
edge.to.updateOverridesEdgeInAdded(edge.overrides)
1386+
}
1387+
}
1388+
}
1389+
1390+
updateOverridesEdgeInRemoved (otherOverrideSet) {
1391+
// If this edge's overrides isn't equal to this node's overrides, then removing it won't change newOverrideSet later.
1392+
if (!this.overrides || !this.overrides.isEqual(otherOverrideSet)) {
1393+
return false
1394+
}
1395+
let newOverrideSet
1396+
for (const edge of this.edgesIn) {
1397+
if (newOverrideSet && edge.overrides) {
1398+
newOverrideSet = OverrideSet.findSpecificOverrideSet(edge.overrides, newOverrideSet)
1399+
} else {
1400+
newOverrideSet = edge.overrides
1401+
}
1402+
}
1403+
if (this.overrides.isEqual(newOverrideSet)) {
1404+
return false
1405+
}
1406+
this.overrides = newOverrideSet
1407+
if (this.overrides) {
1408+
// Optimization: if there's any override set at all, then no non-extraneous node has an empty override set. So if we temporarily have no
1409+
// override set (for example, we removed all the edges in), there's no use updating all the edges out right now. Let's just wait until
1410+
// we have an actual override set later.
1411+
this.recalculateOutEdgesOverrides()
1412+
}
1413+
return true
1414+
}
1415+
1416+
// This logic isn't perfect either. When we have two edges in that have different override sets, then we have to decide which set is correct.
1417+
// This function assumes the more specific override set is applicable, so if we have dependencies A->B->C and A->C
1418+
// and an override set that specifies what happens for C under A->B, this will work even if the new A->C edge comes along and tries to change
1419+
// the override set.
1420+
// The strictly correct logic is not to allow two edges with different overrides to point to the same node, because even if this node can satisfy
1421+
// both, one of its dependencies might need to be different depending on the edge leading to it.
1422+
// However, this might cause a lot of duplication, because the conflict in the dependencies might never actually happen.
1423+
updateOverridesEdgeInAdded (otherOverrideSet) {
1424+
if (!otherOverrideSet) {
1425+
// Assuming there are any overrides at all, the overrides field is never undefined for any node at the end state of the tree.
1426+
// So if the new edge's overrides is undefined it will be updated later. So we can wait with updating the node's overrides field.
1427+
return false
1428+
}
1429+
if (!this.overrides) {
1430+
this.overrides = otherOverrideSet
1431+
this.recalculateOutEdgesOverrides()
1432+
return true
1433+
}
1434+
if (this.overrides.isEqual(otherOverrideSet)) {
1435+
return false
1436+
}
1437+
const newOverrideSet = OverrideSet.findSpecificOverrideSet(this.overrides, otherOverrideSet)
1438+
if (newOverrideSet) {
1439+
if (!this.overrides.isEqual(newOverrideSet)) {
1440+
this.overrides = newOverrideSet
1441+
this.recalculateOutEdgesOverrides()
1442+
return true
1443+
}
1444+
return false
1445+
}
1446+
// This is an error condition. We can only get here if the new override set is in conflict with the existing.
1447+
log.silly('Conflicting override sets', this.name)
1448+
}
1449+
1450+
deleteEdgeIn (edge) {
1451+
this.edgesIn.delete(edge)
13501452
if (edge.overrides) {
1351-
this.overrides = edge.overrides
1453+
this.updateOverridesEdgeInRemoved(edge.overrides)
1454+
}
1455+
}
1456+
1457+
addEdgeIn (edge) {
1458+
// We need to handle the case where the new edge in has an overrides field which is different from the current value.
1459+
if (!this.overrides || !this.overrides.isEqual(edge.overrides)) {
1460+
this.updateOverridesEdgeInAdded(edge.overrides)
13521461
}
13531462

13541463
this.edgesIn.add(edge)

0 commit comments

Comments
 (0)