Skip to content

Commit

Permalink
Merge pull request #313 from jolicode/exception
Browse files Browse the repository at this point in the history
chore: introduce FunctionConfigurationException to streamline error reporting
  • Loading branch information
lyrixx committed Mar 8, 2024
2 parents 6c2dc66 + 2c2956c commit 8309033
Show file tree
Hide file tree
Showing 7 changed files with 62 additions and 38 deletions.
7 changes: 6 additions & 1 deletion src/Console/Command/TaskCommand.php
Expand Up @@ -10,6 +10,7 @@
use Castor\Event\AfterExecuteTaskEvent;
use Castor\Event\BeforeExecuteTaskEvent;
use Castor\EventDispatcher;
use Castor\Exception\FunctionConfigurationException;
use Castor\ExpressionLanguage;
use Castor\SluggerHelper;
use Symfony\Component\Console\Command\Command;
Expand Down Expand Up @@ -107,6 +108,10 @@ protected function configure(): void
$taskArgumentAttribute->suggestedValues,
);
} elseif ($taskArgumentAttribute instanceof AsOption) {
if ('verbose' === $name) {
throw new FunctionConfigurationException('You cannot re-define a "verbose" option. But you can use "output()->isVerbose()" in your code instead.', $this->function);
}

$mode = $taskArgumentAttribute->mode;
$defaultValue = $parameter->isOptional() ? $parameter->getDefaultValue() : null;

Expand All @@ -130,7 +135,7 @@ protected function configure(): void
);
}
} catch (LogicException $e) {
throw new \LogicException(sprintf('The argument "%s" for task "%s" cannot be configured: "%s".', $parameter->getName(), $this->getName(), $e->getMessage()));
throw new FunctionConfigurationException(sprintf('The argument "%s" cannot be configured: "%s".', $parameter->getName(), $e->getMessage()), $this->function, $e);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Context.php
Expand Up @@ -268,12 +268,12 @@ public function offsetGet(mixed $offset): mixed

public function offsetSet(mixed $offset, mixed $value): void
{
throw new \LogicException('Context is immutable');
throw new \LogicException('Context is immutable.');
}

public function offsetUnset(mixed $offset): void
{
throw new \LogicException('Context is immutable');
throw new \LogicException('Context is immutable.');
}

/**
Expand Down
25 changes: 9 additions & 16 deletions src/ContextRegistry.php
Expand Up @@ -2,6 +2,8 @@

namespace Castor;

use Castor\Exception\FunctionConfigurationException;

/** @internal */
class ContextRegistry
{
Expand All @@ -15,14 +17,18 @@ public function addDescriptor(ContextDescriptor $descriptor): void
{
$name = $descriptor->contextAttribute->name;
if (\array_key_exists($name, $this->descriptors)) {
throw new \RuntimeException(sprintf('You cannot defined two context with the same name "%s". There is one defined in "%s" and another in "%s".', $name, $this->describeFunction($this->descriptors[$name]->function), $this->describeFunction($descriptor->function)));
$alreadyDefined = $this->descriptors[$name]->function;

throw new FunctionConfigurationException(sprintf('You cannot define two contexts with the same name "%s". There is one already defined in "%s:%d".', $name, PathHelper::makeRelative((string) $alreadyDefined->getFileName()), $alreadyDefined->getStartLine()), $descriptor->function);
}

$this->descriptors[$name] = $descriptor;

if ($descriptor->contextAttribute->default) {
if ($this->defaultName) {
throw new \RuntimeException(sprintf('You cannot set multiple "default: true" context. There is one defined in "%s" and another in "%s".', $this->defaultName, $this->describeFunction($descriptor->function)));
$alreadyDefined = $this->descriptors[$this->defaultName]->function;

throw new FunctionConfigurationException(sprintf('You cannot set multiple "default: true" context. There is one already defined in "%s:%d".', PathHelper::makeRelative((string) $alreadyDefined->getFileName()), $alreadyDefined->getStartLine()), $descriptor->function);
}
$this->defaultName = $name;
}
Expand Down Expand Up @@ -73,7 +79,7 @@ public function get(?string $name = null): Context

$context = $this->descriptors[$name]->function->invoke();
if (!$context instanceof Context) {
throw new \LogicException(sprintf('The context generator "%s", defined at "%s:%s" must return an instance of "%s", "%s" returned', $name, $this->descriptors[$name]->function->getFileName(), $this->descriptors[$name]->function->getStartLine(), Context::class, get_debug_type($context)));
throw new FunctionConfigurationException(sprintf('The context generator must return an instance of "%s", "%s" returned.', Context::class, get_debug_type($context)), $this->descriptors[$name]->function);
}

return $context;
Expand Down Expand Up @@ -130,17 +136,4 @@ public function getNames(): array

return $names;
}

private function describeFunction(\ReflectionFunction $function): string
{
$name = $function->getName();
$shortFilename = str_replace(PathHelper::getRoot() . '/', '', (string) $function->getFileName());
$location = sprintf('%s:%d', $shortFilename, $function->getStartLine());

if (str_contains($name, '{closure}')) {
return $location;
}

return sprintf('%s@%s', $name, $location);
}
}
24 changes: 24 additions & 0 deletions src/Exception/FunctionConfigurationException.php
@@ -0,0 +1,24 @@
<?php

namespace Castor\Exception;

use Castor\PathHelper;

class FunctionConfigurationException extends \InvalidArgumentException
{
public function __construct(string $message, \ReflectionFunction|\ReflectionClass $function, ?\Throwable $e = null)
{
$message = sprintf(<<<'TXT'
Function "%s()" is not properly configured:
%s
Defined in "%s" line %d.
TXT,
$function->getName(),
$message,
PathHelper::makeRelative((string) $function->getFileName()),
$function->getStartLine(),
);

parent::__construct($message, previous: $e);
}
}
25 changes: 9 additions & 16 deletions src/FunctionFinder.php
Expand Up @@ -7,14 +7,13 @@
use Castor\Attribute\AsListener;
use Castor\Attribute\AsSymfonyTask;
use Castor\Attribute\AsTask;
use Castor\Exception\FunctionConfigurationException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Process\Process;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;

use function Symfony\Component\String\u;

/** @internal */
class FunctionFinder
{
Expand Down Expand Up @@ -49,8 +48,6 @@ public function findFunctions(string $path): iterable
* @param iterable<\SplFileInfo> $files
*
* @return iterable<TaskDescriptor|ContextDescriptor|ContextGeneratorDescriptor|ListenerDescriptor|SymfonyTaskDescriptor>
*
* @throws \ReflectionException
*/
private function doFindFunctions(iterable $files): iterable
{
Expand Down Expand Up @@ -96,8 +93,8 @@ private function resolveTasks(\ReflectionFunction $reflectionFunction): iterable

try {
$taskAttribute = $attributes[0]->newInstance();
} catch (\Throwable $th) {
throw new \InvalidArgumentException(sprintf('Could not instantiate the attribute "%s" on function "%s".', AsTask::class, $reflectionFunction->getName()), 0, $th);
} catch (\Throwable $e) {
throw new FunctionConfigurationException(sprintf('Could not instantiate the attribute "%s".', AsTask::class), $reflectionFunction, $e);
}

if ('' === $taskAttribute->name) {
Expand All @@ -112,7 +109,7 @@ private function resolveTasks(\ReflectionFunction $reflectionFunction): iterable

foreach ($taskAttribute->onSignals as $signal => $callable) {
if (!\is_callable($callable)) {
throw new \InvalidArgumentException(sprintf('The callable for signal "%s" is not callable on function "%s".', $signal, $reflectionFunction->getName()));
throw new FunctionConfigurationException(sprintf('The callable for signal "%s" is not callable.', $signal), $reflectionFunction);
}
}

Expand Down Expand Up @@ -155,7 +152,7 @@ private function resolveSymfonyTask(\ReflectionClass $reflectionClass): iterable
}

if (!$taskAttribute->name) {
throw new \RuntimeException('The task command must have a name.');
throw new FunctionConfigurationException('The task command must have a name.', $reflectionClass);
}

$definition = null;
Expand All @@ -167,7 +164,7 @@ private function resolveSymfonyTask(\ReflectionClass $reflectionClass): iterable
break;
}
if ($definition['name'] !== $taskAttribute->originalName) {
throw new \RuntimeException(sprintf('Could not find a command named "%s" in the Symfony application', $taskAttribute->name));
throw new FunctionConfigurationException(sprintf('Could not find a command named "%s" in the Symfony application', $taskAttribute->name), $reflectionClass);
}

yield new SymfonyTaskDescriptor($taskAttribute, $reflectionClass, $definition);
Expand Down Expand Up @@ -209,16 +206,16 @@ private function resolveContextGenerators(\ReflectionFunction $reflectionFunctio
$contextAttribute = $attributes[0]->newInstance();

if ($reflectionFunction->getParameters()) {
throw new \InvalidArgumentException(sprintf('The contexts generator "%s()" must not have arguments.', $reflectionFunction->getName()));
throw new FunctionConfigurationException('The contexts generator must not have arguments.', $reflectionFunction);
}
$generators = [];
foreach ($reflectionFunction->invoke() as $name => $generator) {
if (!$generator instanceof \Closure) {
throw new \InvalidArgumentException(sprintf('The context generator "%s" is not callable in function "%s()".', $name, $reflectionFunction->getName()));
throw new FunctionConfigurationException(sprintf('The context generator "%s" is not callable.', $name), $reflectionFunction);
}
$r = new \ReflectionFunction($generator);
if ($r->getParameters()) {
throw new \InvalidArgumentException(sprintf('The context generator "%s()::%s" must not have arguments.', $reflectionFunction->getName(), $name));
throw new FunctionConfigurationException(sprintf('The context generator "%s" must not have arguments.', $name), $reflectionFunction);
}
$generators[$name] = $generator;
}
Expand All @@ -237,10 +234,6 @@ private function resolveListeners(\ReflectionFunction $reflectionFunction): iter
/** @var AsListener $listenerAttribute */
$listenerAttribute = $attribute->newInstance();

if (u($listenerAttribute->event)->endsWith('::class') && !class_exists($listenerAttribute->event)) {
throw new \InvalidArgumentException(sprintf('The event "%s" does not exist.', $listenerAttribute->event));
}

yield new ListenerDescriptor($listenerAttribute, $reflectionFunction);
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/HasherHelper.php
Expand Up @@ -39,11 +39,11 @@ public function writeFile(string $path, FileHashStrategy $strategy = FileHashStr
}

if (!is_file($path)) {
throw new \InvalidArgumentException(sprintf('The path "%s" is not a file', $path));
throw new \InvalidArgumentException(sprintf('The path "%s" is not a file.', $path));
}

if (!is_readable($path)) {
throw new \InvalidArgumentException(sprintf('The file "%s" is not readable', $path));
throw new \InvalidArgumentException(sprintf('The file "%s" is not readable.', $path));
}

$this->logger->debug('Hashing file "{path}" with strategy "{strategy}".', [
Expand Down Expand Up @@ -97,7 +97,7 @@ public function writeGlob(string $pattern, FileHashStrategy $strategy = FileHash
$files = glob($pattern);

if (false === $files) {
throw new \InvalidArgumentException(sprintf('The pattern "%s" is invalid', $pattern));
throw new \InvalidArgumentException(sprintf('The pattern "%s" is invalid.', $pattern));
}

foreach ($files as $file) {
Expand Down
9 changes: 9 additions & 0 deletions src/PathHelper.php
Expand Up @@ -41,4 +41,13 @@ public static function realpath(string $path): string

return $realpath;
}

public static function makeRelative(string $path): string
{
if (!Path::isAbsolute($path)) {
throw new \RuntimeException(sprintf('Path "%s" is not absolute.', $path));
}

return Path::makeRelative($path, self::getRoot());
}
}

0 comments on commit 8309033

Please sign in to comment.