From d347a09bf9865a66dd1426fe1362c8d7e26dee2d Mon Sep 17 00:00:00 2001 From: Rati Rukhadze Date: Wed, 9 Sep 2026 11:10:55 +0400 Subject: [PATCH 1/2] feat: add a baseline, migration anchors and pull-request annotations to vacuum:lint --- .gitignore | 1 + CHANGELOG.md | 17 +- README.md | 82 ++++- art/lint-in-ci.png | Bin 0 -> 133783 bytes art/make_readme_art.py | 143 ++++++++ config/vacuum.php | 25 ++ src/Advisor/Rules/ForeignKeyTypeMismatch.php | 4 +- ...nt4PrimaryKey.php => NarrowPrimaryKey.php} | 6 +- src/Console/Commands/LintCommand.php | 171 +++++++-- src/Console/Support/Baseline.php | 165 +++++++++ src/Console/Support/GithubReporter.php | 124 +++++++ src/Schema/MigrationMap.php | 121 +++++++ src/Schema/MigrationScanner.php | 237 +++++++++++++ src/Schema/SourceLocation.php | 22 ++ src/VacuumServiceProvider.php | 17 +- tests/Feature/Command/LintCommandTest.php | 325 +++++++++++++++++- .../Rules/ForeignKeyTypeMismatchTest.php | 2 +- ...ryKeyTest.php => NarrowPrimaryKeyTest.php} | 24 +- tests/Unit/Console/BaselineTest.php | 124 +++++++ tests/Unit/Console/GithubReporterTest.php | 179 ++++++++++ tests/Unit/Schema/MigrationMapTest.php | 140 ++++++++ tests/Unit/Schema/MigrationScannerTest.php | 289 ++++++++++++++++ tests/Unit/ServiceProviderTest.php | 33 ++ .../2024_01_01_000000_create_orders_table.php | 19 + ...2024_01_02_000000_create_dynamic_table.php | 19 + 25 files changed, 2227 insertions(+), 62 deletions(-) create mode 100644 art/lint-in-ci.png rename src/Advisor/Rules/{Int4PrimaryKey.php => NarrowPrimaryKey.php} (96%) create mode 100644 src/Console/Support/Baseline.php create mode 100644 src/Console/Support/GithubReporter.php create mode 100644 src/Schema/MigrationMap.php create mode 100644 src/Schema/MigrationScanner.php create mode 100644 src/Schema/SourceLocation.php rename tests/Unit/Advisor/Rules/{Int4PrimaryKeyTest.php => NarrowPrimaryKeyTest.php} (75%) create mode 100644 tests/Unit/Console/BaselineTest.php create mode 100644 tests/Unit/Console/GithubReporterTest.php create mode 100644 tests/Unit/Schema/MigrationMapTest.php create mode 100644 tests/Unit/Schema/MigrationScannerTest.php create mode 100644 tests/fixtures/migrations/2024_01_01_000000_create_orders_table.php create mode 100644 tests/fixtures/migrations/2024_01_02_000000_create_dynamic_table.php diff --git a/.gitignore b/.gitignore index ccd8821..8a20270 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ Thumbs.db .vscode .env .superpowers/ +__pycache__/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dec6f6..a2a6b98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,20 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +## [1.2.0] - 2026-09-09 + +### Added + +- **A baseline, which is what makes `vacuum:lint` usable on a schema that predates it.** The first run against a five-year-old application prints several hundred findings, and the only available response was to stop running it. `--generate-baseline` writes `vacuum-baseline.json`, you commit it, and from then on the outstanding findings are excused and anything new fails the build. A finding is matched on its rule and its subject and on nothing else — not the prose, not the severity — so rewording a rule never invalidates a file somebody committed months ago. That is defensible only because schema-rule subjects are data-independent: `public.orders.customer_id` means the same thing on every run, which is not true of `slow-statement` and is why `vacuum:check` has no baseline. Entries that stop matching are reported as `Info` rather than silently carried, because a baseline nobody prunes becomes a place the next defect hides, and the score is computed over what is left with the suppressed count printed beside it. + +- **`--format=github`, so findings arrive on the pull request rather than in a log.** Each becomes a workflow-command annotation on the diff, and a markdown table is appended to `$GITHUB_STEP_SUMMARY` when the runner offers one. + +- **Findings are traced back to the migration that introduced them.** `database/migrations` is read with PHP's own tokenizer — the technique the Filament installer already uses, and with the same refusal to guess: a variable table name or a file that does not parse yields no anchor, and the finding is reported without one rather than pointed at a line it did not come from. An application that has run `schema:dump --prune` has little left to trace to — that flag is the one that deletes the migrations after squashing them, while plain `schema:dump` keeps them and is unaffected — which the README says plainly rather than leaving to be discovered. + +### Changed + +- **`int4-primary-key` is now `narrow-primary-key`.** The slug named a type rather than the defect, and the rule has always fired on `smallint` as well — it prints 32,767 or 2,147,483,647 as appropriate. The rename is deliberate and deliberately early: the slug is about to become a key in the baseline file users commit, and renaming it once anybody has a baseline would invalidate all of them. It appears as the `rule` value in the `vacuum:lint --format=json` document, so a pipeline filtering on the old string needs updating. + ## [1.1.0] - 2026-09-08 ### Added @@ -138,7 +152,8 @@ First release. - **A Filament v4 panel** (optional peer — nothing changes for a Blade-only install): a **Vacuum** navigation group with an **Overview** dashboard (health score and grade, database vitals, charts, the findings with copyable remediation, and live running vacuums) and read-only resources for **Tables**, **Indexes**, **Sessions** and **Statements**. Every surface shares the one `Vacuum::auth()` gate and opts out of tenant scoping, so it is at home in a multi-tenant panel. - **Extensibility.** Application rules can be tagged onto the advisor per subject (`TABLE_RULES`, `INDEX_RULES`, and the rest), and both the config and the dashboard views are publishable. -[Unreleased]: https://github.com/heyosseus/vacuum/compare/v1.1.0...HEAD +[Unreleased]: https://github.com/heyosseus/vacuum/compare/v1.2.0...HEAD +[1.2.0]: https://github.com/heyosseus/vacuum/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/heyosseus/vacuum/compare/v1.0.1...v1.1.0 [1.0.0]: https://github.com/heyosseus/vacuum/compare/v0.3.0...v1.0.0 [0.3.0]: https://github.com/heyosseus/vacuum/compare/v0.1.0...v0.3.0 diff --git a/README.md b/README.md index 8636ce5..7ef0cc1 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,21 @@ ![Vacuum — a PostgreSQL monitoring and tuning dashboard for Laravel](art/hero.png) -**A PostgreSQL monitoring and tuning dashboard for Laravel.** +**A PostgreSQL monitoring dashboard for Laravel — and a schema linter for the pipeline that ships it.** Vacuum reads what PostgreSQL already knows about itself — `pg_stat_user_tables`, `pg_stat_user_indexes`, `pg_stat_activity`, `pg_stat_database`, `pg_stat_statements`, `pg_class` — and turns it into a page that says what is wrong, what it is costing you, and the statement that would put it right. It shows you that statement. It never runs it. -> **Status: 1.1.0.** The public API is frozen — see [What semver covers](#what-semver-covers). A breaking change to the rule contracts, `Finding`, the value objects, the configuration keys or the `--format=json` documents now requires a major version. +**It does not need a production database to be useful.** `vacuum:lint` reads the catalog rather than the statistics, so it has something to say against the empty Postgres container your test job already starts: a foreign key with no index behind it, a primary key that stops accepting rows at two billion, a table nothing can address a single row of. One line in the workflow you already have — + +```yaml +- run: php artisan vacuum:lint --format=github +``` + +— and the finding arrives as an annotation on the pull request that introduced it, on the line that introduced it. See [Linting the schema](#linting-the-schema). + +> **Status: 1.2.0.** The public API is frozen — see [What semver covers](#what-semver-covers). A breaking change to the rule contracts, `Finding`, the value objects, the configuration keys or the `--format=json` documents now requires a major version. ## Quick start @@ -31,7 +39,9 @@ Then open `/vacuum`. That is all of it: the installer publishes the config and a ![How Vacuum works: six catalogs PostgreSQL maintains, thirteen rules, and a finding carrying the statement that fixes it](art/how-it-works.png) -Already running a Filament panel? `php artisan vacuum:install --filament` puts the same data inside it — see [Inside Filament](#inside-filament). Want it in CI instead of in a browser? `php artisan vacuum:check` — see [In your pipeline](#in-your-pipeline). +Already running a Filament panel? `php artisan vacuum:install --filament` puts the same data inside it — see [Inside Filament](#inside-filament). + +**In a pipeline there are two commands, and they answer different questions.** `vacuum:check` runs the full advisor against a database that has been *running* — see [In your pipeline](#in-your-pipeline). `vacuum:lint` runs against one that has only been *migrated*, which is what a test job actually has — see [Linting the schema](#linting-the-schema). The first belongs on a schedule against staging; the second belongs in `require-dev`, on every push. ## Contents @@ -41,7 +51,7 @@ Already running a Filament panel? `php artisan vacuum:install --filament` puts t - [The standalone dashboard](#the-standalone-dashboard) · [Inside Filament](#inside-filament) - [Who may look](#who-may-look) · [Which database](#which-database) - [In your pipeline](#in-your-pipeline) — `vacuum:check`, and failing a build -- [Linting the schema](#linting-the-schema) — `vacuum:lint`, and what is wrong before a row exists +- [Linting the schema](#linting-the-schema) — `vacuum:lint`, a baseline, and annotations on the pull request - [History over time](#history-over-time) — direction, forecasts, and what changed - [The SQL console](#the-sql-console) — and what actually makes it safe - [Tuning the thresholds](#tuning-the-thresholds) · [Writing your own rule](#writing-your-own-rule) · [Restyling the dashboard](#restyling-the-dashboard) @@ -231,7 +241,7 @@ php artisan vacuum:check --format=json # score, grade, deductions, finding Two things worth knowing. It **never writes** — the remediation is printed for you to read and decide on, exactly as it is on the page. And if Vacuum is disabled it **fails rather than passing**: a check that goes green because it never looked is worse than no check at all. -### Linting the schema +## Linting the schema `vacuum:check` reads what the database has been *doing*, and a pipeline's database has not done anything. A Postgres container ninety seconds old with the migrations @@ -245,11 +255,13 @@ ever used is exactly the kind of green number this package exists to argue again php artisan vacuum:lint ``` +![vacuum:lint in a pipeline: the workflow step on the left, and the finding as an annotation on the pull request diff on the right](art/lint-in-ci.png) + | Rule | Finds | | --- | --- | | `unindexed-foreign-key` | Foreign keys PostgreSQL created no index for, which `->constrained()` never does | | `foreign-key-type-mismatch` | A key referencing a different type, so the index exists and cannot be used | -| `int4-primary-key` | A primary key that stops accepting rows at 2,147,483,647 | +| `narrow-primary-key` | A primary key too narrow to keep counting -- `integer` or `smallint` -- that stops accepting rows the moment it runs out of values | | `missing-primary-key` | Tables nothing can address a single row of | | `unindexed-morphs` | A polymorphic pair with no composite index leading on the type | | `json-not-jsonb` | `json` where `jsonb` was almost certainly meant | @@ -272,6 +284,61 @@ health score. Adding rules to that score would silently re-grade every existing installation on a `composer update`, and a grade that moves for a reason nobody asked for is worse than one rule fewer. +### Adopting it on a schema that predates it + +Run `vacuum:lint` on a five-year-old application and it will find a great many +things. That is accurate and completely useless: nobody is going to fix four +hundred findings this afternoon, and a build that is red for reasons nobody +intends to act on is a build people learn to ignore. + +So write down what is already there, and let the linter tell you only what is new: + +```bash +php artisan vacuum:lint --generate-baseline +``` + +That writes `vacuum-baseline.json`. **Commit it.** From then on the outstanding +findings are excused and anything new fails the build, which is the only question +worth asking of a legacy schema. + +A baseline matches on the rule and the subject and on nothing else, so rewording a +rule — or making it more serious in a later release — never invalidates the file +you committed. When an entry stops matching anything, because somebody fixed it, +`vacuum:lint` says so as an `Info` finding rather than quietly carrying it: a +baseline nobody prunes becomes a place the next defect hides. + +```bash +php artisan vacuum:lint --no-baseline # report everything, baseline or not +php artisan vacuum:lint --baseline=path # somewhere other than the default +``` + +The score is computed over what is left after suppression, and the count of what was +suppressed is printed with the text output, carried as `suppressed` in the JSON document, +and emitted as a `::notice` for `--format=github`. A number that quietly ignored four +hundred findings would be the kind of green this package exists to argue against — +and the pull request is the one place that number matters most. + +### On the pull request + +`--format=github` emits GitHub Actions workflow commands, so each finding lands as +an annotation on the diff rather than in a log nobody opens. + +```yaml +- run: php artisan vacuum:lint --format=github +``` + +Findings are traced back to the migration that introduced them by parsing +`database/migrations` with PHP's own tokenizer — the same technique the Filament +installer uses, and with the same refusal to guess. A migration whose table name is +a variable, or that does not parse, yields no anchor; the finding is still +reported, without a file and a line. + +**If you have run `php artisan schema:dump --prune`, expect few anchors.** That flag deletes +`database/migrations` after squashing the schema into `database/schema/*.sql`, so the files +that declared your columns are gone and there is nothing left to trace to. Plain +`schema:dump` keeps the migrations and is unaffected. The findings are the same either way; +only the annotations lose their line numbers. + ## History over time Vacuum is point-in-time by default: every page and every `vacuum:check` reads the database as it is this instant. Switch history on and it records a snapshot on a schedule, so it can tell you which way a number is *moving* — bloat that is growing, a freeze age climbing since the last time anything froze it, a cache-hit ratio measured over the last hour rather than over the life of the server. @@ -436,6 +503,8 @@ From 1.0 — and from 1.1 where a line says so — these are public API and a br - **The `vacuum:check --format=json` and `vacuum:lint --format=json` documents**, which are what a pipeline parses. - **Route names** (`vacuum.dashboard` and the rest) and the `Vacuum::auth()` gate. - **The `SCHEMA_RULES` tag** (1.1), alongside the others a custom rule is registered under. +- **The baseline file format** (1.2). It is committed to your repository, which makes it an interface whether or not it is called one. Keys may be added; the `findings` map will not change meaning. +- **`vacuum:lint --format=github`** (1.2) as an accepted value, and its severity mapping. The exact wording of an annotation is not covered. Explicitly **not** covered, and free to change in a minor release: @@ -443,6 +512,7 @@ Explicitly **not** covered, and free to change in a minor release: - The Blade views. Publishing them is supported; the markup inside them is not frozen. - Everything under `Internals` and `Learn`. Both are teaching surfaces, and pinning their shape would freeze the explanation as well as the code. - Anything marked `@internal`. +- `MigrationMap` and `SourceLocation`, which are console implementation detail rather than something a rule or a pipeline consumes. Vacuum supports the PostgreSQL major versions the PostgreSQL project still supports, and CI runs the suite against each of them. A major going end-of-life is a minor release here, not a major one. diff --git a/art/lint-in-ci.png b/art/lint-in-ci.png new file mode 100644 index 0000000000000000000000000000000000000000..b6bcb0381da91d3f2c5ba2a8ac3f01fd26550ec5 GIT binary patch literal 133783 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 aa8IZAp2F<#3Jv@77Zkv%GF4KhpZ*WqZxQqW literal 0 HcmV?d00001 diff --git a/art/make_readme_art.py b/art/make_readme_art.py index 68a1992..2ea50ae 100644 --- a/art/make_readme_art.py +++ b/art/make_readme_art.py @@ -580,9 +580,152 @@ def cli_check() -> None: a.save("cli-check.png") +# --------------------------------------------------------------------------- +# 6. vacuum:lint in a pipeline +# --------------------------------------------------------------------------- + +WORKFLOW = [ + ("services:", DIM), + (" postgres: { image: postgres:17 }", GRAY), + ("", None), + ("steps:", DIM), + (" - run: php artisan migrate --force", GRAY), + (" - run: php artisan vacuum:lint --format=github", WHITE), +] + +# The diff as a reviewer sees it: the line that introduced the finding is the +# line the finding lands on. +DIFF = [ + (" ", "12", "Schema::create('orders', function (Blueprint $table) {", GRAY, False), + ("+", "13", " $table->id();", GRAY, True), + ("+", "14", " $table->foreignId('customer_id');", WHITE, True), + (" ", "15", "});", GRAY, False), +] + + +def lint_pr() -> None: + W, H = 1600, 620 + a = Art(W, H) + + a.text( + W / 2, + 48, + "The database is ninety seconds old and has no rows in it. " + "The finding still lands on the line that caused it.", + size=17, + fill=GRAY, + anchor="mm", + ) + + top, ph = 108, 400 + ax, aw = 70, 520 + bx, bw = 640, 890 + + a.panel(ax, top, aw, ph) + a.panel(bx, top, bw, ph) + a.arrow(600, top + ph / 2, 632, colour=DIM) + + # --- A: the workflow + y = top + 26 + a.text(ax + 24, y, "IN YOUR WORKFLOW", size=12, bold=True, fill=GRAY) + y += 36 + a.rect(ax + 24, y, aw - 48, 172, r=8, fill=(14, 15, 18), outline=LINE, width=1) + ly = y + 20 + for line, colour in WORKFLOW: + if line: + a.text(ax + 40, ly, line, size=13, fill=colour) + ly += 25 + y += 196 + + a.chip(ax + 24, y, "require-dev", TEAL, size=12) + y += 46 + for ln in ( + "No production database, no credentials, no", + "extension and no superuser. Every rule here", + "is answerable the moment migrate finishes.", + ): + a.text(ax + 24, y, ln, size=13.5, fill=DIM) + y += 22 + + # --- B: the pull request + y = top + 26 + a.text(bx + 24, y, "ON THE PULL REQUEST", size=12, bold=True, fill=GRAY) + y += 30 + a.text( + bx + 24, + y, + "database/migrations/2024_01_11_000000_create_orders_table.php", + size=13, + fill=DIM, + ) + y += 30 + + for mark, number, code, colour, added in DIFF: + if added: + a.rect(bx + 24, y - 4, bw - 48, 26, r=4, fill=(*TEAL, 16)) + a.text(bx + 34, y, number, size=13, fill=(70, 74, 82)) + a.text(bx + 72, y, mark, size=14, bold=True, fill=TEAL if added else DIM) + a.text(bx + 92, y, code, size=14, fill=colour) + y += 26 + + # the annotation, hung under the line that produced it + y += 16 + ah = 152 + a.rect(bx + 92, y, bw - 140, ah, r=8, fill=(14, 15, 18), outline=(*AMBER, 90), width=1) + a.rect(bx + 92, y, 4, ah, r=2, fill=AMBER) + + iy = y + 20 + cw = a.chip(bx + 116, iy, "WARNING", AMBER, size=11) + a.text(bx + 116 + cw + 14, iy + 12, "unindexed-foreign-key", size=12.5, fill=DIM, anchor="lm") + iy += 40 + a.text( + bx + 116, + iy, + "orders.customer_id has a foreign key and no index behind it.", + size=14, + fill=WHITE, + ) + iy += 26 + a.text( + bx + 116, + iy, + "PostgreSQL indexes a primary key and creates nothing for this.", + size=13, + fill=GRAY, + ) + iy += 30 + a.rect(bx + 116, iy, bw - 188, 32, r=6, fill=(21, 23, 28), outline=LINE, width=1) + a.text( + bx + 128, + iy + 16, + 'CREATE INDEX CONCURRENTLY ON "public"."orders" ("customer_id");', + size=12, + fill=TEAL, + anchor="lm", + ) + + # --- the verdict + by, bh = 526, 62 + a.rect(70, by, 1460, bh, r=10, fill=(*CORAL, 20), outline=(*CORAL, 70), width=1) + a.rect(70, by, 4, bh, r=2, fill=CORAL) + a.text( + 100, + by + bh / 2, + "Exit 1. A warning from vacuum:check is a database drifting; a warning from " + "vacuum:lint is a schema that was wrong the moment somebody typed it.", + size=17, + bold=True, + fill=CORAL, + anchor="lm", + ) + + a.save("lint-in-ci.png") + + if __name__ == "__main__": hero() how_it_works() scoring() safety() cli_check() + lint_pr() diff --git a/config/vacuum.php b/config/vacuum.php index 08561bb..4f47375 100644 --- a/config/vacuum.php +++ b/config/vacuum.php @@ -309,4 +309,29 @@ 'enabled' => env('VACUUM_LEARN_ENABLED', true), ], + /* + |-------------------------------------------------------------------------- + | Lint + |-------------------------------------------------------------------------- + | + | vacuum:lint reads the shape of the schema rather than its statistics, so it + | has something to say in a pipeline where every statistics-based rule finds + | nothing. These two keys are what it needs from the filesystem. + | + | 'baseline' is resolved against base_path() and is used automatically when + | the file exists. It is what makes the linter adoptable on a schema that + | predates it: without one, the first run on a legacy application prints + | several hundred findings and the only available response is to stop running + | it. + | + | 'migrations_path' is where findings are traced back to the line that + | introduced them. Null means database_path('migrations'). + | + */ + + 'lint' => [ + 'baseline' => env('VACUUM_LINT_BASELINE', 'vacuum-baseline.json'), + 'migrations_path' => env('VACUUM_LINT_MIGRATIONS_PATH'), + ], + ]; diff --git a/src/Advisor/Rules/ForeignKeyTypeMismatch.php b/src/Advisor/Rules/ForeignKeyTypeMismatch.php index 9dcdd03..1271b0b 100644 --- a/src/Advisor/Rules/ForeignKeyTypeMismatch.php +++ b/src/Advisor/Rules/ForeignKeyTypeMismatch.php @@ -35,7 +35,7 @@ * a parent that is the *narrower* side of the mismatch -- a legacy * increments('id') parent referenced by a newer foreignId() child, most often. * Narrowing the child there would entrench the very 32-bit ceiling - * int4-primary-key is warning about on the parent, and it would fail outright + * narrow-primary-key is warning about on the parent, and it would fail outright * the first time a value exceeds what the narrower type can hold. In both cases * the finding still fires, because the mismatch is real and still costs a scan * on every delete; only the ALTER is withheld, because this rule cannot know the @@ -153,7 +153,7 @@ private function narrowParentImpact(Constraint $key): string .'on the parent falls back to a scan. Here the parent is the narrow side of the mismatch: ' ."{$key->referencedTable}'s column is {$parentType} and this one is {$childType}. The fix " .'has to start on the parent -- widening it is what actually raises the ceiling that ' - .'int4-primary-key already warns about there -- and it has to happen before anything about ' + .'narrow-primary-key already warns about there -- and it has to happen before anything about ' .'this column changes: narrowing this column to match the parent would only entrench that ' .'same ceiling, and widening the child on its own does not touch the parent at all, so it ' .'would not help either. A constraint carries the referenced column\'s type but not its ' diff --git a/src/Advisor/Rules/Int4PrimaryKey.php b/src/Advisor/Rules/NarrowPrimaryKey.php similarity index 96% rename from src/Advisor/Rules/Int4PrimaryKey.php rename to src/Advisor/Rules/NarrowPrimaryKey.php index 71f4893..ac330d7 100644 --- a/src/Advisor/Rules/Int4PrimaryKey.php +++ b/src/Advisor/Rules/NarrowPrimaryKey.php @@ -13,7 +13,7 @@ use Heyosseus\Vacuum\Values\TableSchema; /** - * Finds primary keys counted in 32 bits. + * Finds primary keys counted in too few bits. * * This is the wraparound story told in a different register. A four-byte integer * counts to 2,147,483,647 and then the next insert fails, and like wraparound it @@ -39,7 +39,7 @@ * reach in months, so the finding would be pure noise -- and vacuum:lint's * default --fail-on=warning would fail that build on a table nobody can fix. */ -final readonly class Int4PrimaryKey implements SchemaRule +final readonly class NarrowPrimaryKey implements SchemaRule { /** What format_type renders for the integer types narrower than bigint. */ private const array NARROW = ['integer', 'smallint']; @@ -81,7 +81,7 @@ public function inspect(TableSchema $table): array $ceiling = $column->type === 'smallint' ? '32,767' : '2,147,483,647'; $findings[] = new Finding( - rule: 'int4-primary-key', + rule: 'narrow-primary-key', subject: $table->qualifiedName().'.'.$name, severity: Severity::Warning, summary: "The primary key column {$name} is {$column->type}, so it can count to {$ceiling} " diff --git a/src/Console/Commands/LintCommand.php b/src/Console/Commands/LintCommand.php index 18ba853..58f70bd 100644 --- a/src/Console/Commands/LintCommand.php +++ b/src/Console/Commands/LintCommand.php @@ -4,10 +4,13 @@ namespace Heyosseus\Vacuum\Console\Commands; +use Composer\InstalledVersions; use Heyosseus\Vacuum\Advisor\Finding; use Heyosseus\Vacuum\Advisor\Health; use Heyosseus\Vacuum\Advisor\SchemaAdvisor; +use Heyosseus\Vacuum\Console\Support\Baseline; use Heyosseus\Vacuum\Console\Support\FindingReporter; +use Heyosseus\Vacuum\Console\Support\GithubReporter; use Heyosseus\Vacuum\Console\Support\SeverityBar; use Illuminate\Console\Command; use Illuminate\Contracts\Config\Repository; @@ -34,9 +37,9 @@ final class LintCommand extends Command { protected $signature = 'vacuum:lint {--fail-on=warning : The lowest severity that should fail the command: critical, warning, info or never} - {--format=text : text for a person, json for anything else} + {--format=text : text for a person, json for a pipeline, github for pull request annotations} {--baseline= : Path to a baseline file} - {--generate-baseline : Write the baseline and exit} + {--generate-baseline : Write every current finding to the baseline and exit} {--no-baseline : Ignore any baseline that exists}'; protected $description = 'Inspect the schema for defects that are visible without any data'; @@ -45,6 +48,7 @@ public function handle( SchemaAdvisor $advisor, Repository $config, FindingReporter $reporter, + GithubReporter $github, ): int { // A gate that goes green because it never looked is worse than no gate. if ($config->get('vacuum.enabled') !== true) { @@ -55,15 +59,6 @@ public function handle( return self::INVALID; } - if ($this->baselineRequested()) { - $this->components->error( - 'Baselines are not in this release yet. Run without --baseline, --generate-baseline or ' - .'--no-baseline, or pin an earlier expectation of them.', - ); - - return self::INVALID; - } - $option = $this->option('fail-on'); $bar = SeverityBar::parse(is_string($option) ? $option : null); @@ -77,39 +72,162 @@ public function handle( } $findings = $advisor->findings(); - $health = Health::from($findings); - $failed = $bar->fails($findings); - if ($this->option('format') === 'json') { - $this->output->writeln($this->json($health, $findings, $failed)); + if ($this->option('generate-baseline') === true) { + $path = $this->baselinePath($config); + $document = Baseline::record($findings)->encode($this->version()); + + // A read-only checkout, a --baseline= pointing at a directory that does + // not exist, or a full disk all fail file_put_contents() the same way: + // silently, unless the return value is actually checked. + if (@file_put_contents($path, $document) !== strlen($document)) { + $this->components->error("Could not write the baseline to {$path}."); + + return self::FAILURE; + } + + $this->components->info( + count($findings).' findings written to '.$path.'. Commit it, and vacuum:lint will ' + .'report only what is new from now on.', + ); + + return self::SUCCESS; + } + + $baseline = $this->baseline($config); + $kept = []; + $suppressed = 0; + + foreach ($findings as $finding) { + if ($baseline->suppresses($finding)) { + $suppressed++; + + continue; + } + + $kept[] = $finding; + } + + // Stale entries are shown -- the reader needs to know the baseline wants + // pruning -- but never fed to the bar. They are Info because the defect + // being gone is good news, and good news must never be able to redden a + // build at --fail-on=info; fixing something is not a regression. + $display = [...$kept, ...$baseline->stale($findings)]; + + $health = Health::from($display); + $failed = $bar->fails($kept); + + $format = $this->option('format'); + + if ($format === 'json') { + $this->output->writeln($this->json($health, $display, $failed, $suppressed)); + } elseif ($format === 'github') { + $github->report($this->output, $display, $suppressed); + $this->appendStepSummary($github->summary($display)); } else { $reporter->report( $this->output, $health, - $findings, + $display, 'Every table has a key, every foreign key has an index, and every type lines up.', ); + + if ($suppressed > 0) { + $this->line(" {$suppressed} suppressed by the baseline."); + $this->newLine(); + } } return $failed ? self::FAILURE : self::SUCCESS; } /** - * Whether the caller asked for a baseline. The options are declared now so - * that the command's signature does not change when the feature lands; - * accepting them silently in the meantime would be worse than refusing them. + * The baseline in force, which is none when the caller said so or when there + * is no file to read. */ - private function baselineRequested(): bool + private function baseline(Repository $config): Baseline { - if ($this->option('generate-baseline') === true) { - return true; + if ($this->option('no-baseline') === true) { + return Baseline::none(); } - if ($this->option('no-baseline') === true) { - return true; + $path = $this->baselinePath($config); + + if (! is_file($path)) { + return Baseline::none(); + } + + $contents = file_get_contents($path); + + return $contents === false ? Baseline::none() : Baseline::decode($contents); + } + + private function baselinePath(Repository $config): string + { + $option = $this->option('baseline'); + + if (is_string($option) && $option !== '') { + return base_path($option); + } + + $configured = $config->get('vacuum.lint.baseline'); + + return base_path(is_string($configured) ? $configured : 'vacuum-baseline.json'); + } + + /** + * GitHub gives a job a file to append a summary to, and gives it only inside + * Actions -- so the variable's presence is the feature flag, and no + * configuration key is needed for something the runner either offers or does + * not. + * + * A failure here does not fail the command. The step summary is a convenience + * the runner offers on top of the annotations that already carry every + * finding; unlike the baseline, losing it loses nothing the caller depended + * on. It only must not be mistaken for having worked, so a failed append is + * reported rather than swallowed. + */ + private function appendStepSummary(string $markdown): void + { + $path = getenv('GITHUB_STEP_SUMMARY'); + + if (! is_string($path) || $path === '') { + return; + } + + if (@file_put_contents($path, $markdown, FILE_APPEND) !== strlen($markdown)) { + $this->components->warn("Could not append the step summary to {$path}."); + } + } + + /** + * Stamped into the file so a reader knows which rule set produced it. + * + * Vacuum's own version, not the application's. In every real installation the + * application is the root package, and Vacuum is a dependency underneath it -- + * reading the root would stamp the baseline with the app's own tag, or with + * "dev-main" for a checkout, which says nothing about which rule set wrote the + * file. Composer is asked for "heyosseus/vacuum" by name first, so the + * baseline carries Vacuum's own version wherever Composer can report one. + * + * The root package is the fallback, for whenever Composer cannot answer that + * lookup -- which includes this package's own test suite, where Vacuum *is* + * the root and the root's version is the only one there is to report. + */ + private function version(): string + { + if (InstalledVersions::isInstalled('heyosseus/vacuum')) { + $version = InstalledVersions::getPrettyVersion('heyosseus/vacuum'); + + if ($version !== null) { + return $version; + } } - return is_string($this->option('baseline')) && $this->option('baseline') !== ''; + /** @var array{pretty_version: string} $root */ + $root = InstalledVersions::getRootPackage(); + + return $root['pretty_version']; } /** @@ -121,12 +239,13 @@ private function baselineRequested(): bool * * @param list $findings */ - private function json(Health $health, array $findings, bool $failed): string + private function json(Health $health, array $findings, bool $failed, int $suppressed): string { return json_encode([ 'score' => $health->score, 'grade' => $health->grade->value, 'failed' => $failed, + 'suppressed' => $suppressed, 'deductions' => $health->deductions, 'findings' => array_map(static fn (Finding $finding): array => [ 'rule' => $finding->rule, diff --git a/src/Console/Support/Baseline.php b/src/Console/Support/Baseline.php new file mode 100644 index 0000000..476d9b5 --- /dev/null +++ b/src/Console/Support/Baseline.php @@ -0,0 +1,165 @@ +> $entries Rule to the subjects excused under it. + */ + private function __construct(private array $entries) {} + + public static function none(): self + { + return new self([]); + } + + /** + * @param list $findings + */ + public static function record(array $findings): self + { + $entries = []; + + foreach ($findings as $finding) { + $entries[$finding->rule][] = $finding->subject; + } + + foreach ($entries as $rule => $subjects) { + $unique = array_values(array_unique($subjects)); + sort($unique); + $entries[$rule] = $unique; + } + + ksort($entries); + + return new self($entries); + } + + /** + * A baseline that could not be read is no baseline at all. + * + * Reporting everything is the safe direction to fail in: a corrupt file + * should not take a pipeline down, and it must not silently excuse findings + * it never actually listed. + */ + public static function decode(string $json): self + { + try { + /** @var mixed $document */ + $document = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException) { + return self::none(); + } + + if (! is_array($document)) { + return self::none(); + } + + $findings = $document['findings'] ?? null; + + if (! is_array($findings)) { + return self::none(); + } + + $entries = []; + + foreach ($findings as $rule => $subjects) { + if (! is_string($rule)) { + continue; + } + if (! is_array($subjects)) { + continue; + } + + foreach ($subjects as $subject) { + if (is_string($subject)) { + $entries[$rule][] = $subject; + } + } + } + + return new self($entries); + } + + public function suppresses(Finding $finding): bool + { + return in_array($finding->subject, $this->entries[$finding->rule] ?? [], true); + } + + /** + * Entries that match nothing any more, as findings of their own. + * + * Reported rather than pruned silently, and Info rather than a fault: the + * defect being gone is good news, but a baseline nobody tidies becomes a + * place the next one hides. + * + * @param list $findings + * @return list + */ + public function stale(array $findings): array + { + $present = []; + + foreach ($findings as $finding) { + $present[$finding->rule.'|'.$finding->subject] = true; + } + + $stale = []; + + foreach ($this->entries as $rule => $subjects) { + foreach ($subjects as $subject) { + if (isset($present[$rule.'|'.$subject])) { + continue; + } + + $stale[] = new Finding( + rule: 'baseline-stale', + subject: $subject, + severity: Severity::Info, + summary: "The baseline still excuses {$rule} here, and the rule no longer fires.", + impact: 'Nothing is wrong with this subject any more. Regenerate the baseline so the ' + .'file describes what is actually outstanding, rather than accumulating entries ' + .'that excuse nothing and hide the next thing that does.', + ); + } + } + + return $stale; + } + + public function encode(string $version): string + { + return json_encode([ + 'generated_at' => date(DATE_ATOM), + 'vacuum' => $version, + 'findings' => $this->entries, + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)."\n"; + } +} diff --git a/src/Console/Support/GithubReporter.php b/src/Console/Support/GithubReporter.php new file mode 100644 index 0000000..986bfd0 --- /dev/null +++ b/src/Console/Support/GithubReporter.php @@ -0,0 +1,124 @@ + $findings + */ + public function report(OutputStyle $output, array $findings, int $suppressed = 0): void + { + foreach ($findings as $finding) { + $properties = ['title' => $finding->rule]; + $location = $this->map->locate($finding); + + if ($location instanceof SourceLocation) { + $properties = ['file' => $this->relative($location->file), 'line' => (string) $location->line] + $properties; + } + + $pairs = []; + + foreach ($properties as $key => $value) { + $pairs[] = $key.'='.$this->property($value); + } + + $output->writeln( + '::'.$this->level($finding->severity).' '.implode(',', $pairs) + .'::'.$this->message($finding->subject.' — '.$finding->summary), + ); + } + + // The one place a pull request most needs this number: four hundred + // findings suppressed by the baseline is exactly the fact a reviewer + // reading only the diff's annotations would otherwise never see. + if ($suppressed > 0) { + $output->writeln('::notice::'.$this->message("{$suppressed} suppressed by the baseline.")); + } + } + + /** + * @param list $findings + */ + public function summary(array $findings): string + { + $rows = ['| Severity | Rule | Subject |', '| --- | --- | --- |']; + + foreach ($findings as $finding) { + $rows[] = '| '.$finding->severity->value.' | '.$finding->rule.' | `'.$finding->subject.'` |'; + } + + return implode("\n", $rows)."\n"; + } + + private function level(Severity $severity): string + { + return match ($severity) { + Severity::Critical => 'error', + Severity::Warning => 'warning', + Severity::Info, Severity::Unknown => 'notice', + }; + } + + /** + * The percent sign is escaped first, or it would escape the escapes. + */ + private function message(string $value): string + { + return str_replace(['%', "\r", "\n"], ['%25', '%0D', '%0A'], $value); + } + + private function property(string $value): string + { + return str_replace([':', ','], ['%3A', '%2C'], $this->message($value)); + } + + /** + * The path GitHub can actually match against the diff. + * + * The map's paths are absolute, correctly, for every other consumer -- but + * GitHub resolves an annotation's `file` against the repository root, not the + * runner's filesystem, so `/home/runner/work/app/app/database/migrations/…` + * matches nothing and the annotation attaches to no line at all. A path + * outside base_path() -- a migrations directory configured elsewhere, or a + * test fixture -- is left exactly as the map produced it rather than mangled + * into something that looks relative but is not. + */ + private function relative(string $file): string + { + $normalized = str_replace('\\', '/', $file); + $base = rtrim(str_replace('\\', '/', base_path()), '/').'/'; + + return str_starts_with($normalized, $base) ? substr($normalized, strlen($base)) : $file; + } +} diff --git a/src/Schema/MigrationMap.php b/src/Schema/MigrationMap.php new file mode 100644 index 0000000..cc762f2 --- /dev/null +++ b/src/Schema/MigrationMap.php @@ -0,0 +1,121 @@ +|null */ + private ?array $entries = null; + + public function __construct( + private readonly string $directory, + private readonly MigrationScanner $scanner, + ) {} + + /** + * A migration never writes a schema -- `Schema::create('orders', ...)`, not + * `Schema::create('public.orders', ...)` -- unless the application is + * multi-schema and deliberately qualifies it, so the scanner's entries are + * ordinarily unqualified even when the finding is not. Reducing straight to + * the unqualified key, as this used to, means `tenant.orders` and + * `public.orders` collide on the one entry `orders` records: a finding in + * the tenant schema anchors to the default schema's migration, which is + * wrong precisely on the multi-schema and multi-tenant Postgres this + * package's audience runs. The qualified key is tried first, so a migration + * that does write the schema wins the collision it would otherwise lose to. + */ + public function locate(Finding $finding): ?SourceLocation + { + if ($finding->table === null) { + return null; + } + + $entries = $this->entries(); + $qualified = $finding->table; + $table = $this->unqualified($qualified); + $column = $this->column($finding->subject, $qualified); + + if ($column !== null) { + $onQualifiedTable = $entries[$qualified.'.'.$column] ?? null; + + if ($onQualifiedTable instanceof SourceLocation) { + return $onQualifiedTable; + } + + $onUnqualifiedTable = $entries[$table.'.'.$column] ?? null; + + if ($onUnqualifiedTable instanceof SourceLocation) { + return $onUnqualifiedTable; + } + } + + return $entries[$qualified] ?? $entries[$table] ?? null; + } + + /** + * The part of a subject naming a column of the given table, or null when the + * subject is not one -- a table-level finding, or an index finding whose + * subject is the index rather than the table it belongs to. + */ + private function column(string $subject, string $table): ?string + { + $prefix = $table.'.'; + + return str_starts_with($subject, $prefix) ? substr($subject, strlen($prefix)) : null; + } + + /** + * Scanned once per run, and only when something asks. + * + * @return array + */ + private function entries(): array + { + if ($this->entries !== null) { + return $this->entries; + } + + $entries = []; + $files = glob(rtrim($this->directory, '/\\').DIRECTORY_SEPARATOR.'*.php'); + + foreach ($files === false ? [] : $files as $file) { + $source = @file_get_contents($file); + + if ($source === false) { + continue; + } + + foreach ($this->scanner->scan($source) as $key => $line) { + $entries[$key] ??= new SourceLocation($file, $line); + } + } + + return $this->entries = $entries; + } + + private function unqualified(string $table): string + { + $dot = strrpos($table, '.'); + + return $dot === false ? $table : substr($table, $dot + 1); + } +} diff --git a/src/Schema/MigrationScanner.php b/src/Schema/MigrationScanner.php new file mode 100644 index 0000000..9d76409 --- /dev/null +++ b/src/Schema/MigrationScanner.php @@ -0,0 +1,237 @@ + Keys are "table" and "table.column"; values are 1-based lines. + */ + public function scan(string $source): array + { + try { + // TOKEN_PARSE is what makes a genuinely broken migration yield + // nothing rather than whatever the tokenizer's lenient best-effort + // reading of invalid source happens to produce. + /** @var list $tokens */ + $tokens = @token_get_all($source, TOKEN_PARSE); + } catch (CompileError) { + // ParseError extends CompileError, so this also catches source that + // merely fails to parse; a valid parse can still fail to compile, such + // as `abstract final class C {}`, and that has to be caught too. + return []; + } + + $entries = []; + $table = null; + $depth = 0; + $scope = 0; + + foreach ($tokens as $index => $token) { + if ($token === '{') { + $depth++; + + continue; + } + + if ($token === '}') { + $depth--; + + if ($table !== null && $depth < $scope) { + $table = null; + } + + continue; + } + + if (! is_array($token)) { + continue; + } + + $opened = $this->schemaCall($tokens, $index); + + if ($opened !== null) { + $table = $opened[0]; + $scope = $depth + 1; + $entries[$table] ??= $opened[1]; + + continue; + } + + if ($table === null) { + continue; + } + + if ($token[0] !== T_OBJECT_OPERATOR) { + continue; + } + + foreach ($this->columns($tokens, $index) as $column) { + $entries[$table.'.'.$column] ??= $token[2]; + } + } + + return $entries; + } + + /** + * The table a `Schema::create('x', ...)` or `Schema::table('x', ...)` opens, + * with the line it sits on, or null if this is not one. + * + * The receiver has to be checked, not just the method name: `DB::table('x')` + * opens no schema scope at all -- it runs a query -- but shares the name + * `table` with the call that actually declares columns. Without this check a + * data-backfill migration's `DB::table('orders')` would open an `orders` + * scope of its own, and every `$var->method('literal')` after it in the same + * method body would register as a column of a table this migration never + * touched. + * + * A bare `Schema` is not the only way this receiver tokenizes, though: a + * fully-qualified call such as `\Illuminate\Support\Facades\Schema::create` + * tokenizes its receiver as a single T_NAME_FULLY_QUALIFIED token holding the + * whole path, and an aliased import can shorten or lengthen that path + * further, so both a fully- and a partially-qualified name are accepted as + * long as they end with `\Schema`. A name that merely contains the word, + * such as `MySchema`, is not accepted -- that is a different class that only + * happens to share a suffix. + * + * @param list $tokens + * @return array{0: string, 1: int}|null + */ + private function schemaCall(array $tokens, int $index): ?array + { + $token = $tokens[$index]; + + if (! is_array($token) || $token[0] !== T_DOUBLE_COLON) { + return null; + } + + $receiver = $tokens[$index - 1] ?? null; + + if (! is_array($receiver) || ! $this->isSchemaReceiver($receiver)) { + return null; + } + + $method = $tokens[$index + 1] ?? null; + + if (! is_array($method) || ! in_array($method[1], ['create', 'table'], true)) { + return null; + } + + $name = $tokens[$index + 3] ?? null; + + if (! is_array($name) || $name[0] !== T_CONSTANT_ENCAPSED_STRING) { + return null; + } + + return [trim($name[1], "'\""), $method[2]]; + } + + /** + * Whether a token naming the left side of a `::` is some spelling of the + * `Schema` class: a bare `T_STRING` for an unqualified reference, or a + * `T_NAME_FULLY_QUALIFIED` / `T_NAME_QUALIFIED` whose path ends with + * `\Schema` for a fully- or partially-qualified one. + * + * @param array{0: int, 1: string, 2: int} $receiver + */ + private function isSchemaReceiver(array $receiver): bool + { + if ($receiver[0] === T_STRING) { + return $receiver[1] === 'Schema'; + } + + if ($receiver[0] === T_NAME_FULLY_QUALIFIED || $receiver[0] === T_NAME_QUALIFIED) { + return str_ends_with($receiver[1], '\\Schema'); + } + + return false; + } + + /** + * The column names a `->method('x')` call creates, which is usually one and + * for a morphs pair is two. + * + * A chained call -- `$table->foreignId('customer_id')->constrained('users')` + * -- has an object operator too, and its receiver is `foreignId()`'s return + * value rather than the Blueprint. Treating 'users' as a column of orders + * would invent one the table does not have, so the receiver is checked + * before anything else here is even asked. The Blueprint's own parameter is + * conventionally named $table, but this accepts any variable rather than + * that one name specifically: matching a particular name would still be a + * guess, and this class declines rather than guesses. + * + * @param list $tokens + * @return list + */ + private function columns(array $tokens, int $index): array + { + if (! $this->hasVariableReceiver($tokens, $index)) { + return []; + } + + $method = $tokens[$index + 1] ?? null; + + if (! is_array($method) || $method[0] !== T_STRING) { + return []; + } + + $argument = $tokens[$index + 3] ?? null; + + if (! is_array($argument) || $argument[0] !== T_CONSTANT_ENCAPSED_STRING) { + return []; + } + + $name = trim($argument[1], "'\""); + + if (in_array($method[1], self::MORPHS, true)) { + return [$name.'_type', $name.'_id']; + } + + return [$name]; + } + + /** + * Whether the token immediately before an object operator -- skipping + * whitespace, since `$table ->foreignId('x')` is legal PHP -- is a + * variable. True for `$table->foreignId(...)`; false for the `)` a chained + * call's arrow actually follows. + * + * @param list $tokens + */ + private function hasVariableReceiver(array $tokens, int $index): bool + { + $previous = $index - 1; + + while ($previous >= 0 && is_array($tokens[$previous]) && $tokens[$previous][0] === T_WHITESPACE) { + $previous--; + } + + return $previous >= 0 && is_array($tokens[$previous]) && $tokens[$previous][0] === T_VARIABLE; + } +} diff --git a/src/Schema/SourceLocation.php b/src/Schema/SourceLocation.php new file mode 100644 index 0000000..d097977 --- /dev/null +++ b/src/Schema/SourceLocation.php @@ -0,0 +1,22 @@ +app->bind(SyntaxChecker::class, PhpLintChecker::class); + $this->app->bind(MigrationMap::class, function (Application $app): MigrationMap { + /** @var Repository $config */ + $config = $app->make(Repository::class); + $configured = $config->get('vacuum.lint.migrations_path'); + + return new MigrationMap( + is_string($configured) ? $configured : database_path('migrations'), + new MigrationScanner, + ); + }); + // Every panel wants to know what the server supports, and the answer // cannot change underneath a single request. $this->app->singleton( @@ -300,7 +313,7 @@ public function register(): void [ UnindexedForeignKey::class, ForeignKeyTypeMismatch::class, - Int4PrimaryKey::class, + NarrowPrimaryKey::class, MissingPrimaryKey::class, UnindexedMorphs::class, JsonNotJsonb::class, diff --git a/tests/Feature/Command/LintCommandTest.php b/tests/Feature/Command/LintCommandTest.php index 2e762a0..9da8bbe 100644 --- a/tests/Feature/Command/LintCommandTest.php +++ b/tests/Feature/Command/LintCommandTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Composer\InstalledVersions; use Heyosseus\Vacuum\Advisor\Advisor; use Heyosseus\Vacuum\Advisor\Finding; use Heyosseus\Vacuum\Advisor\Inspection; @@ -41,6 +42,53 @@ function schemaFinding(Severity $severity = Severity::Warning): Finding ); } +/** + * Runs the callback with Composer's InstalledVersions replaced by a single, + * fully controlled dataset, then restores exactly what was there before. + * + * InstalledVersions::reload() alone is not enough for this: it clears the + * per-vendor-directory cache but not the flag that decides whether the real + * registered class loaders are consulted at all, so this package's own + * vendor/composer/installed.php -- which lists heyosseus/vacuum whether or not + * it is the root, because in this checkout it genuinely is -- would still + * answer every lookup before the reloaded data got a turn. Forcing + * canGetVendors false is what actually isolates the test from the real + * installation this suite runs inside of. + * + * @param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: array, dev: bool}, versions: array} $data + */ +function withInstalledVersions(array $data, Closure $callback): void +{ + $canGetVendors = new ReflectionProperty(InstalledVersions::class, 'canGetVendors'); + $installed = new ReflectionProperty(InstalledVersions::class, 'installed'); + $installedByVendor = new ReflectionProperty(InstalledVersions::class, 'installedByVendor'); + $installedIsLocalDir = new ReflectionProperty(InstalledVersions::class, 'installedIsLocalDir'); + + $original = [ + $canGetVendors->getValue(), + $installed->getValue(), + $installedByVendor->getValue(), + $installedIsLocalDir->getValue(), + ]; + + $canGetVendors->setValue(null, false); + InstalledVersions::reload($data); + + try { + $callback(); + } finally { + $canGetVendors->setValue(null, $original[0]); + $installed->setValue(null, $original[1]); + $installedByVendor->setValue(null, $original[2]); + $installedIsLocalDir->setValue(null, $original[3]); + } +} + +beforeEach(function (): void { + $this->originalStepSummary = getenv('GITHUB_STEP_SUMMARY'); + $this->stepSummaryFile = null; +}); + it('passes on a clean schema', function (): void { stubAdvisor(); @@ -97,17 +145,276 @@ function schemaFinding(Severity $severity = Severity::Warning): Finding ->and($document['findings'][0]['table'])->toBe('public.orders'); }); -it('says the baseline is not here yet rather than ignoring the flag', function (): void { - // The option exists so that the command's signature does not change when the - // baseline lands. Accepting it silently would be worse than refusing it. +it('suppresses a finding listed in the baseline', function (): void { + stubAdvisor(schemaFinding()); + + $path = base_path('vacuum-baseline.json'); + file_put_contents($path, json_encode([ + 'findings' => ['unindexed-foreign-key' => ['public.orders.customer_id']], + ])); + + $exit = Artisan::call('vacuum:lint', ['--no-interaction' => true]); + + expect($exit)->toBe(0); +}); + +it('says how many it suppressed rather than quietly scoring around them', function (): void { + // A green number computed over findings it never mentioned is exactly the + // lie this package exists to argue against. + stubAdvisor(schemaFinding()); + + $path = base_path('vacuum-baseline.json'); + file_put_contents($path, json_encode([ + 'findings' => ['unindexed-foreign-key' => ['public.orders.customer_id']], + ])); + + Artisan::call('vacuum:lint', ['--no-interaction' => true]); + $output = Artisan::output(); + + expect($output)->toContain('1 suppressed by the baseline'); +}); + +it('ignores the baseline when told to', function (): void { + stubAdvisor(schemaFinding()); + + $path = base_path('vacuum-baseline.json'); + file_put_contents($path, json_encode([ + 'findings' => ['unindexed-foreign-key' => ['public.orders.customer_id']], + ])); + + $exit = Artisan::call('vacuum:lint', ['--no-baseline' => true, '--no-interaction' => true]); + + expect($exit)->toBe(1); +}); + +it('writes a baseline and exits zero even with findings outstanding', function (): void { + stubAdvisor(schemaFinding()); + + $path = base_path('vacuum-baseline.json'); + + $exit = Artisan::call('vacuum:lint', ['--generate-baseline' => true, '--no-interaction' => true]); + + $written = json_decode((string) file_get_contents($path), true); + + expect($exit)->toBe(0) + ->and($written['findings']['unindexed-foreign-key'])->toBe(['public.orders.customer_id']); +}); + +it('stamps the baseline with vacuum\'s own installed version, not the application\'s', function (): void { + stubAdvisor(schemaFinding()); + + $path = base_path('vacuum-baseline.json'); + + withInstalledVersions([ + 'root' => [ + 'name' => 'acme/app', + 'pretty_version' => 'v1.0.0', + 'version' => '1.0.0.0', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__, + 'aliases' => [], + 'dev' => true, + ], + 'versions' => [ + 'acme/app' => ['pretty_version' => 'v1.0.0', 'dev_requirement' => false], + 'heyosseus/vacuum' => ['pretty_version' => '1.2.3', 'dev_requirement' => false], + ], + ], function (): void { + Artisan::call('vacuum:lint', ['--generate-baseline' => true, '--no-interaction' => true]); + }); + + $written = json_decode((string) file_get_contents($path), true); + + expect($written['vacuum'])->toBe('1.2.3'); +}); + +it('falls back to the root package version when composer cannot report vacuum by name', function (): void { + // This is what makes this package's own test suite work: there vacuum is + // genuinely the root, and the root's version is the only one there is. + stubAdvisor(schemaFinding()); + + $path = base_path('vacuum-baseline.json'); + + withInstalledVersions([ + 'root' => [ + 'name' => 'heyosseus/vacuum', + 'pretty_version' => 'dev-feature', + 'version' => 'dev-feature', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__, + 'aliases' => [], + 'dev' => true, + ], + 'versions' => [ + // No heyosseus/vacuum entry: Composer has nothing to report by name. + ], + ], function (): void { + Artisan::call('vacuum:lint', ['--generate-baseline' => true, '--no-interaction' => true]); + }); + + $written = json_decode((string) file_get_contents($path), true); + + expect($written['vacuum'])->toBe('dev-feature'); +}); + +it('fails rather than claiming success when the baseline cannot be written', function (): void { + // The easiest trigger for an unwritable path: a --baseline= pointing into a + // directory that was never created. + stubAdvisor(schemaFinding()); + + $exit = Artisan::call('vacuum:lint', [ + '--generate-baseline' => true, + '--baseline' => 'no-such-directory/vacuum-baseline.json', + '--no-interaction' => true, + ]); + + expect($exit)->toBe(1) + ->and(Artisan::output())->toContain('Could not write the baseline to'); +}); + +it('reads a baseline from an explicit path', function (): void { + stubAdvisor(schemaFinding()); + + $path = base_path('custom-baseline.json'); + file_put_contents($path, json_encode([ + 'findings' => ['unindexed-foreign-key' => ['public.orders.customer_id']], + ])); + + $exit = Artisan::call('vacuum:lint', ['--baseline' => 'custom-baseline.json', '--no-interaction' => true]); + + expect($exit)->toBe(0); +}); + +it('reports a baseline entry that no longer matches, without failing the build', function (): void { stubAdvisor(); - $this->artisan('vacuum:lint', ['--generate-baseline' => true, '--no-interaction' => true]) - ->assertExitCode(2); + $path = base_path('vacuum-baseline.json'); + file_put_contents($path, json_encode([ + 'findings' => ['unindexed-foreign-key' => ['public.orders.long_since_fixed']], + ])); - $this->artisan('vacuum:lint', ['--no-baseline' => true, '--no-interaction' => true]) - ->assertExitCode(2); + $exit = Artisan::call('vacuum:lint', ['--no-interaction' => true]); + $output = Artisan::output(); - $this->artisan('vacuum:lint', ['--baseline' => 'baseline.json', '--no-interaction' => true]) - ->assertExitCode(2); + expect($exit)->toBe(0)->and($output)->toContain('baseline-stale'); +}); + +it('does not fail the build on a stale entry even at --fail-on=info', function (): void { + // Stale entries are Info because the defect being gone is good news, and + // good news must never be able to redden a build. Before this fix they were + // fed to the same bar as everything else, so fixing something could fail + // the build the moment somebody set --fail-on=info. + stubAdvisor(); + + $path = base_path('vacuum-baseline.json'); + file_put_contents($path, json_encode([ + 'findings' => ['unindexed-foreign-key' => ['public.orders.long_since_fixed']], + ])); + + $exit = Artisan::call('vacuum:lint', ['--fail-on' => 'info', '--no-interaction' => true]); + + expect($exit)->toBe(0); +}); + +it('counts suppressed findings in the json document', function (): void { + stubAdvisor(schemaFinding()); + + $path = base_path('vacuum-baseline.json'); + file_put_contents($path, json_encode([ + 'findings' => ['unindexed-foreign-key' => ['public.orders.customer_id']], + ])); + + Artisan::call('vacuum:lint', ['--format' => 'json', '--no-interaction' => true]); + $document = json_decode(Artisan::output(), true); + + expect($document['suppressed'])->toBe(1)->and($document['findings'])->toBe([]); +}); + +it('emits github annotations when asked for them', function (): void { + stubAdvisor(schemaFinding()); + + Artisan::call('vacuum:lint', ['--format' => 'github', '--no-interaction' => true]); + + // Read once: Artisan::output() flushes the buffer. + $output = Artisan::output(); + + expect($output)->toContain('::warning ')->and($output)->not->toContain('/ 100'); +}); + +it('reports how many findings the baseline suppressed as a notice', function (): void { + // The pull request is the one place four hundred silently-suppressed + // findings matter most, so this format must not stay quiet about the count + // the way the original implementation did. + stubAdvisor(schemaFinding()); + + $path = base_path('vacuum-baseline.json'); + file_put_contents($path, json_encode([ + 'findings' => ['unindexed-foreign-key' => ['public.orders.customer_id']], + ])); + + Artisan::call('vacuum:lint', ['--format' => 'github', '--no-interaction' => true]); + + expect(Artisan::output())->toContain('::notice::1 suppressed by the baseline.'); +}); + +it('appends a summary table when the runner offers one', function (): void { + stubAdvisor(schemaFinding()); + + $summary = tempnam(sys_get_temp_dir(), 'vacuum-summary'); + $this->stepSummaryFile = $summary; + putenv('GITHUB_STEP_SUMMARY='.$summary); + + Artisan::call('vacuum:lint', ['--format' => 'github', '--no-interaction' => true]); + + $written = (string) file_get_contents($summary); + + expect($written)->toContain('| Severity | Rule | Subject |'); +}); + +it('warns without failing the command when the step summary cannot be written', function (): void { + // Unlike the baseline, losing the step summary loses nothing the caller + // depended on -- every finding is already in the annotations above it -- so + // this must not be mistaken for having worked, but it also must not fail + // the build over a convenience the runner merely offers. + stubAdvisor(); + + $directory = sys_get_temp_dir().'/vacuum-step-summary-'.bin2hex((string) getmypid()); + putenv('GITHUB_STEP_SUMMARY='.$directory.'/no-such-directory/summary.md'); + + $exit = Artisan::call('vacuum:lint', ['--format' => 'github', '--no-interaction' => true]); + + expect($exit)->toBe(0) + ->and(Artisan::output())->toContain('Could not append the step summary to'); +}); + +afterEach(function (): void { + // A leaked baseline file would silently suppress findings in every later + // test in the suite, so cleanup here does not depend on a preceding test + // actually reaching its own unlink() call. + foreach (['vacuum-baseline.json', 'custom-baseline.json'] as $file) { + $path = base_path($file); + + if (is_file($path)) { + unlink($path); + } + } + + // Only the file this file's own tests created, and only if one was made -- + // GITHUB_STEP_SUMMARY is set by the real runner to a file the job owns + // inside this project's own GitHub Actions run, and deleting whatever it + // currently points at would delete that runner's summary out from under it. + if (is_string($this->stepSummaryFile) && is_file($this->stepSummaryFile)) { + unlink($this->stepSummaryFile); + } + + // Restored to whatever it was before this file's tests ran, rather than + // cleared unconditionally, for the same reason: outside this suite the + // variable is real and belongs to the runner, not to this test file. + if ($this->originalStepSummary === false) { + putenv('GITHUB_STEP_SUMMARY'); + } else { + putenv('GITHUB_STEP_SUMMARY='.$this->originalStepSummary); + } }); diff --git a/tests/Unit/Advisor/Rules/ForeignKeyTypeMismatchTest.php b/tests/Unit/Advisor/Rules/ForeignKeyTypeMismatchTest.php index 2bcea23..7b10e49 100644 --- a/tests/Unit/Advisor/Rules/ForeignKeyTypeMismatchTest.php +++ b/tests/Unit/Advisor/Rules/ForeignKeyTypeMismatchTest.php @@ -66,7 +66,7 @@ function typedKey(array $here, array $there, string $column = 'customer_id'): Ta // A legacy increments('id') parent referenced by a newer foreignId() child // is the most common real shape of this defect: the parent is integer, the // child is bigint. Narrowing the child to match would entrench the very - // ceiling int4-primary-key already warns about on the parent, and it would + // ceiling narrow-primary-key already warns about on the parent, and it would // fail outright once a value in the child exceeds what integer can hold. $findings = app(ForeignKeyTypeMismatch::class)->inspect(typedKey(['bigint'], ['integer'])); diff --git a/tests/Unit/Advisor/Rules/Int4PrimaryKeyTest.php b/tests/Unit/Advisor/Rules/NarrowPrimaryKeyTest.php similarity index 75% rename from tests/Unit/Advisor/Rules/Int4PrimaryKeyTest.php rename to tests/Unit/Advisor/Rules/NarrowPrimaryKeyTest.php index 5c79bb8..e5fe88b 100644 --- a/tests/Unit/Advisor/Rules/Int4PrimaryKeyTest.php +++ b/tests/Unit/Advisor/Rules/NarrowPrimaryKeyTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Heyosseus\Vacuum\Advisor\Rules\Int4PrimaryKey; +use Heyosseus\Vacuum\Advisor\Rules\NarrowPrimaryKey; use Heyosseus\Vacuum\Advisor\Severity; use Heyosseus\Vacuum\Values\Column; use Heyosseus\Vacuum\Values\Constraint; @@ -25,36 +25,36 @@ function keyed(string $type, array $keyColumns = ['id']): TableSchema } it('reports a primary key on integer', function (): void { - $findings = app(Int4PrimaryKey::class)->inspect(keyed('integer')); + $findings = app(NarrowPrimaryKey::class)->inspect(keyed('integer')); expect($findings)->toHaveCount(1) - ->and($findings[0]->rule)->toBe('int4-primary-key') + ->and($findings[0]->rule)->toBe('narrow-primary-key') ->and($findings[0]->severity)->toBe(Severity::Warning) ->and($findings[0]->subject)->toBe('public.orders.id') ->and($findings[0]->summary)->toContain('2,147,483,647'); }); it('reports a primary key on smallint', function (): void { - expect(app(Int4PrimaryKey::class)->inspect(keyed('smallint')))->toHaveCount(1); + expect(app(NarrowPrimaryKey::class)->inspect(keyed('smallint')))->toHaveCount(1); }); it('says nothing about a bigint key', function (): void { - expect(app(Int4PrimaryKey::class)->inspect(keyed('bigint')))->toBe([]); + expect(app(NarrowPrimaryKey::class)->inspect(keyed('bigint')))->toBe([]); }); it('says nothing about a uuid or text key', function (): void { - expect(app(Int4PrimaryKey::class)->inspect(keyed('uuid')))->toBe([]) - ->and(app(Int4PrimaryKey::class)->inspect(keyed('text')))->toBe([]); + expect(app(NarrowPrimaryKey::class)->inspect(keyed('uuid')))->toBe([]) + ->and(app(NarrowPrimaryKey::class)->inspect(keyed('text')))->toBe([]); }); it('says nothing about a table with no primary key', function (): void { // That is missing-primary-key's finding to make, not this one's. Two rules // firing on one defect is how a report becomes noise. - expect(app(Int4PrimaryKey::class)->inspect(new TableSchema('public', 'orders', [], [], [])))->toBe([]); + expect(app(NarrowPrimaryKey::class)->inspect(new TableSchema('public', 'orders', [], [], [])))->toBe([]); }); it('reports each narrow column of a composite key', function (): void { - expect(app(Int4PrimaryKey::class)->inspect(keyed('integer', ['tenant_id', 'order_id'])))->toHaveCount(2); + expect(app(NarrowPrimaryKey::class)->inspect(keyed('integer', ['tenant_id', 'order_id'])))->toHaveCount(2); }); it('says nothing when the key column is not in the column list', function (): void { @@ -65,7 +65,7 @@ function keyed(string $type, array $keyColumns = ['id']): TableSchema ), ], []); - expect(app(Int4PrimaryKey::class)->inspect($orphan))->toBe([]); + expect(app(NarrowPrimaryKey::class)->inspect($orphan))->toBe([]); }); it('says nothing about Laravel\'s own migrations table', function (): void { @@ -83,11 +83,11 @@ function keyed(string $type, array $keyColumns = ['id']): TableSchema ), ], []); - expect(app(Int4PrimaryKey::class)->inspect($migrations))->toBe([]); + expect(app(NarrowPrimaryKey::class)->inspect($migrations))->toBe([]); }); it('offers the widening, and says it rewrites the table', function (): void { - $findings = app(Int4PrimaryKey::class)->inspect(keyed('integer')); + $findings = app(NarrowPrimaryKey::class)->inspect(keyed('integer')); expect($findings[0]->remediation) ->toBe('ALTER TABLE "public"."orders" ALTER COLUMN "id" TYPE bigint;') diff --git a/tests/Unit/Console/BaselineTest.php b/tests/Unit/Console/BaselineTest.php new file mode 100644 index 0000000..5a60841 --- /dev/null +++ b/tests/Unit/Console/BaselineTest.php @@ -0,0 +1,124 @@ +suppresses(baselined()))->toBeFalse(); +}); + +it('suppresses a finding whose rule and subject both appear', function (): void { + $baseline = Baseline::record([baselined()]); + + expect($baseline->suppresses(baselined()))->toBeTrue(); +}); + +it('does not suppress the same subject under a different rule', function (): void { + // Matching on the pair is the whole point: one table can be wrong in two + // unrelated ways and silencing one must not silence the other. + $baseline = Baseline::record([baselined()]); + + expect($baseline->suppresses(baselined(rule: 'json-not-jsonb')))->toBeFalse(); +}); + +it('does not suppress a different subject under the same rule', function (): void { + $baseline = Baseline::record([baselined()]); + + expect($baseline->suppresses(baselined(subject: 'public.orders.warehouse_id')))->toBeFalse(); +}); + +it('ignores everything about a finding except its rule and subject', function (): void { + // Rewording a rule's prose, or raising its severity in a later release, must + // never invalidate a baseline somebody committed months ago. + $baseline = Baseline::record([baselined()]); + + $reworded = new Finding( + rule: 'unindexed-foreign-key', + subject: 'public.orders.customer_id', + severity: Severity::Critical, + summary: 'completely different words', + impact: 'completely different words', + remediation: 'CREATE INDEX something;', + ); + + expect($baseline->suppresses($reworded))->toBeTrue(); +}); + +it('encodes subjects sorted, so the file has no spurious diffs', function (): void { + $baseline = Baseline::record([ + baselined(subject: 'public.orders.warehouse_id'), + baselined(subject: 'public.orders.customer_id'), + ]); + + $document = json_decode($baseline->encode('1.2.0'), true); + + expect($document['findings']['unindexed-foreign-key']) + ->toBe(['public.orders.customer_id', 'public.orders.warehouse_id']) + ->and($document['vacuum'])->toBe('1.2.0') + ->and($document)->toHaveKey('generated_at'); +}); + +it('round-trips through encode and decode', function (): void { + $baseline = Baseline::record([baselined()]); + + expect(Baseline::decode($baseline->encode('1.2.0'))->suppresses(baselined()))->toBeTrue(); +}); + +it('treats a malformed file as no baseline rather than crashing a build', function (): void { + // A corrupt baseline should not take the pipeline down with it; reporting + // everything is the safe direction to fail in. + expect(Baseline::decode('not json at all')->suppresses(baselined()))->toBeFalse() + ->and(Baseline::decode('{"findings": "wrong shape"}')->suppresses(baselined()))->toBeFalse() + ->and(Baseline::decode('[]')->suppresses(baselined()))->toBeFalse() + ->and(Baseline::decode('"just a string"')->suppresses(baselined()))->toBeFalse(); +}); + +it('skips a malformed entry instead of discarding the whole baseline', function (): void { + // json_decode(..., true) turns a numeric-looking JSON key into an int array + // key, so a rule of "0" is indistinguishable from list noise; and a rule + // whose subjects are not a list is just as unusable. Either is dropped on + // its own, rather than a single bad entry invalidating every entry beside it. + $baseline = Baseline::decode( + '{"findings": {' + .'"0": ["public.orders.customer_id"], ' + .'"json-not-jsonb": "public.orders.payload", ' + .'"unindexed-foreign-key": ["public.orders.warehouse_id"]' + .'}}' + ); + + expect($baseline->suppresses(baselined(subject: 'public.orders.warehouse_id')))->toBeTrue() + ->and($baseline->suppresses(baselined()))->toBeFalse() + ->and($baseline->suppresses(baselined(rule: 'json-not-jsonb', subject: 'public.orders.payload')))->toBeFalse(); +}); + +it('reports an entry that no longer matches anything', function (): void { + // A baseline nobody prunes becomes a place defects hide. + $baseline = Baseline::record([baselined(), baselined(subject: 'public.orders.gone')]); + + $stale = $baseline->stale([baselined()]); + + expect($stale)->toHaveCount(1) + ->and($stale[0]->rule)->toBe('baseline-stale') + ->and($stale[0]->severity)->toBe(Severity::Info) + ->and($stale[0]->subject)->toBe('public.orders.gone') + ->and($stale[0]->summary)->toContain('unindexed-foreign-key'); +}); + +it('reports nothing stale when every entry still matches', function (): void { + expect(Baseline::record([baselined()])->stale([baselined()]))->toBe([]); +}); diff --git a/tests/Unit/Console/GithubReporterTest.php b/tests/Unit/Console/GithubReporterTest.php new file mode 100644 index 0000000..1ae07cc --- /dev/null +++ b/tests/Unit/Console/GithubReporterTest.php @@ -0,0 +1,179 @@ +report($output, $findings, $suppressed); + + return $buffer->fetch(); +} + +function annotatable( + Severity $severity = Severity::Warning, + string $summary = 'No index behind the foreign key.', + ?string $remediation = null, + string $subject = 'public.orders.customer_id', +): Finding { + return new Finding( + rule: 'unindexed-foreign-key', + subject: $subject, + severity: $severity, + summary: $summary, + impact: 'stub', + remediation: $remediation, + table: 'public.orders', + ); +} + +it('anchors an annotation to the migration that introduced it', function (): void { + expect(annotated([annotatable()])) + ->toContain('::warning file=') + ->and(annotated([annotatable()]))->toContain('create_orders_table') + ->and(annotated([annotatable()]))->toContain('line=15'); +}); + +it('maps severity onto the three levels GitHub understands', function (): void { + expect(annotated([annotatable(Severity::Critical)]))->toStartWith('::error ') + ->and(annotated([annotatable(Severity::Warning)]))->toStartWith('::warning ') + ->and(annotated([annotatable(Severity::Info)]))->toStartWith('::notice ') + ->and(annotated([annotatable(Severity::Unknown)]))->toStartWith('::notice '); +}); + +it('emits an annotation with no anchor when the map cannot place it', function (): void { + // GitHub attaches an unanchored annotation to the workflow rather than + // dropping it, so a finding the map cannot place is still seen. + $orphan = new Finding( + rule: 'missing-primary-key', + subject: 'public.nowhere', + severity: Severity::Warning, + summary: 'stub', + impact: 'stub', + table: 'public.nowhere', + ); + + expect(annotated([$orphan]))->toContain('::warning title=') + ->and(annotated([$orphan]))->not->toContain('file='); +}); + +it('escapes the characters that would otherwise break the command', function (): void { + // A remediation is multi-line SQL and a summary can contain a comma, so an + // unescaped implementation mangles exactly the findings people most want to + // read. + $awkward = annotatable( + summary: "100% wrong, on two lines\nlike this", + remediation: "CREATE INDEX a;\nCREATE INDEX b;", + ); + + $line = annotated([$awkward]); + + expect($line)->toContain('100%25 wrong') + ->and($line)->toContain('%0A') + ->and(substr_count(trim($line), "\n"))->toBe(0); +}); + +it('escapes a colon inside a property value', function (): void { + // A colon reaches a property value only through the migration file's absolute + // path, and only a Windows path begins "C:\\". Left unescaped it terminates the + // property and swallows the rest of the annotation. + $line = annotated([annotatable()]); + + expect($line)->not->toMatch('/file=[A-Za-z]:/') + ->and($line)->toContain('%3A'); +})->skipOnLinux()->skipOnMac(); + +it('renders a markdown table for the step summary', function (): void { + $summary = (new GithubReporter( + new MigrationMap(__DIR__.'/../../fixtures/migrations', new MigrationScanner) + ))->summary([annotatable()]); + + expect($summary)->toContain('| Severity | Rule | Subject |') + ->and($summary)->toContain('unindexed-foreign-key') + ->and($summary)->toContain('public.orders.customer_id'); +}); + +it('anchors the annotation with a path relative to the repository root', function (): void { + // GitHub matches an annotation's `file` against the diff by resolving it + // against the repository root, not the runner's filesystem. The map's own + // paths are absolute -- correct for every other consumer -- so this has to be + // fixed on the way out, and it has to hold on Windows, where an absolute path + // carries a drive letter and backslashes, exactly as it does on POSIX. + $directory = base_path('database/migrations'); + $created = ! is_dir($directory); + + if ($created) { + mkdir($directory, 0o777, true); + } + + $file = $directory.'/2024_01_05_000000_create_relative_path_table.php'; + file_put_contents($file, <<<'PHP' + id(); + }); + } + }; + PHP); + + try { + $map = new MigrationMap($directory, new MigrationScanner); + $buffer = new BufferedOutput; + $output = new OutputStyle(new ArrayInput([]), $buffer); + + $finding = new Finding( + rule: 'missing-primary-key', + subject: 'public.relative_path', + severity: Severity::Warning, + summary: 'stub', + impact: 'stub', + table: 'public.relative_path', + ); + + (new GithubReporter($map))->report($output, [$finding]); + $line = $buffer->fetch(); + + expect($line)->toContain('file=database/migrations/') + ->and($line)->not->toMatch('#file=/#') + ->and($line)->not->toMatch('#file=[A-Za-z]:#'); + } finally { + unlink($file); + + if ($created) { + rmdir($directory); + } + } +}); + +it('reports how many findings the baseline suppressed', function (): void { + // The pull request is the one place four hundred silently-suppressed findings + // matter most, so this is the one format that must not stay quiet about the + // count the way the original implementation did. + expect(annotated([annotatable()], suppressed: 3)) + ->toContain('::notice::3 suppressed by the baseline.'); +}); + +it('says nothing about suppression when nothing was suppressed', function (): void { + expect(annotated([annotatable()]))->not->toContain('suppressed by the baseline'); +}); diff --git a/tests/Unit/Schema/MigrationMapTest.php b/tests/Unit/Schema/MigrationMapTest.php new file mode 100644 index 0000000..6114261 --- /dev/null +++ b/tests/Unit/Schema/MigrationMapTest.php @@ -0,0 +1,140 @@ +locate(located('public.orders.customer_id')); + + expect($location?->line)->toBe(15) + ->and($location?->file)->toContain('create_orders_table'); +}); + +it('falls back to the table when the column is not in any migration', function (): void { + // A column added by a raw DB::statement is still on a table the map knows. + $location = mapped()->locate(located('public.orders.added_by_hand')); + + expect($location?->line)->toBe(13); +}); + +it('places a table-level finding on the create call', function (): void { + expect(mapped()->locate(located('public.orders'))?->line)->toBe(13); +}); + +it('returns nothing for a table no migration declares', function (): void { + expect(mapped()->locate(located('public.nowhere.column', table: 'public.nowhere')))->toBeNull(); +}); + +it('returns nothing for a finding that is about no table', function (): void { + expect(mapped()->locate(located('SchemaInspection', table: null)))->toBeNull(); +}); + +it('places nothing from a migration whose table name is a variable', function (): void { + expect(mapped()->locate(located('public.invisible.hidden_id', table: 'public.invisible')))->toBeNull(); +}); + +it('survives a directory that does not exist', function (): void { + $map = new MigrationMap(__DIR__.'/no-such-directory', new MigrationScanner); + + expect($map->locate(located('public.orders.customer_id')))->toBeNull(); +}); + +it('scans the directory once and reuses the result on a second lookup', function (): void { + $map = mapped(); + + $map->locate(located('public.orders')); + $second = $map->locate(located('public.orders.customer_id')); + + expect($second?->line)->toBe(15); +}); + +it('falls back to the unqualified entry when the scanner never recorded a schema', function (): void { + // Migrations do not ordinarily write a schema -- Schema::create('orders', ...), + // not Schema::create('tenant.orders', ...) -- so this is the everyday case for + // any schema other than the default one the fixture happens to use: the entry + // the scanner recorded is bare, and the qualified lookup has to miss before + // the unqualified one gets a chance to hit. + $location = mapped()->locate(located('tenant.orders.customer_id', table: 'tenant.orders')); + + expect($location?->line)->toBe(15) + ->and($location?->file)->toContain('create_orders_table'); +}); + +it('prefers a schema-qualified entry over an unqualified one from a different schema', function (): void { + // A multi-schema application can write the schema directly -- + // Schema::create('tenant.orders', ...) -- and when it does, that entry has to + // win the collision it would otherwise lose to public.orders's own migration: + // reducing straight to the unqualified key is exactly the bug this fixes. + $directory = sys_get_temp_dir().'/vacuum-migration-map-schema-'.bin2hex((string) getmypid()); + mkdir($directory, recursive: true); + $file = $directory.'/2024_01_03_000000_create_tenant_orders_table.php'; + + file_put_contents($file, <<<'PHP' + id(); + $table->foreignId('customer_id'); + }); + } + }; + PHP); + + try { + $map = new MigrationMap($directory, new MigrationScanner); + $location = $map->locate(located('tenant.orders.customer_id', table: 'tenant.orders')); + + expect($location?->file)->toContain('create_tenant_orders_table') + ->and($location?->line)->toBe(13); + } finally { + unlink($file); + rmdir($directory); + } +}); + +it('skips a glob match it cannot read as a file', function (): void { + // A directory whose name happens to end in .php still matches the glob, and + // file_get_contents on it fails the way an unreadable file would. Built under + // the system temp directory rather than as a fixture, because an empty + // directory is invisible to git and would not survive a checkout. + $directory = sys_get_temp_dir().'/vacuum-migration-map-'.bin2hex((string) getmypid()); + mkdir($directory.'/unreadable.php', recursive: true); + + try { + $map = new MigrationMap($directory, new MigrationScanner); + + expect($map->locate(located('public.orders.customer_id')))->toBeNull(); + } finally { + rmdir($directory.'/unreadable.php'); + rmdir($directory); + } +}); diff --git a/tests/Unit/Schema/MigrationScannerTest.php b/tests/Unit/Schema/MigrationScannerTest.php new file mode 100644 index 0000000..89b2cfd --- /dev/null +++ b/tests/Unit/Schema/MigrationScannerTest.php @@ -0,0 +1,289 @@ +scan("id(); + $table->foreignId('customer_id'); + }); + } +PHP); + + expect($entries)->toHaveKeys(['orders', 'orders.customer_id']); +}); + +it('records the line each declaration sits on', function (): void { + $entries = scan(<<<'PHP' + public function up(): void + { + Schema::create('orders', function (Blueprint $table) { + $table->foreignId('customer_id'); + }); + } +PHP); + + // The fixture prepends four lines, so the column lands on line 8. + expect($entries['orders.customer_id'])->toBe(8); +}); + +it('expands a morphs pair into the two columns it actually creates', function (): void { + $entries = scan(<<<'PHP' + public function up(): void + { + Schema::create('comments', function (Blueprint $table) { + $table->morphs('commentable'); + $table->nullableMorphs('authorable'); + }); + } +PHP); + + expect($entries)->toHaveKeys([ + 'comments.commentable_type', + 'comments.commentable_id', + 'comments.authorable_type', + 'comments.authorable_id', + ]); +}); + +it('refuses a table name it cannot read', function (): void { + // A variable table name is not something the tokenizer can resolve, and an + // anchor pointing at the wrong file is worse than no anchor at all. + expect(scan(<<<'PHP' + public function up(): void + { + Schema::create($name, function (Blueprint $table) { + $table->foreignId('customer_id'); + }); + } +PHP))->toBe([]); +}); + +it('leaves the table scope at the closing brace', function (): void { + // A column declared after the closure has ended belongs to no table. + $entries = scan(<<<'PHP' + public function up(): void + { + Schema::create('orders', function (Blueprint $table) { + $table->foreignId('customer_id'); + }); + + $other->string('stray'); + } +PHP); + + expect($entries)->not->toHaveKey('orders.stray'); +}); + +it('reads Schema::table as well as Schema::create', function (): void { + $entries = scan(<<<'PHP' + public function up(): void + { + Schema::table('orders', function (Blueprint $table) { + $table->string('note'); + }); + } +PHP); + + expect($entries)->toHaveKey('orders.note'); +}); + +it('keeps the first declaration when a column appears twice', function (): void { + // The first is where the column was written, which is where the defect was + // introduced; a later ->index() call is not. + $entries = scan(<<<'PHP' + public function up(): void + { + Schema::create('orders', function (Blueprint $table) { + $table->foreignId('customer_id'); + }); + + Schema::table('orders', function (Blueprint $table) { + $table->foreignId('customer_id'); + }); + } +PHP); + + expect($entries['orders.customer_id'])->toBe(8); +}); + +it('ignores a method call whose first argument is not a literal', function (): void { + $entries = scan(<<<'PHP' + public function up(): void + { + Schema::create('orders', function (Blueprint $table) { + $table->foreignId($column); + }); + } +PHP); + + expect($entries)->toBe(['orders' => 7]); +}); + +it('does not mistake a chained call for another column', function (): void { + // The receiver of ->constrained() is foreignId()'s return value, not the + // Blueprint, so 'users' is a referenced table and not a column of this one. + $entries = scan(<<<'PHP' + public function up(): void + { + Schema::create('orders', function (Blueprint $table) { + $table->foreignId('customer_id')->constrained('users'); + }); + } +PHP); + + expect($entries)->toHaveKey('orders.customer_id') + ->and($entries)->not->toHaveKey('orders.users'); +}); + +it('reads a column declared with whitespace before the arrow', function (): void { + // Whitespace is a token, so the receiver check has to walk past it. + $entries = scan(<<<'PHP' + public function up(): void + { + Schema::create('orders', function (Blueprint $table) { + $table + ->foreignId('customer_id'); + }); + } +PHP); + + expect($entries)->toHaveKey('orders.customer_id'); +}); + +it('ignores a Schema call that is neither create nor table', function (): void { + $entries = scan(<<<'PHP' + public function up(): void + { + if (Schema::hasTable('orders')) { + // + } + } +PHP); + + expect($entries)->toBe([]); +}); + +it('ignores a call whose method name is not itself a literal', function (): void { + // $table->$method('x') calls through a variable method name, which the + // tokenizer sees as a T_VARIABLE rather than the T_STRING a real column + // declaration would be. + $entries = scan(<<<'PHP' + public function up(): void + { + Schema::create('orders', function (Blueprint $table) { + $table->$method('customer_id'); + }); + } +PHP); + + expect($entries)->toBe(['orders' => 7]); +}); + +it('opens no table scope for DB::table, which runs a query rather than declaring a schema', function (): void { + // DB::table('orders') and Schema::table('orders') share a method name, but + // only one of them is a schema declaration. Without checking the receiver, a + // data-backfill migration's DB::table('orders') would open an 'orders' scope + // of its own, and every ->method('literal') that followed it in the same + // method body -- however unrelated -- would register as a column orders + // never actually gained. + $entries = scan(<<<'PHP' + public function up(): void + { + DB::table('orders')->where('id', 1)->update(['note' => 'x']); + } +PHP); + + expect($entries)->toBe([]); +}); + +it('still reads Schema::table when a DB::table call precedes it', function (): void { + $entries = scan(<<<'PHP' + public function up(): void + { + DB::table('orders')->where('id', 1)->update(['note' => 'x']); + + Schema::table('orders', function (Blueprint $table) { + $table->string('note'); + }); + } +PHP); + + expect($entries)->toHaveKey('orders.note'); +}); + +it('ignores a static call through a variable, whose receiver is not a literal class name', function (): void { + // $model::create(...) is a static call through a variable -- legal PHP -- and + // its receiver tokenizes as T_VARIABLE rather than the T_STRING a literal + // `Schema::` reference would be. + $entries = scan(<<<'PHP' + public function up(): void + { + $model::create('orders', function (Blueprint $table) { + $table->foreignId('customer_id'); + }); + } +PHP); + + expect($entries)->toBe([]); +}); + +it('reads a fully-qualified Schema::create call', function (): void { + // A fully-qualified reference tokenizes its receiver as a single + // T_NAME_FULLY_QUALIFIED token holding the whole path, not the T_STRING a + // bare `Schema` produces, and real migrations do call it fully qualified. + $entries = scan(<<<'PHP' + public function up(): void + { + \Illuminate\Support\Facades\Schema::create('orders', function (Blueprint $table) { + $table->foreignId('customer_id'); + }); + } +PHP); + + expect($entries)->toHaveKeys(['orders', 'orders.customer_id']); +}); + +it('ignores a receiver that merely contains the word Schema', function (): void { + // MySchema::create shares a suffix with Schema::create but is a different + // class entirely; matching on "ends with" rather than "equals" must not be + // fooled by that. + $entries = scan(<<<'PHP' + public function up(): void + { + MySchema::create('orders', function (Blueprint $table) { + $table->foreignId('customer_id'); + }); + } +PHP); + + expect($entries)->toBe([]); +}); + +it('returns nothing for a migration that does not parse as PHP at all', function (): void { + // TOKEN_PARSE is what makes this true: without it the tokenizer reads broken + // source leniently and hands back tokens anyway, which is exactly the silent + // best guess this class exists to refuse. Built directly rather than through + // scan()'s helper above, which always wraps the body in a syntactically valid + // class -- the whole point here is a file that never closes. + expect((new MigrationScanner)->scan('toBe([]); +}); + +it('returns nothing for a migration that parses but does not compile', function (): void { + // This parses fine -- it is only illegal once PHP tries to compile it, which + // TOKEN_PARSE raises as a bare CompileError rather than the ParseError a + // syntax error would raise. ParseError extends CompileError, so catching the + // parent has to be what scan() does, or a file like this would escape as an + // uncaught fatal instead of yielding nothing. + expect((new MigrationScanner)->scan('toBe([]); +}); diff --git a/tests/Unit/ServiceProviderTest.php b/tests/Unit/ServiceProviderTest.php index 6bf0f8b..aff0306 100644 --- a/tests/Unit/ServiceProviderTest.php +++ b/tests/Unit/ServiceProviderTest.php @@ -2,9 +2,12 @@ declare(strict_types=1); +use Heyosseus\Vacuum\Advisor\Finding; use Heyosseus\Vacuum\Advisor\Inspections\ConfigurationInspection; use Heyosseus\Vacuum\Advisor\Inspections\SettingInspection; use Heyosseus\Vacuum\Advisor\Inspections\TableInspection; +use Heyosseus\Vacuum\Advisor\Severity; +use Heyosseus\Vacuum\Schema\MigrationMap; use Heyosseus\Vacuum\VacuumServiceProvider; it('merges the package configuration into the application', function (): void { @@ -33,3 +36,33 @@ ->and($inspections)->toContain(SettingInspection::class) ->and($inspections)->toContain(ConfigurationInspection::class); }); + +it('points the migration map at database_path(migrations) when none is configured', function (): void { + $finding = new Finding( + rule: 'unindexed-foreign-key', + subject: 'public.orders.customer_id', + severity: Severity::Warning, + summary: 'stub', + impact: 'stub', + table: 'public.orders', + ); + + // The default application has no such migration, so this proves the map + // was built at all -- not what it finds. + expect(app(MigrationMap::class)->locate($finding))->toBeNull(); +}); + +it('points the migration map at the configured migrations path', function (): void { + config(['vacuum.lint.migrations_path' => __DIR__.'/../fixtures/migrations']); + + $finding = new Finding( + rule: 'unindexed-foreign-key', + subject: 'public.orders.customer_id', + severity: Severity::Warning, + summary: 'stub', + impact: 'stub', + table: 'public.orders', + ); + + expect(app(MigrationMap::class)->locate($finding)?->line)->toBe(15); +}); diff --git a/tests/fixtures/migrations/2024_01_01_000000_create_orders_table.php b/tests/fixtures/migrations/2024_01_01_000000_create_orders_table.php new file mode 100644 index 0000000..ecb4fcd --- /dev/null +++ b/tests/fixtures/migrations/2024_01_01_000000_create_orders_table.php @@ -0,0 +1,19 @@ +id(); + $table->foreignId('customer_id'); + $table->json('payload'); + }); + } +}; diff --git a/tests/fixtures/migrations/2024_01_02_000000_create_dynamic_table.php b/tests/fixtures/migrations/2024_01_02_000000_create_dynamic_table.php new file mode 100644 index 0000000..e6a51af --- /dev/null +++ b/tests/fixtures/migrations/2024_01_02_000000_create_dynamic_table.php @@ -0,0 +1,19 @@ +foreignId('hidden_id'); + }); + } +}; From 131540c3a618516a9ea9645cfc3d6126cc31c2df Mon Sep 17 00:00:00 2001 From: Rati Rukhadze Date: Wed, 9 Sep 2026 11:25:19 +0400 Subject: [PATCH 2/2] fix: cover the CI-only branches in MigrationMap and vacuum:lint --- src/Schema/MigrationMap.php | 12 +++++++++--- tests/Feature/Command/LintCommandTest.php | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/Schema/MigrationMap.php b/src/Schema/MigrationMap.php index cc762f2..c18339b 100644 --- a/src/Schema/MigrationMap.php +++ b/src/Schema/MigrationMap.php @@ -98,9 +98,15 @@ private function entries(): array $files = glob(rtrim($this->directory, '/\\').DIRECTORY_SEPARATOR.'*.php'); foreach ($files === false ? [] : $files as $file) { - $source = @file_get_contents($file); - - if ($source === false) { + // A glob match that is not a readable file -- a directory named + // something.php is the reproducible case -- fails differently by + // platform: Windows returns false where Linux returns an empty + // string. Coalescing the two is not tidiness, it is what keeps this + // branch reachable on both, rather than covered on a laptop and dead + // in CI. + $source = @file_get_contents($file) ?: ''; + + if ($source === '') { continue; } diff --git a/tests/Feature/Command/LintCommandTest.php b/tests/Feature/Command/LintCommandTest.php index 9da8bbe..0d53a6d 100644 --- a/tests/Feature/Command/LintCommandTest.php +++ b/tests/Feature/Command/LintCommandTest.php @@ -389,6 +389,21 @@ function withInstalledVersions(array $data, Closure $callback): void ->and(Artisan::output())->toContain('Could not append the step summary to'); }); +it('writes no step summary when the runner does not offer one', function (): void { + // This has to unset the variable rather than rely on it being absent. On a + // developer's machine it is unset anyway, so every other github-format test + // covers this branch by accident; inside Actions the runner always sets it, + // and the branch would be covered locally and dead in CI. + stubAdvisor(schemaFinding()); + + putenv('GITHUB_STEP_SUMMARY'); + + $exit = Artisan::call('vacuum:lint', ['--format' => 'github', '--no-interaction' => true]); + + expect($exit)->toBe(1) + ->and(Artisan::output())->toContain('::warning '); +}); + afterEach(function (): void { // A leaked baseline file would silently suppress findings in every later // test in the suite, so cleanup here does not depend on a preceding test