TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:31 +02:00
commit 3e43c11539
407 changed files with 51272 additions and 0 deletions
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\NodeVisitorAbstract;
/**
* A visitor doing some counting.
* It sums the number of ignored lines and lines of effective code.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class CodeStatistics extends NodeVisitorAbstract
{
/**
* @var bool True if a class statement has @extensionScannerIgnoreFile
*/
protected $isCurrentFileIgnored = false;
/**
* @var int Counts @extensionScannerIgnoreLine statements
*/
protected $numberOfIgnoreLines = 0;
/**
* @var int Number of effective code lines - class and method statements, function calls ...
*/
protected $numberOfEffectiveCodeLines = 0;
/**
* @var int Current line number given not is in, runtime helper var
*/
protected $currentLineNumber = 0;
/**
* Called by PhpParser during traversal.
*/
public function enterNode(Node $node): null
{
$startLineOfNode = $node->getAttribute('startLine');
if ($startLineOfNode !== $this->currentLineNumber) {
$this->currentLineNumber = $startLineOfNode;
$this->numberOfEffectiveCodeLines++;
// Class statements may contain the @extensionScannerIgnoreFile statements
if ($node instanceof Class_) {
$comments = $node->getAttribute('comments');
if (!empty($comments)) {
foreach ($comments as $comment) {
if (str_contains($comment->getText(), '@extensionScannerIgnoreFile')) {
$this->isCurrentFileIgnored = true;
break;
}
}
}
}
// First node of line may contain the @extensionScannerIgnoreLine comment
$comments = $node->getAttribute('comments');
if (!empty($comments)) {
foreach ($comments as $comment) {
if (str_contains($comment->getText(), '@extensionScannerIgnoreLine')) {
$this->numberOfIgnoreLines++;
break;
}
}
}
}
return null;
}
/**
* True if a @extensionScannerIgnoreFile has been found.
* Called externally *after* traversing
*
* @return bool
*/
public function isFileIgnored()
{
return $this->isCurrentFileIgnored;
}
/**
* Number of "effective" code lines: No comments, no empty lines,
* but "class" statements, "function" statements, "use xy", etc.
* Called externally *after* traversing
*
* @return int
*/
public function getNumberOfEffectiveCodeLines()
{
return $this->numberOfEffectiveCodeLines;
}
/**
* Returns number of found @extensionScannerIgnoreLine comments
* Called externally *after* traversing
*
* @return int
*/
public function getNumberOfIgnoredLines()
{
return $this->numberOfIgnoreLines;
}
}
@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php;
use PhpParser\BuilderFactory;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\String_;
use PhpParser\NodeVisitorAbstract;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\AbstractCoreMatcher;
/**
* Create a fully qualified class name object from first argument of
* GeneralUtility::makeInstance('My\\Package\\Class\\Name') if given as string
* and not as My\Package\Class\Name::class language construct.
*
* This resolver is to be called after generic NameResolver::class, but before
* other search and find visitors that implement CodeScannerInterface::class
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class GeneratorClassesResolver extends NodeVisitorAbstract
{
/**
* @var BuilderFactory
*/
protected $builderFactory;
public function __construct(?BuilderFactory $builderFactory = null)
{
$this->builderFactory = $builderFactory ?? new BuilderFactory();
}
/**
* Called by PhpParser.
* Create an fqdn object from first makeInstance argument if it is a String
*
* @param Node $node Incoming node
*/
public function enterNode(Node $node)
{
if ($node instanceof StaticCall
&& $node->class instanceof FullyQualified
&& $node->class->toString() === GeneralUtility::class
&& $node->name->name === 'makeInstance'
&& isset($node->args[0]->value)
&& $node->args[0]->value instanceof Expr
) {
$argValue = $node->args[0]->value;
$argAlternative = $this->substituteClassString($argValue);
if ($argAlternative !== null) {
$node->args[0]->value = $argAlternative;
$argValue = $argAlternative;
}
$nodeAlternative = $this->substituteMakeInstance($node, $argValue);
if ($nodeAlternative !== null) {
$node->setAttribute(AbstractCoreMatcher::NODE_RESOLVED_AS, $nodeAlternative);
}
}
return null;
}
/**
* Substitutes class-string values with their corresponding class constant
* representation (`'Vendor\\ClassName'` -> `\Vendor\ClassName::class`).
*/
protected function substituteClassString(Expr $argValue): ?ClassConstFetch
{
// skip non-strings, and those starting with (invalid) namespace separator
if (!$argValue instanceof String_ || $argValue->value[0] === '\\') {
return null;
}
$classString = ltrim($argValue->value, '\\');
$className = new FullyQualified($classString);
$classArg = $this->builderFactory->classConstFetch($className, 'class');
$this->duplicateNodeAttributes($argValue, $className, $classArg);
return $classArg;
}
/**
* Substitutes `makeInstance` invocations with proper `new` invocations.
* `GeneralUtility(\Vendor\ClassName::class, 'a', 'b')` -> `new \Vendor\ClassName('a', 'b')`
*/
protected function substituteMakeInstance(StaticCall $node, Expr $argValue): ?New_
{
if (!$argValue instanceof ClassConstFetch
|| !$argValue->class instanceof FullyQualified
) {
return null;
}
$newExpr = $this->builderFactory->new(
$argValue->class,
array_slice($node->args, 1),
);
$this->duplicateNodeAttributes($node, $newExpr);
return $newExpr;
}
/**
* Duplicates node positions in source file, based on the assumption
* that only lines are relevant. In case this shall be used for
* code-migration, real offset positions would be required.
*/
protected function duplicateNodeAttributes(Node $source, Node ...$targets): void
{
foreach ($targets as $target) {
$target->setAttributes([
'startLine' => $source->getStartLine(),
'endLine' => $source->getEndLine(),
]);
}
}
}
@@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\NodeVisitorAbstract;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\ExtensionScanner\CodeScannerInterface;
/**
* Single "core matcher" classes extend from this.
* It brings a set of protected methods to help single matcher classes doing common stuff.
* This abstract extends the nikic/php-parser NodeVisitorAbstract which implements the main
* parser interface, and it implements the TYPO3 specific CodeScannerInterface to retrieve matches.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
abstract class AbstractCoreMatcher extends NodeVisitorAbstract implements CodeScannerInterface
{
public const NODE_RESOLVED_AS = 'nodeResolvedAs';
/**
* Incoming main configuration array.
*
* @var array
*/
protected $matcherDefinitions = [];
/**
* @var array List of accumulated matches
*/
protected $matches = [];
/**
* Helper property containing an array derived from $this->matcherDefinitions
* created in __construct() if needed.
*
* @var array
*/
protected $flatMatcherDefinitions = [];
/**
* @var int Helper variable for ignored line detection
*/
protected $currentCodeLine = 0;
/**
* @var bool True if line with $lastIgnoredLineNumber is ignored
*/
protected $isCurrentLineIgnored = false;
/**
* @var bool True if the entire file is ignored due to a @extensionScannerIgnoreFile class comment
*/
protected $isFullFileIgnored = false;
/**
* Return list of matches after processing
*/
public function getMatches(): array
{
return $this->matches;
}
/**
* Some matcher need specific keys in the array definition to work properly.
* This method is called typically in __construct() of a matcher to
* verify these are given.
* This method is a measure against broken core configuration. It should be
* pretty quick and is only called in __construct() once, no kitten should be harmed.
*
* This method works on $this->matcherDefinitions.
*
* @param array $requiredArrayKeys List of required keys for single matchers
* @throws \RuntimeException
*/
protected function validateMatcherDefinitions(array $requiredArrayKeys = [])
{
foreach ($this->matcherDefinitions as $key => $matcherDefinition) {
$this->validateMatcherDefinitionKeys($key, $matcherDefinition, $requiredArrayKeys);
}
}
protected function validateMatcherDefinitionKeys(string $key, array $matcherDefinition, array $requiredArrayKeys = []): void
{
// Each config must point to at least one .rst file
if (empty($matcherDefinition['restFiles'])) {
throw new \InvalidArgumentException(
'Each configuration must have at least one referenced "restFiles" entry. Offending key: ' . $key,
1500496068
);
}
foreach ($matcherDefinition['restFiles'] as $file) {
if (empty($file)) {
throw new \InvalidArgumentException(
'Empty restFiles definition',
1500735983
);
}
}
// Config broken if not all required array keys are specified in config
$sharedArrays = array_intersect(array_keys($matcherDefinition), $requiredArrayKeys);
if (count($sharedArrays) !== count($requiredArrayKeys)) {
$missingKeys = array_diff($requiredArrayKeys, array_keys($matcherDefinition));
throw new \InvalidArgumentException(
'Required matcher definitions missing: ' . implode(', ', $missingKeys) . ' offending key: ' . $key,
1500492001
);
}
}
/**
* Initialize helper lookup array $this->flatMatcherDefinitions.
* For class\name->foo matcherDefinitions, it creates a helper array
* containing only the method name as array keys for "weak" matches.
*
* If methods with the same name from different classes are defined,
* a "candidate" array is created containing details of single possible
* matches for further analysis.
*
* @throws \RuntimeException
*/
protected function initializeFlatMatcherDefinitions()
{
$methodNameArray = [];
foreach ($this->matcherDefinitions as $classAndMethod => $details) {
$method = GeneralUtility::trimExplode('::', $classAndMethod);
if (count($method) !== 2) {
$method = GeneralUtility::trimExplode('->', $classAndMethod);
}
if (count($method) !== 2) {
throw new \RuntimeException(
'Keys in $this->matcherDefinitions must have a Class\Name->method or Class\Name::method structure',
1500557309
);
}
$method = $method[1];
if (!array_key_exists($method, $methodNameArray)) {
$methodNameArray[$method]['candidates'] = [];
}
$methodNameArray[$method]['candidates'][] = $details;
}
$this->flatMatcherDefinitions = $methodNameArray;
}
/**
* Test if one argument is given as "...$someArray".
* If so, it kinda defeats any "argument count" approach.
*
* @param array $arguments List of arguments
*/
protected function isArgumentUnpackingUsed(array $arguments = []): bool
{
foreach ($arguments as $arg) {
if ($arg->unpack === true) {
return true;
}
}
return false;
}
/**
* Returns true if a comment before a statement is
* marked as "@extensionScannerIgnoreLine"
*/
protected function isLineIgnored(Node $node): bool
{
// Early return if this line is marked as ignored
$startLineOfNode = $node->getAttribute('startLine');
if ($startLineOfNode === $this->currentCodeLine) {
return $this->isCurrentLineIgnored;
}
if ($this->isCurrentLineIgnored) {
// "ignoreMode" is still active, but we're past the line
// where it was enabled. Reset this beauty.
$this->isCurrentLineIgnored = false;
}
$currentLineIsIgnored = false;
if ($startLineOfNode !== $this->currentCodeLine) {
$this->currentCodeLine = $startLineOfNode;
// First node of a new line may contain the annotation
$comments = $node->getAttribute('comments');
if (!empty($comments)) {
foreach ($comments as $comment) {
if (str_contains($comment->getText(), '@extensionScannerIgnoreLine')) {
$this->isCurrentLineIgnored = true;
$currentLineIsIgnored = true;
break;
}
}
}
}
return $currentLineIsIgnored;
}
/**
* Return true if the node is ignored since the entire file is ignored.
* Sets ignore status if a class node is given having the annotation.
*/
protected function isFileIgnored(Node $node): bool
{
if ($this->isFullFileIgnored) {
return true;
}
$currentFileIsIgnored = false;
if ($node instanceof Class_) {
$comments = $node->getAttribute('comments');
if (!empty($comments)) {
foreach ($comments as $comment) {
if (str_contains($comment->getText(), '@extensionScannerIgnoreFile')) {
$this->isFullFileIgnored = true;
$currentFileIsIgnored = true;
break;
}
}
}
}
return $currentFileIsIgnored;
}
}
@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Find usages of defined methods within a class that are deprecated/removed.
* Requires to extend a TYPO3 API class/abstract.
* This is a strong match.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class AbstractMethodImplementationMatcher extends AbstractCoreMatcher
{
protected const DEFINITION_STATIC = 'static';
protected const DEFINITION_LOCAL = 'local';
protected array $matcherDefinitionLookup = [];
/**
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions();
$this->initializeFlatMatcherDefinitions();
// initializeFlatMatcherDefinitions() unfortunately does not deliver the actual
// method, so we need to do something 99% similar here for a custom
// property, to not require larger changes to the underlying abstract method.
foreach ($this->matcherDefinitions as $classAndMethod => $details) {
$parts = GeneralUtility::trimExplode('::', $classAndMethod);
$definition = self::DEFINITION_STATIC;
if (count($parts) !== 2) {
$parts = GeneralUtility::trimExplode('->', $classAndMethod);
$definition = self::DEFINITION_LOCAL;
}
// Exception-Handling removed, covered by initializeFlatMatcherDefinitions();
$method = $parts[1];
$class = $parts[0];
if (!array_key_exists($class, $this->matcherDefinitionLookup)) {
$this->matcherDefinitionLookup[$class][$definition][$method]['candidates'] = [];
}
$this->matcherDefinitionLookup[$class][$definition][$method]['candidates'][] = $details;
// Builds something like:
// [
// 'TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper' => [
// 'static' => [
// 'renderStatic' => [
// 'candidates' => [
// [
// 'restFiles' => [
// 'Deprecation-104789-RenderStaticForFluidViewHelpers.rst',
// ],
// ],
// ],
// ]
// ],
// ],
// 'TYPO3\CMS\AbstractSomething' => [
// 'local' => [
// 'someMethodName' => [
// 'candidates' => [
// [
// 'restFiles' => [
// 'Breaking-12345-something.rst',
// ],
// ],
// [
// 'restFiles' => [
// 'Breaking-67890-something.rst',
// ],
// ],
// ],
// ]
// ],
// ],
// ];
}
}
/**
* Called by PhpParser.
* Test for a defined method that shall longer be utilized (strong match)
*/
public function enterNode(Node $node): null
{
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof Node\Stmt\Class_
&& $node->extends) {
// We found a class definition.
// Check what classes this definition is extending (Abstract).
// Without a class extending something, this is not API usage and thus not scanned.
// Now check if the extended class is part of our matcherDefinition to inspect
if (array_key_exists($node->extends->name, $this->matcherDefinitionLookup)) {
// Iterate all declared methods (of the inspected custom class, NOT the abstract!)
$lookupMethods = $this->matcherDefinitionLookup[$node->extends->name];
foreach ($node->getMethods() as $method) {
// The matcherDefinition can utilize 'Abstract::staticMethod' or 'Abstract->localMethod',
// which is handled distinctly, so that the matches are stronger.
$lookupKey = $method->isStatic() ? self::DEFINITION_STATIC : self::DEFINITION_LOCAL;
if (isset($lookupMethods[$lookupKey][$method->name->toString()]['candidates'])) {
// The checked method of an object extending a deprecated/BC class was a match.
// Gather final match info (multiple ReST files can apply to a single class+method)
foreach ($lookupMethods[$lookupKey][$method->name->toString()]['candidates'] as $candidate) {
$this->matches[] = [
'restFiles' => $candidate['restFiles'],
'line' => $method->getAttribute('startLine'),
'message' => sprintf(
'Definition of %s method "%s" extends from "%s"',
$lookupKey,
$method->name->toString(),
$node->extends->name
),
'indicator' => 'strong',
];
}
}
}
}
}
return null;
}
}
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Find usages of dropped configuration values and hook registrations.
* Matches on "last" key only.
* Definition of $GLOBALS['foo']['bar'] and usage as $foo['bar'] matches.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class ArrayDimensionMatcher extends AbstractCoreMatcher
{
/**
* Initialize "flat" matcher array from matcher definitions.
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions();
$this->initializeLastArrayKeyNameArray();
}
/**
* Called by PhpParser.
*/
public function enterNode(Node $node): null
{
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof ArrayDimFetch
&& isset($node->dim->value)
&& array_key_exists($node->dim->value, $this->flatMatcherDefinitions)
) {
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'message' => 'Access to array key "' . $node->dim->value . '"',
'indicator' => 'weak',
];
foreach ($this->flatMatcherDefinitions[$node->dim->value]['candidates'] as $candidate) {
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
}
$this->matches[] = $match;
}
return null;
}
/**
* Prepare 'lastKey' => [$details] array in flatMatcherDefinitions
*/
protected function initializeLastArrayKeyNameArray()
{
$methodNameArray = [];
foreach ($this->matcherDefinitions as $fullArrayString => $details) {
// Goal: find last part "foobar" of an array path "$foo['bar']['foobar']"
// Reverse string $foo['bar']['foobar']
$lastKey = strrev($fullArrayString);
// Cut off "['"
$lastKey = substr($lastKey, 2);
$lastKey = GeneralUtility::trimExplode('\'[', $lastKey);
// Last key name
$lastKey = $lastKey[0];
// And reverse key name again
$lastKey = strrev($lastKey);
if (!array_key_exists($lastKey, $methodNameArray)) {
$methodNameArray[$lastKey]['candidates'] = [];
}
$methodNameArray[$lastKey]['candidates'][] = $details;
}
$this->flatMatcherDefinitions = $methodNameArray;
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Scalar\String_;
/**
* Match access to a one dimensional $GLOBAL array
* Example "$GLOBALS['TYPO3_DB']"
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class ArrayGlobalMatcher extends AbstractCoreMatcher
{
/**
* Initialize "flat" matcher array from matcher definitions.
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions();
}
/**
* Called by PhpParser.
*/
public function enterNode(Node $node): null
{
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof ArrayDimFetch
&& $node->var instanceof Variable
&& $node->var->name === 'GLOBALS'
&& $node->dim instanceof String_
&& array_key_exists('$GLOBALS[\'' . $node->dim->value . '\']', $this->matcherDefinitions)
) {
$this->matches[] = [
'restFiles' => $this->matcherDefinitions['$GLOBALS[\'' . $node->dim->value . '\']']['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => 'Access to array global array "' . $node->dim->value . '"',
'indicator' => 'strong',
];
}
return null;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Name\FullyQualified;
/**
* Find usages of class constants.
*
* Test for "Class\Name::THE_CONSTANT", matches are considered "strong"
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class ClassConstantMatcher extends AbstractCoreMatcher
{
/**
* Validate config
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions();
}
/**
* Called by PhpParser.
*/
public function enterNode(Node $node): null
{
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof ClassConstFetch
&& $node->class instanceof FullyQualified
&& array_key_exists($node->class->toString() . '::' . $node->name, $this->matcherDefinitions)
) {
// No weak test implemented - combination class::const name tested
$this->matches[] = [
'restFiles' => $this->matcherDefinitions[$node->class->toString() . '::' . $node->name]['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => 'Call to class constant "' . $node->class->toString() . '::' . $node->name . '"',
'indicator' => 'strong',
];
}
return null;
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Name\FullyQualified;
/**
* Find usages of class / interface names which are entirely deprecated or removed
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class ClassNameMatcher extends AbstractCoreMatcher
{
/**
* Default constructor validates matcher definition.
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions();
}
/**
* Called by PhpParser.
*
* @param Node $node Given node to test
*/
public function enterNode(Node $node): null
{
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof FullyQualified
) {
$fullyQualifiedClassName = $node->toString();
if (array_key_exists($fullyQualifiedClassName, $this->matcherDefinitions)) {
$this->matches[] = [
'restFiles' => $this->matcherDefinitions[$fullyQualifiedClassName]['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => 'Usage of class "' . $fullyQualifiedClassName . '"',
'indicator' => 'strong',
];
}
}
return null;
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\ConstFetch;
/**
* Find usages of class constants.
*
* Test for "THE_CONSTANT", matches are considered "strong"
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class ConstantMatcher extends AbstractCoreMatcher
{
/**
* Validate config
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions();
}
/**
* Called by PhpParser.
*/
public function enterNode(Node $node): null
{
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof ConstFetch
&& array_key_exists($node->name->toString(), $this->matcherDefinitions)
) {
// Access to constants is detected as strong match
$this->matches[] = [
'restFiles' => $this->matcherDefinitions[$node->name->toString()]['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => 'Call to global constant "' . $node->name->toString() . '"',
'indicator' => 'strong',
];
}
return null;
}
}
@@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\New_;
/**
* Finds invocations to class constructors and the amount of passed arguments.
* This matcher supports direct `new MyClass(123)` invocations as well as delegated
* calls to `GeneralUtility::makeInstance(MyClass::class, 123)` using `GeneratorClassResolver`.
*
* These configuration property names are handled independently:
* + numberOfMandatoryArguments
* + maximumNumberOfArguments
* + unusedArgumentNumbers
*
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class ConstructorArgumentMatcher extends AbstractCoreMatcher
{
protected const TOPIC_TYPE_REQUIRED = 'required';
protected const TOPIC_TYPE_DROPPED = 'dropped';
protected const TOPIC_TYPE_CALLED = 'called';
protected const TOPIC_TYPE_UNUSED = 'unused';
/**
* Prepare $this->flatMatcherDefinitions once and validate config
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitionsTopicRequirements([
self::TOPIC_TYPE_REQUIRED => ['numberOfMandatoryArguments'],
self::TOPIC_TYPE_DROPPED => ['maximumNumberOfArguments'],
self::TOPIC_TYPE_CALLED => ['numberOfMandatoryArguments', 'maximumNumberOfArguments'],
self::TOPIC_TYPE_UNUSED => ['unusedArgumentNumbers'],
]);
}
/**
* Called by PhpParser.
* Test for "->deprecated()" (weak match)
*/
public function enterNode(Node $node): null
{
if ($this->isFileIgnored($node) || $this->isLineIgnored($node)) {
return null;
}
$resolvedNode = $node->getAttribute(self::NODE_RESOLVED_AS, null) ?? $node;
if (!$resolvedNode instanceof New_
|| !isset($resolvedNode->class)
|| (isset($node->class) && is_object($node->class) && !method_exists($node->class, '__toString'))
|| !array_key_exists((string)$resolvedNode->class, $this->matcherDefinitions)
) {
return null;
}
// A method call is considered a match if it is not called with argument unpacking
// and number of used arguments is lower than numberOfMandatoryArguments
if ($this->isArgumentUnpackingUsed($resolvedNode->args)) {
return null;
}
// $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
// $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
$this->handleRequiredArguments($node, $resolvedNode);
$this->handleDroppedArguments($node, $resolvedNode);
$this->handleCalledArguments($node, $resolvedNode);
$this->handleUnusedArguments($node, $resolvedNode);
return null;
}
/**
* @param Node $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
* @param Node $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
*/
protected function handleRequiredArguments(Node $node, Node $resolvedNode): bool
{
$className = (string)($resolvedNode->class ?? '');
$candidate = $this->matcherDefinitions[$className][self::TOPIC_TYPE_REQUIRED] ?? null;
$mandatoryArguments = $candidate['numberOfMandatoryArguments'] ?? null;
$numberOfArguments = count($resolvedNode->args ?? []);
if ($candidate === null || $numberOfArguments >= $mandatoryArguments) {
return false;
}
$this->matches[] = [
'restFiles' => $candidate['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => sprintf(
'%s::__construct requires at least %d arguments (%d given).',
$className,
$mandatoryArguments,
$numberOfArguments
),
'indicator' => 'strong',
];
return true;
}
/**
* @param Node $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
* @param Node $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
*/
protected function handleDroppedArguments(Node $node, Node $resolvedNode): bool
{
$className = (string)($resolvedNode->class ?? '');
$candidate = $this->matcherDefinitions[$className][self::TOPIC_TYPE_DROPPED] ?? null;
$maximumArguments = $candidate['maximumNumberOfArguments'] ?? null;
$numberOfArguments = count($resolvedNode->args ?? []);
if ($candidate === null || $numberOfArguments <= $maximumArguments) {
return false;
}
$this->matches[] = [
'restFiles' => $candidate['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => sprintf(
'%s::__construct supports only %d arguments (%d given).',
$className,
$maximumArguments,
$numberOfArguments
),
'indicator' => 'strong',
];
return true;
}
/**
* @param Node $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
* @param Node $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
*/
protected function handleCalledArguments(Node $node, Node $resolvedNode): bool
{
$className = (string)($resolvedNode->class ?? '');
$candidate = $this->matcherDefinitions[$className][self::TOPIC_TYPE_CALLED] ?? null;
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($resolvedNode->args ?? []);
$mandatoryArguments = $candidate['numberOfMandatoryArguments'] ?? null;
$maximumArguments = $candidate['maximumNumberOfArguments'] ?? null;
$numberOfArguments = count($resolvedNode->args ?? []);
if ($candidate === null
|| !$isArgumentUnpackingUsed
&& ($numberOfArguments < $mandatoryArguments || $numberOfArguments > $maximumArguments)) {
return false;
}
$this->matches[] = [
'restFiles' => $candidate['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => sprintf(
'%s::__construct being called (%d arguments given).',
$className,
$numberOfArguments
),
'indicator' => 'weak',
];
return true;
}
/**
* @param Node $node reflects invocation, e.g. `GeneralUtility::makeInstance(MyClass::class, 123)`
* @param Node $resolvedNode reflects resolved and actual usage, e.g. `new MyClass(123)`
*/
protected function handleUnusedArguments(Node $node, Node $resolvedNode): bool
{
$className = (string)($resolvedNode->class ?? '');
$candidate = $this->matcherDefinitions[$className][self::TOPIC_TYPE_UNUSED] ?? null;
// values in array (if any) are actual position counts
// e.g. `[2, 4]` refers to internal argument indexes `[1, 3]`
$unusedArgumentPositions = $candidate['unusedArgumentNumbers'] ?? null;
if ($candidate === null || empty($unusedArgumentPositions)) {
return false;
}
$arguments = $resolvedNode->args ?? [];
// keeping positions having argument values that are not null
$unusedArgumentPositions = array_filter(
$unusedArgumentPositions,
static function (int $position) use ($arguments) {
$index = $position - 1;
return isset($arguments[$index]->value)
&& !$arguments[$index]->value instanceof ConstFetch
&& (
!isset($arguments[$index]->value->name->name->parts[0])
|| $arguments[$index]->value->name->name->parts[0] !== null
);
}
);
if (empty($unusedArgumentPositions)) {
return false;
}
$this->matches[] = [
'restFiles' => $candidate['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => sprintf(
'%s::__construct was called with argument positions %s not being null.',
$className,
implode(', ', $unusedArgumentPositions)
),
'indicator' => 'strong',
];
return true;
}
protected function validateMatcherDefinitionsTopicRequirements(array $topicRequirements): void
{
foreach ($this->matcherDefinitions as $key => $matcherDefinition) {
foreach ($topicRequirements as $topic => $requiredArrayKeys) {
if (empty($matcherDefinition[$topic])) {
continue;
}
$this->validateMatcherDefinitionKeys($key, $matcherDefinition[$topic], $requiredArrayKeys);
}
}
}
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name\FullyQualified;
/**
* Find usages of global function calls which were removed / deprecated.
* This is a strong match.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class FunctionCallMatcher extends AbstractCoreMatcher
{
/**
* Prepare $this->flatMatcherDefinitions once
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions(['numberOfMandatoryArguments', 'maximumNumberOfArguments']);
}
/**
* Called by PhpParser.
* Test for "removedFunction()" (strong match)
*/
public function enterNode(Node $node): null
{
// Match method call (not static)
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof FuncCall
&& $node->name instanceof FullyQualified
&& array_key_exists($node->name->toString(), $this->matcherDefinitions)
) {
$functionName = $node->name->toString();
$matchDefinition = $this->matcherDefinitions[$functionName];
$numberOfArguments = count($node->args);
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
if ($isArgumentUnpackingUsed
|| ($numberOfArguments >= $matchDefinition['numberOfMandatoryArguments']
&& $numberOfArguments <= $matchDefinition['maximumNumberOfArguments'])
) {
$this->matches[] = [
'restFiles' => $matchDefinition['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => 'Call to function "' . $functionName . '"',
'indicator' => 'strong',
];
}
}
return null;
}
}
@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Modifiers;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\ClassMethod;
/**
* Matches interface method arguments which have been dropped.
*
* This does *not* test if a class implements an interface.
* The scanner only looks for:
* - Class method names not having specified number of arguments
* - Method calls with given method name not having this number of arguments
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class InterfaceMethodChangedMatcher extends AbstractCoreMatcher
{
/**
* Default constructor validates config
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
// newNumberOfArguments must exist in all matcherDefinitions
$this->validateMatcherDefinitions(['newNumberOfArguments']);
}
/**
* Called by PhpParser.
* Test for "public function like($arg1, $arg2, $arg3) {}" (weak match)
* Test for "->like($arg1, $arg2, $arg3); (weak match)
*/
public function enterNode(Node $node): null
{
if ($this->isFileIgnored($node) || $this->isLineIgnored($node)) {
return null;
}
// Match method name of a class, must be public, wouldn't make sense as interface if protected/private
if ($node instanceof ClassMethod
&& array_key_exists($node->name->name, $this->matcherDefinitions)
&& $node->flags & Modifiers::PUBLIC // public
&& ($node->flags & Modifiers::STATIC) !== Modifiers::STATIC // not static
) {
$methodName = $node->name->name;
$numberOfUsedArguments = 0;
if (is_array($node->params ?? null)) {
$numberOfUsedArguments = count($node->params);
}
$numberOfAllowedArguments = $this->matcherDefinitions[$methodName]['newNumberOfArguments'];
if ($numberOfUsedArguments > $numberOfAllowedArguments) {
$this->matches[] = [
'restFiles' => $this->matcherDefinitions[$methodName]['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => 'Implementation of dropped interface argument for method "' . $methodName . '()"',
'indicator' => 'weak',
];
}
}
// Match method call (not static) with number of arguments
if ($node instanceof MethodCall
&& $node->name instanceof Identifier
&& array_key_exists($node->name->name, $this->matcherDefinitions)
) {
$methodName = $node->name->name;
$numberOfUsedArguments = 0;
if (is_array($node->args ?? null)) {
$numberOfUsedArguments = count($node->args);
}
// @todo: Test for argument unpacking
$numberOfAllowedArguments = $this->matcherDefinitions[$methodName]['newNumberOfArguments'];
if ($numberOfUsedArguments > $numberOfAllowedArguments) {
$this->matches[] = [
'restFiles' => $this->matcherDefinitions[$methodName]['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => 'Call to interface method "' . $methodName . '()"',
'indicator' => 'weak',
];
}
}
return null;
}
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Comment\Doc;
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
/**
* Find usages of method annotations
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MethodAnnotationMatcher extends AbstractCoreMatcher
{
/**
* Prepare $this->flatMatcherDefinitions once and validate config
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions();
}
/**
* Called by PhpParser.
* Test for method annotations (strong match)
*/
public function enterNode(Node $node): null
{
if ($node instanceof ClassMethod
&& ($docComment = $node->getDocComment()) instanceof Doc
&& !$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
) {
$isPossibleMatch = false;
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'indicator' => 'strong',
];
$matches = [];
preg_match_all(
'/\s*\s@(?<annotations>[^\s.]*).*\n/',
$docComment->getText(),
$matches
);
foreach ($matches['annotations'] as $annotation) {
$annotation = '@' . $annotation;
if (!isset($this->matcherDefinitions[$annotation])) {
continue;
}
$isPossibleMatch = true;
$match['message'] = 'Method "' . $node->name . '" uses an ' . $annotation . ' annotation.';
$match['restFiles'] = array_unique(array_merge(
$match['restFiles'],
$this->matcherDefinitions[$annotation]['restFiles']
));
}
if ($isPossibleMatch) {
$this->matches[] = $match;
}
}
return null;
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
/**
* Find usages of method calls which changed signature and dropped arguments,
* but are called with more arguments.
* This is a "weak" match since we're just testing for method name
* but not connected class.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MethodArgumentDroppedMatcher extends AbstractCoreMatcher
{
/**
* Prepare $this->flatMatcherDefinitions once and validate config
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions(['maximumNumberOfArguments']);
$this->initializeFlatMatcherDefinitions();
}
/**
* Called by PhpParser.
* Test for "->deprecated()" (weak match)
*/
public function enterNode(Node $node): null
{
// Match method call (not static)
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof MethodCall
&& $node->name instanceof Identifier
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
) {
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'indicator' => 'weak',
];
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
$numberOfArguments = count($node->args);
$isPossibleMatch = false;
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
// A method call is considered a match if it is not called with argument unpacking
// and number of used arguments is higher than maximumNumberOfArguments
if (!$isArgumentUnpackingUsed
&& $numberOfArguments > $candidate['maximumNumberOfArguments']
) {
$isPossibleMatch = true;
$match['message'] = 'Method "' . $node->name->name . '()" supports only ' . $candidate['maximumNumberOfArguments'] . ' arguments.';
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
}
}
if ($isPossibleMatch) {
$this->matches[] = $match;
}
}
return null;
}
}
@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name\FullyQualified;
/**
* Find usages of static method calls which were removed / deprecated.
* This is a "strong" match if class name is given and "weak" if not.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MethodArgumentDroppedStaticMatcher extends AbstractCoreMatcher
{
/**
* Prepare $this->flatMatcherDefinitions once and validate config
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions(['maximumNumberOfArguments']);
$this->initializeFlatMatcherDefinitions();
}
/**
* Called by PhpParser.
* Test for "->deprecated()" (weak match)
*/
public function enterNode(Node $node): null
{
// Match static method call
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof StaticCall
) {
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
if ($node->class instanceof FullyQualified && $node->name instanceof Identifier) {
// 'Foo\Bar::aMethod()' -> strong match
$fqdnClassWithMethod = $node->class->toString() . '::' . $node->name->name;
if (!$isArgumentUnpackingUsed
&& array_key_exists($fqdnClassWithMethod, $this->matcherDefinitions)
&& count($node->args) > $this->matcherDefinitions[$fqdnClassWithMethod]['maximumNumberOfArguments']
) {
$this->matches[] = [
'restFiles' => $this->matcherDefinitions[$fqdnClassWithMethod]['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => 'Method "' . $node->name->name . '()" supports only '
. $this->matcherDefinitions[$fqdnClassWithMethod]['maximumNumberOfArguments']
. ' arguments.',
'indicator' => 'strong',
];
}
} elseif ($node->class instanceof Variable
&& $node->name instanceof Identifier
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
) {
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'indicator' => 'weak',
];
$numberOfArguments = count($node->args);
$isPossibleMatch = false;
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
// A method call is considered a match if it is not called with argument unpacking
// and number of used arguments is higher than maximumNumberOfArguments
if (!$isArgumentUnpackingUsed
&& $numberOfArguments > $candidate['maximumNumberOfArguments']
) {
$isPossibleMatch = true;
$match['message'] = 'Method "' . $node->name->name . '()" supports only '
. $candidate['maximumNumberOfArguments'] . ' arguments.';
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
}
}
if ($isPossibleMatch) {
$this->matches[] = $match;
}
}
}
return null;
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
/**
* Find usages of method calls which changed signature and added required arguments.
* This is a "weak" match since we're just testing for method name
* but not connected class.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MethodArgumentRequiredMatcher extends AbstractCoreMatcher
{
/**
* Prepare $this->flatMatcherDefinitions once and validate config
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions(['numberOfMandatoryArguments']);
$this->initializeFlatMatcherDefinitions();
}
/**
* Called by PhpParser.
* Test for "->deprecated()" (weak match)
*/
public function enterNode(Node $node): null
{
// Match method call (not static)
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof MethodCall
&& $node->name instanceof Identifier
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
) {
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'indicator' => 'weak',
];
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
$numberOfArguments = count($node->args);
$isPossibleMatch = false;
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
// A method call is considered a match if it is not called with argument unpacking
// and number of used arguments is lower than numberOfMandatoryArguments
if (!$isArgumentUnpackingUsed
&& $numberOfArguments < $candidate['numberOfMandatoryArguments']
&& $numberOfArguments <= $candidate['maximumNumberOfArguments']
) {
$isPossibleMatch = true;
$match['message'] = 'Method ' . $node->name->name . '() needs at least ' . $candidate['numberOfMandatoryArguments'] . ' arguments.';
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
}
}
if ($isPossibleMatch) {
$this->matches[] = $match;
}
}
return null;
}
}
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name\FullyQualified;
/**
* Find usages of static method calls which gained new mandatory arguments.
* This is a "strong" match if class name is given and "weak" if not.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MethodArgumentRequiredStaticMatcher extends AbstractCoreMatcher
{
/**
* Prepare $this->flatMatcherDefinitions once and validate config
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions(['numberOfMandatoryArguments', 'maximumNumberOfArguments']);
$this->initializeFlatMatcherDefinitions();
}
/**
* Called by PhpParser.
* Test for "::function($1, $2, $3)" (strong match)
*/
public function enterNode(Node $node): null
{
// Match static method call
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof StaticCall
) {
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
if ($node->class instanceof FullyQualified && $node->name instanceof Identifier) {
// 'Foo\Bar::aMethod()' -> strong match
$fqdnClassWithMethod = $node->class->toString() . '::' . $node->name->name;
$numberOfArguments = count($node->args);
if (!$isArgumentUnpackingUsed
&& array_key_exists($fqdnClassWithMethod, $this->matcherDefinitions)
&& $numberOfArguments < $this->matcherDefinitions[$fqdnClassWithMethod]['numberOfMandatoryArguments']
// maximum number of arguments is just a measure against false positives
&& $numberOfArguments <= $this->matcherDefinitions[$fqdnClassWithMethod]['maximumNumberOfArguments']
) {
$this->matches[] = [
'restFiles' => $this->matcherDefinitions[$fqdnClassWithMethod]['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => 'Method "' . $node->name->name . '()" needs at least '
. $this->matcherDefinitions[$fqdnClassWithMethod]['numberOfMandatoryArguments']
. ' arguments.',
'indicator' => 'strong',
];
}
} elseif ($node->class instanceof Variable
&& $node->name instanceof Identifier
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
) {
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'indicator' => 'weak',
];
$numberOfArguments = count($node->args);
$isPossibleMatch = false;
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
// A method call is considered a match if it is not called with argument unpacking
// and number of used arguments is lesser than numberOfMandatoryArguments
if (!$isArgumentUnpackingUsed
&& $numberOfArguments < $candidate['numberOfMandatoryArguments']
// maximum number of arguments is just a measure against false positives
&& $numberOfArguments <= $candidate['maximumNumberOfArguments']
) {
$isPossibleMatch = true;
$match['message'] = 'Method "' . $node->name->name . '()" needs at least '
. $candidate['numberOfMandatoryArguments'] . ' arguments.';
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
}
}
if ($isPossibleMatch) {
$this->matches[] = $match;
}
}
}
return null;
}
}
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
/**
* Match method usages where arguments "in between" are unused but not given as "null":
*
* public function foo($arg1, $unused1 = null, $unused2 = null, $arg4)
* but called with:
* ->foo('arg1', 'notNull', null, 'arg4');
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MethodArgumentUnusedMatcher extends AbstractCoreMatcher
{
/**
* Prepare $this->flatMatcherDefinitions once and validate config
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions(['unusedArgumentNumbers']);
$this->initializeFlatMatcherDefinitions();
}
/**
* Called by PhpParser.
*/
public function enterNode(Node $node): null
{
// Match method call (not static)
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof MethodCall
&& $node->name instanceof Identifier
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
) {
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'indicator' => 'weak',
];
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
$numberOfArguments = count($node->args);
$isPossibleMatch = false;
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
foreach ($candidate['unusedArgumentNumbers'] as $droppedArgumentNumber) {
// A method call is considered a match if name matches, unpacking is not used
// and the registered argument is not given as null.
if (!$isArgumentUnpackingUsed
&& $numberOfArguments >= $droppedArgumentNumber
&& !($node->args[$droppedArgumentNumber - 1]->value instanceof ConstFetch)
&& (!isset($node->args[$droppedArgumentNumber - 1]->value->name->name->parts[0])
|| $node->args[$droppedArgumentNumber - 1]->value->name->name->parts[0] !== null)
) {
$isPossibleMatch = true;
$match['message'] = 'Call to method "' . $node->name->name . '()" with'
. ' argument ' . $droppedArgumentNumber . ' not given as null.';
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
}
}
}
if ($isPossibleMatch) {
$this->matches[] = $match;
}
}
return null;
}
}
@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar;
/**
* Find usages of arguments in method calls which were removed / deprecated.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MethodCallArgumentValueMatcher extends AbstractCoreMatcher
{
/**
* Prepare $this->flatMatcherDefinitions once
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions(['argumentMatches']);
$this->initializeFlatMatcherDefinitions();
}
/**
* Called by PhpParser.
* Test for "->method($someArgument)" (weak match)
* and for "fqcn::method($someArgument)" (strong match)
*/
public function enterNode(Node $node): null
{
// Match method call (not static)
if ($this->isFileIgnored($node)
|| $this->isLineIgnored($node)
) {
return null;
}
if ($node instanceof Node\Expr\StaticCall
&& $node->class instanceof FullyQualified
&& $node->name instanceof Identifier
&& array_key_exists($node->class->toString() . '::' . $node->name->name, $this->matcherDefinitions)
) {
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'message' => 'Call to specific argument (#%s) of static method "' . $node->class->toString() . '::' . $node->name->name . '()"',
'indicator' => 'strong',
];
$matchCandidate = [$this->matcherDefinitions[$node->class->toString() . '::' . $node->name->name]];
} elseif ($node instanceof MethodCall
&& $node->name instanceof Identifier
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
) {
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'message' => 'Call to specific argument (#%s) of method "' . $node->name->name . '()"',
'indicator' => 'weak',
];
$matchCandidate = $this->flatMatcherDefinitions[$node->name->name]['candidates'];
} else {
return null;
}
$isPossibleMatch = false;
// So far, the candidates just have their argument numbering and method name matching applied
// Now let's inspect whether the argument actually holds the value our droids are looking for
foreach ($matchCandidate as $candidate) {
$argumentNumbers = $this->isArgumentMatched($node, $candidate);
if ($argumentNumbers !== []) {
$isPossibleMatch = true;
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
$match['message'] = sprintf($match['message'], implode(', ', $argumentNumbers));
// One match will shortcut checking for others.
break;
}
}
if ($isPossibleMatch) {
$this->matches[] = $match;
}
return null;
}
/**
* Returns the array of matched arguments on a match candidate.
* Returns empty array if either none found or not ALL matches are matched (AND combined)
*/
private function isArgumentMatched(Node $node, array $candidate): array
{
$matchedArgumentNumbers = [];
foreach (($candidate['argumentMatches'] ?? []) as $argumentMatchArray) {
if (isset($node->args[$argumentMatchArray['argumentIndex']]->value->value)
&& $node->args[$argumentMatchArray['argumentIndex']]->value instanceof Scalar
&& $node->args[$argumentMatchArray['argumentIndex']]->value->value === $argumentMatchArray['argumentValue']) {
$matchedArgumentNumbers[] = $argumentMatchArray['argumentIndex'];
}
}
if (count($matchedArgumentNumbers) !== count($candidate['argumentMatches'] ?? [])) {
return [];
}
return $matchedArgumentNumbers;
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
/**
* Find usages of method calls which were removed / deprecated.
* This is a "weak" match since we're just testing for method name
* but not connected class.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MethodCallMatcher extends AbstractCoreMatcher
{
/**
* Prepare $this->flatMatcherDefinitions once
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions(['numberOfMandatoryArguments', 'maximumNumberOfArguments']);
$this->initializeFlatMatcherDefinitions();
}
/**
* Called by PhpParser.
* Test for "->deprecated()" (weak match)
*/
public function enterNode(Node $node): null
{
// Match method call (not static)
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof MethodCall
&& $node->name instanceof Identifier
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
) {
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'message' => 'Call to method "' . $node->name->name . '()"',
'indicator' => 'weak',
];
$numberOfArguments = count($node->args);
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
$isPossibleMatch = false;
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
// A method call is considered a match if it is called with argument unpacking, or
// if the number of given arguments is within range of mandatory / max number of arguments
if ($isArgumentUnpackingUsed
|| ($numberOfArguments >= $candidate['numberOfMandatoryArguments']
&& $numberOfArguments <= $candidate['maximumNumberOfArguments'])
) {
$isPossibleMatch = true;
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
}
}
if ($isPossibleMatch) {
$this->matches[] = $match;
}
}
return null;
}
}
@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name\FullyQualified;
/**
* Find usages of static method calls which were removed / deprecated.
*
* This match is performed either is case of a direct "foo\bar::aMethod()" call
* as "strong" match, or as only "::aMethod()" as "weak" match.
*
* As additional indicator, the number of required, mandatory arguments is
* recognized: If calling a static method as "$foo::aMethod($arg1), but the
* method needs two arguments, this is *not* considered a match. This would
* have raised a fatal PHP error anyway and this is nothing we test here.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MethodCallStaticMatcher extends AbstractCoreMatcher
{
/**
* Validate config and prepare weak matcher array
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions(['numberOfMandatoryArguments', 'maximumNumberOfArguments']);
$this->initializeFlatMatcherDefinitions();
}
/**
* Called by PhpParser.
* Test for "foo\bar::deprecated()" (strong match)
* Test for "::deprecated()" (weak match)
*/
public function enterNode(Node $node): null
{
// Static call, not method call
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof StaticCall
) {
if ($node->class instanceof FullyQualified && $node->name instanceof Identifier) {
// 'Foo\Bar::deprecated()' -> strong match
$fqdnClassWithMethod = $node->class->toString() . '::' . $node->name->name;
if (array_key_exists($fqdnClassWithMethod, $this->matcherDefinitions)) {
$this->matches[] = [
'restFiles' => $this->matcherDefinitions[$fqdnClassWithMethod]['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => 'Use of static class method call "' . $fqdnClassWithMethod . '()"',
'indicator' => 'strong',
];
}
} elseif ($node->class instanceof Variable
&& $node->name instanceof Identifier
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
) {
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'message' => 'Use of static class method call "' . $node->name->name . '()"',
'indicator' => 'weak',
];
$numberOfArguments = count($node->args);
$isArgumentUnpackingUsed = $this->isArgumentUnpackingUsed($node->args);
$isPossibleMatch = false;
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
// A method call is considered a match if it is called with argument unpacking, or
// if the number of given arguments is within range of mandatory / max number of arguments
if ($isArgumentUnpackingUsed
|| ($numberOfArguments >= $candidate['numberOfMandatoryArguments']
&& $numberOfArguments <= $candidate['maximumNumberOfArguments'])
) {
$isPossibleMatch = true;
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
}
}
if ($isPossibleMatch) {
$this->matches[] = $match;
}
}
}
return null;
}
}
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Comment\Doc;
use PhpParser\Node;
use PhpParser\Node\PropertyItem;
use PhpParser\Node\Stmt\Property;
/**
* Find usages of property annotations
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class PropertyAnnotationMatcher extends AbstractCoreMatcher
{
/**
* Prepare $this->flatMatcherDefinitions once and validate config
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions();
}
/**
* Called by PhpParser.
* Test for property annotations (strong match)
*/
public function enterNode(Node $node): null
{
if ($node instanceof Property
&& ($property = reset($node->props)) instanceof PropertyItem
&& ($docComment = $node->getDocComment()) instanceof Doc
&& !$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
) {
/** @var PropertyItem $property */
$isPossibleMatch = false;
$match = [
'restFiles' => [],
'line' => $property->getAttribute('startLine'),
'indicator' => 'strong',
];
$matches = [];
preg_match_all(
'/\s*\s@(?<annotations>[^\s.]*).*\n/',
$docComment->getText(),
$matches
);
foreach ($matches['annotations'] as $annotation) {
$annotation = '@' . $annotation;
if (!isset($this->matcherDefinitions[$annotation])) {
continue;
}
$isPossibleMatch = true;
$match['message'] = 'Property "' . $property->name . '" uses an ' . $annotation . ' annotation.';
$match['restFiles'] = array_unique(array_merge(
$match['restFiles'],
$this->matcherDefinitions[$annotation]['restFiles']
));
}
if ($isPossibleMatch) {
$this->matches[] = $match;
}
}
return null;
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Stmt\Property;
/**
* Find usages of properties which have been deprecated or removed.
* Useful if abstract classes remove properties.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class PropertyExistsStaticMatcher extends AbstractCoreMatcher
{
/**
* Validate config and prepare flat mach array
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions();
}
/**
* Called by PhpParser.
*/
public function enterNode(Node $node): null
{
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof Property
&& $node->isStatic()
&& !$node->isPrivate()
&& array_key_exists($node->props[0]->name->name, $this->matcherDefinitions)
) {
$propertyName = $node->props[0]->name->name;
$match = [
'restFiles' => $this->matcherDefinitions[$propertyName]['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => 'Use of property "' . $node->props[0]->name->name . '"',
'indicator' => 'weak',
];
$this->matches[] = $match;
}
return null;
}
}
@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Identifier;
/**
* Find usages of properties which have been made protected and are
* not called in $this context.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class PropertyProtectedMatcher extends AbstractCoreMatcher
{
/**
* Validate config and prepare flat mach array
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions();
$this->initializeFlatMatcherDefinitions();
}
/**
* Called by PhpParser.
*/
public function enterNode(Node $node): null
{
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof PropertyFetch
&& $node->name instanceof Identifier
&& ($node->var->name ?? '') !== 'this'
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
) {
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'message' => 'Fetch of property "' . $node->name->name . '"',
'indicator' => 'weak',
];
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
}
$this->matches[] = $match;
}
return null;
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Identifier;
/**
* Find usages of properties which were removed / deprecated.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class PropertyPublicMatcher extends AbstractCoreMatcher
{
/**
* Prepare $this->flatMatcherDefinitions once and validate config
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions();
$this->initializeFlatMatcherDefinitions();
}
/**
* Called by PhpParser.
*/
public function enterNode(Node $node): null
{
// Match property access (not static)
if (!$this->isFileIgnored($node)
&& !$this->isLineIgnored($node)
&& $node instanceof PropertyFetch
&& $node->name instanceof Identifier
&& array_key_exists($node->name->name, $this->flatMatcherDefinitions)
) {
$match = [
'restFiles' => [],
'line' => $node->getAttribute('startLine'),
'message' => 'Fetch of property "' . $node->name->name . '"',
'indicator' => 'weak',
];
foreach ($this->flatMatcherDefinitions[$node->name->name]['candidates'] as $candidate) {
$match['restFiles'] = array_unique(array_merge($match['restFiles'], $candidate['restFiles']));
}
$this->matches[] = $match;
}
return null;
}
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php\Matcher;
use PhpParser\Node;
/**
* Find usage of special "magic" strings like TYPO3_MODE, so that
* usage scenarios like `defined('TYPO3_MODE') || die()` will be scanned,
* where the actual constant is NOT used.
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class ScalarStringMatcher extends AbstractCoreMatcher
{
/**
* Default constructor validates matcher definition.
*
* @param array $matcherDefinitions Incoming main configuration
*/
public function __construct(array $matcherDefinitions)
{
$this->matcherDefinitions = $matcherDefinitions;
$this->validateMatcherDefinitions();
}
/**
* Called by PhpParser.
*
* @param Node $node Given node to test
*/
public function enterNode(Node $node): null
{
// Early return
if ($this->isFileIgnored($node)
|| $this->isLineIgnored($node)) {
return null;
}
// Check if the node contains the specific string
if (!$node instanceof Node\Scalar\String_) {
return null;
}
// Note: This is intentionally meant to be an exact match for now, no trimming or substring.
// Could be enhanced in the future with options to the configuration how to match.
// Using weak match to indicate that the magic string usage may not necessarily
// refer to the functionality we're matching. Other than TYPO3_MODE, future definitions
// will probably be weaker than this strong constant comparison.
$stringToMatch = (string)($node->name ?? $node->value);
if (array_key_exists($stringToMatch, $this->matcherDefinitions)) {
$this->matches[] = [
'restFiles' => $this->matcherDefinitions[$stringToMatch]['restFiles'],
'line' => $node->getAttribute('startLine'),
'message' => 'Usage of string "' . $stringToMatch . '"',
'indicator' => 'weak',
];
}
return null;
}
}
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Install\ExtensionScanner\Php;
use PhpParser\NodeVisitor;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\ExtensionScanner\CodeScannerInterface;
/**
* Factory preparing matcher instances
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
*/
class MatcherFactory
{
/**
* Create matcher instances and hand over configuration.
*
* @param array $matcherConfigurations Incoming configuration array
* @return NodeVisitor[]|CodeScannerInterface[]
* @throws \RuntimeException
*/
public function createAll(array $matcherConfigurations)
{
$instances = [];
foreach ($matcherConfigurations as $matcherConfiguration) {
if (empty($matcherConfiguration['class'])) {
throw new \RuntimeException(
'Each matcher must have a class name',
1501415721
);
}
if (empty($matcherConfiguration['configurationFile']) && !isset($matcherConfiguration['configurationArray'])) {
throw new \RuntimeException(
'Each matcher must have either a configurationFile or configurationArray defined',
1501416365
);
}
if (isset($matcherConfiguration['configurationFile']) && isset($matcherConfiguration['configurationArray'])) {
throw new \RuntimeException(
'Having both a configurationFile and configurationArray is invalid',
1501419367
);
}
$configuration = [];
if (isset($matcherConfiguration['configurationFile'])) {
$configuration = GeneralUtility::getFileAbsFileName($matcherConfiguration['configurationFile']);
if (empty($configuration) || !is_file($configuration)) {
throw new \RuntimeException(
'Configuration file ' . $matcherConfiguration['configurationFile'] . ' not found',
1501509605
);
}
$configuration = require $configuration;
if (!is_array($configuration)) {
throw new \RuntimeException(
'Configuration file ' . $matcherConfiguration['configurationFile'] . ' must return an array',
1501509548
);
}
}
if (isset($matcherConfiguration['configurationArray'])) {
if (!is_array($matcherConfiguration['configurationArray'])) {
throw new \RuntimeException(
'Configuration array ' . $matcherConfiguration['configurationArray'] . ' must not be empty',
1501509738
);
}
$configuration = $matcherConfiguration['configurationArray'];
}
$matcherInstance = new $matcherConfiguration['class']($configuration);
if (!$matcherInstance instanceof CodeScannerInterface
|| !$matcherInstance instanceof NodeVisitor) {
throw new \RuntimeException(
'Matcher ' . $matcherConfiguration['class'] . ' must implement CodeScannerInterface'
. ' and NodeVisitor',
1501510168
);
}
$instances[] = $matcherInstance;
}
return $instances;
}
}