pub fn walk(
roots: &[PathBuf],
registry: &EcosystemRegistry,
gitignore_policy: GitignorePolicy,
symlink_policy: SymlinkPolicy,
) -> WalkOutcomeExpand description
Walks every path in roots.
Uses the ignore crate, routing every regular file through registry.for_uri (FR-002)
unchanged from the LSP’s own routing.
A root that is itself a single file (not a directory) is checked directly against the
registry, bypassing the directory walk — this is what lets deps-cli check Cargo.toml
work without needing .gitignore semantics at all.
Every directory root is walked twice (spec 062 review, background code-review fix 1):
once with ignore’s default hidden-file filtering intact (so .git — a potentially huge
tree in a real checkout, and never a source of manifests — is never even descended into,
not merely filtered from the results), and once more per registered ecosystem’s own
dot-prefixed deps_core::Ecosystem::manifest_directory_patterns entry (.github,
.gitlab, …) with hidden-ness lifted for that one subtree specifically. This is derived
from the live registry rather than a hardcoded [".github", ".gitlab"] list, so a future
ecosystem introducing a new dot-directory pattern is picked up automatically.
gitignore_policy (issue #1109): when GitignorePolicy::Ignore (the check
subcommand’s default — see crate::cli::CheckArgs::gitignore_policy), .gitignore and
.ignore files are never consulted, because in a CI security-gate invocation (git checkout && deps-cli check . against an untrusted fork PR) both are attacker-controlled
input: a one-line addition anywhere in the tree would otherwise silently remove a manifest
from the scan. .git itself is still never descended into (that is hidden-filtering, an
unrelated concern — see above), and .git/info/exclude / the user’s global gitignore are
always honored regardless of this policy, since neither travels with a cloned/fetched PR and
both are operator-, not attacker-, controlled. When GitignorePolicy::Respect, standard
.gitignore/.ignore awareness is restored (matching git’s own behavior), and any
manifest-shaped file that awareness excludes is additionally reported via
WalkOutcome::ignored_manifests. The internal PRUNED_DIRECTORIES denylist is applied
regardless of gitignore_policy — it is compiled into the binary, not attacker-controlled
input, so pruning node_modules/target/vendor/… does not reopen the fail-open gap
this policy closes. A manifest sitting directly at the root of a pruned directory (an
unusual but real monorepo layout, e.g. a genuine subproject named vendor) is still
reported via WalkOutcome::ignored_manifests in every mode, so pruning stays visible
rather than a second, narrower silent-omission bug.
One asymmetry is deliberate rather than accidental: in the default
(GitignorePolicy::Ignore) mode, a manifest excluded only by the always-on
.git/info/exclude or global gitignore is never diffed against (that costly double-walk
only runs under GitignorePolicy::Respect), so such a suppression is silent beyond
WalkOutcome::manifests coming back emptier than expected — acceptable because both
sources are operator-, not attacker-, controlled (see above).
symlink_policy (issue #1112): a directory entry that is itself a symlink to a
manifest-shaped file is always detected, regardless of this policy — its path is reported
via WalkOutcome::ignored_manifests, the same sink a pruned or .gitignore-excluded
manifest already uses, so a symlinked manifest can never silently vanish from the report.
Detection alone never reads the target’s content. When symlink_policy is
SymlinkPolicy::Follow, such a symlink is additionally resolved and routed like any other
manifest (appearing in WalkOutcome::manifests instead, with DiscoveredManifest::path
set to the resolved real path used for reading and DiscoveredManifest::display_path kept
as the symlink’s own encountered path). Every routed entry under SymlinkPolicy::Follow —
not only ones where the leaf itself is a symlink, since an entry reached by descending into
a followed symlinked directory is otherwise indistinguishable from an ordinary one — is
canonicalized and checked against the walked root’s own canonicalized absolute path; an
entry that resolves outside the root is never routed, and is reported via
ignored_manifests only when it is itself manifest-shaped (an arbitrary out-of-root symlink
to a non-manifest file is silently skipped, matching detection’s own never-warn-on-non-manifests
invariant). This containment check applies to every registered ecosystem’s own dot-directory
sub-root (e.g. .github) too, and — because ignore/walkdir always follows a walk’s own
root symlink regardless of follow_links — that sub-root containment check runs in every
mode, not only under SymlinkPolicy::Follow. A symlink loop is detected by the underlying
ignore crate and surfaced via WalkOutcome::walk_errors.
Broken symlinks (issue #1124): a symlink whose target is not a manifest (unresolvable,
or a non-regular-file such as a directory) is classified by its own filename, not its
target, and reported via WalkOutcome::broken_manifest_symlinks instead of
ignored_manifests — unconditional across every mode, same as the resolvable case, except a
structural ancestor loop under --follow-symlinks, which reports via walk_errors instead.