diff --git a/docs/checks.md b/docs/checks.md
index f7ffd0e72..4d01edc62 100644
--- a/docs/checks.md
+++ b/docs/checks.md
@@ -36,4 +36,5 @@
| enqueued_styles_scope | performance | Checks whether any stylesheets are loaded on all pages, which is usually not desirable and can lead to performance issues. | [Learn more](https://developer.wordpress.org/plugins/) |
| enqueued_scripts_scope | performance | Checks whether any scripts are loaded on all pages, which is usually not desirable and can lead to performance issues. | [Learn more](https://developer.wordpress.org/plugins/) |
| non_blocking_scripts | performance | Checks whether scripts and styles are enqueued using a recommended loading strategy. | [Learn more](https://developer.wordpress.org/plugins/) |
+| autoloaded_options | performance | Warns when add_option() or update_option() are called without explicitly setting the $autoload parameter. | [Learn more](https://developer.wordpress.org/reference/functions/add_option/) |
| ai_provider | general | Recommends the WordPress AI Client when a plugin integrates directly with a third-party AI provider. | [Learn more](https://developer.wordpress.org/plugins/) |
diff --git a/includes/Checker/Checks/Performance/Autoloaded_Options_Check.php b/includes/Checker/Checks/Performance/Autoloaded_Options_Check.php
new file mode 100644
index 000000000..b738abd0b
--- /dev/null
+++ b/includes/Checker/Checks/Performance/Autoloaded_Options_Check.php
@@ -0,0 +1,78 @@
+ 'php',
+ 'standard' => 'PluginCheck',
+ 'sniffs' => 'PluginCheck.CodeAnalysis.AutoLoadedOptions',
+ );
+ }
+
+ /**
+ * Gets the description for the check.
+ *
+ * Every check must have a short description explaining what the check does.
+ *
+ * @since 2.1.0
+ *
+ * @return string Description.
+ */
+ public function get_description(): string {
+ return __( 'Warns when add_option() or update_option() are called without explicitly setting the $autoload parameter.', 'plugin-check' );
+ }
+
+ /**
+ * Gets the documentation URL for the check.
+ *
+ * Every check must have a URL with further information about the check.
+ *
+ * @since 2.1.0
+ *
+ * @return string The documentation URL.
+ */
+ public function get_documentation_url(): string {
+ return __( 'https://developer.wordpress.org/reference/functions/add_option/', 'plugin-check' );
+ }
+}
diff --git a/includes/Checker/Default_Check_Repository.php b/includes/Checker/Default_Check_Repository.php
index d4d5c548d..b86528fe5 100644
--- a/includes/Checker/Default_Check_Repository.php
+++ b/includes/Checker/Default_Check_Repository.php
@@ -104,6 +104,7 @@ private function register_default_checks() {
'direct_file_access' => new Checks\Plugin_Repo\Direct_File_Access_Check(),
'external_admin_menu_links' => new Checks\Plugin_Repo\External_Admin_Menu_Links_Check(),
'wp_functions_compatibility' => new Checks\Plugin_Repo\WP_Functions_Compatibility_Check(),
+ 'autoloaded_options' => new Checks\Performance\Autoloaded_Options_Check(),
'ai_provider' => new Checks\General\AI_Provider_Check(),
)
);
diff --git a/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/AutoLoadedOptionsSniff.php b/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/AutoLoadedOptionsSniff.php
new file mode 100644
index 000000000..e6b47a5a2
--- /dev/null
+++ b/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/AutoLoadedOptionsSniff.php
@@ -0,0 +1,101 @@
+
+ */
+ protected $autoload_positions = array(
+ 'add_option' => 4,
+ 'update_option' => 3,
+ );
+
+ /**
+ * List of functions to examine.
+ *
+ * @since 2.1.0
+ *
+ * @var array
+ */
+ protected $target_functions = array(
+ 'add_option' => true,
+ 'update_option' => true,
+ );
+
+ /**
+ * Processes this test, when one of its tokens is encountered.
+ *
+ * @since 2.1.0
+ *
+ * @param int $stackPtr The position of the current token in the stack.
+ * @return int|void Integer stack pointer to skip forward or void to continue normal file processing.
+ */
+ public function process_token( $stackPtr ) {
+ if ( isset( $this->target_functions[ strtolower( $this->tokens[ $stackPtr ]['content'] ) ] ) ) {
+ $this->exclude = array();
+
+ return parent::process_token( $stackPtr );
+ }
+ }
+
+ /**
+ * Process the parameters of a matched function call.
+ *
+ * @since 2.1.0
+ *
+ * @param int $stackPtr The position of the current token in the stack.
+ * @param string $group_name The name of the group which was matched.
+ * @param string $matched_content The token content (function name) which was matched in lowercase.
+ * @param array $parameters Array with information about the parameters.
+ * @return void
+ */
+ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) {
+ $position = $this->autoload_positions[ $matched_content ];
+ $found = PassedParameters::getParameterFromStack( $parameters, $position, 'autoload' );
+
+ if ( false === $found ) {
+ $error_code = MessageHelper::stringToErrorcode( $matched_content . '_autoload', true );
+
+ $this->phpcsFile->addWarning(
+ 'The $autoload parameter for %s() is not explicitly set; the option will default to autoloading on every page request. Pass an explicit boolean (true or false) to make the performance trade-off intentional.',
+ $stackPtr,
+ $error_code . 'Missing',
+ array( $matched_content )
+ );
+ }
+ }
+}
diff --git a/phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/AutoLoadedOptionsUnitTest.inc b/phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/AutoLoadedOptionsUnitTest.inc
new file mode 100644
index 000000000..8d8ac81b0
--- /dev/null
+++ b/phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/AutoLoadedOptionsUnitTest.inc
@@ -0,0 +1,28 @@
+ =>
+ */
+ public function getErrorList() {
+ return array();
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @return array =>
+ */
+ public function getWarningList() {
+ return array(
+ 4 => 1,
+ 7 => 1,
+ 10 => 1,
+ 13 => 1,
+ 16 => 1,
+ );
+ }
+
+ /**
+ * Returns the fully qualified class name (FQCN) of the sniff.
+ *
+ * @return string The fully qualified class name of the sniff.
+ */
+ protected function get_sniff_fqcn() {
+ return AutoLoadedOptionsSniff::class;
+ }
+
+ /**
+ * Sets the parameters for the sniff.
+ *
+ * @throws \RuntimeException If unable to set the ruleset parameters required for the test.
+ *
+ * @param Sniff $sniff The sniff being tested.
+ */
+ public function set_sniff_parameters( Sniff $sniff ) {
+ }
+}
diff --git a/phpcs-sniffs/PluginCheck/ruleset.xml b/phpcs-sniffs/PluginCheck/ruleset.xml
index 3c5b50d2d..c19f5fa80 100644
--- a/phpcs-sniffs/PluginCheck/ruleset.xml
+++ b/phpcs-sniffs/PluginCheck/ruleset.xml
@@ -4,6 +4,7 @@
Plugin Check Sniffs
+
diff --git a/tests/phpunit/testdata/plugins/test-plugin-autoloaded-options-check-with-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-autoloaded-options-check-with-errors/load.php
new file mode 100644
index 000000000..119270473
--- /dev/null
+++ b/tests/phpunit/testdata/plugins/test-plugin-autoloaded-options-check-with-errors/load.php
@@ -0,0 +1,28 @@
+run( $check_result );
+
+ $warnings = $check_result->get_warnings();
+
+ $this->assertNotEmpty( $warnings );
+ $this->assertArrayHasKey( 'load.php', $warnings );
+
+ // Both add_option calls without $autoload must produce warnings.
+ $this->assertSame(
+ 'PluginCheck.CodeAnalysis.AutoLoadedOptions.add_option_autoloadMissing',
+ $warnings['load.php'][19][1][0]['code']
+ );
+ $this->assertSame(
+ 'PluginCheck.CodeAnalysis.AutoLoadedOptions.add_option_autoloadMissing',
+ $warnings['load.php'][22][1][0]['code']
+ );
+
+ // Both update_option calls without $autoload must produce warnings.
+ $this->assertSame(
+ 'PluginCheck.CodeAnalysis.AutoLoadedOptions.update_option_autoloadMissing',
+ $warnings['load.php'][25][1][0]['code']
+ );
+ $this->assertSame(
+ 'PluginCheck.CodeAnalysis.AutoLoadedOptions.update_option_autoloadMissing',
+ $warnings['load.php'][28][1][0]['code']
+ );
+ }
+
+ public function test_run_without_errors() {
+ $check = new Autoloaded_Options_Check();
+ $check_context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-autoloaded-options-check-without-errors/load.php' );
+ $check_result = new Check_Result( $check_context );
+
+ $check->run( $check_result );
+
+ $this->assertEmpty( $check_result->get_errors() );
+ $this->assertEmpty( $check_result->get_warnings() );
+ }
+}