From 3bb353d412707c566977acd59dd60891a1916899 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 23 Jul 2026 14:30:45 +0300 Subject: [PATCH] Add FunctionParameterAssignment sniff Warns when a non-by-reference function/method parameter's value is reassigned within the function body, since callers reading the parameter later would see the last-assigned value instead of the originally passed one. Closes #104 --- .../FunctionParameterAssignmentSniff.php | 132 ++++++++++++++++++ .../FunctionParameterAssignmentUnitTest.inc | 52 +++++++ .../FunctionParameterAssignmentUnitTest.php | 67 +++++++++ 3 files changed, 251 insertions(+) create mode 100644 CodingStandard/Sniffs/CodeAnalysis/FunctionParameterAssignmentSniff.php create mode 100644 CodingStandard/Tests/CodeAnalysis/FunctionParameterAssignmentUnitTest.inc create mode 100644 CodingStandard/Tests/CodeAnalysis/FunctionParameterAssignmentUnitTest.php diff --git a/CodingStandard/Sniffs/CodeAnalysis/FunctionParameterAssignmentSniff.php b/CodingStandard/Sniffs/CodeAnalysis/FunctionParameterAssignmentSniff.php new file mode 100644 index 0000000..7598212 --- /dev/null +++ b/CodingStandard/Sniffs/CodeAnalysis/FunctionParameterAssignmentSniff.php @@ -0,0 +1,132 @@ + + * @license https://github.com/aik099/CodingStandard/blob/master/LICENSE BSD 3-Clause + * @link https://github.com/aik099/CodingStandard + */ + +namespace CodingStandard\Sniffs\CodeAnalysis; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; + +/** + * CodingStandard_Sniffs_CodeAnalysis_FunctionParameterAssignmentSniff. + * + * Checks that function/method parameters, that aren't passed by reference, don't have their value + * overwritten within the function body. Code that reads the parameter later expects to see the + * original value and not some other value, that was assigned to it during method execution. + * + * Correct: + * function functionName($param1, &$param2) + * { + * $param2 = 'new value'; + * } + * + * Wrong: + * function functionName($param1, &$param2) + * { + * $param1 = 'new value' . $param1; + * } + * + * @category PHP + * @package PHP_CodeSniffer + * @author Alexander Obuhovich + * @license https://github.com/aik099/CodingStandard/blob/master/LICENSE BSD 3-Clause + * @link https://github.com/aik099/CodingStandard + */ +class FunctionParameterAssignmentSniff implements Sniff +{ + + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return integer[] + */ + public function register() + { + return array(T_FUNCTION); + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token in the + * stack passed in $tokens. + * + * @return void + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + if (isset($tokens[$stackPtr]['scope_opener'], $tokens[$stackPtr]['scope_closer']) === false) { + // Function declaration without a body (e.g. abstract method or interface method). + return; + } + + $parameters = $phpcsFile->getMethodParameters($stackPtr); + + if (empty($parameters) === true) { + return; + } + + $scopeOpener = $tokens[$stackPtr]['scope_opener']; + $scopeCloser = $tokens[$stackPtr]['scope_closer']; + + foreach ($parameters as $parameter) { + if ($parameter['pass_by_reference'] === true) { + continue; + } + + $this->checkParameterUsage($phpcsFile, $parameter['name'], $scopeOpener, $scopeCloser); + } + }//end process() + + + /** + * Checks, that given parameter isn't assigned a new value within the given scope. + * + * @param File $phpcsFile The file being scanned. + * @param string $variableName Name of the variable (including "$") to look for. + * @param int $scopeOpener Position of the function body scope opener. + * @param int $scopeCloser Position of the function body scope closer. + * + * @return void + */ + protected function checkParameterUsage(File $phpcsFile, $variableName, $scopeOpener, $scopeCloser) + { + $tokens = $phpcsFile->getTokens(); + + $searchPtr = $scopeOpener; + + do { + $variablePtr = $phpcsFile->findNext(T_VARIABLE, ($searchPtr + 1), $scopeCloser, false, $variableName); + + if ($variablePtr === false) { + break; + } + + $searchPtr = $variablePtr; + + $assignmentPtr = $phpcsFile->findNext(Tokens::$emptyTokens, ($variablePtr + 1), null, true); + + if ($assignmentPtr !== false + && isset(Tokens::$assignmentTokens[$tokens[$assignmentPtr]['code']]) === true + ) { + $warning = 'Assignment to "%s" function parameter is not allowed'; + $phpcsFile->addWarning($warning, $variablePtr, 'NotAllowed', array($variableName)); + } + } while (true); + }//end checkParameterUsage() +}//end class diff --git a/CodingStandard/Tests/CodeAnalysis/FunctionParameterAssignmentUnitTest.inc b/CodingStandard/Tests/CodeAnalysis/FunctionParameterAssignmentUnitTest.inc new file mode 100644 index 0000000..3b783d3 --- /dev/null +++ b/CodingStandard/Tests/CodeAnalysis/FunctionParameterAssignmentUnitTest.inc @@ -0,0 +1,52 @@ + + * @license https://github.com/aik099/CodingStandard/blob/master/LICENSE BSD 3-Clause + * @link https://github.com/aik099/CodingStandard + */ + +namespace CodingStandard\Tests\CodeAnalysis; + +use TestSuite\AbstractSniffUnitTest; + +/** + * Unit test class for the FunctionParameterAssignment sniff. + * + * @category PHP + * @package PHP_CodeSniffer + * @author Alexander Obuhovich + * @license https://github.com/aik099/CodingStandard/blob/master/LICENSE BSD 3-Clause + * @link https://github.com/aik099/CodingStandard + */ +class FunctionParameterAssignmentUnitTest extends AbstractSniffUnitTest +{ + + + /** + * Returns the lines where errors should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of errors that should occur on that line. + * + * @param string $testFile Name of the file with test data. + * + * @return array(int => int) + */ + public function getErrorList($testFile) + { + return array(); + }//end getErrorList() + + /** + * Returns the lines where warnings should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of warnings that should occur on that line. + * + * @param string $testFile Name of the file with test data. + * + * @return array(int => int) + */ + public function getWarningList($testFile) + { + return array( + 5 => 1, + 19 => 1, + 43 => 1, + 44 => 1, + 50 => 1, + 51 => 1, + ); + }//end getWarningList() +}//end class