Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance code quality by adding PHPStan and fixing level 0 issues #730

Merged
merged 17 commits into from Jun 13, 2023

Conversation

westonruter
Copy link
Member

@westonruter westonruter commented May 16, 2023

Summary

Fixes #727

Relevant technical choices

  • Add a phpstan composer script. This name is chosen instead of analyze since also spelled analyse.
  • Run PHPStan in the php-lint workflow after running PHPCS. This runs whenever a PHP-related file has changed.
  • Also run PHPStan in lint-staged whenever a PHP file is modified.
  • Add PHPStan config with szepeviktor/phpstan-wordpress and bleeding-edge, starting at level 0. Additional PRs will raise the level and fix more issues.
  • Fix issues uncovered by level 0.

Checklist

  • PR has either [Focus] or Infrastructure label.
  • PR has a [Type] label.
  • PR has a milestone or the no milestone label.

@westonruter westonruter added [Type] Enhancement A suggestion for improvement of an existing feature Infrastructure Issues for the overall performance plugin infrastructure labels May 16, 2023
@westonruter westonruter added this to the 2.4.0 milestone May 16, 2023
@westonruter westonruter changed the title Add PHPStan Add PHPStan and fix level 0 issues May 16, 2023
@westonruter westonruter marked this pull request as ready for review May 16, 2023 02:49
@szepeviktor
Copy link

Please consider reaching level 4.
Under level 4 there could be critical bugs and bad design decisions.

@spacedmonkey
Copy link
Member

@westonruter Do this work in PHP 5.6? As I remember, phpstan requires PHP 7.2+

@szepeviktor
Copy link

PHPStan does static analysis, so it does not run the code.
Runtime PHP version can be set! https://phpstan.org/config-reference#phpversion

@spacedmonkey
Copy link
Member

PHPStan does static analysis, so it does not run the code. Runtime PHP version can be set! https://phpstan.org/config-reference#phpversion

I am talking about composer installing and requires.

@@ -14,8 +14,11 @@
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.7",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/extension-installer": "^1.3.0",
"phpstan/phpstan": "^1.10",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically not needed because it's a dependency of szepeviktor/phpstan-wordpress already

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I thought about that. But I thought this might get us PHPStan updates more quickly by being an explicit dependency. Otherwise we'd need to wait for the package to update, AFAIK.

"phpunit/phpunit": "^4|^5|^6|^7|^8|^9",
"squizlabs/php_codesniffer": "^3.5",
"szepeviktor/phpstan-wordpress": "^1.3.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that this package and PHPStan itself require PHP 7.2.

So when we start running tests against PHP 5.6 (see #399), composer install would break.

The plugin itself wouldn't break, as it doesn't use the autoloader.

Same discussion as in #729 really.

Check out WordPress/plugin-check#166 for a workaround by moving these deps to a separate composer.json file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point @swissspidy, let's apply the same approach here as we want to support PHP 5.6 too for our test suite as we should preferably run the tests on that version too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand, there are three possible ways to about this:

  1. Have a separate composer file for PHP 7.2+. (Just seeing Improve PHP cross-version support and testing plugin-check#166 now.)
  2. In GHA workflow, just-in-time composer require this package before doing composer install before running PHPCS
  3. In GHA workflow, just-in-time composer remove this package before doing composer install prior to running PHPUnit in PHP<7.2.

I think the third option is ideal, since all normal development environments would be running PHP 7.2+. It's only the exception case, specifically on GHA, where PHP 5.6 is needed. This would allow a developer to have all packages installed normally.

The first option doesn't seem ideal since there would be a separate composer file to maintain, and Dependabot wouldn't automatically be aware of it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about we address back-compat with PHP 5.6 as part of #399?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Downsides of (2) and (3) are that it's a less reproducible setup for developers, and that it's cumbersome to do for other packages as well (think all the PHPStan extensions or the Slevomat CS package). I did something like that in the past for downgrading/upgrading PHPUnit in CI, but it's not more maintainable than (1).

I wish (1) wasn't needed, but when dealing with such version requirements it's a necessary evil.

Dependabot wouldn't automatically be aware of it.

FWIW this is easily solved with another entry in the Dependabot config. Example:

  - package-ecosystem: "composer"
    directory: "/"
    schedule:
      interval: "daily"

  - package-ecosystem: "composer"
    directory: "/build-cs"
    schedule:
      interval: "daily"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3. In GHA workflow, just-in-time composer remove this package before doing composer install prior to running PHPUnit in PHP<7.2.

I just noticed that Gutenberg actually takes this approach instead of maintaining a separate composer.json.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this less maintainable than maintaining a separate composer.json?

"phpunit/phpunit": "^4|^5|^6|^7|^8|^9",
"squizlabs/php_codesniffer": "^3.5",
"szepeviktor/phpstan-wordpress": "^1.3.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can also highly recommend https://packagist.org/packages/swissspidy/phpstan-no-private to catch calls of seemingly private functions.

Plus https://packagist.org/packages/phpstan/phpstan-phpunit and https://packagist.org/packages/php-stubs/wordpress-tests-stubs to run PHPStan against tests as well. Believe me, it really improves the quality of the tests!

https://packagist.org/packages/phpstan/phpstan-deprecation-rules is also nice to prevent usage of deprecated functions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can also highly recommend https://packagist.org/packages/swissspidy/phpstan-no-private to catch calls of seemingly private functions.

I love this package, but I wonder if this is the best for Performance Lab considering that the code in question will end up in core and thus it is expected that many calls to internal/private functions will be made? As it stands right now...

There are 73 instances of this

 ------ ------------------------------------------------------- 
  Line   modules/database/sqlite/activate.php                   
 ------ ------------------------------------------------------- 
  71     Call to private/internal function wp_set_wpdb_vars().  
  111    Call to private/internal function wp_set_wpdb_vars().  
 ------ ------------------------------------------------------- 

 ------ ------------------------------------------------------------------------------------------------ 
  Line   modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-alter-query.php                 
 ------ ------------------------------------------------------------------------------------------------ 
  53     Call to private/internal method command_tokenizer() of class Perflab_SQLite_Alter_Query.        
  65     Call to private/internal method handle_single_command() of class Perflab_SQLite_Alter_Query.    
  69     Call to private/internal method handle_add_primary_key() of class Perflab_SQLite_Alter_Query.   
  73     Call to private/internal method handle_drop_primary_key() of class Perflab_SQLite_Alter_Query.  
  77     Call to private/internal method handle_modify_command() of class Perflab_SQLite_Alter_Query.    
  81     Call to private/internal method handle_change_command() of class Perflab_SQLite_Alter_Query.    
  85     Call to private/internal method handle_alter_command() of class Perflab_SQLite_Alter_Query.     
  261    Call to private/internal method convert_field_types() of class Perflab_SQLite_Alter_Query.      
  367    Call to private/internal method convert_field_types() of class Perflab_SQLite_Alter_Query.      
  421    Call to private/internal method convert_field_types() of class Perflab_SQLite_Alter_Query.      
  483    Call to private/internal method convert_field_types() of class Perflab_SQLite_Alter_Query.      
  500    Call to private/internal method convert_field_types() of class Perflab_SQLite_Alter_Query.      
  513    Call to private/internal method convert_field_types() of class Perflab_SQLite_Alter_Query.      
 ------ ------------------------------------------------------------------------------------------------ 

 ------ ----------------------------------------------------------------------------------------------- 
  Line   modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-create-query.php               
 ------ ----------------------------------------------------------------------------------------------- 
  76     Call to private/internal method strip_backticks() of class Perflab_SQLite_Create_Query.        
  77     Call to private/internal method quote_illegal_field() of class Perflab_SQLite_Create_Query.    
  78     Call to private/internal method get_table_name() of class Perflab_SQLite_Create_Query.         
  79     Call to private/internal method rewrite_comments() of class Perflab_SQLite_Create_Query.       
  80     Call to private/internal method rewrite_field_types() of class Perflab_SQLite_Create_Query.    
  81     Call to private/internal method rewrite_character_set() of class Perflab_SQLite_Create_Query.  
  82     Call to private/internal method rewrite_engine_info() of class Perflab_SQLite_Create_Query.    
  83     Call to private/internal method rewrite_unsigned() of class Perflab_SQLite_Create_Query.       
  84     Call to private/internal method rewrite_autoincrement() of class Perflab_SQLite_Create_Query.  
  85     Call to private/internal method rewrite_primary_key() of class Perflab_SQLite_Create_Query.    
  86     Call to private/internal method rewrite_foreign_key() of class Perflab_SQLite_Create_Query.    
  87     Call to private/internal method rewrite_unique_key() of class Perflab_SQLite_Create_Query.     
  88     Call to private/internal method rewrite_enum() of class Perflab_SQLite_Create_Query.           
  89     Call to private/internal method rewrite_set() of class Perflab_SQLite_Create_Query.            
  90     Call to private/internal method rewrite_key() of class Perflab_SQLite_Create_Query.            
  91     Call to private/internal method add_if_not_exists() of class Perflab_SQLite_Create_Query.      
  93     Call to private/internal method post_process() of class Perflab_SQLite_Create_Query.           
 ------ ----------------------------------------------------------------------------------------------- 

 ------ ---------------------------------------------------------------------------------------------------- 
  Line   modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-driver.php                      
 ------ ---------------------------------------------------------------------------------------------------- 
  81     Call to private/internal method parse_query() of class Perflab_SQLite_PDO_Driver.                   
  84     Call to private/internal method handle_truncate_query() of class Perflab_SQLite_PDO_Driver.         
  88     Call to private/internal method handle_alter_query() of class Perflab_SQLite_PDO_Driver.            
  92     Call to private/internal method handle_create_query() of class Perflab_SQLite_PDO_Driver.           
  97     Call to private/internal method handle_describe_query() of class Perflab_SQLite_PDO_Driver.         
  101    Call to private/internal method handle_show_query() of class Perflab_SQLite_PDO_Driver.             
  105    Call to private/internal method handle_show_columns_query() of class Perflab_SQLite_PDO_Driver.     
  109    Call to private/internal method handle_show_index() of class Perflab_SQLite_PDO_Driver.             
  115    Call to private/internal method rewrite_date_sub() of class Perflab_SQLite_PDO_Driver.              
  116    Call to private/internal method delete_index_hints() of class Perflab_SQLite_PDO_Driver.            
  117    Call to private/internal method rewrite_regexp() of class Perflab_SQLite_PDO_Driver.                
  119    Call to private/internal method fix_date_quoting() of class Perflab_SQLite_PDO_Driver.              
  120    Call to private/internal method rewrite_between() of class Perflab_SQLite_PDO_Driver.               
  121    Call to private/internal method handle_orderby_field() of class Perflab_SQLite_PDO_Driver.          
  126    Call to private/internal method execute_duplicate_key_update() of class Perflab_SQLite_PDO_Driver.  
  127    Call to private/internal method rewrite_insert_ignore() of class Perflab_SQLite_PDO_Driver.         
  128    Call to private/internal method rewrite_regexp() of class Perflab_SQLite_PDO_Driver.                
  129    Call to private/internal method fix_date_quoting() of class Perflab_SQLite_PDO_Driver.              
  134    Call to private/internal method rewrite_update_ignore() of class Perflab_SQLite_PDO_Driver.         
  136    Call to private/internal method rewrite_limit_usage() of class Perflab_SQLite_PDO_Driver.           
  137    Call to private/internal method rewrite_order_by_usage() of class Perflab_SQLite_PDO_Driver.        
  138    Call to private/internal method rewrite_regexp() of class Perflab_SQLite_PDO_Driver.                
  139    Call to private/internal method rewrite_between() of class Perflab_SQLite_PDO_Driver.               
  144    Call to private/internal method rewrite_limit_usage() of class Perflab_SQLite_PDO_Driver.           
  145    Call to private/internal method rewrite_order_by_usage() of class Perflab_SQLite_PDO_Driver.        
  146    Call to private/internal method rewrite_date_sub() of class Perflab_SQLite_PDO_Driver.              
  147    Call to private/internal method rewrite_regexp() of class Perflab_SQLite_PDO_Driver.                
  148    Call to private/internal method delete_workaround() of class Perflab_SQLite_PDO_Driver.             
  153    Call to private/internal method rewrite_date_sub() of class Perflab_SQLite_PDO_Driver.              
  154    Call to private/internal method rewrite_regexp() of class Perflab_SQLite_PDO_Driver.                
  158    Call to private/internal method rewrite_optimize() of class Perflab_SQLite_PDO_Driver.              
  168    Call to private/internal method return_true() of class Perflab_SQLite_PDO_Driver.                   
 ------ ---------------------------------------------------------------------------------------------------- 

 ------ ------------------------------------------------------------------------------------------------------- 
  Line   modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-engine.php                         
 ------ ------------------------------------------------------------------------------------------------------- 
  1260   Call to private/internal method convert_to_columns_object() of class Perflab_SQLite_PDO_Engine.        
  1262   Call to private/internal method convert_to_index_object() of class Perflab_SQLite_PDO_Engine.          
  1264   Call to private/internal method convert_result_check_or_analyze() of class Perflab_SQLite_PDO_Engine.  
 ------ ------------------------------------------------------------------------------------------------------- 

 ------ ------------------------------------------------------------------------------------------------------- 
  Line   modules/database/sqlite/wp-includes/sqlite/class-perflab-sqlite-pdo-user-defined-functions.php         
 ------ ------------------------------------------------------------------------------------------------------- 
  324    Call to private/internal method derive_interval() of class Perflab_SQLite_PDO_User_Defined_Functions.  
  356    Call to private/internal method derive_interval() of class Perflab_SQLite_PDO_User_Defined_Functions.  
 ------ ------------------------------------------------------------------------------------------------------- 

 ------ ------------------------------------------------------------------------------ 
  Line   modules/images/dominant-color-images/hooks.php                                
 ------ ------------------------------------------------------------------------------ 
  19     Call to private/internal function _dominant_color_get_dominant_color_data().  
 ------ ------------------------------------------------------------------------------ 

 ------ ------------------------------------------------------------------------------------ 
  Line   modules/images/webp-uploads/helper.php                                              
 ------ ------------------------------------------------------------------------------------ 
  222    Call to private/internal function webp_uploads_generate_additional_image_source().  
 ------ ------------------------------------------------------------------------------------ 

 ------ ------------------------------------------------------------------------------------ 
  Line   modules/images/webp-uploads/hooks.php                                               
 ------ ------------------------------------------------------------------------------------ 
  82     Call to private/internal function webp_uploads_generate_additional_image_source().  
  126    Call to private/internal function _wp_image_meta_replace_original().                
 ------ ------------------------------------------------------------------------------------ 

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plus https://packagist.org/packages/phpstan/phpstan-phpunit

Thanks for that. Added in 916dd25, fixing 2 found issues in 5e498d1.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and https://packagist.org/packages/php-stubs/wordpress-tests-stubs

Do we need the test subs since we're already installing wp-phpunit/wp-phpunit? Would this essentially be an alternative to:

scanDirectories:
- vendor/wp-phpunit/wp-phpunit/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://packagist.org/packages/phpstan/phpstan-deprecation-rules is also nice to prevent usage of deprecated functions.

Added in 4554f6a, issues fixed in bd1c378.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love this package, but I wonder if this is the best for for Performance Lab considering that the code in question will end up in core and thus it is expected that many calls to internal/private functions will be made? As it stands right now...

Good point! 😃

I think it can't hurt to use it anyway, since not everything will land in core. Places where usage is OK can be ignored in the PHPStan config accordingly. But maybe in another PR :-)

All the Perflab_SQLite_ reports are actually false positives! I'll get this fixed in a new release. (Edit: see swissspidy/phpstan-no-private#2)
They are not properly documented though. A private function ...() method does not need @access private annotations. That's against the coding standards.

Do we need the test subs since we're already installing wp-phpunit/wp-phpunit? Would this essentially be an alternative to:

Probably not in that case. Counter question: why is wp-phpunit even used here when all tests are running in wp-env anyway?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, also if our code here follows PHPStan, I don't think it would violate anything that core requires - we'd effectively have stricter requirements than core, which I think should work fine as long as it's in that direction and not the opposite :)

why is wp-phpunit even used here when all tests are running in wp-env anyway?

I believe that is for additional flexibility. We don't want to dictate anyone to use wp-env, it's just the easy way to go. If folks want to e.g. run a DB on their own machine, the use of wp-phpunit makes that cleaner and decouples from having to have the entire WP core dev environment.

@westonruter
Copy link
Member Author

Please consider reaching level 4. Under level 4 there could be critical bugs and bad design decisions.

@szepeviktor yes, that's the plan. Just for the initial PR level 0 is the first target.

@westonruter westonruter requested a review from pbearne as a code owner May 16, 2023 16:43
@felixarntz
Copy link
Member

@westonruter @swissspidy Could you please follow up on the discussions in this PR to figure out how to move it forward? Not critical, but since it's milestoned for 2.4.0 and stale for 2 weeks now, it would be great to keep this moving.

@westonruter
Copy link
Member Author

@felixarntz I think the only current holdup is #730 (comment) and how we deal with Composer dependencies that aren't compatible with 5.6. However, since tests don't currently run in 5.6, I think that issue should be punted for when #399 is addressed.

@felixarntz
Copy link
Member

@westonruter That works for me, given #399 is not currently milestoned and it seems to me this PR here is closer to completion than #544 (which is from September 2022 so likely stale at least to some degree).

@westonruter
Copy link
Member Author

I think this just needs an approval than so it can be merged.

Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @westonruter, LGTM!

@felixarntz
Copy link
Member

@westonruter

I think this just needs an approval than so it can be merged.

I've approved it, but every PR needs 2+ approvals, so let's wait for @swissspidy to give it another look next week.

@westonruter
Copy link
Member Author

westonruter commented May 31, 2023

Once this is merged we should open a PR that bumps PHPStan to level 1 and higher.

@swissspidy
Copy link
Member

@westonruter Mind creating the follow-up tickets for this?

@swissspidy swissspidy merged commit 92338e3 into trunk Jun 13, 2023
7 checks passed
@swissspidy swissspidy deleted the add/phpstan branch June 13, 2023 11:48
@felixarntz felixarntz changed the title Add PHPStan and fix level 0 issues Enhance code quality by adding PHPStan and fixing level 0 issues Jun 13, 2023
@westonruter
Copy link
Member Author

Once this is merged we should open a PR that bumps PHPStan to level 1 and higher.

See #775

I can also highly recommend https://packagist.org/packages/swissspidy/phpstan-no-private to catch calls of seemingly private functions.

See #776

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Infrastructure Issues for the overall performance plugin infrastructure [Type] Enhancement A suggestion for improvement of an existing feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Incorporate PHPStan for Static Analysis
6 participants