diff --git a/.gitattributes b/.gitattributes index e1bccc4b..ed810355 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ .gitattributes export-ignore .github/ export-ignore .gitignore export-ignore +CLAUDE.md export-ignore ncs.* export-ignore phpstan*.neon export-ignore tests/ export-ignore diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..e89b7b1c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,329 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Nette PHP Generator is a library for programmatically generating PHP code - classes, functions, namespaces, and complete PHP files. It supports all modern PHP features including property hooks (PHP 8.4), enums, attributes, asymmetric visibility, and more. + +The library provides both a builder API for constructing PHP structures and extraction capabilities for loading existing PHP code. + +## Essential Commands + +### Testing +```bash +# Run all tests +composer run tester +# OR +vendor/bin/tester tests -s + +# Run specific test file +vendor/bin/tester tests/PhpGenerator/ClassType.phpt -s + +# Run tests in specific directory +vendor/bin/tester tests/PhpGenerator/ -s +``` + +### Static Analysis +```bash +# Run PHPStan analysis +composer run phpstan +# OR +vendor/bin/phpstan analyse +``` + +### Code Style +The project follows Nette Coding Standard with configuration in `ncs.php`. Uses tabs for indentation and braces on next line for functions/methods. + +## Architecture + +### Core Components + +**PhpFile** (`PhpFile.php`) +- Top-level container representing a complete PHP file +- Manages namespaces, strict_types declaration, and file-level comments +- Entry point: `PhpFile::add()` for adding namespaces/classes/functions + +**PhpNamespace** (`PhpNamespace.php`) +- Represents a namespace with use statements and contained classes/functions +- Handles type resolution and name simplification based on use statements +- Methods: `addClass()`, `addInterface()`, `addTrait()`, `addEnum()`, `addFunction()` + +**ClassLike** (`ClassLike.php`) +- Abstract base for class-like structures (classes, interfaces, traits, enums) +- Provides common functionality for attributes and comments +- Extended by ClassType, InterfaceType, TraitType, EnumType + +**ClassType** (`ClassType.php`) +- Represents class definitions with full feature support +- Composes traits for properties, methods, constants, and trait usage +- Supports final, abstract, readonly modifiers, extends, and implements + +**Method** (`Method.php`) & **GlobalFunction** (`GlobalFunction.php`) +- Method represents class/interface/trait methods +- GlobalFunction represents standalone functions +- Both use FunctionLike trait for parameters, return types, and body + +**Property** (`Property.php`) +- Represents class properties with type hints, visibility, hooks +- Supports PHP 8.4 property hooks (get/set) via PropertyHook class +- Supports asymmetric visibility (different get/set visibility) + +**Printer** (`Printer.php`) & **PsrPrinter** (`PsrPrinter.php`) +- Printer: Generates code following Nette Coding Standard (tabs, braces on next line) +- PsrPrinter: Generates PSR-2/PSR-12/PER compliant code +- Configurable via properties: `$wrapLength`, `$indentation`, `$linesBetweenMethods`, etc. +- Handles type resolution when printing within namespace context + +**Factory** (`Factory.php`) & **Extractor** (`Extractor.php`) +- Factory: Creates PhpGenerator objects from existing classes/functions +- Extractor: Low-level parser integration (requires nikic/php-parser) +- Enable loading existing code: `ClassType::from()`, `PhpFile::fromCode()` + +**Dumper** (`Dumper.php`) +- Converts PHP values to code representation +- Used internally for default values, but also available for standalone use +- Better output than `var_export()` + +### Trait Organization (`Traits/`) + +Shared functionality is implemented via traits for composition: + +- **FunctionLike**: Parameters, return types, body management for functions/methods +- **PropertyLike**: Core property functionality (type, value, visibility) +- **AttributeAware**: Attribute support for all elements +- **CommentAware**: Doc comment management +- **VisibilityAware**: Visibility modifiers (public/protected/private) +- **ConstantsAware**: Class constant management +- **MethodsAware**: Method collection management +- **PropertiesAware**: Property collection management +- **TraitsAware**: Trait usage management + +This trait-based architecture allows ClassType to compose all necessary features while keeping concerns separated. + +### Type System + +**Type** (`Type.php`) +- Constants for native types (String, Int, Array, etc.) +- Helper methods for union/intersection/nullable types +- Used throughout for type hints and return types + +**Literal** (`Literal.php`) +- Represents raw PHP code that should not be escaped +- Used for default values, constants, expressions +- Supports placeholders for value injection (see Placeholders section below) +- `Literal::new()` helper for creating object instantiation literals + +### Test Structure + +Tests use Nette Tester with `.phpt` extension: +- Located in `tests/PhpGenerator/` +- Mirror source file organization +- Use `test()` function for test cases (defined in `bootstrap.php`) +- Use `testException()` for exception testing +- Helper functions: `same()`, `sameFile()` for assertions + +Example test pattern: +```php +test('description of what is being tested', function () { + $class = new ClassType('Demo'); + // ... test code + Assert::same($expected, (string) $class); +}); +``` + +## Important Features + +### Placeholders in Function Bodies + +The library supports special placeholders for inserting values into method/function bodies: + +- **`?`** - Simple placeholder for single values (strings, numbers, arrays) + ```php + $function->addBody('return substr(?, ?);', [$str, $num]); + // Generates: return substr('any string', 3); + ``` + +- **`...?`** - Variadic placeholder (unpacks arrays as separate arguments) + ```php + $function->setBody('myfunc(...?);', [[1, 2, 3]]); + // Generates: myfunc(1, 2, 3); + ``` + +- **`...?:`** - Named parameters placeholder for PHP 8+ + ```php + $function->setBody('myfunc(...?:);', [['foo' => 1, 'bar' => true]]); + // Generates: myfunc(foo: 1, bar: true); + ``` + +- **`\?`** - Escaped placeholder (literal question mark) + ```php + $function->addBody('return $a \? 10 : ?;', [$num]); + // Generates: return $a ? 10 : 3; + ``` + +### Printer Configuration + +Both `Printer` and `PsrPrinter` can be customized by extending and overriding public properties: + +```php +class MyPrinter extends Nette\PhpGenerator\Printer +{ + public int $wrapLength = 120; // line length for wrapping + public string $indentation = "\t"; // indentation character + public int $linesBetweenProperties = 0; // blank lines between properties + public int $linesBetweenMethods = 2; // blank lines between methods + public int $linesBetweenUseTypes = 0; // blank lines between use statement groups + public bool $bracesOnNextLine = true; // opening brace position for functions/methods + public bool $singleParameterOnOneLine = false; // single parameter formatting + public bool $omitEmptyNamespaces = true; // omit empty namespaces + public string $returnTypeColon = ': '; // separator before return type +} +``` + +**Note:** `Printer` uses Nette Coding Standard (tabs, braces on next line), while `PsrPrinter` follows PSR-2/PSR-12/PER (spaces, braces on same line). + +### Property Hooks (PHP 8.4) + +Property hooks allow defining get/set operations directly on properties: + +```php +$prop = $class->addProperty('firstName')->setType('string'); +$prop->addHook('set', 'strtolower($value)') // Arrow function style + ->addParameter('value')->setType('string'); +$prop->addHook('get') // Block style + ->setBody('return ucfirst($this->firstName);'); +``` + +Property hooks can be marked as `abstract` or `final` using `setAbstract()` / `setFinal()`. + +### Asymmetric Visibility (PHP 8.4) + +Properties can have different visibility for reading vs writing: + +```php +// Using setVisibility() with two parameters +$class->addProperty('name') + ->setVisibility('public', 'private'); // public get, private set + +// Using modifier methods with 'get' or 'set' mode +$class->addProperty('id') + ->setProtected('set'); // protected set, public get (default) +``` + +Generates: `public private(set) string $name;` + +### ClassManipulator + +The `ClassManipulator` class provides advanced class manipulation: + +- **`inheritMethod($name)`** - Copies method from parent/interface for overriding +- **`inheritProperty($name)`** - Copies property from parent class +- **`implement($interface)`** - Automatically implements all methods/properties from interface/abstract class + +```php +$manipulator = new Nette\PhpGenerator\ClassManipulator($class); +$manipulator->implement(SomeInterface::class); +// Now $class contains stub implementations of all interface methods +``` + +### Arrow Functions + +While `Closure` represents anonymous functions, you can generate arrow functions using the printer: + +```php +$closure = new Nette\PhpGenerator\Closure; +$closure->setBody('$a + $b'); // Note: arrow function body without 'return' +$closure->addParameter('a'); +$closure->addParameter('b'); + +echo (new Nette\PhpGenerator\Printer)->printArrowFunction($closure); +// Generates: fn($a, $b) => $a + $b +``` + +### Cloning Members + +Methods, properties, and constants can be cloned under a different name: + +```php +$methodCount = $class->getMethod('count'); +$methodRecount = $methodCount->cloneWithName('recount'); +$class->addMember($methodRecount); +``` + +## Key Patterns + +### Builder Pattern +All classes use fluent interface - methods return `$this`/`static` for chaining: +```php +$class->setFinal() + ->setExtends(ParentClass::class) + ->addImplement(Countable::class); +``` + +### Type Resolution +When a class is part of a namespace, types are automatically resolved: +- Fully qualified names → simplified based on use statements +- Can be disabled: `$printer->setTypeResolving(false)` +- Use `$namespace->simplifyType()` for manual resolution + +### Code Generation Flow +1. Create PhpFile +2. Add namespace(s) +3. Add classes/interfaces/traits/enums/functions to namespace +4. Add members (properties/methods/constants) to classes +5. Convert to string or use Printer explicitly + +### Validation +Classes validate themselves before printing via `validate()` method. Throws `Nette\InvalidStateException` for invalid states (e.g., abstract and final simultaneously). + +### Cloning +All major classes implement `__clone()` to deep-clone contained objects (methods, properties, etc.). + +## Common Tasks + +### Adding New Features +1. Check if feature needs new class or extends existing (e.g., PropertyHook for property hooks) +2. Add tests first in `tests/PhpGenerator/` +3. Update Printer to generate correct syntax +4. Update Factory/Extractor if feature should be loadable from existing code +5. Consider PsrPrinter compatibility + +### Updating for New PHP Versions +1. Add support to builder classes (e.g., new method/property) +2. Update Printer with syntax generation +3. Update Extractor to parse the feature (via php-parser) +4. Add comprehensive tests including edge cases +5. Update README.md with examples + +### Test Expectations +- Test files may have corresponding `.expect` files with expected output +- Use `sameFile()` helper to compare against expectation files +- This keeps test files clean and makes output changes visible in diffs + +## Important Notes & Limitations + +### Loading from Existing Code +- **Requires `nikic/php-parser`** to load method/function bodies with `withBodies: true` / `withBody: true` +- Without php-parser, bodies are empty but signatures are complete +- Single-line comments outside method bodies are ignored when loading (library has no API for them) +- Use `nikic/php-parser` directly if you need to manipulate global code or individual statements + +### PhpFile Restrictions +- **No global code allowed** - PhpFile can only contain namespaces, classes, functions +- Cannot add arbitrary code like `echo 'hello'` outside functions/classes +- Use `setStrictTypes()` for `declare(strict_types=1)` declaration + +### Exception Handling +- Adding duplicate members (same name) throws `Nette\InvalidStateException` +- Use `addMember($member, overwrite: true)` to replace existing members +- Invalid class states (e.g., abstract + final) detected by `validate()` method + +### Removal Methods +- `removeProperty($name)`, `removeConstant($name)`, `removeMethod($name)`, `removeParameter($name)` +- Available on respective container classes + +### Compatibility +- **PhpGenerator 4.1+** supports PHP 8.0 to 8.4 +- **PhpGenerator 4.2** (current) compatible with PHP 8.1 to 8.5 +- Check composer.json for exact version requirements diff --git a/src/PhpGenerator/DumpContext.php b/src/PhpGenerator/DumpContext.php new file mode 100644 index 00000000..97bc3bf1 --- /dev/null +++ b/src/PhpGenerator/DumpContext.php @@ -0,0 +1,21 @@ + */ + private array $refMap = []; /** @@ -30,7 +35,8 @@ final class Dumper */ public function dump(mixed $var, int $column = 0): string { - return $this->dumpVar($var, [], 0, $column); + return $this->dumpReferences($var) + ?? $this->dumpVar($var, column: $column); } @@ -106,7 +112,7 @@ private function dumpArray(array $var, array $parents, int $level, int $column): if (empty($var)) { return '[]'; - } elseif ($level > $this->maxDepth || in_array($var, $parents, strict: true)) { + } elseif ($level > $this->maxDepth || !$this->references && in_array($var, $parents, strict: true)) { throw new Nette\InvalidStateException('Nesting level too deep or recursive dependency.'); } @@ -118,7 +124,16 @@ private function dumpArray(array $var, array $parents, int $level, int $column): $keyPart = $hideKeys && ($k !== $keys[0] || $k === 0) ? '' : $this->dumpVar($k) . ' => '; - $pairs[] = $keyPart . $this->dumpVar($v, $parents, $level + 1, strlen($keyPart) + 1); // 1 = comma after item + + if ( + $this->references + && ($refId = (\ReflectionReference::fromArrayElement($var, $k))?->getId()) + && isset($this->refMap[$refId]) + ) { + $pairs[] = $keyPart . '&$r[' . $this->refMap[$refId] . ']'; + } else { + $pairs[] = $keyPart . $this->dumpVar($v, $parents, $level + 1, strlen($keyPart) + 1); // 1 = comma after item + } } $line = '[' . implode(', ', $pairs) . ']'; @@ -142,10 +157,17 @@ private function dumpObject(object $var, array $parents, int $level, int $column $parents[] = $var; if ($class === \stdClass::class) { + if (!in_array($this->context, [DumpContext::Expression, DumpContext::Parameter, DumpContext::Attribute], strict: true)) { + throw new Nette\InvalidStateException("Cannot dump object of type $class in {$this->context->name} context."); + } + $var = (array) $var; return '(object) ' . $this->dumpArray($var, $parents, $level, $column + 10); } elseif ($class === \DateTime::class || $class === \DateTimeImmutable::class) { + if (!in_array($this->context, [DumpContext::Expression, DumpContext::Parameter, DumpContext::Attribute], strict: true)) { + throw new Nette\InvalidStateException("Cannot dump object of type $class in {$this->context->name} context."); + } assert($var instanceof \DateTimeInterface); return $this->format( "new \\$class(?, new \\DateTimeZone(?))", @@ -165,6 +187,9 @@ private function dumpObject(object $var, array $parents, int $level, int $column throw new Nette\InvalidStateException('Cannot dump object of type Closure.'); } elseif ($this->customObjects) { + if ($this->context !== DumpContext::Expression) { + throw new Nette\InvalidStateException("Cannot dump object of type $class in {$this->context->name} context."); + } return $this->dumpCustomObject($var, $parents, $level); } else { @@ -213,6 +238,53 @@ private function dumpLiteral(Literal $var, int $level): string } + private function dumpReferences(mixed $var): ?string + { + $this->refMap = $refs = []; + if (!$this->references || !is_array($var)) { + return null; + } + + $this->collectReferences($var, $refs); + $refs = array_filter($refs, fn($ref) => $ref[0] >= 2); + if (!$refs) { + return null; + } + + $n = 0; + foreach ($refs as $refId => $_) { + $this->refMap[$refId] = ++$n; + } + + $preamble = ''; + foreach ($this->refMap as $refId => $n) { + $preamble .= '$r[' . $n . '] = ' . $this->dumpVar($refs[$refId][1]) . '; '; + } + + return '(static function () { ' . $preamble . 'return ' . $this->dumpVar($var) . '; })()'; + } + + + /** + * @param mixed[] $var + * @param array $refs + */ + private function collectReferences(array $var, array &$refs): void + { + foreach ($var as $k => $v) { + $refId = (\ReflectionReference::fromArrayElement($var, $k))?->getId(); + if ($refId !== null) { + $refs[$refId] ??= [0, $v]; + $refs[$refId][0]++; + } + + if (is_array($v) && ($refId === null || $refs[$refId][0] === 1)) { + $this->collectReferences($v, $refs); + } + } + } + + /** * Generates PHP statement. Supports placeholders: ? \? $? ->? ::? ...? ...?: ?* */ diff --git a/src/PhpGenerator/Extractor.php b/src/PhpGenerator/Extractor.php index 19013082..eb369ee3 100644 --- a/src/PhpGenerator/Extractor.php +++ b/src/PhpGenerator/Extractor.php @@ -243,18 +243,29 @@ public function extractAll(): PhpFile { $phpFile = new PhpFile; + $comments = []; + $firstStmt = $this->statements[0] ?? null; + if ($firstStmt instanceof Node\Stmt\Declare_) { + $comments = $firstStmt->getComments(); + if (!$firstStmt->getDocComment()) { + $comments = []; + $firstStmt = $this->statements[1] ?? null; + } + } + if ( - ($firstStmt = $this->statements[0] ?? null) - && ($firstStmt = $firstStmt instanceof Node\Stmt\Declare_ ? $this->statements[1] ?? null : $firstStmt) + !$comments + && $firstStmt && !$firstStmt instanceof Node\Stmt\ClassLike && !$firstStmt instanceof Node\Stmt\Function_ ) { $comments = $firstStmt->getComments(); - foreach ($comments as $i => $comment) { - if ($comment instanceof PhpParser\Comment\Doc) { - $phpFile->setComment(Helpers::unformatDocComment($comment->getReformattedText())); - break; - } + } + + foreach ($comments as $comment) { + if ($comment instanceof PhpParser\Comment\Doc) { + $phpFile->setComment(Helpers::unformatDocComment($comment->getReformattedText())); + break; } } diff --git a/src/PhpGenerator/Printer.php b/src/PhpGenerator/Printer.php index 00a0ea07..4ad70b16 100644 --- a/src/PhpGenerator/Printer.php +++ b/src/PhpGenerator/Printer.php @@ -170,7 +170,7 @@ public function printClass( $cases[] = $this->printDocComment($case) . $this->printAttributes($case->getAttributes()) . 'case ' . $case->getName() - . ($case->getValue() === null ? '' : ' = ' . $this->dump($case->getValue())) + . ($case->getValue() === null ? '' : ' = ' . $this->dump($case->getValue(), context: DumpContext::Constant)) . ";\n"; } } @@ -349,7 +349,7 @@ private function formatParameters(Closure|GlobalFunction|Method|PropertyHook $fu . ($param->isReference() ? '&' : '') . ($variadic ? '...' : '') . '$' . $param->getName() - . ($param->hasDefaultValue() && !$variadic ? ' = ' . $this->dump($param->getDefaultValue()) : '') + . ($param->hasDefaultValue() && !$variadic ? ' = ' . $this->dump($param->getDefaultValue(), context: DumpContext::Parameter) : '') . ($param instanceof PromotedParameter ? $this->printHooks($param) : '') . ($multiline ? ",\n" : ', '); } @@ -371,7 +371,7 @@ private function printConstant(Constant $const): string return $this->printDocComment($const) . $this->printAttributes($const->getAttributes()) . $def - . $this->dump($const->getValue(), strlen($def)) . ";\n"; + . $this->dump($const->getValue(), strlen($def), DumpContext::Constant) . ";\n"; } @@ -390,7 +390,7 @@ private function printProperty(Property $property, bool $readOnlyClass = false, $defaultValue = $property->getValue() === null && !$property->isInitialized() ? '' - : ' = ' . $this->dump($property->getValue(), strlen($def) + 3); // 3 = ' = ' + : ' = ' . $this->dump($property->getValue(), strlen($def) + 3, DumpContext::Property); // 3 = ' = ' return $this->printDocComment($property) . $this->printAttributes($property->getAttributes()) @@ -454,6 +454,7 @@ protected function printAttributes(array $attrs, bool $inline = false): string } $this->dumper->indentation = $this->indentation; + $this->dumper->context = DumpContext::Attribute; $items = []; foreach ($attrs as $attr) { $args = $this->dumper->format('...?:', $attr->getArguments()); @@ -513,10 +514,11 @@ protected function indent(string $s): string } - protected function dump(mixed $var, int $column = 0): string + protected function dump(mixed $var, int $column = 0, DumpContext $context = DumpContext::Expression): string { $this->dumper->indentation = $this->indentation; $this->dumper->wrapLength = $this->wrapLength; + $this->dumper->context = $context; $s = $this->dumper->dump($var, $column); $s = Helpers::simplifyTaggedNames($s, $this->namespace); return $s; diff --git a/tests/PhpGenerator/Dumper.context.phpt b/tests/PhpGenerator/Dumper.context.phpt new file mode 100644 index 00000000..f0fe6ff3 --- /dev/null +++ b/tests/PhpGenerator/Dumper.context.phpt @@ -0,0 +1,166 @@ +context = DumpContext::Constant; + Assert::exception( + fn() => $dumper->dump((object) ['a' => 1]), + Nette\InvalidStateException::class, + '%a% stdClass %a% Constant %a%', + ); +}); + + +test('Constant context rejects DateTime', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Constant; + Assert::exception( + fn() => $dumper->dump(new DateTime('2024-01-01')), + Nette\InvalidStateException::class, + '%a% DateTime %a% Constant %a%', + ); +}); + + +test('Constant context rejects custom objects', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Constant; + Assert::exception( + fn() => $dumper->dump(new ArrayObject), + Nette\InvalidStateException::class, + '%a% ArrayObject %a% Constant %a%', + ); +}); + + +test('Constant context allows first-class callable', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Constant; + Assert::contains('(...)', $dumper->dump(strlen(...))); +}); + + +// Property context (same restrictions as Constant) + +test('Property context rejects stdClass', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Property; + Assert::exception( + fn() => $dumper->dump((object) ['a' => 1]), + Nette\InvalidStateException::class, + '%a% Property %a%', + ); +}); + + +test('Property context rejects DateTime', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Property; + Assert::exception( + fn() => $dumper->dump(new DateTime('2024-01-01')), + Nette\InvalidStateException::class, + '%a% Property %a%', + ); +}); + + +test('Property context rejects custom objects', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Property; + Assert::exception( + fn() => $dumper->dump(new ArrayObject), + Nette\InvalidStateException::class, + '%a% Property %a%', + ); +}); + + +// Parameter context (allows new, casts) + +test('Parameter context allows stdClass', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Parameter; + Assert::contains('(object)', $dumper->dump((object) ['a' => 1])); +}); + + +test('Parameter context allows DateTime', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Parameter; + Assert::contains('new \DateTime', $dumper->dump(new DateTime('2024-01-01'))); +}); + + +test('Parameter context rejects custom objects', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Parameter; + Assert::exception( + fn() => $dumper->dump(new ArrayObject), + Nette\InvalidStateException::class, + '%a% ArrayObject %a% Parameter %a%', + ); +}); + + +// Attribute context (same as Parameter) + +test('Attribute context allows stdClass', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Attribute; + Assert::contains('(object)', $dumper->dump((object) ['a' => 1])); +}); + + +test('Attribute context allows DateTime', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Attribute; + Assert::contains('new \DateTime', $dumper->dump(new DateTime('2024-01-01'))); +}); + + +test('Attribute context rejects custom objects', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Attribute; + Assert::exception( + fn() => $dumper->dump(new ArrayObject), + Nette\InvalidStateException::class, + '%a% ArrayObject %a% Attribute %a%', + ); +}); + + +// Expression context (no restrictions, default) + +test('Expression context allows everything', function () { + $dumper = new Dumper; + Assert::same(DumpContext::Expression, $dumper->context); + Assert::contains('(object)', $dumper->dump((object) ['a' => 1])); + Assert::contains('new \DateTime', $dumper->dump(new DateTime('2024-01-01'))); + Assert::contains('createObject', $dumper->dump(new ArrayObject)); +}); + + +// Propagation through arrays + +test('context propagates into array elements', function () { + $dumper = new Dumper; + $dumper->context = DumpContext::Constant; + Assert::exception( + fn() => $dumper->dump(['key' => (object) ['a' => 1]]), + Nette\InvalidStateException::class, + '%a% stdClass %a% Constant %a%', + ); +}); diff --git a/tests/PhpGenerator/Dumper.ref.phpt b/tests/PhpGenerator/Dumper.ref.phpt new file mode 100644 index 00000000..c338a389 --- /dev/null +++ b/tests/PhpGenerator/Dumper.ref.phpt @@ -0,0 +1,132 @@ +dump($arr)); +}); + + +test('ref=true single-use reference is not tracked', function () { + $a = 42; + $arr = [&$a]; + $dumper = new Dumper; + $dumper->references = true; + Assert::same('[42]', $dumper->dump($arr)); +}); + + +test('ref=true shared reference', function () { + $a = 'hello'; + $arr = [&$a, &$a]; + $dumper = new Dumper; + $dumper->references = true; + Assert::same("(static function () { \$r[1] = 'hello'; return [&\$r[1], &\$r[1]]; })()", $dumper->dump($arr)); +}); + + +test('ref=true mixed references and plain values', function () { + $a = 'ref'; + $arr = ['plain', &$a, 'also plain', &$a]; + $dumper = new Dumper; + $dumper->references = true; + Assert::same("(static function () { \$r[1] = 'ref'; return ['plain', &\$r[1], 'also plain', &\$r[1]]; })()", $dumper->dump($arr)); +}); + + +test('ref=true with nested arrays', function () { + $a = 42; + $arr = [[&$a], [&$a]]; + $dumper = new Dumper; + $dumper->references = true; + Assert::same('(static function () { $r[1] = 42; return [[&$r[1]], [&$r[1]]]; })()', $dumper->dump($arr)); +}); + + +test('ref=true with named keys', function () { + $a = 'val'; + $arr = ['x' => &$a, 'y' => &$a]; + $dumper = new Dumper; + $dumper->references = true; + Assert::same("(static function () { \$r[1] = 'val'; return ['x' => &\$r[1], 'y' => &\$r[1]]; })()", $dumper->dump($arr)); +}); + + +test('ref=true multiple reference groups', function () { + $a = 'A'; + $b = 'B'; + $arr = [&$a, &$b, &$a, &$b]; + $dumper = new Dumper; + $dumper->references = true; + Assert::same("(static function () { \$r[1] = 'A'; \$r[2] = 'B'; return [&\$r[1], &\$r[2], &\$r[1], &\$r[2]]; })()", $dumper->dump($arr)); +}); + + +test('ref=true references reset between dump calls', function () { + $dumper = new Dumper; + $dumper->references = true; + + $a = 1; + Assert::same('(static function () { $r[1] = 1; return [&$r[1], &$r[1]]; })()', $dumper->dump([&$a, &$a])); + + $b = 2; + Assert::same('(static function () { $r[1] = 2; return [&$r[1], &$r[1]]; })()', $dumper->dump([&$b, &$b])); +}); + + +test('ref=true cross-dependent values', function () { + $a = 'x'; + $b = [1, 2, &$a]; + $c = [&$b, &$a, &$b]; + $dumper = new Dumper; + $dumper->references = true; + $result = $dumper->dump($c); + Assert::contains('static function ()', $result); + + // verify the generated code recreates correct references + $reconstructed = eval('return ' . $result . ';'); + $reconstructed[1] = 'changed'; + Assert::same('changed', $reconstructed[0][2]); + Assert::same('changed', $reconstructed[2][2]); + $reconstructed[0][0] = 99; + Assert::same(99, $reconstructed[2][0]); +}); + + +test('ref=true recursive reference', function () { + $arr = [1, 2]; + $arr[2] = &$arr; + $dumper = new Dumper; + $dumper->references = true; + $result = $dumper->dump($arr); + Assert::contains('static function ()', $result); + + // verify recursive structure + $reconstructed = eval('return ' . $result . ';'); + Assert::same(1, $reconstructed[0]); + Assert::same(2, $reconstructed[1]); + Assert::type('array', $reconstructed[2]); +}); + + +test('ref=false throws on recursive array', function () { + $arr = [1]; + $arr[1] = &$arr; + $dumper = new Dumper; + Assert::exception( + fn() => $dumper->dump($arr), + Nette\InvalidStateException::class, + '%a%recursive%a%', + ); +}); diff --git a/tests/PhpGenerator/Extractor.extractAll.file.comments.phpt b/tests/PhpGenerator/Extractor.extractAll.file.comments.phpt index ee0b1827..810ecc8a 100644 --- a/tests/PhpGenerator/Extractor.extractAll.file.comments.phpt +++ b/tests/PhpGenerator/Extractor.extractAll.file.comments.phpt @@ -30,6 +30,19 @@ $file = (new Extractor(<<<'XX' Assert::same('doc comment', $file->getComment()); +$file = (new Extractor(<<<'XX' + extractAll(); + +Assert::same('doc comment', $file->getComment()); + + $file = (new Extractor(<<<'XX'