From a21e071465fb8c61ace30963397e73eb4c634ef8 Mon Sep 17 00:00:00 2001 From: Rodrigo Primo Date: Tue, 24 Mar 2026 14:59:40 +0000 Subject: [PATCH] DB/PreparedSQLPlaceholders: add namespaced tests Tests for namespaced calls to the wpdb class currently result in false positives: the sniff treats namespaced wpdb class calls as calls to the global wpdb class. Tests for namespaced calls to a function called sprintf() currently result in false negatives: the sniff treats these calls as calls to the global sprintf() function, causing them to not be flagged when they should be. Both issues are documented in the test case file and GitHub and will be fixed in the future. --- .../DB/PreparedSQLPlaceholdersUnitTest.inc | 304 +++++++++++++++++- .../DB/PreparedSQLPlaceholdersUnitTest.php | 45 +++ 2 files changed, 343 insertions(+), 6 deletions(-) diff --git a/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.inc b/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.inc index 601877bfa4..d94d6e1bd3 100644 --- a/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.inc +++ b/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.inc @@ -14,7 +14,7 @@ $sql = $wpdb->prepare( 'SELECT * FROM `table` WHERE id = ' . $id ); // OK - this $sql = $wpdb->prepare( "SELECT * FROM `table` WHERE id = $id" ); // OK - this will be handled by the PreparedSQL sniff. $sql = $wpdb->prepare( "SELECT * FROM `table` WHERE id = {$id['some%sing']}" ); // OK - this will be handled by the PreparedSQL sniff. $sql = $wpdb?->prepare( 'SELECT * FROM ' . $wpdb->users ); // Warning. -$sql = $wpdb->prepare( "SELECT * FROM `{$wpdb->users}`" ); // Warning. +$sql = $wpdb->PREPARE( "SELECT * FROM `{$wpdb->users}`" ); // Warning. $sql = $wpdb->prepare( "SELECT * FROM `{$wpdb->users}` WHERE id = $id" ); // OK - this will be handled by the PreparedSQL sniff. /* @@ -80,7 +80,7 @@ $where = $wpdb->prepare( ); // OK. $where = $wpdb->prepare( - sprintf( + \sprintf( "{$wpdb->posts}.post_type IN (%s) AND {$wpdb->posts}.post_status IN (%s)", implode( ',', array_fill( 0, count($post_types), '%s' ), ), @@ -99,7 +99,7 @@ $where = $wpdb->prepare( ); // OK. $query = $wpdb->prepare( - sprintf( + Sprintf( 'SELECT COUNT(ID) FROM `%s` WHERE ID IN (%s) @@ -124,7 +124,7 @@ $results = $wpdb->get_results( ); // OK. $query = $wpdb->prepare( - sprintf( + \SPRINTF( 'SELECT COUNT(ID) FROM `%s` WHERE ID in (%s) @@ -382,7 +382,7 @@ $where = $wpdb->prepare( $where = $wpdb->prepare( sprintf( "{$wpdb->posts}.post_type IN (%s)", - \implode( ',', array_fill( 0, count($post_types), '%s' ) ) + \ImplodE( ',', array_fill( 0, count($post_types), '%s' ) ) ), $post_types ); // OK. @@ -397,7 +397,7 @@ $where = $wpdb->prepare( $where = $wpdb->prepare( "{$wpdb->posts}.post_type IN (" - . implode( ',', \array_fill( 0, count($post_types), '%s' ) ) + . implode( ',', \Array_Fill( 0, count($post_types), '%s' ) ) . ") AND {$wpdb->posts}.post_status IN (" . implode( ',', \array_fill( 0, count($post_statusses), '%s' ) ) . ')', @@ -518,3 +518,295 @@ $where = $wpdb->prepare( */ $callback = $wpdb->prepare(...); // OK. +/* + * Safeguard correct handling of all types of namespaced calls to the wpdb::prepare() method. + * + * Note that calling wpdb::prepare() statically will result in an error. Still, the tests are included here since the + * sniff handles those calls. + * + * Except for the fully qualified global calls, the calls below are currently false positives. The sniff + * incorrectly identifies calls to a non-global class named `wpdb` preceded by a namespace separator as calls to the + * global `$wpdb` object. Related to: https://github.com/WordPress/WordPress-Coding-Standards/issues/2710. + */ +$sql = \wpdb::prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s" ); +$sql = \WPDB::prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s" ); +$sql = MyNamespace\wpdb::prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s" ); +$sql = \MyNamespace\wpdb::prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s" ); +$sql = namespace\wpdb::prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s" ); // The sniff should start flagging this once it can resolve relative namespaces. +$sql = namespace\Sub\wpdb::prepare( "SELECT * FROM $wpdb->users WHERE id = %d AND user_login = %s" ); + +/* + * Safeguard correct handling of namespaced implode() calls when nested as a parameter to sprintf() (except fully + * qualified calls to the global implode() function, which is already handled above). + */ +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + MyNamespace\implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + \MyNamespace\implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( // This should NOT be flagged in the future once the sniff is able to resolve relative namespaces. + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + namespace\implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + namespace\Sub\implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); + +/* + * Safeguard correct handling of namespaced array_fill() calls when nested inside sprintf() (except fully qualified + * calls to the global array_fill() function, which is already handled above). + */ +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', MyNamespace\array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', \MyNamespace\array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( // This should NOT be flagged in the future once the sniff is able to resolve relative namespaces. + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', namespace\array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', namespace\Sub\array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); + +/* + * Safeguard correct handling of namespaced implode() calls in direct string concatenation (when NOT nested inside + * sprintf()). Fully qualified calls to the global implode() are already handled above. + */ +$where = $wpdb->prepare( + "SELECT * FROM {$wpdb->posts} WHERE post_status = %s AND post_type IN (" + . MyNamespace\implode( ',', array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); +$where = $wpdb->prepare( + "SELECT * FROM {$wpdb->posts} WHERE post_status = %s AND post_type IN (" + . \MyNamespace\implode( ',', array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); +$where = $wpdb->prepare( // This should NOT be flagged in the future once the sniff is able to resolve relative namespaces. + "SELECT * FROM {$wpdb->posts} WHERE post_status = %s AND post_type IN (" + . namespace\implode( ',', array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); +$where = $wpdb->prepare( + "SELECT * FROM {$wpdb->posts} WHERE post_status = %s AND post_type IN (" + . namespace\Sub\implode( ',', array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); + +/* + * Safeguard correct handling of namespaced array_fill() calls in direct string concatenation (when NOT nested inside + * sprintf()). Fully qualified calls to the global implode() are already handled above. + */ +$where = $wpdb->prepare( + "SELECT * FROM {$wpdb->posts} WHERE post_status = %s AND post_type IN (" + . implode( ',', MyNamespace\array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); +$where = $wpdb->prepare( + "SELECT * FROM {$wpdb->posts} WHERE post_status = %s AND post_type IN (" + . implode( ',', \MyNamespace\array_fill( 0, count($post_types), '%s' ) ) . ')', + 'publish' +); +$where = $wpdb->prepare( // This should NOT be flagged in the future once the sniff is able to resolve relative namespaces. + "SELECT * FROM {$wpdb->posts} WHERE post_status = %s AND post_type IN (" + . implode( ',', namespace\array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); +$where = $wpdb->prepare( + "SELECT * FROM {$wpdb->posts} WHERE post_status = %s AND post_type IN (" + . implode( ',', namespace\Sub\array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); + +/* + * Safeguard correct handling of all types of namespaced calls to sprintf() except fully qualified calls to the + * global sprintf() function, which is already handled above. + * + * The calls below are currently false negatives. The sniff incorrectly treats namespaced sprintf() calls as + * calls to the global sprintf() function and does not flag them. + * See: https://github.com/WordPress/WordPress-Coding-Standards/issues/2720 + */ +$where = $wpdb->prepare( + MyNamespace\sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + \MyNamespace\sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + namespace\sprintf( // This should NOT be flagged in the future once the sniff is able to resolve relative namespaces. + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + namespace\Sub\sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); + +/* + * Safeguard correct handling of method calls to methods named sprintf(), implode(), or array_fill(). + * These should NOT be analyzed as global function calls. + */ + +// sprintf() method calls. The sprintf() calls below are currently false negatives. The sniff incorrectly treats method +// calls with the same name as global functions as calls to the global functions and does not flag them. +// See: https://github.com/WordPress/WordPress-Coding-Standards/issues/2720 +$where = $wpdb->prepare( + $obj->sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + $obj?->sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + MyClass::sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); + +// implode() method calls nested inside sprintf(). +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + $obj->implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + $obj?->implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + MyClass::implode( ',', array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); + +// implode() method calls in direct string concatenation. +$where = $wpdb->prepare( + "post_status = %s AND post_type IN (" + . $obj->implode( ',', array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); +$where = $wpdb->prepare( + "post_status = %s AND post_type IN (" + . $obj?->implode( ',', array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); +$where = $wpdb->prepare( + "post_status = %s AND post_type IN (" + . MyClass::implode( ',', array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); + +// array_fill() method calls nested inside implode() which is nested inside sprintf(). +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', $obj->array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', $obj?->array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); +$where = $wpdb->prepare( + sprintf( + "{$wpdb->posts}.post_type IN (%s)", + implode( ',', MyClass::array_fill( 0, count($post_types), '%s' ) ) + ), + $post_types +); + +// array_fill() method calls nested inside implode() in direct string concatenation. +$where = $wpdb->prepare( + "post_status = %s AND post_type IN (" + . implode( ',', $obj->array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); +$where = $wpdb->prepare( + "post_status = %s AND post_type IN (" + . implode( ',', $obj?->array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); +$where = $wpdb->prepare( + "post_status = %s AND post_type IN (" + . implode( ',', MyClass::array_fill( 0, count($post_types), '%s' ) ) + . ')', + 'publish' +); diff --git a/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.php b/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.php index df0bc753bf..3499ac370f 100644 --- a/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.php +++ b/WordPress/Tests/DB/PreparedSQLPlaceholdersUnitTest.php @@ -106,6 +106,19 @@ public function getErrorList() { // Named parameter support. 418 => 1, + + // Fully qualified \WPDB::prepare() call. + 531 => 1, + 532 => 1, + + /** + * False positives. See https://github.com/WordPress/WordPress-Coding-Standards/issues/2710 and + * comment in the test case file. + */ + 533 => 1, + 534 => 1, + 535 => 1, + 536 => 1, ); } @@ -161,6 +174,38 @@ public function getWarningList() { 482 => 1, 490 => 1, 498 => 1, + + // Namespaced sprintf/implode/array_fill calls. + 542 => 1, + 549 => 1, + 556 => 1, + 563 => 1, + 575 => 1, + 582 => 1, + 589 => 1, + 596 => 1, + 608 => 1, + 614 => 1, + 620 => 1, + 626 => 1, + 637 => 1, + 643 => 1, + 648 => 1, + 654 => 1, + + // Method sprintf/implode/array_fill calls. + 729 => 1, + 736 => 1, + 743 => 1, + 752 => 1, + 758 => 1, + 764 => 1, + 772 => 1, + 779 => 1, + 786 => 1, + 795 => 1, + 801 => 1, + 807 => 1, ); } }