[{"data":1,"prerenderedAt":32},["ShallowReactive",2],{"blog-post-missed-laravel-bug":3,"blog-index":15},{"slug":4,"title":5,"description":6,"date":7,"updated":7,"tags":8,"image":12,"html":13,"reading_minutes":14},"missed-laravel-bug","Our reviewer missed a Laravel bug. Here is what we changed.","A Laravel bug escaped diff-only review because the broken override was outside the pull request. Here is how we added bounded change-impact context.","2026-10-14",[9,10,11],"laravel","change-impact","ai-review","https:\u002F\u002Fcontent.presentr.me\u002Fveridu\u002Fassets\u002Fog.fba80c1ca1.png","\u003Cp>Laravel pull request \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Flaravel\u002Fframework\u002Fpull\u002F55142\" rel=\"noopener\">#55142\u003C\u002Fa> added two events around clearing a cache. The change had tests. It merged and shipped in Laravel v12.4.0.\u003C\u002Fp>\n\u003Cp>It also broke tagged cache clearing when events were enabled.\u003C\u002Fp>\n\u003Cp>The cause was not in any file changed by the pull request. \u003Ccode>TaggedCache\u003C\u002Fcode> extends \u003Ccode>Repository\u003C\u002Fcode> and overrides \u003Ccode>event()\u003C\u002Fcode>. That override calls \u003Ccode>setTags()\u003C\u002Fcode> on every event it receives:\u003C\u002Fp>\n\u003Cpre style=\"background-color:#fff;color:#24292e\">\u003Ccode>\u003Cspan>\u003Cspan style=\"color:#D73A49\">protected\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\"> function\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1\"> event\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">($event)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003Cspan style=\"color:#24292E\">{\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003Cspan style=\"color:#D73A49\">    parent::\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1\">event\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">($event\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">-&gt;\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1\">setTags\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5\">$this\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">-&gt;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">tags\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">-&gt;\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1\">getNames\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">()));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003Cspan style=\"color:#24292E\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The new \u003Ccode>CacheFlushing\u003C\u002Fcode> and \u003Ccode>CacheFlushed\u003C\u002Fcode> classes did not have \u003Ccode>setTags()\u003C\u002Fcode>. Meanwhile, \u003Ccode>Repository::clear()\u003C\u002Fcode> passed those classes through the overridable method:\u003C\u002Fp>\n\u003Cpre style=\"background-color:#fff;color:#24292e\">\u003Ccode>\u003Cspan>\u003Cspan style=\"color:#D73A49\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\"> function\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1\"> clear\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">()\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">:\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\"> bool\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003Cspan style=\"color:#24292E\">{\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003Cspan style=\"color:#005CC5\">    $this\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">-&gt;\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1\">event\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">new\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5\"> CacheFlushing\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5\">$this\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">-&gt;\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1\">getName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">()));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003C\u002Fspan>\n\u003Cspan>\u003Cspan style=\"color:#24292E\">    $result \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5\"> $this\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">-&gt;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">store\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">-&gt;\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1\">flush\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003C\u002Fspan>\n\u003Cspan>\u003Cspan style=\"color:#D73A49\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\"> ($result) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003Cspan style=\"color:#005CC5\">        $this\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">-&gt;\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1\">event\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">new\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5\"> CacheFlushed\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5\">$this\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49\">-&gt;\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1\">getName\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\">()));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003Cspan style=\"color:#24292E\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003C\u002Fspan>\n\u003Cspan>\u003Cspan style=\"color:#D73A49\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E\"> $result;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003Cspan style=\"color:#24292E\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>On a tagged cache, that call resolves to \u003Ccode>TaggedCache::event()\u003C\u002Fcode>. It then calls a method the new event objects do not define. The result is a fatal error.\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fcontent.presentr.me\u002Fveridu\u002Fassets\u002Foverride-chain.0562c3c38f.svg\" alt=\"The clear call passes CacheFlushing to the TaggedCache event override, which calls a missing setTags method\" sizes=\"(max-width: 768px) 100vw, 720px\" width=\"720\" height=\"300\" loading=\"lazy\" \u002F>\u003C\u002Fp>\n\u003Cp>The bug was reported as \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Flaravel\u002Fframework\u002Fissues\u002F55322\" rel=\"noopener\">#55322\u003C\u002Fa>. Laravel fixed it in \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Flaravel\u002Fframework\u002Fpull\u002F55405\" rel=\"noopener\">#55405\u003C\u002Fa> for v12.10.0. The \u003Ca href=\"https:\u002F\u002Fveridu.io\u002Flaravel-code-review\" rel=\"noopener\">full Laravel code review walkthrough\u003C\u002Fa> includes the reproduction and the before-and-after behavior.\u003C\u002Fp>\n\u003Ch2 id=\"the-first-review-missed-it\">The first review missed it\u003C\u002Fh2>\n\u003Cp>We reviewed the change on 2026-10-13. There was no finding about \u003Ccode>setTags()\u003C\u002Fcode>. One reviewer reported nothing. The other produced two low-severity notes about the tests and the pull request description.\u003C\u002Fp>\n\u003Cp>That result exposed a hard limit in diff-only review. The broken code was in \u003Ccode>TaggedCache.php\u003C\u002Fcode>, but the pull request did not touch that file. A reviewer given only the diff could see the new events and the new calls. It could not see the subclass override that changed what those calls meant.\u003C\u002Fp>\n\u003Cp>The code that failed stayed outside the patch throughout the review.\u003C\u002Fp>\n\u003Cp>This was a context problem. The missing relationship was concrete: \u003Ccode>Repository::event()\u003C\u002Fcode> had an override in a subclass, and an added line called that method with a new kind of object.\u003C\u002Fp>\n\u003Ch2 id=\"the-context-we-added\">The context we added\u003C\u002Fh2>\n\u003Cp>Before review, we now inspect each \u003Ccode>$this-&gt;\u003C\u002Fcode> or \u003Ccode>static::\u003C\u002Fcode> call on an added line. We search for subclasses, to a maximum depth of two, that override the called method. When there is a match, both independent reviewers receive the override code and one question: does each override still accept what the new call passes?\u003C\u002Fp>\n\u003Cp>For this pull request, the added \u003Ccode>$this-&gt;event()\u003C\u002Fcode> call led to one override: \u003Ccode>TaggedCache::event()\u003C\u002Fcode>. That was the missing code.\u003C\u002Fp>\n\u003Cp>The search is deliberately bounded. It is text search, not a full call graph. Each search is capped so a large repository cannot fill the review with loosely related code. It does not yet follow facades, container bindings, or event listeners.\u003C\u002Fp>\n\u003Cp>Those limits matter. More context is not always better. A long list of possible relationships can hide the one relationship that changes the behavior of the patch.\u003C\u002Fp>\n\u003Ch2 id=\"we-checked-the-noise-before-using-it\">We checked the noise before using it\u003C\u002Fh2>\n\u003Cp>We ran the search over 165 recent pull requests from nine PHP projects, including Laravel, Symfony, Filament, and Statamic. It fired on three pull requests. Each match was a real override of a method called by the change. None of the three was a bug.\u003C\u002Fp>\n\u003Cp>An earlier version was right three out of five times, and we fixed three problems. It treated every file that imported the parent class as a candidate, so its file cap filled up before it reached the real subclasses. It also reported subclasses and overrides that the pull request itself added, which the reviewer can already see. After those fixes, the largest amount of context added to a review in the check was 1.3 KB.\u003C\u002Fp>\n\u003Cp>Those fixes were tuned on the same pull requests, and three is a small number. We read the result as \"no noise seen\", not as a measured precision rate.\u003C\u002Fp>\n\u003Cp>This check did not establish a bug-detection rate. It tested a narrower question: whether the search usually retrieves the relationship it claims to retrieve without adding unrelated text. One catch is still one example, not a rate.\u003C\u002Fp>\n\u003Ch2 id=\"the-second-review-found-the-fatal-error\">The second review found the fatal error\u003C\u002Fh2>\n\u003Cp>We ran the same change again on 2026-10-14 with the override context included. Both reviewers independently reported the tagged-cache failure. Both cited the retrieved override. The result moved from low severity to high severity. \u003Ca href=\"https:\u002F\u002Fveridu.io\u002Fr\u002F185389c0-83ed-47d3-b56c-aff272c3906c\u002F\" rel=\"noopener\">The review is public\u003C\u002Fa>; a later run on the same commit rated the same finding critical.\u003C\u002Fp>\n\u003Cp>They did not anchor the finding at the same place. One attached it to \u003Ccode>Repository.php\u003C\u002Fcode>, where the new event enters the overridable call. The other attached it to \u003Ccode>CacheFlushing.php\u003C\u002Fcode>, where an event without \u003Ccode>setTags()\u003C\u002Fcode> is defined.\u003C\u002Fp>\n\u003Cp>That difference affected the outcome. A review is blocked only when both reviewers report the same defect at the same place. These findings described the same runtime failure, but their file anchors differed. The agreement rule did not count them as a confirmed finding. The outcome was “needs review,” not blocked.\u003C\u002Fp>\n\u003Cp>We have not loosened that rule. Pairing findings across files could catch cases like this one, but it could also combine separate defects and create false blocks. We need evidence about that tradeoff before changing the rule.\u003C\u002Fp>\n\u003Ch2 id=\"what-this-changes-and-what-it-does-not\">What this changes, and what it does not\u003C\u002Fh2>\n\u003Cp>The useful change is small. When an added line calls an overridable method, the review can now include a nearby subclass override even when the pull request never touches its file. That makes this class of change impact visible.\u003C\u002Fp>\n\u003Cp>It does not cover every Laravel relationship. Facades, container bindings, and listeners still sit outside this search. The depth and size caps can also leave relevant code out. And the agreement issue means that two reviewers can find the same cross-file bug without producing a blocked result.\u003C\u002Fp>\n\u003Cp>That is the current boundary. The original diff-only review missed the bug. The bounded override search gave both reviewers the code they needed to find it. The remaining work is to expand coverage and test cross-file agreement without turning uncertain matches into false blocks.\u003C\u002Fp>\n\u003Cp>You can \u003Ca href=\"https:\u002F\u002Fveridu.io\u002Flaravel-code-review\" rel=\"noopener\">run the review on your own pull request\u003C\u002Fa>.\u003C\u002Fp>\n",5,{"site":16,"generated_at":17,"posts":18,"redirects":31},"veridu","2026-10-14T21:20:51.089Z",[19,28],{"slug":20,"title":21,"description":22,"date":7,"updated":7,"tags":23,"image":26,"url":27,"published_at":17},"three-reviewers-18-prs","Three AI code reviewers, same 18 PRs: two mostly found a subset of the third","On identical input, findings ranged from 40 to 2, and almost everything the smaller reviewers found, the largest found too. What a second reviewer is for.",[11,24,25],"code-review","engineering","https:\u002F\u002Fcontent.presentr.me\u002Fveridu\u002Fassets\u002Fog.66cdf9188b.png","https:\u002F\u002Fveridu.io\u002Fblog\u002Fthree-reviewers-18-prs",{"slug":4,"title":5,"description":6,"date":7,"updated":7,"tags":29,"image":12,"url":30},[9,10,11],"https:\u002F\u002Fveridu.io\u002Fblog\u002Fmissed-laravel-bug",{},1792162752979]