text to translate', 'my-slug' );
+__( 'text to
translate', 'my-slug' );
+__( 'text < to translate', 'my-slug' );
+__( 'text to > translate', 'my-slug' );
+__( '
my address
', 'my-slug' );
+__( '
text to translate
', 'my-slug' );
+__( '<>text to translate>', 'my-slug' );
+__( '<>text to translate<>', 'my-slug' );
+__( '
', 'my-slug' );
+__( '<<>>text to translate<<>', 'my-slug' );
+__( '<<>>123<>', 'my-slug' );
+__( '
Foo', 'my-slug' );
+__( '
Foo', 'my-slug' );
+__( '
Foo', 'my-slug' );
+
+// Test handling of more complex embedded variables and expressions. All of these should throw an error.
+__( "${foo}", 'my-slug' );
+__( "{$foo['bar']}", 'my-slug' );
+__( "{$foo->bar()}", 'my-slug' );
+__( "{$foo['bar']->baz()()}", 'my-slug' );
+__( "${foo->bar}", 'my-slug' );
+__( "${foo["${bar['baz']}"]}", 'my-slug' );
+
+// Safeguard defensive coding.
+__( /* comment */, 'my-slug' ); // Bad, missing first param (parse error).
+
+// phpcs:set WordPress.WP.I18n text_domain[] default
+
+// Safeguard that a comment after a "default" text domain is not removed.
+__( 'String default text domain.' /*comment*/ ); // Warning, text domain can be omitted.
+
+// Test mismatched placeholders.
+_n_noop( 'I have %1$d cat and %2$d dog.' ); // Bad, missing $plural argument, ignore for single/plural placeholder check.
+_n_noop( 'I have %1$d cat and %2$d dog.', $plural ); // Bad, non singular string $plural argument, ignore for single/plural placeholder check.
+_n_noop( 'I have %1$d cat and %2$d dog.', $plural . $text ); // Bad, non singular string $plural argument, ignore for single/plural placeholder check.
+
+_n_noop( 'I have %1$d cat and %2$d dog.', 'I have %1$d cats and %2$d dog.' ); // Ok.
+_n_noop( 'I have %2$d cat and %1$d dog.', 'I have %1$d cats and %2$d dog.' ); // Ok, or at least not something we flag.
+_n_noop( 'I have %1$d cat and %3$d dog.', 'I have %1$d cats and %2$d dog.' ); // Bad, different placeholders used in single vs plural text.
+_n_noop( 'I have %1$d cat, %2$d dog and %3$d canaries.', 'I have %1$d cats, %2$d dog and canaries.' ); // Bad, mismatched placeholders count.
+_n_noop( 'I have %1$d cat, %2$d dog and %3$d canaries.', 'I have %1$d cats, %2$d dog and canaries.' ); // Bad, mismatched numbered placeholders count.
+_n_noop( 'I have %1$d cat, %2$d dog and %2$d canaries.', 'I have %1$d cats, %2$d dog and canaries.' ); // Bad, mismatched numbered placeholders count.
+
+/*
+ * Safeguard support for PHP 8.0+ named parameters.
+ * Each of these function calls uses named parameters with an unconventional param order to test this properly.
+ */
+// phpcs:set WordPress.WP.I18n text_domain[] my-slug
+translate( domain: 'my-slug', text: 'translate me', extra: 'default' ); // Bad: too many arguments + use of translate().
+__( domain: 'my-slug' ); // Bad: missing $text argument.
+__( single: 'translate me', domain: 'my-slug' ); // Bad: missing $text argument (invalid 'single' named arg).
+esc_attr__(
+ domain: 'my-slug',
+ text: 'translate
+me multi line', // OK, multi-line text string.
+);
+esc_html__(
+ domain: PLUGIN_TEXTDOMAIN, // Bad: not single text string literal.
+ text: 'translate ' . 'me', // Bad: not single text string literal.
+);
+_e(
+ domain: 'my-slug',
+ text: $translateMe, // Bad: not single text string literal.
+);
+esc_attr_e(
+ domain: 'my-slug',
+ text: "translate
+$me multi line", // Bad: interpolated variable found.
+);
+esc_html_e(
+ domain: 'different-slug', // Bad: wrong text domain.
+ text: 'translate me',
+);
+esc_html_x(
+ context: 'context',
+ domain: 'my-slug',
+ text: 'trans %s late %1$s me', // Bad: mixing ordered and non-ordered placeholders.
+);
+_x( context: 'context', text: 'translate %1$s me %2$s', domain: 'my-slug' ); // Bad: multiple placeholders, but no ordering.
+_ex( domain: 'my-slug', text: '%s', context: 'context', ); // Bad: no translatable content.
+esc_attr_x( domain: 'my-slug', context: 'context', text: 'translate me
', ); // Bad: wrapped in HTML.
+_n( number: $i, domain: 'my-slug', single: 'translate me', plural: 'translate %s me' ); // Bad: missing singular placeholder.
+_nx( number: $i, domain: 'my-slug', plural: 'translate %s me', single: 'translate %1$s me', context: 'context', ); // Bad: mismatch between single/plural placeholders.
+
+// Safeguard that the SuperfluousDefaultTextDomain fixer handles trailing commas correctly.
+// Note: if the original code contained a trailing comma, it is fine for the fixed code to also contain a trailing comma.
+// phpcs:set WordPress.WP.I18n text_domain[] default
+__( 'translate me', ); // Bad: superfluous domain (positional with trailing comma).
+__( 'translate me' /*comment*/, ); // Bad: superfluous domain (positional with trailing comma and comment).
+
+// Safeguard handling of named params by the SuperfluousDefaultTextDomain fixer.
+__( text: 'translate me' ); // Bad: superfluous domain.
+__( text: 'translate me', ); // Bad: superfluous domain (with trailing comma).
+_n_noop( singular: 'translate me', plural: 'translate me', ); // Bad: superfluous domain.
+_nx_noop( singular: 'translate me' /*comment*/, plural: 'translate me', context: 'context', ); // Bad: superfluous domain.
+
+// These should not be auto-fixable as we don't want to remove the comment.
+__( text: 'translate me', domain: /*comment*/ 'default' ); // Bad: superfluous domain.
+_n_noop( domain: 'default' /*comment*/, singular: 'translate me', plural: 'translate me', ); // Bad: superfluous domain.
+_nx_noop( singular: 'translate me', domain /*comment*/: 'default', plural: 'translate me', context: 'context', ); // Bad: superfluous domain.
+
+// Safeguard bug fix: replacing placeholders in multi-line text strings.
+_nx( 'I have
+%1$d cat and %2$d dog.', 'I have
+%1$d cats and %2$d dogs.', $number, 'Not
+really.' ); // Bad, multiple arguments should be numbered.
+
+// Show an error when the text domain is an empty string.
+esc_html_e( 'foo', '' ); // Bad: text domain mismatch.
+
+// Issue #1948 - Show an error when the text domain is an empty string and no text domain has been passed.
+// phpcs:set WordPress.WP.I18n text_domain[]
+esc_html_e( 'foo', '' ); // Bad: text-domain can not be empty.
+
+// PHP 8.0+: safeguard handling of newly introduced placeholders.
+__( 'There are %1$h monkeys in the %H', 'my-slug' ); // Bad: multiple arguments should be numbered.
+
+// phpcs:enable WordPress.WP.I18n.MissingTranslatorsComment
diff --git a/WordPress/Tests/WP/I18nUnitTest.2.inc b/WordPress/Tests/WP/I18nUnitTest.2.inc
index 4ad5e2895b..dd5c246215 100644
--- a/WordPress/Tests/WP/I18nUnitTest.2.inc
+++ b/WordPress/Tests/WP/I18nUnitTest.2.inc
@@ -2,7 +2,7 @@
/*
* Test sniffing for translator comments.
*/
-// @codingStandardsChangeSetting WordPress.WP.I18n text_domain my-slug
+// phpcs:set WordPress.WP.I18n text_domain[] my-slug
/* Basic test ****************************************************************/
__( 'No placeholders here.', 'my-slug' ); // Ok, no placeholders, so no translators comment needed.
@@ -107,4 +107,22 @@ _e(); // Bad.
// phpcs:ignore Standard.Category.Sniff -- testing that the PHPCS annotations are handled correctly.
printf( __( 'There are %1$d monkeys in the %2$s', 'my-slug' ), intval( $number ), esc_html( $string ) ); // Bad.
-// @codingStandardsChangeSetting WordPress.WP.I18n text_domain false
+__( $notStringLiteral, 'my-slug' ); // Ignore for translators comment, $text not single string literal.
+__( 'text %s' . 'more text', 'my-slug' ); // Ignore for translators comment, $text not single string literal.
+
+
+/*
+ * Safeguard support for PHP 8.0+ named parameters.
+ */
+/* translators: %d: number of cats. */
+_n_noop( domain: 'my-slug', singular: 'I have %d cat.', plural: "I have %d cats.", ); // OK.
+
+esc_attr_e( domain: 'my-slug', translate: 'Text to translate to %1$d languages.' ); // Bad, missing $text param, missing translators comment is ignored.
+
+_n_noop( // Bad, missing translators comment.
+ domain: 'my-slug',
+ plural: "I have %d cats.",
+ singular: 'I have %d cat.',
+);
+
+// phpcs:set WordPress.WP.I18n text_domain[]
diff --git a/WordPress/Tests/WP/I18nUnitTest.3.inc b/WordPress/Tests/WP/I18nUnitTest.3.inc
index bac1dadca7..afcc9fe5fa 100644
--- a/WordPress/Tests/WP/I18nUnitTest.3.inc
+++ b/WordPress/Tests/WP/I18nUnitTest.3.inc
@@ -19,3 +19,9 @@ __( 'string', "something-$domain" ); // Bad, shouldn't use variable for domain.
__( 'string', 'something-else' ); // Bad, text domain mismatch.
__( 'string', "something-else" ); // Bad, text domain mismatch.
+
+/*
+ * Safeguard support for PHP 8.0+ named parameters.
+ */
+__( domain: 'something', text: 'string', ); // OK.
+__( domain: 'something-else', text: 'string', ); // Bad, text domain mismatch.
diff --git a/WordPress/Tests/WP/I18nUnitTest.php b/WordPress/Tests/WP/I18nUnitTest.php
index 6dc1c4a098..3834eb189f 100644
--- a/WordPress/Tests/WP/I18nUnitTest.php
+++ b/WordPress/Tests/WP/I18nUnitTest.php
@@ -3,48 +3,27 @@
* Unit test class for WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
- * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
+ * @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
-namespace WordPress\Tests\WP;
+namespace WordPressCS\WordPress\Tests\WP;
use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;
-use WordPress\PHPCSHelper;
/**
* Unit test class for the I18n sniff.
*
- * @package WPCS\WordPressCodingStandards
+ * @since 0.10.0
+ * @since 0.13.0 Class name changed: this class is now namespaced.
*
- * @since 0.10.0
- * @since 0.13.0 Class name changed: this class is now namespaced.
+ * @covers \WordPressCS\WordPress\Sniffs\WP\I18nSniff
*/
-class I18nUnitTest extends AbstractSniffUnitTest {
-
- /**
- * Get a list of CLI values to set before the file is tested.
- *
- * Used by PHPCS 2.x.
- *
- * @param string $testFile The name of the file being tested.
- *
- * @return array
- */
- public function getCliValues( $testFile ) {
- // Test overruling the text domain from the command line for one test file.
- if ( 'I18nUnitTest.3.inc' === $testFile ) {
- PHPCSHelper::set_config_data( 'text_domain', 'something', true );
- }
-
- return array();
- }
+final class I18nUnitTest extends AbstractSniffUnitTest {
/**
* Set CLI values before the file is tested.
*
- * Used by PHPCS 3.x.
- *
* @param string $testFile The name of the file being tested.
* @param \PHP_CodeSniffer\Config $config The config data for the test run.
*
@@ -54,6 +33,9 @@ public function setCliValues( $testFile, $config ) {
// Test overruling the text domain from the command line for one test file.
if ( 'I18nUnitTest.3.inc' === $testFile ) {
$config->setConfigData( 'text_domain', 'something', true );
+ } else {
+ // Delete the text domain option so it doesn't persist for subsequent test files.
+ $config->setConfigData( 'text_domain', null, true );
}
}
@@ -61,7 +43,8 @@ public function setCliValues( $testFile, $config ) {
* Returns the lines where errors should occur.
*
* @param string $testFile The name of the file being tested.
- * @return array =>
+ *
+ * @return array Key is the line number, value is the number of expected errors.
*/
public function getErrorList( $testFile = '' ) {
@@ -135,11 +118,44 @@ public function getErrorList( $testFile = '' ) {
153 => 1,
157 => 1,
178 => 1,
+ 181 => 3,
+ 184 => 1,
+ 219 => 1,
+ 220 => 1,
+ 221 => 1,
+ 222 => 1,
+ 223 => 1,
+ 224 => 1,
+ 227 => 1,
+ 235 => 1,
+ 236 => 1,
+ 237 => 1,
+ 242 => 2,
+ 251 => 1,
+ 252 => 1,
+ 253 => 1,
+ 260 => 1,
+ 261 => 1,
+ 265 => 1,
+ 269 => 1,
+ 273 => 1,
+ 279 => 1,
+ 281 => 1,
+ 282 => 1,
+ 284 => 1,
+ 305 => 1,
+ 306 => 1,
+ 311 => 1,
+ 315 => 1,
+ 318 => 1,
);
case 'I18nUnitTest.2.inc':
return array(
104 => 2,
+ 110 => 1,
+ 111 => 1,
+ 120 => 1,
);
case 'I18nUnitTest.3.inc':
@@ -154,6 +170,7 @@ public function getErrorList( $testFile = '' ) {
18 => 1,
20 => 1,
21 => 1,
+ 27 => 1,
);
default:
@@ -165,7 +182,8 @@ public function getErrorList( $testFile = '' ) {
* Returns the lines where warnings should occur.
*
* @param string $testFile The name of the file being tested.
- * @return array =>
+ *
+ * @return array Key is the line number, value is the number of expected warnings.
*/
public function getWarningList( $testFile = '' ) {
switch ( $testFile ) {
@@ -173,13 +191,32 @@ public function getWarningList( $testFile = '' ) {
return array(
69 => 1,
70 => 1,
- 100 => 1,
- 101 => 1,
- 102 => 1,
- 103 => 1,
154 => 1,
158 => 1,
159 => 1,
+ 187 => 1,
+ 191 => 1,
+ 193 => 1,
+ 194 => 1,
+ 198 => 1,
+ 199 => 1,
+ 232 => 1,
+ 241 => 1,
+ 242 => 1,
+ 243 => 1,
+ 244 => 1,
+ 251 => 1,
+ 283 => 1,
+ 285 => 1,
+ 290 => 1,
+ 291 => 1,
+ 294 => 1,
+ 295 => 1,
+ 296 => 1,
+ 297 => 1,
+ 300 => 1,
+ 301 => 1,
+ 302 => 1,
);
case 'I18nUnitTest.2.inc':
@@ -191,11 +228,11 @@ public function getWarningList( $testFile = '' ) {
74 => 1,
85 => 1,
108 => 1,
+ 122 => 1,
);
default:
return array();
}
}
-
}
diff --git a/WordPress/Tests/WP/PostsPerPageUnitTest.inc b/WordPress/Tests/WP/PostsPerPageUnitTest.inc
index 61dbdf43d6..7474ba6c3c 100644
--- a/WordPress/Tests/WP/PostsPerPageUnitTest.inc
+++ b/WordPress/Tests/WP/PostsPerPageUnitTest.inc
@@ -17,14 +17,110 @@ $query_args['numberposts'] = '-1'; // OK.
$query_args['my_posts_per_page'] = -1; // OK.
-// @codingStandardsChangeSetting WordPress.WP.PostsPerPage posts_per_page 50
- $query_args['posts_per_page'] = 50; // OK.
- $query_args['posts_per_page'] = 100; // Bad.
- $query_args['posts_per_page'] = 200; // Bad.
- $query_args['posts_per_page'] = 300; // Bad.
-// @codingStandardsChangeSetting WordPress.WP.PostsPerPage posts_per_page 200
- $query_args['posts_per_page'] = 50; // OK.
- $query_args['posts_per_page'] = 100; // OK.
- $query_args['posts_per_page'] = 200; // OK.
- $query_args['posts_per_page'] = 300; // Bad.
-// @codingStandardsChangeSetting WordPress.WP.PostsPerPage posts_per_page 100
+// phpcs:set WordPress.WP.PostsPerPage posts_per_page 50
+$query_args['posts_per_page'] = 50; // OK.
+$query_args['posts_per_page'] = 100; // Bad.
+$query_args['posts_per_page'] = 200; // Bad.
+$query_args['posts_per_page'] = 300; // Bad.
+// phpcs:set WordPress.WP.PostsPerPage posts_per_page 200
+$query_args['posts_per_page'] = 50; // OK.
+$query_args['posts_per_page'] = 100; // OK.
+$query_args['posts_per_page'] = 200; // OK.
+$query_args['posts_per_page'] = 300; // Bad.
+// phpcs:set WordPress.WP.PostsPerPage posts_per_page 100
+
+// phpcs:set WordPress.WP.PostsPerPage exclude[] posts_per_page
+$query_args['posts_per_page'] = 300; // OK, group excluded.
+// phpcs:set WordPress.WP.PostsPerPage exclude[]
+
+// Ensure there will be no false positive for array access brackets when not used for an assignment.
+$var = $query_args['posts_per_page']; // OK.
+$firstChars = $text[0] . $text[1]; // OK.
+
+// Text strings which are not query strings should be ignored.
+_query_posts( 'numberposts' ); // OK.
+
+// Assignments to non-string keys should be ignored. Note: PHP will auto-cast numeric strings to ints, so those should also be disregarded.
+$var[10] = 300; // OK.
+$var[] = 400; // OK.
+$var['239'] = 500; // OK.
+
+// Ensure the sniff disregards comments.
+$query_args['posts_per_page' /* high */ ] = 999; // Bad.
+
+$query_args['posts_per_page'] /* high */ = 999; // Bad.
+
+$args = array(
+ 'posts_per_page' /* high */ => 999, // Bad.
+);
+
+$query_args['posts_per_page'] = /* high */ 999; // Bad.
+$args = array(
+ 'posts_per_page' => /* high */ 999, // Bad.
+);
+
+// Safeguard that when a query string contains duplicate key, the value of the last one is used.
+_query_posts( 'posts_per_page=999&nopaging=true&posts_per_page=50' ); // OK.
+
+// Ensure the error gets reported on the key pointer.
+$query_args[
+ 'posts_per_page'
+] = 300; // Bad, error should be reported on the above line.
+
+// Ensure that PHP 7.4 null coalesce equals get picked up on.
+$query_args['posts_per_page'] ??= 50; // OK.
+$query_args['posts_per_page'] ??= 200; // Bad.
+
+// Ensure that the sniff does not report on PHP 8.0 match arms.
+$val = match($val) {
+ 'posts_per_page' => 999, // OK, not an array assignment.
+};
+
+// Verify handling of arrays without trailing comma after the last array item.
+$args = array( 'posts_per_page' => 999 ); // Bad.
+$args = [
+ 'posts_per_page' => 999
+]; // Bad.
+
+// Verify that the complete value is being captured correctly and that non-numeric values are disregarded.
+$args = array(
+ 'posts_per_page' => min( max( $first, $last ), $default_min ), // Should be ignored as undetermined.
+ 'posts_per_page' => 10 + $extra, // Should be ignored as undetermined.
+ 'posts_per_page' => $value[0][1], // Should be ignored as undetermined.
+ 'posts_per_page' => $value ? 10 * $value : 300, // Should be ignored as undetermined.
+ 'posts_per_page' => get_value( name: 'post_per_page', type: 'query' ), // Should be ignored as undetermined.
+ 'posts_per_page' => function($a): int {
+ return do_something( $a );
+ }, // Should be ignored as undetermined.
+ 'posts_per_page' => [ 100, 200, 300 ], // Should be ignored as undetermined.
+ 'posts_per_page' => array(100, 200, 300), // Should be ignored as undetermined.
+);
+$query_args['posts_per_page'] = my\get_posts_per_page(); // Should be ignored as undetermined.
+$query_args['posts_per_page'] = '1e3'; // Should be ignored as undetermined. Would evaluate to 1000 with an int cast, but WP doesn't cast the value.
+
+// Purely numeric strings should probably be accepted still as this won't make a difference for the database query.
+$query_args['posts_per_page'] = '50'; // OK.
+$query_args['posts_per_page'] = '200'; // Bad.
+$query_args['posts_per_page'] = "200"; // Bad.
+
+// Verify handling of explicitly positive numbers.
+$args = array(
+ 'posts_per_page' => +50, // OK.
+ 'posts_per_page' => +200, // Bad.
+);
+
+// Verify handling of PHP 7.4+ numeric literals, PHP 8.1 octal literals, non-decimal numbers and floats.
+$args = array(
+ 'posts_per_page' => 0b1001011, // OK (75).
+ 'posts_per_page' => 0b10010110, // Bad (150).
+ 'posts_per_page' => 0x4B, // OK (75).
+ 'posts_per_page' => 0x96, // Bad (150).
+ 'posts_per_page' => 0113, // OK (75).
+ 'posts_per_page' => 0226, // Bad (150).
+ 'posts_per_page' => 0O113, // OK (75).
+ 'posts_per_page' => 0o226, // Bad (150).
+ 'posts_per_page' => 7_5, // OK (75).
+ 'posts_per_page' => 1_50, // Bad (150).
+ 'posts_per_page' => 75.0, // OK (75).
+ 'posts_per_page' => 150.000, // Bad (150).
+);
diff --git a/WordPress/Tests/WP/PostsPerPageUnitTest.php b/WordPress/Tests/WP/PostsPerPageUnitTest.php
index 233609bd8a..bf3296ce25 100644
--- a/WordPress/Tests/WP/PostsPerPageUnitTest.php
+++ b/WordPress/Tests/WP/PostsPerPageUnitTest.php
@@ -3,31 +3,32 @@
* Unit test class for WordPress Coding Standard.
*
* @package WPCS\WordPressCodingStandards
- * @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards
+ * @link https://github.com/WordPress/WordPress-Coding-Standards
* @license https://opensource.org/licenses/MIT MIT
*/
-namespace WordPress\Tests\WP;
+namespace WordPressCS\WordPress\Tests\WP;
use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest;
/**
* Unit test class for the PostsPerPage sniff.
*
- * @package WPCS\WordPressCodingStandards
+ * @since 0.3.0
+ * @since 0.13.0 Class name changed: this class is now namespaced.
+ * @since 1.0.0 This sniff has been split into two, with the check for high pagination
+ * limit being part of the WP category, and the check for pagination
+ * disabling being part of the VIP category.
*
- * @since 0.3.0
- * @since 0.13.0 Class name changed: this class is now namespaced.
- * @since 1.0.0 This sniff has been split into two, with the check for high pagination
- * limit being part of the WP category, and the check for pagination
- * disabling being part of the VIP category.
+ * @covers \WordPressCS\WordPress\AbstractArrayAssignmentRestrictionsSniff
+ * @covers \WordPressCS\WordPress\Sniffs\WP\PostsPerPageSniff
*/
-class PostsPerPageUnitTest extends AbstractSniffUnitTest {
+final class PostsPerPageUnitTest extends AbstractSniffUnitTest {
/**
* Returns the lines where errors should occur.
*
- * @return array =>
+ * @return array Key is the line number, value is the number of expected errors.
*/
public function getErrorList() {
return array();
@@ -36,19 +37,36 @@ public function getErrorList() {
/**
* Returns the lines where warnings should occur.
*
- * @return array =>