Skip to content

Commit 500ea3f

Browse files
committed
fix(manifest): leave a component with a missing artifact file without targets
Reachability reads a sidecar component with no targets as not installed, and targets: [] as resolved with no artifact. A component whose files were partly gone from disk got the surviving subset, and one with none got an empty list, so it looked resolved. Producers can now mark an artifact they could not fetch with an unfetched record, and any such or missing file leaves the component without targets.
1 parent 876048d commit 500ea3f

6 files changed

Lines changed: 80 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1111

1212
### Fixed
1313
- With `--dynamic-sbom-inference`, an sbt build that fails before writing its results is reported as failed instead of reusing another build's results.
14+
- JVM dependencies whose jars cannot be fetched, or are gone from disk, no longer reach reachability analysis looking resolved without code.
1415

1516
## [1.10.0](https://github.com/SocketDev/socket-cli/releases/tag/v1.10.0) - 2026-10-22
1617

‎src/commands/manifest/scripts/assemble.mts‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ type MergedNode = {
3333
// projectKey when this node is a build project, else empty.
3434
project: string
3535
targets: Set<string>
36+
unfetched: boolean
3637
}
3738

3839
type SubprojectGraph = {
@@ -52,6 +53,7 @@ type RootNode = {
5253
direct: boolean
5354
project: string
5455
targets: string[]
56+
unfetched: boolean
5557
}
5658

5759
export function assembleFacts(
@@ -116,10 +118,12 @@ function buildPerRoot(parsed: ParsedRecords): Map<string, PerRoot> {
116118
direct: false,
117119
project: n.project,
118120
targets: [],
121+
unfetched: false,
119122
}
120123
nodes.set(id, node)
121124
}
122125
node.direct ||= n.direct
126+
node.unfetched ||= n.unfetched
123127
node.targets.push(...n.targets)
124128
}
125129
for (const [p, c] of r.edges) {
@@ -153,6 +157,7 @@ function mergeById(perRoot: Map<string, PerRoot>): Map<string, MergedNode> {
153157
direct: false,
154158
project: node.project,
155159
targets: new Set(),
160+
unfetched: false,
156161
}
157162
finalNodes.set(coordId, fn)
158163
}
@@ -162,6 +167,9 @@ function mergeById(perRoot: Map<string, PerRoot>): Map<string, MergedNode> {
162167
if (node.direct) {
163168
fn.direct = true
164169
}
170+
if (node.unfetched) {
171+
fn.unfetched = true
172+
}
165173
for (const t of node.targets) {
166174
fn.targets.add(t)
167175
}
@@ -408,10 +416,11 @@ function buildArtifactPaths(
408416
const pathsById: ResolvedArtifactPaths['pathsById'] = new Map()
409417
for (const [id, fn] of finalNodes) {
410418
if (!fn.project) {
411-
pathsById.set(id, {
412-
sources: [],
413-
targets: [...fn.targets].filter(fileExists).sort(),
414-
})
419+
const targets = [...fn.targets].filter(fileExists).sort()
420+
// An artifact with any file that could not be fetched, or is gone from
421+
// disk, has no targets: a partial list would claim it resolved whole.
422+
const unresolved = fn.unfetched || targets.length < fn.targets.size
423+
pathsById.set(id, unresolved ? { sources: [] } : { sources: [], targets })
415424
}
416425
}
417426
// A project's own component shares its id, so this also covers dependency

‎src/commands/manifest/scripts/assemble.test.mts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,54 @@ describe('records → assemble → sidecar', () => {
115115
expect(bom?.targets).toEqual([])
116116
expect(bom?.sources).toEqual([])
117117
})
118+
it('states no targets for an artifact that failed to resolve or is gone from disk', () => {
119+
const records = [
120+
'meta\tmaven\t3.9.6\t17',
121+
'project\tg:app:1\tg\tapp\t1\t.',
122+
'root\t0\tg:app:1\tcompile\t1',
123+
'node\t0\tg:ok:jar:1\tg\tok\t1\tjar\t\t1',
124+
'node\t0\tg:nojar:jar:1\tg\tnojar\t1\tjar\t\t1',
125+
'node\t0\tg:nojar:test-jar:tests:1\tg\tnojar\t1\ttest-jar\ttests\t1',
126+
'node\t0\tg:gone:jar:1\tg\tgone\t1\tjar\t\t1',
127+
'node\t0\tg:bom:pom:1\tg\tbom\t1\tpom\t\t1',
128+
'node\t0\tg:half:jar:1\tg\thalf\t1\tjar\t\t1',
129+
'node\t0\tg:halfgone:jar:1\tg\thalfgone\t1\tjar\t\t1',
130+
'file\t0\tg:ok:jar:1\t/abs/ok.jar',
131+
'file\t0\tg:half:jar:1\t/abs/half-a.jar',
132+
'unfetched\t0\tg:half:jar:1',
133+
'file\t0\tg:halfgone:jar:1\t/abs/halfgone-a.jar',
134+
'file\t0\tg:halfgone:jar:1\t/abs/gone.jar',
135+
'file\t0\tg:gone:jar:1\t/abs/gone.jar',
136+
'file\t0\tg:nojar:test-jar:tests:1\t/abs/nojar-tests.jar',
137+
'unfetched\t0\tg:nojar:jar:1',
138+
].join('\n')
139+
const { artifactPaths, facts } = assembleFacts(parseRecords(records), {
140+
fileExists: p => p !== '/abs/gone.jar',
141+
})
142+
const acc: SidecarAccumulator = new Map()
143+
accumulateSidecar(
144+
acc,
145+
facts,
146+
artifactPaths,
147+
'/abs/pom.xml.socket.facts.json',
148+
)
149+
const byId = new Map(
150+
serializeSidecar(acc)['/abs/pom.xml.socket.facts.json']!.components.map(
151+
c => [c.id, c],
152+
),
153+
)
154+
155+
expect(byId.get('g:ok:jar:1')?.targets).toEqual(['/abs/ok.jar'])
156+
expect(byId.get('g:nojar:jar:1')).not.toHaveProperty('targets')
157+
expect(byId.get('g:nojar:test-jar:tests:1')?.targets).toEqual([
158+
'/abs/nojar-tests.jar',
159+
])
160+
expect(byId.get('g:gone:jar:1')).not.toHaveProperty('targets')
161+
expect(byId.get('g:bom:pom:1')?.targets).toEqual([])
162+
// One of several files missing leaves the artifact incomplete.
163+
expect(byId.get('g:half:jar:1')).not.toHaveProperty('targets')
164+
expect(byId.get('g:halfgone:jar:1')).not.toHaveProperty('targets')
165+
})
118166
it('keeps a coordinate with divergent subtrees as one component with a dependency entry per subtree', () => {
119167
// :a and :b both depend on `lib`, which pulls a different `dep` version in
120168
// each subproject.

‎src/commands/manifest/scripts/facts.mts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ export type SocketFactsSbomProject = AnyPURL & {
6060

6161
// Resolved on-disk paths for a --with-files run, keyed by component or project
6262
// id (a project and its own component share one). `targets` = classpath
63-
// entries (jars / module output dirs); `sources` = module source roots.
63+
// entries (jars / module output dirs), absent when the component's artifact
64+
// did not resolve; `sources` = module source roots.
6465
export type ResolvedArtifactPaths = {
65-
pathsById: Map<string, { sources: string[]; targets: string[] }>
66+
pathsById: Map<string, { sources: string[]; targets?: string[] | undefined }>
6667
// Component ids on each project's resolved classpath (union over its
6768
// configurations), keyed by project id.
6869
classpathByProject: Map<string, string[]>

‎src/commands/manifest/scripts/records.mts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { ResolutionFailure, Unscannable } from './resolution-report.mts'
1717
// node rootId coordId group name version ext classifier direct(0|1) project
1818
// edge rootId parentCoordId childCoordId
1919
// file rootId coordId path (--with-files only)
20+
// unfetched rootId coordId (--with-files only; requested artifact not fetched)
2021
// scanned config
2122
// failure coord detail config projectKey (config empty: no configuration)
2223
// unscannable config detail projectKey (config empty: whole project)
@@ -45,6 +46,7 @@ export type RawNode = {
4546
project: string
4647
// --with-files only.
4748
targets: string[]
49+
unfetched: boolean
4850
}
4951

5052
export type RawRoot = {
@@ -215,6 +217,7 @@ export function parseRecords(text: string): ParsedRecords {
215217
direct: bool(f[8]),
216218
project: f[9] ?? '',
217219
targets: [],
220+
unfetched: false,
218221
})
219222
break
220223
}
@@ -226,6 +229,13 @@ export function parseRecords(text: string): ParsedRecords {
226229
}
227230
break
228231
}
232+
case 'unfetched': {
233+
const node = root(f[1] ?? '').nodes.get(f[2] ?? '')
234+
if (node) {
235+
node.unfetched = true
236+
}
237+
break
238+
}
229239
case 'file': {
230240
const node = root(f[1] ?? '').nodes.get(f[2] ?? '')
231241
if (node && f[3]) {

‎src/commands/manifest/scripts/sidecar.mts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,17 @@ export type SidecarAccumulator = Map<
5353
{ projects: SidecarProjectEntry[]; components: SidecarComponentEntry[] }
5454
>
5555

56-
// `[]` means resolved and found nothing (e.g. a pom/BOM with no artifact).
56+
// `[]` means resolved and found nothing (e.g. a pom/BOM with no artifact); no
57+
// `targets` means the artifact did not resolve.
5758
function attachPaths<T extends { id: string }>(
5859
entry: T,
5960
artifactPaths: ResolvedArtifactPaths,
60-
): T & { targets: string[]; sources: string[] } {
61+
): T & { targets?: string[]; sources: string[] } {
6162
const paths = artifactPaths.pathsById.get(entry.id)
63+
const targets = paths ? paths.targets : []
6264
return {
6365
...entry,
64-
targets: [...(paths?.targets ?? [])],
66+
...(targets ? { targets: [...targets] } : {}),
6567
sources: [...(paths?.sources ?? [])],
6668
}
6769
}

0 commit comments

Comments
 (0)