diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/Reader/SourceArgumentsReaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/Code/Reader/SourceArgumentsReaderTest.php new file mode 100644 index 0000000000000000000000000000000000000000..076d2ab396be5d7f8762f8acb1c500e80cb1c215 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/Reader/SourceArgumentsReaderTest.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\Code\Reader; + +require_once __DIR__ . '/_files/SourceArgumentsReaderTest.php.sample'; + +class SourceArgumentsReaderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var \Magento\Framework\Code\Reader\SourceArgumentsReader + */ + protected $sourceArgumentsReader; + + protected function setUp() + { + $this->sourceArgumentsReader = new \Magento\Framework\Code\Reader\SourceArgumentsReader(); + } + + /** + * @param string $class + * @param array $expectedResult + * @dataProvider getConstructorArgumentTypesDataProvider + */ + public function testGetConstructorArgumentTypes($class, $expectedResult) + { + $class = new \ReflectionClass($class); + $actualResult = $this->sourceArgumentsReader->getConstructorArgumentTypes($class); + $this->assertEquals($expectedResult, $actualResult); + } + + public function getConstructorArgumentTypesDataProvider() + { + return [ + [ + 'Some\Testing\Name\Space\AnotherSimpleClass', + [ + '\Some\Testing\Name\Space\Item', + '\Imported\Name\Space\One', + '\Imported\Name\Space\AnotherTest\Extended', + '\Imported\Name\Space\Test', + '\Imported\Name\Space\Object\Under\Test', + '\Imported\Name\Space\Object', + '\Some\Testing\Name\Space\Test', + 'array', + '' + ], + ], + [ + '\stdClass', + [null] + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Code/Reader/_files/SourceArgumentsReaderTest.php.sample b/dev/tests/integration/testsuite/Magento/Framework/Code/Reader/_files/SourceArgumentsReaderTest.php.sample new file mode 100644 index 0000000000000000000000000000000000000000..b2cb559cce5310f23f911a30f8eb6fd2a74a7f85 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Code/Reader/_files/SourceArgumentsReaderTest.php.sample @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Some\Testing\Name\Space; + +use Imported\Name\Space\One as FirstImport; +use Imported\Name\Space\Object; +use Imported\Name\Space\Test as Testing, \Imported\Name\Space\AnotherTest ; + +class AnotherSimpleClass +{ + public function __construct( + \Some\Testing\Name\Space\Item $itemOne, + FirstImport $itemTwo, + AnotherTest\Extended $itemThree, + Testing $itemFour, + Object\Under\Test $itemFive, + Object $itemSix, + Test $itemSeven, + array $itemEight = [], + $itemNine = 'test' + ) { + } +} \ No newline at end of file diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Di/CompilerTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Di/CompilerTest.php index 1ff0b7bea34a9f7da2efd9e84642f9ec57fa89be..0510c8cb4fec81748ad1b031a0ae9e31ee19a928 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Di/CompilerTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Di/CompilerTest.php @@ -103,6 +103,7 @@ class CompilerTest extends \PHPUnit_Framework_TestCase $this->_validator->add(new \Magento\Framework\Code\Validator\ContextAggregation()); $this->_validator->add(new \Magento\Framework\Code\Validator\TypeDuplication()); $this->_validator->add(new \Magento\Framework\Code\Validator\ArgumentSequence()); + $this->_validator->add(new \Magento\Framework\Code\Validator\ConstructorArgumentTypes()); $this->pluginValidator = new \Magento\Framework\Interception\Code\InterfaceValidator(); } diff --git a/dev/tests/unit/testsuite/Magento/Framework/Code/Validator/ConstructorArgumentTypesTest.php b/dev/tests/unit/testsuite/Magento/Framework/Code/Validator/ConstructorArgumentTypesTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0de3d403d5885e1c0bc2fca674fece3b5b8b069e --- /dev/null +++ b/dev/tests/unit/testsuite/Magento/Framework/Code/Validator/ConstructorArgumentTypesTest.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\Code\Validator; + +class ConstructorArgumentTypesTest extends \PHPUnit_Framework_TestCase +{ + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $argumentsReaderMock; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $sourceArgumentsReaderMock; + + /** + * @var \Magento\Framework\Code\Validator\ConstructorArgumentTypes + */ + protected $model; + + protected function setUp() + { + $this->argumentsReaderMock = $this->getMock( + '\Magento\Framework\Code\Reader\ArgumentsReader', + [], + [], + '', + false + ); + $this->sourceArgumentsReaderMock = $this->getMock( + '\Magento\Framework\Code\Reader\SourceArgumentsReader', + [], + [], + '', + false + ); + $this->model = new \Magento\Framework\Code\Validator\ConstructorArgumentTypes( + $this->argumentsReaderMock, + $this->sourceArgumentsReaderMock + ); + } + + public function testValidate() + { + $className = '\stdClass'; + $classMock = new \ReflectionClass($className); + $this->argumentsReaderMock->expects($this->once())->method('getConstructorArguments')->with($classMock) + ->willReturn([['name' => 'Name1', 'type' => '\Type'], ['name' => 'Name2', 'type' => '\Type2']]); + $this->sourceArgumentsReaderMock->expects($this->once())->method('getConstructorArgumentTypes') + ->with($classMock)->willReturn(['\Type', '\Type2']); + $this->assertTrue($this->model->validate($className)); + } + + /** + * @expectedException \Magento\Framework\Code\ValidationException + * @expectedExceptionMessage Invalid constructor argument(s) in \stdClass + */ + public function testValidateWithException() + { + $className = '\stdClass'; + $classMock = new \ReflectionClass($className); + $this->argumentsReaderMock->expects($this->once())->method('getConstructorArguments')->with($classMock) + ->willReturn([['name' => 'Name1', 'type' => '\FAIL']]); + $this->sourceArgumentsReaderMock->expects($this->once())->method('getConstructorArgumentTypes') + ->with($classMock)->willReturn(['\Type', '\Fail']); + $this->assertTrue($this->model->validate($className)); + } +} + + diff --git a/lib/internal/Magento/Framework/Code/Reader/SourceArgumentsReader.php b/lib/internal/Magento/Framework/Code/Reader/SourceArgumentsReader.php new file mode 100644 index 0000000000000000000000000000000000000000..c31fa9c00d8abd8a1620b08d0943dcb031e9bf13 --- /dev/null +++ b/lib/internal/Magento/Framework/Code/Reader/SourceArgumentsReader.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\Code\Reader; + +class SourceArgumentsReader +{ + /** + * Namespace separator + */ + const NS_SEPARATOR = '\\'; + + /** + * Read constructor argument types from source code and perform namespace resolution if required. + * + * @param \ReflectionClass $class + * @param bool $inherited + * @return array List of constructor argument types. + */ + public function getConstructorArgumentTypes(\ReflectionClass $class, $inherited = false) + { + $output = [null]; + if (!$class->getFileName() || false == $class->hasMethod( + '__construct' + ) || !$inherited && $class->getConstructor()->class !== $class->getName() + ) { + return $output; + } + $reflectionConstructor = $class->getConstructor(); + $fileContent = file($class->getFileName()); + $availableNamespaces = $this->getImportedNamespaces($fileContent); + $availableNamespaces[0] = $class->getNamespaceName(); + $constructorStartLine = $reflectionConstructor->getStartLine() - 1; + $constructorEndLine = $reflectionConstructor->getEndLine(); + $fileContent = array_slice($fileContent, $constructorStartLine, $constructorEndLine - $constructorStartLine); + $source = '<?php ' . trim(implode('', $fileContent)); + $methodTokenized = token_get_all($source); + $argumentsStart = array_search('(', $methodTokenized) + 1; + $argumentsEnd = array_search(')', $methodTokenized); + $arguments = array_slice($methodTokenized, $argumentsStart, $argumentsEnd - $argumentsStart); + foreach ($arguments as &$argument) { + is_array($argument) ?: $argument = [1 => $argument]; + } + unset($argument); + $arguments = array_filter($arguments, function ($token) { + $blacklist = [T_VARIABLE, T_WHITESPACE]; + if (isset($token[0]) && in_array($token[0], $blacklist)) { + return false; + } + return true; + }); + $arguments = array_map(function ($element) { + return $element[1]; + }, $arguments); + $arguments = array_values($arguments); + $arguments = implode('', $arguments); + if (empty($arguments)) { + return $output; + } + $arguments = explode(',', $arguments); + foreach ($arguments as $key => &$argument) { + $argument = $this->removeDefaultValue($argument); + $argument = $this->resolveNamespaces($argument, $availableNamespaces); + } + unset($argument); + return $arguments; + } + + /** + * Perform namespace resolution if required and return fully qualified name. + * + * @param string $argument + * @param array $availableNamespaces + * @return string + */ + protected function resolveNamespaces($argument, $availableNamespaces) + { + if (substr($argument, 0, 1) !== self::NS_SEPARATOR && $argument !== 'array' && !empty($argument)) { + $name = explode(self::NS_SEPARATOR, $argument); + $unqualifiedName = $name[0]; + $isQualifiedName = count($name) > 1 ? true : false; + if (isset($availableNamespaces[$unqualifiedName])) { + $namespace = $availableNamespaces[$unqualifiedName]; + if ($isQualifiedName) { + array_shift($name); + return $namespace . self::NS_SEPARATOR . implode(self::NS_SEPARATOR, $name); + } + return $namespace; + } else { + return self::NS_SEPARATOR . $availableNamespaces[0] . self::NS_SEPARATOR . $argument; + } + } + return $argument; + } + + /** + * Remove default value from argument. + * + * @param string $argument + * @return string + */ + protected function removeDefaultValue($argument) + { + $position = strpos($argument, '='); + if (is_numeric($position)) { + return substr($argument, 0, $position); + } + return $argument; + } + + /** + * Get all imported namespaces. + * + * @param array $file + * @return array + */ + protected function getImportedNamespaces(array $file) + { + $file = implode('', $file); + $file = token_get_all($file); + $classStart = array_search('{', $file); + $file = array_slice($file, 0, $classStart); + $output = []; + foreach ($file as $position => $token) { + if (is_array($token) && $token[0] === T_USE) { + $import = array_slice($file, $position); + $importEnd = array_search(';', $import); + $import = array_slice($import, 0, $importEnd); + $imports = []; + $importsCount = 0; + foreach ($import as $item) { + if ($item === ',') { + $importsCount++; + continue; + } + $imports[$importsCount][] = $item; + } + foreach ($imports as $import) { + $import = array_filter($import, function ($token) { + $whitelist = [T_NS_SEPARATOR, T_STRING, T_AS]; + if (isset($token[0]) && in_array($token[0], $whitelist)) { + return true; + } + return false; + }); + $import = array_map(function ($element) { + return $element[1]; + }, $import); + $import = array_values($import); + if ($import[0] === self::NS_SEPARATOR) { + array_shift($import); + } + $importName = null; + if (in_array('as', $import)) { + $importName = array_splice($import, -1)[0]; + array_pop($import); + } + $useStatement = implode('', $import); + if ($importName) { + $output[$importName] = self::NS_SEPARATOR . $useStatement; + } else { + $key = explode(self::NS_SEPARATOR, $useStatement); + $key = end($key); + $output[$key] = self::NS_SEPARATOR . $useStatement; + } + } + } + } + return $output; + } +} diff --git a/lib/internal/Magento/Framework/Code/Validator/ConstructorArgumentTypes.php b/lib/internal/Magento/Framework/Code/Validator/ConstructorArgumentTypes.php new file mode 100644 index 0000000000000000000000000000000000000000..6fdfe35b430e59fce6838d9b68864e42ae91328f --- /dev/null +++ b/lib/internal/Magento/Framework/Code/Validator/ConstructorArgumentTypes.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © 2015 Magento. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Framework\Code\Validator; + +use Magento\Framework\Code\ValidatorInterface; + +class ConstructorArgumentTypes implements ValidatorInterface +{ + /** + * @var \Magento\Framework\Code\Reader\ArgumentsReader + */ + protected $argumentsReader; + + /** + * @var \Magento\Framework\Code\Reader\SourceArgumentsReader + */ + protected $sourceArgumentsReader; + + /** + * @param \Magento\Framework\Code\Reader\ArgumentsReader $argumentsReader + * @param \Magento\Framework\Code\Reader\SourceArgumentsReader $sourceArgumentsReader + */ + public function __construct( + \Magento\Framework\Code\Reader\ArgumentsReader $argumentsReader = null, + \Magento\Framework\Code\Reader\SourceArgumentsReader $sourceArgumentsReader = null + ) { + $this->argumentsReader = $argumentsReader ?: new \Magento\Framework\Code\Reader\ArgumentsReader(); + $this->sourceArgumentsReader = + $sourceArgumentsReader ?: new \Magento\Framework\Code\Reader\SourceArgumentsReader(); + } + + /** + * Validate class constructor arguments + * + * @param string $className + * @return bool + * @throws \Magento\Framework\Code\ValidationException + */ + public function validate($className) + { + $class = new \ReflectionClass($className); + $expectedArguments = $this->argumentsReader->getConstructorArguments($class); + $actualArguments = $this->sourceArgumentsReader->getConstructorArgumentTypes($class); + $expectedArguments = array_map(function ($element) { + return $element['type']; + }, $expectedArguments); + $result = array_diff($expectedArguments, $actualArguments); + if (!empty($result)) { + throw new \Magento\Framework\Code\ValidationException( + 'Invalid constructor argument(s) in ' . $className + ); + } + return true; + } +} diff --git a/lib/internal/Magento/Framework/Config/Composer/Package.php b/lib/internal/Magento/Framework/Config/Composer/Package.php index 7fe452a54cb59a34977467e0237a6863a07d8ced..ce9742f2c384f24a9fd21c66d39ea4903f899281 100644 --- a/lib/internal/Magento/Framework/Config/Composer/Package.php +++ b/lib/internal/Magento/Framework/Config/Composer/Package.php @@ -23,7 +23,7 @@ class Package * * @param \StdClass $json */ - public function __construct(\StdClass $json) + public function __construct(\stdClass $json) { $this->json = $json; }