-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Forbid using certain functions and classes #57738
Merged
anton-vlasenko
merged 37 commits into
trunk
from
try/forbid-calling-disallowed-functions-sniff
Jan 31, 2024
Merged
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
0005ff9
Add a new property to the PHPCS sniff.
anton-vlasenko 7d473cd
Commit WIP.
anton-vlasenko efa3918
Commit WIP.
anton-vlasenko abaad87
Commit WIP.
anton-vlasenko 1a92b2b
Implement processing function calls.
anton-vlasenko 01dc9c9
Implement the ValidBlockLibraryFunctionNameSniff::processFunctionCall…
anton-vlasenko f176d57
Add the disallowed_function_calls property to phpcs.xml.dist.
anton-vlasenko 4c32ad6
Improve the error message.
anton-vlasenko 0056957
Rename the method to better refelct its purpose.
anton-vlasenko be64f5c
Move the changes to a new sniff.
anton-vlasenko c949041
Update the phpcs.xml.dist file.
anton-vlasenko 8b262af
Fix error.
anton-vlasenko e7e5a1e
Fix property name.
anton-vlasenko 5a0a63d
Fix the sniff.
anton-vlasenko 8e64c08
Fix the config.
anton-vlasenko 3dbd5a8
Remove redundnat code.
anton-vlasenko 66cc48f
Refactor the sniff, add descriptions.
anton-vlasenko b8e4742
Fix doc blocks.
anton-vlasenko dde1e6d
Implement the test for the RestrictedFunctionsAndClassesSniff sniff.
anton-vlasenko 5d9b80e
Implement the test for the RestrictedFunctionsAndClassesSniff sniff.
anton-vlasenko f1cf5bd
Rename Restricted to Forbidden as forbidden is a better description o…
anton-vlasenko f10aee5
Rename properties.
anton-vlasenko 9ff2764
Rename properties.
anton-vlasenko 829310d
Fix typo.
anton-vlasenko 1047a39
Change error codes.
anton-vlasenko c0437fc
Teach the sniff not to flag guarded and, therefore, legitimate uses o…
anton-vlasenko 4cd7b18
Improve checks for guarded functions/classes.
anton-vlasenko 539406c
Better explanation + fix code style.
anton-vlasenko 9b590f8
Add phpcs:ignore statements.
anton-vlasenko c1ebd27
Refactor the code so that the sniff doesn't flag it.
anton-vlasenko 6bfd934
Fix typo.
anton-vlasenko 588a094
Fix code style.
anton-vlasenko 9cd966d
Fix typo.
anton-vlasenko ae6a161
Add unit tests.
anton-vlasenko 8249699
Fallback to isset().
anton-vlasenko 0c8ef69
Improve the regular expression.
anton-vlasenko 255e3ed
Move the exclude pattern statements to the `phpcs.xml.dist` file to a…
anton-vlasenko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
231 changes: 231 additions & 0 deletions
231
...berg-coding-standards/Gutenberg/Sniffs/CodeAnalysis/ForbiddenFunctionsAndClassesSniff.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
<?php | ||
/** | ||
* Gutenberg Coding Standards. | ||
* | ||
* @package gutenberg/gutenberg-coding-standards | ||
* @link https://github.com/WordPress/gutenberg | ||
* @license https://opensource.org/licenses/MIT MIT | ||
*/ | ||
|
||
namespace GutenbergCS\Gutenberg\Sniffs\CodeAnalysis; | ||
|
||
use PHP_CodeSniffer\Files\File; | ||
use PHP_CodeSniffer\Sniffs\Sniff; | ||
|
||
/** | ||
* This sniff enforces usage restrictions on specific classes and functions within the Gutenberg project. | ||
* While it permits the declaration of these classes/functions, their actual usage in code is forbidden based on defined regular expressions. | ||
* | ||
* @package gutenberg/gutenberg-coding-standards | ||
* | ||
* @since 1.0.0 | ||
*/ | ||
final class ForbiddenFunctionsAndClassesSniff implements Sniff { | ||
|
||
/** | ||
* Holds regular expressions for classes whose usage is forbidden. | ||
* Note that declaring functions with names matching these regular expressions is still permitted. | ||
* Useful for enforcing security or architectural constraints. | ||
* | ||
* @var array Array of regex patterns for forbidden classes. | ||
*/ | ||
public $forbidden_classes = array(); | ||
|
||
/** | ||
* Holds regular expressions for classes whose usage is forbidden. | ||
* Note that declaring classes with names matching these regular expressions is still permitted. | ||
* Useful for enforcing security or architectural constraints. | ||
* | ||
* @var array Array of regex patterns for forbidden functions. | ||
*/ | ||
public $forbidden_functions = array(); | ||
|
||
/** | ||
* Registers the tokens that this sniff wants to listen for. | ||
* | ||
* @return array | ||
*/ | ||
public function register() { | ||
$this->on_register_event(); | ||
|
||
return array( T_STRING ); | ||
} | ||
|
||
/** | ||
* Handles processing tokens. | ||
* | ||
* @param File $phpcsFile The file being scanned. | ||
* @param int $stackPtr The position of the current token | ||
* in the stack passed in $tokens. | ||
*/ | ||
public function process( File $phpcsFile, $stackPtr ) { | ||
$this->process_string_token( $phpcsFile, $stackPtr ); | ||
} | ||
|
||
/** | ||
* Detects whether a given string token represents a function call or class usage. | ||
* It then delegates further processing based on the type of usage detected. | ||
* | ||
* @param File $phpcs_file The file being scanned. | ||
* @param int $stack_pointer The position of the current token in the token stack. | ||
*/ | ||
private function process_string_token( File $phpcs_file, $stack_pointer ) { | ||
if ( empty( $this->forbidden_functions ) && empty( $this->forbidden_classes ) ) { | ||
// Nothing to process. | ||
return; | ||
} | ||
|
||
$tokens = $phpcs_file->getTokens(); | ||
|
||
$next_token = $phpcs_file->findPrevious( T_WHITESPACE, ( $stack_pointer + 1 ), null, true, null, true ); | ||
if ( false !== $next_token && ( T_DOUBLE_COLON === $tokens[ $next_token ]['code'] ) ) { | ||
// Static class method or a class constant. | ||
$this->check_class_usage( $phpcs_file, $stack_pointer ); | ||
|
||
return; | ||
} | ||
|
||
$previous_token = $phpcs_file->findPrevious( T_WHITESPACE, $stack_pointer - 1, null, true, null, true ); | ||
if ( false !== $previous_token && ( T_NEW === $tokens[ $previous_token ]['code'] ) ) { | ||
// Static method or a constant usage. | ||
$this->check_class_usage( $phpcs_file, $stack_pointer ); | ||
|
||
return; | ||
} | ||
|
||
if ( false !== $next_token && ( T_OPEN_PARENTHESIS === $tokens[ $next_token ]['code'] ) ) { | ||
// Function. | ||
$this->check_function_usage( $phpcs_file, $stack_pointer ); | ||
} | ||
} | ||
|
||
/** | ||
* Checks for forbidden class usage. | ||
* | ||
* @param File $phpcs_file File being scanned. | ||
* @param int $stack_pointer Position of the text token in the token stack. | ||
*/ | ||
private function check_class_usage( File $phpcs_file, $stack_pointer ) { | ||
if ( empty( $this->forbidden_classes ) ) { | ||
// Nothing to process. | ||
return; | ||
} | ||
|
||
if ( $this->check_if_token_guarded( $phpcs_file, $stack_pointer ) ) { | ||
// The token is guarded. Nothing to process. | ||
return; | ||
} | ||
|
||
$tokens = $phpcs_file->getTokens(); | ||
$class_name = $tokens[ $stack_pointer ]['content']; | ||
|
||
foreach ( $this->forbidden_classes as $forbidden_class ) { | ||
$regexp = sprintf( '/^%s$/', $forbidden_class ); | ||
if ( 1 !== preg_match( $regexp, $class_name ) ) { | ||
// The class has a valid name; bypassing further checks. | ||
return; | ||
} | ||
} | ||
|
||
$error_message = 'It\'s not allowed to use the "' . $class_name . '" class as its name matches the forbidden pattern: "' . $regexp . '".'; | ||
$phpcs_file->addError( $error_message, $stack_pointer, 'ForbiddenClassUsage' ); | ||
} | ||
|
||
/** | ||
* Checks for forbidden function usage. | ||
* | ||
* @param File $phpcs_file File being scanned. | ||
* @param int $stack_pointer Position of the text token in the token stack. | ||
*/ | ||
private function check_function_usage( File $phpcs_file, $stack_pointer ) { | ||
if ( empty( $this->forbidden_functions ) ) { | ||
// Nothing to process. | ||
return; | ||
} | ||
|
||
if ( $this->check_if_token_guarded( $phpcs_file, $stack_pointer ) ) { | ||
// The token is guarded. Nothing to process. | ||
return; | ||
} | ||
|
||
$tokens = $phpcs_file->getTokens(); | ||
$function_name = $tokens[ $stack_pointer ]['content']; | ||
|
||
foreach ( $this->forbidden_functions as $disallowed_function_call ) { | ||
$regexp = sprintf( '/^%s$/', $disallowed_function_call ); | ||
if ( 1 !== preg_match( $regexp, $function_name ) ) { | ||
// The function has a valid name; bypassing further checks. | ||
return; | ||
} | ||
} | ||
|
||
$error_message = 'It\'s not allowed to call the "' . $function_name . '()" function as its name matches the forbidden pattern: "' . $regexp . '".'; | ||
$phpcs_file->addError( $error_message, $stack_pointer, 'ForbiddenFunctionCall' ); | ||
} | ||
|
||
/** | ||
* This function looks for a specific if condition that wraps the token | ||
* to determine if it's protected from execution outside of Gutenberg. | ||
* | ||
* Example of a guarded function (works similarly for classes): | ||
* if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) { | ||
* // gutenberg_prefixed_i_am_allowed_to_be_called_since_i_am_guarded_function(); | ||
* } | ||
* | ||
* @param File $phpcs_file File being scanned. | ||
* @param int $stack_pointer Position of the text token in the token stack. | ||
* | ||
* @return bool Returns true if the token is guarded. | ||
*/ | ||
private function check_if_token_guarded( File $phpcs_file, $stack_pointer ) { | ||
if ( false === $phpcs_file->hasCondition( $stack_pointer, T_IF ) ) { | ||
// No wrapping IF tokens. Stop processing. | ||
return false; | ||
} | ||
|
||
$tokens = $phpcs_file->getTokens(); | ||
|
||
// It has to start from the matched condition closest to the passed token. | ||
$conditions = array_reverse( $tokens[ $stack_pointer ]['conditions'], true ); | ||
|
||
// Matches the "if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN" string. | ||
$regexp = '/if\s*\(\s*defined\(\s*(\'|")IS_GUTENBERG_PLUGIN(\'|")\s*\)\s*&&\s*IS_GUTENBERG_PLUGIN/'; | ||
|
||
foreach ( $conditions as $wrapping_if_token => $condition ) { | ||
if ( T_IF !== $condition ) { | ||
continue; | ||
} | ||
|
||
$end_of_wrapping_if_token = $phpcs_file->findEndOfStatement( $wrapping_if_token ); | ||
$content = $phpcs_file->getTokensAsString( $wrapping_if_token, $end_of_wrapping_if_token - $wrapping_if_token ); | ||
|
||
if ( 1 === preg_match( $regexp, $content ) ) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* The purpose of this method is to run callbacks | ||
* after the class properties have been set. | ||
*/ | ||
private function on_register_event() { | ||
$this->forbidden_classes = self::sanitize( $this->forbidden_classes ); | ||
$this->forbidden_functions = self::sanitize( $this->forbidden_functions ); | ||
} | ||
|
||
/** | ||
* Sanitize an array of values by trimming each element and removing empty elements. | ||
* | ||
* @param array $values The values being sanitized. | ||
* | ||
* @return array | ||
*/ | ||
private static function sanitize( $values ) { | ||
$values = array_map( 'trim', $values ); | ||
|
||
return array_filter( $values ); | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if there would ever be a reason to make this check configurable? Checking for
IS_GUTENBERG_PLUGIN
makes sense in block_library or any other PHP files that may be auto-generated in core from the npm packages. I guess this kind of logic wouldn't be needed at all if we were using the sniff in plugin-specific PHP files.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The sniff is currently only checking the
/packages/block-library
folder: https://github.com/WordPress/gutenberg/pull/57738/files#diff-05ae9cddcaec1e845771a7db224961439f83ef5939ec67d3a48744cb34d7e58bR157So, it doesn't check files in other folders.
I hope I've understood your question correctly.