DEV Community

Tisankan
Tisankan

Posted on Originally published at github.com

I analysed the 162 most popular Dart packages. 59% of what a scanner would flag never runs in your app.

I maintain capdrift, a tool that reads a Dart package and reports what it is
able to do. I ran it across the 162 most-downloaded packages on pub.dev and
kept the raw output. Here is what is actually in there.

Method: capdrift inspect <pkg> --format json against each package's latest
version, resolved AST rather than regex, no code executed. 162 packages, 1,805
findings, zero analysis failures. Four packages returned incomplete analyses and
are named at the bottom.

The headline number

Where the finding lives Findings Share
test/ and example/ 1,062 58.8%
lib/, bin/, native code 633 35.1%
tool/ and unclassified 110 6.1%

Nearly six in ten findings are in code that never reaches anyone who depends
on the package.
A test that opens a socket is a test. An example app that
reads the filesystem is an example. Neither runs when you add the package to
your pubspec.yaml.

This is not a rounding error, and per-package it gets stark:

Package Total findings Actually ships
dio 142 4
lottie 55 0
permission_handler 67 4
pdf 60 1
build_runner 130 75

dio is the second most popular HTTP client in the ecosystem. A scanner that
walks every .dart file in the archive reports 142 findings. Four of them are
things a consumer of dio is actually exposed to. The other 138 are its own
test suite and example app.

If you have ever watched a team turn off a security scanner, this is the
mechanism. It is not that the tool was wrong. Every one of those 142 findings is
a real API call in a real file. The tool just failed to say which ones could
possibly matter, and a report that is 97% irrelevant trains people to close the
tab.

What actually ships

Restricting to code that runs for a consumer, 633 findings across 81 packages:

Capability Findings Packages
Filesystem 263 35
Platform / device info 241 49
Native code (FFI) 63 6
Subprocess execution 24 9
Network 17 10
Environment variables 15 7
Dynamic code loading 10 8

47 of 162 packages, 29%, have no capabilities at all. They compute and
return. collection, meta, most of the pure-Dart utility layer. That is a
healthier ecosystem picture than I expected going in, and it is worth saying
because "supply chain" writing tends to imply everything is on fire.

Half the ecosystem by count is inert. The risk concentrates in a much smaller
set than the raw finding count suggests.

Dynamic code loading, 11 packages

This is the category I care most about, because a package that loads code at
runtime can do things no static analysis will ever see:

  • build_runner calls Isolate.spawnUri, which downloads Dart from a URI and runs it. Reasonable for a code generator. Worth knowing you have it.
  • drift uses both Isolate.spawn and Isolate.spawnUri.
  • dio, sentry, sentry_flutter, file_picker, flutter_map, extended_image use Isolate.spawn.
  • analyzer, pigeon, and collection touch dart:mirrors.

collection is the one that surprised me. It is a Dart-team package and about
as boring as a dependency gets. The mirrors usage is confined to its own test
support, which is exactly the surface distinction this whole post is about, and
I would not have known that from a finding count.

The result I was hoping for and did not get

I expected to open with build hooks. Code in hook/ runs on your machine at
pub get time, before you execute a line of your own app, and it is the single
most under-examined surface in the Dart ecosystem.

Zero of the 162 packages ship a hook/ directory. Not a few. None.

I checked this was not my own bug before believing it: capdrift's test corpus
contains three synthetic hook fixtures, including one that shells out to curl
from a build hook, and recall on that corpus is 11/11. The detector fires. There
is simply nothing to find yet, because native-assets build hooks are new and the
established packages predate them.

That is a real finding and I would rather publish it than quietly drop it. The
attack surface is real, the tooling should be ready, and right now the ecosystem
has not walked into it. If you are writing a package that adds a hook/
directory, you will be among the first, and it is worth knowing that reviewers
have no habits built up around reading them.

Limitations, plainly

  • 162 packages ranked by downloads is not all of pub.dev. Popular packages are better maintained than average, so this is a best case, not a typical case.
  • Latest version only. No historical trend.
  • Four packages returned incomplete analyses: fl_chart, get, hive_flutter, photo_view. Their numbers are in the totals but their trees did not fully resolve, so treat them as floors.
  • Static analysis cannot see through reflection. The three dart:mirrors packages above may do more than is reported, which is why capdrift flags mirrors as a caveat about the analysis rather than a finding about the package.

Reproduce it

dart pub global activate capdrift
capdrift inspect dio --format json
Enter fullscreen mode Exit fullscreen mode

Every number above is reproducible from this directory:

cd docs/research/2026-08-dart-capability-survey
python3 analyse.py
Enter fullscreen mode Exit fullscreen mode

data/ holds the raw capdrift JSON for all 162 packages, corpus.txt is the
package list, and analyse.py prints the figures this write-up quotes. If a
number here disagrees with the script, the write-up is wrong. Run with capdrift
0.2.0 on 2026-08-23, latest version of each package at that date.

capdrift is MIT licensed at github.com/Tisankan-dev/capdrift.

If you run it against your own dependency tree and it reports something wrong,
that is the most useful thing you could send me. It is the one class of bug my
own fixtures cannot produce.

Top comments (0)