Skip to content

Commit

Permalink
ENGCOM-6686: Github #26532: di:setup:compile fails with anonymous cla…
Browse files Browse the repository at this point in the history
…sses #26533

 - Merge Pull Request #26533 from joni-jones/magento2:G#26532
 - Merged commits:
   1. fd3f2a2
   2. 136d559
   3. 25fd925
   4. e165d99
  • Loading branch information
magento-engcom-team committed Jan 30, 2020
2 parents 2c553dd + e165d99 commit 4af1018
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 92 deletions.
117 changes: 63 additions & 54 deletions setup/src/Magento/Setup/Module/Di/Code/Scanner/PhpScanner.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\Setup\Module\Di\Code\Scanner;

use Magento\Framework\Api\Code\Generator\ExtensionAttributesGenerator;
use Magento\Framework\Api\Code\Generator\ExtensionAttributesInterfaceGenerator;
use Magento\Framework\ObjectManager\Code\Generator\Factory as FactoryGenerator;
use Magento\Framework\Reflection\TypeProcessor;
use Magento\Setup\Module\Di\Compiler\Log\Log;
use \Magento\Framework\Reflection\TypeProcessor;

/**
* Class PhpScanner
*
* @package Magento\Setup\Module\Di\Code\Scanner
* Finds factory and extension attributes classes which require auto-generation.
*/
class PhpScanner implements ScannerInterface
{
Expand Down Expand Up @@ -50,18 +50,31 @@ public function __construct(Log $log, TypeProcessor $typeProcessor = null)
* @param string $entityType
* @return string[]
*/
protected function _findMissingClasses($file, $classReflection, $methodName, $entityType)
private function findMissingFactories($file, $classReflection, $methodName, $entityType)
{
$missingClasses = [];
if ($classReflection->hasMethod($methodName)) {
$constructor = $classReflection->getMethod($methodName);
$parameters = $constructor->getParameters();
/** @var $parameter \ReflectionParameter */
foreach ($parameters as $parameter) {
preg_match('/\[\s\<\w+?>\s([\w\\\\]+)/s', $parameter->__toString(), $matches);
if (isset($matches[1]) && substr($matches[1], -strlen($entityType)) == $entityType) {
$missingClassName = $matches[1];
if ($this->shouldGenerateClass($missingClassName, $entityType, $file)) {
if (!$classReflection->hasMethod($methodName)) {
return $missingClasses;
}

$factorySuffix = '\\' . ucfirst(FactoryGenerator::ENTITY_TYPE);
$constructor = $classReflection->getMethod($methodName);
$parameters = $constructor->getParameters();
/** @var $parameter \ReflectionParameter */
foreach ($parameters as $parameter) {
preg_match('/\[\s\<\w+?>\s([\w\\\\]+)/s', $parameter->__toString(), $matches);
if (isset($matches[1]) && substr($matches[1], -strlen($entityType)) == $entityType) {
$missingClassName = $matches[1];
if ($this->shouldGenerateClass($missingClassName, $entityType, $file)) {

if (substr($missingClassName, -strlen($factorySuffix)) == $factorySuffix) {
$entityName = rtrim(substr($missingClassName, 0, -strlen($factorySuffix)), '\\');
$this->_log->add(
Log::CONFIGURATION_ERROR,
$missingClassName,
'Invalid Factory declaration for class ' . $entityName . ' in file ' . $file
);
} else {
$missingClasses[] = $missingClassName;
}
}
Expand Down Expand Up @@ -110,24 +123,12 @@ protected function getSourceClassName($missingClassName, $entityType)
*/
protected function _fetchFactories($reflectionClass, $file)
{
$factorySuffix = '\\' . ucfirst(FactoryGenerator::ENTITY_TYPE);
$absentFactories = $this->_findMissingClasses(
$absentFactories = $this->findMissingFactories(
$file,
$reflectionClass,
'__construct',
ucfirst(FactoryGenerator::ENTITY_TYPE)
);
foreach ($absentFactories as $key => $absentFactory) {
if (substr($absentFactory, -strlen($factorySuffix)) == $factorySuffix) {
$entityName = rtrim(substr($absentFactory, 0, -strlen($factorySuffix)), '\\');
$this->_log->add(
Log::CONFIGURATION_ERROR,
$absentFactory,
'Invalid Factory declaration for class ' . $entityName . ' in file ' . $file
);
unset($absentFactories[$key]);
}
}
return $absentFactories;
}

Expand All @@ -150,21 +151,19 @@ protected function _fetchMissingExtensionAttributesClasses($reflectionClass, $fi
$missingClassName = $returnType['type'];
if ($this->shouldGenerateClass($missingClassName, $entityType, $file)) {
$missingExtensionInterfaces[] = $missingClassName;

$extension = rtrim(substr($missingClassName, 0, -strlen('Interface')), '\\');
if (!class_exists($extension)) {
$missingExtensionInterfaces[] = $extension;
}
$extensionFactory = $extension . 'Factory';
if (!class_exists($extensionFactory)) {
$missingExtensionInterfaces[] = $extensionFactory;
}
}
}
$missingExtensionClasses = [];
$missingExtensionFactories = [];
foreach ($missingExtensionInterfaces as $missingExtensionInterface) {
$extension = rtrim(substr($missingExtensionInterface, 0, -strlen('Interface')), '\\');
if (!class_exists($extension)) {
$missingExtensionClasses[] = $extension;
}
$extensionFactory = $extension . 'Factory';
if (!class_exists($extensionFactory)) {
$missingExtensionFactories[] = $extensionFactory;
}
}
return array_merge($missingExtensionInterfaces, $missingExtensionClasses, $missingExtensionFactories);

return $missingExtensionInterfaces;
}

/**
Expand All @@ -178,11 +177,11 @@ public function collectEntities(array $files)
{
$output = [[]];
foreach ($files as $file) {
$classes = $this->_getDeclaredClasses($file);
$classes = $this->getDeclaredClasses($file);
foreach ($classes as $className) {
$reflectionClass = new \ReflectionClass($className);
$output [] = $this->_fetchFactories($reflectionClass, $file);
$output [] = $this->_fetchMissingExtensionAttributesClasses($reflectionClass, $file);
$output[] = $this->_fetchFactories($reflectionClass, $file);
$output[] = $this->_fetchMissingExtensionAttributesClasses($reflectionClass, $file);
}
}
return array_unique(array_merge(...$output));
Expand Down Expand Up @@ -211,23 +210,30 @@ protected function _fetchNamespace($tokenIterator, $count, $tokens)
}

/**
* Fetch class names from tokenized PHP file
* Fetches class name from tokenized PHP file.
*
* @param string $namespace
* @param int $tokenIterator
* @param int $count
* @param array $tokens
* @return array
* @return string|null
*/
protected function _fetchClasses($namespace, $tokenIterator, $count, $tokens)
private function fetchClass($namespace, $tokenIterator, $count, $tokens):? string
{
$classes = [];
// anonymous classes should be omitted
if (is_array($tokens[$tokenIterator - 2]) && $tokens[$tokenIterator - 2][0] === T_NEW) {
return null;
}

for ($tokenOffset = $tokenIterator + 1; $tokenOffset < $count; ++$tokenOffset) {
if ($tokens[$tokenOffset] === '{') {
$classes[] = $namespace . "\\" . $tokens[$tokenIterator + 2][1];
if ($tokens[$tokenOffset] !== '{') {
continue;
}

return $namespace . "\\" . $tokens[$tokenIterator + 2][1];
}
return $classes;

return null;
}

/**
Expand All @@ -236,9 +242,9 @@ protected function _fetchClasses($namespace, $tokenIterator, $count, $tokens)
* @param string $file
* @return array
*/
protected function _getDeclaredClasses($file)
private function getDeclaredClasses($file): array
{
$classes = [[]];
$classes = [];
$namespaceParts = [];
// phpcs:ignore
$tokens = token_get_all(file_get_contents($file));
Expand All @@ -252,10 +258,13 @@ protected function _getDeclaredClasses($file)
if (($tokens[$tokenIterator][0] == T_CLASS || $tokens[$tokenIterator][0] == T_INTERFACE)
&& $tokens[$tokenIterator - 1][0] != T_DOUBLE_COLON
) {
$classes[] = $this->_fetchClasses(join('', $namespaceParts), $tokenIterator, $count, $tokens);
$class = $this->fetchClass(join('', $namespaceParts), $tokenIterator, $count, $tokens);
if ($class !== null && !in_array($class, $classes)) {
$classes[] = $class;
}
}
}
return array_unique(array_merge(...$classes));
return $classes;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\Setup\Test\Unit\Module\Di\Code\Reader;

use Magento\Framework\App\Filesystem\DirectoryList;
Expand Down Expand Up @@ -37,6 +39,6 @@ public function testGetList()
$pathToScan = str_replace('\\', '/', realpath(__DIR__ . '/../../') . '/_files/app/code/Magento/SomeModule');
$actual = $this->model->getList($pathToScan);
$this->assertTrue(is_array($actual));
$this->assertCount(5, $actual);
$this->assertCount(6, $actual);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,74 +3,74 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\Setup\Test\Unit\Module\Di\Code\Scanner;

require_once __DIR__ . '/../../_files/app/code/Magento/SomeModule/Helper/Test.php';
require_once __DIR__ . '/../../_files/app/code/Magento/SomeModule/ElementFactory.php';
require_once __DIR__ . '/../../_files/app/code/Magento/SomeModule/Model/DoubleColon.php';
require_once __DIR__ . '/../../_files/app/code/Magento/SomeModule/Api/Data/SomeInterface.php';
require_once __DIR__ . '/../../_files/app/code/Magento/SomeModule/Model/StubWithAnonymousClass.php';

use Magento\Framework\Reflection\TypeProcessor;
use Magento\Setup\Module\Di\Code\Scanner\PhpScanner;
use Magento\Setup\Module\Di\Compiler\Log\Log;
use PHPUnit\Framework\MockObject\MockObject;

class PhpScannerTest extends \PHPUnit\Framework\TestCase
{
/**
* @var \Magento\Setup\Module\Di\Code\Scanner\PhpScanner
* @var PhpScanner
*/
protected $_model;
private $scanner;

/**
* @var string
*/
protected $_testDir;

/**
* @var array
*/
protected $_testFiles = [];
private $testDir;

/**
* @var \PHPUnit_Framework_MockObject_MockObject
* @var Log|MockObject
*/
protected $_logMock;
private $log;

protected function setUp()
{
$this->_logMock = $this->createMock(\Magento\Setup\Module\Di\Compiler\Log\Log::class);
$this->_model = new \Magento\Setup\Module\Di\Code\Scanner\PhpScanner($this->_logMock, new TypeProcessor());
$this->_testDir = str_replace('\\', '/', realpath(__DIR__ . '/../../') . '/_files');
$this->log = $this->createMock(Log::class);
$this->scanner = new PhpScanner($this->log, new TypeProcessor());
$this->testDir = str_replace('\\', '/', realpath(__DIR__ . '/../../') . '/_files');
}

public function testCollectEntities()
{
$this->_testFiles = [
$this->_testDir . '/app/code/Magento/SomeModule/Helper/Test.php',
$this->_testDir . '/app/code/Magento/SomeModule/Model/DoubleColon.php',
$this->_testDir . '/app/code/Magento/SomeModule/Api/Data/SomeInterface.php'
$testFiles = [
$this->testDir . '/app/code/Magento/SomeModule/Helper/Test.php',
$this->testDir . '/app/code/Magento/SomeModule/Model/DoubleColon.php',
$this->testDir . '/app/code/Magento/SomeModule/Api/Data/SomeInterface.php',
$this->testDir . '/app/code/Magento/SomeModule/Model/StubWithAnonymousClass.php',
];

$this->_logMock->expects(
$this->at(0)
)->method(
'add'
)->with(
4,
'Magento\SomeModule\Module\Factory',
'Invalid Factory for nonexistent class Magento\SomeModule\Module in file ' . $this->_testFiles[0]
);
$this->_logMock->expects(
$this->at(1)
)->method(
'add'
)->with(
4,
'Magento\SomeModule\Element\Factory',
'Invalid Factory declaration for class Magento\SomeModule\Element in file ' . $this->_testFiles[0]
);
$this->log->expects(self::at(0))
->method('add')
->with(
4,
'Magento\SomeModule\Module\Factory',
'Invalid Factory for nonexistent class Magento\SomeModule\Module in file ' . $testFiles[0]
);
$this->log->expects(self::at(1))
->method('add')
->with(
4,
'Magento\SomeModule\Element\Factory',
'Invalid Factory declaration for class Magento\SomeModule\Element in file ' . $testFiles[0]
);

$result = $this->scanner->collectEntities($testFiles);

$this->assertEquals(
self::assertEquals(
['\\' . \Magento\Eav\Api\Data\AttributeExtensionInterface::class],
$this->_model->collectEntities($this->_testFiles)
$result
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\SomeModule\Model;

use Magento\SomeModule\DummyFactory;

class StubWithAnonymousClass
{
/**
* @var DummyFactory
*/
private $factory;

public function __construct(DummyFactory $factory)
{
$this->factory = $factory;
}

public function getSerializable(): \JsonSerializable
{
return new class() implements \JsonSerializable {
/**
* @inheritDoc
*/
public function jsonSerialize()
{
return [1, 2, 3];
}
};
}
}

0 comments on commit 4af1018

Please sign in to comment.