Repository: WordPress/WordPress-Coding-Standards (php)

Issue:
Support "default" as value for "// phpcs:set ... minimum_wp_version"
## Is your feature request related to a problem?
Looking at the repeated `// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 5.8` reset comments in a [test file](https://github.com/WordPress/WordPress-Coding-Standards/blob/feature/alternativefunctions-phpcsutils-and-modern-php/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc), then it seems like it's an ever-increasing list of places in code where future updates would need to be made when the default value changes, which could lead to some being missed.

## Describe the solution you'd like
I wondered if something like `// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version default` could be supported (and perhaps similar for any other numerical/version/often-changing values), such that the consuming sniff(s) could look up the `default` value in a single place and use the actual value. It centralises the default value for that WPCS release.

## Additional context (optional)
Very much an internally-facing potential improvement, which may not warrant the extra complexity, even if it was possible, which I'm not sure it is, as it may just be assigning a value to a class property - maybe a magic `__set()` in the [new trait](https://github.com/WordPress/WordPress-Coding-Standards/blob/develop/WordPress/Helpers/MinimumWPVersionTrait.php) could be used?

```php
public function __set( $name, $value ) {
	if ( 'minimum_wp_version' === $name && 'default' === $value ) {
		$this->minimum_wp_version = '5.8'; // Update this for each WPCS release - no need to touch test files.
	}
}
```


Pull request description:
Helpers/MinimumWPVersionTrait: reduce use of magic number + other minor improvement
### Helpers/MinimumWPVersionTrait: split wp_version_compare() method

.. into two methods:
* `wp_version_compare()` which does the actual compare.
* `normalize_version_number()` to handle the version number normalization.

This is just a code simplification/removal of duplicate code. No functional changes.

### Helpers/MinimumWPVersionTrait: reduce use of magic number

The default value for the `minimum_wp_version` property is used in a number of places, mostly in the test files when resetting a changed value for the property to its default, making it a "magic number".

Preferably, a magic number should be assigned to a constant and the constant should be used everywhere.
Unfortunately, that's not possible as a) constants in traits are not allowed prior to PHP 8.2 and b) code in test case files does not get executed, so using the constant is not an option anyway.

This commit now makes couple of changes to improve/stabilize working with this "magic" number.
* The magic number is now only set in one place  - in the new `$default_minimum_wp_version` property.
* A user-provided `minimum_wp_version` property value from a ruleset will now be validated in the same way as a CLI value for the same.
    If no value is found or an invalid value is found, the default value for the `minimum_wp_version` property will be used.
    Includes making it explicit in the `composer.json` file that the `Filter` extension is a prerequisite for this package.
* In test case files, a "reset" of the `minimum_wp_version` property can now unset the value, lowering the maintenance burden when the default value for the `minimum_wp_version` property changes.
    Note: test files for the `Deprecated*` sniffs will still need updating whenever the value changes.

---

**Open question**

I wonder if we should rename the `get_wp_version_from_cli()` method to a better name. The method was already renamed in WPCS 3.0.0, so renaming it again does not add a breaking change which wasn't already there in the first place.

The reason is that, on the one hand, the `get_` prefix suggests that the method returns a value, which it doesn't. And on the other hand, the `_from_cli` isn't fully covering the functionality anymore.

Name suggestions welcome.

Some (rough) ideas:
* `validate_minimum_wp_version()`
* `set_minimum_wp_version()`

Opinions ?

---

Fixes #2171

### :new: Helpers/MinimumWPVersionTrait: rename method

... from `get_wp_version_from_cli()` to `set_minimum_wp_version()`.

Patch:
diff --git a/WordPress/Helpers/MinimumWPVersionTrait.php b/WordPress/Helpers/MinimumWPVersionTrait.php
index 22eb5ab3..df39567a 100644
--- a/WordPress/Helpers/MinimumWPVersionTrait.php
+++ b/WordPress/Helpers/MinimumWPVersionTrait.php
@@ -17,7 +17,7 @@ use PHPCSUtils\BackCompat\Helper;
  *
  * Usage instructions:
  * - Add appropriate `use` statement(s) to the file/class which intends to use this functionality.
- * - Call the `MinimumWPVersionTrait::get_wp_version_from_cli()` method in the `process()`/`process_token()`
+ * - Call the `MinimumWPVersionTrait::set_minimum_wp_version()` method in the `process()`/`process_token()`
  *   method.
  * - After that, the `MinimumWPVersionTrait::$minimum_wp_version` property can be freely used
  *   in the sniff.
@@ -68,7 +68,22 @@ trait MinimumWPVersionTrait {
 	 *
 	 * @var string WordPress version.
 	 */
-	public $minimum_wp_version = '5.8';
+	public $minimum_wp_version;
+
+	/**
+	 * Default minimum supported WordPress version.
+	 *
+	 * By default, the minimum_wp_version presumes that a project will support the current
+	 * WP version and up to three releases before.
+	 *
+	 * {@internal This should be a constant, but constants in traits are not supported
+	 *            until PHP 8.2.}}
+	 *
+	 * @since 3.0.0
+	 *
+	 * @var string WordPress version.
+	 */
+	private $default_minimum_wp_version = '5.8';
 
 	/**
 	 * Overrule the minimum supported WordPress version with a command-line/config value.
@@ -80,28 +95,34 @@ trait MinimumWPVersionTrait {
 	 *
 	 * @since 0.14.0
 	 * @since 3.0.0  - Moved from the Sniff class to this dedicated Trait.
-	 *               - Renamed from `get_wp_version_from_cl()` to `get_wp_version_from_cli()`.
+	 *               - Renamed from `get_wp_version_from_cl()` to `set_minimum_wp_version()`.
 	 */
-	protected function get_wp_version_from_cli() {
-		$cli_supported_version = Helper::getConfigData( 'minimum_wp_version' );
+	protected function set_minimum_wp_version() {
+		$minimum_wp_version = '';
 
-		if ( empty( $cli_supported_version ) ) {
-			return;
+		// Use a ruleset provided value if available.
+		if ( ! empty( $this->minimum_wp_version ) ) {
+			$minimum_wp_version = $this->minimum_wp_version;
+		}
+
+		// A CLI provided value overrules a ruleset provided value.
+		$cli_supported_version = Helper::getConfigData( 'minimum_wp_version' );
+		if ( ! empty( $cli_supported_version ) ) {
+			$minimum_wp_version = $cli_supported_version;
 		}
 
-		$cli_supported_version = trim( $cli_supported_version );
-		if ( ! empty( $cli_supported_version )
-			&& filter_var( $cli_supported_version, \FILTER_VALIDATE_FLOAT ) !== false
-		) {
-			$this->minimum_wp_version = $cli_supported_version;
+		// If no valid value was provided, use the default.
+		if ( filter_var( $minimum_wp_version, \FILTER_VALIDATE_FLOAT ) === false ) {
+			$minimum_wp_version = $this->default_minimum_wp_version;
 		}
+
+		$this->minimum_wp_version = $minimum_wp_version;
 	}
 
 	/**
 	 * Compares two version numbers.
 	 *
-	 * Ensures that the version numbers are comparable via the PHP version_compare() function
-	 * by making sure they comply with the minimum "PHP-standardized" version number requirements.
+	 * @since 3.0.0
 	 *
 	 * @param string $version1 First version number.
 	 * @param string $version2 Second version number.
@@ -110,14 +131,31 @@ trait MinimumWPVersionTrait {
 	 * @return bool
 	 */
 	protected function wp_version_compare( $version1, $version2, $operator ) {
-		if ( preg_match( '`^\d+\.\d+$`', $version1 ) ) {
-			$version1 .= '.0';
-		}
+		$version1 = $this->normalize_version_number( $version1 );
+		$version2 = $this->normalize_version_number( $version2 );
 
-		if ( preg_match( '`^\d+\.\d+$`', $version2 ) ) {
-			$version2 .= '.0';
+		return version_compare( $version1, $version2, $operator );
+	}
+
+	/**
+	 * Normalize a version number.
+	 *
+	 * Ensures that a version number is comparable via the PHP version_compare() function
+	 * by making sure it complies with the minimum "PHP-standardized" version number requirements.
+	 *
+	 * Presumes the input is a numeric version number string. The behaviour with other input is undetermined.
+	 *
+	 * @since 3.0.0
+	 *
+	 * @param string $version Version number.
+	 *
+	 * @return string
+	 */
+	private function normalize_version_number( $version ) {
+		if ( preg_match( '`^\d+\.\d+$`', $version ) ) {
+			$version .= '.0';
 		}
 
-		return version_compare( $version1, $version2, $operator );
+		return $version;
 	}
 }
diff --git a/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php b/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php
index e5d6b589..1930837d 100644
--- a/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php
+++ b/WordPress/Sniffs/WP/AlternativeFunctionsSniff.php
@@ -221,7 +221,7 @@ class AlternativeFunctionsSniff extends AbstractFunctionRestrictionsSniff {
 	 */
 	public function process_matched_token( $stackPtr, $group_name, $matched_content ) {
 
-		$this->get_wp_version_from_cli();
+		$this->set_minimum_wp_version();
 
 		/*
 		 * Deal with exceptions.
diff --git a/WordPress/Sniffs/WP/CapabilitiesSniff.php b/WordPress/Sniffs/WP/CapabilitiesSniff.php
index c246c1cf..a70985b5 100644
--- a/WordPress/Sniffs/WP/CapabilitiesSniff.php
+++ b/WordPress/Sniffs/WP/CapabilitiesSniff.php
@@ -434,7 +434,7 @@ class CapabilitiesSniff extends AbstractFunctionParameterSniff {
 		}
 
 		if ( isset( $this->deprecated_capabilities[ $matched_parameter ] ) ) {
-			$this->get_wp_version_from_cli( $this->phpcsFile );
+			$this->set_minimum_wp_version( $this->phpcsFile );
 			$is_error = $this->wp_version_compare( $this->deprecated_capabilities[ $matched_parameter ], $this->minimum_wp_version, '<' );
 
 			$data = array(
diff --git a/WordPress/Sniffs/WP/DeprecatedClassesSniff.php b/WordPress/Sniffs/WP/DeprecatedClassesSniff.php
index 6fedb059..201ae5ed 100644
--- a/WordPress/Sniffs/WP/DeprecatedClassesSniff.php
+++ b/WordPress/Sniffs/WP/DeprecatedClassesSniff.php
@@ -97,7 +97,7 @@ class DeprecatedClassesSniff extends AbstractClassRestrictionsSniff {
 	 */
 	public function process_matched_token( $stackPtr, $group_name, $matched_content ) {
 
-		$this->get_wp_version_from_cli();
+		$this->set_minimum_wp_version();
 
 		$class_name = ltrim( strtolower( $matched_content ), '\\' );
 
diff --git a/WordPress/Sniffs/WP/DeprecatedFunctionsSniff.php b/WordPress/Sniffs/WP/DeprecatedFunctionsSniff.php
index 129ce3b2..af461e10 100644
--- a/WordPress/Sniffs/WP/DeprecatedFunctionsSniff.php
+++ b/WordPress/Sniffs/WP/DeprecatedFunctionsSniff.php
@@ -1526,7 +1526,7 @@ class DeprecatedFunctionsSniff extends AbstractFunctionRestrictionsSniff {
 	 */
 	public function process_matched_token( $stackPtr, $group_name, $matched_content ) {
 
-		$this->get_wp_version_from_cli();
+		$this->set_minimum_wp_version();
 
 		$function_name = strtolower( $matched_content );
 
diff --git a/WordPress/Sniffs/WP/DeprecatedParameterValuesSniff.php b/WordPress/Sniffs/WP/DeprecatedParameterValuesSniff.php
index 365d8ffb..b8132999 100644
--- a/WordPress/Sniffs/WP/DeprecatedParameterValuesSniff.php
+++ b/WordPress/Sniffs/WP/DeprecatedParameterValuesSniff.php
@@ -224,7 +224,7 @@ class DeprecatedParameterValuesSniff extends AbstractFunctionParameterSniff {
 	 * @return void
 	 */
 	public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
-		$this->get_wp_version_from_cli();
+		$this->set_minimum_wp_version();
 
 		foreach ( $this->target_functions[ $matched_content ] as $position => $parameter_args ) {
 			$found_param = PassedParameters::getParameterFromStack( $parameters, $position, $parameter_args['name'] );
diff --git a/WordPress/Sniffs/WP/DeprecatedParametersSniff.php b/WordPress/Sniffs/WP/DeprecatedParametersSniff.php
index a8891dfc..f8ecb799 100644
--- a/WordPress/Sniffs/WP/DeprecatedParametersSniff.php
+++ b/WordPress/Sniffs/WP/DeprecatedParametersSniff.php
@@ -323,7 +323,7 @@ class DeprecatedParametersSniff extends AbstractFunctionParameterSniff {
 	 */
 	public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
 
-		$this->get_wp_version_from_cli();
+		$this->set_minimum_wp_version();
 
 		$paramCount = \count( $parameters );
 		foreach ( $this->target_functions[ $matched_content ] as $position => $parameter_args ) {
diff --git a/composer.json b/composer.json
index 433c2138..8c236b03 100644
--- a/composer.json
+++ b/composer.json
@@ -17,6 +17,7 @@
 	],
 	"require": {
 		"php": ">=5.4",
+		"ext-filter": "*",
 		"squizlabs/php_codesniffer": "^3.7.1",
 		"phpcsstandards/phpcsutils": "^1.0",
 		"phpcsstandards/phpcsextra": "^1.0"


Test patch:
diff --git a/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc b/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc
index e5212cf5..a88e5638 100644
--- a/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc
+++ b/WordPress/Tests/WP/AlternativeFunctionsUnitTest.inc
@@ -83,7 +83,7 @@ fputs(); // Warning.
 parse_url( 'http://example.com/' ); // OK, alternative was not yet available.
 // phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 4.4
 parse_url( 'http://example.com/' ); // Warning, not using $component param, so can switch over.
-// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 5.8
+// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version
 
 // phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 4.0
 parse_url($url, PHP_URL_QUERY); // OK, alternative was not yet available.
@@ -91,7 +91,7 @@ parse_url($url, PHP_URL_QUERY); // OK, alternative was not yet available.
 parse_url($url, PHP_URL_QUERY); // OK, $component param not yet available.
 // phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 4.7
 parse_url($url, PHP_URL_SCHEME); // Warning, using $component param, but also using WP 4.7+, so can switch over.
-// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 5.8
+// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version
 
 /*
  * Tests for support for PHP 8.0+ named parameters.
@@ -108,7 +108,7 @@ parse_url(component : PHP_URL_QUERY, url : $url); // OK.
 parse_url(url : $url); // Warning.
 // phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 4.7
 parse_url(component: PHP_URL_SCHEME, url: $url, ); // Warning.
-// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version 5.8
+// phpcs:set WordPress.WP.AlternativeFunctions minimum_wp_version
 
 // Safeguard support for PHP 8.0+ named parameters for the custom logic related to file_get_contents().
 file_get_contents( use_include_path: true, filename: $local_file, ); // OK.
diff --git a/WordPress/Tests/WP/CapabilitiesUnitTest.1.inc b/WordPress/Tests/WP/CapabilitiesUnitTest.1.inc
index f0de254a..b17784db 100644
--- a/WordPress/Tests/WP/CapabilitiesUnitTest.1.inc
+++ b/WordPress/Tests/WP/CapabilitiesUnitTest.1.inc
@@ -45,7 +45,7 @@ if ( author_can( $post, '' ) ) { } // Error.
 // phpcs:set WordPress.WP.Capabilities minimum_wp_version 2.9
 if ( author_can( $post, 'level_3' ) ) { } // Warning.
 
-// phpcs:set WordPress.WP.Capabilities minimum_wp_version 5.9
+// phpcs:set WordPress.WP.Capabilities minimum_wp_version
 if ( author_can( $post, 'level_5' ) ) { } // Error.
 add_options_page( 'page_title', 'menu_title', 'level_10', 'menu_slug', 'function' ); // Error.