PD002: Transitive-Only Phantom
Severity: medium · Auto-fix: no
A package is imported directly in source code but is not declared in dependencies, devDependencies, peerDependencies, or optionalDependencies. It is only present in the resolved graph because a parent package depends on it transitively.
This is a latent correctness risk. If the parent package drops the dependency, bumps it to a breaking major, or changes how it re-exports the module, the import silently breaks - with no change to your own package.json and no lockfile conflict warning.
Example
{
"dependencies": {
"semver-utils": "^2.0.0"
}
}
// src/release.ts
import semver from "semver";
semver-utils depends on semver transitively. semver is present in node_modules and importable today. If semver-utils v3 drops semver from its own dependencies or bundles it differently, the import in src/release.ts breaks silently on the next npm install.
Terminal output
MEDIUM (1)
┌───────┬────────┬───────────────────────────────────────────────────────────────┐
│ Rule │ Package│ Message │
├───────┼────────┼───────────────────────────────────────────────────────────────┤
│ PD002 │ semver │ semver is imported in source but only present as a transitive │
│ │ │ dependency - declare it explicitly │
│ │ │ package.json │
│ │ │ Imported in: src/release.ts. If the parent package drops or │
│ │ │ changes this dependency, your code will break without warning.│
│ │ │ Run: npm install semver │
└───────┴────────┴───────────────────────────────────────────────────────────────┘
Fix
PD002 has no auto-fix patch because declaring a dependency is an explicit intent decision. Run the install command shown in the finding:
npm install semver # npm
pnpm add semver # pnpm
yarn add semver # Yarn
bun add semver # Bun
Use --save-dev / -D if the import is only in test, build, or tooling code:
npm install --save-dev semver
pnpm add -D semver
Why this is medium severity
Medium because:
- Works today, breaks later. The package is currently available in
node_modulesfor all package managers - the risk is future, not immediate. This distinguishes it from PD001, where strict environments break today. - Silent on upgrade. When a parent drops or changes a transitive dependency,
npm installsucceeds. The break only surfaces at runtime on the next version upgrade. - Scope is limited. PD002 only fires when source code actually imports the package and the package is present in the lockfile. Packages that are neither imported nor present produce no finding.
PD001 vs PD002
| Aspect | PD001 | PD002 |
|---|---|---|
| How it gets into the graph | Override pin | Transitive dependency of a parent |
| Breaks today? | Yes, under pnpm strict | No - works for all package managers |
| Severity | high | medium |
| Auto-fix? | No | No |
PD001 and PD002 never fire on the same package. If a package is in overrides, PD001 owns it. PD002 only fires when the package reaches the graph purely through the normal dependency tree.