Skip to content

Commit c2ced1a

Browse files
committed
Cleanup arborist
1 parent 85b76ac commit c2ced1a

File tree

1 file changed

+68
-41
lines changed

1 file changed

+68
-41
lines changed

src/shadow/arborist.ts

Lines changed: 68 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import events from 'node:events'
2-
import { existsSync, readFileSync, realpathSync } from 'node:fs'
2+
import { readFileSync, realpathSync } from 'node:fs'
33
import https from 'node:https'
44
import path from 'node:path'
55
import rl from 'node:readline'
@@ -35,14 +35,16 @@ type ArboristClass = typeof BaseArborist & {
3535
new (...args: any): typeof BaseArborist
3636
}
3737

38-
type EdgeClass = BaseEdge & {
38+
type EdgeClass = Omit<BaseEdge, 'overrides' | 'reload'> & {
3939
optional: boolean
4040
overrides: OverrideSet | undefined
4141
peer: boolean
4242
peerConflicted: boolean
4343
rawSpec: string
4444
get spec(): string
45+
get to(): NodeClass | null
4546
new (...args: any): EdgeClass
47+
reload(hard?: boolean): void
4648
satisfiedBy(node: NodeClass): boolean
4749
}
4850

@@ -78,17 +80,20 @@ type InstallEffect = {
7880
newPackage: PURLParts
7981
}
8082

81-
type NodeClass = BaseNode & {
83+
type NodeClass = Omit<BaseNode, 'edgesOut' | 'isTop' | 'parent' | 'resolve'> & {
8284
name: string
8385
version: string
8486
edgesIn: Set<SafeEdge>
8587
edgesOut: Map<string, SafeEdge>
8688
hasShrinkwrap: boolean
8789
inShrinkwrap: boolean | undefined
90+
isTop: boolean | undefined
8891
overrides: OverrideSet | undefined
89-
new (...args: any): BaseNode
92+
parent: NodeClass | null
93+
new (...args: any): NodeClass
9094
addEdgeIn(edge: SafeEdge): void
9195
addEdgeOut(edge: SafeEdge): void
96+
resolve(name: string): NodeClass
9297
}
9398

9499
interface OverrideSet {
@@ -220,7 +225,7 @@ async function* batchScan(
220225
}
221226
})
222227
}
223-
// TODO: migrate to SDK
228+
// TODO: Migrate to SDK.
224229
const pkgDataReq = https
225230
.request(`${API_V0_URL}/scan/batch`, {
226231
method: 'POST',
@@ -243,8 +248,9 @@ async function* batchScan(
243248

244249
function deleteEdgeIn(node: NodeClass, edge: SafeEdge) {
245250
node.edgesIn.delete(edge)
246-
if (edge.overrides) {
247-
updateNodeOverrideSetDueToEdgeRemoval(node, edge.overrides)
251+
const { overrides } = edge
252+
if (overrides) {
253+
updateNodeOverrideSetDueToEdgeRemoval(node, overrides)
248254
}
249255
}
250256

@@ -293,35 +299,43 @@ function findSpecificOverrideSet(
293299
overrideSet = overrideSet.parent
294300
}
295301
console.error('Conflicting override sets')
302+
return undefined
296303
}
297304

298305
function maybeReadfileSync(filepath: string): string | undefined {
299306
try {
300-
return existsSync(filepath) ? readFileSync(filepath, 'utf8') : undefined
307+
return readFileSync(filepath, 'utf8')
301308
} catch {}
302309
return undefined
303310
}
304311

305312
function overrideSetsChildrenAreEqual(
306313
overrideSet: OverrideSet,
307314
other: OverrideSet
308-
) {
309-
const { children } = overrideSet
310-
const { children: otherChildren } = other
311-
if (children.size !== otherChildren.size) {
312-
return false
313-
}
314-
for (const key of children.keys()) {
315-
if (!otherChildren.has(key)) {
316-
return false
315+
): boolean {
316+
const queue: [OverrideSet, OverrideSet][] = [[overrideSet, other]]
317+
let pos = 0
318+
let { length: queueLength } = queue
319+
while (pos < queueLength) {
320+
if (pos === LOOP_SENTINEL) {
321+
throw new Error('Detected infinite loop while comparing override sets')
317322
}
318-
const child = <OverrideSet>children.get(key)
319-
const otherChild = <OverrideSet>otherChildren.get(key)
320-
if (child!.value !== otherChild!.value) {
323+
const { 0: currSet, 1: currOtherSet } = queue[pos++]!
324+
const { children } = currSet
325+
const { children: otherChildren } = currOtherSet
326+
if (children.size !== otherChildren.size) {
321327
return false
322328
}
323-
if (!overrideSetsChildrenAreEqual(child, otherChild)) {
324-
return false
329+
for (const key of children.keys()) {
330+
if (!otherChildren.has(key)) {
331+
return false
332+
}
333+
const child = <OverrideSet>children.get(key)
334+
const otherChild = <OverrideSet>otherChildren.get(key)
335+
if (child!.value !== otherChild!.value) {
336+
return false
337+
}
338+
queue[queueLength++] = [child, otherChild]
325339
}
326340
}
327341
return true
@@ -479,7 +493,7 @@ function pkgidParts(pkgid: string) {
479493

480494
function recalculateOutEdgesOverrides(node: NodeClass) {
481495
// For each edge out propagate the new overrides through.
482-
for (const [, edge] of node.edgesOut) {
496+
for (const edge of node.edgesOut.values()) {
483497
edge.reload(true)
484498
if (edge.to) {
485499
updateNodeOverrideSet(edge.to, edge.overrides)
@@ -505,25 +519,27 @@ function updateNodeOverrideSetDueToEdgeRemoval(
505519
node: NodeClass,
506520
other: OverrideSet
507521
) {
522+
const { overrides } = node
508523
// If this edge's overrides isn't equal to this node's overrides, then removing
509524
// it won't change newOverrideSet later.
510-
if (!node.overrides || !overrideSetsEqual(node.overrides, other)) {
525+
if (!overrides || !overrideSetsEqual(overrides, other)) {
511526
return false
512527
}
513528
let newOverrideSet
514529
for (const edge of node.edgesIn) {
530+
const { overrides: edgeOverrides } = edge
515531
if (newOverrideSet) {
516-
newOverrideSet = findSpecificOverrideSet(edge.overrides, newOverrideSet)
532+
newOverrideSet = findSpecificOverrideSet(edgeOverrides, newOverrideSet)
517533
} else {
518-
newOverrideSet = edge.overrides
534+
newOverrideSet = edgeOverrides
519535
}
520536
}
521-
if (overrideSetsEqual(node.overrides, newOverrideSet)) {
537+
if (overrideSetsEqual(overrides, newOverrideSet)) {
522538
return false
523539
}
524540
node.overrides = newOverrideSet
525-
if (node.overrides) {
526-
// Optimization: if there's any override set at all, then no non-extraneous
541+
if (newOverrideSet) {
542+
// Optimization: If there's any override set at all, then no non-extraneous
527543
// node has an empty override set. So if we temporarily have no override set
528544
// (for example, we removed all the edges in), there's no use updating all
529545
// the edges out right now. Let's just wait until we have an actual override
@@ -565,15 +581,17 @@ function updateNodeOverrideSet(
565581
}
566582
const newOverrideSet = findSpecificOverrideSet(overrides, otherOverrideSet)
567583
if (newOverrideSet) {
568-
if (!overrideSetsEqual(overrides, newOverrideSet)) {
569-
node.overrides = newOverrideSet
570-
recalculateOutEdgesOverrides(node)
571-
return true
584+
if (overrideSetsEqual(overrides, newOverrideSet)) {
585+
return false
572586
}
573-
return false
587+
node.overrides = newOverrideSet
588+
recalculateOutEdgesOverrides(node)
589+
return true
574590
}
575591
// This is an error condition. We can only get here if the new override set is
576592
// in conflict with the existing.
593+
console.error('Conflicting override sets')
594+
return false
577595
}
578596

579597
function walk(
@@ -635,8 +653,14 @@ function walk(
635653
return needInfoOn
636654
}
637655

638-
// An edge in the dependency graph
639-
// Represents a dependency relationship of some kind
656+
// Copied from
657+
// https://github.com/npm/cli/blob/v10.9.0/workspaces/arborist/lib/edge.js:
658+
// The npm application
659+
// Copyright (c) npm, Inc. and Contributors
660+
// Licensed on the terms of The Artistic License 2.0
661+
//
662+
// An edge in the dependency graph.
663+
// Represents a dependency relationship of some kind.
640664

641665
class SafeEdge extends Edge {
642666
#safeAccept: string | undefined
@@ -648,7 +672,6 @@ class SafeEdge extends Edge {
648672
constructor(options: EdgeOptions) {
649673
const { accept, from } = options
650674
// Defer to supper to validate options and assign non-private values.
651-
// @ts-ignore: Incorrectly typed.
652675
super(options)
653676
if (accept !== undefined) {
654677
this.#safeAccept = accept || '*'
@@ -660,7 +683,7 @@ class SafeEdge extends Edge {
660683
this.reload(true)
661684
}
662685

663-
// return the edge data, and an explanation of how that edge came to be here
686+
// Return the edge data, and an explanation of how that edge came to be here.
664687
// @ts-ignore: Edge#explain is defined with an unused `seen = []` param.
665688
override explain() {
666689
if (!this.#safeExplanation) {
@@ -733,7 +756,6 @@ class SafeEdge extends Edge {
733756
return this.#safeAccept
734757
}
735758

736-
// @ts-ignore: Incorrectly typed as a property instead of an accessor.
737759
override get error() {
738760
if (!this.#safeError) {
739761
if (!this.#safeTo) {
@@ -770,6 +792,8 @@ class SafeEdge extends Edge {
770792
const newTo = this.#safeFrom?.resolve(this.name)
771793
if (newTo !== this.#safeTo) {
772794
if (this.#safeTo) {
795+
// Instead of `this.#safeTo.edgesIn.delete(this)` we patch based on
796+
// https://github.com/npm/cli/pull/7025.
773797
deleteEdgeIn(this.#safeTo, this)
774798
}
775799
this.#safeTo = <NodeClass>newTo ?? null
@@ -785,6 +809,8 @@ class SafeEdge extends Edge {
785809
detach() {
786810
this.#safeExplanation = null
787811
if (this.#safeTo) {
812+
// Instead of `this.#safeTo.edgesIn.delete(this)` we patch based on
813+
// https://github.com/npm/cli/pull/7025.
788814
deleteEdgeIn(this.#safeTo, this)
789815
}
790816
if (this.#safeFrom) {
@@ -830,11 +856,12 @@ export class SafeArborist extends Arborist {
830856
): Promise<NodeClass> {
831857
// SafeArborist has suffered side effects and must be rebuilt from scratch.
832858
const arb = new Arborist(...(this as any)[kCtorArgs])
833-
const ret = <NodeClass>await arb.reify(...args)
859+
const ret = <unknown>await arb.reify(...args)
834860
Object.assign(this, arb)
835-
return ret
861+
return <NodeClass>ret
836862
}
837863

864+
// @ts-ignore Incorrectly typed.
838865
override async reify(
839866
...args: Parameters<InstanceType<ArboristClass>['reify']>
840867
): Promise<NodeClass> {

0 commit comments

Comments
 (0)