Changeset 65372
- Timestamp:
- 11/10/2026 06:51:08 AM (14 hours ago)
- Location:
- trunk
- Files:
-
- 6 edited
-
.github/workflows/phpstan-pull-request-comments.yml (modified) (2 diffs)
-
.github/workflows/reusable-phpstan-static-analysis-v1.yml (modified) (7 diffs)
-
composer.json (modified) (1 diff)
-
package.json (modified) (1 diff)
-
tests/phpstan/README.md (modified) (4 diffs)
-
tests/phpstan/generate-baselines.php (modified) (9 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/.github/workflows/phpstan-pull-request-comments.yml
r64539 r65372 262 262 '> **The PHPStan baselines are out of date, so the PHPStan check fails.**', 263 263 '>', 264 '> An error recorded in a baseline no longer occurs, or occurs a different number of times. If this pull request fixed it, that is expected: nothing needs to change in the code, but each baseline entry records an exact count for a specific file, and the baselines need to catch up. Re generate themand commit the result:',264 '> An error recorded in a baseline no longer occurs, or occurs a different number of times. If this pull request fixed it, that is expected: nothing needs to change in the code, but each baseline entry records an exact count for a specific file, and the baselines need to catch up. Remove the stale entries, and commit the result:', 265 265 '>', 266 266 '> ```bash', 267 '> npm run typecheck:php: baselines',267 '> npm run typecheck:php:fix', 268 268 '> ```', 269 269 '>', … … 271 271 '>', 272 272 '> ```bash', 273 '> composer phpstan: baselines',273 '> composer phpstan:fix', 274 274 '> ```', 275 275 '>', 276 '> Where an error below names an `@phpstan-ignore` comment in the code rather than a baseline entry, remove that comment instead; regenerating will not clear it.',277 '>', 278 `> See [Re generating the baselines](${ readmeUrl }#regenerating-the-baselines) for details.`,276 '> This also deletes any baseline left with no entries, and removes any `@phpstan-ignore` comment in the code that no longer ignores anything.', 277 '>', 278 `> See [Removing stale entries with \`--fix\`](${ readmeUrl }#removing-stale-entries-with---fix) for details, and for regenerating the baselines instead.`, 279 279 ].join( '\n' ) ); 280 280 } else { -
trunk/.github/workflows/reusable-phpstan-static-analysis-v1.yml
r64539 r65372 151 151 tip: message.tip ?? null, 152 152 ignorable: message.ignorable, 153 fixable: message.fixable ?? false, 153 154 } ); 154 155 } 155 156 } 156 157 157 // Errors that are not specific to a file, such as an `ignoreErrors` entry without a158 // path that no longer matches. These cannot be ignored either.158 // Errors that are not specific to a file, such as an `ignoreErrors` entry from a config 159 // file PHPStan cannot locate it in. These cannot be ignored either. 159 160 for ( const message of report.errors ) { 160 errors.push( { file: null, line: null, message, identifier: null, tip: null, ignorable: false } );161 errors.push( { file: null, line: null, message, identifier: null, tip: null, ignorable: false, fixable: false } ); 161 162 } 163 164 // PHPStan marks an error it can fix itself, such as a stale baseline entry, as fixable. 165 const fixHint = 'This can be fixed automatically by running `composer phpstan:fix` (or `npm run typecheck:php:fix`).'; 162 166 163 167 for ( const error of errors ) { … … 169 173 properties.startLine = error.line; 170 174 } 171 const message = error.tip ? `${ error.message }\n${ error.tip }` : error.message;175 const message = [ error.message, error.tip, error.fixable ? fixHint : null ].filter( Boolean ).join( '\n' ); 172 176 if ( error.ignorable ) { 173 177 core.warning( message, properties ); … … 177 181 } 178 182 179 // Tips are left out of the results, since they are only shown in the annotations, next to the code. 183 // Tips and whether an error is fixable are left out of the results, since they are only shown in the 184 // annotations, next to the code. 180 185 // 181 186 // The commit analyzed is the pull request merged into its base branch, whose second parent is the head … … 197 202 pull_request: context.payload.pull_request.number, 198 203 changed_in_base: changedInBase, 199 errors: errors.map( ( { tip, ...error } ) => error ),204 errors: errors.map( ( { tip, fixable, ...error } ) => error ), 200 205 } ) ); 201 206 } … … 267 272 **If you fixed the error, this is expected.** Each baseline entry records an exact count for a specific file, so that a new occurrence of an already baselined error is reported rather than absorbed. That same exactness means fixing one leaves the baseline describing a state that no longer exists. There is nothing to fix in the code; the baselines just need to catch up. 268 273 269 Re generate themand commit the result:274 Remove the stale entries, and commit the result: 270 275 271 276 ```bash 272 npm run typecheck:php: baselines277 npm run typecheck:php:fix 273 278 ``` 274 279 … … 276 281 277 282 ```bash 278 composer phpstan: baselines283 composer phpstan:fix 279 284 ``` 280 285 281 That r ewrites the files under [`tests/phpstan/baselines`][baselines], deletes any whose errors are now all fixed, and updates the list of them in `phpstan.neon.dist`.282 283 Where the report names an `@phpstan-ignore` annotation in the code rather than a baseline entry, remove that annotation instead; regenerating will not clear it.286 That runs PHPStan with `--fix`, which lowers the counts in the files under [`tests/phpstan/baselines`][baselines], removes the entries that no longer match, and removes any `@phpstan-ignore` annotation in the code that no longer ignores anything. It then deletes any baseline left with no entries, and updates the list of them in `phpstan.neon.dist`. 287 288 Alternatively, regenerate the baselines with `npm run typecheck:php:baselines` or `composer phpstan:baselines`. That also deletes any baseline whose errors are now all fixed, and updates the list of them in `phpstan.neon.dist`, but it does not clear an `@phpstan-ignore` annotation. 284 289 285 290 See [`tests/phpstan/README.md`][readme] for details. … … 295 300 } >> "${GITHUB_STEP_SUMMARY}" 296 301 297 echo "::error title=PHPStan baselines are out of date::An ignored error no longer occurs, or occurs a different number of times. If you fixed it, that is expected: run \`npm run typecheck:php: baselines\` or \`composer phpstan:baselines\` and commit the updated baselines. See ${README_URL}"302 echo "::error title=PHPStan baselines are out of date::An ignored error no longer occurs, or occurs a different number of times. If you fixed it, that is expected: run \`npm run typecheck:php:fix\` or \`composer phpstan:fix\` and commit the updated baselines. See ${README_URL}" 298 303 else 299 304 echo "::error title=PHPStan reported errors that cannot be ignored::PHPStan reported ${unignorable} error(s) that cannot be ignored or reported as warnings. See the error annotations for details, and ${README_URL} for how to run the analysis locally." -
trunk/composer.json
r65310 r65372 78 78 "phpstan": "@php ./vendor/bin/phpstan analyse --memory-limit=2G", 79 79 "phpstan:baselines": [ "Composer\\Config::disableProcessTimeout", "@php ./tests/phpstan/generate-baselines.php" ], 80 "phpstan:fix": [ "Composer\\Config::disableProcessTimeout", "@phpstan --configuration=phpstan.neon.dist --fix", "@phpstan:baselines --prune", "@phpstan --configuration=phpstan.neon.dist" ], 80 81 "compat": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --standard=phpcompat.xml.dist --report=summary,source", 81 82 "format": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf --report=summary,source", -
trunk/package.json
r65315 r65372 155 155 "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan --", 156 156 "typecheck:php:baselines": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan:baselines --", 157 "typecheck:php:fix": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan:fix --", 157 158 "gutenberg:copy": "node tools/gutenberg/copy.js", 158 159 "gutenberg:verify": "node tools/gutenberg/utils.js", -
trunk/tests/phpstan/README.md
r65265 r65372 51 51 - In a comment on the pull request, which lists every error in a table along with the commit it was first reported for. The comment is updated as new commits are pushed, moving each error that a commit resolves into a collapsed list of resolved errors, which also names that commit. Once all of the errors are resolved, the comment is marked as resolved and collapsed. 52 52 53 Some errors cannot be ignored, and so cannot be reported as warnings either. These fail the check. The most common is an [out-of-date baseline](#re generating-the-baselines), which is reported when an error recorded in a baseline was fixed without the baseline being regenerated. Such an error is reported against a file rather than a line in it, so its annotation does not appear alongside the changes, and it would easily go unnoticed if the check passed.53 Some errors cannot be ignored, and so cannot be reported as warnings either. These fail the check. The most common is an [out-of-date baseline](#removing-stale-entries-with---fix), which is reported when an error recorded in a baseline was fixed without the baseline being updated. Such an error is reported against a file rather than a line in it, so its annotation does not appear alongside the changes, and it would easily go unnoticed if the check passed. 54 54 55 55 ## The PHPStan configuration … … 217 217 Both the path and the count matter. A new occurrence of an already baselined error does not match the entry, even in a file that is already listed, and is reported as a new error. That is the point of recording them this way: the baselines describe exactly what exists today, so nothing new slips in behind them. 218 218 219 The consequence is that **fixing a baselined error means regenerating its baseline as part of the same change**, because the count no longer matches. A count that no longer matches is reported as an `ignore.count` error, which PHPStan does not allow to be ignored or baselined. 219 The consequence is that **fixing a baselined error means updating its baseline as part of the same change**, because the count no longer matches. A count that no longer matches is reported as an `ignore.count` error, and an entry that matches nothing at all as an `ignore.unmatched` error, neither of which PHPStan allows to be ignored or baselined. Either [remove the stale entries with `--fix`](#removing-stale-entries-with---fix), or [regenerate the baselines](#regenerating-the-baselines). 220 221 ### Removing stale entries with `--fix` 222 223 PHPStan marks an error it can fix automatically with 🔧, and ends its report by saying how many there are. Stale ignores are among them: a baseline entry whose count is now too high, or that matches nothing at all, and an `@phpstan-ignore` comment that no longer ignores anything. Fix them in place with: 224 225 ```bash 226 npm run typecheck:php:fix 227 ``` 228 229 or, outside the Docker environment: 230 231 ```bash 232 composer phpstan:fix 233 ``` 234 235 This runs three steps, each only when the one before it succeeds: 236 237 1. PHPStan with its `--fix` option, which lowers the count of the entry, or removes the entry, leaving the rest of the baseline as it was. It is usually much faster than regenerating, since it reuses the result cache of the run that reported the errors. Unlike regenerating, it also removes an `@phpstan-ignore` comment that no longer matches. 238 2. `composer phpstan:baselines -- --prune`, which deletes any baseline left with no entries, along with its `includes` entry in `phpstan.neon.dist`. Removing the last entry of a baseline leaves the file behind, as `ignoreErrors: []`. Pruning analyzes nothing, so it can also be run on its own. 239 3. The analysis once more, which reuses the result cache and so is quick. PHPStan's `--fix` reports only what it fixed, and succeeds even when other errors remain, so this is what shows the errors that are left. 240 241 Keep in mind: 242 243 - **It fails when nothing is fixable.** PHPStan then says `No fixable errors found` and stops the script there, so run `composer phpstan` to see whether any errors remain. 244 - **It always analyzes the whole tree with `phpstan.neon.dist`**, whatever a local `phpstan.neon` says. PHPStan only reports stale ignores when it analyzes every path, and a configuration at another rule level can report a baselined error with a different message, in which case `--fix` would remove an entry that the default configuration still needs. 245 - **It fixes every fixable error the run reports**, not only stale ignores. A few other errors can also be fixed automatically, such as a [useless cast](https://phpstan.org/error-identifiers/cast.useless), so check the diff before committing it. 246 - PHPStan's `--fix-ignored`, run as `composer phpstan -- --fix-ignored`, goes further and also fixes the fixable errors which a baseline currently ignores. That is a way to work through a baseline, but expect larger diffs, and follow it with `composer phpstan:baselines -- --prune`. 247 248 The feature is marked experimental in PHPStan. See [the PHPStan documentation](https://phpstan.org/user-guide/command-line-usage) for details. 220 249 221 250 ### Regenerating the baselines 222 251 223 The baselines are generated, and should not be edited by hand . Regenerate them with:252 The baselines are generated, and should not be edited by hand, other than by `--fix` as described [above](#removing-stale-entries-with---fix). Regenerate them with: 224 253 225 254 ```bash … … 242 271 npm run typecheck:php:baselines -- --combined 243 272 273 # without analyzing anything, delete the baselines that list no entries: 274 npm run typecheck:php:baselines -- --prune 275 244 276 # the remaining options: 245 277 npm run typecheck:php:baselines -- --help … … 253 285 composer phpstan:baselines -- --identifier=variable.undefined 254 286 composer phpstan:baselines -- --combined 287 composer phpstan:baselines -- --prune 255 288 composer phpstan:baselines -- --help 256 289 ``` -
trunk/tests/phpstan/generate-baselines.php
r63460 r65372 19 19 * edit of the configuration. 20 20 * 21 * With `--prune`, nothing is analyzed: the baselines that no longer list any 22 * entries are deleted, and the list in the configuration is updated to match. That 23 * completes running PHPStan with `--fix`, which removes the entries of fixed 24 * errors in place but leaves a baseline it has emptied behind, and so 25 * `composer phpstan:fix` runs it with `--prune` after `--fix`. 26 * 21 27 * The intermediate baseline is generated in PHPStan's PHP format and read back 22 28 * with `require`, so the entries arrive as an array. Nothing has to parse, or … … 30 36 * composer phpstan:baselines -- --identifier=isset.variable --identifier=empty.variable 31 37 * composer phpstan:baselines -- --combined > all-errors.neon 38 * composer phpstan:baselines -- --prune 32 39 * 33 40 * @package WordPress … … 77 84 $only_identifiers = array(); 78 85 $combined = false; 86 $prune = false; 79 87 80 88 foreach ( $args as $arg ) { … … 86 94 if ( '--combined' === $arg ) { 87 95 $combined = true; 96 continue; 97 } 98 99 if ( '--prune' === $arg ) { 100 $prune = true; 88 101 continue; 89 102 } … … 124 137 fwrite( STDERR, "Configuration not found: $config_option\n" ); 125 138 exit( 1 ); 139 } 140 141 /* 142 * Pruning needs no analysis. Running PHPStan with `--fix` lowers the counts of the 143 * entries whose errors were fixed and removes those that no longer match, but when 144 * it removes the last entry of a baseline it leaves the file behind, as 145 * `ignoreErrors: []`. That file suppresses nothing, so all that remains is to 146 * delete it and drop it from the configuration's `includes`, which is known from 147 * the file alone. 148 */ 149 if ( $prune ) { 150 if ( $combined ) { 151 fwrite( STDERR, "--prune and --combined cannot be used together.\n" ); 152 exit( 1 ); 153 } 154 155 $pruned = 0; 156 157 foreach ( find_baselines( $output_dir ) as $file ) { 158 $identifier = basename( $file, '.neon' ); 159 160 if ( $only_identifiers && ! in_array( $identifier, $only_identifiers, true ) ) { 161 continue; 162 } 163 164 $is_empty = is_empty_baseline( read_file( $file ) ); 165 166 if ( null === $is_empty ) { 167 fwrite( STDERR, sprintf( "%s: no `ignoreErrors` list found, left untouched.\n", $output_option . '/' . basename( $file ) ) ); 168 continue; 169 } 170 171 if ( ! $is_empty ) { 172 continue; 173 } 174 175 if ( ! unlink( $file ) ) { 176 fwrite( STDERR, sprintf( "%s: could not be deleted.\n", $output_option . '/' . basename( $file ) ) ); 177 exit( 1 ); 178 } 179 180 printf( "%s: no entries remain, file deleted.\n", $output_option . '/' . basename( $file ) ); 181 ++$pruned; 182 } 183 184 if ( 0 === $pruned ) { 185 echo "No empty baselines found.\n"; 186 } 187 188 update_config_includes( $config_path, $config_option, $output_dir ); 189 exit( 0 ); 126 190 } 127 191 … … 347 411 --combined Print one combined baseline to stdout instead of writing 348 412 per-identifier files. Nothing is written to disk. 413 --prune Without analyzing anything, delete the baselines that no 414 longer list any entries, such as one emptied by running 415 PHPStan with --fix, and update the configuration's 416 `includes` to match. With --identifier, only those 417 baselines are considered. 349 418 --memory-limit=<v> Passed through to PHPStan. Default: 2G 350 419 -h, --help Show this message. … … 364 433 composer phpstan:baselines -- --combined 365 434 435 Remove the entries of fixed errors, and any baseline left empty: 436 composer phpstan:fix 437 438 Remove only the baselines that list no entries: 439 composer phpstan:baselines -- --prune 440 366 441 TEXT; 367 442 } … … 370 445 * Reads a file, failing loudly rather than continuing with false. 371 446 * 372 * @param non- falsy-string $path Absolute path to the file.447 * @param non-empty-string $path Absolute path to the file. 373 448 * @return string File contents. 374 449 */ … … 531 606 532 607 return $files; 608 } 609 610 /** 611 * Determines whether a baseline ignores no errors. 612 * 613 * That is the case when its `ignoreErrors` is an empty inline list, as PHPStan's 614 * `--fix` writes after removing the last entry, or a key with nothing nested under 615 * it. Only whole-line comments are skipped, since a message pattern such as 616 * `'#^…$#'` contains `#` and so a trailing comment cannot be told apart from it 617 * without parsing the NEON. 618 * 619 * @param string $contents Baseline file contents. 620 * @return bool|null Whether the baseline is empty, or null when it has no `ignoreErrors` key. 621 */ 622 function is_empty_baseline( string $contents ): ?bool { 623 $lines = explode( "\n", $contents ); 624 625 foreach ( $lines as $i => $line ) { 626 if ( 1 !== preg_match( '/^(\s*)ignoreErrors:\s*(.*)$/', $line, $matches ) ) { 627 continue; 628 } 629 630 $value = trim( $matches[2] ); 631 632 if ( '' !== $value && '#' !== $value[0] ) { 633 return 1 === preg_match( '/^\[\s*\](\s*#.*)?$/', $value ); 634 } 635 636 // Nothing after the key, so its value is whatever is nested beneath it. 637 $indent = strlen( $matches[1] ); 638 639 foreach ( array_slice( $lines, $i + 1 ) as $next ) { 640 $trimmed = trim( $next ); 641 642 if ( '' === $trimmed || '#' === $trimmed[0] ) { 643 continue; 644 } 645 646 $next_indent = strlen( $next ) - strlen( ltrim( $next ) ); 647 648 return $next_indent <= $indent; 649 } 650 651 return true; 652 } 653 654 return null; 533 655 } 534 656
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)