mirror of https://github.com/rectorphp/rector.git
Reworking trait scope run (#952)
* try reworking trait scope run * load configs from extension installer * add trait type * load configs from extension installer in PHPStan internally in Rector too * [ci-review] Rector Rectify Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
parent
6bfe013300
commit
1f63ed3e55
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\Comments\Tests\CommentRemover;
|
||||
namespace Rector\Tests\Comments\CommentRemover;
|
||||
|
||||
use Iterator;
|
||||
use Rector\Comments\CommentRemover;
|
||||
|
|
|
@ -11,7 +11,7 @@ use Rector\Comments\NodeTraverser\CommentRemovingNodeTraverser;
|
|||
use Rector\NodeTypeResolver\Node\AttributeKey;
|
||||
|
||||
/**
|
||||
* @see \Rector\Comments\Tests\CommentRemover\CommentRemoverTest
|
||||
* @see \Rector\Tests\Comments\CommentRemover\CommentRemoverTest
|
||||
*/
|
||||
final class CommentRemover
|
||||
{
|
||||
|
|
|
@ -161,6 +161,7 @@ final class FamilyRelationsAnalyzer
|
|||
}
|
||||
}
|
||||
|
||||
/** @var string[] $ancestorNames */
|
||||
return $ancestorNames;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,11 +11,14 @@ use PHPStan\Dependency\DependencyResolver;
|
|||
use PHPStan\DependencyInjection\Container;
|
||||
use PHPStan\DependencyInjection\ContainerFactory;
|
||||
use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
|
||||
use PHPStan\ExtensionInstaller\GeneratedConfig;
|
||||
use PHPStan\File\FileHelper;
|
||||
use PHPStan\PhpDoc\TypeNodeResolver;
|
||||
use PHPStan\Reflection\ReflectionProvider;
|
||||
use Rector\Core\Configuration\Option;
|
||||
use Rector\Core\Exception\ShouldNotHappenException;
|
||||
use Rector\NodeTypeResolver\Reflection\BetterReflection\SourceLocatorProvider\DynamicSourceLocatorProvider;
|
||||
use ReflectionClass;
|
||||
use Symplify\PackageBuilder\Parameter\ParameterProvider;
|
||||
|
||||
/**
|
||||
|
@ -36,6 +39,9 @@ final class PHPStanServicesFactory
|
|||
$additionalConfigFiles[] = __DIR__ . '/../../../config/phpstan/static-reflection.neon';
|
||||
$additionalConfigFiles[] = __DIR__ . '/../../../config/phpstan/better-infer.neon';
|
||||
|
||||
$extensionConfigFiles = $this->resolveExtensionConfigs();
|
||||
$additionalConfigFiles = array_merge($additionalConfigFiles, $extensionConfigFiles);
|
||||
|
||||
$existingAdditionalConfigFiles = array_filter($additionalConfigFiles, 'file_exists');
|
||||
|
||||
$this->container = $containerFactory->create(sys_get_temp_dir(), $existingAdditionalConfigFiles, []);
|
||||
|
@ -112,4 +118,39 @@ final class PHPStanServicesFactory
|
|||
{
|
||||
return $this->container->getByType(DynamicSourceLocatorProvider::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function resolveExtensionConfigs(): array
|
||||
{
|
||||
// same logic as in PHPStan for extension installed - https://github.com/phpstan/phpstan-src/blob/5956ec4f6cd09c8d7db9466ed4e7f25706f37a43/src/Command/CommandHelper.php#L195-L222
|
||||
if (! class_exists('PHPStan\ExtensionInstaller\GeneratedConfig')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$reflectionClass = new ReflectionClass(GeneratedConfig::class);
|
||||
$generatedConfigClassFileName = $reflectionClass->getFileName();
|
||||
if ($generatedConfigClassFileName === false) {
|
||||
throw new ShouldNotHappenException();
|
||||
}
|
||||
|
||||
$generatedConfigDirectory = dirname($generatedConfigClassFileName);
|
||||
|
||||
$extensionConfigFiles = [];
|
||||
|
||||
foreach (GeneratedConfig::EXTENSIONS as $extension) {
|
||||
$fileNames = $extension['extra']['includes'] ?? [];
|
||||
foreach ($fileNames as $fileName) {
|
||||
$configFilePath = $generatedConfigDirectory . '/' . $extension['relative_install_path'] . '/' . $fileName;
|
||||
if (! file_exists($configFilePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extensionConfigFiles[] = $configFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
return $extensionConfigFiles;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ final class NodeScopeAndMetadataDecorator
|
|||
$nodes = $nodeTraverser->traverse($nodes);
|
||||
|
||||
$smartFileInfo = $file->getSmartFileInfo();
|
||||
|
||||
$nodes = $this->phpStanNodeScopeResolver->processNodes($nodes, $smartFileInfo);
|
||||
|
||||
$nodeTraverserForPreservingName = new NodeTraverser();
|
||||
|
|
|
@ -7,7 +7,6 @@ namespace Rector\NodeTypeResolver\NodeTypeResolver;
|
|||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\PropertyFetch;
|
||||
use PhpParser\Node\Stmt\ClassLike;
|
||||
use PhpParser\Node\Stmt\Trait_;
|
||||
use PHPStan\Analyser\Scope;
|
||||
use PHPStan\Reflection\ReflectionProvider;
|
||||
use PHPStan\Type\MixedType;
|
||||
|
@ -17,7 +16,6 @@ use Rector\NodeNameResolver\NodeNameResolver;
|
|||
use Rector\NodeTypeResolver\Contract\NodeTypeResolverInterface;
|
||||
use Rector\NodeTypeResolver\Node\AttributeKey;
|
||||
use Rector\NodeTypeResolver\NodeTypeResolver;
|
||||
use Rector\NodeTypeResolver\PHPStan\Collector\TraitNodeScopeCollector;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
|
||||
/**
|
||||
|
@ -29,7 +27,6 @@ final class PropertyFetchTypeResolver implements NodeTypeResolverInterface
|
|||
|
||||
public function __construct(
|
||||
private NodeNameResolver $nodeNameResolver,
|
||||
private TraitNodeScopeCollector $traitNodeScopeCollector,
|
||||
private ReflectionProvider $reflectionProvider
|
||||
) {
|
||||
}
|
||||
|
@ -61,16 +58,6 @@ final class PropertyFetchTypeResolver implements NodeTypeResolverInterface
|
|||
|
||||
/** @var Scope|null $scope */
|
||||
$scope = $node->getAttribute(AttributeKey::SCOPE);
|
||||
|
||||
if (! $scope instanceof Scope) {
|
||||
$classNode = $node->getAttribute(AttributeKey::CLASS_NODE);
|
||||
if ($classNode instanceof Trait_) {
|
||||
/** @var string $traitName */
|
||||
$traitName = $classNode->getAttribute(AttributeKey::CLASS_NAME);
|
||||
$scope = $this->traitNodeScopeCollector->getScopeForTrait($traitName);
|
||||
}
|
||||
}
|
||||
|
||||
if (! $scope instanceof Scope) {
|
||||
$classNode = $node->getAttribute(AttributeKey::CLASS_NODE);
|
||||
// fallback to class, since property fetches are not scoped by PHPStan
|
||||
|
|
|
@ -7,7 +7,6 @@ namespace Rector\NodeTypeResolver\NodeTypeResolver;
|
|||
use PhpParser\Node;
|
||||
use PhpParser\Node\Expr\Variable;
|
||||
use PhpParser\Node\Param;
|
||||
use PhpParser\Node\Stmt\Trait_;
|
||||
use PHPStan\Analyser\Scope;
|
||||
use PHPStan\Type\MixedType;
|
||||
use PHPStan\Type\Type;
|
||||
|
@ -15,7 +14,6 @@ use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
|
|||
use Rector\NodeNameResolver\NodeNameResolver;
|
||||
use Rector\NodeTypeResolver\Contract\NodeTypeResolverInterface;
|
||||
use Rector\NodeTypeResolver\Node\AttributeKey;
|
||||
use Rector\NodeTypeResolver\PHPStan\Collector\TraitNodeScopeCollector;
|
||||
|
||||
/**
|
||||
* @see \Rector\Tests\NodeTypeResolver\PerNodeTypeResolver\VariableTypeResolver\VariableTypeResolverTest
|
||||
|
@ -29,7 +27,6 @@ final class VariableTypeResolver implements NodeTypeResolverInterface
|
|||
|
||||
public function __construct(
|
||||
private NodeNameResolver $nodeNameResolver,
|
||||
private TraitNodeScopeCollector $traitNodeScopeCollector,
|
||||
private PhpDocInfoFactory $phpDocInfoFactory
|
||||
) {
|
||||
}
|
||||
|
@ -85,18 +82,6 @@ final class VariableTypeResolver implements NodeTypeResolverInterface
|
|||
return $nodeScope;
|
||||
}
|
||||
|
||||
// is node in trait
|
||||
$classLike = $variable->getAttribute(AttributeKey::CLASS_NODE);
|
||||
if ($classLike instanceof Trait_) {
|
||||
/** @var string $traitName */
|
||||
$traitName = $variable->getAttribute(AttributeKey::CLASS_NAME);
|
||||
$traitNodeScope = $this->traitNodeScopeCollector->getScopeForTrait($traitName);
|
||||
|
||||
if ($traitNodeScope !== null) {
|
||||
return $traitNodeScope;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->resolveFromParentNodes($variable);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\NodeTypeResolver\PHPStan\Collector;
|
||||
|
||||
use PHPStan\Analyser\Scope;
|
||||
|
||||
final class TraitNodeScopeCollector
|
||||
{
|
||||
/**
|
||||
* @var array<string, Scope>
|
||||
*/
|
||||
private array $scopeByTraitNodeHash = [];
|
||||
|
||||
public function addForTrait(string $traitName, Scope $scope): void
|
||||
{
|
||||
// probably set from another class
|
||||
if (isset($this->scopeByTraitNodeHash[$traitName])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->scopeByTraitNodeHash[$traitName] = $scope;
|
||||
}
|
||||
|
||||
public function getScopeForTrait(string $traitName): ?Scope
|
||||
{
|
||||
return $this->scopeByTraitNodeHash[$traitName] ?? null;
|
||||
}
|
||||
}
|
|
@ -16,12 +16,12 @@ use PHPStan\AnalysedCodeException;
|
|||
use PHPStan\Analyser\MutatingScope;
|
||||
use PHPStan\Analyser\NodeScopeResolver;
|
||||
use PHPStan\Analyser\Scope;
|
||||
use PHPStan\Analyser\ScopeContext;
|
||||
use PHPStan\BetterReflection\Reflection\Exception\NotAnInterfaceReflection;
|
||||
use PHPStan\BetterReflection\Reflector\ClassReflector;
|
||||
use PHPStan\BetterReflection\SourceLocator\Type\AggregateSourceLocator;
|
||||
use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator;
|
||||
use PHPStan\Node\UnreachableStatementNode;
|
||||
use PHPStan\Reflection\ClassReflection;
|
||||
use PHPStan\Reflection\ReflectionProvider;
|
||||
use PHPStan\Type\ObjectType;
|
||||
use Rector\Caching\Detector\ChangedFilesDetector;
|
||||
|
@ -30,8 +30,8 @@ use Rector\Core\Exception\ShouldNotHappenException;
|
|||
use Rector\Core\PhpParser\Node\BetterNodeFinder;
|
||||
use Rector\Core\StaticReflection\SourceLocator\ParentAttributeSourceLocator;
|
||||
use Rector\Core\StaticReflection\SourceLocator\RenamedClassesSourceLocator;
|
||||
use Rector\Core\Stubs\DummyTraitClass;
|
||||
use Rector\NodeTypeResolver\Node\AttributeKey;
|
||||
use Rector\NodeTypeResolver\PHPStan\Collector\TraitNodeScopeCollector;
|
||||
use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\RemoveDeepChainMethodCallNodeVisitor;
|
||||
use Symplify\PackageBuilder\Reflection\PrivatesAccessor;
|
||||
use Symplify\SmartFileSystem\SmartFileInfo;
|
||||
|
@ -62,7 +62,6 @@ final class PHPStanNodeScopeResolver
|
|||
private ReflectionProvider $reflectionProvider,
|
||||
private RemoveDeepChainMethodCallNodeVisitor $removeDeepChainMethodCallNodeVisitor,
|
||||
private ScopeFactory $scopeFactory,
|
||||
private TraitNodeScopeCollector $traitNodeScopeCollector,
|
||||
private PrivatesAccessor $privatesAccessor,
|
||||
private RenamedClassesSourceLocator $renamedClassesSourceLocator,
|
||||
private ParentAttributeSourceLocator $parentAttributeSourceLocator,
|
||||
|
@ -81,26 +80,26 @@ final class PHPStanNodeScopeResolver
|
|||
$scope = $this->scopeFactory->createFromFile($smartFileInfo);
|
||||
|
||||
// skip chain method calls, performance issue: https://github.com/phpstan/phpstan/issues/254
|
||||
$nodeCallback = function (Node $node, Scope $scope): void {
|
||||
// traversing trait inside class that is using it scope (from referenced) - the trait traversed by Rector is different (directly from parsed file)
|
||||
if ($scope->isInTrait()) {
|
||||
// has just entereted trait, to avoid adding it for ever ynode
|
||||
$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
|
||||
if ($parentNode instanceof Trait_) {
|
||||
/** @var ClassReflection $classReflection */
|
||||
$classReflection = $scope->getTraitReflection();
|
||||
$traitName = $classReflection->getName();
|
||||
$nodeCallback = function (Node $node, MutatingScope $scope) use (&$nodeCallback): void {
|
||||
if ($node instanceof Trait_) {
|
||||
$traitName = $this->resolveClassName($node);
|
||||
|
||||
$this->traitNodeScopeCollector->addForTrait($traitName, $scope);
|
||||
}
|
||||
$traitReflectionClass = $this->reflectionProvider->getClass($traitName);
|
||||
|
||||
$scopeContext = $this->createDummyClassScopeContext($scope);
|
||||
$traitScope = clone $scope;
|
||||
$this->privatesAccessor->setPrivateProperty($traitScope, 'context', $scopeContext);
|
||||
|
||||
$traitScope = $traitScope->enterTrait($traitReflectionClass);
|
||||
|
||||
$this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $traitScope, $nodeCallback);
|
||||
return;
|
||||
}
|
||||
|
||||
// the class reflection is resolved AFTER entering to class node
|
||||
// so we need to get it from the first after this one
|
||||
if ($node instanceof Class_ || $node instanceof Interface_) {
|
||||
/** @var Scope $scope */
|
||||
/** @var MutatingScope $scope */
|
||||
$scope = $this->resolveClassOrInterfaceScope($node, $scope);
|
||||
}
|
||||
|
||||
|
@ -217,24 +216,25 @@ final class PHPStanNodeScopeResolver
|
|||
$nodeTraverser->traverse($nodes);
|
||||
}
|
||||
|
||||
private function resolveClassOrInterfaceScope(Class_ | Interface_ $classLike, Scope $scope): Scope
|
||||
{
|
||||
private function resolveClassOrInterfaceScope(
|
||||
Class_ | Interface_ $classLike,
|
||||
MutatingScope $mutatingScope
|
||||
): Scope {
|
||||
$className = $this->resolveClassName($classLike);
|
||||
|
||||
// is anonymous class? - not possible to enter it since PHPStan 0.12.33, see https://github.com/phpstan/phpstan-src/commit/e87fb0ec26f9c8552bbeef26a868b1e5d8185e91
|
||||
if ($classLike instanceof Class_ && Strings::match($className, self::ANONYMOUS_CLASS_START_REGEX)) {
|
||||
$classReflection = $this->reflectionProvider->getAnonymousClassReflection($classLike, $scope);
|
||||
$classReflection = $this->reflectionProvider->getAnonymousClassReflection($classLike, $mutatingScope);
|
||||
} elseif (! $this->reflectionProvider->hasClass($className)) {
|
||||
return $scope;
|
||||
return $mutatingScope;
|
||||
} else {
|
||||
$classReflection = $this->reflectionProvider->getClass($className);
|
||||
}
|
||||
|
||||
/** @var MutatingScope $scope */
|
||||
return $scope->enterClass($classReflection);
|
||||
return $mutatingScope->enterClass($classReflection);
|
||||
}
|
||||
|
||||
private function resolveClassName(Class_ | Interface_ $classLike): string
|
||||
private function resolveClassName(Class_ | Interface_ | Trait_ $classLike): string
|
||||
{
|
||||
if (property_exists($classLike, 'namespacedName')) {
|
||||
return (string) $classLike->namespacedName;
|
||||
|
@ -293,4 +293,16 @@ final class PHPStanNodeScopeResolver
|
|||
]);
|
||||
$this->privatesAccessor->setPrivateProperty($classReflector, 'sourceLocator', $aggregateSourceLocator);
|
||||
}
|
||||
|
||||
private function createDummyClassScopeContext(MutatingScope $mutatingScope): ScopeContext
|
||||
{
|
||||
// this has to be faked, because trait PHPStan does not traverse trait without a class
|
||||
/** @var ScopeContext $scopeContext */
|
||||
$scopeContext = $this->privatesAccessor->getPrivateProperty($mutatingScope, 'context');
|
||||
$dummyClassReflection = $this->reflectionProvider->getClass(DummyTraitClass::class);
|
||||
|
||||
// faking a class reflection
|
||||
return ScopeContext::create($scopeContext->getFile())
|
||||
->enterClass($dummyClassReflection);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,11 @@ trait MultipleFilesChangedTrait
|
|||
$fixturePath = $this->getFixtureTempDirectory() . '/' . $fixtureFileInfo->getFilename();
|
||||
$this->createFixtureDir($fixturePath);
|
||||
$fixtureContent = $originalContent;
|
||||
if (trim($expectedContent)) {
|
||||
|
||||
/** @var string $expectedContent */
|
||||
$trimmedExpectedContent = trim($expectedContent);
|
||||
|
||||
if ($trimmedExpectedContent !== '') {
|
||||
$fixtureContent .= $separator . $expectedContent;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,12 +8,3 @@ parameters:
|
|||
# see https://github.com/rectorphp/rector/issues/3490#issue-634342324
|
||||
featureToggles:
|
||||
disableRuntimeReflectionProvider: true
|
||||
|
||||
includes:
|
||||
- vendor/symplify/phpstan-rules/packages/symfony/config/services.neon
|
||||
- vendor/rector/phpstan-rules/config/config.neon
|
||||
- vendor/symplify/phpstan-extensions/config/config.neon
|
||||
|
||||
- vendor/symplify/phpstan-rules/config/services/services.neon
|
||||
- vendor/symplify/phpstan-rules/packages/object-calisthenics/config/object-calisthenics-rules.neon
|
||||
- vendor/symplify/phpstan-rules/packages/cognitive-complexity/config/cognitive-complexity-services.neon
|
||||
|
|
|
@ -184,7 +184,10 @@ parameters:
|
|||
# class_exists is forbidden to enforce static reflection, but in a compiler pass we want runtime autoloading
|
||||
-
|
||||
message: '#Function "class_exists\(\)" cannot be used/left in the code#'
|
||||
path: src/DependencyInjection/CompilerPass/VerifyRectorServiceExistsCompilerPass.php
|
||||
paths:
|
||||
- src/DependencyInjection/CompilerPass/VerifyRectorServiceExistsCompilerPass.php
|
||||
# for config class reflection
|
||||
- packages/NodeTypeResolver/DependencyInjection/PHPStanServicesFactory.php
|
||||
|
||||
# known values from other methods
|
||||
-
|
||||
|
@ -505,3 +508,6 @@ parameters:
|
|||
- '#(.*?) but returns array<int, PhpParser\\Node\\Arg\|PhpParser\\Node\\VariadicPlaceholder\>#'
|
||||
|
||||
- '#Parameter "\w+" cannot have default value#'
|
||||
|
||||
# scope & mutating scope mish-mash
|
||||
- '#Parameter \#4 \$nodeCallback of method PHPStan\\Analyser\\NodeScopeResolver\:\:processStmtNodes\(\) expects callable\(PhpParser\\Node, PHPStan\\Analyser\\Scope\)\: void, Closure\(PhpParser\\Node, PHPStan\\Analyser\\MutatingScope\)\: void given#'
|
||||
|
|
|
@ -2,14 +2,6 @@
|
|||
|
||||
namespace Rector\Tests\Arguments\Rector\ClassMethod\ArgumentAdderRector\Fixture;
|
||||
|
||||
class Value
|
||||
{
|
||||
public function doSomething()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class SomeProperty
|
||||
{
|
||||
public $value;
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace Rector\Tests\Php74\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
trait JsonBodyTrait
|
||||
{
|
||||
private $body;
|
||||
|
||||
public function setJsonBody(array $body): self
|
||||
{
|
||||
$this->body = $body;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getJsonBody(): ?array
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
-----
|
||||
<?php
|
||||
|
||||
namespace Rector\Tests\Php74\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
trait JsonBodyTrait
|
||||
{
|
||||
private ?array $body = null;
|
||||
|
||||
public function setJsonBody(array $body): self
|
||||
{
|
||||
$this->body = $body;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getJsonBody(): ?array
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
|
@ -1,23 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Rector\Tests\Php74\Rector\Property\TypedPropertyRector\Fixture;
|
||||
|
||||
trait JsonBodyTrait
|
||||
{
|
||||
private $body;
|
||||
|
||||
public function setJsonBody( array $body ): self {
|
||||
$this->body = $body;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getJsonBody(): ?array {
|
||||
return $this->body;
|
||||
}
|
||||
}
|
||||
|
||||
class UsingTraig
|
||||
{
|
||||
use JsonBodyTrait;
|
||||
}
|
||||
|
|
@ -70,6 +70,10 @@ CODE_SAMPLE
|
|||
return null;
|
||||
}
|
||||
|
||||
if ($scope->isInTrait()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$classReflection = $scope->getClassReflection();
|
||||
if (! $classReflection instanceof ClassReflection) {
|
||||
return null;
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\Core\Stubs;
|
||||
|
||||
/**
|
||||
* Used for faking class reflection for trait reflections
|
||||
*/
|
||||
final class DummyTraitClass
|
||||
{
|
||||
}
|
Loading…
Reference in New Issue