Skip to content

v2.0.0 — eliminate Laravel false positives#1

Merged
Amoifr merged 5 commits intomainfrom
dev
May 6, 2026
Merged

v2.0.0 — eliminate Laravel false positives#1
Amoifr merged 5 commits intomainfrom
dev

Conversation

@Amoifr
Copy link
Copy Markdown
Owner

@Amoifr Amoifr commented May 6, 2026

Summary

End-to-end refactor that turns --type=laravel into a real, opinionated
preset. On a real Laravel project (Partoo, 136 files, 110 classes):

Metric Before 2.0 After 2.0
Layer violations 12+ 1 (a real Domain → Infrastructure)
SOLID violations 100+ 6 (all legitimate)
OrganizationController DIP 104/104 concrete 10/10 actually-injected
Architecture score B (73)

What's in this PR

  • A1 + A2 — Layer classification rewritten (feat(arch): a98f3c6)
    • LayerDetector now consumes ordered LayerRule glob patterns (first match wins) instead of array_merge_recursive of namespace + suffix lookups.
    • New Wiring layer for ServiceProvider / DI extensions, exempt from layer-violation reporting because by nature they reference concrete classes from any layer.
    • LaravelProjectType and SymfonyProjectType ship full opinionated rules (App\Actions\** → Application, App\Providers\** → Wiring, etc.).
  • B1 + B2 — DIP scope reduction + framework whitelists (feat(solid): 6c56690)
    • DIP only counts type-hinted, injectable dependencies (type_hint_param, type_hint_return, type_hint_property). Traits, extends, new, static calls, attributes, instanceof, catches, implements — none are counted anymore.
    • Framework primitives are excluded by default (Eloquent / Carbon / Closure / facades / Queue traits / OpenAPI attributes for Laravel; Doctrine / Forms / HttpFoundation / Twig for Symfony).
    • Bug fix: $fqn shadowing in detectDipViolations was making the violation className point to the last seen dependency instead of the analysed class.
  • D2 — phpquality.json project configuration (feat(config): 42a549d)
    • Optional file at the project root to extend the framework preset (rules prepended, ignore lists unioned).
    • New CLI: --config.
  • D1 — @phpquality-ignore annotations + baseline (feat(config): 7e14434)
    • Docblock annotation on classes / interfaces. Codes: solid.srp, solid.dip, solid.isp, architecture.layer.
    • BaselineManager produces a stable hash per violation. New CLI: --generate-baseline, --baseline. Report exposes summary.suppressedByBaseline and warns about obsolete entries.
  • Release artefacts (release: 701479b)
    • Integration test under tests/Integration/ pinning the six anti-regression assertions on a self-contained Laravel-mini fixture.
    • CHANGELOG.md 2.0.0 section with full breaking-change list and "Migrating from 1.x".
    • README.md preamble + new options + phpquality.json schema + baseline workflow.
    • DOCKERHUB.md mirrored.
    • composer.json branch-alias bumped to 2.0.x-dev.

Breaking changes

  • *Action is no longer Controller in Laravel — it maps to Application.
  • Wiring is a new layer; ServiceProvider no longer generates layer violations.
  • DIP scores aren't comparable to 1.x (scope reduced).
  • ProjectTypeInterface gains 3 methods (defaults provided in AbstractProjectType).
  • ProjectAnalyzer constructor now requires ProjectConfigLoader + BaselineManager (Symfony autowiring picks them up).
  • SolidAnalyzer::analyze() and ArchitectureAnalyzer::analyze() signatures gained optional context parameters.

Test plan

  • vendor/bin/phpunit — 544 / 544 green (was 510 before this PR; +34 new tests across LayerDetectorTest, SolidAnalyzerTest, ProjectConfigLoaderTest, BaselineManagerTest, IgnoreAnnotationParserTest, and the new LaravelEndToEndTest).
  • E2E smoke run on ~/src/partoo/storeloc-saas/app via tests/Integration/run-partoo-smoke.php — confirms the headline numbers above.
  • After merge: tag v2.0.0 to trigger the Docker Hub workflow (amoifr13/phpquality:2.0.0 / 2.0 / 2 / latest) and Packagist update.

Migration

  • Custom ProjectTypeInterface implementations: extend AbstractProjectType (gets defaults for free) or implement getLayerRules() / getDipIgnoreList() / getWiringPatterns().
  • Existing CI runs that asserted on violation counts: expect the counts to drop. To freeze the current state:
    phpquality:analyze ... --generate-baseline=phpquality.baseline.json
    # subsequent runs:
    phpquality:analyze ... --baseline=phpquality.baseline.json --fail-on-violation

🤖 Generated with Claude Code

Amoifr and others added 5 commits May 6, 2026 09:46
Replace the namespace+suffix lookup (broken by `array_merge_recursive` for
framework presets) with an ordered list of glob LayerRule (first match wins).
This fixes the headline false positive on Laravel: `App\Actions\*` was
previously misclassified as Controller, generating ~12 spurious
"Infrastructure → Controller" violations on a typical app.

Add a new `Wiring` layer for ServiceProviders, container extensions, DI
bindings — exempt from layer-violation reporting because by nature they
must reference concrete classes from any layer. Detection by glob pattern
+ extends Illuminate\Support\ServiceProvider / Symfony bundles.

Extend ProjectTypeInterface with three new methods (defaults in
AbstractProjectType): getLayerRules(), getWiringPatterns(),
getDipIgnoreList(). LaravelProjectType and SymfonyProjectType ship real
opinionated presets so `--type=laravel` / `--type=symfony` is no longer a
near-no-op.

ArchitectureAnalyzer is rewired to consume these via project type and
(in upcoming commits) ProjectConfig.

Tests: LayerDetectorTest extended with anti-regression cases covering
the Action→Application reclassification, ServiceProvider→Wiring,
glob matching semantics, and project-supplied custom rules.

Refs: A1, A2 of the diagnostic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DIP detection no longer counts every non-`use` dependency. It is now
restricted to truly *injected* dependencies: type_hint_param,
type_hint_return, type_hint_property. Traits, extends, new, static_call,
const, instanceof, catch, implements, attributes — none can be substituted
via DI, so they are excluded.

This alone eliminates the pathological case of OrganizationController
flagged with 104/104 concrete dependencies (most of which were OpenAPI
attributes and use statements). On the Partoo Laravel project the count
falls to a defensible 10/10 actually-injected dependencies.

DependencyVisitor stores granular kinds (type_hint_param/return/property)
instead of a single 'type_hint'. SolidAnalyzer dedupes by FQN (a class
type-hinting `Foo` in three params counts once, not three times).

Framework-supplied concretes are excluded via getDipIgnoreList():
  - Laravel: Eloquent Model / Relations / Builder, Carbon, Closure,
    facades, queue/bus traits (Queueable, Dispatchable, Serializes…),
    Http\Request / Response, OpenAPI attributes, App\Models\**, App\Data\**.
  - Symfony: Doctrine ORM, HttpFoundation, Form, Validator, Security,
    Messenger, Routing, Serializer, DI, Twig.

Wiring classes (Providers, container extensions) are exempt from DIP
scoring as well — binding concretes is their entire purpose.

Bug also fixed: a `$fqn` shadowing in detectDipViolations() caused the
violation's `className` field to point to the last dependency seen
instead of the analysed class. Renamed to `$depFqn`.

Tests: SolidAnalyzerTest extended with anti-regression cases for
Queueable/Dispatchable jobs, Eloquent type-hints, FQN dedup, wiring
exemption, and the DIP scope reduction itself.

Refs: B1, B2 of the diagnostic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a project-level configuration file at the root of the analyzed
project. Schema (all keys optional):

  {
    "layers":           { "rules": [{ "match": "App\\Foo\\**", "layer": "Application" }] },
    "wiring":           { "patterns": ["**ServiceProvider"] },
    "abstractionRatio": { "ignore":   ["App\\Models\\**"] },
    "ignore":           { "violations": ["solid.dip:App\\Foo\\Bar"] },
    "baseline":         "phpquality.baseline.json"
  }

Merge semantics with the framework preset:
  - layers.rules:     project rules are PREPENDED (first match wins)
  - wiring.patterns:  union with framework patterns
  - abstractionRatio.ignore: union with framework whitelist
  - ignore.violations: pre-canonicalised "code:FQN" pairs
  - baseline:         relative path resolved against the source dir

ProjectAnalyzer gains setConfigPath(), setBaselinePath(),
setGenerateBaselinePath() and now requires ProjectConfigLoader +
BaselineManager (see next commit) via Symfony autowiring. The CLI
exposes --config / --baseline / --generate-baseline.

This makes `--type=laravel` truly a preset that the project can extend
without forking the bundle.

Tests: ProjectConfigLoaderTest covers fallback to framework defaults,
prepend / union merge semantics, baseline path resolution (relative vs
absolute), invalid JSON, and explicit --config override.

Refs: D2 of the diagnostic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two complementary suppression mechanisms — both make the tool usable
on existing projects without rewriting them.

== @phpquality-ignore docblock annotation ==

Parsed by IgnoreAnnotationParser from the docblock of any class /
interface during AST traversal. Recognised codes: solid.srp, solid.dip,
solid.isp, architecture.layer. Comma-separated values supported,
end-of-line "— rationale" trailing text is ignored.

  /**
   * @phpquality-ignore solid.dip — wiring intentionnel
   */
  final class FooService { /* … */ }

ArchitectureAnalyzer collects per-class ignores from DependencyVisitor
output and the project config (`ignore.violations` keys) before invoking
SOLID and layer-violation checks.

== Baseline ==

BaselineManager generates a stable hash per violation
(filePath | line | code | source→target / className) and serialises a
sorted, deduplicated list. The hash strips the path prefix up to /src/
or /app/ so the file is comparable across machines.

  phpquality:analyze … --generate-baseline=phpquality.baseline.json
  phpquality:analyze … --baseline=phpquality.baseline.json

The latter filters violations whose hash is in the baseline; the
analyser exposes summary.suppressedByBaseline. Entries that match no
current violation are reported as obsolete (regenerate-please warning).

`--generate-baseline` is exclusive of `--fail-on-violation` (by design,
running it accepts the current state).

Tests: IgnoreAnnotationParserTest (8 cases: case-insensitive tag,
comma-separated, dedup, "—" terminator, multi-line); BaselineManagerTest
(round-trip, partial application, obsolete-entry detection, path
normalisation, malformed-file handling).

Refs: D1 of the diagnostic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
End-to-end validation of the four PR2.0 changes (A1, A2, B1, B2, D1, D2)
on a real Laravel project (Partoo storeloc-saas, 136 files, 110 classes):

  Layer violations:    12+  →  1  (the one remaining is a real
                                   Domain → Infrastructure violation)
  SOLID violations:   100+  →  6  (all legitimate; biggest controller is
                                   10/10 instead of 104/104)
  AppServiceProvider:  flagged → exempt (Wiring layer)
  Architecture score:           B (73)

Add an integration test under tests/Integration/ that pins those
behaviours on a self-contained laravel-mini fixture (Action, Job,
Provider, Controller, Repository, Model, ignore-annotated Service):

  - testActionIsApplicationNotController              (A1)
  - testServiceProviderIsWiring                       (A2)
  - testNoLayerViolationsOnCommonLaravelStructure     (A2)
  - testJobUsingQueueableHasNoDipViolation            (B1+B2)
  - testIgnoreAnnotationSuppressesBillingServiceDip   (D1)
  - testBaselineRoundTripSuppressesAllRemainingViolations  (D1)

A standalone PHP smoke runner is also included (run-partoo-smoke.php)
for ad-hoc execution against any local Laravel/Symfony project, no
Symfony kernel required.

Documentation:
  - CHANGELOG.md gets a 2.0.0 section listing every breaking change
    with rationale and a "Migrating from 1.x" subsection.
  - README.md adds a "Migrating from 1.x" preamble and documents the
    new options (--config, --baseline, --generate-baseline, --wizard),
    the phpquality.json schema, the @phpquality-ignore annotation,
    and a baseline-based CI workflow.
  - DOCKERHUB.md mirrors the new options + a "What's new in 2.0"
    blurb pointing at the changelog.
  - composer.json branch-alias bumped to 2.0.x-dev.

Total test count: 510 → 544. All green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Amoifr Amoifr merged commit 716df2f into main May 6, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant