diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist
index cf03cc18..bc350b79 100644
--- a/.phpcs.xml.dist
+++ b/.phpcs.xml.dist
@@ -68,6 +68,7 @@
+
diff --git a/WordPress/Sniffs/Security/EscapeOutputSniff.php b/WordPress/Sniffs/Security/EscapeOutputSniff.php
index 5d5a8272..7d2d4727 100644
--- a/WordPress/Sniffs/Security/EscapeOutputSniff.php
+++ b/WordPress/Sniffs/Security/EscapeOutputSniff.php
@@ -216,22 +216,35 @@ class EscapeOutputSniff extends AbstractFunctionRestrictionsSniff {
case \T_THROW:
// Find the open parentheses, while stepping over the exception creation tokens.
- $ignore = Tokens::$emptyTokens;
- $ignore += Collections::namespacedNameTokens();
- $ignore += Collections::functionCallTokens();
- $ignore += Collections::objectOperators();
+ $ignore = Tokens::$emptyTokens;
+ $ignore += Collections::namespacedNameTokens();
+ $ignore += Collections::functionCallTokens();
+ $ignore += Collections::objectOperators();
+ $ignore[ \T_READONLY ] = \T_READONLY;
- $next_relevant = $this->phpcsFile->findNext( $ignore, ( $stackPtr + 1 ), null, true );
- if ( false === $next_relevant ) {
- return;
- }
-
- if ( \T_NEW === $this->tokens[ $next_relevant ]['code'] ) {
+ $next_relevant = $stackPtr;
+ do {
$next_relevant = $this->phpcsFile->findNext( $ignore, ( $next_relevant + 1 ), null, true );
if ( false === $next_relevant ) {
return;
}
- }
+
+ if ( \T_NEW === $this->tokens[ $next_relevant ]['code'] ) {
+ continue;
+ }
+
+ // Skip over attribute declarations when searching for the open parenthesis.
+ if ( \T_ATTRIBUTE === $this->tokens[ $next_relevant ]['code'] ) {
+ if ( isset( $this->tokens[ $next_relevant ]['attribute_closer'] ) === false ) {
+ return;
+ }
+
+ $next_relevant = $this->tokens[ $next_relevant ]['attribute_closer'];
+ continue;
+ }
+
+ break;
+ } while ( $next_relevant < ( $this->phpcsFile->numTokens - 1 ) );
if ( \T_OPEN_PARENTHESIS !== $this->tokens[ $next_relevant ]['code']
|| isset( $this->tokens[ $next_relevant ]['parenthesis_closer'] ) === false
diff --git a/WordPress/Tests/Security/EscapeOutputUnitTest.1.inc b/WordPress/Tests/Security/EscapeOutputUnitTest.1.inc
index eaf96f5b..e1766c7d 100644
--- a/WordPress/Tests/Security/EscapeOutputUnitTest.1.inc
+++ b/WordPress/Tests/Security/EscapeOutputUnitTest.1.inc
@@ -662,3 +662,17 @@ die( status: esc_html( $foo ) ); // Ok.
exit( status: $foo ); // Bad.
die( status: $foo ); // Bad.
+
+/*
+ * Issue https://github.com/WordPress/WordPress-Coding-Standards/issues/2552
+ * Ensure that readonly anonymous classes and anonymous classes with attributes are handled
+ * correctly when part of a throw statement.
+ */
+throw new #[MyAttribute] readonly class( esc_html( $message ) ) extends Exception {}; // Good.
+throw new readonly class( $unescaped ) {}; // Bad.
+throw new #[MyAttribute] class( $unescaped ) extends Exception {}; // Bad.
+throw new
+#[Attribute1]
+/* some comment */
+#[Attribute2('text', 10)]
+readonly class( $unescaped ) {}; // Bad.
diff --git a/WordPress/Tests/Security/EscapeOutputUnitTest.21.inc b/WordPress/Tests/Security/EscapeOutputUnitTest.21.inc
new file mode 100644
index 00000000..249b6091
--- /dev/null
+++ b/WordPress/Tests/Security/EscapeOutputUnitTest.21.inc
@@ -0,0 +1,8 @@
+ 1,
663 => 1,
664 => 1,
+ 672 => 1,
+ 673 => 1,
+ 678 => 1,
);
case 'EscapeOutputUnitTest.6.inc':