Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: introduce FunctionConfigurationException to streamline error reporting #313

Merged
merged 2 commits into from Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -250,11 +250,11 @@ 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.');
}
}
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());
}
}