Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/Builder/ActionStats.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@ public static function fromArray(array $data): self
}

/**
* @return array<string, int|list<string>>
* @return array{
* actions: int, linked: int, toClass: int, unlinkedCount: int, unlinked: list<string>, forwards: int,
* forwardsLinked: int, templates: int, unresolvedTargetCount: int, unresolvedTargets: list<string>,
* citations: int, citingFiles: int, unreadable: list<string>,
* }
*/
public function toArray(): array
{
Expand Down
6 changes: 5 additions & 1 deletion src/Builder/ApiSchemaStats.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ public static function fromArray(array $data): self
}

/**
* @return array<string, int|list<string>>
* @return array{
* documents: int, operations: int, implemented: int, notImplemented: list<string>, routesCovered: int,
* undocumentedCount: int, undocumented: list<string>, rpcs: int, rpcsImplemented: int,
* rpcsNotImplemented: list<string>, otherApis: int, requests: int,
* }
*/
public function toArray(): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Builder/BuildState.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class BuildState
* @param string $names ids every name resolved to
* @param list<Edge> $overrides
* @param array<string, list<array{?Edge, string, string}>> $calls file => [edge added, outcome, method] per call site
* @param array<string, array{int, int, array<mixed>}> $configuration parsed configuration files: [mtime, size, data]
* @param array<string, array{int, int, array{definitions: array<string, string>, parameters: array<string, string>, tags: list<array{id: ?string, instanceof: ?string, name: string, attributes: array<string, string>}>, arguments: list<array{id: string, tag: ?string, service: ?string, locator?: bool}>}}> $configuration parsed configuration files: [mtime, size, what each declares]
*/
public function __construct(
public readonly string $options,
Expand Down
6 changes: 5 additions & 1 deletion src/Builder/BusStats.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ public static function fromArray(array $data): self
}

/**
* @return array<string, mixed>
* @return array{
* handlers: array<string, int>, sends: array<string, int>, untypedSends: int,
* messagesWithoutHandlerCount: int, messagesWithoutHandler: list<string>,
* messagesNeverSentCount: int, messagesNeverSent: list<string>, contracts: int,
* }
*/
public function toArray(): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Builder/CallStats.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function total(): int
}

/**
* @return array<string, int>
* @return array{total: int, inferred: int, ambiguous: int, outsideProject: int, unknownReceiver: int, chainOutsideProject: int}
*/
public function toArray(): array
{
Expand Down
192 changes: 95 additions & 97 deletions src/Builder/ConfigurationEvaluator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@

namespace PhpGraph\Builder;

use PhpGraph\Extractor\ConfigurationHelper;
use PhpGraph\Extractor\ConfigurationHelpers;
use PhpGraph\Extractor\DefinitionKind;
use PhpGraph\Extractor\PendingHelperCall;
use PhpGraph\Extractor\ServiceDefinitionFact;
use PhpGraph\Extractor\ServiceTagFact;
use PhpGraph\Extractor\Template;
use PhpGraph\Extractor\TemplateKind;

/**
* Evaluates the service configuration written in helpers (see ConfigurationHelpers): each call whose arguments are
Expand All @@ -18,10 +25,10 @@ final class ConfigurationEvaluator
{
private const MAX_DEPTH = 5;

/** @var array<string, array{params: list<string>, facts: list<array<string, mixed>>, file: string}> lowercase helper id => helper */
/** @var array<string, array{helper: ConfigurationHelper, file: string}> lowercase helper id => helper */
private array $helpers = [];

/** @var list<array{caller: ?string, callee: string, args: list<array{?string, ?array<mixed>}>, file: string, service: string}> */
/** @var list<array{call: PendingHelperCall, file: string, service: string}> */
private array $calls = [];

/** @var array{definitions: array<string, array<string, string>>, tags: array<string, list<array{id: ?string, instanceof: ?string, name: string, attributes: array<string, string>}>>, arguments: array<string, list<array{id: string, tag: ?string, service: ?string, locator?: bool}>>} */
Expand All @@ -34,33 +41,27 @@ public function __construct(private readonly \Closure $constant)
{
}

/**
* @param array{params: list<string>, facts: list<array<string, mixed>>} $helper
*/
public function addHelper(string $id, array $helper, string $file): void
public function addHelper(string $id, ConfigurationHelper $helper, string $file): void
{
$this->helpers[strtolower($id)] = $helper + ['file' => $file];
$this->helpers[strtolower($id)] = ['helper' => $helper, 'file' => $file];
}

/**
* @param array{caller: ?string, callee: string, args: list<array{?string, ?array<mixed>}>, line: int} $call
*/
public function addCall(array $call, string $file, string $service): void
public function addCall(PendingHelperCall $call, string $file, string $service): void
{
$this->calls[] = ['caller' => $call['caller'], 'callee' => $call['callee'], 'args' => $call['args'], 'file' => $file, 'service' => $service];
$this->calls[] = ['call' => $call, 'file' => $file, 'service' => $service];
}

/**
* @return array{definitions: array<string, array<string, string>>, tags: array<string, list<array{id: ?string, instanceof: ?string, name: string, attributes: array<string, string>}>>, arguments: array<string, list<array{id: string, tag: ?string, service: ?string, locator?: bool}>>}
*/
public function evaluate(): array
{
foreach ($this->calls as $call) {
foreach ($this->calls as ['call' => $call, 'file' => $file, 'service' => $service]) {
// The calls whose arguments do not depend on the parameters of a caller: the others run with their caller.
if (!isset($this->helpers[strtolower($call['callee'])]) || $this->usesParameters($call['args'])) {
if (!isset($this->helpers[strtolower($call->callee)]) || $call->usesParameters()) {
continue;
}
$this->run($call['callee'], $this->bind($call['callee'], $call['args'], [], $call['file']), $call['service'], 0);
$this->run($call->callee, $this->bind($call, [], $file), $service, 0);
}

return $this->result;
Expand All @@ -76,107 +77,89 @@ private function run(string $callee, array $bindings, string $service, int $dept
return;
}

$value = fn (?array $template): ?string => $template === null ? null : $this->value($template, $bindings, $helper['file']);
foreach ($helper['facts'] as $fact) {
if ($fact['kind'] === 'set') {
$id = $value(\is_array($fact['id']) ? $fact['id'] : null);
$class = $value(\is_array($fact['class'] ?? null) ? $fact['class'] : null);
$value = fn (?Template $template): ?string => $template === null ? null : $this->value($template, $bindings, $helper['file']);
foreach ($helper['helper']->facts as $fact) {
if ($fact instanceof ServiceDefinitionFact) {
$id = $value($fact->id);
$class = $value($fact->class);
if ($id !== null) {
$this->result['definitions'][$service][$id] ??= $class ?? $id;
}
} elseif ($fact['kind'] === 'tag' && \is_array($fact['definition'] ?? null)) {
[$kind, $template] = $fact['definition'];
$target = $value(\is_array($template) ? $template : null);
$name = $value(\is_array($fact['name']) ? $fact['name'] : null);
} elseif ($fact instanceof ServiceTagFact) {
$target = $value($fact->target);
$name = $value($fact->name);
if ($target === null || $name === null) {
continue;
}
$this->result['tags'][$service][] = match ($kind) {
'instanceof' => ['id' => null, 'instanceof' => ltrim($target, '\\'), 'name' => $name, 'attributes' => []],
'load' => ['id' => rtrim(ltrim($target, '\\'), '\\') . '\\', 'instanceof' => null, 'name' => $name, 'attributes' => []],
default => ['id' => $target, 'instanceof' => null, 'name' => $name, 'attributes' => []],
$this->result['tags'][$service][] = match ($fact->kind) {
DefinitionKind::Instanceof => ['id' => null, 'instanceof' => ltrim($target, '\\'), 'name' => $name, 'attributes' => []],
DefinitionKind::Load => ['id' => rtrim(ltrim($target, '\\'), '\\') . '\\', 'instanceof' => null, 'name' => $name, 'attributes' => []],
DefinitionKind::Set => ['id' => $target, 'instanceof' => null, 'name' => $name, 'attributes' => []],
};
} elseif ($fact['kind'] === 'argument') {
$id = $value(\is_array($fact['id']) ? $fact['id'] : null);
$tag = $value(\is_array($fact['tag'] ?? null) ? $fact['tag'] : null);
$target = $value(\is_array($fact['service'] ?? null) ? $fact['service'] : null);
} else {
$id = $value($fact->id);
$tag = $value($fact->tag);
$target = $value($fact->service);
if ($id !== null && ($tag !== null || $target !== null)) {
$this->result['arguments'][$service][] = ['id' => $id, 'tag' => $tag, 'service' => $target, 'locator' => ($fact['locator'] ?? false) === true];
$this->result['arguments'][$service][] = ['id' => $id, 'tag' => $tag, 'service' => $target, 'locator' => $fact->locator];
}
}
}

foreach ($this->calls as $call) {
if ($call['caller'] !== null && strtolower($call['caller']) === strtolower($callee) && $this->usesParameters($call['args'])) {
$this->run($call['callee'], $this->bind($call['callee'], $call['args'], $bindings, $helper['file']), $service, $depth + 1);
foreach ($this->calls as ['call' => $call]) {
if ($call->caller !== null && strtolower($call->caller) === strtolower($callee) && $call->usesParameters()) {
$this->run($call->callee, $this->bind($call, $bindings, $helper['file']), $service, $depth + 1);
}
}
}

/**
* The values of a call's arguments, by the callee's parameter names: positional, then named.
*
* @param list<array{?string, ?array<mixed>}> $args
* @param array<string, ?string> $bindings the caller's
* @param array<string, ?string> $bindings the caller's
*
* @return array<string, ?string>
*/
private function bind(string $callee, array $args, array $bindings, string $file): array
private function bind(PendingHelperCall $call, array $bindings, string $file): array
{
$params = $this->helpers[strtolower($callee)]['params'] ?? [];
$params = $this->helpers[strtolower($call->callee)]['helper']->params ?? [];
$bound = [];
foreach ($args as $position => [$name, $template]) {
$param = $name ?? ($params[$position] ?? null);
foreach ($call->arguments as $position => $argument) {
$param = $argument->name ?? ($params[$position] ?? null);
if ($param !== null) {
$bound[$param] = $template === null ? null : $this->value($template, $bindings, $file);
$bound[$param] = $argument->value === null ? null : $this->value($argument->value, $bindings, $file);
}
}

return $bound;
}

/**
* @param list<array{?string, ?array<mixed>}> $args
*/
private function usesParameters(array $args): bool
{
foreach ($args as [, $template]) {
if ($template !== null && str_contains(serialize($template), 's:5:"param"')) {
return true;
}
}

return false;
}

/**
* @param array<mixed> $template
* @param array<string, ?string> $bindings
*/
private function value(array $template, array $bindings, string $file): ?string
private function value(Template $template, array $bindings, string $file): ?string
{
$parts = $template[1] ?? null;

return match ($template[0] ?? null) {
'lit' => \is_string($parts) ? $parts : null,
'param' => \is_string($parts) ? $bindings[$parts] ?? null : null,
'dir' => \dirname($file),
'const' => \is_string($parts) ? ($this->constant)($parts, $file) : null,
'concat' => \is_array($parts) ? $this->concat($parts, $bindings, $file) : null,
'call' => \is_string($parts) && \is_array($template[2] ?? null) ? $this->call($parts, $template[2], $bindings, $file) : null,
default => null,
return match ($template->kind) {
TemplateKind::Literal => $template->value,
TemplateKind::Parameter => $bindings[$template->value] ?? null,
TemplateKind::Directory => \dirname($file),
TemplateKind::Constant => ($this->constant)($template->value, $file),
TemplateKind::Concat => $this->concat($template->parts, $bindings, $file),
TemplateKind::Call => $this->call($template->value, $template->parts, $bindings, $file),
// An array is the argument of a function only.
TemplateKind::List => null,
};
}

/**
* @param array<mixed> $parts
* @param list<Template> $parts
* @param array<string, ?string> $bindings
*/
private function concat(array $parts, array $bindings, string $file): ?string
{
$text = '';
foreach ($parts as $part) {
$value = \is_array($part) ? $this->value($part, $bindings, $file) : null;
$value = $this->value($part, $bindings, $file);
if ($value === null) {
return null;
}
Expand All @@ -187,7 +170,7 @@ private function concat(array $parts, array $bindings, string $file): ?string
}

/**
* @param array<mixed> $arguments templates, or ['list', [...]] for an array
* @param list<Template> $arguments
* @param array<string, ?string> $bindings
*/
private function call(string $function, array $arguments, array $bindings, string $file): ?string
Expand All @@ -198,13 +181,10 @@ private function call(string $function, array $arguments, array $bindings, strin

$values = [];
foreach ($arguments as $argument) {
if (!\is_array($argument)) {
return null;
}
if (($argument[0] ?? null) === 'list' && \is_array($argument[1] ?? null)) {
if ($argument->kind === TemplateKind::List) {
$list = [];
foreach ($argument[1] as $item) {
$list[] = \is_array($item) ? $this->value($item, $bindings, $file) : null;
foreach ($argument->parts as $item) {
$list[] = $this->value($item, $bindings, $file);
}
if (\in_array(null, $list, true)) {
return null;
Expand All @@ -220,28 +200,46 @@ private function call(string $function, array $arguments, array $bindings, strin
}

try {
$result = match ($function) {
'str_replace' => \count($values) === 3 ? str_replace($values[0], $values[1], \is_array($values[2]) ? implode('', $values[2]) : $values[2]) : null,
'sprintf' => isset($values[0]) && array_filter($values, 'is_string') === $values ? \sprintf(...$values) : null,
'implode' => \count($values) === 2 && \is_string($values[0]) && \is_array($values[1]) ? implode($values[0], $values[1]) : null,
default => isset($values[0]) && \is_string($values[0]) && array_filter($values, 'is_string') === $values
? match ($function) {
'trim' => trim($values[0], ...\array_slice($values, 1, 1)),
'ltrim' => ltrim($values[0], ...\array_slice($values, 1, 1)),
'rtrim' => rtrim($values[0], ...\array_slice($values, 1, 1)),
'strtolower' => strtolower($values[0]),
'strtoupper' => strtoupper($values[0]),
'ucfirst' => ucfirst($values[0]),
'lcfirst' => lcfirst($values[0]),
'dirname' => \dirname($values[0]),
default => basename($values[0]),
}
: null,
};
return $this->apply($function, $values);
} catch (\Throwable) {
return null;
}
}

/**
* @param list<string|list<string>> $values
*/
private function apply(string $function, array $values): ?string
{
if ($function === 'str_replace') {
if (\count($values) !== 3) {
return null;
}
$subject = \is_array($values[2]) ? implode('', $values[2]) : $values[2];

return \is_string($result) ? $result : null;
return str_replace($values[0], $values[1], $subject);
}
$allStrings = array_filter($values, 'is_string') === $values;
if ($function === 'sprintf') {
return isset($values[0]) && $allStrings ? \sprintf(...$values) : null;
}
if ($function === 'implode') {
return \count($values) === 2 && \is_string($values[0]) && \is_array($values[1]) ? implode($values[0], $values[1]) : null;
}
if (!isset($values[0]) || !\is_string($values[0]) || !$allStrings) {
return null;
}

return match ($function) {
'trim' => trim($values[0], ...\array_slice($values, 1, 1)),
'ltrim' => ltrim($values[0], ...\array_slice($values, 1, 1)),
'rtrim' => rtrim($values[0], ...\array_slice($values, 1, 1)),
'strtolower' => strtolower($values[0]),
'strtoupper' => strtoupper($values[0]),
'ucfirst' => ucfirst($values[0]),
'lcfirst' => lcfirst($values[0]),
'dirname' => \dirname($values[0]),
default => basename($values[0]),
};
}
}
Loading
Loading