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..c60f7d37a
--- /dev/null
+++ b/includes/Checker/Checks/General/Php_Error_Reporting_Check.php
@@ -0,0 +1,143 @@
+ 'php',
+ 'standard' => 'PluginCheck',
+ 'sniffs' => 'PluginCheck.CodeAnalysis.PhpErrorReporting',
+ );
+ }
+
+ /**
+ * 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';
+ }
+
+ /**
+ * Amends the given result with a message for the specified file.
+ *
+ * Translates each PhpErrorReportingSniff error code into a single unified
+ * warning code (`php_error_reporting_detected`) so the check exposes one
+ * stable, user-facing message regardless of which pattern was detected.
+ *
+ * @since 1.9.0
+ *
+ * @param Check_Result $result The check result to amend, including the plugin context to check.
+ * @param bool $error Whether it is an error or notice.
+ * @param string $message Error message.
+ * @param string $code Error code.
+ * @param string $file Absolute path to the file where the issue was found.
+ * @param int $line The line on which the message occurred. Default is 0 (unknown line).
+ * @param int $column The column on which the message occurred. Default is 0 (unknown column).
+ * @param string $docs URL for further information about the message.
+ * @param int $severity Severity level. Default is 5.
+ */
+ protected function add_result_message_for_file( Check_Result $result, $error, $message, $code, $file, $line = 0, $column = 0, string $docs = '', $severity = 5 ) {
+ if ( 0 === strpos( $code, 'PluginCheck.CodeAnalysis.PhpErrorReporting.' ) ) {
+ $warning_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' )
+ );
+
+ $docs = $this->get_documentation_url();
+ $code = 'php_error_reporting_detected';
+ $severity = 8;
+
+ parent::add_result_message_for_file(
+ $result,
+ false,
+ $warning_message,
+ $code,
+ $file,
+ $line,
+ $column,
+ $docs,
+ $severity
+ );
+ return;
+ }
+
+ parent::add_result_message_for_file(
+ $result,
+ $error,
+ $message,
+ $code,
+ $file,
+ $line,
+ $column,
+ $docs,
+ $severity
+ );
+ }
+}
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/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/PhpErrorReportingSniff.php b/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/PhpErrorReportingSniff.php
new file mode 100644
index 000000000..176426f80
--- /dev/null
+++ b/phpcs-sniffs/PluginCheck/Sniffs/CodeAnalysis/PhpErrorReportingSniff.php
@@ -0,0 +1,235 @@
+
+ */
+ public function register() {
+ return array(
+ T_STRING,
+ T_CONST,
+ );
+ }
+
+ /**
+ * Processes a matched token.
+ *
+ * @since 1.9.0
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token.
+ *
+ * @return void
+ */
+ public function process( File $phpcsFile, $stackPtr ) {
+ $tokens = $phpcsFile->getTokens();
+
+ if ( T_CONST === $tokens[ $stackPtr ]['code'] ) {
+ $this->check_const_declaration( $phpcsFile, $stackPtr, $tokens );
+ return;
+ }
+
+ $content = strtolower( $tokens[ $stackPtr ]['content'] );
+ if ( ! in_array( $content, self::TARGET_FUNCTIONS, true ) ) {
+ return;
+ }
+
+ // Must be followed by an opening parenthesis (function call), not a class/namespace reference.
+ $next_non_empty = $phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true );
+ if ( false === $next_non_empty || T_OPEN_PARENTHESIS !== $tokens[ $next_non_empty ]['code'] ) {
+ return;
+ }
+
+ $this->check_function_call( $phpcsFile, $stackPtr, $tokens, $content );
+ }
+
+ /**
+ * Inspects a function call and reports a violation if the pattern matches.
+ *
+ * @since 1.9.0
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $stackPtr Position of the function name token.
+ * @param array $tokens Token stack.
+ * @param string $func_name Lowercase function name.
+ *
+ * @return void
+ */
+ private function check_function_call( File $phpcsFile, $stackPtr, $tokens, $func_name ) {
+ $opener = $phpcsFile->findNext( T_OPEN_PARENTHESIS, $stackPtr + 1 );
+ if ( false === $opener || ! isset( $tokens[ $opener ]['parenthesis_closer'] ) ) {
+ return;
+ }
+
+ // Direct call to error_reporting() — no argument check needed.
+ if ( 'error_reporting' === $func_name ) {
+ $this->report( $phpcsFile, $stackPtr, 'DirectErrorReportingCall' );
+ return;
+ }
+
+ // Resolve the actual first parameter (token-based find is wrong for calls like
+ // `ini_set( some_function(), 'error_reporting' )`).
+ $params = PassedParameters::getParameters( $phpcsFile, $stackPtr );
+ if ( empty( $params ) || ! isset( $params[1] ) ) {
+ return;
+ }
+
+ $first_string = $phpcsFile->findNext(
+ T_CONSTANT_ENCAPSED_STRING,
+ $params[1]['start'],
+ $params[1]['end'] + 1
+ );
+ if ( false === $first_string ) {
+ return;
+ }
+
+ $argument = trim( $tokens[ $first_string ]['content'], "\"' \t" );
+
+ if ( 'ini_set' === $func_name || 'ini_alter' === $func_name ) {
+ $normalized = strtolower( $argument );
+ if ( in_array( $normalized, self::ERROR_INI_DIRECTIVES, true ) ) {
+ $this->report(
+ $phpcsFile,
+ $stackPtr,
+ MessageHelper::stringToErrorcode( 'IniDirective' . ucfirst( $normalized ) )
+ );
+ }
+ return;
+ }
+
+ if ( 'define' === $func_name && in_array( $argument, self::DEBUG_CONSTANTS, true ) ) {
+ $this->report(
+ $phpcsFile,
+ $stackPtr,
+ MessageHelper::stringToErrorcode( 'Define' . $argument )
+ );
+ }
+ }
+
+ /**
+ * Inspects a `const` declaration block for debug constants.
+ *
+ * Handles both single (`const WP_DEBUG = true;`) and comma-separated
+ * (`const A = 1, WP_DEBUG = true;`) declarations.
+ *
+ * @since 1.9.0
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $stackPtr Position of the T_CONST token.
+ * @param array $tokens Token stack.
+ *
+ * @return void
+ */
+ private function check_const_declaration( File $phpcsFile, $stackPtr, $tokens ) {
+ $end = $phpcsFile->findNext( array( T_SEMICOLON, T_OPEN_CURLY_BRACKET ), $stackPtr + 1 );
+ if ( false === $end ) {
+ return;
+ }
+
+ for ( $i = $stackPtr + 1; $i < $end; $i++ ) {
+ if ( T_STRING === $tokens[ $i ]['code'] && in_array( $tokens[ $i ]['content'], self::DEBUG_CONSTANTS, true ) ) {
+ $this->report(
+ $phpcsFile,
+ $i,
+ MessageHelper::stringToErrorcode( 'Const' . $tokens[ $i ]['content'] )
+ );
+ }
+ }
+ }
+
+ /**
+ * Emits a single, stable error for any detected pattern.
+ *
+ * The check layer translates this to its own user-facing message and severity.
+ *
+ * @since 1.9.0
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $stackPtr Position of the matched token.
+ * @param string $code Error code suffix (sniff-specific).
+ *
+ * @return void
+ */
+ private function report( File $phpcsFile, $stackPtr, $code ) {
+ $tokens = $phpcsFile->getTokens();
+ $is_const = ( T_CONST === $tokens[ $stackPtr ]['code'] );
+ $message = $is_const
+ ? 'Detected production-time debug constant definition: %s.'
+ : 'Detected production-time change to PHP error reporting: %s().';
+
+ MessageHelper::addMessage(
+ $phpcsFile,
+ $message,
+ $stackPtr,
+ true,
+ $code,
+ array( $tokens[ $stackPtr ]['content'] )
+ );
+ }
+}
diff --git a/phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/PhpErrorReportingUnitTest.inc b/phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/PhpErrorReportingUnitTest.inc
new file mode 100644
index 000000000..8fc5b0c99
--- /dev/null
+++ b/phpcs-sniffs/PluginCheck/Tests/CodeAnalysis/PhpErrorReportingUnitTest.inc
@@ -0,0 +1,55 @@
+ =>
+ */
+ public function getErrorList() {
+ return array(
+ 10 => 1,
+ 13 => 1,
+ 16 => 1,
+ 19 => 1,
+ 22 => 1,
+ 25 => 1,
+ 28 => 1,
+ 31 => 1,
+ 34 => 1,
+ 37 => 1,
+ 40 => 1,
+ 43 => 1,
+ );
+ }
+
+ /**
+ * Returns the lines where warnings should occur.
+ *
+ * @return array =>
+ */
+ public function getWarningList() {
+ return array();
+ }
+
+ /**
+ * Returns the fully qualified class name (FQCN) of the sniff.
+ *
+ * @return string
+ */
+ protected function get_sniff_fqcn() {
+ return PhpErrorReportingSniff::class;
+ }
+
+ /**
+ * Sets the parameters for the sniff.
+ *
+ * @param Sniff $sniff The sniff being tested.
+ *
+ * @return void
+ */
+ public function set_sniff_parameters( Sniff $sniff ) {
+ }
+}
diff --git a/phpcs-sniffs/PluginCheck/ruleset.xml b/phpcs-sniffs/PluginCheck/ruleset.xml
index 18666cc40..d50f8c898 100644
--- a/phpcs-sniffs/PluginCheck/ruleset.xml
+++ b/phpcs-sniffs/PluginCheck/ruleset.xml
@@ -8,6 +8,7 @@
+
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..99851aa7c
--- /dev/null
+++ b/tests/phpunit/testdata/plugins/test-plugin-php-error-reporting-with-errors/load.php
@@ -0,0 +1,32 @@
+run( $check_result );
+
+ $warnings = $check_result->get_warnings();
+
+ $this->assertNotEmpty( $warnings );
+ $this->assertArrayHasKey( 'load.php', $warnings );
+
+ // Assert exact per-line coverage so the test fails if any specific pattern stops being detected.
+ $expected_lines = array( 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32 );
+ $this->assertCount( 11, $warnings['load.php'], 'Expected exactly 11 distinct lines to be flagged.' );
+
+ foreach ( $expected_lines as $line ) {
+ $line_warnings = $warnings['load.php'][ $line ] ?? array();
+ $this->assertNotEmpty( $line_warnings, "Expected a warning on line {$line}, but none was found." );
+
+ $first_column_warnings = reset( $line_warnings );
+ $warning_data = reset( $first_column_warnings );
+
+ $this->assertEquals( 'php_error_reporting_detected', $warning_data['code'], "Line {$line} has the wrong warning 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() );
+ }
+}