From ff19bbb4479a4752b90890025c83c8aa3a393fd1 Mon Sep 17 00:00:00 2001 From: Rebecca Hum Date: Wed, 31 Jul 2024 12:38:59 -0600 Subject: [PATCH 001/165] Disable WordPress.Security.EscapeOutput.ExceptionNotEscaped --- WordPress-VIP-Go/ruleset.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WordPress-VIP-Go/ruleset.xml b/WordPress-VIP-Go/ruleset.xml index 8cd531c3..35591e4a 100644 --- a/WordPress-VIP-Go/ruleset.xml +++ b/WordPress-VIP-Go/ruleset.xml @@ -251,5 +251,7 @@ 0 - + + 0 + From 0b496da8ec3fbb97d771a9666ddc05dafc49e728 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 27 Aug 2024 17:42:31 +0200 Subject: [PATCH 002/165] PHPStan: fix the build The last few builds were failing due to the below PHPStan error: ``` ------ --------------------------------- Line tests/RulesetTest.php ------ --------------------------------- 146 Left side of && is always true. ------ --------------------------------- ``` Fixed now. Note: the condition could possibly be removed, but I have a feeling I added it for a reason, so I left it in under the guise of "better safe than sorry". --- tests/RulesetTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/RulesetTest.php b/tests/RulesetTest.php index f6fed8fe..6c92e731 100644 --- a/tests/RulesetTest.php +++ b/tests/RulesetTest.php @@ -143,7 +143,7 @@ private function run() { */ private function collect_phpcs_result() { $php = ''; - if ( \PHP_BINARY && in_array( \PHP_SAPI, [ 'cgi-fcgi', 'cli', 'cli-server', 'phpdbg' ], true ) ) { + if ( defined( 'PHP_BINARY' ) && in_array( \PHP_SAPI, [ 'cgi-fcgi', 'cli', 'cli-server', 'phpdbg' ], true ) ) { $php = \PHP_BINARY . ' '; } From 12154a65de39050aae86dcac126e8a6861742999 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 8 Nov 2024 11:02:40 +0100 Subject: [PATCH 003/165] Docs: improve consistency and specificity [1] This fixes up the `@return` type declarations for all `register()` methods. --- .../Sniffs/AbstractVariableRestrictionsSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php | 2 +- .../Sniffs/Constants/RestrictedConstantsSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Files/IncludingFileSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Files/IncludingNonPHPFileSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Functions/CheckReturnValueSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php | 2 +- WordPressVIPMinimum/Sniffs/JS/DangerouslySetInnerHTMLSniff.php | 2 +- WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php | 2 +- WordPressVIPMinimum/Sniffs/JS/InnerHTMLSniff.php | 2 +- WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php | 2 +- WordPressVIPMinimum/Sniffs/JS/StrippingTagsSniff.php | 2 +- WordPressVIPMinimum/Sniffs/JS/WindowSniff.php | 2 +- .../Sniffs/Performance/CacheValueOverrideSniff.php | 2 +- .../Sniffs/Performance/FetchingRemoteDataSniff.php | 2 +- .../Sniffs/Performance/TaxonomyMetaInOptionsSniff.php | 2 +- .../Sniffs/Security/EscapingVoidReturnFunctionsSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Security/ExitAfterRedirectSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php | 2 +- .../Sniffs/Security/ProperEscapingFunctionSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Security/TwigSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Security/UnderscorejsSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Security/VuejsSniff.php | 2 +- .../Sniffs/UserExperience/AdminBarRemovalSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php | 2 +- 28 files changed, 28 insertions(+), 28 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php b/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php index 06b61bfb..17317d14 100644 --- a/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php @@ -58,7 +58,7 @@ abstract class AbstractVariableRestrictionsSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { // Retrieve the groups only once and don't set up a listener if there are no groups. diff --git a/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php b/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php index b2a8dafb..75db4dfe 100644 --- a/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php +++ b/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php @@ -20,7 +20,7 @@ class ConstantStringSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ diff --git a/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php b/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php index 619c2631..fec79e06 100644 --- a/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php @@ -38,7 +38,7 @@ class RestrictedConstantsSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ diff --git a/WordPressVIPMinimum/Sniffs/Files/IncludingFileSniff.php b/WordPressVIPMinimum/Sniffs/Files/IncludingFileSniff.php index f1bfc2db..b6429051 100644 --- a/WordPressVIPMinimum/Sniffs/Files/IncludingFileSniff.php +++ b/WordPressVIPMinimum/Sniffs/Files/IncludingFileSniff.php @@ -107,7 +107,7 @@ public function getGroups() { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return Tokens::$includeTokens; diff --git a/WordPressVIPMinimum/Sniffs/Files/IncludingNonPHPFileSniff.php b/WordPressVIPMinimum/Sniffs/Files/IncludingNonPHPFileSniff.php index ab32dc9f..311dce7e 100644 --- a/WordPressVIPMinimum/Sniffs/Files/IncludingNonPHPFileSniff.php +++ b/WordPressVIPMinimum/Sniffs/Files/IncludingNonPHPFileSniff.php @@ -44,7 +44,7 @@ class IncludingNonPHPFileSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return Tokens::$includeTokens; diff --git a/WordPressVIPMinimum/Sniffs/Functions/CheckReturnValueSniff.php b/WordPressVIPMinimum/Sniffs/Functions/CheckReturnValueSniff.php index 5eb40b2f..3fdbd936 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/CheckReturnValueSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/CheckReturnValueSniff.php @@ -57,7 +57,7 @@ class CheckReturnValueSniff extends Sniff { /** * Returns the token types that this sniff is interested in. * - * @return array(int) + * @return array */ public function register() { return [ T_STRING ]; diff --git a/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php b/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php index ca2f2acc..740cb5d3 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php @@ -62,7 +62,7 @@ class DynamicCallsSniff extends Sniff { /** * Returns the token types that this sniff is interested in. * - * @return array(int) + * @return array */ public function register() { return [ T_VARIABLE => T_VARIABLE ]; diff --git a/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php index 7bf70d9a..b9e966b4 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php @@ -27,7 +27,7 @@ class AlwaysReturnInFilterSniff extends Sniff { /** * Returns the token types that this sniff is interested in. * - * @return array(int) + * @return array */ public function register() { return [ T_STRING ]; diff --git a/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php index 88d49e62..07e5ce8d 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php @@ -21,7 +21,7 @@ class PreGetPostsSniff extends Sniff { /** * Returns the token types that this sniff is interested in. * - * @return array(int) + * @return array */ public function register() { return [ T_STRING ]; diff --git a/WordPressVIPMinimum/Sniffs/JS/DangerouslySetInnerHTMLSniff.php b/WordPressVIPMinimum/Sniffs/JS/DangerouslySetInnerHTMLSniff.php index ce68a775..f8ec0216 100644 --- a/WordPressVIPMinimum/Sniffs/JS/DangerouslySetInnerHTMLSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/DangerouslySetInnerHTMLSniff.php @@ -27,7 +27,7 @@ class DangerouslySetInnerHTMLSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ diff --git a/WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php index 0a106f22..373833ca 100644 --- a/WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php @@ -52,7 +52,7 @@ class HTMLExecutingFunctionsSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ diff --git a/WordPressVIPMinimum/Sniffs/JS/InnerHTMLSniff.php b/WordPressVIPMinimum/Sniffs/JS/InnerHTMLSniff.php index f35f9894..8b235f77 100644 --- a/WordPressVIPMinimum/Sniffs/JS/InnerHTMLSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/InnerHTMLSniff.php @@ -27,7 +27,7 @@ class InnerHTMLSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ diff --git a/WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php b/WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php index 049b35c0..17eb4445 100644 --- a/WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php @@ -27,7 +27,7 @@ class StringConcatSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ diff --git a/WordPressVIPMinimum/Sniffs/JS/StrippingTagsSniff.php b/WordPressVIPMinimum/Sniffs/JS/StrippingTagsSniff.php index 88fef947..2b38105e 100644 --- a/WordPressVIPMinimum/Sniffs/JS/StrippingTagsSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/StrippingTagsSniff.php @@ -27,7 +27,7 @@ class StrippingTagsSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ diff --git a/WordPressVIPMinimum/Sniffs/JS/WindowSniff.php b/WordPressVIPMinimum/Sniffs/JS/WindowSniff.php index d54d5690..3e292dfa 100644 --- a/WordPressVIPMinimum/Sniffs/JS/WindowSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/WindowSniff.php @@ -27,7 +27,7 @@ class WindowSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ diff --git a/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php b/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php index 8d39c1a2..b1acb06f 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php @@ -18,7 +18,7 @@ class CacheValueOverrideSniff extends Sniff { /** * Returns the token types that this sniff is interested in. * - * @return array(int) + * @return array */ public function register() { return [ T_STRING ]; diff --git a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php index fab5eada..823f2a8d 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php @@ -19,7 +19,7 @@ class FetchingRemoteDataSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ T_STRING ]; diff --git a/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php b/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php index 07607275..a2c50420 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php @@ -46,7 +46,7 @@ class TaxonomyMetaInOptionsSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ T_STRING ]; diff --git a/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php index 6a6ba281..93c959cd 100644 --- a/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php @@ -26,7 +26,7 @@ class EscapingVoidReturnFunctionsSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ diff --git a/WordPressVIPMinimum/Sniffs/Security/ExitAfterRedirectSniff.php b/WordPressVIPMinimum/Sniffs/Security/ExitAfterRedirectSniff.php index 3c90c315..f41e4d82 100644 --- a/WordPressVIPMinimum/Sniffs/Security/ExitAfterRedirectSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/ExitAfterRedirectSniff.php @@ -19,7 +19,7 @@ class ExitAfterRedirectSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ T_STRING ]; diff --git a/WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php b/WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php index b026b443..68cc423c 100644 --- a/WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php @@ -25,7 +25,7 @@ class MustacheSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ diff --git a/WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php b/WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php index 12ee05c7..1a9e9334 100644 --- a/WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php @@ -107,7 +107,7 @@ class ProperEscapingFunctionSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { $this->echo_or_concat_tokens += Tokens::$emptyTokens; diff --git a/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php b/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php index 58e90e74..612e8845 100644 --- a/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php @@ -19,7 +19,7 @@ class StaticStrreplaceSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ T_STRING ]; diff --git a/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php b/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php index fdc8f643..e1a5abed 100644 --- a/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php @@ -25,7 +25,7 @@ class TwigSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ diff --git a/WordPressVIPMinimum/Sniffs/Security/UnderscorejsSniff.php b/WordPressVIPMinimum/Sniffs/Security/UnderscorejsSniff.php index ecb40db2..7bb0d698 100644 --- a/WordPressVIPMinimum/Sniffs/Security/UnderscorejsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/UnderscorejsSniff.php @@ -50,7 +50,7 @@ class UnderscorejsSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { $targets = Tokens::$textStringTokens; diff --git a/WordPressVIPMinimum/Sniffs/Security/VuejsSniff.php b/WordPressVIPMinimum/Sniffs/Security/VuejsSniff.php index 7f9d9e73..bf7de67a 100644 --- a/WordPressVIPMinimum/Sniffs/Security/VuejsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/VuejsSniff.php @@ -25,7 +25,7 @@ class VuejsSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index 3db0aea6..b4c986cb 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -118,7 +118,7 @@ class AdminBarRemovalSniff extends AbstractFunctionParameterSniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { // Set up all string targets. diff --git a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php index 09f00bf9..47ce66ec 100644 --- a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php @@ -35,7 +35,7 @@ class ServerVariablesSniff extends Sniff { /** * Returns an array of tokens this test wants to listen for. * - * @return array + * @return array */ public function register() { return [ From 3a56af30559b44ece96c551e4ddae309f273c781 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 8 Nov 2024 11:52:52 +0100 Subject: [PATCH 004/165] Docs: improve consistency and specificity [2] This fixes up the `@var` type declarations for all `AbstractFunctionParameterSniff::$target_functions` properties. --- WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php | 5 +---- WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php | 5 +---- .../Sniffs/Performance/LowExpiryCacheTimeSniff.php | 5 +---- .../Sniffs/Security/PHPFilterFunctionsSniff.php | 5 +---- .../Sniffs/UserExperience/AdminBarRemovalSniff.php | 2 +- 5 files changed, 5 insertions(+), 17 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php b/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php index cf29e454..a06b01c2 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php @@ -26,10 +26,7 @@ class StripTagsSniff extends AbstractFunctionParameterSniff { /** * Functions this sniff is looking for. * - * @var array The only requirement for this array is that the top level - * array keys are the names of the functions you're looking for. - * Other than that, the array can have arbitrary content - * depending on your needs. + * @var array Key is the function name, value irrelevant. */ protected $target_functions = [ 'strip_tags' => true, diff --git a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php index d15dac7d..5908a702 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php @@ -26,10 +26,7 @@ class RestrictedHooksSniff extends AbstractFunctionParameterSniff { /** * Functions this sniff is looking for. * - * @var array The only requirement for this array is that the top level - * array keys are the names of the functions you're looking for. - * Other than that, the array can have arbitrary content - * depending on your needs. + * @var array Key is the function name, value irrelevant. */ protected $target_functions = [ 'add_filter' => true, diff --git a/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php b/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php index cededb87..105b38ac 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php @@ -30,10 +30,7 @@ class LowExpiryCacheTimeSniff extends AbstractFunctionParameterSniff { /** * Functions this sniff is looking for. * - * @var array The only requirement for this array is that the top level - * array keys are the names of the functions you're looking for. - * Other than that, the array can have arbitrary content - * depending on your needs. + * @var array Key is the function name, value irrelevant. */ protected $target_functions = [ 'wp_cache_set' => true, diff --git a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php index e2907d9a..34c73014 100644 --- a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php @@ -26,10 +26,7 @@ class PHPFilterFunctionsSniff extends AbstractFunctionParameterSniff { /** * Functions this sniff is looking for. * - * @var array The only requirement for this array is that the top level - * array keys are the names of the functions you're looking for. - * Other than that, the array can have arbitrary content - * depending on your needs. + * @var array Key is the function name, value irrelevant. */ protected $target_functions = [ 'filter_var' => true, diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index b4c986cb..51b69d80 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -48,7 +48,7 @@ class AdminBarRemovalSniff extends AbstractFunctionParameterSniff { /** * Functions this sniff is looking for. * - * @var array + * @var array Key is the function name, value irrelevant. */ protected $target_functions = [ 'show_admin_bar' => true, From 02b07facd04248e80b9db08cefb066693a2f2fe1 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 8 Nov 2024 11:54:55 +0100 Subject: [PATCH 005/165] Docs: improve consistency and specificity [3] This fixes up the `@return` type declarations for all `AbstractVariableRestrictionsSniff::getGroups()` methods. --- .../Sniffs/AbstractVariableRestrictionsSniff.php | 2 +- .../Sniffs/Variables/RestrictedVariablesSniff.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php b/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php index 17317d14..437c33c0 100644 --- a/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php @@ -91,7 +91,7 @@ public function register() { * ) * ) * - * @return array + * @return array>> */ abstract public function getGroups(); diff --git a/WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php b/WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php index 65687642..3e1275cb 100644 --- a/WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php @@ -31,7 +31,7 @@ class RestrictedVariablesSniff extends AbstractVariableRestrictionsSniff { * ) * ) * - * @return array + * @return array>> */ public function getGroups() { return [ From 6ab21f81a5bde4d33b51f174d5509fb684003b86 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 8 Nov 2024 12:00:09 +0100 Subject: [PATCH 006/165] Docs: improve consistency and specificity [4] This fixes up the `@return` type declarations for all `AbstractArrayAssignmentRestrictionsSniff::getGroups()` methods. --- WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Performance/RegexpCompareSniff.php | 2 +- .../Sniffs/Performance/RemoteRequestTimeoutSniff.php | 2 +- WordPressVIPMinimum/Sniffs/Performance/WPQueryParamsSniff.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php b/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php index c751f021..4aca9ddd 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php @@ -23,7 +23,7 @@ class NoPagingSniff extends AbstractArrayAssignmentRestrictionsSniff { /** * Groups of variables to restrict. * - * @return array + * @return array>> */ public function getGroups() { return [ diff --git a/WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php b/WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php index 2747b242..a9531d71 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php @@ -24,7 +24,7 @@ class OrderByRandSniff extends AbstractArrayAssignmentRestrictionsSniff { /** * Groups of variables to restrict. * - * @return array + * @return array>> */ public function getGroups() { return [ diff --git a/WordPressVIPMinimum/Sniffs/Performance/RegexpCompareSniff.php b/WordPressVIPMinimum/Sniffs/Performance/RegexpCompareSniff.php index 536bd1ee..c12ed35d 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/RegexpCompareSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/RegexpCompareSniff.php @@ -18,7 +18,7 @@ class RegexpCompareSniff extends AbstractArrayAssignmentRestrictionsSniff { /** * Groups of variables to restrict. * - * @return array + * @return array>> */ public function getGroups() { return [ diff --git a/WordPressVIPMinimum/Sniffs/Performance/RemoteRequestTimeoutSniff.php b/WordPressVIPMinimum/Sniffs/Performance/RemoteRequestTimeoutSniff.php index 072ba5af..266a5c55 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/RemoteRequestTimeoutSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/RemoteRequestTimeoutSniff.php @@ -17,7 +17,7 @@ class RemoteRequestTimeoutSniff extends AbstractArrayAssignmentRestrictionsSniff /** * Groups of variables to restrict. * - * @return array + * @return array>> */ public function getGroups() { return [ diff --git a/WordPressVIPMinimum/Sniffs/Performance/WPQueryParamsSniff.php b/WordPressVIPMinimum/Sniffs/Performance/WPQueryParamsSniff.php index 9c247f8d..2b004c9c 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/WPQueryParamsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/WPQueryParamsSniff.php @@ -28,7 +28,7 @@ class WPQueryParamsSniff extends AbstractArrayAssignmentRestrictionsSniff { /** * Groups of variables to restrict. * - * @return array + * @return array>> */ public function getGroups() { return [ From b95dde72e15c5493a81c93a4ea4577f28ffdf3d7 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 8 Nov 2024 11:14:54 +0100 Subject: [PATCH 007/165] DeclarationCompatibility::$checkClasses: improve type specificity --- .../Sniffs/Classes/DeclarationCompatibilitySniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index a71b4907..d610933b 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -27,7 +27,7 @@ class DeclarationCompatibilitySniff extends AbstractScopeSniff { /** * A list of classes and methods to check. * - * @var array>> + * @var array>>> */ public $checkClasses = [ 'WP_Widget' => [ From 97d83d589de20088d163ed35b2b809899a84fa1e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 8 Nov 2024 12:04:39 +0100 Subject: [PATCH 008/165] Docs: improve specificity Miscellaneous other doc specificity fixes. Note: this doesn't fix everything throughout the codebase, but is an iteration to improve things nonetheless. --- .../Sniffs/Classes/DeclarationCompatibilitySniff.php | 2 +- .../Sniffs/Classes/RestrictedExtendClassesSniff.php | 2 +- .../Sniffs/Constants/RestrictedConstantsSniff.php | 4 ++-- .../Sniffs/Files/IncludingFileSniff.php | 10 +++++----- .../Sniffs/Files/IncludingNonPHPFileSniff.php | 4 ++-- .../Sniffs/Functions/CheckReturnValueSniff.php | 6 +++--- .../Sniffs/Functions/DynamicCallsSniff.php | 4 ++-- .../Sniffs/Functions/RestrictedFunctionsSniff.php | 2 +- .../Sniffs/Hooks/RestrictedHooksSniff.php | 2 +- .../Sniffs/JS/HTMLExecutingFunctionsSniff.php | 2 +- WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php | 4 ++-- WordPressVIPMinimum/Sniffs/JS/WindowSniff.php | 2 +- .../Sniffs/Performance/LowExpiryCacheTimeSniff.php | 2 +- .../Sniffs/Performance/TaxonomyMetaInOptionsSniff.php | 4 ++-- .../Sniffs/Security/PHPFilterFunctionsSniff.php | 2 +- .../Sniffs/Security/ProperEscapingFunctionSniff.php | 8 ++++---- .../Sniffs/UserExperience/AdminBarRemovalSniff.php | 8 ++++---- .../Sniffs/Variables/ServerVariablesSniff.php | 2 +- tests/RulesetTest.php | 6 +++--- 19 files changed, 38 insertions(+), 38 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index d610933b..57419759 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -315,7 +315,7 @@ private function addError( $parentClassName, $methodName, $currentMethodSignatur * * @param array $methodSignature Signature of a method. * - * @return array + * @return array */ private function generateParamList( $methodSignature ) { $paramList = []; diff --git a/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php b/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php index a8b076a2..94eaf5a2 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php @@ -19,7 +19,7 @@ class RestrictedExtendClassesSniff extends AbstractClassRestrictionsSniff { /** * Groups of classes to restrict. * - * @return array + * @return array>> */ public function getGroups() { return [ diff --git a/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php b/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php index fec79e06..055f1aba 100644 --- a/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php @@ -19,7 +19,7 @@ class RestrictedConstantsSniff extends Sniff { /** * List of restricted constant names. * - * @var array + * @var array */ public $restrictedConstantNames = [ 'A8C_PROXIED_REQUEST', @@ -28,7 +28,7 @@ class RestrictedConstantsSniff extends Sniff { /** * List of restricted constant declarations. * - * @var array + * @var array */ public $restrictedConstantDeclaration = [ 'JETPACK_DEV_DEBUG', diff --git a/WordPressVIPMinimum/Sniffs/Files/IncludingFileSniff.php b/WordPressVIPMinimum/Sniffs/Files/IncludingFileSniff.php index b6429051..7aea965b 100644 --- a/WordPressVIPMinimum/Sniffs/Files/IncludingFileSniff.php +++ b/WordPressVIPMinimum/Sniffs/Files/IncludingFileSniff.php @@ -20,7 +20,7 @@ class IncludingFileSniff extends AbstractFunctionRestrictionsSniff { /** * List of function used for getting paths. * - * @var array + * @var array */ public $getPathFunctions = [ 'dirname', @@ -55,7 +55,7 @@ class IncludingFileSniff extends AbstractFunctionRestrictionsSniff { /** * List of restricted constants. * - * @var array + * @var array */ public $restrictedConstants = [ 'TEMPLATEPATH' => 'get_template_directory', @@ -65,7 +65,7 @@ class IncludingFileSniff extends AbstractFunctionRestrictionsSniff { /** * List of allowed constants. * - * @var array + * @var array */ public $allowedConstants = [ 'ABSPATH', @@ -77,7 +77,7 @@ class IncludingFileSniff extends AbstractFunctionRestrictionsSniff { * List of keywords allowed for use in custom constants. * Note: Customizing this property will overwrite current default values. * - * @var array + * @var array */ public $allowedKeywords = [ 'PATH', @@ -87,7 +87,7 @@ class IncludingFileSniff extends AbstractFunctionRestrictionsSniff { /** * Functions used for modify slashes. * - * @var array + * @var array */ public $slashingFunctions = [ 'trailingslashit', diff --git a/WordPressVIPMinimum/Sniffs/Files/IncludingNonPHPFileSniff.php b/WordPressVIPMinimum/Sniffs/Files/IncludingNonPHPFileSniff.php index 311dce7e..342edd19 100644 --- a/WordPressVIPMinimum/Sniffs/Files/IncludingNonPHPFileSniff.php +++ b/WordPressVIPMinimum/Sniffs/Files/IncludingNonPHPFileSniff.php @@ -23,7 +23,7 @@ class IncludingNonPHPFileSniff extends Sniff { * * Files with these extensions are allowed to be `include`d. * - * @var array Key is the extension, value is irrelevant. + * @var array Key is the extension, value is irrelevant. */ private $php_extensions = [ 'php' => true, @@ -34,7 +34,7 @@ class IncludingNonPHPFileSniff extends Sniff { /** * File extensions used for SVG and CSS files. * - * @var array Key is the extension, value is irrelevant. + * @var array Key is the extension, value is irrelevant. */ private $svg_css_extensions = [ 'css' => true, diff --git a/WordPressVIPMinimum/Sniffs/Functions/CheckReturnValueSniff.php b/WordPressVIPMinimum/Sniffs/Functions/CheckReturnValueSniff.php index 3fdbd936..3bf6c0df 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/CheckReturnValueSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/CheckReturnValueSniff.php @@ -24,7 +24,7 @@ class CheckReturnValueSniff extends Sniff { /** * Pairs we are about to check. * - * @var array + * @var array> */ public $catch = [ 'esc_url' => [ @@ -48,7 +48,7 @@ class CheckReturnValueSniff extends Sniff { /** * Tokens we are about to examine, which are not functions. * - * @var array + * @var array */ public $notFunctions = [ 'foreach' => T_FOREACH, @@ -293,7 +293,7 @@ public function findNonCheckedVariables( $stackPtr ) { * Function used as as callback for the array_reduce call. * * @param string|null $carry The final string. - * @param mixed $item Processed item. + * @param array $item Processed item. * * @return string */ diff --git a/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php b/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php index 740cb5d3..50abf546 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php @@ -29,7 +29,7 @@ class DynamicCallsSniff extends Sniff { /** * Functions that should not be called dynamically. * - * @var array + * @var array */ private $disallowed_functions = [ 'assert' => true, @@ -48,7 +48,7 @@ class DynamicCallsSniff extends Sniff { * * Populated at run-time. * - * @var array The key is the name of the variable, the value, its assigned value. + * @var array The key is the name of the variable, the value, its assigned value. */ private $variables_arr = []; diff --git a/WordPressVIPMinimum/Sniffs/Functions/RestrictedFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Functions/RestrictedFunctionsSniff.php index 4513140c..debdec70 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/RestrictedFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/RestrictedFunctionsSniff.php @@ -18,7 +18,7 @@ class RestrictedFunctionsSniff extends AbstractFunctionRestrictionsSniff { /** * Groups of functions to restrict. * - * @return array + * @return array|array>> */ public function getGroups() { diff --git a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php index 5908a702..bfe524e6 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php @@ -36,7 +36,7 @@ class RestrictedHooksSniff extends AbstractFunctionParameterSniff { /** * List of restricted filters by groups. * - * @var array + * @var array>> */ private $restricted_hook_groups = [ 'upload_mimes' => [ diff --git a/WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php index 373833ca..9188752b 100644 --- a/WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php @@ -24,7 +24,7 @@ class HTMLExecutingFunctionsSniff extends Sniff { * Value indicates whether the function's arg is the content to be inserted, or the target where the inserted * content is to be inserted before/after/replaced. For the latter, the content is in the preceding method's arg. * - * @var array + * @var array */ public $HTMLExecutingFunctions = [ 'after' => 'content', // jQuery. diff --git a/WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php b/WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php index 17eb4445..56902d07 100644 --- a/WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php @@ -63,8 +63,8 @@ public function process_token( $stackPtr ) { /** * Consolidated violation. * - * @param int $stackPtr The position of the current token in the stack passed in $tokens. - * @param array $data Replacements for the error message. + * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @param array $data Replacements for the error message. * * @return void */ diff --git a/WordPressVIPMinimum/Sniffs/JS/WindowSniff.php b/WordPressVIPMinimum/Sniffs/JS/WindowSniff.php index 3e292dfa..dc69d0b6 100644 --- a/WordPressVIPMinimum/Sniffs/JS/WindowSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/WindowSniff.php @@ -38,7 +38,7 @@ public function register() { /** * List of window properties that need to be flagged. * - * @var array + * @var array> */ private $windowProperties = [ 'location' => [ diff --git a/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php b/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php index 105b38ac..aa5a42db 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php @@ -41,7 +41,7 @@ class LowExpiryCacheTimeSniff extends AbstractFunctionParameterSniff { /** * List of WP time constants, see https://codex.wordpress.org/Easier_Expression_of_Time_Constants. * - * @var array + * @var array */ protected $wp_time_constants = [ 'MINUTE_IN_SECONDS' => 60, diff --git a/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php b/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php index a2c50420..e5adb2a7 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php @@ -19,7 +19,7 @@ class TaxonomyMetaInOptionsSniff extends Sniff { /** * List of options_ functions * - * @var array + * @var array */ public $option_functions = [ 'get_option', @@ -31,7 +31,7 @@ class TaxonomyMetaInOptionsSniff extends Sniff { /** * List of possible variable names holding term ID. * - * @var array + * @var array */ public $taxonomy_term_patterns = [ 'category_id', diff --git a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php index 34c73014..739145c6 100644 --- a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php @@ -38,7 +38,7 @@ class PHPFilterFunctionsSniff extends AbstractFunctionParameterSniff { /** * List of restricted filter names. * - * @var array + * @var array */ private $restricted_filters = [ 'FILTER_DEFAULT' => true, diff --git a/WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php b/WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php index 1a9e9334..fcb4d1b3 100644 --- a/WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php @@ -28,7 +28,7 @@ class ProperEscapingFunctionSniff extends Sniff { /** * List of escaping functions which are being tested. * - * @var array + * @var array */ protected $escaping_functions = [ 'esc_url' => 'url', @@ -45,7 +45,7 @@ class ProperEscapingFunctionSniff extends Sniff { /** * List of tokens we can skip. * - * @var array + * @var array */ private $echo_or_concat_tokens = [ @@ -63,7 +63,7 @@ class ProperEscapingFunctionSniff extends Sniff { * for public methods which extending sniffs may be * relying on. * - * @var array + * @var array */ private $url_attrs = [ 'href', @@ -79,7 +79,7 @@ class ProperEscapingFunctionSniff extends Sniff { * for public methods which extending sniffs may be * relying on. * - * @var array + * @var array */ private $attr_endings = [ '=', diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index 51b69d80..4d5c74ac 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -26,7 +26,7 @@ class AdminBarRemovalSniff extends AbstractFunctionParameterSniff { /** * A list of tokenizers this sniff supports. * - * @var array + * @var array */ public $supportedTokenizers = [ 'PHP', @@ -58,7 +58,7 @@ class AdminBarRemovalSniff extends AbstractFunctionParameterSniff { /** * CSS properties this sniff is looking for. * - * @var array + * @var array> */ protected $target_css_properties = [ 'visibility' => [ @@ -78,7 +78,7 @@ class AdminBarRemovalSniff extends AbstractFunctionParameterSniff { /** * CSS selectors this sniff is looking for. * - * @var array + * @var array */ protected $target_css_selectors = [ '.show-admin-bar', @@ -90,7 +90,7 @@ class AdminBarRemovalSniff extends AbstractFunctionParameterSniff { * * Set from the register() method. * - * @var array + * @var array */ private $string_tokens = []; diff --git a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php index 47ce66ec..c069febb 100644 --- a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php @@ -18,7 +18,7 @@ class ServerVariablesSniff extends Sniff { /** * List of restricted constant names. * - * @var array + * @var array> */ public $restrictedVariables = [ 'authVariables' => [ diff --git a/tests/RulesetTest.php b/tests/RulesetTest.php index 6c92e731..38b8aafc 100644 --- a/tests/RulesetTest.php +++ b/tests/RulesetTest.php @@ -28,7 +28,7 @@ class RulesetTest { * * This is the giant array in the ruleset-test.php files. * - * @var array + * @var array>> */ public $expected = []; @@ -82,8 +82,8 @@ class RulesetTest { /** * Init the object by processing the test file. * - * @param string $ruleset Name of the ruleset e.g. WordPressVIPMinimum or WordPress-VIP-Go. - * @param array $expected The array of expected errors, warnings and messages. + * @param string $ruleset Name of the ruleset e.g. WordPressVIPMinimum or WordPress-VIP-Go. + * @param array>> $expected The array of expected errors, warnings and messages. */ public function __construct( $ruleset, $expected = [] ) { $this->ruleset = $ruleset; From e2d48fa3af081da1105069e53cce56f7587880a6 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 13 Oct 2024 23:57:11 +0200 Subject: [PATCH 009/165] GH Actions: always quote variables ... to satisfy shellcheck rule SC2086: "Double quote to prevent globbing and word splitting". Ref: https://www.shellcheck.net/wiki/SC2086 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab08a031..4c7d784e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -101,9 +101,9 @@ jobs: id: set_ini run: | if [[ "${{ matrix.dependencies }}" != "dev" ]]; then - echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' >> $GITHUB_OUTPUT + echo 'PHP_INI=error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On' >> "$GITHUB_OUTPUT" else - echo 'PHP_INI=error_reporting=-1, display_errors=On' >> $GITHUB_OUTPUT + echo 'PHP_INI=error_reporting=-1, display_errors=On' >> "$GITHUB_OUTPUT" fi - name: Install PHP From 42a49a8363be517c4512c1f39604dace57aaa5e0 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 22 Aug 2024 04:01:34 +0200 Subject: [PATCH 010/165] Composer: prevent a lock file from being created Composer 1.10.0 introduced a `lock` config option, which, when set to `false` will prevent a `composer.lock` file from being created and will ignore it when one exists. This is a useful option for libraries such as this where the `lock` file has no meaning. It also makes life easier for contributors as they don't have to remember that for this repo they should use `composer update` instead of `composer install`. Both will now work the same. Refs: https://getcomposer.org/doc/06-config.md#lock --- .github/workflows/quicktest.yml | 4 ++++ .github/workflows/test.yml | 4 ++++ composer.json | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index fff3cfa4..5aeae65b 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -51,6 +51,10 @@ jobs: ini-values: error_reporting=E_ALL & ~E_DEPRECATED, display_errors=On coverage: none + - name: Enable creation of `composer.lock` file + if: ${{ matrix.dependencies == 'lowest' }} + run: composer config --unset lock + # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab08a031..17c10b36 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -123,6 +123,10 @@ jobs: sirbrillig/phpcs-variable-analysis:"2.x" wp-coding-standards/wpcs:"dev-develop" + - name: Enable creation of `composer.lock` file + if: ${{ matrix.dependencies == 'lowest' }} + run: composer config --unset lock + # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies diff --git a/composer.json b/composer.json index c65c23bb..e02138dc 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,8 @@ "config": { "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true - } + }, + "lock": false }, "scripts": { "test-ruleset": "bin/ruleset-tests", From 9771ef42b549de58869205f9c5cef1a931af43a3 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 21 Nov 2024 20:07:24 +0100 Subject: [PATCH 011/165] GH Actions: PHP 8.4 has been released * Builds against PHP 8.4 are no longer allowed to fail. * Add _allowed to fail_ build against PHP 8.5. Ref: https://www.php.net/releases/8.4/en.php --- .github/workflows/test.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 99609523..ffbbb3d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,10 +28,10 @@ jobs: strategy: matrix: - php: ['5.4', 'latest', '8.4'] + php: ['5.4', 'latest', '8.5'] name: "Lint: PHP ${{ matrix.php }}" - continue-on-error: ${{ matrix.php == '8.4' }} + continue-on-error: ${{ matrix.php == '8.5' }} steps: - name: Checkout code @@ -67,9 +67,9 @@ jobs: # - PHPCS will run without errors on PHP 5.4 - 7.4 on any supported version. # - PHP 8.0 needs PHPCS 3.5.7+ to run without errors, and we require a higher minimum version. # - PHP 8.1 needs PHPCS 3.6.1+ to run without errors, but works best with 3.7.1+, and we require at least this minimum version. - # - PHP 8.2 and 8.3 need PHPCS 3.8.0+ to run without errors (though the errors don't affect the tests). + # - PHP 8.2, 8.3 and 8.4 need PHPCS 3.8.0+ to run without errors (though the errors don't affect the tests). matrix: - php: ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] + php: ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] dependencies: ['lowest', 'stable'] include: @@ -80,16 +80,16 @@ jobs: dependencies: 'dev' - php: '7.4' dependencies: 'dev' - - php: '8.3' + - php: '8.4' dependencies: 'dev' # Test against upcoming PHP version. - - php: '8.4' + - php: '8.5' dependencies: 'dev' name: "Test: PHP ${{ matrix.php }} - PHPCS ${{ matrix.dependencies }}" - continue-on-error: ${{ matrix.php == '8.4' }} + continue-on-error: ${{ matrix.php == '8.5' }} steps: - name: Checkout code From ec3cc04b2b9ce11683d012bd1a3ec0cd6b1b6a68 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 25 Mar 2025 11:28:13 +0100 Subject: [PATCH 012/165] GH Actions: use the xmllint-validate action runner Instead of doing all the installation steps for xmllint validation in the workflow, use the :sparkles: new dedicated `phpcsstandards/xmllint-validate` action runner instead. Notes: * This does mean that the `bin/xml-lint` script is now no longer used in CI. I've left the script in place as there may be contributors who want to run these checks locally and the script can help them with that, though happy to remove the script if nobody uses it. Ref: https://github.com/marketplace/actions/xmllint-validate --- .github/workflows/basics.yml | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index 8a6c8fa1..9ae66670 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -22,6 +22,9 @@ jobs: name: 'Basic CS and QA checks' runs-on: ubuntu-latest + env: + XMLLINT_INDENT: ' ' # This is a tab character. + steps: - name: Checkout code uses: actions/checkout@v4 @@ -33,21 +36,6 @@ jobs: coverage: none tools: cs2pr - # Updating the lists can fail intermittently, typically after Microsoft has released a new package. - # This should not be blocking for this job, so ignore any errors from this step. - # Ref: https://github.com/dotnet/core/issues/4167 - - name: Update the available packages list - continue-on-error: true - run: sudo apt-get update - - - name: Install xmllint - run: sudo apt-get install --no-install-recommends -y libxml2-utils - - # Show XML violations inline in the file diff. - # @link https://github.com/marketplace/actions/xmllint-problem-matcher - - name: Enable showing XML issues inline - uses: korelstar/xmllint-problem-matcher@v1 - # Validate the composer.json file. # @link https://getcomposer.org/doc/03-cli.md#validate - name: Validate Composer installation @@ -65,8 +53,19 @@ jobs: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") - - name: 'Validate XML against schema and check code style' - run: ./bin/xml-lint + # Validate the XML file. + - name: Validate rulesets against schema + uses: phpcsstandards/xmllint-validate@v1 + with: + pattern: "./*/ruleset.xml" + xsd-file: "vendor/squizlabs/php_codesniffer/phpcs.xsd" + + # Check the code-style consistency of the XML file. + # Note: this needs xmllint, but that will be installed via the phpcsstandards/xmllint-validate action runner in the previous step. + - name: Check XML code style + run: | + diff -B --tabsize=4 ./WordPressVIPMinimum/ruleset.xml <(xmllint --format "./WordPressVIPMinimum/ruleset.xml") + diff -B --tabsize=4 ./WordPress-VIP-Go/ruleset.xml <(xmllint --format "./WordPress-VIP-Go/ruleset.xml") # Check the code-style consistency of the PHP files. - name: Check PHP code style From 771cb525cbb5491f7230bd85d8f9cec4ff454566 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 25 Mar 2025 11:28:50 +0100 Subject: [PATCH 013/165] GH Actions: add some additional XML validation checks ... for dev tool files. --- .github/workflows/basics.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index 9ae66670..b3143c46 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -67,6 +67,30 @@ jobs: diff -B --tabsize=4 ./WordPressVIPMinimum/ruleset.xml <(xmllint --format "./WordPressVIPMinimum/ruleset.xml") diff -B --tabsize=4 ./WordPress-VIP-Go/ruleset.xml <(xmllint --format "./WordPress-VIP-Go/ruleset.xml") + # Validate dev tool related XML files. + - name: Validate Project PHPCS ruleset against schema + uses: phpcsstandards/xmllint-validate@v1 + with: + pattern: ".phpcs.xml.dist" + xsd-file: "vendor/squizlabs/php_codesniffer/phpcs.xsd" + + # Notes: + # - PHPUnit 9.5 (which will be installed in this job) doesn't ship XSD files further back than 8.5. + # - PHPUnit 9.3 introduced some new configuration options and deprecated the old versions of those. + # For cross-version compatibility with older PHPUnit versions, those new config options cannot be used yet, + # which is why the PHPUnit 9 validation is done against the PHPUnit 9.2 schema. + - name: "Validate PHPUnit config for use with PHPUnit 8" + uses: phpcsstandards/xmllint-validate@v1 + with: + pattern: "phpunit.xml.dist" + xsd-file: "vendor/phpunit/phpunit/schema/8.5.xsd" + + - name: "Validate PHPUnit config for use with PHPUnit 9" + uses: phpcsstandards/xmllint-validate@v1 + with: + pattern: "phpunit.xml.dist" + xsd-file: "vendor/phpunit/phpunit/schema/9.2.xsd" + # Check the code-style consistency of the PHP files. - name: Check PHP code style id: phpcs From 5e625129cb711203f0e1aa84e701593f64d2b093 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 25 Mar 2025 11:36:50 +0100 Subject: [PATCH 014/165] Remove the `bin/xml-lint` script ... now it is no longer used in CI. --- .github/CONTRIBUTING.md | 2 +- bin/xml-lint | 17 ----------------- composer.json | 5 ++--- 3 files changed, 3 insertions(+), 21 deletions(-) delete mode 100755 bin/xml-lint diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8a17c256..5da9ed79 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -61,7 +61,7 @@ This package contains Composer scripts to quickly run the developer checks which After `composer install`, you can do: -- `composer lint`: Lint PHP and XML files in against parse errors. +- `composer lint`: Lint PHP files against parse errors. - `composer cs`: Check the code style and code quality of the codebase via PHPCS. - `composer test`: Run the unit tests for the VIPCS sniffs. - `composer test-coverage`: Run the unit tests for the VIPCS sniffs with coverage enabled. diff --git a/bin/xml-lint b/bin/xml-lint deleted file mode 100755 index 6ab4e113..00000000 --- a/bin/xml-lint +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -# -# Check XML files. -# -# @link http://xmlsoft.org/xmllint.html -# -# EXAMPLE TO RUN LOCALLY: -# -# ./bin/xml-lint - -# Validate the ruleset XML files. -xmllint --noout --schema ./vendor/squizlabs/php_codesniffer/phpcs.xsd ./*/ruleset.xml - -# Check the code-style consistency of the XML files. -export XMLLINT_INDENT=" " # This is a tab character. -diff -B --tabsize=4 ./WordPressVIPMinimum/ruleset.xml <(xmllint --format "./WordPressVIPMinimum/ruleset.xml") -diff -B --tabsize=4 ./WordPress-VIP-Go/ruleset.xml <(xmllint --format "./WordPress-VIP-Go/ruleset.xml") diff --git a/composer.json b/composer.json index e02138dc..4908900a 100644 --- a/composer.json +++ b/composer.json @@ -39,8 +39,7 @@ "scripts": { "test-ruleset": "bin/ruleset-tests", "lint": [ - "bin/php-lint", - "bin/xml-lint" + "bin/php-lint" ], "cs": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs", "test": "bin/unit-tests", @@ -57,7 +56,7 @@ ] }, "scripts-descriptions": { - "lint": "VIPCS: Lint PHP and XML files in against parse errors.", + "lint": "VIPCS: Lint PHP files against parse errors.", "cs": "VIPCS: Check the code style and code quality of the codebase via PHPCS.", "test": "VIPCS: Run the unit tests for the VIPCS sniffs.", "test-coverage": "VIPCS: Run the unit tests for the VIPCS sniffs with coverage enabled.", From a843d6e0370e6ad39ba3b832492d6e3dbdcd1bf0 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 19 May 2025 15:11:22 +0200 Subject: [PATCH 015/165] Rulesets: update schema URL PHPCS now offers permalinks for the schema. Refs: * PHPCSStandards/PHP_CodeSniffer 1094 * https://github.com/PHPCSStandards/schema.phpcodesniffer.com --- .phpcs.xml.dist | 2 +- WordPress-VIP-Go/ruleset.xml | 2 +- WordPressVIPMinimum/ruleset.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 098e5acb..d3bb857a 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -1,5 +1,5 @@ - + The custom ruleset for the VIP Coding Standards itself. . diff --git a/WordPress-VIP-Go/ruleset.xml b/WordPress-VIP-Go/ruleset.xml index 35591e4a..3ec7e05b 100644 --- a/WordPress-VIP-Go/ruleset.xml +++ b/WordPress-VIP-Go/ruleset.xml @@ -1,5 +1,5 @@ - + WordPress VIP Go Coding Standards diff --git a/WordPressVIPMinimum/ruleset.xml b/WordPressVIPMinimum/ruleset.xml index 6a3f10c1..9441dc62 100644 --- a/WordPressVIPMinimum/ruleset.xml +++ b/WordPressVIPMinimum/ruleset.xml @@ -1,5 +1,5 @@ - + WordPress VIP Minimum Coding Standards - + + + diff --git a/WordPressVIPMinimum/Sniffs/JS/DangerouslySetInnerHTMLSniff.php b/WordPressVIPMinimum/Sniffs/JS/DangerouslySetInnerHTMLSniff.php index f8ec0216..7182d945 100644 --- a/WordPressVIPMinimum/Sniffs/JS/DangerouslySetInnerHTMLSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/DangerouslySetInnerHTMLSniff.php @@ -7,6 +7,7 @@ namespace WordPressVIPMinimum\Sniffs\JS; +use PHP_CodeSniffer\Sniffs\DeprecatedSniff; use PHP_CodeSniffer\Util\Tokens; use WordPressVIPMinimum\Sniffs\Sniff; @@ -15,7 +16,7 @@ * * Looks for instances of React's dangerouslySetInnerHMTL. */ -class DangerouslySetInnerHTMLSniff extends Sniff { +class DangerouslySetInnerHTMLSniff extends Sniff implements DeprecatedSniff { /** * A list of tokenizers this sniff supports. @@ -67,4 +68,31 @@ public function process_token( $stackPtr ) { $data = [ $this->tokens[ $stackPtr ]['content'] ]; $this->phpcsFile->addError( $message, $stackPtr, 'Found', $data ); } + + /** + * Provide the version number in which the sniff was deprecated. + * + * @return string + */ + public function getDeprecationVersion() { + return 'VIP-Coding-Standard v3.1.0'; + } + + /** + * Provide the version number in which the sniff will be removed. + * + * @return string + */ + public function getRemovalVersion() { + return 'VIP-Coding-Standard v4.0.0'; + } + + /** + * Provide a custom message to display with the deprecation. + * + * @return string + */ + public function getDeprecationMessage() { + return 'Support for scanning JavaScript files will be removed from PHP_CodeSniffer, so this sniff is no longer viable.'; + } } diff --git a/WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php index 9188752b..b67027e3 100644 --- a/WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php @@ -7,6 +7,7 @@ namespace WordPressVIPMinimum\Sniffs\JS; +use PHP_CodeSniffer\Sniffs\DeprecatedSniff; use PHP_CodeSniffer\Util\Tokens; use WordPressVIPMinimum\Sniffs\Sniff; @@ -15,7 +16,7 @@ * * Flags functions which are executing HTML passed to it. */ -class HTMLExecutingFunctionsSniff extends Sniff { +class HTMLExecutingFunctionsSniff extends Sniff implements DeprecatedSniff { /** * List of HTML executing functions. @@ -128,4 +129,31 @@ public function process_token( $stackPtr ) { } } } + + /** + * Provide the version number in which the sniff was deprecated. + * + * @return string + */ + public function getDeprecationVersion() { + return 'VIP-Coding-Standard v3.1.0'; + } + + /** + * Provide the version number in which the sniff will be removed. + * + * @return string + */ + public function getRemovalVersion() { + return 'VIP-Coding-Standard v4.0.0'; + } + + /** + * Provide a custom message to display with the deprecation. + * + * @return string + */ + public function getDeprecationMessage() { + return 'Support for scanning JavaScript files will be removed from PHP_CodeSniffer, so this sniff is no longer viable.'; + } } diff --git a/WordPressVIPMinimum/Sniffs/JS/InnerHTMLSniff.php b/WordPressVIPMinimum/Sniffs/JS/InnerHTMLSniff.php index 8b235f77..7032fb89 100644 --- a/WordPressVIPMinimum/Sniffs/JS/InnerHTMLSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/InnerHTMLSniff.php @@ -7,6 +7,7 @@ namespace WordPressVIPMinimum\Sniffs\JS; +use PHP_CodeSniffer\Sniffs\DeprecatedSniff; use PHP_CodeSniffer\Util\Tokens; use WordPressVIPMinimum\Sniffs\Sniff; @@ -15,7 +16,7 @@ * * Looks for instances of .innerHMTL. */ -class InnerHTMLSniff extends Sniff { +class InnerHTMLSniff extends Sniff implements DeprecatedSniff { /** * A list of tokenizers this sniff supports. @@ -81,4 +82,31 @@ public function process_token( $stackPtr ) { $this->phpcsFile->addWarning( $message, $stackPtr, 'Found', $data ); } } + + /** + * Provide the version number in which the sniff was deprecated. + * + * @return string + */ + public function getDeprecationVersion() { + return 'VIP-Coding-Standard v3.1.0'; + } + + /** + * Provide the version number in which the sniff will be removed. + * + * @return string + */ + public function getRemovalVersion() { + return 'VIP-Coding-Standard v4.0.0'; + } + + /** + * Provide a custom message to display with the deprecation. + * + * @return string + */ + public function getDeprecationMessage() { + return 'Support for scanning JavaScript files will be removed from PHP_CodeSniffer, so this sniff is no longer viable.'; + } } diff --git a/WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php b/WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php index 56902d07..9a4f9aa5 100644 --- a/WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php @@ -7,6 +7,7 @@ namespace WordPressVIPMinimum\Sniffs\JS; +use PHP_CodeSniffer\Sniffs\DeprecatedSniff; use PHP_CodeSniffer\Util\Tokens; use WordPressVIPMinimum\Sniffs\Sniff; @@ -15,7 +16,7 @@ * * Looks for HTML string concatenation. */ -class StringConcatSniff extends Sniff { +class StringConcatSniff extends Sniff implements DeprecatedSniff { /** * A list of tokenizers this sniff supports. @@ -72,4 +73,31 @@ private function addFoundError( $stackPtr, array $data ) { $message = 'HTML string concatenation detected, this is a security risk, use DOM node construction or a templating language instead: %s.'; $this->phpcsFile->addError( $message, $stackPtr, 'Found', $data ); } + + /** + * Provide the version number in which the sniff was deprecated. + * + * @return string + */ + public function getDeprecationVersion() { + return 'VIP-Coding-Standard v3.1.0'; + } + + /** + * Provide the version number in which the sniff will be removed. + * + * @return string + */ + public function getRemovalVersion() { + return 'VIP-Coding-Standard v4.0.0'; + } + + /** + * Provide a custom message to display with the deprecation. + * + * @return string + */ + public function getDeprecationMessage() { + return 'Support for scanning JavaScript files will be removed from PHP_CodeSniffer, so this sniff is no longer viable.'; + } } diff --git a/WordPressVIPMinimum/Sniffs/JS/StrippingTagsSniff.php b/WordPressVIPMinimum/Sniffs/JS/StrippingTagsSniff.php index 2b38105e..0c737d6b 100644 --- a/WordPressVIPMinimum/Sniffs/JS/StrippingTagsSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/StrippingTagsSniff.php @@ -7,6 +7,7 @@ namespace WordPressVIPMinimum\Sniffs\JS; +use PHP_CodeSniffer\Sniffs\DeprecatedSniff; use PHP_CodeSniffer\Util\Tokens; use WordPressVIPMinimum\Sniffs\Sniff; @@ -15,7 +16,7 @@ * * Looks for incorrect way of stripping tags. */ -class StrippingTagsSniff extends Sniff { +class StrippingTagsSniff extends Sniff implements DeprecatedSniff { /** * A list of tokenizers this sniff supports. @@ -70,4 +71,31 @@ public function process_token( $stackPtr ) { $this->phpcsFile->addError( $message, $stackPtr, 'VulnerableTagStripping' ); } } + + /** + * Provide the version number in which the sniff was deprecated. + * + * @return string + */ + public function getDeprecationVersion() { + return 'VIP-Coding-Standard v3.1.0'; + } + + /** + * Provide the version number in which the sniff will be removed. + * + * @return string + */ + public function getRemovalVersion() { + return 'VIP-Coding-Standard v4.0.0'; + } + + /** + * Provide a custom message to display with the deprecation. + * + * @return string + */ + public function getDeprecationMessage() { + return 'Support for scanning JavaScript files will be removed from PHP_CodeSniffer, so this sniff is no longer viable.'; + } } diff --git a/WordPressVIPMinimum/Sniffs/JS/WindowSniff.php b/WordPressVIPMinimum/Sniffs/JS/WindowSniff.php index dc69d0b6..2505acb4 100644 --- a/WordPressVIPMinimum/Sniffs/JS/WindowSniff.php +++ b/WordPressVIPMinimum/Sniffs/JS/WindowSniff.php @@ -7,6 +7,7 @@ namespace WordPressVIPMinimum\Sniffs\JS; +use PHP_CodeSniffer\Sniffs\DeprecatedSniff; use PHP_CodeSniffer\Util\Tokens; use WordPressVIPMinimum\Sniffs\Sniff; @@ -15,7 +16,7 @@ * * Looks for instances of window properties that should be flagged. */ -class WindowSniff extends Sniff { +class WindowSniff extends Sniff implements DeprecatedSniff { /** * A list of tokenizers this sniff supports. @@ -125,4 +126,31 @@ public function process_token( $stackPtr ) { $message = 'Data from JS global "%s" may contain user-supplied values and should be sanitized before output to prevent XSS.'; $this->phpcsFile->addError( $message, $stackPtr, $nextNextToken, $data ); } + + /** + * Provide the version number in which the sniff was deprecated. + * + * @return string + */ + public function getDeprecationVersion() { + return 'VIP-Coding-Standard v3.1.0'; + } + + /** + * Provide the version number in which the sniff will be removed. + * + * @return string + */ + public function getRemovalVersion() { + return 'VIP-Coding-Standard v4.0.0'; + } + + /** + * Provide a custom message to display with the deprecation. + * + * @return string + */ + public function getDeprecationMessage() { + return 'Support for scanning JavaScript files will be removed from PHP_CodeSniffer, so this sniff is no longer viable.'; + } } diff --git a/WordPressVIPMinimum/ruleset.xml b/WordPressVIPMinimum/ruleset.xml index 9441dc62..c0d97376 100644 --- a/WordPressVIPMinimum/ruleset.xml +++ b/WordPressVIPMinimum/ruleset.xml @@ -9,7 +9,16 @@ descriptive error message during the loading of the ruleset instead of a fatal "class not found" error once the sniffs start running. --> - + + + + From 124863b49e132b22121ed1318d3e233dec8888e3 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Jul 2025 00:35:40 +0200 Subject: [PATCH 021/165] Fix dangerous use of contents of constants The value of tokens constants is not guaranteed, so should never be compared against directly. Either use the token 'code' and compare against the constant (preferred). Or use the token 'type' and compare against the constant name. But **_never_** compare against the value of the constant. Comparing against the 'type' should generally only be needed for tokens which may not exist in older PHPCS/PHP versions. --- .../Sniffs/Hooks/AlwaysReturnInFilterSniff.php | 4 ++-- WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php index b9e966b4..56bae690 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php @@ -76,7 +76,7 @@ public function process_token( $stackPtr ) { return; } - if ( $this->tokens[ $callbackPtr ]['code'] === 'PHPCS_T_CLOSURE' ) { + if ( $this->tokens[ $callbackPtr ]['code'] === T_CLOSURE ) { $this->processFunctionBody( $callbackPtr ); } elseif ( $this->tokens[ $callbackPtr ]['code'] === T_ARRAY || $this->tokens[ $callbackPtr ]['code'] === T_OPEN_SHORT_ARRAY @@ -262,7 +262,7 @@ private function isInsideIfConditonal( $stackPtr ) { } // Similar case may be a conditional closure. - if ( end( $this->tokens[ $stackPtr ]['conditions'] ) === 'PHPCS_T_CLOSURE' ) { + if ( end( $this->tokens[ $stackPtr ]['conditions'] ) === T_CLOSURE ) { return false; } diff --git a/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php index 07e5ce8d..934a6add 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php @@ -76,7 +76,7 @@ public function process_token( $stackPtr ) { return; } - if ( $this->tokens[ $callbackPtr ]['code'] === 'PHPCS_T_CLOSURE' ) { + if ( $this->tokens[ $callbackPtr ]['code'] === T_CLOSURE ) { $this->processClosure( $callbackPtr ); } elseif ( $this->tokens[ $callbackPtr ]['code'] === T_ARRAY || $this->tokens[ $callbackPtr ]['code'] === T_OPEN_SHORT_ARRAY From be6a93214ea3cd252a85e9a822f239eb10f02502 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Jul 2025 00:39:44 +0200 Subject: [PATCH 022/165] Fix unnecessary comparison against token type --- WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php index 934a6add..95d97b80 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php @@ -388,7 +388,7 @@ private function isWPQueryMethodCall( $stackPtr, $method = null ) { true ); - if ( ! $next || $this->tokens[ $next ]['type'] !== 'T_OBJECT_OPERATOR' ) { + if ( ! $next || $this->tokens[ $next ]['code'] !== T_OBJECT_OPERATOR ) { return false; } From b451c8913c562abb07b15e00879cc1f69fccc555 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 16 Jul 2025 15:59:56 +0200 Subject: [PATCH 023/165] Security/Twig: sniff for all text string tokens As things were, double quoted strings with interpolation and PHP 5.3+ nowdocs were not examined. Includes ensuring that all tokens which should be examined have tests associated with them. --- .../Sniffs/Security/TwigSniff.php | 7 +-- .../Tests/Security/TwigUnitTest.inc | 50 ++++++++++++++++++- .../Tests/Security/TwigUnitTest.php | 8 +++ 3 files changed, 59 insertions(+), 6 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php b/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php index e1a5abed..9a2e4c97 100644 --- a/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php @@ -8,6 +8,7 @@ namespace WordPressVIPMinimum\Sniffs\Security; +use PHP_CodeSniffer\Util\Tokens; use WordPressVIPMinimum\Sniffs\Sniff; /** @@ -28,11 +29,7 @@ class TwigSniff extends Sniff { * @return array */ public function register() { - return [ - T_CONSTANT_ENCAPSED_STRING, - T_INLINE_HTML, - T_HEREDOC, - ]; + return Tokens::$textStringTokens; } /** diff --git a/WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc b/WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc index d0423e0c..a230d8d2 100644 --- a/WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc @@ -9,4 +9,52 @@ {% autoescape %} {{ safe_value|raw }} {% endautoescape %} - \ No newline at end of file + + + +{% autoescape false %} + Everything will be outputted as is in this block +{% endautoescape %} + +{% autoescape %} + {{ safe_value|raw }} +{% endautoescape %} +'; + +echo " +"; + +echo <<<'EOD' + +EOD; + +echo << +{% autoescape false %} + Everything will be $outputted as is in this block +{% endautoescape %} +EOD; +echo <<<"EOD" +{% autoescape %} + {{ safe_value|raw }} +{% endautoescape %} + +EOD; diff --git a/WordPressVIPMinimum/Tests/Security/TwigUnitTest.php b/WordPressVIPMinimum/Tests/Security/TwigUnitTest.php index 0c390843..0cea1379 100644 --- a/WordPressVIPMinimum/Tests/Security/TwigUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/TwigUnitTest.php @@ -34,6 +34,14 @@ public function getWarningList() { return [ 5 => 1, 10 => 1, + 17 => 1, + 22 => 1, + 27 => 1, + 32 => 1, + 39 => 1, + 44 => 1, + 51 => 1, + 57 => 1, ]; } } From b10aaeba84a1f7153822bb1ed3303e3d42cc5965 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 16 Jul 2025 16:15:40 +0200 Subject: [PATCH 024/165] Security/Twig: bug fix - prevent false positives on interpolated expressions Double quoted strings and heredocs may contain interpolated expressions, which could lead to false positives. Fixed now. Includes unit tests proving the bug. --- WordPressVIPMinimum/Sniffs/Security/TwigSniff.php | 12 ++++++++++-- WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php b/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php index 9a2e4c97..90435a7e 100644 --- a/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php @@ -9,6 +9,7 @@ namespace WordPressVIPMinimum\Sniffs\Security; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\TextStrings; use WordPressVIPMinimum\Sniffs\Sniff; /** @@ -40,14 +41,21 @@ public function register() { * @return void */ public function process_token( $stackPtr ) { + // Strip any potentially interpolated expressions. + $only_text = $this->tokens[ $stackPtr ]['content']; + if ( $this->tokens[ $stackPtr ]['code'] === T_DOUBLE_QUOTED_STRING + || $this->tokens[ $stackPtr ]['code'] === T_HEREDOC + ) { + $only_text = TextStrings::stripEmbeds( $only_text ); + } - if ( preg_match( '/autoescape\s+false/', $this->tokens[ $stackPtr ]['content'] ) === 1 ) { + if ( preg_match( '/autoescape\s+false/', $only_text ) === 1 ) { // Twig autoescape disabled. $message = 'Found Twig autoescape disabling notation.'; $this->phpcsFile->addWarning( $message, $stackPtr, 'AutoescapeFalse' ); } - if ( preg_match( '/\|\s*raw/', $this->tokens[ $stackPtr ]['content'] ) === 1 ) { + if ( preg_match( '/\|\s*raw/', $only_text ) === 1 ) { // Twig default unescape filter. $message = 'Found Twig default unescape filter: "|raw".'; $this->phpcsFile->addWarning( $message, $stackPtr, 'RawFound' ); diff --git a/WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc b/WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc index a230d8d2..0e766537 100644 --- a/WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc @@ -25,7 +25,7 @@ echo ' +HERE; + +echo <<<'NOW' + +NOW; diff --git a/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.php b/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.php index 2c441539..4bf2980d 100644 --- a/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.php @@ -38,6 +38,13 @@ public function getWarningList() { 7 => 1, 8 => 1, 18 => 1, + 26 => 1, + 30 => 1, + 31 => 1, + 34 => 1, + 40 => 1, + 41 => 1, + 42 => 1, ]; } } From daf17f0b88c74c079a7bf33391cc83b28352dcca Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 16 Jul 2025 16:44:37 +0200 Subject: [PATCH 026/165] Security/Mustache: improve tests Make sure some tests contain more complex PHP interpolated expressions to ensure this doesn't lead to false positives. Note: as I haven't been able to come up with valid code samples which would confuse the Mustache syntax with PHP interpolated expressions, I have not implemented the use of the `TextStrings::stripEmbeds()` method. --- WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc b/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc index a000822c..9b3004fe 100644 --- a/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc @@ -23,11 +23,11 @@ echo '{{&data}}'; // NOK: data.
{{{data}}}
"; // NOK: data. +echo "{{{data}}}"; // NOK: data. echo << -{{{data}}} +{{{data}}} {{&data}} HERE; echo <<<"HERE" From 95305d374735c0eaf3fcc4b25b7434c0df81ed0f Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 16 Jul 2025 18:48:30 +0200 Subject: [PATCH 027/165] Security/Mustache: bug fix - potential false positives for delimiter change The sniff didn't have enough guard code to safeguard that the test cases I've added in this commit would be handled correctly. Fixed now. --- .../Sniffs/Security/MustacheSniff.php | 17 ++++++++++++----- .../Tests/Security/MustacheUnitTest.inc | 7 +++++++ .../Tests/Security/MustacheUnitTest.php | 1 + 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php b/WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php index 918d7fbe..d2f34241 100644 --- a/WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php @@ -56,12 +56,19 @@ public function process_token( $stackPtr ) { $this->phpcsFile->addWarning( $message, $stackPtr, 'VariableNotation' ); } - if ( strpos( $this->tokens[ $stackPtr ]['content'], '{{=' ) !== false ) { + $start_delimiter_change = strpos( $this->tokens[ $stackPtr ]['content'], '{{=' ); + if ( $start_delimiter_change !== false ) { // Mustache delimiter change. - $new_delimiter = trim( str_replace( [ '{{=', '=}}' ], '', substr( $this->tokens[ $stackPtr ]['content'], 0, strpos( $this->tokens[ $stackPtr ]['content'], '=}}' ) + 3 ) ) ); - $message = 'Found Mustache delimiter change notation. New delimiter is: %s.'; - $data = [ $new_delimiter ]; - $this->phpcsFile->addWarning( $message, $stackPtr, 'DelimiterChange', $data ); + $end_delimiter_change = strpos( $this->tokens[ $stackPtr ]['content'], '=}}' ); + if ( $end_delimiter_change !== false && $start_delimiter_change < $end_delimiter_change ) { + $start_new_delimiter = $start_delimiter_change + 3; + $new_delimiter_length = $end_delimiter_change - ( $start_delimiter_change + 3 ); + $new_delimiter = substr( $this->tokens[ $stackPtr ]['content'], $start_new_delimiter, $new_delimiter_length ); + + $message = 'Found Mustache delimiter change notation. New delimiter is: %s.'; + $data = [ $new_delimiter ]; + $this->phpcsFile->addWarning( $message, $stackPtr, 'DelimiterChange', $data ); + } } if ( strpos( $this->tokens[ $stackPtr ]['content'], 'SafeString' ) !== false ) { diff --git a/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc b/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc index 9b3004fe..b2c50437 100644 --- a/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc @@ -42,3 +42,10 @@ echo <<<'NOW' {{=<% %>=}} NOW; + +// Don't throw false positives on incorrect/incomplete mustache delimiter change. +$m = '{{=<%'; // Missing end of delimiter change. +$m = '%>=}} {{=<%'; // Incorrect order, not a delimiter change. + +// Correctly recognize mid-line delimiter change. +$m = '{{default_tags}} {{=<% %>=}} <% erb_style_tags %> <%={{ }}=%> {{ default_tags_again }}'; // NOK: delimiter change. diff --git a/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.php b/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.php index 4bf2980d..1e192a74 100644 --- a/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.php @@ -45,6 +45,7 @@ public function getWarningList() { 40 => 1, 41 => 1, 42 => 1, + 51 => 1, ]; } } From 0aebe1fbc6970676ec0aef570130e26c6a0e0e09 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 16 Jul 2025 16:40:00 +0200 Subject: [PATCH 028/165] Security/Mustache: prevent false positive on partial text Includes test. --- WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php | 2 +- WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php b/WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php index d2f34241..e27e0150 100644 --- a/WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php @@ -71,7 +71,7 @@ public function process_token( $stackPtr ) { } } - if ( strpos( $this->tokens[ $stackPtr ]['content'], 'SafeString' ) !== false ) { + if ( strpos( $this->tokens[ $stackPtr ]['content'], '.SafeString' ) !== false ) { // Handlebars.js Handlebars.SafeString does not get escaped. $message = 'Found Handlebars.SafeString call which does not get escaped.'; $this->phpcsFile->addWarning( $message, $stackPtr, 'SafeString' ); diff --git a/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc b/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc index b2c50437..67d808d2 100644 --- a/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/MustacheUnitTest.inc @@ -49,3 +49,6 @@ $m = '%>=}} {{=<%'; // Incorrect order, not a delimiter change. // Correctly recognize mid-line delimiter change. $m = '{{default_tags}} {{=<% %>=}} <% erb_style_tags %> <%={{ }}=%> {{ default_tags_again }}'; // NOK: delimiter change. + +// Prevent false positives on SafeString being a partial name. +echo 'MySafeString'; From f79a41e76b3ffe388e35d91e80f6a8409019b296 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Jul 2025 16:59:58 +0200 Subject: [PATCH 029/165] LICENSE: update information about included projects --- LICENSE.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 5517f00d..79a2790e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -63,5 +63,8 @@ Included Files This project includes: -- [WordPress-Coding-Standards](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards), which is Copyright © 2009 John Godley and contributors. Released under the MIT license https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/blob/develop/LICENSE -- [PHP_CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer), Copyright © 2012, Squiz Pty Ltd (ABN 77 084 670 600). Released under the following license: https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt +- [PHP_CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer), Copyright © 2012, Squiz Pty Ltd (ABN 77 084 670 600); Copyright (c) 2023, PHPCSStandards and contributors. Released under the [BSD 3-Clause "New" or "Revised" License](https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt) +- [PHPCSUtils](https://github.com/PHPCSStandards/PHPCSUtils), Copyright © 2019, PHPCSUtils Contributors. Released under the [LGPL v3.0 License](https://github.com/PHPCSStandards/PHPCSUtils/blob/develop/LICENSE) +- [PHPCSExtra](https://github.com/PHPCSStandards/PHPCSExtra), Copyright © 2019, PHPCSExtra Contributors. Released under the [LGPL v3.0 License](https://github.com/PHPCSStandards/PHPCSExtra/blob/develop/LICENSE) +- [WordPress-Coding-Standards](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards), which is Copyright © 2009 John Godley and contributors. Released under the [MIT license](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/blob/develop/LICENSE). +- [VariableAnalysis](https://github.com/sirbrillig/phpcs-variable-analysis). Released under the [BSD license](https://github.com/sirbrillig/phpcs-variable-analysis/blob/2.x/LICENSE). From 16f28288887a0c832fbe85682c9f9fe9ee5941d3 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Jul 2025 17:04:53 +0200 Subject: [PATCH 030/165] Docs: standardize the sniff file docblocks Ensuring that all (sniff) files which are distributed contain a link back to the repo and license information. --- .../Sniffs/Classes/DeclarationCompatibilitySniff.php | 2 ++ .../Sniffs/Classes/RestrictedExtendClassesSniff.php | 2 ++ WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php | 3 ++- .../Sniffs/Constants/RestrictedConstantsSniff.php | 3 ++- WordPressVIPMinimum/Sniffs/Files/IncludingFileSniff.php | 4 +++- WordPressVIPMinimum/Sniffs/Files/IncludingNonPHPFileSniff.php | 4 +++- .../Sniffs/Functions/CheckReturnValueSniff.php | 2 ++ WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php | 2 ++ .../Sniffs/Functions/RestrictedFunctionsSniff.php | 2 ++ WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php | 2 ++ .../Sniffs/Hooks/AlwaysReturnInFilterSniff.php | 2 ++ WordPressVIPMinimum/Sniffs/Hooks/PreGetPostsSniff.php | 2 ++ WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php | 2 ++ .../Sniffs/JS/DangerouslySetInnerHTMLSniff.php | 4 +++- WordPressVIPMinimum/Sniffs/JS/HTMLExecutingFunctionsSniff.php | 4 +++- WordPressVIPMinimum/Sniffs/JS/InnerHTMLSniff.php | 4 +++- WordPressVIPMinimum/Sniffs/JS/StringConcatSniff.php | 4 +++- WordPressVIPMinimum/Sniffs/JS/StrippingTagsSniff.php | 4 +++- WordPressVIPMinimum/Sniffs/JS/WindowSniff.php | 4 +++- .../Sniffs/Performance/CacheValueOverrideSniff.php | 2 ++ .../Sniffs/Performance/FetchingRemoteDataSniff.php | 1 + .../Sniffs/Performance/LowExpiryCacheTimeSniff.php | 2 ++ WordPressVIPMinimum/Sniffs/Performance/RegexpCompareSniff.php | 2 ++ .../Sniffs/Performance/RemoteRequestTimeoutSniff.php | 2 ++ .../Sniffs/Performance/TaxonomyMetaInOptionsSniff.php | 1 + WordPressVIPMinimum/Sniffs/Performance/WPQueryParamsSniff.php | 1 + .../Sniffs/Security/EscapingVoidReturnFunctionsSniff.php | 1 + .../Sniffs/Security/ExitAfterRedirectSniff.php | 1 + WordPressVIPMinimum/Sniffs/Security/MustacheSniff.php | 1 + .../Sniffs/Security/PHPFilterFunctionsSniff.php | 2 ++ .../Sniffs/Security/ProperEscapingFunctionSniff.php | 1 + WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php | 1 + WordPressVIPMinimum/Sniffs/Security/TwigSniff.php | 1 + WordPressVIPMinimum/Sniffs/Security/UnderscorejsSniff.php | 1 + WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php | 3 ++- 35 files changed, 68 insertions(+), 11 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index 57419759..9a930319 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -3,6 +3,8 @@ * WordPressVIPMinimum Coding Standard. * * @package VIPCS\WordPressVIPMinimum + * @link https://github.com/Automattic/VIP-Coding-Standards + * @license https://opensource.org/license/gpl-2-0 GPL-2.0 */ namespace WordPressVIPMinimum\Sniffs\Classes; diff --git a/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php b/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php index 94eaf5a2..bb99d4fa 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php @@ -3,6 +3,8 @@ * WordPressVIPMinimum Coding Standard. * * @package VIPCS\WordPressVIPMinimum + * @link https://github.com/Automattic/VIP-Coding-Standards + * @license https://opensource.org/license/gpl-2-0 GPL-2.0 */ namespace WordPressVIPMinimum\Sniffs\Classes; diff --git a/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php b/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php index 75db4dfe..fa44b8f4 100644 --- a/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php +++ b/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php @@ -1,9 +1,10 @@ Date: Thu, 17 Jul 2025 16:59:41 +0200 Subject: [PATCH 031/165] Docs/Imported sniffs: add `@license` tag for the GPL v2 license These sniffs already contained the standardized file docblock, but with a `@license` tag for the MIT license as these sniffs were originally created in the WordPressCS project, which is licensed under MIT. This commit adds an extra `@license` tag for the GPL v2 license under which VIPCS is licensed. --- WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php | 1 + WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php | 1 + WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php | 1 + WordPressVIPMinimum/Sniffs/Sniff.php | 1 + .../Sniffs/UserExperience/AdminBarRemovalSniff.php | 1 + .../Sniffs/Variables/RestrictedVariablesSniff.php | 1 + 6 files changed, 6 insertions(+) diff --git a/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php b/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php index 437c33c0..a2b149ab 100644 --- a/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php @@ -4,6 +4,7 @@ * * @package VIPCS\WordPressVIPMinimum * @link https://github.com/Automattic/VIP-Coding-Standards + * @license https://opensource.org/license/gpl-2-0 GPL-2.0 * @license https://opensource.org/licenses/MIT MIT */ diff --git a/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php b/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php index 4aca9ddd..20250dd6 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php @@ -4,6 +4,7 @@ * * @package VIPCS\WordPressVIPMinimum * @link https://github.com/Automattic/VIP-Coding-Standards + * @license https://opensource.org/license/gpl-2-0 GPL-2.0 * @license https://opensource.org/licenses/MIT MIT */ diff --git a/WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php b/WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php index a9531d71..42b3e5a0 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php @@ -4,6 +4,7 @@ * * @package VIPCS\WordPressVIPMinimum * @link https://github.com/Automattic/VIP-Coding-Standards + * @license https://opensource.org/license/gpl-2-0 GPL-2.0 * @license https://opensource.org/licenses/MIT MIT */ diff --git a/WordPressVIPMinimum/Sniffs/Sniff.php b/WordPressVIPMinimum/Sniffs/Sniff.php index 8a53228f..66a987d0 100644 --- a/WordPressVIPMinimum/Sniffs/Sniff.php +++ b/WordPressVIPMinimum/Sniffs/Sniff.php @@ -4,6 +4,7 @@ * * @package VIPCS\WordPressVIPMinimum * @link https://github.com/Automattic/VIP-Coding-Standards + * @license https://opensource.org/license/gpl-2-0 GPL-2.0 * @license https://opensource.org/licenses/MIT MIT */ diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index 4d5c74ac..d40f64d8 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -4,6 +4,7 @@ * * @package VIPCS\WordPressVIPMinimum * @link https://github.com/Automattic/VIP-Coding-Standards + * @license https://opensource.org/license/gpl-2-0 GPL-2.0 * @license https://opensource.org/licenses/MIT MIT */ diff --git a/WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php b/WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php index 3e1275cb..d5b56b31 100644 --- a/WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php @@ -4,6 +4,7 @@ * * @package VIPCS\WordPressVIPMinimum * @link https://github.com/Automattic/VIP-Coding-Standards + * @license https://opensource.org/license/gpl-2-0 GPL-2.0 * @license https://opensource.org/licenses/MIT MIT */ From 6329f711031c6d0995c1dcd041992fac276519d0 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Jul 2025 16:40:31 +0200 Subject: [PATCH 032/165] Docs: remove `@since` tags which are related to WPCS Some sniffs were originally hosted in the WordPressCS project and later moved to VIPCS. In that move, the WPCS `@since` tags were not scrubbed/updated. This is misleading as the `@since` tags now refer to versions which never existed for VIPCS. Note: `@since` tags which do refer to VIPCS versions have been left untouched. --- WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php | 2 -- WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php | 2 -- .../Sniffs/UserExperience/AdminBarRemovalSniff.php | 2 -- .../Sniffs/Variables/RestrictedVariablesSniff.php | 2 -- WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.php | 2 -- WordPressVIPMinimum/Tests/Performance/OrderByRandUnitTest.php | 2 -- .../Tests/UserExperience/AdminBarRemovalUnitTest.php | 2 -- .../Tests/Variables/RestrictedVariablesUnitTest.php | 3 --- 8 files changed, 17 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php b/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php index 4aca9ddd..68684829 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php @@ -15,8 +15,6 @@ * Flag returning high or infinite posts_per_page. * * @link https://docs.wpvip.com/technical-references/code-review/#no-limit-queries - * - * @since 0.5.0 */ class NoPagingSniff extends AbstractArrayAssignmentRestrictionsSniff { diff --git a/WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php b/WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php index a9531d71..ee65aecd 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/OrderByRandSniff.php @@ -16,8 +16,6 @@ * Flag using orderby => rand. * * @link https://docs.wpvip.com/technical-references/code-review/vip-errors/#h-order-by-rand - * - * @since 0.5.0 */ class OrderByRandSniff extends AbstractArrayAssignmentRestrictionsSniff { diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index 4d5c74ac..399b203e 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -18,8 +18,6 @@ * Discourages removal of the admin bar. * * @link https://docs.wpvip.com/technical-references/code-review/vip-warnings/#h-removing-the-admin-bar - * - * @since 0.5.0 */ class AdminBarRemovalSniff extends AbstractFunctionParameterSniff { diff --git a/WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php b/WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php index 3e1275cb..3104e8b8 100644 --- a/WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Variables/RestrictedVariablesSniff.php @@ -13,8 +13,6 @@ /** * Restricts usage of some variables in VIP context. - * - * @since 0.5.0 */ class RestrictedVariablesSniff extends AbstractVariableRestrictionsSniff { diff --git a/WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.php b/WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.php index c5586e4c..514df012 100644 --- a/WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.php @@ -12,8 +12,6 @@ /** * Unit test class for the NoPaging sniff. * - * @since 0.5.0 - * * @covers \WordPressVIPMinimum\Sniffs\Performance\NoPagingSniff */ class NoPagingUnitTest extends AbstractSniffUnitTest { diff --git a/WordPressVIPMinimum/Tests/Performance/OrderByRandUnitTest.php b/WordPressVIPMinimum/Tests/Performance/OrderByRandUnitTest.php index a39fb4d8..b79bbc1c 100644 --- a/WordPressVIPMinimum/Tests/Performance/OrderByRandUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/OrderByRandUnitTest.php @@ -12,8 +12,6 @@ /** * Unit test class for the OrderByRand sniff. * - * @since 0.5.0 - * * @covers \WordPressVIPMinimum\Sniffs\Performance\OrderByRandSniff */ class OrderByRandUnitTest extends AbstractSniffUnitTest { diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php index 6fd137c6..8973a169 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php @@ -12,8 +12,6 @@ /** * Unit test class for the AdminBarRemoval sniff. * - * @since 0.5.0 - * * @covers \WordPressVIPMinimum\Sniffs\UserExperience\AdminBarRemovalSniff */ class AdminBarRemovalUnitTest extends AbstractSniffUnitTest { diff --git a/WordPressVIPMinimum/Tests/Variables/RestrictedVariablesUnitTest.php b/WordPressVIPMinimum/Tests/Variables/RestrictedVariablesUnitTest.php index 36b833b2..a5659be0 100644 --- a/WordPressVIPMinimum/Tests/Variables/RestrictedVariablesUnitTest.php +++ b/WordPressVIPMinimum/Tests/Variables/RestrictedVariablesUnitTest.php @@ -12,9 +12,6 @@ /** * Unit test class for the VIP_RestrictedVariables sniff. * - * @since 0.3.0 - * @since 0.13.0 Class name changed: this class is now namespaced. - * * @covers \WordPressVIPMinimum\Sniffs\Variables\RestrictedVariablesSniff */ class RestrictedVariablesUnitTest extends AbstractSniffUnitTest { From 9e92d4bb37e25cc65b4f4b6b802b1bdcd85ed4c0 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 03:59:45 +0200 Subject: [PATCH 033/165] PHPCS ruleset: fix PHPCompatibility exclusions The sniffs which trigger these errors were renamed in PHPCompatibility 9.0.0. Clearly, these constants have so far not been used in VIPCS ;-) Includes making the list complete for the tokens currently back-filled by PHPCS for forward-compatibility with PHPCompatibility `dev-develop`/next release. Ref: https://github.com/PHPCompatibility/PHPCompatibility/releases/tag/9.0.0 --- .phpcs.xml.dist | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index d74ead7a..4ca016ad 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -47,18 +47,29 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + +
From 8581c40044265539c6835be25ec0a2f4b1b6a9f2 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 01:36:19 +0200 Subject: [PATCH 034/165] Security/PHPFilterFunctions: improve the tests * Test against false positives for calls to methods or namespaced function calls. * Test against false positives for attribute class using the same name as the function. * Ensure function import `use` statements are not flagged. We're not interested in those. * Document that if the filter is passed in dynamically (via a variable or function call), the sniff will stay silent. * Add some more variations to the pre-existing tests: - Non-lowercase function call(s). - Fully qualified function calls. - Use PHP 7.3+ trailing comma's in a few function calls. --- .../Security/PHPFilterFunctionsUnitTest.inc | 61 ++++++++++++++----- .../Security/PHPFilterFunctionsUnitTest.php | 24 ++++---- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc index 9960f4ed..58195afe 100644 --- a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc @@ -1,29 +1,58 @@ filter_var_array($a, $b); +$this?->filter_input_array($a, $b); +MyClass::filter_var($a, $b); +echo FILTER_INPUT; +namespace\filter_var_array($a, $b); + +// Looks like a function call, but is a PHP 8.0+ class instantiation via an attribute. +#[Filter_Input('text')] +function foo() {} + +// PHP 8.1 first class callable. +// As we have no knowledge about what parameters will be passed, we shouldn't flag this. +add_filter('my_filter', filter_var(...)); + + +/* + * These should all be okay. + */ filter_var( $url, FILTER_SANITIZE_URL ); -filter_var( 'test', FILTER_SANITIZE_STRING ); -filter_var( "test", FILTER_SANITIZE_STRING ); -filter_input( INPUT_GET, 'foo', FILTER_SANITIZE_STRING ); +\filter_var( 'test', FILTER_SANITIZE_STRING ); +FILTER_INPUT( INPUT_GET, 'foo', FILTER_SANITIZE_STRING, ); filter_input( INPUT_GET, "foo" , FILTER_SANITIZE_STRING ); -filter_var_array( $array, FILTER_SANITIZE_STRING ); filter_input_array( $array, FILTER_SANITIZE_STRING ); -filter_input_array( $array,FILTER_SANITIZE_STRING ); -// Bad. +// Ignore as undetermined. +filter_var( "test", get_filter() ); +\Filter_Var_Array( $array, $filterName ); +filter_input_array( $array,$obj->get_filter() , ); + +// Incomplete function call, should be ignored by the sniff. +$incorrect_but_ok = filter_input(); + +/* + * These should all be flagged with a warning. + */ filter_input( INPUT_GET, 'foo' ); // Missing third parameter. -filter_input( INPUT_GET, 'foo', FILTER_DEFAULT ); // This filter ID does nothing. -filter_input( INPUT_GET, "foo", FILTER_UNSAFE_RAW ); // This filter ID does nothing. +\filter_input( INPUT_GET, 'foo', FILTER_DEFAULT ); // This filter ID does nothing. +filter_input( INPUT_GET, "foo", FILTER_UNSAFE_RAW ,); // This filter ID does nothing. + filter_var( $url ); // Missing second parameter. -filter_var( $url, FILTER_DEFAULT ); // This filter ID does nothing. +Filter_Var( $url, FILTER_DEFAULT ); // This filter ID does nothing. filter_var( 'https://google.com', FILTER_UNSAFE_RAW ); // This filter ID does nothing. -filter_var_array( $array ); // Missing second parameter. + +filter_var_array( $array, ); // Missing second parameter. filter_var_array( $array, FILTER_DEFAULT ); // This filter ID does nothing. filter_var_array( $array, FILTER_UNSAFE_RAW ); // This filter ID does nothing. + filter_input_array( $array ); // Missing second parameter. -filter_input_array( $array, FILTER_DEFAULT ); // This filter ID does nothing. -filter_input_array( $array, FILTER_UNSAFE_RAW ); // This filter ID does nothing. +\FILTER_INPUT_ARRAY( $array, FILTER_DEFAULT ); // This filter ID does nothing. +filter_input_array( $array, FILTER_UNSAFE_RAW, ); // This filter ID does nothing. diff --git a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php index dae998cc..201f4a8d 100644 --- a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php @@ -32,18 +32,18 @@ public function getErrorList() { */ public function getWarningList() { return [ - 18 => 1, - 19 => 1, - 20 => 1, - 21 => 1, - 22 => 1, - 23 => 1, - 24 => 1, - 25 => 1, - 26 => 1, - 27 => 1, - 28 => 1, - 29 => 1, + 44 => 1, + 45 => 1, + 46 => 1, + 48 => 1, + 49 => 1, + 50 => 1, + 52 => 1, + 53 => 1, + 54 => 1, + 56 => 1, + 57 => 1, + 58 => 1, ]; } } From dafc562563bd348d742384cef35419177ab9cced Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 02:51:12 +0200 Subject: [PATCH 035/165] Security/PHPFilterFunctions: remove redundant `strtoupper()` calls The `isset( $this->restricted_filters[ $parameters[3]['raw'] ] )` check on the array is case-sensitive, so if the filter constant name was not in upper case already, it wouldn't be matched anyhow. Also note: the case-sensitivity is correct as constants in PHP are case-sensitive (unless explicitly declared as case-insensitive, which these aren't: https://3v4l.org/vq53U#veol). Includes updating one of the pre-existing tests to document this. --- .../Sniffs/Security/PHPFilterFunctionsSniff.php | 4 ++-- .../Tests/Security/PHPFilterFunctionsUnitTest.inc | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php index bc4002ac..c4fc0678 100644 --- a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php @@ -69,7 +69,7 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p if ( isset( $parameters[3], $this->restricted_filters[ $parameters[3]['raw'] ] ) ) { $message = 'Please use an appropriate filter to sanitize, as "%s" does no filtering, see: http://php.net/manual/en/filter.filters.sanitize.php.'; - $data = [ strtoupper( $parameters[3]['raw'] ) ]; + $data = [ $parameters[3]['raw'] ]; $this->phpcsFile->addWarning( $message, $stackPtr, 'RestrictedFilter', $data ); } } else { @@ -81,7 +81,7 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p if ( isset( $parameters[2], $this->restricted_filters[ $parameters[2]['raw'] ] ) ) { $message = 'Please use an appropriate filter to sanitize, as "%s" does no filtering, see http://php.net/manual/en/filter.filters.sanitize.php.'; - $data = [ strtoupper( $parameters[2]['raw'] ) ]; + $data = [ $parameters[2]['raw'] ]; $this->phpcsFile->addWarning( $message, $stackPtr, 'RestrictedFilter', $data ); } } diff --git a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc index 58195afe..6493e791 100644 --- a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc @@ -28,7 +28,7 @@ filter_var( $url, FILTER_SANITIZE_URL ); \filter_var( 'test', FILTER_SANITIZE_STRING ); FILTER_INPUT( INPUT_GET, 'foo', FILTER_SANITIZE_STRING, ); filter_input( INPUT_GET, "foo" , FILTER_SANITIZE_STRING ); -filter_input_array( $array, FILTER_SANITIZE_STRING ); +filter_input_array( $array, filter_default ); // Constants are case-sensitive, so this is not the FILTER_DEFAULT constant. // Ignore as undetermined. filter_var( "test", get_filter() ); From e953ac727c77377f2bb0aabe1c2adf8aa8704d96 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 02:54:08 +0200 Subject: [PATCH 036/165] Security/PHPFilterFunctions: prevent some false positives The `'raw'` key in the parameter arrays returned from the `PassedParameters` class contains - as per the name - the _raw_ contents of the parameter. Since PHPCSUtils 1.0.0-alpha4, the return array also contain a `'clean'` index, which contains the contents of the parameter cleaned of comments. By switching to using that key, some false positives get prevented. Includes unit tests demonstrating the issue and safeguarding the fix. --- .../Sniffs/Security/PHPFilterFunctionsSniff.php | 8 ++++---- .../Tests/Security/PHPFilterFunctionsUnitTest.inc | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php index c4fc0678..0ed98497 100644 --- a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php @@ -67,9 +67,9 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $this->phpcsFile->addWarning( $message, $stackPtr, 'MissingThirdParameter', $data ); } - if ( isset( $parameters[3], $this->restricted_filters[ $parameters[3]['raw'] ] ) ) { + if ( isset( $parameters[3], $this->restricted_filters[ $parameters[3]['clean'] ] ) ) { $message = 'Please use an appropriate filter to sanitize, as "%s" does no filtering, see: http://php.net/manual/en/filter.filters.sanitize.php.'; - $data = [ $parameters[3]['raw'] ]; + $data = [ $parameters[3]['clean'] ]; $this->phpcsFile->addWarning( $message, $stackPtr, 'RestrictedFilter', $data ); } } else { @@ -79,9 +79,9 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $this->phpcsFile->addWarning( $message, $stackPtr, 'MissingSecondParameter', $data ); } - if ( isset( $parameters[2], $this->restricted_filters[ $parameters[2]['raw'] ] ) ) { + if ( isset( $parameters[2], $this->restricted_filters[ $parameters[2]['clean'] ] ) ) { $message = 'Please use an appropriate filter to sanitize, as "%s" does no filtering, see http://php.net/manual/en/filter.filters.sanitize.php.'; - $data = [ $parameters[2]['raw'] ]; + $data = [ $parameters[2]['clean'] ]; $this->phpcsFile->addWarning( $message, $stackPtr, 'RestrictedFilter', $data ); } } diff --git a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc index 6493e791..9b422978 100644 --- a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc @@ -43,7 +43,7 @@ $incorrect_but_ok = filter_input(); */ filter_input( INPUT_GET, 'foo' ); // Missing third parameter. \filter_input( INPUT_GET, 'foo', FILTER_DEFAULT ); // This filter ID does nothing. -filter_input( INPUT_GET, "foo", FILTER_UNSAFE_RAW ,); // This filter ID does nothing. +filter_input( INPUT_GET, "foo", FILTER_UNSAFE_RAW /* comment */ ,); // This filter ID does nothing. filter_var( $url ); // Missing second parameter. Filter_Var( $url, FILTER_DEFAULT ); // This filter ID does nothing. From e10dd27c43357947222c055cfaa99ecb4ca5ed24 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 03:12:03 +0200 Subject: [PATCH 037/165] Security/PHPFilterFunctions: add support for PHP 8.0+ named parameters 1. Changed the `$target_functions` property to contain information about the target parameter name and position. 2. Adjusted the logic in the sniff to allow for named parameters using the new PHPCSUtils 1.0.0-alpha4 `PassedParameters::getParameterFromStack()` method. 3. The parameter names used are in line with the name as per the PHP 8.0 release. PHP itself renamed a lot of parameters in PHP 8.0. As named parameters did not exist before PHP 8.0, the parameter name as per PHP 8.0 (or above) is the only relevant name. Also see: https://github.com/php/doc-en/pull/2044 4. Updated the error messages to use the parameter name instead of its position. As a lot of the logic is now independent of which function is called, this commit also reduces code duplication in the sniff by some logic changes. Includes additional unit tests. Note: in the context of named parameters, it would be advisable to rename the `MissingSecondParameter` and `MissingThirdParameter` error codes to a dynamic error code using the parameter name instead, but as that would be a BC-break, this will need to wait for the next major (if deemed worth making the change). --- .../Security/PHPFilterFunctionsSniff.php | 65 +++++++++++-------- .../Security/PHPFilterFunctionsUnitTest.inc | 25 +++++-- .../Security/PHPFilterFunctionsUnitTest.php | 5 ++ 3 files changed, 64 insertions(+), 31 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php index 0ed98497..b1a5b662 100644 --- a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php @@ -9,6 +9,7 @@ namespace WordPressVIPMinimum\Sniffs\Security; +use PHPCSUtils\Utils\PassedParameters; use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** @@ -26,15 +27,27 @@ class PHPFilterFunctionsSniff extends AbstractFunctionParameterSniff { protected $group_name = 'php_filter_functions'; /** - * Functions this sniff is looking for. + * Functions this sniff is looking for and information on which parameter to check for in those function calls. * - * @var array Key is the function name, value irrelevant. + * @var array Key is the function name. */ protected $target_functions = [ - 'filter_var' => true, - 'filter_input' => true, - 'filter_var_array' => true, - 'filter_input_array' => true, + 'filter_var' => [ + 'param_position' => 2, + 'param_name' => 'filter', + ], + 'filter_input' => [ + 'param_position' => 3, + 'param_name' => 'filter', + ], + 'filter_var_array' => [ + 'param_position' => 2, + 'param_name' => 'options', + ], + 'filter_input_array' => [ + 'param_position' => 2, + 'param_name' => 'options', + ], ]; /** @@ -60,30 +73,28 @@ class PHPFilterFunctionsSniff extends AbstractFunctionParameterSniff { * normal file processing. */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - if ( $matched_content === 'filter_input' ) { - if ( count( $parameters ) === 2 ) { - $message = 'Missing third parameter for "%s".'; - $data = [ $matched_content ]; - $this->phpcsFile->addWarning( $message, $stackPtr, 'MissingThirdParameter', $data ); - } + $param_position = $this->target_functions[ $matched_content ]['param_position']; + $param_name = $this->target_functions[ $matched_content ]['param_name']; - if ( isset( $parameters[3], $this->restricted_filters[ $parameters[3]['clean'] ] ) ) { - $message = 'Please use an appropriate filter to sanitize, as "%s" does no filtering, see: http://php.net/manual/en/filter.filters.sanitize.php.'; - $data = [ $parameters[3]['clean'] ]; - $this->phpcsFile->addWarning( $message, $stackPtr, 'RestrictedFilter', $data ); - } - } else { - if ( count( $parameters ) === 1 ) { - $message = 'Missing second parameter for "%s".'; - $data = [ $matched_content ]; - $this->phpcsFile->addWarning( $message, $stackPtr, 'MissingSecondParameter', $data ); - } + $target_param = PassedParameters::getParameterFromStack( $parameters, $param_position, $param_name ); + if ( $target_param === false ) { + $message = 'Missing $%s parameter for "%s()".'; + $data = [ $param_name, $matched_content ]; - if ( isset( $parameters[2], $this->restricted_filters[ $parameters[2]['clean'] ] ) ) { - $message = 'Please use an appropriate filter to sanitize, as "%s" does no filtering, see http://php.net/manual/en/filter.filters.sanitize.php.'; - $data = [ $parameters[2]['clean'] ]; - $this->phpcsFile->addWarning( $message, $stackPtr, 'RestrictedFilter', $data ); + // Error codes should probably be made more descriptive, but that would be a BC-break. + $error_code = 'MissingSecondParameter'; + if ( $matched_content === 'filter_input' ) { + $error_code = 'MissingThirdParameter'; } + + $this->phpcsFile->addWarning( $message, $stackPtr, $error_code, $data ); + return; + } + + if ( isset( $this->restricted_filters[ $target_param['clean'] ] ) ) { + $message = 'Please use an appropriate filter to sanitize, as "%s" does no filtering, see: http://php.net/manual/en/filter.filters.sanitize.php.'; + $data = [ $target_param['clean'] ]; + $this->phpcsFile->addWarning( $message, $stackPtr, 'RestrictedFilter', $data ); } } } diff --git a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc index 9b422978..004ceaf2 100644 --- a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc @@ -41,18 +41,35 @@ $incorrect_but_ok = filter_input(); /* * These should all be flagged with a warning. */ -filter_input( INPUT_GET, 'foo' ); // Missing third parameter. +filter_input( INPUT_GET, 'foo' ); // Missing $filter parameter. \filter_input( INPUT_GET, 'foo', FILTER_DEFAULT ); // This filter ID does nothing. filter_input( INPUT_GET, "foo", FILTER_UNSAFE_RAW /* comment */ ,); // This filter ID does nothing. -filter_var( $url ); // Missing second parameter. +filter_var( $url ); // Missing $filter parameter. Filter_Var( $url, FILTER_DEFAULT ); // This filter ID does nothing. filter_var( 'https://google.com', FILTER_UNSAFE_RAW ); // This filter ID does nothing. -filter_var_array( $array, ); // Missing second parameter. +filter_var_array( $array, ); // Missing $options parameter. filter_var_array( $array, FILTER_DEFAULT ); // This filter ID does nothing. filter_var_array( $array, FILTER_UNSAFE_RAW ); // This filter ID does nothing. -filter_input_array( $array ); // Missing second parameter. +filter_input_array( $array ); // Missing $options parameter. \FILTER_INPUT_ARRAY( $array, FILTER_DEFAULT ); // This filter ID does nothing. filter_input_array( $array, FILTER_UNSAFE_RAW, ); // This filter ID does nothing. + +// Safeguard handling of function calls using PHP 8.0+ named parameters. +filter_input(var_name: $var_name, filter: FILTER_SANITIZE_STRING, type: FILTER_DEFAULT); // OK, invalid input value for $type, but that's not our concern. +filter_input(var_name: $var_name, filter: $filter, type: $type); // Ignore, undetermined. +filter_input( + var_name: $var_name, + filter: FILTER_DEFAULT, // This filter ID does nothing. + type: $type, +); + +filter_var(filter: FILTER_SANITIZE_URL); // OK, well not really, missing required parameter, but that's not our concern. +filter_var($value, options: FILTER_NULL_ON_FAILURE); // Missing $filter parameter. +filter_var(value: $value, filters: FILTER_SANITIZE_URL); // Typo in parameter name, report as missing $filter parameter. + +filter_var_array(options: FILTER_UNSAFE_RAW, array: $array); // This filter ID does nothing. + +filter_input_array($type, add_empty: false, options: FILTER_DEFAULT,); // This filter ID does nothing. diff --git a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php index 201f4a8d..1f4473fd 100644 --- a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php @@ -44,6 +44,11 @@ public function getWarningList() { 56 => 1, 57 => 1, 58 => 1, + 63 => 1, + 70 => 1, + 71 => 1, + 73 => 1, + 75 => 1, ]; } } From c65e55c59da28f1689027e37f21adf0c1100639e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 03:25:10 +0200 Subject: [PATCH 038/165] Security/PHPFilterFunctions: report restricted filter on the correct token Throw the warning about the use of a restricted filter on the actual filter constant, not on the function call token. This will more clearly indicate the issue when people use the PHPCS `code` view. --- .../Sniffs/Security/PHPFilterFunctionsSniff.php | 5 ++++- .../Tests/Security/PHPFilterFunctionsUnitTest.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php index b1a5b662..17945b3e 100644 --- a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php @@ -9,6 +9,7 @@ namespace WordPressVIPMinimum\Sniffs\Security; +use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Utils\PassedParameters; use WordPressCS\WordPress\AbstractFunctionParameterSniff; @@ -92,9 +93,11 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p } if ( isset( $this->restricted_filters[ $target_param['clean'] ] ) ) { + $first_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, $target_param['start'], ( $target_param['end'] + 1 ), true ); + $message = 'Please use an appropriate filter to sanitize, as "%s" does no filtering, see: http://php.net/manual/en/filter.filters.sanitize.php.'; $data = [ $target_param['clean'] ]; - $this->phpcsFile->addWarning( $message, $stackPtr, 'RestrictedFilter', $data ); + $this->phpcsFile->addWarning( $message, $first_non_empty, 'RestrictedFilter', $data ); } } } diff --git a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php index 1f4473fd..33d23ffd 100644 --- a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php @@ -44,7 +44,7 @@ public function getWarningList() { 56 => 1, 57 => 1, 58 => 1, - 63 => 1, + 65 => 1, 70 => 1, 71 => 1, 73 => 1, From 4febbf34adf236f4cc846c13a760fa9da80ac518 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 04:18:50 +0200 Subject: [PATCH 039/165] Security/PHPFilterFunctions: ignore function calls using PHP 5.6+ argument unpacking This sniff ignores the function call if it cannot reliably be determined if a warning should be thrown. So, for function calls using PHP 5.6+ argument unpacking, I've elected to also ignore these function calls. Includes tests covering the change. --- .../Security/PHPFilterFunctionsSniff.php | 22 +++++++++++++++++++ .../Security/PHPFilterFunctionsUnitTest.inc | 6 +++++ .../Security/PHPFilterFunctionsUnitTest.php | 1 + 3 files changed, 29 insertions(+) diff --git a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php index 17945b3e..a507ac13 100644 --- a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php @@ -79,6 +79,28 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $target_param = PassedParameters::getParameterFromStack( $parameters, $param_position, $param_name ); if ( $target_param === false ) { + /* + * Check for PHP 5.6+ argument unpacking. + * + * No need for extensive defensive coding, we already know this is syntactically a valid function call, + * otherwise this method would not have been reached. + */ + $tokens = $this->phpcsFile->getTokens(); + $open_parens = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + $has_ellipses = $this->phpcsFile->findNext( T_ELLIPSIS, ( $open_parens + 1 ), $tokens[ $open_parens ]['parenthesis_closer'] ); + + if ( $has_ellipses !== false ) { + $target_nesting_level = 1; + if ( isset( $tokens[ $open_parens ]['nested_parenthesis'] ) ) { + $target_nesting_level = ( count( $tokens[ $open_parens ]['nested_parenthesis'] ) + 1 ); + } + + if ( $target_nesting_level === count( $tokens[ $has_ellipses ]['nested_parenthesis'] ) ) { + // Bow out as undetermined. + return; + } + } + $message = 'Missing $%s parameter for "%s()".'; $data = [ $param_name, $matched_content ]; diff --git a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc index 004ceaf2..f9a4f5ab 100644 --- a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc @@ -73,3 +73,9 @@ filter_var(value: $value, filters: FILTER_SANITIZE_URL); // Typo in parameter na filter_var_array(options: FILTER_UNSAFE_RAW, array: $array); // This filter ID does nothing. filter_input_array($type, add_empty: false, options: FILTER_DEFAULT,); // This filter ID does nothing. + +// Ignore function calls using PHP 5.6 argument unpacking as we don't know what parameters were passed. +filter_input(INPUT_GET, ...$params); +trim(filter_input(INPUT_GET, ...$params)); +// ... but only ignore unpacking if done at the correct nesting level. +filter_input(INPUT_GET, $obj->getVarname(...$params)); // Missing $filter parameter. diff --git a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php index 33d23ffd..e708a38c 100644 --- a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php @@ -49,6 +49,7 @@ public function getWarningList() { 71 => 1, 73 => 1, 75 => 1, + 81 => 1, ]; } } From 824bb1be72349dcafa6c5ceb2b95cc6dc141e6e5 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 04:37:42 +0200 Subject: [PATCH 040/165] Security/PHPFilterFunctions: document unsupported PHP feature The `$options` parameter for `filter_var_array()` and `filter_input_array()` can take either an integer (filter constant) or an array with options, which could include an option setting the filter constant. At this time, this sniff does not handle an array with options being passed to these functions. Adding support for this will make the sniff much more complicated as PHP supports multiple array formats. Additionally, if the `$options` parameter is passed as an array, the likelyhood that the array is passed in as a variable increases exponentially, so then the next problem would be finding the variable definition and analysing that. All in all, this is a rabbit hole without end. Refs: * https://www.php.net/manual/en/function.filter-var-array.php * https://www.php.net/manual/en/function.filter-input-array.php --- .../Security/PHPFilterFunctionsSniff.php | 4 ++++ .../Security/PHPFilterFunctionsUnitTest.inc | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php index a507ac13..06b67503 100644 --- a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php @@ -16,6 +16,10 @@ /** * This sniff ensures that proper sanitization is occurring when PHP's filter_* functions are used. * + * {@internal The $options parameter for filter_var_array() and filter_input_array() can take either an + * integer (filter constant) or an array with options, which could include an option setting the filter constant. + * At this time, this sniff does not handle an array with options being passed.} + * * @since 0.4.0 */ class PHPFilterFunctionsSniff extends AbstractFunctionParameterSniff { diff --git a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc index f9a4f5ab..0159aac5 100644 --- a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.inc @@ -79,3 +79,22 @@ filter_input(INPUT_GET, ...$params); trim(filter_input(INPUT_GET, ...$params)); // ... but only ignore unpacking if done at the correct nesting level. filter_input(INPUT_GET, $obj->getVarname(...$params)); // Missing $filter parameter. + +// False negatives: $options arrays are currently not (yet) supported by this sniff. +// See: https://www.php.net/manual/en/function.filter-var-array.php and https://www.php.net/manual/en/function.filter-input-array.php +filter_var_array( + $array, + array('keyA' => FILTER_DEFAULT), // This filter ID does nothing. +); +filter_input_array( + $array, + [ + 'keyA' => [ + 'filter' => FILTER_UNSAFE_RAW, // This filter ID does nothing. + 'flags' => FILTER_FORCE_ARRAY, + ], + 'keyB' => [ + 'filter' => FILTER_SANITIZE_ENCODED, + ], + ] +); From 497fbcea3180454b25e456bbb1d307f4209caa05 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 05:20:58 +0200 Subject: [PATCH 041/165] Security/StaticStrreplace: extend AbstractFunctionParameterSniff As things were, the determination of whether or not a `T_STRING` is a call to the global PHP native `str_replace()` function was severely flawed. By switching the sniff over to be based on the WordPressCS `AbstractFunctionParameterSniff` class, this flaw is mitigated. Includes adding a slew of additional tests, some of which (line 8 - 13) are specific to the flaw being addressed in this commit. Additionally, the tests have been made more comprehensive and varied by: * Testing against false positives for calls to methods or namespaced function calls (= the issue being addressed in this PR). * Testing against false positives for attribute class using the same name as the function. * Ensure function import `use` statements are not flagged. We're not interested in those. * Safeguarding that function calls using PHP 5.6+ argument unpacking are not flagged. * Safeguarding that the function is not flagged when used as a PHP 8.1+ first class callable. * Adding more variations to the pre-existing tests: - Non-lowercase function call(s). - Fully qualified function calls. - Use PHP 7.3+ trailing comma's in a few function calls. - Use both single quoted as well as double quoted text strings. - Use other non-plain-text tokens in the passed parameters. - Multi-line function call. - Comments in function call. - `$subject` parameter passed as array. --- .../Sniffs/Security/StaticStrreplaceSniff.php | 35 +++++++----- .../Security/StaticStrreplaceUnitTest.inc | 55 +++++++++++++++++-- .../Security/StaticStrreplaceUnitTest.php | 5 +- 3 files changed, 73 insertions(+), 22 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php b/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php index 8ace2cd5..eeac8528 100644 --- a/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php @@ -10,34 +10,41 @@ namespace WordPressVIPMinimum\Sniffs\Security; use PHP_CodeSniffer\Util\Tokens; -use WordPressVIPMinimum\Sniffs\Sniff; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** * Restricts usage of str_replace with all 3 params being static. */ -class StaticStrreplaceSniff extends Sniff { +class StaticStrreplaceSniff extends AbstractFunctionParameterSniff { /** - * Returns an array of tokens this test wants to listen for. + * The group name for this group of functions. * - * @return array + * @var string */ - public function register() { - return [ T_STRING ]; - } + protected $group_name = 'str_replace'; + + /** + * Functions this sniff is looking for. + * + * @var array Key is the function name, value irrelevant. + */ + protected $target_functions = [ + 'str_replace' => true, + ]; /** - * Process this test when one of its tokens is encountered + * Process the parameters of a matched function. * - * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. * * @return void */ - public function process_token( $stackPtr ) { - - if ( $this->tokens[ $stackPtr ]['content'] !== 'str_replace' ) { - return; - } + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); diff --git a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc index d5dd8a1f..8c599c60 100644 --- a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc @@ -1,13 +1,56 @@ str_replace('foo', 'bar', 'foobar'); +$this?->str_replace('foo', 'bar', 'foobar'); +MyClass::str_replace('foo', 'bar', 'foobar'); +echo STR_REPLACE; +namespace\str_replace('foo', 'bar', 'foobar'); + +// Looks like a function call, but is a PHP 8.0+ class instantiation via an attribute. +#[Str_Replace('foo', 'bar', 'foobar')] +function foo() {} + +// Ignore PHP 5.6 argument unpacking. +str_replace('foo', 'bar', ...$params); -str_replace( 'foo', 'bar', $foobar ); // Ok. +// Ignore PHP 8.1 first class callable. +add_filter('my_filter', str_replace(...)); -str_replace( array( 'foo', 'bar' ), array( 'bar', 'foo' ), 'foobar' ); // Bad. +// Incomplete function calls, should be ignored by the sniff. +str_replace(); +str_replace('foo', 'bar'); -str_replace( array( 'foo', 'bar' ), array( 'bar', 'foo' ), $foobar ); // Ok. -str_replace( array( 'foo', 'bar' ), array( $foo, $bar ), $foobar ); // Ok. +/* + * These should all be okay. + */ +str_replace( $foo->getSearch(), 'bar', 'foobar' ); +\str_replace( 'foo', $bar, 'foobar' ); +str_replace( 'foo', 'bar', $foobar, ); +STR_REPLACE( $foo, $bar, 'foobar' ); +str_replace( 'foo', get_replacements(), $foobar ); -str_replace( array( $foo, $bar ), array( 'bar', 'foo' ), $foobar ); // Ok. \ No newline at end of file +str_replace( array( 'foo', "$bar" ), $bar, 'foobar' ); + +str_replace( array( $foo, $bar ), array( 'bar', 'foo' ), array( 'foobar', 'foobar' ) ); +\Str_Replace( [ 'foo', 'bar' ], array( $foo, SOME_CONSTANT ), 'foobar' ); +str_replace( array( 'foo', "bar" ), [ 'bar', 'foo' ], $foobar, ); +str_replace( [ $foo, $bar->prop ], /*comment*/ [ 'bar', 'foo' ], $foobar ); + + +/* + * These should all be flagged with a warning. + */ +str_replace( 'foo', 'bar', 'foobar' ); // Bad. +Str_replace( + 'foo', // Comment. + 'bar', /* comment */ + 'foobar' +); // Bad. +str_replace( array( 'foo', 'bar' ), array( 'bar', 'foo' ), array( 'foobar', 'foobar' ) ); // Bad. diff --git a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php index c67ff014..9b4f29b3 100644 --- a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php @@ -23,8 +23,9 @@ class StaticStrreplaceUnitTest extends AbstractSniffUnitTest { */ public function getErrorList() { return [ - 3 => 1, - 7 => 1, + 50 => 1, + 51 => 1, + 56 => 1, ]; } From a14e531ced6f54b27229538f54af219a78761c55 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 05:57:06 +0200 Subject: [PATCH 042/165] Security/StaticStrreplace: add tests with optional fourth parameter ... to ensure and safeguard the sniff does not get confused over that parameter. Ref: https://www.php.net/manual/en/function.str-replace.php --- .../Tests/Security/StaticStrreplaceUnitTest.inc | 5 +++-- .../Tests/Security/StaticStrreplaceUnitTest.php | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc index 8c599c60..e005c82e 100644 --- a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc @@ -10,7 +10,7 @@ $this->str_replace('foo', 'bar', 'foobar'); $this?->str_replace('foo', 'bar', 'foobar'); MyClass::str_replace('foo', 'bar', 'foobar'); echo STR_REPLACE; -namespace\str_replace('foo', 'bar', 'foobar'); +namespace\str_replace('foo', 'bar', 'foobar', $count); // Looks like a function call, but is a PHP 8.0+ class instantiation via an attribute. #[Str_Replace('foo', 'bar', 'foobar')] @@ -34,7 +34,7 @@ str_replace( $foo->getSearch(), 'bar', 'foobar' ); \str_replace( 'foo', $bar, 'foobar' ); str_replace( 'foo', 'bar', $foobar, ); STR_REPLACE( $foo, $bar, 'foobar' ); -str_replace( 'foo', get_replacements(), $foobar ); +str_replace( 'foo', get_replacements(), $foobar, $count ); str_replace( array( 'foo', "$bar" ), $bar, 'foobar' ); @@ -54,3 +54,4 @@ Str_replace( 'foobar' ); // Bad. str_replace( array( 'foo', 'bar' ), array( 'bar', 'foo' ), array( 'foobar', 'foobar' ) ); // Bad. +str_replace( 'foo', 'bar', 'foobar', $count ); // Bad. diff --git a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php index 9b4f29b3..a226d6cd 100644 --- a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php @@ -26,6 +26,7 @@ public function getErrorList() { 50 => 1, 51 => 1, 56 => 1, + 57 => 1, ]; } From 26bddea8ea04ebfcd06e0676ccc422dd7031d8fe Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 05:38:35 +0200 Subject: [PATCH 043/165] Security/StaticStrreplace: bug fix - false negatives for PHP 5.4+ short array parameters Fixed now. Includes tests. Note: some of the "should not be flagged" tests already use short arrays since the tests were expanded. --- .../Sniffs/Security/StaticStrreplaceSniff.php | 17 ++++++++++++----- .../Tests/Security/StaticStrreplaceUnitTest.inc | 6 ++++++ .../Tests/Security/StaticStrreplaceUnitTest.php | 1 + 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php b/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php index eeac8528..6912523d 100644 --- a/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php @@ -10,6 +10,8 @@ namespace WordPressVIPMinimum\Sniffs\Security; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Arrays; use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** @@ -55,15 +57,20 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $next_start_ptr = $openBracket + 1; for ( $i = 0; $i < 3; $i++ ) { $param_ptr = $this->phpcsFile->findNext( array_merge( Tokens::$emptyTokens, [ T_COMMA ] ), $next_start_ptr, null, true ); + if ( $param_ptr === false ) { + // Live coding or parse error. Ignore. + return; + } - if ( $this->tokens[ $param_ptr ]['code'] === T_ARRAY ) { - $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_ptr + 1, null, true ); - if ( $this->tokens[ $openBracket ]['code'] !== T_OPEN_PARENTHESIS ) { + if ( isset( Collections::arrayOpenTokensBC()[ $this->tokens[ $param_ptr ]['code'] ] ) ) { + $arrayOpenClose = Arrays::getOpenClose( $this->phpcsFile, $param_ptr ); + if ( $arrayOpenClose === false ) { + // Short list, parse error or live coding, bow out. return; } - // Find the closing bracket. - $closeBracket = $this->tokens[ $openBracket ]['parenthesis_closer']; + $openBracket = $arrayOpenClose['opener']; + $closeBracket = $arrayOpenClose['closer']; $array_item_ptr = $this->phpcsFile->findNext( array_merge( Tokens::$emptyTokens, [ T_COMMA ] ), $openBracket + 1, $closeBracket, true ); while ( $array_item_ptr !== false ) { diff --git a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc index e005c82e..5cf098dd 100644 --- a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc @@ -55,3 +55,9 @@ Str_replace( ); // Bad. str_replace( array( 'foo', 'bar' ), array( 'bar', 'foo' ), array( 'foobar', 'foobar' ) ); // Bad. str_replace( 'foo', 'bar', 'foobar', $count ); // Bad. + +// Handle PHP 5.4+ short array syntax correctly. +\str_replace( [ 'foo', 'bar' ], [ 'bar', 'foo' ], [ 'foobar', 'foobar' ], ); // Bad. + +// Don't confuse PHP 7.1+ short list with short array. +str_replace( [ $a, $b ] = $search, [ 'bar', 'foo' ], 'foobar', ); // OK. diff --git a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php index a226d6cd..057edeeb 100644 --- a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php @@ -27,6 +27,7 @@ public function getErrorList() { 51 => 1, 56 => 1, 57 => 1, + 60 => 1, ]; } From 3cff6936fce85a256f77ebad18a6eb9cba8352ed Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 15:47:09 +0200 Subject: [PATCH 044/165] Security/StaticStrreplace: use PHPCSUtils for array parsing As things were, the sniff walked the tokens in an array and expected a comma to always belong to the array, i.e. `[ 'text', 'text' ]`. This falls foul as soon as the code being walked gets slightly more complex, like using nested arrays or dynamic values in the array: `[ 'text', [ $a, $b ], fnc( 'text', 'next' ) ]`. Now, at this time, this is not strictly problematic for this sniff as it will bow out for any token which is not a `T_CONSTANT_ENCAPSED_STRING`, so would bow out for nested arrays and dynamic values. Having said this, using the PHPCSUtils `PassedParameters::getParameters()` for parsing an array to it's individual items should still make the code more stable and also benefits from the PHPCSUtils build-in caching. --- .../Sniffs/Security/StaticStrreplaceSniff.php | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php b/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php index 6912523d..e987a776 100644 --- a/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Tokens\Collections; use PHPCSUtils\Utils\Arrays; +use PHPCSUtils\Utils\PassedParameters; use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** @@ -54,6 +55,9 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p return; } + $static_text_tokens = Tokens::$emptyTokens; + $static_text_tokens[ T_CONSTANT_ENCAPSED_STRING ] = T_CONSTANT_ENCAPSED_STRING; + $next_start_ptr = $openBracket + 1; for ( $i = 0; $i < 3; $i++ ) { $param_ptr = $this->phpcsFile->findNext( array_merge( Tokens::$emptyTokens, [ T_COMMA ] ), $next_start_ptr, null, true ); @@ -69,21 +73,16 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p return; } - $openBracket = $arrayOpenClose['opener']; - $closeBracket = $arrayOpenClose['closer']; - - $array_item_ptr = $this->phpcsFile->findNext( array_merge( Tokens::$emptyTokens, [ T_COMMA ] ), $openBracket + 1, $closeBracket, true ); - while ( $array_item_ptr !== false ) { - - if ( $this->tokens[ $array_item_ptr ]['code'] !== T_CONSTANT_ENCAPSED_STRING ) { + $array_items = PassedParameters::getParameters( $this->phpcsFile, $param_ptr ); + foreach ( $array_items as $array_item ) { + $has_non_static_text = $this->phpcsFile->findNext( $static_text_tokens, $array_item['start'], $array_item['end'], true ); + if ( $has_non_static_text !== false ) { return; } - $array_item_ptr = $this->phpcsFile->findNext( array_merge( Tokens::$emptyTokens, [ T_COMMA ] ), $array_item_ptr + 1, $closeBracket, true ); } - $next_start_ptr = $closeBracket + 1; + $next_start_ptr = $arrayOpenClose['closer'] + 1; continue; - } if ( $this->tokens[ $param_ptr ]['code'] !== T_CONSTANT_ENCAPSED_STRING ) { From 45cb9d5a822e95f1312a0aa2f966a59e0ee32de4 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 16:42:09 +0200 Subject: [PATCH 045/165] Security/StaticStrreplace: use pre-parsed function call argument information / support PHP 8.0+ named arguments As things were, the sniff walked the tokens in the function call itself, but what with the switch to extending the `AbstractFunctionParameterSniff` class, we already have the start and end pointers of each passed parameter available, so let's start using that information instead of doing the token walking within the sniff. By using the pre-parsed information, the sniff automatically will start supporting the use of PHP 8.0+ named arguments in function calls. Includes tests. --- .../Sniffs/Security/StaticStrreplaceSniff.php | 45 ++++++++++--------- .../Security/StaticStrreplaceUnitTest.inc | 8 ++++ .../Security/StaticStrreplaceUnitTest.php | 2 + 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php b/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php index e987a776..1b25ce56 100644 --- a/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php @@ -10,8 +10,8 @@ namespace WordPressVIPMinimum\Sniffs\Security; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Exceptions\UnexpectedTokenType; use PHPCSUtils\Tokens\Collections; -use PHPCSUtils\Utils\Arrays; use PHPCSUtils\Utils\PassedParameters; use WordPressCS\WordPress\AbstractFunctionParameterSniff; @@ -48,32 +48,37 @@ class StaticStrreplaceSniff extends AbstractFunctionParameterSniff { * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - - $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); - - if ( $this->tokens[ $openBracket ]['code'] !== T_OPEN_PARENTHESIS ) { + $search_param = PassedParameters::getParameterFromStack( $parameters, 1, 'search' ); + $replace_param = PassedParameters::getParameterFromStack( $parameters, 2, 'replace' ); + $subject_param = PassedParameters::getParameterFromStack( $parameters, 3, 'subject' ); + + if ( $search_param === false || $replace_param === false || $subject_param === false ) { + /* + * Either an invalid function call (missing PHP required parameter); or function call + * with argument unpacking; or live coding. + * In all these cases, this is not the code pattern this sniff is looking for, so bow out. + */ return; } $static_text_tokens = Tokens::$emptyTokens; $static_text_tokens[ T_CONSTANT_ENCAPSED_STRING ] = T_CONSTANT_ENCAPSED_STRING; - $next_start_ptr = $openBracket + 1; - for ( $i = 0; $i < 3; $i++ ) { - $param_ptr = $this->phpcsFile->findNext( array_merge( Tokens::$emptyTokens, [ T_COMMA ] ), $next_start_ptr, null, true ); - if ( $param_ptr === false ) { - // Live coding or parse error. Ignore. - return; + foreach ( [ $search_param, $replace_param, $subject_param ] as $param ) { + $has_non_static_text = $this->phpcsFile->findNext( $static_text_tokens, $param['start'], ( $param['end'] + 1 ), true ); + if ( $has_non_static_text === false ) { + // The parameter contained only tokens which could be considered static text. + continue; } - if ( isset( Collections::arrayOpenTokensBC()[ $this->tokens[ $param_ptr ]['code'] ] ) ) { - $arrayOpenClose = Arrays::getOpenClose( $this->phpcsFile, $param_ptr ); - if ( $arrayOpenClose === false ) { + if ( isset( Collections::arrayOpenTokensBC()[ $this->tokens[ $has_non_static_text ]['code'] ] ) ) { + try { + $array_items = PassedParameters::getParameters( $this->phpcsFile, $has_non_static_text ); + } catch ( UnexpectedTokenType $e ) { // Short list, parse error or live coding, bow out. return; } - $array_items = PassedParameters::getParameters( $this->phpcsFile, $param_ptr ); foreach ( $array_items as $array_item ) { $has_non_static_text = $this->phpcsFile->findNext( $static_text_tokens, $array_item['start'], $array_item['end'], true ); if ( $has_non_static_text !== false ) { @@ -81,16 +86,12 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p } } - $next_start_ptr = $arrayOpenClose['closer'] + 1; + // The array only contained items with tokens which could be considered static text. continue; } - if ( $this->tokens[ $param_ptr ]['code'] !== T_CONSTANT_ENCAPSED_STRING ) { - return; - } - - $next_start_ptr = $param_ptr + 1; - + // Non-static text token found. Not what we're looking for. + return; } $message = 'This code pattern is often used to run a very dangerous shell programs on your server. The code in these files needs to be reviewed, and possibly cleaned.'; diff --git a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc index 5cf098dd..cfb888b7 100644 --- a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc @@ -61,3 +61,11 @@ str_replace( 'foo', 'bar', 'foobar', $count ); // Bad. // Don't confuse PHP 7.1+ short list with short array. str_replace( [ $a, $b ] = $search, [ 'bar', 'foo' ], 'foobar', ); // OK. + +// Safeguard support for PHP 8.0+ named parameters. +str_replace( search: [ 'foo', 'bar' ], replace: 'baz', count: $count ); // OK, well not really, missing required $subject param, but not our concern. +str_replace( search: 'foo', replace: 'baz', subjects: [ 'foobar', 'foobar' ] ); // OK, well not really, typo in $subject param name, not our concern. +str_replace( subject: $subject, count: $count, replace: 'baz', search: 'foo', ); // OK, different parameter order. + +str_replace( count: $count, subject: [ 'foobar', 'foobar' ], search: 'foo', replace: 'baz', ); // Bad with different parameter order. +str_replace( [ 'foo', 'bar' ], 'baz', count: $count, subject: 'foobar' ); // Bad, with mixed named and unnamed params. diff --git a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php index 057edeeb..67b34286 100644 --- a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.php @@ -28,6 +28,8 @@ public function getErrorList() { 56 => 1, 57 => 1, 60 => 1, + 70 => 1, + 71 => 1, ]; } From b225a93969e03233c2c0694cccda330a4d21257f Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 17:26:54 +0200 Subject: [PATCH 046/165] Security/StaticStrreplace: handle more "static" tokens This commit primarily addresses that the sniff did not take PHP 5.3 nowdocs into account as "static text" tokens. However, heredocs can also be "static text" if there is no interpolation and there are some other variants of valid code which would still make the code effectively "static text". Fixed now by checking the code more thoroughly. The actual checking has been moved to a new `is_parameter_static_text()` method as if the passed parameter is an array, the same checks would need to be done again for each array item. By having the check in a separate method, the method can recursively call itself for array items. Includes tests. --- .../Sniffs/Security/StaticStrreplaceSniff.php | 82 ++++++++++++++----- .../Security/StaticStrreplaceUnitTest.inc | 30 +++++++ .../Security/StaticStrreplaceUnitTest.php | 4 + 3 files changed, 97 insertions(+), 19 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php b/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php index 1b25ce56..017c36a5 100644 --- a/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/StaticStrreplaceSniff.php @@ -10,9 +10,10 @@ namespace WordPressVIPMinimum\Sniffs\Security; use PHP_CodeSniffer\Util\Tokens; -use PHPCSUtils\Exceptions\UnexpectedTokenType; use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Arrays; use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** @@ -61,40 +62,83 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p return; } - $static_text_tokens = Tokens::$emptyTokens; - $static_text_tokens[ T_CONSTANT_ENCAPSED_STRING ] = T_CONSTANT_ENCAPSED_STRING; - foreach ( [ $search_param, $replace_param, $subject_param ] as $param ) { - $has_non_static_text = $this->phpcsFile->findNext( $static_text_tokens, $param['start'], ( $param['end'] + 1 ), true ); - if ( $has_non_static_text === false ) { + if ( $this->is_parameter_static_text( $param ) === false ) { + // Non-static text token found. Not what we're looking for. + return; + } + } + + $message = 'This code pattern is often used to run a very dangerous shell programs on your server. The code in these files needs to be reviewed, and possibly cleaned.'; + $this->phpcsFile->addError( $message, $stackPtr, 'StaticStrreplace' ); + } + + /** + * Check whether the current parameter, or array item, only contains tokens which should be regarded + * as a valid part of a static text string. + * + * @param array $param_info Array with information about a single parameter or array item. + * Must be an array as returned via the PassedParameters class. + * + * @return bool + */ + private function is_parameter_static_text( $param_info ) { + // List of tokens which can be skipped over without further examination. + $static_tokens = [ + T_CONSTANT_ENCAPSED_STRING => T_CONSTANT_ENCAPSED_STRING, + T_PLUS => T_PLUS, + T_STRING_CONCAT => T_STRING_CONCAT, + ]; + $static_tokens += Tokens::$emptyTokens; + + for ( $i = $param_info['start']; $i <= $param_info['end']; $i++ ) { + $next_to_examine = $this->phpcsFile->findNext( $static_tokens, $i, ( $param_info['end'] + 1 ), true ); + if ( $next_to_examine === false ) { // The parameter contained only tokens which could be considered static text. - continue; + return true; } - if ( isset( Collections::arrayOpenTokensBC()[ $this->tokens[ $has_non_static_text ]['code'] ] ) ) { - try { - $array_items = PassedParameters::getParameters( $this->phpcsFile, $has_non_static_text ); - } catch ( UnexpectedTokenType $e ) { + if ( isset( Collections::arrayOpenTokensBC()[ $this->tokens[ $next_to_examine ]['code'] ] ) ) { + $arrayOpenClose = Arrays::getOpenClose( $this->phpcsFile, $next_to_examine ); + if ( $arrayOpenClose === false ) { // Short list, parse error or live coding, bow out. - return; + return false; } + $array_items = PassedParameters::getParameters( $this->phpcsFile, $next_to_examine ); foreach ( $array_items as $array_item ) { - $has_non_static_text = $this->phpcsFile->findNext( $static_text_tokens, $array_item['start'], $array_item['end'], true ); - if ( $has_non_static_text !== false ) { - return; + if ( $this->is_parameter_static_text( $array_item ) === false ) { + return false; } } // The array only contained items with tokens which could be considered static text. + $i = $arrayOpenClose['closer']; continue; } - // Non-static text token found. Not what we're looking for. - return; + if ( $this->tokens[ $next_to_examine ]['code'] === T_START_HEREDOC ) { + $heredoc_text = TextStrings::getCompleteTextString( $this->phpcsFile, $next_to_examine ); + $stripped_text = TextStrings::stripEmbeds( $heredoc_text ); + if ( $heredoc_text !== $stripped_text ) { + // Heredoc with interpolated expression(s). Not a static text. + return false; + } + } + + if ( ( $this->tokens[ $next_to_examine ]['code'] === T_START_HEREDOC + || $this->tokens[ $next_to_examine ]['code'] === T_START_NOWDOC ) + && isset( $this->tokens[ $next_to_examine ]['scope_closer'] ) + ) { + // No interpolation. Skip to end of a heredoc/nowdoc. + $i = $this->tokens[ $next_to_examine ]['scope_closer']; + continue; + } + + // Any other token means this parameter should be regarded as non-static text. Not what we're looking for. + return false; } - $message = 'This code pattern is often used to run a very dangerous shell programs on your server. The code in these files needs to be reviewed, and possibly cleaned.'; - $this->phpcsFile->addError( $message, $stackPtr, 'StaticStrreplace' ); + return true; } } diff --git a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc index cfb888b7..4b7af5dc 100644 --- a/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/StaticStrreplaceUnitTest.inc @@ -69,3 +69,33 @@ str_replace( subject: $subject, count: $count, replace: 'baz', search: 'foo', ); str_replace( count: $count, subject: [ 'foobar', 'foobar' ], search: 'foo', replace: 'baz', ); // Bad with different parameter order. str_replace( [ 'foo', 'bar' ], 'baz', count: $count, subject: 'foobar' ); // Bad, with mixed named and unnamed params. + +// Make sure some variations of how plain strings can be passed are handled. +str_replace( 'fo' . 'o', 'bar', 'foobar' ); // Bad. +str_replace( array( 'foo', 'bar' ) + array( 'baz', 'fop' ), array( 'bar', 'foo' ), 'foobar' ); // Bad. + +// Safeguard correct handling of PHP 5.3+ nowdocs. +str_replace( + 'foo', + 'bar', + <<<'EOD' +foobar +EOD +); // Bad. + +// Safeguard correct handling of heredocs without interpolation. +str_replace( array(<< 1, 70 => 1, 71 => 1, + 74 => 1, + 75 => 1, + 78 => 1, + 87 => 1, ]; } From 3f998775a214ba8958f01b2817061720603164ef Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 18 Jul 2025 20:30:19 +0200 Subject: [PATCH 047/165] Security/PHPFilterFunctions: change condition As the error code - at this time - is tied to the parameter position, might as well check on the parameter position for this condition. --- WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php index 06b67503..32e9f7a9 100644 --- a/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/PHPFilterFunctionsSniff.php @@ -110,7 +110,7 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p // Error codes should probably be made more descriptive, but that would be a BC-break. $error_code = 'MissingSecondParameter'; - if ( $matched_content === 'filter_input' ) { + if ( $param_position === 3 ) { $error_code = 'MissingThirdParameter'; } From 0d5e1eca02ef564f63ee29d2f57733a400019230 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 19 Jul 2025 20:42:23 +0200 Subject: [PATCH 048/165] Variables/ServerVariables: improve the tests * Expand the tests safeguarding against false positives. * Ensure all five indices are covered by at least one test. * Add some more variations to the pre-existing tests: - Unconventional spacing. - Comments in unexpected places. --- .../Variables/ServerVariablesUnitTest.inc | 40 ++++++++++++++++--- .../Variables/ServerVariablesUnitTest.php | 9 +++-- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc index a22bae0c..2fe595d3 100644 --- a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc @@ -1,9 +1,37 @@ 1, - 5 => 1, - 6 => 1, - 7 => 1, + 31 => 1, + 32 => 1, + 33 => 1, + 36 => 1, + 37 => 1, ]; } From 6f8ebb6369e7a35160032a78c479c57ea45f1c60 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 19 Jul 2025 21:30:42 +0200 Subject: [PATCH 049/165] Variables/ServerVariables: use the correct terminology The sniff looks for specific indexes/keys for array access. This commit fixes some documentation and variable names used to use this terminology correctly. --- .../Sniffs/Variables/ServerVariablesSniff.php | 12 ++++++------ .../Tests/Variables/ServerVariablesUnitTest.inc | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php index ffd039e2..d47fff6e 100644 --- a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php @@ -17,7 +17,7 @@ class ServerVariablesSniff extends Sniff { /** - * List of restricted constant names. + * List of restricted indices. * * @var array> */ @@ -58,15 +58,15 @@ public function process_token( $stackPtr ) { return; } - $variableNamePtr = $this->phpcsFile->findNext( [ T_CONSTANT_ENCAPSED_STRING ], $stackPtr + 1, null, false, null, true ); - $variableName = str_replace( [ "'", '"' ], '', $this->tokens[ $variableNamePtr ]['content'] ); + $indexPtr = $this->phpcsFile->findNext( [ T_CONSTANT_ENCAPSED_STRING ], $stackPtr + 1, null, false, null, true ); + $indexName = str_replace( [ "'", '"' ], '', $this->tokens[ $indexPtr ]['content'] ); - if ( isset( $this->restrictedVariables['authVariables'][ $variableName ] ) ) { + if ( isset( $this->restrictedVariables['authVariables'][ $indexName ] ) ) { $message = 'Basic authentication should not be handled via PHP code.'; $this->phpcsFile->addError( $message, $stackPtr, 'BasicAuthentication' ); - } elseif ( isset( $this->restrictedVariables['userControlledVariables'][ $variableName ] ) ) { + } elseif ( isset( $this->restrictedVariables['userControlledVariables'][ $indexName ] ) ) { $message = 'Header "%s" is user-controlled and should be properly validated before use.'; - $data = [ $variableName ]; + $data = [ $indexName ]; $this->phpcsFile->addError( $message, $stackPtr, 'UserControlledHeaders', $data ); } } diff --git a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc index 2fe595d3..e8e2128b 100644 --- a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc @@ -13,7 +13,7 @@ $_server['PHP_AUTH_USER']; // Variable names are case-sensitive. */ var_dump($_SERVER); // No key access. $_SERVER[] = 10; -$_SERVER['SOME_OTHER_VARIABLE']; +$_SERVER['SOME_OTHER_INDEX']; $_SERVER['php_auth_user']; // Array indices are case-sensitive. // PHP 7.4+ spread in array. From 97f62dd329bb0e1bf41eeb4e6ff7849225d7a11f Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 19 Jul 2025 20:45:39 +0200 Subject: [PATCH 050/165] Variables/ServerVariables: bug fix - incorrect quote stripping As things were, quotes _within_ a text string would also be stripped. Not that it's very likely for any of the `$_SERVER` keys to ever have these, but that's beside the point. --- .../Sniffs/Variables/ServerVariablesSniff.php | 3 ++- .../Tests/Variables/ServerVariablesUnitTest.inc | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php index d47fff6e..4bcd1fca 100644 --- a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php @@ -9,6 +9,7 @@ namespace WordPressVIPMinimum\Sniffs\Variables; +use PHPCSUtils\Utils\TextStrings; use WordPressVIPMinimum\Sniffs\Sniff; /** @@ -59,7 +60,7 @@ public function process_token( $stackPtr ) { } $indexPtr = $this->phpcsFile->findNext( [ T_CONSTANT_ENCAPSED_STRING ], $stackPtr + 1, null, false, null, true ); - $indexName = str_replace( [ "'", '"' ], '', $this->tokens[ $indexPtr ]['content'] ); + $indexName = TextStrings::stripQuotes( $this->tokens[ $indexPtr ]['content'] ); if ( isset( $this->restrictedVariables['authVariables'][ $indexName ] ) ) { $message = 'Basic authentication should not be handled via PHP code.'; diff --git a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc index e8e2128b..628654ca 100644 --- a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc @@ -35,3 +35,11 @@ $_SERVER [ ]; $_SERVER /*comment*/ [ /*comment*/ 'HTTP_X_FORWARDED_FOR' /*comment*/ ]; $_SERVER["REMOTE_ADDR"]; // Let's test one with double quotes too. + + +/* + * Prevent various false positives. + */ +// These look like "forbidden" indices, but aren't. +$_SERVER['PHP_"AUTH"_PW']; +$_SERVER["PHP_'AUTH'_PW"]; From d0362edde233b0871a215972f88bf191304d9e78 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 19 Jul 2025 20:53:00 +0200 Subject: [PATCH 051/165] Variables/ServerVariables: bug fix - false positives on OO property access While it should probably be considered bad practice, it is allowed to declare an OO property called `$_SERVER` and this sniff should not get confused by that. Includes tests. --- .../Sniffs/Variables/ServerVariablesSniff.php | 7 +++++++ .../Tests/Variables/ServerVariablesUnitTest.inc | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php index 4bcd1fca..5cfa1ced 100644 --- a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php @@ -9,6 +9,7 @@ namespace WordPressVIPMinimum\Sniffs\Variables; +use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Utils\TextStrings; use WordPressVIPMinimum\Sniffs\Sniff; @@ -59,6 +60,12 @@ public function process_token( $stackPtr ) { return; } + $prevNonEmpty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); + if ( $this->tokens[ $prevNonEmpty ]['code'] === T_DOUBLE_COLON ) { + // Access to OO property mirroring the name of the superglobal. Not our concern. + return; + } + $indexPtr = $this->phpcsFile->findNext( [ T_CONSTANT_ENCAPSED_STRING ], $stackPtr + 1, null, false, null, true ); $indexName = TextStrings::stripQuotes( $this->tokens[ $indexPtr ]['content'] ); diff --git a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc index 628654ca..6b2d098a 100644 --- a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc @@ -43,3 +43,15 @@ $_SERVER["REMOTE_ADDR"]; // Let's test one with double quotes too. // These look like "forbidden" indices, but aren't. $_SERVER['PHP_"AUTH"_PW']; $_SERVER["PHP_'AUTH'_PW"]; + +// Sniff should not get confused over static OO property access. +class PeopleDoSillyThings extends AllowedByPHP { + public static $_SERVER = []; + + public function show() { + var_export(self::$_SERVER['PHP_AUTH_USER']); + var_export(parent :: $_SERVER['PHP_AUTH_PW']); + var_export(static::$_SERVER['HTTP_X_FORWARDED_FOR']); + var_export(OtherClass::$_SERVER['HTTP_X_IP_TRAIL']); + } +} From 5bbd1b9eef49137a5240847613238e257703b3eb Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 19 Jul 2025 21:27:07 +0200 Subject: [PATCH 052/165] Variables/ServerVariables: bug fix - faulty array key determination The code to find the array index was flawed and could walk beyond the brackets of this array access. Additionally, array access keys comprised of multiple tokens were not handled correctly. Includes tests. Note: WordPressCS has helper functions to retrieve the array access name, but those are marked as internal, which is the reason to introduce a custom function. --- .../Sniffs/Variables/ServerVariablesSniff.php | 48 ++++++++++++++++++- .../Variables/ServerVariablesUnitTest.inc | 6 +++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php index 5cfa1ced..fc213d52 100644 --- a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php @@ -66,7 +66,12 @@ public function process_token( $stackPtr ) { return; } - $indexPtr = $this->phpcsFile->findNext( [ T_CONSTANT_ENCAPSED_STRING ], $stackPtr + 1, null, false, null, true ); + $indexPtr = $this->get_array_access_key( $stackPtr ); + if ( $indexPtr === false ) { + // Couldn't find an array index token usable for the purposes of this sniff. Bow out as undetermined. + return; + } + $indexName = TextStrings::stripQuotes( $this->tokens[ $indexPtr ]['content'] ); if ( isset( $this->restrictedVariables['authVariables'][ $indexName ] ) ) { @@ -78,4 +83,45 @@ public function process_token( $stackPtr ) { $this->phpcsFile->addError( $message, $stackPtr, 'UserControlledHeaders', $data ); } } + + /** + * Get the array access key. + * + * Find the array access key and check if it is: + * - comprised of a single functional token. + * - that token is a T_CONSTANT_ENCAPSED_STRING. + * + * @param int $stackPtr The position of either a variable or the close bracket of a previous array access. + * + * @return int|false Stack pointer to the index token; or FALSE for + * live coding, non-indexed array assignment, or non plain text array keys. + */ + private function get_array_access_key( $stackPtr ) { + $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( $openBracket === false + || $this->tokens[ $openBracket ]['code'] !== T_OPEN_SQUARE_BRACKET + || isset( $this->tokens[ $openBracket ]['bracket_closer'] ) === false + ) { + // If it isn't an open bracket, this isn't array access. And without closer, it is a parse error/live coding. + return false; + } + + $closeBracket = $this->tokens[ $openBracket ]['bracket_closer']; + + $indexPtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $openBracket + 1 ), $closeBracket, true ); + if ( $indexPtr === false + || $this->tokens[ $indexPtr ]['code'] !== T_CONSTANT_ENCAPSED_STRING + ) { + // No array access (like for array assignment without key) or key is not plain text. + return false; + } + + $hasOtherTokens = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $indexPtr + 1 ), $closeBracket, true ); + if ( $hasOtherTokens !== false ) { + // The array index is comprised of multiple tokens. Bow out as undetermined. + return false; + } + + return $indexPtr; + } } diff --git a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc index 6b2d098a..029d1fbd 100644 --- a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc @@ -55,3 +55,9 @@ class PeopleDoSillyThings extends AllowedByPHP { var_export(OtherClass::$_SERVER['HTTP_X_IP_TRAIL']); } } + +// Safeguard the sniff looks at the access key for the $_SERVER variable only and doesn't walk too far. +echo $_SERVER[$other_key] . $NOT_SERVER['PHP_AUTH_PW']; + +// Safeguard the sniff doesn't get confused over partially dynamic keys. +echo $_SERVER[$other_key . 'PHP_AUTH_PW']; From 67bba5713ac09a29cec19fc52e55b7918e2c3def Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 19 Jul 2025 22:14:44 +0200 Subject: [PATCH 053/165] Variables/ServerVariables: bug fix - false negatives for `$GLOBALS['_SERVER']` The `$GLOBALS['_SERVER']` superglobals access is equivalent to using `$_SERVER`, so should be examined too. Includes tests. --- .../Sniffs/Variables/ServerVariablesSniff.php | 27 ++++++++++++++++--- .../Variables/ServerVariablesUnitTest.inc | 12 +++++++++ .../Variables/ServerVariablesUnitTest.php | 5 ++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php index fc213d52..f3769380 100644 --- a/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Variables/ServerVariablesSniff.php @@ -55,18 +55,39 @@ public function register() { */ public function process_token( $stackPtr ) { - if ( $this->tokens[ $stackPtr ]['content'] !== '$_SERVER' ) { - // Not the variable we are looking for. + if ( $this->tokens[ $stackPtr ]['content'] !== '$_SERVER' + && $this->tokens[ $stackPtr ]['content'] !== '$GLOBALS' + ) { + // Not a variable we are looking for. return; } + $searchStart = $stackPtr; + if ( $this->tokens[ $stackPtr ]['content'] === '$GLOBALS' ) { + $globalsIndexPtr = $this->get_array_access_key( $stackPtr ); + if ( $globalsIndexPtr === false ) { + // Couldn't find an array index token usable for the purposes of this sniff. Bow out. + return; + } + + $globalsIndexName = TextStrings::stripQuotes( $this->tokens[ $globalsIndexPtr ]['content'] ); + if ( $globalsIndexName !== '_SERVER' ) { + // Not access to `$GLOBALS['_SERVER']`. + return; + } + + // Set the start point for the next array access key search to the close bracket of this array index. + // No need for defensive coding as we already know there will be a valid close bracket next. + $searchStart = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $globalsIndexPtr + 1 ), null, true ); + } + $prevNonEmpty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $stackPtr - 1 ), null, true ); if ( $this->tokens[ $prevNonEmpty ]['code'] === T_DOUBLE_COLON ) { // Access to OO property mirroring the name of the superglobal. Not our concern. return; } - $indexPtr = $this->get_array_access_key( $stackPtr ); + $indexPtr = $this->get_array_access_key( $searchStart ); if ( $indexPtr === false ) { // Couldn't find an array index token usable for the purposes of this sniff. Bow out as undetermined. return; diff --git a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc index 029d1fbd..28de0071 100644 --- a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.inc @@ -61,3 +61,15 @@ echo $_SERVER[$other_key] . $NOT_SERVER['PHP_AUTH_PW']; // Safeguard the sniff doesn't get confused over partially dynamic keys. echo $_SERVER[$other_key . 'PHP_AUTH_PW']; + +// Access of the array indices via the $GLOBALS superglobal should also be recognized. +$GLOBALS['NOT_SERVER']['PHP_AUTH_USER']; // OK. +$GLOBALS[$a]['PHP_AUTH_USER']; // OK. +$GLOBALS['_SERVER']['SOME_OTHER_KEY']; // OK. +$GLOBALS['_SERVER'][$a]; // OK. + +$GLOBALS['_SERVER']['PHP_AUTH_USER']; +$GLOBALS['_SERVER']['PHP_AUTH_PW']; +$GLOBALS["_SERVER"]['HTTP_X_IP_TRAIL']; +$GLOBALS['_SERVER']['HTTP_X_FORWARDED_FOR']; +$GLOBALS['_SERVER']["REMOTE_ADDR"]; diff --git a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.php b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.php index 5222e8ce..610dabd0 100644 --- a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.php +++ b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.php @@ -28,6 +28,11 @@ public function getErrorList() { 33 => 1, 36 => 1, 37 => 1, + 71 => 1, + 72 => 1, + 73 => 1, + 74 => 1, + 75 => 1, ]; } From 5641bca0a64a8f5357c999f8902102240cf0e8ac Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 19 Jul 2025 23:04:14 +0200 Subject: [PATCH 054/165] Constants/ConstantString: extend AbstractFunctionParameterSniff As things were, the determination of whether or not a `T_STRING` is a call to the global PHP native `define[d]()` function was severely flawed. By switching the sniff over to be based on the WordPressCS `AbstractFunctionParameterSniff` class, this flaw is mitigated. Includes tests. --- .../Sniffs/Constants/ConstantStringSniff.php | 54 ++++++++----------- .../Constants/ConstantStringUnitTest.inc | 23 ++++++++ .../Constants/ConstantStringUnitTest.php | 8 ++- 3 files changed, 52 insertions(+), 33 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php b/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php index fa44b8f4..c50aca58 100644 --- a/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php +++ b/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php @@ -11,51 +11,43 @@ use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Utils\PassedParameters; -use WordPressVIPMinimum\Sniffs\Sniff; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** * Sniff for properly using constant name when checking whether a constant is defined. */ -class ConstantStringSniff extends Sniff { +class ConstantStringSniff extends AbstractFunctionParameterSniff { /** - * Returns an array of tokens this test wants to listen for. + * The group name for this group of functions. * - * @return array + * @var string */ - public function register() { - return [ - T_STRING, - ]; - } + protected $group_name = 'constant_functions'; + + /** + * Functions this sniff is looking for. + * + * @var array Key is the function name, value irrelevant. + */ + protected $target_functions = [ + 'define' => true, + 'defined' => true, + ]; /** - * Process this test when one of its tokens is encountered. + * Process the parameters of a matched function. * - * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. * * @return void */ - public function process_token( $stackPtr ) { - - if ( in_array( $this->tokens[ $stackPtr ]['content'], [ 'define', 'defined' ], true ) === false ) { - return; - } - - // Find the next non-empty token. - $nextToken = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true, null, true ); - - if ( $this->tokens[ $nextToken ]['code'] !== T_OPEN_PARENTHESIS ) { - // Not a function call. - return; - } - - if ( isset( $this->tokens[ $nextToken ]['parenthesis_closer'] ) === false ) { - // Not a function call. - return; - } - - $param = PassedParameters::getParameter( $this->phpcsFile, $stackPtr, 1, 'constant_name' ); + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + $param = PassedParameters::getParameterFromStack( $parameters, 1, 'constant_name' ); if ( $param === false ) { // Target parameter not found. return; diff --git a/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.inc b/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.inc index ec4ab2f4..5b619b52 100644 --- a/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.inc @@ -31,3 +31,26 @@ if ( ! defined($generator->get()) { // OK. $defined = defined(); // OK, ignore. $defined = defined( /*comment*/ ); // OK, ignore. + +// Safeguard that calls to namespaced functions or methods are not confused with the global functions. +if ( ! Not\defined( WPCOM_VIP ) ) { // Okay. + $obj->define( WPCOM_VIP, true ); // Okay. +} + +if ( ! \Fully\Qualified\defined( WPCOM_VIP ) ) { // Okay. + namespace\define( WPCOM_VIP, true ); // Okay. +} + +// Safeguard that fully qualified function calls are handled correctly. +if ( ! \defined( 'WPCOM_VIP' ) ) { // Okay. + \define( 'WPCOM_VIP', true, ); // Okay. +} + +if ( ! \defined( WPCOM_VIP, ) ) { // Error. + \define( WPCOM_VIP, true ); // Error. +} + +// Safeguard correct handling of case-insensitivity of function names. +if ( ! DEFINED( WPCOM_VIP ) ) { // Error. + Define( WPCOM_VIP, true ); // Error. +} diff --git a/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.php b/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.php index f2cfe47d..5b6bee28 100644 --- a/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.php +++ b/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.php @@ -23,8 +23,12 @@ class ConstantStringUnitTest extends AbstractSniffUnitTest { */ public function getErrorList() { return [ - 7 => 1, - 8 => 1, + 7 => 1, + 8 => 1, + 49 => 1, + 50 => 1, + 54 => 1, + 55 => 1, ]; } From 397990fe0d8518e087ed8ac57c3a26be6a1fbe0c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 19 Jul 2025 23:19:20 +0200 Subject: [PATCH 055/165] Constants/ConstantString: safeguard and document handling of various new php syntaxes --- .../Tests/Constants/ConstantStringUnitTest.inc | 12 ++++++++++++ .../Tests/Constants/ConstantStringUnitTest.php | 1 + 2 files changed, 13 insertions(+) diff --git a/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.inc b/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.inc index 5b619b52..ffd04cd6 100644 --- a/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.inc @@ -54,3 +54,15 @@ if ( ! \defined( WPCOM_VIP, ) ) { // Error. if ( ! DEFINED( WPCOM_VIP ) ) { // Error. Define( WPCOM_VIP, true ); // Error. } + +// Safeguard correct handling of function calls using PHP 8.0+ named parameters. +\defined( constant_nam: WPCOM_VIP ); // Okay, well not really, typo in param name, but that's not the concern of the sniff. +define(value: true, constant_name: 'WPCOM_VIP', ); // Okay. +define(case_insensitive: true, constant_name: WPCOM_VIP ); // Error. + +// Looks like a function call, but is a PHP 8.0+ class instantiation via an attribute. +#[Define('name', 'value')] +function foo() {} + +define(...$params); // PHP 5.6 argument unpacking will be ignored. +add_action('my_action', define(...)); // PHP 8.1 first class callable will be ignored. diff --git a/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.php b/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.php index 5b6bee28..73efa934 100644 --- a/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.php +++ b/WordPressVIPMinimum/Tests/Constants/ConstantStringUnitTest.php @@ -29,6 +29,7 @@ public function getErrorList() { 50 => 1, 54 => 1, 55 => 1, + 61 => 1, ]; } From 68260d29b846b7fe30c9eacb1e54ac17aa2571ca Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 05:04:04 +0200 Subject: [PATCH 056/165] Constants/ConstantString: improve clarity of error message --- WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php b/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php index c50aca58..c44366a4 100644 --- a/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php +++ b/WordPressVIPMinimum/Sniffs/Constants/ConstantStringSniff.php @@ -64,7 +64,7 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $tstring_token = $this->phpcsFile->findNext( T_STRING, $param['start'], $param['end'] + 1 ); - $message = 'Constant name, as a string, should be used along with `%s()`.'; + $message = 'The `%s()` function expects to be passed the constant name as a text string.'; $data = [ $this->tokens[ $stackPtr ]['content'] ]; $this->phpcsFile->addError( $message, $tstring_token, 'NotCheckingConstantName', $data ); } From f55b2445e74d298c1ff4baeb90403bf01bdcbb7d Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 06:03:42 +0200 Subject: [PATCH 057/165] Hooks/RestrictedHooks: improve the tests * Test against false positives for calls to methods or namespaced function calls. * Test against false positives for attribute class using the same name as the function. * Ensure function import `use` statements are not flagged. We're not interested in those. * Ensure PHP 8.1 first class callables are not flagged. We cannot determine the hook name for those. * Document how dynamic hook names are handled. * Add some more variations to the pre-existing tests: - Non-lowercase function call(s). - Fully qualified function calls. - Use PHP 7.3+ trailing comma's in a few function calls. --- .../Tests/Hooks/RestrictedHooksUnitTest.inc | 63 +++++++++++++++---- .../Tests/Hooks/RestrictedHooksUnitTest.php | 30 ++++----- 2 files changed, 66 insertions(+), 27 deletions(-) diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc index 9236f8ee..4d9a69fa 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc @@ -1,19 +1,58 @@ add_action($a, $b); +$this?->add_filter($a, $b); +MyClass::add_action($a, $b); +echo ADD_FILTER; +namespace\add_action($a, $b); + + +/* + * These should all be okay. + */ +add_filter( 'not_target_hook', 'good_example_function' ); +\add_filter( 'upload_mimesX' , 'good_example_function' ); + +add_action(...$params); // PHP 5.6 argument unpacking. + +// Looks like a function call, but is a PHP 8.0+ class instantiation via an attribute. +#[ADD_FILTER('text')] +function foo() {} + +// PHP 8.1 first class callable. +// As we have no knowledge about what parameters will be passed, we shouldn't flag this. +array_walk($filters, add_filter(...)); + +// Ignore as undetermined. +Add_Filter( $hook_name, 'undetermined' ); +\add_action( $obj->get_filterName(), 'undetermined' ); +add_filter( MyClass::FILTER_NAME, 'undetermined', ); +\add_filter( "upload_$mimes", 'undetermined' ); + +// Incomplete function call, should be ignored by the sniff. +$incorrect_but_ok = add_filter(); +$incorrect_but_ok = add_action(); + + +/* + * These should all be flagged with a warning. + */ +add_filter( 'do_robotstxt', 'bad_example_function' ); // Simple string. +add_action('upload_mimes' , [$obj, 'method']); // Incorrect spacing. +add_filter( 'robots_txt','bad_example_function'); // Incorrect spacing. +\add_filter( "http_request_timeout" , fn($param) => $param * 10); // Double quotes. +add_filter( 'upLoad_mimeS' , $callback); // Uppercase characters. +ADD_FILTER( 'upload_' . 'mimes' ,'bad_example_function'); // Single concatenation. add_filter( 'upl' . 'oad_' . 'mimes' ,'bad_example_function'); // Multiple concatenation. -add_filter( "upload_" . 'mimes' ,'bad_example_function'); // Single concatenation with double and single quotes. +add_filter( "upload_" . 'mimes' , bad_example_function(...)); // Single concatenation with double and single quotes. add_filter( 'upl' . "oad_" . "mimes" ,'bad_example_function'); // Multiple concatenation with double and single quotes. -add_filter( 'upload_mimes', function() { // Anonymous callback. +\add_action( 'http_request_args', function() { // Anonymous callback. // Do stuff. }); add_action( 'upload_mimes', 'bad_example_function' ); // Check `add_action()`, which is an alias for `add_filter()`. diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php index 2f6172be..2d404cd4 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php @@ -33,21 +33,21 @@ public function getErrorList() { */ public function getWarningList() { return [ - 7 => 1, - 8 => 1, - 9 => 1, - 10 => 1, - 11 => 1, - 12 => 1, - 13 => 1, - 14 => 1, - 15 => 1, - 16 => 1, - 19 => 1, - 20 => 1, - 21 => 1, - 22 => 1, - 23 => 1, + 46 => 1, + 47 => 1, + 48 => 1, + 49 => 1, + 50 => 1, + 51 => 1, + 52 => 1, + 53 => 1, + 54 => 1, + 55 => 1, + 58 => 1, + 59 => 1, + 60 => 1, + 61 => 1, + 62 => 1, ]; } } From 3608427846f8e9eb1bedd5014d66c803e9eb9922 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 01:52:15 +0200 Subject: [PATCH 058/165] Hooks/RestrictedHooks: bug fix - disregard comments in hook name parameter The PHPCSUtils `PassedParameters::getParameters()` return value includes a `'clean'` array index, which contains the contents of the parameter stripped of surrounding whitespace and comments. The `RestrictedHooks` sniff uses the parameter contents to compare against a list of restricted hooks, but would break if the hook name parameter would contain a comment. Fixed now. Includes test. --- WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php | 2 +- WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php index 3d64ec5c..70949f60 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php @@ -118,7 +118,7 @@ private function normalize_hook_name_from_parameter( $parameter ) { } } } else { - $hook_name = $parameter['raw']; + $hook_name = $parameter['clean']; } // Remove quotes (double and single), and use lowercase. diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc index 4d9a69fa..78a16deb 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc @@ -58,6 +58,6 @@ add_filter( 'upl' . "oad_" . "mimes" ,'bad_example_function'); // Multiple conca add_action( 'upload_mimes', 'bad_example_function' ); // Check `add_action()`, which is an alias for `add_filter()`. add_filter( 'http_request_timeout', 'bad_example_function' ); // Simple string. add_filter('http_request_args', 'bad_example_function' ); // Simple string + incorrect spacing. -add_action( 'do_robotstxt', 'my_do_robotstxt'); // Simple string. +add_action( /*comment*/ 'do_robotstxt', 'my_do_robotstxt'); // Simple string. add_filter( 'robots_txt', function() { // Anonymous callback. } ); From 165aef3f741cbe917a3787ee518a5969dabb61a6 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 06:21:01 +0200 Subject: [PATCH 059/165] Hooks/RestrictedHooks: add support for handling PHP 8.0+ function calls using named parameters Includes minor efficiency fix by moving the call to `normalize_hook_name_from_parameter()` out of the loops. Includes tests. --- .../Sniffs/Hooks/RestrictedHooksSniff.php | 11 ++++++++++- .../Tests/Hooks/RestrictedHooksUnitTest.inc | 7 +++++++ .../Tests/Hooks/RestrictedHooksUnitTest.php | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php index 70949f60..5b8a8466 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php @@ -9,6 +9,7 @@ namespace WordPressVIPMinimum\Sniffs\Hooks; +use PHPCSUtils\Utils\PassedParameters; use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** @@ -82,9 +83,17 @@ class RestrictedHooksSniff extends AbstractFunctionParameterSniff { * normal file processing. */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + $hook_name_param = PassedParameters::getParameterFromStack( $parameters, 1, 'hook_name' ); + if ( $hook_name_param === false ) { + // Missing required parameter. Nothing to examine. Bow out. + return; + } + + $normalized_hook_name = $this->normalize_hook_name_from_parameter( $hook_name_param ); + foreach ( $this->restricted_hook_groups as $group => $group_args ) { foreach ( $group_args['hooks'] as $hook ) { - if ( $this->normalize_hook_name_from_parameter( $parameters[1] ) === $hook ) { + if ( $normalized_hook_name === $hook ) { $addMethod = 'add' . $group_args['type']; $this->phpcsFile->{$addMethod}( $group_args['msg'], $stackPtr, $hook ); } diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc index 78a16deb..e79810cf 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc @@ -61,3 +61,10 @@ add_filter('http_request_args', 'bad_example_function' ); // Simple string + inc add_action( /*comment*/ 'do_robotstxt', 'my_do_robotstxt'); // Simple string. add_filter( 'robots_txt', function() { // Anonymous callback. } ); + +// Safeguard correct handling of function calls using PHP 8.0+ named parameters. +add_action(callback: 'invalid', priority: 10); // OK, well, not really, missing required $hook_name param, but that's not the concern of this sniff. +add_action(callback: 'do_robotstxt', hook_name: 'not_our_target'); // OK. +add_action(hookName: 'not_our_target', callback: 'do_robotstxt',); // OK, well, not really, typo in param name, but that's not the concern of the sniff. + +add_filter(priority: 10, hook_name: 'robots_txt', callback: some_function(...) ); // Warning. diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php index 2d404cd4..05edf684 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php @@ -48,6 +48,7 @@ public function getWarningList() { 60 => 1, 61 => 1, 62 => 1, + 70 => 1, ]; } } From d43835c6b7cb00ff3d666be74f9617d64c6918ab Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 06:28:10 +0200 Subject: [PATCH 060/165] Hooks/RestrictedHooks: use PHPCSUtils MessageHelper --- .../Sniffs/Hooks/RestrictedHooksSniff.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php index 5b8a8466..70902217 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php @@ -9,6 +9,7 @@ namespace WordPressVIPMinimum\Sniffs\Hooks; +use PHPCSUtils\Utils\MessageHelper; use PHPCSUtils\Utils\PassedParameters; use WordPressCS\WordPress\AbstractFunctionParameterSniff; @@ -44,7 +45,7 @@ class RestrictedHooksSniff extends AbstractFunctionParameterSniff { private $restricted_hook_groups = [ 'upload_mimes' => [ // TODO: This error message needs a link to the VIP Documentation, see https://github.com/Automattic/VIP-Coding-Standards/issues/235. - 'type' => 'Warning', + 'type' => 'warning', 'msg' => 'Please ensure that the mimes being filtered do not include insecure types (i.e. SVG, SWF, etc.). Manual inspection required.', 'hooks' => [ 'upload_mimes', @@ -52,7 +53,7 @@ class RestrictedHooksSniff extends AbstractFunctionParameterSniff { ], 'http_request' => [ // https://docs.wpvip.com/technical-references/code-quality-and-best-practices/retrieving-remote-data/. - 'type' => 'Warning', + 'type' => 'warning', 'msg' => 'Please ensure that the timeout being filtered is not greater than 3s since remote requests require the user to wait for completion before the rest of the page will load. Manual inspection required.', 'hooks' => [ 'http_request_timeout', @@ -61,7 +62,7 @@ class RestrictedHooksSniff extends AbstractFunctionParameterSniff { ], 'robotstxt' => [ // https://docs.wpvip.com/how-tos/modify-the-robots-txt-file/. - 'type' => 'Warning', + 'type' => 'warning', 'msg' => 'Don\'t forget to flush the robots.txt cache by going to Settings > Reading and toggling the privacy settings.', 'hooks' => [ 'do_robotstxt', @@ -94,8 +95,8 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p foreach ( $this->restricted_hook_groups as $group => $group_args ) { foreach ( $group_args['hooks'] as $hook ) { if ( $normalized_hook_name === $hook ) { - $addMethod = 'add' . $group_args['type']; - $this->phpcsFile->{$addMethod}( $group_args['msg'], $stackPtr, $hook ); + $isError = ( $group_args['type'] === 'error' ); + MessageHelper::addMessage( $this->phpcsFile, $group_args['msg'], $stackPtr, $isError, $hook ); } } } From c1f6aff6d8faa2c04cbb2a6abfd4556a4e2f93f7 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 06:40:32 +0200 Subject: [PATCH 061/165] Hooks/RestrictedHooks: bug fix - hook names are case-sensitive WordPress stores hooknames as provided as an array key and uses `isset()` to determine whether to run an action/filter. It doesn't "normalize" hook names to lowercase, so neither should the sniff. Includes test to safeguard the fix. Refs: * https://github.com/WordPress/wordpress-develop/blob/c726220a21d13fdb5409372b652c9460c59ce1db/src/wp-includes/plugin.php#L124-L126 * https://github.com/WordPress/wordpress-develop/blob/c726220a21d13fdb5409372b652c9460c59ce1db/src/wp-includes/plugin.php#L173-L210 --- WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php | 4 ++-- WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc | 5 ++++- WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php index 70902217..7211ac83 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php @@ -131,7 +131,7 @@ private function normalize_hook_name_from_parameter( $parameter ) { $hook_name = $parameter['clean']; } - // Remove quotes (double and single), and use lowercase. - return strtolower( str_replace( [ "'", '"' ], '', $hook_name ) ); + // Remove quotes (double and single). + return str_replace( [ "'", '"' ], '', $hook_name ); } } diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc index e79810cf..b271db15 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc @@ -47,7 +47,7 @@ add_filter( 'do_robotstxt', 'bad_example_function' ); // Simple string. add_action('upload_mimes' , [$obj, 'method']); // Incorrect spacing. add_filter( 'robots_txt','bad_example_function'); // Incorrect spacing. \add_filter( "http_request_timeout" , fn($param) => $param * 10); // Double quotes. -add_filter( 'upLoad_mimeS' , $callback); // Uppercase characters. + ADD_FILTER( 'upload_' . 'mimes' ,'bad_example_function'); // Single concatenation. add_filter( 'upl' . 'oad_' . 'mimes' ,'bad_example_function'); // Multiple concatenation. add_filter( "upload_" . 'mimes' , bad_example_function(...)); // Single concatenation with double and single quotes. @@ -68,3 +68,6 @@ add_action(callback: 'do_robotstxt', hook_name: 'not_our_target'); // OK. add_action(hookName: 'not_our_target', callback: 'do_robotstxt',); // OK, well, not really, typo in param name, but that's not the concern of the sniff. add_filter(priority: 10, hook_name: 'robots_txt', callback: some_function(...) ); // Warning. + +// Hook names are case-sensitive. +add_filter( 'upLoad_mimeS' , $callback); // OK, not our target. diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php index 05edf684..95d0129a 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php @@ -37,7 +37,6 @@ public function getWarningList() { 47 => 1, 48 => 1, 49 => 1, - 50 => 1, 51 => 1, 52 => 1, 53 => 1, From 52ee7222082f38e966289204df40c1f916b1ca49 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 07:07:03 +0200 Subject: [PATCH 062/165] Hooks/RestrictedHooks: bug fix - false negatives due to incorrect token walking The `$parameter['start']` key points to the very first token in the parameter, irrespective of what that token is (whitespace, comment, functional). The `$parameter['end']` key points to the very last token in the parameter, irrespective of what that token is (whitespace, comment, functional). This means that when walking the tokens of a parameter both the 'start' token, as well as the 'end' token need to be examined. The code in the `normalize_hook_name_from_parameter()` method handling concatenated hook names did not do this correctly, leading to false negatives. Fixed now. Includes test + updating some pre-existing tests which clearly had a trailing space in the parameter to get the test to pass despite this bug. --- .../Sniffs/Hooks/RestrictedHooksSniff.php | 2 +- .../Tests/Hooks/RestrictedHooksUnitTest.inc | 9 ++++++--- .../Tests/Hooks/RestrictedHooksUnitTest.php | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php index 7211ac83..7900ee55 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php @@ -122,7 +122,7 @@ private function normalize_hook_name_from_parameter( $parameter ) { if ( $concat_ptr ) { $hook_name = ''; - for ( $i = $parameter['start'] + 1; $i < $parameter['end']; $i++ ) { + for ( $i = $parameter['start']; $i <= $parameter['end']; $i++ ) { if ( $this->tokens[ $i ]['code'] === T_CONSTANT_ENCAPSED_STRING ) { $hook_name .= str_replace( [ "'", '"' ], '', $this->tokens[ $i ]['content'] ); } diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc index b271db15..36f6f156 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc @@ -48,10 +48,10 @@ add_action('upload_mimes' , [$obj, 'method']); // Incorrect spacing. add_filter( 'robots_txt','bad_example_function'); // Incorrect spacing. \add_filter( "http_request_timeout" , fn($param) => $param * 10); // Double quotes. -ADD_FILTER( 'upload_' . 'mimes' ,'bad_example_function'); // Single concatenation. -add_filter( 'upl' . 'oad_' . 'mimes' ,'bad_example_function'); // Multiple concatenation. +ADD_FILTER( 'upload_' . 'mimes','bad_example_function'); // Single concatenation. +add_filter( 'upl' . 'oad_' . 'mimes','bad_example_function'); // Multiple concatenation. add_filter( "upload_" . 'mimes' , bad_example_function(...)); // Single concatenation with double and single quotes. -add_filter( 'upl' . "oad_" . "mimes" ,'bad_example_function'); // Multiple concatenation with double and single quotes. +add_filter( 'upl' . "oad_" . "mimes",'bad_example_function'); // Multiple concatenation with double and single quotes. \add_action( 'http_request_args', function() { // Anonymous callback. // Do stuff. }); @@ -71,3 +71,6 @@ add_filter(priority: 10, hook_name: 'robots_txt', callback: some_function(...) ) // Hook names are case-sensitive. add_filter( 'upLoad_mimeS' , $callback); // OK, not our target. + +// Bug fix - spacing vs concatenation. +add_filter('do_' . 'robots' . 'txt', 'bad_example_function'); // Warning. diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php index 95d0129a..8e1c65dc 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php @@ -48,6 +48,7 @@ public function getWarningList() { 61 => 1, 62 => 1, 70 => 1, + 76 => 1, ]; } } From ef84c2070b46cb885a942081b050a530e0b93614 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 07:19:59 +0200 Subject: [PATCH 063/165] Hooks/RestrictedHooks: bug fix - false positives for dynamic name with concatenation As things were, the code in the `normalize_hook_name_from_parameter()` method would check for concatenation and if found, would gather the contents of all `T_CONSTANT_ENCAPSED_STRING` tokens found in the parameter. However, this presumes all the concatenated parts are text strings and disregards the possibility that variables would be concatenated in, leading to false positives. Fixed now. Includes tests. --- .../Sniffs/Hooks/RestrictedHooksSniff.php | 41 ++++++++++--------- .../Tests/Hooks/RestrictedHooksUnitTest.inc | 4 ++ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php index 7900ee55..239600cd 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php @@ -9,6 +9,7 @@ namespace WordPressVIPMinimum\Sniffs\Hooks; +use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Utils\MessageHelper; use PHPCSUtils\Utils\PassedParameters; use WordPressCS\WordPress\AbstractFunctionParameterSniff; @@ -91,6 +92,10 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p } $normalized_hook_name = $this->normalize_hook_name_from_parameter( $hook_name_param ); + if ( $normalized_hook_name === '' ) { + // Dynamic hook name. Cannot reliably determine if it's one of the targets. Bow out. + return; + } foreach ( $this->restricted_hook_groups as $group => $group_args ) { foreach ( $group_args['hooks'] as $hook ) { @@ -107,31 +112,27 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p * * @param array $parameter Array with information about a parameter. * - * @return string Normalized hook name. + * @return string Normalized hook name or an empty string if the hook name could not be determined. */ private function normalize_hook_name_from_parameter( $parameter ) { - // If concatenation is found, build hook name. - $concat_ptr = $this->phpcsFile->findNext( - T_STRING_CONCAT, - $parameter['start'], - $parameter['end'], - false, - null, - true - ); + $allowed_tokens = Tokens::$emptyTokens; + $allowed_tokens += [ + T_STRING_CONCAT => T_STRING_CONCAT, + T_CONSTANT_ENCAPSED_STRING => T_CONSTANT_ENCAPSED_STRING, + ]; - if ( $concat_ptr ) { - $hook_name = ''; - for ( $i = $parameter['start']; $i <= $parameter['end']; $i++ ) { - if ( $this->tokens[ $i ]['code'] === T_CONSTANT_ENCAPSED_STRING ) { - $hook_name .= str_replace( [ "'", '"' ], '', $this->tokens[ $i ]['content'] ); - } + $has_disallowed_token = $this->phpcsFile->findNext( $allowed_tokens, $parameter['start'], ( $parameter['end'] + 1 ), true ); + if ( $has_disallowed_token !== false ) { + return ''; + } + + $hook_name = ''; + for ( $i = $parameter['start']; $i <= $parameter['end']; $i++ ) { + if ( $this->tokens[ $i ]['code'] === T_CONSTANT_ENCAPSED_STRING ) { + $hook_name .= str_replace( [ "'", '"' ], '', $this->tokens[ $i ]['content'] ); } - } else { - $hook_name = $parameter['clean']; } - // Remove quotes (double and single). - return str_replace( [ "'", '"' ], '', $hook_name ); + return $hook_name; } } diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc index 36f6f156..e90c5625 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc @@ -74,3 +74,7 @@ add_filter( 'upLoad_mimeS' , $callback); // OK, not our target. // Bug fix - spacing vs concatenation. add_filter('do_' . 'robots' . 'txt', 'bad_example_function'); // Warning. + +// Ignore partially dynamic hook names. +add_filter( 'robots_' . $something . 'txt' , $callback); // OK, ignored as undetermined. +add_filter( 'http_request_timeout' . $something, $callback); // OK, ignored as undetermined. From 32623e772933f5ed28c880f47e464ab0b6dfc944 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 02:08:09 +0200 Subject: [PATCH 064/165] Hooks/RestrictedHooks: bug fix - quotes within a hook name would be stripped Use PHPCSUtils `TextStrings::stripQuotes()` to only strip surrounding quotes and not remove any quotes potentially present _within_ the text string. Includes tests. --- WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php | 3 ++- WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php index 239600cd..2ced308b 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/RestrictedHooksSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Utils\MessageHelper; use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** @@ -129,7 +130,7 @@ private function normalize_hook_name_from_parameter( $parameter ) { $hook_name = ''; for ( $i = $parameter['start']; $i <= $parameter['end']; $i++ ) { if ( $this->tokens[ $i ]['code'] === T_CONSTANT_ENCAPSED_STRING ) { - $hook_name .= str_replace( [ "'", '"' ], '', $this->tokens[ $i ]['content'] ); + $hook_name .= TextStrings::stripQuotes( $this->tokens[ $i ]['content'] ); } } diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc index e90c5625..6eab3fdd 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.inc @@ -78,3 +78,7 @@ add_filter('do_' . 'robots' . 'txt', 'bad_example_function'); // Warning. // Ignore partially dynamic hook names. add_filter( 'robots_' . $something . 'txt' , $callback); // OK, ignored as undetermined. add_filter( 'http_request_timeout' . $something, $callback); // OK, ignored as undetermined. + +// Ensure quote stripping is done correctly. +add_filter( 'upload"_mimes', 'bad_example_function' ); // OK, not a filter we're looking for. +add_filter( "upload_'mimes", 'bad_example_function' ); // OK, not a filter we're looking for. From b52dad564c657ca6557edac7f30de1b50933bab6 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 17:00:20 +0200 Subject: [PATCH 065/165] Performance/LowExpiryCacheTime: move a parse error test to own test case file --- ...t.inc => LowExpiryCacheTimeUnitTest.1.inc} | 2 +- .../LowExpiryCacheTimeUnitTest.2.inc | 5 ++ .../LowExpiryCacheTimeUnitTest.php | 82 ++++++++++--------- 3 files changed, 51 insertions(+), 38 deletions(-) rename WordPressVIPMinimum/Tests/Performance/{LowExpiryCacheTimeUnitTest.inc => LowExpiryCacheTimeUnitTest.1.inc} (98%) create mode 100644 WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.2.inc diff --git a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.inc b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc similarity index 98% rename from WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.inc rename to WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc index a6572d6d..37a12138 100644 --- a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc @@ -86,7 +86,7 @@ wp_cache_add( 'test', $data, $group, WEEK_IN_SECONDS / 3 + HOUR_IN_SECONDS ); / wp_cache_set( $key, $data, '', (24 * 60 * 60) ); // OK. wp_cache_set( $key, $data, '', (-(2 * 60) + 600) ); // OK. wp_cache_set( $key, $data, '', (2 * 60) ); // Bad. -wp_cache_set( $key, $data, '', (-(2 * 60) + 600 ); // OK - includes parse error, close parenthesis missing. + // Test handling of numbers passed as strings. wp_cache_set( 'test', $data, $group, '300' ); // OK - type cast to integer within the function. diff --git a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.2.inc b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.2.inc new file mode 100644 index 00000000..dc33e31f --- /dev/null +++ b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.2.inc @@ -0,0 +1,5 @@ + Key is the line number, value is the number of expected warnings. */ - public function getWarningList() { - return [ - 27 => 1, - 28 => 1, - 29 => 1, - 30 => 1, - 32 => 1, - 33 => 1, - 34 => 1, - 35 => 1, - 37 => 1, - 38 => 1, - 39 => 1, - 40 => 1, - 47 => 1, - 52 => 1, - 56 => 1, - 74 => 1, - 75 => 1, - 76 => 1, - 77 => 1, - 78 => 1, - 79 => 1, - 82 => ( PHP_VERSION_ID > 50600 ) ? 0 : 1, - 88 => 1, - 94 => 1, - 95 => 1, - 105 => 1, - 112 => 1, - 113 => 1, - 114 => 1, - 115 => 1, - 116 => 1, - 119 => 1, - 120 => 1, - 123 => 1, - ]; + public function getWarningList( $testFile = '' ) { + switch ( $testFile ) { + case 'LowExpiryCacheTimeUnitTest.1.inc': + return [ + 27 => 1, + 28 => 1, + 29 => 1, + 30 => 1, + 32 => 1, + 33 => 1, + 34 => 1, + 35 => 1, + 37 => 1, + 38 => 1, + 39 => 1, + 40 => 1, + 47 => 1, + 52 => 1, + 56 => 1, + 74 => 1, + 75 => 1, + 76 => 1, + 77 => 1, + 78 => 1, + 79 => 1, + 82 => ( PHP_VERSION_ID > 50600 ) ? 0 : 1, + 88 => 1, + 94 => 1, + 95 => 1, + 105 => 1, + 112 => 1, + 113 => 1, + 114 => 1, + 115 => 1, + 116 => 1, + 119 => 1, + 120 => 1, + 123 => 1, + ]; + + default: + return []; + } } } From 3043a9da92c83de1f7c74314b74791bbd4e9d153 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 07:53:06 +0200 Subject: [PATCH 066/165] Performance/LowExpiryCacheTime: improve the tests * Add some more variations to the pre-existing tests: - Non-lowercase function call(s). - Fully qualified function calls. - Use PHP 7.3+ trailing comma's in a few function calls. * Add tests for some edge-cases (empty text string, parse error). * Add tests with a few additional PHP syntaxes to document these are handled correctly. --- .../LowExpiryCacheTimeUnitTest.1.inc | 36 +++++++++++++------ .../LowExpiryCacheTimeUnitTest.php | 4 +++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc index 37a12138..0835799b 100644 --- a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc @@ -10,12 +10,12 @@ wp_cache_set( $testing, $data, 'test_group', 5*MINUTE_IN_SECONDS ); wp_cache_set( 123, $data, 'test_group', 5 * MINUTE_IN_SECONDS ); wp_cache_set( 1234, $data, '', 425 ); wp_cache_set( $testing, $data, null, 350 ); -wp_cache_set( $testing, $data ); +\wp_cache_set( $testing, $data ); wp_cache_set( 'test', $data, $group ); wp_cache_add( 'test', $data, $group, 300 ); wp_cache_add( $testing, $data, 'test_group', 6*MINUTE_IN_SECONDS ); -wp_cache_add( 1234, $data, '', 425 ); +WP_CACHE_ADD( 1234, $data, '', 425 ); wp_cache_add( $testing, $data, null, 350 ); wp_cache_replace( 'test', $data, $group, 300 ); @@ -30,13 +30,13 @@ wp_cache_set( 123, $data, null, 1.5 * MINUTE_IN_SECONDS ); // Lower than 300. wp_cache_set( $testing, $data, '', 1.5 * MINUTE_IN_SECONDS ); // Lower than 300. wp_cache_add( 'test', $data, $group, 100 ); // Lower than 300. -wp_cache_add( 'test', $data, $group, 2*MINUTE_IN_SECONDS ); // Lower than 300. +\wp_cache_add( 'test', $data, $group, 2*MINUTE_IN_SECONDS ); // Lower than 300. wp_cache_add( 123, $data, null, 1.5 * MINUTE_IN_SECONDS ); // Lower than 300. wp_cache_add( $testing, $data, '', 1.5 * MINUTE_IN_SECONDS ); // Lower than 300. wp_cache_replace( 'test', $data, $group, 100 ); // Lower than 300. wp_cache_replace( 'test', $data, $group, 2*MINUTE_IN_SECONDS ); // Lower than 300. -wp_cache_replace( 123, $data, null, 1.5 * MINUTE_IN_SECONDS ); // Lower than 300. +WP_CACHE_REPLACE( 123, $data, null, 1.5 * MINUTE_IN_SECONDS ); // Lower than 300. wp_cache_replace( $testing, $data, '', 1.5 * MINUTE_IN_SECONDS ); // Lower than 300. // Test error being reported on the line containing the parameter. @@ -49,7 +49,7 @@ wp_cache_replace( // Test calculations with floats. wp_cache_replace( $testing, $data, '', 7.5 * MINUTE_IN_SECONDS ); // OK. -wp_cache_replace( $testing, $data, '', 500 * 0.1 ); // Bad. +wp_cache_replace( $testing, $data, '', 500 * 0.1, ); // Bad. // Test comment handling. wp_cache_add( 'test', $data, $group, /* Deliberately left empty */ ); // OK. @@ -71,11 +71,11 @@ wp_cache_add( ); // OK. // Test variable/constant with or without calculation being passed. -wp_cache_set( $key, $data, '', $time ); // Manual inspection warning. +WP_Cache_Set( $key, $data, '', $time ); // Manual inspection warning. wp_cache_set( $key, $data, '', PREFIX_FIVE_MINUTES ); // Manual inspection warning. wp_cache_set( $key, $data, '', 20 * $time ); // Manual inspection warning. -wp_cache_set( $key, $data, '', $base + $extra ); // Manual inspection warning. -wp_cache_set( $key, $data, '', 300 + $extra ); // Manual inspection warning. +wp_cache_set( $key, $data, '', $base + $extra, ); // Manual inspection warning. +\wp_cache_set( $key, $data, '', 300 + $extra ); // Manual inspection warning. wp_cache_set( $key, $data, '', PREFIX_CUSTOM_TIME * 5); // Manual inspection warning. // Test calculations with additional aritmetic operators. @@ -84,7 +84,7 @@ wp_cache_add( 'test', $data, $group, WEEK_IN_SECONDS / 3 + HOUR_IN_SECONDS ); / // Test calculations grouped with parentheses. wp_cache_set( $key, $data, '', (24 * 60 * 60) ); // OK. -wp_cache_set( $key, $data, '', (-(2 * 60) + 600) ); // OK. +wp_cache_set( $key, $data, '', (-(2 * 60) + 600), ); // OK. wp_cache_set( $key, $data, '', (2 * 60) ); // Bad. @@ -113,7 +113,7 @@ wp_cache_set( 'test', $data, $group, month_in_seconds ); // Bad - constants are wp_cache_set( 'test', $data, $group, HOUR_IN_SECONDS::methodName() ); // Bad - not a constant. wp_cache_set( 'test', $data, $group, $obj->MONTH_IN_SECONDS ); // Bad - not a constant. wp_cache_set( 'test', $data, $group, $obj::MONTH_IN_SECONDS ); // Bad - not the WP constant. -wp_cache_set( 'test', $data, $group, PluginNamespace\SubLevel\DAY_IN_SECONDS ); // Bad - not the WP constant. +WP_Cache_Set( 'test', $data, $group, PluginNamespace\SubLevel\DAY_IN_SECONDS, ); // Bad - not the WP constant. // Test passing negative number as cache time. wp_cache_set( 'test', $data, $group, -300 ); // Bad. @@ -121,3 +121,19 @@ wp_cache_add( $testing, $data, 'test_group', -6 * MINUTE_IN_SECONDS ); // Bad. // Test more complex logic in the parameter. wp_cache_add( $key, $data, '', ($toggle ? 200 : 400) ); // Manual inspection warning. + +// Test handling of non-numeric data in text string. +wp_cache_set( 'test', $data, $group, '' ); // Manual inspection warning. +wp_cache_set( 'test', $data, $group, '300 Mulberry Street' ); // Manual inspection warning. + +// Test handling of edge case/parse error. +wp_cache_set( 'test', $data, $group,\); // OK, ignore. + +// Test handling of some modern PHP syntaxes. +wp_cache_add('test', $data, $group, ...$params); // PHP 5.6 argument unpacking. Manual inspection warning. +wp_cache_add( $key, $data, '', $toggle ?? 400) ); // PHP 7.0 null coalesce. Manual inspection warning. +add_action('my_action', wp_cache_set(...)); // PHP 8.1 first class callable. OK, ignore. + +// Looks like a function call, but is a PHP 8.0+ class instantiation via an attribute. +#[WP_Cache_Replace('text', 'data', 'group', 50)] +function foo() {} diff --git a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.php b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.php index a63d2e51..c3a28c0f 100644 --- a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.php @@ -70,6 +70,10 @@ public function getWarningList( $testFile = '' ) { 119 => 1, 120 => 1, 123 => 1, + 126 => 1, + 127 => 1, + 133 => 1, + 134 => 1, ]; default: From 7c5899bf01999f3d52f089b352c1a843f1d18f83 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 07:53:06 +0200 Subject: [PATCH 067/165] Performance/LowExpiryCacheTime: add support for PHP 7.4+ numeric literals and 8.1+ octal literals While PHPCS fully supports these syntaxes at the tokenizer level, this sniff includes the contents of the found token in a string which will be run through `eval()`, which means that if the number was originally provided in a PHP 7.4+ syntax and the sniffs are run on PHP 7.2, the `eval` will result in a fatal error, breaking the sniff (and possibly the PHPCS run). By using the PHPCSUtils `Numbers` class, we can bypass this and retrieve the decimal value of a integer/float, no matter in what syntax the number is provided. If we then use the decimal value in the string which will be run through `eval()`, the potential fatal error is avoided. Includes tests. --- .../Sniffs/Performance/LowExpiryCacheTimeSniff.php | 7 +++++-- .../Performance/LowExpiryCacheTimeUnitTest.1.inc | 13 +++++++++++++ .../Performance/LowExpiryCacheTimeUnitTest.php | 5 +++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php b/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php index 2bb6d4ce..44e5f79c 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php @@ -10,6 +10,7 @@ namespace WordPressVIPMinimum\Sniffs\Performance; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\Numbers; use PHPCSUtils\Utils\TextStrings; use WordPressCS\WordPress\AbstractFunctionParameterSniff; @@ -104,8 +105,10 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p if ( $this->tokens[ $i ]['code'] === T_LNUMBER || $this->tokens[ $i ]['code'] === T_DNUMBER ) { - // Integer or float. - $tokensAsString .= $this->tokens[ $i ]['content']; + // Make sure that PHP 7.4 numeric literals and PHP 8.1 explicit octals don't cause problems. + $number_info = Numbers::getCompleteNumber( $this->phpcsFile, $i ); + $tokensAsString .= $number_info['decimal']; + $i = $number_info['last_token']; continue; } diff --git a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc index 0835799b..10266103 100644 --- a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc @@ -137,3 +137,16 @@ add_action('my_action', wp_cache_set(...)); // PHP 8.1 first class callable. OK, // Looks like a function call, but is a PHP 8.0+ class instantiation via an attribute. #[WP_Cache_Replace('text', 'data', 'group', 50)] function foo() {} + +// Alternative numeric base. +wp_cache_set( $key, $data, '', 0620 ); // Octal number. OK (=400). +wp_cache_set( $key, $data, '', 0x190 ); // Hexidecimal number. OK (=400). +wp_cache_set( $key, $data, '', 0b110010000 ); // PHP 5.4 binary number. OK (=400). +wp_cache_set( $key, $data, '', 1_000 ); // PHP 7.4 numeric literal with underscore. OK. +wp_cache_set( $key, $data, '', 0o620 ); // PHP 8.1 octal literal. OK (=400). + +wp_cache_set( $key, $data, '', 0226 ); // Octal number. Bad (=150). +wp_cache_set( $key, $data, '', 0x96 ); // Hexidecimal number. Bad (=150). +wp_cache_set( $key, $data, '', 0b10010110 ); // PHP 5.4 binary number. Bad (=150). +wp_cache_set( $key, $data, '', 1_50 ); // PHP 7.4 numeric literal with underscore. Bad. +wp_cache_set( $key, $data, '', 0o226 ); // PHP 8.1 octal literal. Bad (=150). diff --git a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.php b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.php index c3a28c0f..53351e48 100644 --- a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.php @@ -74,6 +74,11 @@ public function getWarningList( $testFile = '' ) { 127 => 1, 133 => 1, 134 => 1, + 148 => 1, + 149 => 1, + 150 => 1, + 151 => 1, + 152 => 1, ]; default: From 38c0bdbf72810ee5c4bcc1f8375b759dd1d75313 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 01:41:35 +0200 Subject: [PATCH 068/165] Performance/LowExpiryCacheTime: cleaner error message The PHPCSUtils `PassedParameters::getParameters()` return value includes a `'clean'` array index, which contains the contents of the parameter stripped of surrounding whitespace and comments. The `LowExpiryCacheTime` sniff uses the parameter contents in the error message, so using the `clean` value makes for a more readable error message. Includes updating two pre-existing tests. Before: ``` 76 | WARNING | Cache expiry time could not be determined. Please inspect that the fourth parameter passed to wp_cache_set() evaluates to 300 | | seconds or more. Found: "20 * $time /*comment*/" (WordPressVIPMinimum.Performance.LowExpiryCacheTime.CacheTimeUndetermined) ``` After: ``` 76 | WARNING | Cache expiry time could not be determined. Please inspect that the fourth parameter passed to wp_cache_set() evaluates to 300 | | seconds or more. Found: "20 * $time" (WordPressVIPMinimum.Performance.LowExpiryCacheTime.CacheTimeUndetermined) ``` --- .../Sniffs/Performance/LowExpiryCacheTimeSniff.php | 2 +- .../Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php b/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php index 44e5f79c..d45df674 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php @@ -80,7 +80,7 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $message = 'Cache expiry time could not be determined. Please inspect that the fourth parameter passed to %s() evaluates to 300 seconds or more. Found: "%s"'; $error_code = 'CacheTimeUndetermined'; - $data = [ $matched_content, $parameters[4]['raw'] ]; + $data = [ $matched_content, $parameters[4]['clean'] ]; for ( $i = $param['start']; $i <= $param['end']; $i++ ) { if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) === true ) { diff --git a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc index 10266103..44e5f722 100644 --- a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc @@ -73,7 +73,7 @@ wp_cache_add( // Test variable/constant with or without calculation being passed. WP_Cache_Set( $key, $data, '', $time ); // Manual inspection warning. wp_cache_set( $key, $data, '', PREFIX_FIVE_MINUTES ); // Manual inspection warning. -wp_cache_set( $key, $data, '', 20 * $time ); // Manual inspection warning. +wp_cache_set( $key, $data, '', 20 * $time /*comment*/ ); // Manual inspection warning. wp_cache_set( $key, $data, '', $base + $extra, ); // Manual inspection warning. \wp_cache_set( $key, $data, '', 300 + $extra ); // Manual inspection warning. wp_cache_set( $key, $data, '', PREFIX_CUSTOM_TIME * 5); // Manual inspection warning. @@ -110,7 +110,7 @@ wp_cache_set( 'test', $data, $group, \MONTH_IN_SECONDS ); // OK. // Test passing something which may look like one of the time constants, but isn't. wp_cache_set( 'test', $data, $group, month_in_seconds ); // Bad - constants are case-sensitive. -wp_cache_set( 'test', $data, $group, HOUR_IN_SECONDS::methodName() ); // Bad - not a constant. +wp_cache_set( 'test', $data, $group, /*comment*/ HOUR_IN_SECONDS::methodName() ); // Bad - not a constant. wp_cache_set( 'test', $data, $group, $obj->MONTH_IN_SECONDS ); // Bad - not a constant. wp_cache_set( 'test', $data, $group, $obj::MONTH_IN_SECONDS ); // Bad - not the WP constant. WP_Cache_Set( 'test', $data, $group, PluginNamespace\SubLevel\DAY_IN_SECONDS, ); // Bad - not the WP constant. From 8acb4ae3baae1747c25be581241366a620351f96 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 17:17:44 +0200 Subject: [PATCH 069/165] Performance/LowExpiryCacheTime: add support for handling PHP 8.0+ function calls using named parameters Includes tests. --- .../Sniffs/Performance/LowExpiryCacheTimeSniff.php | 9 +++++---- .../Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc | 9 +++++++++ .../Tests/Performance/LowExpiryCacheTimeUnitTest.php | 2 ++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php b/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php index d45df674..340c89a2 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/LowExpiryCacheTimeSniff.php @@ -11,6 +11,7 @@ use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Utils\Numbers; +use PHPCSUtils\Utils\PassedParameters; use PHPCSUtils\Utils\TextStrings; use WordPressCS\WordPress\AbstractFunctionParameterSniff; @@ -68,21 +69,21 @@ class LowExpiryCacheTimeSniff extends AbstractFunctionParameterSniff { * normal file processing. */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - if ( isset( $parameters[4] ) === false ) { + $expire_param = PassedParameters::getParameterFromStack( $parameters, 4, 'expire' ); + if ( $expire_param === false ) { // If no cache expiry time, bail (i.e. we don't want to flag for something like feeds where it is cached indefinitely until a hook runs). return; } - $param = $parameters[4]; $tokensAsString = ''; $reportPtr = null; $openParens = 0; $message = 'Cache expiry time could not be determined. Please inspect that the fourth parameter passed to %s() evaluates to 300 seconds or more. Found: "%s"'; $error_code = 'CacheTimeUndetermined'; - $data = [ $matched_content, $parameters[4]['clean'] ]; + $data = [ $matched_content, $expire_param['clean'] ]; - for ( $i = $param['start']; $i <= $param['end']; $i++ ) { + for ( $i = $expire_param['start']; $i <= $expire_param['end']; $i++ ) { if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) === true ) { $tokensAsString .= ' '; continue; diff --git a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc index 44e5f722..603881ad 100644 --- a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.1.inc @@ -150,3 +150,12 @@ wp_cache_set( $key, $data, '', 0x96 ); // Hexidecimal number. Bad (=150). wp_cache_set( $key, $data, '', 0b10010110 ); // PHP 5.4 binary number. Bad (=150). wp_cache_set( $key, $data, '', 1_50 ); // PHP 7.4 numeric literal with underscore. Bad. wp_cache_set( $key, $data, '', 0o226 ); // PHP 8.1 octal literal. Bad (=150). + +// Safeguard handling of function calls using PHP 8.0+ named parameters. +wp_cache_add(data: $data, group: $group); // OK, well, not really, missing required $key param, but that's not the concern of this sniff. +wp_cache_replace(data: $data, expire: 400, group: $group); // OK. +wp_cache_add($key, group: $group, data: $data, expires: 100,); // OK, well, not really, typo in param name, but that's not the concern of the sniff. +wp_cache_replace($key, expire: 400, group: $group, data: 100,); // OK. + +wp_cache_replace($key, $data, expire: 100 ); // Bad. +wp_cache_replace(expire: 100, data: $data, key: $group); // Bad. diff --git a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.php b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.php index 53351e48..060cff0d 100644 --- a/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/LowExpiryCacheTimeUnitTest.php @@ -79,6 +79,8 @@ public function getWarningList( $testFile = '' ) { 150 => 1, 151 => 1, 152 => 1, + 160 => 1, + 161 => 1, ]; default: From 174a08bfdfc7a3d11ccb1fcaaf22ab1ad10dfd79 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Jul 2025 18:57:33 +0200 Subject: [PATCH 070/165] Functions/StripTags: improve the tests * Test against false positives for calls to methods or namespaced function calls. * Test against false positives for attribute class using the same name as the function. * Ensure function import `use` statements are not flagged. We're not interested in those. * Add some more variations to the pre-existing tests: - Non-lowercase function call(s). - Fully qualified function calls. - Use PHP 7.3+ trailing comma's in a few function calls. Includes cleaning up some "Warning" comments which contained irrelevant information as the sniff isn't looking for the things which were previously mentioned. --- .../Tests/Functions/StripTagsUnitTest.inc | 32 +++++++++++++++---- .../Tests/Functions/StripTagsUnitTest.php | 10 +++--- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc index 1e625c4e..b599439e 100644 --- a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc @@ -1,13 +1,33 @@ haxx0red'; -$html = '
'; +/* + * Not the sniff target. + */ +use strip_tags; +use MyNs\{ + function strip_tags, +}; + +my\ns\strip_tags($a, $b); +$this->strip_tags($a, $b); +$this?->strip_tags($a, $b); +MyClass::strip_tags($a, $b); +echo STRIP_TAGS; +namespace\strip_tags($a, $b); + +// Looks like a function call, but is a PHP 8.0+ class instantiation via an attribute. +#[Strip_tags('text')] +function foo() {} strip_tag( 'Test', $html ); // Ok - similarly-named function. wp_strip_all_tags( $string ); // Ok. + +/* + * These should all be flagged with a warning. + */ strip_tags( 'Testing' ); // Warning. -strip_tags( 'Test', $html ); // Warning. -strip_tags( 'Test' . ', ' . 'HTML' ); // Warning - concatenation on first parameter. -strip_tags( 'Test, String', $html ); // Warning - comma in first parameter. -strip_tags( $string ); // Warning. +STRIP_TAGS( 'Test', $html ); // Warning. +\strip_tags( 'Test' . ', ' . 'HTML', ); // Warning. +strip_tags( 'Test, String', $html, ); // Warning. +Strip_Tags( $string ); // Warning. diff --git a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php index 3dfe3703..0dff19b1 100644 --- a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php @@ -32,11 +32,11 @@ public function getErrorList() { */ public function getWarningList() { return [ - 9 => 1, - 10 => 1, - 11 => 1, - 12 => 1, - 13 => 1, + 29 => 1, + 30 => 1, + 31 => 1, + 32 => 1, + 33 => 1, ]; } } From ece1b1ea5b5390d50c407619ad7027e23bb5ccaa Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Jul 2025 19:16:25 +0200 Subject: [PATCH 071/165] Functions/StripTags: always flag use of the function This function should never be used, so should always be flagged, no matter whether the code is (not yet) valid. Realistically, the existing `StripTagsOneParameter` error code should be replaced with the new `Used` error code, but that would be a breaking change, which would have to wait for the next major (if deemed worth it). I checked via a code search and the error code as-is, is [referenced in various code bases](https://github.com/search?q=StripTagsOneParameter&type=code), so I'm maintaining BC for now. --- .../Sniffs/Functions/StripTagsSniff.php | 33 ++++++++++++++++--- .../Tests/Functions/StripTagsUnitTest.inc | 3 ++ .../Tests/Functions/StripTagsUnitTest.php | 1 + 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php b/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php index ce9fb845..4090cbfd 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php @@ -43,16 +43,41 @@ class StripTagsSniff extends AbstractFunctionParameterSniff { * in lowercase. * @param array $parameters Array with information about the parameters. * - * @return int|void Integer stack pointer to skip forward or void to continue - * normal file processing. + * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { if ( count( $parameters ) === 1 ) { - $message = '`strip_tags()` does not strip CSS and JS in between the script and style tags. Use `wp_strip_all_tags()` to strip all tags.'; - $this->phpcsFile->addWarning( $message, $stackPtr, 'StripTagsOneParameter' ); + $this->add_warning( $stackPtr, 'StripTagsOneParameter' ); } elseif ( isset( $parameters[2] ) ) { $message = '`strip_tags()` does not strip CSS and JS in between the script and style tags. Use `wp_kses()` instead to allow only the HTML you need.'; $this->phpcsFile->addWarning( $message, $stackPtr, 'StripTagsTwoParameters' ); } } + + /** + * Process the function if no parameters were found. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * + * @return void + */ + public function process_no_parameters( $stackPtr, $group_name, $matched_content ) { + $this->add_warning( $stackPtr ); + } + + /** + * Add a warning if the function is used at all. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $error_code Error code to use for the warning. + * + * @return void + */ + private function add_warning( $stackPtr, $error_code = 'Used' ) { + $message = '`strip_tags()` does not strip CSS and JS in between the script and style tags. Use `wp_strip_all_tags()` to strip all tags.'; + $this->phpcsFile->addWarning( $message, $stackPtr, $error_code ); + } } diff --git a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc index b599439e..44d674a7 100644 --- a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc @@ -31,3 +31,6 @@ STRIP_TAGS( 'Test', $html ); // Warning. \strip_tags( 'Test' . ', ' . 'HTML', ); // Warning. strip_tags( 'Test, String', $html, ); // Warning. Strip_Tags( $string ); // Warning. + +// The function should always be flagged, even during live coding (missing required parameter). +strip_tags(); diff --git a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php index 0dff19b1..37aedc07 100644 --- a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php @@ -37,6 +37,7 @@ public function getWarningList() { 31 => 1, 32 => 1, 33 => 1, + 36 => 1, ]; } } From 043943d83a4fedfc2b0c4fa447422ed8641a6a02 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Jul 2025 19:19:54 +0200 Subject: [PATCH 072/165] Functions/StripTags: document handling of PHP 5.6 argument unpacking This is flagged under the `StripTagsOneParameter` error code. --- WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc | 2 ++ WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php | 1 + 2 files changed, 3 insertions(+) diff --git a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc index 44d674a7..1695cf39 100644 --- a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc @@ -34,3 +34,5 @@ Strip_Tags( $string ); // Warning. // The function should always be flagged, even during live coding (missing required parameter). strip_tags(); + +strip_tags(...$params); // PHP 5.6 argument unpacking. diff --git a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php index 37aedc07..37672646 100644 --- a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php @@ -38,6 +38,7 @@ public function getWarningList() { 32 => 1, 33 => 1, 36 => 1, + 38 => 1, ]; } } From 1318d5620cf67e7df779e7c867e4065388a05df0 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Jul 2025 19:30:23 +0200 Subject: [PATCH 073/165] Functions/StripTags: add support for handling PHP 8.0+ function calls using named parameters This does change the logic of the sniff a little as well, as if there are parameters, but all of those would use incorrect parameter names, we should still want the function call to be flagged. Includes tests. --- .../Sniffs/Functions/StripTagsSniff.php | 10 ++++++++-- .../Tests/Functions/StripTagsUnitTest.inc | 8 ++++++++ .../Tests/Functions/StripTagsUnitTest.php | 5 +++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php b/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php index 4090cbfd..6be47507 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php @@ -9,6 +9,7 @@ namespace WordPressVIPMinimum\Sniffs\Functions; +use PHPCSUtils\Utils\PassedParameters; use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** @@ -46,11 +47,16 @@ class StripTagsSniff extends AbstractFunctionParameterSniff { * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - if ( count( $parameters ) === 1 ) { + $string_param = PassedParameters::getParameterFromStack( $parameters, 1, 'string' ); + $allowed_tags_param = PassedParameters::getParameterFromStack( $parameters, 2, 'allowed_tags' ); + + if ( $string_param !== false && $allowed_tags_param === false ) { $this->add_warning( $stackPtr, 'StripTagsOneParameter' ); - } elseif ( isset( $parameters[2] ) ) { + } elseif ( $allowed_tags_param !== false ) { $message = '`strip_tags()` does not strip CSS and JS in between the script and style tags. Use `wp_kses()` instead to allow only the HTML you need.'; $this->phpcsFile->addWarning( $message, $stackPtr, 'StripTagsTwoParameters' ); + } else { + $this->add_warning( $stackPtr ); } } diff --git a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc index 1695cf39..ebe6e738 100644 --- a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc @@ -36,3 +36,11 @@ Strip_Tags( $string ); // Warning. strip_tags(); strip_tags(...$params); // PHP 5.6 argument unpacking. + +// Safeguard correct handling of function calls using PHP 8.0+ named parameters. +strip_tags(allowed_tags: $allowed, string: $html ); // Warning. +strip_tags(string: $html); // Warning. +strip_tags(allowed_tags: $allowed); // Warning. Invalid function call, but that's not the concern of this sniff. + +\strip_tags(string: $html, allowed_tag: $allowed); // Warning (mind: deliberate typo in param name). +strip_tags(html: $html); // Warning (mind: deliberately using incorrect param name). diff --git a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php index 37672646..e459f6a8 100644 --- a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php @@ -39,6 +39,11 @@ public function getWarningList() { 33 => 1, 36 => 1, 38 => 1, + 41 => 1, + 42 => 1, + 43 => 1, + 45 => 1, + 46 => 1, ]; } } From 914a244845a7c6e7fb3c0ddd0ee7dded93174407 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Jul 2025 19:45:11 +0200 Subject: [PATCH 074/165] Functions/StripTags: add support for handling PHP 8.1 first class callables First class callables are basically syntax sugar for closures. So: ```php add_action('my_action', strip_tags(...)); ``` ... could also be written as: ```php add_action('my_action', function(...$args) { return strip_tags(...$args); }); ``` Example: https://3v4l.org/cra8s#veol As the code would be flagged when used in a closure, it is my opinion, it should also be flagged when used as a first class callable. This commit implements this in a WPCS cross-version compatible manner. Prior to WPCS 3.2.0, first class callables would be send to the `process_no_parameters()` method. As of WPCS 3.2.0, they will be send to the dedicated `process_first_class_callable()` method. See https://github.com/WordPress/WordPress-Coding-Standards/pull/2544 --- .../Sniffs/Functions/StripTagsSniff.php | 14 ++++++++++++++ .../Tests/Functions/StripTagsUnitTest.inc | 2 ++ .../Tests/Functions/StripTagsUnitTest.php | 1 + 3 files changed, 17 insertions(+) diff --git a/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php b/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php index 6be47507..5c1bb62b 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/StripTagsSniff.php @@ -74,6 +74,20 @@ public function process_no_parameters( $stackPtr, $group_name, $matched_content $this->add_warning( $stackPtr ); } + /** + * Process the function if it is used as a first class callable. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * + * @return void + */ + public function process_first_class_callable( $stackPtr, $group_name, $matched_content ) { + $this->add_warning( $stackPtr ); + } + /** * Add a warning if the function is used at all. * diff --git a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc index ebe6e738..2636026c 100644 --- a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.inc @@ -44,3 +44,5 @@ strip_tags(allowed_tags: $allowed); // Warning. Invalid function call, but that' \strip_tags(string: $html, allowed_tag: $allowed); // Warning (mind: deliberate typo in param name). strip_tags(html: $html); // Warning (mind: deliberately using incorrect param name). + +add_action('my_action', strip_tags(...)); // PHP 8.1 first class callable. diff --git a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php index e459f6a8..cd5ebf6c 100644 --- a/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Functions/StripTagsUnitTest.php @@ -44,6 +44,7 @@ public function getWarningList() { 43 => 1, 45 => 1, 46 => 1, + 48 => 1, ]; } } From 680aa31f0c70d7f013986e0efa755353a5dcf4f2 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 21 Jul 2025 01:25:42 +0200 Subject: [PATCH 075/165] Performance/FetchingRemoteData: extend AbstractFunctionParameterSniff As things were, the determination of whether or not a `T_STRING` is a call to the global PHP native `str_replace()` function was severely flawed. By switching the sniff over to be based on the WordPressCS `AbstractFunctionParameterSniff` class, this flaw is mitigated. Includes adding a slew of additional tests, some of which (line 8 - 13, 28, 37, 45) are specific to the flaw being addressed in this commit. Additionally, the tests have been made more comprehensive and varied by: * Testing against false positives for calls to methods or namespaced function calls (= the issue being addressed in this PR). * Testing against false positives for attribute class using the same name as the function. * Ensure function import `use` statements are not flagged. We're not interested in those. * Adding more variations to the pre-existing tests: - Non-lowercase function call(s). - Fully qualified function calls. - Use PHP 7.3+ trailing comma's in a few function calls. - Use both single quoted as well as double quoted text strings in the tests. - Use other non-plain-text tokens in the passed parameter to test the `FileGetContentsUnknown` warning, which was previously untested. - Multi-line function call. - Comments in function call. --- .../Performance/FetchingRemoteDataSniff.php | 47 ++++++++++++------- .../FetchingRemoteDataUnitTest.inc | 45 ++++++++++++++++-- .../FetchingRemoteDataUnitTest.php | 8 +++- 3 files changed, 79 insertions(+), 21 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php index 358ff2ca..4db4f752 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php @@ -10,45 +10,58 @@ namespace WordPressVIPMinimum\Sniffs\Performance; use PHP_CodeSniffer\Util\Tokens; -use WordPressVIPMinimum\Sniffs\Sniff; +use PHPCSUtils\Utils\PassedParameters; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** * Restricts usage of file_get_contents(). */ -class FetchingRemoteDataSniff extends Sniff { +class FetchingRemoteDataSniff extends AbstractFunctionParameterSniff { /** - * Returns an array of tokens this test wants to listen for. + * The group name for this group of functions. * - * @return array + * @var string */ - public function register() { - return [ T_STRING ]; - } + protected $group_name = 'file_get_contents'; /** - * Process this test when one of its tokens is encountered. + * Functions this sniff is looking for. * - * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @var array Key is the function name, value irrelevant. + */ + protected $target_functions = [ + 'file_get_contents' => true, + ]; + + /** + * Process the parameters of a matched function. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. * * @return void */ - public function process_token( $stackPtr ) { - - $functionName = $this->tokens[ $stackPtr ]['content']; - if ( $functionName !== 'file_get_contents' ) { + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { + $filename_param = PassedParameters::getParameterFromStack( $parameters, 1, 'filename' ); + if ( $filename_param === false ) { + // Missing required parameter. Probably live coding, nothing to examine (yet). Bow out. return; } - $data = [ $this->tokens[ $stackPtr ]['content'] ]; + $data = [ $matched_content ]; - $fileNameStackPtr = $this->phpcsFile->findNext( Tokens::$stringTokens, $stackPtr + 1, null, false, null, true ); - if ( $fileNameStackPtr === false ) { + $has_text_string = $this->phpcsFile->findNext( Tokens::$stringTokens, $filename_param['start'], ( $filename_param['end'] + 1 ) ); + if ( $has_text_string === false ) { $message = '`%s()` is highly discouraged for remote requests, please use `wpcom_vip_file_get_contents()` or `vip_safe_wp_remote_get()` instead. If it\'s for a local file please use WP_Filesystem instead.'; $this->phpcsFile->addWarning( $message, $stackPtr, 'FileGetContentsUnknown', $data ); + return; } - $fileName = $this->tokens[ $fileNameStackPtr ]['content']; + $fileName = $this->tokens[ $has_text_string ]['content']; $isRemoteFile = ( strpos( $fileName, '://' ) !== false ); if ( $isRemoteFile === true ) { diff --git a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc index 9edbd772..e2957f79 100644 --- a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc @@ -1,7 +1,46 @@ file_get_contents($url); +$this?->file_get_contents($file); +MyClass::file_get_contents($file); +echo FILE_GET_CONTENTS; +namespace\file_get_contents($url); -$external_resource = file_get_contents( 'https://example.com' ); // NOK. \ No newline at end of file + +/* + * These should all be okay. + */ +$file_content = file_get_contents( 'my-file.css' ); +$file_content = \File_Get_Contents( /*comment*/ 'my-file.svg', ); +$file_content = file_get_contents( __DIR__ . "/filename.css" ); + +// Looks like a function call, but is a PHP 8.0+ class instantiation via an attribute. +#[File_Get_Contents('text')] +function foo() {} + +// Incomplete function call, should be ignored by the sniff. +$live_coding = file_get_contents(); + + +/* + * These should all be flagged with a warning. + */ +// Warning FileGetContentsRemoteFile. +$external_resource = file_get_contents( 'https://example.com', ); +\file_get_contents( "http://$url" ); +$external_resource = FILE_GET_CONTENTS( + // Sort of protocol relative URL (the ":" is superfluous). + '://example.com' +); +$external_resource = file_get_contents( 'HTTP://example.com', ); + +// Warning FileGetContentsUnknown. +file_get_contents( $url, true, $context, $offset, $length ); +\File_GET_Contents( $obj->get_fileName() ); +file_get_contents( MyClass::FILE_NAME, ); diff --git a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php index 05c27d4d..428ca980 100644 --- a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php @@ -32,7 +32,13 @@ public function getErrorList() { */ public function getWarningList() { return [ - 7 => 1, + 35 => 1, + 36 => 1, + 37 => 1, + 41 => 1, + 44 => 1, + 45 => 1, + 46 => 1, ]; } } From 4f190eb4667ecf55fdc7b242f5b888ee81807212 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 21 Jul 2025 01:35:54 +0200 Subject: [PATCH 076/165] Performance/FetchingRemoteData: add tests for PHP 5.6 arg unpacking and PHP 8.0 named args Both are already handled correctly after the switch to the WordPressCS `AbstractFunctionParameterSniff` class. --- .../FetchingRemoteDataUnitTest.inc | 31 +++++++++++++++++++ .../FetchingRemoteDataUnitTest.php | 3 ++ 2 files changed, 34 insertions(+) diff --git a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc index e2957f79..e42ce125 100644 --- a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc @@ -44,3 +44,34 @@ $external_resource = file_get_contents( 'HTTP://example.com', ); file_get_contents( $url, true, $context, $offset, $length ); \File_GET_Contents( $obj->get_fileName() ); file_get_contents( MyClass::FILE_NAME, ); + +file_get_contents(...$params); // PHP 5.6 argument unpacking. Warning FileGetContentsUnknown. + +// Safeguard correct handling of function calls using PHP 8.0+ named parameters. +file_get_contents(context: $context, ); // OK, well, not really, missing required $filename param, but that's not the concern of this sniff. +file_get_contents(file: 'https://example.com',); // OK, well, not really, typo in param name, but that's not the concern of the sniff. +file_get_contents( + context: stream_context_create( + options: [ + $proxy => 'tcp://proxy.example.com:5100', + ], + ), + filename: 'my-file.svg', +); // OK. + +file_get_contents( + context: stream_context_create( + options: [ + $cafile => __DIR__ . '/cacert.pem', + ], + ), + filename: 'https://example.com' +); // Warning FileGetContentsRemoteFile. +file_get_contents( + context: stream_context_create( + options: [ + $cafile => __DIR__ . '/cacert.pem', + ], + ), + filename: $fileName, +); // Warning FileGetContentsUnknown. diff --git a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php index 428ca980..c7537e84 100644 --- a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php @@ -39,6 +39,9 @@ public function getWarningList() { 44 => 1, 45 => 1, 46 => 1, + 48 => 1, + 62 => 1, + 70 => 1, ]; } } From d9f59555ecd305a33273c57f4888b0e6dc29fc96 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 21 Jul 2025 01:52:56 +0200 Subject: [PATCH 077/165] Performance/FetchingRemoteData: add support for handling PHP 8.1 first class callables As first class callables will result in the function being called, but without us knowing the passed parameters, I believe it is appropriate to flag the use of `file_get_contents()` as a first class callable under the `FileGetContentsUnknown` warning. This commit implements this in a WPCS cross-version compatible manner. Prior to WPCS 3.2.0, first class callables would be send to the `process_no_parameters()` method. As of WPCS 3.2.0, they will be send to the dedicated `process_first_class_callable()` method. --- .../Performance/FetchingRemoteDataSniff.php | 66 ++++++++++++++++++- .../FetchingRemoteDataUnitTest.inc | 2 + .../FetchingRemoteDataUnitTest.php | 1 + 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php index 4db4f752..185c5ac6 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php @@ -56,9 +56,7 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $has_text_string = $this->phpcsFile->findNext( Tokens::$stringTokens, $filename_param['start'], ( $filename_param['end'] + 1 ) ); if ( $has_text_string === false ) { - $message = '`%s()` is highly discouraged for remote requests, please use `wpcom_vip_file_get_contents()` or `vip_safe_wp_remote_get()` instead. If it\'s for a local file please use WP_Filesystem instead.'; - $this->phpcsFile->addWarning( $message, $stackPtr, 'FileGetContentsUnknown', $data ); - return; + $this->add_contents_unknown_warning( $stackPtr, $data ); } $fileName = $this->tokens[ $has_text_string ]['content']; @@ -69,4 +67,66 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $this->phpcsFile->addWarning( $message, $stackPtr, 'FileGetContentsRemoteFile', $data ); } } + + /** + * Process the function if no parameters were found. + * + * {@internal This method is only needed for handling PHP 8.1+ first class callables on WPCS < 3.2.0. + * Once the minimum supported WPCS is 3.2.0 or higher, this method should be removed.} + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * + * @return void + */ + public function process_no_parameters( $stackPtr, $group_name, $matched_content ) { + // Check if this is a first class callable. + $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( $next === false + || $this->tokens[ $next ]['code'] !== T_OPEN_PARENTHESIS + || isset( $this->tokens[ $next ]['parenthesis_closer'] ) === false + ) { + // Live coding/parse error. Ignore. + return; + } + + // First class callable. + $firstNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next + 1 ), null, true ); + if ( $this->tokens[ $firstNonEmpty ]['code'] === T_ELLIPSIS ) { + $secondNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $firstNonEmpty + 1 ), null, true ); + if ( $this->tokens[ $secondNonEmpty ]['code'] === T_CLOSE_PARENTHESIS ) { + $this->add_contents_unknown_warning( $stackPtr, [ $matched_content ] ); + } + } + } + + /** + * Process the function if it is used as a first class callable. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * + * @return void + */ + public function process_first_class_callable( $stackPtr, $group_name, $matched_content ) { + $this->add_contents_unknown_warning( $stackPtr, [ $matched_content ] ); + } + + /** + * Add a warning if the function is used with unknown parameter(s) or with a $filename parameter for which + * it could not be determined if it references a local file or a remote file. + * + * @param int $stackPtr The position of the current token in the stack. + * @param array $data Data to use for string replacement in the error message. + * + * @return void + */ + private function add_contents_unknown_warning( $stackPtr, $data ) { + $message = '`%s()` is highly discouraged for remote requests, please use `wpcom_vip_file_get_contents()` or `vip_safe_wp_remote_get()` instead. If it\'s for a local file please use WP_Filesystem instead.'; + $this->phpcsFile->addWarning( $message, $stackPtr, 'FileGetContentsUnknown', $data ); + } } diff --git a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc index e42ce125..9af71362 100644 --- a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc @@ -75,3 +75,5 @@ file_get_contents( ), filename: $fileName, ); // Warning FileGetContentsUnknown. + +array_walk($files, file_get_contents(...)); // PHP 8.1 first class callable. Warning FileGetContentsUnknown. diff --git a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php index c7537e84..a62609bc 100644 --- a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php @@ -42,6 +42,7 @@ public function getWarningList() { 48 => 1, 62 => 1, 70 => 1, + 79 => 1, ]; } } From 01eff5cfcd3a3164175cdf2e8252936174256a30 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 21 Jul 2025 02:12:09 +0200 Subject: [PATCH 078/165] Performance/FetchingRemoteData: bug fix - recognize use of __DIR__ as local file Unless people get really creative with ternaries and such, the use of `__DIR__` within the parameter basically means that we know they are retrieving a local file and we can silently ignore the function call. Includes test. --- .../Sniffs/Performance/FetchingRemoteDataSniff.php | 12 ++++++++++-- .../Tests/Performance/FetchingRemoteDataUnitTest.inc | 3 +++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php index 185c5ac6..54112313 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php @@ -52,9 +52,17 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p return; } - $data = [ $matched_content ]; + $data = [ $matched_content ]; + $param_start = $filename_param['start']; + $search_end = ( $filename_param['end'] + 1 ); - $has_text_string = $this->phpcsFile->findNext( Tokens::$stringTokens, $filename_param['start'], ( $filename_param['end'] + 1 ) ); + $has_magic_dir = $this->phpcsFile->findNext( T_DIR, $param_start, $search_end ); + if ( $has_magic_dir !== false ) { + // In all likelyhood a local file (disregarding creative code). + return; + } + + $has_text_string = $this->phpcsFile->findNext( Tokens::$stringTokens, $param_start, $search_end ); if ( $has_text_string === false ) { $this->add_contents_unknown_warning( $stackPtr, $data ); } diff --git a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc index 9af71362..60fef22f 100644 --- a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc @@ -77,3 +77,6 @@ file_get_contents( ); // Warning FileGetContentsUnknown. array_walk($files, file_get_contents(...)); // PHP 8.1 first class callable. Warning FileGetContentsUnknown. + +// Bug: don't throw a warning when __DIR__ is used as this indicates use with a local file. +\file_get_contents( __DIR__ . $filename ); // OK. From 11092fd5bac7e97a362eafcf51ba0947c465f7b8 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 21 Jul 2025 02:25:19 +0200 Subject: [PATCH 079/165] Performance/FetchingRemoteData: bug fix - examine all text string tokens in the parameter As things were, only the first text string token in the parameter was being examined, which resulted in false negatives. Includes test. --- .../Sniffs/Performance/FetchingRemoteDataSniff.php | 11 +++++++++-- .../Tests/Performance/FetchingRemoteDataUnitTest.inc | 3 +++ .../Tests/Performance/FetchingRemoteDataUnitTest.php | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php index 54112313..752b3882 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php @@ -67,9 +67,16 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $this->add_contents_unknown_warning( $stackPtr, $data ); } - $fileName = $this->tokens[ $has_text_string ]['content']; + $isRemoteFile = false; + while ( $has_text_string !== false ) { + if ( strpos( $this->tokens[ $has_text_string ]['content'], '://' ) !== false ) { + $isRemoteFile = true; + break; + } + + $has_text_string = $this->phpcsFile->findNext( Tokens::$stringTokens, ( $has_text_string + 1 ), $search_end ); + } - $isRemoteFile = ( strpos( $fileName, '://' ) !== false ); if ( $isRemoteFile === true ) { $message = '`%s()` is highly discouraged for remote requests, please use `wpcom_vip_file_get_contents()` or `vip_safe_wp_remote_get()` instead.'; $this->phpcsFile->addWarning( $message, $stackPtr, 'FileGetContentsRemoteFile', $data ); diff --git a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc index 60fef22f..44326762 100644 --- a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc @@ -80,3 +80,6 @@ array_walk($files, file_get_contents(...)); // PHP 8.1 first class callable. War // Bug: don't throw a warning when __DIR__ is used as this indicates use with a local file. \file_get_contents( __DIR__ . $filename ); // OK. + +// Bug: check all text string tokens in the parameter. +$result = file_get_contents(( is_ssl() ? 'http' : 'https' ) . '://example.com'); // Warning FileGetContentsRemoteFile. diff --git a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php index a62609bc..5b9daa41 100644 --- a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php @@ -43,6 +43,7 @@ public function getWarningList() { 62 => 1, 70 => 1, 79 => 1, + 85 => 1, ]; } } From 7d23be50f7f12ea16754ef2c4ae7715bb3148994 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 21 Jul 2025 02:42:23 +0200 Subject: [PATCH 080/165] Performance/FetchingRemoteData: bug fix - false negative for compound parameters As things were, if the parameter did contain a text string token and that text string token did *not* contain the text "://", the sniff would stay silent, even when the parameter contained additional non-text string tokens, which means we cannot reliably determine whether it is a local file or not. This commit changes the order of the checks to _first_ check if any of the text string tokens in the parameter contain the "://" text. If so, it will throw the (more informative) `FileGetContentsRemoteFile` warning. If not, it will check if the parameter contained anything but text string related or empty tokens. If so, it will throw the (more generic) `FileGetContentsUnknown` warning. Includes test. --- .../Performance/FetchingRemoteDataSniff.php | 25 +++++++++++++------ .../FetchingRemoteDataUnitTest.inc | 3 +++ .../FetchingRemoteDataUnitTest.php | 1 + 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php index 752b3882..807a5529 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php @@ -62,24 +62,35 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p return; } - $has_text_string = $this->phpcsFile->findNext( Tokens::$stringTokens, $param_start, $search_end ); - if ( $has_text_string === false ) { - $this->add_contents_unknown_warning( $stackPtr, $data ); - } - $isRemoteFile = false; - while ( $has_text_string !== false ) { + $search_start = $param_start; + // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- Valid usage. + while ( ( $has_text_string = $this->phpcsFile->findNext( Tokens::$stringTokens, $search_start, $search_end ) ) !== false ) { if ( strpos( $this->tokens[ $has_text_string ]['content'], '://' ) !== false ) { $isRemoteFile = true; break; } - $has_text_string = $this->phpcsFile->findNext( Tokens::$stringTokens, ( $has_text_string + 1 ), $search_end ); + $search_start = ( $has_text_string + 1 ); } if ( $isRemoteFile === true ) { $message = '`%s()` is highly discouraged for remote requests, please use `wpcom_vip_file_get_contents()` or `vip_safe_wp_remote_get()` instead.'; $this->phpcsFile->addWarning( $message, $stackPtr, 'FileGetContentsRemoteFile', $data ); + return; + } + + /* + * Okay, so we haven't been able to determine for certain this is a remote file. + * Check for tokens which would make the parameter contents dynamic. + */ + $ignore = Tokens::$emptyTokens; + $ignore += Tokens::$stringTokens; + $ignore += [ T_STRING_CONCAT => T_STRING_CONCAT ]; + + $has_non_text_string = $this->phpcsFile->findNext( $ignore, $param_start, $search_end, true ); + if ( $has_non_text_string !== false ) { + $this->add_contents_unknown_warning( $stackPtr, $data ); } } diff --git a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc index 44326762..40e48b7f 100644 --- a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.inc @@ -83,3 +83,6 @@ array_walk($files, file_get_contents(...)); // PHP 8.1 first class callable. War // Bug: check all text string tokens in the parameter. $result = file_get_contents(( is_ssl() ? 'http' : 'https' ) . '://example.com'); // Warning FileGetContentsRemoteFile. + +// Bug: don't presume if the parameter contains a text string token, that will be the only token. +\file_get_contents( $url . '/filename.css' ); // Warning FileGetContentsUnknown. diff --git a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php index 5b9daa41..670ef4c5 100644 --- a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php @@ -44,6 +44,7 @@ public function getWarningList() { 70 => 1, 79 => 1, 85 => 1, + 88 => 1, ]; } } From dfe6a2cc084a985325d30f1b863bd9c00d458451 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 23 Jul 2025 02:25:12 +0200 Subject: [PATCH 081/165] Classes/RestrictedExtendClasses: improve the tests Add tests with: * Variations of namespaced class names. * Anonymous classes extending. * Group exclusion, as supported via the extended abstract. --- .../RestrictedExtendClassesUnitTest.inc | 18 ++++++++++++++---- .../RestrictedExtendClassesUnitTest.php | 1 + 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/WordPressVIPMinimum/Tests/Classes/RestrictedExtendClassesUnitTest.inc b/WordPressVIPMinimum/Tests/Classes/RestrictedExtendClassesUnitTest.inc index 49b4aff1..dc302c63 100644 --- a/WordPressVIPMinimum/Tests/Classes/RestrictedExtendClassesUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Classes/RestrictedExtendClassesUnitTest.inc @@ -2,11 +2,21 @@ class OkTestClass extends WPCOM_VIP_CLI_Command { } // Ok - correct class. class AnotherOkTestClass extends WPC__CLI_Command { } // Ok - spacing + similar class. -class Ok_TestClass extends wpcom_vip_cli_command { } // Ok - lowercase correct class. -class Ok_Test_Class extends WpCoM_VIP_cli_command { } // Ok - mixed case correct class. +class Ok_TestClass extends \wpcom_vip_cli_command { } // Ok - lowercase correct class. +$ok = new class extends WpCoM_VIP_cli_command { }; // Ok - mixed case correct class. class WP_CLI_Command { } // Ok - not extending. class BadTestClass extends WP_CLI_Command { } // Warning - wrong class. -class AnotherBadTestClass extends wp_CLI_comMand { } // Warning - mixed case wrong class. -class AnotherBad_TestClass extends WP_CLI_comMand { } // Warning - spacing + mixed case wrong class. +class AnotherBadTestClass extends \wp_CLI_comMand { } // Warning - mixed case wrong class. +$bad = new class() extends WP_CLI_comMand { }; // Warning - spacing + mixed case wrong class. class Bad_Test_Class extends wp_cli_command { } // Warning - lowercase wrong class. + +// phpcs:set WordPressVIPMinimum.Classes.RestrictedExtendClasses exclude[] wp_cli +class IgnoredClass extends WP_CLI_Command { } // OK, group is excluded. + +// Reset to the default value. +// phpcs:set WordPressVIPMinimum.Classes.RestrictedExtendClasses exclude[] + +class Ok_NotTheTargetFQN extends \Fully\Qualified\WP_CLI_Command { } // Ok - not the right namespace. +class Ok_NotTheTargetPQN extends Partially\Qualified\WP_CLI_Command { } // Ok - not the right namespace. +class Bad_NamespaceRelative extends namespace\WP_CLI_Command { } // Warning - this is not a namespaced file, so "namespace" resolves to the global namespace. diff --git a/WordPressVIPMinimum/Tests/Classes/RestrictedExtendClassesUnitTest.php b/WordPressVIPMinimum/Tests/Classes/RestrictedExtendClassesUnitTest.php index 51c1cf00..f5605082 100644 --- a/WordPressVIPMinimum/Tests/Classes/RestrictedExtendClassesUnitTest.php +++ b/WordPressVIPMinimum/Tests/Classes/RestrictedExtendClassesUnitTest.php @@ -36,6 +36,7 @@ public function getWarningList() { 10 => 1, 11 => 1, 12 => 1, + 22 => 1, ]; } } From 73f70809cea277cad367e88b666591c37559870c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 23 Jul 2025 02:32:24 +0200 Subject: [PATCH 082/165] Classes/RestrictedExtendClasses: efficiency fix The `AbstractClassRestrictionsSniff` class does quite a lot of processing before passing off to the `process_matched_token()` method, which can cause quite some overhead for a sniff like this, which is only interested in a class being extended, so let's make it so this sniff only listens to the `T_EXTENDS` token. This should make the sniff significantly faster. --- .../Classes/RestrictedExtendClassesSniff.php | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php b/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php index bb99d4fa..a395031d 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php @@ -35,6 +35,20 @@ public function getGroups() { ]; } + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() { + $targets = parent::register(); + if ( empty( $targets ) ) { + return $targets; + } + + return [ T_EXTENDS ]; + } + /** * Process a matched token. * @@ -46,13 +60,6 @@ public function getGroups() { * @return void */ public function process_matched_token( $stackPtr, $group_name, $matched_content ) { - $tokens = $this->phpcsFile->getTokens(); - - if ( $tokens[ $stackPtr ]['code'] !== T_EXTENDS ) { - // If not extending, bail. - return; - } - foreach ( $this->getGroups() as $group => $group_args ) { $this->phpcsFile->{ 'add' . $group_args['type'] }( $group_args['message'], $stackPtr, $group ); } From cb553b0c45e117473020e564caed7fec5ace96f8 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 21 Jul 2025 03:31:55 +0200 Subject: [PATCH 083/165] Classes/RestrictedExtendClasses: minor tweak Not strictly necessary, but improves the code readability a little. --- .../Sniffs/Classes/RestrictedExtendClassesSniff.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php b/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php index a395031d..5120bb9a 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/RestrictedExtendClassesSniff.php @@ -9,6 +9,7 @@ namespace WordPressVIPMinimum\Sniffs\Classes; +use PHPCSUtils\Utils\MessageHelper; use WordPressCS\WordPress\AbstractClassRestrictionsSniff; /** @@ -61,7 +62,8 @@ public function register() { */ public function process_matched_token( $stackPtr, $group_name, $matched_content ) { foreach ( $this->getGroups() as $group => $group_args ) { - $this->phpcsFile->{ 'add' . $group_args['type'] }( $group_args['message'], $stackPtr, $group ); + $isError = ( $group_args['type'] === 'error' ); + MessageHelper::addMessage( $this->phpcsFile, $group_args['message'], $stackPtr, $isError, $group ); } } } From a8b3078ac2527ab06e75cb4c52df697b8538c381 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 14 Jul 2025 17:37:07 +0200 Subject: [PATCH 084/165] Composer: raise the minimum supported versions of all CS dependencies ... to benefit from new functionality and to ensure all minimum versions are versions compatible with PHPCS 4.0 (in so far as currently available - VariableAnalysis, WordPressCS and PHPCompatibility are not yet compatible with PHPCS 4.0). Includes updating the README which was missed for the previous Composer update. Refs: * https://github.com/PHPCSStandards/PHPCSUtils/releases * https://github.com/PHPCSStandards/PHP_CodeSniffer/releases * https://github.com/PHPCSStandards/PHPCSExtra/releases * https://github.com/sirbrillig/phpcs-variable-analysis/releases * https://github.com/PHPCSStandards/PHPCSDevTools/releases * https://github.com/WordPress/WordPress-Coding-Standards/releases --- README.md | 10 +++++----- composer.json | 12 ++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c0d08b3f..afea6c2e 100644 --- a/README.md +++ b/README.md @@ -16,11 +16,11 @@ The rulesets use rules from the [WordPress Coding Standards](https://github.com/ ## Minimal requirements * PHP 5.4+ -* [PHPCS 3.8.0+](https://github.com/PHPCSStandards/PHP_CodeSniffer/releases) -* [PHPCSUtils 1.0.9+](https://github.com/PHPCSStandards/PHPCSUtils) -* [PHPCSExtra 1.2.1+](https://github.com/PHPCSStandards/PHPCSExtra) -* [WPCS 3.0.0+](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/releases) -* [VariableAnalysis 2.11.17+](https://github.com/sirbrillig/phpcs-variable-analysis/releases) +* [PHPCS 3.13.2+](https://github.com/PHPCSStandards/PHP_CodeSniffer/releases) +* [PHPCSUtils 1.1.0+](https://github.com/PHPCSStandards/PHPCSUtils) +* [PHPCSExtra 1.4.0+](https://github.com/PHPCSStandards/PHPCSExtra) +* [WPCS 3.1.0+](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/releases) +* [VariableAnalysis 2.12.0+](https://github.com/sirbrillig/phpcs-variable-analysis/releases) ## Installation diff --git a/composer.json b/composer.json index 2db52bec..4579a657 100644 --- a/composer.json +++ b/composer.json @@ -17,17 +17,17 @@ ], "require": { "php": ">=5.4", - "phpcsstandards/phpcsextra": "^1.2.1", - "phpcsstandards/phpcsutils": "^1.0.11", - "sirbrillig/phpcs-variable-analysis": "^2.11.18", - "squizlabs/php_codesniffer": "^3.9.2", - "wp-coding-standards/wpcs": "^3.1.0" + "phpcsstandards/phpcsextra": "^1.4.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "sirbrillig/phpcs-variable-analysis": "^2.12.0", + "squizlabs/php_codesniffer": "^3.13.2", + "wp-coding-standards/wpcs": "^3.2.0" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.4.0", "php-parallel-lint/php-console-highlighter": "^1.0.0", "phpcompatibility/php-compatibility": "^9", - "phpcsstandards/phpcsdevtools": "^1.0", + "phpcsstandards/phpcsdevtools": "^1.2.3", "phpunit/phpunit": "^4 || ^5 || ^6 || ^7 || ^8 || ^9" }, "config": { From af4f80daabaee2ad74611d62797d34ef0413f4b0 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 25 Jul 2025 04:00:16 +0200 Subject: [PATCH 085/165] Performance/FetchingRemoteData: remove redundant method This method was introduced in PR 855 as a temporary stop-gap to allow for supporting PHP 8.1+ first class callables. As of WPCS 3.2.0, first class callables are no longer passed to the `process_no_parameters()` method, but to the new dedicated `process_first_class_callable()` method, so this method is no longer needed. --- .../Performance/FetchingRemoteDataSniff.php | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php index 807a5529..e5b3eb11 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/FetchingRemoteDataSniff.php @@ -94,40 +94,6 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p } } - /** - * Process the function if no parameters were found. - * - * {@internal This method is only needed for handling PHP 8.1+ first class callables on WPCS < 3.2.0. - * Once the minimum supported WPCS is 3.2.0 or higher, this method should be removed.} - * - * @param int $stackPtr The position of the current token in the stack. - * @param string $group_name The name of the group which was matched. - * @param string $matched_content The token content (function name) which was matched - * in lowercase. - * - * @return void - */ - public function process_no_parameters( $stackPtr, $group_name, $matched_content ) { - // Check if this is a first class callable. - $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); - if ( $next === false - || $this->tokens[ $next ]['code'] !== T_OPEN_PARENTHESIS - || isset( $this->tokens[ $next ]['parenthesis_closer'] ) === false - ) { - // Live coding/parse error. Ignore. - return; - } - - // First class callable. - $firstNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $next + 1 ), null, true ); - if ( $this->tokens[ $firstNonEmpty ]['code'] === T_ELLIPSIS ) { - $secondNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $firstNonEmpty + 1 ), null, true ); - if ( $this->tokens[ $secondNonEmpty ]['code'] === T_CLOSE_PARENTHESIS ) { - $this->add_contents_unknown_warning( $stackPtr, [ $matched_content ] ); - } - } - } - /** * Process the function if it is used as a first class callable. * From 5ed8cb3a4374abbc2bd0adade8c9d5d153d8072b Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 02:14:49 +0200 Subject: [PATCH 086/165] Classes/DeclarationCompatibility: bug fix - incorrect method name The sniff would try to find a `register_one()` method declaration and check that, while the method in the parent class is called `_register_one()`. This would lead to both false positives (if a `register_one()` method would be declared in the child class) and false negatives (if an incompatible `_register_one()` method would be declared in the child class). Fixed now. Includes updating the tests. --- .../Sniffs/Classes/DeclarationCompatibilitySniff.php | 2 +- .../Tests/Classes/DeclarationCompatibilityUnitTest.inc | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index 9a930319..c5614cd8 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -70,7 +70,7 @@ class DeclarationCompatibilitySniff extends AbstractScopeSniff { 'default' => '1', ], ], - 'register_one' => [ + '_register_one' => [ 'number' => [ 'default' => '-1', ], diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.inc index 740a7883..237c1426 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.inc @@ -46,7 +46,7 @@ class MyWidget extends WP_Widget { function form_callback() { } // Bad, missing $widget_args param. - function register_one() { + function _register_one() { } // Bad, missing $number param. function save_settings() { @@ -103,7 +103,7 @@ class MyWidget2 extends WP_Widget { function form_callback( $widget_args = 2 ) { } // Ok, despite having different default value. - function register_one( $number ) { + function _register_one( $number ) { } // Bad, the param should have a default value. Should be $number. function save_settings( $setting ) { @@ -143,3 +143,7 @@ class MyWalker extends Walker { function end_el( &$output, $data_object, $depth = 0, $args = array() ) { } // Ok. } + +class MyWidget3 extends WP_Widget { + function register_one( $number ) {} // Okay. Not one of the methods we're looking for. +} From e9ac45fbad75f610d97679d1c1006858d57ca46a Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 23:03:25 +0200 Subject: [PATCH 087/165] Classes/DeclarationCompatibility: improve the tests Completely rewrite and greatly expand the tests to ensure a significantly larger portion of what the sniff checks is actually tested. --- .../DeclarationCompatibilityUnitTest.1.inc | 101 ++++++++++++ .../DeclarationCompatibilityUnitTest.2.inc | 94 +++++++++++ .../DeclarationCompatibilityUnitTest.3.inc | 12 ++ .../DeclarationCompatibilityUnitTest.inc | 149 ------------------ .../DeclarationCompatibilityUnitTest.php | 115 ++++++++++---- 5 files changed, 294 insertions(+), 177 deletions(-) create mode 100644 WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc create mode 100644 WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc create mode 100644 WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.3.inc delete mode 100644 WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.inc diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc new file mode 100644 index 00000000..1ee50816 --- /dev/null +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc @@ -0,0 +1,101 @@ + Key is the line number, value is the number of expected errors. */ - public function getErrorList() { - return [ - 4 => 1, - 7 => 1, - 10 => 1, - 13 => 1, - 16 => 1, - 19 => 1, - 25 => 1, - 40 => 1, - 43 => 1, - 46 => 1, - 49 => 1, - 52 => 1, - 61 => 1, - 67 => 1, - 70 => 1, - 76 => 1, - 79 => 1, - 88 => 1, - 106 => 1, - 112 => 1, - 119 => 1, - 128 => 1, - 134 => 1, - 137 => 1, - 140 => 1, - ]; + public function getErrorList( $testFile = '' ) { + switch ( $testFile ) { + case 'DeclarationCompatibilityUnitTest.1.inc': + return [ + 53 => 1, + 54 => 1, + 55 => 1, + 56 => 1, + 57 => 1, + 58 => 1, + 59 => 1, + 60 => 1, + 61 => 1, + 62 => 1, + 63 => 1, + 64 => 1, + 68 => 1, + 69 => 1, + 70 => 1, + 71 => 1, + 75 => 1, + 76 => 1, + 77 => 1, + 78 => 1, + 79 => 1, + 80 => 1, + 81 => 1, + 82 => 1, + 83 => 1, + 84 => 1, + 85 => 1, + 86 => 1, + 87 => 1, + 88 => 1, + 89 => 1, + 90 => 1, + 91 => 1, + 92 => 1, + 96 => 1, + 97 => 1, + 98 => 1, + 99 => 1, + 100 => 1, + ]; + + case 'DeclarationCompatibilityUnitTest.2.inc': + return [ + 43 => 1, + 44 => 1, + 45 => 1, + 46 => 1, + 47 => 1, + 48 => 1, + 49 => 1, + 50 => 1, + 51 => 1, + 55 => 1, + 56 => 1, + 57 => 1, + 58 => 1, + 59 => 1, + 60 => 1, + 61 => 1, + 62 => 1, + 66 => 1, + 67 => 1, + 68 => 1, + 72 => 1, + 73 => 1, + 74 => 1, + 75 => 1, + 79 => 1, + 80 => 1, + 81 => 1, + 82 => 1, + 83 => 1, + 84 => 1, + 88 => 1, + 92 => 1, + 93 => 1, + ]; + + default: + return []; + } } /** From d029cf33e19beae86d3bdcd8a6eb53aeecc4512c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 04:20:05 +0200 Subject: [PATCH 088/165] Classes/DeclarationCompatibility: don't listen outside classes The method signature of the `AbstractScopeSniff::__construct()` method is as follows: ```php public function __construct(array $scopeTokens, array $tokens, $listenOutside=false) {} ``` ... with the documentation for the `$listenOutside` parameter being as follows: > $listenOutside If `true` this test will also alert the extending class when a token is found outside > the scope, by calling the processTokenOutsideScope method. Except the `DeclarationCompatibility` sniff is not interested in tokens "outside scope" and has no implementation for the `processTokenOutsideScope()` method, so it makes no sense to have the `$listenOutside` parameter set to `true`. --- .../Sniffs/Classes/DeclarationCompatibilitySniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index c5614cd8..e0ec1713 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -189,7 +189,7 @@ class DeclarationCompatibilitySniff extends AbstractScopeSniff { * Constructs the test with the tokens it wishes to listen for. */ public function __construct() { - parent::__construct( [ T_CLASS ], [ T_FUNCTION ], true ); + parent::__construct( [ T_CLASS ], [ T_FUNCTION ], false ); } /** From 8e6ac380e15ce0516ec3c3d119e3f9ceb73b8a95 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 22:59:00 +0200 Subject: [PATCH 089/165] Classes/DeclarationCompatibility: normalize method signature It is more common and more predictable to have the `$phpcsFile` and `$stackPtr` parameters at the start of the parameter list, rather than at the end. --- .../Sniffs/Classes/DeclarationCompatibilitySniff.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index e0ec1713..81cec678 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -262,7 +262,7 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco } if ( count( $signatureParams ) !== count( $parentSignature ) ) { - $this->addError( $originalParentClassName, $methodName, $signatureParams, $parentSignature, $phpcsFile, $stackPtr ); + $this->addError( $phpcsFile, $stackPtr, $originalParentClassName, $methodName, $signatureParams, $parentSignature ); return; } @@ -281,7 +281,7 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco $param['variable_length'] !== $signatureParams[ $i ]['variable_length'] ) ) { - $this->addError( $originalParentClassName, $methodName, $signatureParams, $parentSignature, $phpcsFile, $stackPtr ); + $this->addError( $phpcsFile, $stackPtr, $originalParentClassName, $methodName, $signatureParams, $parentSignature ); return; } } @@ -292,16 +292,16 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco /** * Generates an error with nice current and parent class method notations * + * @param File $phpcsFile The PHP_CodeSniffer file where the token was found. + * @param int $stackPtr The position of the current token in the stack. * @param string $parentClassName The name of the extended (parent) class. * @param string $methodName The name of the method currently being examined. * @param array $currentMethodSignature The list of params and their options of the method which is being examined. * @param array $parentMethodSignature The list of params and their options of the parent class method. - * @param File $phpcsFile The PHP_CodeSniffer file where the token was found. - * @param int $stackPtr The position of the current token in the stack. * * @return void */ - private function addError( $parentClassName, $methodName, $currentMethodSignature, $parentMethodSignature, $phpcsFile, $stackPtr ) { + private function addError( File $phpcsFile, $stackPtr, $parentClassName, $methodName, $currentMethodSignature, $parentMethodSignature ) { $currentSignature = sprintf( '%s::%s(%s)', $this->currentClass, $methodName, implode( ', ', $this->generateParamList( $currentMethodSignature ) ) ); From 5e2b887c2310c6a88187ec32e2e0f4d2cc343329 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 23:12:43 +0200 Subject: [PATCH 090/165] Classes/DeclarationCompatibility: remove redundant property Remove a property which keeps track of the current "state". These type of properties are error prone and in this case not necessary anyway as the value of the property is only used in one place and could just have easily been retrieved where it is actually needed. Include minor reformatting of a few long function calls for readability. --- .../Classes/DeclarationCompatibilitySniff.php | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index 81cec678..2603f540 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -19,13 +19,6 @@ */ class DeclarationCompatibilitySniff extends AbstractScopeSniff { - /** - * The name of the class we are currently checking. - * - * @var string - */ - private $currentClass = ''; - /** * A list of classes and methods to check. * @@ -203,12 +196,6 @@ public function __construct() { */ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currScope ) { - $className = ObjectDeclarations::getName( $phpcsFile, $currScope ); - - if ( $className !== $this->currentClass ) { - $this->currentClass = $className; - } - $methodName = FunctionDeclarations::getName( $phpcsFile, $stackPtr ); $parentClassName = ObjectDeclarations::findExtendedClassName( $phpcsFile, $currScope ); @@ -262,7 +249,7 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco } if ( count( $signatureParams ) !== count( $parentSignature ) ) { - $this->addError( $phpcsFile, $stackPtr, $originalParentClassName, $methodName, $signatureParams, $parentSignature ); + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $signatureParams, $parentSignature ); return; } @@ -281,7 +268,7 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco $param['variable_length'] !== $signatureParams[ $i ]['variable_length'] ) ) { - $this->addError( $phpcsFile, $stackPtr, $originalParentClassName, $methodName, $signatureParams, $parentSignature ); + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $signatureParams, $parentSignature ); return; } } @@ -294,6 +281,7 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco * * @param File $phpcsFile The PHP_CodeSniffer file where the token was found. * @param int $stackPtr The position of the current token in the stack. + * @param int $currScope A pointer to the start of the scope. * @param string $parentClassName The name of the extended (parent) class. * @param string $methodName The name of the method currently being examined. * @param array $currentMethodSignature The list of params and their options of the method which is being examined. @@ -301,11 +289,14 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco * * @return void */ - private function addError( File $phpcsFile, $stackPtr, $parentClassName, $methodName, $currentMethodSignature, $parentMethodSignature ) { + private function addError( File $phpcsFile, $stackPtr, $currScope, $parentClassName, $methodName, $currentMethodSignature, $parentMethodSignature ) { + $currentClassName = ObjectDeclarations::getName( $phpcsFile, $currScope ); - $currentSignature = sprintf( '%s::%s(%s)', $this->currentClass, $methodName, implode( ', ', $this->generateParamList( $currentMethodSignature ) ) ); + $currentSignature = implode( ', ', $this->generateParamList( $currentMethodSignature ) ); + $currentSignature = sprintf( '%s::%s(%s)', $currentClassName, $methodName, $currentSignature ); - $parentSignature = sprintf( '%s::%s(%s)', $parentClassName, $methodName, implode( ', ', $this->generateParamList( $parentMethodSignature ) ) ); + $parentSignature = implode( ', ', $this->generateParamList( $parentMethodSignature ) ); + $parentSignature = sprintf( '%s::%s(%s)', $parentClassName, $methodName, $parentSignature ); $message = 'Declaration of `%s` should be compatible with `%s`.'; $data = [ $currentSignature, $parentSignature ]; From f4daea0194944fe000c8c0552047028a2f1f0096 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 06:14:26 +0200 Subject: [PATCH 091/165] Classes/DeclarationCompatibility: code simplification [1] / deprecate `public` `$checkClassesGroups` property Okay, so the class contained a `public` `$checkClassesGroups` property with as its value a multi-level array. Sniff properties which are `public` can be set/overruled from a custom PHPCS ruleset to allow for customizing sniff behaviour, but multi-level property declarations are not supported for setting a property from a custom PHPCS ruleset, at least not without adding extensive special handling for these within the sniff code. Independently of that, looking at the `$checkClassesGroups` property and the intentions for the sniff, it does not appear that this property was ever **_intended_** to be overrulable. Now, I don't expect anyone to have overruled the property, but removing a `public` property should be considered a BC-break even so. Even more so, as since PHP 8.2 deprecated dynamic properties, PHPCS will show a warning to users if their ruleset attempts to set a property which wasn't explicitly declared. So, what to do ? Well, with this commit, I propose the following: * Deprecate the `$checkClassesGroups` property and render it unused by the sniff. The deprecation should be added to the action list in ticket 849 of deprecated code to remove in VIPCS 4.0.0. * Introduce a new `private` `$extendedClassToSignatures` property with a simplified array format for the information the original property contained. * Start using the new property in the sniff code, which allows for some code simplification. Related to 234 --- .../Classes/DeclarationCompatibilitySniff.php | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index 2603f540..6559c718 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -164,18 +164,28 @@ class DeclarationCompatibilitySniff extends AbstractScopeSniff { /** * List of grouped classes with same methods (as they extend the same parent class) * + * @deprecated 3.1.0 This should never have been a public property and is now unused. + * * @var array */ - public $checkClassesGroups = [ - 'Walker' => [ - 'Walker_Category_Checklist', - 'Walker_Category', - 'Walker_CategoryDropdown', - 'Walker_PageDropdown', - 'Walker_Nav_Menu', - 'Walker_Page', - 'Walker_Comment', - ], + public $checkClassesGroups = []; + + /** + * Classes this sniff checks for being extended. + * + * @var array Key is the name of a potentially extended class, + * value the canonical name for the method signatures definition. + */ + private $extendedClassToSignatures = [ + 'WP_Widget' => 'WP_Widget', + 'Walker' => 'Walker', + 'Walker_Category_Checklist' => 'Walker', + 'Walker_Category' => 'Walker', + 'Walker_CategoryDropdown' => 'Walker', + 'Walker_PageDropdown' => 'Walker', + 'Walker_Nav_Menu' => 'Walker', + 'Walker_Page' => 'Walker', + 'Walker_Comment' => 'Walker', ]; /** @@ -204,26 +214,15 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco return; } - // Need to define the originalParentClassName since we might override the parentClassName due to signature notations grouping. + // Store the originalParentClassName since we might override the parentClassName due to signature notations grouping. $originalParentClassName = $parentClassName; - - if ( array_key_exists( $parentClassName, $this->checkClasses ) === false ) { + if ( isset( $this->extendedClassToSignatures[ $parentClassName ] ) === false ) { // This class does not extend a class we are interested in. - foreach ( $this->checkClassesGroups as $parent => $children ) { - // But it might be one of the grouped classes. - foreach ( $children as $child ) { - if ( $child === $parentClassName ) { - $parentClassName = $parent; - break 2; - } - } - } - if ( array_key_exists( $parentClassName, $this->checkClasses ) === false ) { - // This class really does not extend a class we are interested in. - return; - } + return; } + $parentClassName = $this->extendedClassToSignatures[ $parentClassName ]; + if ( array_key_exists( $methodName, $this->checkClasses[ $parentClassName ] ) === false && in_array( $methodName, $this->checkClasses[ $parentClassName ], true ) === false ) { From b6863bca30cd26beb6ed6010cfae0bd6d0d55e9c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 06:42:51 +0200 Subject: [PATCH 092/165] Classes/DeclarationCompatibility: code simplification [2] / deprecate `public` `$checkClasses` property Similar to the previous commit, the class contained a `public` `$checkClasses` property with as its value a complex multi-level array. Sniff properties which are `public` can be set/overruled from a custom PHPCS ruleset to allow for customizing sniff behaviour, but multi-level property declarations are not supported for setting a property from a custom PHPCS ruleset, at least not without adding extensive special handling for these within the sniff code. Independently of that, looking at the `$checkClasses` property and the intentions for the sniff, it does not appear that this property was ever **_intended_** to be overrulable. Now, I don't expect anyone to have overruled the property, but removing a `public` property should be considered a BC-break even so. Even more so, as since PHP 8.2 deprecated dynamic properties, PHPCS will show a warning to users if their ruleset attempts to set a property which wasn't explicitly declared. Aside from all this, the array format of the property was also inconsistent - sometimes a parameter name was an array key with an array value, sometimes, the parameter name was the array value. Having to take both situations into account makes the code more complicated than it needs to be and significantly raises the cognitive load for anyone working on this sniff. So, what to do ? Well, with this commit, I propose the following: * Deprecate the `$checkClasses` property and render it unused by the sniff. The deprecation should be added to the action list in ticket 849 of deprecated code to remove in VIPCS 4.0.0. * Introduce a new `private` `$methodSignatures` property with a more consistent array format for the information the original property contained. Parameter names are now always array keys and the value will always be an array, albeit sometimes an empty array. * Start using the new property in the sniff code, which allows for some code simplification. Related to 234 --- .../Classes/DeclarationCompatibilitySniff.php | 147 ++++++++++-------- 1 file changed, 84 insertions(+), 63 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index 6559c718..91122cad 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -22,16 +22,42 @@ class DeclarationCompatibilitySniff extends AbstractScopeSniff { /** * A list of classes and methods to check. * + * @deprecated 3.1.0 This should never have been a public property. + * * @var array>>> */ - public $checkClasses = [ + public $checkClasses = []; + + /** + * List of grouped classes with same methods (as they extend the same parent class) + * + * @deprecated 3.1.0 This should never have been a public property. + * + * @var array + */ + public $checkClassesGroups = []; + + /** + * A list of classes and information on the methods to check for those classes. + * + * @var array>>> + */ + private $methodSignatures = [ 'WP_Widget' => [ - 'widget' => [ 'args', 'instance' ], - 'update' => [ 'new_instance', 'old_instance' ], - 'form' => [ 'instance' ], + 'widget' => [ + 'args' => [], + 'instance' => [], + ], + 'update' => [ + 'new_instance' => [], + 'old_instance' => [], + ], + 'form' => [ + 'instance' => [], + ], 'WP_Widget' => [ - 'id_base', - 'name', + 'id_base' => [], + 'name' => [], 'widget_options' => [ 'default' => 'array()', ], @@ -39,16 +65,22 @@ class DeclarationCompatibilitySniff extends AbstractScopeSniff { 'default' => 'array()', ], ], - 'get_field_name' => [ 'field_name' ], - 'get_field_id' => [ 'field_name' ], + 'get_field_name' => [ + 'field_name' => [], + ], + 'get_field_id' => [ + 'field_name' => [], + ], '_register' => [], - '_set' => [ 'number' ], + '_set' => [ + 'number' => [], + ], '_get_display_callback' => [], '_get_update_callback' => [], '_get_form_callback' => [], 'is_preview' => [], 'display_callback' => [ - 'args', + 'args' => [], 'widget_args' => [ 'default' => '1', ], @@ -68,9 +100,12 @@ class DeclarationCompatibilitySniff extends AbstractScopeSniff { 'default' => '-1', ], ], - 'save_settings' => [ 'settings' ], + 'save_settings' => [ + 'settings' => [], + ], 'get_settings' => [], ], + 'Walker' => [ 'start_lvl' => [ 'output' => [ @@ -98,7 +133,7 @@ class DeclarationCompatibilitySniff extends AbstractScopeSniff { 'output' => [ 'pass_by_reference' => true, ], - 'data_object', + 'data_object' => [], 'depth' => [ 'default' => '0', ], @@ -110,50 +145,50 @@ class DeclarationCompatibilitySniff extends AbstractScopeSniff { ], ], 'end_el' => [ - 'output' => [ + 'output' => [ 'pass_by_reference' => true, ], - 'data_object', - 'depth' => [ + 'data_object' => [], + 'depth' => [ 'default' => '0', ], - 'args' => [ + 'args' => [ 'default' => 'array()', ], ], 'display_element' => [ - 'element', + 'element' => [], 'children_elements' => [ 'pass_by_reference' => true, ], - 'max_depth', - 'depth', - 'args', + 'max_depth' => [], + 'depth' => [], + 'args' => [], 'output' => [ 'pass_by_reference' => true, ], ], 'walk' => [ - 'elements', - 'max_depth', - 'args' => [ + 'elements' => [], + 'max_depth' => [], + 'args' => [ 'variable_length' => true, ], ], 'paged_walk' => [ - 'elements', - 'max_depth', - 'page_num', - 'per_page', - 'args' => [ + 'elements' => [], + 'max_depth' => [], + 'page_num' => [], + 'per_page' => [], + 'args' => [ 'variable_length' => true, ], ], 'get_number_of_root_elements' => [ - 'elements', + 'elements' => [], ], 'unset_children' => [ - 'element', + 'element' => [], 'children_elements' => [ 'pass_by_reference' => true, ], @@ -161,15 +196,6 @@ class DeclarationCompatibilitySniff extends AbstractScopeSniff { ], ]; - /** - * List of grouped classes with same methods (as they extend the same parent class) - * - * @deprecated 3.1.0 This should never have been a public property and is now unused. - * - * @var array - */ - public $checkClassesGroups = []; - /** * Classes this sniff checks for being extended. * @@ -222,17 +248,14 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco } $parentClassName = $this->extendedClassToSignatures[ $parentClassName ]; - - if ( array_key_exists( $methodName, $this->checkClasses[ $parentClassName ] ) === false && - in_array( $methodName, $this->checkClasses[ $parentClassName ], true ) === false - ) { - // This method is not a one we are interested in. + if ( isset( $this->methodSignatures[ $parentClassName ][ $methodName ] ) === false ) { + // This method is not one we are interested in. return; } $signatureParams = FunctionDeclarations::getParameters( $phpcsFile, $stackPtr ); - $parentSignature = $this->checkClasses[ $parentClassName ][ $methodName ]; + $parentSignature = $this->methodSignatures[ $parentClassName ][ $methodName ]; if ( count( $signatureParams ) > count( $parentSignature ) ) { $extra_params = array_slice( $signatureParams, count( $parentSignature ) - count( $signatureParams ) ); @@ -254,22 +277,20 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco $i = 0; foreach ( $parentSignature as $key => $param ) { - if ( is_array( $param ) === true ) { - if ( - ( - array_key_exists( 'default', $param ) === true && - array_key_exists( 'default', $signatureParams[ $i ] ) === false - ) || ( - array_key_exists( 'pass_by_reference', $param ) === true && - $param['pass_by_reference'] !== $signatureParams[ $i ]['pass_by_reference'] - ) || ( - array_key_exists( 'variable_length', $param ) === true && - $param['variable_length'] !== $signatureParams[ $i ]['variable_length'] - ) - ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $signatureParams, $parentSignature ); - return; - } + if ( + ( + array_key_exists( 'default', $param ) === true && + array_key_exists( 'default', $signatureParams[ $i ] ) === false + ) || ( + array_key_exists( 'pass_by_reference', $param ) === true && + $param['pass_by_reference'] !== $signatureParams[ $i ]['pass_by_reference'] + ) || ( + array_key_exists( 'variable_length', $param ) === true && + $param['variable_length'] !== $signatureParams[ $i ]['variable_length'] + ) + ) { + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $signatureParams, $parentSignature ); + return; } ++$i; } @@ -313,8 +334,8 @@ private function generateParamList( $methodSignature ) { $paramList = []; foreach ( $methodSignature as $param => $options ) { $paramName = '$'; - if ( is_array( $options ) === false ) { - $paramList[] = '$' . $options; + if ( empty( $options ) === true ) { + $paramList[] = '$' . $param; continue; } From a4a1b8abc45dfafe9bbe17077bf6ac4baf35d86a Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 18:31:52 +0200 Subject: [PATCH 093/165] Classes/DeclarationCompatibility: rename some parameters .. to improve code readability. --- .../Classes/DeclarationCompatibilitySniff.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index 91122cad..3255c1fd 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -253,12 +253,12 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco return; } - $signatureParams = FunctionDeclarations::getParameters( $phpcsFile, $stackPtr ); + $childParams = FunctionDeclarations::getParameters( $phpcsFile, $stackPtr ); - $parentSignature = $this->methodSignatures[ $parentClassName ][ $methodName ]; + $parentParams = $this->methodSignatures[ $parentClassName ][ $methodName ]; - if ( count( $signatureParams ) > count( $parentSignature ) ) { - $extra_params = array_slice( $signatureParams, count( $parentSignature ) - count( $signatureParams ) ); + if ( count( $childParams ) > count( $parentParams ) ) { + $extra_params = array_slice( $childParams, count( $parentParams ) - count( $childParams ) ); $all_extra_params_have_default = true; foreach ( $extra_params as $extra_param ) { if ( array_key_exists( 'default', $extra_param ) === false || $extra_param['default'] !== 'true' ) { @@ -270,26 +270,26 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco } } - if ( count( $signatureParams ) !== count( $parentSignature ) ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $signatureParams, $parentSignature ); + if ( count( $childParams ) !== count( $parentParams ) ) { + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); return; } $i = 0; - foreach ( $parentSignature as $key => $param ) { + foreach ( $parentParams as $key => $param ) { if ( ( array_key_exists( 'default', $param ) === true && - array_key_exists( 'default', $signatureParams[ $i ] ) === false + array_key_exists( 'default', $childParams[ $i ] ) === false ) || ( array_key_exists( 'pass_by_reference', $param ) === true && - $param['pass_by_reference'] !== $signatureParams[ $i ]['pass_by_reference'] + $param['pass_by_reference'] !== $childParams[ $i ]['pass_by_reference'] ) || ( array_key_exists( 'variable_length', $param ) === true && - $param['variable_length'] !== $signatureParams[ $i ]['variable_length'] + $param['variable_length'] !== $childParams[ $i ]['variable_length'] ) ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $signatureParams, $parentSignature ); + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); return; } ++$i; From 5fc17c6af7076481ac68b668519bef762df55c31 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 18:33:28 +0200 Subject: [PATCH 094/165] Classes/DeclarationCompatibility: remove duplicate calls to `count()` --- .../Sniffs/Classes/DeclarationCompatibilitySniff.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index 3255c1fd..fa5b2fb4 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -253,12 +253,14 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco return; } - $childParams = FunctionDeclarations::getParameters( $phpcsFile, $stackPtr ); + $childParams = FunctionDeclarations::getParameters( $phpcsFile, $stackPtr ); + $childParamCount = count( $childParams ); - $parentParams = $this->methodSignatures[ $parentClassName ][ $methodName ]; + $parentParams = $this->methodSignatures[ $parentClassName ][ $methodName ]; + $parentParamCount = count( $parentParams ); - if ( count( $childParams ) > count( $parentParams ) ) { - $extra_params = array_slice( $childParams, count( $parentParams ) - count( $childParams ) ); + if ( $childParamCount > $parentParamCount ) { + $extra_params = array_slice( $childParams, $parentParamCount - $childParamCount ); $all_extra_params_have_default = true; foreach ( $extra_params as $extra_param ) { if ( array_key_exists( 'default', $extra_param ) === false || $extra_param['default'] !== 'true' ) { @@ -270,7 +272,7 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco } } - if ( count( $childParams ) !== count( $parentParams ) ) { + if ( $childParamCount !== $parentParamCount ) { $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); return; } From 957c44288f25f2f7ee0838bd9a4573f5c77ca719 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 07:14:13 +0200 Subject: [PATCH 095/165] Classes/DeclarationCompatibility: bug fix - reference mismatch not reported If the parent class specifies a parameter by reference and the child class did not, the sniff would report an incompatible signature. However, if the child class specified a parameter by reference, but the parent class did not, the sniff would fail to report the incompatible signature, while this is just as much a signature mismatch in PHP. See: https://3v4l.org/hLfbR#veol Fixed now. Includes tests. --- .../Classes/DeclarationCompatibilitySniff.php | 5 ++ .../DeclarationCompatibilityUnitTest.1.inc | 17 +++- .../DeclarationCompatibilityUnitTest.2.inc | 14 ++- .../DeclarationCompatibilityUnitTest.php | 87 ++++++++++++------- 4 files changed, 88 insertions(+), 35 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index fa5b2fb4..7d4a2cbe 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -284,8 +284,13 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco array_key_exists( 'default', $param ) === true && array_key_exists( 'default', $childParams[ $i ] ) === false ) || ( + // Parameter in parent class has reference, child does not. array_key_exists( 'pass_by_reference', $param ) === true && $param['pass_by_reference'] !== $childParams[ $i ]['pass_by_reference'] + ) || ( + // Parameter in parent class does *not* have reference, child does. + array_key_exists( 'pass_by_reference', $param ) === false && + $childParams[ $i ]['pass_by_reference'] === true ) || ( array_key_exists( 'variable_length', $param ) === true && $param['variable_length'] !== $childParams[ $i ]['variable_length'] diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc index 1ee50816..6e98e147 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc @@ -4,7 +4,7 @@ class WidgetOkay extends WP_Widget { // Not a method we're interested in. Should be ignored. - public function register_one($whatever, $you, $want = null) {} + public function register_one($whatever, &$you, $want = null) {} // Methods which are being checked. public function widget( $args, $instance ) {} @@ -99,3 +99,18 @@ class WidgetBadOptionalParamChangedToRequiredMissingDefaultValue extends WP_Widg public function form_callback( $widget_args ) {} public function _register_one( $number ) {} } + +class WidgetBadChildHasParamReferenceParentDoesNot extends WP_Widget { + public function widget( $args, &$instance ) {} + public function update( &$new_instance, $old_instance ) {} + public function form( &$instance ) {} + public function WP_Widget( $id_base, &$name, $widget_options = array(), &$control_options = array() ) {} + public function get_field_name( &$field_name ) {} + public function get_field_id( &$field_name ) {} + public function _set( &$number ) {} + public function display_callback( &$args, $widget_args = 1 ) {} + public function update_callback( &$deprecated = 1 ) {} + public function form_callback( &$widget_args = 1 ) {} + public function _register_one( &$number = -1 ) {} + public function save_settings( &$settings ) {} +} diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc index f0233898..547bc448 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc @@ -4,7 +4,7 @@ class WalkerOkay extends Walker { // Not a method we're interested in. Should be ignored. - public function custom_method($whatever, $you, $want = null) {} + public function custom_method($whatever, &$you, $want = null) {} // Methods which are being checked. public function start_lvl( &$output, $depth = 0, $args = array() ) {} @@ -92,3 +92,15 @@ class WalkerBadVariadicArgumentNotDeclaredAsVariadic extends Walker { public function walk( $elements, $max_depth, $args ) {} public function paged_walk( $elements, $max_depth, $page_num, $per_page, $args ) {} } + +class WalkerBadChildHasParamReferenceParentDoesNot extends Walker_Category { + public function start_lvl( &$output, &$depth = 0, $args = array() ) {} + public function end_lvl( &$output, $depth = 0, &$args = array() ) {} + public function start_el( &$output, $data_object, $depth = 0, $args = array(), &$current_object_id = 0 ) {} + public function end_el( &$output, $data_object, &$depth = 0, $args = array(), ) {} + public function display_element( &$element, &$children_elements, $max_depth, $depth, $args, &$output ) {} + public function walk( $elements, &$max_depth, ...$args ) {} + public function paged_walk( $elements, $max_depth, &$page_num, $per_page, ...$args ) {} + public function get_number_of_root_elements( &$elements, ) {} + public function unset_children( &$element, &$children_elements ) {} +} diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php index ff651fe8..678cc860 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php @@ -66,43 +66,64 @@ public function getErrorList( $testFile = '' ) { 98 => 1, 99 => 1, 100 => 1, + 104 => 1, + 105 => 1, + 106 => 1, + 107 => 1, + 108 => 1, + 109 => 1, + 110 => 1, + 111 => 1, + 112 => 1, + 113 => 1, + 114 => 1, + 115 => 1, ]; case 'DeclarationCompatibilityUnitTest.2.inc': return [ - 43 => 1, - 44 => 1, - 45 => 1, - 46 => 1, - 47 => 1, - 48 => 1, - 49 => 1, - 50 => 1, - 51 => 1, - 55 => 1, - 56 => 1, - 57 => 1, - 58 => 1, - 59 => 1, - 60 => 1, - 61 => 1, - 62 => 1, - 66 => 1, - 67 => 1, - 68 => 1, - 72 => 1, - 73 => 1, - 74 => 1, - 75 => 1, - 79 => 1, - 80 => 1, - 81 => 1, - 82 => 1, - 83 => 1, - 84 => 1, - 88 => 1, - 92 => 1, - 93 => 1, + 43 => 1, + 44 => 1, + 45 => 1, + 46 => 1, + 47 => 1, + 48 => 1, + 49 => 1, + 50 => 1, + 51 => 1, + 55 => 1, + 56 => 1, + 57 => 1, + 58 => 1, + 59 => 1, + 60 => 1, + 61 => 1, + 62 => 1, + 66 => 1, + 67 => 1, + 68 => 1, + 72 => 1, + 73 => 1, + 74 => 1, + 75 => 1, + 79 => 1, + 80 => 1, + 81 => 1, + 82 => 1, + 83 => 1, + 84 => 1, + 88 => 1, + 92 => 1, + 93 => 1, + 97 => 1, + 98 => 1, + 99 => 1, + 100 => 1, + 101 => 1, + 102 => 1, + 103 => 1, + 104 => 1, + 105 => 1, ]; default: From 735558f4b2eecc3de34af273587cf83ffab03a70 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 04:50:17 +0200 Subject: [PATCH 096/165] Classes/DeclarationCompatibility: bug fix - false positives for valid extra parameters It's perfectly valid for a child class to overload a parent method and to add additional optional parameters to the method. The sniff did include code to handle this, but the logic of that code block was incorrect. The old condition basically checked if the 'default' array index existed and the value of the default parameter was not `true`. Not sure where that came from, but it makes no sense. Fixed now. Includes adding support for additional optional parameters being declared as variadic (PHP 5.6+), which makes them optional by nature. Includes minor efficiency fix - no need to continue examining extra parameters to see if they are optional if we already saw one which was not. Includes tests. --- .../Classes/DeclarationCompatibilitySniff.php | 5 ++++- .../DeclarationCompatibilityUnitTest.1.inc | 22 +++++++++++++++++++ .../DeclarationCompatibilityUnitTest.2.inc | 13 +++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index 7d4a2cbe..6ef0e928 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -263,8 +263,11 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco $extra_params = array_slice( $childParams, $parentParamCount - $childParamCount ); $all_extra_params_have_default = true; foreach ( $extra_params as $extra_param ) { - if ( array_key_exists( 'default', $extra_param ) === false || $extra_param['default'] !== 'true' ) { + if ( isset( $extra_param['default'] ) === false + && $extra_param['variable_length'] === false + ) { $all_extra_params_have_default = false; + break; } } if ( $all_extra_params_have_default === true ) { diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc index 6e98e147..e569d873 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc @@ -114,3 +114,25 @@ class WidgetBadChildHasParamReferenceParentDoesNot extends WP_Widget { public function _register_one( &$number = -1 ) {} public function save_settings( &$settings ) {} } + +// It's perfectly valid for child classes to declare extra optional parameters. +class WidgetOkayExtraOptionalParams extends WP_Widget { + public function widget( $args, $instance, $extra_param = array() ) {} + public function update( $new_instance, $old_instance, ...$extra_param ) {} + public function form( $instance, $extra_param = true ) {} + public function WP_Widget( $id_base, $name, $widget_options = array(), $control_options = array(), $extra_param = array() ) {} + public function get_field_name( $field_name, $extra_param = 'default') {} + public function get_field_id( $field_name, $extra_param = 'id') {} + public function _register( $extra_param = false ) {} + public function _set( $number, $extra_param = 5 ) {} + public function _get_display_callback($paramA = 'A', $paramB = 'B' ) {} + public function _get_update_callback( $my_param = true, $my_param2 = 10) {} + public function _get_form_callback(...$extra_param) {} + public function is_preview( $is_preview = true ) {} + public function display_callback( $args, $widget_args = 1, $extra_param = [] ) {} + public function update_callback( $deprecated = 1, ...$extra_param ) {} + public function form_callback( $widget_args = 1, ...$extra_param ) {} + public function _register_one( $number = -1, $extra_param = []) {} + public function save_settings( $settings, $extra_param = 10.5) {} + public function get_settings( $settings = 'foo', $setting = self::BAR, ...$my_param ) {} +} diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc index 547bc448..d65e771b 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc @@ -104,3 +104,16 @@ class WalkerBadChildHasParamReferenceParentDoesNot extends Walker_Category { public function get_number_of_root_elements( &$elements, ) {} public function unset_children( &$element, &$children_elements ) {} } + +// It's perfectly valid for child classes to declare extra optional parameters. +class WalkerOkayExtraOptionalParams extends Walker_CategoryDropdown { + public function start_lvl( &$output, $depth = 0, $args = array(), $extra_param = true ) {} + public function end_lvl( &$output, $depth = 0, $args = array(), ...$extra_param ) {} + public function start_el( &$output, $data_object, $depth = 0, $args = array(), $current_object_id = 0, $extra_param = array() ) {} + public function end_el( &$output, $data_object, $depth = 0, $args = array(), $extra_param = [], ) {} + public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output, $extra_param = 'default' ) {} + public function walk( $elements, $max_depth, $extra_param = null, ...$args ) {} + public function paged_walk( $elements, $max_depth, $page_num, $per_page, $extra_param = null, ...$args ) {} + public function get_number_of_root_elements( $elements, $extra_param = 10, ) {} + public function unset_children( $element, &$children_elements, $extra_param = '', ) {} +} From d960ea43b64d1a497f09aea0bfa17e0387035317 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 19:42:49 +0200 Subject: [PATCH 097/165] Classes/DeclarationCompatibility: bug fix - properly handle PHP 5.6+ variadic parameters As things were, the sniff would verify that if a parent method would expect a parameter to be variadic, the child method would also declare the parameter as variadic. This check is insufficient and leads to both false positives and false negatives. Based on testing this extensively, I've boiled the rules down to this: 1. Variadic parameters must always be the last parameter in a signature. 2. If a parent method has the last parameter as optional by declaring a default value, a child method which makes that last parameter variadic and removes the default value, still complies with the signature requirements as variadic parameters are optional by nature. 3. If a parent method has the last parameter as variadic, the child method must also have the last parameter as variadic, but the child method _may_ insert additional optional parameters before the variadic parameter. This commit adjusts the sniff code to apply all three of these rules properly. Includes tests. Tests for the third rule are, in part, included in the tests added in the previous commit in the `*OkayExtraOptionalParams` classes. --- .../Classes/DeclarationCompatibilitySniff.php | 38 ++++++++++++++++--- .../DeclarationCompatibilityUnitTest.1.inc | 29 ++++++++++++++ .../DeclarationCompatibilityUnitTest.2.inc | 20 ++++++++++ .../DeclarationCompatibilityUnitTest.php | 18 +++++++++ 4 files changed, 100 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index 6ef0e928..519b0051 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -259,6 +259,36 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco $parentParams = $this->methodSignatures[ $parentClassName ][ $methodName ]; $parentParamCount = count( $parentParams ); + /* + * If there are parameters, verify if the last parameter of both the parent and the child are variadic. + * Only the last parameter can be variadic and if the parent has this, the child must also, + * independently of potential extra optional parameters having been inserted before that last parameter. + * + * Also note that a child can make the last parameter variadic, even if the parent parameter was not. + * This will no longer trigger a warning since PHP 8.0. + */ + if ( $childParamCount > 0 && $parentParamCount > 0 ) { + $childLastParam = $childParams[ $childParamCount - 1 ]; + $parentLastParam = $parentParams[ array_keys( $parentParams )[ $parentParamCount - 1 ] ]; + + if ( ( isset( $parentLastParam['variable_length'] ) === true && $parentLastParam['variable_length'] === true ) + && $childLastParam['variable_length'] !== true + ) { + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); + return; + } + } + + if ( $childParamCount > 0 ) { + // Check that no other parameters in the child signature are declared as variadic. + for ( $i = 0; $i < ( $childParamCount - 1 ); $i++ ) { + if ( $childParams[ $i ]['variable_length'] === true ) { + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); + return; + } + } + } + if ( $childParamCount > $parentParamCount ) { $extra_params = array_slice( $childParams, $parentParamCount - $childParamCount ); $all_extra_params_have_default = true; @@ -284,8 +314,9 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco foreach ( $parentParams as $key => $param ) { if ( ( - array_key_exists( 'default', $param ) === true && - array_key_exists( 'default', $childParams[ $i ] ) === false + array_key_exists( 'default', $param ) === true + && array_key_exists( 'default', $childParams[ $i ] ) === false + && $childParams[ $i ]['variable_length'] === false ) || ( // Parameter in parent class has reference, child does not. array_key_exists( 'pass_by_reference', $param ) === true && @@ -294,9 +325,6 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco // Parameter in parent class does *not* have reference, child does. array_key_exists( 'pass_by_reference', $param ) === false && $childParams[ $i ]['pass_by_reference'] === true - ) || ( - array_key_exists( 'variable_length', $param ) === true && - $param['variable_length'] !== $childParams[ $i ]['variable_length'] ) ) { $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc index e569d873..71fba1ba 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc @@ -136,3 +136,32 @@ class WidgetOkayExtraOptionalParams extends WP_Widget { public function save_settings( $settings, $extra_param = 10.5) {} public function get_settings( $settings = 'foo', $setting = self::BAR, ...$my_param ) {} } + +class WidgetOkayChildCanMakeLastParamVariadic extends WP_Widget { + public function widget( $args, ...$instance ) {} + public function update( $new_instance, ...$old_instance ) {} + public function form( ...$instance ) {} + public function WP_Widget( $id_base, $name, $widget_options = array(), ...$control_options ) {} + public function get_field_name( ...$field_name ) {} + public function get_field_id( ...$field_name ) {} + public function _set( ...$number ) {} + public function display_callback( $args, ...$widget_args ) {} + public function update_callback( ...$deprecated ) {} + public function form_callback( ...$widget_args ) {} + public function _register_one( ...$number ) {} + public function save_settings( ...$settings ) {} +} + +class WidgetBadVariadicParamNotLast extends WP_Widget { + public function widget( ...$args, $instance ) {} + public function update( ...$new_instance, $old_instance ) {} + public function WP_Widget( $id_base, $name, ...$widget_options = array(), $control_options = array() ) {} + public function get_field_name( ...$field_name, $extra_param = null ) {} + public function get_field_id( ...$field_name, $extra_param = null ) {} + public function _set( ...$number, $extra_param = null ) {} + public function display_callback( ...$args, $widget_args = 1 ) {} + public function update_callback( ...$deprecated, $extra_param = null ) {} + public function form_callback( ...$widget_args, $extra_param = null ) {} + public function _register_one( ...$number, $extra_param = null ) {} + public function save_settings( ...$settings, $extra_param = null ) {} +} diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc index d65e771b..e9495d11 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc @@ -117,3 +117,23 @@ class WalkerOkayExtraOptionalParams extends Walker_CategoryDropdown { public function get_number_of_root_elements( $elements, $extra_param = 10, ) {} public function unset_children( $element, &$children_elements, $extra_param = '', ) {} } + +class WalkerOkayChildCanMakeLastParamVariadic extends Walker_Page { + public function start_lvl( &$output, $depth = 0, ...$args ) {} + public function end_lvl( &$output, $depth = 0, ...$args ) {} + public function start_el( &$output, $data_object, $depth = 0, $args = array(), ...$current_object_id ) {} + public function end_el( &$output, $data_object, $depth = 0, ...$args, ) {} + public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &...$output ) {} + public function get_number_of_root_elements( ...$elements, ) {} + public function unset_children( $element, &...$children_elements ) {} +} + +class WalkerBadVariadicParamNotLast extends Walker_Nav_Menu { + public function start_lvl( &$output, ...$depth, $args = array() ) {} + public function end_lvl( &...$output, $depth = 0, $args = array() ) {} + public function start_el( &$output, ...$data_object, $depth = 0, $args = array(), $current_object_id = 0 ) {} + public function end_el( &$output, $data_object, ...$depth, $args = array(), ) {} + public function display_element( $element, &$children_elements, ...$max_depth, $depth, $args, &$output ) {} + public function get_number_of_root_elements( ...$elements, $extra_param = 10, ) {} + public function unset_children( ...$element, &$children_elements ) {} +} diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php index 678cc860..0f97bcfb 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php @@ -78,6 +78,17 @@ public function getErrorList( $testFile = '' ) { 113 => 1, 114 => 1, 115 => 1, + 156 => 1, + 157 => 1, + 158 => 1, + 159 => 1, + 160 => 1, + 161 => 1, + 162 => 1, + 163 => 1, + 164 => 1, + 165 => 1, + 166 => 1, ]; case 'DeclarationCompatibilityUnitTest.2.inc': @@ -124,6 +135,13 @@ public function getErrorList( $testFile = '' ) { 103 => 1, 104 => 1, 105 => 1, + 132 => 1, + 133 => 1, + 134 => 1, + 135 => 1, + 136 => 1, + 137 => 1, + 138 => 1, ]; default: From 08a7195dac111319585ccb03466c833c4bcf271b Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 19:46:18 +0200 Subject: [PATCH 098/165] Classes/DeclarationCompatibility: minor code consistency tweak --- .../Sniffs/Classes/DeclarationCompatibilitySniff.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index 519b0051..fbee2d25 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -319,12 +319,12 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco && $childParams[ $i ]['variable_length'] === false ) || ( // Parameter in parent class has reference, child does not. - array_key_exists( 'pass_by_reference', $param ) === true && - $param['pass_by_reference'] !== $childParams[ $i ]['pass_by_reference'] + array_key_exists( 'pass_by_reference', $param ) === true + && $param['pass_by_reference'] !== $childParams[ $i ]['pass_by_reference'] ) || ( // Parameter in parent class does *not* have reference, child does. - array_key_exists( 'pass_by_reference', $param ) === false && - $childParams[ $i ]['pass_by_reference'] === true + array_key_exists( 'pass_by_reference', $param ) === false + && $childParams[ $i ]['pass_by_reference'] === true ) ) { $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); From 54a1580e1a3c0ec488cef4f8e061fba973eb9453 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 20:00:50 +0200 Subject: [PATCH 099/165] Classes/DeclarationCompatibility: bug fix - false negatives for errors icw extra optional parameters Now the checks for optional parameters and variadic parameters have been fixed, it becomes clear that IF the child method declares extra optional parameters AND the child method has a signature mismatch in the parameters inherited from the parent method, the sniff would stay silent as the sniff bows out on the `if ( $all_extra_params_have_default === true )` condition, resulting in false negatives. Fixed now. Includes tests. --- .../Sniffs/Classes/DeclarationCompatibilitySniff.php | 10 +++++----- .../Classes/DeclarationCompatibilityUnitTest.1.inc | 6 ++++++ .../Classes/DeclarationCompatibilityUnitTest.2.inc | 6 ++++++ .../Tests/Classes/DeclarationCompatibilityUnitTest.php | 4 ++++ 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index fbee2d25..a2df322f 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -300,12 +300,12 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco break; } } - if ( $all_extra_params_have_default === true ) { - return; // We're good. - } - } - if ( $childParamCount !== $parentParamCount ) { + if ( $all_extra_params_have_default === false ) { + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); + return; + } + } elseif ( $childParamCount !== $parentParamCount ) { $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); return; } diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc index 71fba1ba..527406f3 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc @@ -165,3 +165,9 @@ class WidgetBadVariadicParamNotLast extends WP_Widget { public function _register_one( ...$number, $extra_param = null ) {} public function save_settings( ...$settings, $extra_param = null ) {} } + +// The fact that all extra parameters are optional, should not hide other errors. +class WidgetBadExtraOptionalParamsANDOtherError extends WP_Widget { + public function update_callback( $deprecated, $extra_param = [] ) {} // Missing default value for $deprecated. + public function form_callback( &$widget_args, ...$extra_param ) {} // Invalid reference for $widget_args. +} diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc index e9495d11..74fcda6f 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc @@ -137,3 +137,9 @@ class WalkerBadVariadicParamNotLast extends Walker_Nav_Menu { public function get_number_of_root_elements( ...$elements, $extra_param = 10, ) {} public function unset_children( ...$element, &$children_elements ) {} } + +// The fact that all extra parameters are optional, should not hide other errors. +class WalkerBadExtraOptionalParamsANDOtherError extends Walker_Category_Checklist { + public function start_lvl( $output, $depth = 0, $args = array(), $extra_param = true ) {} // Missing reference for $output. + public function end_lvl( &$output, $depth, $args = array(), ...$extra_param ) {} // Missing default value for $depth. +} diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php index 0f97bcfb..c4381f86 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php @@ -89,6 +89,8 @@ public function getErrorList( $testFile = '' ) { 164 => 1, 165 => 1, 166 => 1, + 171 => 1, + 172 => 1, ]; case 'DeclarationCompatibilityUnitTest.2.inc': @@ -142,6 +144,8 @@ public function getErrorList( $testFile = '' ) { 136 => 1, 137 => 1, 138 => 1, + 143 => 1, + 144 => 1, ]; default: From 931ec2081b366626731ecbca592563e5c73eb823 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 22:51:19 +0200 Subject: [PATCH 100/165] Classes/DeclarationCompatibility: bug fix - class and function names are not case-sensitive As things were, the comparisons/checks whether a function should be examined and whether a function was included in a class extending a class which should be examined, were all done case-sensitively, while PHP treats (ascii-based) class and function names case-insensitively. Fixed now. Includes code to ensure that the class and method names used in the error messages are in "proper case", i.e. the expected/official case for each class/method. Tested via updating some pre-existing tests. --- .../Classes/DeclarationCompatibilitySniff.php | 58 +++++++++++++++---- .../DeclarationCompatibilityUnitTest.1.inc | 44 +++++++------- .../DeclarationCompatibilityUnitTest.2.inc | 24 ++++---- 3 files changed, 80 insertions(+), 46 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index a2df322f..a97ca548 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -214,10 +214,39 @@ class DeclarationCompatibilitySniff extends AbstractScopeSniff { 'Walker_Comment' => 'Walker', ]; + /** + * Translate from case-insensitive names to proper case method names. + * + * @var array> Primary key is the class name in proper case. + * Value is an array with method names in lowercase as keys + * and these same method names in proper case as values. + */ + private $methodToProperCase = []; + + /** + * Translate from case-insensitive names to proper case class names. + * + * @var array Key is the lowercase name of a class, value the proper case. + */ + private $classToProperCase = []; + /** * Constructs the test with the tokens it wishes to listen for. */ public function __construct() { + // Lowercase all names to allow for correct comparisons, as PHP treats class/function names case-insensitively. + // But also store translation tables to be able to get the proper case. + foreach ( $this->methodSignatures as $key => $value ) { + $methodNames = array_keys( $value ); + $this->methodToProperCase[ $key ] = array_change_key_case( array_combine( $methodNames, $methodNames ), CASE_LOWER ); + + $this->methodSignatures[ $key ] = array_change_key_case( $value, CASE_LOWER ); + } + + $classNames = array_keys( $this->extendedClassToSignatures ); + $this->classToProperCase = array_change_key_case( array_combine( $classNames, $classNames ), CASE_LOWER ); + $this->extendedClassToSignatures = array_change_key_case( $this->extendedClassToSignatures, CASE_LOWER ); + parent::__construct( [ T_CLASS ], [ T_FUNCTION ], false ); } @@ -232,7 +261,8 @@ public function __construct() { */ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currScope ) { - $methodName = FunctionDeclarations::getName( $phpcsFile, $stackPtr ); + $methodName = FunctionDeclarations::getName( $phpcsFile, $stackPtr ); + $methodNameLC = strtolower( $methodName ); $parentClassName = ObjectDeclarations::findExtendedClassName( $phpcsFile, $currScope ); if ( $parentClassName === false ) { @@ -240,23 +270,27 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco return; } - // Store the originalParentClassName since we might override the parentClassName due to signature notations grouping. - $originalParentClassName = $parentClassName; - if ( isset( $this->extendedClassToSignatures[ $parentClassName ] ) === false ) { + $parentClassNameLC = strtolower( $parentClassName ); + if ( isset( $this->extendedClassToSignatures[ $parentClassNameLC ] ) === false ) { // This class does not extend a class we are interested in. return; } - $parentClassName = $this->extendedClassToSignatures[ $parentClassName ]; - if ( isset( $this->methodSignatures[ $parentClassName ][ $methodName ] ) === false ) { + // Store the originalParentClassName since we might override the parentClassName due to signature notations grouping. + $originalParentClassNamePC = $this->classToProperCase[ $parentClassNameLC ]; + + $parentClassName = $this->extendedClassToSignatures[ $parentClassNameLC ]; + if ( isset( $this->methodSignatures[ $parentClassName ][ $methodNameLC ] ) === false ) { // This method is not one we are interested in. return; } + $methodNamePC = $this->methodToProperCase[ $parentClassName ][ $methodNameLC ]; + $childParams = FunctionDeclarations::getParameters( $phpcsFile, $stackPtr ); $childParamCount = count( $childParams ); - $parentParams = $this->methodSignatures[ $parentClassName ][ $methodName ]; + $parentParams = $this->methodSignatures[ $parentClassName ][ $methodNameLC ]; $parentParamCount = count( $parentParams ); /* @@ -274,7 +308,7 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco if ( ( isset( $parentLastParam['variable_length'] ) === true && $parentLastParam['variable_length'] === true ) && $childLastParam['variable_length'] !== true ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); return; } } @@ -283,7 +317,7 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco // Check that no other parameters in the child signature are declared as variadic. for ( $i = 0; $i < ( $childParamCount - 1 ); $i++ ) { if ( $childParams[ $i ]['variable_length'] === true ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); return; } } @@ -302,11 +336,11 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco } if ( $all_extra_params_have_default === false ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); return; } } elseif ( $childParamCount !== $parentParamCount ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); return; } @@ -327,7 +361,7 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco && $childParams[ $i ]['pass_by_reference'] === true ) ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassName, $methodName, $childParams, $parentParams ); + $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); return; } ++$i; diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc index 527406f3..34216176 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc @@ -64,35 +64,35 @@ class WidgetBadTooFewParamsNoParam extends WP_Widget { public function save_settings() {} // Bad, missing $settings param. } -final class WidgetBadTooFewParamsNotAllParams extends WP_Widget { +final class WidgetBadTooFewParamsNotAllParams extends WP_WIDGET { public function widget( $args ) {} // Bad, missing $instance param. public function update( $new_instance ) {} // Bad, missing $old_instance param. public function WP_Widget( $id_base, $name, ) {} // Bad, missing $widget_options and $control_options params. public function display_callback( $args ) {} // Bad, missing $widget_args param. } -class WidgetBadExtraRequiredParams extends WP_Widget { - public function widget( $args, $instance, $extra_param ) {} - public function update( $new_instance, $old_instance, $extra_param ) {} - public function form( $instance, $extra_param ) {} - public function WP_Widget( $id_base, $name, $widget_options = array(), $control_options = array(), $extra_param) {} - public function get_field_name( $field_name, $extra_param ) {} - public function get_field_id( $field_name, $extra_param ) {} - public function _register( $extra_param ) {} - public function _set( $number, $extra_param ) {} - public function _get_display_callback($extra_paramA, $extra_paramB) {} - public function _get_update_callback( $extra_paramA, $extra_paramB ) {} - public function _get_form_callback($extra_param) {} - public function is_preview($extra_param) {} - public function display_callback( $args, $widget_args = 1, $extra_param ) {} - public function update_callback( $deprecated = 1, $extra_param, ) {} - public function form_callback( $widget_args = 1, $extra_param ) {} - public function _register_one( $number = -1, $extra_param ) {} - public function save_settings( $settings, $extra_param) {} - public function get_settings( $extra_paramA, $extra_paramB, $extra_paramC ) {} +class WidgetBadExtraRequiredParamsAndMethodNamesAreCaseInsensitive extends WP_Widget { + public function Widget( $args, $instance, $extra_param ) {} + public function Update( $new_instance, $old_instance, $extra_param ) {} + public function Form( $intance, $extra_param ) {} + public function wp_widget( $id_base, $name, $widget_options = array(), $control_options = array(), $extra_param) {} + public function Get_Field_Name( $field_name, $extra_param ) {} + public function Get_Field_ID( $field_name, $extra_param ) {} + public function _Register( $extra_param ) {} + public function _Set( $number, $extra_param ) {} + public function _Get_Display_Callback($extra_paramA, $extra_paramB) {} + public function _Get_Update_Callback( $extra_paramA, $extra_paramB ) {} + public function _Get_Form_Callback($extra_param) {} + public function is_Preview($extra_param) {} + public function display_Callback( $args, $widget_args = 1, $extra_param ) {} + public function update_Callback( $deprecated = 1, $extra_param, ) {} + public function form_Callback( $widget_args = 1, $extra_param ) {} + public function _register_ONE( $number = -1, $extra_param ) {} + public function SAVE_SETTINGS( $settings, $extra_param) {} + public function get_Settings( $extra_paramA, $extra_paramB, $extra_paramC ) {} }; -class WidgetBadOptionalParamChangedToRequiredMissingDefaultValue extends WP_Widget { +class WidgetBadOptionalParamChangedToRequiredMissingDefaultValue extends wp_widget { public function WP_Widget( $id_base, $name, $widget_options, $control_options = array() ) {} // Bad, missing default value for $widget_options. public function display_callback( $args, $widget_args ) {} // Bad, missing default value for $widget_args. public function update_callback( $deprecated ) {} @@ -152,7 +152,7 @@ class WidgetOkayChildCanMakeLastParamVariadic extends WP_Widget { public function save_settings( ...$settings ) {} } -class WidgetBadVariadicParamNotLast extends WP_Widget { +class WidgetBadVariadicParamNotLast extends Wp_widget { public function widget( ...$args, $instance ) {} public function update( ...$new_instance, $old_instance ) {} public function WP_Widget( $id_base, $name, ...$widget_options = array(), $control_options = array() ) {} diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc index 74fcda6f..adfb8a39 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc @@ -39,19 +39,19 @@ class WalkerOkayDifferentDefaultValue extends Walker_Category_Checklist { public function end_el( &$output, $data_object, $depth = 10, $args = '' ) {} }; -class WalkerBadTooFewParamsNoParam extends Walker_Category { - public function start_lvl() {} - public function end_lvl() {} - public function start_el() {} - public function end_el() {} - public function display_element() {} - public function walk() {} - public function paged_walk() {} - public function get_number_of_root_elements() {} - public function unset_children() {} +class WalkerBadTooFewParamsNoParamAndMethodNamesAreCaseInsensitive extends Walker_Category { + public function start_LVL() {} + public function End_Lvl() {} + public function START_EL() {} + public function end_EL() {} + public function Display_Element() {} + public function WalK() {} + public function paged_WALK() {} + public function get_Number_Of_Root_Elements() {} + public function unset_Children() {} } -readonly class WalkerBadTooFewParamsNotAllParams extends Walker_CategoryDropdown { +readonly class WalkerBadTooFewParamsNotAllParams extends walker_categorydropdown { public function start_lvl( &$output, $depth = 0 ) {} // Bad, missing $args param. public function end_lvl( &$output ) {} // Bad, missing $depth and $args params. public function start_el( &$output, $data_object, $depth = 0, ) {} // Bad, missing $args and $current_object_id params. @@ -128,7 +128,7 @@ class WalkerOkayChildCanMakeLastParamVariadic extends Walker_Page { public function unset_children( $element, &...$children_elements ) {} } -class WalkerBadVariadicParamNotLast extends Walker_Nav_Menu { +class WalkerBadVariadicParamNotLast extends WALKER_NAV_MENU { public function start_lvl( &$output, ...$depth, $args = array() ) {} public function end_lvl( &...$output, $depth = 0, $args = array() ) {} public function start_el( &$output, ...$data_object, $depth = 0, $args = array(), $current_object_id = 0 ) {} From 724d67c55fd9b2adf8bf9ca35324cf8957920738 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 23:24:13 +0200 Subject: [PATCH 101/165] Classes/DeclarationCompatibility: move some variable declarations ... to where they are actually needed/used. No need to have this information available before this point. --- .../Sniffs/Classes/DeclarationCompatibilitySniff.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index a97ca548..ff9c3728 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -261,9 +261,6 @@ public function __construct() { */ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currScope ) { - $methodName = FunctionDeclarations::getName( $phpcsFile, $stackPtr ); - $methodNameLC = strtolower( $methodName ); - $parentClassName = ObjectDeclarations::findExtendedClassName( $phpcsFile, $currScope ); if ( $parentClassName === false ) { // This class does not extend any other class. @@ -280,6 +277,8 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco $originalParentClassNamePC = $this->classToProperCase[ $parentClassNameLC ]; $parentClassName = $this->extendedClassToSignatures[ $parentClassNameLC ]; + $methodName = FunctionDeclarations::getName( $phpcsFile, $stackPtr ); + $methodNameLC = strtolower( $methodName ); if ( isset( $this->methodSignatures[ $parentClassName ][ $methodNameLC ] ) === false ) { // This method is not one we are interested in. return; From 5b168fe36e2eb1a923be3d6142e266dc51dbdc52 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 24 Jul 2025 23:48:30 +0200 Subject: [PATCH 102/165] Classes/DeclarationCompatibility: bug fix - recognize extended class if fully qualified It is perfectly valid in namespaced code to use fully qualified class names for classes not imported via an import `use` statement. The sniff did not handle this correctly, leading to false negatives. Fixed now. Includes some updated tests to safeguard this. --- .../Sniffs/Classes/DeclarationCompatibilitySniff.php | 2 +- .../Tests/Classes/DeclarationCompatibilityUnitTest.1.inc | 4 ++-- .../Tests/Classes/DeclarationCompatibilityUnitTest.2.inc | 4 ++-- .../Tests/Classes/DeclarationCompatibilityUnitTest.3.inc | 8 ++++++++ 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index ff9c3728..7997f723 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -267,7 +267,7 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco return; } - $parentClassNameLC = strtolower( $parentClassName ); + $parentClassNameLC = ltrim( strtolower( $parentClassName ), '\\' ); // Trim off potential FQN indicator. if ( isset( $this->extendedClassToSignatures[ $parentClassNameLC ] ) === false ) { // This class does not extend a class we are interested in. return; diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc index 34216176..4bc4f04a 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc @@ -41,7 +41,7 @@ readonly class WidgetOkayDefaultValueWhereParentHasNone extends WP_Widget { public function save_settings( $settings = array() ) {} } -class WidgetOkayDifferentDefaultValue extends WP_Widget { +class WidgetOkayDifferentDefaultValue extends \WP_Widget { public function WP_Widget( $id_base, $name, $widget_options = null, $control_options = null ) {} public function display_callback( $args, $widget_args = [] ) {} public function update_callback( $deprecated = true, ) {} @@ -100,7 +100,7 @@ class WidgetBadOptionalParamChangedToRequiredMissingDefaultValue extends wp_widg public function _register_one( $number ) {} } -class WidgetBadChildHasParamReferenceParentDoesNot extends WP_Widget { +class WidgetBadChildHasParamReferenceParentDoesNot extends \WP_Widget { public function widget( $args, &$instance ) {} public function update( &$new_instance, $old_instance ) {} public function form( &$instance ) {} diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc index adfb8a39..f39d2524 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc @@ -20,7 +20,7 @@ class WalkerOkay extends Walker { // These methods all change required parameters from the parent class into optional ones by adding a default value. // This will not cause a signature mismatch in PHP since PHP 5.3. -final class WalkerOkayDefaultValueWhereParentHasNone extends Walker_Page { +final class WalkerOkayDefaultValueWhereParentHasNone extends \Walker_Page { public function start_lvl( &$output = '', $depth = 0, $args = array() ) {} public function end_lvl( &$output = '', $depth = 0, $args = array() ) {} public function start_el( &$output = '', $data_object = new stdClass, $depth = 0, $args = array(), $current_object_id = 0 ) {} @@ -62,7 +62,7 @@ readonly class WalkerBadTooFewParamsNotAllParams extends walker_categorydropdown public function unset_children( $element ) {} // Bad, missing $children_elements param. } -class WalkerBadExtraRequiredParams extends Walker_PageDropdown { +class WalkerBadExtraRequiredParams extends \Walker_PageDropdown { public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output, $extra_param ) {} public function get_number_of_root_elements( $elements, $extra_param, ) {} public function unset_children( $element, &$children_elements, $extra_paramA, $extra_paramB ) {} diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.3.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.3.inc index 69ec3568..94d8fcaa 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.3.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.3.inc @@ -10,3 +10,11 @@ class DoesNotExtend { class ExtendsDifferentClass extends FooBar { function foo() {} } + +class ExtendsFullyQualifiedClass extends \Different\NS\WP_Widget { + function save_settings() {} +} + +class ExtendsPartiallyQualifiedClass extends Also\Different\NS\Walker_Page { + function save_settings() {} +} From e65437975bb74dc30425adceea4a561059f6719a Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 25 Jul 2025 03:33:05 +0200 Subject: [PATCH 103/165] Classes/DeclarationCompatibility: bug fix - false positives for nested functions / refactor As things were, the `DeclarationCompatibilitySniff` class extended the PHPCS native `AbstractScopeSniff`. The `AbstractScopeSniff` has a fundamental problem, in that it will call the child sniff for every target token in every target scope. To illustrate, let's have a look at the tests I've added: ```php class WidgetOkayNestedFunctionIsNotMethod extends WP_Widget { public function hasNested() { // Functions can be declared nested within other functions. That doesn't make them methods. function is_preview($extra_param) {} } } ``` As both the `hasNested()` function as well as the `is_preview()` function are nested somewhere in a `T_CLASS` scope, the `DeclarationCompatibilitySniff` will be called for both `T_FUNCTION` tokens and give both the `T_CLASS` token for the `WidgetOkayNestedFunctionIsNotMethod` class as the "current scope". Along the same lines, for the second code sample: ```php class WidgetOkayNestedClassDoesNotExtend extends WP_Widget { function hasNestedClass() { return new class { public function display_callback() {} }; } } ``` The `DeclarationCompatibilitySniff::processTokenWithinScope()` method will be called for both the `hasNestedClass()` as well as the `display_callback()` functions and both will have the `T_CLASS` token for the `WidgetOkayNestedClassDoesNotExtend` class as the "current scope", no matter that the `display_callback()` method is in actual fact a method on the anonymous class and not on the `WidgetOkayNestedClassDoesNotExtend` class. As can be expected, this leads to false positives as both the (global) `is_preview()` function from the first code sample as well as the `display_callback()` method in the anonymous class from the second code sample would be analyzed as if they were methods on the top-level class. Now, this can be fixed by adding a check at the start of the `processTokenWithinScope()` function to make sure that the "current scope" is the "deepest" valid scope, however, there is a better solution, which should also improve the performance of the sniff. As of PHPCSUtils 1.1.0, PHPCSUtils offers a highly optimized `ObjectDeclarations::getDeclaredMethods()` method, which will return an array with the names of all found methods and the stack pointer to the `T_FUNCTION` keyword for each. So now, instead of the `AbstractScopeSniff` being called for every single function and needing to do processing before deciding whether to bow out or to pass off to child classes, we can sniff for the - much rarer - `T_CLASS` token instead, then check if the class extends one of the classes we are looking for and if so, retrieve the list of methods on that class via the new PHPCSUtils method and just loop through them. As the results for the PHPCSUtils method are cached, this also means that if _multiple_ sniffs would use the `ObjectDeclarations::getDeclared*()` methods, the result for subsequent calls will be returned nearly instantaneous, giving a PHPCS run an even bigger performance boost. --- .../Classes/DeclarationCompatibilitySniff.php | 191 +++++++++--------- .../DeclarationCompatibilityUnitTest.3.inc | 17 ++ 2 files changed, 112 insertions(+), 96 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index 7997f723..98a2c2b3 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -10,14 +10,14 @@ namespace WordPressVIPMinimum\Sniffs\Classes; use PHP_CodeSniffer\Files\File; -use PHP_CodeSniffer\Sniffs\AbstractScopeSniff; +use PHP_CodeSniffer\Sniffs\Sniff; use PHPCSUtils\Utils\FunctionDeclarations; use PHPCSUtils\Utils\ObjectDeclarations; /** * Class WordPressVIPMinimum_Sniffs_Classes_DeclarationCompatibilitySniff */ -class DeclarationCompatibilitySniff extends AbstractScopeSniff { +class DeclarationCompatibilitySniff implements Sniff { /** * A list of classes and methods to check. @@ -231,9 +231,11 @@ class DeclarationCompatibilitySniff extends AbstractScopeSniff { private $classToProperCase = []; /** - * Constructs the test with the tokens it wishes to listen for. + * Returns the token types that this sniff is interested in. + * + * @return array */ - public function __construct() { + public function register() { // Lowercase all names to allow for correct comparisons, as PHP treats class/function names case-insensitively. // But also store translation tables to be able to get the proper case. foreach ( $this->methodSignatures as $key => $value ) { @@ -247,21 +249,20 @@ public function __construct() { $this->classToProperCase = array_change_key_case( array_combine( $classNames, $classNames ), CASE_LOWER ); $this->extendedClassToSignatures = array_change_key_case( $this->extendedClassToSignatures, CASE_LOWER ); - parent::__construct( [ T_CLASS ], [ T_FUNCTION ], false ); + return [ T_CLASS ]; } /** - * Processes this test when one of its tokens is encountered. + * Processes the tokens that this sniff is interested in. * - * @param File $phpcsFile The PHP_CodeSniffer file where the token was found. - * @param int $stackPtr The position of the current token in the stack passed in $tokens. - * @param int $currScope A pointer to the start of the scope. + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. * * @return void */ - protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currScope ) { - - $parentClassName = ObjectDeclarations::findExtendedClassName( $phpcsFile, $currScope ); + public function process( File $phpcsFile, $stackPtr ) { + $parentClassName = ObjectDeclarations::findExtendedClassName( $phpcsFile, $stackPtr ); if ( $parentClassName === false ) { // This class does not extend any other class. return; @@ -275,95 +276,101 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco // Store the originalParentClassName since we might override the parentClassName due to signature notations grouping. $originalParentClassNamePC = $this->classToProperCase[ $parentClassNameLC ]; + $parentClassName = $this->extendedClassToSignatures[ $parentClassNameLC ]; - $parentClassName = $this->extendedClassToSignatures[ $parentClassNameLC ]; - $methodName = FunctionDeclarations::getName( $phpcsFile, $stackPtr ); - $methodNameLC = strtolower( $methodName ); - if ( isset( $this->methodSignatures[ $parentClassName ][ $methodNameLC ] ) === false ) { - // This method is not one we are interested in. + $methods = ObjectDeclarations::getDeclaredMethods( $phpcsFile, $stackPtr ); + if ( empty( $methods ) ) { return; } - $methodNamePC = $this->methodToProperCase[ $parentClassName ][ $methodNameLC ]; - - $childParams = FunctionDeclarations::getParameters( $phpcsFile, $stackPtr ); - $childParamCount = count( $childParams ); - - $parentParams = $this->methodSignatures[ $parentClassName ][ $methodNameLC ]; - $parentParamCount = count( $parentParams ); - - /* - * If there are parameters, verify if the last parameter of both the parent and the child are variadic. - * Only the last parameter can be variadic and if the parent has this, the child must also, - * independently of potential extra optional parameters having been inserted before that last parameter. - * - * Also note that a child can make the last parameter variadic, even if the parent parameter was not. - * This will no longer trigger a warning since PHP 8.0. - */ - if ( $childParamCount > 0 && $parentParamCount > 0 ) { - $childLastParam = $childParams[ $childParamCount - 1 ]; - $parentLastParam = $parentParams[ array_keys( $parentParams )[ $parentParamCount - 1 ] ]; - - if ( ( isset( $parentLastParam['variable_length'] ) === true && $parentLastParam['variable_length'] === true ) - && $childLastParam['variable_length'] !== true - ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); - return; + foreach ( $methods as $methodName => $functionPtr ) { + $methodNameLC = strtolower( $methodName ); + if ( isset( $this->methodSignatures[ $parentClassName ][ $methodNameLC ] ) === false ) { + // This method is not one we are interested in. + continue; } - } - if ( $childParamCount > 0 ) { - // Check that no other parameters in the child signature are declared as variadic. - for ( $i = 0; $i < ( $childParamCount - 1 ); $i++ ) { - if ( $childParams[ $i ]['variable_length'] === true ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); - return; + $methodNamePC = $this->methodToProperCase[ $parentClassName ][ $methodNameLC ]; + + $childParams = FunctionDeclarations::getParameters( $phpcsFile, $functionPtr ); + $childParamCount = count( $childParams ); + + $parentParams = $this->methodSignatures[ $parentClassName ][ $methodNameLC ]; + $parentParamCount = count( $parentParams ); + + /* + * If there are parameters, verify if the last parameter of both the parent and the child are variadic. + * Only the last parameter can be variadic and if the parent has this, the child must also, + * independently of potential extra optional parameters having been inserted before that last parameter. + * + * Also note that a child can make the last parameter variadic, even if the parent parameter was not. + * This will no longer trigger a warning since PHP 8.0. + */ + if ( $childParamCount > 0 && $parentParamCount > 0 ) { + $childLastParam = $childParams[ $childParamCount - 1 ]; + $parentLastParam = $parentParams[ array_keys( $parentParams )[ $parentParamCount - 1 ] ]; + + if ( ( isset( $parentLastParam['variable_length'] ) === true && $parentLastParam['variable_length'] === true ) + && $childLastParam['variable_length'] !== true + ) { + $this->addError( $phpcsFile, $functionPtr, $stackPtr, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); + continue; } } - } - if ( $childParamCount > $parentParamCount ) { - $extra_params = array_slice( $childParams, $parentParamCount - $childParamCount ); - $all_extra_params_have_default = true; - foreach ( $extra_params as $extra_param ) { - if ( isset( $extra_param['default'] ) === false - && $extra_param['variable_length'] === false - ) { - $all_extra_params_have_default = false; - break; + if ( $childParamCount > 0 ) { + // Check that no other parameters in the child signature are declared as variadic. + for ( $i = 0; $i < ( $childParamCount - 1 ); $i++ ) { + if ( $childParams[ $i ]['variable_length'] === true ) { + $this->addError( $phpcsFile, $functionPtr, $stackPtr, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); + continue 2; + } } } - if ( $all_extra_params_have_default === false ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); - return; + if ( $childParamCount > $parentParamCount ) { + $extra_params = array_slice( $childParams, $parentParamCount - $childParamCount ); + $all_extra_params_have_default = true; + foreach ( $extra_params as $extra_param ) { + if ( isset( $extra_param['default'] ) === false + && $extra_param['variable_length'] === false + ) { + $all_extra_params_have_default = false; + break; + } + } + + if ( $all_extra_params_have_default === false ) { + $this->addError( $phpcsFile, $functionPtr, $stackPtr, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); + continue; + } + } elseif ( $childParamCount !== $parentParamCount ) { + $this->addError( $phpcsFile, $functionPtr, $stackPtr, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); + continue; } - } elseif ( $childParamCount !== $parentParamCount ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); - return; - } - $i = 0; - foreach ( $parentParams as $key => $param ) { - if ( - ( - array_key_exists( 'default', $param ) === true - && array_key_exists( 'default', $childParams[ $i ] ) === false - && $childParams[ $i ]['variable_length'] === false - ) || ( - // Parameter in parent class has reference, child does not. - array_key_exists( 'pass_by_reference', $param ) === true - && $param['pass_by_reference'] !== $childParams[ $i ]['pass_by_reference'] - ) || ( - // Parameter in parent class does *not* have reference, child does. - array_key_exists( 'pass_by_reference', $param ) === false - && $childParams[ $i ]['pass_by_reference'] === true - ) - ) { - $this->addError( $phpcsFile, $stackPtr, $currScope, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); - return; + $i = 0; + foreach ( $parentParams as $key => $param ) { + if ( + ( + array_key_exists( 'default', $param ) === true + && array_key_exists( 'default', $childParams[ $i ] ) === false + && $childParams[ $i ]['variable_length'] === false + ) || ( + // Parameter in parent class has reference, child does not. + array_key_exists( 'pass_by_reference', $param ) === true + && $param['pass_by_reference'] !== $childParams[ $i ]['pass_by_reference'] + ) || ( + // Parameter in parent class does *not* have reference, child does. + array_key_exists( 'pass_by_reference', $param ) === false + && $childParams[ $i ]['pass_by_reference'] === true + ) + ) { + $this->addError( $phpcsFile, $functionPtr, $stackPtr, $originalParentClassNamePC, $methodNamePC, $childParams, $parentParams ); + continue 2; + } + ++$i; } - ++$i; } } @@ -371,8 +378,8 @@ protected function processTokenWithinScope( File $phpcsFile, $stackPtr, $currSco * Generates an error with nice current and parent class method notations * * @param File $phpcsFile The PHP_CodeSniffer file where the token was found. - * @param int $stackPtr The position of the current token in the stack. - * @param int $currScope A pointer to the start of the scope. + * @param int $stackPtr The position of the current T_FUNCTION token in the stack. + * @param int $currScope A pointer to the start of the OO scope. * @param string $parentClassName The name of the extended (parent) class. * @param string $methodName The name of the method currently being examined. * @param array $currentMethodSignature The list of params and their options of the method which is being examined. @@ -433,12 +440,4 @@ private function generateParamList( $methodSignature ) { return $paramList; } - - /** - * Do nothing outside the scope. Has to be implemented accordingly to parent abstract class. - * - * @param File $phpcsFile PHPCS File. - * @param int $stackPtr Stack position. - */ - public function processTokenOutsideScope( File $phpcsFile, $stackPtr ) {} } diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.3.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.3.inc index 94d8fcaa..b82b03c0 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.3.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.3.inc @@ -18,3 +18,20 @@ class ExtendsFullyQualifiedClass extends \Different\NS\WP_Widget { class ExtendsPartiallyQualifiedClass extends Also\Different\NS\Walker_Page { function save_settings() {} } + +class NoMethods extends WP_Widget {} + +class WidgetOkayNestedFunctionIsNotMethod extends WP_Widget { + public function hasNested() { + // Functions can be declared nested within other functions. That doesn't make them methods. + function is_preview($extra_param) {} + } +} + +class WidgetOkayNestedClassDoesNotExtend extends WP_Widget { + function hasNestedClass() { + return new class { + public function display_callback() {} + }; + } +} From d9e13cc4259e82a84db2ad4eed183db055973dce Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 25 Jul 2025 03:38:32 +0200 Subject: [PATCH 104/165] Classes/DeclarationCompatibility: add support for PHP 7.0+ anonymous classes Now support for nested structures has been fixed, we can add support for anonymous classes. Anonymous classes can also extend the WP native classes, so should be checked too. Includes updating some of the existing tests to safeguard this and adding some extra tests with nested structures. --- .../Classes/DeclarationCompatibilitySniff.php | 11 ++++++++-- .../DeclarationCompatibilityUnitTest.1.inc | 20 +++++++++++++++++-- .../DeclarationCompatibilityUnitTest.2.inc | 4 ++-- .../DeclarationCompatibilityUnitTest.php | 2 ++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index 98a2c2b3..d603aa22 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -249,7 +249,10 @@ public function register() { $this->classToProperCase = array_change_key_case( array_combine( $classNames, $classNames ), CASE_LOWER ); $this->extendedClassToSignatures = array_change_key_case( $this->extendedClassToSignatures, CASE_LOWER ); - return [ T_CLASS ]; + return [ + T_CLASS, + T_ANON_CLASS, + ]; } /** @@ -388,7 +391,11 @@ public function process( File $phpcsFile, $stackPtr ) { * @return void */ private function addError( File $phpcsFile, $stackPtr, $currScope, $parentClassName, $methodName, $currentMethodSignature, $parentMethodSignature ) { - $currentClassName = ObjectDeclarations::getName( $phpcsFile, $currScope ); + $tokens = $phpcsFile->getTokens(); + $currentClassName = '[AnonymousClass]'; + if ( $tokens[ $currScope ]['code'] !== T_ANON_CLASS ) { + $currentClassName = ObjectDeclarations::getName( $phpcsFile, $currScope ); + } $currentSignature = implode( ', ', $this->generateParamList( $currentMethodSignature ) ); $currentSignature = sprintf( '%s::%s(%s)', $currentClassName, $methodName, $currentSignature ); diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc index 4bc4f04a..8ae13ced 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.1.inc @@ -41,7 +41,7 @@ readonly class WidgetOkayDefaultValueWhereParentHasNone extends WP_Widget { public function save_settings( $settings = array() ) {} } -class WidgetOkayDifferentDefaultValue extends \WP_Widget { +$WidgetOkayDifferentDefaultValue = new #[MyAttribute] readonly class extends \WP_Widget { public function WP_Widget( $id_base, $name, $widget_options = null, $control_options = null ) {} public function display_callback( $args, $widget_args = [] ) {} public function update_callback( $deprecated = true, ) {} @@ -71,7 +71,7 @@ final class WidgetBadTooFewParamsNotAllParams extends WP_WIDGET { public function display_callback( $args ) {} // Bad, missing $widget_args param. } -class WidgetBadExtraRequiredParamsAndMethodNamesAreCaseInsensitive extends WP_Widget { +$WidgetBadExtraRequiredParamsAndMethodNamesAreCaseInsensitive = new class() extends WP_Widget { public function Widget( $args, $instance, $extra_param ) {} public function Update( $new_instance, $old_instance, $extra_param ) {} public function Form( $intance, $extra_param ) {} @@ -171,3 +171,19 @@ class WidgetBadExtraOptionalParamsANDOtherError extends WP_Widget { public function update_callback( $deprecated, $extra_param = [] ) {} // Missing default value for $deprecated. public function form_callback( &$widget_args, ...$extra_param ) {} // Invalid reference for $widget_args. } + +class WidgetBadNestedClassExtends { + function nested() { + return new class extends WP_Widget { + public function display_callback() {} + }; + } +} + +class WidgetBadNestedClassAlsoExtends extends WP_Widget { + function nested() { + return new class extends WP_Widget { + public function display_callback() {} + }; + } +} diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc index f39d2524..8317e84d 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.2.inc @@ -32,7 +32,7 @@ final class WalkerOkayDefaultValueWhereParentHasNone extends \Walker_Page { public function unset_children( $element = new stdClass, &$children_elements = array(), ) {} } -class WalkerOkayDifferentDefaultValue extends Walker_Category_Checklist { +$WalkerOkayDifferentDefaultValue = new class() extends Walker_Category_Checklist { public function start_lvl( &$output, $depth = 2, $args = null ) {} public function end_lvl( &$output, $depth = 1, $args = new stdClass ) {} public function start_el( &$output, $data_object, $depth = 5, $args = null, $current_object_id = 10 ) {} @@ -62,7 +62,7 @@ readonly class WalkerBadTooFewParamsNotAllParams extends walker_categorydropdown public function unset_children( $element ) {} // Bad, missing $children_elements param. } -class WalkerBadExtraRequiredParams extends \Walker_PageDropdown { +$WalkerBadExtraRequiredParams = new #[MyAttribute] readonly class extends \Walker_PageDropdown { public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output, $extra_param ) {} public function get_number_of_root_elements( $elements, $extra_param, ) {} public function unset_children( $element, &$children_elements, $extra_paramA, $extra_paramB ) {} diff --git a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php index c4381f86..7f38c00c 100644 --- a/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php +++ b/WordPressVIPMinimum/Tests/Classes/DeclarationCompatibilityUnitTest.php @@ -91,6 +91,8 @@ public function getErrorList( $testFile = '' ) { 166 => 1, 171 => 1, 172 => 1, + 178 => 1, + 186 => 1, ]; case 'DeclarationCompatibilityUnitTest.2.inc': From a900e2fd375d874dcc7661b1235c12d631b879c4 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 25 Jul 2025 03:52:12 +0200 Subject: [PATCH 105/165] Classes/DeclarationCompatibility: minor code stability tweaks * Prefer `isset()` over `array_key_exists()` as it's faster and improves code readability. * Add additional conditions to support the possibility of the `pass_by_reference` and `variable_length` keys in the parameter sub-arrays in the `$methodSignatures` property being set to `false`. --- .../Classes/DeclarationCompatibilitySniff.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index d603aa22..af3ae353 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -353,19 +353,20 @@ public function process( File $phpcsFile, $stackPtr ) { } $i = 0; - foreach ( $parentParams as $key => $param ) { + foreach ( $parentParams as $param ) { if ( ( - array_key_exists( 'default', $param ) === true - && array_key_exists( 'default', $childParams[ $i ] ) === false + isset( $param['default'] ) === true + && isset( $childParams[ $i ]['default'] ) === false && $childParams[ $i ]['variable_length'] === false ) || ( // Parameter in parent class has reference, child does not. - array_key_exists( 'pass_by_reference', $param ) === true + isset( $param['pass_by_reference'] ) === true && $param['pass_by_reference'] !== $childParams[ $i ]['pass_by_reference'] ) || ( // Parameter in parent class does *not* have reference, child does. - array_key_exists( 'pass_by_reference', $param ) === false + ( isset( $param['pass_by_reference'] ) === false + || $param['pass_by_reference'] === false ) && $childParams[ $i ]['pass_by_reference'] === true ) ) { @@ -424,21 +425,21 @@ private function generateParamList( $methodSignature ) { continue; } - if ( array_key_exists( 'name', $options ) === true ) { + if ( isset( $options['name'] ) === true ) { $paramName = $options['name']; } else { $paramName .= $param; } - if ( array_key_exists( 'variable_length', $options ) === true && $options['variable_length'] === true ) { + if ( isset( $options['variable_length'] ) === true && $options['variable_length'] === true ) { $paramName = '...' . $paramName; } - if ( array_key_exists( 'pass_by_reference', $options ) === true && $options['pass_by_reference'] === true ) { + if ( isset( $options['pass_by_reference'] ) === true && $options['pass_by_reference'] === true ) { $paramName = '&' . $paramName; } - if ( array_key_exists( 'default', $options ) === true && empty( $options['default'] ) === false ) { + if ( isset( $options['default'] ) === true && empty( $options['default'] ) === false ) { $paramName .= ' = ' . trim( $options['default'] ); } From 632d810292e4950fc698e5814dfb26d6e0684f9c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 25 Jul 2025 04:16:37 +0200 Subject: [PATCH 106/165] Classes/DeclarationCompatibility: minor documentation improvements --- .../Classes/DeclarationCompatibilitySniff.php | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php index af3ae353..10381feb 100644 --- a/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php +++ b/WordPressVIPMinimum/Sniffs/Classes/DeclarationCompatibilitySniff.php @@ -29,7 +29,7 @@ class DeclarationCompatibilitySniff implements Sniff { public $checkClasses = []; /** - * List of grouped classes with same methods (as they extend the same parent class) + * List of grouped classes with same methods (as they extend the same parent class). * * @deprecated 3.1.0 This should never have been a public property. * @@ -379,15 +379,16 @@ public function process( File $phpcsFile, $stackPtr ) { } /** - * Generates an error with nice current and parent class method notations + * Generates an error with nice current and parent class method notations. * - * @param File $phpcsFile The PHP_CodeSniffer file where the token was found. - * @param int $stackPtr The position of the current T_FUNCTION token in the stack. - * @param int $currScope A pointer to the start of the OO scope. - * @param string $parentClassName The name of the extended (parent) class. - * @param string $methodName The name of the method currently being examined. - * @param array $currentMethodSignature The list of params and their options of the method which is being examined. - * @param array $parentMethodSignature The list of params and their options of the parent class method. + * @param File $phpcsFile The PHP_CodeSniffer file where the token was found. + * @param int $stackPtr The position of the current T_FUNCTION token in the stack. + * @param int $currScope A pointer to the start of the OO scope. + * @param string $parentClassName The name of the extended (parent) class. + * @param string $methodName The name of the method currently being examined. + * @param array> $currentMethodSignature The list of params and their options of the method + * which is being examined. + * @param array> $parentMethodSignature The list of params and their options of the parent class method. * * @return void */ From 40b0dac7efabbd11177f77af816f45850e58920c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 27 Jul 2025 01:45:20 +0200 Subject: [PATCH 107/165] Ruleset: improve include/exclude patterns `include-pattern` and `exclude-pattern` directives are a special flavour of regular expressions. From the documentation: > Important > > The ignore patterns are treated as regular expressions. If you do specify a regular expression, be aware that `*` is converted to `.*` for convenience in simple patterns, like those used in the example above. So use `*` anywhere you would normally use `.*`. Also ensure you escape any `.` characters that you want treated as a literal dot, such as when checking file extensions. So if you are checking for `.inc` in your ignore pattern, use `\.inc` instead. This commit fixes unescaped characters which have special meaning in regular expressions to ensure they are interpreted as literals. Ref: https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Advanced-Usage#ignoring-files-and-folders --- WordPressVIPMinimum/ruleset.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/ruleset.xml b/WordPressVIPMinimum/ruleset.xml index c0d97376..f5bdb847 100644 --- a/WordPressVIPMinimum/ruleset.xml +++ b/WordPressVIPMinimum/ruleset.xml @@ -28,7 +28,7 @@ - *.twig + *\.twig @@ -67,10 +67,10 @@ - *.php - *.inc - *.js - *.css + *\.php + *\.inc + *\.js + *\.css From 2338970280a2580932fed259c05671192a83fd94 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 26 Jul 2025 17:51:28 +0200 Subject: [PATCH 108/165] Security/EscapingVoidReturnFunctions: extend AbstractFunctionParameterSniff As things were, the determination of whether or not a `T_STRING` is a call to any of the global WP `esc_*()` or `wp_kses*()` functions, was severely flawed. By switching the sniff over to be based on the WordPressCS `AbstractFunctionParameterSniff` class, this flaw is mitigated. Take note of the `$target_functions` using wildcards to match all function calls starting with the prefixes indicates (as it was before). Includes adding a slew of additional tests, some of which (line 8 - 15, line 52, 60, 63) are specific to the flaw being addressed in this commit. Additionally, the tests have been made more comprehensive and varied by: * Testing against false positives for calls to methods or namespaced function calls (= the issue being addressed in this PR). * Ensure function import `use` statements are not flagged. We're not interested in those. * Safeguarding that function calls using PHP 5.6+ argument unpacking are not flagged. * Safeguarding handling of PHP 8.0+ attribute class using the same name as a target function. * Safeguarding that the function is not flagged when used as a PHP 8.1+ first class callable. * Adding tests with more variations: - Variation in the escaping functions being called. - Variation in the printing functions being called. - Non-lowercase function call(s), for both the escaping functions as well as the printing functions. This was previously handled correctly for the printing functions (via WPCS), but not for the escaping functions. - Fully qualified function calls for the escaping functions. - Use PHP 7.3+ trailing comma's in a few function calls. - Multi-line function call. - Comments in unexpected places. --- .../EscapingVoidReturnFunctionsSniff.php | 41 +++++------ .../EscapingVoidReturnFunctionsUnitTest.inc | 68 ++++++++++++++++++- .../EscapingVoidReturnFunctionsUnitTest.php | 18 ++++- 3 files changed, 105 insertions(+), 22 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php index 02d2ab6d..fda5e9f5 100644 --- a/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php @@ -10,8 +10,8 @@ namespace WordPressVIPMinimum\Sniffs\Security; use PHP_CodeSniffer\Util\Tokens; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; use WordPressCS\WordPress\Helpers\PrintingFunctionsTrait; -use WordPressVIPMinimum\Sniffs\Sniff; /** * Flag functions that don't return anything, yet are wrapped in an escaping function call. @@ -20,37 +20,40 @@ * * @uses \WordPressCS\WordPress\Helpers\PrintingFunctionsTrait::$customPrintingFunctions */ -class EscapingVoidReturnFunctionsSniff extends Sniff { +class EscapingVoidReturnFunctionsSniff extends AbstractFunctionParameterSniff { use PrintingFunctionsTrait; /** - * Returns an array of tokens this test wants to listen for. + * The group name for this group of functions. * - * @return array + * @var string */ - public function register() { - return [ - T_STRING, - ]; - } + protected $group_name = 'escaping_void'; + + /** + * Functions this sniff is looking for. + * + * @var array Keys are target functions, value irrelevant. + */ + protected $target_functions = [ + 'esc_*' => true, + 'wp_kses*' => true, + ]; /** - * Process this test when one of its tokens is encountered + * Process the parameters of a matched function. * - * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. * * @return void */ - public function process_token( $stackPtr ) { - - if ( strpos( $this->tokens[ $stackPtr ]['content'], 'esc_' ) !== 0 && strpos( $this->tokens[ $stackPtr ]['content'], 'wp_kses' ) !== 0 ) { - // Not what we are looking for. - return; - } - + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { $next_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); - if ( $this->tokens[ $next_token ]['code'] !== T_OPEN_PARENTHESIS ) { // Not a function call. return; diff --git a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc index 9a9de966..1276894d 100644 --- a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc @@ -1,4 +1,68 @@ esc_js( _deprecated_file() ); +$this?->esc_textarea( _e( $something ) ); +MyClass::esc_url( wp_die(), ); +echo WP_KSES; +namespace\wp_kses_post( user_error( $something ) ); + +function esc_js( _E $param ) {} // Class "_E" as type declaration. + +/* + * These should all be okay. + */ +// Incomplete function call, should be ignored by the sniff. +$incorrect_but_ok = esc_html(); +$incorrect_but_ok = wp_kses(); + +// Parameter does not contain a function call. +esc_html_x( $hook_name ); +\wp_kses_data( $obj->_deprecated_function() ); +esc_attr__( CONSTANT_NAME, ); +\ESC_ATTR_X( "do_$something" ); + +esc_attr(...$params); // PHP 5.6 argument unpacking. + +#[Esc_Textarea('text')] // PHP 8.0+ class instantiation via an attribute. Can't contain a nested function call anyway. +function foo() {} + +array_walk($text_strings, \esc_html__(...),); // PHP 8.1 first class callable. + +// Parameter contains a function call but not calling one of the printing functions. +esc_html( __( $something ) ); +Esc_URL_Raw( \get( $url ) ); +\esc_xml( deprecated_argument() ); // Note: missing "_" prefix for printing function. +wp_kses_one_attr ( + /* comment */ + get_attr(), +); + + +/* + * These should all be flagged. + */ +esc_html( _e( $something ) ); +esc_attr__( _deprecated_argument( $a ) ); +ESC_ATTR_E( _Deprecated_Constructor($a), ); +\esc_attr_x( _deprecated_file(), ); +esc_attr( _deprecated_function() ); +esc_HTML__( _deprecated_hook() ); +esc_html_e( _Doing_It_Wrong( $a ) ); +esc_html_X( _e( $foo ), ); +\esc_html( /*comment*/ _ex( $foo ) ); +esc_js( printf( $foo ) ); +Esc_textarea( trigger_error( $foo ) ); +\esc_URL_raw( user_Error( $foo, '' ), ); +esc_url( vprintf( $foo, ) /*comment*/ ); +WP_Kses_Data( WP_DIE( $foo ), ); +wp_kses_one_attr /*comment*/ ( wp_dropdown_pages( $pages ) ); +wp_kses_post( + _deprecated_function( $fn ) +); +wp_kses( _ex() ); diff --git a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php index 52734c3f..7b1b6106 100644 --- a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php @@ -23,7 +23,23 @@ class EscapingVoidReturnFunctionsUnitTest extends AbstractSniffUnitTest { */ public function getErrorList() { return [ - 3 => 1, + 50 => 1, + 51 => 1, + 52 => 1, + 53 => 1, + 54 => 1, + 55 => 1, + 56 => 1, + 57 => 1, + 58 => 1, + 59 => 1, + 60 => 1, + 61 => 1, + 62 => 1, + 63 => 1, + 64 => 1, + 65 => 1, + 68 => 1, ]; } From b01a801f5e6e16fc774469a526af0111c4868a9b Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 26 Jul 2025 18:07:38 +0200 Subject: [PATCH 109/165] Security/EscapingVoidReturnFunctions: document support for `$customPrintingFunctions` As the sniff uses the WPCS `WordPressCS\WordPress\Helpers\PrintingFunctionsTrait`, a `public` (user-settable) `$customPrintingFunctions` property is automatically supported. This commit adds tests documenting this. --- .../Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc | 6 ++++++ .../Tests/Security/EscapingVoidReturnFunctionsUnitTest.php | 2 ++ 2 files changed, 8 insertions(+) diff --git a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc index 1276894d..0f68e1a0 100644 --- a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc @@ -66,3 +66,9 @@ wp_kses_post( _deprecated_function( $fn ) ); wp_kses( _ex() ); + +// Adding custom printing functions is supported. +// phpcs:set WordPressVIPMinimum.Security.EscapingVoidReturnFunctions customPrintingFunctions[] to_screen,my_print +esc_attr( to_screen( $var1 ) ); +\wp_kses_post( my_print() ); +// phpcs:set WordPressVIPMinimum.Security.EscapingVoidReturnFunctions customPrintingFunctions[] diff --git a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php index 7b1b6106..14636677 100644 --- a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php @@ -40,6 +40,8 @@ public function getErrorList() { 64 => 1, 65 => 1, 68 => 1, + 72 => 1, + 73 => 1, ]; } From 0193bee16b16abdbeddcf7be1c062f77640c463f Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 26 Jul 2025 18:21:04 +0200 Subject: [PATCH 110/165] Security/EscapingVoidReturnFunctions: bug fix - false negative with fully qualified printing functions As things were, the sniff would not recognize nested printing functions if the printing function was a fully qualified function call. Fixed now. Safeguarded by updating some of the pre-existing tests. --- .../Sniffs/Security/EscapingVoidReturnFunctionsSniff.php | 5 ++++- .../Security/EscapingVoidReturnFunctionsUnitTest.inc | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php index fda5e9f5..7df88e54 100644 --- a/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php @@ -59,7 +59,10 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p return; } - $next_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, $next_token + 1, null, true ); + $ignore = Tokens::$emptyTokens; + $ignore[ T_NS_SEPARATOR ] = T_NS_SEPARATOR; + + $next_token = $this->phpcsFile->findNext( $ignore, $next_token + 1, null, true ); if ( $this->tokens[ $next_token ]['code'] !== T_STRING ) { // Not what we are looking for. diff --git a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc index 0f68e1a0..c635f49c 100644 --- a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc @@ -48,22 +48,22 @@ wp_kses_one_attr ( * These should all be flagged. */ esc_html( _e( $something ) ); -esc_attr__( _deprecated_argument( $a ) ); +esc_attr__( \_deprecated_argument( $a ) ); ESC_ATTR_E( _Deprecated_Constructor($a), ); \esc_attr_x( _deprecated_file(), ); esc_attr( _deprecated_function() ); esc_HTML__( _deprecated_hook() ); esc_html_e( _Doing_It_Wrong( $a ) ); -esc_html_X( _e( $foo ), ); +esc_html_X( \_e( $foo ), ); \esc_html( /*comment*/ _ex( $foo ) ); esc_js( printf( $foo ) ); Esc_textarea( trigger_error( $foo ) ); -\esc_URL_raw( user_Error( $foo, '' ), ); +\esc_URL_raw( \user_Error( $foo, '' ), ); esc_url( vprintf( $foo, ) /*comment*/ ); WP_Kses_Data( WP_DIE( $foo ), ); wp_kses_one_attr /*comment*/ ( wp_dropdown_pages( $pages ) ); wp_kses_post( - _deprecated_function( $fn ) + \_deprecated_function( $fn ) ); wp_kses( _ex() ); From 8bf925f33a7382185daeda3a33c5d6ce85af416a Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 26 Jul 2025 18:26:14 +0200 Subject: [PATCH 111/165] Security/EscapingVoidReturnFunctions: bug fix - false negative for `tag_escape()` The `tag_escape()` function is also one of the WP native escaping functions, but was not examined by this sniff. Fixed now. Includes test. Ref: * https://developer.wordpress.org/reference/functions/tag_escape/ --- .../Sniffs/Security/EscapingVoidReturnFunctionsSniff.php | 5 +++-- .../Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc | 3 +++ .../Tests/Security/EscapingVoidReturnFunctionsUnitTest.php | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php index 7df88e54..7929251d 100644 --- a/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php @@ -37,8 +37,9 @@ class EscapingVoidReturnFunctionsSniff extends AbstractFunctionParameterSniff { * @var array Keys are target functions, value irrelevant. */ protected $target_functions = [ - 'esc_*' => true, - 'wp_kses*' => true, + 'esc_*' => true, + 'tag_escape' => true, + 'wp_kses*' => true, ]; /** diff --git a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc index c635f49c..4a7f3303 100644 --- a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc @@ -72,3 +72,6 @@ wp_kses( _ex() ); esc_attr( to_screen( $var1 ) ); \wp_kses_post( my_print() ); // phpcs:set WordPressVIPMinimum.Security.EscapingVoidReturnFunctions customPrintingFunctions[] + +tag_escape( $tag ); // OK. +tag_escape( _e() ); // Bad. diff --git a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php index 14636677..a1ac0e1f 100644 --- a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php @@ -42,6 +42,7 @@ public function getErrorList() { 68 => 1, 72 => 1, 73 => 1, + 77 => 1, ]; } From ab01e6f5ad6df88b0ef7ef098c57c87a72e5928c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 26 Jul 2025 18:34:39 +0200 Subject: [PATCH 112/165] Security/EscapingVoidReturnFunctions: bug fix - false positive for printing, not functions As things were, the sniff would presume that if the first non-empty token within the escaping function call was a `T_STRING`, it would be a function call. As the `T_STRING` token is used for quite a few things, this would lead to false positives. Fixed now by making sure there is an open parenthesis after the `T_STRING`. Includes test. Ref: * https://developer.wordpress.org/reference/functions/tag_escape/ --- .../Sniffs/Security/EscapingVoidReturnFunctionsSniff.php | 7 ++++++- .../Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php index 7929251d..20de24ad 100644 --- a/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php @@ -64,12 +64,17 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $ignore[ T_NS_SEPARATOR ] = T_NS_SEPARATOR; $next_token = $this->phpcsFile->findNext( $ignore, $next_token + 1, null, true ); - if ( $this->tokens[ $next_token ]['code'] !== T_STRING ) { // Not what we are looking for. return; } + $next_after = $this->phpcsFile->findNext(Tokens::$emptyTokens, $next_token + 1, null, true ); + if ( $this->tokens[ $next_after ]['code'] !== T_OPEN_PARENTHESIS ) { + // Not a function call inside the escaping function. + return; + } + if ( $this->is_printing_function( $this->tokens[ $next_token ]['content'] ) ) { $message = 'Attempting to escape `%s()` which is printing its output.'; $data = [ $this->tokens[ $next_token ]['content'] ]; diff --git a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc index 4a7f3303..cc9273f6 100644 --- a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc @@ -75,3 +75,7 @@ esc_attr( to_screen( $var1 ) ); tag_escape( $tag ); // OK. tag_escape( _e() ); // Bad. + +// Bug: these are not function calls inside. +esc_attr__( User_Error::CONSTANT_NAME, ); // OK. +esc_js( _doing_it_wrong::class ); // OK, PHP 5.5 ::class resolution. From e17d08baa35c8e49d19807db8c841189b9616e48 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 26 Jul 2025 23:32:55 +0200 Subject: [PATCH 113/165] Security/EscapingVoidReturnFunctions: add support for PHP 8.0+ named parameters To allow support for named parameters, we need to know the position and name of the parameter to examine within a function call. Previously, this sniff used "wildcard" matching for `esc_*` and `wp_kses*` functions. While the parameter position is the same for all of these, the parameter name is not, so we can no longer use wildcard matching if we want the sniff to support PHP 8.0+ function calls using named parameters. To this end: 1. Change the `$target_functions` property to be explicit about the functions the sniff is targetting. Notes: - I've included all WP native `esc_*` functions with the exception of `esc_sql()` which doesn't feel like it belongs in this list. Please let me know if you prefer that `esc_sql()` is still included. - I've included all WP native `wp_kses_*` functions with the exception of `wp_kses_allowed_html()` which is not an escaping function. - Also take note that this also means that custom/user defined `esc_*`/`wp_kses*` functions wrapping printing functions will no longer be flagged. 2. Changed the `$target_functions` property to contain information about the target parameter name and position. 3. Adjusted the logic in the sniff to allow for named parameters using the new PHPCSUtils 1.0.0-alpha4 `PassedParameters::getParameterFromStack()` method. Includes additional unit tests. --- .../EscapingVoidReturnFunctionsSniff.php | 96 ++++++++++++++++--- .../EscapingVoidReturnFunctionsUnitTest.inc | 19 ++++ .../EscapingVoidReturnFunctionsUnitTest.php | 3 + 3 files changed, 107 insertions(+), 11 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php index 20de24ad..ddbdef9e 100644 --- a/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/EscapingVoidReturnFunctionsSniff.php @@ -10,6 +10,7 @@ namespace WordPressVIPMinimum\Sniffs\Security; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\PassedParameters; use WordPressCS\WordPress\AbstractFunctionParameterSniff; use WordPressCS\WordPress\Helpers\PrintingFunctionsTrait; @@ -34,12 +35,82 @@ class EscapingVoidReturnFunctionsSniff extends AbstractFunctionParameterSniff { /** * Functions this sniff is looking for. * - * @var array Keys are target functions, value irrelevant. + * @var array Keys are the target functions, + * value, the name and position of the target parameter. */ protected $target_functions = [ - 'esc_*' => true, - 'tag_escape' => true, - 'wp_kses*' => true, + 'esc_attr' => [ + 'param_position' => 1, + 'param_name' => 'text', + ], + 'esc_attr__' => [ + 'param_position' => 1, + 'param_name' => 'text', + ], + 'esc_attr_e' => [ + 'param_position' => 1, + 'param_name' => 'text', + ], + 'esc_attr_x' => [ + 'param_position' => 1, + 'param_name' => 'text', + ], + 'esc_html' => [ + 'param_position' => 1, + 'param_name' => 'text', + ], + 'esc_html__' => [ + 'param_position' => 1, + 'param_name' => 'text', + ], + 'esc_html_e' => [ + 'param_position' => 1, + 'param_name' => 'text', + ], + 'esc_html_x' => [ + 'param_position' => 1, + 'param_name' => 'text', + ], + 'esc_js' => [ + 'param_position' => 1, + 'param_name' => 'text', + ], + 'esc_textarea' => [ + 'param_position' => 1, + 'param_name' => 'text', + ], + 'esc_url' => [ + 'param_position' => 1, + 'param_name' => 'url', + ], + 'esc_url_raw' => [ + 'param_position' => 1, + 'param_name' => 'url', + ], + 'esc_xml' => [ + 'param_position' => 1, + 'param_name' => 'text', + ], + 'tag_escape' => [ + 'param_position' => 1, + 'param_name' => 'tag_name', + ], + 'wp_kses' => [ + 'param_position' => 1, + 'param_name' => 'content', + ], + 'wp_kses_data' => [ + 'param_position' => 1, + 'param_name' => 'data', + ], + 'wp_kses_one_attr' => [ + 'param_position' => 1, + 'param_name' => 'attr', + ], + 'wp_kses_post' => [ + 'param_position' => 1, + 'param_name' => 'data', + ], ]; /** @@ -54,23 +125,26 @@ class EscapingVoidReturnFunctionsSniff extends AbstractFunctionParameterSniff { * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - $next_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); - if ( $this->tokens[ $next_token ]['code'] !== T_OPEN_PARENTHESIS ) { - // Not a function call. + $param_position = $this->target_functions[ $matched_content ]['param_position']; + $param_name = $this->target_functions[ $matched_content ]['param_name']; + + $target_param = PassedParameters::getParameterFromStack( $parameters, $param_position, $param_name ); + if ( $target_param === false ) { + // Missing (required) target parameter. Probably live coding, nothing to examine (yet). Bow out. return; } $ignore = Tokens::$emptyTokens; $ignore[ T_NS_SEPARATOR ] = T_NS_SEPARATOR; - $next_token = $this->phpcsFile->findNext( $ignore, $next_token + 1, null, true ); - if ( $this->tokens[ $next_token ]['code'] !== T_STRING ) { + $next_token = $this->phpcsFile->findNext( $ignore, $target_param['start'], ( $target_param['end'] + 1 ), true ); + if ( $next_token === false || $this->tokens[ $next_token ]['code'] !== T_STRING ) { // Not what we are looking for. return; } - $next_after = $this->phpcsFile->findNext(Tokens::$emptyTokens, $next_token + 1, null, true ); - if ( $this->tokens[ $next_after ]['code'] !== T_OPEN_PARENTHESIS ) { + $next_after = $this->phpcsFile->findNext( Tokens::$emptyTokens, $next_token + 1, ( $target_param['end'] + 1 ), true ); + if ( $next_after === false || $this->tokens[ $next_after ]['code'] !== T_OPEN_PARENTHESIS ) { // Not a function call inside the escaping function. return; } diff --git a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc index cc9273f6..3ab7ba59 100644 --- a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.inc @@ -79,3 +79,22 @@ tag_escape( _e() ); // Bad. // Bug: these are not function calls inside. esc_attr__( User_Error::CONSTANT_NAME, ); // OK. esc_js( _doing_it_wrong::class ); // OK, PHP 5.5 ::class resolution. + +// Safeguard handling of function calls using PHP 8.0+ named parameters. +esc_attr_x( context: get_context(), domain: _e(),); // OK, well, not really, missing required $text param, but that's not the concern of this sniff. +esc_url_raw(protocols: $protocols, url: $url); // OK. +wp_kses_one_attr(element: $element, att: trigger_error( $foo ) ); // OK, well, not really, typo in param name, but that's not the concern of the sniff. +wp_kses( content: \not_deprecated_function( $fn ) ); // OK. +wp_kses( content: /*comment*/ ); // OK, well, not really, invalid function call, but that's not the concern of the sniff. + +esc_html_x(context: $c, text: _doing_it_wrong() ); // Bad. +wp_kses(allowed_html: $allowed_html, content: printf() ); // Bad. +esc_url(protocols: $protocols, url: vprintf()); // Bad. + +// These are no longer flagged as they are not "escaping" functions for the purpose of this sniff. +esc_sql( trigger_error( $foo ) ); // OK. +wp_kses_allowed_html( _deprecated_function() ); // OK. + +// These are no longer flagged as these are not the WP native escaping functions, so we don't know what parameter to check. +esc_something( trigger_error( $foo ) ); // OK. +wp_kses_page( _deprecated_function() ); // OK. diff --git a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php index a1ac0e1f..954e2bb2 100644 --- a/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/EscapingVoidReturnFunctionsUnitTest.php @@ -43,6 +43,9 @@ public function getErrorList() { 72 => 1, 73 => 1, 77 => 1, + 90 => 1, + 91 => 1, + 92 => 1, ]; } From b9f3242cf80b1c0079da5430a3c00e38c7e5c492 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 27 Jul 2025 05:07:13 +0200 Subject: [PATCH 114/165] Functions/DynamicCalls: move parse error test to own file --- ...nitTest.inc => DynamicCallsUnitTest.1.inc} | 3 --- .../Functions/DynamicCallsUnitTest.2.inc | 4 ++++ .../Tests/Functions/DynamicCallsUnitTest.php | 21 +++++++++++++------ 3 files changed, 19 insertions(+), 9 deletions(-) rename WordPressVIPMinimum/Tests/Functions/{DynamicCallsUnitTest.inc => DynamicCallsUnitTest.1.inc} (92%) create mode 100644 WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.2.inc diff --git a/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.inc b/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.1.inc similarity index 92% rename from WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.inc rename to WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.1.inc index fc307b2f..020383bb 100644 --- a/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.1.inc @@ -33,6 +33,3 @@ $ensure_no_notices_are_thrown_on_parse_error = /*comment*/ ; $test_double_quoted_string = "assert"; $test_double_quoted_string(); // Bad. - -// Intentional parse error. This has to be the last test in the file. -$my_notokay_func diff --git a/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.2.inc b/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.2.inc new file mode 100644 index 00000000..fca40064 --- /dev/null +++ b/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.2.inc @@ -0,0 +1,4 @@ + Key is the line number, value is the number of expected errors. */ - public function getErrorList() { - return [ - 9 => 1, - 15 => 1, - 35 => 1, - ]; + public function getErrorList( $testFile = '' ) { + + switch ( $testFile ) { + case 'DynamicCallsUnitTest.1.inc': + return [ + 9 => 1, + 15 => 1, + 35 => 1, + ]; + + default: + return []; + } } /** From 0f6f71b03c84cd2c5b640e6f8ee43bedf4936a56 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 27 Jul 2025 05:41:22 +0200 Subject: [PATCH 115/165] Functions/DynamicCalls: add extra test ... to raise code coverage. --- WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.1.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.1.inc b/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.1.inc index 020383bb..129d0717 100644 --- a/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.1.inc @@ -3,7 +3,7 @@ function my_test() { echo esc_html( "foo" ); } - +$irrelevant = 10; $my_notokay_func = 'extract'; $my_notokay_func(); // Bad. From 3a3da1a5fd7f86b8444ec44baeff17209f5f28aa Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 27 Jul 2025 05:33:39 +0200 Subject: [PATCH 116/165] Functions/DynamicCalls: remove unnecessary property The `$stackPtr` is not being changed in the functions using it, so there is no need to store it, it can just be passed as a parameter. --- .../Sniffs/Functions/DynamicCallsSniff.php | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php b/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php index a6bf96a3..68ab3091 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php @@ -54,13 +54,6 @@ class DynamicCallsSniff extends Sniff { */ private $variables_arr = []; - /** - * The position in the stack where the token was found. - * - * @var int - */ - private $stackPtr; - /** * Returns the token types that this sniff is interested in. * @@ -78,24 +71,24 @@ public function register() { * @return void */ public function process_token( $stackPtr ) { - $this->stackPtr = $stackPtr; - // First collect all variables encountered and their values. - $this->collect_variables(); + $this->collect_variables( $stackPtr ); // Then find all dynamic calls, and report them. - $this->find_dynamic_calls(); + $this->find_dynamic_calls( $stackPtr ); } /** * Finds any variable-definitions in the file being processed and stores them * internally in a private array. * + * @param int $stackPtr The position in the stack where the token was found. + * * @return void */ - private function collect_variables() { + private function collect_variables( $stackPtr ) { - $current_var_name = $this->tokens[ $this->stackPtr ]['content']; + $current_var_name = $this->tokens[ $stackPtr ]['content']; /* * Find assignments ( $foo = "bar"; ) by finding all non-whitespaces, @@ -103,7 +96,7 @@ private function collect_variables() { */ $t_item_key = $this->phpcsFile->findNext( Tokens::$emptyTokens, - $this->stackPtr + 1, + $stackPtr + 1, null, true, null, @@ -160,9 +153,11 @@ private function collect_variables() { * * Report on this when found, using the name of the function in the message. * + * @param int $stackPtr The position in the stack where the token was found. + * * @return void */ - private function find_dynamic_calls() { + private function find_dynamic_calls( $stackPtr ) { // No variables detected; no basis for doing anything. if ( empty( $this->variables_arr ) ) { return; @@ -172,20 +167,20 @@ private function find_dynamic_calls() { * If variable is not found in our registry of variables, do nothing, as we cannot be * sure that the function being called is one of the disallowed ones. */ - if ( ! isset( $this->variables_arr[ $this->tokens[ $this->stackPtr ]['content'] ] ) ) { + if ( ! isset( $this->variables_arr[ $this->tokens[ $stackPtr ]['content'] ] ) ) { return; } /* * Check if we have an '(' next. */ - $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $this->stackPtr + 1 ), null, true ); + $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); if ( $next === false || $this->tokens[ $next ]['code'] !== T_OPEN_PARENTHESIS ) { return; } $message = 'Dynamic calling is not recommended in the case of %s().'; - $data = [ $this->variables_arr[ $this->tokens[ $this->stackPtr ]['content'] ] ]; - $this->phpcsFile->addError( $message, $this->stackPtr, 'DynamicCalls', $data ); + $data = [ $this->variables_arr[ $this->tokens[ $stackPtr ]['content'] ] ]; + $this->phpcsFile->addError( $message, $stackPtr, 'DynamicCalls', $data ); } } From 3e73a966fedb5d01c1de3407647098e5c85ec266 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 27 Jul 2025 05:40:36 +0200 Subject: [PATCH 117/165] Functions/DynamicCalls: minor tweak For a `findNext()` to find the first non-empty token, limiting to the "current statement" is not really necessary as it will stop quickly anyway. --- .../Sniffs/Functions/DynamicCallsSniff.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php b/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php index 68ab3091..312a2b36 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php @@ -94,15 +94,7 @@ private function collect_variables( $stackPtr ) { * Find assignments ( $foo = "bar"; ) by finding all non-whitespaces, * and checking if the first one is T_EQUAL. */ - $t_item_key = $this->phpcsFile->findNext( - Tokens::$emptyTokens, - $stackPtr + 1, - null, - true, - null, - true - ); - + $t_item_key = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); if ( $t_item_key === false || $this->tokens[ $t_item_key ]['code'] !== T_EQUAL ) { return; } From 14e1be4d3896d7e59704ddf972adcf6e9d02b97d Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 27 Jul 2025 05:46:55 +0200 Subject: [PATCH 118/165] Functions/DynamicCalls: bug fix - better end of statement determination Stabilize the "find end of statement" search. After all, assignments may end on a comma or various other tokens too. Includes two tests to demonstrate the bug. --- .../Sniffs/Functions/DynamicCallsSniff.php | 25 ++++++++++++++++--- .../Functions/DynamicCallsUnitTest.1.inc | 13 ++++++++++ .../Tests/Functions/DynamicCallsUnitTest.php | 3 +++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php b/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php index 312a2b36..2236d44c 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php @@ -45,6 +45,22 @@ class DynamicCallsSniff extends Sniff { 'parse_str' => true, ]; + /** + * Potential end tokens for which the end pointer has to be set back by one. + * + * {@internal The PHPCS `findEndOfStatement()` method is not completely consistent + * in how it returns the statement end. This is just a simple way to bypass + * the inconsistency for our purposes.} + * + * @var array + */ + private $inclusiveStopPoints = [ + T_COLON => true, + T_COMMA => true, + T_DOUBLE_ARROW => true, + T_SEMICOLON => true, + ]; + /** * Array of variable assignments encountered, along with their values. * @@ -102,10 +118,13 @@ private function collect_variables( $stackPtr ) { /* * Find assignments which only assign a plain text string. */ - $end_of_statement = $this->phpcsFile->findNext( [ T_SEMICOLON, T_CLOSE_TAG ], ( $t_item_key + 1 ) ); - $value_ptr = null; + $end_of_statement = $this->phpcsFile->findEndOfStatement( ( $t_item_key + 1 ) ); + if ( isset( $this->inclusiveStopPoints[ $this->tokens[ $end_of_statement ]['code'] ] ) === true ) { + --$end_of_statement; + } - for ( $i = $t_item_key + 1; $i < $end_of_statement; $i++ ) { + $value_ptr = null; + for ( $i = $t_item_key + 1; $i <= $end_of_statement; $i++ ) { if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) === true ) { continue; } diff --git a/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.1.inc b/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.1.inc index 129d0717..976c4c00 100644 --- a/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.1.inc @@ -33,3 +33,16 @@ $ensure_no_notices_are_thrown_on_parse_error = /*comment*/ ; $test_double_quoted_string = "assert"; $test_double_quoted_string(); // Bad. + +function hasStaticVars() { + static $staticvar_foo = 'irrelevant', $staticvar_bar = 'func_num_args', $staticvar_baz = 'nothing'; + $staticvar_foo(); // OK. + $staticvar_bar(); // Bad. + $staticvar_baz(); // OK. +} + +function functionParams( $param_foo = 'func_get_arg', $param_bar = 'nothing', $param_baz = 'func_num_args') { + $param_foo(); // Bad. + $param_bar(); // OK. + $param_baz(); // Bad. +} diff --git a/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.php b/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.php index ab7a08e3..1b1c238e 100644 --- a/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Functions/DynamicCallsUnitTest.php @@ -31,6 +31,9 @@ public function getErrorList( $testFile = '' ) { 9 => 1, 15 => 1, 35 => 1, + 40 => 1, + 45 => 1, + 47 => 1, ]; default: From ce822662ee3ca7d28b451174cee720a00215d1fc Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 27 Jul 2025 07:42:17 +0200 Subject: [PATCH 119/165] Functions/RestrictedFunctions::is_targetted_token(): improve the method The `RestrictedFunctions::is_targetted_token()` method was basically a duplicate of the upstream/parent method with some additional code to handle one specific situation (method calls on a specific variable). However, the parent method has been updated significantly since the code was copied and this sniff wasn't benefitting from that. This commit rewrites the `RestrictedFunctions::is_targetted_token()` method to defer to the parent method in all cases, except for the one specific situation we want to account for. Additionally, it stabilizes and improves the token walking done for that specific situation, as well as takes the PHP 8.0+ nullsafe object operator into account. Includes a number of tests, some to cover the improvements inherited from the parent method, some to cover the improved code in the overloaded method. Includes documenting how PHP 8.1+ first class callables are handled by the sniff. --- .../Functions/RestrictedFunctionsSniff.php | 56 ++++++------------- .../Functions/RestrictedFunctionsUnitTest.inc | 16 ++++++ .../Functions/RestrictedFunctionsUnitTest.php | 3 + 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Functions/RestrictedFunctionsSniff.php b/WordPressVIPMinimum/Sniffs/Functions/RestrictedFunctionsSniff.php index a5a4ecec..bff5ce8d 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/RestrictedFunctionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/RestrictedFunctionsSniff.php @@ -303,47 +303,27 @@ public function getGroups() { * @return bool */ public function is_targetted_token( $stackPtr ) { - // Exclude function definitions, class methods, and namespaced calls. - if ( $this->tokens[ $stackPtr ]['code'] === \T_STRING && isset( $this->tokens[ $stackPtr - 1 ] ) ) { - // Check if this is really a function. - $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); - if ( $next !== false && $this->tokens[ $next ]['code'] !== T_OPEN_PARENTHESIS ) { - return false; - } + if ( empty( $this->groups[ $this->tokens[ $stackPtr ]['content'] ]['object_var'] ) ) { + return parent::is_targetted_token( $stackPtr ); + } - $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true ); - if ( $prev !== false ) { + // Start difference to parent class method. + // Check to see if the token is a method call on a specific object variable. + $next = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); + if ( $next === false || $this->tokens[ $next ]['code'] !== T_OPEN_PARENTHESIS ) { + return false; + } - // Start difference to parent class method. - // Check to see if function is a method on a specific object variable. - if ( ! empty( $this->groups[ $this->tokens[ $stackPtr ]['content'] ]['object_var'] ) ) { - $prevPrev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 2, null, true ); + $prev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true ); + if ( $this->tokens[ $prev ]['code'] !== T_OBJECT_OPERATOR + && $this->tokens[ $prev ]['code'] !== T_NULLSAFE_OBJECT_OPERATOR + ) { + return false; + } - return $this->tokens[ $prev ]['code'] === \T_OBJECT_OPERATOR && isset( $this->groups[ $this->tokens[ $stackPtr ]['content'] ]['object_var'][ $this->tokens[ $prevPrev ]['content'] ] ); - } // End difference to parent class method. + $prevPrev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $prev - 1, null, true ); - // Skip sniffing if calling a same-named method, or on function definitions. - $skipped = [ - \T_FUNCTION => \T_FUNCTION, - \T_CLASS => \T_CLASS, - \T_AS => \T_AS, // Use declaration alias. - \T_DOUBLE_COLON => \T_DOUBLE_COLON, - \T_OBJECT_OPERATOR => \T_OBJECT_OPERATOR, - \T_NEW => \T_NEW, - ]; - if ( isset( $skipped[ $this->tokens[ $prev ]['code'] ] ) ) { - return false; - } - // Skip namespaced functions, ie: `\foo\bar()` not `\bar()`. - if ( $this->tokens[ $prev ]['code'] === \T_NS_SEPARATOR ) { - $pprev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, $prev - 1, null, true ); - if ( $pprev !== false && $this->tokens[ $pprev ]['code'] === \T_STRING ) { - return false; - } - } - } - return true; - } - return false; + return $this->tokens[ $prevPrev ]['code'] === T_VARIABLE + && isset( $this->groups[ $this->tokens[ $stackPtr ]['content'] ]['object_var'][ $this->tokens[ $prevPrev ]['content'] ] ); } } diff --git a/WordPressVIPMinimum/Tests/Functions/RestrictedFunctionsUnitTest.inc b/WordPressVIPMinimum/Tests/Functions/RestrictedFunctionsUnitTest.inc index 19cba6b3..4a3f57b4 100644 --- a/WordPressVIPMinimum/Tests/Functions/RestrictedFunctionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Functions/RestrictedFunctionsUnitTest.inc @@ -230,3 +230,19 @@ $popular = custom_stats_get_csv( 'postviews', [ 'days' => 2, 'limit' => 20 ] $foo = new Link; // OK, class, not function. $foo = new Mail(); // OK, class, not function. + +class ReturnByRef { + function &chmod($a) {} // OK, method declaration, not function call. +} + +// Class instantiations in PHP 8.0+ attributes are not function calls. +class AttributesShouldBeIgnored { + #[WP_Is_Mobile()] // OK. + public function foo() {} +} + +$wp_rewrite->flush_rules; // OK, property access, not function call. +$wp_rewrite /*comment*/ -> /*comment*/ flush_rules(); // Error. +$wp_rewrite?->flush_rules(); // Error. + +array_walk($roles, add_role(...)); // Error. PHP 8.1 first class callable. diff --git a/WordPressVIPMinimum/Tests/Functions/RestrictedFunctionsUnitTest.php b/WordPressVIPMinimum/Tests/Functions/RestrictedFunctionsUnitTest.php index 0b4fcc5c..1c727fac 100644 --- a/WordPressVIPMinimum/Tests/Functions/RestrictedFunctionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Functions/RestrictedFunctionsUnitTest.php @@ -97,6 +97,9 @@ public function getErrorList() { 199 => 1, 200 => 1, 228 => 1, + 245 => 1, + 246 => 1, + 248 => 1, ]; } From a9daba1ba2ed05fdd6b650301b780d2764eb58f1 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 28 Jul 2025 13:50:23 +0200 Subject: [PATCH 120/165] Remove unused `WordPressVIPMinimum.Security.Twig` sniff As per the analysis in 859 Closes 859 Also loosely related to 442 --- .../Sniffs/Security/TwigSniff.php | 65 ------------------- .../Tests/Security/TwigUnitTest.inc | 60 ----------------- .../Tests/Security/TwigUnitTest.php | 47 -------------- WordPressVIPMinimum/ruleset.xml | 4 -- 4 files changed, 176 deletions(-) delete mode 100644 WordPressVIPMinimum/Sniffs/Security/TwigSniff.php delete mode 100644 WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc delete mode 100644 WordPressVIPMinimum/Tests/Security/TwigUnitTest.php diff --git a/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php b/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php deleted file mode 100644 index 0fe792f4..00000000 --- a/WordPressVIPMinimum/Sniffs/Security/TwigSniff.php +++ /dev/null @@ -1,65 +0,0 @@ - - */ - public function register() { - return Tokens::$textStringTokens; - } - - /** - * Processes this test, when one of its tokens is encountered. - * - * @param int $stackPtr The position of the current token in the stack passed in $tokens. - * - * @return void - */ - public function process_token( $stackPtr ) { - // Strip any potentially interpolated expressions. - $only_text = $this->tokens[ $stackPtr ]['content']; - if ( $this->tokens[ $stackPtr ]['code'] === T_DOUBLE_QUOTED_STRING - || $this->tokens[ $stackPtr ]['code'] === T_HEREDOC - ) { - $only_text = TextStrings::stripEmbeds( $only_text ); - } - - if ( preg_match( '/autoescape\s+false/', $only_text ) === 1 ) { - // Twig autoescape disabled. - $message = 'Found Twig autoescape disabling notation.'; - $this->phpcsFile->addWarning( $message, $stackPtr, 'AutoescapeFalse' ); - } - - if ( preg_match( '/\|\s*raw/', $only_text ) === 1 ) { - // Twig default unescape filter. - $message = 'Found Twig default unescape filter: "|raw".'; - $this->phpcsFile->addWarning( $message, $stackPtr, 'RawFound' ); - } - } -} diff --git a/WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc b/WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc deleted file mode 100644 index 0e766537..00000000 --- a/WordPressVIPMinimum/Tests/Security/TwigUnitTest.inc +++ /dev/null @@ -1,60 +0,0 @@ - - - - -{% autoescape false %} - Everything will be outputted as is in this block -{% endautoescape %} - -{% autoescape %} - {{ safe_value|raw }} -{% endautoescape %} -'; - -echo " -"; - -echo <<<'EOD' - -EOD; - -echo << -{% autoescape false %} - Everything will be $outputted as is in this {$obj->blocks[SOME_FLAG | raw]->name} -{% endautoescape %} -EOD; -echo <<<"EOD" -{% autoescape %} - {{ safe_value|raw }} -{% endautoescape %} - -EOD; diff --git a/WordPressVIPMinimum/Tests/Security/TwigUnitTest.php b/WordPressVIPMinimum/Tests/Security/TwigUnitTest.php deleted file mode 100644 index 0cea1379..00000000 --- a/WordPressVIPMinimum/Tests/Security/TwigUnitTest.php +++ /dev/null @@ -1,47 +0,0 @@ - Key is the line number, value is the number of expected errors. - */ - public function getErrorList() { - return []; - } - - /** - * Returns the lines where warnings should occur. - * - * @return array Key is the line number, value is the number of expected warnings. - */ - public function getWarningList() { - return [ - 5 => 1, - 10 => 1, - 17 => 1, - 22 => 1, - 27 => 1, - 32 => 1, - 39 => 1, - 44 => 1, - 51 => 1, - 57 => 1, - ]; - } -} diff --git a/WordPressVIPMinimum/ruleset.xml b/WordPressVIPMinimum/ruleset.xml index f5bdb847..2a45818a 100644 --- a/WordPressVIPMinimum/ruleset.xml +++ b/WordPressVIPMinimum/ruleset.xml @@ -27,10 +27,6 @@ - - *\.twig - - From b76edcc222317036793c456b04c63bfd35139f67 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 28 Jul 2025 14:26:31 +0200 Subject: [PATCH 121/165] Hard-deprecate the WordPressVIPMinimum.Functions.DynamicCalls sniff Removing the sniff needs to wait for a new major, as it would be a breaking change, however, in the mean time, we can: * Exclude the sniff from both rulesets. This will prevent PHP_CodeSniffer from throwing deprecation notices about the sniff. * Deprecate the sniff itself, in case users of the VIP-Coding-Standards would be explicitly including the sniff from within their own ruleset. Part of 517 --- WordPress-VIP-Go/ruleset-test.inc | 6 ++-- WordPress-VIP-Go/ruleset-test.php | 1 - WordPress-VIP-Go/ruleset.xml | 1 + .../Sniffs/Functions/DynamicCallsSniff.php | 30 ++++++++++++++++++- WordPressVIPMinimum/ruleset-test.inc | 6 ++-- WordPressVIPMinimum/ruleset-test.php | 1 - WordPressVIPMinimum/ruleset.xml | 5 ++++ 7 files changed, 41 insertions(+), 9 deletions(-) diff --git a/WordPress-VIP-Go/ruleset-test.inc b/WordPress-VIP-Go/ruleset-test.inc index bd7e3de6..c4ec4eeb 100644 --- a/WordPress-VIP-Go/ruleset-test.inc +++ b/WordPress-VIP-Go/ruleset-test.inc @@ -341,9 +341,9 @@ $my_theme_options = get_option( 'my_theme', false ); if ( array_key_exists( 'key', $my_theme_options ) ) { } // Error. echo 'My term link'; // Error. -// WordPressVIPMinimum.Functions.DynamicCalls -$my_notokay_func = 'extract'; -$my_notokay_func(); // Error. + + + // WordPressVIPMinimum.Functions.RestrictedFunctions diff --git a/WordPress-VIP-Go/ruleset-test.php b/WordPress-VIP-Go/ruleset-test.php index d8701db4..83cb5514 100644 --- a/WordPress-VIP-Go/ruleset-test.php +++ b/WordPress-VIP-Go/ruleset-test.php @@ -36,7 +36,6 @@ 337 => 1, 341 => 1, 342 => 1, - 346 => 1, 350 => 1, 351 => 1, 352 => 1, diff --git a/WordPress-VIP-Go/ruleset.xml b/WordPress-VIP-Go/ruleset.xml index e3272707..c7eddcf5 100644 --- a/WordPress-VIP-Go/ruleset.xml +++ b/WordPress-VIP-Go/ruleset.xml @@ -6,6 +6,7 @@ + diff --git a/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php b/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php index 2236d44c..3f19977a 100644 --- a/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Functions/DynamicCallsSniff.php @@ -9,6 +9,7 @@ namespace WordPressVIPMinimum\Sniffs\Functions; +use PHP_CodeSniffer\Sniffs\DeprecatedSniff; use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Utils\TextStrings; use WordPressVIPMinimum\Sniffs\Sniff; @@ -26,7 +27,7 @@ * * @link http://php.net/manual/en/migration71.incompatible.php */ -class DynamicCallsSniff extends Sniff { +class DynamicCallsSniff extends Sniff implements DeprecatedSniff { /** * Functions that should not be called dynamically. @@ -194,4 +195,31 @@ private function find_dynamic_calls( $stackPtr ) { $data = [ $this->variables_arr[ $this->tokens[ $stackPtr ]['content'] ] ]; $this->phpcsFile->addError( $message, $stackPtr, 'DynamicCalls', $data ); } + + /** + * Provide the version number in which the sniff was deprecated. + * + * @return string + */ + public function getDeprecationVersion() { + return 'VIP-Coding-Standard v3.1.0'; + } + + /** + * Provide the version number in which the sniff will be removed. + * + * @return string + */ + public function getRemovalVersion() { + return 'VIP-Coding-Standard v4.0.0'; + } + + /** + * Provide a custom message to display with the deprecation. + * + * @return string + */ + public function getDeprecationMessage() { + return ''; + } } diff --git a/WordPressVIPMinimum/ruleset-test.inc b/WordPressVIPMinimum/ruleset-test.inc index a20a8494..3a104038 100644 --- a/WordPressVIPMinimum/ruleset-test.inc +++ b/WordPressVIPMinimum/ruleset-test.inc @@ -303,9 +303,9 @@ $my_theme_options = get_option( 'my_theme', false ); if ( array_key_exists( 'key', $my_theme_options ) ) { } // Error. echo 'My term link'; // Error. -// WordPressVIPMinimum.Functions.DynamicCalls -$my_notokay_func = 'extract'; -$my_notokay_func(); // Error. + + + // WordPressVIPMinimum.Functions.RestrictedFunctions diff --git a/WordPressVIPMinimum/ruleset-test.php b/WordPressVIPMinimum/ruleset-test.php index e78ae6a7..4fc80840 100644 --- a/WordPressVIPMinimum/ruleset-test.php +++ b/WordPressVIPMinimum/ruleset-test.php @@ -81,7 +81,6 @@ 299 => 1, 303 => 1, 304 => 1, - 308 => 1, 312 => 1, 313 => 1, 314 => 1, diff --git a/WordPressVIPMinimum/ruleset.xml b/WordPressVIPMinimum/ruleset.xml index f5bdb847..50379d8d 100644 --- a/WordPressVIPMinimum/ruleset.xml +++ b/WordPressVIPMinimum/ruleset.xml @@ -18,6 +18,11 @@ rule in which to place the exclusion, so this will have to do for now. --> + + + From fe71096c3c4611f6ac9d6c3a227919716e5ed3e1 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 29 Jul 2025 21:03:46 +0200 Subject: [PATCH 122/165] UserExperience/AdminBarRemoval: add additional tests * Testing against false positives for calls to methods or namespaced function calls (= the issue being addressed in this PR). * Safeguarding that function calls using PHP 5.6+ argument unpacking are flagged. * Safeguarding handling of PHP 8.0+ attribute class using the same name as a target function. * Safeguarding that the function is not flagged when used as a PHP 8.1+ first class callable. * Adding tests with more variations: - Non-lowercase function call(s). - Fully qualified function calls for the escaping functions. - Use PHP 7.3+ trailing comma's in a few function calls. - Text strings using single quotes and double quotes. --- .../AdminBarRemovalUnitTest.inc | 36 +++++++++++++++++++ .../AdminBarRemovalUnitTest.php | 6 ++++ 2 files changed, 42 insertions(+) diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc index f452ae83..8e237467 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc @@ -107,3 +107,39 @@ EOT; show_admin_bar( false ); // OK. +$this?->show_admin_bar( false ); // OK. +MyClass::add_filter( 'show_admin_bar', '__return_false' ); // OK. +echo ADD_FILTER; // OK. +namespace\add_filter( 'show_admin_bar', '__return_false' ); // OK. + +#[Show_Admin_Bar(false)] // OK. PHP 8.0+ class instantiation via an attribute. Can't contain a nested function call anyway. +function foo() {} + +array_walk($filters, \add_filter(...),); // OK. PHP 8.1 first class callable. + +// Incomplete function calls, should be ignored by the sniff. +$incorrect_but_ok = show_admin_bar(); // OK. +$incorrect_but_ok = add_filter(); // OK. + +// Safeguard that the sniff only flags the "show_admin_bar" filter. +add_filter( 'not_show_admin_bar', '__return_false', ); // OK. + +// Document that dynamic values will be flagged. +show_admin_bar( $unknown ); // Bad. +add_filter( 'show_admin_bar', $callable, ); // Bad. +add_filter('show_admin_bar', ...$params); // Bad. PHP 5.6 argument unpacking. + +// Document that fully qualified function calls and functions in unconventional case will correctly be recognized. +\add_filter( "show_admin_bar", '__return_true' ); // OK. +\Add_Filter( 'show_admin_bar', "__return_false", ); // Bad. + +\show_Admin_bar( true, ); // OK. +\show_admin_bar( false ); // Bad. +\SHOW_ADMIN_BAR( false, ); // Bad. diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php index 8973a169..30455dc4 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php @@ -51,6 +51,12 @@ public function getErrorList( $testFile = '' ) { 103 => 1, 104 => 1, 105 => 1, + 135 => 1, + 136 => 1, + 137 => 1, + 141 => 1, + 144 => 1, + 145 => 1, ]; case 'AdminBarRemovalUnitTest.css': From d2bfefd39f164bb20d680b94b35f1902983d7765 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 02:01:10 +0200 Subject: [PATCH 123/165] UserExperience/AdminBarRemoval: bug fix - disregard comments in parameter values The PHPCSUtils `PassedParameters::getParameters()` return value includes a `'clean'` array index, which contains the contents of the parameter stripped of surrounding whitespace and comments. The `AdminBarRemoval` sniff uses the parameter contents in a couple of places to compare against a specific text string, but would break if the parameter value would contain a comment. Fixed now. Includes tests. --- .../Sniffs/UserExperience/AdminBarRemovalSniff.php | 8 +++++--- .../UserExperience/AdminBarRemovalUnitTest.inc | 13 +++++++++++++ .../UserExperience/AdminBarRemovalUnitTest.php | 1 + 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index ae98644b..35f1256b 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -201,19 +201,21 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p switch ( $matched_content ) { case 'show_admin_bar': $error = true; - if ( $this->remove_only === true && $parameters[1]['raw'] === 'true' ) { + if ( $this->remove_only === true && $parameters[1]['clean'] === 'true' ) { $error = false; } break; case 'add_filter': - $filter_name = TextStrings::stripQuotes( $parameters[1]['raw'] ); + $filter_name = TextStrings::stripQuotes( $parameters[1]['clean'] ); if ( $filter_name !== 'show_admin_bar' ) { break; } $error = true; - if ( $this->remove_only === true && isset( $parameters[2]['raw'] ) && TextStrings::stripQuotes( $parameters[2]['raw'] ) === '__return_true' ) { + if ( $this->remove_only === true && isset( $parameters[2]['clean'] ) + && TextStrings::stripQuotes( $parameters[2]['clean'] ) === '__return_true' + ) { $error = false; } break; diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc index 8e237467..1a174526 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc @@ -143,3 +143,16 @@ add_filter('show_admin_bar', ...$params); // Bad. PHP 5.6 argument unpacking. \show_Admin_bar( true, ); // OK. \show_admin_bar( false ); // Bad. \SHOW_ADMIN_BAR( false, ); // Bad. + +// Comments in parameters should be ignored. +show_admin_bar( true /* turn it on */ ); // OK. +add_filter( + // Admin bar gives access to admin for users with the right permissions. + 'show_admin_bar', + '__return_false' +); // Bad. +add_filter( + 'show_admin_bar', + // Turn it on. + '__return_true' +); // OK. diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php index 30455dc4..d244f95a 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php @@ -57,6 +57,7 @@ public function getErrorList( $testFile = '' ) { 141 => 1, 144 => 1, 145 => 1, + 149 => 1, ]; case 'AdminBarRemovalUnitTest.css': From dda5e878c996901b8663bb7e5b83fdbbe018ca34 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 21 Jul 2025 03:20:32 +0200 Subject: [PATCH 124/165] UserExperience/AdminBarRemoval: add support for handling PHP 8.0+ function calls using named parameters Includes tests. --- .../UserExperience/AdminBarRemovalSniff.php | 22 ++++++++++++++----- .../AdminBarRemovalUnitTest.inc | 11 ++++++++++ .../AdminBarRemovalUnitTest.php | 2 ++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index 35f1256b..8975133f 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Utils\GetTokensAsString; +use PHPCSUtils\Utils\PassedParameters; use PHPCSUtils\Utils\TextStrings; use WordPressCS\WordPress\AbstractFunctionParameterSniff; @@ -200,21 +201,32 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $error = false; switch ( $matched_content ) { case 'show_admin_bar': + $show_param = PassedParameters::getParameterFromStack( $parameters, 1, 'show' ); + if ( $show_param === false ) { + break; + } + $error = true; - if ( $this->remove_only === true && $parameters[1]['clean'] === 'true' ) { + if ( $this->remove_only === true && $show_param['clean'] === 'true' ) { $error = false; } break; case 'add_filter': - $filter_name = TextStrings::stripQuotes( $parameters[1]['clean'] ); + $hook_name_param = PassedParameters::getParameterFromStack( $parameters, 1, 'hook_name' ); + if ( $hook_name_param === false ) { + break; + } + + $filter_name = TextStrings::stripQuotes( $hook_name_param['clean'] ); if ( $filter_name !== 'show_admin_bar' ) { break; } - $error = true; - if ( $this->remove_only === true && isset( $parameters[2]['clean'] ) - && TextStrings::stripQuotes( $parameters[2]['clean'] ) === '__return_true' + $callback_param = PassedParameters::getParameterFromStack( $parameters, 2, 'callback' ); + $error = true; + if ( $this->remove_only === true && $callback_param !== false + && TextStrings::stripQuotes( $callback_param['clean'] ) === '__return_true' ) { $error = false; } diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc index 1a174526..67ae59fb 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc @@ -156,3 +156,14 @@ add_filter( // Turn it on. '__return_true' ); // OK. + +// Safeguard handling of function calls using PHP 8.0+ named parameters. +show_admin_bar( shown: false ); // OK, well not really, typo in param name, but that's not the concern of the sniff. +\show_admin_bar( show: true ); // OK. +show_admin_bar( show: $toggle ); // Bad. + +add_filter(callback: '__return_false', priority: 10); // OK, well, not really, missing required $hook_name param, but that's not the concern of this sniff. +\add_filter(callback: '__return_false', hook_name: 'not_our_target'); // OK. +add_filter(hookName: 'show_admin_bar', callback: '__return_false',); // OK, well, not really, typo in param name, but that's not the concern of the sniff. +add_filter( callback: '__return_true', hook_name: 'show_admin_bar', ); // Ok. +\add_filter( callback: '__return_false', hook_name: 'show_admin_bar', ); // Bad. diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php index d244f95a..5c162a90 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php @@ -58,6 +58,8 @@ public function getErrorList( $testFile = '' ) { 144 => 1, 145 => 1, 149 => 1, + 163 => 1, + 169 => 1, ]; case 'AdminBarRemovalUnitTest.css': From 2520ce515375c017dfb90be621f5525a840e040e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 29 Jul 2025 21:20:45 +0200 Subject: [PATCH 125/165] UserExperience/AdminBarRemoval: bug fix - recognize `add_action()` as alias ... for `add_filter()`. Includes test. --- .../Sniffs/UserExperience/AdminBarRemovalSniff.php | 2 ++ .../Tests/UserExperience/AdminBarRemovalUnitTest.inc | 3 +++ .../Tests/UserExperience/AdminBarRemovalUnitTest.php | 1 + 3 files changed, 6 insertions(+) diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index 8975133f..7e2c4160 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -53,6 +53,7 @@ class AdminBarRemovalSniff extends AbstractFunctionParameterSniff { protected $target_functions = [ 'show_admin_bar' => true, 'add_filter' => true, + 'add_action' => true, // Alias of add_filter(). ]; /** @@ -212,6 +213,7 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p } break; + case 'add_action': case 'add_filter': $hook_name_param = PassedParameters::getParameterFromStack( $parameters, 1, 'hook_name' ); if ( $hook_name_param === false ) { diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc index 67ae59fb..84cc193f 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc @@ -167,3 +167,6 @@ add_filter(callback: '__return_false', priority: 10); // OK, well, not really, m add_filter(hookName: 'show_admin_bar', callback: '__return_false',); // OK, well, not really, typo in param name, but that's not the concern of the sniff. add_filter( callback: '__return_true', hook_name: 'show_admin_bar', ); // Ok. \add_filter( callback: '__return_false', hook_name: 'show_admin_bar', ); // Bad. + +// Bug: add_action() is an alias of add_filter. +add_action( 'show_admin_bar', $callable, ); // Bad. diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php index 5c162a90..8a525957 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php @@ -60,6 +60,7 @@ public function getErrorList( $testFile = '' ) { 149 => 1, 163 => 1, 169 => 1, + 172 => 1, ]; case 'AdminBarRemovalUnitTest.css': From ba6bce6da72638ef0f871627a2c14ddb3469e716 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 29 Jul 2025 21:41:28 +0200 Subject: [PATCH 126/165] UserExperience/AdminBarRemoval: add support for recognizing PHP 8.1+ first class callables ... when used as the `$callback` parameter for `add_filter()`/`add_action()`. Includes tests. --- .../UserExperience/AdminBarRemovalSniff.php | 15 +++++++++++---- .../UserExperience/AdminBarRemovalUnitTest.inc | 7 +++++++ .../UserExperience/AdminBarRemovalUnitTest.php | 3 +++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index 7e2c4160..d8e9b193 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -227,10 +227,17 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $callback_param = PassedParameters::getParameterFromStack( $parameters, 2, 'callback' ); $error = true; - if ( $this->remove_only === true && $callback_param !== false - && TextStrings::stripQuotes( $callback_param['clean'] ) === '__return_true' - ) { - $error = false; + if ( $this->remove_only === true && $callback_param !== false ) { + $clean_param = TextStrings::stripQuotes( $callback_param['clean'] ); + + $expected = Tokens::$emptyTokens + Tokens::$stringTokens; + $has_non_textstring = $this->phpcsFile->findNext( $expected, $callback_param['start'], ( $callback_param['end'] + 1 ), true ); + if ( ( $has_non_textstring === false && $clean_param === '__return_true' ) + || ( $has_non_textstring !== false + && preg_match( '`^\\\\?__return_true\s*\(\s*\.\.\.\s*\)$`', $clean_param ) === 1 ) + ) { + $error = false; + } } break; } diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc index 84cc193f..23e5965d 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc @@ -170,3 +170,10 @@ add_filter( callback: '__return_true', hook_name: 'show_admin_bar', ); // Ok. // Bug: add_action() is an alias of add_filter. add_action( 'show_admin_bar', $callable, ); // Bad. + +// Safeguard handling of filter callback being passed as PHP 8.1+ first class callable. +add_filter( 'show_admin_bar', __return_true(...) ); // OK. +add_filter( 'show_admin_bar', \__return_true( ... ) ); // OK. +add_action( 'show_admin_bar', __return_false ( ... ) , ); // Bad. +add_filter( 'show_admin_bar', \__return_false(...) ); // Bad. +add_filter( 'show_admin_bar', "__return_true(...)" ); // Bad. Invalid callback. diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php index 8a525957..a9cc15db 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php @@ -61,6 +61,9 @@ public function getErrorList( $testFile = '' ) { 163 => 1, 169 => 1, 172 => 1, + 177 => 1, + 178 => 1, + 179 => 1, ]; case 'AdminBarRemovalUnitTest.css': From 25ce9dfa6112761154be5d3cdbf1255458480549 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 29 Jul 2025 21:48:41 +0200 Subject: [PATCH 127/165] UserExperience/AdminBarRemoval: bug fix - function names are case-insensitive ... even when passed as callbacks. Includes test. --- .../Sniffs/UserExperience/AdminBarRemovalSniff.php | 2 +- .../Tests/UserExperience/AdminBarRemovalUnitTest.inc | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index d8e9b193..6b2d8c33 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -228,7 +228,7 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p $callback_param = PassedParameters::getParameterFromStack( $parameters, 2, 'callback' ); $error = true; if ( $this->remove_only === true && $callback_param !== false ) { - $clean_param = TextStrings::stripQuotes( $callback_param['clean'] ); + $clean_param = strtolower( TextStrings::stripQuotes( $callback_param['clean'] ) ); $expected = Tokens::$emptyTokens + Tokens::$stringTokens; $has_non_textstring = $this->phpcsFile->findNext( $expected, $callback_param['start'], ( $callback_param['end'] + 1 ), true ); diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc index 23e5965d..abc0a248 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.inc @@ -177,3 +177,7 @@ add_filter( 'show_admin_bar', \__return_true( ... ) ); // OK. add_action( 'show_admin_bar', __return_false ( ... ) , ); // Bad. add_filter( 'show_admin_bar', \__return_false(...) ); // Bad. add_filter( 'show_admin_bar', "__return_true(...)" ); // Bad. Invalid callback. + +// Bug fix: function names are case-insensitive. +\add_filter( "show_admin_bar", '__Return_TRUE' ); // OK. +add_filter( 'show_admin_bar', __Return_True(...) ); // OK. From 52c0e1474eda4d123cffa121a5020d533aa6cfb3 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Mon, 21 Jul 2025 20:36:34 +0200 Subject: [PATCH 128/165] UserExperience/AdminBarRemoval: remove some redundant logic No need for a `switch` here. --- .../Tests/UserExperience/AdminBarRemovalUnitTest.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php index a9cc15db..88f7e823 100644 --- a/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php +++ b/WordPressVIPMinimum/Tests/UserExperience/AdminBarRemovalUnitTest.php @@ -98,15 +98,6 @@ public function getErrorList( $testFile = '' ) { * @return array Key is the line number, value is the number of expected warnings. */ public function getWarningList( $testFile = '' ) { - switch ( $testFile ) { - case 'AdminBarRemovalUnitTest.css': - return []; - - case 'AdminBarRemovalUnitTest.inc': - return []; - - default: - return []; - } + return []; } } From b42d40500d7dd5d066a045ad1212f47c08268270 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 15 Jul 2025 03:27:06 +0200 Subject: [PATCH 129/165] UserExperience/AdminBarRemoval: bug fix - CSS file might be handled as PHP file A number of IDEs will use STDIN to run PHPCS and will pass the `stdin_path` to PHPCS to set the file name, which often results in the filename being quoted. In that case, the original logic in this sniff would break as the `$file_extension` would end up being `'css"'`, which doesn't match the expected `'css'`. The new PHPCSUtils 1.1.0 `FilePath::getName()` method will strip quotes from the file name, as well as normalize the slashes to forward (*nix) slashes. Applying that method fixes the bug. --- .../Sniffs/UserExperience/AdminBarRemovalSniff.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index 6b2d8c33..de4c526d 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -11,6 +11,7 @@ namespace WordPressVIPMinimum\Sniffs\UserExperience; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\FilePath; use PHPCSUtils\Utils\GetTokensAsString; use PHPCSUtils\Utils\PassedParameters; use PHPCSUtils\Utils\TextStrings; @@ -158,7 +159,7 @@ public function register() { */ public function process_token( $stackPtr ) { - $file_name = $this->phpcsFile->getFilename(); + $file_name = FilePath::getName( $this->phpcsFile ); $file_extension = substr( strrchr( $file_name, '.' ), 1 ); if ( $file_extension === 'css' ) { From cd88a8b123069e669c241c91a0a8fc27b7dafe77 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Tue, 15 Jul 2025 03:28:35 +0200 Subject: [PATCH 130/165] UserExperience/AdminBarRemoval: minor simplification Let PHP sort out splitting the extension off the file name. --- .../Sniffs/UserExperience/AdminBarRemovalSniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index de4c526d..4f859fc4 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -160,7 +160,7 @@ public function register() { public function process_token( $stackPtr ) { $file_name = FilePath::getName( $this->phpcsFile ); - $file_extension = substr( strrchr( $file_name, '.' ), 1 ); + $file_extension = pathinfo( $file_name, \PATHINFO_EXTENSION ); if ( $file_extension === 'css' ) { if ( $this->tokens[ $stackPtr ]['code'] === \T_STYLE ) { From aef212e73c45a65bdd7bb0138c4279048e7bc56a Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Jul 2025 04:00:09 +0200 Subject: [PATCH 131/165] Performance/CacheValueOverride: rename test case file Preliminary commit to allow for adding additional test case files. --- ...est.inc => CacheValueOverrideUnitTest.1.inc} | 0 .../Performance/CacheValueOverrideUnitTest.php | 17 +++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) rename WordPressVIPMinimum/Tests/Performance/{CacheValueOverrideUnitTest.inc => CacheValueOverrideUnitTest.1.inc} (100%) diff --git a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.inc b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc similarity index 100% rename from WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.inc rename to WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc diff --git a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php index 71f89f1f..97ba9397 100644 --- a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php @@ -19,12 +19,21 @@ class CacheValueOverrideUnitTest extends AbstractSniffUnitTest { /** * Returns the lines where errors should occur. * + * @param string $testFile The name of the file being tested. + * * @return array Key is the line number, value is the number of expected errors. */ - public function getErrorList() { - return [ - 5 => 1, - ]; + public function getErrorList( $testFile = '' ) { + + switch ( $testFile ) { + case 'CacheValueOverrideUnitTest.1.inc': + return [ + 5 => 1, + ]; + + default: + return []; + } } /** From 4a885628cd5c1e1e648028f4dc94119616b08792 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Jul 2025 01:47:13 +0200 Subject: [PATCH 132/165] Performance/CacheValueOverride: extend AbstractFunctionRestrictionsSniff As things were, the determination of whether or not a `T_STRING` is a call to the global WP native `wp_cache_get()` function was severely flawed. By switching the sniff over to be based on the WordPressCS `AbstractFunctionRestrictionsSniff` class, this flaw is mitigated. This flaw did not lead to false positive due to the subsequent token walking being very rigid. Includes adding a slew of additional tests to document the sniff behaviour. Additionally, the tests have been made more comprehensive and varied by: * Testing against false positives for calls to methods or namespaced function calls (= the issue being addressed in this PR). * Testing against false positives for attribute class using the same name as the function. * Ensure function import `use` statements are not flagged. We're not interested in those. * Adding more variations to the pre-existing tests: - Non-lowercase function call(s). --- .../Performance/CacheValueOverrideSniff.php | 66 ++++------------ .../CacheValueOverrideUnitTest.1.inc | 78 +++++++++++++++++-- .../CacheValueOverrideUnitTest.php | 3 +- 3 files changed, 88 insertions(+), 59 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php b/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php index 83bdccc4..30277551 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php @@ -10,46 +10,38 @@ namespace WordPressVIPMinimum\Sniffs\Performance; use PHP_CodeSniffer\Util\Tokens; -use WordPressVIPMinimum\Sniffs\Sniff; +use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; /** * This sniff check whether a cached value is being overridden. */ -class CacheValueOverrideSniff extends Sniff { +class CacheValueOverrideSniff extends AbstractFunctionRestrictionsSniff { /** - * Returns the token types that this sniff is interested in. + * Groups of functions to restrict. * - * @return array + * @return array>> */ - public function register() { - return [ T_STRING ]; + public function getGroups() { + return [ + 'wp_cache_get' => [ + 'functions' => [ 'wp_cache_get' ], + ], + ]; } - /** - * Processes the tokens that this sniff is interested in. + * Process a matched token. * - * @param int $stackPtr The position in the stack where the token was found. + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. * * @return void */ - public function process_token( $stackPtr ) { - - $functionName = $this->tokens[ $stackPtr ]['content']; - - if ( $functionName !== 'wp_cache_get' ) { - // Not a function we are looking for. - return; - } - - if ( $this->isFunctionCall( $stackPtr ) === false ) { - // Not a function call. - return; - } - + public function process_matched_token( $stackPtr, $group_name, $matched_content ) { $variablePos = $this->isVariableAssignment( $stackPtr ); - if ( $variablePos === false ) { // Not a variable assignment. return; @@ -82,32 +74,6 @@ public function process_token( $stackPtr ) { } } - /** - * Check whether the examined code is a function call. - * - * @param int $stackPtr The position of the current token in the stack. - * - * @return bool - */ - private function isFunctionCall( $stackPtr ) { - - // Find the next non-empty token. - $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); - - if ( $this->tokens[ $openBracket ]['code'] !== T_OPEN_PARENTHESIS ) { - // Not a function call. - return false; - } - - // Find the previous non-empty token. - $search = Tokens::$emptyTokens; - $search[] = T_BITWISE_AND; - $previous = $this->phpcsFile->findPrevious( $search, $stackPtr - 1, null, true ); - - // It's a function definition, not a function call, so return false. - return ! ( $this->tokens[ $previous ]['code'] === T_FUNCTION ); - } - /** * Check whether the examined code is a variable assignment. * diff --git a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc index 61bbed03..055aad34 100644 --- a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc @@ -1,17 +1,79 @@ wp_cache_get(); $methodCall = false; +$nullsafeMethodCall = $this?->wp_cache_get(); $nullsafeMethodCall = false; +$staticMethodCall = MyClass::wp_cache_get(); $staticMethodCall = false; +echo WP_CACHE_GET; +$namespacedFunction = namespace\wp_cache_get(); $namespacedFunction = false; + +function &wp_cache_get() {} + +// Looks like a function call, but is a PHP 8.0+ class instantiation via an attribute. +#[WP_Cache_Get('text')] +function foo() {} -if ( empty( $bad_wp_users ) ) { - $bad_wp_users = get_users(); -} -// Good code start. +/* + * These should all be okay. + */ $good_wp_users = wp_cache_get( md5( self::CACHE_KEY . '_wp_users'), self::CACHE_GROUP ); if ( empty( $good_wp_users ) ) { $good_wp_users = false; $good_wp_users = get_users(); -} \ No newline at end of file +} + +function NoAssignment() { + $unrelated_var = false; + \wp_cache_get( $key, $group ) === false && do_something_to_create_new_cache(); + $unrelated_var = false; +} + +function functionCallsAreNotCaseSensitiveGood() { + $retrieved = WP_Cache_Get( ...$params ); + if ( empty( $retrieved ) ) { + $retrieved = false; + } +} + +function fqnFunctionCallGood() { + $no_params_not_our_concern = \wp_cache_get(); + if ( empty( $no_params_not_our_concern ) ) { + $no_params_not_our_concern = false; + } +} + +function cacheOverrideDoesntAssignFalse() { + $not_false = wp_cache_get( ...$params ); + $not_false = true; +} + + +function cacheOverrideAssignsUnknownValue( $unknown ) { + $not_false = wp_cache_get( ...$params ); + $not_false = $unknown; +} + + +/* + * These should all be flagged. + */ +$bad_wp_users = wp_cache_get( md5( self::CACHE_KEY . '_wp_users'), self::CACHE_GROUP ); +$bad_wp_users = false; // Bad. + +if ( empty( $bad_wp_users ) ) { + $bad_wp_users = get_users(); +} + +function functionCallsAreNotCaseSensitiveBad() { + $cache_retrieved = WP_Cache_Get( ...$params ); + // Comment. + do_something_else(); + $cache_retrieved = false; // Bad. +} diff --git a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php index 97ba9397..21773c7e 100644 --- a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php @@ -28,7 +28,8 @@ public function getErrorList( $testFile = '' ) { switch ( $testFile ) { case 'CacheValueOverrideUnitTest.1.inc': return [ - 5 => 1, + 68 => 1, + 78 => 1, ]; default: From 0cb596ede2aeb7eb65a2ac5e77758fe717774a63 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Jul 2025 01:53:41 +0200 Subject: [PATCH 133/165] Performance/CacheValueOverride: bug fix - false negative with fully qualified function call The `isVariableAssignment()` method walks back from the function call to see if the result is being assigned, but did not take fully qualified function calls into account. Fixed now. Includes test. --- .../Sniffs/Performance/CacheValueOverrideSniff.php | 3 ++- .../Tests/Performance/CacheValueOverrideUnitTest.1.inc | 5 +++++ .../Tests/Performance/CacheValueOverrideUnitTest.php | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php b/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php index 30277551..12a4a77c 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php @@ -83,9 +83,10 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content */ private function isVariableAssignment( $stackPtr ) { - // Find the previous non-empty token. + // Find the previous non-empty token, but allow for FQN function calls. $search = Tokens::$emptyTokens; $search[] = T_BITWISE_AND; + $search[] = T_NS_SEPARATOR; $previous = $this->phpcsFile->findPrevious( $search, $stackPtr - 1, null, true ); if ( $this->tokens[ $previous ]['code'] !== T_EQUAL ) { diff --git a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc index 055aad34..ffe54015 100644 --- a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc @@ -77,3 +77,8 @@ function functionCallsAreNotCaseSensitiveBad() { do_something_else(); $cache_retrieved = false; // Bad. } + +function fqnFunctionCallBad() { + $shouldBeCaught = \wp_cache_get(); + $shouldBeCaught = false; // Bad. +} diff --git a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php index 21773c7e..dbdb350b 100644 --- a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php @@ -30,6 +30,7 @@ public function getErrorList( $testFile = '' ) { return [ 68 => 1, 78 => 1, + 83 => 1, ]; default: From c7acb304963d2ad2f7e2fe71fbbd2088347e5126 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Jul 2025 02:38:48 +0200 Subject: [PATCH 134/165] Performance/CacheValueOverride: bug fix - false positive for PHP 8.1+ first class callable assignment If a PHP 8.1+ first class callable is assigned to a variable, the assignment captures the callable, not the result of calling the callable. This commit adds some dedicated code to handle this better and prevent the false positive. Includes tests. --- .../Performance/CacheValueOverrideSniff.php | 27 ++++++++++++++----- .../CacheValueOverrideUnitTest.1.inc | 7 +++++ .../CacheValueOverrideUnitTest.2.inc | 5 ++++ 3 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.2.inc diff --git a/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php b/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php index 12a4a77c..f008ba24 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php @@ -41,6 +41,27 @@ public function getGroups() { * @return void */ public function process_matched_token( $stackPtr, $group_name, $matched_content ) { + $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $stackPtr + 1 ), null, true ); + if ( $openBracket === false || isset( $this->tokens[ $openBracket ]['parenthesis_closer'] ) === false ) { + // Import use statement for function or parse error/live coding. Ignore. + return; + } + + $closeBracket = $this->tokens[ $openBracket ]['parenthesis_closer']; + $firstNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $openBracket + 1 ), null, true ); + $nextNonEmpty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $firstNonEmpty + 1 ), null, true ); + if ( $nextNonEmpty === false ) { + // Parse error/live coding. Ignore. + return; + } + + if ( $this->tokens[ $firstNonEmpty ]['code'] === T_ELLIPSIS + && $nextNonEmpty === $closeBracket + ) { + // First class callable. Ignore. + return; + } + $variablePos = $this->isVariableAssignment( $stackPtr ); if ( $variablePos === false ) { // Not a variable assignment. @@ -50,12 +71,6 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content $variableToken = $this->tokens[ $variablePos ]; $variableName = $variableToken['content']; - // Find the next non-empty token. - $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); - - // Find the closing bracket. - $closeBracket = $this->tokens[ $openBracket ]['parenthesis_closer']; - $nextVariableOccurrence = $this->phpcsFile->findNext( T_VARIABLE, $closeBracket + 1, null, false, $variableName ); $rightAfterNextVariableOccurence = $this->phpcsFile->findNext( Tokens::$emptyTokens, $nextVariableOccurrence + 1, null, true, null, true ); diff --git a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc index ffe54015..664001a8 100644 --- a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc @@ -82,3 +82,10 @@ function fqnFunctionCallBad() { $shouldBeCaught = \wp_cache_get(); $shouldBeCaught = false; // Bad. } + +// Ignore PHP 8.1 first class callable. +// Ignore as the assignment is not for the return value of the function, but for the callable. +function firstClassCallable() { + $callable = wp_cache_get(...); + $callable = false; // OK. +} diff --git a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.2.inc b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.2.inc new file mode 100644 index 00000000..37435fd1 --- /dev/null +++ b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.2.inc @@ -0,0 +1,5 @@ + Date: Wed, 30 Jul 2025 03:55:36 +0200 Subject: [PATCH 135/165] Performance/CacheValueOverride: bug fix - false positives for variables in different scopes As things were, the searching for the "next use of the variable to which the cache was assigned" did not limit itself to the current variable scope. In effect, that meant that "function local" variables could be mistaken for global variables and visa versa, as demonstrated by the new tests. Fixed now. Includes tests. --- .../Performance/CacheValueOverrideSniff.php | 32 +++++++++++++++-- .../CacheValueOverrideUnitTest.1.inc | 36 +++++++++++++++++++ .../CacheValueOverrideUnitTest.php | 1 + 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php b/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php index f008ba24..054a636f 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php @@ -10,6 +10,8 @@ namespace WordPressVIPMinimum\Sniffs\Performance; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Conditions; use WordPressCS\WordPress\AbstractFunctionRestrictionsSniff; /** @@ -71,16 +73,40 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content $variableToken = $this->tokens[ $variablePos ]; $variableName = $variableToken['content']; - $nextVariableOccurrence = $this->phpcsFile->findNext( T_VARIABLE, $closeBracket + 1, null, false, $variableName ); + // Figure out the scope we need to search in. + $searchEnd = $this->phpcsFile->numTokens; + $functionPtr = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, [ T_FUNCTION, T_CLOSURE ] ); + if ( $functionPtr !== false && isset( $this->tokens[ $functionPtr ]['scope_closer'] ) ) { + $searchEnd = $this->tokens[ $functionPtr ]['scope_closer']; + } + + $nextVariableOccurrence = false; + for ( $i = $closeBracket + 1; $i < $searchEnd; $i++ ) { + if ( $this->tokens[ $i ]['code'] === T_VARIABLE && $this->tokens[ $i ]['content'] === $variableName ) { + $nextVariableOccurrence = $i; + break; + } + + // Skip over any and all closed scopes. + if ( isset( Collections::closedScopes()[ $this->tokens[ $i ]['code'] ] ) ) { + if ( isset( $this->tokens[ $i ]['scope_closer'] ) ) { + $i = $this->tokens[ $i ]['scope_closer']; + } + } + } + + if ( $nextVariableOccurrence === false ) { + return; + } - $rightAfterNextVariableOccurence = $this->phpcsFile->findNext( Tokens::$emptyTokens, $nextVariableOccurrence + 1, null, true, null, true ); + $rightAfterNextVariableOccurence = $this->phpcsFile->findNext( Tokens::$emptyTokens, $nextVariableOccurrence + 1, $searchEnd, true, null, true ); if ( $this->tokens[ $rightAfterNextVariableOccurence ]['code'] !== T_EQUAL ) { // Not a value override. return; } - $valueAfterEqualSign = $this->phpcsFile->findNext( Tokens::$emptyTokens, $rightAfterNextVariableOccurence + 1, null, true, null, true ); + $valueAfterEqualSign = $this->phpcsFile->findNext( Tokens::$emptyTokens, $rightAfterNextVariableOccurence + 1, $searchEnd, true, null, true ); if ( $this->tokens[ $valueAfterEqualSign ]['code'] === T_FALSE ) { $message = 'Obtained cached value in `%s` is being overridden. Disabling caching?'; diff --git a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc index 664001a8..75874857 100644 --- a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc +++ b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.1.inc @@ -89,3 +89,39 @@ function firstClassCallable() { $callable = wp_cache_get(...); $callable = false; // OK. } + +// Bug fix - the variable re-assignment must be in the same scope. +function controlStructuresAreNotClosedScopes() { + if (false === ($cache = \wp_cache_get( $key, $group ))) { + // Create new cache. + } + $cache = false; // Bad. +} + + +function closedScopeA() { + $myCache = \wp_cache_get(); +} + +function closedScopeB($myCache = false) {} // OK. + +function closedScopeC() { + $yourCache = wp_cache_get(); + + $closure = function () { + $yourCache = false; // OK. + }; +} + +$globalScope = wp_cache_get(); +function closedScopeD() { + $globalScope = false; // OK. +} + +$globalScope = wp_cache_get(); +function closedScopeE($globalScope = false;) {} // OK. + +$globalScope = wp_cache_get(); +class FooBar { + function method($globalScope = false) {} // OK. +} diff --git a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php index dbdb350b..5f02033d 100644 --- a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.php @@ -31,6 +31,7 @@ public function getErrorList( $testFile = '' ) { 68 => 1, 78 => 1, 83 => 1, + 98 => 1, ]; default: From 3202749f067bccef07d9edacd64fc7d88e029c40 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Jul 2025 03:57:47 +0200 Subject: [PATCH 136/165] Performance/CacheValueOverride: add some more defensive coding ... to guard against the sniff throwing PHP notices/warnings due to parse errors/live coding. Any such PHP notice/warning stops the PHPCS run for a file, so should be avoided. Includes tests. --- .../Sniffs/Performance/CacheValueOverrideSniff.php | 6 ++++-- .../Tests/Performance/CacheValueOverrideUnitTest.3.inc | 6 ++++++ .../Tests/Performance/CacheValueOverrideUnitTest.4.inc | 6 ++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.3.inc create mode 100644 WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.4.inc diff --git a/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php b/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php index 054a636f..4a06a7f1 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/CacheValueOverrideSniff.php @@ -101,14 +101,16 @@ public function process_matched_token( $stackPtr, $group_name, $matched_content $rightAfterNextVariableOccurence = $this->phpcsFile->findNext( Tokens::$emptyTokens, $nextVariableOccurrence + 1, $searchEnd, true, null, true ); - if ( $this->tokens[ $rightAfterNextVariableOccurence ]['code'] !== T_EQUAL ) { + if ( $rightAfterNextVariableOccurence === false + || $this->tokens[ $rightAfterNextVariableOccurence ]['code'] !== T_EQUAL + ) { // Not a value override. return; } $valueAfterEqualSign = $this->phpcsFile->findNext( Tokens::$emptyTokens, $rightAfterNextVariableOccurence + 1, $searchEnd, true, null, true ); - if ( $this->tokens[ $valueAfterEqualSign ]['code'] === T_FALSE ) { + if ( $valueAfterEqualSign !== false && $this->tokens[ $valueAfterEqualSign ]['code'] === T_FALSE ) { $message = 'Obtained cached value in `%s` is being overridden. Disabling caching?'; $data = [ $variableName ]; $this->phpcsFile->addError( $message, $nextVariableOccurrence, 'CacheValueOverride', $data ); diff --git a/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.3.inc b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.3.inc new file mode 100644 index 00000000..e7bf536a --- /dev/null +++ b/WordPressVIPMinimum/Tests/Performance/CacheValueOverrideUnitTest.3.inc @@ -0,0 +1,6 @@ + Date: Fri, 7 Jun 2024 01:54:08 +0200 Subject: [PATCH 137/165] Security/Underscorejs: start using the PHPCSUtils `FilePath` utility The `FilePath::getName()` method will strip quotes from the file name, as well as normalize the slashes to forward (*nix) slashes. This allows for a minor simplication in the code and improves code readability. --- WordPressVIPMinimum/Sniffs/Security/UnderscorejsSniff.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Security/UnderscorejsSniff.php b/WordPressVIPMinimum/Sniffs/Security/UnderscorejsSniff.php index dff1655d..33339362 100644 --- a/WordPressVIPMinimum/Sniffs/Security/UnderscorejsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/UnderscorejsSniff.php @@ -10,6 +10,7 @@ namespace WordPressVIPMinimum\Sniffs\Security; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\FilePath; use PHPCSUtils\Utils\TextStrings; use WordPressVIPMinimum\Sniffs\Sniff; @@ -72,7 +73,7 @@ public function process_token( $stackPtr ) { /* * Ignore Gruntfile.js files as they are configuration, not code. */ - $file_name = TextStrings::stripQuotes( $this->phpcsFile->getFileName() ); + $file_name = FilePath::getName( $this->phpcsFile ); $file_name = strtolower( basename( $file_name ) ); if ( $file_name === 'gruntfile.js' ) { From d5e5c27b2e40e90b208566ab21610d4f85806763 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 23 Jul 2025 23:54:51 +0200 Subject: [PATCH 138/165] Constants/RestrictedConstants: minor efficiency fix This commit introduces `private` property versions of the pre-existing `public` properties, where the array format is different. The `private` properties have the target constant names as the array key, not as the value. This allows for using `isset()` instead of `in_array()`. --- .../Constants/RestrictedConstantsSniff.php | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php b/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php index ac833cce..1b0a7294 100644 --- a/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php @@ -36,12 +36,39 @@ class RestrictedConstantsSniff extends Sniff { 'WP_CRON_CONTROL_SECRET', ]; + /** + * List of (global) constant names, which should not be referenced in userland code, nor (re-)declared. + * + * {@internal The `public` versions of these properties can't be removed until the next major, + * though a decision is still needed whether they should be removed at all. + * Also see: Automattic/VIP-Coding-Standards#234 for more context and discussion about this.} + * + * @var array Key is the constant name, value is irrelevant. + */ + private $restrictedConstants = []; + + /** + * List of (global) constants, which should not be (re-)declared, but may be referenced. + * + * {@internal The `public` versions of these properties can't be removed until the next major, + * though a decision is still needed whether they should be removed at all. + * Also see: Automattic/VIP-Coding-Standards#234 for more context and discussion about this.} + * + * @var array Key is the constant name, value is irrelevant. + */ + private $restrictedRedeclaration = []; + /** * Returns an array of tokens this test wants to listen for. * * @return array */ public function register() { + // For now, set the `private` properties based on the values of the `public` properties. + // This should be revisited when Automattic/VIP-Coding-Standards#234 gets actioned. + $this->restrictedConstants = array_flip( $this->restrictedConstantNames ); + $this->restrictedRedeclaration = array_flip( $this->restrictedConstantDeclaration ); + return [ T_CONSTANT_ENCAPSED_STRING, T_STRING, @@ -63,12 +90,14 @@ public function process_token( $stackPtr ) { $constantName = trim( $this->tokens[ $stackPtr ]['content'], "\"'" ); } - if ( in_array( $constantName, $this->restrictedConstantNames, true ) === false && in_array( $constantName, $this->restrictedConstantDeclaration, true ) === false ) { + if ( isset( $this->restrictedConstants[ $constantName ] ) === false + && isset( $this->restrictedRedeclaration[ $constantName ] ) === false + ) { // Not the constant we are looking for. return; } - if ( $this->tokens[ $stackPtr ]['code'] === T_STRING && in_array( $constantName, $this->restrictedConstantNames, true ) === true ) { + if ( $this->tokens[ $stackPtr ]['code'] === T_STRING && isset( $this->restrictedConstants[ $constantName ] ) === true ) { $message = 'Code is touching the `%s` constant. Make sure it\'s used appropriately.'; $data = [ $constantName ]; $this->phpcsFile->addWarning( $message, $stackPtr, 'UsingRestrictedConstant', $data ); @@ -102,7 +131,7 @@ public function process_token( $stackPtr ) { if ( $this->tokens[ $previous ]['content'] === 'define' ) { $message = 'The definition of `%s` constant is prohibited. Please use a different name.'; $this->phpcsFile->addError( $message, $previous, 'DefiningRestrictedConstant', $data ); - } elseif ( in_array( $constantName, $this->restrictedConstantNames, true ) === true ) { + } elseif ( isset( $this->restrictedConstants[ $constantName ] ) === true ) { $message = 'Code is touching the `%s` constant. Make sure it\'s used appropriately.'; $this->phpcsFile->addWarning( $message, $previous, 'UsingRestrictedConstant', $data ); } From 43e02ffa3fb0071015afcec561394f5068ed3c7e Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 20 Jul 2025 04:53:43 +0200 Subject: [PATCH 139/165] Constants/RestrictedConstants: fix nonsensical comparison The only token this sniff needs to check for (at this time) is the `T_STRING` token, which is used for both constant names as well as function names. Any other token in the `Tokens::$functionNameTokens` token array will never match the subsequent contents comparison anyhow. --- .../Sniffs/Constants/RestrictedConstantsSniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php b/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php index 1b0a7294..77ef9759 100644 --- a/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php @@ -126,7 +126,7 @@ public function process_token( $stackPtr ) { return; } - if ( in_array( $this->tokens[ $previous ]['code'], Tokens::$functionNameTokens, true ) === true ) { + if ( $this->tokens[ $previous ]['code'] === T_STRING ) { $data = [ $constantName ]; if ( $this->tokens[ $previous ]['content'] === 'define' ) { $message = 'The definition of `%s` constant is prohibited. Please use a different name.'; From 169a74e067f9e1dc488050413be257343feca615 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 23 Jul 2025 14:36:57 +0200 Subject: [PATCH 140/165] Constants/RestrictedConstants: use PHPCSUtils `TextStrings::stripQuotes()` ... to only strip the wrapping quotes and not remove any quotes inside the wrapping quotes, which are part of the actual text string. Includes test. --- .../Sniffs/Constants/RestrictedConstantsSniff.php | 3 ++- .../Tests/Constants/RestrictedConstantsUnitTest.inc | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php b/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php index 77ef9759..416c947f 100644 --- a/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Constants/RestrictedConstantsSniff.php @@ -10,6 +10,7 @@ namespace WordPressVIPMinimum\Sniffs\Constants; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\TextStrings; use WordPressVIPMinimum\Sniffs\Sniff; /** @@ -87,7 +88,7 @@ public function process_token( $stackPtr ) { if ( $this->tokens[ $stackPtr ]['code'] === T_STRING ) { $constantName = $this->tokens[ $stackPtr ]['content']; } else { - $constantName = trim( $this->tokens[ $stackPtr ]['content'], "\"'" ); + $constantName = TextStrings::stripQuotes( $this->tokens[ $stackPtr ]['content'] ); } if ( isset( $this->restrictedConstants[ $constantName ] ) === false diff --git a/WordPressVIPMinimum/Tests/Constants/RestrictedConstantsUnitTest.inc b/WordPressVIPMinimum/Tests/Constants/RestrictedConstantsUnitTest.inc index 3fd9cd23..ff0c678f 100644 --- a/WordPressVIPMinimum/Tests/Constants/RestrictedConstantsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Constants/RestrictedConstantsUnitTest.inc @@ -20,4 +20,6 @@ if ( defined( 'JETPACK_DEV_DEBUG' ) ) { // Okay. Can touch. if ( constant( 'WP_CRON_CONTROL_SECRET' ) ) { // Okay. Can touch. -} \ No newline at end of file +} + +define( '"A8C_PROXIED_REQUEST"', false ); // Okay, well not really as the name is not a valid PHP constant name, but that's not our concern. From 57cf78e14a07fca450b4a34c75c6916f26f95056 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Jul 2025 01:58:09 +0200 Subject: [PATCH 141/165] CS: various minor fixes --- WordPressVIPMinimum/Tests/Files/IncludingFileUnitTest.php | 1 + WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php | 1 + 2 files changed, 2 insertions(+) diff --git a/WordPressVIPMinimum/Tests/Files/IncludingFileUnitTest.php b/WordPressVIPMinimum/Tests/Files/IncludingFileUnitTest.php index 4c4043b1..202bd4ae 100644 --- a/WordPressVIPMinimum/Tests/Files/IncludingFileUnitTest.php +++ b/WordPressVIPMinimum/Tests/Files/IncludingFileUnitTest.php @@ -8,6 +8,7 @@ namespace WordPressVIPMinimum\Tests\Files; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; + /** * Unit test class for the IncludingFile sniff. * diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php index 8e1c65dc..ec00a1f9 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php @@ -8,6 +8,7 @@ namespace WordPressVIPMinimum\Tests\Hooks; use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; + /** * Unit test class for the Filters/RestrictedHooks sniff. * From 5fca17d2a0904e9a3016c3c40b98594a28eb324d Mon Sep 17 00:00:00 2001 From: jrfnl Date: Thu, 17 Jul 2025 00:31:27 +0200 Subject: [PATCH 142/165] Docs: various minor fixes ... picked up along the way. --- .github/CONTRIBUTING.md | 14 +++++++++----- .../Sniffs/AbstractVariableRestrictionsSniff.php | 2 -- .../Security/ProperEscapingFunctionSniff.php | 2 +- .../Sniffs/UserExperience/AdminBarRemovalSniff.php | 4 ++-- .../Constants/RestrictedConstantsUnitTest.php | 2 +- .../Tests/Hooks/AlwaysReturnInFilterUnitTest.php | 2 +- .../Tests/Hooks/RestrictedHooksUnitTest.php | 2 +- .../Tests/JS/DangerouslySetInnerHTMLUnitTest.php | 2 +- WordPressVIPMinimum/Tests/JS/InnerHTMLUnitTest.php | 2 +- WordPressVIPMinimum/Tests/JS/WindowUnitTest.php | 2 +- .../Performance/FetchingRemoteDataUnitTest.php | 2 +- .../Tests/Security/PHPFilterFunctionsUnitTest.php | 2 +- .../Variables/RestrictedVariablesUnitTest.php | 2 +- .../Tests/Variables/ServerVariablesUnitTest.php | 2 +- 14 files changed, 22 insertions(+), 20 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5da9ed79..bb5f3fdf 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -91,6 +91,7 @@ When you introduce new `public` sniff properties, or your sniff extends a class ### Pre-requisites * VIP Coding Standards * WordPress-Coding-Standards +* VariableAnalysis * PHPCSUtils 1.x * PHP_CodeSniffer 3.x * PHPUnit 4.x - 9.x @@ -144,15 +145,18 @@ The easiest way to do this is to add a `phpunit.xml` file to the root of your VI Expected output: ``` -PHPUnit 9.6.15 by Sebastian Bergmann and contributors. +Note: Tests are running in "CS" mode -........................................ 40 / 40 (100%) +PHPUnit 9.6.23 by Sebastian Bergmann and contributors. -45 sniff test files generated 175 unique error codes; 0 were fixable (0%) +Runtime: PHP 8.4.10 +Configuration: phpunit.xml.dist -Time: 150 ms, Memory: 20.00 MB +....................................... 39 / 39 (100%) -OK (40 tests, 0 assertions) +52 sniff test files generated 172 unique error codes; 0 were fixable (0%) + +Time: 00:00.862, Memory: 58.00 MB ``` ### Unit Testing conventions diff --git a/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php b/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php index a2b149ab..4619807c 100644 --- a/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/AbstractVariableRestrictionsSniff.php @@ -122,8 +122,6 @@ protected function setup_groups() { * @param int $stackPtr The position of the current token in the stack. * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. - * - * @throws \PHP_CodeSniffer\Exceptions\RuntimeException Exception. */ public function process_token( $stackPtr ) { diff --git a/WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php b/WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php index ffcdce77..d746aa2c 100644 --- a/WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php +++ b/WordPressVIPMinimum/Sniffs/Security/ProperEscapingFunctionSniff.php @@ -187,7 +187,7 @@ public function process_token( $stackPtr ) { $html = $this->phpcsFile->findPrevious( $ignore, $stackPtr - 1, null, true ); - // Use $textStringTokens b/c heredoc and nowdoc tokens will never be encountered in this context anyways.. + // Use $textStringTokens b/c heredoc and nowdoc tokens will never be encountered in this context anyways. if ( $html === false || isset( Tokens::$textStringTokens[ $this->tokens[ $html ]['code'] ] ) === false ) { return; } diff --git a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php index ae98644b..c6fe02ee 100644 --- a/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php +++ b/WordPressVIPMinimum/Sniffs/UserExperience/AdminBarRemovalSniff.php @@ -85,7 +85,7 @@ class AdminBarRemovalSniff extends AbstractFunctionParameterSniff { ]; /** - * String tokens within PHP files we want to deal with. + * Text string tokens within PHP files we want to deal with. * * Set from the register() method. * @@ -120,7 +120,7 @@ class AdminBarRemovalSniff extends AbstractFunctionParameterSniff { * @return array */ public function register() { - // Set up all string targets. + // Set up all text string targets. $this->string_tokens = Tokens::$textStringTokens; $targets = $this->string_tokens; diff --git a/WordPressVIPMinimum/Tests/Constants/RestrictedConstantsUnitTest.php b/WordPressVIPMinimum/Tests/Constants/RestrictedConstantsUnitTest.php index 3c718f6b..4429b324 100644 --- a/WordPressVIPMinimum/Tests/Constants/RestrictedConstantsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Constants/RestrictedConstantsUnitTest.php @@ -10,7 +10,7 @@ use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** - * Unit test class for the ConstantRestrictions sniff. + * Unit test class for the RestrictedConstants sniff. * * @covers \WordPressVIPMinimum\Sniffs\Constants\RestrictedConstantsSniff */ diff --git a/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.php b/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.php index fa04f72e..fe022949 100644 --- a/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.php +++ b/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.php @@ -10,7 +10,7 @@ use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** - * Unit test class for the Hooks/AlwaysReturn sniff. + * Unit test class for the AlwaysReturnInFilter sniff. * * @covers \WordPressVIPMinimum\Sniffs\Hooks\AlwaysReturnInFilterSniff */ diff --git a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php index 8e1c65dc..56da7d62 100644 --- a/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php +++ b/WordPressVIPMinimum/Tests/Hooks/RestrictedHooksUnitTest.php @@ -9,7 +9,7 @@ use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** - * Unit test class for the Filters/RestrictedHooks sniff. + * Unit test class for the RestrictedHooks sniff. * * @since 0.4.0 * diff --git a/WordPressVIPMinimum/Tests/JS/DangerouslySetInnerHTMLUnitTest.php b/WordPressVIPMinimum/Tests/JS/DangerouslySetInnerHTMLUnitTest.php index 8f2ca838..68e85bb1 100644 --- a/WordPressVIPMinimum/Tests/JS/DangerouslySetInnerHTMLUnitTest.php +++ b/WordPressVIPMinimum/Tests/JS/DangerouslySetInnerHTMLUnitTest.php @@ -10,7 +10,7 @@ use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** - * Unit test class for the HTML String concatenation in JS sniff. + * Unit test class for the DangerouslySetInnerHTML sniff. * * @covers \WordPressVIPMinimum\Sniffs\JS\DangerouslySetInnerHTMLSniff */ diff --git a/WordPressVIPMinimum/Tests/JS/InnerHTMLUnitTest.php b/WordPressVIPMinimum/Tests/JS/InnerHTMLUnitTest.php index 20c01177..3c9e5de3 100644 --- a/WordPressVIPMinimum/Tests/JS/InnerHTMLUnitTest.php +++ b/WordPressVIPMinimum/Tests/JS/InnerHTMLUnitTest.php @@ -10,7 +10,7 @@ use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** - * Unit test class for the HTML String concatenation in JS sniff. + * Unit test class for the InnerHTML sniff. * * @covers \WordPressVIPMinimum\Sniffs\JS\InnerHTMLSniff */ diff --git a/WordPressVIPMinimum/Tests/JS/WindowUnitTest.php b/WordPressVIPMinimum/Tests/JS/WindowUnitTest.php index 32ed5cae..0de812d0 100644 --- a/WordPressVIPMinimum/Tests/JS/WindowUnitTest.php +++ b/WordPressVIPMinimum/Tests/JS/WindowUnitTest.php @@ -10,7 +10,7 @@ use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** - * Unit test class for the HTML String concatenation in JS sniff. + * Unit test class for the JS/Window sniff. * * @covers \WordPressVIPMinimum\Sniffs\JS\WindowSniff */ diff --git a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php index 670ef4c5..9f9ede3a 100644 --- a/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/FetchingRemoteDataUnitTest.php @@ -10,7 +10,7 @@ use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** - * Unit test class for the ExitAfterRedirect sniff. + * Unit test class for the FetchingRemoteData sniff. * * @covers \WordPressVIPMinimum\Sniffs\Performance\FetchingRemoteDataSniff */ diff --git a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php index e708a38c..1faa0728 100644 --- a/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Security/PHPFilterFunctionsUnitTest.php @@ -10,7 +10,7 @@ use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** - * Unit test class for the WP_Query params sniff. + * Unit test class for the PHPFilterFunctions sniff. * * @covers \WordPressVIPMinimum\Sniffs\Security\PHPFilterFunctionsSniff */ diff --git a/WordPressVIPMinimum/Tests/Variables/RestrictedVariablesUnitTest.php b/WordPressVIPMinimum/Tests/Variables/RestrictedVariablesUnitTest.php index a5659be0..e5b7ecf2 100644 --- a/WordPressVIPMinimum/Tests/Variables/RestrictedVariablesUnitTest.php +++ b/WordPressVIPMinimum/Tests/Variables/RestrictedVariablesUnitTest.php @@ -10,7 +10,7 @@ use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** - * Unit test class for the VIP_RestrictedVariables sniff. + * Unit test class for the RestrictedVariables sniff. * * @covers \WordPressVIPMinimum\Sniffs\Variables\RestrictedVariablesSniff */ diff --git a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.php b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.php index 610dabd0..c7d89d70 100644 --- a/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.php +++ b/WordPressVIPMinimum/Tests/Variables/ServerVariablesUnitTest.php @@ -10,7 +10,7 @@ use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; /** - * Unit test class for the Variable Analysis sniff. + * Unit test class for the ServerVariables sniff. * * @covers \WordPressVIPMinimum\Sniffs\Variables\ServerVariablesSniff */ From 16c798ec6317448445a1cfa4c79f469e3a77dacc Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Jul 2025 05:49:29 +0200 Subject: [PATCH 143/165] Performance/TaxonomyMetaInOptions: extend AbstractFunctionParameterSniff As things were, the determination of whether or not a `T_STRING` is a call to any of the global WP options functions, was severely flawed. By switching the sniff over to be based on the WordPressCS `AbstractFunctionParameterSniff` class, this flaw is mitigated. Includes adding a slew of additional tests, some of which (line 8 - 13, line 50, 51) are specific to the flaw being addressed in this commit. Additionally, the tests have been made more comprehensive and varied by: * Testing against false positives for calls to methods or namespaced function calls (= the issue being addressed in this PR). * Ensure function import `use` statements are not flagged. We're not interested in those. * Safeguarding that function calls using PHP 5.6+ argument unpacking are not flagged. * Safeguarding handling of PHP 8.0+ attribute class using the same name as a target function. * Safeguarding that the functions are not flagged when used as a PHP 8.1+ first class callable. * Adding tests with more variations: - Non-lowercase function call(s). - Fully qualified function calls. - Use PHP 7.3+ trailing comma's in a few function calls. - Comments in unexpected places. --- .../TaxonomyMetaInOptionsSniff.php | 42 ++++++++----- .../TaxonomyMetaInOptionsUnitTest.inc | 59 +++++++++++++++++-- .../TaxonomyMetaInOptionsUnitTest.php | 12 ++-- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php b/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php index 4541d60a..c2adf1ac 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php @@ -10,16 +10,18 @@ namespace WordPressVIPMinimum\Sniffs\Performance; use PHP_CodeSniffer\Util\Tokens; -use WordPressVIPMinimum\Sniffs\Sniff; +use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** * Restricts the implementation of taxonomy term meta via options. */ -class TaxonomyMetaInOptionsSniff extends Sniff { +class TaxonomyMetaInOptionsSniff extends AbstractFunctionParameterSniff { /** * List of options_ functions * + * @deprecated 3.1.0 This property should never have been public. + * * @var array */ public $option_functions = [ @@ -45,27 +47,37 @@ class TaxonomyMetaInOptionsSniff extends Sniff { ]; /** - * Returns an array of tokens this test wants to listen for. + * The group name for this group of functions. * - * @return array + * @var string */ - public function register() { - return [ T_STRING ]; - } + protected $group_name = 'option_functions'; /** - * Process this test when one of its tokens is encountered - * - * @param int $stackPtr The position of the current token in the stack passed in $tokens. + * Functions this sniff is looking for. * - * @return void + * @var array Keys are the target functions, value irrelevant. */ - public function process_token( $stackPtr ) { + protected $target_functions = [ + 'get_option' => true, + 'add_option' => true, + 'update_option' => true, + 'delete_option' => true, + ]; - if ( in_array( $this->tokens[ $stackPtr ]['content'], $this->option_functions, true ) === false ) { - return; - } + /** + * Process the parameters of a matched function. + * + * @param int $stackPtr The position of the current token in the stack. + * @param string $group_name The name of the group which was matched. + * @param string $matched_content The token content (function name) which was matched + * in lowercase. + * @param array $parameters Array with information about the parameters. + * + * @return void + */ + public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); if ( $this->tokens[ $openBracket ]['code'] !== T_OPEN_PARENTHESIS ) { diff --git a/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc b/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc index 6e67c82b..a3beb493 100644 --- a/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc @@ -1,8 +1,55 @@ term_id" ); // NOK. -delete_option( "taxonomy_rating_{$obj->term_id}" ); // NOK. -get_option( "taxonomy_rating_$tag_id" ); // NOK. -update_option( 'taxonomy_rating_' . $category_id ); // NOK. -get_option( "taxonomy_rating_{$cat_id}" ); // NOK. -add_option( 'taxonomy_rating_' . $obj->term_id ); // NOK. \ No newline at end of file +/* + * Not the sniff target. + */ +use function add_option; + +my\ns\get_option( "taxonomy_rating_$tag_id" ); +$this->delete_option( "taxonomy_rating_$tag_id" ); +$this?->update_option( "taxonomy_rating_$tag_id" ); +MyClass::get_option( "taxonomy_rating_$tag_id", ); +echo DELETE_OPTION; +namespace\add_option( "taxonomy_rating_$tag_id" ); + +function &update_option() {} + +/* + * These should all be okay. + */ +// Incomplete function call, should be ignored by the sniff. +$incorrect_but_ok = get_option(); +$incorrect_but_ok = update_option(); + +// Ignore as undetermined. +Get_Option( $option ); +\add_option( $obj->get_optionname() ); +update_option( OPTION_NAME, ); +\DELETE_OPTION( MyClass::$option_name ); + +get_option(...$params); // PHP 5.6 argument unpacking, parameter value unknown. + +// Looks like a function call, but is a PHP 8.0+ class instantiation via an attribute. +#[Get_Option('taxonomy_rating_')] +function foo() {} + +array_walk($options, \update_option(...),); // PHP 8.1 first class callable. + +// Ignore as not touching taxonomy term patterns. +\get_option( 'option_name' ); +add_option( 'option_' . $post_id, ); +update_Option( "prefix_{$setting}" ); +add_option( 'option_' . MY_CONSTANT, ); +add_option( 'foo_' . $obj->$propname ); +update_option( 'foo_' . $obj->page_id ); // NOK. + + +/* + * These should all be flagged. + */ +get_option( "taxonomy_rating_$obj->term_id", ); // NOK. +\delete_option( "taxonomy_rating_{$obj->term_id}" ); // NOK. +get_option( /*comment*/ "taxonomy_rating_$tag_id" ); // NOK. +UpDate_OPTION( 'taxonomy_rating_' . $category_id /*comment*/ , ); // NOK. +\Get_Option( "taxonomy_rating_{$cat_id}" ); // NOK. +add_option( 'taxonomy_rating_' . $obj->term_id ); // NOK. diff --git a/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.php b/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.php index dff27d68..1fa15375 100644 --- a/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.php @@ -32,12 +32,12 @@ public function getErrorList() { */ public function getWarningList() { return [ - 3 => 1, - 4 => 1, - 5 => 1, - 6 => 1, - 7 => 1, - 8 => 1, + 50 => 1, + 51 => 1, + 52 => 1, + 53 => 1, + 54 => 1, + 55 => 1, ]; } } From 1f24aadb0c68fa0c59cd9a2240d9a7e8adb186dd Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Jul 2025 05:57:19 +0200 Subject: [PATCH 144/165] Performance/TaxonomyMetaInOptions: add support for handling PHP 8.0+ function calls using named parameters Includes tests. --- .../Performance/TaxonomyMetaInOptionsSniff.php | 12 ++++++++---- .../Performance/TaxonomyMetaInOptionsUnitTest.inc | 8 ++++++++ .../Performance/TaxonomyMetaInOptionsUnitTest.php | 1 + 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php b/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php index c2adf1ac..8c451dd0 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php @@ -10,6 +10,7 @@ namespace WordPressVIPMinimum\Sniffs\Performance; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Utils\PassedParameters; use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** @@ -78,13 +79,16 @@ class TaxonomyMetaInOptionsSniff extends AbstractFunctionParameterSniff { * @return void */ public function process_parameters( $stackPtr, $group_name, $matched_content, $parameters ) { - $openBracket = $this->phpcsFile->findNext( Tokens::$emptyTokens, $stackPtr + 1, null, true ); - - if ( $this->tokens[ $openBracket ]['code'] !== T_OPEN_PARENTHESIS ) { + $target_param = PassedParameters::getParameterFromStack( $parameters, 1, 'option' ); + if ( $target_param === false ) { + // Missing (required) target parameter. Probably live coding, nothing to examine (yet). Bow out. return; } - $param_ptr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $openBracket + 1, null, true ); + $param_start = $target_param['start']; + $param_end = ( $target_param['end'] + 1 ); // Add one to include the last token in the parameter in findNext searches. + + $param_ptr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $param_start, $param_end, true ); if ( $this->tokens[ $param_ptr ]['code'] === T_DOUBLE_QUOTED_STRING ) { foreach ( $this->taxonomy_term_patterns as $taxonomy_term_pattern ) { diff --git a/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc b/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc index a3beb493..a6868479 100644 --- a/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc @@ -53,3 +53,11 @@ get_option( /*comment*/ "taxonomy_rating_$tag_id" ); // NOK. UpDate_OPTION( 'taxonomy_rating_' . $category_id /*comment*/ , ); // NOK. \Get_Option( "taxonomy_rating_{$cat_id}" ); // NOK. add_option( 'taxonomy_rating_' . $obj->term_id ); // NOK. + + +// Safeguard handling of function calls using PHP 8.0+ named parameters. +get_option(default_value: false); // OK, well, not really, missing required $option param, but that's not the concern of this sniff. +add_option(value: "$tag_id", option: 'option_name'); // OK. +update_option(autoload: true, optin: "$tag_id", value: 10,); // OK, well, not really, typo in param name, but that's not the concern of the sniff. + +add_option(value: $value, option: "$tag_id" ); // Bad. diff --git a/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.php b/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.php index 1fa15375..ed4fc304 100644 --- a/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.php @@ -38,6 +38,7 @@ public function getWarningList() { 53 => 1, 54 => 1, 55 => 1, + 63 => 1, ]; } } From ae8557a81e828ef140d486ff6281c7c83611186c Mon Sep 17 00:00:00 2001 From: jrfnl Date: Wed, 30 Jul 2025 06:15:26 +0200 Subject: [PATCH 145/165] Performance/TaxonomyMetaInOptions: add support for handling PHP 8.0+ nullsafe object operator Includes test. --- .../Sniffs/Performance/TaxonomyMetaInOptionsSniff.php | 4 +++- .../Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc | 3 +++ .../Tests/Performance/TaxonomyMetaInOptionsUnitTest.php | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php b/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php index 8c451dd0..b4a10f33 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/TaxonomyMetaInOptionsSniff.php @@ -117,7 +117,9 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p } $object_operator = $this->phpcsFile->findNext( Tokens::$emptyTokens, $variable_name + 1, null, true ); - if ( $this->tokens[ $object_operator ]['code'] !== T_OBJECT_OPERATOR ) { + if ( $this->tokens[ $object_operator ]['code'] !== T_OBJECT_OPERATOR + && $this->tokens[ $object_operator ]['code'] !== T_NULLSAFE_OBJECT_OPERATOR + ) { return; } diff --git a/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc b/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc index a6868479..a89e6ec0 100644 --- a/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.inc @@ -61,3 +61,6 @@ add_option(value: "$tag_id", option: 'option_name'); // OK. update_option(autoload: true, optin: "$tag_id", value: 10,); // OK, well, not really, typo in param name, but that's not the concern of the sniff. add_option(value: $value, option: "$tag_id" ); // Bad. + +// Safeguard handling of PHP 8.0+ nullsafe object operator. +get_option( 'taxonomy_rating_' . $obj?->term_id ); // NOK. diff --git a/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.php b/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.php index ed4fc304..cc57df21 100644 --- a/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/TaxonomyMetaInOptionsUnitTest.php @@ -39,6 +39,7 @@ public function getWarningList() { 54 => 1, 55 => 1, 63 => 1, + 66 => 1, ]; } } From 9f97da21f358b4d424a50010e1be38ff00a03167 Mon Sep 17 00:00:00 2001 From: mujuonly Date: Mon, 6 Oct 2025 14:41:43 +0530 Subject: [PATCH 146/165] Update WPQueryParamsSniff.php Doc link update --- WordPressVIPMinimum/Sniffs/Performance/WPQueryParamsSniff.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressVIPMinimum/Sniffs/Performance/WPQueryParamsSniff.php b/WordPressVIPMinimum/Sniffs/Performance/WPQueryParamsSniff.php index 57c7ffd1..10f09ba1 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/WPQueryParamsSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/WPQueryParamsSniff.php @@ -46,7 +46,7 @@ public function getGroups() { 'PostNotIn' => [ 'name' => 'PostNotIn', 'type' => 'warning', - 'message' => 'Using exclusionary parameters, like %s, in calls to get_posts() should be done with caution, see https://wpvip.com/documentation/performance-improvements-by-removing-usage-of-post__not_in/ for more information.', + 'message' => 'Using exclusionary parameters, like %s, in calls to get_posts() should be done with caution, see https://docs.wpvip.com/databases/optimize-queries/using-post__not_in/ for more information.', 'keys' => [ 'post__not_in', 'exclude', From f7fdd19f330be1b1c764a38e79f6de23d644f9cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 09:06:25 +0000 Subject: [PATCH 147/165] GH Actions: Bump actions/checkout from 4 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/basics.yml | 4 ++-- .github/workflows/quicktest.yml | 2 +- .github/workflows/test.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index b3143c46..8ddeefbc 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -113,7 +113,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index 5aeae65b..a020910d 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -40,7 +40,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ffbbb3d2..30d84a10 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -93,7 +93,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 # With stable PHPCS dependencies, allow for PHP deprecation notices. # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore. From 853dd1ac467e5be05205487ba6ce61108df31475 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 22 Mar 2026 12:02:57 +0000 Subject: [PATCH 148/165] ci: update PHPCS dev branch from master to 3.x The PHPCS repository moved from squizlabs to phpcsstandards and no longer has a master branch. The dev CI jobs need to track the 3.x branch until WPCS (and by extension VIPCS) is compatible with PHPCS 4.x. --- .github/workflows/basics.yml | 4 ++-- .github/workflows/test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index 8ddeefbc..9029858f 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -42,8 +42,8 @@ jobs: run: composer validate --no-check-all --strict - name: 'Composer: adjust dependencies' - # Using PHPCS `master` as an early detection system for bugs upstream. - run: composer require --no-update --no-scripts squizlabs/php_codesniffer:"dev-master" --no-interaction + # Using PHPCS `3.x` as an early detection system for bugs upstream. + run: composer require --no-update --no-scripts squizlabs/php_codesniffer:"3.x-dev" --no-interaction # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30d84a10..4d2ced0a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -117,7 +117,7 @@ jobs: if: ${{ matrix.dependencies == 'dev' }} run: > composer require --no-update --no-scripts --no-interaction - squizlabs/php_codesniffer:"dev-master" + squizlabs/php_codesniffer:"3.x-dev" phpcsstandards/phpcsutils:"dev-develop" phpcsstandards/phpcsextra:"dev-develop" sirbrillig/phpcs-variable-analysis:"2.x" From dbe0fc0852b3be2b01dccbd34fcf269de1544341 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 22 Mar 2026 12:58:20 +0000 Subject: [PATCH 149/165] ci: bump minimum PHP version from 5.4 to 7.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WPCS 3.3.0 and its develop branch now require PHP 7.2+, making the existing PHP 5.4–7.1 CI matrix entries unable to install dev dependencies. Since the VIP platform requires PHP 8.3 and consuming plugins typically require 7.4+, raising the minimum to 7.4 aligns VIPCS with its actual usage whilst keeping the dev dependency early warning system functional. --- .github/workflows/quicktest.yml | 4 ++-- .github/workflows/test.yml | 11 ++--------- composer.json | 4 ++-- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index a020910d..1278e439 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -26,9 +26,9 @@ jobs: strategy: matrix: include: - - php: '5.4' + - php: '7.4' dependencies: 'stable' - - php: '5.4' + - php: '7.4' dependencies: 'lowest' - php: 'latest' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4d2ced0a..7bfcf14e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: strategy: matrix: - php: ['5.4', 'latest', '8.5'] + php: ['7.4', 'latest', '8.5'] name: "Lint: PHP ${{ matrix.php }}" continue-on-error: ${{ matrix.php == '8.5' }} @@ -64,20 +64,13 @@ jobs: # - php: The PHP versions to test against. # - dependencies: The PHPCS dependencies versions to test against. # IMPORTANT: test runs shouldn't fail because of PHPCS being incompatible with a PHP version. - # - PHPCS will run without errors on PHP 5.4 - 7.4 on any supported version. - # - PHP 8.0 needs PHPCS 3.5.7+ to run without errors, and we require a higher minimum version. - # - PHP 8.1 needs PHPCS 3.6.1+ to run without errors, but works best with 3.7.1+, and we require at least this minimum version. # - PHP 8.2, 8.3 and 8.4 need PHPCS 3.8.0+ to run without errors (though the errors don't affect the tests). matrix: - php: ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] + php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] dependencies: ['lowest', 'stable'] include: # Test against dev versions of all dependencies with select PHP versions for early detection of issues. - - php: '5.4' - dependencies: 'dev' - - php: '7.0' - dependencies: 'dev' - php: '7.4' dependencies: 'dev' - php: '8.4' diff --git a/composer.json b/composer.json index 4579a657..347825d7 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": ">=5.4", + "php": ">=7.4", "phpcsstandards/phpcsextra": "^1.4.0", "phpcsstandards/phpcsutils": "^1.1.0", "sirbrillig/phpcs-variable-analysis": "^2.12.0", @@ -28,7 +28,7 @@ "php-parallel-lint/php-console-highlighter": "^1.0.0", "phpcompatibility/php-compatibility": "^9", "phpcsstandards/phpcsdevtools": "^1.2.3", - "phpunit/phpunit": "^4 || ^5 || ^6 || ^7 || ^8 || ^9" + "phpunit/phpunit": "^8 || ^9" }, "config": { "allow-plugins": { From fdd5a4c56cbbfb8d47199cef49ce8eefcf0efbe9 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 22 Mar 2026 13:00:08 +0000 Subject: [PATCH 150/165] ci: drop PHPUnit 8 support With PHP 7.4 as the minimum, PHPUnit 9 (which requires PHP 7.3+) covers the entire supported range. This removes the PHPUnit 8 schema validation step and tightens the Composer constraint accordingly. --- .github/workflows/basics.yml | 13 ++----------- composer.json | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index 9029858f..ccb9f0af 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -74,17 +74,8 @@ jobs: pattern: ".phpcs.xml.dist" xsd-file: "vendor/squizlabs/php_codesniffer/phpcs.xsd" - # Notes: - # - PHPUnit 9.5 (which will be installed in this job) doesn't ship XSD files further back than 8.5. - # - PHPUnit 9.3 introduced some new configuration options and deprecated the old versions of those. - # For cross-version compatibility with older PHPUnit versions, those new config options cannot be used yet, - # which is why the PHPUnit 9 validation is done against the PHPUnit 9.2 schema. - - name: "Validate PHPUnit config for use with PHPUnit 8" - uses: phpcsstandards/xmllint-validate@v1 - with: - pattern: "phpunit.xml.dist" - xsd-file: "vendor/phpunit/phpunit/schema/8.5.xsd" - + # Note: PHPUnit 9.3 introduced some new configuration options and deprecated the old versions of those. + # For cross-version compatibility, the validation is done against the PHPUnit 9.2 schema. - name: "Validate PHPUnit config for use with PHPUnit 9" uses: phpcsstandards/xmllint-validate@v1 with: diff --git a/composer.json b/composer.json index 347825d7..bfe45898 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "php-parallel-lint/php-console-highlighter": "^1.0.0", "phpcompatibility/php-compatibility": "^9", "phpcsstandards/phpcsdevtools": "^1.2.3", - "phpunit/phpunit": "^8 || ^9" + "phpunit/phpunit": "^9" }, "config": { "allow-plugins": { From 840d1a8393125bdd10c43c23a8bd773970c7af3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:10:56 +0000 Subject: [PATCH 151/165] GH Actions: Bump ramsey/composer-install from 3 to 4 Bumps [ramsey/composer-install](https://github.com/ramsey/composer-install) from 3 to 4. - [Release notes](https://github.com/ramsey/composer-install/releases) - [Commits](https://github.com/ramsey/composer-install/compare/v3...v4) --- updated-dependencies: - dependency-name: ramsey/composer-install dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/basics.yml | 4 ++-- .github/workflows/quicktest.yml | 2 +- .github/workflows/test.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index ccb9f0af..5c29d4fe 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -48,7 +48,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@v4" with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") @@ -117,7 +117,7 @@ jobs: # Dependencies need to be installed to make sure the PHPCS and PHPUnit classes are recognized. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@v4" with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index 1278e439..9dcddd62 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -58,7 +58,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@v4" with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7bfcf14e..197db4f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: tools: cs2pr - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@v4" with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") @@ -123,7 +123,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@v4" with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") From 74ba5b62e381a81cdc6d83f8ec7af45ab3b1af03 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 22 Mar 2026 13:51:30 +0000 Subject: [PATCH 152/165] fix: resolve bugs and adopt PHPCSUtils in AlwaysReturnInFilterSniff The `isReturningVoid()` method wrapped `Tokens::$emptyTokens` in an extra array, preventing whitespace tokens from being skipped. This meant `return ;` (with a space) was not detected as a void return. Additional improvements: - Eliminate duplicate `findNext()` call in `processFunction()` - Use `FunctionDeclarations::getName()` for reliable name matching - Use `TextStrings::stripQuotes()` instead of `substr()` for stripping quotes from callback function names - Handle arrow function (`T_FN`) callbacks, which always return a value implicitly Ref #520 --- .../Sniffs/Hooks/AlwaysReturnInFilterSniff.php | 16 ++++++++++------ .../Tests/Hooks/AlwaysReturnInFilterUnitTest.inc | 9 +++++++++ .../Tests/Hooks/AlwaysReturnInFilterUnitTest.php | 1 + 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php index 2cf1060c..4d45c9d3 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Util\Tokens; use PHPCSUtils\Utils\Arrays; use PHPCSUtils\Utils\FunctionDeclarations; +use PHPCSUtils\Utils\TextStrings; use WordPressVIPMinimum\Sniffs\Sniff; /** @@ -80,6 +81,9 @@ public function process_token( $stackPtr ) { if ( $this->tokens[ $callbackPtr ]['code'] === T_CLOSURE ) { $this->processFunctionBody( $callbackPtr ); + } elseif ( $this->tokens[ $callbackPtr ]['code'] === T_FN ) { + // Arrow functions always return a value implicitly. No check needed. + return; } elseif ( $this->tokens[ $callbackPtr ]['code'] === T_ARRAY || $this->tokens[ $callbackPtr ]['code'] === T_OPEN_SHORT_ARRAY ) { @@ -133,7 +137,7 @@ private function processArray( $stackPtr ) { */ private function processString( $stackPtr, $start = 0, $end = null ) { - $callbackFunctionName = substr( $this->tokens[ $stackPtr ]['content'], 1, -1 ); + $callbackFunctionName = TextStrings::stripQuotes( $this->tokens[ $stackPtr ]['content'] ); $callbackFunctionPtr = $this->phpcsFile->findNext( T_STRING, @@ -165,10 +169,10 @@ private function processFunction( $stackPtr, $start = 0, $end = null ) { $functionName = $this->tokens[ $stackPtr ]['content']; $offset = $start; - while ( $this->phpcsFile->findNext( [ T_FUNCTION ], $offset, $end ) !== false ) { - $functionStackPtr = $this->phpcsFile->findNext( [ T_FUNCTION ], $offset, $end ); - $functionNamePtr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $functionStackPtr + 1, null, true, null, true ); - if ( $this->tokens[ $functionNamePtr ]['code'] === T_STRING && $this->tokens[ $functionNamePtr ]['content'] === $functionName ) { + // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition -- Valid usage. + while ( ( $functionStackPtr = $this->phpcsFile->findNext( T_FUNCTION, $offset, $end ) ) !== false ) { + $declaredName = FunctionDeclarations::getName( $this->phpcsFile, $functionStackPtr ); + if ( $declaredName === $functionName ) { $this->processFunctionBody( $functionStackPtr ); return; } @@ -294,7 +298,7 @@ private function isInsideIfConditonal( $stackPtr ) { private function isReturningVoid( $stackPtr ) { $nextToReturnTokenPtr = $this->phpcsFile->findNext( - [ Tokens::$emptyTokens ], + Tokens::$emptyTokens, $stackPtr + 1, null, true diff --git a/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.inc b/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.inc index 2748e074..0e9cc609 100644 --- a/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.inc @@ -188,6 +188,15 @@ class tokenizer_bug_test { public function tokenizer_bug( $param ): namespace\Foo {} // Error. } +// Arrow function callbacks always return implicitly. Ok. +add_filter( 'good_arrow_function', fn( $x ) => $x . ' suffix' ); + +add_filter( 'good_arrow_function_null', fn( $x ) => null ); // Ok, still returns a value (null). + +add_filter( 'bad_void_return_with_space', function() { // Error. + return ; +} ); + // Intentional parse error. This has to be the last test in the file! class parse_error_test { public function __construct() { diff --git a/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.php b/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.php index fe022949..0c913191 100644 --- a/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.php +++ b/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.php @@ -31,6 +31,7 @@ public function getErrorList() { 129 => 1, 163 => 1, 188 => 1, + 196 => 1, ]; } From 67c8068e7f5917a0af3f0393ff6ed198daa79e87 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 22 Mar 2026 13:57:22 +0000 Subject: [PATCH 153/165] feat: flag posts_per_page/numberposts set to -1 WPCS's PostsPerPageSniff uses a `> 100` comparison to flag high values, which means `-1` (WordPress's "no limit" sentinel) slips through uncaught. The NoPaging sniff is the natural home for this check since setting posts_per_page to -1 is functionally equivalent to nopaging. Adds a `posts_per_page` group to NoPagingSniff covering both `posts_per_page` and `numberposts` keys, with a callback that specifically targets the `-1` value via TextStrings::stripQuotes(). Also expands NoPaging test coverage substantially (#533): nopaging with integer 1, false, 0, variable assignments, and short array syntax are now all exercised. Both ruleset test files updated to account for the new test cases. Refs #364, #533. --- WordPress-VIP-Go/ruleset-test.inc | 3 + WordPress-VIP-Go/ruleset-test.php | 51 ++++++------ .../Sniffs/Performance/NoPagingSniff.php | 21 ++++- .../Tests/Performance/NoPagingUnitTest.inc | 72 ++++++++++++++++- .../Tests/Performance/NoPagingUnitTest.php | 15 +++- WordPressVIPMinimum/ruleset-test.inc | 3 + WordPressVIPMinimum/ruleset-test.php | 77 ++++++++++--------- 7 files changed, 172 insertions(+), 70 deletions(-) diff --git a/WordPress-VIP-Go/ruleset-test.inc b/WordPress-VIP-Go/ruleset-test.inc index c4ec4eeb..5a17d238 100644 --- a/WordPress-VIP-Go/ruleset-test.inc +++ b/WordPress-VIP-Go/ruleset-test.inc @@ -466,6 +466,9 @@ $args = array( 'nopaging' => true, // Error. ); _query_posts( 'nopaging=true' ); // Error. +$args = array( + 'posts_per_page' => -1, // Error. +); // WordPressVIPMinimum.Performance.OrderByRand $args = array( diff --git a/WordPress-VIP-Go/ruleset-test.php b/WordPress-VIP-Go/ruleset-test.php index 83cb5514..f7af1404 100644 --- a/WordPress-VIP-Go/ruleset-test.php +++ b/WordPress-VIP-Go/ruleset-test.php @@ -87,15 +87,13 @@ 462 => 1, 466 => 1, 468 => 1, - 472 => 1, - 474 => 1, - 480 => 1, - 486 => 1, - 494 => 1, - 507 => 1, - 511 => 1, - 512 => 1, - 513 => 1, + 470 => 1, + 475 => 1, + 477 => 1, + 483 => 1, + 489 => 1, + 497 => 1, + 510 => 1, 514 => 1, 515 => 1, 516 => 1, @@ -104,16 +102,19 @@ 519 => 1, 520 => 1, 521 => 1, - 525 => 1, - 527 => 1, - 545 => 1, - 560 => 1, - 564 => 1, - 565 => 1, - 566 => 1, + 522 => 1, + 523 => 1, + 524 => 1, + 528 => 1, + 530 => 1, + 548 => 1, + 563 => 1, 567 => 1, - 572 => 1, - 574 => 1, + 568 => 1, + 569 => 1, + 570 => 1, + 575 => 1, + 577 => 1, ], 'warnings' => [ 7 => 1, @@ -223,14 +224,14 @@ 454 => 1, 455 => 1, 456 => 1, - 502 => 1, - 503 => 1, - 530 => 1, + 505 => 1, + 506 => 1, 533 => 1, - 540 => 1, - 550 => 1, - 556 => 1, - 579 => 1, + 536 => 1, + 543 => 1, + 553 => 1, + 559 => 1, + 582 => 1, ], 'messages' => [ 7 => [ diff --git a/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php b/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php index 311951d4..d3814514 100644 --- a/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php +++ b/WordPressVIPMinimum/Sniffs/Performance/NoPagingSniff.php @@ -10,10 +10,11 @@ namespace WordPressVIPMinimum\Sniffs\Performance; +use PHPCSUtils\Utils\TextStrings; use WordPressCS\WordPress\AbstractArrayAssignmentRestrictionsSniff; /** - * Flag returning high or infinite posts_per_page. + * Flag disabling pagination via `nopaging` or `posts_per_page`/`numberposts` set to `-1`. * * @link https://docs.wpvip.com/technical-references/code-review/#no-limit-queries */ @@ -28,11 +29,19 @@ public function getGroups() { return [ 'nopaging' => [ 'type' => 'error', - 'message' => 'Disabling pagination is prohibited in VIP context, do not set `%s` to `%s` ever.', + 'message' => 'Disabling pagination is prohibited in VIP context, do not set `%s` to `%s`.', 'keys' => [ 'nopaging', ], ], + 'posts_per_page' => [ + 'type' => 'error', + 'message' => 'Setting `%s` to `%s` disables pagination and is prohibited in VIP context.', + 'keys' => [ + 'posts_per_page', + 'numberposts', + ], + ], ]; } @@ -49,6 +58,12 @@ public function getGroups() { public function callback( $key, $val, $line, $group ) { $key = strtolower( $key ); - return ( $key === 'nopaging' && ( $val === 'true' || $val === '1' ) ); + if ( $key === 'nopaging' ) { + return ( $val === 'true' || $val === '1' ); + } + + // posts_per_page / numberposts: flag -1 (no limit). + $val = TextStrings::stripQuotes( $val ); + return ( $val === '-1' ); } } diff --git a/WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.inc b/WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.inc index 9af35243..8272e8e2 100644 --- a/WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.inc @@ -1,15 +1,85 @@ true, // Bad. ); _query_posts( 'nopaging=true' ); // Bad. -$query_args['my_posts_per_page'] = -1; // OK. +$query_args['my_posts_per_page'] = -1; // OK - not a recognized key. // Verify handling with no trailing comma at end of array. $args = array( 'nopaging' => true // Bad. ); $args = [ 'nopaging' => true ]; // Bad. + +// nopaging with integer 1. +$args = [ 'nopaging' => 1 ]; // Bad. + +// nopaging with false - should not trigger. +$args = [ 'nopaging' => false ]; // OK. + +// nopaging with integer 0 - should not trigger. +$args = [ 'nopaging' => 0 ]; // OK. + +// nopaging via variable assignment. +$args['nopaging'] = true; // Bad. + +// nopaging via variable assignment with false. +$args['nopaging'] = false; // OK. + +// === posts_per_page tests === + +// posts_per_page set to -1 in long array syntax. +$args = array( + 'posts_per_page' => -1, // Bad. +); + +// posts_per_page set to -1 in short array syntax. +$args = [ 'posts_per_page' => -1 ]; // Bad. + +// posts_per_page set to -1 as string. +$args = [ 'posts_per_page' => '-1' ]; // Bad. + +// posts_per_page set to a normal value - should not trigger. +$args = [ 'posts_per_page' => 50 ]; // OK. + +// posts_per_page set to 1 - should not trigger. +$args = [ 'posts_per_page' => 1 ]; // OK. + +// numberposts set to -1. +$args = [ 'numberposts' => -1 ]; // Bad. + +// numberposts set to a normal value - should not trigger. +$args = [ 'numberposts' => 10 ]; // OK. + +// posts_per_page via variable assignment. +$args['posts_per_page'] = -1; // Bad. + +// posts_per_page via variable assignment with normal value. +$args['posts_per_page'] = 10; // OK. + +// Query string with posts_per_page=-1. +_query_posts( 'posts_per_page=-1&orderby=date' ); // Bad. + +// Query string with posts_per_page set to a normal value. +_query_posts( 'posts_per_page=50' ); // OK. + +// Query string with numberposts=-1. +_query_posts( 'numberposts=-1' ); // Bad. + +// === Ignoring with phpcs:ignore comments === + +// phpcs:ignore WordPressVIPMinimum.Performance.NoPaging.posts_per_page_posts_per_page -- Intentional: fetching all items for export. +$args = [ 'posts_per_page' => -1 ]; // OK - ignored. + +$args = [ + // phpcs:ignore WordPressVIPMinimum.Performance.NoPaging.posts_per_page_numberposts -- Intentional: legacy API requires all results. + 'numberposts' => -1, // OK - ignored. +]; + +// phpcs:ignore WordPressVIPMinimum.Performance.NoPaging.nopaging_nopaging -- Intentional: sitemap generation. +$args = [ 'nopaging' => true ]; // OK - ignored. diff --git a/WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.php b/WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.php index 514df012..89e9e267 100644 --- a/WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.php +++ b/WordPressVIPMinimum/Tests/Performance/NoPagingUnitTest.php @@ -23,10 +23,19 @@ class NoPagingUnitTest extends AbstractSniffUnitTest { */ public function getErrorList() { return [ - 4 => 1, - 7 => 1, - 13 => 1, + 6 => 1, + 9 => 1, 15 => 1, + 17 => 1, + 20 => 1, + 29 => 1, + 38 => 1, + 42 => 1, + 45 => 1, + 54 => 1, + 60 => 1, + 66 => 1, + 72 => 1, ]; } diff --git a/WordPressVIPMinimum/ruleset-test.inc b/WordPressVIPMinimum/ruleset-test.inc index 3a104038..b47ae03c 100644 --- a/WordPressVIPMinimum/ruleset-test.inc +++ b/WordPressVIPMinimum/ruleset-test.inc @@ -463,6 +463,9 @@ $args = array( // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.Un 'nopaging' => true, // Error. ); _query_posts( 'nopaging=true' ); // Error. +$args = array( // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + 'posts_per_page' => -1, // Error. +); // WordPressVIPMinimum.Performance.OrderByRand $args = array( // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable diff --git a/WordPressVIPMinimum/ruleset-test.php b/WordPressVIPMinimum/ruleset-test.php index 4fc80840..4d31b841 100644 --- a/WordPressVIPMinimum/ruleset-test.php +++ b/WordPressVIPMinimum/ruleset-test.php @@ -155,15 +155,13 @@ 451 => 1, 463 => 1, 465 => 1, - 469 => 1, - 471 => 1, - 477 => 1, - 483 => 1, - 491 => 1, - 505 => 1, - 509 => 1, - 510 => 1, - 511 => 1, + 467 => 1, + 472 => 1, + 474 => 1, + 480 => 1, + 486 => 1, + 494 => 1, + 508 => 1, 512 => 1, 513 => 1, 514 => 1, @@ -172,29 +170,32 @@ 517 => 1, 518 => 1, 519 => 1, - 523 => 1, - 525 => 1, - 550 => 1, - 551 => 1, + 520 => 1, + 521 => 1, + 522 => 1, + 526 => 1, + 528 => 1, + 553 => 1, 554 => 1, - 569 => 1, - 570 => 1, + 557 => 1, + 572 => 1, 573 => 1, - 574 => 1, - 575 => 1, + 576 => 1, + 577 => 1, 578 => 1, 581 => 1, - 582 => 1, - 583 => 1, - 588 => 1, - 590 => 1, - 594 => 1, - 595 => 1, - 596 => 1, + 584 => 1, + 585 => 1, + 586 => 1, + 591 => 1, + 593 => 1, 597 => 1, - 612 => 1, - 614 => 1, - 621 => 1, + 598 => 1, + 599 => 1, + 600 => 1, + 615 => 1, + 617 => 1, + 624 => 1, ], 'warnings' => [ 32 => 1, @@ -273,21 +274,21 @@ 457 => 1, 458 => 1, 459 => 1, - 499 => 1, - 500 => 1, - 504 => 1, - 528 => 1, - 529 => 1, - 530 => 1, + 502 => 1, + 503 => 1, + 507 => 1, 531 => 1, 532 => 1, + 533 => 1, + 534 => 1, 535 => 1, 538 => 1, - 545 => 1, - 559 => 1, - 565 => 1, - 589 => 1, - 618 => 1, + 541 => 1, + 548 => 1, + 562 => 1, + 568 => 1, + 592 => 1, + 621 => 1, ], 'messages' => [ 130 => [ From 0f4de98334f47ec8a19a86292902ff848d65f0b8 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Sun, 22 Mar 2026 16:58:53 +0000 Subject: [PATCH 154/165] feat: warn on exit/die/throw in filter callbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a filter callback uses exit, die, or throw instead of returning a value, the sniff previously flagged it as MissingReturnStatement. This is technically correct but misleading — the developer has intentionally chosen to terminate execution rather than accidentally forgetting a return. The sniff now distinguishes between these cases: callbacks containing terminating statements receive a TerminatingInsteadOfReturn warning with a more specific message, while genuinely missing returns remain errors. This gives users a distinct error code to suppress when the pattern is intentional, without silently ignoring problematic code. Refs #719. --- .../Hooks/AlwaysReturnInFilterSniff.php | 31 +++++++++++-- .../Hooks/AlwaysReturnInFilterUnitTest.inc | 45 +++++++++++++++++++ .../Hooks/AlwaysReturnInFilterUnitTest.php | 5 +++ 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php b/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php index 4d45c9d3..dea2730b 100644 --- a/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php +++ b/WordPressVIPMinimum/Sniffs/Hooks/AlwaysReturnInFilterSniff.php @@ -246,9 +246,15 @@ private function processFunctionBody( $stackPtr ) { } if ( $outsideConditionalReturn === 0 ) { - $message = 'Please, make sure that a callback to `%s` filter is always returning some value.'; - $data = [ $filterName ]; - $this->phpcsFile->addError( $message, $functionBodyScopeStart, 'MissingReturnStatement', $data ); + if ( $this->hasTerminatingStatement( $functionBodyScopeStart, $functionBodyScopeEnd ) ) { + $message = 'The callback for the `%s` filter uses a terminating statement (`exit`, `die`, or `throw`) instead of returning a value. Filter callbacks should always return a value.'; + $data = [ $filterName ]; + $this->phpcsFile->addWarning( $message, $functionBodyScopeStart, 'TerminatingInsteadOfReturn', $data ); + } else { + $message = 'Please, make sure that a callback to `%s` filter is always returning some value.'; + $data = [ $filterName ]; + $this->phpcsFile->addError( $message, $functionBodyScopeStart, 'MissingReturnStatement', $data ); + } } } @@ -288,6 +294,25 @@ private function isInsideIfConditonal( $stackPtr ) { return false; } + /** + * Check whether the function body contains an exit, die, or throw statement. + * + * @param int $scopeStart The scope opener of the function body. + * @param int $scopeEnd The scope closer of the function body. + * + * @return bool + */ + private function hasTerminatingStatement( $scopeStart, $scopeEnd ) { + + $terminatingPtr = $this->phpcsFile->findNext( + [ T_EXIT, T_THROW ], + $scopeStart + 1, + $scopeEnd + ); + + return $terminatingPtr !== false; + } + /** * Is the token returning void * diff --git a/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.inc b/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.inc index 0e9cc609..086b0eab 100644 --- a/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.inc +++ b/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.inc @@ -197,6 +197,51 @@ add_filter( 'bad_void_return_with_space', function() { // Error. return ; } ); +// === exit/die/throw in filter callbacks (issue #719) === + +// Exit used as unconditional termination path — warning, not error. +function exit_in_filter( $redirect_url ) { // Warning. + if ( some_condition() ) { + return $redirect_url; + } + wp_redirect( $redirect_url ); + exit; +} +add_filter( 'redirect_canonical', 'exit_in_filter' ); + +// Die used as unconditional termination path — warning, not error. +add_filter( 'die_in_filter', function( $url ) { // Warning. + if ( some_condition() ) { + return $url; + } + die( 'Terminated' ); +} ); + +// Throw used as unconditional termination path — warning, not error. +class throw_in_filter_class { + public function __construct() { + add_filter( 'throw_in_filter', [ $this, 'validate' ] ); + } + + public function validate( $value ) { // Warning. + if ( is_valid( $value ) ) { + return $value; + } + throw new \RuntimeException( 'Invalid value' ); + } +} + +// Exit with no return at all — warning, not error. +add_filter( 'exit_no_return', function( $input ) { // Warning. + do_something( $input ); + exit; +} ); + +// No return and no exit/die/throw — remains an error. +add_filter( 'still_missing_return', function( $input ) { // Error. + do_something( $input ); +} ); + // Intentional parse error. This has to be the last test in the file! class parse_error_test { public function __construct() { diff --git a/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.php b/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.php index 0c913191..9201985f 100644 --- a/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.php +++ b/WordPressVIPMinimum/Tests/Hooks/AlwaysReturnInFilterUnitTest.php @@ -32,6 +32,7 @@ public function getErrorList() { 163 => 1, 188 => 1, 196 => 1, + 241 => 1, ]; } @@ -43,6 +44,10 @@ public function getErrorList() { public function getWarningList() { return [ 180 => 1, + 203 => 1, + 213 => 1, + 226 => 1, + 235 => 1, ]; } } From 3950f81a523322151cdd14706430793f76377624 Mon Sep 17 00:00:00 2001 From: Mahangu Weerasinghe Date: Thu, 4 Jun 2026 08:01:15 +0530 Subject: [PATCH 155/165] chore: pin third-party GitHub Actions to commit SHAs Tracking: DEVPROD-1072 --- .github/workflows/basics.yml | 14 +++++++------- .github/workflows/quicktest.yml | 4 ++-- .github/workflows/test.yml | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index 5c29d4fe..933c62bc 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v6 - name: Install PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 with: php-version: 'latest' coverage: none @@ -48,14 +48,14 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v4" + uses: "ramsey/composer-install@5c2bcf28d7b060ef3c601d7b476d5430a7b46c27" # v4 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") # Validate the XML file. - name: Validate rulesets against schema - uses: phpcsstandards/xmllint-validate@v1 + uses: phpcsstandards/xmllint-validate@0fd9c4a9046055f621fca4bbdccb8eab1fd59fdc # v1.0.1 with: pattern: "./*/ruleset.xml" xsd-file: "vendor/squizlabs/php_codesniffer/phpcs.xsd" @@ -69,7 +69,7 @@ jobs: # Validate dev tool related XML files. - name: Validate Project PHPCS ruleset against schema - uses: phpcsstandards/xmllint-validate@v1 + uses: phpcsstandards/xmllint-validate@0fd9c4a9046055f621fca4bbdccb8eab1fd59fdc # v1.0.1 with: pattern: ".phpcs.xml.dist" xsd-file: "vendor/squizlabs/php_codesniffer/phpcs.xsd" @@ -77,7 +77,7 @@ jobs: # Note: PHPUnit 9.3 introduced some new configuration options and deprecated the old versions of those. # For cross-version compatibility, the validation is done against the PHPUnit 9.2 schema. - name: "Validate PHPUnit config for use with PHPUnit 9" - uses: phpcsstandards/xmllint-validate@v1 + uses: phpcsstandards/xmllint-validate@0fd9c4a9046055f621fca4bbdccb8eab1fd59fdc # v1.0.1 with: pattern: "phpunit.xml.dist" xsd-file: "vendor/phpunit/phpunit/schema/9.2.xsd" @@ -107,7 +107,7 @@ jobs: uses: actions/checkout@v6 - name: Install PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 with: php-version: 'latest' coverage: none @@ -117,7 +117,7 @@ jobs: # Dependencies need to be installed to make sure the PHPCS and PHPUnit classes are recognized. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v4" + uses: "ramsey/composer-install@5c2bcf28d7b060ef3c601d7b476d5430a7b46c27" # v4 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index 9dcddd62..2c7d74a4 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v6 - name: Set up PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 with: php-version: ${{ matrix.php }} # With stable PHPCS dependencies, allow for PHP deprecation notices. @@ -58,7 +58,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v4" + uses: "ramsey/composer-install@5c2bcf28d7b060ef3c601d7b476d5430a7b46c27" # v4 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 197db4f5..59d57de4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,14 +38,14 @@ jobs: uses: actions/checkout@v6 - name: Install PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 with: php-version: ${{ matrix.php }} coverage: none tools: cs2pr - name: Install Composer dependencies - uses: "ramsey/composer-install@v4" + uses: "ramsey/composer-install@5c2bcf28d7b060ef3c601d7b476d5430a7b46c27" # v4 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") @@ -100,7 +100,7 @@ jobs: fi - name: Install PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 with: php-version: ${{ matrix.php }} ini-values: ${{ steps.set_ini.outputs.PHP_INI }} @@ -123,7 +123,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v4" + uses: "ramsey/composer-install@5c2bcf28d7b060ef3c601d7b476d5430a7b46c27" # v4 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") From d2036016b3c2643a47105945eafe5bfbb25ab892 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 09:03:54 +0000 Subject: [PATCH 156/165] GH Actions: Bump shivammathur/setup-php from 2.37.1 to 2.37.2 Bumps [shivammathur/setup-php](https://github.com/shivammathur/setup-php) from 2.37.1 to 2.37.2. - [Release notes](https://github.com/shivammathur/setup-php/releases) - [Commits](https://github.com/shivammathur/setup-php/compare/7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc...f3e473d116dcccaddc5834248c87452386958240) --- updated-dependencies: - dependency-name: shivammathur/setup-php dependency-version: 2.37.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/basics.yml | 4 ++-- .github/workflows/quicktest.yml | 2 +- .github/workflows/test.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index 933c62bc..aacadbc2 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v6 - name: Install PHP - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: 'latest' coverage: none @@ -107,7 +107,7 @@ jobs: uses: actions/checkout@v6 - name: Install PHP - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: 'latest' coverage: none diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index 2c7d74a4..fa582ed2 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v6 - name: Set up PHP - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: ${{ matrix.php }} # With stable PHPCS dependencies, allow for PHP deprecation notices. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 59d57de4..902c43f3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -38,7 +38,7 @@ jobs: uses: actions/checkout@v6 - name: Install PHP - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: ${{ matrix.php }} coverage: none @@ -100,7 +100,7 @@ jobs: fi - name: Install PHP - uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 with: php-version: ${{ matrix.php }} ini-values: ${{ steps.set_ini.outputs.PHP_INI }} From b3c1f8bb029bc2e0df5c0ebfeab86692ca3b3d65 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Mon, 15 Jun 2026 11:10:00 +0100 Subject: [PATCH 157/165] ci: pin composer-install to 4.0.0 and checkout to 6.0.3 Pin ramsey/composer-install to the 4.0.0 release SHA rather than the floating v4 branch tip. The previous pin tracked the v4 branch, so the version comment could only ever read "# v4" and a SHA bump carried no visible meaning in a diff. Pinning to the 4.0.0 tag gives an honest SHA-to-version mapping and lets Dependabot follow releases, matching the style already used for setup-php and xmllint-validate. Also pin actions/checkout to the 6.0.3 release SHA. It was previously left on the floating @v6 major tag; pinning it brings the first-party action in line with the SHA-and-version convention and makes future Dependabot bumps self-explanatory. --- .github/workflows/basics.yml | 8 ++++---- .github/workflows/quicktest.yml | 4 ++-- .github/workflows/test.yml | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index aacadbc2..e07a6bd8 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3 - name: Install PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 @@ -48,7 +48,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@5c2bcf28d7b060ef3c601d7b476d5430a7b46c27" # v4 + uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # 4.0.0 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") @@ -104,7 +104,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3 - name: Install PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 @@ -117,7 +117,7 @@ jobs: # Dependencies need to be installed to make sure the PHPCS and PHPUnit classes are recognized. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@5c2bcf28d7b060ef3c601d7b476d5430a7b46c27" # v4 + uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # 4.0.0 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index fa582ed2..37712cb9 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -40,7 +40,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3 - name: Set up PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 @@ -58,7 +58,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@5c2bcf28d7b060ef3c601d7b476d5430a7b46c27" # v4 + uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # 4.0.0 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 902c43f3..8587449c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3 - name: Install PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 @@ -45,7 +45,7 @@ jobs: tools: cs2pr - name: Install Composer dependencies - uses: "ramsey/composer-install@5c2bcf28d7b060ef3c601d7b476d5430a7b46c27" # v4 + uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # 4.0.0 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") @@ -86,7 +86,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3 # With stable PHPCS dependencies, allow for PHP deprecation notices. # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore. @@ -123,7 +123,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@5c2bcf28d7b060ef3c601d7b476d5430a7b46c27" # v4 + uses: "ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda" # 4.0.0 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") From bba2ad7bc578e3c8cf3b39f928923532cedd6503 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:03:24 +0000 Subject: [PATCH 158/165] GH Actions: Bump actions/checkout from 6.0.3 to 7.0.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/basics.yml | 4 ++-- .github/workflows/quicktest.yml | 2 +- .github/workflows/test.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index e07a6bd8..64f0fe44 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 - name: Install PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 @@ -104,7 +104,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 - name: Install PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index 37712cb9..23282bdc 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -40,7 +40,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 - name: Set up PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8587449c..541d05f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 - name: Install PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 @@ -86,7 +86,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # 6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 # With stable PHPCS dependencies, allow for PHP deprecation notices. # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore. From 007419f48e47d6766c8618dca80c4b938be56e6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 09:03:27 +0000 Subject: [PATCH 159/165] GH Actions: Bump phpcsstandards/xmllint-validate from 1.0.1 to 2.0.0 Bumps [phpcsstandards/xmllint-validate](https://github.com/phpcsstandards/xmllint-validate) from 1.0.1 to 2.0.0. - [Release notes](https://github.com/phpcsstandards/xmllint-validate/releases) - [Commits](https://github.com/phpcsstandards/xmllint-validate/compare/0fd9c4a9046055f621fca4bbdccb8eab1fd59fdc...5189514594c8d5f4cf21b7e5af50f54d697973d7) --- updated-dependencies: - dependency-name: phpcsstandards/xmllint-validate dependency-version: 2.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/basics.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index e07a6bd8..79d639ff 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -55,7 +55,7 @@ jobs: # Validate the XML file. - name: Validate rulesets against schema - uses: phpcsstandards/xmllint-validate@0fd9c4a9046055f621fca4bbdccb8eab1fd59fdc # v1.0.1 + uses: phpcsstandards/xmllint-validate@5189514594c8d5f4cf21b7e5af50f54d697973d7 # v2.0.0 with: pattern: "./*/ruleset.xml" xsd-file: "vendor/squizlabs/php_codesniffer/phpcs.xsd" @@ -69,7 +69,7 @@ jobs: # Validate dev tool related XML files. - name: Validate Project PHPCS ruleset against schema - uses: phpcsstandards/xmllint-validate@0fd9c4a9046055f621fca4bbdccb8eab1fd59fdc # v1.0.1 + uses: phpcsstandards/xmllint-validate@5189514594c8d5f4cf21b7e5af50f54d697973d7 # v2.0.0 with: pattern: ".phpcs.xml.dist" xsd-file: "vendor/squizlabs/php_codesniffer/phpcs.xsd" @@ -77,7 +77,7 @@ jobs: # Note: PHPUnit 9.3 introduced some new configuration options and deprecated the old versions of those. # For cross-version compatibility, the validation is done against the PHPUnit 9.2 schema. - name: "Validate PHPUnit config for use with PHPUnit 9" - uses: phpcsstandards/xmllint-validate@0fd9c4a9046055f621fca4bbdccb8eab1fd59fdc # v1.0.1 + uses: phpcsstandards/xmllint-validate@5189514594c8d5f4cf21b7e5af50f54d697973d7 # v2.0.0 with: pattern: "phpunit.xml.dist" xsd-file: "vendor/phpunit/phpunit/schema/9.2.xsd" From ad5765344d3921bda8c53124f4916993171bf9ef Mon Sep 17 00:00:00 2001 From: Michael Chan Date: Tue, 7 Jul 2026 21:31:38 +1000 Subject: [PATCH 160/165] Refactor tests and test runner, add BOM sniff, add bom specific ruleset to prevent changing the superfluous space check --- WordPress-VIP-Go/ruleset-test-bom.inc | 3 +++ WordPress-VIP-Go/ruleset-test.php | 18 ++++++++++++++++-- WordPressVIPMinimum/ruleset-test-bom.inc | 3 +++ WordPressVIPMinimum/ruleset-test.php | 18 ++++++++++++++++-- WordPressVIPMinimum/ruleset.xml | 1 + tests/RulesetTest.php | 18 ++++++++++++++---- 6 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 WordPress-VIP-Go/ruleset-test-bom.inc create mode 100644 WordPressVIPMinimum/ruleset-test-bom.inc diff --git a/WordPress-VIP-Go/ruleset-test-bom.inc b/WordPress-VIP-Go/ruleset-test-bom.inc new file mode 100644 index 00000000..7ecb784f --- /dev/null +++ b/WordPress-VIP-Go/ruleset-test-bom.inc @@ -0,0 +1,3 @@ + [ + 1 => 1, + ], + 'warnings' => [], + 'messages' => [ + 1 => [ + 'File contains UTF-8 byte order mark, which may corrupt your application', + ], + ], +]; + require __DIR__ . '/../tests/RulesetTest.php'; // Run the tests! -$test = new RulesetTest( 'WordPress-VIP-Go', $expected ); -if ( $test->passes() ) { +$test = new RulesetTest( 'WordPress-VIP-Go', $expected ); +$bom_test = new RulesetTest( 'WordPress-VIP-Go', $bom_expected, 'ruleset-test-bom.inc' ); +if ( $test->passes() && $bom_test->passes() ) { printf( 'All WordPress-VIP-Go tests passed!' . PHP_EOL ); exit( 0 ); } diff --git a/WordPressVIPMinimum/ruleset-test-bom.inc b/WordPressVIPMinimum/ruleset-test-bom.inc new file mode 100644 index 00000000..7ecb784f --- /dev/null +++ b/WordPressVIPMinimum/ruleset-test-bom.inc @@ -0,0 +1,3 @@ + [ + 1 => 1, + ], + 'warnings' => [], + 'messages' => [ + 1 => [ + 'File contains UTF-8 byte order mark, which may corrupt your application', + ], + ], +]; + require __DIR__ . '/../tests/RulesetTest.php'; // Run the tests! -$test = new RulesetTest( 'WordPressVIPMinimum', $expected ); -if ( $test->passes() ) { +$test = new RulesetTest( 'WordPressVIPMinimum', $expected ); +$bom_test = new RulesetTest( 'WordPressVIPMinimum', $bom_expected, 'ruleset-test-bom.inc' ); +if ( $test->passes() && $bom_test->passes() ) { printf( 'All WordPressVIPMinimum tests passed!' . PHP_EOL ); exit( 0 ); } diff --git a/WordPressVIPMinimum/ruleset.xml b/WordPressVIPMinimum/ruleset.xml index c735a3b9..c0f66990 100644 --- a/WordPressVIPMinimum/ruleset.xml +++ b/WordPressVIPMinimum/ruleset.xml @@ -60,6 +60,7 @@ + diff --git a/tests/RulesetTest.php b/tests/RulesetTest.php index 3fb863f4..dc03e3ef 100644 --- a/tests/RulesetTest.php +++ b/tests/RulesetTest.php @@ -74,6 +74,13 @@ class RulesetTest { */ private $phpcs_bin = 'phpcs'; + /** + * Name of the fixture file (relative to the ruleset directory) to check. + * + * @var string + */ + private $fixture = 'ruleset-test.inc'; + /** * String returned by PHP_CodeSniffer report for an Error. */ @@ -84,10 +91,12 @@ class RulesetTest { * * @param string $ruleset Name of the ruleset e.g. WordPressVIPMinimum or WordPress-VIP-Go. * @param array>> $expected The array of expected errors, warnings and messages. + * @param string $fixture Name of the fixture file within the ruleset directory to check. Defaults to `ruleset-test.inc`. */ - public function __construct( $ruleset, $expected = [] ) { + public function __construct( $ruleset, $expected = [], $fixture = 'ruleset-test.inc' ) { $this->ruleset = $ruleset; $this->expected = $expected; + $this->fixture = $fixture; // Travis and Windows support. $phpcs_bin = getenv( 'PHPCS_BIN' ); @@ -99,7 +108,7 @@ public function __construct( $ruleset, $expected = [] ) { } // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - printf( 'Testing the ' . $this->ruleset . ' ruleset.' . PHP_EOL ); + printf( 'Testing the ' . $this->ruleset . ' ruleset against ' . $this->fixture . '.' . PHP_EOL ); $output = $this->collect_phpcs_result(); @@ -149,11 +158,12 @@ private function collect_phpcs_result() { $report_file = dirname( __DIR__ ) . '/ruleset-tests-report.json'; $shell = sprintf( - '%1$s%2$s --severity=1 --standard=%3$s --report-json=%4$s ./%3$s/ruleset-test.inc', + '%1$s%2$s --severity=1 --standard=%3$s --report-json=%4$s ./%3$s/%5$s', $php, // Current PHP executable if available. $this->phpcs_bin, $this->ruleset, - $report_file + $report_file, + $this->fixture ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_shell_exec -- This is test code, not production. From 06c89838ac8127312301c8c0fd5b30fc28a213be Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Tue, 7 Jul 2026 14:38:54 +0100 Subject: [PATCH 161/165] test: preserve BOM in ruleset test fixtures via EditorConfig The BOM sniff test relies on `ruleset-test-bom.inc` beginning with a UTF-8 byte order mark. The global `[*]` section sets `charset = utf-8`, which prompts EditorConfig-aware editors to strip that mark on save, silently invalidating the Generic.Files.ByteOrderMark ruleset test. A scoped `[*-bom.inc]` override with `charset = utf-8-bom` instructs those editors to keep the mark, protecting the fixture from accidental damage. --- .editorconfig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.editorconfig b/.editorconfig index 30288000..69ef6731 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,9 @@ [*.md] trim_trailing_whitespace = false + + # Preserve the UTF-8 byte order mark in the BOM ruleset test fixtures, so that + # editors honouring EditorConfig do not silently strip it (which would + # invalidate the Generic.Files.ByteOrderMark ruleset test). + [*-bom.inc] + charset = utf-8-bom From 759204488580f360666227084071f8fdae316330 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Tue, 7 Jul 2026 14:39:02 +0100 Subject: [PATCH 162/165] test: report discrepancies from both ruleset tests Guarding the success path with `$test->passes() && $bom_test->passes()` short-circuits: when the primary ruleset test fails, the BOM test's `passes()` never runs, so its discrepancy report is never printed. That hides half the picture from anyone debugging a failure. Evaluating both results into variables before the check lets each test report its own discrepancies independently. --- WordPress-VIP-Go/ruleset-test.php | 6 +++++- WordPressVIPMinimum/ruleset-test.php | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/WordPress-VIP-Go/ruleset-test.php b/WordPress-VIP-Go/ruleset-test.php index 6af4b359..6f929fbb 100644 --- a/WordPress-VIP-Go/ruleset-test.php +++ b/WordPress-VIP-Go/ruleset-test.php @@ -342,7 +342,11 @@ // Run the tests! $test = new RulesetTest( 'WordPress-VIP-Go', $expected ); $bom_test = new RulesetTest( 'WordPress-VIP-Go', $bom_expected, 'ruleset-test-bom.inc' ); -if ( $test->passes() && $bom_test->passes() ) { + +// Evaluate both tests before the check so each reports its own discrepancies rather than being short-circuited away. +$test_passes = $test->passes(); +$bom_test_passes = $bom_test->passes(); +if ( $test_passes && $bom_test_passes ) { printf( 'All WordPress-VIP-Go tests passed!' . PHP_EOL ); exit( 0 ); } diff --git a/WordPressVIPMinimum/ruleset-test.php b/WordPressVIPMinimum/ruleset-test.php index 19dd17ef..8fd42f54 100644 --- a/WordPressVIPMinimum/ruleset-test.php +++ b/WordPressVIPMinimum/ruleset-test.php @@ -327,7 +327,11 @@ // Run the tests! $test = new RulesetTest( 'WordPressVIPMinimum', $expected ); $bom_test = new RulesetTest( 'WordPressVIPMinimum', $bom_expected, 'ruleset-test-bom.inc' ); -if ( $test->passes() && $bom_test->passes() ) { + +// Evaluate both tests before the check so each reports its own discrepancies rather than being short-circuited away. +$test_passes = $test->passes(); +$bom_test_passes = $bom_test->passes(); +if ( $test_passes && $bom_test_passes ) { printf( 'All WordPressVIPMinimum tests passed!' . PHP_EOL ); exit( 0 ); } From 7b2e08d6773472b9cb975aeb260cc9a90411b0f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 09:03:34 +0000 Subject: [PATCH 163/165] GH Actions: Bump actions/checkout from 7.0.0 to 7.0.1 Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/basics.yml | 4 ++-- .github/workflows/quicktest.yml | 2 +- .github/workflows/test.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/basics.yml b/.github/workflows/basics.yml index 6284a4a4..ba8886e7 100644 --- a/.github/workflows/basics.yml +++ b/.github/workflows/basics.yml @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 7.0.1 - name: Install PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 @@ -104,7 +104,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 7.0.1 - name: Install PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/quicktest.yml b/.github/workflows/quicktest.yml index 23282bdc..76bbffc0 100644 --- a/.github/workflows/quicktest.yml +++ b/.github/workflows/quicktest.yml @@ -40,7 +40,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 7.0.1 - name: Set up PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 541d05f7..afa59cdc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 7.0.1 - name: Install PHP uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2 @@ -86,7 +86,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # 7.0.1 # With stable PHPCS dependencies, allow for PHP deprecation notices. # Unit tests don't need to fail on those for stable releases where those issues won't get fixed anymore. From c198d3e5ff94f1c1d2e943f8cb2f1e2e4db22419 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Mon, 27 Jul 2026 14:51:26 +0100 Subject: [PATCH 164/165] build: raise minimum WordPressCS to 3.4.1 WordPressCS 3.4.1 is a security release, and 3.3.0-3.4.1 also improve the accuracy of several sniffs that VIPCS bundles, so requiring it passes both benefits on to VIPCS users. The other coding-standard floors are raised in step to match what WordPressCS 3.4.1 itself requires: PHP_CodeSniffer 3.13.5, PHPCSUtils 1.2.3, PHPCSExtra 1.5.1 and VariableAnalysis 2.13.0. The README requirements and the PHPCompatibility testVersion (now 7.4-, matching the existing PHP floor) are brought back into sync, as both had drifted from composer.json. --- .phpcs.xml.dist | 2 +- README.md | 12 ++++++------ composer.json | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist index 4ca016ad..ea93917a 100644 --- a/.phpcs.xml.dist +++ b/.phpcs.xml.dist @@ -44,7 +44,7 @@ - + diff --git a/README.md b/README.md index afea6c2e..2b6c2e27 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,12 @@ The rulesets use rules from the [WordPress Coding Standards](https://github.com/ ## Minimal requirements -* PHP 5.4+ -* [PHPCS 3.13.2+](https://github.com/PHPCSStandards/PHP_CodeSniffer/releases) -* [PHPCSUtils 1.1.0+](https://github.com/PHPCSStandards/PHPCSUtils) -* [PHPCSExtra 1.4.0+](https://github.com/PHPCSStandards/PHPCSExtra) -* [WPCS 3.1.0+](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/releases) -* [VariableAnalysis 2.12.0+](https://github.com/sirbrillig/phpcs-variable-analysis/releases) +* PHP 7.4+ +* [PHPCS 3.13.5+](https://github.com/PHPCSStandards/PHP_CodeSniffer/releases) +* [PHPCSUtils 1.2.3+](https://github.com/PHPCSStandards/PHPCSUtils) +* [PHPCSExtra 1.5.1+](https://github.com/PHPCSStandards/PHPCSExtra) +* [WPCS 3.4.1+](https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/releases) +* [VariableAnalysis 2.13.0+](https://github.com/sirbrillig/phpcs-variable-analysis/releases) ## Installation diff --git a/composer.json b/composer.json index bfe45898..30ca3ab7 100644 --- a/composer.json +++ b/composer.json @@ -17,11 +17,11 @@ ], "require": { "php": ">=7.4", - "phpcsstandards/phpcsextra": "^1.4.0", - "phpcsstandards/phpcsutils": "^1.1.0", - "sirbrillig/phpcs-variable-analysis": "^2.12.0", - "squizlabs/php_codesniffer": "^3.13.2", - "wp-coding-standards/wpcs": "^3.2.0" + "phpcsstandards/phpcsextra": "^1.5.1", + "phpcsstandards/phpcsutils": "^1.2.3", + "sirbrillig/phpcs-variable-analysis": "^2.13.0", + "squizlabs/php_codesniffer": "^3.13.5", + "wp-coding-standards/wpcs": "^3.4.1" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.4.0", From e9b8992f0ea5825c1a6bc42c6d6ac63de584bdb1 Mon Sep 17 00:00:00 2001 From: Gary Jones Date: Mon, 27 Jul 2026 15:24:40 +0100 Subject: [PATCH 165/165] docs: add changelog for 3.1.0 release Assemble the 3.1.0 entry from all PRs merged since 3.0.1, following the project convention of compiling the changelog in one place at release time rather than per-PR. --- CHANGELOG.md | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e04b2ac..be060469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,99 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.1.0] - 2026-07-27 + +Props: @GaryJones, @jrfnl, @mahangu, @mchanDev, @mujuonly, @rebeccahum + +This release raises the minimum requirements and reworks a large number of the VIPCS sniffs. + +The minimum [WordPressCS](https://github.com/WordPress/WordPress-Coding-Standards/releases/tag/3.4.1) requirement is raised to 3.4.1 (from 3.2.0) — a security release which also brings numerous accuracy improvements to sniffs that VIPCS bundles — along with PHP_CodeSniffer 3.13.5, PHPCSUtils 1.2.3, PHPCSExtra 1.5.1 and VariableAnalysis 2.13.0. The minimum PHP version is now 7.4. Many sniffs have been reworked to fix false positives, add support for modern PHP syntaxes and adopt PHPCSUtils. The JavaScript-specific sniffs and the `DynamicCalls` sniff have been hard-deprecated ahead of their removal in 4.0.0, and the unused `Security.Twig` sniff has been removed. + +Please ensure you run `composer update automattic/vipwpcs --with-dependencies` to benefit from this. + +### Added +- [#882](https://github.com/Automattic/VIP-Coding-Standards/pull/882): Performance/NoPaging: flag `posts_per_page` and `numberposts` set to `-1` (WordPress' "no limit" value), which previously slipped past the WPCS `> 100` check. +- [#883](https://github.com/Automattic/VIP-Coding-Standards/pull/883): Hooks/AlwaysReturnInFilter: `exit`, `die` or `throw` in a filter callback now raises a dedicated `TerminatingInsteadOfReturn` warning, instead of the generic `MissingReturnStatement` error. +- [#890](https://github.com/Automattic/VIP-Coding-Standards/pull/890): Add a Byte Order Mark (BOM) check to the `WordPressVIPMinimum` ruleset. + +### Changed +- [#824](https://github.com/Automattic/VIP-Coding-Standards/pull/824): Disable `WordPress.Security.EscapeOutput.ExceptionNotEscaped` in the `WordPressVIPMinimum` ruleset, as throwing an exception with a translated message triggered it and the check is considered controversial. +- [#854](https://github.com/Automattic/VIP-Coding-Standards/pull/854): Functions/StripTags: always flag use of `strip_tags()`, as the function should never be used on the VIP platform. +- As VIPCS bundles a number of WordPressCS sniffs and the minimum WordPressCS version has been raised to 3.4.1, users will now receive more accurate results from the following WPCS sniffs (see the [WordPressCS 3.3.0](https://github.com/WordPress/WordPress-Coding-Standards/releases/tag/3.3.0), [3.4.0](https://github.com/WordPress/WordPress-Coding-Standards/releases/tag/3.4.0) and [3.4.1](https://github.com/WordPress/WordPress-Coding-Standards/releases/tag/3.4.1) release notes for full details): + * `WordPress.DB.DirectDatabaseQuery`: recognises more caching functions (such as the `wp_cache_*_multiple()` and `wp_cache_*_salted()` functions), and has fewer false positives when caching functions are called using a non-canonical function name. + * `WordPress.DB.PreparedSQL`: fewer false positives for correctly escaped SQL called using a non-canonical function name, and for static method calls to a non-global class named `wpdb`. + * `WordPress.Security.EscapeOutput`: adds support for attributes on anonymous classes (PHP 8.0), `readonly` anonymous classes (PHP 8.3) and `exit` as a function call (PHP 8.4); expands `*::class` false-positive protection; fixes false positives/negatives for `get_search_query()` and `_deprecated_file()` used with non-standard casing; and no longer treats `wp_kses_allowed_html()` as an escaping function, which may surface a new warning where that function's return value was being output directly. + * `WordPress.Security.NonceVerification`: fewer false positives when the nonce-checking function is called using a non-canonical function name. + * `WordPress.Security.ValidatedSanitizedInput`: clearer error message for the `InputNotValidated` error code. + * `WordPress.WP.AlternativeFunctions`: fixes a false negative when class members share a name with select global WP functions/constants, and a false positive for the fully qualified stream constants `\STDIN`, `\STDOUT` and `\STDERR`. + * `WordPress.WP.CronInterval`: fixes a false positive when the callback reference used a different case to the function declaration. + +### Deprecated +- [#839](https://github.com/Automattic/VIP-Coding-Standards/pull/839): Hard-deprecate all JavaScript-specific sniffs (`WordPressVIPMinimum.JS.*`). They are excluded from the rulesets and will be removed in VIPCS 4.0.0. +- [#865](https://github.com/Automattic/VIP-Coding-Standards/pull/865): Hard-deprecate the `WordPressVIPMinimum.Functions.DynamicCalls` sniff. It is excluded from the rulesets and will be removed in VIPCS 4.0.0. + +### Removed +- [#864](https://github.com/Automattic/VIP-Coding-Standards/pull/864): Remove the unused `WordPressVIPMinimum.Security.Twig` sniff. + +### Fixed +- [#840](https://github.com/Automattic/VIP-Coding-Standards/pull/840): Fix dangerous comparisons against the value of token constants. +- [#842](https://github.com/Automattic/VIP-Coding-Standards/pull/842): Security/Mustache: fix potential false positives on a delimiter change, and examine double-quoted strings with interpolation and nowdocs which were previously skipped. +- [#846](https://github.com/Automattic/VIP-Coding-Standards/pull/846), [#848](https://github.com/Automattic/VIP-Coding-Standards/pull/848): Security/PHPFilterFunctions: fix false positives for method calls, namespaced function calls and attributes which share the function name. +- [#847](https://github.com/Automattic/VIP-Coding-Standards/pull/847): Security/StaticStrreplace: fix flawed detection of `str_replace()` by extending the WordPressCS `AbstractFunctionParameterSniff`; method calls, namespaced calls, first-class callables and argument unpacking are no longer flagged. +- [#850](https://github.com/Automattic/VIP-Coding-Standards/pull/850): Variables/ServerVariables: fix incorrect quote stripping and expand safeguards against false positives. +- [#851](https://github.com/Automattic/VIP-Coding-Standards/pull/851): Constants/ConstantString: fix flawed detection of `define()`/`defined()` by extending `AbstractFunctionParameterSniff`, and clarify the error message. +- [#852](https://github.com/Automattic/VIP-Coding-Standards/pull/852): Hooks/RestrictedHooks: fix false positives (method/namespaced calls, first-class callables, attributes) and disregard comments in the hook-name parameter. +- [#853](https://github.com/Automattic/VIP-Coding-Standards/pull/853): Performance/LowExpiryCacheTime: avoid a possible fatal error when a PHP 7.4+ numeric literal or 8.1+ octal literal is used as the cache time. +- [#855](https://github.com/Automattic/VIP-Coding-Standards/pull/855): Performance/FetchingRemoteData: fix flawed function-call detection by extending `AbstractFunctionParameterSniff`. +- [#858](https://github.com/Automattic/VIP-Coding-Standards/pull/858): Classes/DeclarationCompatibility: modernise and fix the sniff. +- [#861](https://github.com/Automattic/VIP-Coding-Standards/pull/861): Security/EscapingVoidReturnFunctions: fix flawed detection of `esc_*()`/`wp_kses*()` calls by extending `AbstractFunctionParameterSniff`; safeguard argument unpacking, attributes and first-class callables. +- [#862](https://github.com/Automattic/VIP-Coding-Standards/pull/862): Functions/DynamicCalls: fix end-of-statement determination and other minor issues. +- [#866](https://github.com/Automattic/VIP-Coding-Standards/pull/866): UserExperience/AdminBarRemoval: several fixes — case-insensitive function-name matching, recognise `add_action()` as an alias, disregard comments in parameters, and no longer treat a CSS file as PHP; plus PHP 8.0+ function-call and 8.1+ first-class-callable support. +- [#867](https://github.com/Automattic/VIP-Coding-Standards/pull/867): Performance/CacheValueOverride: fix false negatives for fully qualified calls and false positives for PHP 8.1+ first-class callables and variables. +- [#869](https://github.com/Automattic/VIP-Coding-Standards/pull/869): Constants/RestrictedConstants: fix a nonsensical comparison and use PHPCSUtils for quote stripping. +- [#872](https://github.com/Automattic/VIP-Coding-Standards/pull/872): Performance/TaxonomyMetaInOptions: extend `AbstractFunctionParameterSniff` and add PHP 8.0+ function-call and nullsafe-operator support. +- [#881](https://github.com/Automattic/VIP-Coding-Standards/pull/881): Hooks/AlwaysReturnInFilter: resolve bugs and adopt PHPCSUtils. + +### Security +- [WordPressCS 3.4.1](https://github.com/WordPress/WordPress-Coding-Standards/security/advisories/GHSA-3pwp-g2mj-5p3v) fixes a vulnerability in the `WordPress.WP.EnqueuedResourceParameters` sniff whereby running it over untrusted code could lead to arbitrary command execution on the scanning host. The VIPCS rulesets do not enable that sniff, so VIPCS' own behaviour is unaffected, but raising the minimum WordPressCS version removes the affected releases from the dependency tree and protects users who additionally run the `WordPress` or `WordPress-Extra` standards. + +### Maintenance +- Composer: + * [#857](https://github.com/Automattic/VIP-Coding-Standards/pull/857): Raise the minimum supported versions of all coding-standard dependencies. This release requires WordPressCS 3.4.1, PHP_CodeSniffer 3.13.5, PHPCSUtils 1.2.3, PHPCSExtra 1.5.1 and VariableAnalysis 2.13.0. + * [#828](https://github.com/Automattic/VIP-Coding-Standards/pull/828): Prevent a `composer.lock` file from being created. + * [#837](https://github.com/Automattic/VIP-Coding-Standards/pull/837): Bump the PHP Parallel Lint requirement. +- [#880](https://github.com/Automattic/VIP-Coding-Standards/pull/880): Bump the minimum PHP version to 7.4, drop PHPUnit 8 support and fix CI. +- Rulesets: + * [#835](https://github.com/Automattic/VIP-Coding-Standards/pull/835): Update the schema URL. + * [#860](https://github.com/Automattic/VIP-Coding-Standards/pull/860): Fix unescaped special characters in the `include`/`exclude` patterns. +- Sniffs: + * [#843](https://github.com/Automattic/VIP-Coding-Standards/pull/843): Fix license information and standardise the file docblocks. + * [#844](https://github.com/Automattic/VIP-Coding-Standards/pull/844): Remove `@since` tags which relate to WordPressCS. + * [#856](https://github.com/Automattic/VIP-Coding-Standards/pull/856): Classes/RestrictedExtendClasses: only listen for the `extends` keyword, significantly improving performance, and add tests. + * [#863](https://github.com/Automattic/VIP-Coding-Standards/pull/863): Functions/RestrictedFunctions: improve the `is_targetted_token()` method. + * [#868](https://github.com/Automattic/VIP-Coding-Standards/pull/868): Security/Underscorejs: start using the PHPCSUtils `FilePath` utility. + * [#870](https://github.com/Automattic/VIP-Coding-Standards/pull/870): Various minor CS fixes. +- Docs: + * [#826](https://github.com/Automattic/VIP-Coding-Standards/pull/826): Improve consistency and specificity. + * [#871](https://github.com/Automattic/VIP-Coding-Standards/pull/871): Various minor fixes. + * [#875](https://github.com/Automattic/VIP-Coding-Standards/pull/875): Performance/WPQueryParams: update a documentation link. + * Update the minimum requirements in the `README`. +- QA: + * [#825](https://github.com/Automattic/VIP-Coding-Standards/pull/825): Fix the PHPStan build. + * [#834](https://github.com/Automattic/VIP-Coding-Standards/pull/834): Silence a deprecation notice during the CS run. + * [#845](https://github.com/Automattic/VIP-Coding-Standards/pull/845): Fix the PHPCompatibility exclusions in the internal ruleset. + * Align the PHPCompatibility `testVersion` with the PHP 7.4 minimum requirement. +- Tests: + * [#833](https://github.com/Automattic/VIP-Coding-Standards/pull/833): Fix `RulesetTest`. + * [#838](https://github.com/Automattic/VIP-Coding-Standards/pull/838): Remove a PHPCS version toggle from `AlwaysReturnInFilterUnitTest`. + * [#890](https://github.com/Automattic/VIP-Coding-Standards/pull/890): Refactor the tests and test runner, and preserve/handle a BOM in the ruleset-test fixtures. +- GitHub Actions: + * [#827](https://github.com/Automattic/VIP-Coding-Standards/pull/827): Always quote variables. + * [#829](https://github.com/Automattic/VIP-Coding-Standards/pull/829): Run against PHP 8.4. + * [#830](https://github.com/Automattic/VIP-Coding-Standards/pull/830): Use the `xmllint-validate` action runner and add extra checks. + * [#884](https://github.com/Automattic/VIP-Coding-Standards/pull/884), [#887](https://github.com/Automattic/VIP-Coding-Standards/pull/887): Pin third-party GitHub Actions to commit SHAs. + * [#877](https://github.com/Automattic/VIP-Coding-Standards/pull/877), [#879](https://github.com/Automattic/VIP-Coding-Standards/pull/879), [#885](https://github.com/Automattic/VIP-Coding-Standards/pull/885), [#888](https://github.com/Automattic/VIP-Coding-Standards/pull/888), [#889](https://github.com/Automattic/VIP-Coding-Standards/pull/889): Dependency updates via Dependabot. + ## [3.0.1] - 2024-05-14 Props: @GaryJones, @jrnfl, @terriann, @rebeccahum @@ -709,6 +802,7 @@ Initial release. Props: david-binda, pkevan. +[3.1.0]: https://github.com/Automattic/VIP-Coding-Standards/compare/3.0.1...3.1.0 [3.0.1]: https://github.com/Automattic/VIP-Coding-Standards/compare/3.0.0...3.0.1 [3.0.0]: https://github.com/Automattic/VIP-Coding-Standards/compare/2.3.4...3.0.0 [2.3.4]: https://github.com/Automattic/VIP-Coding-Standards/compare/2.3.3...2.3.4