Skip to content

Commit 773bdf3

Browse files
authored
feat: add mago analyze (#205)
* feat(ci): add mago analyze on a shared Magento build Introduce a Static Analysis workflow that builds a full Magento install once and runs PHPStan and `mago analyze` against it in parallel, instead of each tool rebuilding Magento. Replaces the standalone PHPStan workflow (the PHPStan job keeps the same "PHPStan Analysis" check name). - static-analysis.yml: a build-magento job uploads the install as an artifact; the phpstan and mago-analyze jobs consume it without a live database (static analysis only reads code). phpstan.neon is copied into the install because it is export-ignored from the path-repo copy. - mago.toml: exclude phtml templates from analysis (runtime-injected template scope) and ignore the Magento-idiomatic mixed-assignment / mixed-operand codes (PHPStan level 9 is the type gate for those). - ddev: `ddev mago analyze` now runs against the installed Magento so it resolves the full class graph, matching what CI analyzes. * fix: resolve mago analyze type-safety findings in module source Fix the type errors mago analyze reports against a full Magento install: - CheckCommand: cast ini_get('memory_limit') (string|false declared string); use the PHP_VERSION constant instead of phpversion(); read the MySQL version via fetchOne('SELECT VERSION()') instead of from(null). - Type the loose getPaths() return via inline @var (array<string,string>) in CompatibilityChecker and ThemePath. - Cast mixed operands/arguments: theme loop index, hrtime() metrics, stat() mode, and the getOption() flags used in a boolean expression. - InspectorHints: widen render()'s $dictionary param to match the parent TemplateEngineInterface; cast the BP constant to string. - BlockCacheCollector: guard the untyped layout blocks with is_object() so method_exists() is safe and both analyzers narrow the type (drops the now-redundant @PHPStan-Ignore lines). - IncompatibilityDetector: suppress the lone mago const-array-shape finding with a scoped @mago-expect pragma (the shape is correct, as PHPStan confirms). - Drop redundant (string) casts and an obsolete @var docblock.
1 parent 0ca37b4 commit 773bdf3

16 files changed

Lines changed: 279 additions & 157 deletions

File tree

.ddev/commands/web/mago

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

33
set -euo pipefail
44

5-
## Description: Run Mago (PHP linter and formatter)
5+
## Description: Run Mago (PHP linter, formatter and static analyzer)
66
## Usage: mago <command> [options]
77
## Example: ddev mago lint
8-
## Example: ddev mago fmt
98
## Example: ddev mago fmt --dry-run
9+
## Example: ddev mago analyze
1010

1111
cd /var/www/html
1212

@@ -16,4 +16,19 @@ if [[ ! -x vendor/bin/mago ]]; then
1616
composer install --no-interaction
1717
fi
1818

19+
# `analyze` needs the full Magento class graph to resolve framework/module
20+
# classes. Run it against the installed Magento (workspace = magento/), with the
21+
# module source as the target, so it matches what CI analyzes. lint/fmt operate
22+
# on the module source directly and run from the repo root.
23+
if [[ ${1-} == "analyze" ]]; then
24+
shift
25+
target="vendor/openforgeproject/mageforge/src"
26+
# Optional explicit path override (must be relative to magento/).
27+
if [[ $# -gt 0 && ! ${1} =~ ^- ]]; then
28+
target="${1}"
29+
shift
30+
fi
31+
exec vendor/bin/mago --workspace magento --config mago.toml analyze "${target}" "$@"
32+
fi
33+
1934
vendor/bin/mago "$@"

.github/workflows/phpstan.yml

Lines changed: 0 additions & 120 deletions
This file was deleted.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
name: Static Analysis
2+
3+
# Builds a full Magento install ONCE (build-magento) and shares it via an
4+
# artifact, so multiple static-analysis tools (PHPStan, Mago analyze) run
5+
# against the same Magento codebase instead of each rebuilding it. The analysis
6+
# jobs need no live database — they only read code — so they run in parallel
7+
# without service containers.
8+
9+
on:
10+
pull_request:
11+
branches: [main]
12+
push:
13+
branches: [main]
14+
workflow_dispatch:
15+
16+
permissions:
17+
contents: read
18+
19+
jobs:
20+
build-magento:
21+
name: Build Magento (shared)
22+
runs-on: ubuntu-latest
23+
24+
services:
25+
mariadb:
26+
image: mariadb:11.4
27+
env:
28+
MYSQL_ROOT_PASSWORD: magento
29+
MYSQL_DATABASE: magento
30+
ports:
31+
- 3306:3306
32+
options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3
33+
34+
opensearch:
35+
image: opensearchproject/opensearch:3
36+
ports:
37+
- 9200:9200
38+
env:
39+
discovery.type: single-node
40+
DISABLE_SECURITY_PLUGIN: true
41+
OPENSEARCH_JAVA_OPTS: -Xms512m -Xmx512m
42+
options: --health-cmd="curl http://localhost:9200/_cluster/health" --health-interval=10s --health-timeout=5s --health-retries=10
43+
44+
steps:
45+
- name: Checkout code
46+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
47+
with:
48+
path: mageforge
49+
50+
- name: Setup PHP
51+
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
52+
with:
53+
php-version: "8.4"
54+
extensions: mbstring, intl, gd, xml, soap, zip, bcmath, pdo_mysql, curl, sockets
55+
tools: composer:v2
56+
57+
- name: Cache Composer packages
58+
id: composer-cache
59+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
60+
with:
61+
path: ~/.composer/cache/files
62+
key: ${{ runner.os }}-composer-2.4.8-${{ hashFiles('**/composer.json') }}
63+
restore-keys: ${{ runner.os }}-composer-2.4.8
64+
65+
- name: Download Magento
66+
run: |
67+
composer create-project \
68+
--repository-url=https://mirror.mage-os.org/ \
69+
magento/project-community-edition \
70+
magento2
71+
72+
- name: Install Magento
73+
working-directory: magento2
74+
env:
75+
COMPOSER_AUTH: ${{ secrets.COMPOSER_AUTH }}
76+
run: |
77+
composer config minimum-stability stable
78+
composer config prefer-stable true
79+
composer install --no-interaction --no-progress
80+
bin/magento setup:install \
81+
--base-url=http://localhost \
82+
--db-host=127.0.0.1 \
83+
--db-name=magento \
84+
--db-user=root \
85+
--db-password=magento \
86+
--admin-firstname=Admin \
87+
--admin-lastname=User \
88+
--admin-email=admin@example.com \
89+
--admin-user=admin \
90+
--admin-password=admin12345 \
91+
--language=en_US \
92+
--currency=USD \
93+
--timezone=Europe/Berlin \
94+
--use-rewrites=1 \
95+
--backend-frontname=admin \
96+
--search-engine=opensearch \
97+
--opensearch-host=localhost \
98+
--opensearch-port=9200 \
99+
--opensearch-index-prefix=magento \
100+
--cleanup-database
101+
102+
- name: Install MageForge module and PHPStan tooling
103+
working-directory: magento2
104+
run: |
105+
# Add the module from the current checkout as a copied (non-symlinked)
106+
# path repository so its source ends up inside the shared artifact.
107+
composer config repositories.mageforge-local '{"type": "path", "url": "../mageforge", "options": {"symlink": false}}'
108+
composer require --no-update openforgeproject/mageforge:@dev
109+
110+
# Allow the PHPStan extension installer plugin
111+
composer config --no-plugins allow-plugins.phpstan/extension-installer true
112+
113+
# PHPStan + Magento extension (consumed by the phpstan job)
114+
composer require --dev --no-update bitexpert/phpstan-magento "phpstan/phpstan:^2.0" phpstan/extension-installer
115+
116+
composer update --with-dependencies
117+
bin/magento setup:upgrade
118+
119+
# phpstan.neon is export-ignored, so the copied path repository omits
120+
# it; place it next to the analysed source for the phpstan job.
121+
cp ../mageforge/phpstan.neon vendor/openforgeproject/mageforge/phpstan.neon
122+
123+
- name: Pack Magento install
124+
run: |
125+
# Exclude runtime-only/disposable dirs to keep the artifact small;
126+
# static analysis only needs vendor/, app/, generated/ and app/etc/.
127+
tar czf magento.tar.gz \
128+
--exclude='magento2/var' \
129+
--exclude='magento2/pub/static' \
130+
--exclude='magento2/pub/media' \
131+
--exclude='magento2/.git' \
132+
--exclude='magento2/dev/tests' \
133+
magento2
134+
135+
- name: Upload Magento artifact
136+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
137+
with:
138+
name: magento-build
139+
path: magento.tar.gz
140+
retention-days: 1
141+
compression-level: 0 # already gzipped
142+
143+
phpstan:
144+
name: PHPStan Analysis
145+
runs-on: ubuntu-latest
146+
needs: build-magento
147+
148+
steps:
149+
- name: Setup PHP
150+
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
151+
with:
152+
php-version: "8.4"
153+
extensions: mbstring, intl, gd, xml, soap, zip, bcmath, pdo_mysql, curl, sockets
154+
tools: composer:v2
155+
156+
- name: Download Magento artifact
157+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
158+
with:
159+
name: magento-build
160+
161+
- name: Unpack Magento install
162+
run: tar xzf magento.tar.gz
163+
164+
- name: Run PHPStan
165+
working-directory: magento2
166+
run: |
167+
vendor/bin/phpstan analyse -c vendor/openforgeproject/mageforge/phpstan.neon vendor/openforgeproject/mageforge/src
168+
169+
mago-analyze:
170+
name: Mago Analyze
171+
runs-on: ubuntu-latest
172+
needs: build-magento
173+
174+
steps:
175+
- name: Checkout code
176+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
177+
with:
178+
path: mageforge
179+
180+
- name: Setup PHP
181+
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
182+
with:
183+
php-version: "8.4"
184+
tools: composer:v2
185+
186+
- name: Cache Composer packages
187+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
188+
with:
189+
path: ~/.composer/cache/files
190+
key: ${{ runner.os }}-composer-mago-${{ hashFiles('mageforge/composer.json') }}
191+
restore-keys: ${{ runner.os }}-composer-mago
192+
193+
- name: Install module dev dependencies (Mago binary)
194+
working-directory: mageforge
195+
run: composer install --no-interaction --no-progress
196+
197+
- name: Download Magento artifact
198+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
199+
with:
200+
name: magento-build
201+
202+
- name: Unpack Magento install
203+
run: tar xzf magento.tar.gz
204+
205+
# Run Mago from inside the built Magento (workspace = magento2), so the
206+
# `includes = ["vendor"]` from mago.toml resolves the full Magento class
207+
# graph. Only the module source is analyzed; phtml templates and the
208+
# Magento-idiomatic `mixed-*` codes are filtered via mago.toml. The Mago
209+
# binary and config come from the separate module checkout (its
210+
# require-dev isn't part of the artifact).
211+
- name: Mago analyze
212+
working-directory: magento2
213+
run: |
214+
../mageforge/vendor/bin/mago \
215+
--config ../mageforge/mago.toml \
216+
analyze vendor/openforgeproject/mageforge/src \
217+
--reporting-format=github

mago.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ enable-short-tags = false
1515
inline-empty-constructor-braces = false
1616
inline-empty-classlike-braces = false
1717

18+
[analyzer]
19+
# phtml templates rely on variables ($block, $escaper, $secureRenderer, …) that
20+
# Magento's template engine injects into the render scope at runtime. A static
21+
# analyzer cannot know them, so every template would report "undefined variable"
22+
# plus a cascade of mixed-* follow-ups. Exclude templates from `mago analyze`
23+
# only — `mago lint`/`mago fmt` still cover them (they don't type-check scope).
24+
excludes = ["**/*.phtml"]
25+
# Magento framework APIs (ObjectManager, InputInterface::getOption(), collection
26+
# items, …) are pervasively typed as `mixed`, so `mixed-assignment`/`mixed-operand`
27+
# fire throughout idiomatic Magento code without pointing at real bugs. PHPStan
28+
# level 9 (with the Magento extension, see phpstan.neon) is the project's type
29+
# gate for these; `mago analyze` gates on the higher-confidence type errors.
30+
ignore = ["mixed-assignment", "mixed-operand"]
31+
1832
[linter.rules]
1933
# Code-size metrics: Magento CLI commands and services are naturally verbose;
2034
# refactoring purely to satisfy thresholds is not a goal of this codebase.

0 commit comments

Comments
 (0)