Skip to content

Commit b83c384

Browse files
joshuapeaseclaude
andauthored
Green static analysis and enforce it in CI (#22)
* [#21] Remove the MockAsset concept - Delete MockAsset, MockAssetBuilder, and the Assets service that existed only to build them — work-in-progress placeholder-image code slated for a ground-up rebuild later. - Removes 11 of the 13 PHPStan level-4 errors at their source (bogus @throws tags referencing non-existent exception classes) instead of baselining WIP code. - Drop the now-stale MockAsset gotcha from CLAUDE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * [#21] Narrow getSettings() return type for PHPStan - Add `@method Settings getSettings()` to the Plugin docblock so PHPStan resolves Settings::$directory at both call sites (Plugin::_registerUrlRules and ViewController::actionTemplate). - One annotation, no runtime change; resolves the final 2 of the 13 level-4 errors. `composer phpstan` now exits 0 across src/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * [#18] Expand ECS scope to tests/ and green the codebase - Add tests/ to ecs.php paths; skip Codeception-generated code (the gitignored _generated actions trait, the committed actor stubs that `codecept build` regenerates, and compiled _craft/storage artifacts). - Autofix the style findings this surfaced — pre-existing trailing whitespace, a missing EOF newline, an unused `yii\web\View as BaseView` import in Plugin.php, and closure spacing in the test helpers. - `composer check-cs` now exits 0 across src/ + tests/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * [#18] Expand PHPStan scope to tests/ at level 4 - Add tests/ to phpstan.neon paths with excludePaths for Codeception generated code (the _generated actions, the actor stubs, and the compiled _craft/storage). Committed .gitignore files keep those dirs present on a clean checkout so the excludePaths always resolve. - Add three narrow ignoreErrors patterns for the unresolvable actor-class symbols ($I typed as FunctionalTester, $this->tester, and $I->* module methods). The patterns name the actor classes explicitly so they can't mask real src/ findings. - `composer phpstan` now exits 0 across src/ + tests/ at level 4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * [#18] Run ECS and PHPStan as a CI gate - Add a `static-analysis` job to ci.yml that runs `composer check-cs` and `composer phpstan` on push and pull request. - Single database-free job (not folded into the Codeception MySQL/Postgres matrix, which would run static analysis redundantly). PHP/Composer setup mirrors the non-DB steps of codecept.yml, with the Composer cache keyed on composer.json since the lockfile is git-ignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * [#18] Add static-analysis CI gate plan doc Implementation plan for greening PHPStan/ECS and enforcing them in CI, covering issues #18 and #21. Marked completed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 732b14f commit b83c384

13 files changed

Lines changed: 339 additions & 743 deletions

File tree

.github/workflows/ci.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,51 @@ jobs:
1616
uses: ./.github/workflows/codecept.yml
1717
with:
1818
php_versions: '["8.2"]'
19+
20+
# Static analysis needs no database, so it runs as a single job rather than
21+
# across the Codeception MySQL/PostgreSQL matrix. PHP/Composer setup mirrors
22+
# the non-DB steps of codecept.yml.
23+
static-analysis:
24+
name: Static analysis
25+
runs-on: ubuntu-latest
26+
env:
27+
PHP_EXTENSIONS: ctype,curl,dom,iconv,imagick,intl,json,mbstring,openssl,pcre,pdo,reflection,spl,zip
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@v4
31+
- name: Setup cache environment
32+
id: extcache
33+
uses: shivammathur/cache-extensions@v1
34+
with:
35+
php-version: '8.2'
36+
extensions: ${{ env.PHP_EXTENSIONS }}
37+
key: extension-cache-v1 # change to clear the extension cache.
38+
- name: Cache extensions
39+
uses: actions/cache@v4
40+
with:
41+
path: ${{ steps.extcache.outputs.dir }}
42+
key: ${{ steps.extcache.outputs.key }}
43+
restore-keys: ${{ steps.extcache.outputs.key }}
44+
- name: Setup PHP
45+
uses: shivammathur/setup-php@v2
46+
with:
47+
php-version: '8.2'
48+
extensions: ${{ env.PHP_EXTENSIONS }}
49+
ini-values: memory_limit=1G
50+
tools: composer:v2
51+
- name: Get Composer cache directory
52+
id: composer-cache
53+
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
54+
# composer.lock is git-ignored in this repo, so cache keys off composer.json.
55+
- name: Cache Composer dependencies
56+
uses: actions/cache@v4
57+
with:
58+
path: ${{ steps.composer-cache.outputs.dir }}
59+
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
60+
restore-keys: ${{ runner.os }}-composer-
61+
- name: Install Composer dependencies
62+
run: composer install --no-interaction --no-ansi --no-progress
63+
- name: Run ECS (code style)
64+
run: composer check-cs
65+
- name: Run PHPStan (static analysis)
66+
run: composer phpstan

CLAUDE.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,4 @@ Everything wires up in `src/Plugin.php` inside `Craft::$app->onInit()`:
8181
### Gotchas
8282

8383
- **`Navigation::getNav()` hardcodes the `'parts-kit'` folder name** (`src/services/Navigation.php:25`) instead of reading `settings.directory`. The controllers and URL rules respect a custom `directory`, but the nav scan does not—changing `directory` will break nav generation until this is fixed.
84-
- **`MockAsset` / `MockAssetBuilder`** (`src/models/`) are a work-in-progress feature for rendering placeholder images (generated as Imagick data URLs), exposed via the `assets` service's `make()` builder. Most `MockAsset` methods throw `NotSupportedException`; only width/height/`getUrl()` are implemented.
8584
- `root.twig` loads the UI from `https://unpkg.com/@viget/parts-kit@^0/...`—the browsing UI requires network access to the CDN.

docs/plans/2026-06-02-001-chore-static-analysis-ci-gate-plan.md

Lines changed: 250 additions & 0 deletions
Large diffs are not rendered by default.

ecs.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,20 @@
88
return static function(ECSConfig $ecsConfig): void {
99
$ecsConfig->paths([
1010
__DIR__ . '/src',
11+
__DIR__ . '/tests',
1112
__FILE__,
1213
]);
1314

15+
// Codeception-generated code: the gitignored actions trait, the committed
16+
// actor stubs that `codecept build` regenerates, and the compiled
17+
// storage artifacts. None are hand-written, so keep ECS off them.
18+
$ecsConfig->skip([
19+
__DIR__ . '/tests/_support/_generated',
20+
__DIR__ . '/tests/_support/UnitTester.php',
21+
__DIR__ . '/tests/_support/FunctionalTester.php',
22+
__DIR__ . '/tests/_craft/storage',
23+
]);
24+
1425
$ecsConfig->sets([
1526
SetList::CRAFT_CMS_4,
1627
]);

phpstan.neon

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,21 @@ parameters:
55
level: 4
66
paths:
77
- src
8+
- tests
9+
excludePaths:
10+
# Codeception-generated code PHPStan can't resolve without building:
11+
# the gitignored actions trait, the committed actor stubs, and the
12+
# compiled Craft test-instance storage.
13+
- tests/_support/_generated/*
14+
- tests/_support/UnitTester.php
15+
- tests/_support/FunctionalTester.php
16+
- tests/_craft/storage/*
17+
ignoreErrors:
18+
# The Codeception actor classes (UnitTester/FunctionalTester) and the
19+
# module methods they expose ($I->amOnPage(), $this->tester, etc.) are
20+
# generated code that is excluded from analysis and absent from a clean
21+
# checkout, so PHPStan cannot resolve these symbols. The patterns name
22+
# the actor classes explicitly, so they never mask real src/ findings.
23+
- '#has invalid type (Unit|Functional)Tester\.#'
24+
- '#has unknown class (Unit|Functional)Tester as its type\.#'
25+
- '#Call to method \S+\(\) on an unknown class (Unit|Functional)Tester\.#'

src/Plugin.php

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,17 @@
1313
use craft\web\UrlManager;
1414
use craft\web\View;
1515
use viget\partskit\models\Settings;
16-
use viget\partskit\services\Assets;
1716
use viget\partskit\services\Navigation;
1817
use yii\base\Event;
19-
use yii\web\View as BaseView;
2018

2119
/**
2220
* Craft Parts Kit plugin
2321
*
2422
* @method static Plugin getInstance()
23+
* @method Settings getSettings()
2524
* @author Viget <craft@viget.com>
2625
* @copyright Viget
2726
* @license MIT
28-
* @property-read Assets $assetService
2927
*/
3028
class Plugin extends BasePlugin
3129
{
@@ -40,7 +38,6 @@ public static function config(): array
4038
return [
4139
'components' => [
4240
'navigation' => Navigation::class,
43-
'assets' => Assets::class,
4441
],
4542
];
4643
}
@@ -60,7 +57,7 @@ public function init(): void
6057
parent::init();
6158

6259
// Defer most setup tasks until Craft is fully initialized
63-
Craft::$app->onInit(function () {
60+
Craft::$app->onInit(function() {
6461
$this->attachEventHandlers();
6562
// ...
6663
});
@@ -71,7 +68,7 @@ private function attachEventHandlers(): void
7168
Event::on(
7269
View::class,
7370
View::EVENT_REGISTER_SITE_TEMPLATE_ROOTS,
74-
function (RegisterTemplateRootsEvent $event) {
71+
function(RegisterTemplateRootsEvent $event) {
7572
$event->roots[self::TEMPLATE_ROOT] = $this->getBasePath() . '/templates';
7673
}
7774
);
@@ -88,7 +85,7 @@ function (RegisterTemplateRootsEvent $event) {
8885
Event::on(
8986
CraftVariable::class,
9087
CraftVariable::EVENT_INIT,
91-
static function (Event $e) {
88+
static function(Event $e) {
9289
/** @var CraftVariable $variable */
9390
$variable = $e->sender;
9491
$variable->set('partsKit', self::getInstance());
@@ -99,7 +96,7 @@ static function (Event $e) {
9996
Event::on(
10097
UserPermissions::class,
10198
UserPermissions::EVENT_REGISTER_PERMISSIONS,
102-
function (RegisterUserPermissionsEvent $event) {
99+
function(RegisterUserPermissionsEvent $event) {
103100
$event->permissions[] = [
104101
'heading' => 'Parts Kit',
105102
'permissions' => [
@@ -113,9 +110,9 @@ function (RegisterUserPermissionsEvent $event) {
113110
}
114111

115112
/**
116-
* Overrides the default `/parts-kit` paths to route them
117-
* through our custom controller action.
118-
*
113+
* Overrides the default `/parts-kit` paths to route them
114+
* through our custom controller action.
115+
*
119116
* This gives full control over the rendering of the parts kit UI and
120117
* bypasses the need for {% layout %} tags in parts kit templates.
121118
*/

src/controllers/ViewController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
/**
1313
* View controller
14-
*
14+
*
1515
* Renders the root parts kit as well as invividual part examples
1616
*/
1717
class ViewController extends Controller
@@ -31,7 +31,7 @@ public function actionRoot(): Response
3131
/**
3232
* actions/parts-kit/view/template
3333
* parts-kit/<template>
34-
*
34+
*
3535
* @see Plugin::_registerUrlRules()
3636
*/
3737
public function actionTemplate(string $template = 'layout'): Response

0 commit comments

Comments
 (0)