├── bin ├── php7cc └── php7cc.php ├── .gitignore ├── src ├── Helper │ ├── OSDetector.php │ ├── Path │ │ ├── PathHelperInterface.php │ │ ├── UnixPathHelper.php │ │ ├── WindowsPathHelper.php │ │ └── PathHelperFactory.php │ └── RegExp │ │ ├── RegExpParser.php │ │ └── RegExp.php ├── CLIOutputInterface.php ├── NodeVisitor │ ├── ResolverInterface.php │ ├── NewAssignmentByReferenceVisitor.php │ ├── VisitorInterface.php │ ├── HexadecimalNumberStringVisitor.php │ ├── GlobalNewFunctionVisitor.php │ ├── InvalidOctalLiteralVisitor.php │ ├── NamespacedNewFunctionVisitor.php │ ├── MultipleSwitchDefaultsVisitor.php │ ├── DivisionModuloByZeroVisitor.php │ ├── SessionSetSaveHandlerVisitor.php │ ├── DuplicateFunctionParameterVisitor.php │ ├── IndirectVariableOrMethodAccessVisitor.php │ ├── ListVisitor.php │ ├── YieldExpressionVisitor.php │ ├── HTTPRawPostDataVisitor.php │ ├── MktimeVisitor.php │ ├── Resolver.php │ ├── EscapedUnicodeCodepointVisitor.php │ ├── AbstractVisitor.php │ ├── GlobalVariableVariableVisitor.php │ ├── YieldInExpressionContextVisitor.php │ ├── BitwiseShiftVisitor.php │ ├── NewClassVisitor.php │ ├── AbstractNewFunctionVisitor.php │ ├── PasswordHashSaltVisitor.php │ ├── PregReplaceEvalVisitor.php │ ├── PHP4ConstructorVisitor.php │ ├── ArrayOrObjectValueAssignmentByReferenceVisitor.php │ ├── SetcookieEmptyNameVisitor.php │ ├── ReservedClassNameVisitor.php │ ├── FuncGetArgsVisitor.php │ ├── RemovedFunctionCallVisitor.php │ └── ForeachVisitor.php ├── Error │ └── CheckError.php ├── ResultPrinterInterface.php ├── Infrastructure │ ├── CLIOutputBridge.php │ ├── Application.php │ └── PHP7CCCommand.php ├── NodeStatementsRemover.php ├── CompatibilityViolation │ ├── ContextInterface.php │ ├── StringContext.php │ ├── CheckMetadata.php │ ├── AbstractContext.php │ ├── FileContext.php │ └── Message.php ├── NodeAnalyzer │ └── FunctionAnalyzer.php ├── NodeTraverser │ └── Traverser.php ├── Iterator │ ├── ExcludedPathFilteringRecursiveIterator.php │ ├── AbstractRecursiveFilterIterator.php │ ├── ExtensionFilteringRecursiveIterator.php │ └── FileDirectoryListRecursiveIterator.php ├── Lexer │ └── ExtendedLexer.php ├── AbstractBaseMessage.php ├── ExcludedPathCanonicalizer.php ├── ContextChecker.php ├── PathChecker.php ├── PathCheckExecutor.php ├── PathTraversableFactory.php ├── PathCheckSettings.php ├── CLIResultPrinter.php └── Token │ └── TokenCollection.php ├── test ├── code │ ├── Iterator │ │ ├── realpath.php │ │ ├── FileDirectoryListRecursiveIteratorTest.php │ │ ├── ExtensionFilteringRecursiveIteratorTest.php │ │ ├── ExcludedPathFilteringRecursiveIteratorTest.php │ │ └── AbstractFilteringIteratorTest.php │ ├── NodeVisitor │ │ ├── PHP4ConstructorVisitorTest.php │ │ ├── ResolverTest.php │ │ └── BitwiseShiftVisitorTest.php │ ├── Helper │ │ ├── Path │ │ │ ├── UnixPathHelperTest.php │ │ │ ├── AbstractPathHelperTest.php │ │ │ └── WindowsPathHelperTest.php │ │ └── RegExp │ │ │ ├── RegExpParserTest.php │ │ │ └── RegExpTest.php │ ├── CompatibilityViolation │ │ └── FileContextTest.php │ ├── NodeAnalyzer │ │ └── FunctionAnalyzerTest.php │ ├── ExcludedPathCanonicalizerTest.php │ └── ContextCheckerTest.php └── resource │ ├── list │ ├── emptyList.test │ └── stringUnpacking.test │ ├── indirectVariableOrMethodAccess │ ├── indirectVariableAccess.test │ ├── indirectMethodCall.test │ └── indirectPropertyAccess.test │ ├── globalVariableVariable │ └── globalVariableVariable.test │ ├── invalidOctalLiteral │ └── invalidOctalLiteral.test │ ├── newAssignmentByReference │ └── newAssignmentByReference.test │ ├── reservedClassName │ ├── reservedUse.test │ ├── reservedClassName.test │ ├── reservedTraitName.test │ ├── reservedClassAliasTest.test │ ├── reservedInterfaceName.test │ ├── futureReservedUse.test │ ├── futureReservedTraitName.test │ ├── futureReservedClassName.test │ ├── futureReservedInterfaceName.test │ └── futureReservedClassAlias.test │ ├── yieldInExpressionContext │ └── yieldInExpressionContext.test │ ├── foreach │ ├── internalArrayPointerAccessInByValueForeach.test │ ├── addingToArrayInByReferenceForeach.test │ ├── arrayModificationWithFunctionInByReferenceForeach.test │ └── nestedByReferenceForeach.test │ ├── escapedUnicodeCodepoint │ ├── escapedUnicodeCodepointInHeredoc.test │ └── escapedUnicodeCodepointInString.test │ ├── removedFunctionCall │ └── removedFunctionCall.test │ ├── bitwiseShift │ ├── bitwiseShiftByNegativeDecimalNumber.test │ ├── bitwiseShiftByNegativeOctalNumber.test │ ├── bitwiseShiftByNegativeBinaryNumber.test │ ├── bitwiseShiftByNegativeHexadecimalNumber.test │ ├── bitwiseShiftLargerThanIntWidthDecimal.test │ ├── bitwiseShiftLargerThanIntWidthOctal.test │ ├── bitwiseShiftLargerThanIntWidthHexadecimal.test │ └── bitwiseShiftLargerThanIntWidthBinary.test │ ├── passwordHashSalt │ └── passwordHashSalt.test │ ├── sessionSetSaveHandler │ └── sessionSetSaveHandler.test │ ├── divisionModuloByZero │ ├── moduloByZero.test │ └── divisionByZero.test │ ├── arrayAssignByRef │ ├── arrayAssignmentByReference.test │ └── objectAssignmentByReference.test │ ├── yieldExpression │ └── yieldExpression.test │ ├── hexadecimalStringNumber │ └── hexadecimalStringNumber.test │ ├── pregReplaceEval │ └── pregReplaceEval.test │ ├── funcGetArgs │ └── funcGetArgsPowerOperator.test │ ├── duplicateFunctionParameter │ └── duplicateFunctionParameterDeclaration.test │ ├── mktime │ └── mktime.test │ ├── newClass │ ├── newClass.test │ └── newClassWithTrait.test │ ├── multipleSwitchDefaults │ └── multipleSwitchDefaults.test │ ├── newFunction │ └── newFunction.test │ ├── httpRawPostData │ └── httpRawPostData.test │ ├── setcookieEmptyName │ └── setcookieEmptyName.test │ └── php4Constructor │ └── php4Constructor.test ├── .php_cs ├── CHANGELOG.md ├── composer.json ├── phpunit.xml.dist ├── box.json ├── LICENSE ├── .travis.yml ├── CONTRIBUTING.md └── README.md /bin/php7cc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | bar; 5 | ----- 6 | Complex variable without curly braces in global keyword 7 | ----- 8 | bar}; 10 | ----- 11 | 12 | ----- 13 | bar }; 15 | ----- 16 | -------------------------------------------------------------------------------- /test/resource/invalidOctalLiteral/invalidOctalLiteral.test: -------------------------------------------------------------------------------- 1 | Invalid octal literal 2 | ----- 3 | in(__DIR__) 5 | ->exclude(array( 6 | 'vendor', 7 | 'test/resource', 8 | )); 9 | 10 | $fixers = array( 11 | '-phpdoc_to_comment', 12 | '-concat_without_spaces', 13 | 'concat_with_spaces', 14 | 'newline_after_open_tag', 15 | ); 16 | 17 | return Symfony\CS\Config\Config::create() 18 | ->finder($finder) 19 | ->fixers($fixers) 20 | ; -------------------------------------------------------------------------------- /test/resource/bitwiseShift/bitwiseShiftByNegativeDecimalNumber.test: -------------------------------------------------------------------------------- 1 | Bitwise shift by a negative decimal number 2 | ----- 3 | > -2; 4 | ----- 5 | Bitwise shift by a negative number 6 | ----- 7 | > 0; 12 | ----- 13 | 14 | ----- 15 | > 10; 20 | ----- 21 | 22 | ----- 23 | > -02; 4 | ----- 5 | Bitwise shift by a negative number 6 | ----- 7 | > 00; 12 | ----- 13 | 14 | ----- 15 | > 012; 20 | ----- 21 | 22 | ----- 23 | getRawText(); 12 | if ($this->getLine()) { 13 | $text = sprintf('Line %d. %s', $this->getLine(), $text); 14 | } 15 | 16 | return $text . '. Processing aborted.'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/resource/bitwiseShift/bitwiseShiftByNegativeBinaryNumber.test: -------------------------------------------------------------------------------- 1 | Bitwise shift by a negative binary number 2 | ----- 3 | > -0b10; 4 | ----- 5 | Bitwise shift by a negative number 6 | ----- 7 | > 0b0; 12 | ----- 13 | 14 | ----- 15 | > 0b1010; 20 | ----- 21 | 22 | ----- 23 | > -0x2; 4 | ----- 5 | Bitwise shift by a negative number 6 | ----- 7 | > 0x0; 12 | ----- 13 | 14 | ----- 15 | > 0xa; 20 | ----- 21 | 22 | ----- 23 | isAbsolute($path); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/resource/escapedUnicodeCodepoint/escapedUnicodeCodepointInString.test: -------------------------------------------------------------------------------- 1 | Escaped unicode codepoints in double quoted string 2 | ----- 3 | > 32; 4 | ----- 5 | Bitwise shift by 32 bits 6 | ----- 7 | > 64; 12 | ----- 13 | Bitwise shift by 64 bits 14 | ----- 15 | > 16; 20 | ----- 21 | 22 | ----- 23 | > 040; 4 | ----- 5 | Bitwise shift by 32 bits 6 | ----- 7 | > 0100; 12 | ----- 13 | Bitwise shift by 64 bits 14 | ----- 15 | > 020; 20 | ----- 21 | 22 | ----- 23 | &$b) { 13 | $a[] = $c; 14 | } 15 | 16 | ----- 17 | Possible adding to array on the last iteration of a by-reference foreach loop 18 | ----- 19 | 's')); 5 | ----- 6 | Deprecated option "salt" passed to password_hash function 7 | ----- 8 | 'bar')); 10 | ----- 11 | 12 | ----- 13 | > 0x20; 4 | ----- 5 | Bitwise shift by 32 bits 6 | ----- 7 | > 0x40; 12 | ----- 13 | Bitwise shift by 64 bits 14 | ----- 15 | > 0x10; 20 | ----- 21 | 22 | ----- 23 | run(); 22 | -------------------------------------------------------------------------------- /test/resource/indirectVariableOrMethodAccess/indirectMethodCall.test: -------------------------------------------------------------------------------- 1 | Indirect method call 2 | ----- 3 | $bar['baz'](); 5 | ----- 6 | Indirect variable, property or method access 7 | ----- 8 | {$bar['baz']}(); 15 | ----- 16 | 17 | ----- 18 | { $bar['baz'] }(); 25 | ----- 26 | 27 | ----- 28 | > 0b100000; 4 | ----- 5 | Bitwise shift by 32 bits 6 | ----- 7 | > 0b1000000; 12 | ----- 13 | Bitwise shift by 64 bits 14 | ----- 15 | > 0b10000; 20 | ----- 21 | 22 | ----- 23 | &$b) { 12 | array_push($a, $c); 13 | } 14 | ----- 15 | Possible array modification using internal function in a by-reference foreach loop 16 | ----- 17 | isAbsolute($path) && preg_match('#^[a-zA-Z]\\:(?!\\\\)#', $path) === 0); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/resource/foreach/nestedByReferenceForeach.test: -------------------------------------------------------------------------------- 1 | Nested by-reference foreach loops 2 | ----- 3 | expr instanceof Node\Expr\New_) { 15 | $this->addContextMessage( 16 | 'Result of new is assigned by reference', 17 | $node 18 | ); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/resource/hexadecimalStringNumber/hexadecimalStringNumber.test: -------------------------------------------------------------------------------- 1 | Hexadecimal number in string 2 | ----- 3 | b = &$a->bar; 5 | ----- 6 | Possible object property creation during by-reference assignment 7 | ----- 8 | {'foo'} = &$a->bar; 10 | ----- 11 | Possible object property creation during by-reference assignment 12 | ----- 13 | $b = &$a->$c; 15 | ----- 16 | Possible object property creation during by-reference assignment 17 | ----- 18 | b = $a->bar; 20 | ----- 21 | 22 | ----- 23 | b = &$c; 25 | ----- 26 | 27 | ----- 28 | $b = &$c; 30 | ----- 31 | 32 | ----- 33 | b = $c; 35 | ----- 36 | -------------------------------------------------------------------------------- /test/resource/reservedClassName/futureReservedInterfaceName.test: -------------------------------------------------------------------------------- 1 | Reserved for future use interface name usage 2 | ----- 3 | osDetector = $osDetector; 20 | } 21 | 22 | /** 23 | * @return PathHelperInterface 24 | */ 25 | public function createPathHelper() 26 | { 27 | return $this->osDetector->isWindows() ? new WindowsPathHelper() : new UnixPathHelper(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/resource/reservedClassName/futureReservedClassAlias.test: -------------------------------------------------------------------------------- 1 | Reserved for future use class name usage as a class alias 2 | ----- 3 | value)) { 19 | $this->addContextMessage( 20 | 'String containing number in hexadecimal notation', 21 | $node 22 | ); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/NodeVisitor/GlobalNewFunctionVisitor.php: -------------------------------------------------------------------------------- 1 | namespacedName) && count($node->namespacedName->parts) == 1; 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function getMessageText($functionName) 24 | { 25 | return sprintf('Cannot redeclare global function "%s"', $functionName); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/resource/funcGetArgs/funcGetArgsPowerOperator.test: -------------------------------------------------------------------------------- 1 | func_get_args or func_get_arg calls after argument modification by power operator 2 | ----- 3 | =5.3.3", 13 | "symfony/console": "~2.3 || ~3.0", 14 | "symfony/finder": "~2.3 || ~3.0", 15 | "pimple/pimple": "~3.0", 16 | "nikic/php-parser": "~1.4" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "4.7.*", 20 | "mikey179/vfsStream": "~1.5", 21 | "fabpot/php-cs-fixer": "~1.10" 22 | }, 23 | "bin": [ 24 | "bin/php7cc" 25 | ], 26 | "extra": { 27 | "branch-alias": { 28 | "dev-master": "1.2-dev" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | ./test/ 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/NodeVisitor/InvalidOctalLiteralVisitor.php: -------------------------------------------------------------------------------- 1 | getAttribute('originalValue', ''); 19 | 20 | if (preg_match('/^0[0-7]*[89]+/', $originalNumberValue)) { 21 | $this->addContextMessage( 22 | sprintf('Invalid octal literal %s', $originalNumberValue), 23 | $node 24 | ); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/resource/newClass/newClass.test: -------------------------------------------------------------------------------- 1 | New class added 2 | ----- 3 | namespacedName) && count($node->namespacedName->parts) > 1; 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function getMessageText($functionName) 24 | { 25 | return sprintf( 26 | 'Your namespaced function "%s" could replace the new global function added in PHP 7', 27 | $functionName 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Infrastructure/CLIOutputBridge.php: -------------------------------------------------------------------------------- 1 | output = $output; 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function write($string) 27 | { 28 | $this->output->write($string); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function writeln($string) 35 | { 36 | $this->output->writeln($string); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/code/NodeVisitor/PHP4ConstructorVisitorTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('PhpParser\Node\Stmt\Class_') 23 | ->disableOriginalConstructor() 24 | ->getMock(); 25 | 26 | // triggers a notice that it shouldn't 27 | $visitor->enterNode($node); 28 | 29 | $this->assertTrue(true); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "chmod": "0755", 3 | "directories": [ 4 | "src", 5 | "bin" 6 | ], 7 | "finder": [ 8 | { 9 | "exclude": [ 10 | "mikey179", 11 | "phpunit", 12 | "phpspec", 13 | "phpdocumentor", 14 | "sebastian", 15 | "phpseclib", 16 | "fabpot", 17 | "phine", 18 | "justinrainbow", 19 | "herrera-io", 20 | "seld", 21 | "doctrine", 22 | "tedivm", 23 | "kherge" 24 | ], 25 | "in": [ 26 | "vendor" 27 | ] 28 | } 29 | ], 30 | "files": [ 31 | "LICENSE", 32 | "README.md", 33 | "vendor/phpseclib/phpseclib/phpseclib/Crypt/Random.php", 34 | "vendor/herrera-io/json/src/lib/json_version.php", 35 | "vendor/herrera-io/phar-update/src/lib/constants.php" 36 | ], 37 | "main": "bin/php7cc.php", 38 | "output": "php7cc.phar", 39 | "stub": true 40 | } -------------------------------------------------------------------------------- /src/NodeVisitor/MultipleSwitchDefaultsVisitor.php: -------------------------------------------------------------------------------- 1 | cases as $case) { 20 | if ($case->cond === null) { 21 | ++$defaultCaseCount; 22 | } 23 | } 24 | 25 | if ($defaultCaseCount > 1) { 26 | $this->addContextMessage( 27 | 'Multiple default cases defined for the switch statement', 28 | $node 29 | ); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/resource/multipleSwitchDefaults/multipleSwitchDefaults.test: -------------------------------------------------------------------------------- 1 | Defining multiple default cases for a switch statement 2 | ----- 3 | stmts = array(); 26 | } 27 | 28 | if ($node instanceof Node\Stmt\Switch_) { 29 | $node->cases = array(); 30 | } 31 | 32 | $resultNodes[] = $node; 33 | } 34 | 35 | return $resultNodes; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/resource/newFunction/newFunction.test: -------------------------------------------------------------------------------- 1 | New function added 2 | ----- 3 | name instanceof Node\Name; 18 | if (!$isFunctionCallByStaticName) { 19 | return $isFunctionCallByStaticName; 20 | } 21 | 22 | $calledFunctionName = strtolower($node->name->toString()); 23 | 24 | return is_array($checkedFunctionName) 25 | ? isset($checkedFunctionName[$calledFunctionName]) 26 | : $calledFunctionName === $checkedFunctionName; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/NodeTraverser/Traverser.php: -------------------------------------------------------------------------------- 1 | visitors as $visitor) { 21 | if ($visitor instanceof VisitorInterface) { 22 | $visitor->initializeContext($context); 23 | $visitor->setTokenCollection($tokenCollection); 24 | } 25 | } 26 | } 27 | 28 | return parent::traverse($nodes); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/CompatibilityViolation/StringContext.php: -------------------------------------------------------------------------------- 1 | checkedCode = $checkedCode; 24 | $this->checkedResourceName = $checkedResourceName; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function getCheckedResourceName() 31 | { 32 | return $this->checkedResourceName; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getCheckedCode() 39 | { 40 | return $this->checkedCode; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/NodeVisitor/DivisionModuloByZeroVisitor.php: -------------------------------------------------------------------------------- 1 | right : $node->expr; 22 | if ($divisor instanceof Node\Scalar\LNumber && $divisor->value == 0) { 23 | $this->addContextMessage( 24 | sprintf('%s by zero', $isDivision ? 'Division' : 'Modulo'), 25 | $node 26 | ); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/code/Helper/Path/UnixPathHelperTest.php: -------------------------------------------------------------------------------- 1 | isAbsolutePathProvider(); 29 | foreach ($absolutePathData as $i => $data) { 30 | $absolutePathData[$i][1] = !$absolutePathData[$i][1]; 31 | } 32 | 33 | return $absolutePathData; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/resource/httpRawPostData/httpRawPostData.test: -------------------------------------------------------------------------------- 1 | $HTTP_RAW_POST_DATA variable access 2 | ----- 3 | excludedPaths = array_flip($excludedPaths); 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function accept() 26 | { 27 | return !isset($this->excludedPaths[realpath($this->key())]); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function getChildren() 34 | { 35 | return new static($this->getInnerIterator()->getChildren(), array_flip($this->excludedPaths)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Helper/RegExp/RegExpParser.php: -------------------------------------------------------------------------------- 1 | rawText = $text; 29 | $this->line = $line; 30 | $this->text = $this->generateText(); 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getRawText() 37 | { 38 | return $this->rawText; 39 | } 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getText() 45 | { 46 | return $this->text; 47 | } 48 | 49 | /** 50 | * @return int 51 | */ 52 | public function getLine() 53 | { 54 | return $this->line; 55 | } 56 | 57 | abstract protected function generateText(); 58 | } 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 sstalle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/NodeVisitor/SessionSetSaveHandlerVisitor.php: -------------------------------------------------------------------------------- 1 | functionAnalyzer = $functionAnalyzer; 24 | } 25 | 26 | public function enterNode(Node $node) 27 | { 28 | if ($this->functionAnalyzer->isFunctionCallByStaticName($node, array('session_set_save_handler' => true))) { 29 | $this->addContextMessage( 30 | 'Check that callbacks that are passed to "session_set_save_handler" ' 31 | . 'and return false or -1 (if any) operate correctly', 32 | $node 33 | ); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Infrastructure/Application.php: -------------------------------------------------------------------------------- 1 | setArguments(); 26 | 27 | return $inputDefinition; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function getCommandName(InputInterface $input) 34 | { 35 | return PHP7CCCommand::COMMAND_NAME; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | protected function getDefaultCommands() 42 | { 43 | $defaultCommands = parent::getDefaultCommands(); 44 | $defaultCommands[] = new PHP7CCCommand(); 45 | 46 | return $defaultCommands; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/NodeVisitor/DuplicateFunctionParameterVisitor.php: -------------------------------------------------------------------------------- 1 | getParams() as $parameter) { 23 | $currentParameterName = $parameter->name; 24 | if (!isset($parametersNames[$currentParameterName])) { 25 | $parametersNames[$currentParameterName] = false; 26 | } elseif (!$parametersNames[$currentParameterName]) { 27 | $this->addContextMessage( 28 | sprintf('Duplicate function parameter name "%s"', $currentParameterName), 29 | $node 30 | ); 31 | 32 | $parametersNames[$currentParameterName] = true; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/CompatibilityViolation/CheckMetadata.php: -------------------------------------------------------------------------------- 1 | startTime = microtime(true); 25 | } 26 | 27 | public function endCheck() 28 | { 29 | $this->endTime = microtime(true); 30 | } 31 | 32 | /** 33 | * @return float In seconds 34 | */ 35 | public function getElapsedTime() 36 | { 37 | $endTime = $this->endTime; 38 | if ($endTime === null) { 39 | $endTime = microtime(true); 40 | } 41 | 42 | return $endTime - $this->startTime; 43 | } 44 | 45 | /** 46 | * @return int 47 | */ 48 | public function getCheckedFileCount() 49 | { 50 | return $this->checkedFileCount; 51 | } 52 | 53 | public function incrementCheckedFileCount() 54 | { 55 | ++$this->checkedFileCount; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/NodeVisitor/IndirectVariableOrMethodAccessVisitor.php: -------------------------------------------------------------------------------- 1 | name instanceof Node\Expr\ArrayDimFetch 19 | ) { 20 | return; 21 | } 22 | 23 | $nodeName = $node->name; 24 | if ($this->tokenCollection->isTokenEqualToOrPrecededBy($nodeName->getAttribute('startTokenPos') - 1, '{') 25 | && $this->tokenCollection->isTokenEqualToOrFollowedBy($nodeName->getAttribute('endTokenPos') + 1, '}') 26 | ) { 27 | return; 28 | } 29 | 30 | $this->addContextMessage( 31 | 'Indirect variable, property or method access', 32 | $node 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Iterator/AbstractRecursiveFilterIterator.php: -------------------------------------------------------------------------------- 1 | getFlags(); 16 | if ($iteratorFlags & \RecursiveDirectoryIterator::CURRENT_AS_PATHNAME 17 | || $iteratorFlags & \RecursiveDirectoryIterator::CURRENT_AS_SELF 18 | ) { 19 | throw new \InvalidArgumentException( 20 | 'This iterator requires \RecursiveDirectoryIterator with CURRENT_AS_FILEINFO flag set' 21 | ); 22 | } 23 | 24 | if ($iteratorFlags & \RecursiveDirectoryIterator::KEY_AS_FILENAME) { 25 | throw new \InvalidArgumentException( 26 | 'This iterator requires \RecursiveDirectoryIterator with KEY_AS_PATHNAME flag set' 27 | ); 28 | } 29 | } 30 | 31 | parent::__construct($iterator); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/NodeVisitor/ListVisitor.php: -------------------------------------------------------------------------------- 1 | vars as $var) { 17 | if ($var !== null) { 18 | $hasNonNullVar = true; 19 | break; 20 | } 21 | } 22 | 23 | if (!$hasNonNullVar) { 24 | $this->addContextMessage( 25 | 'Empty list assignment', 26 | $node 27 | ); 28 | } 29 | } 30 | 31 | if ($node instanceof Node\Expr\Assign && $node->var instanceof Node\Expr\List_ 32 | && ($node->expr instanceof Node\Scalar\String_ || $node->expr instanceof Node\Expr\Cast\String_) 33 | ) { 34 | $this->addContextMessage( 35 | 'list unpacking string', 36 | $node 37 | ); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/NodeVisitor/YieldExpressionVisitor.php: -------------------------------------------------------------------------------- 1 | lowerPrecedenceExpressionClasses = array_flip($this->lowerPrecedenceExpressionClasses); 21 | } 22 | 23 | public function enterNode(Node $node) 24 | { 25 | if (!($node instanceof Node\Expr\Yield_ && $node->value && $node->value instanceof Node\Expr)) { 26 | return; 27 | } 28 | 29 | $valueClass = get_class($node->value); 30 | if (isset($this->lowerPrecedenceExpressionClasses[$valueClass])) { 31 | $this->addContextMessage( 32 | 'Yielding expression with precedence lower than "yield"', 33 | $node 34 | ); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/CompatibilityViolation/AbstractContext.php: -------------------------------------------------------------------------------- 1 | messages[] = $message; 25 | } 26 | 27 | /** 28 | * @return array|Message[] 29 | */ 30 | public function getMessages() 31 | { 32 | return $this->messages; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function addError(CheckError $error) 39 | { 40 | $this->errors[] = $error; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function getErrors() 47 | { 48 | return $this->errors; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function hasMessagesOrErrors() 55 | { 56 | return $this->messages || $this->errors; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/CompatibilityViolation/FileContext.php: -------------------------------------------------------------------------------- 1 | file = $file; 26 | $this->useRelativePaths = $useRelativePaths; 27 | } 28 | 29 | /** 30 | * @return SplFileInfo 31 | */ 32 | public function getFile() 33 | { 34 | return $this->file; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function getCheckedResourceName() 41 | { 42 | $file = $this->getFile(); 43 | 44 | return $this->useRelativePaths ? $file->getRelativePathname() : $file->getRealPath(); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getCheckedCode() 51 | { 52 | return $this->getFile()->getContents(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/NodeVisitor/HTTPRawPostDataVisitor.php: -------------------------------------------------------------------------------- 1 | name === static::HTTP_RAW_POST_DATA_VARIABLE_NAME; 17 | $isVariableAccessedThroughGlobals = $node instanceof Node\Expr\ArrayDimFetch 18 | && $node->var instanceof Node\Expr\Variable 19 | && $node->var->name == 'GLOBALS' 20 | && $node->dim instanceof Node\Scalar\String_ 21 | && $node->dim->value === static::HTTP_RAW_POST_DATA_VARIABLE_NAME; 22 | 23 | if ($isVariableAccessedByName || $isVariableAccessedThroughGlobals) { 24 | $this->addContextMessage( 25 | sprintf( 26 | 'Removed "%s" variable used', 27 | static::HTTP_RAW_POST_DATA_VARIABLE_NAME 28 | ), 29 | $node 30 | ); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/NodeVisitor/MktimeVisitor.php: -------------------------------------------------------------------------------- 1 | functionAnalyzer = $functionAnalyzer; 29 | $this->mktimeFamilyFunctions = array_flip($this->mktimeFamilyFunctions); 30 | } 31 | 32 | public function enterNode(Node $node) 33 | { 34 | if (!$this->functionAnalyzer->isFunctionCallByStaticName($node, $this->mktimeFamilyFunctions) 35 | || count($node->args) < 7 36 | ) { 37 | return; 38 | } 39 | 40 | $this->addContextMessage( 41 | sprintf('Removed argument $is_dst used for function "%s"', $node->name->__toString()), 42 | $node 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/CompatibilityViolation/Message.php: -------------------------------------------------------------------------------- 1 | level = $level; 34 | $this->nodes = $nodes; 35 | } 36 | 37 | /** 38 | * @return int 39 | */ 40 | public function getLevel() 41 | { 42 | return $this->level; 43 | } 44 | 45 | /** 46 | * @return Node[] 47 | */ 48 | public function getNodes() 49 | { 50 | return $this->nodes; 51 | } 52 | 53 | protected function generateText() 54 | { 55 | return sprintf('Line %d. %s', $this->getLine(), $this->getRawText()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/code/Helper/Path/AbstractPathHelperTest.php: -------------------------------------------------------------------------------- 1 | pathHelper = $this->createPathHelper(); 17 | } 18 | 19 | /** 20 | * @dataProvider isAbsolutePathProvider 21 | */ 22 | public function testIsAbsolutePath($path, $isAbsolute) 23 | { 24 | $this->assertSame($isAbsolute, $this->pathHelper->isAbsolute($path)); 25 | } 26 | 27 | /** 28 | * @dataProvider isDirectoryRelativePathProvider 29 | */ 30 | public function testIsDirectoryRelativePath($path, $isDirectoryRelative) 31 | { 32 | $this->assertSame($isDirectoryRelative, $this->pathHelper->isDirectoryRelative($path)); 33 | } 34 | 35 | /** 36 | * @return array 37 | */ 38 | abstract public function isAbsolutePathProvider(); 39 | 40 | /** 41 | * @return array 42 | */ 43 | abstract public function isDirectoryRelativePathProvider(); 44 | 45 | /** 46 | * @return PathHelperInterface 47 | */ 48 | abstract public function createPathHelper(); 49 | } 50 | -------------------------------------------------------------------------------- /test/code/Helper/Path/WindowsPathHelperTest.php: -------------------------------------------------------------------------------- 1 | addVisitor($visitor); 27 | } 28 | $this->level = $level; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function resolve() 35 | { 36 | $level = $this->level; 37 | 38 | return array_values(array_filter($this->visitors, function (VisitorInterface $visitor) use ($level) { 39 | return $visitor->getLevel() >= $level; 40 | })); 41 | } 42 | 43 | /** 44 | * @param int $level 45 | */ 46 | public function setLevel($level) 47 | { 48 | $this->level = $level; 49 | } 50 | 51 | /** 52 | * @param VisitorInterface $visitor 53 | */ 54 | protected function addVisitor(VisitorInterface $visitor) 55 | { 56 | $this->visitors[] = $visitor; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | cache: 6 | directories: 7 | - "$HOME/.composer/cache" 8 | 9 | php: 10 | - 5.3 11 | - 5.4 12 | - 5.5 13 | - 5.6 14 | - 7.0 15 | 16 | env: 17 | - COMPOSER_FLAGS="--prefer-lowest" 18 | - COMPOSER_FLAGS="" 19 | 20 | install: 21 | - composer update $COMPOSER_FLAGS --no-interaction 22 | 23 | script: 24 | - "./vendor/bin/phpunit" 25 | 26 | before_deploy: 27 | - "composer require kherge/box:~2.5" 28 | - "composer update --prefer-lowest" 29 | - "./vendor/bin/box build" 30 | 31 | deploy: 32 | provider: releases 33 | api_key: 34 | secure: k+qvKnFTICcFZzx868c7IfM2fneyiA2F5RdDmFmTAhAulxAVnxnNcJBul4iBdZeE5UATaqK/Jydkj66safJZNdBz8z3yILf3redNamOO3np0TM9vo8QCjE+PMEOnDL5sY+7DgfEW3sApClRnWwqK0+X6UjPypktF6PfFMLVxqeXklTOxHuwn9JYmlTWb3CQYLOJ/dyDxPGMelDhHR4A0WD71J/8+XjJIm3zppJMae33b5FE7XavOT2d2LyDtfVjMMXxWys4a8W+Q5KkT+TrejD2J+zluB++eqNBH5hU+/jVq14qflI8gfQ1B/uZxMj1YbBfOTNpG/9oPdDJ7Yk2XspfzxbmzUtVccZt2iEQxAmoD1HJ9F2StWSdksUa2GGIhKuYi8iRK4r1UwOp98/wFmAURsDp0jbkkWdys3Pv4Jp8zLwZvqX0jT/T6F0hIaOBrS9408R+QQz0kZOQ6sLuBo0tt5v5lczg4DpsbkaXj+4RcTEbOl44s8sUdtAsoyLotT4a14kKteNtyS5rI155qFS3uNM+oTzpaor4Aa/bfbA9D2mBbOXblXWjvGqFlouHJPjwbFU+rRcCCZyG1hG+Vf4YVUEPnVvXLuecWeY7tMvNEQe5Ne5MzhBOS4+LlukcfbwLemBpD8w+SQkKd2wahGtE/x5YNTeSQo+AlDJnXx+c= 35 | file: php7cc.phar 36 | skip_cleanup: true 37 | on: 38 | repo: sstalle/php7cc 39 | php: 5.6 40 | tags: true 41 | condition: "$COMPOSER_FLAGS = --prefer-lowest" 42 | -------------------------------------------------------------------------------- /src/NodeVisitor/EscapedUnicodeCodepointVisitor.php: -------------------------------------------------------------------------------- 1 | getAttribute('isDoubleQuoted')) { 20 | $unquotedStringValue = substr($node->getAttribute('originalValue'), 1, -1); 21 | } elseif ($node->getAttribute('isHereDoc')) { 22 | // Skip T_START_HEREDOC, T_END_HEREDOC 23 | $unquotedStringValue = ''; 24 | foreach (range($node->getAttribute('startTokenPos') + 1, $node->getAttribute('endTokenPos') - 1) as $i) { 25 | $unquotedStringValue .= $this->tokenCollection->getTokenStringValueAt($i); 26 | } 27 | } 28 | 29 | if (!$unquotedStringValue) { 30 | return; 31 | } 32 | 33 | $matches = array(); 34 | if (preg_match('/((?addContextMessage( 36 | sprintf('Unicode codepoint escaping "%s" in a string', $matches[0]), 37 | $node 38 | ); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/NodeVisitor/AbstractVisitor.php: -------------------------------------------------------------------------------- 1 | context = $context; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function setTokenCollection(TokenCollection $tokenCollection) 36 | { 37 | $this->tokenCollection = $tokenCollection; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function getLevel() 44 | { 45 | return static::LEVEL; 46 | } 47 | 48 | /** 49 | * @param string $text 50 | * @param Node $node 51 | */ 52 | protected function addContextMessage($text, Node $node) 53 | { 54 | $this->context->addMessage(new Message($text, $node->getAttribute('startLine'), $this->getLevel(), array($node))); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/NodeVisitor/GlobalVariableVariableVisitor.php: -------------------------------------------------------------------------------- 1 | vars as $globalVariable) { 19 | if (!( 20 | $globalVariable->name instanceof Node\Expr\PropertyFetch 21 | || $globalVariable->name instanceof Node\Expr\StaticPropertyFetch 22 | || $globalVariable->name instanceof Node\Expr\ArrayDimFetch 23 | ) 24 | ) { 25 | continue; 26 | } 27 | 28 | $startTokenPosition = $globalVariable->getAttribute('startTokenPos') + 1; 29 | $endTokenPosition = $globalVariable->getAttribute('endTokenPos'); 30 | if ($this->tokenCollection->isTokenEqualToOrPrecededBy($startTokenPosition, '{') 31 | && $this->tokenCollection->isTokenEqualToOrFollowedBy($endTokenPosition, '}') 32 | ) { 33 | continue; 34 | } 35 | 36 | $this->addContextMessage( 37 | 'Complex variable without curly braces in global keyword', 38 | $node 39 | ); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/resource/indirectVariableOrMethodAccess/indirectPropertyAccess.test: -------------------------------------------------------------------------------- 1 | Indirect property access 2 | ----- 3 | $bar['baz']; 5 | ----- 6 | Indirect variable, property or method access 7 | ----- 8 | $params['name']; 10 | ----- 11 | Indirect variable, property or method access 12 | ----- 13 | $field['name']; 15 | ----- 16 | Indirect variable, property or method access 17 | ----- 18 | $schema['node_name']; 20 | ----- 21 | Indirect variable, property or method access 22 | ----- 23 | {$bar['baz']}; 25 | ----- 26 | 27 | ----- 28 | { $bar['baz'] }; 30 | ----- 31 | 32 | ----- 33 | db->{$this->config[$name]}; 35 | ----- 36 | 37 | ----- 38 | db->$this->config[$name]; 40 | ----- 41 | 42 | 43 | ----- 44 | {$params['name']}; 46 | ----- 47 | 48 | ----- 49 | {$field['name']}; 51 | ----- 52 | 53 | ----- 54 | record->{$detail->to['name']}; 56 | ----- 57 | 58 | ----- 59 | record->$detail->to['name']; 61 | ----- 62 | 63 | ----- 64 | record->sku->getFirst()->{$detail->to['name']}; 66 | ----- 67 | 68 | ----- 69 | record->sku->getFirst()->$detail->to['name']; 71 | ----- 72 | 73 | ----- 74 | record->{$detail->to['name']}; 76 | ----- 77 | 78 | ----- 79 | record->$detail->to['name']; 81 | ----- 82 | 83 | ----- 84 | {$schema['node_name']}; 86 | ----- 87 | -------------------------------------------------------------------------------- /src/ExcludedPathCanonicalizer.php: -------------------------------------------------------------------------------- 1 | pathHelper = $pathHelper; 20 | } 21 | 22 | /** 23 | * Makes all excluded paths absolute. Non-existent paths are removed. 24 | * 25 | * @param string[] $checkedPaths 26 | * @param string[] $excludedPaths 27 | * 28 | * @return \string[] 29 | */ 30 | public function canonicalize(array $checkedPaths, array $excludedPaths) 31 | { 32 | $checkedDirectories = array_filter($checkedPaths, function ($path) { 33 | return is_dir($path); 34 | }); 35 | $canonicalizedPaths = array(); 36 | 37 | foreach ($excludedPaths as $path) { 38 | if (!$this->pathHelper->isDirectoryRelative($path) && ($canonicalizedPath = realpath($path))) { 39 | $canonicalizedPaths[] = $canonicalizedPath; 40 | } else { 41 | foreach ($checkedDirectories as $checkedDirectory) { 42 | $nestedExcludedDirectory = realpath(realpath($checkedDirectory) . DIRECTORY_SEPARATOR . $path); 43 | $nestedExcludedDirectory && $canonicalizedPaths[] = $nestedExcludedDirectory; 44 | } 45 | } 46 | } 47 | 48 | return $canonicalizedPaths; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ContextChecker.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 37 | $this->lexer = $lexer; 38 | $this->traverser = $traverser; 39 | } 40 | 41 | /** 42 | * @param ContextInterface $context 43 | * 44 | * @return FileContext 45 | */ 46 | public function checkContext(ContextInterface $context) 47 | { 48 | try { 49 | $parsedStatements = $this->parser->parse($context->getCheckedCode()); 50 | $this->traverser->traverse($parsedStatements, $context, $this->lexer->getTokens()); 51 | } catch (\Exception $e) { 52 | $context->addError(new CheckError($e->getMessage())); 53 | } catch (\ParseException $e) { 54 | $context->addError(new CheckError($e->getMessage(), $e->getLine())); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/NodeVisitor/YieldInExpressionContextVisitor.php: -------------------------------------------------------------------------------- 1 | expressionStack = new \SplStack(); 20 | } 21 | 22 | public function enterNode(Node $node) 23 | { 24 | if ($node instanceof Node\Expr\Yield_) { 25 | $startTokenPosition = $node->getAttribute('startTokenPos'); 26 | $endTokenPosition = $node->getAttribute('endTokenPos'); 27 | 28 | if (!( 29 | $this->tokenCollection->isTokenPrecededBy($startTokenPosition, '(') 30 | && $this->tokenCollection->isTokenFollowedBy($endTokenPosition, ')') 31 | ) 32 | && !$this->expressionStack->isEmpty() 33 | ) { 34 | $this->addContextMessage( 35 | '"yield" usage in expression context', 36 | $this->expressionStack->top() 37 | ); 38 | } 39 | } elseif ($node instanceof Node\Expr) { 40 | $this->expressionStack->push($node); 41 | } 42 | } 43 | 44 | public function leaveNode(Node $node) 45 | { 46 | if (!$this->expressionStack->isEmpty() && $node === $this->expressionStack->top()) { 47 | $this->expressionStack->pop(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/NodeVisitor/BitwiseShiftVisitor.php: -------------------------------------------------------------------------------- 1 | intSize = $intSize; 25 | } 26 | 27 | public function enterNode(Node $node) 28 | { 29 | $isLeftShift = $node instanceof Node\Expr\BinaryOp\ShiftLeft; 30 | $isRightShift = $node instanceof Node\Expr\BinaryOp\ShiftRight; 31 | if (!$isLeftShift && !$isRightShift) { 32 | return; 33 | } 34 | 35 | $rightOperand = $node->right; 36 | if ($rightOperand instanceof Node\Expr\UnaryMinus && $rightOperand->expr instanceof Node\Scalar\LNumber 37 | && $rightOperand->expr->value > 0 38 | ) { 39 | $this->addContextMessage( 40 | 'Bitwise shift by a negative number', 41 | $node 42 | ); 43 | } elseif ($rightOperand instanceof Node\Scalar\LNumber && $rightOperand->value >= $this->intSize) { 44 | $this->addContextMessage( 45 | sprintf('Bitwise shift by %d bits', $rightOperand->value), 46 | $node 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/code/CompatibilityViolation/FileContextTest.php: -------------------------------------------------------------------------------- 1 | assertSame( 19 | $useRelativePaths ? $file->getRelativePathname() : $file->getRealPath(), 20 | $context->getCheckedResourceName() 21 | ); 22 | } 23 | 24 | public function testGetCheckedResourceNameProvider() 25 | { 26 | return array( 27 | array('/foo/bar.php', 'bar.php', true), 28 | array('C:\baz.php', 'test\bar.php', true), 29 | array('/foo/bar/baz.php', 'test/bar.php', false), 30 | array('C:\bar\baz.php', 'bar.php', false), 31 | ); 32 | } 33 | } 34 | 35 | class SplFileInfo extends BaseSplFileInfo 36 | { 37 | /** 38 | * @var string 39 | */ 40 | protected $fullPath; 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function __construct($file, $relativePath, $relativePathname) 46 | { 47 | parent::__construct($file, $relativePath, $relativePathname); 48 | $this->fullPath = $file; 49 | } 50 | 51 | public function getRealPath() 52 | { 53 | return $this->fullPath; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/NodeVisitor/NewClassVisitor.php: -------------------------------------------------------------------------------- 1 | namespacedName) // Property set by the NameResolver visitor 42 | && count($node->namespacedName->parts) === 1 43 | && ($lowerCasedClassName = strtolower($node->name)) 44 | && array_key_exists($lowerCasedClassName, self::$lowerCasedNewClasses)) { 45 | $this->addContextMessage( 46 | sprintf( 47 | 'Class/trait/interface "%s" was added in the global namespace', 48 | self::$lowerCasedNewClasses[$lowerCasedClassName] 49 | ), 50 | $node 51 | ); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/resource/setcookieEmptyName/setcookieEmptyName.test: -------------------------------------------------------------------------------- 1 | Calling setcookie or setrawcookie with an empty cookie name 2 | ----- 3 | allowedExtensions = $allowedExtensions; 29 | $this->alwaysAllowedFiles = array_flip($alwaysAllowedFiles); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function accept() 36 | { 37 | $currentKey = $this->key(); 38 | $isFile = $currentKey && is_file($currentKey); 39 | 40 | if ($isFile && isset($this->alwaysAllowedFiles[realpath($currentKey)])) { 41 | return true; 42 | } 43 | 44 | return !$isFile || in_array(pathinfo($currentKey, PATHINFO_EXTENSION), $this->allowedExtensions); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getChildren() 51 | { 52 | return new static( 53 | $this->getInnerIterator()->getChildren(), 54 | $this->allowedExtensions, 55 | array_flip($this->alwaysAllowedFiles) 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/NodeVisitor/AbstractNewFunctionVisitor.php: -------------------------------------------------------------------------------- 1 | name)) 42 | && array_key_exists($lowerCasedFunction, self::$lowerCasedNewFunctions) 43 | && $this->accepts($node) 44 | ) { 45 | $this->addContextMessage($this->getMessageText(self::$lowerCasedNewFunctions[$lowerCasedFunction]), $node); 46 | } 47 | } 48 | 49 | /** 50 | * @param Node\Stmt\Function_ $node 51 | * 52 | * @return bool 53 | */ 54 | abstract protected function accepts(Node\Stmt\Function_ $node); 55 | 56 | /** 57 | * @param string $functionName 58 | * 59 | * @return string 60 | */ 61 | abstract protected function getMessageText($functionName); 62 | } 63 | -------------------------------------------------------------------------------- /test/resource/php4Constructor/php4Constructor.test: -------------------------------------------------------------------------------- 1 | PHP 4 constructors are now deprecated 2 | ----- 3 | functionAnalyzer = $functionAnalyzer; 25 | } 26 | 27 | public function enterNode(Node $node) 28 | { 29 | if (!$this->functionAnalyzer->isFunctionCallByStaticName($node, array('password_hash' => true)) 30 | || !isset($node->args[static::PASSWORD_HASH_OPTIONS_ARGUMENT_INDEX]) 31 | || !($node->args[static::PASSWORD_HASH_OPTIONS_ARGUMENT_INDEX]->value instanceof Node\Expr\Array_) 32 | ) { 33 | return; 34 | } 35 | 36 | /** @var Node\Expr\Array_ $passwordHashOptions */ 37 | $passwordHashOptions = $node->args[static::PASSWORD_HASH_OPTIONS_ARGUMENT_INDEX]->value; 38 | /** @var $node Node\Expr\FuncCall */ 39 | foreach ($passwordHashOptions->items as $option) { 40 | if ($option->key instanceof Node\Scalar\String_ && $option->key->value === 'salt') { 41 | $this->addContextMessage( 42 | 'Deprecated option "salt" passed to password_hash function', 43 | $node 44 | ); 45 | 46 | break; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/NodeVisitor/PregReplaceEvalVisitor.php: -------------------------------------------------------------------------------- 1 | regExpParser = $regExpParser; 32 | $this->functionAnalyzer = $functionAnalyzer; 33 | } 34 | 35 | public function enterNode(Node $node) 36 | { 37 | if (!$this->functionAnalyzer->isFunctionCallByStaticName($node, 'preg_replace')) { 38 | return; 39 | } 40 | 41 | /** @var Node\Expr\FuncCall $node */ 42 | $regExpPatternArgument = $node->args[0]; 43 | if (!$regExpPatternArgument->value instanceof Node\Scalar\String_) { 44 | return; 45 | } 46 | 47 | $regExp = $this->regExpParser->parse($regExpPatternArgument->value->value); 48 | if ($regExp->hasModifier(static::PREG_REPLACE_EVAL_MODIFIER)) { 49 | $this->addContextMessage( 50 | sprintf('Removed regular expression modifier "%s" used', static::PREG_REPLACE_EVAL_MODIFIER), 51 | $node 52 | ); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/NodeVisitor/PHP4ConstructorVisitor.php: -------------------------------------------------------------------------------- 1 | name; 16 | $hasPhp4Constructor = false; 17 | $hasPhp5Constructor = false; 18 | $php4ConstructorNode = null; 19 | 20 | // Anonymous class can't use php4 constructor by definition 21 | if (empty($currentClassName)) { 22 | return; 23 | } 24 | 25 | // Checks if class is namespaced (property namespacedName was set by the NameResolver visitor) 26 | if (count($node->namespacedName->parts) > 1) { 27 | return; 28 | } 29 | 30 | foreach ($node->stmts as $stmt) { 31 | // Check for constructors 32 | if ($stmt instanceof Node\Stmt\ClassMethod) { 33 | if ($stmt->name === '__construct') { 34 | $hasPhp5Constructor = true; 35 | } 36 | 37 | if ($stmt->name === $currentClassName) { 38 | $hasPhp4Constructor = true; 39 | $php4ConstructorNode = $stmt; 40 | } 41 | } 42 | } 43 | 44 | if ($hasPhp4Constructor && !$hasPhp5Constructor) { 45 | $this->addContextMessage( 46 | 'PHP 4 constructors are now deprecated', 47 | $php4ConstructorNode 48 | ); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/NodeVisitor/ArrayOrObjectValueAssignmentByReferenceVisitor.php: -------------------------------------------------------------------------------- 1 | checkArrayValueByReferenceCreation($node) || $this->checkObjectPropertyByReferenceCreation($node); 19 | } 20 | 21 | /** 22 | * @param Node\Expr\AssignRef $node 23 | * 24 | * @return bool 25 | */ 26 | protected function checkArrayValueByReferenceCreation(Node\Expr\AssignRef $node) 27 | { 28 | if ($node->var instanceof Node\Expr\ArrayDimFetch && $node->var->dim 29 | && $node->expr instanceof Node\Expr\ArrayDimFetch && $node->expr->dim 30 | ) { 31 | $this->addContextMessage( 32 | 'Possible array element creation during by-reference assignment', 33 | $node 34 | ); 35 | 36 | return true; 37 | } 38 | 39 | return false; 40 | } 41 | 42 | /** 43 | * @param Node\Expr\AssignRef $node 44 | * 45 | * @return bool 46 | */ 47 | protected function checkObjectPropertyByReferenceCreation(Node\Expr\AssignRef $node) 48 | { 49 | if ($node->var instanceof Node\Expr\PropertyFetch && $node->expr instanceof Node\Expr\PropertyFetch) { 50 | $this->addContextMessage( 51 | 'Possible object property creation during by-reference assignment', 52 | $node 53 | ); 54 | 55 | return true; 56 | } 57 | 58 | return false; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/NodeVisitor/SetcookieEmptyNameVisitor.php: -------------------------------------------------------------------------------- 1 | true, 18 | 'setrawcookie' => true, 19 | ); 20 | 21 | /** 22 | * @var FunctionAnalyzer 23 | */ 24 | protected $functionAnalyzer; 25 | 26 | /** 27 | * @param FunctionAnalyzer $functionAnalyzer 28 | */ 29 | public function __construct(FunctionAnalyzer $functionAnalyzer) 30 | { 31 | $this->functionAnalyzer = $functionAnalyzer; 32 | } 33 | 34 | public function enterNode(Node $node) 35 | { 36 | if (!$this->functionAnalyzer->isFunctionCallByStaticName($node, self::$setcookieFamilyFunctions)) { 37 | return; 38 | } 39 | 40 | /** @var Node\Expr\FuncCall $node */ 41 | $cookieNameArgumentValue = isset($node->args[0]) ? $node->args[0]->value : null; 42 | $isEmptyString = $cookieNameArgumentValue && $cookieNameArgumentValue instanceof Node\Scalar\String_ 43 | && $cookieNameArgumentValue->value === ''; 44 | $isEmptyConstant = $cookieNameArgumentValue && $cookieNameArgumentValue instanceof Node\Expr\ConstFetch 45 | && in_array(strtolower($cookieNameArgumentValue->name->toString()), array('null', 'false'), true); 46 | 47 | if ($isEmptyConstant || $isEmptyString) { 48 | $this->addContextMessage( 49 | sprintf('Function "%s" called with an empty cookie name', $node->name->toString()), 50 | $node 51 | ); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/PathChecker.php: -------------------------------------------------------------------------------- 1 | contextChecker = $fileChecker; 28 | $this->resultPrinter = $resultPrinter; 29 | } 30 | 31 | /** 32 | * @param \Traversable $traversablePaths 33 | * @param bool $useRelativePaths 34 | */ 35 | public function check(\Traversable $traversablePaths, $useRelativePaths) 36 | { 37 | $checkMetadata = new CheckMetadata(); 38 | 39 | /** @var SplFileInfo $fileInfo */ 40 | foreach ($traversablePaths as $fileInfo) { 41 | $this->checkFile($checkMetadata, $fileInfo, $useRelativePaths); 42 | } 43 | 44 | $checkMetadata->endCheck(); 45 | 46 | $this->resultPrinter->printMetadata($checkMetadata); 47 | } 48 | 49 | /** 50 | * @param CheckMetadata $checkMetadata 51 | * @param SplFileInfo $fileInfo 52 | * @param bool $useRelativePaths 53 | */ 54 | protected function checkFile(CheckMetadata $checkMetadata, SplFileInfo $fileInfo, $useRelativePaths) 55 | { 56 | $context = new FileContext($fileInfo, $useRelativePaths); 57 | $this->contextChecker->checkContext($context); 58 | 59 | if ($context->hasMessagesOrErrors()) { 60 | $this->resultPrinter->printContext($context); 61 | } 62 | 63 | $checkMetadata->incrementCheckedFileCount(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/PathCheckExecutor.php: -------------------------------------------------------------------------------- 1 | pathTraversableFactory = $pathTraversableFactory; 43 | $this->pathChecker = $pathChecker; 44 | $this->traverser = $traverser; 45 | $this->visitorResolver = $visitorResolver; 46 | } 47 | 48 | /** 49 | * @param PathCheckSettings $checkSettings 50 | */ 51 | public function check(PathCheckSettings $checkSettings) 52 | { 53 | $this->visitorResolver->setLevel($checkSettings->getMessageLevel()); 54 | foreach ($this->visitorResolver->resolve() as $visitor) { 55 | $this->traverser->addVisitor($visitor); 56 | } 57 | 58 | $this->pathChecker->check( 59 | $this->pathTraversableFactory->createTraversable( 60 | $checkSettings->getCheckedPaths(), 61 | $checkSettings->getCheckedFileExtensions(), 62 | $checkSettings->getExcludedPaths() 63 | ), 64 | $checkSettings->getUseRelativePaths() 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/PathTraversableFactory.php: -------------------------------------------------------------------------------- 1 | excludedPathCanonicalizer = $excludedPathCanonicalizer; 22 | } 23 | 24 | /** 25 | * @param string[] $paths Files and/or directories to check 26 | * @param string[] $checkedExtensions Only files having these extensions will be checked 27 | * @param string[] $excludedPaths 28 | * 29 | * @return \Traversable 30 | */ 31 | public function createTraversable(array $paths, array $checkedExtensions, array $excludedPaths) 32 | { 33 | $directlyPassedFiles = array(); 34 | $excludedPaths = $this->excludedPathCanonicalizer->canonicalize($paths, $excludedPaths); 35 | foreach ($paths as $path) { 36 | if (is_dir($path) && !$checkedExtensions) { 37 | throw new \DomainException('At least 1 extension must be specified to check a directory'); 38 | } elseif (is_file($path)) { 39 | $directlyPassedFiles[] = realpath($path); 40 | } 41 | } 42 | 43 | $fileDirectoryIterator = new FileDirectoryListRecursiveIterator($paths); 44 | $extensionFilteringIterator = new ExtensionFilteringRecursiveIterator( 45 | $fileDirectoryIterator, 46 | $checkedExtensions, 47 | $directlyPassedFiles 48 | ); 49 | $excludedPathFilteringIterator = new ExcludedPathFilteringRecursiveIterator( 50 | $extensionFilteringIterator, 51 | $excludedPaths 52 | ); 53 | 54 | return new \RecursiveIteratorIterator( 55 | $excludedPathFilteringIterator, 56 | \RecursiveIteratorIterator::LEAVES_ONLY 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/code/NodeVisitor/ResolverTest.php: -------------------------------------------------------------------------------- 1 | getLevel() >= $level) { 24 | $expectedVisitors[] = $visitor; 25 | } 26 | } 27 | 28 | $this->assertSame($expectedVisitors, $resolver->resolve()); 29 | } 30 | 31 | public function testResolvesCorrectlyAccordingToLevelProvider() 32 | { 33 | $data = array( 34 | array(array(), Message::LEVEL_INFO), 35 | array(array(Message::LEVEL_INFO, Message::LEVEL_INFO), Message::LEVEL_INFO), 36 | array(array(Message::LEVEL_INFO, Message::LEVEL_WARNING), Message::LEVEL_WARNING), 37 | array(array(Message::LEVEL_INFO, Message::LEVEL_WARNING, Message::LEVEL_ERROR, Message::LEVEL_WARNING), Message::LEVEL_ERROR), 38 | array(array(Message::LEVEL_INFO, Message::LEVEL_INFO), Message::LEVEL_ERROR), 39 | ); 40 | 41 | foreach ($data as $i => $item) { 42 | $visitors = array(); 43 | foreach ($item[0] as $level) { 44 | $visitors[] = new DummyVisitor($level); 45 | } 46 | 47 | $data[$i][0] = $visitors; 48 | } 49 | 50 | return $data; 51 | } 52 | } 53 | 54 | class DummyVisitor extends AbstractVisitor 55 | { 56 | /** 57 | * @var int 58 | */ 59 | protected $level; 60 | 61 | /** 62 | * @param int $level 63 | */ 64 | public function __construct($level) 65 | { 66 | $this->level = $level; 67 | } 68 | 69 | public function getLevel() 70 | { 71 | return $this->level; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Iterator/FileDirectoryListRecursiveIterator.php: -------------------------------------------------------------------------------- 1 | data = $data; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function current() 38 | { 39 | $fileName = realpath($this->data[$this->position]); 40 | 41 | return new SplFileInfo($fileName, '', basename($fileName)); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function next() 48 | { 49 | ++$this->position; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function key() 56 | { 57 | return $this->data[$this->position]; 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function valid() 64 | { 65 | return isset($this->data[$this->position]); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function rewind() 72 | { 73 | $this->position = 0; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function hasChildren() 80 | { 81 | return is_dir($this->data[$this->position]); 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function getChildren() 88 | { 89 | return new RecursiveDirectoryIterator( 90 | $this->data[$this->position], 91 | \RecursiveDirectoryIterator::KEY_AS_PATHNAME 92 | | \RecursiveDirectoryIterator::CURRENT_AS_FILEINFO 93 | | \RecursiveDirectoryIterator::SKIP_DOTS 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/code/NodeVisitor/BitwiseShiftVisitorTest.php: -------------------------------------------------------------------------------- 1 | setExpectedException('\InvalidArgumentException'); 18 | } 19 | 20 | new \Sstalle\php7cc\NodeVisitor\BitwiseShiftVisitor($intSize); 21 | } 22 | 23 | /** 24 | * @dataProvider testDetectsShiftsLargerThanIntSizeProvider 25 | */ 26 | public function testDetectsShiftsLargerThanIntSize($intSize, $node, $expectedMessageCount) 27 | { 28 | $visitor = new \Sstalle\php7cc\NodeVisitor\BitwiseShiftVisitor($intSize); 29 | $testContext = new StringContext('', 'test'); 30 | $visitor->initializeContext($testContext); 31 | $visitor->enterNode($node); 32 | 33 | $this->assertEquals($expectedMessageCount, count($testContext->getMessages())); 34 | } 35 | 36 | public function testThrowsExceptionForInvalidIntSizeProvider() 37 | { 38 | return array( 39 | array(0, false), 40 | array(-5, false), 41 | array(-1, false), 42 | array(8, true), 43 | array(32, true), 44 | ); 45 | } 46 | 47 | public function testDetectsShiftsLargerThanIntSizeProvider() 48 | { 49 | $data = array(); 50 | 51 | foreach (array(16, 32, 64) as $intSize) { 52 | foreach (array(8, 16, 32, 64, 128) as $shiftWidth) { 53 | $data[] = array( 54 | $intSize, 55 | new Expr\BinaryOp\ShiftLeft(new LNumber(1), new LNumber($shiftWidth)), 56 | $shiftWidth >= $intSize ? 1 : 0, 57 | ); 58 | 59 | $data[] = array( 60 | $intSize, 61 | new Expr\BinaryOp\ShiftRight(new LNumber(1), new LNumber($shiftWidth)), 62 | $shiftWidth >= $intSize ? 1 : 0, 63 | ); 64 | } 65 | } 66 | 67 | return $data; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/PathCheckSettings.php: -------------------------------------------------------------------------------- 1 | checkedPaths = $checkedPaths; 49 | $this->checkedFileExtensions = $checkedFileExtensions; 50 | } 51 | 52 | /** 53 | * @return array 54 | */ 55 | public function getCheckedPaths() 56 | { 57 | return $this->checkedPaths; 58 | } 59 | 60 | /** 61 | * @return array 62 | */ 63 | public function getCheckedFileExtensions() 64 | { 65 | return $this->checkedFileExtensions; 66 | } 67 | 68 | /** 69 | * @return array 70 | */ 71 | public function getExcludedPaths() 72 | { 73 | return $this->excludedPaths; 74 | } 75 | 76 | /** 77 | * @param array $excludedPaths 78 | */ 79 | public function setExcludedPaths($excludedPaths) 80 | { 81 | $this->excludedPaths = $excludedPaths; 82 | } 83 | 84 | /** 85 | * @return int 86 | */ 87 | public function getMessageLevel() 88 | { 89 | return $this->messageLevel; 90 | } 91 | 92 | /** 93 | * @param int $messageLevel 94 | */ 95 | public function setMessageLevel($messageLevel) 96 | { 97 | $this->messageLevel = $messageLevel; 98 | } 99 | 100 | /** 101 | * @return bool 102 | */ 103 | public function getUseRelativePaths() 104 | { 105 | return $this->useRelativePaths; 106 | } 107 | 108 | /** 109 | * @param bool $useRelativePaths 110 | */ 111 | public function setUseRelativePaths($useRelativePaths) 112 | { 113 | $this->useRelativePaths = $useRelativePaths; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/NodeVisitor/ReservedClassNameVisitor.php: -------------------------------------------------------------------------------- 1 | functionAnalyzer = $functionAnalyzer; 47 | $this->reservedNamesToMessagesMap = array_merge( 48 | array_fill_keys( 49 | $this->reservedClassNames, 50 | static::RESERVED_NAME_MESSAGE 51 | ), 52 | array_fill_keys( 53 | $this->futureReservedClassNames, 54 | static::FUTURE_RESERVED_NAME_MESSAGE 55 | ) 56 | ); 57 | } 58 | 59 | public function enterNode(Node $node) 60 | { 61 | $checkedName = ''; 62 | $usagePatternName = null; 63 | 64 | if ($node instanceof Node\Stmt\ClassLike) { 65 | $checkedName = $node->name; 66 | $usagePatternName = 'as a class, interface or trait name'; 67 | } elseif ($this->functionAnalyzer->isFunctionCallByStaticName($node, 'class_alias')) { 68 | /** @var Node\Expr\FuncCall $node */ 69 | $secondArgument = isset($node->args[1]) ? $node->args[1] : null; 70 | 71 | if (!$secondArgument || !$secondArgument->value instanceof Node\Scalar\String_) { 72 | return; 73 | } 74 | 75 | $checkedName = $secondArgument->value->value; 76 | $usagePatternName = 'as a class alias'; 77 | } elseif ($node instanceof Node\Stmt\UseUse) { 78 | $checkedName = $node->alias; 79 | $usagePatternName = 'as a use statement alias'; 80 | } 81 | 82 | $checkedName = strtolower($checkedName); 83 | if ($checkedName && isset($this->reservedNamesToMessagesMap[$checkedName])) { 84 | $this->addContextMessage( 85 | sprintf($this->reservedNamesToMessagesMap[$checkedName], $checkedName, $usagePatternName), 86 | $node 87 | ); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/code/Helper/RegExp/RegExpParserTest.php: -------------------------------------------------------------------------------- 1 | parser = new RegExpParser(); 17 | } 18 | 19 | /** 20 | * @expectedException \InvalidArgumentException 21 | */ 22 | public function testThrowsExceptionOnEmptyRegExp() 23 | { 24 | $this->parser->parse(''); 25 | } 26 | 27 | /** 28 | * @dataProvider throwsExceptionOnRegExpWithoutClosingDelimiterProvider 29 | * @expectedException \InvalidArgumentException 30 | */ 31 | public function testThrowsExceptionOnRegExpWithoutClosingDelimiter($regExp) 32 | { 33 | $this->parser->parse($regExp); 34 | } 35 | 36 | /** 37 | * @dataProvider parsesRegExpCorrectlyProvider 38 | */ 39 | public function testParsesRegExpCorrectly( 40 | $regExp, 41 | $expectedStartDelimiter, 42 | $expectedEndDelimiter, 43 | $expectedExpression, 44 | $expectedModifiers 45 | ) { 46 | $parsedRegExp = $this->parser->parse($regExp); 47 | 48 | $this->assertEquals($expectedStartDelimiter, $parsedRegExp->getStartDelimiter()); 49 | $this->assertEquals($expectedEndDelimiter, $parsedRegExp->getEndDelimiter()); 50 | $this->assertEquals($expectedExpression, $parsedRegExp->getExpression()); 51 | $this->assertEquals($expectedModifiers, $parsedRegExp->getModifiers()); 52 | } 53 | 54 | public function throwsExceptionOnRegExpWithoutClosingDelimiterProvider() 55 | { 56 | return array( 57 | array('/foo'), 58 | array('#foo'), 59 | ); 60 | } 61 | 62 | public function parsesRegExpCorrectlyProvider() 63 | { 64 | return array( 65 | array( 66 | '/foo/bar', 67 | '/', 68 | '/', 69 | 'foo', 70 | 'bar', 71 | ), 72 | array( 73 | '(foo)b', 74 | '(', 75 | ')', 76 | 'foo', 77 | 'b', 78 | ), 79 | array( 80 | '#foo#', 81 | '#', 82 | '#', 83 | 'foo', 84 | '', 85 | ), 86 | array( 87 | '{a}', 88 | '{', 89 | '}', 90 | 'a', 91 | '', 92 | ), 93 | array( 94 | '[a]', 95 | '[', 96 | ']', 97 | 'a', 98 | '', 99 | ), 100 | array( 101 | '', 102 | '<', 103 | '>', 104 | 'a', 105 | '', 106 | ), 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Helper/RegExp/RegExp.php: -------------------------------------------------------------------------------- 1 | ')', 12 | '[' => ']', 13 | '{' => '}', 14 | '<' => '>', 15 | ); 16 | 17 | /** 18 | * @var string 19 | */ 20 | protected $startDelimiter; 21 | 22 | /** 23 | * @var string 24 | */ 25 | protected $endDelimiter; 26 | 27 | /** 28 | * @var string 29 | */ 30 | protected $expression; 31 | 32 | /** 33 | * @var string 34 | */ 35 | protected $modifiers; 36 | 37 | /** 38 | * @param string $startDelimiter 39 | * @param string $endDelimiter 40 | * @param string $expression 41 | * @param string $modifiers 42 | */ 43 | public function __construct($startDelimiter, $endDelimiter, $expression, $modifiers) 44 | { 45 | if (!$startDelimiter || !$endDelimiter) { 46 | throw new \InvalidArgumentException('Delimiter must not be empty'); 47 | } 48 | 49 | foreach (array($startDelimiter, $endDelimiter) as $delimiter) { 50 | if (preg_match('/[\\\\a-z0-9\s+]/', strtolower($delimiter)) === 1) { 51 | throw new \InvalidArgumentException(sprintf('Invalid delimiter %s used', $startDelimiter)); 52 | } 53 | } 54 | 55 | $hasPairedDelimiter = isset(static::$delimiterPairs[$startDelimiter]); 56 | if (($hasPairedDelimiter && static::$delimiterPairs[$startDelimiter] !== $endDelimiter) 57 | || (!$hasPairedDelimiter && $startDelimiter !== $endDelimiter) 58 | ) { 59 | throw new \InvalidArgumentException( 60 | sprintf('Start delimiter %s does not match end delimiter %s', $startDelimiter, $endDelimiter) 61 | ); 62 | } 63 | 64 | $this->startDelimiter = $startDelimiter; 65 | $this->endDelimiter = $endDelimiter; 66 | $this->expression = $expression; 67 | $this->modifiers = $modifiers; 68 | } 69 | 70 | /** 71 | * @return string 72 | */ 73 | public function getStartDelimiter() 74 | { 75 | return $this->startDelimiter; 76 | } 77 | 78 | /** 79 | * @return string 80 | */ 81 | public function getEndDelimiter() 82 | { 83 | return $this->endDelimiter; 84 | } 85 | 86 | /** 87 | * @return string 88 | */ 89 | public function getExpression() 90 | { 91 | return $this->expression; 92 | } 93 | 94 | /** 95 | * @return string 96 | */ 97 | public function getModifiers() 98 | { 99 | return $this->modifiers; 100 | } 101 | 102 | /** 103 | * @param string $modifier 104 | * 105 | * @return bool 106 | */ 107 | public function hasModifier($modifier) 108 | { 109 | return strpos($this->getModifiers(), $modifier) !== false; 110 | } 111 | 112 | /** 113 | * @return string 114 | */ 115 | public static function getDelimiterPairs() 116 | { 117 | return self::$delimiterPairs; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/NodeVisitor/FuncGetArgsVisitor.php: -------------------------------------------------------------------------------- 1 | functionAnalyzer = $functionAnalyzer; 49 | } 50 | 51 | public function beforeTraverse(array $nodes) 52 | { 53 | $this->argumentModificationStack = new \SplStack(); 54 | } 55 | 56 | public function enterNode(Node $node) 57 | { 58 | $isCurrentNodeFunctionLike = $node instanceof Node\FunctionLike; 59 | if ($isCurrentNodeFunctionLike || $this->argumentModificationStack->isEmpty() 60 | || !$this->argumentModificationStack->top() 61 | || !$this->functionAnalyzer->isFunctionCallByStaticName($node, array_flip(array('func_get_arg', 'func_get_args'))) 62 | ) { 63 | $isCurrentNodeFunctionLike && $this->argumentModificationStack->push(false); 64 | 65 | return; 66 | } 67 | 68 | /** @var Node\Expr\FuncCall $node */ 69 | $functionName = $node->name->toString(); 70 | $this->addContextMessage( 71 | sprintf('Function argument(s) returned by "%s" might have been modified', $functionName), 72 | $node 73 | ); 74 | } 75 | 76 | public function leaveNode(Node $node) 77 | { 78 | if ($this->argumentModificationStack->isEmpty()) { 79 | return; 80 | } 81 | 82 | if ($node instanceof Node\FunctionLike) { 83 | $this->argumentModificationStack->pop(); 84 | 85 | return; 86 | } 87 | 88 | foreach ($this->possiblyArgumentModifyingClasses as $class) { 89 | if ($node instanceof $class) { 90 | $this->argumentModificationStack->pop(); 91 | $this->argumentModificationStack->push(true); 92 | 93 | return; 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/code/Helper/RegExp/RegExpTest.php: -------------------------------------------------------------------------------- 1 | assertSame($hasModifier, $regexp->hasModifier($testedModifier)); 62 | } 63 | 64 | public function throwsExceptionWithEmptyDelimiterProvider() 65 | { 66 | return array( 67 | array(null), 68 | array(''), 69 | ); 70 | } 71 | 72 | public function throwsExceptionWithInvalidDelimiterProvider() 73 | { 74 | return array( 75 | array('a'), 76 | array('A'), 77 | array('0'), 78 | array('\\'), 79 | array(' '), 80 | ); 81 | } 82 | 83 | public function throwsExceptionWithNonMatchingDelimitersProvider() 84 | { 85 | return array( 86 | array('/', '#'), 87 | array('(', '('), 88 | array('[', '['), 89 | array('{', '{'), 90 | array('<', '<'), 91 | ); 92 | } 93 | 94 | public function hasModifierProvider() 95 | { 96 | return array( 97 | array('abc', 'a', true), 98 | array('abc', 'b', true), 99 | array('b', 'b', true), 100 | array('', 'b', false), 101 | array('a', 'b', false), 102 | array('aec', 'b', false), 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/code/Iterator/FileDirectoryListRecursiveIteratorTest.php: -------------------------------------------------------------------------------- 1 | getChild($path)->url(); 21 | } 22 | 23 | $i = 0; 24 | $iterator = new \RecursiveIteratorIterator( 25 | new \Sstalle\php7cc\Iterator\FileDirectoryListRecursiveIterator($pathUrls), 26 | \RecursiveIteratorIterator::LEAVES_ONLY 27 | ); 28 | /** @var SplFileInfo $fileInfo */ 29 | foreach ($iterator as $fileInfo) { 30 | $this->assertEquals($expectedFileNames[$i++], $fileInfo->getRelativePathname()); 31 | } 32 | } 33 | 34 | public function testRelativePathNamesProvider() 35 | { 36 | return array( 37 | array( 38 | array( 39 | 'folder' => array( 40 | 'subfolder' => array( 41 | 'test.php' => '1', 42 | ), 43 | 'anothersubfolder' => array( 44 | 'test2.php' => '1', 45 | ), 46 | ), 47 | ), 48 | array( 49 | 'folder', 50 | ), 51 | array( 52 | 'subfolder/test.php', 53 | 'anothersubfolder/test2.php', 54 | ), 55 | ), 56 | array( 57 | array( 58 | 'folder' => array( 59 | 'subfolder' => array( 60 | 'test.php' => '1', 61 | ), 62 | 'anothersubfolder' => array( 63 | 'test2.php' => '1', 64 | ), 65 | ), 66 | ), 67 | array( 68 | 'folder/subfolder/test.php', 69 | ), 70 | array( 71 | 'test.php', 72 | ), 73 | ), 74 | array( 75 | array( 76 | 'folder' => array( 77 | 'subfolder' => array( 78 | 'test.php' => '1', 79 | ), 80 | 'anothersubfolder' => array( 81 | 'test2.php' => '1', 82 | ), 83 | ), 84 | 'anotherfolder' => array( 85 | 'test3.php' => '1', 86 | ), 87 | ), 88 | array( 89 | 'folder/subfolder', 90 | 'folder/anothersubfolder/test2.php', 91 | ), 92 | array( 93 | 'test.php', 94 | 'test2.php', 95 | 'test3.php', 96 | ), 97 | ), 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/code/Iterator/ExtensionFilteringRecursiveIteratorTest.php: -------------------------------------------------------------------------------- 1 | array(), 18 | 'folder' => array( 19 | 'subfolder' => array( 20 | 'subfolderphp.php' => '1', 21 | ), 22 | 'folderphp.php' => '1', 23 | 'folderphp.test' => '1', 24 | ), 25 | 'topphp.php' => '1', 26 | ), 27 | array( 28 | array('test'), 29 | ), 30 | array( 31 | 'folderphp.test', 32 | ), 33 | ), 34 | array( 35 | array( 36 | 'empty' => array(), 37 | 'folder' => array( 38 | 'subfolder' => array( 39 | 'subfolderphp.php' => '1', 40 | ), 41 | 'folderphp.php' => '1', 42 | 'folderphp.test' => '1', 43 | ), 44 | 'topphp.php' => '1', 45 | ), 46 | array( 47 | array('php'), 48 | ), 49 | array( 50 | 'subfolderphp.php', 51 | 'folderphp.php', 52 | 'topphp.php', 53 | ), 54 | ), 55 | array( 56 | array( 57 | 'empty' => array(), 58 | 'folder' => array( 59 | 'subfolder' => array( 60 | 'subfolderphp.php' => '1', 61 | ), 62 | 'folderphp.php' => '1', 63 | 'folderphp.test' => '1', 64 | ), 65 | 'topphp.php' => '1', 66 | ), 67 | array( 68 | array('php', 'test'), 69 | ), 70 | array( 71 | 'subfolderphp.php', 72 | 'folderphp.php', 73 | 'folderphp.test', 74 | 'topphp.php', 75 | ), 76 | ), 77 | array( 78 | array( 79 | 'empty' => array(), 80 | 'folder' => array( 81 | 'subfolder' => array( 82 | 'subfolderphp.php' => '1', 83 | ), 84 | 'folderphp.php' => '1', 85 | 'folderphp.test' => '1', 86 | ), 87 | 'topphp.php' => '1', 88 | ), 89 | array( 90 | array(), 91 | ), 92 | array(), 93 | ), 94 | ); 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public function getIteratorClass() 101 | { 102 | return '\\Sstalle\\php7cc\\Iterator\\ExtensionFilteringRecursiveIterator'; 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | public function getDefaultConstructorArguments() 109 | { 110 | return array(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /test/code/Iterator/ExcludedPathFilteringRecursiveIteratorTest.php: -------------------------------------------------------------------------------- 1 | array(), 16 | 'folder' => array( 17 | 'subfolder' => array( 18 | 'subfolderphp.php' => '1', 19 | ), 20 | 'folderphp.php' => '1', 21 | 'folderphp.test' => '1', 22 | ), 23 | 'topphp.php' => '1', 24 | ), 25 | array( 26 | array('vfs://root/folder'), 27 | ), 28 | array( 29 | 'topphp.php', 30 | ), 31 | ), 32 | array( 33 | array( 34 | 'empty' => array(), 35 | 'folder' => array( 36 | 'subfolder' => array( 37 | 'subfolderphp.php' => '1', 38 | ), 39 | 'folderphp.php' => '1', 40 | 'folderphp.test' => '1', 41 | ), 42 | 'topphp.php' => '1', 43 | ), 44 | array( 45 | array('vfs://root/folder/subfolder'), 46 | ), 47 | array( 48 | 'folderphp.php', 49 | 'folderphp.test', 50 | 'topphp.php', 51 | ), 52 | ), 53 | array( 54 | array( 55 | 'empty' => array(), 56 | 'folder' => array( 57 | 'subfolder' => array( 58 | 'subfolderphp.php' => '1', 59 | ), 60 | 'folderphp.php' => '1', 61 | 'folderphp.test' => '1', 62 | ), 63 | 'topphp.php' => '1', 64 | ), 65 | array( 66 | array(), 67 | ), 68 | array( 69 | 'subfolderphp.php', 70 | 'folderphp.php', 71 | 'folderphp.test', 72 | 'topphp.php', 73 | ), 74 | ), 75 | array( 76 | array( 77 | 'empty' => array( 78 | 'empty.php' => 'empty', 79 | ), 80 | 'folder' => array( 81 | 'subfolder' => array( 82 | 'subfolderphp.php' => '1', 83 | ), 84 | 'folderphp.php' => '1', 85 | 'folderphp.test' => '1', 86 | ), 87 | 'topphp.php' => '1', 88 | ), 89 | array( 90 | array('vfs://root/folder', 'vfs://root/empty'), 91 | ), 92 | array( 93 | 'topphp.php', 94 | ), 95 | ), 96 | ); 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function getIteratorClass() 103 | { 104 | return '\\Sstalle\\php7cc\\Iterator\\ExcludedPathFilteringRecursiveIterator'; 105 | } 106 | 107 | /** 108 | * {@inheritdoc} 109 | */ 110 | public function getDefaultConstructorArguments() 111 | { 112 | return array(array()); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/CLIResultPrinter.php: -------------------------------------------------------------------------------- 1 | null, 14 | Message::LEVEL_WARNING => 'yellow', 15 | Message::LEVEL_ERROR => 'red', 16 | ); 17 | 18 | /** 19 | * @var CLIOutputInterface 20 | */ 21 | protected $output; 22 | 23 | /** 24 | * @var StandardPrettyPrinter 25 | */ 26 | protected $prettyPrinter; 27 | 28 | /** 29 | * @var NodeStatementsRemover 30 | */ 31 | protected $nodeStatementsRemover; 32 | 33 | /** 34 | * @param CLIOutputInterface $output 35 | * @param StandardPrettyPrinter $prettyPrinter 36 | * @param NodeStatementsRemover $nodeStatementsRemover 37 | */ 38 | public function __construct( 39 | CLIOutputInterface $output, 40 | StandardPrettyPrinter $prettyPrinter, 41 | NodeStatementsRemover $nodeStatementsRemover 42 | ) { 43 | $this->output = $output; 44 | $this->prettyPrinter = $prettyPrinter; 45 | $this->nodeStatementsRemover = $nodeStatementsRemover; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function printContext(ContextInterface $context) 52 | { 53 | $this->output->writeln(''); 54 | $this->output->writeln(sprintf('File: %s', $context->getCheckedResourceName())); 55 | 56 | foreach ($context->getMessages() as $message) { 57 | $this->output->writeln( 58 | $this->formatMessage($message) 59 | ); 60 | } 61 | 62 | foreach ($context->getErrors() as $error) { 63 | $this->output->writeln( 64 | sprintf( 65 | '> %s', 66 | $error->getText() 67 | ) 68 | ); 69 | } 70 | 71 | $this->output->writeln(''); 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function printMetadata(CheckMetadata $metadata) 78 | { 79 | $checkedFileCount = $metadata->getCheckedFileCount(); 80 | $elapsedTime = $metadata->getElapsedTime(); 81 | 82 | $this->output->writeln( 83 | sprintf( 84 | 'Checked %d file%s in %.3f second%s', 85 | $checkedFileCount, 86 | $checkedFileCount > 1 ? 's' : '', 87 | $elapsedTime, 88 | $elapsedTime > 1 ? 's' : '' 89 | ) 90 | ); 91 | } 92 | 93 | /** 94 | * @param Message $message 95 | * 96 | * @return string 97 | */ 98 | private function formatMessage(Message $message) 99 | { 100 | $nodes = $this->nodeStatementsRemover->removeInnerStatements($message->getNodes()); 101 | $prettyPrintedNodes = str_replace("\n", "\n ", $this->prettyPrinter->prettyPrint($nodes)); 102 | 103 | $text = $message->getRawText(); 104 | $color = self::$colors[$message->getLevel()]; 105 | 106 | if ($color) { 107 | $text = sprintf( 108 | '%s', 109 | $color, 110 | $text, 111 | $color 112 | ); 113 | } 114 | 115 | return sprintf( 116 | "> Line %s: %s\n %s", 117 | $message->getLine(), 118 | $text, 119 | $prettyPrintedNodes 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /test/code/Iterator/AbstractFilteringIteratorTest.php: -------------------------------------------------------------------------------- 1 | url(), 17 | \RecursiveDirectoryIterator::CURRENT_AS_FILEINFO 18 | | \RecursiveDirectoryIterator::KEY_AS_PATHNAME 19 | | \RecursiveDirectoryIterator::SKIP_DOTS 20 | | \RecursiveDirectoryIterator::UNIX_PATHS 21 | ); 22 | 23 | $iteratorClassReflection = new \ReflectionClass($this->getIteratorClass()); 24 | /** @var \RecursiveFilterIterator $filteringIterator */ 25 | array_unshift($filterArguments, $directoryIterator); 26 | $filteringIterator = $iteratorClassReflection->newInstanceArgs($filterArguments); 27 | $actualResult = array(); 28 | 29 | foreach ( 30 | new \RecursiveIteratorIterator($filteringIterator, \RecursiveIteratorIterator::LEAVES_ONLY) 31 | as $fileName 32 | ) { 33 | $actualResult[] = pathinfo($fileName, PATHINFO_BASENAME); 34 | } 35 | 36 | $this->assertEquals($expectedResult, $actualResult); 37 | } 38 | 39 | /** 40 | * @dataProvider throwsExceptionForInnerIteratorInvalidFlagsProvider 41 | */ 42 | public function testExceptionsForInnerIteratorFlags($flags, $expectException) 43 | { 44 | $dir = vfsStream::setup('root', null, array()); 45 | $directoryIterator = new \RecursiveDirectoryIterator( 46 | $dir->url(), 47 | $flags 48 | ); 49 | 50 | if ($expectException) { 51 | $this->setExpectedException('\\InvalidArgumentException'); 52 | } 53 | 54 | $iteratorClassReflection = new \ReflectionClass($this->getIteratorClass()); 55 | $iteratorClassReflection->newInstanceArgs( 56 | array_merge(array($directoryIterator), $this->getDefaultConstructorArguments()) 57 | ); 58 | } 59 | 60 | public function throwsExceptionForInnerIteratorInvalidFlagsProvider() 61 | { 62 | return array( 63 | array( 64 | \RecursiveDirectoryIterator::CURRENT_AS_PATHNAME 65 | | \RecursiveDirectoryIterator::KEY_AS_PATHNAME, 66 | true, 67 | ), 68 | array( 69 | \RecursiveDirectoryIterator::CURRENT_AS_SELF 70 | | \RecursiveDirectoryIterator::KEY_AS_PATHNAME, 71 | true, 72 | ), 73 | array( 74 | \RecursiveDirectoryIterator::CURRENT_AS_FILEINFO 75 | | \RecursiveDirectoryIterator::KEY_AS_FILENAME, 76 | true, 77 | ), 78 | array( 79 | \RecursiveDirectoryIterator::CURRENT_AS_PATHNAME 80 | | \RecursiveDirectoryIterator::KEY_AS_FILENAME, 81 | true, 82 | ), 83 | array( 84 | \RecursiveDirectoryIterator::CURRENT_AS_FILEINFO 85 | | \RecursiveDirectoryIterator::KEY_AS_PATHNAME, 86 | false, 87 | ), 88 | ); 89 | } 90 | 91 | /** 92 | * @return array 93 | */ 94 | abstract public function filterFilesProvider(); 95 | 96 | /** 97 | * @return string 98 | */ 99 | abstract public function getIteratorClass(); 100 | 101 | /** 102 | * @return array 103 | */ 104 | abstract public function getDefaultConstructorArguments(); 105 | } 106 | -------------------------------------------------------------------------------- /test/code/NodeAnalyzer/FunctionAnalyzerTest.php: -------------------------------------------------------------------------------- 1 | assertSame($this->functionAnalyzer->isFunctionCallByStaticName($node, 'foo'), false); 22 | } 23 | 24 | public function testIsFunctionCallByStaticNameReturnsFalseForDynamicName() 25 | { 26 | $node = new FuncCall(new Variable('foo')); 27 | $this->assertSame($this->functionAnalyzer->isFunctionCallByStaticName($node, 'foo'), false); 28 | } 29 | 30 | /** 31 | * @dataProvider isFunctionCallByStaticNameChecksLowercaseFunctionNameCorrectlyProvider 32 | */ 33 | public function testIsFunctionCallByStaticNameChecksLowercaseFunctionNameCorrectly($node, $checkedFunctionNames, $result) 34 | { 35 | $this->assertSame($this->functionAnalyzer->isFunctionCallByStaticName($node, $checkedFunctionNames), $result); 36 | } 37 | 38 | /** 39 | * @dataProvider isFunctionCallByStaticNameChecksMixedCaseFunctionNameCorrectlyProvider 40 | */ 41 | public function testIsFunctionCallByStaticNameChecksMixedCaseFunctionNameCorrectly($node, $checkedFunctionNames, $result) 42 | { 43 | $this->assertSame($this->functionAnalyzer->isFunctionCallByStaticName($node, $checkedFunctionNames), $result); 44 | } 45 | 46 | public function isFunctionCallByStaticNameChecksLowercaseFunctionNameCorrectlyProvider() 47 | { 48 | return array( 49 | array( 50 | $this->buildFuncCallNodeWithStaticName('foo'), 51 | 'foo', 52 | true, 53 | ), 54 | array( 55 | $this->buildFuncCallNodeWithStaticName('foo'), 56 | array('foo' => true), 57 | true, 58 | ), 59 | array( 60 | $this->buildFuncCallNodeWithStaticName('bar'), 61 | array('foo' => true, 'bar' => true), 62 | true, 63 | ), 64 | array( 65 | $this->buildFuncCallNodeWithStaticName('foo'), 66 | 'bar', 67 | false, 68 | ), 69 | array( 70 | $this->buildFuncCallNodeWithStaticName('foo'), 71 | array('bar' => true), 72 | false, 73 | ), 74 | array( 75 | $this->buildFuncCallNodeWithStaticName('baz'), 76 | array('foo' => true, 'bar' => true), 77 | false, 78 | ), 79 | ); 80 | } 81 | 82 | public function isFunctionCallByStaticNameChecksMixedCaseFunctionNameCorrectlyProvider() 83 | { 84 | return array( 85 | array( 86 | $this->buildFuncCallNodeWithStaticName('fOo'), 87 | 'foo', 88 | true, 89 | ), 90 | array( 91 | $this->buildFuncCallNodeWithStaticName('FoO'), 92 | array('foo' => true), 93 | true, 94 | ), 95 | ); 96 | } 97 | 98 | /** 99 | * @param string $name 100 | * 101 | * @return FuncCall 102 | */ 103 | protected function buildFuncCallNodeWithStaticName($name) 104 | { 105 | return new FuncCall(new Name(array($name))); 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | protected function setUp() 112 | { 113 | $this->functionAnalyzer = new FunctionAnalyzer(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Token/TokenCollection.php: -------------------------------------------------------------------------------- 1 | $rawToken) { 20 | if (is_array($rawToken) && count($rawToken) < 3) { 21 | throw new \InvalidArgumentException(sprintf('Array token at index %d has less than 3 elements', $i)); 22 | } 23 | } 24 | 25 | $this->tokens = $rawTokens; 26 | } 27 | 28 | /** 29 | * @param int $tokenPosition 30 | * 31 | * @return string 32 | */ 33 | public function getTokenStringValueAt($tokenPosition) 34 | { 35 | if (!isset($this->tokens[$tokenPosition])) { 36 | throw new \OutOfBoundsException(sprintf('Token at offset %d does not exist', $tokenPosition)); 37 | } 38 | 39 | $originalToken = $this->tokens[$tokenPosition]; 40 | 41 | return is_string($originalToken) ? $originalToken : $originalToken[static::TOKEN_ORIGINAL_VALUE_OFFSET]; 42 | } 43 | 44 | /** 45 | * @param int $tokenPosition 46 | * @param string $stringValue 47 | * 48 | * @return bool 49 | */ 50 | public function isTokenEqualTo($tokenPosition, $stringValue) 51 | { 52 | return $this->getTokenStringValueAt($tokenPosition) === $stringValue; 53 | } 54 | 55 | /** 56 | * @param int $tokenPosition 57 | * @param string $stringValue 58 | * 59 | * @return bool 60 | */ 61 | public function isTokenPrecededBy($tokenPosition, $stringValue) 62 | { 63 | return $this->isNextNonWhitespaceTokenEqualTo($tokenPosition, $stringValue, false); 64 | } 65 | 66 | /** 67 | * @param int $tokenPosition 68 | * @param string $stringValue 69 | * 70 | * @return bool 71 | */ 72 | public function isTokenFollowedBy($tokenPosition, $stringValue) 73 | { 74 | return $this->isNextNonWhitespaceTokenEqualTo($tokenPosition, $stringValue, true); 75 | } 76 | 77 | /** 78 | * @param int $tokenPosition 79 | * @param string $stringValue 80 | * 81 | * @return bool 82 | */ 83 | public function isTokenEqualToOrPrecededBy($tokenPosition, $stringValue) 84 | { 85 | return $this->isTokenEqualTo($tokenPosition, $stringValue) 86 | || $this->isTokenPrecededBy($tokenPosition, $stringValue); 87 | } 88 | 89 | /** 90 | * @param int $tokenPosition 91 | * @param int $stringValue 92 | * 93 | * @return bool 94 | */ 95 | public function isTokenEqualToOrFollowedBy($tokenPosition, $stringValue) 96 | { 97 | return $this->isTokenEqualTo($tokenPosition, $stringValue) 98 | || $this->isTokenFollowedBy($tokenPosition, $stringValue); 99 | } 100 | 101 | /** 102 | * Whitespace tokens are ignored when $stringValue is not whitespace. 103 | * 104 | * @param int $tokenPosition 105 | * @param string $stringValue 106 | * @param bool $scanForward Scan forward if true, otherwise backward 107 | * 108 | * @return bool 109 | */ 110 | protected function isNextNonWhitespaceTokenEqualTo($tokenPosition, $stringValue, $scanForward) 111 | { 112 | $ignoreWhitespace = !ctype_space($stringValue); 113 | 114 | while (isset($this->tokens[$scanForward ? ++$tokenPosition : --$tokenPosition])) { 115 | $currentTokenString = $this->getTokenStringValueAt($tokenPosition); 116 | if ($ignoreWhitespace && ctype_space($currentTokenString)) { 117 | continue; 118 | } 119 | 120 | return $stringValue === $currentTokenString; 121 | } 122 | 123 | return false; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, **thank you** for contributing, **you are awesome**! 4 | 5 | This project uses the fork & pull model of development. This means that in order to contribute 6 | you need to submit a [pull request](https://help.github.com/articles/using-pull-requests/). 7 | 8 | For relatively small features, improvements and bug fixes ([example](https://github.com/sstalle/php7cc/commit/a9f40a363fab2b24506465f8849a82cb3542739a)), 9 | you can submit pull requests without prior discussion. If you are planning on doing something that requires 10 | a lot of changes and/or big refactoring ([example](https://github.com/sstalle/php7cc/commit/600f0f9848af1f5ab631114304e0683d512f532b)), 11 | please open an issue first so it can be thoroughly considered and examined. 12 | 13 | Here are a few rules to follow in order to ease code reviews and discussions before 14 | maintainers accept and merge your work: 15 | 16 | * [Follow the coding standards](#coding-standards) 17 | * [Run and update the tests](#running-and-updating-test-suite) 18 | * [Document your work](#documenting-your-work) 19 | 20 | Please [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing) 21 | before submitting your Pull Request. One may ask you to [squash your 22 | commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) 23 | too. This is used to "clean" your Pull Request before merging it (we don't want 24 | commits such as `fix tests`, `fix 2`, `fix 3`, etc.). 25 | 26 | 27 | ## Coding standards 28 | You MUST follow the [PSR-1](http://www.php-fig.org/psr/1/), 29 | [PSR-2](http://www.php-fig.org/psr/2/) and 30 | [Symfony Coding Standard](http://symfony.com/doc/current/contributing/code/standards.html) 31 | (the only exception is that you must add single spaces around the concatenation operator). 32 | If you don't know about any of them, you should really read the recommendations. 33 | 34 | To fix your code according to the project standards, you can run 35 | [PHP-CS-Fixer tool](http://cs.sensiolabs.org/) before commit: 36 | ```bash 37 | vendor/bin/php-cs-fixer fix . --config-file=.php_cs 38 | ``` 39 | 40 | 41 | ## Running and updating test suite 42 | * You MUST run the test suite. 43 | * You MUST write tests for PHP 7 compatibility errors. 44 | * You SHOULD write (or update) unit tests for any other non-trivial functionality. 45 | 46 | Test suite can be run using the following command: 47 | ```bash 48 | vendor/bin/phpunit 49 | ``` 50 | 51 | In most cases you should not write unit tests for compatibility violation checking visitors (like the ones found 52 | in ```src/NodeVisitor```). To test them, you should create a subfolder in ```test/resource``` 53 | folder and put a ```.test``` file in it. ```.test``` files have multiple sections separated by 54 | `-----`: 55 | 56 | 1. First section is the description of the test suite. It can also contain PHP version constraint. 57 | 2. Second section is the php code to be tested. It must be syntactically correct, unless it is an expression 58 | or a statement that had been correct in PHP 5 but is no longer correct in PHP 7. 59 | 3. Third section is a newline separated array of messages and errors that 60 | should be emitted for the code from the previous section. Errors are instances of 61 | \Exception and \ParseException that are thrown during the checks. If there should be no messages 62 | and no errors, just leave a blank like in this section. Please keep in mind that test suites are 63 | not isolated, so you may get messages from other checkers in your test suite. 64 | 65 | Second and third sections can be repeated one or more times. 66 | 67 | Some tests require a particular version of PHP. For example, the `yield` keyword 68 | had been introduced in PHP 5.5, and tests containing it cannot be run on the lower versions. 69 | To specify a version constraint for the test suite, add a new line of the following format 70 | to the first section: 71 | ``` 72 | PHP 73 | ``` 74 | Operator is one of the operators supported by `version_compare` function. Multiple space separated 75 | constraints can be specified. 76 | 77 | 78 | ## Documenting your work 79 | You SHOULD write documentation for the code you add. 80 | 81 | Also, please, write [commit messages that make 82 | sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 83 | While creating your Pull Request on GitHub, you MUST write a description 84 | which gives the context and/or explains why you are creating it. 85 | 86 | Thank you! 87 | -------------------------------------------------------------------------------- /test/code/ExcludedPathCanonicalizerTest.php: -------------------------------------------------------------------------------- 1 | canonicalize(false, $checkedPaths, $excludedPaths, $expectedPaths); 23 | } 24 | 25 | /** 26 | * @dataProvider canonicalizeRelativePathsProvider 27 | */ 28 | public function testCanonicalizeRelativePaths($checkedPaths, $excludedPaths, $expectedPaths) 29 | { 30 | $this->canonicalize(true, $checkedPaths, $excludedPaths, $expectedPaths); 31 | } 32 | 33 | protected function canonicalize( 34 | $isDirectoryRelative, 35 | $checkedPaths, 36 | $excludedPaths, 37 | $expectedPaths 38 | ) { 39 | $stub = $this->getMockBuilder('Sstalle\\php7cc\\Helper\\Path\\PathHelperInterface') 40 | ->getMock(); 41 | $stub->method('isDirectoryRelative') 42 | ->willReturn($isDirectoryRelative); 43 | $canonicalizer = new ExcludedPathCanonicalizer($stub); 44 | 45 | $this->assertEquals($expectedPaths, $canonicalizer->canonicalize($checkedPaths, $excludedPaths)); 46 | } 47 | 48 | public function canonicalizeAbsolutePathsProvider() 49 | { 50 | return array( 51 | array( 52 | array('/foo'), 53 | array(), 54 | array(), 55 | ), 56 | array( 57 | array('/foo', '/bar'), 58 | array('baz'), 59 | array('baz'), 60 | ), 61 | array( 62 | array('/foo', '/bar'), 63 | array('baz', 'quux'), 64 | array('baz', 'quux'), 65 | ), 66 | array( 67 | array(), 68 | array('bar', 'baz'), 69 | array('bar', 'baz'), 70 | ), 71 | array( 72 | array('foo', 'bar'), 73 | array('baz', 'quux'), 74 | array('baz', 'quux'), 75 | ), 76 | ); 77 | } 78 | 79 | public function canonicalizeRelativePathsProvider() 80 | { 81 | return array( 82 | array( 83 | array('/foo'), 84 | array(), 85 | array(), 86 | ), 87 | array( 88 | array('/foo'), 89 | array('bar'), 90 | array( 91 | $this->implodeWithDirectorySeparator(array('/foo', 'bar')), 92 | ), 93 | ), 94 | array( 95 | array('/foo', '/bar'), 96 | array('baz'), 97 | array( 98 | $this->implodeWithDirectorySeparator(array('/foo', 'baz')), 99 | $this->implodeWithDirectorySeparator(array('/bar', 'baz')), 100 | ), 101 | ), 102 | array( 103 | array('/foo'), 104 | array('bar', 'baz'), 105 | array( 106 | $this->implodeWithDirectorySeparator(array('/foo', 'bar')), 107 | $this->implodeWithDirectorySeparator(array('/foo', 'baz')), 108 | ), 109 | ), 110 | array( 111 | array('/foo', '/bar'), 112 | array('baz', 'quux'), 113 | array( 114 | $this->implodeWithDirectorySeparator(array('/foo', 'baz')), 115 | $this->implodeWithDirectorySeparator(array('/bar', 'baz')), 116 | $this->implodeWithDirectorySeparator(array('/foo', 'quux')), 117 | $this->implodeWithDirectorySeparator(array('/bar', 'quux')), 118 | ), 119 | ), 120 | array( 121 | array('foo', 'bar'), 122 | array('baz', 'quux'), 123 | array(), 124 | ), 125 | array( 126 | array('foo'), 127 | array('baz'), 128 | array(), 129 | ), 130 | ); 131 | } 132 | 133 | protected function implodeWithDirectorySeparator(array $pieces) 134 | { 135 | return implode(DIRECTORY_SEPARATOR, $pieces); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/NodeVisitor/RemovedFunctionCallVisitor.php: -------------------------------------------------------------------------------- 1 | functionAnalyzer = $functionAnalyzer; 149 | $this->removedFunctionNames = array_flip($this->removedFunctionNames); 150 | } 151 | 152 | public function enterNode(Node $node) 153 | { 154 | if (!$this->functionAnalyzer->isFunctionCallByStaticName($node, $this->removedFunctionNames)) { 155 | return; 156 | } 157 | 158 | /** @var Node\Expr\FuncCall $node */ 159 | $this->addContextMessage( 160 | sprintf('Removed function "%s" called', $node->name->toString()), 161 | $node 162 | ); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Infrastructure/PHP7CCCommand.php: -------------------------------------------------------------------------------- 1 | Message::LEVEL_INFO, 30 | 'warning' => Message::LEVEL_WARNING, 31 | 'error' => Message::LEVEL_ERROR, 32 | ); 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function configure() 38 | { 39 | $this->setName(static::COMMAND_NAME) 40 | ->setDescription('Checks PHP 5.3 - 5.6 code for compatibility with PHP7') 41 | ->addArgument( 42 | static::PATHS_ARGUMENT_NAME, 43 | InputArgument::REQUIRED | InputArgument::IS_ARRAY, 44 | 'Which file or directory do you want to check?' 45 | )->addOption( 46 | static::EXTENSIONS_OPTION_NAME, 47 | 'e', 48 | InputOption::VALUE_OPTIONAL, 49 | 'Which file extensions do you want to check (separate multiple extensions with commas)?', 50 | 'php' 51 | )->addOption( 52 | static::EXCEPT_OPTION_NAME, 53 | 'x', 54 | InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 55 | 'Excluded files and directories', 56 | array() 57 | )->addOption( 58 | static::MESSAGE_LEVEL_OPTION_NAME, 59 | 'l', 60 | InputOption::VALUE_REQUIRED, 61 | 'Only show messages having this or higher severity level (can be info, message or warning)', 62 | 'info' 63 | )->addOption( 64 | static::RELATIVE_PATHS_OPTION_NAME, 65 | 'r', 66 | InputOption::VALUE_NONE, 67 | 'Output paths relative to a checked directory instead of full paths to files' 68 | )->addOption( 69 | static::INT_SIZE_OPTION_NAME, 70 | null, 71 | InputOption::VALUE_REQUIRED, 72 | 'Target system\'s integer size in bits (needed for bitwise shift checks)', 73 | BitwiseShiftVisitor::MIN_INT_SIZE 74 | ); 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | protected function execute(InputInterface $input, OutputInterface $output) 81 | { 82 | $paths = $input->getArgument(static::PATHS_ARGUMENT_NAME); 83 | foreach ($paths as $path) { 84 | if (!is_file($path) && !is_dir($path)) { 85 | $output->writeln(sprintf('Path %s must be a file or a directory', $path)); 86 | 87 | return; 88 | } 89 | } 90 | 91 | $extensionsArgumentValue = $input->getOption(static::EXTENSIONS_OPTION_NAME); 92 | $extensions = explode(',', $extensionsArgumentValue); 93 | if (!is_array($extensions)) { 94 | $output->writeln( 95 | sprintf( 96 | 'Something went wrong while parsing file extensions you specified. ' . 97 | 'Check that %s is a comma-separated list of extensions', 98 | $extensionsArgumentValue 99 | ) 100 | ); 101 | 102 | return; 103 | } 104 | 105 | $messageLevelName = $input->getOption(static::MESSAGE_LEVEL_OPTION_NAME); 106 | if (!isset(static::$messageLevelMap[$messageLevelName])) { 107 | $output->writeln(sprintf('Unknown message level %s', $messageLevelName)); 108 | 109 | return; 110 | } 111 | $messageLevel = static::$messageLevelMap[$messageLevelName]; 112 | 113 | $intSize = (int) $input->getOption(static::INT_SIZE_OPTION_NAME); 114 | if ($intSize <= 0) { 115 | $output->writeln('Integer size must be greater than 0'); 116 | 117 | return; 118 | } 119 | 120 | $containerBuilder = new ContainerBuilder(); 121 | $container = $containerBuilder->buildContainer($output, $intSize); 122 | 123 | $checkSettings = new PathCheckSettings($paths, $extensions); 124 | $checkSettings->setExcludedPaths($input->getOption(static::EXCEPT_OPTION_NAME)); 125 | $checkSettings->setMessageLevel($messageLevel); 126 | $checkSettings->setUseRelativePaths($input->getOption(static::RELATIVE_PATHS_OPTION_NAME)); 127 | 128 | $container['pathCheckExecutor']->check($checkSettings); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP 7 Compatibility Checker(php7cc) 2 | #### Introduction 3 | php7cc is a command line tool designed to make migration from PHP 5.3-5.6 to PHP 7 easier. 4 | It searches for potentially troublesome statements in existing code and generates reports containing 5 | file names, line numbers and short problem descriptions. It does not automatically fix 6 | code to work with the new PHP version. 7 | 8 | #### What kind of problems does it detect? 9 | There are 2 types of issues reported by php7cc: 10 | 11 | 1. **Errors** that will definitely cause some kind of trouble (a fatal, a syntax error, a notice, etc.) on PHP 7. These are highlighted in red. 12 | 2. **Warnings** that may or may not lead to logical errors. For example, statements that are legal in both PHP 5 and PHP 7, but change their behaviour between versions fall into this category. Warnings are highlighted in yellow. 13 | 14 | A list of statements that may cause errors or warnings to be reported can be found in the [php-src repository](https://github.com/php/php-src/blob/PHP-7.0/UPGRADING). 15 | 16 | ***Although php7cc tries to detect as much problems as accurately as possible, sometimes 100% reliable detection 17 | is very hard to achieve. That's why you should also run a comprehensive test suite for the code 18 | you are going to migrate.*** 19 | 20 | # Prerequisites 21 | To run php7cc, you need php installed, minimum required version is 5.3.3. PHP 7 is supported, 22 | but files with syntax errors (for example, invalid numeric literals 23 | or invalid UTF-8 codepoint escape sequences) can't be processed. You will only get the 24 | warning message about the first syntax error for such files. 25 | 26 | You may also need [composer](https://getcomposer.org/) to install php7cc. 27 | 28 | # Installation 29 | #### Phar package 30 | You can download a phar package for any stable version from the Github 31 | [releases](https://github.com/sstalle/php7cc/releases) page. 32 | 33 | #### Composer (globally) 34 | Make sure you have composer installed. Then execute the following command: 35 | ```bash 36 | composer global require sstalle/php7cc 37 | ``` 38 | It is also recommended to add ```~/.composer/vendor/bin``` to your ```PATH``` environment 39 | variable: 40 | ```bash 41 | export PATH="$PATH:$HOME/.composer/vendor/bin" 42 | ``` 43 | This makes it possible to run php7cc by entering just the executable name. 44 | 45 | #### Composer (locally, per project) 46 | Make sure you have composer installed. Then execute the following command from your project 47 | directory: 48 | ```bash 49 | composer require sstalle/php7cc --dev 50 | ``` 51 | 52 | #### Docker image 53 | A docker image is available on [Docker Hub](https://hub.docker.com/r/ypereirareis/php7cc/) 54 | (contributed and maintained by [ypereirareis](https://github.com/ypereirareis)). 55 | 56 | # Usage 57 | Examples in this section assume that you have installed php7cc globally using composer 58 | and that you have added it's vendor binaries directory to your ```PATH```. If this is not 59 | the case, just substitute ```php7cc``` with the correct path to the binary of phar package. 60 | For local per project installation the executable will be located at ```/vendor/bin/php7cc```. 61 | 62 | #### Getting help 63 | To see the full list of available options, run: 64 | ```bash 65 | php7cc --help 66 | ``` 67 | 68 | #### Checking a single file or directory 69 | To check a file or a directory, pass its name as the first argument. Directories are checked 70 | recursively. 71 | 72 | So, to check a file you could run: 73 | ```bash 74 | php7cc /path/to/my/file.php 75 | ``` 76 | To check a directory: 77 | ```bash 78 | php7cc /path/to/my/directory/ 79 | ``` 80 | 81 | #### Specifying file extensions to check 82 | When checking a directory, you can also specify a comma-separated list of file extensions that 83 | should be checked. By default, only .php files are processed. 84 | 85 | For example, if you want to check .php, .inc and .lib files, you could run: 86 | ```bash 87 | php7cc --extensions=php,inc,lib /path/to/my/directory/ 88 | ``` 89 | 90 | #### Excluding file or directories 91 | You can specify a list of absolute or relative paths to exclude from checking. 92 | Relative paths are relative to the checked directories. 93 | 94 | So, if you want to exclude vendor and test directories, you could run: 95 | ```bash 96 | php7cc --except=vendor --except=/path/to/my/directory/test /path/to/my/directory/ 97 | ``` 98 | In this example, directories ```/path/to/my/directory/vendor```, ```/path/to/my/directory/test``` and their contents will not be checked. 99 | 100 | #### Specifying minimum issue level 101 | If you set a minimum issue level, only issues having that or higher severity level will be 102 | reported by `php7cc`. There are 3 issue levels: "info", "warning" and "error". "info" is 103 | reserved for future use and is the same as "warning". 104 | 105 | Example usage: 106 | ```bash 107 | php7cc --level=error /path/to/my/directory/ 108 | ``` 109 | Only errors, but not warnings will be shown in this case. 110 | 111 | # Troubleshooting 112 | #### Maximum function nesting level of 100/250/N reached, aborting! 113 | You should increase maximum function nesting level in your PHP or Xdebug config file like this: 114 | ```cfg 115 | xdebug.max_nesting_level = 1000 116 | ``` 117 | 118 | #### Allowed memory size of N bytes exhausted 119 | You should increase amount of memory available to CLI PHP scripts or disable PHP memory limit. 120 | The latter can be done by setting the `memory_limit` PHP option to -1. This option can be set by editing 121 | `php.ini` or by passing a command-line argument to PHP executable like this: 122 | ```bash 123 | php -d memory_limit=-1 php7cc.php /path/to/my/directory 124 | ``` 125 | 126 | # Other useful links 127 | #### Contributing 128 | Please read the [contributing guidelines](CONTRIBUTING.md). 129 | #### Credits 130 | [The list of contributors](https://github.com/sstalle/php7cc/graphs/contributors) is available on the corresponding 131 | Github page. 132 | -------------------------------------------------------------------------------- /src/NodeVisitor/ForeachVisitor.php: -------------------------------------------------------------------------------- 1 | functionAnalyzer = $functionAnalyzer; 51 | $this->foreachStack = new \SplStack(); 52 | $this->arrayPointerModifyingFunctions = array_flip($this->arrayPointerModifyingFunctions); 53 | $this->arrayModifyingFunctions = array_flip($this->arrayModifyingFunctions); 54 | } 55 | 56 | public function enterNode(Node $node) 57 | { 58 | if ($node instanceof Node\Stmt\Foreach_) { 59 | $this->checkNestedByReferenceForeach($node); 60 | $this->foreachStack->push($node); 61 | } elseif (!$this->foreachStack->isEmpty()) { 62 | $this->checkInternalArrayPointerAccessInByValueForeach($node); 63 | $this->checkArrayModificationByFunctionInByReferenceForeach($node); 64 | $this->checkAddingToArrayInByReferenceForeach($node); 65 | } 66 | } 67 | 68 | public function leaveNode(Node $node) 69 | { 70 | if ($node instanceof Node\Stmt\Foreach_) { 71 | $this->foreachStack->pop(); 72 | } 73 | } 74 | 75 | /** 76 | * @param Node $node 77 | */ 78 | protected function checkInternalArrayPointerAccessInByValueForeach(Node $node) 79 | { 80 | if ($this->hasFunctionCallWithForeachArgument($node, $this->arrayPointerModifyingFunctions, true)) { 81 | $this->addContextMessage( 82 | 'Possible internal array pointer access/modification in a by-value foreach loop', 83 | $node 84 | ); 85 | } 86 | } 87 | 88 | /** 89 | * @param Node $node 90 | */ 91 | protected function checkArrayModificationByFunctionInByReferenceForeach(Node $node) 92 | { 93 | if ($this->hasFunctionCallWithForeachArgument($node, $this->arrayModifyingFunctions, false)) { 94 | $this->addContextMessage( 95 | 'Possible array modification using internal function in a by-reference foreach loop', 96 | $node 97 | ); 98 | } 99 | } 100 | 101 | /** 102 | * @param Node $node 103 | * @param array $functions 104 | * @param null|bool $skippedByRefType Reference type (by value/by reference) to skip 105 | * 106 | * @return bool 107 | */ 108 | protected function hasFunctionCallWithForeachArgument(Node $node, array $functions, $skippedByRefType = null) 109 | { 110 | if (!$this->functionAnalyzer->isFunctionCallByStaticName($node, $functions)) { 111 | return false; 112 | } 113 | 114 | /** @var Node\Expr\FuncCall $node */ 115 | foreach ($node->args as $argument) { 116 | /** @var Node\Stmt\Foreach_ $foreach */ 117 | foreach ($this->foreachStack as $foreach) { 118 | if ($skippedByRefType !== null && $foreach->byRef === $skippedByRefType) { 119 | continue; 120 | } 121 | 122 | if ($argument->value instanceof Node\Expr\Variable 123 | && $argument->value->name === $this->getForeachVariableName($foreach) 124 | ) { 125 | return true; 126 | } 127 | } 128 | } 129 | 130 | return false; 131 | } 132 | 133 | /** 134 | * @param Node $node 135 | */ 136 | protected function checkAddingToArrayInByReferenceForeach(Node $node) 137 | { 138 | if (!($node instanceof Node\Expr\Assign || $node instanceof Node\Expr\AssignRef) 139 | || !$node->var instanceof Node\Expr\ArrayDimFetch || !$node->var->var instanceof Node\Expr\Variable 140 | ) { 141 | return; 142 | } 143 | 144 | /** @var Node\Stmt\Foreach_ $foreach */ 145 | foreach ($this->foreachStack as $foreach) { 146 | if (!$foreach->byRef) { 147 | continue; 148 | } 149 | 150 | if ($node->var->var->name === $this->getForeachVariableName($foreach)) { 151 | $this->addContextMessage( 152 | 'Possible adding to array on the last iteration of a by-reference foreach loop', 153 | $node 154 | ); 155 | } 156 | } 157 | } 158 | 159 | protected function checkNestedByReferenceForeach(Node\Stmt\Foreach_ $foreach) 160 | { 161 | if (!$foreach->byRef) { 162 | return; 163 | } 164 | 165 | /** @var Node\Stmt\Foreach_ $ancestorForeach */ 166 | foreach ($this->foreachStack as $ancestorForeach) { 167 | if ($ancestorForeach->byRef) { 168 | $this->addContextMessage( 169 | 'Nested by-reference foreach loop, make sure there is no iteration over the same array', 170 | $foreach 171 | ); 172 | 173 | return; 174 | } 175 | } 176 | } 177 | 178 | protected function getForeachVariableName(Node\Stmt\Foreach_ $foreach) 179 | { 180 | if ($foreach->expr instanceof Node\Expr\Variable) { 181 | return $foreach->expr->name; 182 | } elseif (($foreach->expr instanceof Node\Expr\Assign || $foreach->expr instanceof Node\Expr\AssignRef) 183 | && $foreach->expr->var instanceof Node\Expr\Variable 184 | ) { 185 | return $foreach->expr->var->name; 186 | } 187 | 188 | return; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /test/code/ContextCheckerTest.php: -------------------------------------------------------------------------------- 1 | buildContainer( 14 | new Symfony\Component\Console\Output\NullOutput(), 15 | \Sstalle\php7cc\NodeVisitor\BitwiseShiftVisitor::MIN_INT_SIZE 16 | ); 17 | $contextChecker = $container['contextChecker']; 18 | /** @var \PhpParser\NodeTraverserInterface $traverser */ 19 | $traverser = $container['traverser']; 20 | /** @var \Sstalle\php7cc\NodeVisitor\ResolverInterface $resolver */ 21 | $resolver = $container['nodeVisitorResolver']; 22 | $resolver->setLevel(\Sstalle\php7cc\CompatibilityViolation\Message::LEVEL_INFO); 23 | foreach ($resolver->resolve() as $visitor) { 24 | $traverser->addVisitor($visitor); 25 | } 26 | 27 | $context = new \Sstalle\php7cc\CompatibilityViolation\StringContext($code, 'test'); 28 | 29 | $contextChecker->checkContext($context); 30 | $expectedMessageCount = count($expectedMessages); 31 | $actualMessages = array_merge($context->getMessages(), $context->getErrors()); 32 | $actualMessageCount = count($actualMessages); 33 | $this->assertEquals($expectedMessageCount, $actualMessageCount, $name); 34 | if ($expectedMessageCount == $actualMessageCount) { 35 | /** @var \Sstalle\php7cc\AbstractBaseMessage $message */ 36 | foreach ($actualMessages as $i => $message) { 37 | $this->assertEquals( 38 | $this->canonicalize($expectedMessages[$i]), 39 | $this->canonicalize($message->getRawText()), 40 | $name 41 | ); 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Copypasted from PhpParser\CodeTestAbstract. 48 | * 49 | * @return array 50 | */ 51 | public function messageProvider() 52 | { 53 | $it = new \RecursiveDirectoryIterator(__DIR__ . '/../resource'); 54 | $it = new \RecursiveIteratorIterator($it, \RecursiveIteratorIterator::LEAVES_ONLY); 55 | $it = new \RegexIterator($it, '(\.' . preg_quote('test') . '$)'); 56 | $tests = array(); 57 | foreach ($it as $file) { 58 | $fileName = realpath($file->getPathname()); 59 | $fileContents = file_get_contents($fileName); 60 | // parse sections 61 | $fileContents = explode('-----', $fileContents); 62 | $parts = array_map(function ($i, $section) { 63 | return $i % 2 != 0 ? $section : trim($section); 64 | }, array_keys($fileContents), $fileContents); 65 | // first part is the name 66 | 67 | $name = $this->canonicalize(array_shift($parts)); 68 | if ($this->containsVersionConstraint($name)) { 69 | if (!$this->satisfiesVersionConstraint($name)) { 70 | continue; 71 | } 72 | 73 | $name = $this->stripVersionConstraint($name); 74 | } 75 | 76 | $fullName = $name . ' (' . $fileName . ')'; 77 | // multiple sections possible with always two forming a pair 78 | foreach (array_chunk($parts, 2) as $chunk) { 79 | $messages = array_filter(explode("\n", $this->canonicalize($chunk[1]))); 80 | $tests[] = array($fullName, ltrim($chunk[0]), $messages); 81 | } 82 | } 83 | 84 | return $tests; 85 | } 86 | 87 | /** 88 | * @param string $name 89 | * 90 | * @return string 91 | */ 92 | protected function stripVersionConstraint($name) 93 | { 94 | $nameParts = explode("\n", $name); 95 | array_pop($nameParts); 96 | 97 | return implode("\n", $nameParts); 98 | } 99 | 100 | /** 101 | * @param string $name 102 | * 103 | * @return bool 104 | */ 105 | protected function containsVersionConstraint($name) 106 | { 107 | $nameParts = explode("\n", $name); 108 | 109 | return count($nameParts) > 1 && substr(end($nameParts), 0, 3) === 'PHP'; 110 | } 111 | 112 | /** 113 | * @param string $name 114 | * 115 | * @return bool 116 | */ 117 | protected function satisfiesVersionConstraint($name) 118 | { 119 | if ($this->containsVersionConstraint($name)) { 120 | $nameParts = explode("\n", $name); 121 | // last line contains version constraints 122 | $versionConstraints = array(); 123 | preg_match_all( 124 | '/\\s+(<|lt|<=|le|>|gt|>=|ge|==|=|eq|!=|<>|ne)([a-zA-Z0-9\\.\\-]+)/', 125 | end($nameParts), 126 | $versionConstraints 127 | ); 128 | 129 | if (!count(array_shift($versionConstraints))) { 130 | throw new \RuntimeException( 131 | sprintf( 132 | 'Version constraint %s was specified for test suite "%s" but no constraints could be extracted', 133 | end($nameParts), 134 | $this->stripVersionConstraint($name) 135 | ) 136 | ); 137 | }; 138 | 139 | foreach (range(0, count($versionConstraints[0]) - 1) as $constraintIndex) { 140 | if (!version_compare( 141 | PHP_VERSION, 142 | $versionConstraints[1][$constraintIndex], 143 | $versionConstraints[0][$constraintIndex] 144 | )) { 145 | return false; 146 | } 147 | } 148 | } 149 | 150 | return true; 151 | } 152 | 153 | /** 154 | * Copypasted from PhpParser\CodeTestAbstract. 155 | * 156 | * @param $str string 157 | * 158 | * @return string 159 | */ 160 | protected function canonicalize($str) 161 | { 162 | // trim from both sides 163 | $str = trim($str); 164 | // normalize EOL to \n 165 | $str = str_replace(array("\r\n", "\r"), "\n", $str); 166 | 167 | // trim right side of all lines 168 | return implode("\n", array_map('rtrim', explode("\n", $str))); 169 | } 170 | } 171 | --------------------------------------------------------------------------------