diff --git a/includes/Checker/Checks/General/Php_Error_Reporting_Check.php b/includes/Checker/Checks/General/Php_Error_Reporting_Check.php
new file mode 100644
index 000000000..1748ee54a
--- /dev/null
+++ b/includes/Checker/Checks/General/Php_Error_Reporting_Check.php
@@ -0,0 +1,267 @@
+plugin()->path();
+
+ foreach ( $php_files as $file ) {
+ // Skip test suite folders or files relative to the plugin's root path.
+ $relative_file = str_replace( $plugin_path, '', $file );
+ if ( preg_match( '#^(?:tests|test|testdata|phpunit)/#i', $relative_file ) || preg_match( '#/phpunit[^/]*$#i', $relative_file ) ) {
+ continue;
+ }
+
+ $this->check_file( $result, $file );
+ }
+ }
+
+ /**
+ * Scans a single PHP file for error reporting violations.
+ *
+ * @since 1.9.0
+ *
+ * @param Check_Result $result The check result to amend.
+ * @param string $file Absolute path to the file.
+ */
+ private function check_file( Check_Result $result, string $file ) {
+ $contents = file_get_contents( $file );
+ if ( false === $contents ) {
+ return;
+ }
+
+ // Try AST-based detection first.
+ $parser = ( new ParserFactory() )->create( ParserFactory::PREFER_PHP7 );
+ try {
+ $ast = $parser->parse( $contents );
+ if ( null !== $ast ) {
+ $this->check_ast( $result, $file, $ast );
+ return;
+ }
+ } catch ( Error $e ) {
+ // Fall through to regex-based detection if parsing fails.
+ }
+
+ $this->check_regex( $result, $file, $contents );
+ }
+
+ /**
+ * Scans the AST of a file for error reporting violations.
+ *
+ * @since 1.9.0
+ *
+ * @param Check_Result $result The check result to amend.
+ * @param string $file Absolute path to the file.
+ * @param array $ast The parsed AST nodes.
+ *
+ * @SuppressWarnings(PHPMD.NPathComplexity)
+ */
+ private function check_ast( Check_Result $result, string $file, array $ast ) {
+ $node_finder = new NodeFinder();
+ $func_calls = $node_finder->findInstanceOf( $ast, Expr\FuncCall::class );
+
+ foreach ( $func_calls as $func_call ) {
+ // @phpstan-ignore-next-line Access to property $name on Expr\FuncCall.
+ if ( ! $func_call->name instanceof Node\Name ) {
+ continue;
+ }
+
+ // @phpstan-ignore-next-line Access to property $name on Expr\FuncCall.
+ $func_name = strtolower( $func_call->name->toString() );
+ $line = method_exists( $func_call, 'getStartLine' ) ? $func_call->getStartLine() : 0;
+
+ // 1. Direct calls to error_reporting().
+ if ( 'error_reporting' === $func_name ) {
+ $this->add_violation( $result, $file, $line );
+ continue;
+ }
+
+ // 2. ini_set() / ini_alter().
+ if ( in_array( $func_name, array( 'ini_set', 'ini_alter' ), true ) ) {
+ if ( ! empty( $func_call->args[0] ) ) {
+ $first_arg = $func_call->args[0]->value;
+ if ( $first_arg instanceof Node\Scalar\String_ ) {
+ $arg_value = strtolower( $first_arg->value );
+ if ( in_array( $arg_value, array( 'error_reporting', 'display_errors' ), true ) ) {
+ $this->add_violation( $result, $file, $line );
+ continue;
+ }
+ }
+ }
+ }
+
+ // 3. define() overrides.
+ if ( 'define' === $func_name ) {
+ if ( ! empty( $func_call->args[0] ) ) {
+ $first_arg = $func_call->args[0]->value;
+ if ( $first_arg instanceof Node\Scalar\String_ ) {
+ $arg_value = $first_arg->value;
+ if ( in_array( $arg_value, array( 'WP_DEBUG', 'WP_DEBUG_LOG', 'WP_DEBUG_DISPLAY', 'SCRIPT_DEBUG' ), true ) ) {
+ $this->add_violation( $result, $file, $line );
+ continue;
+ }
+ }
+ }
+ }
+ }
+
+ // Also check for the const keyword: e.g. const WP_DEBUG = true.
+ $consts = $node_finder->findInstanceOf( $ast, Stmt\Const_::class );
+ foreach ( $consts as $const_stmt ) {
+ // @phpstan-ignore-next-line Access to property $consts on Stmt\Const_.
+ foreach ( $const_stmt->consts as $const ) {
+ $const_name = $const->name->toString();
+ $line = method_exists( $const, 'getStartLine' ) ? $const->getStartLine() : 0;
+ if ( in_array( $const_name, array( 'WP_DEBUG', 'WP_DEBUG_LOG', 'WP_DEBUG_DISPLAY', 'SCRIPT_DEBUG' ), true ) ) {
+ $this->add_violation( $result, $file, $line );
+ }
+ }
+ }
+ }
+
+ /**
+ * Fallback regex-based detection for error reporting violations.
+ *
+ * @since 1.9.0
+ *
+ * @param Check_Result $result The check result to amend.
+ * @param string $file Absolute path to the file.
+ * @param string $contents File contents.
+ */
+ private function check_regex( Check_Result $result, string $file, string $contents ) {
+ // Clean comments before checking regex.
+ $cleaned = preg_replace( '/\/\*.*?\*\//s', '', $contents );
+ $cleaned = preg_replace( '/\/\/.*$/m', '', $cleaned );
+ $cleaned = preg_replace( '/#.*$/m', '', $cleaned );
+
+ $patterns = array(
+ // error_reporting(...).
+ '/\berror_reporting\s*\(/i',
+ // ini_set(...) / ini_alter(...).
+ '/\bini_(?:set|alter)\s*\(\s*[\'"](?:error_reporting|display_errors)[\'"]/i',
+ // define(...).
+ '/\bdefine\s*\(\s*[\'"](?:WP_DEBUG|WP_DEBUG_LOG|WP_DEBUG_DISPLAY|SCRIPT_DEBUG)[\'"]/i',
+ // const WP_DEBUG = ....
+ '/\bconst\s+(?:WP_DEBUG|WP_DEBUG_LOG|WP_DEBUG_DISPLAY|SCRIPT_DEBUG)\b/i',
+ );
+
+ // Scan line by line to locate line numbers.
+ $lines = explode( "\n", $cleaned );
+ foreach ( $lines as $index => $line_content ) {
+ $line_num = $index + 1;
+ foreach ( $patterns as $pattern ) {
+ if ( preg_match( $pattern, $line_content ) ) {
+ $this->add_violation( $result, $file, $line_num );
+ break; // Only flag once per line.
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds a standard warning message for a violation.
+ *
+ * @since 1.9.0
+ *
+ * @param Check_Result $result The check result to amend.
+ * @param string $file Absolute path to the file.
+ * @param int $line The line number on which the warning occurred.
+ */
+ private function add_violation( Check_Result $result, string $file, int $line ) {
+ $message = sprintf(
+ '%1$s
%2$s
%3$s
%4$s',
+ __( 'Do not change PHP error reporting in production code', 'plugin-check' ),
+ __( 'A plugin should not modify PHP\'s error-reporting configuration. Calls such as error_reporting(), ini_set(\'display_errors\', …), or redefining WP_DEBUG, WP_DEBUG_LOG, WP_DEBUG_DISPLAY or SCRIPT_DEBUG change behaviour for every other plugin and theme on the site.', 'plugin-check' ),
+ __( 'This can leak sensitive information (paths, secrets, stack traces) and breaks the standard debugging workflow for site owners and other developers. The host\'s php.ini and the site\'s wp-config.php are the correct places to control this.', 'plugin-check' ),
+ __( 'Please remove these calls, or move them behind a strictly developer-only flag that is never set in shipped code.', 'plugin-check' )
+ );
+
+ $this->add_result_warning_for_file(
+ $result,
+ $message,
+ 'php_error_reporting_detected',
+ $file,
+ $line,
+ 0,
+ 'https://www.php.net/manual/en/function.error-reporting.php',
+ 8
+ );
+ }
+
+ /**
+ * Gets the description for the check.
+ *
+ * Every check must have a short description explaining what the check does.
+ *
+ * @since 1.9.0
+ *
+ * @return string Description.
+ */
+ public function get_description(): string {
+ return __( 'Detects runtime changes to PHP error reporting configuration or WordPress debug constants.', 'plugin-check' );
+ }
+
+ /**
+ * Gets the documentation URL for the check.
+ *
+ * Every check must have a URL with further information about the check.
+ *
+ * @since 1.9.0
+ *
+ * @return string The documentation URL.
+ */
+ public function get_documentation_url(): string {
+ return 'https://www.php.net/manual/en/function.error-reporting.php';
+ }
+}
diff --git a/includes/Checker/Default_Check_Repository.php b/includes/Checker/Default_Check_Repository.php
index c22371044..f6ada18c0 100644
--- a/includes/Checker/Default_Check_Repository.php
+++ b/includes/Checker/Default_Check_Repository.php
@@ -72,6 +72,7 @@ private function register_default_checks() {
'wp_plugin_check_checks',
array(
'i18n_usage' => new Checks\General\I18n_Usage_Check(),
+ 'php_error_reporting' => new Checks\General\Php_Error_Reporting_Check(),
'enqueued_scripts_size' => new Checks\Performance\Enqueued_Scripts_Size_Check(),
'enqueued_styles_size' => new Checks\Performance\Enqueued_Styles_Size_Check(),
'code_obfuscation' => new Checks\Plugin_Repo\Code_Obfuscation_Check(),
diff --git a/tests/phpunit/testdata/plugins/test-plugin-php-error-reporting-with-errors/load.php b/tests/phpunit/testdata/plugins/test-plugin-php-error-reporting-with-errors/load.php
new file mode 100644
index 000000000..c776b4c1e
--- /dev/null
+++ b/tests/phpunit/testdata/plugins/test-plugin-php-error-reporting-with-errors/load.php
@@ -0,0 +1,26 @@
+run( $check_result );
+
+ $warnings = $check_result->get_warnings();
+
+ $this->assertNotEmpty( $warnings );
+ $this->assertArrayHasKey( 'load.php', $warnings );
+
+ $this->assertEquals( 8, $check_result->get_warning_count() );
+
+ $first_line_warnings = reset( $warnings['load.php'] );
+ $first_column_warnings = reset( $first_line_warnings );
+ $warning_data = reset( $first_column_warnings );
+
+ $this->assertEquals( 'php_error_reporting_detected', $warning_data['code'] );
+ }
+
+ public function test_run_without_errors() {
+ $check = new Php_Error_Reporting_Check();
+ $context = new Check_Context( UNIT_TESTS_PLUGIN_DIR . 'test-plugin-php-error-reporting-without-errors/load.php' );
+ $check_result = new Check_Result( $context );
+
+ $check->run( $check_result );
+
+ $this->assertEquals( 0, $check_result->get_warning_count() );
+ $this->assertEquals( 0, $check_result->get_error_count() );
+ }
+}