├── 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 |
--------------------------------------------------------------------------------