Skip to content

Commit

Permalink
MethodValue rewrite
Browse files Browse the repository at this point in the history
This allows us to squeeze a lot more perf out of complex systems
by omitting the access paths and depth. Effectively we can avoid
cloning all methods for every instance and just reuse the existing
MethodValues repeatedly.

Totally incompatible with text renderers since they pretty much
require depth to be set correctly
  • Loading branch information
jnvsor committed Nov 10, 2024
1 parent 971e582 commit 1ca8499
Show file tree
Hide file tree
Showing 16 changed files with 317 additions and 207 deletions.
Binary file modified build/kint.phar
Binary file not shown.
6 changes: 0 additions & 6 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@
<code><![CDATA[!KINT_PHP84]]></code>
</TypeDoesNotContainType>
</file>
<file src="src/Parser/ClassMethodsPlugin.php">
<TypeDoesNotContainType>
<code><![CDATA[!KINT_PHP80]]></code>
<code><![CDATA[!KINT_PHP80]]></code>
</TypeDoesNotContainType>
</file>
<file src="src/Parser/ClassStaticsPlugin.php">
<RedundantCondition>
<code><![CDATA[KINT_PHP84]]></code>
Expand Down
8 changes: 6 additions & 2 deletions src/Parser/ClassHooksPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
namespace Kint\Parser;

use Kint\Value\AbstractValue;
use Kint\Value\Context\MethodContext;
use Kint\Value\Context\PropertyContext;
use Kint\Value\DeclaredCallableBag;
use Kint\Value\InstanceValue;
use Kint\Value\MethodValue;
use Kint\Value\Representation\ContainerRepresentation;
Expand Down Expand Up @@ -88,8 +90,10 @@ public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractVa
continue;
}

$m = new MethodValue($hook);
$m->getContext()->depth = 1; // We don't have subs, but don't want search
$m = new MethodValue(
new MethodContext($hook),
new DeclaredCallableBag($hook)
);

$this->cache_verbose[$cowner][$cname][] = $m;

Expand Down
127 changes: 68 additions & 59 deletions src/Parser/ClassMethodsPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,17 @@

use Kint\Value\AbstractValue;
use Kint\Value\Context\MethodContext;
use Kint\Value\DeclaredCallableBag;
use Kint\Value\InstanceValue;
use Kint\Value\MethodValue;
use Kint\Value\Representation\CallableDefinitionRepresentation;
use Kint\Value\Representation\ContainerRepresentation;
use ReflectionClass;

/**
* @psalm-type OwnedMethodValue = MethodValue&object{context: MethodContext}
*/
class ClassMethodsPlugin extends AbstractPlugin implements PluginCompleteInterface
{
/** @psalm-var array<class-string, OwnedMethodValue[]> */
public static bool $show_access_path = true;

/** @psalm-var array<class-string, MethodValue[]> */
private array $cache = [];

public function getTypes(): array
Expand All @@ -67,82 +66,92 @@ public function parseComplete(&$var, AbstractValue $v, int $trigger): AbstractVa
return $v;
}

$class = $v->getClassName();
if ($contents = $this->getCachedMethods($v->getClassName())) {
if (self::$show_access_path && null !== $v->getContext()->getAccessPath()) {
$parser = $this->getParser();

foreach ($contents as $key => $val) {
if ($val->getContext()->isAccessible($parser->getCallerClass())) {
$val = clone $val;
$val->getContext()->setAccessPathFromParent($v);
}

$contents[$key] = $val;
}
}

$v->addRepresentation(new ContainerRepresentation('Available methods', $contents, 'methods'));
}

// assuming class definition will not change inside one request
return $v;
}

/**
* @psalm-param class-string $class
*
* @psalm-return MethodValue[]
*/
private function getCachedMethods(string $class): array
{
if (!isset($this->cache[$class])) {
$methods = [];

$r = new ReflectionClass($class);

foreach ($r->getMethods() as $mr) {
if (!KINT_PHP80 && $mr->isPrivate() && $mr->getDeclaringClass()->name !== $r->getName()) {
continue; // @codeCoverageIgnore
}

/** @psalm-var OwnedMethodValue $method */
$method = new MethodValue($mr);
$methods[$method->getContext()->getName()] = $method;
$parent_methods = [];
if ($parent = \get_parent_class($class)) {
$parent_methods = $this->getCachedMethods($parent);
}

while ($r = $r->getParentClass()) {
foreach ($r->getMethods() as $mr) {
if (!$mr->isPrivate() && !$mr->isStatic()) {
continue;
}

if (!KINT_PHP80 && $mr->isPrivate() && $mr->getDeclaringClass()->name !== $r->getName()) {
continue; // @codeCoverageIgnore
foreach ($r->getMethods() as $mr) {
if ($mr->getDeclaringClass()->name === $class) {
$method = new MethodValue(new MethodContext($mr), new DeclaredCallableBag($mr));
if ($mr->isPrivate()) {
$methods[] = $method;
} else {
$methods[$mr->name] = $method;
if (!$mr->isStatic()) {
unset($parent_methods[$mr->name]);
}
}
} elseif (isset($parent_methods[$mr->name])) {
$method = $parent_methods[$mr->name];
unset($parent_methods[$mr->name]);

if (isset($methods[$mr->name])) {
$c = $methods[$mr->name]->getContext();
if ($c->owner_class === $mr->getDeclaringClass()->name) {
continue;
}
if (!$method->getContext()->inherited) {
$method = clone $method;
$method->getContext()->inherited = true;
}

/** @psalm-var OwnedMethodValue $method */
$method = new MethodValue($mr);
$methods[] = $method;
$methods[$mr->name] = $method;
} elseif ($mr->getDeclaringClass()->isInterface()) {
$c = new MethodContext($mr);
$c->inherited = true;
$methods[$mr->name] = new MethodValue($c, new DeclaredCallableBag($mr));
}
}

$this->cache[$class] = $methods;
}

if (!empty($this->cache[$class])) {
$cdepth = $v->getContext()->getDepth();
$parser = $this->getParser();
$contents = [];

// Can't cache access paths or depth
foreach ($this->cache[$class] as $key => $m) {
$method = clone $m;
$mc = $method->getContext();
foreach ($parent_methods as $method) {
$needs_inherited = !$method->getContext()->inherited;
$needs_conflict = $method->getContext()->static && isset($methods[$method->getContext()->name]);

$mc->depth = $cdepth + 1;
if ($needs_inherited || $needs_conflict) {
$method = clone $method;

if ($mc->isAccessible($parser->getCallerClass())) {
$mc->setAccessPathFromParent($v);
}

if ($mc->owner_class !== $class && ($d = $method->getRepresentation('callable_definition')) instanceof CallableDefinitionRepresentation) {
$d = clone $d;
$d->inherited = true;
$method->replaceRepresentation($d);
}

if ($key !== $mc->name) {
$mc->name = $mc->owner_class.'::'.$mc->name;
if ($needs_inherited) {
$method->getContext()->inherited = true;
}
if ($needs_conflict) {
$method->getContext()->name_conflict = true;
}
}

$contents[] = $method;
$methods[] = $method;
}

$v->addRepresentation(new ContainerRepresentation('Available methods', $contents, 'methods'));
$this->cache[$class] = $methods;
}

return $v;
return $this->cache[$class];
}
}
11 changes: 9 additions & 2 deletions src/Renderer/Rich/CallableDefinitionPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

use Kint\Utils;
use Kint\Value\AbstractValue;
use Kint\Value\Context\ClassDeclaredContext;
use Kint\Value\MethodValue;
use Kint\Value\Representation\CallableDefinitionRepresentation;
use Kint\Value\Representation\RepresentationInterface;

Expand All @@ -41,8 +43,13 @@ public function renderTab(RepresentationInterface $r, AbstractValue $v): ?string
}

$docstring = [];
if (null !== ($class = $r->getClassName()) && $r->inherited) {
$docstring[] = 'Inherited from '.$this->renderer->escape($class);

if ($v instanceof MethodValue) {
$c = $v->getContext();

if ($c->inherited && ClassDeclaredContext::ACCESS_PRIVATE !== $c->access) {
$docstring[] = 'Inherited from '.$this->renderer->escape($c->owner_class);
}
}

$docstring[] = 'Defined in '.$this->renderer->escape(Utils::shortenPath($r->getFileName())).':'.$r->getLine();
Expand Down
86 changes: 70 additions & 16 deletions src/Value/Context/MethodContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
namespace Kint\Value\Context;

use Kint\Value\InstanceValue;
use ReflectionMethod;

class MethodContext extends ClassDeclaredContext
{
Expand Down Expand Up @@ -55,6 +56,47 @@ class MethodContext extends ClassDeclaredContext
public bool $abstract = false;
public bool $static = false;

/**
* Whether the method was inherited from a parent class or interface.
*
* It's important to note that while private parent methods are considered
* "inherited" they are shown to the user as "Declared" in the
* CallableDefinitionRepresentation since they can't be overridden, while
* the class name is prepended for clarity.
*/
public bool $inherited = false;

/**
* Whether the name conflicts with overridden methods.
*
* When you override a normal method the parent method becaomes inaccessible
* for use outside the instance but with static methods both parent::method()
* and child::method() can be accessed at the same time.
*
* When a child static method name conflicts with a parent static method
* name we flag the parent one with name_conflict so that the class name is
* prepended for clarity.
*/
public bool $name_conflict = false;

public function __construct(ReflectionMethod $method)
{
parent::__construct(
$method->getName(),
$method->getDeclaringClass()->name,
ClassDeclaredContext::ACCESS_PUBLIC
);
$this->depth = 1;
$this->static = $method->isStatic();
$this->abstract = $method->isAbstract();
$this->final = $method->isFinal();
if ($method->isProtected()) {
$this->access = ClassDeclaredContext::ACCESS_PROTECTED;
} elseif ($method->isPrivate()) {
$this->access = ClassDeclaredContext::ACCESS_PRIVATE;
}
}

public function getOperator(): string
{
if ($this->static) {
Expand All @@ -64,6 +106,15 @@ public function getOperator(): string
return '->';
}

public function getName(): string
{
if ($this->name_conflict || (ClassDeclaredContext::ACCESS_PRIVATE === $this->access && $this->inherited)) {
return $this->owner_class.'::'.$this->name;
}

return parent::getName();
}

public function getModifiers(): string
{
if ($this->abstract) {
Expand All @@ -83,28 +134,31 @@ public function getModifiers(): string
return $out;
}

public function setAccessPathFromParent(InstanceValue $parent): void
public function setAccessPathFromParent(?InstanceValue $parent): void
{
$c = $parent->getContext();

$name = \strtolower($this->getName());

if ('__construct' === $name) {
$this->access_path = 'new \\'.$parent->getClassName().'()';
} elseif ($this->static && !isset(self::MAGIC_NAMES[$name])) {
if ($this->static && !isset(self::MAGIC_NAMES[$name])) {
$this->access_path = '\\'.$this->owner_class.'::'.$this->name.'()';
} elseif (null === $c->getAccessPath()) {
$this->access_path = null;
} elseif ('__invoke' === $name) {
$this->access_path = $c->getAccessPath().'()';
} elseif ('__clone' === $name) {
$this->access_path = 'clone '.$c->getAccessPath();
} elseif ('__tostring' === $name) {
$this->access_path = '(string) '.$c->getAccessPath();
} elseif (isset(self::MAGIC_NAMES[$name])) {
} elseif (null === $parent) {
$this->access_path = null;
} else {
$this->access_path = $c->getAccessPath().'->'.$this->name.'()';
$c = $parent->getContext();
if ('__construct' === $name) {
$this->access_path = 'new \\'.$parent->getClassName().'()';
} elseif (null === $c->getAccessPath()) {
$this->access_path = null;
} elseif ('__invoke' === $name) {
$this->access_path = $c->getAccessPath().'()';
} elseif ('__clone' === $name) {
$this->access_path = 'clone '.$c->getAccessPath();
} elseif ('__tostring' === $name) {
$this->access_path = '(string) '.$c->getAccessPath();
} elseif (isset(self::MAGIC_NAMES[$name])) {
$this->access_path = null;
} else {
$this->access_path = $c->getAccessPath().'->'.$this->name.'()';
}
}
}
}
8 changes: 8 additions & 0 deletions src/Value/DeclaredCallableBag.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

use Kint\Utils;
use ReflectionFunctionAbstract;
use ReflectionMethod;

/** @psalm-api */
final class DeclaredCallableBag
Expand All @@ -43,6 +44,9 @@ final class DeclaredCallableBag
public ?int $startline;
/** @psalm-readonly */
public ?int $endline;
/** @psalm-readonly
* @psalm-var ?class-string */
public ?string $class = null;
/**
* @psalm-readonly
*
Expand All @@ -67,6 +71,10 @@ public function __construct(ReflectionFunctionAbstract $callable)
$this->docstring = false === $t ? null : $t;
$this->return_reference = $callable->returnsReference();

if ($callable instanceof ReflectionMethod) {
$this->class = $callable->getDeclaringClass()->name;
}

$rt = $callable->getReturnType();
if ($rt) {
$this->returntype = Utils::getTypeString($rt);
Expand Down
Loading

0 comments on commit 1ca8499

Please sign in to comment.