- Go 74.9%
- JavaScript 21.4%
- Python 1.3%
- TypeScript 1.1%
- CSS 0.9%
- Other 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
CI / test (ubuntu-latest, Go oldest) (push) Successful in 2m26s
CI / test (ubuntu-latest, Go stable) (push) Successful in 2m27s
CI / lint (push) Successful in 1m0s
CI / extension (push) Successful in 52s
CI / vulncheck (push) Successful in 9s
CI / dist (push) Successful in 1m36s
|
||
| .forgejo/workflows | ||
| .github/workflows | ||
| .vscode | ||
| cmd/depphunter | ||
| docs | ||
| extension | ||
| internal | ||
| scripts | ||
| vendor | ||
| web | ||
| .gitattributes | ||
| .gitignore | ||
| .markdownlint.yaml | ||
| .markdownlintignore | ||
| .vscodeignore | ||
| CONTRIBUTING.md | ||
| endcard.html | ||
| go.mod | ||
| go.sum | ||
| icon.png | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| renovate.json | ||
| tsconfig.json | ||
depphunter
Note: This codebase was developed with the assistance of AI tools (Claude, by Anthropic). All code is reviewed and tested before being merged.
| Initial view | Dependency trace |
|---|---|
![]() |
![]() |
| Walk mode | Night mode |
![]() |
![]() |
| Flight mode | Vulnerability hunting |
![]() |
![]() |
depphunter renders a code base as an interactive isometric archipelago in a browser.
-
The mainland is the repository. Directories are terraces and files are buildings, whose height is the file's line count and whose color is its language. The map is drawn as a city: facades with windows, streets with sidewalks and crossings, ramps between levels, parks, and a surrounding sea. Selecting a node dims the rest, and dimmed buildings lose their detail so that the selection remains legible.
-
The islands are external ecosystems - Go modules, a standard library, npm
- with one building per dependency.
-
Selecting a node shows what it depends on and what depends on it, as arcs over the map with an arrow at the far end of each. Double-clicking expands or collapses a directory or a file; a file expands into its symbols.
-
Walk mode (
V) presents the same map in first person on a small planet.WASDmoves, the mouse looks andSpacejumps. The walker holds a tool, drawn in the hands at the end of an arm; using it on a building selects the module, draws its dependency trails and marks it with a beacon for the rest of the session.Ten tools occupy slots
1to9and0;Ecycles through the primary tools,Qthrough the secondary ones, and holdingRopens a wheel of all of them. The seven primary tools are what the hunt is done with: a fishing rod (the default), a butterfly net, a camera, a bubble wand, a fire extinguisher, a tracking dart and a nail gun. Each has its own animation, its own aim helper and its own valid targets - the dart acts on buildings, the net, the bubbles and the extinguisher on bugs (the extinguisher also puts out fires), the rod, the nail gun and the camera on either. What a tool throws travels according to its own flight model, and the two launchers are opposites rather than variants: the tracking dart is the longest and most deliberate shot, lobbed high and steering in the air towards the wall ahead of it, one shot per click through the scope; the nail gun reaches only across a street, fires flat and fast for as long as the button is held down, and scatters. The fire extinguisher is held down in the same way. A bubble decelerates and rises; foam spreads and drops. The net throws nothing and must be brought within reach; the camera shows its lens view live on its back, and every use of it keeps the frame, except a use on a module that has already been tagged, with no bug in front of it, which reads that module the way every other tool does there. A photograph is of the city and of nothing else: neither the camera nor the hand holding it is in it — what the walker holds is drawn over the world in a pass of its own, and that pass is left out — and neither is anything the interface has put on the map, so nothing is lit by a selection, nothing is dimmed by one and no dependency arcs cross the rooftops. A screenshot (P) is the screen rather than what the camera was pointed at, so it keeps all of them.What it keeps goes into the photographs (
G), a contact sheet of the session's pictures captioned with whatever was in the frame. Each can be saved to a PNG file, let go of, or — from the street — put back up on the camera: the camera comes out if it is not already in hand and is brought all the way up to the walker's face, square on, until the picture on its back screen — which stands in for the live view while it is there — covers nearly the whole of it. After a few seconds it goes back down and gives the hand back to whatever was in it. A click puts it away sooner. The pictures are a session and nothing more, so anything worth keeping is saved to a file.The four secondary tools touch nothing on the map and carry the walker instead. The grapple gun, fired with
ForC, hooks the building the walker is looking at and draws them up the facade and onto the roof, from where a shot over the edge is the way down. Its claw closes on a parapet, not on a flat wall: a hook that strikes more than two stories below a roof's edge glances off, tumbles down and is reeled back in, and pulls nobody anywhere; the jet backpack flies; the swim ring lets the bay be swum, passing under the bridges rather than over them and stepping back up onto a shore that stands half a unit above the water. The latter two run on a tank, which empties only while the tool is doing its work and fills again whenever it is not, so neither is a way of getting everywhere; a gauge beside the health bar says what is left, running dry in the air is a fall, and a tool that has run out stays stopped until its tank has filled back to a quarter, when it works again by itself. The parachute is the way down that is flown:Foff a roof or out of the jet throws the pilot chute, and about a second later the canopy is open over the walker — a ram-air wing on its lines, lit like anything held, swinging the view as the walker swings under it. It comes down at under three meters a second and about four units forward for every one down, and it has its own speed and heading:AandDsteer, banking and sinking faster in the turn,WandStrim it faster and steeper or slower and sinking a little more, andSpaceflares it. A landing under it costs how fast the walker arrives rather than how far they came, so a flare begun about two meters up lands for nothing, one begun too high stalls, and a wall flown into costs what arriving at that speed would. Thrown too low, it is still opening at the ground and the landing is judged mostly as the fall it nearly was. Its tank is the pack: spent whole when it is thrown and repacked on the ground, with the canopy draped where it came down fading as it is, and putting it away while it is open cuts it loose to drift off and leaves the walker falling. It is the eleventh tool and the digits number ten, so it has a key of its own,T, and the first slot in the row. One tool of each kind is carried at a time, one to a hand — the primary in the right, the secondary in the left — so the map can be flown over and its bugs netted without putting either down. A click uses the right hand, andF,Cor the middle button the left. The HUD lays the slots out the way the walker is: what the left hand carries on the left and what the right hand hunts with on the right, each group behind a small hand of its own, because a row read at a glance in the middle of something else should not have to be parsed. The rod climbs too, from the hunting hand, which leaves the other free for the jet or the swim ring: a cast that comes down on a roof winds the walker up onto it, more slowly than the grapple and on a shorter line. A fish hook is not made for brick, though, so a cast at a wall, however high, always skips off the way a glancing grapple does. The right button holds the scope, and the mouse wheel zooms the view.Running and jumping are paid for out of a second gauge, the walker's wind. A sprint drains it in a few seconds and each jump takes a little more; it fills again while walking or standing, and a walker who has run it out has to get a quarter of it back before they can run or jump again, so the way across a district is a series of dashes rather than one long one. Flying costs nothing: that is the jet's tank, not the walker's chest.
The walker has a health bar. A fall is what it would be in life, measured against the walker, who is half a unit tall: nothing up to about three meters, which a jump off a terrace wall stays under, then a share of the bar that grows with the height, and the end of the walk from about seventeen meters — five or six floors — however full the backpack. Being reeled down a line counts as falling. A bug's bite costs more the worse the finding is, and deep water with nothing to float on takes all of it in a couple of seconds, the walker going under as it does: the view sinks and bobs, the water closes over it from the bottom of the screen with bubbles rising through it, and all of it drains away again if a shore is reached in time — so taking the swim ring off out over the bay is the end of that walk, and so is walking into it without it unless a shore is reached first. The bay is not a wall around the map: it is ground half a unit below the shore, so it can be stepped down into the way a curb can, waded about in, and — because the shore is further up than a step — climbed back out of, whether by somebody wading or somebody swimming in the ring. A bridge deck is where that stops. Walking off an edge into a drop is not a thing anybody means to do, so the railings have to be gone over rather than through: off a deck, or off anything else standing well above the surface, the water has to be jumped into. Every bug in the backpack raises the bar and mends by as much. At nothing the screen goes red, walk mode ends and the map returns — nothing caught is lost — and walking in again starts at full health. Away from bites and fire, the bar slowly fills again by itself.
Each finding a scanner reported is represented by one bug, up to 140 of the most severe, except a vulnerability proven reachable, which burns as a fire instead (see Findings). A bug is shaped by its severity as well as colored by it: a critical finding is a caterpillar that crawls and never flies, a high or medium one a beetle, and a low or informational one a mite. Bugs are placed at several heights on a building's facade and around its roof as well as in the streets, each oriented to the surface it holds on to; some hold on to nothing and instead fly a circuit around the building, rising, falling and banking at the corners. Catching one opens what was reported about it. A module the walker has tagged carries a beacon in the color of the worst finding in it, and its ring on the tracker matches, so the hunt's own trophies say which of them were worth having. A tracker in the corner of the screen sweeps the surrounding map and tightens as the walker approaches a bug, so that the last part of the approach can be made on the sweep rather than by guesswork.
A first visit is given a short introduction explaining what the shapes stand for, that the map can be walked into, and what the bugs are, and a first walk is given one of its own covering what moves, what is in each hand, what using it does, the bugs, and fire and staying alive; the help (
?) documents every key and every tool, and can show either introduction again.Terraces are laid out as city blocks: the space between buildings forms a connected street network with sidewalks, lane markings and crossings; ramps and stairs connect levels; unoccupied lots become parks; and a bridge crosses the water to every island.
The tool runs entirely locally. It is a single binary, requires no Node.js, and
makes no network request unless --online is given (see
Package indexes).
Two ways to run it
depphunter is a command-line tool that serves the map over HTTP to a browser. The VS Code extension is a wrapper around that same tool: it starts a server for the open folder and displays the map in a tab beside the code. The page served is identical in both cases.
| Command-line tool | VS Code extension | |
|---|---|---|
| Installation | a release archive, or go install |
the Marketplace or Open VSX (the binary is bundled) |
| Invocation | depphunter [path] |
depphunter: Open the Map, or a folder's context menu |
| The map opens in | the default browser | a tab of the editor's own, beside the code |
| Configuration | flags, DEPPHUNTER_* variables, .depphunter.yaml |
depphunter.* settings, and the same .depphunter.yaml |
| Additionally | exports to JSON, GraphML, DOT and HTML | maintains one server per folder for the editor's lifetime |
Contents
- Command-line tool: Install · Usage · Configuration · Watch mode and cache · Exports · HTTP API · Opening files in an editor
- VS Code extension: Install · Use · The panel beside the code · Settings · Remote workspaces · Why the map is in a tab of its own · How the framing works
- The map: Keyboard & mouse · Styles · Findings · Git history · Versions and pinning · Dependencies of dependencies · Package indexes · Private dependencies · The resolution report · CI pipelines · Infrastructure as code · Jsonnet and CUE · Dhall, Puppet and Rego · Shaders and GPU code · Nix · Gleam · Elm · PureScript · Crystal · F# and Paket · D and dub · Fortran and fpm · Haxe and haxelib · Ada, GPR and Alire · Racket and raco · Common Lisp and Quicklisp · Solidity, Foundry and Hardhat · Nim and nimble · Interface definitions · Shell scripts · Documentation · Symbol references · Languages
- Security · Contributing · License
Command-line tool
Install
Download an archive for the target platform from the
releases (checksums in
checksums.txt), or build it with Go 1.27.1 or newer:
| OS | Architectures | Archive |
|---|---|---|
| Linux | amd64, arm64, armv7, 386, riscv64 | .tar.gz |
| macOS | amd64 (Intel), arm64 (Apple silicon) | .tar.gz |
| Windows | amd64, arm64, 386 | .zip |
| FreeBSD | amd64, arm64 | .tar.gz |
go install github.com/sarumaj/depphunter-cli/cmd/depphunter@latest
Usage
depphunter # analyze the current directory and open a browser
depphunter ~/src/app # analyze another directory
depphunter --no-open --addr 127.0.0.1:8080
depphunter --watch # re-analyze on change and update the open map
depphunter --findings trivy.json # place a scanner's report on the map
depphunter --export dot -o deps.dot # write the graph and exit
depphunter --export html -o map.html # write a self-contained map
| Flag | Default | |
|---|---|---|
--addr |
127.0.0.1:0 |
listen address; port 0 selects a free port |
--no-open |
print the URL rather than opening a browser | |
--exclude |
glob of paths to omit; repeatable | |
--max-file-size |
2097152 |
files above this size are listed but not read |
--config |
<path>/.depphunter.yaml |
configuration file to read |
--theme |
auto |
auto, light or dark |
--color-by |
language |
language, size, commits, churn, age or authors |
--height-scale |
sqrt |
linear, sqrt or log |
--style |
city |
presentation of the map: city, circuit or galaxy |
--show-std |
false |
include standard-library islands |
--expand-depth |
0 |
directory depth expanded initially; 0 picks one, -1 expands all |
--ui-default |
seed a view setting for a repository that has saved none: key=value, repeatable (see below) |
|
--watch |
false |
re-analyze on file change and update the open map |
--no-cache |
neither read nor write the analysis cache | |
--no-history |
do not read git history | |
--history-commits |
10000 |
read at most this many commits |
--resolve-depth |
0 |
levels of transitive dependencies to resolve from lock files; -1 for all |
--private |
(GOPRIVATE etc.) | glob naming packages the organization owns; never sent to a public index or to OSV |
--trust-index |
(none) | index URL to treat as configured on this machine, so a repository naming it is unmarked |
--python |
(VIRTUAL_ENV, .venv) |
Python interpreter whose installed packages resolve imports no package index has |
--online |
false |
query package indexes for what the project's own files do not record |
--explain |
false |
write the resolution report once the analysis is complete |
--lsp |
resolve symbol references using the installed language servers | |
--lsp-timeout |
5m |
time budget for the language servers |
--findings |
scanner report to place on the map; repeatable, globs permitted | |
--no-vulns |
place no scanner reports and do not query the OSV database | |
--no-links |
do not follow the links in the repository's Markdown | |
-v, --version |
print the version and exit | |
-h, --help |
list the flags with their defaults | |
--editor |
auto-detected | editor command template, e.g. "code -g {file}:{line}" |
--embed |
origin permitted to frame the map, e.g. vscode-webview:; repeatable |
|
--export |
write json, graphml, dot or html and exit |
|
-o, --output |
stdout | output file for --export |
Long flags take two hyphens (--addr, not -addr), and a flag's value may be
separated from it by either a space or =.
Diagnostic output - what was analyzed, the address being served, and the
resolution report - is written to stdout, so that
it can be piped or redirected like any other output. The sole exception is
--export without -o, where stdout carries the exported document; the log is
then written to stderr instead, so that a redirected export contains nothing but
the export. Errors are written to stderr in all cases.
Configuration
Settings are resolved in the following order of increasing precedence: built-in
defaults; the user configuration
($XDG_CONFIG_HOME/depphunter/config.yaml, or the platform equivalent); the
project configuration .depphunter.yaml; the DEPPHUNTER_* environment
variables (ADDR, OPEN, EXCLUDE, MAX_FILE_SIZE, THEME, COLOR_BY,
HEIGHT_SCALE, STYLE, SHOW_STD, EXPAND_DEPTH, TOOL, WATCH, CACHE,
EDITOR, PYTHON, HISTORY, HISTORY_COMMITS, RESOLVE_DEPTH, ONLINE,
EXPLAIN, VULNS, LINKS, LSP, LSP_TIMEOUT, and the comma-separated
FINDINGS, PRIVATE and TRUST_INDEXES); and the command-line flags. For
exclude, findings, private and trust_indexes, the lists from every
source — the user file, the project file, the environment and the flags — are
combined rather than replacing one another. --private also takes in the Go toolchain's
GOPRIVATE, GONOPROXY and GONOSUMDB patterns, from the environment or
the file go env -w writes.
The project configuration arrives with the repository, so only the settings
on an allow-list are read from it: every setting except editor (a command
depphunter executes), online, python and trust_indexes, and its
findings paths must stay inside the repository. A setting added later is
user-only until it is put on the list. A file passed with --config is trusted
like the user configuration, and must exist.
--ui-default accepts the keys theme, color_by, height_scale, style,
show_std, expand_depth, tool and path_filter.
# .depphunter.yaml
exclude: [testdata, "*.pb.go"]
history_commits: 5000
ui:
color_by: language
height_scale: sqrt
show_std: false
expand_depth: 0
tool: rod # walk mode: rod, net, camera, bubbles,
# extinguisher, dart, nailer, parachute,
# grapple, jetpack, ring
hide_languages: [Markdown] # filters, as the Filters panel sets them
hide_islands: [npm]
path_filter: "!**/testdata/**"
The browser's Save settings button writes the current color, height, style,
theme, depth, walk-mode tool, standard-library islands and filters into the
ui: section of .depphunter.yaml (or the --config file), keeping the file's
other keys and comments.
That section is where a repository's view lives, and it is what the map reads on
the way in. --theme and the rest override it, as a flag does; --ui-default
sits underneath it instead, replacing only what depphunter would otherwise have
started at. So --ui-default theme=dark decides how a repository that has never
been saved opens, and stops deciding the moment somebody presses Save. That is
what the editor extension sends its appearance settings as, so the two never
disagree about which one won.
Watch mode and cache
Parse results are cached by file content under the user cache directory
(~/.cache/depphunter on Linux), so that a subsequent run parses only the files
that have changed; for the CPython standard library this reduces analysis from
1.2 s to 20 ms. Under --watch, depphunter monitors the directories it
analyzed, re-analyzes once changes have settled for 300 ms, and pushes the
resulting map to the browser, which preserves the current expansion, selection
and filters and briefly highlights the files that changed.
Exports
--export (or the Export menu in the browser) writes:
| Format | Contents |
|---|---|
json |
the complete graph document used by the interface: nodes, symbols and edges |
graphml |
the complete graph with all attributes, for Gephi, yEd or NetworkX |
dot |
the dependency graph for Graphviz: files, package directories and external packages, clustered by directory. Standard-library packages and files without imports are omitted |
html |
the interactive map as a single file, requiring neither depphunter nor a network: the interface, the graph, the source text (files up to 256 KB, 24 MB in total) and the current view — colors, height, theme, depth and filters. --export html uses the configured view |
Export → PNG image, or P, writes the map as currently displayed, including
labels, at the screen's resolution. It is available in an exported HTML page as
well.
The HTML export contains the repository's source code and, where git history was read, the names of commit authors. It should be distributed under the same conditions as the repository itself.
Dependency graphs are shallow, which leads Graphviz to lay them out long and
narrow. For large graphs, unflatten -l 3 -c 5 deps.dot | dot -Tsvg -o deps.svg
produces a more even result.
HTTP API
The server that hosts the map exposes a small HTTP API, which is also what the
VS Code extension's side panel reads. Every request requires the session token,
supplied in the cookie that the initial address is exchanged for or, when the
server runs with --embed, in an X-Depphunter-Token header (or a ?token=
parameter, which the event stream uses). Every request that modifies state additionally
requires the header X-Depphunter-Request: 1, which a cross-site page cannot
set.
| Endpoint | What it is |
|---|---|
GET /api/graph |
the graph document: nodes, symbols and edges |
GET /api/config |
the view the map opens with, and the server's capabilities |
GET /api/file?path= |
the source of a file present on the map, and of no other file; 415 for a binary file, naming its type; &as=raw its bytes |
GET /api/history, /api/references, /api/findings |
computed in the background: 202 while in progress, 204 if there is no result |
GET /api/export?format=&ui= |
json, graphml, dot or html; ui is the view (JSON) an html export opens with |
GET /api/resolution?format= |
how the dependencies were resolved: json (default), md or text |
GET /api/events |
Server-Sent Events: graph, history, references, findings, selection, backpack |
| ----------------------------- | ------------------------------------------------------------------------------------------- |
POST /api/selection |
{"id": "f:src/main.go", "origin": "…"}; the map follows |
GET /api/backpack?format= |
the collected findings as json, csv or md |
PUT /api/backpack |
{"items": [...], "origin": "…"}; replaces the contents |
POST /api/open |
{"path": "…", "line": 12}; opens the file in the configured editor |
GET /api/locate?id= |
{"folder": "…"}, the directory a package is installed in on this machine; 404 when none is found |
POST /api/browse |
{"id": "p:npm:lodash", "to": "page"}; opens a package's page, repository or folder |
POST /api/settings |
writes the ui: section of the configuration file |
origin identifies the client that made the change and is echoed in the
resulting announcement, so that a client can distinguish its own change from
another's. The selection and the backpack belong to the session; the backpack's
durable store is the browser's, held per repository, and the page uploads it as
it loads.
/api/graph carries an ETag, so that a client already holding the document
may send If-None-Match and receive 304. The tag is a fingerprint of the nodes
and edges rather than of the time at which they were read, so a re-analysis that
finds the same project yields the same entity — which is the question a client
reconnecting to a restarted server is in fact asking. The document reaches twenty
megabytes for a repository of a hundred thousand nodes, and in the majority of
requests nothing about it has changed.
Every announcement carries an identifier, and the stream opens with a greeting stating its current position:
event: hello
data: {"version":3,"etag":"\"9e4f…\"","seq":11,"resumed":true}
resumed is the stream's answer to the Last-Event-ID returned by the client:
nothing has been announced since the event it last received, so nothing has been
missed. Without it, a reconnection is indistinguishable from a first connection
and any announcement made while the connection was down is lost — and since a
browser retries an EventSource on its own, this occurs without the user's
knowledge.
Opening files in an editor
The Open in editor button in the side panel's corner — labeled with the
editor, "VS Code ↗", and pinned beside maximize and close however far the panel
is scrolled — or O, opens the selected file at the line of the selected
symbol. The command is taken from --editor,
DEPPHUNTER_EDITOR, the user configuration or a --config file; failing those,
depphunter detects a graphical editor from $VISUAL, $EDITOR or PATH (VS
Code, Cursor, Zed, Sublime Text, the JetBrains IDEs and others). If none is
found, the button delegates to VS Code's vscode:// URL handler. Under the VS
Code extension, the command is set to the editor the
extension is running in.
VS Code extension
The extension displays the map in a tab beside the code. It starts depphunter
for the open folder, waits for the address it reports, and shows that address in
an editor tab of its own (why). The map
behaves exactly as it does in a browser tab, live updates included. It requires
VS Code 1.74 or later. Its source is under extension/.
Install the extension
Install depphunter (sarumaj.depphunter) from the Visual Studio Marketplace
or Open VSX; the registry
serves the build for your platform. Every
release also provides one
.vsix per platform, each containing the binary for that platform. Select the
matching build — linux-x64, darwin-arm64, win32-x64 and so on — and install
it with Extensions: Install from VSIX… in the command palette, or from a
terminal:
code --install-extension depphunter_1.2.3_vscode_darwin-arm64.vsix
No further installation is required. The extension and the server it starts
are produced by the same release and, for a release tagged vX.Y.Z, carry the
same version, so the two cannot diverge. A _universal build is also provided
for platforms not listed above; it contains no binary and falls back to
depphunter on PATH.
To run a different build — one under development, or a newer release on a machine
whose extension has not been updated — set depphunter.path to it. An explicit
setting always takes precedence over the bundled binary.
The binary the extension runs is also put first on PATH in the editor's
integrated terminals, so depphunter typed there — depphunter --export html,
say — is the same version as the map. Only terminals the editor opens are
changed, not the shell profile; ones already open are offered a relaunch.
Nothing is added where the binary is found on PATH anyway, and
depphunter.addToPath turns it off.
Use
Select the depphunter icon in the activity bar. The Maps view lists the
window's folders, and any subfolder mapped from the explorer while its server
runs; selecting one maps it. The entry's buttons open the map and, while a
server runs, restart and stop it; the view's title bar opens the map in the
browser, shows the log and opens the settings. The same action is available in
the command palette as depphunter: Open the Map and in the explorer's context
menu for any folder.
| Command | What it does |
|---|---|
depphunter: Open the Map |
Maps the folder, or displays a map already produced |
depphunter: Open the Map in the Browser |
Opens the same map outside the editor, for this instance |
depphunter: Restart the Server |
Restarts it, which is how changed settings take effect |
depphunter: Stop the Server |
Stops it; the next invocation analyzes afresh |
depphunter: Show the Server Log |
The server's output, verbatim |
depphunter: Show the Resolution Report |
Which index each package resolved from, and how the walk proceeded |
depphunter: Export the Graph |
JSON, GraphML, DOT or a self-contained HTML map |
depphunter: Export the Backpack |
The collected findings as Markdown, CSV or JSON |
depphunter: Add to the Backpack |
Puts a finding in the backpack without walking to its bug |
depphunter: Refresh the Side Panel |
Reads the graph and the backpack from the server again |
depphunter: Open Settings |
The extension's settings, documented below |
One server is maintained per folder and kept until the window closes or the
server is stopped explicitly: analyzing a large repository takes time, and under
--watch it need happen only once. A status bar item is shown while a server is
running; selecting it opens the map. If a server exits unexpectedly, a warning
offers to show its log or restart it.
The panel beside the code
The same panel holds three further views below Maps, all showing the map opened most recently:
-
Dependencies presents the graph as a tree. A directory expands into its contents, a file into its imports, an island into its packages, and a package into its own dependencies, as far as
--resolve-depthreached. Expanding a row requires no further request: every edge is already present in the graph the panel fetched once. A branch leading back to a node already expanded above it is shown once more, marked↻, and left collapsed, since dependency graphs contain cycles. A package that is not pinned, or that resolves from an index this machine does not configure, is marked in the list itself rather than only in its tooltip. -
Backpack holds the findings collected while walking the map, ordered by severity, with those absent from the most recent scan marked as resolved at the end. Removing an entry here removes it from the map's backpack as well.
-
Findings lists everything the scanners reported, most severe first and then by package or file, whether caught or not. The
+beside an entry puts it in the backpack exactly as catching its bug on the map would — the same entry, recorded against the same building — and the map's bug stops walking; an entry already in the backpack is marked so and offers×to take it out again.depphunter: Add to the Backpackin the command palette asks which of those not yet caught to add. A catch on the map is marked here as it happens.
The Dependencies view's title bar holds the resolution report, the graph export and a refresh, and a file's row has a button that opens the file; the Backpack's title bar exports it.
A package's row has buttons that open its page on the index it is published on
and the repository of its source in the browser, and one that reveals the folder
it is installed in: in the Explorer when that is in the workspace — a
node_modules or a vendor directory — and otherwise, for a shared cache such
as Go's module cache or ~/.m2, in a window of its own or the system's file
manager. The map's details panel offers the same three links, which in the
editor's tab the server hands to the extension to open.
The two views and the map form a single interface: selecting a row selects the corresponding building on the map, and selecting a building on the map expands the tree to its row. The server is what makes this so — it holds the selection and the collected findings while the map is open and announces changes to either (API) — which is also why a second browser tab stays in step.
Settings
The settings, in the groups the Settings editor presents them in. Each names the
flag it passes, and passes it only when set to something other than depphunter's
own behavior — an enum left at default, a number left empty, a switch left at
depphunter's default — so that a folder's .depphunter.yaml continues to govern
everything not set here.
| Setting | Default | Flag | What it does |
|---|---|---|---|
| General | |||
depphunter.path |
(empty) | The binary to run. Empty selects the bundled binary, falling back to depphunter on PATH. |
|
depphunter.addToPath |
true |
Put that binary first on PATH in the editor's terminals. |
|
depphunter.openIn |
webview |
webview (a dedicated tab), simpleBrowser (the built-in browser) or externalBrowser (the system default). |
|
depphunter.watch |
true |
--watch |
Re-analyze on file change and update the map. |
depphunter.config |
"" |
--config |
A configuration file to read instead of the folder's .depphunter.yaml, relative to the folder. |
depphunter.editorCommand |
"" |
--editor |
The command Open in editor invokes. Empty selects the current editor. |
depphunter.args |
[] |
Further arguments, one per entry, appended last. Usage lists them. | |
| Analysis | |||
depphunter.exclude |
[] |
--exclude |
Globs of paths to omit. |
depphunter.maxFileSize |
(empty) | --max-file-size |
Files larger than this many bytes are not read. |
depphunter.resolveDepth |
(empty) | --resolve-depth |
Levels of transitive dependencies to resolve from lock files; -1 for all. |
depphunter.online |
false |
--online |
Query package indexes and the OSV database over the network. |
depphunter.cache |
true |
--no-cache |
Read and write the analysis cache. |
depphunter.history |
true |
--no-history |
Read git history for the history overlays. |
depphunter.historyCommits |
(empty) | --history-commits |
Read at most this many commits. |
depphunter.private |
[] |
--private |
Globs naming the packages the organization owns. Never sent to a public index or to OSV. |
depphunter.trustIndexes |
[] |
--trust-index |
Index URLs to treat as configured on this machine, so a repository that names one is not marked. |
depphunter.explain |
false |
--explain |
Write the resolution report to the output channel whenever the map is built. |
| Appearance (seeds: a repository that has saved a view of its own keeps it) | |||
depphunter.style |
default |
--ui-default |
city, circuit or galaxy. |
depphunter.theme |
default |
--ui-default |
auto, light or dark. |
depphunter.colorBy |
default |
--ui-default |
language, size, commits, churn, age or authors. |
depphunter.heightScale |
default |
--ui-default |
linear, sqrt or log. |
depphunter.expandDepth |
(empty) | --ui-default |
Directory levels expanded initially; 0 picks for you, -1 expands all. |
depphunter.showStd |
false |
--ui-default |
Include standard-library islands. |
| Findings | |||
depphunter.findings |
[] |
--findings |
Scanner reports to place on the map, relative to the folder. Globs permitted. |
depphunter.vulns |
true |
--no-vulns |
Place reports on the map and, under online, query the OSV database. |
depphunter.links |
true |
--no-links |
Follow the folder's Markdown links and report those that lead nowhere. |
| References | |||
depphunter.lsp |
false |
--lsp |
Resolve symbol references using the installed language servers. |
depphunter.lspTimeout |
"" |
--lsp-timeout |
Time budget for the language servers, e.g. 90s. |
What is left out is left out on purpose: --addr, --no-open and --embed
are how the extension hosts the map and are not for changing, and --export
(with --output) writes a file and exits rather than serving. --python has
no setting of its own; pass it through depphunter.args.
They are ordinary settings, so they can be set per workspace in
.vscode/settings.json:
{
"depphunter.style": "circuit",
"depphunter.findings": ["reports/trivy.json", "reports/*.sarif"],
"depphunter.exclude": ["vendor"],
"depphunter.lsp": true,
"depphunter.resolveDepth": 1
}
A server reads its settings at start-up, so a change takes effect on the next
depphunter: Restart the Server; the extension offers to perform the restart.
Anything the settings do not cover belongs in depphunter.args or in the
project's .depphunter.yaml, which the server reads as usual.
Open in editor opens the file at the line currently in view, in this editor:
the extension locates this editor's own command-line launcher (on Windows, its
executable) and passes it to the server, rather than leaving the server to find
whatever is on the PATH the extension host inherited.
depphunter.editorCommand overrides this where the file should be opened
elsewhere.
Remote workspaces
Over SSH, WSL and dev containers the port is forwarded to localhost on the
local machine and the extension works unchanged. In Codespaces the forwarded
address is a public hostname, which the server rejects as a DNS-rebinding
attempt, since it answers only to localhost, 127.0.0.1 and ::1; there, run
depphunter from a terminal instead. On vscode.dev without a remote the
extension does not load at all, since it needs a Node extension host.
Why the map is in a tab of its own
The editor's built-in browser is the natural place for a local page, but it places that page in a sandbox of its own, and the pointer lock is among the capabilities that sandbox withholds. Walk mode therefore cannot capture the mouse there, and since a sandbox can only be narrowed further down the frame chain, the page has no means of recovering it.
The map is opened in a dedicated editor tab instead: one webview containing one
iframe, and that iframe carries no sandbox attribute. Adding none removes
nothing, so the pointer lock the editor granted the webview is preserved. What
is given up is an address bar and a back button, on a single page that requires
neither. depphunter.openIn may still select simpleBrowser, where walk mode
reports the limitation and turns the view by dragging instead.
How the framing works
In either case the page is inside a webview and is therefore framed by origins
belonging to the editor. depphunter refuses to be framed by default, so the
extension starts it with --embed, naming every frame above the page — all
three, because frame-ancestors is evaluated against the entire chain and not
only the frame immediately containing the page:
--embed vscode-webview: --embed vscode-file: --embed https://*.vscode-cdn.net
vscode-webview:is the webview, which is given an origin of its own for every session (vscode-webview://<uuid>), so there is no exact name to give.vscode-file:is the editor's window, served fromvscode-file://vscode-appin the desktop editor.https://*.vscode-cdn.netis the webview in the browser build.
If any one of them is omitted, the browser refuses the page before it loads, leaving an empty tab with the reason recorded only in the webview's own developer tools. No other origin may frame the page; any that attempts to is refused in the same way. The remaining effects of this mode are described under Inside an editor.
In this mode the session token remains in the address rather than being exchanged for a cookie, because a cookie set by the map would be a third-party cookie within the frame and would never be returned. This is why the extension reads the address from the server's own output rather than constructing it from the port: that address is the only place the token appears. The extension always starts the server in this mode, so even a map it opens in an external browser keeps the token in the address.
The map
The remainder of this document describes the map itself, which behaves
identically whether it was started by the command-line tool or by the extension.
Where a section names a flag, the extension passes it through its own setting if
one exists (see Settings; depphunter.style and the other
appearance settings only seed the default) and through depphunter.args
otherwise.
Keyboard & mouse
Panning is bounded at the point where the center of the view lies a quarter of the map's extent (plus a small margin) beyond its edge, and zooming out at the point where the map occupies roughly a third of the view. In walk mode the walker may travel 3 units out over the water and 12 units above the tallest building.
| Drag / right-drag / wheel | pan, orbit, zoom |
| Click / double-click | select, expand or collapse; double-click on open ground enters walk mode |
| Middle-drag | zoom |
Enter, Backspace |
expand or collapse the selection, select parent |
→ ← in the panel |
expand or collapse a dependency row |
Enter while reading |
close the details and resume |
E Q in walk mode |
the next tool for the right hand, the left hand |
Q E |
rotate by 90° |
Home |
fit the map to the view |
R |
reset the view |
+ - |
expand or collapse one level throughout |
/ |
search files, symbols and packages |
O |
open the selected file in the editor |
P |
write the map to a PNG image |
| Legend click | show or hide a language |
| Pin click | read the findings recorded on a building |
| Find in the details | search a file's source: every match highlighted, Enter/Shift+Enter or ↓/↑ step through them, Esc clears |
+ beside a finding |
add it to the backpack |
L |
list every finding the map shows, to read or add to the backpack from (arrows move, Enter opens, + adds) |
B |
open the backpack |
G |
open the photographs the camera has taken; from the street each can be put up on the camera and looked at there |
X |
open the export menu |
K |
save settings to the config file |
| The figure | the walker's last position in walk mode |
Esc |
close the photographs, the backpack or the findings list, or clear the selection |
V |
enter walk mode ^ |
? |
show all the controls |
In walk mode:
| Mouse | look. The pointer is captured at the reticle; Esc releases it and a click on the map captures it again. Where it cannot be captured at all — a frame that withholds the pointer lock — walk mode reports this once, and a click then uses the tool rather than requesting the lock again |
R |
the tool wheel: every tool at once, the primary ones down its right side and the secondary ones down its left, with an empty left hand at the bottom. Hold R, point with the mouse and release; or tap R to leave it up and take what is under the cursor with R, Enter or a click. Esc, the right button, or releasing with the cursor still in the middle changes nothing. While it is up the walker is held where they stand and the city behind it is blurred: nothing moves them, spends a tank or bites them, so changing hands costs no time. Pointing at a tool already in hand keeps it |
T 1 2 3 |
select a secondary tool, or press the same key again to put it down. Only one is carried at a time. The parachute has T, because the ten digits are taken |
4 … 0 |
select a primary tool |
E |
the next primary tool, cycling. The right hand is never empty |
Q |
the next secondary tool, and after the last of them an empty left hand — which is how the walker comes down out of the air or steps off the water deliberately. Five presses return to the starting state |
W A S D/arrows |
move and turn; Shift runs, which spends the walker's wind |
Space |
jump, which costs a little wind; while flying, ascend; under a parachute, flare |
| Jet backpack in hand | flight. While flying, W and S move along the view direction — looking down and pressing W descends — and C descends vertically. F (or middle click) opens the throttle for a burst. The view banks into turns and sideways moves, and levels out again on landing |
| Swim ring worn | the bay can be swum, slower than walking, down in the water to the chest, passing under the bridges rather than over them; taking it off over deep water drowns the walker |
| Parachute in hand | F (or C, or middle click) throws it open off a roof or out of the jet; it takes about a second to open. Under the canopy A D/arrows steer, W S trim faster or slower, and Space flares; the landing costs the touchdown speed, and a flare about two meters up costs nothing. Putting it away while open cuts it loose |
| Click | use the right hand; held down, the nail gun and the extinguisher keep firing. A module within reach is selected, and a bug that is caught is displayed and retained |
F, C / middle click |
use the left hand. A secondary tool selects and catches nothing: the grapple hooks the building being looked at, the jet gives a burst of thrust. While flying, C descends instead |
B |
the backpack, from the street as well as from the map |
X / K |
open the export menu; save the current view to the config file |
H |
stow or draw both hands. A stowed tool remains functional and throws from the walker's eye |
| Hold right button | look through the scope |
Enter |
show the details of whatever the reticle is on, as a second use of the tool would. The street softens around the building, which stays sharp with a little of what is around it. This releases the pointer; a click on the map resumes |
| Wheel | zoom |
+ - (or [ ]) |
planet radius, and therefore curvature |
V / M / Esc |
return to the map (Esc first releases a captured pointer). Re-entering walk mode restores the previous position |
The map draws the walker at their last position, as a figure facing the direction they faced — under a canopy, if they left walk mode under one — and re-entering walk mode restores that position, still under it, unless a node was selected on the map in the interim, in which case the walker is placed at that node instead, with the parachute packed.
On foot the bay can be stepped down into and waded in, but deep water drowns a
walker without the swim ring, and every island is reachable by bridge; the
walker may travel at most 3 units out over the water. The ground beneath the
walker is never a target, so aiming at the street selects nothing. While a bug
is being caught, the street softens around it for the second the catch takes,
so it is the one thing in focus. Expanding and
collapsing are reserved to the map view, since either rebuilds the entire city
and is disorienting from street level. The list of controls collapses once the
walker begins to move; ? displays all of them.
Styles
The same map, presented in three ways (--style, or the Style menu):
| Style | What it is |
|---|---|
city |
Buildings of seven types - residential slabs with balconies, glass office towers, brick walk-ups, concrete panel blocks, art-deco towers with setbacks, warehouses, and shops under flats - picked per file from its name and its proportions, never by color; streets with crossings and parks, wooded shores, and bridges between the islands |
circuit |
A printed circuit board: chip packages with rows of pins, heatsinks in place of tall buildings, copper traces along every street with vias set into them, solder pads and silkscreen around each part, capacitors in place of trees and lit LEDs in place of lamps. Beyond the edge of the board lies the backplane it is plugged into, which is live: charge travels along its tracks, indicating that it cannot be walked on |
galaxy |
Platforms suspended in darkness: crystal spires with strata of light and star-like windows, joined by luminous conduits. In place of sea and sky there is the band of the galaxy with its dust lanes, two nebulae behind it and three layers of stars in front. The void the platforms hang in drifts in layers at three different speeds, so the map view redraws continuously under this style (and under circuit) |
Only the environment differs. The colors that carry data — the language palette, the history overlays, hover and selection — are identical in all three (packages take a tint matching the style), so a style alters the presentation and never the reading of the code. Nothing else differs either: the same layout, the same streets, the same walk.
A city building's type comes from its file's name and its proportions, so it is the same building on every run. Near the walker, or once the map is zoomed in close, the buildings grow balconies, awnings, cornices, water tanks, air-conditioning units, antennas and roof railings. They are decoration and nothing more: the walker, the grapple and the jetpack meet the building's own walls, and a balcony stands out of a facade by less than the walker's width. The tallest art-deco towers step back in tiers whose top is the building's height, and their ledges are roofs like any other.
Findings
depphunter runs no scanner; it reads the reports an existing pipeline has
already produced. Point --findings at the JSON emitted by CI — the flag is
repeatable and accepts globs — and each report is placed on the map:
| Tool | written by |
|---|---|
govulncheck -format json |
the advisory, and the call site that reaches it |
npm audit --json |
npm 7 and later, and the npm 6 advisory table |
trivy … --format json |
vulnerabilities, misconfigurations and secret hits |
osv-scanner --format json |
lock-file scans |
golangci-lint run --out-format json |
one finding per issue |
eslint -f json |
one finding per message |
The format is determined from the report's structure rather than from its file name, so the names a pipeline assigns are immaterial:
govulncheck -format json ./... > reports/govulncheck.json
trivy fs --format json -o reports/trivy.json .
depphunter --findings 'reports/*.json'
Each finding is placed on the node it concerns: a vulnerability on the package
it affects, a linter's diagnostic on the file it refers to. A directory carries
the most severe finding beneath it. Severities are normalized onto one scale —
critical, high, medium, low, info — derived, for Trivy and OSV entries, from the
advisory's CVSS v3 vector where one is present (npm audit's own rating is used as
given), because the severity a distribution assigns frequently
disagrees with it; Trivy classifies CVE-2020-8203 as MEDIUM against a vector
scoring 9.8. A linter's "error" is deliberately not treated as a critical
advisory: golangci-lint and eslint findings are capped at medium, since
otherwise the streets would fill with bugs representing missing comments;
Trivy's misconfigurations and secret hits keep Trivy's rating. An advisory
govulncheck finds no call into is capped at low.
Under --online, depphunter additionally queries OSV for
every external package the map pins to a version — batched queries of 500
packages each, followed by the advisories it matched — covering Go, npm, PyPI,
crates.io, Maven, NuGet (C# and F# packages alike), GitHub Actions, Conan (as
OSV's ConanCenter; vcpkg has no OSV ecosystem), Composer (as Packagist),
RubyGems, Swift packages (as SwiftURL, by URL), pub, Hex (Elixir, Erlang and
Gleam packages alike), CRAN, Bioconductor, Hackage, opam and Julia; OSV has no
ecosystem for Terraform modules and providers, Buf Schema Registry modules,
CocoaPods, Carthage, LuaRocks, Wally, CPAN, Zig, Bazel modules and repositories
(the Maven, PyPI, Go, npm and crates.io packages Bazel's module extensions
install are asked about as such), Nix flake inputs, nixpkgs packages, Elm
packages, PureScript packages, Crystal shards, the GitHub, git and HTTP
dependencies Paket fetches, dub packages, fpm packages, haxelib libraries,
Alire crates, Racket packages, Quicklisp projects, Soldeer packages, git
submodules (a Hardhat project's npm packages are asked about as npm),
nimble packages, jsonnet-bundler packages, CUE modules (the Go modules CUE
definitions were generated from are asked about as Go), Dhall packages or
Puppet modules by name and version.
A package fixed to a full git commit (40 hex digits, or 64 for SHA-256) is also asked about by that commit — OSV matches a commit against the repositories and commit ranges its advisories record, whatever the ecosystem — in the same batch, so the git dependencies of the ecosystems above get an answer after all. The commit is read from:
| Pin | Ecosystems |
|---|---|
| a version that is a full commit | Carthage, Zig, Paket GitHub and git files, CMake FetchContent, Terraform git module sources, SwiftPM revisions, git submodules, jsonnet-bundler, dub, fpm, nimble, haxelib, Alire, Quicklisp, Soldeer, CocoaPods, Bazel git_override/git_repository, Puppet git modules, PureScript git packages, and the git dependencies of mix, rebar3, Gleam, CRAN remotes, Hackage, opam, Clojure and Julia |
<version>+git.commit.<sha> |
Crystal shards |
github:owner/repo#<sha>, git+<url>#<sha> |
npm |
| the commit a lock file records for a git dependency | package-lock.json, yarn.lock, pnpm-lock.yaml and bun.lock |
dev-<branch>#<sha>, and a composer.lock branch's source reference |
Composer |
the lock's GIT section revision |
Bundler |
the Cargo.lock source git+<url>#<sha> |
Cargo |
a direct reference name @ git+<url>@<sha> (which now pins) |
PyPI |
the locked rev (the map shows it shortened) |
Nix flake inputs, niv and npins |
Only a commit of a repository on a public forge (github.com, gitlab.com,
bitbucket.org, codeberg.org, sr.ht) is sent — or, for shards, Alire, haxelib,
Soldeer, fpm, nimble and Quicklisp packages that name no repository, one their
plugin found on a public forge (a repository elsewhere is recorded as the
package's origin and makes it private). Nothing a --private pattern or
GOPRIVATE matches, by package name or by repository, is sent. A package that
is private only because it was installed from a public repository instead of an
index (a mix git dependency, a Bundler GIT gem, an npm git dependency) is
asked about by its commit alone, never by its name and version, and a version
that is itself a git reference is not sent as a version. A shortened commit
(Bun's seven digits) is not a pin OSV can be asked about. What a commit matched
is reported by OSV (git commit), with the fixed commit of the advisory's
range for that repository; an advisory the package's name and version already
returned, under its id or an alias, is shown once.
Floating packages are not queried, since they resolve to a different version
on the next installation. Answers are cached for six hours. --no-vulns
disables all of this.
Viewed from above, every building carrying findings bears a pin, colored by the most severe of them and growing taller with their number, so that the red pins identify where to look next from across the map. Hovering over a pin gives the count; selecting it displays the findings. The side panel lists them under Findings, ordered by severity, each expanding in place to show the description, the fixed version and the advisory link. A collapsed directory lists the findings beneath it as well, so that a district marked red for something several levels down can be reached from its pin.
In walk mode the findings appear in the streets: each is a bug patrolling the building it belongs to, colored by severity and shaped by it — a caterpillar for a critical finding, a beetle for the middle of the range, a mite for a note. Catching one with the current primary tool — the net, the bubble wand and the fire extinguisher are meant for it — displays what it carries. The HUD reports how many remain, and a bug left uncaught bites: the worse the finding, the more it costs.
A vulnerability govulncheck proved reachable is not a bug but a fire: the package and the file that calls into it burn, on the map as well as in the street, and the fire spreads to the files that import them until it is put out with the extinguisher.
The backpack (B) is shared between the two views. Adding a finding with
the + beside it in the panel is equivalent to catching its bug in the street;
in both cases the bug stops moving. Its contents survive a re-layout, a depth
change and a reload. An entry is kept until it is cleared; once the scanners
stop reporting it, it is struck through rather than removed, so that a resolved
finding remains visible as such.
The findings list (L, or Findings in the toolbar) is the map view's
way to every finding at once: all of those on what the filters leave on the
map, most severe first and then by package or file, each with its identifier,
title and location. Pointing at a row highlights its building and picking it
selects the building and opens the finding in the panel. The + at the end of
a row is the same catch as the panel's + and the street's: the same backpack
entry, the same building, the same update to the server — so the bug stops
walking without anybody walking to it. A finding already in the backpack reads
"in the backpack", and its ✓ takes it out again. The list is worked through
with the keys as well: the arrows move, Enter opens, + adds and Esc
closes. In walk mode the findings are bugs, and the list is left to the map.
A repository is larger than it appears from within it, so the corner of the walk HUD carries a tracker: a sweep centered on the walker and rotating with them, with one dot per bug in its severity's color, one ring per module already tagged with the tool, and an arrow at the rim for each bug or fire beyond its range. The range adapts to what remains, and beneath it are the distance to the nearest bug and what that bug carries.
Git history
In a git work tree, depphunter reads the history of the analyzed files — by
default the most recent 10,000 non-merge commits; --history-commits changes
the limit and --no-history disables it — in the background once the map is
displayed, and caches the result per commit. The color menu then offers:
| Mode | color shows |
|---|---|
| Commits | commits per file; the per-file mean for collapsed directories |
| Lines changed | lines added plus lines deleted; the per-file mean for collapsed directories |
| Last change | recency of the last change, with recent changes strongest |
| Authors | the number of distinct authors |
Files with no commits in range are given a separate neutral color. The
Since slider in the legend restricts commits, lines changed and authors to a
time range; tooltips and the side panel report the same figures, and the panel
additionally lists the principal authors. Renamed files retain the history of
their former names. Under --watch, a new commit updates the overlay.
git is only ever asked what is on disk: it is never allowed to fetch, never
consults a credential helper and never prompts. A partial clone
(--filter=blob:none) lacks the file contents that counting lines and finding
renames read, so its history is read from the commits alone: commits, last
change and authors are shown, "Lines changed" is not offered, and renames are
not followed.
Versions and pinning
Every external package carries the version the project resolves it to — or,
where nothing resolves it, the range it is declared with — and whether anything
fixes it at that version. Lock files, exact specifiers
(==1.2.3, RequiredVersion), single-version ranges ([1.2.3]), commits and
digests pin a dependency; ranges, wildcards, snapshots and mutable tags do not.
A dependency that nothing pins is drawn in amber (violet under galaxy),
labeled ⚠ floating in the side panel, and marked in its tooltip. Where a
lock file resolved a range, the panel reports both: 4.3.1, requested as
^4.2.0.
| Ecosystem | pinned by | floats on |
|---|---|---|
| Go modules | the version in go.mod, which the build selects; a script's go install x@v1.2.3 |
— (a require always names a version); a script's @latest or @v1.2 |
| npm | package-lock.json (or npm-shrinkwrap.json), pnpm-lock.yaml, yarn.lock, bun.lock (in each, a git dependency by the commit it names), an exact 1.2.3 |
any range, including 1.2, which denotes 1.2.x; a tarball URL; a git dependency locked to a branch or tag only; only bun.lockb |
| crates.io | Cargo.lock, a script's cargo install x@1.2.3 |
the manifest alone, where "1.2.3" denotes ^1.2.3 |
| PyPI | index-url, PIP_INDEX_URL, -i; a uv index with default = true, UV_DEFAULT_INDEX; Poetry's default and primary sources, asked in order; a Pipfile's first source; a PDM source or [pypi] url named pypi, PDM_PYPI_URL |
extra-index-url, PIP_EXTRA_INDEX_URL; any other uv index, UV_INDEX; a supplemental Poetry source; a Pipfile's other sources; any other PDM source |
| Maven | a plain version, [1.2.3], gradle.lockfile (and Gradle 6's gradle/dependency-locks/*.lockfile) |
ranges, LATEST, RELEASE, -SNAPSHOT, dynamic 1.+ and latest.*, unexpanded ${…} |
| NuGet | an exact version, [1.2.3], Paket's = 1.2.3, paket.lock, packages.lock.json |
wildcards (2.*) and ranges, Paket's ~> and >=, no version in Paket or #r |
| Paket remote files | paket.lock's commit, a commit in paket.dependencies |
a branch, tag or tag range, no ref, every HTTP file |
| PowerShell Gallery | RequiredVersion (a manifest, #Requires, Install-Module), a bare Install-PSResource -Version or PSDepend version |
ModuleVersion and -MinimumVersion, which are minimums; a NuGet range; latest |
| GitHub Actions | a full commit SHA | tags, branches, or no ref |
| GitLab CI includes | a commit | tags, branches, templates, remote includes, or no ref |
| Container images | an @sha256: digest |
tags |
| vcpkg | an overrides entry |
version>=, which is a minimum; no version and no baseline |
| Conan | an exact reference (zlib/1.2.13), conan.lock |
a version range ([>=1.0 <2], [~1.2]) |
| Composer | composer.lock, installed.json, a bare 1.2.3 or 1.2, dev-main#<sha> |
^, ~, *, 1.2.*, alternatives, ranges and branches |
| RubyGems | Gemfile.lock (a git gem by its revision), a bare 1.2.3, = 1.2.3 |
~>, >=, <, !=, several requirements, no version |
| Swift packages | Package.resolved, exact:, a bare "1.2.3", revision: |
from:, .upToNextMajor, .upToNextMinor, ranges, branch: |
| pub | pubspec.lock (a git package by its commit), a bare 1.2.3, a git commit |
^, ranges, any, no constraint, a git branch or tag |
| Hex | mix.lock, rebar.lock, Gleam's manifest.toml, a bare 1.2.3, == 1.2.3, a git ref commit |
~>, >=, or and and requirements, a git branch or tag |
| CRAN, Bioconductor | renv.lock, packrat.lock (a GitHub package by its commit), (== 1.2.3) |
(>= 1.2), no version, a Remotes branch or tag |
| Hackage | cabal's plan.json, cabal.project.freeze, stack.yaml.lock, extra-deps, ==1.2.3, a repository commit |
^>= and other ranges, no version, a repository tag or branch |
| Terraform modules | a registry version of 1.2.3 or = 1.2.3, the version terraform init installed (.terraform/modules/modules.json), a git ref commit |
~> and other ranges, no version, a git tag or branch, no ref, an archive |
| Terraform providers | .terraform.lock.hcl (the root module's, for the modules it calls), a single exact constraint |
~>, >= and other constraints, no constraint |
| jsonnet-bundler | jsonnetfile.lock.json (a nested project without a lock: the lock that installs it), a commit as version |
a branch (main, master) or no version; a tag (v1.2.3) is shown, neither |
| CUE modules | the exact v of a cue.mod/module.cue dependency (the modules system selects versions as Go does) |
a dependency without v |
| Dhall packages | a sha256: integrity check on the import (Dhall refuses other content) |
a URL naming no version, or a branch; a version or commit in the URL is shown, neither |
| Puppet modules | a Puppetfile's exact version, a git :commit (or a hash as :ref), a fixture's exact ref |
:latest, no version, a :branch, a metadata.json range; a :tag is shown, neither |
| Buf Schema Registry | buf.lock, a commit ref (:0123…), a plugin's exact version |
a label, tag or branch ref, no ref, a plugin without a version |
| CMake FetchContent | a GIT_TAG commit, a URL_HASH, an archive of a commit |
a branch GIT_TAG (main, origin/…), no GIT_TAG, a download without a hash |
| CocoaPods | Podfile.lock (a git pod by its checkout commit), a bare '1.2.3', '= 1.2.3', a :commit |
~>, >= and other ranges, no version, a :branch, a git pod without a reference |
| Carthage | Cartfile.resolved, == 1.2.3, a quoted commit |
~>, >=, no requirement |
| LuaRocks | luarocks.lock, == 1.2.3 or a bare 1.2.3 in a rockspec (LuaRocks reads it as ==) |
~>, >= and other constraints, no version |
| Wally | wally.lock, =1.2.3 |
a bare 1.2.3 (a caret range in Wally), ^1, other ranges |
| CPAN | cpanfile.snapshot (Carton), == 1.2 in a cpanfile or META prerequisites |
a bare 1.2 (a minimum in CPAN::Meta), >= 1, < 2 and other ranges, 0 or no version |
| opam | *.opam.locked, dune.lock/, {= "1.2"} or (= 1.2), a pin-depends commit |
>= 5.6 & < 6 and other ranges, no constraint, a pin-depends branch or tag |
| Julia | Manifest.toml (and Manifest-v1.11.toml), =1.2.3 in [compat], a [sources] commit rev |
a bare 1.2 (a caret range in Pkg), ~1.2, >= 1, 1.2 - 1.5, no [compat] entry |
| Zig | a .hash in build.zig.zon (Zig verifies the download), a commit in the URL |
a branch archive (refs/heads/), a URL without a ref or hash; a tag is shown, neither |
| Clojure (Maven) | an exact :mvn/version or Leiningen version (Maven's rule), a full :git/sha |
RELEASE, LATEST, ranges, snapshots; a :git/tag alone is shown, neither |
| Bazel modules | MODULE.bazel.lock, a bazel_dep version, single_version_override, an override's commit or integrity |
no version; a git_override branch; a git_override tag is shown, neither |
| Bazel repositories | an http_archive sha256 or integrity, a git_repository commit, an archive of a commit |
a branch (archive or branch =), no ref and no hash; a tag is shown, neither |
| Nix | flake.lock, a commit (rev=, /<commit>) or narHash in the reference, niv and npins pins |
a branch (nixos-24.05, refs/heads/), a channel, <nixpkgs>, a registry name, no ref; a tag is shown, neither |
| Elm | an application's elm.json (exact versions of direct, indirect and test dependencies) |
a package's elm.json ranges (1.0.0 <= v < 2.0.0) |
| PureScript | spago.lock, a registry version in extraPackages, a git commit (ref, a packages.dhall version) |
>=7.0.0 <8.0.0 and bower's ^6.0.0, no package set; a package set's name or a git tag is shown, neither |
| Crystal shards | shard.lock, a commit:, a path dependency (in the repository) |
~>, >= and other ranges, a branch:, no requirement; a tag: or an exact version: is shown, neither |
| dub | dub.selections.json, ==1.2.3 or a bare 1.2.3 (exact in dub), a repository at a commit, a path |
~>, ^, >= and other ranges, *, a ~branch, no version |
| fpm | a git rev, a registry dependency's v, a path |
a git branch or no ref, a registry dependency without v, a metapackage's "*"; a git tag is shown, neither |
| haxelib | a lix pin (haxe_libraries/<name>.hxml: a haxelib version or a git commit), -lib x:1.2.3, an exact haxelib.json or <haxelib version>, a git: commit |
no version, a git branch or a bare git URL; a git tag or a lix tag is shown, neither |
| Alire crates | alire.lock, =1.2.3 or a bare 1.2.3 (exact in Alire), a pin to a git commit or a version, a path pin (in the repository) |
^, ~, >=, /=, * and & combinations, a pin to a git branch or without a commit, a path pin elsewhere |
| Racket packages | a git source's #<commit>, a #:checksum (raco keeps no lock file) |
a #:version (a minimum), no version, a git branch; a version tag is shown, neither |
| Quicklisp | qlfile.lock, a dist version (ql x 2023-10-21, ql :all, the lock's dist for every project), a git :ref commit, an ocicl.csv digest |
:latest, a git :branch or no ref, (:version x "1.2") (a minimum), a project without a qlfile; a git :tag is shown, neither |
| Soldeer | soldeer.lock, an exact "5.0.2" in foundry.toml, a git rev commit |
a version requirement (^1.0.0), a git branch or no ref; a git tag is shown, neither |
| Nimble packages | nimble.lock, atlas.lock, == 1.2.3 or a bare 1.2.3 (exact in nimble), a #<commit> |
a range (>=, ^=, ~=, &), no version, #head or a branch; a #tag is shown, neither |
| Git submodules | the commit git records for the submodule (its gitlink) | no recorded commit (a .gitmodules branch is shown as the version) |
A package a shell script installs (pip install, npm install -g, go install, cargo install, gem install) follows its ecosystem's row; one
installed without a version, or at latest, floats.
The JSON and GraphML exports carry requested and floating per package.
Dependencies of dependencies
--resolve-depth extends the graph beyond what the code imports directly to
what those packages themselves require: 1 adds one level, 2 adds two, and
-1 continues as far as the available information reaches. That information
comes from the lock files the repository already carries and from what
package managers installed into it (or, for Elm, Zig, dub and CUE, into this
machine's package cache); nothing is fetched, and the analysis remains offline.
| Lock file | gives |
|---|---|
package-lock.json, npm-shrinkwrap.json |
each copy's requirements, found up its path |
pnpm-lock.yaml (v5-v9) |
packages: or, in v9, snapshots: |
yarn.lock (classic and Berry) |
each entry's dependencies, by descriptor |
bun.lock (Bun 1.2) |
each package's dependencies, nested ones first |
Cargo.lock |
dependencies per crate |
uv.lock, poetry.lock, pdm.lock |
each distribution's own requirements |
| an installed Python environment | each distribution's Requires-Dist |
conan.lock (Conan 1, graph_lock) |
the requires of each node |
composer.lock, installed.json |
each package's require, without the platform |
Gemfile.lock |
the dependencies listed under each spec |
.build/checkouts/*/Package.swift |
the checked-out package's .package lines |
mix.lock |
each Hex package's requirements |
manifest.toml (Gleam) |
each package's requirements |
renv.lock, packrat/packrat.lock |
each R package's requirements |
dist-newstyle/cache/plan.json |
what each package of cabal's build plan depends on |
Podfile.lock |
the pods each pod's specs depend on |
Carthage/Checkouts/<name>/Cartfile |
a checked-out dependency's own entries |
wally.lock |
each Wally package's dependencies |
rockspecs in lua_modules/, .luarocks/ |
an installed rock's dependencies |
cpanfile.snapshot (Carton) |
each distribution's requirements |
dune.lock/ (dune package management) |
each package's depends |
Manifest.toml (Julia, formats 1 and 2) |
each package's deps |
zig-pkg/<hash>/ or Zig's global cache |
a fetched package's own build.zig.zon |
MODULE.bazel.lock (before Bazel 7.2) |
the resolved module graph (moduleDepGraph) |
maven_install.json (rules_jvm_external) |
each artifact's dependencies |
flake.lock (versions 5 to 7) |
each input's own inputs, follows resolved |
elm.json of packages in ELM_HOME |
an installed Elm package's dependencies |
spago.lock (PureScript) |
each package's dependencies |
manifests of packages in .spago/ |
an installed package's dependencies |
lib/<shard>/shard.yml (shards) |
an installed shard's dependencies |
lib/.shards.info (shards) |
the versions of the shards installed in lib/ |
| recipes of packages dub fetched | a fetched dub package's dependencies |
build/dependencies/<name>/fpm.toml |
a fetched fpm package's dependencies |
lix's haxe_libraries/<name>.hxml |
a pinned library's -lib lines |
haxelib.json of installed haxelibs |
an installed library's dependencies |
alire.lock (Alire) |
each release's depends-on, at the chosen version |
alire.toml of crates Alire fetched |
a fetched crate's depends-on |
nimble.lock (nimble) |
each package's dependencies |
.nimble of installed nimble packages |
an installed package's requires |
jsonnetfile.json in jb's vendor/ |
an installed package's dependencies |
module.cue in cue's module cache |
a fetched module's deps |
metadata.json in r10k's modules/ |
an installed Puppet module's dependencies |
.asd in .qlot/, ocicl's systems/ |
an installed system's :depends-on |
dependencies/<name>-<version>/ (Soldeer) |
an installed package's [dependencies] |
.terraform/modules/modules.json |
an installed module's own module calls |
Packages added in this way are marked transitive, meaning that no file in
the repository imports them. Edges between packages are of kind depends, as
distinct from the import edges that originate at a file, so that the count of
files importing a package remains exactly that. Two versions of one package
remain a single building, so an edge between packages is an edge between names.
For npm, a package reached this way takes the version of the copy its dependent
loads: node_modules/a/node_modules/b before the hoisted node_modules/b, the
yarn.lock entry whose descriptors hold the dependent's range, the version
pnpm-lock.yaml names. A dependency under an alias (npm:real@^1) is the real
package; workspace, portal:, link: and file: dependencies are the
project's own and add no package: one on a workspace package of the project
(also a name the locks install only as that workspace) is an edge to the
workspace's directory. The peer dependencies npm 7 and later install are edges
of package-lock.json too, an optional one only when it is installed. A
package that installs on some platforms only — the binaries esbuild and its
like ship as one optional dependency per platform, all of which a lock file
lists — stays in the graph, and its node's platform (Yarn Berry's
conditions, or the os, cpu and libc that npm, pnpm and Bun copy into
the lock) says where, as os=linux & cpu=x64; the side panel shows it as
"installs on". A git dependency (github:owner/repo, git+ssh://…), whether
the project or a package needs it, is pinned to the commit its lock file
records, which is its version; the repository, without any token its URL
holds, is its origin, so the registry is not asked about it (see
Findings for what the vulnerability database is asked). One the
lock names only a branch or a tag of is not pinned, and the resolution report
notes it (git-unpinned).
A Python package that no lock file gives edges for (Pipfile.lock records none)
falls back to the installed environment (see Languages).
Ecosystems that keep the dependency graph outside the repository — Go modules,
NuGet packages no paket.lock or packages.lock.json records (both record the
graph, and answer offline), container images, a Composer, Bundler, Mix or R
project that commits no lock, pub, whose pubspec.lock is a flat list, rebar3,
whose rebar.lock records only a depth, and a Haskell project without cabal's
build plan on disk (cabal.project.freeze and stack.yaml.lock list versions
only), Terraform registry modules terraform init has not installed, pods no
Podfile.lock records, rocks no LuaRocks tree of the repository holds
(luarocks.lock is a flat list), CPAN distributions no cpanfile.snapshot
records, opam packages no dune.lock/ records (an *.opam.locked is a flat
list) and Julia packages no Manifest.toml records, Gleam packages no
manifest.toml records, Elm packages the compiler has not installed in
ELM_HOME (an application's elm.json lists indirect packages flat),
PureScript packages no spago.lock records and spago has not installed into
.spago/ (a spago.dhall project's package set is remote), dub packages dub
has not fetched onto this machine (dub.selections.json is a flat list), Alire
crates no alire.lock records and Alire has not fetched, Maven artifacts of
Java, Kotlin, Scala and Clojure builds (Maven, Gradle without its lock files,
sbt, tools.deps and Leiningen), Bazel modules (a lock file since Bazel 7.2
records versions only), Puppet modules r10k has not installed, Racket packages
(raco keeps no lock file), Wally packages no wally.lock records, Buf Schema
Registry modules (buf.lock is a flat list), CUE modules missing from cue's
module cache on this machine ($CUE_CACHE_DIR, else cue in the user's cache
directory), Swift registry packages SwiftPM has not checked out under
.build (Package.resolved is a flat list), Conan packages no Conan 1 lock
records (a Conan 2 conan.lock is a flat list), the actions and reusable
workflows of other repositories that a GitHub workflow names, and PowerShell
Gallery modules — require --online, described below; vcpkg, Bioconductor
packages no lock records, Swift packages from git repositories
that SwiftPM has not checked out and Terraform modules fetched from git or an
archive are not resolved beyond the first level at present. Content a CMake
build fetches is not resolved beyond the first level either, nor are Carthage
dependencies not checked out into Carthage/Checkouts/ (Cartfile.resolved is
flat) and Zig packages Zig has not fetched into zig-pkg/ or its global cache
(there is no Zig registry for --online to ask), nor Bazel's WORKSPACE
repositories, nor niv and npins sources (their sources.json is flat), nor
Crystal shards shards has not installed into lib/ (shard.lock is flat, and
shards are git repositories with no index to ask), nor the GitHub, git and HTTP
files Paket fetches, nor fpm packages fpm has not fetched into
build/dependencies/ (fpm keeps no lock file, and its registry has no
dependency API), nor haxelib libraries that neither lix pins nor haxelib
installed (lib.haxe.org offers no JSON API to ask). Quicklisp projects that
neither Qlot installed into .qlot/ nor ocicl into systems/ need --online
too: qlfile.lock and ocicl.csv are flat lists. A git submodule of a Foundry
project depends on the submodules of its own .gitmodules when it is checked
out; Soldeer packages are followed only where Soldeer installed them into
dependencies/ (soldeer.lock is flat, and Soldeer's registry has no
dependency data: its API serves each revision as a zip archive, which is not
downloaded), nor are nimble packages no nimble.lock records and nothing
installed (atlas.lock is flat, and the package list has no dependencies to
ask), nor remote Dhall imports (they are not fetched). A Terraform provider
depends on nothing.
The side panel presents these as a tree: every row under Depends on and
Used by expands into that node's own dependencies, and so on recursively.
Nothing is fetched, since the edges are already present in the map, so a row
expands immediately; ▸/▾ or the arrow keys expand and collapse it, and the
expansion state is preserved when a --watch update redraws the panel. A
package that depends on something which in turn depends on it is shown once
more, marked ↻, and left collapsed, since lock files do contain cycles and a
tree following one would not terminate.
Package indexes
Every external package records the index it comes from. depphunter reads both
the index configuration present on this machine and the configuration the
repository carries — .npmrc, including @scope:registry; .yarnrc.yml,
those of the directories above it and ~/.yarnrc.yml (npmRegistryServer,
npmScopes, YARN_NPM_REGISTRY_SERVER); Yarn 1's .yarnrc and
~/.yarnrc; Bun's
bunfig.toml and global bunfig ([install] registry, [install.scopes]);
pip.conf and a requirements file's --index-url and --extra-index-url;
uv's indexes and find-links in pyproject.toml, uv.toml and the user's
and system's uv.toml (UV_INDEX, UV_DEFAULT_INDEX, UV_FIND_LINKS),
Poetry's sources in
pyproject.toml, a Pipfile's and Pipfile.lock's sources, PDM's sources
in pyproject.toml and pdm.toml and its own config.toml (PDM_PYPI_URL),
each with the packages pinned to one; NuGet.config and the source
lines of paket.dependencies and feeds of paket.lock (nuget.org itself and
directories aside); a POM's <repositories> and the maven
repositories of Gradle build and settings scripts (not those of
pluginManagement or buildscript), and the resolvers of an sbt build,
other than Maven Central; the mirrors of Maven's settings.xml and the
repositories of its active profiles; the repositories of Gradle's init
scripts, of the Clojure CLI's user deps.edn, of Leiningen's :user profile
and of ~/.sbt/repositories, and COURSIER_REPOSITORIES;
.cargo/config.toml; the composer repositories of
composer.json and of Composer's own config.json; a Gemfile's source
lines (a source ... do block serves only its gems), Gemfile.lock's
remotes, ~/.gemrc and Bundler's rubygems.org mirror; a pubspec.yaml's
hosted: servers, the servers pubspec.lock resolved from and
PUB_HOSTED_URL; HEX_API_URL, HEX_API or the api_url of Hex's
hex.config; the repositories of renv.lock (asked in order, each also
serving the packages recorded from it), options(repos = ...) in .Rprofile,
~/.Rprofile and R_PROFILE_USER, and RENV_CONFIG_REPOS_OVERRIDE; the
repository stanzas and active-repositories of cabal.project and of the
configuration cabal reads (CABAL_CONFIG, CABAL_DIR, ~/.cabal/config,
~/.config/cabal/config) other than Hackage itself; a Podfile's source
lines (asked in order) and the spec repositories of Podfile.lock (each
serving the pods installed from it) other than CocoaPods' own, and the spec
repositories cloned in ~/.cocoapods/repos; the registries of SwiftPM's
registries.json, the user's and the repository's; the remotes of Conan's
remotes.json, in this machine's Conan home and in the home a repository's
.conanrc names; the GitHub instances GITHUB_API_URL, GH_HOST and the
GitHub CLI's hosts.yml name; the repositories PSResourceGet and
PowerShellGet register; the rocks_servers of a
project's .luarocks/config-5.x.lua and of the user's configuration
(LUAROCKS_CONFIG_5_x, LUAROCKS_CONFIG, $XDG_CONFIG_HOME/luarocks,
~/.luarocks); the registries of DUB_REGISTRY,
of dub's settings.json (registryUrls) and of a dub.settings.json, asked
before code.dlang.org; the dists a qlfile adds (dist, and the Ultralisp
dist of an ultralisp line) and those installed in ~/quicklisp, asked
before the Quicklisp dist; the CPAN mirrors cpanm (PERL_CPANM_OPT's
--mirror under --mirror-only) and Carton (PERL_CARTON_MIRROR) install
from; the repositories of opam's current switch and a dune-workspace's
repository stanzas; the indexes alr uses; the registries installed in the
Julia depots; the Maven repositories of a
deps.edn or bb.edn (:mvn/repos), a project.clj or build.boot
(:repositories) and a shadow-cljs.edn other than Maven Central and
Clojars; the --registry lines of a .bazelrc and ~/.bazelrc, and of the
files their import and try-import lines name, other than the Bazel Central
Registry; and GOPROXY — and the
side panel names the index each package resolves from. A container image
requires no configuration, since ghcr.io/org/app names its registry directly,
and neither does a Terraform module: app.terraform.io/acme/vpc/aws names its
registry, which is trusted when Terraform's CLI configuration names the host
(see Authenticated registries).
Configuration locations
The files named above as ~/... are where each tool keeps its configuration by
default. depphunter looks for the machine's configuration, and the credentials
kept beside it, where the tool itself would, following the tool's own variables
and platform paths:
| Tool | Where, in the tool's order of precedence |
|---|---|
| npm | npm_config_* variables in any case (npm_config_registry, npm_config_@scope:registry, npm_config_//host/:_authToken; the lower-case spelling wins); the user's npmrc (npm_config_userconfig, else ~/.npmrc); the global npmrc (npm_config_globalconfig, else etc/npmrc under npm_config_prefix; npm's built-in prefix is not guessed) |
| Yarn | YARN_NPM_REGISTRY_SERVER, YARN_NPM_AUTH_TOKEN, _IDENT, _ALWAYS_AUTH; Yarn Berry's file (named YARN_RC_FILENAME, else .yarnrc.yml) in each directory above the analyzed checkout, then ~/.yarnrc.yml, merged key by key, ${VAR} resolved from the environment; Yarn 1's ~/.yarnrc (registries only: Yarn 1 takes credentials from the npmrc) |
| Bun | $XDG_CONFIG_HOME/.bunfig.toml when it exists, else ~/.bunfig.toml, $VAR resolved from the environment |
| pip | PIP_INDEX_URL and PIP_EXTRA_INDEX_URL; the file PIP_CONFIG_FILE names; the user's pip.conf ($XDG_CONFIG_HOME/pip, ~/Library/Application Support/pip on macOS, %APPDATA%\pip\pip.ini on Windows, and the legacy ~/.pip), skipped when PIP_CONFIG_FILE names an existing file; the site-wide /etc/pip.conf, $XDG_CONFIG_DIRS/pip/pip.conf, %ProgramData%\pip\pip.ini. PIP_CONFIG_FILE=/dev/null switches every file off |
| uv | UV_DEFAULT_INDEX, UV_INDEX, UV_FIND_LINKS, UV_INDEX_STRATEGY and the legacy UV_INDEX_URL, UV_EXTRA_INDEX_URL; the file UV_CONFIG_FILE names alone; else the user's uv.toml ($XDG_CONFIG_HOME/uv, ~/.config/uv; %APPDATA%\uv on Windows) and the system's (the first $XDG_CONFIG_DIRS/uv/uv.toml, else /etc/uv/uv.toml; %ProgramData%\uv on Windows). UV_NO_CONFIG reads no uv.toml, the repository's included |
| Poetry | config.toml and auth.toml in POETRY_CONFIG_DIR, else %APPDATA%\pypoetry on Windows, ~/Library/Application Support/pypoetry on macOS, else $XDG_CONFIG_HOME/pypoetry (~/.config/pypoetry); POETRY_REPOSITORIES_<NAME>_URL, POETRY_HTTP_BASIC_<NAME>_USERNAME/_PASSWORD |
| PDM | PDM_PYPI_URL, PDM_PYPI_USERNAME, PDM_PYPI_PASSWORD over config.toml in PDM_CONFIG_FILE, else %LOCALAPPDATA%\pdm\pdm on Windows, ~/Library/Application Support/pdm on macOS, else $XDG_CONFIG_HOME/pdm (~/.config/pdm) |
| Cargo | CARGO_REGISTRIES_<NAME>_INDEX, _TOKEN and _CREDENTIAL_PROVIDER (the name upper-cased, - as _), CARGO_REGISTRY_TOKEN and _CREDENTIAL_PROVIDER for crates.io, CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS; config/config.toml and credentials/credentials.toml in CARGO_HOME, else ~/.cargo (the file without an extension when both exist) |
| Go | the environment, else the go env file (GOENV, else go/env in the user configuration directory; GOENV=off for none), for GOPROXY, GOPRIVATE, GONOPROXY, GONOSUMDB and GOAUTH (its netrc and off entries; git and command entries are not run) |
| containers | REGISTRY_AUTH_FILE alone when set; else $XDG_RUNTIME_DIR/containers/auth.json (Linux; ~/.config/containers/auth.json elsewhere), $XDG_CONFIG_HOME/containers/auth.json, and Docker's config.json in DOCKER_CONFIG, else ~/.docker; the first file holding a registry's credential wins, as in containers-auth.json(5) |
| registries | CONTAINERS_REGISTRIES_CONF, else $XDG_CONFIG_HOME/containers/registries.conf (~/.config/...) when it exists, else /etc/containers/registries.conf; then the *.conf of /etc/containers/registries.conf.d and the user's registries.conf.d (only the user's beside the user's file), by name |
| dockerd | daemon.json: the rootless daemon's ($XDG_CONFIG_HOME/docker, ~/.config/docker) when it exists, else /etc/docker; Docker Desktop's ~/.docker on macOS and Windows, else %ProgramData%\docker\config |
| netrc | NETRC; else ~/_netrc on Windows when it exists; else ~/.netrc |
| NuGet | nuget.config in each directory above the analyzed checkout; %APPDATA%\NuGet\NuGet.Config on Windows, else ~/.nuget/NuGet/NuGet.Config and ~/.config/NuGet/NuGet.Config, then the *.config of the config directory beside it; the machine-wide *.config of NuGet\Config under %ProgramFiles(x86)% on Windows, else NUGET_COMMON_APPLICATION_DATA, /Library/Application Support (macOS) or /etc/opt (Linux) |
| Composer | one home, for repositories and credentials alike: COMPOSER_HOME; %APPDATA%\Composer on Windows; else the first that exists of $XDG_CONFIG_HOME/composer (~/.config/composer) and ~/.composer |
| Bundler | BUNDLE_USER_CONFIG; else config in BUNDLE_USER_HOME; else ~/.bundle/config |
| Maven | ~/.m2/settings.xml, then conf/settings.xml under MAVEN_HOME, else M2_HOME (the user's file wins); -s and MAVEN_ARGS are not followed |
| Gradle | the init scripts of GRADLE_USER_HOME, else ~/.gradle: init.gradle(.kts), then init.d/*.gradle(.kts) by name; gradle.properties there for credentials |
| sbt | -Dsbt.repository.config, else repositories in -Dsbt.global.base, else ~/.sbt, those properties and -Dsbt.override.build.repos read from JAVA_OPTS, then SBT_OPTS; credentials in SBT_CREDENTIALS, ~/.sbt/.credentials, ~/.ivy2/.credentials |
| Coursier | COURSIER_REPOSITORIES; COURSIER_CREDENTIALS (inline, or a file: a path or a file: URL, file:///C:/x being C:\x on Windows); else credentials.properties in COURSIER_CONFIG_DIR, %APPDATA%\Coursier\config on Windows, ~/Library/Application Support/Coursier on macOS, else $XDG_CONFIG_HOME/coursier (~/.config/coursier) |
| Clojure | deps.edn in CLJ_CONFIG, else $XDG_CONFIG_HOME/clojure when that is set, else ~/.clojure; Leiningen's profiles.clj in LEIN_HOME, else ~/.lein |
| Dart | pub-tokens.json in %APPDATA%\dart on Windows, ~/Library/Application Support/dart on macOS, else $XDG_CONFIG_HOME/dart (~/.config/dart) |
| Hex | HEX_API_URL, HEX_API, HEX_API_KEY, HEX_REPOS_KEY; hex.config in HEX_HOME, else $XDG_CONFIG_HOME/hex (~/.config/hex) under MIX_XDG=1, else ~/.hex |
| rebar3 | {hex, [{repos, ...}]} of rebar.config in .config/rebar3 under REBAR_GLOBAL_CONFIG_DIR, else the home directory; hex.config there, under REBAR_CACHE_DIR when only that is set |
| cabal | CABAL_CONFIG, else config in CABAL_DIR, else ~/.cabal/config while ~/.cabal exists and the XDG file does not, else $XDG_CONFIG_HOME/cabal/config (~/.config/cabal/config); %APPDATA%\cabal\config on Windows |
| LuaRocks | per Lua version, the file LUAROCKS_CONFIG_5_x, else LUAROCKS_CONFIG, names when it exists; else config-5.x.lua in $XDG_CONFIG_HOME/luarocks (~/.config/luarocks) when it exists, else in ~/.luarocks; %APPDATA%\luarocks on Windows |
| dub | DUB_REGISTRY; settings.json in DUB_HOME, else dub under DPATH, else %APPDATA%\dub on Windows, else ~/.dub; then %ProgramData%\dub on Windows, else /etc/dub and /var/lib/dub; its skipRegistry too |
| Quicklisp | the distinfo-subscription-url of each dist installed in ~/quicklisp/dists |
| Bazel | ~/.bazelrc and the files its import and try-import lines name (not %workspace% ones) |
| cpanm | PERL_CPANM_OPT (its --mirror, --mirror-only and --from), PERL_CARTON_MIRROR |
| opam | OPAMROOT, else %LOCALAPPDATA%\opam on Windows, else ~/.opam: repo/repos-config, the root's config and the current switch's (OPAMSWITCH) switch-config, and the repository copies in repo/ |
| Alire | ALIRE_SETTINGS_DIR (Alire 1: ALR_CONFIG), else %USERPROFILE%\.config\alire on Windows, else $XDG_CONFIG_HOME/alire (~/.config/alire): indexes/<name>/index.toml and the checkout beside it |
| Julia | JULIA_DEPOT_PATH (;-separated on Windows, else :; an empty entry is the default depot), else ~/.julia: each depot's registries/<Name>/ and registries/<Name>.toml with the archive it names |
| r10k | /etc/puppetlabs/r10k/r10k.yaml, else /etc/r10k.yaml: forge: baseurl and authorization_token |
| cue | CUE_REGISTRY; logins.json in CUE_CONFIG_DIR, else cue in the user configuration directory |
| buf | BUF_TOKEN; the netrc entries buf registry login writes |
| CocoaPods | CP_REPOS_DIR, else repos below CP_HOME_DIR, else ~/.cocoapods/repos: each spec repository, a git clone by its origin remote or a CDN source by its .url |
| SwiftPM | registries.json in ~/Library/org.swift.swiftpm/configuration on macOS when it is there, else in configuration below $XDG_CONFIG_HOME/swiftpm or ~/.swiftpm; the netrc for swift package-registry login (the keychain is not read) |
| Conan | CONAN_HOME (a leading ~ being the home directory), else ~/.conan2: remotes.json, credentials.json (the environment variables its template names substituted) and extensions/plugins/auth_remote.py (found, not run); CONAN_LOGIN_USERNAME[_<REMOTE>], CONAN_PASSWORD[_<REMOTE>] |
| GitHub | GITHUB_API_URL, GH_HOST, GH_TOKEN, GITHUB_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN; the GitHub CLI's hosts.yml in GH_CONFIG_DIR, else gh below XDG_CONFIG_HOME, else %AppData%\GitHub CLI on Windows, else ~/.config/gh (a token gh keeps in the keyring is not read) |
| PowerShell | PSResourceGet's PSResourceGet/PSResourceRepository.xml in %LOCALAPPDATA% on Windows, ~/Library/Application Support on macOS when it is there, else $XDG_DATA_HOME (~/.local/share); PowerShellGet 2's PSRepositories.xml in %LOCALAPPDATA%\Microsoft\Windows\PowerShell\PowerShellGet on Windows, else powershell/PowerShellGet below $XDG_CACHE_HOME (~/.cache) |
Nothing is read from the repository through these variables' defaults; they only say where this machine's own files are.
The two sources are not treated alike. An index named by this machine's own
configuration is trusted. One that appears only in the repository is recorded
and marked ⚠ index, because a repository directing a package manager at an
index that nothing here configures is the form a dependency-confusion attack
takes. No request is ever made to such an index, unless it is vouched for with
--trust-index.
Which index is asked
Package managers differ in how a configured index relates to the public one, and depphunter follows each of them. A source either replaces the public default or is asked beside it:
| Ecosystem | replaces the public default | asked beside it |
|---|---|---|
| PyPI | index-url, PIP_INDEX_URL, -i; a uv index with default = true (a repository's before the user's uv.toml's); a PDM source named pypi; Poetry's primary sources, asked in order, PyPI only when one of them is it (a declared PyPI of any priority drops the implicit one) |
extra-index-url, PIP_EXTRA_INDEX_URL; any other uv index or find-links location; any other PDM source; after the primary index: a supplemental (or legacy secondary) Poetry source, the PDM sources after PyPI under respect-source-order |
| Maven | a settings.xml mirror of central; a mirror of * or external:* (or one without mirrorOf), which stands in for every repository; a settings profile or Clojure repository with the id central; COURSIER_REPOSITORIES and, under -Dsbt.override.build.repos=true, ~/.sbt/repositories, lists asked in order |
a POM's <repositories>, Gradle's maven { url … } (a build's or an init script's), sbt's resolvers, Clojure's :mvn/repos and :repositories (a project's or the user's), an active settings profile's repositories, ~/.sbt/repositories, a mirror of any other repository; Clojars after Central for a Clojure project |
| Composer | nothing: "packagist.org": false switches Packagist off |
every composer repository |
| NuGet | nothing: nuget.org is off when the merged configuration leaves it out (a <clear/> with no closer entry for it, or it disabled), or when paket.dependencies lists sources and none is nuget.org |
every enabled feed; every Paket source |
| Go | GOPROXY: its proxies are asked in order, and proxy.golang.org only if it is on the list |
— |
| CRAN | R's repos option (options(repos = ...) of an R profile), renv.lock's repositories and RENV_CONFIG_REPOS_OVERRIDE: each list asked in order, CRAN only when it is on it (as @CRAN@ or a mirror) |
the literal repositories of a repos value that extends one it does not spell out, c(getOption("repos"), ...) |
| Hackage | a mirror under Hackage's own name; cabal's active-repositories, whose repositories are asked last to first; a cabal configuration listing repositories without Hackage leaves it out |
every other repository stanza of cabal's configuration and of cabal.project |
| LuaRocks | rocks_servers, asked in order (a group's mirrors each after any failure of the one before), luarocks.org only when it is on the list |
— |
| CPAN | cpanm's --mirror list under --mirror-only or --from, asked in order, MetaCPAN only when a public CPAN mirror is on it |
PERL_CARTON_MIRROR |
| opam | the repositories of the opam switch, asked in its order of priority, opam-repository only when it is one of them; a dune-workspace lock_dir's repositories, likewise |
a dune-workspace's repository stanzas when no lock_dir lists any |
| Alire | alr's indexes, asked by priority, the community index only when it is one of them | — |
| CocoaPods | a Podfile's source lines, asked in its order, the CDN only when it is one of them (or none is listed) |
— |
| SwiftPM | nothing: there is no public registry | — (a package is asked of the one registry registries.json maps its scope to, else of the default registry) |
| Conan | remotes.json's remotes, asked in its order, ConanCenter only when it is one of them (or there is no remotes.json); a remote's allowed_packages limit what it is asked for |
— |
| GitHub Actions | nothing: api.github.com is always asked last | the GitHub instances of GITHUB_API_URL, GH_HOST and hosts.yml, asked first |
| PowerShell Gallery | the repositories PSResourceGet registers, asked by priority, and PowerShellGet 2's; the Gallery only when one of them is it (or nothing is registered) | — (an install naming -Repository is asked of that repository alone) |
| crates.io | [source.crates-io] replace-with (followed to the end of the chain) |
— |
| npm and every other ecosystem | the first unscoped source found (npm's registry, then Yarn's npmRegistryServer, ~/.yarnrc's registry and Bun's [install] registry) |
— |
A package is asked of the sources beside the public default first, in the
order they were found (this machine's, then the repository's), and of the
public default, or what replaces it, last. The next index is asked only when
one says it does not have the package: a 404 or 410, or an answer that does
not list it. An index that fails in any other way — unreachable, 401, 500 —
ends the question, since the next index's package of that name may not be the
same package; the one exception is a GOPROXY entry followed by |, which the
go command also passes over on any failure. GOPROXY entries after direct
or off are not reached, and GOPROXY=direct or GOPROXY=off leaves no proxy
to ask.
A module the go command fetches directly from version control — one matching
GONOPROXY or GOPRIVATE, or reached through direct — is not asked at all.
Reading its go.mod from the forge's raw-file URL would name a module this
machine's own configuration declares private, with its version, to a host
outside that configuration, which is what GOPRIVATE is there to prevent.
Whether a proxy is sent the netrc's credential follows GOAUTH: unset or
listing netrc, it is sent; off, or a list of only git <dir> and command
entries, and it is not. Those two forms run a program for the credential,
which depphunter does not do.
A container image is asked where this machine's container tools pull it from.
The [[registry]] of registries.conf whose prefix matches the most of the
image's name (docker.io/library/nginx for nginx) rewrites it: its
[[registry.mirror]] entries are asked first, in order — those
pull-from-mirror or mirror-by-digest-only limit to digests only for a
digest, to tags only for a tag — and its location last, each with the
matched prefix replaced by its own location. A mirror that fails in any way is
passed over, as Podman and Docker pass over one. A blocked = true registry
is never asked, and the report says so. A Docker Hub image no [[registry]]
matches is asked of the Docker daemon's registry-mirrors first, then of
Docker Hub. The map attributes the image to its registry either way; the
report names the mirror that answered. unqualified-search-registries,
short-name-mode and short-name aliases are not applied, since the map names
every Docker Hub image the short way however it was written, and Docker itself
searches no list. A location is asked over https even when insecure = true.
A CPAN mirror — a DarkPAN, a Pinto or OrePAN2 repository, a minicpan
directory — is read from its modules/02packages.details.txt.gz, once: a
distribution it does not list is asked of the next index, as cpanm does.
The archives are not downloaded, so a distribution it lists is given the
dependencies of the same release on MetaCPAN (by the author its path names);
one MetaCPAN does not describe is shown without dependencies, with a
no-release note. A distribution a private pattern covers is not named to
MetaCPAN, which is why a DarkPAN's own distributions belong under
--private patterns. opam and alr keep a copy of every repository and index
they use (~/.opam/repo/<name> or its .tar.gz,
~/.config/alire/indexes/<name>/repo), and that copy is read instead of the
network: it lists a package's versions, which a file server cannot, and it
holds private repositories whatever serves them. A range is answered by the
newest version it admits — in opam's version order, or Alire's semantic
versioning — without either tool's solver. Without a copy, opam-repository
and the Alire community index are listed through GitHub's contents API
(60 requests an hour without a credential for api.github.com in the netrc);
another repository served over HTTP is asked only about pinned versions, and
one only git serves is not read (a no-copy note says so).
Julia's registries are read the same way: every registry installed in a depot
(JULIA_DEPOT_PATH, else ~/.julia) — a git checkout in
registries/<Name>/, or the archive a Pkg server served
(registries/<Name>.toml naming <Name>.tar.gz, read once) — is read from
that copy, whatever host it came from, and General's copy is read in place of
its files on GitHub. As in Pkg, a package is looked up by its UUID (from
Project.toml and Manifest.toml, and each registry's Deps.toml) in every
installed registry that lists it, so another registry's package of the same
name is not mistaken for it; one no registry lists is General's. With
registries installed and General not among them, General is not asked. Without
any installed registry, General's files are read from GitHub.
JULIA_PKG_SERVER is not asked: a Pkg server serves whole registries as
archives by tree hash (/registries, /registry/<uuid>/<hash>), and the
copy it installed in the depot is what is read.
CocoaPods' spec repositories are read the same way. Each git clone in
CocoaPods' repos directory (pod repo add; CP_REPOS_DIR, else
~/.cocoapods/repos) is read in place of the repository its origin remote
names — compared as CocoaPods compares them, without case, .git or a
trailing slash — so a company's private spec repository answers from the disk,
whatever host serves it, and a clone of the trunk repository answers before
the CDN is asked; the CDN source's own directory (~/.cocoapods/repos/trunk)
answers with the version lists and podspecs CocoaPods already downloaded. A
clone is read as CocoaPods lays it out: below Specs/ or its root, sharded as
its CocoaPods-version.yml says, a .podspec.json or a Ruby .podspec
(whose dependency lines are read without running Ruby). A clone is no index
of its own: the pods are asked of the repositories the Podfile lists, in its
order, and a repository the project names that this machine has a clone of is
trusted, since this machine uses it already. A git spec repository without a
clone is not read (a no-copy note says so) and the next one is asked.
A Swift registry package (.package(id: "scope.name")) is asked of the
registry SwiftPM's registries.json maps its scope to, else of the default
registry ([default]) — the repository's file (in .swiftpm/configuration
beside its Package.swift) over the user's, scope by scope, as SwiftPM merges
them. A registry only the
repository's file names is marked ⚠ index and not asked until it is
vouched for, whatever credential this machine holds for its host; one the
user's file names too is trusted. A package named by its repository's URL is
asked of no registry.
A Conan package is asked of the remotes of Conan 2's remotes.json, in the
file's order, as Conan asks them: this machine's (in CONAN_HOME, else
~/.conan2; ConanCenter alone when there is no file), then those of the home a
repository's .conanrc names (conan_home=./.conan2, read from the disk, the
.conanrc nearest the conanfile up to the repository's root). A disabled
remote and a local-recipes-index folder are not asked, and a remote's
allowed_packages patterns limit it to the references they match. The
.conanrc's home is the repository's choice wherever it is: its remotes are
marked ⚠ index and not asked until vouched for, unless this machine's own
remotes.json lists the same remote, and its credentials.json is not read.
ConanCenter (center2.conan.io, and the older center.conan.io) is the public
index, never asked about a private package. A version range is resolved as
Conan resolves it, against the remote's search, and a package the repository's
conan.lock pins is asked about, and answered, at the lock's version and
recipe revision.
A GitHub action or reusable workflow (owner/repo[/path]@ref) names no host:
an Enterprise Server runs the one it holds and, with GitHub Connect, falls back
to github.com's. It is asked of the GitHub instances this machine works with
first — the one GITHUB_API_URL names (on a runner of that instance), the one
GH_HOST names and every other host of the GitHub CLI's hosts.yml, each at
its REST API (<host>/api/v3, api.<name>.ghe.com) — and then of github.com;
an organization's own action (--private 'actions:acme/*') is never named to
github.com. A repository cannot name an instance.
A PowerShell module is asked of the repositories this machine registers, as
PowerShell's installers ask them: PSResourceGet's (PSResourceRepository.xml)
by priority, then by name, then PowerShellGet 2's (PSRepositories.xml) in
their order; the Gallery only when one of them is it, or when neither file
exists. A local repository and a container registry are not asked. An
Install-Module or Install-PSResource naming -Repository, or a PSDepend
dependency naming its Repository, is asked of that repository alone, by the
name it was registered under, and so are the module's dependencies, as
PowerShellGet installs them from the same repository.
Five kinds of source are authoritative and have no fallback: a scoped source
that covers the package (an npm @scope:registry, a Gemfile source block, a
pubspec's hosted: server, a Python package pinned to an index by uv's
[tool.uv.sources], Poetry's source =, Pipenv's index =, or by PDM's
include_packages, whose matching sources are all asked, in order),
the NuGet feeds packageSourceMapping maps the package to, a Cargo alternative
registry, a private Hex organization (organization: "acme" or
repo: "hexpm:acme" in mix.exs, "hexpm:acme" in mix.lock), and the host
an image (with the mirrors this machine configures for it, below) or Terraform
module names. A package missing
from one of them is not looked for on the public index, which is exactly where a
dependency-confusion attack would plant it. A Cargo registry of
[registries.<name>] serves only the crates that declare it —
registry = "<name>" in Cargo.toml, or its index as Cargo.lock's
source — and every other crate stays with crates.io.
NuGet's configuration files are merged as NuGet merges them: the machine-wide
files, the additional user files (~/.nuget/NuGet/config/*.config), the
user's NuGet.Config, the nuget.config files of the directories above the
analyzed one, then the repository's nuget.config files (a deeper one over a
shallower one), a closer file's entry replacing the same key
and a <clear/> dropping everything before it — so a repository's <clear/>
drops this machine's feeds as well. A feed <disabledPackageSources> disables
is not asked. Under <packageSourceMapping>, a package is asked only of the
feeds mapped to its most specific pattern — an exact id over Contoso.*, a
longer prefix over a shorter one, * last — so Contoso.Billing mapped to
the company feed is never named to nuget.org, even when the feed lacks it. A
package no pattern covers is asked of every feed and nuget.org as usual, where
NuGet itself would refuse it; Paket's feeds are not mapped.
Python's tools are read in their own terms. A repository's uv indexes are
asked before those of the user's and the system's uv.toml, and its default
index wins over theirs, as uv reads a project's configuration first; uv's
variables still come before both. A uv.toml beside a pyproject.toml
replaces its [tool.uv] indexes, as in uv, while its [tool.uv.sources]
still pins packages, as uv resolves the names: to an index UV_INDEX or
UV_DEFAULT_INDEX names, else to the pyproject.toml's own
[[tool.uv.index]], never to a uv.toml's; UV_NO_CONFIG and
UV_CONFIG_FILE leave a repository's uv.toml unread. A flat index (an
index with format = "flat", find-links, UV_FIND_LINKS) is a page or a
directory listing distribution files: its file names give the versions, and
a file's PEP 658 .metadata its requirements, or, for a directory this
machine names, the METADATA of a wheel in it. A repository's flat index is
recorded only when it is a URL, untrusted like any repository index. Poetry's
primary sources (after any of priority default) are asked in the order
written, and
PyPI only when one of them is a source named PyPI; its supplemental (and legacy
secondary) sources are asked after them, or after PyPI when no source is
primary, and a source named PyPI of any priority drops the implicit PyPI, as
Poetry does. A Pipfile's first source
replaces PyPI and its others are asked beside it, as Pipenv passes them to
pip. A PDM source named pypi replaces PyPI; a package some sources'
include_packages patterns match is asked of those sources alone, and a
source is never asked for a package its exclude_packages patterns match
(nor PyPI, when the pypi source excludes it). Under PDM's
respect-source-order its sources are asked in order around PyPI (after it
when no source is named pypi). uv under index-strategy
unsafe-best-match or with find-links, and PDM without
respect-source-order, merge the versions all their indexes have: depphunter
still asks the indexes in order and takes the first that has the package, and
--explain says so once (merged). Poetry's
config.toml repositories are where it publishes, not sources. When several
of this machine's tools name a replacement for PyPI, pip's comes first, then
uv's, then PDM's.
R, cabal and LuaRocks ask every repository they are given. R's repos
option, renv.lock's repositories and RENV_CONFIG_REPOS_OVERRIDE are lists
asked in order, CRAN (written as a mirror or as @CRAN@) only where it is on
the list; R keeps the highest version any repository has, while depphunter
takes the first repository that has the package. cabal combines the
repositories of its configuration and of cabal.project, so a company
repository is asked before Hackage, and a configuration that lists
repositories without Hackage leaves Hackage out; active-repositories
(:rest, :none, :override) chooses the repositories and is searched last
to first, as cabal does. A rocks_servers list replaces LuaRocks' default:
its servers are asked in order, luarocks.org only when listed. Paket asks only
the sources paket.dependencies lists, so nuget.org is not asked when they
leave it out.
A source asked beside the public default does not decide where a package is
drawn from until something is asked: a repository's --extra-index-url no
longer marks every PyPI package ⚠ index. The repository's index is still
never queried unless vouched for; the package is asked of the indexes this
machine may query, and only one that none of them has is attributed to the
repository's index and marked. With --online the map moves each package to
the index that actually answered.
--online permits depphunter to query the trusted indexes for dependencies the
repository does not record, which is how --resolve-depth reaches the
ecosystems whose graph is held outside the repository:
| Ecosystem | asked for | answer |
|---|---|---|
| Go | <proxy>/<module>/@v/<version>.mod |
its direct (non-// indirect) require entries |
| npm | <registry>/<package>/<version>, or /latest when unpinned |
its dependencies |
| PyPI | <host>/pypi/<name>/<version>/json, or /pypi/<name>/json when unpinned; elsewhere, if that is 404, the Simple API page <index>/<name>/ and a file's PEP 658 <file>.metadata |
requires_dist (Requires-Dist), excluding extras |
| crates.io | <index>/<se>/<rd>/<name>, sparse index |
its normal deps, excluding optional ones |
| NuGet | <feed>/<id>/<version>/<id>.nuspec |
<dependencies>, both flat and by group |
| OCI | the manifest, then its config blob | the base image it was built on |
| Composer | <repository>/p2/<vendor>/<name>.json (metadata-url elsewhere) |
the version's require, excluding the platform |
| RubyGems | <server>/info/<name>, the compact index Bundler reads |
the version's runtime dependencies |
| pub | <server>/api/packages/<name>, the package API pub reads |
the version's (or latest's) dependencies |
| Hex | <api>/packages/<name>, then /releases/<version> (or latest stable); <api>/repos/<org>/packages/<name> for a private organization's package |
its requirements, excluding optional ones |
| CRAN | crandb's /<name>/<version> (or current); src/contrib/PACKAGES elsewhere |
Depends, Imports and LinkingTo, without R's base packages |
| Bioconductor | <bioconductor.org/packages>/<release>/bioc/src/contrib/PACKAGES, the release renv.lock records (Bioconductor.Version), else release (read once) |
Depends, Imports and LinkingTo; one the same PACKAGES lacks is CRAN's |
| Hackage | <server>/package/<name>/preferred, then /package/<name>-<version>/<name>.cabal (or newest) |
its libraries' build-depends, without GHC's own packages |
| Terraform modules | <modules.v1>/<namespace>/<name>/<provider>/versions (service discovery off the public registry) |
the providers and registry modules of the version asked for, or the newest its constraint allows |
| CocoaPods | <cdn>/Specs/<a>/<b>/<c>/<pod>/<version>/<pod>.podspec.json (newest: the shard's version list); a clone in ~/.cocoapods/repos instead (any host; .podspec.json or .podspec) |
its and its default subspecs' dependencies |
| SwiftPM registry | <registry>/<scope>/<name> for the releases, then <registry>/<scope>/<name>/<version>/Package.swift (the registry registries.json maps the scope to) |
its .package(id:) and .package(url:) dependencies |
| Conan | <remote>/v2/conans/<name>/<version>/<user>/<channel>/latest, then .../revisions/<revision>/files/conanfile.py; a range from <remote>/v2/conans/search?q=<name>/* |
the recipe's requires, pinned as conan.lock pins them |
| GitHub Actions | <api>/repos/<owner>/<repo>/contents/<path>/action.yml?ref=<ref> (then action.yaml; a reusable workflow's own file), raw.githubusercontent.com for github.com without a token |
a composite action's steps, a Docker action's image (or its Dockerfile's FROM), a reusable workflow's jobs |
| PowerShell Gallery | <repository>/Packages(Id='<name>',Version='<version>'), else <repository>/FindPackagesById()?id='<name>' (NuGet v2 OData; a NuGet v3 feed as NuGet's) |
its Dependencies (the manifest's RequiredModules) |
| LuaRocks | <server>/<rock>-<version>.rockspec, versions from <server>/manifest-5.1.zip (read once) |
its run-time dependencies, without lua |
| CPAN | MetaCPAN's <api>/v1/release/<AUTHOR>/<name> (as cpanfile.snapshot records) or /_search for a pinned version, else /v1/release/<distribution>; /v1/module/<module> per dependency; a mirror's 02packages first |
its run-time requirements as distributions, without perl's own modules |
| opam | <repository>/packages/<name>/<name>.<version>/opam, from opam's copy of the repository, else over HTTP; for a range, the newest version admitted, listed by the copy (opam-repository: GitHub's contents API) |
its depends, without the compiler and what only tests or documentation need |
| Julia | <registry>/<L>/<Name>/Versions.toml, then Deps.toml and Compat.toml, from a depot's copy of the registry (any host), else over HTTP (General) |
the dependencies of the pinned or newest admitted release, with their compat ranges, without julia |
| Maven (group:artifact) | <repository>/<group path>/<artifact>/<version>/<artifact>-<version>.pom and its parents, the version from maven-metadata.xml when unpinned; Clojars after Central for a Clojure project |
its compile and runtime dependencies, excluding optional ones |
| Bazel modules | <registry>/modules/<name>/<version>/MODULE.bazel, the newest version not yanked from metadata.json when unversioned (the Bazel Central Registry, or a .bazelrc --registry) |
its bazel_deps, excluding dev dependencies |
| Elm | <site>/packages/<author>/<name>/<version>/elm.json, the newest release a range admits from releases.json when unversioned (package.elm-lang.org) |
its dependencies as ranges, excluding test dependencies |
| PureScript | <owner>/registry-index/main/<shard>/<name> (a JSON manifest per line), the newest version a range admits from <owner>/registry/main/metadata/<name>.json when unversioned (the registry on raw.githubusercontent.com) |
its dependencies as ranges |
| dub | <registry>/api/packages/<name>/<version>/info, the newest release a specification admits from <registry>/api/packages/<name>/info when not exact (DUB_REGISTRY and the settings' registryUrls first, then code.dlang.org) |
its, its sub-packages' and its default configuration's dependencies, not optional or path ones |
| Alire crates | <index>/index/<first two letters>/<crate>/<crate>-<version>.toml, from alr's checkout, else over HTTP (alire-index's stable-1.4.0); for a range, the newest release admitted, listed likewise (see opam) |
its depends-on, every case(...) alternative |
| Quicklisp | the dist's distinfo (quicklisp.txt, or <dist>/<version>/distinfo.txt for a dist version), then the systems.txt it names (read once); a qlfile's or ~/quicklisp's other dists first |
the dependencies of the project's own systems, each named by its project, without ASDF, UIOP and SBCL's contribs |
| Puppet Forge | <api>/v3/releases/<slug>-<version>, the newest release a range admits from <api>/v3/modules/<slug> when not exact (forgeapi.puppet.com, a Puppetfile's forge, r10k's forge.baseurl) |
its metadata.json dependencies, by slug, with their requirements |
| Racket | <catalog>/pkg/<name>, the entry's dependencies (pkgs.racket-lang.org) |
its dependencies, base as the base collections, #:version a minimum |
| Wally | <scope>/<name> in the registry's GitHub repository (a manifest per line; config.json's fallback registries when missing), from the registry wally.toml names |
its dependencies and server-dependencies, each from the registry its manifest names |
| Buf Schema Registry | GraphService/GetGraph, then ModuleService/GetModules and OwnerService/GetOwners (Connect JSON), at the registry the module name carries |
its direct dependencies, each pinned by its commit, not another registry's |
| CUE modules | the manifest <registry>/v2/<module>/manifests/<version>, then its module file blob (registry.cue.works, or where CUE_REGISTRY routes the module) |
its deps, each at the version it names |
A container image has no dependency list. What it has is the image it was built
on, which is the source of its unpatched vulnerabilities, and that is what is
followed: the manifest — one platform's, where the manifest is a multi-platform
index — and then the small config blob it references, read for
org.opencontainers.image.base.name in the manifest's annotations or the image's
labels, for an image named in a pipeline, a Dockerfile or a Compose file alike.
No layers are downloaded. A registry requiring a pull token is given the
opportunity to say so, and the token endpoint it names is followed only over
HTTPS, or back to the registry's own host.
An identity token this machine holds for the registry
(identitytoken, as docker login stores an Azure Container Registry
refresh token, or a credential helper's <token> answer) is exchanged there
for the pull token with the OAuth2 refresh-token grant — only at a token
endpoint on the registry's own host, over HTTPS.
A private PyPI index that serves only pip's Simple API — GitLab, AWS
CodeArtifact, Azure Artifacts, Google Artifact Registry, devpi, a plain Nexus
or Artifactory repository — answers 404 at the JSON API, so its project page is
read instead (PEP 691 JSON, or PEP 503 HTML), with the release taken from the
file names: the pinned version, or the newest that is neither yanked nor a
pre-release. Its dependencies are the Requires-Dist of the metadata file the
index keeps beside a wheel or source archive (PEP 658), checked against the
page's hash. A release whose files advertise no metadata gets no answer, and
the report says so: archives are not downloaded. File URLs are taken relative
to the page, and each request carries the credential this machine holds for
its own URL — the index's for a file below the index's path, none for a file
on a host or path it has none for.
Every Maven package on the map names its
artifact (com.google.guava:guava, cheshire:cheshire), so its POM is read,
whichever plugin placed it there; only a Bazel hub target that no artifact
list names is not asked about. Clojars, where Clojure's libraries are
published, is asked after Maven Central whenever the repository has a Clojure
manifest, as Leiningen and tools.deps do without being told to (not when a
mirror of * stands in for both).
Lock files take precedence: an index is queried only where the repository is silent, and an entire level of the walk is queried at once rather than one package at a time. Answers are cached for one day under the cache directory.
Authenticated registries
An index behind authentication is read like any other, provided this machine is already configured for it. Credentials are taken from the user's own files and environment — never from the repository — and each is sent to the host it was written for and to no other.
| Source | Holds |
|---|---|
npm's user and global npmrc, npm_config_//<host>/:<field> |
_authToken, _auth, and username with _password, per registry host and path |
this machine's .yarnrc.yml files, YARN_NPM_AUTH_* |
Yarn Berry's npmAuthToken and npmAuthIdent: top level, npmScopes, npmRegistries |
| Bun's global bunfig | token, or username and password, of [install] registry and [install.scopes] |
~/.netrc (NETRC; ~/_netrc on Windows) |
the machine/login/password triples git, curl, Go (unless GOAUTH says not) and pip read |
Maven's settings.xml (~/.m2, MAVEN_HOME) |
each <server>, matched to a <mirror>, profile <repository> or user deps.edn repository |
this machine's NuGet.Config and nuget.config files |
<packageSourceCredentials> (ClearTextPassword), matched to its enabled <packageSources> entry |
NuGetPackageSourceCredentials_<source> |
Username=...;Password=... for a source those files name, over its file entry |
VSS_NUGET_EXTERNAL_FEED_ENDPOINTS |
the Azure Artifacts credential provider's endpoint passwords |
%NAME% in a repository nuget.config or Paket source |
a pipeline secret for a repository feed, only when vouched for (below) |
${NAME} in a repository .yarnrc.yml or bunfig.toml |
a pipeline secret for a repository registry, only when vouched for (below) |
${NAME} in a repository Pipfile or PDM source URL |
a pipeline secret for a repository index, only when vouched for (below) |
uv's UV_INDEX_<NAME>_USERNAME, _PASSWORD |
for the index of that name in uv.toml, UV_INDEX or UV_DEFAULT_INDEX |
Poetry's auth.toml and config.toml |
[http-basic.<name>] for its repository <name>; POETRY_HTTP_BASIC_<NAME>_* over them |
PDM's config.toml, PDM_PYPI_USERNAME/_PASSWORD |
username and password of [pypi] and [pypi.<name>], each for its URL |
~/.docker/config.json (DOCKER_CONFIG) |
stored auths and identitytokens, and the helpers of credsStore and credHelpers |
containers/auth.json, REGISTRY_AUTH_FILE |
the same, for Podman and Skopeo |
credentials.toml in CARGO_HOME (~/.cargo) |
a token per registry, matched to its index through config.toml there (legacy credentials and config too) |
CARGO_REGISTRIES_<NAME>_TOKEN, CARGO_REGISTRY_TOKEN |
the same token supplied by a pipeline instead, over the files |
[registries.<name>] token in Cargo's config.toml |
a token kept beside the index; credentials.toml and the variables win |
~/.terraformrc, ~/.tofurc, TF_CLI_CONFIG_FILE |
Terraform's and OpenTofu's credentials "<host>" tokens; a host block names a registry without one |
~/.terraform.d/credentials.tfrc.json |
the tokens terraform login stores (OpenTofu's under ~/.config/opentofu) |
TF_TOKEN_<host> |
a token supplied by a pipeline, for HCP Terraform and the hosts named above |
auth.json in Composer's home |
http-basic, bearer, gitlab-token, gitlab-oauth, github-oauth, per host |
COMPOSER_AUTH |
the same keys supplied by a pipeline, merged host by host over auth.json |
~/.bundle/config |
Bundler's BUNDLE_<HOST> credentials (user:password, or a token), per host or source URL |
BUNDLE_<HOST> |
the same credential supplied by a pipeline, over the file's |
~/.sbt/.credentials, SBT_CREDENTIALS |
sbt's host, user and password, whatever the realm; ~/.ivy2/.credentials too |
Coursier's credentials.properties, COURSIER_CREDENTIALS |
<name>.host, .username and .password; inline host(realm) user:password lines |
gradle.properties in GRADLE_USER_HOME |
<name>Username and <name>Password for an init script's maven { name = "<name>" … } |
pub-tokens.json in Dart's configuration directory |
dart pub token add tokens (or the variable an env entry names), per hosted URL |
HEX_API_KEY, hex.config (HEX_HOME, ~/.hex) |
the Hex user key, api_key or OAuth token; each hexpm:<org> auth_key; HEX_REPOS_KEY |
rebar3's hex.config (~/.config/rebar3) |
hexpm api_key, $oauth token; each hexpm:<org> api_key and repo_key |
BUF_TOKEN, the netrc entry buf registry login writes |
a Buf Schema Registry token per host (a bare BUF_TOKEN for buf.build only) |
cue's logins.json (CUE_CONFIG_DIR) |
the tokens cue login stores, per registry host, over Docker's |
r10k's r10k.yaml |
forge: authorization_token, for the paths of forge: baseurl |
the netrc entry swift package-registry login writes |
a registry's token (Bearer, as the user's registries.json says) or user and password |
Conan's credentials.json, CONAN_LOGIN_USERNAME |
a remote's user and password, for its token (below) |
GH_TOKEN, GITHUB_TOKEN, gh's hosts.yml |
a GitHub host's token, for its REST API alone (below) |
| the index URL itself | https://user:password@host/simple, as a private pip or Cargo mirror is set |
Between them these cover Nexus, Artifactory, Azure Artifacts, ProGet, GitHub
Packages, Harbor, GHCR, a private crate registry, a private Terraform
registry, Private Packagist, Satis, Repman and GitLab's Composer registry,
Gemfury and the commercial gem servers Bundler is pointed at, private pub
servers, Hex organizations, a private Puppet Forge, Buf Schema Registry, CUE
registry or Swift package registry. What SwiftPM keeps in the macOS keychain
(its default there) is not read: log in with --disable-keychain to keep the
credential in the netrc.
A Conan remote is asked without a login first, as Conan asks it. When it
answers 401, the login Conan would use for it — the entry of the home's
credentials.json for the remote's name (a Jinja2 template's
os.getenv("NAME") substituted), else CONAN_LOGIN_USERNAME_<REMOTE> and
CONAN_PASSWORD_<REMOTE> (the name upper case, - as _), else
CONAN_LOGIN_USERNAME and CONAN_PASSWORD — goes as Basic credentials to the
remote's /v2/users/authenticate alone, over https or on this machine, and
the token it answers with is sent as a Bearer token from then on. Conan's
auth_remote.py plugin is a program and is not run (--explain notes it,
helper-not-run, when a remote wants a login this machine does not hold), and
the tokens conan remote login keeps in the home's .conan.db database are
not read.
A GitHub REST API gets the token the GitHub CLI would send it, and no other
host does: github.com's API GH_TOKEN, else GITHUB_TOKEN, else the
oauth_token of github.com in hosts.yml; an Enterprise Server's
GH_ENTERPRISE_TOKEN, else GITHUB_ENTERPRISE_TOKEN, else its own entry of
hosts.yml — on a runner of that instance (GITHUB_API_URL) its
GITHUB_TOKEN, which then does not go to github.com — sent to its /api/v3/
paths alone. Without a token for api.github.com, a github.com action's files
are read from raw.githubusercontent.com, which serves public repositories
without the API's limit of 60 requests an hour (--explain notes the limit,
forbidden, when the API refuses). A token gh keeps in the system's keyring,
its default, is not read, and gh auth token is not run. PowerShell
repositories are sent the credentials this machine holds for their hosts; the
SecretManagement vault entries a registration names are not read.
Each file is looked for where its tool looks for it; see Configuration locations.
A Cargo token is sent as Cargo sends it: the whole Authorization header,
exactly as written. A registry that wants a scheme has the token written with
it (Artifactory's Bearer <token>); crates.io and most others take it bare.
Each named registry's token goes to its own index path only, so registries
sharing a host keep their own tokens, and it is sent whether or not the
registry's config.json says auth-required. crates.io's token goes to
crates.io alone: never to its index, which Cargo reads anonymously, nor to a
registry replacing it. A registry whose credential-provider (or, without
one, registry.global-credential-providers) leaves out cargo:token keeps
its credential in a keychain or a program, which depphunter does not run: its
plaintext token is skipped.
Bazel's credential helpers (--credential_helper=[<scope>=]<helper> in a
.bazelrc or ~/.bazelrc) are programs, and are not run either. A registry
a helper covers is asked with the netrc's credential for its host, if any, and
--explain says so (helper-not-run).
A pub token is the one dart pub token add stored in pub-tokens.json, in
Dart's configuration directory: the entry's token, or the variable its env
names, as a pipeline sets it. It is sent as a Bearer token to the URLs under
the hosted URL it was added for, as pub sends it — the whole host for a server
at its root, only its path for one under a path — and nowhere else.
A private Hex organization's package is asked of the organization's part of
the Hex API, <api>/repos/<org>/packages/<name>, and never of hex.pm's public
packages, where a package of the same name would be someone else's. The key
sent there is Hex's own: HEX_API_KEY, else the api_key of hex.config,
else the OAuth token mix hex.user auth stored while it has not expired — a
user's, which reaches every organization the user belongs to; without one,
the auth_key mix hex.organization auth <org> --key KEY stored for
hexpm:<org>, for that organization alone, and HEX_REPOS_KEY for the
others. A key goes out as the whole Authorization header and a token as a
Bearer token, only under <api>/repos/; public packages are asked without
either. Without a key for the organization nothing is asked, and the
resolution report says the key is missing. An organization key generated
without --key carries only the repository:<org> permission, which hex.pm's
API refuses: use a user key, or one generated with --permission api:read.
The requirements of an organization's package are asked of the organization
too, since the API does not say which repository each is in; mix.lock,
which does, answers first. A repository with a URL of its own (a mini_repo,
HEX_MIRROR) serves Hex's protobuf registry, not the API, and is not asked;
nor are the encrypted keys of older Hex versions read.
rebar3 records no repository for a package — not in rebar.config's deps,
not in rebar.lock — and asks the repositories its configuration names in
order: {hex, [{repos, [#{name => <<"hexpm:acme">>}]}]} in the project's
rebar.config, then in the global ~/.config/rebar3/rebar.config, then
hex.pm's public packages, unless the first entry is {repos, replace, [...]}. A rebar3 project's Hex packages are asked in that same order, each
repository only when the ones before it lack the package: one found in an
organization is never named to the public side, while one no organization has
is looked for on hex.pm, as rebar3 would fetch it from there. The question
stops at an organization this machine has no key for (the report says so) and
at a repository with no API. Keys also come from rebar3's
~/.config/rebar3/hex.config: a repository's own api_key first, hexpm's
api_key or the $oauth token of rebar3 hex user auth as the user's
(after Mix's), and each hexpm:<org> repo_key that rebar3 hex organization auth stores for its organization alone.
In ~/.npmrc, settings.xml and NuGet.Config, a value that is exactly
${NAME}, ${env.NAME} or %NAME% is read from the environment, so a password
may be held there. An encrypted password — Maven's {...} form,
NuGet's Windows-encrypted form — is left untouched: it cannot be decrypted here,
and transmitting the ciphertext would yield only a 401.
A credential written into an index URL is removed from that URL before it is recorded. The index a package resolves from is drawn on the map, named in the side panel and written into every export, and the HTML export is a file this document recommends sharing. Only a URL supplied by this machine's own configuration contributes a credential; one supplied by the repository is stripped and discarded, since a repository able to supply a credential would also be choosing where it is sent.
A pipeline usually keeps the repository's nuget.config or
paket.dependencies naming the company feed and supplies the secret in a
variable: a password: "%NAME%" on a Paket source line, a
ClearTextPassword of %NAME%, or a NuGetPackageSourceCredentials_<source>
variable for a source only the repository defines. The secret is this
machine's but the address is the repository's, so it is sent only to a feed on
the host of a NuGet source this machine configures, or one vouched for with
--trust-index; a password written out in
the repository is discarded. A NuGet credential for pkgs.dev.azure.com, which
every Azure DevOps organization shares, serves only its organization's path.
NuGet's Windows-encrypted Password and Paket's encrypted credential store
(paket config add-credentials) are not read.
The same holds for Yarn and Bun: a repository .yarnrc.yml whose
npmAuthToken or npmAuthIdent is exactly ${NAME} (or ${NAME:-x} with
NAME set), and a repository bunfig.toml whose token or password is
exactly $NAME or ${NAME}, get the variable's value only for a registry on
the host of one this machine's npm, Yarn or Bun configuration names, or one
vouched for with --trust-index. A token or password written out, one only a
fallback fills, and a user:password in a repository registry URL are
discarded; a repository registry URL made of a variable is not recorded.
Python's tools name a credential after its index. uv takes
UV_INDEX_<NAME>_USERNAME and UV_INDEX_<NAME>_PASSWORD (the name
upper-cased, anything but a letter or digit as _), Poetry the
[http-basic.<name>] of its auth.toml or config.toml and
POETRY_HTTP_BASIC_<NAME>_*, PDM the username and password beside each
[pypi.<name>] url. For an index this machine's own configuration names, the
credential goes to that index's path — its URL without /simple (devpi's
/+simple), so the JSON API, the files and their metadata below it are
covered — over netrc's. For an index of that name only the
repository defines, and for a $NAME or ${NAME} user name or password in a
repository Pipfile or PDM source URL, the secret is lent as for Yarn and
Bun: only to an index vouched for with --trust-index or on the host of one
of this machine's PyPI indexes, Poetry repositories or explicit uv indexes. A
password written into a repository index URL is discarded, and one Poetry
keeps in the system keyring is not read.
In this machine's own Yarn Berry configuration an npmRegistries entry's
credential goes to its registry, a scope's to the scope's npmRegistryServer,
and the top-level one (or YARN_NPM_AUTH_TOKEN, YARN_NPM_AUTH_IDENT) to the
default registry — the file's npmRegistryServer or YARN_NPM_REGISTRY_SERVER,
else registry.yarnpkg.com and registry.npmjs.org. npmAuthToken is sent as a
Bearer token and npmAuthIdent (user:password) as Basic credentials; a
${VAR} naming an unset variable without a fallback is dropped, as Yarn
refuses it. What the npmrc holds for a registry wins over Yarn's and Bun's.
A Yarn credential goes only with the requests Yarn sends it with: a scope's
with the scope's packages, a registry's with every package under
npmAlwaysAuth: true (or YARN_NPM_ALWAYS_AUTH) and otherwise with the
scoped packages only, so without it an unscoped package is asked
anonymously, as Yarn asks. YARN_NPM_SCOPES is not read: Yarn refuses a map
from the environment.
Files above the analyzed directory. Yarn and NuGet also read their files
(.yarnrc.yml, or the name YARN_RC_FILENAME gives it; nuget.config) in
every directory above a project, and so does depphunter above the directory it
analyzes. Up to the top of that directory's checkout (the nearest directory
above it holding a .git) they are the repository's, like the files the scan
finds: untrusted, and given no credential of this machine's. Above the
checkout they are this machine's configuration — the ~/work/.yarnrc.yml
that points every project at the company registry — trusted, with their
credentials read. Yarn's are merged key by key, the closest winning, the
home's .yarnrc.yml last.
An npm credential serves its registry's path. A key such as
//gitlab.corp/api/v4/projects/1/packages/npm/:_authToken is sent only
under that path, the longest matching key winning, so two
projects' registries on one host each keep their own token and a Markdown link
elsewhere on the host carries none; //host/:_authToken serves the whole
host. Yarn's and Bun's credentials for a registry with a path are limited the
same way.
Composer's home is the one Composer itself uses: COMPOSER_HOME when set, else
$XDG_CONFIG_HOME/composer (~/.config/composer) or ~/.composer, whichever
exists first (%APPDATA%\Composer on Windows). The same keys under the
"config" of its config.json are read too, auth.json over them and
COMPOSER_AUTH over both. An http-basic entry, and a gitlab-token with a
username (a deploy or job token), is sent as Basic credentials; bearer,
gitlab-oauth, github-oauth and a bare gitlab-token as a Bearer token, a
github.com token to api.github.com as well. For a host named under several
of these, the one Composer loads last wins, bearer over http-basic.
bitbucket-oauth is not read: it is an OAuth consumer key and secret that
Composer exchanges for a token on every run. The auth.json beside a
repository's composer.json, and the "config" of a composer.json, belong to
the repository and are never read.
Bundler names a credential after its server: bundle config set --global gems.example.com user:password writes BUNDLE_GEMS__EXAMPLE__COM, the host
upper-cased with . as __ and - as ___, and a pipeline sets the same
variable. The user's config is BUNDLE_USER_CONFIG, else config in
BUNDLE_USER_HOME, else ~/.bundle/config; a variable replaces the file's
entry. A key may also be a source URL
(BUNDLE_HTTPS://RUBYGEMS__PKG__GITHUB__COM/ACME/), filed under its host; a
host key wins over it. The value goes out as Basic credentials, both halves
URL-unescaped as Bundler does; a bare token is the user name with an empty
password. The application's .bundle/config, beside the Gemfile or wherever
BUNDLE_APP_CONFIG points, belongs to the repository and is never read.
The JVM build tools share the Maven ecosystem. Maven's settings contribute the
repositories of the profiles <activeProfiles> lists, or, when it lists none,
of those with <activeByDefault>true</activeByDefault>; a profile activated by
a property, the OS, the JDK or a file is not read, since that depends on the
build, and <pluginRepositories> serve Maven's plugins. A <server> goes to
the host of the mirror or repository with its id, in either settings file or
in the Clojure CLI's user deps.edn, which tools.deps matches by repository
name. ~/.sbt/repositories is asked beside Central unless
-Dsbt.override.build.repos=true is in SBT_OPTS or JAVA_OPTS: then its
list is all that is asked, in order (a project's .sbtopts is the repository's
and is not read). COURSIER_REPOSITORIES replaces Coursier's defaults in the
same way; ivy: and local entries are skipped. A replacement one tool
configures applies to every Maven package. Gradle's PasswordCredentials
convention is followed for the init scripts in its user home only: a
repository a project's build script declares is not given a credential, since
the repository would then choose where it is sent. Leiningen's
credentials.clj.gpg is not decrypted, and Maven's settings-security.xml is
not used to decrypt a {...} password.
Most container registries no longer store a credential in config.json; they
name a helper instead, and depphunter runs it as docker login does —
docker-credential-<name> get, with the registry on standard input. This is the
only program depphunter executes that was not named on its command line, so the
helper's name must be a bare name, is resolved on PATH only, and is given ten
seconds; a configuration naming a path obtains nothing. A helper that returns an
identity token rather than a password is not used, since only registries accept
one.
Private and internal dependencies
Most of an enterprise repository is the organization's own code, and two of the operations depphunter performs for a public package must not be performed for such packages:
- querying a public index for its dependencies, which does not answer and discloses to proxy.golang.org, or registry.npmjs.org, that the package exists; and
- querying the OSV database about it, which discloses the name and version of internal code to a third party.
Neither case can be inferred — a module path on a company host is indistinguishable from any other — so the packages are declared:
depphunter --private 'corp.example/*' --private 'npm:@acme/*' .
A pattern is a glob with GOPRIVATE's semantics: it matches a package whose
leading path elements match it, so that corp.example/* covers
corp.example/team/billing. It applies equally to an npm scope (@acme/*), a
Maven group (com.acme.*, which covers the artifact com.acme.billing:api;
maven:com.acme:lib names one artifact) and a registry path
(harbor.corp/*). Prefixing a
pattern with an ecosystem — npm:, go:, maven:, nuget:, oci:, pypi:,
crates:, actions:, gitlab-ci:, psgallery:, c-external:, vcpkg:,
conan:, composer:, rubygems:, swiftpm:, pub:, hex:, cran:,
bioconductor:, hackage:, terraform-module:, terraform-provider:,
buf:, cmake-fetch:, pkg-config:, cocoapods:, carthage:, luarocks:,
wally:, cpan:, opam:, julia:, zig:, bazel:, bazel-repo:, nix:,
nixpkgs:, elm:, purescript:, shards:, paket:, dub:, fpm:,
fortran-external:, haxelib:, alire:, raco:, quicklisp:,
soldeer:, git-submodule:, nimble:, jsonnet-bundler:, cue:,
dhall: or puppet-forge: — restricts it to that ecosystem.
GOPRIVATE, GONOPROXY and GONOSUMDB are read in addition to whatever is
configured here — from the environment, or else from the file go env -w
writes (GOENV, by default go/env in the user configuration directory), as
the go command reads them — so a Go project whose machine is already
configured requires no further setting.
A package matched in this way is drawn with a private label, is never named
to that ecosystem's public index, and is never sent to the vulnerability
database — neither its name and version nor, when it is a git dependency, its
commit (a pattern matching its repository, github.com/acme/*, keeps the
commit back too). It is still queried against an index this machine
configures, since an internal registry already knows of it, so a private
registry continues to
answer for what its packages depend on — whether it replaces the public index
or is asked beside it (an extra pip index, a NuGet feed). A private package that
such an index does not have is not looked for on the public one, and one whose
only other source is the repository's own index is drawn from that index,
marked. private may also be set in a
repository's own .depphunter.yaml; the only effect available to it is to make
depphunter disclose less, and the repository is the authority on which of its
dependencies are internal. A Python package installed from a directory, an
archive or a version-control URL is treated as private without any pattern (see
Languages).
Vouching for an internal index
An index that appears only in the repository is marked ⚠ index and never
queried, because a repository directing a package manager at an index that
nothing here configures is the form dependency confusion takes. In an
organization whose repositories carry their own .npmrc naming the company
registry, this marks every package, and a warning that is always present conveys
nothing.
--trust-index identifies an index as the organization's own:
depphunter --trust-index https://nexus.corp/repository/npm-group .
It may be set only in the user's own configuration file, the environment or on the command line. A repository cannot vouch for itself; were that permitted, the marking would guard nothing.
| Setting | Flag | Environment |
|---|---|---|
private |
--private |
DEPPHUNTER_PRIVATE |
trust_indexes |
--trust-index |
DEPPHUNTER_TRUST_INDEXES |
Both are repeatable. A --private value may itself be a comma-separated list,
and both environment variables take one; a --trust-index value is a single
URL.
The resolution report
None of the preceding resolution is visible in the map it produces. A package resolving from an internal Nexus and one resolving from registry.npmjs.org are drawn identically, and a dependency tree that terminates two levels down is indistinguishable whether the dependencies end there, no lock file covers them, or a proxy returned 404. The resolution report records the difference.
depphunter --resolve-depth 2 --online --explain .
It is a single account of one analysis, available in three forms:
| Form | Intended use |
|---|---|
--explain, written to the log |
a digest to read while the run is still in the terminal |
GET /api/resolution |
the complete report as JSON, for comparison between runs or assertions in CI |
?format=md, ?format=text |
the same report rendered; the Markdown form is what the editor opens |
The report records six things:
- the indexes known to the run — each index's URL, the scope it serves, and
whether it was learned from this machine, from the repository, from the
repository with
--trust-indexsubsequently vouching for it, from a container image reference, or is the ecosystem's public default; - what resolved from where — one row per index, giving the number of packages resolving from it and how many of those are private, so that an unexpected index is a single row rather than a search through the map; packages installed from outside any index form a row of their own;
- the walk — per ecosystem and per level: how many packages were queried, how many answered, how many were new, and the elapsed time; the answers line also counts the questions answered from what a Python environment has installed;
- the ecosystems not walked at all — when
--onlineis not given, an ecosystem whose dependency graph is held outside the repository (Go modules, NuGet, Maven, container images) is named, rather than silently contributing nothing; - what the resolvers noticed — notes a plugin or the index client adds
once per file or registry, each with a code: a
bun.lockbnothing reads (lock-unread); a lock that pins versions but records no edges, so that offline the walk adds nothing past it —rebar.lockwithout amix.lock, a Conan 2conan.lock,Package.resolvedwithout.build/checkouts,pubspec.lock, an opam lock withoutdune.lock,luarocks.lock(lock-flat); a lock the scan left out as git-ignored that was read from disk, so it reflects this checkout's last install (lock-ignored); a Hex organization with no key on this machine (no-key) or whose API refused the key (forbidden, an organization key withoutapi:read), or a GitHub API that refused to answer (forbidden, its limit without a token); and a NuGet package nopackageSourceMappingpattern covers, which NuGet itself would not restore (unmapped); a CPAN release MetaCPAN does not describe (no-release); an opam repository, Alire index, Julia registry or CocoaPods spec repository only git serves, with no copy on this machine (no-copy); and a Bazel registry a.bazelrcnames a credential helper for, or a Conan remote that wants a login only Conan'sauth_remote.pyplugin could supply, which is not run (helper-not-run); a Python package asked of several indexes while uv or PDM is configured to merge their versions (merged); apackage-lock.jsonoryarn.lockgit dependency locked to a branch or tag, not a commit (git-unpinned); a directoryPYTHONPATHor a Python tool's settings add to the import roots (import-root); and - the questions nothing answered — every package for which no answer was obtained, with the reason: no lock file covers it; its index is named only by the repository; it is private and its index is the public one; the proxy requires a version it was not given; depphunter asks no index for this ecosystem, or cannot ask one because an import names no artifact; the package was installed from outside any index; or a request was made and returned a given status.
The last of these is the principal reason for the report. These reasons are indistinguishable on the map, each drawing a package with nothing beneath it, yet they mean entirely different things. A 404, or a 401 from a feed whose credentials are wrong, is a configuration fault that the map can express only as an absence.
the walk past what the code imports
PLUGIN LEVEL ASKED ANSWERED ADDED EDGES TIME
go 0 25 13 6 27 120ms
javascript 0 7 3 9 10 3346ms
answers
87 asked; 3 from lock files; 56 from indexes (56 fetched, 0 cached, 0
already asked); 28 unanswered (4 of them asked and failed); 89 requests; 159
external packages on the map (87 transitive, 0 private, 0 from an index
nothing here vouches for)
what the resolvers noticed
beam erl/rebar.lock (lock-flat): rebar.lock pins versions but records no
edges, only a depth: offline, --resolve-depth adds nothing past the packages
it pins (--online asks Hex)
nothing answered for these
COUNT WHY
15 depphunter asks no index for this ecosystem
8 this ecosystem's index cannot be asked (an import names no artifact)
1 https://registry.npmjs.org/@scope%2ftool/1.2.0: 404 Not Found
The JSON retains what the written report abbreviates: every question, up to 20 000, ordered by level, ecosystem and package, and for each one the URLs requested, in the order made, with the status returned by each — which is how the three round trips a container image requires can be distinguished when one of them fails.
In the editor, depphunter: Show the Resolution Report opens the same report
as a document beside the code, and the depphunter.explain setting writes the
digest to the depphunter output channel whenever the map is built. Under
--watch, the digest follows a re-analysis only where the map actually changed;
a report after every saved file would obscure the one belonging to the change
under examination.
CI pipelines
The code that executes with a repository's secrets is also a dependency, and it
is declared nowhere a package manager reads. depphunter takes it from the
pipeline files themselves — .github/workflows/*.yml and *.yaml,
action.yml/action.yaml, .gitlab-ci.yml, *.gitlab-ci.yml and any YAML
file under .gitlab/ — and places it on the map
alongside the packages:
- GitHub Actions: each step's
uses:, reusable workflows (jobs.<id>.uses), and a composite action's own steps. A./pathresolves to theaction.ymlor workflow inside this repository, so a local action's own dependencies chain on. With--online, an action or reusable workflow of another repository is read at the reference the workflow names (see Package indexes): a composite action's steps, a Docker action's image and a reusable workflow's jobs are its dependencies. A repository used at several paths (actions/cache/save,actions/cache/restore) is one package, read at the first path met. - GitLab CI: every
include:form -local,project(withrefandfile),template,remoteandcomponent- plus the includes a bridge job triggers. - Container images:
container:,services:,docker://…and a Docker action'sruns.imageon GitHub;image:,services:anddefault:on GitLab, per job and pipeline-wide.
Jobs become the symbols of their file, so a pipeline expands into its jobs the way a source file expands into its functions.
Pinning is stricter here than in a package ecosystem, because a reference that
can be rewritten is not a pin: only a commit or a digest qualifies.
actions/checkout@v4 floats, since the tag may be moved to other code, and so
does nginx:1.25.3, since an image tag may be republished at its owner's
discretion. The hardening convention of pinning to a commit and recording the
version in a trailing comment is read as both: actions/setup-go@3041bf5… # v5.0.1 reports the commit as the version and v5.0.1 as what was requested. A
GitLab template or a remote include names no version at all and may still change
beneath the repository, so it is likewise treated as floating.
Container builds
A Dockerfile names the images an application is built on, and a Compose file
names the images it runs beside; both are dependencies no package manifest
records. depphunter reads Dockerfile and Containerfile under any casing,
the variants named after them (Dockerfile.dev, api.Dockerfile), and the
Compose files compose.yaml, compose.*.yaml and docker-compose*.yml, in
either .yml or .yaml:
- Dockerfile: the image of every
FROM,COPY --from=andRUN --mount=…,from=, and the frontend a# syntax=directive names. A reference to an earlier stage, by name or index, is part of the build and not a dependency, nor isscratch. Named stages become the file's symbols. - Compose: a service's
image:, unless the service has abuild:, in which caseimage:is only the tag of the result and the service points at the Dockerfile inside the repository that builds it, so that file's own base images chain on. Images of an inline Dockerfile anddocker-image://build contexts are included; services become the file's symbols. A service thatextends:another takes itsimage:andbuild:(abuild:from another file keeps that file's directory), and the top-levelinclude:(short and long form) and anextends:file:are edges to those files. An included file not named as a Compose file is read for the including one. Remote includes and files outside the repository are not followed.
Build arguments are expanded from their defaults, so ARG BASE=node:20 and
FROM ${BASE} name node:20, and Compose's ${VAR:-default} likewise.
Compose values also come from the .env file beside the Compose file (the
included file's own for an included file), read from disk since it is often
not committed. Its values only complete references: they are never shown, and
a name that holds a credential (*PASSWORD*, *TOKEN*, *SECRET*, *_KEY
and the like) is not read at all. A value only --build-arg or the
environment supplies is not in the repository and is not guessed: an image
whose name depends on one is shown as unresolved, under the reference as
written, and one whose tag depends on one keeps the tag as written.
The images join those of the CI pipelines in one Container images island,
with the same rule that only a digest pins, the same oci: private patterns
and the same base-image lookup under --online. Docker Hub's long names
(docker.io/library/nginx) are shortened to the name used everywhere else
(nginx), so one image is one building however it is written.
Infrastructure as code
A Terraform or OpenTofu configuration installs modules and providers that
execute with the credentials of the cloud it manages, and none of them appears
in a package manifest. depphunter reads .tf and .tofu files (and their
.tf.json form), .tfvars files, .terraform.lock.hcl and Terragrunt's
.hcl files. A module is a directory, and its files are read together:
- Modules: a
moduleblock'ssource. A local path (./modules/vpc) is an edge to that directory; a registry address (terraform-aws-modules/vpc/aws, with a host for a private registry and//subdirfor a submodule) and anything fetched from git, a web server or a bucket (git::https://…?ref=v1.2.0,github.com/org/repo//sub) join the Terraform modules island, the latter named by the normalized URL. - Providers:
required_providersentries,providerblocks and, where a module declares nothing, the provider a resource type implies (aws_instanceuseshashicorp/aws), in the Terraform providers island. The lock file pins them for its module and the modules it calls.registry.terraform.io/andregistry.opentofu.org/are dropped from names, since both registries serve the same namespaces. - Inside a module:
var.x,local.x,module.x,data.t.nandaws_instance.weblink the referring file to the file declaring them, andfile()andtemplatefile()with a literal path link to the file read. - Terragrunt: the
terraformblock'ssource(with the locals of an included file substituted, as in Gruntwork's_envcommonlayout),dependencyanddependenciespaths, and whatfind_in_parent_folders()finds.
Resources, data sources, modules, variables, outputs, locals and provider
configurations become the file's symbols. Only a commit pins a git module; a
registry module is pinned by an exact version, else by the version
terraform init installed for the call (.terraform/modules/modules.json,
read from disk), whose entries --resolve-depth also follows to the
installed module's own calls. Nothing is evaluated, so a source or version
computed from variables is not followed.
Jsonnet and CUE
Jsonnet and CUE generate the configuration that Kubernetes, Grafana and Prometheus then run, and their libraries come from git repositories and module registries that no other manifest lists.
Jsonnet libraries are installed by jsonnet-bundler (jb) into
vendor/; the jsonnet-bundler packages island names each dependency by
its repository and subdirectory
(github.com/grafana/jsonnet-libs/ksonnet-util). depphunter reads
.jsonnet and .libsonnet files, jsonnetfile.json and
jsonnetfile.lock.json; what jb install wrote into vendor/ beside a
jsonnetfile.json is not read as source:
- Imports:
import,importstrandimportbinresolve as jsonnet finds the file: relative to the importing file, then in thevendor/andlib/directories of the jsonnet-bundler projects above it (nearest first), thenJSONNET_PATH, then under the importer's ancestors (a tool run with-Jat a parent directory). A file found invendor/belongs to the dependency that installed it, by its full path (github.com/org/repo/subdir/...) or its legacy link (vendor/<name>: thenamefield, else the subdirectory's last element); with nothing installed,jsonnetfile.jsonnames the package the same way, and a local source leads to its files. - Manifests: every dependency of
jsonnetfile.jsonand entry ofjsonnetfile.lock.jsonis an import of its package. The lock pins (the declared version shown as requested); without it a commit pins, a tag is shown, neither pinned nor floating, and a branch floats. A nested project without a lock (kube-prometheus's library) is pinned by the lock that installs it.--resolve-depthfollows thejsonnetfile.jsonof installed packages.
The file's leading locals and the fields of the object it returns are
the symbols; imports in comments, strings and ||| text blocks are not
read.
CUE packages are directories of a module (cue.mod/module.cue). The
CUE modules island holds the dependencies module.cue declares
(deps: "github.com/x/y@v0": v: "v0.3.1", named without the major
version and pinned by v, since the modules system selects exact
versions), the modules vendored the old way into cue.mod/pkg, and what
cue get generated from other sources; CUE's builtin packages (strings,
encoding/json, tool/exec) form a hidden CUE standard library. An
import of the module's own path links to every file of that package in its
directory. What cue get go generated into cue.mod/gen (and
cue.mod/usr augments) is linked to the Go module go.mod requires
for it — the same node the Go plugin draws — to the Go standard library,
or to the module's own Go package directory. --resolve-depth follows a
dependency's own module.cue in cue's module cache on this machine
($CUE_CACHE_DIR, else cue in the user's cache directory), at the versions
the repository's module.cue selects. Files under cue.mod/pkg,
cue.mod/gen and cue.mod/usr are not read as source. The package clause,
top-level definitions (#Name) and fields are the symbols.
OSV has no Jsonnet or CUE ecosystem, so neither kind of package is asked about
by name and version; a jsonnet-bundler package locked to a commit of a
repository on a public forge is asked about by that commit. jsonnet-bundler has
no registry; with --online a CUE module's own dependencies are read from its
registry (registry.cue.works, or the one CUE_REGISTRY routes it to).
Dhall, Puppet and Rego
Three more configuration languages: Dhall programs import files and URLs, Puppet manifests name classes of modules from the Forge, and Rego policies import each other's packages.
Dhall (.dhall): every import is an edge — a relative path to its file
(as Text, as Location and as Bytes too, and every branch of an
alternative a ? b), a URL to a package of the Dhall packages island.
The Prelude is github.com/dhall-lang/dhall-lang/Prelude, whether it comes
from prelude.dhall-lang.org or dhall-lang's repository; a file GitHub,
GitLab or jsDelivr serve raw belongs to its repository, with the reference
as its version; any other URL is named by its host and its path up to a
version segment (example.com/dhall for .../dhall/v1.2.0/util.dhall) or
its directory. A sha256: hash pins the import, since Dhall refuses
content that does not match it; without one a URL naming no version or a
branch floats. Absolute and home-relative paths and env: imports name
nothing in the repository and are dropped. The bindings of a file's
leading let chain and the fields of the record it returns are its
symbols. spago's spago.dhall, packages.dhall and test.dhall stay with
the PureScript plugin, which reads them with the same Dhall reader.
Puppet (.pp, Puppetfile, a module's metadata.json and
.fixtures.yml): include, require, contain, class { 'x': },
inherits, Class['x'], declarations, references and collectors of
defined and custom types, namespaced function calls, namespaced data
types (Stdlib::Port), template(), epp(), file() and
puppet:///modules/ sources name a module by their first segment.
- A module of the repository (
site-modules/,site/,dist/, or the repository itself, named by itsmetadata.json) links to the file Puppet's autoloader reads:profile::webtosite-modules/profile/manifests/web.pp, a function tofunctions/orlib/puppet/functions/, a type alias totypes/, a template totemplates/. - Another module is what the module's own
metadata.jsonand.fixtures.yml, else the Puppetfiles above the file, declare: a Forge module by its slug (puppetlabs-stdlib), a git module by its repository. A module r10k installed intomodules/beside the Puppetfile is named by its installedmetadata.json, and--resolve-depthfollows that file'sdependencies;modules/andspec/fixtures/modulesare not read as source. - Puppet's own resource types (
file,package,service,exec, ...), data types and functions are no dependency; stdlib's unnamespaced functions (merge(),pick()) and well-known types (file_line) are stdlib's. Comments, strings, heredocs and regular expressions are not read, and a Free Pascal.ppunit yields nothing.
Classes, defined types, nodes, functions, type aliases and Bolt plans are the symbols.
Rego (.rego): import data.a.b links to every file declaring
package a.b — or, for data.a.b.rule, the longest package the path
starts with — and so do references to data.a.b... in rules, directly or
through an imported name, unless an import already links that package.
import input, import rego.v1 and import future.keywords are built
in; data no policy declares (JSON documents, bundles) is dropped, since
OPA has no package manager. The package, rules and functions are the
symbols.
OSV has no Dhall or Puppet Forge ecosystem and Dhall has no registry; only a
Dhall import URL or a git-fetched Puppet module naming a full commit of a
repository on a public forge is asked about, by that commit. With --online
the Forge (or the one a Puppetfile's forge line or r10k's forge.baseurl
names) is asked for a module's dependencies.
Shaders and GPU code
GPU code comes in two families. CUDA, OpenCL C and Metal are dialects of C and C++, and the C/C++ plugin reads them; GLSL, HLSL and WGSL are shading languages of their own, which a shader plugin reads.
CUDA (.cu, .cuh) and Metal (.metal) are read as C++, OpenCL
C kernels (.clh, and .cl files whose head shows a directive or a
kernel; any other .cl file is Common Lisp's) as C, with the includes and
include path of C and C++: a .cu file links to its .cuh and .h
headers, a Metal shader to the header it shares with the app. Kernels
(__global__, __kernel, kernel, vertex, fragment) and device
functions are function symbols; __launch_bounds__(N) is stepped over and
a launch (k<<<grid, block>>>()) is a call. The toolkits' headers are
standard-library islands, tried after the project's own files (a
repository vendoring Thrust or CUB links to its copy): the CUDA
Toolkit (cuda_runtime.h, cuda_fp16.h, the cuBLAS, cuFFT, cuRAND,
cuDNN, NPP and NVTX headers, each as written; thrust/, cub/, cuda/
(libcu++), nv/, nvtx3/, cooperative_groups/ and crt/ by directory),
the OpenCL headers (CL/cl.h, CL/opencl.hpp; not SYCL's
CL/sycl.hpp), and the Apple SDKs, where <metal_stdlib> is Metal's,
<simd/simd.h> simd's and <OpenCL/opencl.h> the OpenCL framework's —
the same nodes a Swift import Metal reaches.
GLSL (.glsl, .vert, .frag, .geom, .tesc, .tese, .comp,
the ray tracing stages .rgen, .rchit, .rahit, .rmiss, .rint,
.rcall, .vsh, .fsh, and .fs, .vs, .gs, .mesh and .task
when their head shows GLSL) and HLSL (.hlsl, .hlsli, .fx, .fxh,
and Unreal's .usf and .ush): #include is read by the C
preprocessor's scanner (comments, #if 0) and resolved as a C header of
the project is - beside the includer, a compilation database's include
paths, include/, src/, a unique file ending in the path — then against
the includer's parent directories, then ignoring case (HLSL is mostly
written on Windows). Unreal's virtual paths link to the engine's or the
plugin's Shaders/ directory when the repository has it (/Plugin/MyFX/x
to Plugins/MyFX/Shaders/x), else to the Unreal Engine shaders
island (Engine, or the plugin's name); a GL_ARB named string (/x.glsl)
to the file from the root. Whatever else a shader includes is handed to
the compiler by the application and dropped. Functions (entry points with
their semantics and attributes), structs, GLSL interface blocks (uniform Camera { ... }), cbuffers, techniques and HLSL namespaces are the
symbols.
WGSL (.wgsl, and WESL's .wesl) has no include; Bevy's naga_oil
adds #import a::b::item (with {...} lists over several lines, as
and quoted files) and #define_import_path a::b, WESL import package::a::item;, super:: and package names. A module path links to
the file declaring it with #define_import_path, to the file its path
names below the package root (package::render::view to
src/render/view.wesl) or the importer's directory (super::), and a
quoted file to the file beside the importer or below assets/ (Bevy's
asset path). A path whose first segment is a crate the Cargo manifests
declare links to that crate: a crate of the workspace to the module's file
below its src/, a crates.io dependency to its crates.io package — and
Bevy's crates (bevy_pbr, bevy_render, ...), which ship the shaders a
game imports, to themselves when the game declares bevy, pinned by
Cargo.lock or floating on bevy's requirement. fn, struct, var,
const, override and alias declarations are the symbols.
Preprocessor and naga_oil conditions other than #if 0 are not evaluated
(every branch counts), and engine virtual paths other than Unreal's are
not known. glsl_analyzer, shader-language-server and wgsl-analyzer serve
--lsp; a sniffed .fs shader still goes to the F# server and an OpenCL
.cl kernel to the Common Lisp one (servers are chosen by extension).
Nix
A Nix project states how it is built, what it builds with and which
revision of nixpkgs everything comes from. depphunter reads .nix files,
flakes (flake.nix and flake.lock) and the pins of niv
(nix/sources.json) and npins (npins/sources.json), without evaluating
anything:
- Files:
import ./x.nix,callPackage ./x { }and a NixOS module'simports = [ ./a.nix ./b ]are edges to the file, a directory meaning itsdefault.nix; any other relative path (builtins.readFile ./VERSION,src = ./src,"${./script.sh}") is an edge to that file or directory. Text inside strings is not read, interpolations are. - Flake inputs: each input of
flake.nixis a package of the Nix flakes and sources island named by its URL:github:NixOS/nixpkgs/nixos-24.05isgithub.com/nixos/nixpkgs(GitHub names in lower case),gitlab:,sourcehut:,git+https://…and archives by their repository, FlakeHub byflakehub.com/f/owner/repo.flake.lockpins them at the locked commit (shown shortened,ad57eef), with the branch or tag asked for as the requested version; its nodes' own inputs,followsresolved, are what--resolve-depthfollows. Apath:input is an edge to that flake, afollowsthe followed input, andinputs.xin a module the flake's input. - Registry names and channels:
<nixpkgs>,flake:nixpkgsand anoutputsargument no input declares name the registry alias (nixpkgs) rather than what each machine'sNIX_PATHor flake registry makes of it, so they float. A NixOS channel's tarball isnixpkgsat that channel. - niv and npins: each source is a package pinned by its revision or hash,
and
sources.nixpkgs(orpins.nixpkgs) in a file that imported the loader is an edge to it.builtins.fetchTarball,fetchGit,fetchTreeandgetFlakewith literal arguments are named the same way. - Nixpkgs packages: the attributes in
buildInputs,nativeBuildInputs,propagatedBuildInputs,checkInputs,packages(mkShell,home.packages) andenvironment.systemPackages-pkgs.jq,with pkgs; [ openssl zlib ], the arguments of acallPackage-style file - are packages of the Nixpkgs island, versioned and pinned by the project's nixpkgs input (or niv's or npins'nixpkgs). Inside nixpkgs itself they are itspkgs/by-namefiles.
Top-level let bindings and the attributes a file returns (paths cut at two
names) are the symbols, and a flake's outputs (packages.default,
nixosModules.default). The pinning rule is the one used elsewhere: a lock,
a commit or a content hash pins, a tag neither pins nor floats, a branch or
nothing floats. There is no Nix package index for --online to ask, and no
vulnerability database covers Nix by name; a flake input, niv or npins source
locked to a git commit on a public forge is asked about by that commit.
Gleam
Gleam compiles to Erlang and JavaScript and publishes to Hex, so its packages
are Hex packages: they share the Hex island, its pinning rule, OSV's Hex
advisories and the hex.pm client of --online with Elixir and Erlang, and a
package that a Gleam module imports and a mix.lock locks is one building.
depphunter reads .gleam modules, gleam.toml and the manifest.toml Gleam
writes beside it, without running gleam:
- Modules:
import a/b/c(with or without.{type T, f}andas c) is an edge tosrc/a/b/c.gleam,test/…ordev/…of the importing package, or of a path dependency; a module of a package gleam downloaded intobuild/packages/names that package when the directory is on disk. - Packages: other modules belong to the declared or locked package named
by their leading segments joined by
_:lustre/elementis lustre,gleam/erlang/processgleam_erlang,gleam/otp/actorgleam_otp.gleam/list,gleam/stringand the rest of the standard library aregleam_stdlib, a versioned package like any other, not a hidden island. - Externals:
@external(erlang, "mod", "f")is an edge to the project'smod.erl, an Erlang/OTP module, the package of that name or application (hpackis hpack_erl), or the compiled Gleam modulegleam@listnames;@external(javascript, "./ffi.mjs", "f")to that file, or to the package a path climbing out of the package names; a bare specifier is an npm package. - Manifests:
gleam.toml's dependencies andmanifest.toml's packages are imports of what they name. The manifest pins, keeps a range asked for as the requested version, and gives--resolve-deptheach package's requirements; without it== 1.2.3or a bare1.2.3pins, a git commit pins and a branch floats. A path dependency is an edge to itsgleam.toml.
Functions, constants, types and their constructors (Order.Cancelled) are
the symbols. Elixir and Erlang code calling a compiled Gleam module
(:gleam@list.map, gleam@list:map) reaches the same file or package.
Elm
Elm applications record the exact version of every package they install, and
packages declare ranges; the Elm packages island names them
author/name, as elm.json and package.elm-lang.org do. depphunter reads
.elm modules and elm.json, without running elm; elm-stuff/ is not read:
- Modules:
import A.B(with or withoutasandexposing) is an edge toA/B.elmunder the source directories of the project the file belongs to: an application'ssource-directories, a package'ssrc/, andtests/for elm-test. An examples application listing../srcreaches the library's files; when severalelm.jsonfiles list a file, the one whose own directory holds it comes first. - Packages: a module of a package the compiler installed in
ELM_HOME(else~/.elm) is that package, by itsexposed-modules. Without it, elm/core's modules (Dict,List,Task, ...) areelm/core, a versioned package like any other, and other modules go to the listed package a curated table (Htmlelm/html,Html.Styledrtfeldman/elm-css,Json.Decode.PipelineNoRedInk/elm-json-decode-pipeline) or the package's own name (List.Extraelm-community/list-extra) names. A module nothing names is dropped: its name does not say which author published it. The modules Elm imports by default are not edges. - Manifests: every package
elm.jsonlists — direct, indirect and test dependencies alike — is an import of it, and each source directory an edge to that directory. An application's exact versions pin; a package's ranges (1.0.0 <= v < 2.0.0) float.--resolve-depthfollows theelm.jsonof packages installed inELM_HOME, and--onlineasks package.elm-lang.org. For a package, the compilerelm-tooling.jsonpins decides whichELM_HOMEdirectory is looked in first; the other tools it pins (elm-format, elm-json, elm-test-rs) are programs, not dependencies.
Functions and values, types and type aliases, the constructors of custom types
(Msg.Clicked), ports and infix operators are the symbols. OSV has no Elm
ecosystem, so Elm packages are not checked for advisories.
PureScript
PureScript projects are built by spago from a package set or the registry's
ranges; the PureScript packages island names packages as the registry
does (prelude, halogen), and packages installed from git by their
repository. depphunter reads .purs modules, spago.yaml, spago.lock,
spago 0.20's spago.dhall and packages.dhall, and a legacy bower.json,
without running spago; .spago/ is not read as source, and neither is
output/ beside a spago configuration:
- Modules:
import A.B(qualified, with an import list orhiding) is an edge to the file whosemoduleheader namesA.B, among the sources of the importing file's package first —src/andtest/of aspago.yamlpackage, thesourcesglobs of aspago.dhall— then its workspace's other packages. A module with aforeign importneeds its JavaScript companion, an edge to the.jsfile of the same name beside it. The Prim modules (Prim,Prim.Row,Prim.TypeError, ...) come with the compiler: a hidden PureScript built-ins island. - Packages: a module of a package spago installed in
.spago/(or bower inbower_components/) is that package. Without it, a curated table (prelude's modules,Data.Mapordered-collections,Effect.Affaff,Control.Monad.Statetransformers,Halogen.*halogen) or the listed package the module spells (Node.FSnode-fs,Data.Maybemaybe) names it. A module nothing names is dropped. - Manifests: every package a manifest lists or
spago.lockrecords is an import of it.spago.lockpins (withspago.yaml's range as the requested version); a registry range floats; a bare name follows the package set, whose name (registry 60.0.0,psc-0.15.0-20220507) is shown as the version, neither pinned nor floating, since the set's versions are not known offline. Git packages (extraPackages, apackages.dhalloverride) are named by their repository and pinned by a commit; a tag is shown, neither. The Dhall reader followslet,//,#,withand local imports, and names the remote package set without fetching it.--resolve-depthfollowsspago.lock, else the manifests (spago.yaml,spago.dhall,purs.json) of the packages spago installed into.spago/, and--onlineasks the registry's index.
Values and functions, data and newtype types with their constructors,
type synonyms, classes with their members, named instances, foreign imports
and infix operators are the symbols. OSV has no PureScript ecosystem, so
PureScript packages are checked for advisories only by the commit a git
package is pinned to.
Crystal
Crystal applications and libraries get their shards from git repositories
through shards; the Crystal shards island names each shard as
shard.yml does (kemal, db), the name require and lib/ use.
depphunter reads .cr files, shard.yml, shard.lock and
shard.override.yml without running the compiler or shards; lib/ beside a
shard.yml, where shards installs, is not read as source (a lib/
directory elsewhere is), and neither is the compiler's .crystal/ cache:
- Requires:
require "./x"andrequire "../x"are edges tox.cr(orx/x.cr) relative to the file;require "./dir/*"to each.crfile ofdirandrequire "./dir/**"to each below it. A require by name is looked up the way the compiler'sCRYSTAL_PATHdoes: a shard installed inlib/(the directory is the shard), then the project's own files (a shard requiring itself by name, as its specs andbin/templates do; Crystal's standard library requiring itself fromsrc/), then the directories of the repositoryCRYSTAL_PATHlists (relative to the repository's root, or absolute inside it), by the same rules aslib/, then a shard the manifests name, then the standard library by its first segment (json,http/client,digest/sha256: a hidden Crystal standard library island), then a declared shard spelled with-/_or acrystal-prefix (sqlite3iscrystal-sqlite3). Other names are unresolved shards named by their first segment. - Manifests: every dependency and development dependency of a
shard.yml(github:,gitlab:,bitbucket:,codeberg:,git:,path:) is an import of it, and every target'smain:file an edge; ashard.override.ymlreplaces the entries it names.shard.lockpins (1.2.3,0.3.1+git.commit.<sha>), with the requirement as the requested version; without it acommit:pins, atag:or an exactversion:is shown, neither pinned nor floating (a tag can be moved), and abranch:, a range and no requirement float. A shard from a git server other than GitHub, GitLab, Bitbucket, Codeberg or sourcehut carries its URL as its origin, and a path dependency is an edge to its directory.--resolve-depthfollows theshard.ymlof shards installed inlib/, andlib/.shards.infopins whatshard.lockdoes not name; there is no index for--online.
Modules, classes, structs, enums, libs (C bindings with their funs,
structs and unions), annotations, methods (Owner.name, as in Ruby),
macros, constants, aliases, records and the attributes of getter and
property are the symbols; what a macro defines is not, and
@[Link("ssl")] libraries are not mapped. OSV has no Crystal ecosystem, so
shards are checked for advisories only by the commit shard.lock pins them to.
F# and Paket
F# projects get their packages from NuGet, through <PackageReference>
items or through Paket. depphunter reads .fs, .fsi and .fsx files,
.fsproj files and Paket's paket.dependencies, paket.lock and
paket.references without running the compiler, MSBuild or Paket. F# and
C# share one NuGet reader, so a package both languages use is one building
of the NuGet island; FSharp.Core is a NuGet package like any other
(the SDK references it implicitly), not part of the hidden .NET base
library. A .fs file that is a GLSL fragment shader or Forth is told
apart by its first lines and not read as F#; what Paket installed
(packages/ and paket-files/ beside paket.dependencies) and FAKE's
.fake/ cache are not read either:
- Compile order: a project compiles its files in the order its
.fsprojlists them (<Compile Include>items are edges of the project file), and a file can only use what earlier files declare.open X.Y,open type X.Y, a module abbreviation (module P = Shop.Pricing) and a qualified name in code (Cart.add,Shop.Domain.Cart.empty) are edges to the files declaring that namespace, module or type - as written or relative to the enclosing namespaces and what is opened, an[<AutoOpen>]module's contents also under its parent - among the earlier files of the project and the files of the projects it references (<ProjectReference>, C# projects too). A name only later files declare is dropped. - Packages: an
openno project file declares goes to a script's#r "nuget: ..."package, a declared package whose id prefixes the namespace (or extends it:Fake.CoreisFake.Core.Target), FSharp.Core (Microsoft.FSharp.*,FSharp.Collections,FSharp.Control, ...), the .NET base library (System.*,Microsoft.*) or an unresolved package.<PackageReference>items,Directory.Packages.propsversions and the packages ofpaket.referencesare imports, and so is every line ofpaket.dependenciesand every entry ofpaket.lock. - Paket:
paket.lockpins (thepaket.dependenciesconstraint is the requested version) and records the graph between packages, which--resolve-depthfollows offline; without it Paket's= 1.2.3and bare versions pin and~>and>=float. GitHub, gist, git and HTTP dependencies form a Paket git, GitHub and HTTP sources island named by where they come from (github.com/fsharp/FAKE), pinned by the locked commit. Each directory with apaket.dependenciesis a root of its own. - Scripts:
#loadis an edge to the script (relative to the file or an#Idirectory),#r "nuget: X, 1.2.3"a package (pinned when exact),#rof an assembly Paket installed underpackages/that package, of a framework assembly (System.Xml.Linq) the base library; other assemblies are dropped.
Namespaces, modules (with their nested modules), let bindings, types
with their members, exceptions and the vals of signature files are the
symbols. Unqualified names an open brings in are not linked (that needs
the compiler), MSBuild conditions are ignored, and an F# open of a C#
project's namespace is not linked to it.
D and dub
D applications and libraries get their packages from the dub registry
through dub; the dub packages island names each package as the registry
publishes it (vibe-d, mir-algorithm), a sub-package (vibe-d:http)
by its base package. depphunter reads .d and .di files, dub.json,
dub.sdl and dub.selections.json without running the compiler or dub. A
.d file that is a make dependency file (app.o: app.d ..., as gcc -MD and
dmd -makedeps write) or a DTrace script is told apart by its first lines
and not read as D, and dub's .dub/ directory is not read:
- Imports:
import a.b.c;(lists, renamed, selective,staticandpublicimports, and imports inside functions) is an edge toa/b/c.dora/b/c/package.dunder the import directories of the file's dub package (importPathsandsourcePaths, elsesource/orsrc/) and of the repository's packages it depends on (sub-packages, path dependencies), else under the file's own directory and its ancestors (so Phobos' and druntime's own modules link in their repositories).import("file")(andmixin(import("file"))) is an edge to the file understringImportPaths(views/by default). Nothing in comments, strings, token stringsq{ }or after__EOF__is read. - Packages: druntime's and Phobos' modules (
core.*,std.*,etc.c.*,object) are a hidden D runtime and standard library island. Another module goes to the package dub fetched onto this machine that has it (.dub/packages,$DUB_HOME,$DPATHor~/.dub/packages), else to the declared package its leading segments spell (mir.randomismir-random,unit_threadedunit-threaded) or a curated table names (vibe.*is vibe-d, or vibe-core, vibe-http, ... when declared,arsd.domarsd-official), else to an unresolved package named by the table or the module's first segment. - Recipes: every dependency of
dub.jsonordub.sdl- its configurations' and inline sub-packages' too - is an import of it, a sub-package directory an edge to it, and a single-file package's/+ dub.sdl: +/recipe an import of its module.dub.selections.jsonpins (with the recipe's specification as the requested version; arepositoryentry by its commit, with the repository as origin); without it==1.2.3and a bare1.2.3, which dub reads as exact, pin, and~>,^,>=,*and~branchfloat. Apathdependency is an edge to its directory.--resolve-depthfollows the recipes of packages dub fetched;--onlineasks the registry (code.dlang.org).
Modules, classes, structs, interfaces, unions, enums, templates and mixin
templates (nested ones qualified), functions and members (Owner.name,
Owner.this), aliases and top-level manifest constants are the symbols.
What a mixin generates is not read, every version and static if
branch counts, and OSV has no D ecosystem, so dub packages are checked for
advisories only by the commit a git dependency is pinned to.
Fortran and fpm
Most Fortran is built by CMake or Make, and newer libraries by the Fortran
Package Manager (fpm), which fetches git and registry packages; the fpm
packages island names each by its dependency key in fpm.toml
(json-fortran, toml-f, stdlib). depphunter reads free-form (.f90,
.F90, .f03, .f08, ...) and fixed-form (.f, .for, .f77, .F, ...)
sources, fypp templates (.fypp) and fpm.toml without running the compiler,
the C preprocessor, fypp or fpm. A .f or .for file that is Forth is told
apart by its first lines, and fpm's build/ directory is not read as source:
- Modules:
use m(use, intrinsic ::,only:lists and renames) is an edge to the file that definesmodule m, found by name (case-insensitive) among all the project's sources, whatever the build system; a submodule is an edge to its parent module's or parent submodule's file. Intrinsic modules (iso_fortran_env,iso_c_binding,ieee_*,omp_libasopenmp,openacc) are a hidden Fortran intrinsic modules island;mpi,mpi_f08,hdf5,netcdf, PETSc, FFTW and MKL modules are the C libraries#include <mpi.h>names too, on the C/C++ external island. Another module goes to the dependency fpm fetched intobuild/dependencies/that defines it, else to the declared dependency its name spells (json_moduleis json-fortran,tomlftoml-f,stdlib_kindsstdlib), a module[build] external-moduleslists (the Fortran external modules island), a curated table's package, else an unresolved module of that island. Nothing in comments or strings is read,!$OpenMP lines are, and every#ifdefbranch counts. - Includes:
include 'file',#include "file"and fypp's#:includeare edges to the file beside the includer, else in fpm'sinclude-diror an ancestor'sinclude/;mpif.handfftw3.f03go to their C library. - fpm.toml: every dependency (
[dependencies],[dev-dependencies], features' and programs' own) is an import of it. A gitrevpins, atagis shown, neither pinned nor floating, and abranchor no ref floats; a registry dependency'svpins; a metapackage (stdlib = "*") floats,openmpis the intrinsic modules andmpi,hdf5,netcdfandblasthe C libraries. A git server other than the public forges is the package's origin. fpm keeps no lock file: whatbuild/cache.tomlrecords it fetched is shown as the version, and--resolve-depthfollows thefpm.tomlof what it fetched intobuild/dependencies/.pathdependencies,[library] source-dirand programs'mainfiles are edges.
Modules, submodules, programs, block data, derived types, generic interfaces
and functions and subroutines (Module.proc, with any prefix: pure elemental real(dp) function) are the symbols; interface bodies are not.
fypp templates are not expanded (names such as ${k}$ are skipped), old-style
external procedures and call statements are not linked, and OSV has no
Fortran ecosystem and fpm's registry no dependency API, so fpm packages are
not asked about by --online and are checked for advisories only by the
commit a git dependency is pinned to.
Haxe and haxelib
Haxe compiles to JavaScript, C++, the JVM, Python, Lua, PHP, HashLink and
more; its libraries come from haxelib, pinned by lix in
haxe_libraries/<name>.hxml, and OpenFL and Lime games describe their build
in Project.xml. The haxelib libraries island names each by its haxelib
name (openfl, tink_core, thx.core). depphunter reads .hx modules,
.hxml build files, haxelib.json, lix's pins and Lime project files
without running the compiler, haxelib or lix; a project.xml is read only
when it is a Lime <project> naming a haxelib or a source path, and a local
haxelib repository (.haxelib/) is not read as source:
- Modules:
import a.b.C(withas/inaliases, sub-typesa.b.C.D, fieldsa.b.C.f,a.b.C.*andstd.paths),using a.b.Toolsand qualified type names in code (haxe.Json.parse(...)) are edges to the module's file, found by the package every module of the repository declares, whatever the class paths;import a.b.*is an edge to every module of the package. The standard library (haxe,sys,js,flash,cpp,hl, the other targets' packages and top-level types such asStringToolsandLambda) is a hidden Haxe standard library island. Another module goes to the library haxelib or lix installed that has it, else to the declared library its package spells (tink.coreistink_core), a curated table's library (hxdis heaps,js.nodehxnodejs,haxe.uihaxeui-core), else to an unresolved library; nothing in comments, strings, interpolations or regular expressions is read, and every#ifbranch counts. Each module has an edge to theimport.hxfiles the compiler applies to it: in its directory and those above, up to its class path. - Build files:
.hxmlfiles'-lib(-L,--library),-cp,-main, root modules,-resource, included.hxmlfiles and the classes--macrocalls name, over all--nextsections;haxelib.json'sdependenciesandclassPath; a Lime project's<haxelib>,<source>,<classpath>,<app main>and<include>. The libraries a file may use are those of the build files in its directory and above. - Pinning: a lix pin fixes a haxelib version or a git commit (a tag is
shown, a branch floats); without lix,
-lib x:1.2.3, an exact version inhaxelib.jsonor<haxelib version>and agit:commit pin, a git tag is shown, and no version floats, showing the version installed. A library in development inside the repository (lix's${SCOPE_DIR}, or the repository's ownhaxelib.json) is an edge to its class path. - Installed libraries: haxelib's local
.haxelib/and global repository (HAXELIB_PATH,~/.haxelib,~/haxelib) and lix's cache (HAXE_LIBCACHE) say which library has a module, and--resolve-depthfollows the-liblines of lix pins and installed libraries'haxelib.json.
Types (class, interface, enum, enum abstract, abstract,
typedef), their functions (Type.name) and module-level functions and
variables are the symbols. Macros are not run, same-package types used
without an import are not linked, and OSV has no Haxe ecosystem and
lib.haxe.org no JSON API, so haxelib libraries are not asked about by
--online and are checked for advisories only by the commit a git library is
pinned to.
Ada, GPR and Alire
Ada (SPARK included) keeps a unit's spec in a .ads file and its body in an
.adb file, GPRbuild builds from GNAT project files (.gpr), and Alire
fetches crates named in alire.toml. The Alire crates island names each
crate by its crate name (aunit, gnatcoll, utilada). depphunter reads
sources, project files, alire.toml and Alire's lock file without running
GNAT, GPRbuild or alr; Alire's alire/ directory beside an alire.toml and
a build's obj/ beside a project file are not read as source:
- Units:
with A.B, C;(limited,privateandlimited privatetoo, compared without case), a body's own spec, a child unit's parent and a subunit's (separate (P)) parent body are edges to the file that declares the unit, found by the unit every source of the repository declares, so GNAT's file naming, krunched names and a Naming package need not be followed; files in the source directories of the importing file's projects win. Predefined units (Ada.*,System.*,Interfaces.*,GNAT.*,Standardand the Ada 83 renamings such asText_IO) are a hidden Ada predefined units island. Another unit goes to the crate Alire fetched that has it or its parent unit, else to the declared crate its name spells (Acme_Log.Sinksis acme_log,TOMLada_toml) or a curated table's crate (AWS,GNATCOLL,AUnit, XML/Ada'sDOMandSax, GtkAda'sGtk,Libadalang,VSS), else to an unresolved crate named by its first segment; a unit Alire generates (<crate>_config) is dropped. Nothing in comments, strings or character literals is read, attribute ticks (X'First) are told from character literals, and only the first branch of a gnatprep#ifcounts. - Project files:
with "x.gpr",extendsand an aggregate'sProject_Filesare edges to the repository's project file, else to the crate that ships it;Source_Dirs(src/**too) andMainare edges to the directories and main files. A project file is not evaluated: every case alternative counts, and anexternalgives its default. - Manifests:
alire.toml's[[depends-on]](everycase(os)alternative),[[pins]]andproject-filesare imports. The lock file (alire/alire.lock, oralire.lockbeside the manifest before Alire 1.1) pins what it solved; otherwise=1.2.3and a bare1.2.3pin and^,~,>=and*float, showing the version Alire fetched. A pin to a git commit pins, a branch or no commit floats, apathpin in the repository is an edge to itsalire.toml, and a git server other than the public forges is the crate's origin. - Fetched crates: the crates in
alire/cache/dependencies/andalire/cache/pins/, and those the manifest names in Alire 2's shared cache (~/.local/share/alire/releases), say which crate has a unit or ships a project file;--resolve-depthfollows the lock file's solution, else a fetched crate'salire.toml, and--onlinereads the release manifest of an exact version, or of the newest release a range admits, from the indexes alr uses (see Package indexes).
Library units, the packages, subprograms (Unit.Name) and types (class
for tagged types, struct for records, interface, enum, task,
protected) of packages, and subunits are the symbols. Scenario variables
are not evaluated, units generated at build time are unresolved, and OSV has
no Ada ecosystem, so Alire crates are checked for advisories only by the
commit a git pin names.
Racket and raco
Racket modules (.rkt, .rktl load files, .scrbl Scribble documents)
start with a #lang line and require other modules by a relative path
("util.rkt") or a collection path (racket/list: the file list.rkt of
the collection racket); raco installs the catalog packages that provide
collections, named in a package's info.rkt. The Racket packages island
names each package as raco does (rackunit-lib, gui-lib, a git source by
its repository or ?path= name). depphunter reads modules and info.rkt
without running Racket or raco; .rktd data files are claimed with nothing
to link, a .scm or .ss file only when a #lang line starts it (else it
is Chez Scheme, Guile or R6RS), and nothing in a compiled/ directory:
- Modules: the
#langline (each meta-language such asat-exp, and the language'slang/reader.rktor module),#reader, and at module level (submodules andbeginincluded)requirein every form (only-in,except-in,prefix-in,rename-in,for-syntax,for-label,for-meta,submod,lib,file,planet,multi-in,path-up), Typed Racket'srequire/typedfamily,lazy-require,include,load, and Scribble's@(require ...)andinclude-section. Comments (;, nested#| |#,#;datum comments,@;), strings, here strings, regexps, characters, quoted data and Scribble text are not read. - Resolution: a relative path is an edge to the file (a
.sspath to the.rktbeside it); a collection path to the repository's collection when one has the file: the directory of a package'sinfo.rkt(itscollection, else the package's name), every directory of a multi-collection package ((define collection 'multi)) and of acollects/tree, so monorepos such as racket/racket and typed-racket link across packages. Else the base collections (racket/*,syntax/*,net/url,json,data/queue, ...) are a hidden Racket base collections island; other collections go to the package a curated table of the main distribution names (rackunitis rackunit-lib,typed/rackettyped-racket-lib,racket/guigui-lib,net/smtpnet-lib) or the declared package the collection spells (rebellion,acme-logfor acme-log-lib), else to an unresolved package named by the collection. The declared packages are those of the file's own package. - Packages:
info.rkt'sdepsandbuild-depsare imports (a package of the repository is an edge to itsinfo.rkt). raco keeps no lock file: a git source's#<commit>and a#:checksumpin, a version tag is shown, and a branch, a#:version(a minimum) and no version float. A git server other than the public forges is the package's origin; a PLaneT module path is aplanet/owner/pkgpackage.
Module-level definitions are the symbols (define as func or var, class
values with their define/public methods, struct, macros, Typed Racket's
define-type, and module submodules with their definitions as
sub.name). Macros are not expanded and installed packages are not read;
with --online the package catalog (pkgs.racket-lang.org) is asked for a
package's dependencies. OSV has no Racket ecosystem, so Racket packages are not
checked for advisories.
Common Lisp and Quicklisp
Common Lisp sources (.lisp, .lsp, and .cl unless an OpenCL kernel)
are built by ASDF from the systems .asd files define, and their
dependencies come from Quicklisp dists, pinned by Qlot (qlfile,
qlfile.lock) or ocicl (ocicl.csv). The Quicklisp projects island
names each dependency by the Quicklisp project releasing it (cl-ppcre
for cl-ppcre-unicode, cl-str for str, a git source by its repository).
depphunter reads sources and .asd files without running a Lisp; what
Qlot installed into .qlot/ and ocicl into systems/ beside an
ocicl.csv is not read as source, only its .asd files for
--resolve-depth:
- Systems: a
defsystem's:componentsare edges from the.asdto its files (modules,:pathnames and component types followed), and its:depends-on(with(:version ...),(:feature ...),(:require ...)) name systems: one an.asdof the repository defines (secondaryfoo/barsystems too; a package-inferred system'sfoo/bar/bazis the filebar/baz.lisp) is an edge to it, ASDF, UIOP and SBCL's contribs go to the hidden Common Lisp built-ins island, others to their Quicklisp project.ql:quickload,asdf:load-system,requireandloadin scripts name systems and files likewise. - Packages: a
defpackage's (oruiop:define-package's):use,:import-from,:local-nicknamesand:mix,in-packageand each qualified symbol's package (ppcre:scan, once per file) go to the file of the repository defining the package; the standard's, an implementation's (sb-ext), ASDF's and UIOP's to the built-ins; others to the system the file's systems declare that the package belongs to (a curated table:bt2is bordeaux-threads,ppcrecl-ppcre,5amfiveam;asdf:register-system-packages; the package's own name). In a package-inferred system a package is its system. A package nothing declares is an unresolved project; anin-packageor a qualified symbol of one is dropped. - Pins: a Quicklisp dist is an immutable snapshot, so its version pins.
qlfile.lockpins every project (a dist version, a git commit) and the lock's dist pins every project the qlfile does not list; without a lock a dist version (ql x 2023-10-21,ql :all) and a git:refpin, a:tagis shown and:latestor a branch float;ocicl.csvpins by image digest. A project nothing pins floats.--resolve-depthfollows the:depends-onof the systems Qlot or ocicl installed, and--onlinereads a dist'ssystems.txtfor what a project depends on.
Reader conditionals are read on both sides (#+sbcl and #-sbcl alike;
#+nil and #+(or) drop their form). Top-level definitions are the
symbols (defun, defmacro, defgeneric, defmethod named with its
specializers, defclass, define-condition, defstruct, deftype,
defvar, defparameter, defconstant, defpackage, defsystem).
Macros are not expanded, and OSV has no Common Lisp ecosystem, so
Quicklisp projects are checked for advisories only by the commit a Qlot or
ocicl git source is locked to.
Solidity, Foundry and Hardhat
Solidity contracts (.sol) are built by Foundry, whose libraries are git
submodules under lib/ (or Soldeer packages under dependencies/), or by
Hardhat, whose libraries are npm packages in node_modules. depphunter reads
foundry.toml, remappings.txt, soldeer.lock, the .gitmodules beside a
foundry.toml and the npm manifests without running solc, forge or Hardhat;
what lib/, dependencies/, out/ and cache/ beside a foundry.toml, and
artifacts/, cache/ and typechain-types/ beside a hardhat.config.*
hold is not read:
- Imports resolve as solc does:
./and../paths are files; else the project's remappings (foundry.toml's, every profile's, thenremappings.txt's;context:prefix=targetapplies to files under the context, the longest context then the longest prefix wins) map the path; for a prefix neither lists, the remappings Foundry infers (dep/=lib/dep/src/orlib/dep/) and Soldeer generates (name-version/=dependencies/name-version/) apply. A path into a library is the library, even when its files are on disk. Otherwise an npm package apackage.jsondeclares (@openzeppelin/contracts/...,hardhat/console.sol) is that package, and a path from the project's root (src/Counter.sol,contracts/Token.sol) its file. What nothing resolves is an unresolved package named by its first segment (an npm package outside Foundry). - Git submodules form the Git submodules island, named by their
repository (
github.com/foundry-rs/forge-std) and pinned by the commit git records for them, with the.gitmodulesbranch as requested; a checked-out submodule's own submodules are its dependencies. - Soldeer packages form the Soldeer packages island:
foundry.toml's[dependencies]pinned bysoldeer.lock, else by an exact version or a git commitrev. A package installed intodependencies/<name>-<version>/depends, for--resolve-depth, on its ownfoundry.tomlandsoldeer.lockentries. - npm packages of Hardhat projects are JavaScript's: one node for a package both a contract and a script import, versioned by the lock files.
Contracts, interfaces and libraries, with their functions, modifiers,
events, errors, structs, enums, value types and constants, and a file's free
functions and constants are the symbols; imports in comments, NatSpec,
strings and assembly blocks are not read. OSV has no Solidity ecosystem,
so Soldeer packages and git submodules are checked for advisories only by the
commit they are pinned to, when their repository is on a public forge.
Nim and nimble
Nim packages come from git repositories through nimble (or Atlas); the
Nimble packages island names each package as requires does
(chronos, jester), or, for a requirement written as a URL, by its
repository (github.com/status-im/nim-chronos). depphunter reads .nim
modules, NimScript (config.nims, *.nims), .nimble files,
nimble.lock, atlas.lock and nim.cfg without running the compiler,
nimble or Atlas; what nimble installed into nimbledeps/, what Atlas cloned
into deps/ beside a .nimble file and the compiler's nimcache/ are not
read as source:
- Imports:
import,from ... importandincludein every form (import a, b/c,std/[os, strutils],pkg/x,"x.nim",as,except), in everywhenbranch, resolve as the compiler finds a module:./xand../xbeside the importing file; another path beside it, then under the package'ssrcDir, then under the--paths of thenim.cfgandconfig.nimsfiles of its directory and those above it;std/xand the standard library's own module names (os,tables,asyncdispatch) are a hidden Nim standard library island (in Nim's own repository, the files of itslib/). Other names, and everypkg/x, are the package of the repositorynimble.develop(or a develop file it includes) develops in place, else the nimble package whose installed files have the module (nimbledeps/pkgs2/, Atlas's checkouts,nimble.paths, the nimble directory~/.nimbleorNIMBLE_DIRfor the packages the manifests name: sdl2_nim shipssdl2), else the requirement their first segment names (nim-widgetsiswidgets), else an unresolved package of that name. - Manifests: every
requiresandtaskRequiresof a.nimblefile (also inwhenbranches,featureblocks and tasks) is an import of its package, and everybinan edge to its main module;nimitself is the compiler and is left out.nimble.lock(with its tasks' packages) andatlas.lockpin (the requirement shown as requested); without them== 1.2.3, a bare1.2.3and a#<commit>pin, a#tagis shown, neither pinned nor floating, and a range,#head, a branch or no version float. A package from a git server other than GitHub, GitLab, Bitbucket, Codeberg or sourcehut carries its URL as its origin, and onenimble.developdevelops from a directory of the repository is an edge to that directory.--resolve-depthfollowsnimble.lock'sdependenciesand installed packages'.nimblefiles.
Top-level routines (proc, func, method, iterator, converter,
template, macro), types (objects as classes, concepts as interfaces,
enums), constants and variables are the symbols; imports in comments,
strings and routine bodies are not read. OSV has no Nim ecosystem, so
nimble packages are checked for advisories only by the commit a lock pins
them to, and the official package
list has no versions or dependencies to ask for --online.
Interface definitions
Protocol Buffers definitions are shared between services and languages, and
the protos they import from elsewhere — Google's API annotations, validation
rules, gRPC-Gateway's OpenAPI options — are dependencies no language manifest
records. depphunter reads .proto files and Buf's buf.yaml, buf.work.yaml,
buf.lock and generation templates (buf.gen.yaml, buf.gen.*.yaml):
- Imports:
import,import publicandimport weakname a file relative to an import root. The roots are those Buf's configuration declares — the directories of abuf.work.yaml, the module paths of a v2buf.yaml, a v1buf.yaml's own directory — and, where no Buf configuration applies, the directories the repository's build scripts give protoc with-Ior--proto_path(Makefiles, shell and PowerShell scripts, justfiles, Taskfiles and CMake files that mention protoc; variables the script sets are expanded, and the scripts nearest the importer come first), then the ones protoc is usually given: the repository root,proto/,protos/,api/,src/main/proto/and the importer's directory and its ancestors. Buf's roots come before the scripts'. Failing those, an import resolves to the only project file whose path ends in it (a copy underthird_party/). - Well-known types:
google/protobuf/*.proto(timestamp.proto,descriptor.protoand the rest that protoc and buf ship) form a hidden Protobuf well-known types island. - Buf Schema Registry: an import the project does not have resolves to the
module
buf.yamldeclares indeps(orbuf.lockrecords) that provides it, in the Buf Schema Registry island, namedbuf.build/owner/repository. A short table knows where common protos come from —google/api/andgoogle/type/frombuf.build/googleapis/googleapis,validate/from protoc-gen-validate,buf/validate/from protovalidate,protoc-gen-openapiv2/from grpc-gateway,gogoproto/from gogo — so a protoc project that declares nothing still names the module, marked unresolved; any other missing import is unresolved under its first directory. - Buf's files:
deps, lock entries, remote plugins (buf.build/protocolbuffers/go:v1.35.1) and module inputs are packages of that island; workspace directories and module paths are edges to those directories.
Messages (nested ones as Outer.Inner), enums, services, their rpc methods
(Service.Method), extend blocks, oneofs and the package become the file's
symbols. A type used from another file needs no edge of its own: protobuf
requires importing the file that declares it. Only buf.lock's commit, or a
commit given as the ref, pins a module. With --online a module's own
dependencies are read from the registry its name carries (buf.build, or a
private Buf Schema Registry this machine's buf credentials name); a remote
plugin depends on nothing and is not asked.
Shell scripts
Build, CI, install and deployment scripts decide what runs as much as any
manifest, and they call each other. depphunter reads shell scripts — .sh,
.bash, .zsh, .ksh, .bats, Oh My Zsh's .zsh-theme, the shells'
startup files (.bashrc, .zshrc, .profile and the rest), direnv's
.envrc, and any file without an extension whose #! line runs sh, bash,
zsh, dash, ksh, mksh or ash, directly or through env:
- Sourced files:
sourceand., with the path worked out from what the file says — literals, variables it assigned earlier, and the idioms for the script's own directory:$(dirname "$0"),${BASH_SOURCE%/*},SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)", zsh's${0:A:h}.$(git rev-parse --show-toplevel)is the repository root. A bare relative path is relative to the working directory, which a script cannot know; it is looked up beside the script, then at the root. - Scripts it runs: a command that is a path (
./build.sh,"$DIR/deploy", also afterexec,env,sudoortime), and the script an interpreter is handed (bash x.sh,python3 tools/gen.py,node x.js). - Paths below the environment:
"$PLUGIN_PATH/common/functions", where the variable comes from whatever runs the script, resolves to the one project file ending incommon/functions(two elements at least). - direnv and bats:
source_env,source_upanddotenvin an.envrc;loadin a bats test. - Installed packages:
pip install(andpython -m pip, uv, pipx),npm install/pnpm add/yarn add,go install pkg@version,cargo installandgem installadd packages to the PyPI, npm, Go modules, crates.io and RubyGems islands the manifests use, under the same pinning rules, so vulnerability lookups and private patterns cover them;pip install -r requirements.txtis an edge to that file.
Functions, bats tests, aliases, and the exported, read-only and upper-case
variables a script sets at its top level become its symbols. Nothing is run:
a path under ~ or $HOME, one computed in a loop or by eval, or from a
variable another file sets is not followed, and packages installed with
apt-get, apk, brew or another system package manager are not read, as
no island holds them.
Documentation
A README that links to CONTRIBUTING.md depends on that file, and one that
links to internal/server/server.go depends on that. Both break when the target
is moved, and no package manifest records the relationship, so the links are
placed on the map alongside the imports.
A link to a file or a directory in the repository becomes an edge from the
document to it, drawn as any other dependency is, and the headings become the
file's symbols: a document expands into its sections as a source file expands
into its functions. Inline links, reference definitions, autolinks and the
href and src attributes of raw HTML are all included, since a badge is also
a link. A link within a fenced block or a code span is not, since it is rendered
as text rather than followed.
There is no island for external hosts. A link to https://example.test is not a
dependency the map can characterize, and a legend of third-party domain names
would add nothing.
A link that leads nowhere becomes a finding instead:
| What is wrong | Reported as |
|---|---|
| the file or directory is not there | link/missing-file |
| the heading it names is not in it | link/missing-anchor |
| the reference was never defined | link/undefined-reference |
| the host says the page is gone | link/gone (--online) |
The first three require nothing beyond the repository, so they are checked on every run and are exact: a path either names something on disk or it does not, and a fragment either matches a heading of the file it points at or it does not. A link to a file that exists but is absent from the map — ignored, excluded or generated at build time — is not broken; it simply produces no edge.
The fourth requires a third-party server and is therefore performed only under
--online. Links are checked with the same credentials the indexes use, host by
host, so that a link into a private repository or an internal wiki is checked
rather than reported missing on the strength of an anonymous 404. The check is
otherwise deliberately conservative: a link is reported when a host states that
the page is gone, that is 404 or 410, and not when the host refuses a robot,
rate-limits, times out or fails. Those are the responses a
checker receives from Cloudflare and from GitHub's own bot rules, and treating
them as link rot would produce a finding for every link that functions correctly
in a browser. Links that could not be checked are reported in the log rather
than placed on the map. Answers are cached for one day.
Two categories of Markdown are excluded from all of the above, because a finding
against either would be a defect nobody is expected to correct: vendored
documentation, whose links point at the parts of its own repository that
vendoring does not copy, and fixtures under testdata, which are incorrect by
design.
--no-links disables the whole of it.
Symbol references
Imports establish which files depend on which. Under --lsp, depphunter
additionally queries language servers for which symbols use which. It uses the
servers found on PATH, and the go install locations for gopls:
| Language | Server |
|---|---|
| Go | gopls |
| JavaScript / TypeScript | typescript-language-server |
| Python | pyright-langserver, basedpyright-langserver or pylsp |
| Rust | rust-analyzer |
| Java | jdtls |
| Kotlin | kotlin-language-server |
| Scala | metals |
| C# | csharp-ls |
| F# | fsautocomplete --adaptive-lsp-server-enabled |
| C / C++ / Objective-C | clangd (CUDA, Metal and OpenCL headers too) |
| PHP | intelephense or phpactor |
| Ruby | ruby-lsp or solargraph |
| Swift | sourcekit-lsp |
| Dart | dart language-server |
| Elixir | elixir-ls (or language_server.sh), lexical or nextls |
| Erlang | elp or erlang_ls |
| R | R --slave -e languageserver::run() |
| Haskell | haskell-language-server-wrapper or haskell-language-server |
| Terraform / OpenTofu | terraform-ls serve or tofu-ls serve |
| Jsonnet | jsonnet-language-server |
| CUE | cue lsp |
| Dhall | dhall-lsp-server |
| Puppet | puppet-languageserver --stdio |
| Rego | regal language-server |
| GLSL | glsl_analyzer |
| HLSL | shader-language-server |
| WGSL / WESL | wgsl-analyzer or wgsl_analyzer |
| Protocol Buffers | buf lsp serve, bufls serve or protols |
| Shell (sh, Bash, bats) | bash-language-server start |
| CMake | neocmakelsp --stdio or cmake-language-server |
| Lua | lua-language-server |
| Luau | luau-lsp lsp |
| Perl | perlnavigator --stdio, pls or perl -MPerl::LanguageServer -e Perl::LanguageServer::run |
| OCaml | ocamllsp |
| Julia | julia --startup-file=no --history-file=no -e "using LanguageServer; runserver()" |
| Zig | zls |
| Clojure | clojure-lsp |
| Bazel (Starlark) | starpls server, bazel-lsp or bzl lsp serve |
| Nix | nil or nixd |
| Gleam | gleam lsp |
| Elm | elm-language-server --stdio |
| PureScript | purescript-language-server --stdio |
| Crystal | crystalline |
| D | serve-d |
| Haxe | haxe-language-server |
| Ada | ada_language_server |
| Racket | racket -l racket-langserver |
| Common Lisp | cl-lsp |
| Solidity | nomicfoundation-solidity-language-server --stdio or solidity-ls --stdio |
| Nim | nimlangserver or nimlsp |
| Fortran | fortls |
The servers run in the background once the map is displayed — gopls requires
approximately 7 s for this repository — within the budget set by
--lsp-timeout; results are cached until the map's files, symbols or imports
change, or a different set of language servers is installed. Servers that index
slowly, rust-analyzer, jdtls, metals, clangd and the PHP, Ruby, Swift, Elixir,
Erlang, Haskell, Julia, Clojure, F# and Nim servers in particular, may answer
before indexing has finished, so a first run can report fewer references than
a later one. The legend's Imports / References switch then determines what the
selection arcs and the side panel show: for a function, what it uses and what
uses it.
The JSON and GraphML exports include the reference edges.
Languages
| Ecosystem | Imports resolved through | Islands |
|---|---|---|
| Go | every go.mod (multi-module, local replace) |
Go modules, Go standard library |
| JavaScript / TypeScript | relative paths, tsconfig/jsconfig paths, workspaces, package.json + package-lock.json / yarn.lock / pnpm-lock.yaml / bun.lock; also the scripts of Vue, Svelte and Astro components, and SvelteKit's $lib |
npm, Node.js built-ins |
| Python | relative imports, src/ layouts, PYTHONPATH and configured source roots (below), requirements files, setup.cfg, literal setup.py lists, pyproject.toml, Pipfile, poetry.lock/uv.lock/pdm.lock/Pipfile.lock, installed environments (below) |
PyPI, Python standard library |
| Rust | the module tree (crate::, self::, super::, mod x;), workspace and path crates, Cargo.toml (renamed and workspace dependencies) + Cargo.lock |
crates.io, Rust standard library |
| Java | source files by package path (any source root), pom.xml (properties, dependency management), Gradle scripts (string and map notation), version catalogs and lock files, sbt builds; imports to the declared group:artifact shipping the package |
Maven, Java standard library |
| Kotlin | source files by the package they declare (any directory; Java files by path), the Java manifests | Maven, Kotlin and Java standard libraries |
| Scala | source files by the package they declare (any directory; Java files by path), build.sbt (%, %% with the scalaVersion suffix, versions held in a val), the Java manifests |
Maven, Scala and Java standard libraries |
| C# | namespaces to project folders (RootNamespace + folder), PackageReference, Directory.Packages.props, and the packages of .fsproj files, Paket and packages.lock.json through the NuGet reader shared with F# |
NuGet, .NET base library |
| F# | open and qualified names to the files declaring the namespace, module or type, earlier in the .fsproj compile order or in a referenced project; <Compile>, <ProjectReference>, <PackageReference>; paket.dependencies / paket.lock / paket.references (groups, GitHub, git and HTTP files); #load and #r "nuget: ..." in scripts |
NuGet, .NET base library, Paket git, GitHub and HTTP sources |
| C / C++ / CUDA / Metal | #include beside the includer, the include paths of compile_commands.json (-I, -iquote, -isystem, /I), include/ and src/, and a unique project file ending in the included path; libraries by vcpkg.json, conanfile.txt, conanfile.py and conan.lock, and by the content CMake fetches; CUDA (.cu, .cuh), Metal and OpenCL C kernels read the same way |
vcpkg, Conan, C/C++ external, C and C++ standard libraries, system headers, CUDA Toolkit, OpenCL headers, Apple SDKs |
| CMake | add_subdirectory, include of files and of modules on CMAKE_MODULE_PATH, target sources, configure_file templates, presets; find_package as the includes resolve, FetchContent, ExternalProject, CPM.cmake, pkg_check_modules |
vcpkg, Conan, C/C++ external, fetched content, pkg-config, CMake modules |
| PHP | use statements (grouped, function, const) and fully qualified names in code, same-namespace extends/implements, require/include of spelled-out paths; project files by what they declare and composer.json PSR-4/PSR-0; packages by the autoload prefixes of composer.lock or installed.json |
Packagist, PHP standard library |
| Ruby | require/require_relative/load/autoload of spelled-out paths on a guessed load path (lib, test, spec, gemspec require paths, path gems); gems by Gemfile, gemspecs and Gemfile.lock, whose gem lines are imports; Rails constants by Zeitwerk naming |
RubyGems, Ruby standard library |
| Swift | import (every #if branch); Package.swift targets to their directories, else a directory named after the module; packages by Package.swift, Package.resolved and Xcode's project.pbxproj, products first, whose .package lines are imports; types used across a module's files |
Swift packages, Swift standard library, Apple SDKs |
| Objective-C | #import/#include as C includes (beside the file, compile_commands.json, Xcode header search paths, include/ and src/, a unique file ending in it), @import; framework headers and modules to pods by Podfile, Podfile.lock and podspecs, or to Carthage by Cartfile and Cartfile.resolved, whose entries are imports; headers under Pods/ and Carthage/ to their dependency |
CocoaPods, Carthage, Apple SDKs, C/C++ islands |
| Dart | import, export (every configurable URI), part and part of; relative URIs, package: URIs to a package's own lib/, path dependencies, pub workspace members and melos packages; packages by pubspec.yaml and pubspec.lock, whose dependencies are imports |
pub, Dart SDK libraries, Flutter SDK |
| Elixir / Erlang | Elixir alias/import/require/use and every module reference, Erlang remote calls, -behaviour, -include/-include_lib; modules to the files defining them (umbrella apps too); packages by mix.exs, rebar.config, mix.lock and rebar.lock, whose dependencies are imports |
Hex, Elixir standard library, Erlang/OTP |
| R | library/require/requireNamespace/loadNamespace, pkg::, pacman, box::use, roxygen @import; source() paths, knitr children; calls to a package's own functions across its files; packages by DESCRIPTION, NAMESPACE, renv.lock and packrat.lock, whose dependencies are imports |
CRAN, Bioconductor, R base packages |
| Haskell | import (every CPP branch, PackageImports, {-# SOURCE #-}); modules to the files whose headers declare them (the importer's package, its project's and its dependencies' local packages), Happy/Alex sources by path; packages by .cabal, package.yaml, cabal.project, stack.yaml, the freeze file, stack.yaml.lock and plan.json, whose dependencies are imports |
Hackage, GHC libraries |
| Lua / Luau / Teal | require (a string, pcall(require, …), Luau paths and .luaurc aliases, Roblox instances), dofile/loadfile; modules to files by a rockspec's build.modules, else ?.lua/?/init.lua under the file's directories and their lua/, src/ and lib/ and .luarc.json's roots; Roblox instances through Rojo projects; rocks by rockspecs and luarocks.lock, Wally packages by wally.toml and wally.lock, whose dependencies are imports |
LuaRocks, Wally, Lua standard library, Lua host runtimes |
| Perl | use/no/require (and in a string eval), use parent/use base, Moose's with/extends, Corinna's :isa, require/do of files; modules to Foo/Bar.pm under use lib (literal, FindBin, __FILE__, Mojo::File and Path::Tiny chains), the distribution's lib/ and t/lib, each lib/ above the file, else a file declaring the package; distributions by cpanfile.snapshot, cpanfile, META.json/META.yml, Makefile.PL, Build.PL and dist.ini, whose requirements are imports |
CPAN, Perl core modules |
| OCaml | module paths (Foo.bar, open, include, module M = Foo, functor arguments), #require; modules to the files of the importer's dune library, executable or test (include_subdirs, modules), to what an opened module of the project declares and to local libraries (Lib.Module of a wrapped one); libraries and packages by dune files, dune-project, *.opam, *.opam.locked and dune.lock/, whose dependencies are imports |
opam, OCaml standard library |
| Julia | using/import (relative .Sub/..Parent through the include graph), include/includet (joinpath(@__DIR__, …)); the package's own src/Name.jl and submodules, local packages by UUID, [sources] or a manifest path; packages by Project.toml ([deps], [weakdeps], [extras], [compat], [extensions], [workspace]) and Manifest.toml, whose entries are imports |
Julia, Julia standard library |
| Zig | @import of files (@embedFile too), std/builtin, root (the compilation's root source file) and module names to what build.zig wires (b.addModule, b.createModule, .imports, addImport, a dependency's .module()), else the build.zig.zon dependency of that name; b.path() in build code; @cInclude as C includes; packages by build.zig.zon (named by URL, pinned by .hash), whose dependencies are imports |
Zig, Zig standard library, C/C++ islands |
| Clojure | ns :require/:use/:import, top-level require/import/load (prefix lists, every reader-conditional branch) to files under the source paths of deps.edn, project.clj, shadow-cljs.edn, bb.edn (a.b-c is a/b_c.clj), :local/root projects, Clojure's own namespaces, artifacts the manifests declare (a table and naming rules), JDK classes; npm strings in ClojureScript |
Maven, Clojure standard library, JDK, npm, Node.js built-ins |
| Bazel | load() and label attributes (srcs, hdrs, deps, data, ...) to files and to the BUILD file of each package, glob() expanded within the package; other repositories by MODULE.bazel (bazel_dep, overrides, MODULE.bazel.lock), WORKSPACE and .bzl repository rules (http_archive, git_repository, local_repository, go_repository), and the hub repositories of rules_jvm_external, rules_python, Gazelle, rules_js and rules_rust |
Bazel modules and repositories, Maven, PyPI, Go modules, npm, crates.io |
| Nix | import, callPackage and NixOS module imports of paths (a directory is its default.nix), other path literals; flake inputs (github:, gitlab:, git+https:, tarballs, path:, registry names, follows) pinned by flake.lock, inputs.x; <nixpkgs>; niv and npins sources; builtins.fetchTarball/fetchGit/fetchTree; nixpkgs attributes in buildInputs, nativeBuildInputs, packages, systemPackages |
Nix flakes and sources, Nixpkgs |
| Gleam | import (with unqualified lists and aliases) to the src/, test/ and dev/ modules of the package, of path dependencies and of build/packages when on disk; gleam/* to gleam_stdlib, gleam_erlang, gleam_otp and the like, other modules to the package their leading segments name; @external Erlang modules (compiled Gleam modules, .erl files, OTP, packages) and JavaScript files or npm packages; packages by gleam.toml and manifest.toml, whose dependencies and packages are imports |
Hex, Erlang/OTP, npm |
| Elm | import to A/B.elm under the source directories of every elm.json listing the file (an application's source-directories, a package's src/, tests/ for elm-test), kernel modules to their .js; other modules to the package whose installed elm.json in ELM_HOME exposes them, elm/core's modules to elm/core, else a curated module table or the listed package the module spells; packages by elm.json, whose dependencies are imports |
Elm packages |
| PureScript | import to the file declaring the module in the importing package's sources (src/ and test/ of a spago.yaml package, a spago.dhall's sources), then its workspace's; foreign import to the .js beside the module; Prim modules to the compiler's built-ins; other modules to the package spago installed in .spago/ that provides them, else a curated module table or the listed package the module spells; packages by spago.yaml, spago.lock, spago.dhall, packages.dhall and bower.json, whose packages are imports |
PureScript packages, PureScript built-ins |
| Crystal | require of relative paths and globs (./dir/*, ./dir/**) to files; by name through lib/ (the installed shard), the project's own src/ and shard name, the standard library, then the shards of shard.yml / shard.lock / shard.override.yml, whose dependencies and targets' main: files are imports |
Crystal shards, Crystal standard library |
| D | import to a/b.d or a/b/package.d under the importPaths/sourcePaths (source/, src/) of the file's dub package and the repository packages it depends on, else its ancestors; import("file") under stringImportPaths (views/); other modules to the package dub fetched, a declared package they spell or a curated table; dub.json, dub.sdl, dub.selections.json and single-file recipes, whose dependencies are imports |
dub packages, D runtime and standard library |
| Fortran | use to the file that defines the module (any build system, case-insensitive), submodules to their parent; include/#include beside the file or in include-dir; other modules to the package fpm fetched, a declared package they spell, the C library (MPI, HDF5, NetCDF) or a curated table; fpm.toml dependencies are imports |
fpm packages, Fortran external modules, Fortran intrinsic modules |
| Haxe | import, using and qualified names in code to the module's file, found by the package every module declares (sub-types to their module, a.b.* to every module of the package); others to the standard library, the library haxelib or lix installed that has them, a declared library they spell or a curated table; .hxml, haxelib.json, lix pins and Lime Project.xml files, whose libraries, class paths and main classes are imports |
haxelib libraries, Haxe standard library |
| Ada | with clauses to the file declaring the unit (the source directories of the file's projects first), a body to its spec, separate (P) to the parent body, a child to its parent; others to the predefined units, the crate Alire fetched that has them, a declared crate they spell or a curated table; .gpr files' with, Source_Dirs and Main, and alire.toml's dependencies, pins and project files |
Alire crates, Ada predefined units |
| Racket | #lang, require in every form (only-in, for-syntax, submod, lib, file, planet, multi-in), require/typed, include, load, Scribble's @(require ...) and include-section; relative paths to files, collection paths to the repository's collections (packages' info.rkt, multi-collection packages, collects/), else the base collections, a curated table or the info.rkt deps they spell; info.rkt deps and build-deps |
Racket packages, Racket base collections |
| Common Lisp | .asd defsystem components (modules and :pathname) and :depends-on (local .asd, secondary and package-inferred systems, the implementation, else the Quicklisp project), defpackage :use/:import-from/:local-nicknames, in-package and pkg:sym to the file defining the package or the declared system it belongs to, ql:quickload, asdf:load-system, require, load; qlfile, qlfile.lock and ocicl.csv entries |
Quicklisp projects, Common Lisp built-ins |
| Solidity | import in every form to files by relative path, the remappings of foundry.toml and remappings.txt (with contexts) and those Foundry and Soldeer infer, npm packages a package.json declares, else the project's root; libraries under lib/ (git submodules by .gitmodules, pinned by the recorded commit) and dependencies/ (Soldeer, pinned by soldeer.lock); foundry.toml remappings and dependencies, remappings.txt, soldeer.lock and .gitmodules entries |
Soldeer packages, Git submodules, npm |
| Nim | import, from, include (std/[a, b], pkg/x, every when branch) to files beside the importer, under the package's srcDir and nim.cfg/config.nims --paths; std modules to the standard library (Nim's own lib/ in its repository); else the nimble package whose installed files (nimbledeps/, Atlas's deps/, nimble.paths, ~/.nimble) have the module, else the requirement its first segment names; .nimble requires, taskRequires, bin, nimble.lock and atlas.lock entries |
Nimble packages, Nim standard library |
| PowerShell | using module, Import-Module, dot-sourced and &-invoked scripts ($PSScriptRoot), #Requires -Modules, module manifests (RequiredModules, RootModule, NestedModules), Install-Module and Install-PSResource commands, PSDepend files (requirements.psd1, *.depend.psd1) |
PowerShell Gallery, built-in modules |
| CI pipelines | GitHub workflows and composite actions (uses:, reusable workflows, container:, services:), GitLab pipelines (every include: form, components, image:, services:) |
GitHub Actions, GitLab CI, Container images |
| Protocol Buffers | import (public, weak) under the import roots of buf.work.yaml and buf.yaml (v1 and v2), else the -I roots build scripts give protoc, the repository root, proto/, protos/, api/, src/main/proto/ and the importer's directories, else a unique project file ending in the path; modules by buf.yaml deps and buf.lock and a table of common protos; buf.gen.yaml remote plugins |
Buf Schema Registry, Protobuf well-known types |
| Terraform / OpenTofu | module sources to local directories, registry and remote modules; required_providers, provider blocks and resource type prefixes to providers, pinned by .terraform.lock.hcl; references to what other files of the module declare; file()/templatefile() paths; Terragrunt source, dependency and find_in_parent_folders() |
Terraform modules, Terraform providers |
| Jsonnet | import, importstr, importbin relative to the importer, then the vendor/ and lib/ of the jsonnet-bundler projects above it, JSONNET_PATH and the importer's ancestors; files jb installed (full path or legacy link) and paths jsonnetfile.json names to their dependency (repository + subdir), local sources to their files; jsonnetfile.json and jsonnetfile.lock.json entries |
jsonnet-bundler packages |
| CUE | import of the module's own packages (every file of the package in its directory, by the module path of cue.mod/module.cue), CUE's builtin packages, cue.mod/gen and cue.mod/usr to the Go module go.mod requires (or the Go standard library, or the module's own Go package), cue.mod/pkg vendored modules, module.cue deps by module path |
CUE modules, CUE standard library, Go modules, Go standard library |
| Dhall | imports: relative paths to their files (as Text, as Location, every ? branch), URLs to the Prelude, the repository GitHub, GitLab or jsDelivr serve, else host and path up to the version, pinned by sha256:; absolute, home and env: imports dropped |
Dhall packages |
| Puppet | include/require/contain, class { }, inherits, Class[], defined and custom types, x::y(), X::Y types, template(), epp(), file(), puppet:///modules/ to the autoloader's file of a repository module, else the module metadata.json, .fixtures.yml or the Puppetfile declares (Forge slug or git repository), else r10k's installed modules/; Puppetfile, metadata.json and .fixtures.yml entries |
Puppet modules |
| Rego | import data.a.b and data.a.b... references (also through imported names) to every file of the longest package the path names |
(none: policies import only the repository's own) |
| Shaders (GLSL, HLSL, WGSL) | #include as C headers of the project (then parent directories, ignoring case), Unreal virtual paths (/Engine/, /Plugin/Name/), naga_oil #import and #define_import_path, WESL import (package::, super::), crates the Cargo manifests declare (Bevy's bevy_* through bevy) |
Unreal Engine shaders, crates.io |
| Shell scripts | source/. and scripts run by path or interpreter, with $(dirname "$0"), ${BASH_SOURCE%/*}, SCRIPT_DIR variables, zsh's ${0:A:h} and git rev-parse --show-toplevel evaluated; direnv source_env/source_up/dotenv, bats load; packages installed with pip, npm, pnpm, yarn, go install, cargo install and gem install |
PyPI, npm, Go modules, crates.io, RubyGems |
| Dockerfile / Compose | FROM, COPY --from, RUN --mount from= and # syntax= with ARG defaults expanded and stages told apart; Compose image:, and build: to the Dockerfile in the repository, ${VAR} from the .env file beside it, include: and extends: of local files |
Container images |
| Markdown | links to files and directories in the repository (inline, reference, autolink, and the href and src of raw HTML); headings become the file's symbols |
(none: a link is not a package) |
Python imports of the repository's own modules are looked up under the
directories of its manifests and their src/, and also under the roots
projects declare elsewhere, where they lie inside the repository and hold
Python files: the PYTHONPATH depphunter runs with (relative entries against
the repository root); the PYTHONPATH of .env files (read like
python-dotenv, with ${workspaceFolder} and ${PWD} as the repository root,
also from a git-ignored .env; no other value of the file is kept);
.vscode/settings.json's python.analysis.extraPaths,
python.autoComplete.extraPaths, python.envFile and the integrated
terminal's PYTHONPATH; Pyright's and basedpyright's extraPaths and
executionEnvironments (the latter for the files under their root);
pytest's pythonpath; mypy's mypy_path; and the source directories of
setuptools (package-dir, packages.find where), Poetry (from), Hatch,
PDM and maturin. A project's own roots are tried first, deepest first, then
the configured ones (the process's PYTHONPATH first, then deeper files
first), then the repository root. --explain notes each root added and what
added it (import-root).
Python packages that no index has - an in-house package installed from a
directory, a wheel file or a Git repository - are resolved from what a Python
environment has installed. The environment is the interpreter named with
--python (DEPPHUNTER_PYTHON, or python: in the user's own configuration
file), otherwise the activated virtual environment (VIRTUAL_ENV), otherwise
the project's own .venv or venv; the per-user site directory (pip install --user) is not read. Its site-packages directories are read, along with the
base interpreter's when the virtual environment includes system site-packages;
the interpreter itself is never run. A distribution counts only when its
metadata records that it was installed from somewhere other than an index
(direct_url.json, PEP 610). Such a package takes its name and version from its
installed metadata, and its dependencies from its Requires-Dist. It is treated
as private: it is never named to an index or to OSV, it is attributed to where
it was installed from rather than to any index (so it is never marked ⚠
index), and the side panel says where that was. An import that an
index-installed distribution provides but no manifest declares remains
unresolved, but under that distribution's name. The extension has no setting for
this; pass --python through depphunter.args.
Java imports name packages rather than artifacts, so each is matched to the
declared Maven artifact that ships it, and the package is named
group:artifact — as POMs, Maven Central, OSV and Trivy name it, and as the
Clojure and Bazel plugins do, so a library several builds reach is one
building. The longest package prefix wins: one from a table of well-known
libraries (com.google.common is com.google.guava:guava,
org.apache.commons.io is commons-io:commons-io, okhttp3 is
com.squareup.okhttp3:okhttp), one the artifact's name suggests
(org.springframework:spring-context gives org.springframework.context,
com.fasterxml.jackson.core:jackson-databind gives
com.fasterxml.jackson.databind, cats-effect gives cats.effect), or its
group. Among artifacts matching equally, the one whose group names the
import's root package wins (liquibase is org.liquibase's, not an
extension's named after it), then the one whose name the import
spells (io.ktor.client.engine.cio is ktor-client-cio), then the
family's main one (spring-boot, a -core). A table artifact the build does
not declare still matches when another of its group is declared, since it
comes with it: com.fasterxml.jackson.annotation is jackson-annotations
beside jackson-databind, at its version, and a Spring Boot starter brings
spring-boot. A package the table gives an artifact that is not declared is
no other artifact's (com.google.common.jimfs is com.google.jimfs:jimfs, not
Guava's). An import nothing declared matches is unresolved, named after
the table's artifact (javax.servlet:javax.servlet-api) or guessed from its
package (net.sf.saxon.s9api becomes net.sf.saxon:saxon).
Vue, Svelte and Astro components are part of the JavaScript/TypeScript
ecosystem: the code in each component's <script> blocks (<script setup>,
Svelte's module script) and in an Astro component's --- frontmatter is read as
JavaScript or TypeScript, by its lang, and resolved like any other; a
<script src> is an import too, and a component imported from a script, as
./Button.vue, is an edge to that file. Scripts that are markup rather than the
component's code - inside a Vue <template> or <svelte:head>, or an Astro
script left inline - are not read, nor is @import in a <style>. Each
component is a symbol named after its file, beside its functions and constants.
A .ts file that opens with an XML declaration or document type is a Qt
Linguist translation, not TypeScript: it is labeled XML and not parsed.
Kotlin and Scala share Java's resolution: the same manifests, the same Maven
islands, the same pinning rule, and the JDK. A Kotlin or Scala file need not sit
in a directory named after its package, so each is found by the package it
declares and its top-level definitions, told by their nesting rather than their
column (a Scala package block's are in its own package); this index serves all
three languages, so a Java class may import a Kotlin one and the reverse.
kotlin.* and scala.* form their own standard-library islands; kotlinx.*
and the modules split from the Scala library, such as scala.xml, are ordinary
Maven dependencies. Scala imports are relative, so each is tried against the
packages around the file first, then against scala._ unless a dependency owns
that root (io.circe is not scala.io); an import of a value's members
(import builder._) is not a dependency and is dropped, as is an import of a
class in the default package, which Gradle scripts declare. sbt's %% appends
the Scala binary version to an artifact's name, as Maven Central publishes it:
with scalaVersion := "3.3.3", "org.typelevel" %% "cats-effect" is
org.typelevel:cats-effect_3 (a val holding the version works too; the
file's own scalaVersion, else the root build's). Without a scalaVersion the
name stays as written, and %%% is read as %%, since the Scala.js or Native
platform suffix depends on the project.
C and C++ are one plugin, since their files include each other: .c files are
read as C and every other extension (.h, .cc, .cpp, .cxx, .c++,
.hpp, .hh, .hxx, .h++, .ipp, .inl) as C++, which covers nearly all
C headers too. Includes are read the way the preprocessor reads
them, line by line, and resolved in the compilers' order: #include "x" first
beside the including file; then the include directories of a
compile_commands.json (at the root or in a build*/ or cmake-build-*/
directory, read from disk even when git ignores it), the file's own entry or,
for a header, every entry's; then include/, src/ and the root; and last the
one project file whose path ends in the include (foo/bar.h in
libs/foo/bar.h), or the nearest one to the includer. Projects often include
their own headers with <>, so that form is looked up the same way, except
that a standard or system header is taken from the project only through the
compilation database. What the project does not have is a C or C++ standard
header (<stdio.h>, <vector>), a system header (POSIX, sys/*, Windows,
Apple frameworks, intrinsics), or else a third-party library named after its
first directory (<boost/asio.hpp> is boost, <zlib.h> is zlib), shown
unresolved; a quoted bare name the project lacks, such as a generated
config.h, is dropped. Only #if 0 and #if 1 are evaluated: the includes of
every other branch count, whatever the platform, and macros are not expanded,
so #include CONFIG_H is not followed.
A library that a vcpkg or Conan manifest declares takes the header's place
under its package: the manifests in the including file's directory and those
above it are searched, the nearest first (or, for a file under no manifest,
every manifest in the project, the shallowest first — sibling directories are
often built under one), for a package named like the header's library
(ignoring case and - against _), a known alias of it
(gtest/googletest, nlohmann/nlohmann-json, Eigen/eigen3,
SDL2, GLFW/glfw3, google/protobuf, absl/abseil and a few more),
or its name after lib (<curl/curl.h> is Conan's libcurl); a Boost header
belongs to the port of its directory (<boost/asio.hpp> to boost-asio)
before boost, and a Qt module's directory (<QtCore/QString>) to its own
port before qtbase and qt. vcpkg's vcpkg.json is read for its
dependencies and those of its features (names or objects with version>=;
platforms are not evaluated) and overrides; Conan's conanfile.txt for its [requires]
and tool sections, a conanfile.py for the string literals given to
self.requires() and its kin or assigned to requires and tool_requires,
and a conan.lock beside them in either Conan 1 or Conan 2 form, whose
libraries count as declared since the build installs them; a reference keeps
its user, channel and the recipe revision a lock pins. The recipe is not
run, so a reference built at run time (an f-string that substitutes) is not
seen and a conditional one counts whatever the condition. With --online, a
Conan package's own requirements are the requires of its recipe on the Conan
remotes (see Package indexes), read the same way. A vcpkg
port pins only by an override: a builtin-baseline fixes versions through the
registry's history, which the repository does not carry, so a port without a
version is then neither pinned nor floating, while without a baseline it
floats. A header no
manifest's package claims goes, by the same names, to content the CMake build
fetches (below). Everything else stays in C/C++ external, without a version.
CMake builds — CMakeLists.txt, *.cmake, *.cmake.in package configuration
templates and CMakePresets.json/CMakeUserPresets.json — tie the build to
the code and the libraries. add_subdirectory(dir) is an edge to
dir/CMakeLists.txt; include() of a path to that file, of a module name to
Name.cmake on CMAKE_MODULE_PATH (as set() and list(APPEND) in the file
and the CMakeLists.txt above it build it), else to a module CMake ships
(FetchContent, GNUInstallDirs, CTest, the Check* modules) in a hidden
CMake modules island; the sources of add_library(), add_executable()
and target_sources() and configure_file()'s template are edges to those
files; presets' include and toolchainFile too. Paths are evaluated from
the file's variables, those the CMakeLists.txt files above it set, and
CMAKE_CURRENT_SOURCE_DIR, CMAKE_CURRENT_LIST_DIR, CMAKE_SOURCE_DIR,
PROJECT_SOURCE_DIR and <Project>_SOURCE_DIR; conditions and loops are not
evaluated, and binary-directory, environment and configure-time values are not
followed. find_package(X) lands where an #include of X's headers lands, so
the build file and the sources meet on one node: a vcpkg or Conan package the
manifests declare, else content the build fetches under the header's name,
else the C/C++ external library named after the include directory (ZLIB is
zlib, nlohmann_json is nlohmann, Eigen3 is Eigen, each Boost and Qt
component on its own: Qt6 Widgets is QtWidgets). Before that fallback come
a project of that name in the repository, the project's own FindX.cmake, and
content it fetches under that name; CMake's find modules for tools and the
platform (Threads, OpenMP, Python3, Git, Doxygen, CUDAToolkit) are
CMake modules.
FetchContent_Declare(), ExternalProject_Add() and CPM.cmake's
CPMAddPackage() ("gh:owner/repo@1.2.3" or keywords) are packages of the
CMake fetched content island named by repository or download URL
(github.com/google/googletest; a GitHub release asset or archive by its
repository and ref), and FetchContent_MakeAvailable(name) links to the
declaration. The sources' includes of fetched headers land on it too: an
include's directory or bare header name (<doctest/doctest.h>,
<magic_enum.hpp>, <nlohmann/json.hpp>) matched, as for vcpkg and Conan,
against the name the content is declared under, its repository's name and
owner-repository, the declaration nearest above the source first. A
GIT_TAG commit or a URL_HASH pins; a tag is shown but can be moved, so it
neither pins nor floats; a branch or no tag floats.
pkg_check_modules() modules are the pkg-config modules island. Targets,
functions, macros, options, cache variables, projects and presets become
symbols.
PHP files (.php, .phtml, .inc) are read for their use statements,
grouped ones (use App\{A, B as C}) and use function/use const
included, and for the classes code names: those of new, X::, extends,
implements, a trait use and catch, qualified as PHP qualifies them (the
file's namespace, then its aliases), and fully qualified function calls
(\f()). A require or include counts when its path is spelled out:
string literals, __DIR__, dirname(__FILE__) and dirname(__DIR__, n)
joined with .; a relative path is looked up beside the file, then at the
project roots, as PHP's include path would. A name resolves to the project file
that declares it - read from every PHP file's namespace and definitions, so
classmaps and projects without Composer work - or by the PSR-4 and PSR-0 rules
of each composer.json (a path repository's package counts as the project's
own); then to PHP itself, grouped by extension (Exception is core,
DateTime is date, PDO is pdo); then to the Composer package whose
autoload prefix it falls under, which composer.lock records for every
installed package (Symfony\Component\HttpFoundation\ is
symfony/http-foundation), or vendor/composer/installed.json where there is
no lock. The platform requirements (php, ext-*, lib-*) are not
packages. A namespace no prefix claims goes to the declared package its
segments name (PHPUnit is phpunit/phpunit, Psr\Http\Message is
psr/http-message), or else is shown unresolved under the name its first two
segments suggest. Composer reads a bare version as exact, so 1.2.3 pins even
without a lock; the lock pins everything it holds and gives
--resolve-depth its edges. A global function nobody here defines (a
framework helper loaded by a files autoload) is dropped, since its package
cannot be told from its name.
Ruby files (.rb, .rake, .gemspec, .ru, and Gemfile, Rakefile,
Guardfile, Capfile) are read for require, require_relative, load and
autoload whose path is spelled out: string literals, __dir__,
File.dirname(__FILE__), File.expand_path(path, base), File.join and +.
A require is looked up on the load path Bundler, Rake and RSpec would set
up: the lib, test and spec directories of the file's directory and those
above it, every gemspec's require paths, the lib of path gems, and a Rails
application's app/*; then in Ruby itself (json, set, net/http, yaml
as psych), unless the project declares or locks that library as a gem, and
then to a gem: active_support is activesupport, rails is railties,
rspec/core is rspec-core, and a path nothing declares is shown unresolved
under its first segment. A Gemfile's gem lines and a gemspec's
add_dependency calls are imports of what they declare, since a Rails
application's gems are loaded by Bundler.require and seldom required by
name; a gem the repository builds itself is its gemspec. Gemfile.lock pins
every gem it holds (a git gem by its revision) and gives --resolve-depth its
edges; without it a bare 1.2.3 or = 1.2.3 pins. In a Rails application
(config/application.rb), constants resolve to the files Zeitwerk loads them
from - every app/* directory and its concerns are roots, lib too under
autoload_lib - trying the modules around the reference innermost first, so
models, controllers and concerns are connected without a require.
Swift files are read for import declarations, with every branch of an #if
block read, since each is built on some platform. A module the project builds
resolves to its sources' directory: a target of any Package.swift at its
path: or Sources/<name> (Tests/<name> for tests), or, for an Xcode
project, whose targets are not read, a directory named after the module. The
toolchain's modules (Foundation, XCTest, Glibc) form a Swift standard
library island and Apple's frameworks (SwiftUI, UIKit, Combine) an Apple
SDKs island. Any other module is looked up among the packages the project
declares: the product a target takes from a package
(.product(name: "NIOCore", package: "swift-nio"), or an Xcode product
dependency), a table of well-known modules (Logging is swift-log), then the
package whose name the module's name spells (Collections is
swift-collections); a module nothing declares is shown unresolved. A package
is named by its URL without scheme or .git (github.com/apple/swift-nio),
the name OSV and Trivy use. Package.resolved (beside Package.swift or in
an Xcode project's xcshareddata/swiftpm) pins the packages it holds, and a
Package.swift's .package lines are imports of what they declare. Since a
module's files see each other's declarations without imports, the type names
a file uses connect it to the file of its module, or of a project module it
imports, that declares them. A module no SwiftPM manifest provides may be a pod
or a Carthage framework: an app's Podfile or Cartfile is read for it too. A
registry package (.package(id: "mona.LinkedList")) is named by its identity;
with --online, --resolve-depth reads its Package.swift from the registry
SwiftPM's registries.json maps its scope to.
Swift is read by a small scanner: the tree-sitter grammar failed on about one
file in seven and spent up to three seconds on each of them.
Objective-C sources (.m, .mm, and .h files that show Objective-C in
their first lines: #import, @interface, @protocol, @class) share the
C/C++ plugin's include reading and resolution, #import included; a .h
file without those stays C or C++, and a .m file without a preprocessor
line, a // comment or an Objective-C keyword is MATLAB (or Mercury, by its
:- declarations) and is not read. An include or @import of an Apple
framework (<UIKit/UIKit.h>, <objc/runtime.h>) goes to the Apple SDKs
island Swift uses. A framework header of a pod (<AFNetworking/AFNetworking.h>,
<SDWebImage/UIImageView+WebCache.h>), a module (@import Firebase;) and a
bare header named like a pod ("Masonry.h") resolve to the pod the nearest
Podfile or podspec declares or Podfile.lock pins, matched by name, its
module spelling (libPhoneNumber_iOS), without a platform suffix
(lottie-ios is Lottie) or by a table of modules named otherwise (GRDB is
GRDB.swift's); a header found in a committed Pods/ directory is its pod's,
not the project's. A pod line's subspec (Firebase/Analytics) is its pod,
:path pods are the project's own directories, and Podfile.lock gives every
pod's version and what it depends on; with --online, a pod the lock does
not cover is read from the CDN or from a clone of its spec repository in
~/.cocoapods/repos. Cartfile entries are the Carthage
island, named by repository as Swift packages are (github.com/Mantle/Mantle)
and pinned by Cartfile.resolved; --resolve-depth follows the Cartfile of
each dependency checked out into Carthage/Checkouts/. Classes, categories
(NSString(Shop)), protocols, methods by selector (Cart.addItem:count:),
properties, C functions, NS_ENUMs, typedefs, constants and macros are symbols.
The HEADER_SEARCH_PATHS and USER_HEADER_SEARCH_PATHS of an Xcode project,
from its project.pbxproj or its .xcconfig files, join the include path of
the files below it, C and C++ files too: $(SRCROOT), $(inherited), settings
defined beside them and recursive /** entries are understood, and entries
outside the repository are dropped.
Objective-C is read by a small scanner: the tree-sitter grammar took 19 to 36 ms
per file and failed on one file in twenty.
Dart files are read for their import, export, part and part of
directives; a conditional import contributes every URI it names (if (dart.library.io) 'io.dart'), since each is compiled on some platform. A
relative URI resolves against the file, and package:<name>/<path> to
lib/<path> of the package named: the file's own (the nearest
pubspec.yaml), a path dependency or override (pubspec_overrides.yaml
included), a path package of pubspec.lock, a member of the same pub workspace
or a package of the same melos repository. Any other package comes from pub,
as pubspec.lock (read from disk when git-ignored; a workspace member's is
the root's) pins it or as pubspec.yaml declares it; dart: libraries form
a Dart SDK island and Flutter's own packages (flutter, flutter_test,
flutter_localizations, anything taken sdk: flutter) a Flutter SDK island.
A pubspec's dependencies are imports of what they declare, and a workspace's
members imports of their pubspecs. Dart is read by a small scanner rather than
the tree-sitter grammar, which on real Flutter code was slow and failed on one
file in ten.
Elixir files are read for the modules they name — in alias (including
alias Foo.{A, B} and __MODULE__), import, require and use, and in any
other reference: a remote call, a struct, a behavior — expanded through the
aliases in effect, those that the quote blocks of a used module of the project
inject (use MyAppWeb, :controller) and a Phoenix router's scope alias; and
for the Erlang modules they call (:ets.new). Erlang files are read for
-include, -include_lib, -behaviour, -import and remote calls
(mod:fun). A module resolves to the file that defines it — every
defmodule in the repository, umbrella applications included, and
<module>.erl — or, when the project defines only a prefix of it (generated
route helpers), to that prefix's file. Elixir's own modules form an Elixir
standard library island and OTP's modules and applications an Erlang/OTP
island. Any other module is attributed to a Hex package: the one that defines
it under deps/ or _build/ when those are on disk, else the package a
curated table or its name prefix names among those mix.exs and rebar.config
declare and mix.lock and rebar.lock lock (Phoenix.LiveView to
phoenix_live_view, cowboy_req to cowboy). The locks pin, and mix.lock's
requirements give the edges between packages. Manifest dependencies and an
.app.src's applications are imports of what they name. Both languages are
read by small lexers: the tree-sitter grammars were slower and lost Erlang
files to macros in patterns.
R scripts, packages and the R chunks of R Markdown and Quarto documents are
read for library(), require(), requireNamespace(), loadNamespace(),
pacman::p_load(), box::use(), every pkg:: qualifier and roxygen
@import tags, and for the files source() (with here::here() or
file.path()), box::use(./module), targets::tar_source() and a knitr
child pull in. A package name resolves to the importing package's own R/
directory, another package of the repository, R's base packages (and a
recommended package such as MASS or survival that the project neither declares
nor locks) as a hidden island, and otherwise to CRAN or Bioconductor as
renv.lock or packrat.lock pins it or the nearest DESCRIPTION declares
it. Files of one package import nothing from each other, so a call to a
function the package defines links the calling file to the defining one.
DESCRIPTION and NAMESPACE are imports of what they list. R is read by a
small lexer; the tree-sitter grammar parsed well but was about nine times
slower.
Haskell modules, literate modules (bird tracks and \begin{code}), boot and
hsc2hs files are read for their import declarations, PackageImports and
{-# SOURCE #-} included. A module resolves to the file whose header declares
it - in the importer's own package, then a package of its project or one it
depends on - and otherwise to the package that provides it: base,
ghc-prim, template-haskell, ghc and GHC's other own libraries as a
hidden island, anything else to Hackage, found by a curated module table and
by matching the module's name against the packages build-depends declares
(Network.HTTP.Client is http-client). cabal's build plan
(dist-newstyle/cache/plan.json), cabal.project.freeze, exact
cabal.project constraints, stack.yaml.lock and extra-deps pin; a range
floats, and a package a Stackage snapshot fixes shows the snapshot's name, as
its version is not known offline. build-depends, hpack dependencies,
cabal.project and stack.yaml packages and extra-deps are imports of what
they name (a packages: glob such as libs/*/ of every package it matches),
and the local project files cabal.project imports (import:) add their
packages and constraints. Haskell is read by a small lexer; the tree-sitter
grammar was several times slower and lost 18% of the files measured to CPP and
extensions.
Lua, LuaJIT, Luau (Roblox's, in .luau and .lua files) and Teal files are
read for their require calls, dofile and loadfile. A module resolves to
the file a rockspec's build.modules maps it to, else to ?.lua or
?/init.lua under the requiring file's directory, each directory above it and
their lua/ (a Neovim plugin's), src/ and lib/, then .luarc.json's
library; the standard library and LuaJIT's modules are a hidden island, and so
are the modules host programs provide (Neovim's vim.*, LÖVE's love.*,
OpenResty's ngx.* and bundled resty.* libraries, Lune's @lune/*). Anything
else is a rock, found among what the rockspecs declare and luarocks.lock pins
by a curated table (lfs is luafilesystem, ssl luasec) and the usual
spellings of its name. Roblox's require(script.Parent.X) and
game:GetService("ReplicatedStorage").Shared.X are placed as Rojo builds the
game from its project files, Luau's require("./x"), "@self/x" and .luaurc
aliases by path, and a path through a Packages folder to the Wally package
wally.toml names so. --resolve-depth follows wally.lock and the rockspecs
LuaRocks keeps for the rocks it installed into lua_modules/ or .luarocks/,
and with --online the Wally registry wally.toml names (a GitHub repository,
read file by file). Rockspec dependencies and wally.toml entries are imports
of what they name, and the top-level functions, methods, module tables, exported
fields and Luau and Teal types are the files' symbols. Lua is read by a small
lexer; the tree-sitter grammar took 3 to 5 ms per file and failed on Roblox's
.lua files, which are Luau.
Perl files (.pl unless it reads as Prolog, .pm, .t, .psgi,
Makefile.PL and scripts whose #! line runs perl) are read for use,
no, require and do, the classes use parent, use base, Mojo::Base,
Moose's extends and with and Corinna's :isa name, and Test::More's
use_ok. A module resolves to Foo/Bar.pm under the file's use lib
directories (literal, or computed from FindBin, __FILE__, Mojo::File's
curfile or Path::Tiny), its distribution's lib/ and t/lib, the lib/
of each directory above it and the repository's; the modules perl ships are a
hidden island, unless a manifest requires one with a version or Carton's
cpanfile.snapshot installs it (a dual-life module such as List::Util).
Anything else is a CPAN distribution, named as MetaCPAN names it
(libwww-perl for LWP::UserAgent): the one cpanfile.snapshot says provides
the module, else the one the manifests (cpanfile, META.json,
META.yml, Makefile.PL, Build.PL, dist.ini) require the module or a
namespace above it from (Plack::Request is Plack's), else a distribution
named by a curated table or after the module, unresolved. The manifests'
requirements are imports, and packages, subs, constants and Moose attributes
are the files' symbols. Perl is read by a small lexer that knows POD,
here-documents and quote-like operators; the tree-sitter grammar took 5 to
23 ms per file and failed on 3 to 6% of the files measured.
OCaml files (.ml, .mli, ocamllex's .mll and Menhir's .mly) have no
imports: they name modules, and dune decides what a name means. Every module
path a file names (Foo.bar, open Foo, include Foo, module M = Foo, a
functor's argument; not a constructor such as Some x) resolves to the file
of the module in the same dune library, executable or test (its directory,
the tree include_subdirs adds, the modules its modules field selects), to
what a module of the project the file opens declares (open Import), to a
library of the repository the component uses (Shop.Cart of a wrapped
library, any module of an unwrapped one), to OCaml's standard library (a
hidden island; List after open Core is Core's), or to the opam package of
a library the component's libraries names (lwt.unix is lwt, Lwt_io is
lwt's). dune's libraries and ppx rewriters, dune-project's and opam files'
dependencies and pin-depends are imports of what they name, pinned by
*.opam.locked, dune's dune.lock/, {= "1.2"} or a pinned commit. The
top-level lets, types, modules, exceptions, classes and vals, and dune's
libraries and executables, are the symbols. OCaml is read by a small lexer;
the tree-sitter grammar took 3.7 to 7 ms per file and failed on 2 to 11% of
the files measured.
Julia files (.jl) name modules with using and import and pull files in
with include. A file is placed in a module through the include graph, so
a relative using .Sub or ..Parent resolves to the file defining that
module; using Shop from the package's own tests, docs or extensions is its
src/Shop.jl (a submodule, Shop.Cart, the file defining it), and a
dependency the nearest Project.toml declares is a package of the repository
when its UUID, a [sources] path or the manifest's path says so, a
standard library (a hidden island, unless the manifest installed an
upgradable one such as Statistics from a registry), or a Julia package pinned
by Manifest.toml — found on disk when not committed, a versioned
Manifest-v1.11.toml first — or floating on its [compat] entry, where a
bare 1.2 is a caret range and only =1.2.3 pins. include("x.jl") and
Revise's includet are edges to the file, joinpath(@__DIR__, ...)
included. Project.toml's dependencies, extensions and workspace projects
and Manifest.toml's entries are imports of what they name. Modules,
functions (each once, whatever its methods), macros, types, constants and
enums at the top level of modules are the symbols. Julia is read by a small
lexer; the tree-sitter grammar took 21 to 314 ms per file and failed on 6
to 28% of the files measured.
Zig files (.zig) name files, the standard library and modules with
@import. A path ("cli/args.zig") and @embedFile are relative to the
importing file, std and builtin are the hidden standard library, and
root is the root source file of the compilation that reaches the file. A
module name resolves through the package's build code, read without running
it: b.addModule and b.createModule with a root_source_file, .imports
lists, addImport and addAnonymousImport (and Zig 0.11's step.addModule),
followed through variables, if (...) |dep| captures, struct fields and
function returns, to a project file, a build.zig.zon dependency's
.module() or a generated options module, which is dropped; unwired, it is
the dependency of that name. b.path() in build code is an edge to the file,
b.dependency() to the package. @cInclude inside @cImport resolves as a C
#include does. build.zig.zon's dependencies are imports: a .path is a
directory of the repository, a .url a package named after its repository
(github.com/ziglibs/known-folders, from an archive or a git+https URL)
with the URL's commit or tag as its version, pinned by its .hash. Functions,
tests, containers (with their members as Type.member), constants and
variables at the top level are the symbols. Zig is read by a small lexer; the
tree-sitter grammar took 8 to 14 ms per file and failed on up to 7% of the
files measured.
Clojure, ClojureScript and babashka files (.clj, .cljs, .cljc, .bb,
and scripts whose #! line runs bb) name namespaces in the ns form's
:require, :use and :require-macros and in top-level require calls,
prefix lists included, and every branch of a reader conditional is read; #_
discards a form and :as-alias loads nothing. A namespace is the project file
at its path (shop.db-util is shop/db_util.clj, .cljs or .cljc by the
importer's platform) under the source paths of the manifests above the file -
deps.edn :paths and aliases' :extra-paths, project.clj :source-paths
and :test-paths, shadow-cljs.edn :source-paths, bb.edn :paths - and
of the projects their :local/root dependencies name, else any file whose
ns form declares it. clojure.core, clojure.string and the rest of
Clojure's own namespaces, ClojureScript's cljs.* and the Closure Library
(goog) are the hidden Clojure standard library, as are the libraries built
into babashka for its scripts; anything else is the Maven artifact a manifest
declares for it, found by a table of popular libraries (ring.util.* is
ring/ring-core, honey.sql is com.github.seancorfield/honeysql) and by
naming habits (next.jdbc, cheshire.core, taoensso.timbre,
reitit.ring from metosin/reitit-ring), else an unresolved one.
:import names JDK classes (the Java standard library), records and types of
the project's namespaces, Java files of the project, and classes of declared
artifacts, matched as Java imports are (see above: com.google.common is
com.google.guava/guava, com.fasterxml.jackson.annotation is
jackson-annotations beside a declared jackson-databind, at its version),
else by their coordinates; a class of a jar only a dependency brings is left
out rather than guessed; a
ClojureScript string require (["react" :as react]) is an npm package from
the package.json beside the build. The manifests' dependencies - deps.edn
and bb.edn :deps and aliases, project.clj :dependencies, profiles and
:plugins, shadow-cljs.edn and build.boot :dependencies - are imports:
Maven artifacts named group:artifact ([ring "1.9.0"] is ring:ring), git
dependencies by their lib name with their repository as origin, and
:local/root projects. Namespaces, top-level def, defn, defmacro,
defmulti and defmethod, protocols (with their methods), records, types
and deftest are the symbols. Clojure is read by a small reader; the
tree-sitter grammar took 3.7 to 6.6 ms per file (28 s for metabase).
Bazel's BUILD, .bzl, MODULE.bazel and WORKSPACE files are Starlark. A
load() and the labels of a target's label attributes (srcs, hdrs,
data, deps, runtime_deps, exports, proto and any attribute ending
in deps) name files of the repository - "util.cc", ":util",
"//lib:helpers", "@//lib" - which resolve to the file, else to the BUILD
file of the package (a rule, or a file a rule generates); glob() is
expanded against the package's files, not descending into subpackages. A
label of another repository (@repo//pkg:x) resolves to what declares it: a
bazel_dep in MODULE.bazel - a module of the Bazel Central Registry at the
version MODULE.bazel.lock selected or the one declared, or what a
single_version_override, git_override, archive_override or
local_path_override puts in its place - a WORKSPACE (or .bzl macro)
http_archive named by its URL and pinned by its sha256, a
git_repository, a local_repository directory, or the hub repository of a
module extension: @maven//:com_google_guava_guava and artifact() are the
Maven artifact com.google.guava:guava at the version maven_install.json
pins, @pypi//requests and requirement("requests") the PyPI distribution
of the requirements lock, @com_github_pkg_errors//:errors the Go module
go_deps read from go.mod, //:node_modules/lodash (rules_js) the npm
package pnpm-lock.yaml resolved, @crates//:serde the crate of
Cargo.lock - the same packages the other plugins name, so they meet on one
island. Repositories Bazel provides (@bazel_tools, @local_config_cc) are
hidden; one nothing declares is an unresolved module. MODULE.bazel's and
WORKSPACE's declarations are imports themselves. Targets (//pkg:name,
kind = the rule or macro), .bzl functions, rules, providers and globals,
and the module's name are the symbols. Macros are not expanded. Starlark is
read by a small scanner; the tree-sitter grammar took 2.2 to 7.2 ms per file
(4.6 s for envoy's 1981 files).
Terraform and OpenTofu configurations, variable files, lock files and Terragrunt configurations are read by a small HCL scanner: the tree-sitter grammar parsed every file measured correctly but was about twenty times slower. See Infrastructure as code.
Jsonnet and CUE are read by small lexers: the tree-sitter grammars parsed all but about 1 % (Jsonnet) and 2 % (CUE) of the files measured correctly, at 2 to 4 ms per file, while imports and top-level declarations need only tokens. See Jsonnet and CUE.
Dhall, Puppet and Rego are read by small scanners too (Dhall by the reader
spago's files already used): the tree-sitter grammars took 2.5 (Dhall), 9
(Puppet) and 4.7 (Rego) ms per file and left errors in 104 of 154 Puppet
files and 282 of 431 Rego files measured (the Rego grammar predates if
and contains), while the scanners take 0.02 to 0.1 ms per file. See
Dhall, Puppet and Rego.
GLSL, HLSL and WGSL are read by small scanners as well: the tree-sitter grammars left errors in 31 of 345 GLSL, 154 of 377 HLSL and 309 of 328 WGSL and WESL files measured (naga_oil's and WESL's imports are no WGSL) at 2 to 9 ms per file, while the scanners take under 0.2 ms. CUDA, OpenCL and Metal go through the C/C++ scanner; the CUDA grammar took 41 ms per file. See Shaders and GPU code.
Nix expressions are read by a small lexer and parser: the tree-sitter grammar
parsed almost every file measured correctly but took 1.9 ms per file, and 12 s
for nixpkgs' python-packages.nix alone. See Nix.
Gleam modules are read by a small lexer: the tree-sitter grammar parsed 207 of 209 files measured correctly but took 4.9 ms per file, and everything needed is token-level. See Gleam.
Elm modules are read by a small lexer too: the tree-sitter grammar parsed 411 of 412 files measured correctly but took 4 to 7 ms per file, and Elm's layout rule puts every top-level declaration in column 0. See Elm.
PureScript modules are read by a small lexer as well: the tree-sitter grammar
took 86 ms per file and parsed 87 of the 380 files measured with errors.
spago.dhall and packages.dhall are read by a small Dhall evaluator. See
PureScript.
Crystal is read by a small lexer too: the tree-sitter grammar took 6 to 25 ms per file and parsed 259 of the 715 files measured with errors. See Crystal.
F# is read by a small lexer and an indentation-based scanner: the tree-sitter grammar took 55 to 816 ms per file (17.9 s for one), parsed 25 of the 97 files measured with errors and did not finish FSharp.Core's sources within 15 minutes. See F# and Paket.
D is read by a small lexer too: the tree-sitter grammar took 27 to 71 ms per
file (1.1 s for one) and parsed 27 of the 563 files measured with errors.
dub.sdl is read by a small SDLang reader. See D and dub.
Fortran is read by a small statement reader, in free and fixed form: the tree-sitter grammar reads free form only (2099 of LAPACK's 2141 fixed-form files parsed with errors) and took 47 ms per file on json-fortran (2.1 s for one). See Fortran and fpm.
Haxe is read by a small lexer and declaration scanner: the tree-sitter grammar parsed 905 of the 983 files of tink_core, HaxeFlixel and Heaps measured with errors, at 9 to 13 ms per file. See Haxe and haxelib.
Ada and GNAT project files are read by a small lexer and block scanner: the tree-sitter grammar parses Ada well, but at 5 to 8 ms per file it took forty times as long as the scanner, and the resolver reads every source's unit besides. See Ada, GPR and Alire.
Racket, Scribble documents and info.rkt are read by a small reader: the
tree-sitter grammar parses Racket well but took 3.6 to 5.8 ms per file, and
it does not read Scribble's @-forms. See Racket and raco.
Common Lisp, .asd files and qlfile.lock are read by a small reader: the
tree-sitter grammar took 30 ms per file and parsed a fifth of the files
measured with errors. See
Common Lisp and Quicklisp.
Solidity is read by a small scanner: the tree-sitter grammar parsed every file measured correctly but took 7 to 28 ms per file on average. See Solidity, Foundry and Hardhat.
Nim, NimScript and .nimble files are read by a small lexer and an
indentation-based scanner: the vendored tree-sitter Nim grammar is licensed
under the MPL-2.0 and is left out of this build. See
Nim and nimble.
Protocol Buffers definitions are read by a small scanner: the tree-sitter grammar took 2.4 to 3 ms per file and failed on every file using editions. See Interface definitions.
Shell scripts are read by a small scanner: the tree-sitter bash grammar took 2.7 to 3.6 ms per file, about twenty times longer, and failed on two thirds of the zsh files measured. See Shell scripts.
CMake files are read by a small scanner: the tree-sitter cmake grammar parsed every file measured correctly but took 6.4 ms per file on average.
C and C++ definitions are read by a scanner too: the tree-sitter C and C++ grammars parsed more than half of the files of the projects measured with errors, and grpc's generated protobuf tables ran each into the parse bound, 90 s for one analysis (3.5 s with the scanner).
Files in other languages appear on the map without dependency edges. Parsing
uses a pure-Go tree-sitter runtime for JavaScript/TypeScript, Python, Rust,
Java, Kotlin, Scala, PHP and Ruby; Go uses the standard library's own parser,
CI, Compose, Buf and shards files a YAML parser, and C, C++, C#, PowerShell,
Markdown, Dart, Elixir, Erlang, R, Haskell, HCL, Protocol Buffers, shell
scripts, CMake files, Swift, Objective-C, CocoaPods and Carthage manifests, Lua,
Luau, Teal and LuaRocks files, Perl and its CPAN manifests, OCaml, dune and opam
files, Julia, Zig and build.zig.zon, Clojure and its EDN manifests, Bazel's
Starlark files, Nix expressions, Gleam, Elm and PureScript modules, spago's
Dhall files, Crystal, F# and Paket's files, D and dub.sdl, Fortran, Haxe
and its build files, Ada and GNAT project files, Racket, Scribble and
info.rkt, Common Lisp and its Qlot and ocicl files, Solidity and
Foundry's remappings.txt and .gitmodules, Nim, NimScript, .nimble
files and nim.cfg, Jsonnet, CUE, Dhall, Puppet, Rego, GLSL, HLSL, WGSL,
Dockerfiles, the markup of Vue, Svelte and Astro components, R Markdown
chunks and C preprocessor directives small built-in scanners — so the binary
continues to cross-compile without a C toolchain.
Security
The server binds to loopback by default and prints a URL containing a random
token, which the browser exchanges for a cookie. Requests lacking the token,
requests carrying a foreign Host header while bound to loopback — that is,
DNS rebinding — and
requests for files outside the analyzed project are all rejected. Nothing may
frame the map: X-Frame-Options: DENY and frame-ancestors 'none'. A file's
raw bytes, which the side panel previews pictures, clips and recordings from,
are served only under a known image, video or audio type or as
application/octet-stream, and always with default-src 'none'; sandbox, so
nothing in a repository can run as the map.
A repository is read only inside itself. The scan lists no symbolic link, and
what the analysis reads besides the files it lists - a lock file git ignores,
the dependencies a package manager installed into the project, a report or a
.depphunter.yaml the repository names - is opened through the repository's
root: a symbolic link that stays inside the repository is followed, as pnpm's
node_modules/.pnpm links are, but one that leads out of it, or a path that
climbs out with .., is not, so a committed link cannot get a file elsewhere
on this machine parsed into the map or sent to a package index. Each such file
is measured once it is open, so a link cannot pass off a large file as a small
one. This machine's own configuration, caches and installed packages outside
the repository are read where the package managers keep them.
The side panel shows a file's source; a binary file's content stays hidden behind a button that shows its first 64 KB as a hex dump, since its bytes are rarely worth reading and there can be a great many of them. For such a file the corner's open button becomes Hex editor ↗: in the VS Code extension it opens the file in Microsoft's Hex Editor, which edits the bytes and their text side by side, and offers to install it if it is missing. A launcher on the command line cannot ask for one, so elsewhere the file opens as it is, and the status line says how to reopen it in the Hex Editor. depphunter itself never writes to the repository.
Inside an editor
That last provision is also what prevents an editor from displaying the map in
its own built-in browser, which is a frame like any other. --embed <origin>
permits the origins it names and no others; it is what a wrapper such as an
editor extension passes when it starts the server, naming every frame above the
page, since frame-ancestors is evaluated against the entire chain rather than
the immediate parent (see How the framing works).
Three consequences follow, and nothing else changes:
frame-ancestorsnames those origins rather than'none', andX-Frame-Optionsis not sent, since it provides no means of naming an origin that browsers still honor.- The token remains in the address rather than being exchanged for a cookie. A
cookie set by the map is a third-party cookie within another origin's frame,
and browsers do not return those; the page reads the token from its own URL
and supplies it on every call, in a header, or in the query string for the
event stream, which cannot set headers.
Referrer-Policy: no-referrerkeeps it out of any outgoing request. - The interface's static files — the page,
app.jsand its imports, styles and models — are served without the token, since a frame cannot attach one to a<script src>. They are identical in every release and disclose nothing about the project. Everything under/api, and the entry address/, still requires it.
The option is deliberately a flag and nothing else: no configuration file and no environment variable can enable it, so a repository cannot arrange to be framed by a page of its choosing. Each origin is validated before it reaches the header, so nothing passed on the command line can terminate the directive early or begin another.
Contributing
Building and testing, the structure of the code, and working on the VS Code extension are documented in CONTRIBUTING.md.
License
BSD 3-Clause © 2026 Dawid Ciepiela. The embedded three.js (MIT), highlight.js (BSD 3-Clause) and fzf-for-js (BSD 3-Clause), and the 3D models built from webxr-input-profiles (MIT) and a low-poly nature pack (CC0), retain their own licenses; see web/static/vendor.





