Commit b3a6f4a8e8 for woocommerce

commit b3a6f4a8e8ba7dae3c8c5ec0887e049f37ab0e74
Author: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com>
Date:   Fri Jan 23 17:11:39 2026 +0100

    Ensure that checking compatibility with deprecated features does not trigger deprecation notices (#62884)

    * Ensure that checking compatibility with deprecated features does not trigger deprecation notices

    Some plugins have build systems that fail when any deprecation notice is triggered. This commit adds additional backwards-compatibility protection to chagnes introduced in  https://github.com/woocommerce/woocommerce/pull/62264/

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Add @since tag to new method

    * Refactor: Move deprecation logging to public API boundary in FeaturesUtil

    * Simpler empty check

    ---------

    Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>

diff --git a/plugins/woocommerce/changelog/62884-fix-features-deprecation-notice b/plugins/woocommerce/changelog/62884-fix-features-deprecation-notice
new file mode 100644
index 0000000000..02dc0d1209
--- /dev/null
+++ b/plugins/woocommerce/changelog/62884-fix-features-deprecation-notice
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Ensure there's no deprecation warning about deprecated feature flags when plugins are activated.
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index cc63d6f639..f8e96c04d1 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -808,37 +808,16 @@ class FeaturesController {
 	 *
 	 * @since 10.5.0
 	 */
-	private function get_feature_definition( string $feature_id ): ?array {
+	public function get_feature_definition( string $feature_id ): ?array {
 		return $this->get_feature_definitions()[ $feature_id ] ?? null;
 	}

-	/**
-	 * Log usage of a deprecated feature.
-	 *
-	 * This method ensures logging only happens once per request to avoid spam.
-	 *
-	 * @param string $feature_id       The feature id being checked.
-	 * @param string $deprecated_since The version since which the feature is deprecated.
-	 *
-	 * @since 10.5.0
-	 */
-	private function log_deprecated_feature_usage( string $feature_id, string $deprecated_since ): void {
-		static $logged = array();
-
-		if ( isset( $logged[ $feature_id ] ) ) {
-			return;
-		}
-		$logged[ $feature_id ] = true;
-
-		wc_deprecated_function(
-			"FeaturesUtil::feature_is_enabled('{$feature_id}')",
-			$deprecated_since
-		);
-	}
-
 	/**
 	 * Check if a given feature is currently enabled.
 	 *
+	 * Note: This method does not log deprecation notices for deprecated features.
+	 * Deprecation logging is handled by FeaturesUtil::feature_is_enabled() which is the public API.
+	 *
 	 * @param  string $feature_id Unique feature id.
 	 * @return bool True if the feature is enabled, false if not or if the feature doesn't exist.
 	 */
@@ -851,7 +830,6 @@ class FeaturesController {

 		// Handle deprecated features - return the backwards-compatible value.
 		if ( ! empty( $feature['deprecated_since'] ) ) {
-			$this->log_deprecated_feature_usage( $feature_id, $feature['deprecated_since'] );
 			return (bool) ( $feature['deprecated_value'] ?? false );
 		}

@@ -1042,6 +1020,7 @@ class FeaturesController {
 		$this->verify_did_woocommerce_init( __FUNCTION__ );

 		$features = $this->get_feature_definitions();
+
 		if ( $enabled_features_only ) {
 			$features = array_filter(
 				$features,
diff --git a/plugins/woocommerce/src/Utilities/FeaturesUtil.php b/plugins/woocommerce/src/Utilities/FeaturesUtil.php
index 30049fd398..f8a10ccb3b 100644
--- a/plugins/woocommerce/src/Utilities/FeaturesUtil.php
+++ b/plugins/woocommerce/src/Utilities/FeaturesUtil.php
@@ -39,7 +39,37 @@ class FeaturesUtil {
 	 * @return bool True if the feature is enabled, false if not or if the feature doesn't exist.
 	 */
 	public static function feature_is_enabled( string $feature_id ): bool {
-		return wc_get_container()->get( FeaturesController::class )->feature_is_enabled( $feature_id );
+		$features_controller = wc_get_container()->get( FeaturesController::class );
+		$feature             = $features_controller->get_feature_definition( $feature_id );
+
+		// Log deprecation notice for deprecated features (only from the public API).
+		if ( ! empty( $feature['deprecated_since'] ) ) {
+			self::log_deprecated_feature_usage( $feature_id, $feature['deprecated_since'] );
+		}
+
+		return $features_controller->feature_is_enabled( $feature_id );
+	}
+
+	/**
+	 * Log usage of a deprecated feature.
+	 *
+	 * This method ensures logging only happens once per request to avoid spam.
+	 *
+	 * @param string $feature_id       The feature id being checked.
+	 * @param string $deprecated_since The version since which the feature is deprecated.
+	 */
+	private static function log_deprecated_feature_usage( string $feature_id, string $deprecated_since ): void {
+		static $logged = array();
+
+		if ( isset( $logged[ $feature_id ] ) ) {
+			return;
+		}
+		$logged[ $feature_id ] = true;
+
+		wc_deprecated_function(
+			"FeaturesUtil::feature_is_enabled('{$feature_id}')",
+			$deprecated_since
+		);
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php
index cc3a9aeeb9..9973d23637 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Features/FeaturesControllerTest.php
@@ -620,6 +620,225 @@ class FeaturesControllerTest extends \WC_Unit_Test_Case {
 		$this->assertEquals( $expected, $result );
 	}

+	/**
+	 * @testdox Deprecated features are included in 'get_compatible_features_for_plugin' results.
+	 */
+	public function test_deprecated_features_included_in_get_compatible_features_for_plugin() {
+		add_action(
+			'woocommerce_register_feature_definitions',
+			function ( $features_controller ) {
+				$features = array(
+					'active_feature'     => array(
+						'name'                         => 'Active feature',
+						'description'                  => 'An active feature',
+						'is_experimental'              => false,
+						'default_plugin_compatibility' => 'compatible',
+					),
+					'deprecated_feature' => array(
+						'name'                         => 'Deprecated feature',
+						'description'                  => 'A deprecated feature',
+						'is_experimental'              => false,
+						'default_plugin_compatibility' => 'compatible',
+						'deprecated_since'             => '10.5.0',
+						'deprecated_value'             => true,
+					),
+				);
+
+				$this->reset_features_list( $features_controller, $features );
+			},
+			20
+		);
+
+		$this->sut = new FeaturesController();
+		$this->sut->init( wc_get_container()->get( LegacyProxy::class ), $this->fake_plugin_util );
+		$this->simulate_inside_before_woocommerce_init_hook();
+
+		$this->sut->declare_compatibility( 'active_feature', 'the_plugin', true );
+		$this->sut->declare_compatibility( 'deprecated_feature', 'the_plugin', true );
+		$this->reset_legacy_proxy_mocks();
+		$this->simulate_after_woocommerce_init_hook();
+
+		// Test without enabled_features_only - all features should appear.
+		$result = $this->sut->get_compatible_features_for_plugin( 'the_plugin', false );
+
+		// Both features should appear in compatible list.
+		$this->assertContains( 'active_feature', $result['compatible'] );
+		$this->assertContains( 'deprecated_feature', $result['compatible'] );
+	}
+
+	/**
+	 * @testdox Deprecated features with deprecated_value=true are included when filtering by enabled features.
+	 */
+	public function test_deprecated_features_with_true_value_included_when_filtering_enabled_features() {
+		add_action(
+			'woocommerce_register_feature_definitions',
+			function ( $features_controller ) {
+				$features = array(
+					'active_feature'              => array(
+						'name'                         => 'Active feature',
+						'description'                  => 'An active feature',
+						'is_experimental'              => false,
+						'default_plugin_compatibility' => 'compatible',
+					),
+					'deprecated_enabled_feature'  => array(
+						'name'                         => 'Deprecated enabled feature',
+						'description'                  => 'A deprecated feature with true value',
+						'is_experimental'              => false,
+						'default_plugin_compatibility' => 'compatible',
+						'deprecated_since'             => '10.5.0',
+						'deprecated_value'             => true,
+					),
+					'deprecated_disabled_feature' => array(
+						'name'                         => 'Deprecated disabled feature',
+						'description'                  => 'A deprecated feature with false value',
+						'is_experimental'              => false,
+						'default_plugin_compatibility' => 'compatible',
+						'deprecated_since'             => '10.5.0',
+						'deprecated_value'             => false,
+					),
+				);
+
+				$this->reset_features_list( $features_controller, $features );
+			},
+			20
+		);
+
+		$this->sut = new FeaturesController();
+		$this->sut->init( wc_get_container()->get( LegacyProxy::class ), $this->fake_plugin_util );
+		$this->simulate_inside_before_woocommerce_init_hook();
+
+		$this->sut->declare_compatibility( 'active_feature', 'the_plugin', true );
+		$this->sut->declare_compatibility( 'deprecated_enabled_feature', 'the_plugin', true );
+		$this->sut->declare_compatibility( 'deprecated_disabled_feature', 'the_plugin', true );
+		$this->reset_legacy_proxy_mocks();
+		$this->simulate_after_woocommerce_init_hook();
+
+		update_option( 'woocommerce_feature_active_feature_enabled', 'yes' );
+
+		// Test with enabled_features_only = true.
+		$result = $this->sut->get_compatible_features_for_plugin( 'the_plugin', true );
+
+		// Active and deprecated_enabled features should appear (deprecated_value=true).
+		$this->assertContains( 'active_feature', $result['compatible'] );
+		$this->assertContains( 'deprecated_enabled_feature', $result['compatible'] );
+		// Deprecated with deprecated_value=false should NOT appear.
+		$this->assertNotContains( 'deprecated_disabled_feature', $result['compatible'] );
+		$this->assertNotContains( 'deprecated_disabled_feature', $result['incompatible'] );
+		$this->assertNotContains( 'deprecated_disabled_feature', $result['uncertain'] );
+	}
+
+	/**
+	 * @testdox Deprecated features can be checked in 'get_incompatible_plugins' without triggering deprecation notices.
+	 */
+	public function test_deprecated_features_in_get_incompatible_plugins_without_notices() {
+		add_action(
+			'woocommerce_register_feature_definitions',
+			function ( $features_controller ) {
+				$features = array(
+					'active_feature'              => array(
+						'name'                         => 'Active feature',
+						'description'                  => 'An active feature',
+						'is_experimental'              => false,
+						'default_plugin_compatibility' => 'incompatible',
+					),
+					'deprecated_enabled_feature'  => array(
+						'name'                         => 'Deprecated enabled feature',
+						'description'                  => 'A deprecated feature that is considered enabled',
+						'is_experimental'              => false,
+						'default_plugin_compatibility' => 'incompatible',
+						'deprecated_since'             => '10.5.0',
+						'deprecated_value'             => true,
+					),
+					'deprecated_disabled_feature' => array(
+						'name'                         => 'Deprecated disabled feature',
+						'description'                  => 'A deprecated feature that is considered disabled',
+						'is_experimental'              => false,
+						'default_plugin_compatibility' => 'incompatible',
+						'deprecated_since'             => '10.5.0',
+						'deprecated_value'             => false,
+					),
+				);
+
+				$this->reset_features_list( $features_controller, $features );
+			},
+			20
+		);
+
+		// phpcs:disable Squiz.Commenting
+		$fake_plugin_util = new class() extends PluginUtil {
+			private $active_plugins;
+
+			public function __construct() {
+			}
+
+			public function set_active_plugins( $plugins ) {
+				$this->active_plugins = $plugins;
+			}
+
+			public function get_woocommerce_aware_plugins( bool $active_only = false ): array {
+				return $this->active_plugins;
+			}
+
+			public function is_woocommerce_aware_plugin( $plugin ): bool {
+				return in_array( $plugin, $this->active_plugins, true );
+			}
+
+			public function get_plugins_excluded_from_compatibility_ui() {
+				return array();
+			}
+
+			public function get_wp_plugin_id( $plugin_file ) {
+				return $plugin_file;
+			}
+		};
+		// phpcs:enable Squiz.Commenting
+
+		// Set private $proxy via reflection.
+		$parent_reflection = new \ReflectionClass( PluginUtil::class );
+		$proxy_prop        = $parent_reflection->getProperty( 'proxy' );
+		$proxy_prop->setAccessible( true );
+		$proxy_prop->setValue( $fake_plugin_util, wc_get_container()->get( LegacyProxy::class ) );
+
+		$this->sut = new FeaturesController();
+		$this->sut->init( wc_get_container()->get( LegacyProxy::class ), $fake_plugin_util );
+		$this->simulate_inside_before_woocommerce_init_hook();
+
+		$fake_plugin_util->set_active_plugins( array( 'test_plugin' ) );
+
+		$this->register_legacy_proxy_function_mocks(
+			array(
+				'is_plugin_active' => function ( $plugin ) {
+					unset( $plugin );
+					return true;
+				},
+			)
+		);
+
+		$this->reset_legacy_proxy_mocks();
+		$this->simulate_after_woocommerce_init_hook();
+
+		$this->register_legacy_proxy_function_mocks(
+			array(
+				'is_plugin_active' => function ( $plugin ) {
+					unset( $plugin );
+					return true;
+				},
+			)
+		);
+
+		update_option( 'woocommerce_feature_active_feature_enabled', 'yes' );
+
+		$incompatible_plugins = function () {
+			return $this->get_incompatible_plugins( 'all', array( 'test_plugin' => array() ) );
+		};
+		$result               = $incompatible_plugins->call( $this->sut );
+
+		// The method should complete without triggering deprecation notices.
+		// Deprecated features with deprecated_value=true are still included in checks.
+		// Deprecated features with deprecated_value=false are excluded from enabled-only checks.
+		$this->assertIsArray( $result );
+	}
+
 	/**
 	 * @testdox 'get_compatible_plugins_for_feature' fails when invoked before the 'woocommerce_init' hook.
 	 */