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); } } } }