From 59de5c7ca11613dda2378e6814b56da49b5704e8 Mon Sep 17 00:00:00 2001 From: Exanlv <51094537+Exanlv@users.noreply.github.com> Date: Sat, 27 Jun 2026 10:52:23 +0200 Subject: [PATCH 1/7] Implement yanker to retrieve all required data in 1 go --- src/Analyzer/Dto/Yanked.php | 15 ++ src/Analyzer/Exception/EvaldCodeException.php | 7 + src/Analyzer/Yanker.php | 149 ++++++++++++++++++ tests/Analyzer/AnalyzerTestCase.php | 48 ++++++ tests/Analyzer/YankerTest.php | 82 ++++++++++ 5 files changed, 301 insertions(+) create mode 100644 src/Analyzer/Dto/Yanked.php create mode 100644 src/Analyzer/Exception/EvaldCodeException.php create mode 100644 src/Analyzer/Yanker.php create mode 100644 tests/Analyzer/AnalyzerTestCase.php create mode 100644 tests/Analyzer/YankerTest.php diff --git a/src/Analyzer/Dto/Yanked.php b/src/Analyzer/Dto/Yanked.php new file mode 100644 index 0000000..1a47383 --- /dev/null +++ b/src/Analyzer/Dto/Yanked.php @@ -0,0 +1,15 @@ + + * } + */ + $captures = []; + + /** + * @var callable(Closure(string|array $token): bool, Closure(array $tokens): void): void + */ + $captureUntil = function (Closure $end, Closure $then) use (&$captures): void { + $captures[] = [$end, $then, []]; + }; + + $handleCaptures = function (string|array $token) use (&$captures) { + foreach ($captures as $i => &$capture) { + $capture[2][] = $token; + + if ($capture[0]($token)) { + $capture[1]($capture[2]); + unset($captures[$i]); + } + } + }; + + $currentClass = []; + $currentMethodArgs = null; + $wasWhitespace = false; + foreach ($tokens as $token) { + if (static::isType($token, T_COMMENT)) { + continue; + } + + $isWhitespace = static::isType($token, T_WHITESPACE); + if ($isWhitespace) { + if ($wasWhitespace) { + continue; + } + + $wasWhitespace = true; + } else { + $wasWhitespace = false; + } + + if ($token === '{') { + $blockLevel++; + } + + if ($token === '}') { + $blockLevel--; + + $lastClass = static::lastItemInArray($currentClass) ?? -1; + + if ($blockLevel < $lastClass) { + array_pop($currentClass); + } + } + + if ($blockLevel === 0 && static::isType($token, T_USE)) { + $captureUntil(fn (string|array $token) => $token === ';', function (array $tokens) use (&$uses) { + $uses[] = $tokens; + }); + } + + if (static::isType($token, T_NAMESPACE)) { + $captureUntil(fn (string|array $token) => $token === ';', function (array $tokens) use (&$namespace) { + $namespace = $tokens; + }); + } + + if (static::isType($token, T_CLASS)) { + $captureUntil(fn(string|array $token) => static::isType($token, T_STRING), function (array $tokens) use (&$currentClass, $blockLevel) { + $nameToken = array_pop($tokens); + $currentClass[$nameToken[1]] = $blockLevel + 1; + }); + } + + if (static::isType($token, T_FUNCTION) && count($currentClass) > 0) { + $captureUntil(fn (string|array $token) => static::isType($token, T_STRING), + function (array $fnTokens) use ($captureUntil, $currentClass, &$currentMethodArgs, &$args, $methods) { + $nameToken = array_pop($fnTokens); + $currentMethodArgs = array_key_last($currentClass) . '.' . $nameToken[1]; + + $captureUntil( + fn (string|array $token) => $token === '{' || $token === ';', + function (array $tokens) use (&$currentMethodArgs, &$args, $fnTokens, $methods) { + $tokens = [ + ...$fnTokens, + ...$tokens, + ]; + + if (in_array($currentMethodArgs, $methods)) { + $args[$currentMethodArgs] = $tokens; + } + + $currentMethodArgs = null; + } + ); + } + ); + } + + $handleCaptures($token); + } + + return new Yanked($namespace, $uses, $args); + } + + private static function isType(string|array $token, int $type): bool + { + return is_array($token) && $token[0] === $type; + } + + private static function lastItemInArray(array $items): mixed + { + if ($items === []) { + return null; + } + + return end($items); + } +} diff --git a/tests/Analyzer/AnalyzerTestCase.php b/tests/Analyzer/AnalyzerTestCase.php new file mode 100644 index 0000000..5ae03c0 --- /dev/null +++ b/tests/Analyzer/AnalyzerTestCase.php @@ -0,0 +1,48 @@ +containsStringRecursive($needle, $haystack); + + Assert::assertTrue( + $found, + $message !== '' ? $message : "Failed asserting that array contains token '{$needle}'." + ); + } + + protected function assertDoesntContainTokenAnywhere(string $needle, array $haystack, string $message = ''): void + { + $found = $this->containsStringRecursive($needle, $haystack); + + Assert::assertFalse( + $found, + $message !== '' ? $message : "Failed asserting that array does not contain token '{$needle}'." + ); + } + + private function containsStringRecursive(string $needle, mixed $value): bool + { + if (is_string($value)) { + return $value === $needle; + } + + if (is_array($value)) { + foreach ($value as $item) { + if ($this->containsStringRecursive($needle, $item)) { + return true; + } + } + } + + return false; + } +} diff --git a/tests/Analyzer/YankerTest.php b/tests/Analyzer/YankerTest.php new file mode 100644 index 0000000..4d1b625 --- /dev/null +++ b/tests/Analyzer/YankerTest.php @@ -0,0 +1,82 @@ +assertContainsTokenAnywhere('Some\Cool\Namespace', $yanked->namespace); + + $this->assertContainsTokenAnywhere('Some\Namespaced\Class', $yanked->uses); + + $this->assertContainsTokenAnywhere('Some\OtherClass\Too', $yanked->uses); + $this->assertContainsTokenAnywhere('SomeAlias', $yanked->uses); + + $this->assertContainsTokenAnywhere('A\Third\Class', $yanked->uses); + $this->assertContainsTokenAnywhere('With\Funky\Syntax', $yanked->uses); + $this->assertContainsTokenAnywhere('WayToMakeYouCry', $yanked->uses); + $this->assertContainsTokenAnywhere('AndMore', $yanked->uses); + + $this->assertDoesntContainTokenAnywhere('ShouldntBeCaptured', $yanked->uses); + + $this->assertContainsTokenAnywhere('Some\Import\In\Wrong\But\Valid\Place', $yanked->uses); + + $this->assertDoesntContainTokenAnywhere('/* Why would you put a comment here? */', $yanked->uses); + $this->assertDoesntContainTokenAnywhere('// There\'s even a comment here', $yanked->uses); + + foreach ($yanked->uses as $import) { + $this->assertEquals(T_USE, $import[0][0]); + $this->assertEquals(';', $import[count($import) - 1]); + } + } + + #[Test] + public function it_extracts_method_args_from_classes() + { + $yanked = Yanker::fetch(<< Date: Sun, 5 Jul 2026 03:03:45 +0200 Subject: [PATCH 2/7] 2nd pass on extracting args --- src/Analyzer/Dto/Yanked.php | 2 +- src/Analyzer/Extractor/Imports.php | 62 +++++++++ src/Analyzer/Extractor/MethodArgs.php | 176 ++++++++++++++++++++++++++ src/Analyzer/Extractor/Nemaspace.php | 37 ++++++ src/Analyzer/TokenEmitter.php | 123 ++++++++++++++++++ src/Analyzer/TokenFilter.php | 31 +++++ src/Analyzer/Yanker.php | 140 ++------------------ tests/Analyzer/AnalyzerTestCase.php | 6 + tests/Analyzer/YankerTest.php | 138 +++++++++++++++++++- 9 files changed, 581 insertions(+), 134 deletions(-) create mode 100644 src/Analyzer/Extractor/Imports.php create mode 100644 src/Analyzer/Extractor/MethodArgs.php create mode 100644 src/Analyzer/Extractor/Nemaspace.php create mode 100644 src/Analyzer/TokenEmitter.php create mode 100644 src/Analyzer/TokenFilter.php diff --git a/src/Analyzer/Dto/Yanked.php b/src/Analyzer/Dto/Yanked.php index 1a47383..5514c0b 100644 --- a/src/Analyzer/Dto/Yanked.php +++ b/src/Analyzer/Dto/Yanked.php @@ -9,7 +9,7 @@ public function __construct( public ?array $namespace, public array $uses, - public array $arg, + public ?array $args, ) { } } diff --git a/src/Analyzer/Extractor/Imports.php b/src/Analyzer/Extractor/Imports.php new file mode 100644 index 0000000..cc800d7 --- /dev/null +++ b/src/Analyzer/Extractor/Imports.php @@ -0,0 +1,62 @@ +on( + TokenFilter::eq('{'), + function () use (&$blockLevel) { + $this->blockLevel++; + } + ); + + $tokenEmitter->on( + TokenFilter::eq('}'), + function () use (&$blockLevel) { + $this->blockLevel--; + } + ); + + $tokenEmitter->on( + TokenFilter::ofType(T_USE), + $this->handleUse(...), + ); + } + + private function handleUse(array $token) + { + if ($this->blockLevel > 0) { + return; + } + + $statement = [$token]; + + $capture = $this->tokenEmitter->all(function (string|array $token) use (&$statement) { + $statement[] = $token; + }); + + $end = $this->tokenEmitter->on( + TokenFilter::eq(';'), + function () use (&$end, $capture, &$statement) { + /** @var int $end */ + + $this->imports[] = $statement; + + $this->tokenEmitter->remove($end); + $this->tokenEmitter->remove($capture); + } + ); + } +} diff --git a/src/Analyzer/Extractor/MethodArgs.php b/src/Analyzer/Extractor/MethodArgs.php new file mode 100644 index 0000000..bd17d17 --- /dev/null +++ b/src/Analyzer/Extractor/MethodArgs.php @@ -0,0 +1,176 @@ +toCapture[0][0])) { + $this->targetAnonymousClasses(); + } else { + $this->targetRealClasses(); + } + } + + private function targetRealClasses(): void + { + $this->tokenEmitter->on( + TokenFilter::ofType(T_CLASS), + fn () => $this->tokenEmitter->once( + TokenFilter::ofType(T_STRING), + $this->handleClass(...) + ), + ); + } + + private function targetAnonymousClasses(): void + { + $this->tokenEmitter->onLineChange(function () { + $this->anonymousClassIndex = 0; + }); + + $this->tokenEmitter->on( + TokenFilter::ofType(T_NEW), + fn () => $this->tokenEmitter->once( + fn (string|array $token) => is_string($token) || $token[0] !== T_WHITESPACE, + $this->handlePotentialAnonymousClass(...), + ) + ); + } + + /** + * @param 'class'|'method'|'arg' $type + * @param string $value + * @return bool + */ + private function shouldCapture(string $type, string $value): bool + { + $position = [ + 'class' => 0, + 'method' => 1, + 'arg' => 2, + ][$type]; + + if ($this->toCapture[$position] === $value) { + return true; + } + + return false; + } + + private function handleClass(array $token) + { + if (!$this->shouldCapture('class', $token[1])) { + return; + } + + $blockLevel = 0; + + $this->tokenEmitter->counter(TokenFilter::eq('{'), TokenFilter::eq('}'), $blockLevel); + + $capture = $this->tokenEmitter->on( + TokenFilter::ofType(T_FUNCTION), + fn () => $this->tokenEmitter->once( + TokenFilter::ofType(T_STRING), + $this->handleMethod(...), + ) + ); + + $this->tokenEmitter->once(function (string|array $token) use (&$blockLevel) { + return $blockLevel === 0 && $token === '}'; + }, function () use ($capture) { + $this->tokenEmitter->remove($capture); + }); + } + + public function handlePotentialAnonymousClass(string|array $token) + { + if (!is_array($token) || $token[0] !== T_CLASS) { + return; + } + + $fakeToken = [ + T_STRING, + self::anonymousClassInternalName($token[2], $this->anonymousClassIndex++), + $token[2], + ]; + + $this->handleClass($fakeToken); + } + + public function handleMethod(array $token) + { + if (!$this->shouldCapture('method', $token[1])) { + return; + } + + $capture = $this->tokenEmitter->on(TokenFilter::ofType(T_VARIABLE), $this->handleArg(...)); + + $this->tokenEmitter->once( + TokenFilter::any( + TokenFilter::eq('{'), + TokenFilter::eq(';'), + ), + fn () => $this->tokenEmitter->remove($capture), + ); + } + + public function handleArg(array $token) + { + if (!$this->shouldCapture('arg', $token[1])) { + return; + } + + $arg = []; + $parenthesisLevel = 0; + + $toCancel = []; + + $toCancel[] = $this->tokenEmitter->on(TokenFilter::eq('('), function () use (&$parenthesisLevel) { + $parenthesisLevel++; + }); + + $toCancel[] = $this->tokenEmitter->on(TokenFilter::eq(')'), function () use (&$parenthesisLevel) { + $parenthesisLevel--; + }); + + $toCancel[] = $this->tokenEmitter->all(function (array|string $token) use (&$arg) { + $arg[] = $token; + }); + + $this->tokenEmitter->once(function (string|array $token) use (&$parenthesisLevel) { + $nextArgStarting = $parenthesisLevel === 0 && $token === ','; + $methodEnding = $parenthesisLevel === -1 && $token === ')'; + + return $nextArgStarting || $methodEnding; + }, function () use (&$arg, $toCancel) { + $this->tokenEmitter->remove(...$toCancel); + + $this->captured = array_slice($arg, 3, -1); + }); + } +} diff --git a/src/Analyzer/Extractor/Nemaspace.php b/src/Analyzer/Extractor/Nemaspace.php new file mode 100644 index 0000000..b6c6494 --- /dev/null +++ b/src/Analyzer/Extractor/Nemaspace.php @@ -0,0 +1,37 @@ +tokenEmitter->on(TokenFilter::ofType(T_NAMESPACE), function (array $token) { + $this->namespace = [$token]; + + $capture = $this->tokenEmitter->all(function (string|array $token) { + $this->namespace[] = $token; + }); + + $end = $this->tokenEmitter->on( + TokenFilter::eq(';'), + function () use (&$end, $capture) { + /** @var int $end */ + + $this->tokenEmitter->remove($end); + $this->tokenEmitter->remove($capture); + } + ); + }); + } +} diff --git a/src/Analyzer/TokenEmitter.php b/src/Analyzer/TokenEmitter.php new file mode 100644 index 0000000..6b8b084 --- /dev/null +++ b/src/Analyzer/TokenEmitter.php @@ -0,0 +1,123 @@ + + */ + private array $handlers = []; + + public function on(Closure $match, Closure $handler): int + { + $id = $this->getId(); + $this->handlers[$id] = ['match' => $match, 'handler' => $handler]; + + return $id; + } + + public function onLineChange(Closure $handler): int + { + $line = 0; + + return $this->all(function (string|array $token) use (&$line, $handler) { + if (is_string($token)) { + return; + } + + if ($token[2] > $line) { + $line = $token[2]; + $handler(); + } + }); + } + + public function all(Closure $handler): int + { + $id = $this->getId(); + $this->handlers[$id] = ['handler' => $handler]; + + return $id; + } + + public function once(Closure $match, Closure $handler): void + { + $id = $this->getId(); + $this->handlers[$id] = ['match' => $match, 'handler' => $handler, 'once' => true]; + } + + public function counter(Closure $up, Closure $down, int &$value): array + { + return [ + $this->on($up, function () use (&$value) { + $value++; + }), + $this->on($down, function () use (&$value) { + $value--; + }) + ]; + } + + public function remove(int ...$ids): void + { + foreach ($ids as $id) { + unset($this->handlers[$id]); + } + } + + private function getId(): int + { + do { + $id = rand(100_000, 999_999); + } while (isset($this->handlers[$id])); + + return $id; + } + + public function emit(array $tokens): void + { + $wasWhitespace = false; + foreach ($tokens as $token) { + if (static::isType($token, T_COMMENT)) { + continue; + } + + $isWhitespace = static::isType($token, T_WHITESPACE); + if ($isWhitespace) { + if ($wasWhitespace) { + continue; + } + + $wasWhitespace = true; + } else { + $wasWhitespace = false; + } + + $this->emitSingularToken($token); + } + } + + private function emitSingularToken(array|string $token): void + { + foreach ($this->handlers as $i => $handler) { + if (!isset($handler['match']) || $handler['match']($token)) { + $handler['handler']($token); + + if ($handler['once'] ?? false) { + unset($this->handlers[$i]); + } + } + } + + } + + private static function isType(string|array $token, int $type): bool + { + return is_array($token) && $token[0] === $type; + } +} diff --git a/src/Analyzer/TokenFilter.php b/src/Analyzer/TokenFilter.php new file mode 100644 index 0000000..632a88f --- /dev/null +++ b/src/Analyzer/TokenFilter.php @@ -0,0 +1,31 @@ + $token === $value; + } + + public static function ofType(int $type): Closure + { + return fn (array|string $token) => is_array($token) && $token[0] === $type; + } + + public static function any(Closure ...$closures): Closure + { + return function (array|string $token) use ($closures) { + foreach ($closures as $closure) { + if ($closure($token)) { + return true; + } + } + + return false; + }; + } +} diff --git a/src/Analyzer/Yanker.php b/src/Analyzer/Yanker.php index 6720593..47a913f 100644 --- a/src/Analyzer/Yanker.php +++ b/src/Analyzer/Yanker.php @@ -4,146 +4,30 @@ namespace Exan\Moock\Analyzer; -use Closure; use Exan\Moock\Analyzer\Dto\Yanked; +use Exan\Moock\Analyzer\Extractor\Imports; +use Exan\Moock\Analyzer\Extractor\MethodArgs; +use Exan\Moock\Analyzer\Extractor\Nemaspace; class Yanker { public static function fetch( string $contents, - array $methods = [], + array $method, ): Yanked { + $tokenEmitter = new TokenEmitter(); $tokens = token_get_all($contents, TOKEN_PARSE); - $methods = array_unique($methods); - - $uses = []; + $imports = []; $namespace = null; - $args = []; - - $blockLevel = 0; - - /** - * @var array{ - * 0: Closure(string|array): bool, - * 1: Closure(array): void, - * 2: array - * } - */ - $captures = []; - - /** - * @var callable(Closure(string|array $token): bool, Closure(array $tokens): void): void - */ - $captureUntil = function (Closure $end, Closure $then) use (&$captures): void { - $captures[] = [$end, $then, []]; - }; - - $handleCaptures = function (string|array $token) use (&$captures) { - foreach ($captures as $i => &$capture) { - $capture[2][] = $token; - - if ($capture[0]($token)) { - $capture[1]($capture[2]); - unset($captures[$i]); - } - } - }; - - $currentClass = []; - $currentMethodArgs = null; - $wasWhitespace = false; - foreach ($tokens as $token) { - if (static::isType($token, T_COMMENT)) { - continue; - } - - $isWhitespace = static::isType($token, T_WHITESPACE); - if ($isWhitespace) { - if ($wasWhitespace) { - continue; - } - - $wasWhitespace = true; - } else { - $wasWhitespace = false; - } - - if ($token === '{') { - $blockLevel++; - } - - if ($token === '}') { - $blockLevel--; - - $lastClass = static::lastItemInArray($currentClass) ?? -1; + $methodArgs = null; - if ($blockLevel < $lastClass) { - array_pop($currentClass); - } - } - - if ($blockLevel === 0 && static::isType($token, T_USE)) { - $captureUntil(fn (string|array $token) => $token === ';', function (array $tokens) use (&$uses) { - $uses[] = $tokens; - }); - } - - if (static::isType($token, T_NAMESPACE)) { - $captureUntil(fn (string|array $token) => $token === ';', function (array $tokens) use (&$namespace) { - $namespace = $tokens; - }); - } - - if (static::isType($token, T_CLASS)) { - $captureUntil(fn(string|array $token) => static::isType($token, T_STRING), function (array $tokens) use (&$currentClass, $blockLevel) { - $nameToken = array_pop($tokens); - $currentClass[$nameToken[1]] = $blockLevel + 1; - }); - } - - if (static::isType($token, T_FUNCTION) && count($currentClass) > 0) { - $captureUntil(fn (string|array $token) => static::isType($token, T_STRING), - function (array $fnTokens) use ($captureUntil, $currentClass, &$currentMethodArgs, &$args, $methods) { - $nameToken = array_pop($fnTokens); - $currentMethodArgs = array_key_last($currentClass) . '.' . $nameToken[1]; - - $captureUntil( - fn (string|array $token) => $token === '{' || $token === ';', - function (array $tokens) use (&$currentMethodArgs, &$args, $fnTokens, $methods) { - $tokens = [ - ...$fnTokens, - ...$tokens, - ]; - - if (in_array($currentMethodArgs, $methods)) { - $args[$currentMethodArgs] = $tokens; - } - - $currentMethodArgs = null; - } - ); - } - ); - } - - $handleCaptures($token); - } - - return new Yanked($namespace, $uses, $args); - } - - private static function isType(string|array $token, int $type): bool - { - return is_array($token) && $token[0] === $type; - } + new Imports($tokenEmitter, $imports); + new Nemaspace($tokenEmitter, $namespace); + new MethodArgs($tokenEmitter, $method, $methodArgs); - private static function lastItemInArray(array $items): mixed - { - if ($items === []) { - return null; - } + $tokenEmitter->emit($tokens); - return end($items); + return new Yanked($namespace, $imports, $methodArgs); } } diff --git a/tests/Analyzer/AnalyzerTestCase.php b/tests/Analyzer/AnalyzerTestCase.php index 5ae03c0..63ed8a0 100644 --- a/tests/Analyzer/AnalyzerTestCase.php +++ b/tests/Analyzer/AnalyzerTestCase.php @@ -45,4 +45,10 @@ private function containsStringRecursive(string $needle, mixed $value): bool return false; } + + protected function implode(array $tokens) + { + $tokens = array_map(fn (string|array $token) => is_array($token) ? $token[1] : $token, $tokens); + return implode('', $tokens); + } } diff --git a/tests/Analyzer/YankerTest.php b/tests/Analyzer/YankerTest.php index 4d1b625..6b5f156 100644 --- a/tests/Analyzer/YankerTest.php +++ b/tests/Analyzer/YankerTest.php @@ -6,7 +6,6 @@ use Exan\Moock\Analyzer\Yanker; use PHPUnit\Framework\Attributes\Test; -use PHPUnit\Framework\TestCase; class YankerTest extends AnalyzerTestCase { @@ -33,7 +32,7 @@ class SomeClass { } use Some\Import\In\Wrong\But\Valid\Place; - PHP); + PHP, ['SomeClass', 'someMethod', '$arg']); $this->assertContainsTokenAnywhere('Some\Cool\Namespace', $yanked->namespace); @@ -68,15 +67,144 @@ public function it_extracts_method_args_from_classes() class MyClass { - public function myMethod(string \$arg = 'test'): void + public function myMethod(string \$arg /* Some comment */ = /* More comment */ 'test'): void { if (true) { return; } } } - PHP, ['MyClass.myMethod']); - dd($yanked); + class OtherClass + { + public function myMethod(string \$arg = 'not-test'): void + { + if (true) { + return; + } + } + } + PHP, ['MyClass', 'myMethod', '$arg']); + + $this->assertEquals([[ + T_CONSTANT_ENCAPSED_STRING, + "'test'", + 5 + ]], $yanked->args); + } + + #[Test] + public function it_extracts_args_not_at_the_start_of_method() + { + $yanked = Yanker::fetch(<<assertEquals('new Something(new OtherThing())', $this->implode($yanked->args)); + } + + #[Test] + public function it_extracts_args_not_at_the_start_or_end() + { + $yanked = Yanker::fetch(<<assertEquals('new Something(\'class MyClass\')', $this->implode($yanked->args)); + } + + #[Test] + public function it_extracts_args_from_anonymous_classes() + { + $yanked = Yanker::fetch(<<assertEquals('new Something(\'class MyClass\')', $this->implode($yanked->args)); + } + + #[Test] + public function it_extracts_args_from_the_correct_anonymous_class_on_single_line() + { + $code = 'assertEquals([[ + T_CONSTANT_ENCAPSED_STRING, + '"expected"', + 1 + ]], $yanked->args); + } + + #[Test] + public function it_extracts_args_from_the_correct_anonymous_class_on_separated_lines() + { + $yanked = Yanker::fetch(<<assertEquals([[ + T_CONSTANT_ENCAPSED_STRING, + "'expected'", + 13 + ]], $yanked->args); } } From 849c34bbe685aaa641f71b864294ac6c33e3a585 Mon Sep 17 00:00:00 2001 From: Exanlv <51094537+Exanlv@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:05:20 +0200 Subject: [PATCH 3/7] Use new parser --- src/Analyzer/Extractor.php | 190 ------------------ src/Analyzer/Extractor/Imports.php | 1 + src/Analyzer/Extractor/MethodArgs.php | 6 +- src/Analyzer/Extractor/Nemaspace.php | 2 + src/Analyzer/TokenEmitter.php | 2 +- src/Analyzer/TokenFilter.php | 1 + src/Analyzer/Utilize.php | 1 + src/Analyzer/Yanker.php | 7 + src/Methods/Mocker.php | 56 +++--- .../InstantiatedDefaultArgsAbstractClass.php | 14 ++ tests/Methods/MockerTest.php | 12 ++ 11 files changed, 71 insertions(+), 221 deletions(-) delete mode 100644 src/Analyzer/Extractor.php create mode 100644 tests/Components/InstantiatedDefaultArgsAbstractClass.php diff --git a/src/Analyzer/Extractor.php b/src/Analyzer/Extractor.php deleted file mode 100644 index fb9d1a8..0000000 --- a/src/Analyzer/Extractor.php +++ /dev/null @@ -1,190 +0,0 @@ - $tokens - * @return array - */ - public static function lines(array $tokens, int $start, int $end): array - { - $capture = false; - $captured = []; - - foreach ($tokens as $token) { - if (!$capture && is_array($token)) { - $isWithinLines = $token[2] >= $start && $token[2] <= $end; - - if (!$capture && $isWithinLines) { - $capture = true; - } elseif ($capture && !$isWithinLines) { - break; - } - } - - if ($capture) { - $captured[] = $token; - } - } - - return $captured; - } - - /** - * @param array $tokens - * @return array - */ - public static function function(array $tokens, string $functionName): array - { - $capture = false; - $captured = []; - - foreach ($tokens as $i => $token) { - if ( - is_array($token) - && $token[0] === T_FUNCTION - && is_array($tokens[$i + 2]) - && $tokens[$i + 2][0] === T_STRING - ) { - $isRequestedFunction = $tokens[$i + 2][1] === $functionName; - - if (!$capture && $isRequestedFunction) { - $capture = true; - } elseif ($capture && !$isRequestedFunction) { - break; - } - } - - if ($capture) { - $captured[] = $token; - } - } - - while ( - count($captured) - && is_array($captured[count($captured) - 1]) - && in_array( - $captured[count($captured) - 1][0], - [T_WHITESPACE, T_PUBLIC, T_PROTECTED, T_PRIVATE] - ) - ) { - array_pop($captured); - } - - return $captured; - } - - /** - * @param array $tokens - * @return array - */ - public static function arg(array $tokens, string $argName): array - { - $argName = '$' . trim($argName, '$'); - $capture = false; - $captured = []; - - $trimToLast = ')'; - - foreach ($tokens as $token) { - if (is_array($token) - && $token[0] === T_VARIABLE - ) { - $isRequestedArg = $token[1] === $argName; - - if (!$capture && $isRequestedArg) { - $capture = true; - } elseif ($capture && !$isRequestedArg) { - $trimToLast = ','; - break; - } - } - - if ($token === '{') { - break; - } - - if ($capture) { - $captured[] = $token; - } - } - - while ( - count($captured) - && array_pop($captured) !== $trimToLast - ) { - } - - return $captured; - } - - /** - * @param array $tokens - * @return array - */ - public static function uses(array $tokens): array - { - $uses = []; - $use = null; - - $blockScope = 0; - - foreach ($tokens as $token) { - if ($token === '{') { - $blockScope++; - } - - if ($token === '}') { - $blockScope--; - } - - if ($token === ';') { - $use = null; - } - - if ($blockScope === 0 && is_array($token) && $token[0] === T_USE) { - $use = count($uses); - $uses[] = []; - } - - if ($use !== null) { - $uses[$use][] = $token; - } - } - - return $uses; - } - - public static function namespace(array $tokens): array - { - $namespace = []; - $capturing = false; - - foreach ($tokens as $token) { - if (is_array($token) && $token[0] === T_NAMESPACE) { - $capturing = true; - } - - if ($capturing && $token === ';') { - break; - } - - if ($capturing) { - $namespace[] = $token; - } - } - - return $namespace; - } -} diff --git a/src/Analyzer/Extractor/Imports.php b/src/Analyzer/Extractor/Imports.php index cc800d7..2c427af 100644 --- a/src/Analyzer/Extractor/Imports.php +++ b/src/Analyzer/Extractor/Imports.php @@ -7,6 +7,7 @@ use Exan\Moock\Analyzer\TokenEmitter; use Exan\Moock\Analyzer\TokenFilter; +/** @internal */ class Imports { private int $blockLevel = 0; diff --git a/src/Analyzer/Extractor/MethodArgs.php b/src/Analyzer/Extractor/MethodArgs.php index bd17d17..24858f2 100644 --- a/src/Analyzer/Extractor/MethodArgs.php +++ b/src/Analyzer/Extractor/MethodArgs.php @@ -7,6 +7,7 @@ use Exan\Moock\Analyzer\TokenEmitter; use Exan\Moock\Analyzer\TokenFilter; +/** @internal */ class MethodArgs { private static function anonymousClassInternalName(int $line, int $i): string @@ -39,7 +40,10 @@ public function __construct( private function targetRealClasses(): void { $this->tokenEmitter->on( - TokenFilter::ofType(T_CLASS), + TokenFilter::any( + TokenFilter::ofType(T_CLASS), + TokenFilter::ofType(T_INTERFACE), + ), fn () => $this->tokenEmitter->once( TokenFilter::ofType(T_STRING), $this->handleClass(...) diff --git a/src/Analyzer/Extractor/Nemaspace.php b/src/Analyzer/Extractor/Nemaspace.php index b6c6494..ffc9dc6 100644 --- a/src/Analyzer/Extractor/Nemaspace.php +++ b/src/Analyzer/Extractor/Nemaspace.php @@ -9,6 +9,8 @@ /** * Deliberate typo. Namespace is reserved + * + * @internal */ class Nemaspace { diff --git a/src/Analyzer/TokenEmitter.php b/src/Analyzer/TokenEmitter.php index 6b8b084..626af52 100644 --- a/src/Analyzer/TokenEmitter.php +++ b/src/Analyzer/TokenEmitter.php @@ -6,6 +6,7 @@ use Closure; +/** @internal */ class TokenEmitter { /** @@ -113,7 +114,6 @@ private function emitSingularToken(array|string $token): void } } } - } private static function isType(string|array $token, int $type): bool diff --git a/src/Analyzer/TokenFilter.php b/src/Analyzer/TokenFilter.php index 632a88f..865af11 100644 --- a/src/Analyzer/TokenFilter.php +++ b/src/Analyzer/TokenFilter.php @@ -4,6 +4,7 @@ use Closure; +/** @internal */ class TokenFilter { public static function eq(mixed $value): Closure diff --git a/src/Analyzer/Utilize.php b/src/Analyzer/Utilize.php index f4746b7..9191215 100644 --- a/src/Analyzer/Utilize.php +++ b/src/Analyzer/Utilize.php @@ -54,6 +54,7 @@ private static function parseLine($line): array { array_shift($line); // use array_shift($line); // (whitespace) + array_pop($line); // ; return array_map(static::parseIndividualUse(...), static::individualUses($line)); } diff --git a/src/Analyzer/Yanker.php b/src/Analyzer/Yanker.php index 47a913f..bc434d0 100644 --- a/src/Analyzer/Yanker.php +++ b/src/Analyzer/Yanker.php @@ -9,6 +9,13 @@ use Exan\Moock\Analyzer\Extractor\MethodArgs; use Exan\Moock\Analyzer\Extractor\Nemaspace; +/** + * @internal + * + * I am not a language dev, please don't judge me too harshly for this poor excuse of a parser :) + * + * Valid PHP can be assumed, as the files have gone through reflection already prior to reaching this stage. + */ class Yanker { public static function fetch( diff --git a/src/Methods/Mocker.php b/src/Methods/Mocker.php index b577172..6a95c95 100644 --- a/src/Methods/Mocker.php +++ b/src/Methods/Mocker.php @@ -4,13 +4,14 @@ namespace Exan\Moock\Methods; -use Exan\Moock\Analyzer\Extractor; use Exan\Moock\Analyzer\Utilize; +use Exan\Moock\Analyzer\Yanker; use Exan\Moock\Formatting\Variables as FormatsVariables; use ReflectionClass; use ReflectionMethod; use ReflectionNamedType; use ReflectionParameter; +use RuntimeException; /** * @internal @@ -21,7 +22,7 @@ class Mocker public readonly array $methods; - /** @param string[] */ + /** @param string[] $interfaces */ public function __construct( public readonly array $interfaces, ) { @@ -118,51 +119,48 @@ private function getParameterSignature(ReflectionParameter $parameter, Reflectio $defaultValue = $parameter->getDefaultValue(); $signature .= ' = ' . (is_object($defaultValue) - ? $this->extractDefaultParamSignature($parameter, $declaringMethod, $declaringClass) + ? $this->extractDefaultParamSignature($parameter, $declaringMethod) : $this->formatValue($defaultValue)); } return $signature; } - private function extractDefaultParamSignature(ReflectionParameter $parameter, ReflectionMethod $method, ReflectionClass $class): string + private function extractDefaultParamSignature(ReflectionParameter $parameter, ReflectionMethod $method): string { // public function myMethod(MyClass $myArg = new MyClass()) // Unfortunately, reflection only gives the exact instance of the default value. It is therefore impossible to recreate the // code used to instantiate an object based on reflection. It needs to be extracted from the original file instead. - $fileContents = file_get_contents($class->getFileName()); - - $tokens = array_filter( - token_get_all($fileContents), - fn (string|array $token) => !is_array($token) || !in_array($token[0], [T_COMMENT, T_DOC_COMMENT]) - ); + $class = $method->getDeclaringClass(); - $tokens = array_values($tokens); + $fileName = $class->getFileName(); - $isWhitespace = fn (string|array $token) => is_array($token) && $token[0] === T_WHITESPACE; - foreach ($tokens as $i => $token) { - if ($isWhitespace($token) && isset($tokens[$i + 1]) && $isWhitespace($tokens[$i + 1])) { - unset($tokens[$i]); - } + if (str_contains($fileName, 'eval()\'d code')) { + throw new RuntimeException(sprintf( + 'Unable to retrieve class construction in default parameters from eval-based class `%s`', + $fileName) + ); } - $tokens = array_values($tokens); + $fileContents = file_get_contents($class->getFileName()); - $uses = Extractor::uses($tokens); - $namespace = Extractor::namespace($tokens); - $utilize = Utilize::fromTokens(count($namespace) > 0 ? $namespace[2][1] : null, $uses); + if ($class->isAnonymous()) { + $fullName = explode(':', $class->getName()); + $className = array_pop($fullName); + } else { + $fullName = explode('\\', $class->getName()); + $className = array_pop($fullName); + } - $class = Extractor::lines($tokens, $method->getStartLine(), $method->getEndLine()); - $method = Extractor::function($class, $method->getName()); - $arg = Extractor::arg($method, $parameter->getName()); + $yanked = Yanker::fetch($fileContents, [$className, $method->getName(), '$' . $parameter->getName()]); - while ( - count($arg) - && (!is_array($arg) || $arg[0][0] !== T_NEW) - ) { - array_shift($arg); - } + $utilize = Utilize::fromTokens( + $yanked->namespace === null ? null : $yanked->namespace[2][1], + $yanked->uses + ); + + $arg = $yanked->args; array_shift($arg); // new array_shift($arg); // (whitespace) diff --git a/tests/Components/InstantiatedDefaultArgsAbstractClass.php b/tests/Components/InstantiatedDefaultArgsAbstractClass.php new file mode 100644 index 0000000..6b1fb75 --- /dev/null +++ b/tests/Components/InstantiatedDefaultArgsAbstractClass.php @@ -0,0 +1,14 @@ +assertTrue($mock->{$method}()); } + #[Test] + #[DataProvider('interfaceObjectInstantiationDataProvider')] + public function it_instantiates_objects_when_declared_on_abstract_class(string $method, Closure $validator): void + { + $mock = Mock::interface(InstantiatedDefaultArgsAbstractClass::class); + + Mock::method($mock->{$method}(...))->replace($validator); + + $this->assertTrue($mock->{$method}()); + } + public static function interfaceObjectInstantiationDataProvider(): array { return [ From 0933dcfaa908b7c67c662852fe3859f66e29f2db Mon Sep 17 00:00:00 2001 From: Exanlv <51094537+Exanlv@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:06:34 +0200 Subject: [PATCH 4/7] cs --- src/Analyzer/Dto/Yanked.php | 3 +- src/Analyzer/Exception/EvaldCodeException.php | 4 +- src/Analyzer/Extractor/Imports.php | 10 +- src/Analyzer/Extractor/MethodArgs.php | 20 +-- src/Analyzer/Extractor/Nemaspace.php | 6 +- src/Analyzer/TokenEmitter.php | 10 +- src/Analyzer/TokenFilter.php | 2 + src/Methods/Mocker.php | 8 +- tests/Analyzer/YankerTest.php | 166 +++++++++--------- 9 files changed, 115 insertions(+), 114 deletions(-) diff --git a/src/Analyzer/Dto/Yanked.php b/src/Analyzer/Dto/Yanked.php index 5514c0b..0e43a77 100644 --- a/src/Analyzer/Dto/Yanked.php +++ b/src/Analyzer/Dto/Yanked.php @@ -10,6 +10,5 @@ public function __construct( public ?array $namespace, public array $uses, public ?array $args, - ) { - } + ) {} } diff --git a/src/Analyzer/Exception/EvaldCodeException.php b/src/Analyzer/Exception/EvaldCodeException.php index 8963759..fd96b33 100644 --- a/src/Analyzer/Exception/EvaldCodeException.php +++ b/src/Analyzer/Exception/EvaldCodeException.php @@ -2,6 +2,4 @@ declare(strict_types=1); -class EvaldCodeException extends RuntimeException -{ -} +class EvaldCodeException extends RuntimeException {} diff --git a/src/Analyzer/Extractor/Imports.php b/src/Analyzer/Extractor/Imports.php index 2c427af..4e9a51a 100644 --- a/src/Analyzer/Extractor/Imports.php +++ b/src/Analyzer/Extractor/Imports.php @@ -18,14 +18,14 @@ public function __construct( ) { $tokenEmitter->on( TokenFilter::eq('{'), - function () use (&$blockLevel) { + function () use (&$blockLevel): void { $this->blockLevel++; } ); $tokenEmitter->on( TokenFilter::eq('}'), - function () use (&$blockLevel) { + function () use (&$blockLevel): void { $this->blockLevel--; } ); @@ -36,7 +36,7 @@ function () use (&$blockLevel) { ); } - private function handleUse(array $token) + private function handleUse(array $token): void { if ($this->blockLevel > 0) { return; @@ -44,13 +44,13 @@ private function handleUse(array $token) $statement = [$token]; - $capture = $this->tokenEmitter->all(function (string|array $token) use (&$statement) { + $capture = $this->tokenEmitter->all(function (string|array $token) use (&$statement): void { $statement[] = $token; }); $end = $this->tokenEmitter->on( TokenFilter::eq(';'), - function () use (&$end, $capture, &$statement) { + function () use (&$end, $capture, &$statement): void { /** @var int $end */ $this->imports[] = $statement; diff --git a/src/Analyzer/Extractor/MethodArgs.php b/src/Analyzer/Extractor/MethodArgs.php index 24858f2..de9ca21 100644 --- a/src/Analyzer/Extractor/MethodArgs.php +++ b/src/Analyzer/Extractor/MethodArgs.php @@ -53,7 +53,7 @@ private function targetRealClasses(): void private function targetAnonymousClasses(): void { - $this->tokenEmitter->onLineChange(function () { + $this->tokenEmitter->onLineChange(function (): void { $this->anonymousClassIndex = 0; }); @@ -86,7 +86,7 @@ private function shouldCapture(string $type, string $value): bool return false; } - private function handleClass(array $token) + private function handleClass(array $token): void { if (!$this->shouldCapture('class', $token[1])) { return; @@ -106,12 +106,12 @@ private function handleClass(array $token) $this->tokenEmitter->once(function (string|array $token) use (&$blockLevel) { return $blockLevel === 0 && $token === '}'; - }, function () use ($capture) { + }, function () use ($capture): void { $this->tokenEmitter->remove($capture); }); } - public function handlePotentialAnonymousClass(string|array $token) + public function handlePotentialAnonymousClass(string|array $token): void { if (!is_array($token) || $token[0] !== T_CLASS) { return; @@ -126,7 +126,7 @@ public function handlePotentialAnonymousClass(string|array $token) $this->handleClass($fakeToken); } - public function handleMethod(array $token) + public function handleMethod(array $token): void { if (!$this->shouldCapture('method', $token[1])) { return; @@ -143,7 +143,7 @@ public function handleMethod(array $token) ); } - public function handleArg(array $token) + public function handleArg(array $token): void { if (!$this->shouldCapture('arg', $token[1])) { return; @@ -154,15 +154,15 @@ public function handleArg(array $token) $toCancel = []; - $toCancel[] = $this->tokenEmitter->on(TokenFilter::eq('('), function () use (&$parenthesisLevel) { + $toCancel[] = $this->tokenEmitter->on(TokenFilter::eq('('), function () use (&$parenthesisLevel): void { $parenthesisLevel++; }); - $toCancel[] = $this->tokenEmitter->on(TokenFilter::eq(')'), function () use (&$parenthesisLevel) { + $toCancel[] = $this->tokenEmitter->on(TokenFilter::eq(')'), function () use (&$parenthesisLevel): void { $parenthesisLevel--; }); - $toCancel[] = $this->tokenEmitter->all(function (array|string $token) use (&$arg) { + $toCancel[] = $this->tokenEmitter->all(function (array|string $token) use (&$arg): void { $arg[] = $token; }); @@ -171,7 +171,7 @@ public function handleArg(array $token) $methodEnding = $parenthesisLevel === -1 && $token === ')'; return $nextArgStarting || $methodEnding; - }, function () use (&$arg, $toCancel) { + }, function () use (&$arg, $toCancel): void { $this->tokenEmitter->remove(...$toCancel); $this->captured = array_slice($arg, 3, -1); diff --git a/src/Analyzer/Extractor/Nemaspace.php b/src/Analyzer/Extractor/Nemaspace.php index ffc9dc6..a55be05 100644 --- a/src/Analyzer/Extractor/Nemaspace.php +++ b/src/Analyzer/Extractor/Nemaspace.php @@ -18,16 +18,16 @@ public function __construct( private readonly TokenEmitter $tokenEmitter, private ?array &$namespace, ) { - $this->tokenEmitter->on(TokenFilter::ofType(T_NAMESPACE), function (array $token) { + $this->tokenEmitter->on(TokenFilter::ofType(T_NAMESPACE), function (array $token): void { $this->namespace = [$token]; - $capture = $this->tokenEmitter->all(function (string|array $token) { + $capture = $this->tokenEmitter->all(function (string|array $token): void { $this->namespace[] = $token; }); $end = $this->tokenEmitter->on( TokenFilter::eq(';'), - function () use (&$end, $capture) { + function () use (&$end, $capture): void { /** @var int $end */ $this->tokenEmitter->remove($end); diff --git a/src/Analyzer/TokenEmitter.php b/src/Analyzer/TokenEmitter.php index 626af52..cc0b5de 100644 --- a/src/Analyzer/TokenEmitter.php +++ b/src/Analyzer/TokenEmitter.php @@ -26,7 +26,7 @@ public function onLineChange(Closure $handler): int { $line = 0; - return $this->all(function (string|array $token) use (&$line, $handler) { + return $this->all(function (string|array $token) use (&$line, $handler): void { if (is_string($token)) { return; } @@ -55,12 +55,12 @@ public function once(Closure $match, Closure $handler): void public function counter(Closure $up, Closure $down, int &$value): array { return [ - $this->on($up, function () use (&$value) { + $this->on($up, function () use (&$value): void { $value++; }), - $this->on($down, function () use (&$value) { + $this->on($down, function () use (&$value): void { $value--; - }) + }), ]; } @@ -74,7 +74,7 @@ public function remove(int ...$ids): void private function getId(): int { do { - $id = rand(100_000, 999_999); + $id = random_int(100_000, 999_999); } while (isset($this->handlers[$id])); return $id; diff --git a/src/Analyzer/TokenFilter.php b/src/Analyzer/TokenFilter.php index 865af11..8f73043 100644 --- a/src/Analyzer/TokenFilter.php +++ b/src/Analyzer/TokenFilter.php @@ -1,5 +1,7 @@ getFileName(); if (str_contains($fileName, 'eval()\'d code')) { - throw new RuntimeException(sprintf( - 'Unable to retrieve class construction in default parameters from eval-based class `%s`', - $fileName) + throw new RuntimeException( + sprintf( + 'Unable to retrieve class construction in default parameters from eval-based class `%s`', + $fileName + ) ); } diff --git a/tests/Analyzer/YankerTest.php b/tests/Analyzer/YankerTest.php index 6b5f156..6478e70 100644 --- a/tests/Analyzer/YankerTest.php +++ b/tests/Analyzer/YankerTest.php @@ -10,29 +10,29 @@ class YankerTest extends AnalyzerTestCase { #[Test] - public function it_captures_namespace_and_imports() + public function it_captures_namespace_and_imports(): void { $yanked = Yanker::fetch(<<assertContainsTokenAnywhere('Some\Cool\Namespace', $yanked->namespace); @@ -60,100 +60,100 @@ class SomeClass { } #[Test] - public function it_extracts_method_args_from_classes() + public function it_extracts_method_args_from_classes(): void { $yanked = Yanker::fetch(<<assertEquals([[ T_CONSTANT_ENCAPSED_STRING, "'test'", - 5 + 5, ]], $yanked->args); } #[Test] - public function it_extracts_args_not_at_the_start_of_method() + public function it_extracts_args_not_at_the_start_of_method(): void { $yanked = Yanker::fetch(<<assertEquals('new Something(new OtherThing())', $this->implode($yanked->args)); } #[Test] - public function it_extracts_args_not_at_the_start_or_end() + public function it_extracts_args_not_at_the_start_or_end(): void { $yanked = Yanker::fetch(<<assertEquals('new Something(\'class MyClass\')', $this->implode($yanked->args)); } #[Test] - public function it_extracts_args_from_anonymous_classes() + public function it_extracts_args_from_anonymous_classes(): void { $yanked = Yanker::fetch(<<assertEquals('new Something(\'class MyClass\')', $this->implode($yanked->args)); } #[Test] - public function it_extracts_args_from_the_correct_anonymous_class_on_single_line() + public function it_extracts_args_from_the_correct_anonymous_class_on_single_line(): void { $code = 'assertEquals([[ T_CONSTANT_ENCAPSED_STRING, '"expected"', - 1 + 1, ]], $yanked->args); } #[Test] - public function it_extracts_args_from_the_correct_anonymous_class_on_separated_lines() + public function it_extracts_args_from_the_correct_anonymous_class_on_separated_lines(): void { $yanked = Yanker::fetch(<<assertEquals([[ T_CONSTANT_ENCAPSED_STRING, "'expected'", - 13 + 13, ]], $yanked->args); } } From fdf0725d72b087a0b77a19c55c716def416a9790 Mon Sep 17 00:00:00 2001 From: Exanlv <51094537+Exanlv@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:10:40 +0200 Subject: [PATCH 5/7] Update Imports and Namespace parser to use post-creation helpers --- src/Analyzer/Extractor/Imports.php | 21 +++------------------ src/Analyzer/Extractor/Nemaspace.php | 10 +--------- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/src/Analyzer/Extractor/Imports.php b/src/Analyzer/Extractor/Imports.php index 4e9a51a..3fcd81c 100644 --- a/src/Analyzer/Extractor/Imports.php +++ b/src/Analyzer/Extractor/Imports.php @@ -16,19 +16,7 @@ public function __construct( public readonly TokenEmitter $tokenEmitter, public array &$imports, ) { - $tokenEmitter->on( - TokenFilter::eq('{'), - function () use (&$blockLevel): void { - $this->blockLevel++; - } - ); - - $tokenEmitter->on( - TokenFilter::eq('}'), - function () use (&$blockLevel): void { - $this->blockLevel--; - } - ); + $tokenEmitter->counter(TokenFilter::eq('{'), TokenFilter::eq('}'), $this->blockLevel); $tokenEmitter->on( TokenFilter::ofType(T_USE), @@ -48,14 +36,11 @@ private function handleUse(array $token): void $statement[] = $token; }); - $end = $this->tokenEmitter->on( + $this->tokenEmitter->once( TokenFilter::eq(';'), - function () use (&$end, $capture, &$statement): void { - /** @var int $end */ - + function () use ($capture, &$statement): void { $this->imports[] = $statement; - $this->tokenEmitter->remove($end); $this->tokenEmitter->remove($capture); } ); diff --git a/src/Analyzer/Extractor/Nemaspace.php b/src/Analyzer/Extractor/Nemaspace.php index a55be05..4e34354 100644 --- a/src/Analyzer/Extractor/Nemaspace.php +++ b/src/Analyzer/Extractor/Nemaspace.php @@ -25,15 +25,7 @@ public function __construct( $this->namespace[] = $token; }); - $end = $this->tokenEmitter->on( - TokenFilter::eq(';'), - function () use (&$end, $capture): void { - /** @var int $end */ - - $this->tokenEmitter->remove($end); - $this->tokenEmitter->remove($capture); - } - ); + $this->tokenEmitter->once(TokenFilter::eq(';'), fn () => $this->tokenEmitter->remove($capture)); }); } } From 067101b0b19ac0c728d5ad48b378e46c732c7288 Mon Sep 17 00:00:00 2001 From: Exanlv <51094537+Exanlv@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:16:15 +0200 Subject: [PATCH 6/7] Add test case for class extension mocking --- .../InstantiatedDefaultArgsBaseClass.php | 20 +++++++++++++++++++ .../InstantiatedDefaultArgsExtension.php | 10 ++++++++++ tests/Methods/MockerTest.php | 12 +++++++++++ 3 files changed, 42 insertions(+) create mode 100644 tests/Components/InstantiatedDefaultArgsBaseClass.php create mode 100644 tests/Components/InstantiatedDefaultArgsExtension.php diff --git a/tests/Components/InstantiatedDefaultArgsBaseClass.php b/tests/Components/InstantiatedDefaultArgsBaseClass.php new file mode 100644 index 0000000..766a338 --- /dev/null +++ b/tests/Components/InstantiatedDefaultArgsBaseClass.php @@ -0,0 +1,20 @@ +assertTrue($mock->{$method}()); } + #[Test] + #[DataProvider('interfaceObjectInstantiationDataProvider')] + public function it_instantiates_objects_when_declared_on_parent_class(string $method, Closure $validator): void + { + $mock = Mock::interface(InstantiatedDefaultArgsExtension::class); + + Mock::method($mock->{$method}(...))->replace($validator); + + $this->assertTrue($mock->{$method}()); + } + public static function interfaceObjectInstantiationDataProvider(): array { return [ From daf0db58f006e9399a086060cbc46ccb6021e14a Mon Sep 17 00:00:00 2001 From: Exanlv <51094537+Exanlv@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:16:31 +0200 Subject: [PATCH 7/7] cs --- tests/Components/InstantiatedDefaultArgsExtension.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/Components/InstantiatedDefaultArgsExtension.php b/tests/Components/InstantiatedDefaultArgsExtension.php index 40984d0..e48f16f 100644 --- a/tests/Components/InstantiatedDefaultArgsExtension.php +++ b/tests/Components/InstantiatedDefaultArgsExtension.php @@ -4,7 +4,4 @@ namespace Tests\Components; -class InstantiatedDefaultArgsExtension extends InstantiatedDefaultArgsBaseClass -{ - -} +class InstantiatedDefaultArgsExtension extends InstantiatedDefaultArgsBaseClass {}