Skip to content

Releases: SNO7E-G/Sediment

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 26 Jul 10:02

Completes the grading rubric and the artifact surface. Grade B was published
from the start but could never be assigned, because nothing detected the
conditional cleanup it describes; that gap is now closed, and the last artifact
types the manifest reserved are detected.

Added

  • Conditional cleanup detection, and with it grade B. An uninstall routine
    gated on a stored setting — if (!get_option('..._delete_data')) return;, or
    the same removals wrapped inside the if — is now recognised, and the gating
    option and its default are reported in the cleanup block and the manifest.
    This is the plugin that is technically clean and practically dirty: the setting
    almost always defaults to off, so on a real site nothing is removed. Naming it
    B is more useful than folding it into A or C.

    A guard only counts when it actually gates cleanup — it bails out early, or a
    removal sits inside it — so an unrelated if (get_option('schema') === 'v2')
    in the same routine does not cost an otherwise clean plugin its A.

  • Directorieswp_mkdir_p and mkdir, with WP_CONTENT_DIR,
    WP_PLUGIN_DIR, and ABSPATH rewritten to portable {content_dir},
    {plugin_dir}, and {abspath} tokens, the same way {prefix} works for
    tables. A path that is only a root with nothing under it is skipped rather than
    reported as a directory the plugin created.

  • Rewrite rulesadd_rewrite_rule, add_rewrite_endpoint, and
    add_rewrite_tag. They weigh lightly: a rule is one entry in a single option
    and disappears on the next permalink flush.

  • Action Scheduler jobsas_schedule_recurring_action,
    as_schedule_single_action, as_schedule_cron_action, and
    as_enqueue_async_action. A queued job behaves like a cron event, so it weighs
    like one.

Fixed

  • A cron event scheduled with arguments is no longer reported as cleaned by an
    argument-less wp_clear_scheduled_hook().
    That call only removes events
    registered without arguments, so the event actually survives and keeps firing —
    Sediment was crediting a cleanup that does not happen. Clearing it needs
    wp_unschedule_hook(), which is what the generated uninstall.php now emits
    for those events.

  • The $wpdb handle is recognised when held as a property ($this->wpdb),
    not only as the global variable, so table creation and drops written that way
    are no longer missed. Recognition lives in one helper rather than three
    hand-rolled checks, and still requires the name wpdb — an alias under a
    different name is missed rather than guessed at.

  • Cleanup detection for the new types — as_unschedule_all_actions,
    as_unschedule_action, rmdir, and flush_rewrite_rules — so a plugin that
    tidies up after itself is credited for it.

Changed

  • A key written from several places counts as cleaned only when every write of
    it is cleaned.
    A hook scheduled both with and without arguments is one key
    with two fates; the previous "any write cleaned" merge could report such a
    plugin as spotless while an event kept firing.
  • Scores are held inside the band their letter allows, so a C with one stray
    transient can no longer outrank a B on a leaderboard.
  • Grade summaries no longer state which way a gating setting must be set.
    Sediment sees which option decides, not the polarity of the comparison, and
    "keep my data" gates are as common as "delete my data" ones — so the wording
    names the setting and its default rather than asserting a direction.
  • Every artifact type the manifest reserves is now populated by detection; the
    creates groups are unchanged, so consumers need no update. A finding type
    with no group now raises an error instead of vanishing from the document.
  • WordPress's own directories (uploads, plugins, themes, …) are recognised
    as core, so they are never attributed to a plugin or offered for deletion.

Security

  • A table name cut short by a dynamic tail is no longer claimed. Resolving
    "CREATE TABLE {$wpdb->prefix}logs{$suffix}" stops mid-name, and treating the
    fragment as the table invented one the plugin never creates — which a generated
    uninstall.php would then drop. Such names now require proof that the name
    ended before the cut.
  • wp_clear_scheduled_hook($hook, $args) no longer credits an argument-less
    event, since it clears only events registered with those exact arguments.
  • The $wpdb handle is accepted only as $wpdb, $this->wpdb, or
    self::$wpdb, so an unrelated object's ->wpdb cannot credit a table drop.

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 25 Jul 23:34

Coverage expansion and the machine-readable output. The analyzer now sees the
artifact types that prefix-matching tools structurally cannot — metadata, roles,
and custom content types — and can be consumed by other tools and by CI.

Added

  • Metadata detectionadd_*_meta / update_*_meta for posts, users, terms,
    and comments, plus register_meta. Because register_meta's object type comes
    from its first argument rather than the function name, that argument is resolved
    and mapped; if it does not resolve to one of the four known literals, nothing is
    emitted rather than guessing which meta table is touched.
  • Roles and capabilitiesadd_role (including the capability names in its
    literal capabilities array) and $role->add_cap().
  • Custom content typesregister_post_type and register_taxonomy. This is
    the class of leftovers competitors miss entirely: uninstall an e-commerce plugin
    and its products remain as unreachable rows in wp_posts, often tens of
    thousands of them. Prefix matching cannot see this; source parsing can.
  • sediment scan --json emits the manifest (schema 1.0): plugin metadata
    read from the plugin header, grade and score, coverage counts with a resolution
    rate, the cleanup path, and every artifact grouped by key with its confidence,
    cleaned flag, and all the sources that write it. Unresolvable writes are
    listed under unresolved rather than hidden, and artifact types not yet
    detected ship as empty arrays so the schema never breaks for consumers. This is
    the contract every downstream consumer reads — CI, the Index, and the WordPress
    plugin — instead of reaching into the analyzer.
  • sediment check --fail-on=<grade> exits non-zero when a plugin grades worse
    than a threshold, so a plugin author can gate their own CI on their database
    footprint the same way they gate on tests.
  • docs/manifest-schema.md documents the manifest and the guarantees a consumer
    can rely on — every group always present, {prefix} never expanded, sources
    always an array, cleaned per item, and unresolved reported rather than
    hidden.
  • Releases are now published straight from this changelog: when the top section
    carries a date instead of "unreleased", pushing it to main tags the commit and
    publishes the release with those notes.
  • Cleanup detection for the new types: delete_post_meta,
    delete_post_meta_by_key, delete_user_meta, delete_term_meta,
    delete_comment_meta, delete_metadata (with its object type resolved), and
    remove_role.

Changed

  • A post type left behind now caps the grade at D. Orphaned content is rows in
    wp_posts that no longer render anywhere, which is as damaging as an orphaned
    table. The rubric in docs/grading.md and the README was updated to match.
  • The grader weighs the new artifact types by damage: a post type like a table,
    metadata above a plain option because it multiplies per object, roles and
    capabilities because every user carries them.
  • The generated uninstall.php removes metadata (delete_post_meta_by_key /
    delete_metadata) and roles (remove_role), and deliberately never deletes
    posts or terms
    — an uninstall routine must not destroy user content silently.
    Registered post types and taxonomies are listed as a comment for a human to
    decide on.

v0.1.1 — Correctness & safety hardening

Choose a tag to compare

@SNO7E-G SNO7E-G released this 22 Jul 02:49

Sediment v0.1.1 — Correctness & safety hardening

A focused pass after a full adversarial review of the analyzer. There are no new
commands; the analyzer is simply more conservative and more correct. Every change
errs toward under-claiming rather than guessing — a key is reported as owned,
cleaned, or safe to delete only when the source genuinely proves it.

Fixed

  • Cross-namespace resolution. Names resolve to their fully-qualified form, so
    two classes or functions that share a short name in different namespaces no
    longer cross-resolve, and a same-named method in an unrelated class can no
    longer credit an uninstall callback.
  • Local-variable binding. Variables bound by function parameters, foreach,
    global, static, catch, and destructuring now resolve to dynamic; a
    single literal assignment can no longer claim a variable whose value varies at
    runtime and flow into a generated deletion.
  • Anonymous classes are keyed per file, closing a cross-file symbol leak.
  • Table SQL is read with one anchored, statement-aware parser shared by
    detection and cleanup: CREATE TABLE inside an INSERT value is ignored, and
    every statement in a multi-statement dbDelta is captured.
  • Cleanup scoping. Only confident removals count, and only inside the
    plugin-root uninstall.php (a top-level statement or a function it invokes) or
    a registered callback matched case-insensitively. A dead function in
    uninstall.php, a $wpdb->prepare(...)-only drop, and a pattern/dynamic
    removal no longer credit cleanup.
  • Never crash. A scan survives an unreadable directory, and the first pass is
    guarded like the second.

Added

  • The WordPress core allowlist now covers roles (wp_user_roles and friends),
    uninstall_plugins, recently_activated, and more core cron hooks — so a
    generated uninstall.php can never emit a delete for them.

Changed

  • The generator rebuilds the {prefix} token from $wpdb->prefix for options,
    cron, and transients (not only tables), wherever it appears in a key.
  • The grader counts unique keys rather than call sites, treats unknown-autoload
    options as autoloaded, caps the score when there is no uninstall routine, and
    reports low coverage instead of "creates no data" for an all-dynamic plugin.
  • Scanned source is escaped before it reaches the terminal report.

Full details are in the changelog.

v0.1.0 — Analyzer preview

Choose a tag to compare

@SNO7E-G SNO7E-G released this 22 Jul 02:49

Sediment v0.1.0 — Analyzer preview

The first public preview: a complete static analyzer for a WordPress plugin's
database footprint, from detection through grading and teardown generation. It
reads source only — no WordPress runtime, no database — and runs on PHP 8.3+.

What it does

  • Detection. Options (with the autoload flag), custom tables (dbDelta and
    CREATE TABLE via $wpdb->query, with $wpdb->prefix resolved to a
    {prefix} token), cron events (with recurrence), and transients.
  • Static resolution. Keys built from define()/const, class constants
    (self::, Foo::), $this-> properties, string concatenation, string
    interpolation, and local variables — each carrying an honest confidence level
    (verified, resolved, pattern, dynamic). It never presents a guess as a
    fact, and reports the fraction of write calls it could resolve.
  • Cleanup diff. Parses uninstall.php and register_uninstall_hook
    callbacks with the same engine and marks every artifact cleaned or left behind.
  • Grading. sediment grade returns an A–F letter and a 0–100 score weighted
    by real-world damage (an orphaned autoloaded option outweighs many small rows).
  • Generation. sediment uninstall writes a php -l-valid teardown that
    removes only the high-confidence, non-core artifacts a plugin leaves behind.
  • Safety. A WordPress core allowlist and a dedicated core-protection CI job
    assert that core artifacts can never enter a deletable set.

Install

composer require --dev sediment/analyzer
# or from a clone
git clone https://github.com/SNO7E-G/Sediment.git && cd Sediment && composer install

php bin/sediment scan path/to/plugin
php bin/sediment grade path/to/plugin
php bin/sediment uninstall path/to/plugin > uninstall.php

Full details are in the changelog.