sediment/analyzer

A static analyzer that reads a WordPress plugin's source code and reports exactly what it leaves behind in your database.

Maintainers

Package info

github.com/SNO7E-G/Sediment

Documentation

Type:project

pkg:composer/sediment/analyzer

Transparency log

Statistics

Installs: 9

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0

v0.9.0 2026-08-16 17:52 UTC

This package is auto-updated.

Last update: 2026-08-16 18:26:09 UTC


README

Everything your plugins left behind, layer by layer.

A static analyzer that reads a WordPress plugin's source and reports exactly what it leaves behind in your database.

Tests Latest release Packagist Status: alpha PHP 8.3+ License: GPL-2.0-or-later

Uninstalling a WordPress plugin deletes its files and almost nothing else. The options it registered keep autoloading on every request, its custom tables keep bloating your backups, and its cron events keep firing hooks whose code is long gone. Analysis of the plugin directory has found that more than 40% of plugins leave orphaned data behind, and only 28.6% remove it cleanly — a problem that has been documented since 2008 and remains unsolved.

The hard part was never deletion. It is attribution: knowing that a stray smk_last_sync_ts row belongs to a plugin you removed years ago, and is therefore safe to drop. Cleanup tools guess at this by matching name prefixes, and a wrong guess in a tool that deletes data will wreck a site.

Sediment takes a different route. It reads the source. A plugin's own code contains the literal calls that create its data — add_option(), dbDelta(), wp_schedule_event() — so parsing them statically turns attribution from a guess into something you can point at: includes/setup.php:412.

What sets it apart

It reads code but never runs it. There is no WordPress install, no database, and nothing executed; a scan works anywhere PHP 8.3 or newer runs. Every finding carries a confidence level, and the report tells you what fraction of write calls it could resolve, so the tool is candid about its own coverage instead of pretending to certainty it doesn't have. And it is real AST analysis built on nikic/php-parser — not a pile of regular expressions.

Installation

Download the PHAR from the latest release and run it — one file, no Composer, no dependencies beyond PHP 8.3:

php sediment.phar scan path/to/plugin

Or add it to a plugin project as a development dependency:

composer require --dev sediment/analyzer

Or work from a clone:

git clone https://github.com/SNO7E-G/Sediment.git
cd Sediment
composer install
php bin/sediment scan path/to/plugin

Usage

Point scan at a plugin directory:

php bin/sediment scan path/to/plugin
Options (3)
 function        key             confidence   autoload   cleaned   source
 add_option      acme_version    verified     no         yes       acme.php:31
 add_option      acme_settings   verified     yes        no        acme.php:33
 update_option   acme_pro_*      pattern      unknown    no        pro.php:88

Tables (1)
 function   key                confidence   cleaned   source
 dbDelta    {prefix}acme_logs  resolved     no        install.php:12

 Resolution rate: 92.0% (23 of 25 write calls resolved to a key).
 Cleanup path: uninstall.php — 8 of 12 detected artifacts removed on uninstall.

Grade a plugin, generate the uninstall.php it should have shipped with, emit a machine-readable manifest, or gate your CI on the result:

php bin/sediment grade path/to/plugin
php bin/sediment uninstall path/to/plugin > uninstall.php
php bin/sediment scan path/to/plugin --json > manifest.json
php bin/sediment check path/to/plugin --fail-on=C   # exit 1 if worse than C

Guard a release against footprint creep, or scan a whole collection at once:

php bin/sediment diff manifest.json path/to/plugin   # exit 1 if the footprint got worse
php bin/sediment batch path/to/plugins --out ./manifests

diff compares against a manifest you committed earlier: a new artifact that isn't cleaned up, one that stopped being cleaned up, or a worse grade fails the build. Adding data you also remove on uninstall doesn't. batch writes one manifest per plugin plus a grade distribution, and is built to survive a run of thousands: each plugin is scanned in its own child process under a wall-clock --timeout and a --memory-limit, --jobs N runs that many children at once, one pathological plugin costs one line in the --report instead of the run, and --resume carries on from the manifests already written. index then turns a directory of manifests into the Index artifacts — a reverse lookup from artifact key to the plugins that create it, aggregate stats, and a QA report that refuses to build a dataset attributing WordPress core data to a plugin.

The manifest carries the grade, the coverage counts, and every artifact with its confidence, cleaned flag, and source lines — it is the contract other tools build on, frozen as a JSON Schema and documented in docs/manifest-schema.md, with the stability rules in docs/stability.md. check is the same analysis wired for CI, so a plugin author can fail a build on their database footprint the way they already fail it on tests.

What it detects

Options (with the autoload flag), custom tables, cron events, transients, post/user/term/comment metadata, roles and capabilities, custom post types and taxonomies, directories, rewrite rules, and Action Scheduler jobs.

Custom post types are the group prefix-matching tools structurally cannot reach. Uninstall an e-commerce plugin and its products stay behind as unreachable rows in wp_posts — often tens of thousands. No prefix reveals that. Reading the source does.

Sediment also reads how a plugin cleans up. A routine gated behind a "delete my data on uninstall" setting is reported as conditional — with the exact option and the value it defaults to — because a plugin that only cleans up for the few users who found that checkbox is clean in its code and dirty on real sites.

grade returns a letter and a weighted-damage score. uninstall writes a teardown that removes only the artifacts Sediment attributed with high confidence — never a WordPress core key, an already-cleaned key, or a guess.

How confidence works

Sediment labels every finding by how certain the attribution is, and it never presents a guess as a fact:

Level Meaning
verified The key is a literal string in the source.
resolved The key resolves from a constant, class constant, or literal property.
pattern The key is partly dynamic but exposes a stable prefix, such as _mp_*.
dynamic The key is only knowable at runtime. It is recorded honestly and never acted on.

Grading

A grade reflects what a plugin leaves behind, weighted by real-world cost rather than a raw count — one orphaned autoloaded option matters more than twenty small rows nobody reads:

Grade Meaning
A Removes everything it creates, unconditionally, on uninstall.
B Removes everything, but only when the user opts in (conditionally clean) — the grade names the setting.
C Removes some data; leaves a few harmless rows — none autoloaded, no tables or cron.
D Leaves tables, autoloaded options, cron events, or orphaned content behind.
F Ships no uninstall routine at all.

The rubric is published in full at docs/grading.md so a grade is always something you can explain, not a black box.

Limitations

Static analysis cannot see a key that is assembled entirely at runtime, so those are reported as dynamic and never acted on — the resolution rate tells you how much of a plugin fell into that bucket.

Sediment follows a write one hop back through a plugin's own settings layer, so update_option($key, ...) inside a helper resolves to the keys its callers actually pass. Across a corpus of ten real plugins the median resolves at 84%, but the largest ones resolve least: Yoast SEO manages 64% and WooCommerce 78%, because much of what they write is keyed at runtime. Sediment publishes those numbers rather than papering over them, and every unresolved write is listed with its source line so a human can settle it. The full corpus and its results are in docs/corpus.md. A handful of narrower edges (cron events scheduled with arguments, an aliased $wpdb handle) are documented plainly in docs/limitations.md. Publishing these is deliberate: an audit tool earns trust by being honest about its own blind spots.

Roadmap

Sediment is built in stages, each useful on its own:

  1. Analyzer (this repository) — detect what a plugin writes, diff it against the plugin's cleanup routine, grade the result, and generate the uninstall.php it should have shipped with.
  2. The Indexshipped. A public, CC0 dataset mapping the 5,000 most popular plugins to the data they create, so a leftover row can be traced back to the plugin that made it: one manifest per plugin plus a reverse lookup, on the index-data branch, rebuilt on CI from a pinned list anyone can rerun. The findings — among them that only 210 of 4,995 plugins remove everything they create — are in docs/index.md.
  3. Inspector — a read-only WordPress plugin that scans installed plugins on disk and shows what each will leave behind before you click Delete.

The full plan, with what each release has to prove before the next begins, is in ROADMAP.md.

Status

Release candidate. Detection across every artifact type above, static resolution, the cleanup diff (including conditional cleanup), grading, the manifest, the CI check, and the uninstall.php generator all work today, backed by a broad test suite. The Index has shipped: 4,995 of the top 5,000 wordpress.org plugins scanned with zero QA violations, published as open data — see docs/index.md for what it found, and docs/pilot.md for the 500-plugin pilot that earned the pipeline its trust. Per the roadmap, 1.0 follows thirty days after 0.9 with no schema-breaking defect. Releases are cut when a meaningful body of work is ready rather than per change, so each one carries real features — see the changelog and the releases. The manifest schema is frozen at 2.0 and covered by the rules in docs/stability.md; the rest of the tool may still change before 1.0.

Development

composer install
composer test
php bin/sediment scan tests/fixtures/dirty-plugin

See CONTRIBUTING.md for how a change should look — especially the rule that nothing may be reported with more confidence than the source justifies.

License

Released under GPL-2.0-or-later. The Index dataset is dedicated to the public domain under CC0, so the open data can never be walled off.