Skip to content
This repository was archived by the owner on Jan 20, 2022. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix: move pathological nest fixing link to PlaceDep
When a dependency graph cycles back on itself incompatibly like this:

```
a@1 -> b@1
b@1 -> a@2
a@2 -> b@2
b@2 -> a@1
```

We would find ourselves unable to handle the conflict via nesting.  For
example:

```
root
+-- a@1 -> b@1
+-- b@1 -> a@2
    +-- a@2 -> b@2
    +-- b@2 -> a@1
        +-- a@1 -> b@1
        +-- b@1 -> a@2
            +-- a@2 -> b@2
            +-- b@2 -> a@1 (cycling forever)
```

In order to address this, we create a link when such a cycle is
detected.

```
root
+-- a@1 -> b@1
+-- b@1 -> a@2
    +-- a@2 -> b@2
    +-- b@2 -> a@1
        +-- a@1 -> b@1
        +-- b@1 -> link to root/node_modules/b@1
```

Prior to the recent refactor to move much of the dependency placement
logic out of Arborist.buildIdealTree and into the PlaceDep class, this
link was created right at the moment when a new dependency was created
in a temp tree.

However, if we feed that Link object into the PlaceDep flow, it will
(correctly) see that the Link does not match the Node further up the
tree, and attempt to replace it.

Compounding the problem (and why it appeared in `npm dedupe` and not
`npm install`) is the fact that explicitly named updates are _always_
treated as a "problem edge", so that they can be re-evaluated.

So, rather than creating a Node to be tested against the tree, it was
creating a Link object, and then attempting to replace the Link's target
with the Link itself, which caused some havoc.

This patch moves the loop detection and remediating Link creation into
the PlaceDep class, which is the more proper place for it, as that class
owns the "put deps into the tree" logic, and this is clearly a "put deps
into the tree" type of situation.

Via: @ParadoxInfinite
Close: npm/cli#3632
Close: #308
Fix: npm/cli#3565
  • Loading branch information
isaacs committed Aug 12, 2021
commit 6c54ba9157f7bc7375b07ecaed2c0c9ce2b55959
11 changes: 0 additions & 11 deletions lib/arborist/build-ideal-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -982,7 +982,6 @@ This is a one-time fix-up, please be patient...
// Note that the virtual root will also have virtual copies of the
// targets of any child Links, so that they resolve appropriately.
const parent = parent_ || this[_virtualRoot](edge.from)
const realParent = edge.peer ? edge.from.resolveParent : edge.from

const spec = npa.resolve(edge.name, edge.spec, edge.from.path)
const first = await this[_nodeFromSpec](edge.name, spec, parent, edge)
Expand Down Expand Up @@ -1013,16 +1012,6 @@ This is a one-time fix-up, please be patient...
required.has(secondEdge.from) && secondEdge.type !== 'peerOptional'))
required.add(node)

// handle otherwise unresolvable dependency nesting loops by
// creating a symbolic link
// a1 -> b1 -> a2 -> b2 -> a1 -> ...
// instead of nesting forever, when the loop occurs, create
// a symbolic link to the earlier instance
for (let p = edge.from.resolveParent; p; p = p.resolveParent) {
if (p.matches(node) && !p.isTop)
return new Link({ parent: realParent, target: p })
}

// keep track of the thing that caused this node to be included.
const src = parent.sourceReference
this[_peerSetSource].set(node, src)
Expand Down
17 changes: 16 additions & 1 deletion lib/place-dep.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const {
} = CanPlaceDep
const debug = require('./debug.js')

const Link = require('./link.js')
const gatherDepSet = require('./gather-dep-set.js')
const peerEntrySets = require('./peer-entry-sets.js')

Expand Down Expand Up @@ -256,6 +257,20 @@ class PlaceDep {
return
}

// we were told to place it here in the target, so either it does not
// already exist in the tree, OR it's shadowed.
// handle otherwise unresolvable dependency nesting loops by
// creating a symbolic link
// a1 -> b1 -> a2 -> b2 -> a1 -> ...
// instead of nesting forever, when the loop occurs, create
// a symbolic link to the earlier instance
for (let p = target; p; p = p.resolveParent) {
if (p.matches(dep) && !p.isTop) {
this.placed = new Link({ parent: target, target: p })
return
}
}

// XXX if we are replacing SOME of a peer entry group, we will need to
// remove any that are not being replaced and will now be invalid, and
// re-evaluate them deeper into the tree.
Expand All @@ -268,7 +283,7 @@ class PlaceDep {
integrity: dep.integrity,
legacyPeerDeps: this.legacyPeerDeps,
error: dep.errors[0],
...(dep.isLink ? { target: dep.target, realpath: dep.target.path } : {}),
...(dep.isLink ? { target: dep.target, realpath: dep.realpath } : {}),
})

this.oldDep = target.children.get(this.name)
Expand Down
44 changes: 21 additions & 23 deletions tap-snapshots/test/arborist/build-ideal-tree.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -76495,7 +76495,7 @@ ArboristNode {
}
`

exports[`test/arborist/build-ideal-tree.js TAP pathologically nested dependency cycle > expect resolving Promise 1`] = `
exports[`test/arborist/build-ideal-tree.js TAP pathologically nested dependency cycle > must match snapshot 1`] = `
ArboristNode {
"children": Map {
"@isaacs/pathological-dep-nesting-a" => ArboristNode {
Expand Down Expand Up @@ -76549,27 +76549,6 @@ ArboristNode {
"@isaacs/pathological-dep-nesting-b" => ArboristNode {
"children": Map {
"@isaacs/pathological-dep-nesting-a" => ArboristNode {
"children": Map {
"@isaacs/pathological-dep-nesting-b" => ArboristLink {
"edgesIn": Set {
EdgeIn {
"from": "node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-a",
"name": "@isaacs/pathological-dep-nesting-b",
"spec": "1",
"type": "prod",
},
},
"location": "node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-a/node_modules/@isaacs/pathological-dep-nesting-b",
"name": "@isaacs/pathological-dep-nesting-b",
"path": "{CWD}/test/fixtures/pathological-dep-nesting-cycle/node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-a/node_modules/@isaacs/pathological-dep-nesting-b",
"realpath": "{CWD}/test/fixtures/pathological-dep-nesting-cycle/node_modules/@isaacs/pathological-dep-nesting-b",
"resolved": "file:../../../../../../../..",
"target": ArboristNode {
"location": "node_modules/@isaacs/pathological-dep-nesting-b",
},
"version": "1.0.0",
},
},
"edgesIn": Set {
EdgeIn {
"from": "node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-b",
Expand All @@ -76582,7 +76561,7 @@ ArboristNode {
"@isaacs/pathological-dep-nesting-b" => EdgeOut {
"name": "@isaacs/pathological-dep-nesting-b",
"spec": "1",
"to": "node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-a/node_modules/@isaacs/pathological-dep-nesting-b",
"to": "node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-b",
"type": "prod",
},
},
Expand All @@ -76592,6 +76571,25 @@ ArboristNode {
"resolved": "https://registry.npmjs.org/@isaacs/pathological-dep-nesting-a/-/pathological-dep-nesting-a-1.0.0.tgz",
"version": "1.0.0",
},
"@isaacs/pathological-dep-nesting-b" => ArboristLink {
"edgesIn": Set {
EdgeIn {
"from": "node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-a",
"name": "@isaacs/pathological-dep-nesting-b",
"spec": "1",
"type": "prod",
},
},
"location": "node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-b",
"name": "@isaacs/pathological-dep-nesting-b",
"path": "{CWD}/test/fixtures/pathological-dep-nesting-cycle/node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-b/node_modules/@isaacs/pathological-dep-nesting-b",
"realpath": "{CWD}/test/fixtures/pathological-dep-nesting-cycle/node_modules/@isaacs/pathological-dep-nesting-b",
"resolved": "file:../../../../..",
"target": ArboristNode {
"location": "node_modules/@isaacs/pathological-dep-nesting-b",
},
"version": "1.0.0",
},
},
"edgesIn": Set {
EdgeIn {
Expand Down
90 changes: 90 additions & 0 deletions tap-snapshots/test/place-dep.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4369,6 +4369,96 @@ exports[`test/place-dep.js TAP placement tests nest peer set under dependent nod
Array []
`

exports[`test/place-dep.js TAP placement tests pathologically nested dependency cycle > changes to tree 1`] = `
--- expected
+++ actual
@@ -118,13 +118,6 @@
"spec": "2",
"from": "node_modules/b/node_modules/a",
},
- EdgeIn {
- "type": "prod",
- "name": "b",
- "spec": "1",
- "error": "INVALID",
- "from": "node_modules/b/node_modules/b/node_modules/a",
- },
},
"children": Map {
"a" => ArboristNode {
@@ -141,8 +134,7 @@
"type": "prod",
"name": "b",
"spec": "1",
- "to": "node_modules/b/node_modules/b",
- "error": "INVALID",
+ "to": "node_modules/b/node_modules/b/node_modules/b",
},
},
"edgesIn": Set {
@@ -154,6 +146,29 @@
},
},
},
+ "b" => ArboristLink {
+ "name": "b",
+ "version": "1.0.0",
+ "location": "node_modules/b/node_modules/b/node_modules/b",
+ "path": "/some/path/node_modules/b/node_modules/b/node_modules/b",
+ "realpath": "/some/path/node_modules/b",
+ "resolved": "file:../../..",
+ "extraneous": true,
+ "dev": true,
+ "optional": true,
+ "peer": true,
+ "edgesIn": Set {
+ EdgeIn {
+ "type": "prod",
+ "name": "b",
+ "spec": "1",
+ "from": "node_modules/b/node_modules/b/node_modules/a",
+ },
+ },
+ "target": ArboristNode {
+ "location": "node_modules/b",
+ },
+ },
},
},
},

`

exports[`test/place-dep.js TAP placement tests pathologically nested dependency cycle > placements 1`] = `
Array [
Object {
"canPlace": Symbol(OK),
"canPlaceSelf": Symbol(OK),
"checks": Map {
"node_modules/b/node_modules/b/node_modules/a" => Array [
Symbol(OK),
Symbol(OK),
],
"node_modules/b/node_modules/b" => Array [
Symbol(OK),
Symbol(OK),
],
"node_modules/b" => Array [
Symbol(CONFLICT),
Symbol(CONFLICT),
],
},
"dep": "[email protected]",
"edge": "{ node_modules/b/node_modules/b/node_modules/a prod b@1 }",
"placed": "node_modules/b/node_modules/b/node_modules/b",
},
]
`

exports[`test/place-dep.js TAP placement tests pathologically nested dependency cycle > warnings 1`] = `
Array []
`

exports[`test/place-dep.js TAP placement tests peer all the way down, conflict but not ours > changes to tree 1`] = `
--- expected
+++ actual
Expand Down
7 changes: 4 additions & 3 deletions test/arborist/build-ideal-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -818,9 +818,10 @@ t.test('update global space single dep', t => {
})

// if we get this wrong, it'll spin forever and use up all the memory
t.test('pathologically nested dependency cycle', t =>
t.resolveMatchSnapshot(printIdeal(
resolve(fixtures, 'pathological-dep-nesting-cycle'))))
t.test('pathologically nested dependency cycle', async t => {
t.matchSnapshot(await printIdeal(
resolve(fixtures, 'pathological-dep-nesting-cycle')))
})

t.test('resolve file deps from cwd', t => {
const cwd = process.cwd()
Expand Down
28 changes: 28 additions & 0 deletions test/place-dep.js
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,34 @@ t.test('placement tests', t => {
},
})

// pathologically nested dep cycle
// a1 -> b1 -> a2 -> b2 -> a1
runTest('pathologically nested dependency cycle', {
tree: new Node({
path,
pkg: { dependencies: { a: '1' }},
children: [
{ pkg: { name: 'a', version: '1.0.0', dependencies: { b: '1' }}},
{
pkg: { name: 'b', version: '1.0.0', dependencies: { a: '2' }},
children: [
{ pkg: { name: 'a', version: '2.0.0', dependencies: { b: '2' }}},
{
pkg: { name: 'b', version: '2.0.0', dependencies: { a: '1' }},
children: [
{ pkg: { name: 'a', version: '1.0.0', dependencies: { b: '1' }}},
],
},
],
},
],
}),
dep: new Node({
pkg: { name: 'b', version: '1.0.0', dependencies: { a: '2' }},
}),
nodeLoc: 'node_modules/b/node_modules/b/node_modules/a',
})

// peer dep shenanigans
runTest('basic placement of a production dep with peer deps', {
tree: new Node({
Expand Down