Skip to main content

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:

  1. Works today, breaks later. The package is currently available in node_modules for all package managers - the risk is future, not immediate. This distinguishes it from PD001, where strict environments break today.
  2. Silent on upgrade. When a parent drops or changes a transitive dependency, npm install succeeds. The break only surfaces at runtime on the next version upgrade.
  3. 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

AspectPD001PD002
How it gets into the graphOverride pinTransitive dependency of a parent
Breaks today?Yes, under pnpm strictNo - works for all package managers
Severityhighmedium
Auto-fix?NoNo

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.


Complementary rules

  • PD001 - same problem, but the package is only available via an override pin. High severity because strict-mode environments break immediately.
  • OA001 - flags override targets that are not present anywhere in the resolved tree at all.