Skip to content

Commit 92fe63d

Browse files
committed
Dumper: added context-aware validation
| Expression | member | param | |---------------------------|--------|-------| | scalar, array, Enum::Case | 8.1+ | 8.1+ | | `new ClassName(...)` | — | 8.1+ | | `strlen(...)` | 8.5+ | 8.5+ | | `(object) [...]` cast | — | 8.5+ | | `fun()` | — | — |
1 parent a89249e commit 92fe63d

4 files changed

Lines changed: 205 additions & 5 deletions

File tree

src/PhpGenerator/DumpContext.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types=1);
2+
3+
/**
4+
* This file is part of the Nette Framework (https://nette.org)
5+
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
namespace Nette\PhpGenerator;
9+
10+
11+
/**
12+
* Context in which a dumped value will be used.
13+
*/
14+
enum DumpContext
15+
{
16+
case Expression;
17+
case Constant;
18+
case Property;
19+
case Parameter;
20+
case Attribute;
21+
}

src/PhpGenerator/Dumper.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ final class Dumper
2323
public int $wrapLength = 120;
2424
public string $indentation = "\t";
2525
public bool $customObjects = true;
26+
public DumpContext $context = DumpContext::Expression;
2627

2728

2829
/**
@@ -142,10 +143,17 @@ private function dumpObject(object $var, array $parents, int $level, int $column
142143
$parents[] = $var;
143144

144145
if ($class === \stdClass::class) {
146+
if (!in_array($this->context, [DumpContext::Expression, DumpContext::Parameter, DumpContext::Attribute], strict: true)) {
147+
throw new Nette\InvalidStateException("Cannot dump object of type $class in {$this->context->name} context.");
148+
}
149+
145150
$var = (array) $var;
146151
return '(object) ' . $this->dumpArray($var, $parents, $level, $column + 10);
147152

148153
} elseif ($class === \DateTime::class || $class === \DateTimeImmutable::class) {
154+
if (!in_array($this->context, [DumpContext::Expression, DumpContext::Parameter, DumpContext::Attribute], strict: true)) {
155+
throw new Nette\InvalidStateException("Cannot dump object of type $class in {$this->context->name} context.");
156+
}
149157
assert($var instanceof \DateTimeInterface);
150158
return $this->format(
151159
"new \\$class(?, new \\DateTimeZone(?))",
@@ -165,6 +173,9 @@ private function dumpObject(object $var, array $parents, int $level, int $column
165173
throw new Nette\InvalidStateException('Cannot dump object of type Closure.');
166174

167175
} elseif ($this->customObjects) {
176+
if ($this->context !== DumpContext::Expression) {
177+
throw new Nette\InvalidStateException("Cannot dump object of type $class in {$this->context->name} context.");
178+
}
168179
return $this->dumpCustomObject($var, $parents, $level);
169180

170181
} else {

src/PhpGenerator/Printer.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ public function printClass(
169169
$cases[] = $this->printDocComment($case)
170170
. $this->printAttributes($case->getAttributes())
171171
. 'case ' . $case->getName()
172-
. ($case->getValue() === null ? '' : ' = ' . $this->dump($case->getValue()))
172+
. ($case->getValue() === null ? '' : ' = ' . $this->dump($case->getValue(), context: DumpContext::Constant))
173173
. ";\n";
174174
}
175175
}
@@ -346,7 +346,7 @@ private function formatParameters(Closure|GlobalFunction|Method|PropertyHook $fu
346346
. ($param->isReference() ? '&' : '')
347347
. ($variadic ? '...' : '')
348348
. '$' . $param->getName()
349-
. ($param->hasDefaultValue() && !$variadic ? ' = ' . $this->dump($param->getDefaultValue()) : '')
349+
. ($param->hasDefaultValue() && !$variadic ? ' = ' . $this->dump($param->getDefaultValue(), context: DumpContext::Parameter) : '')
350350
. ($param instanceof PromotedParameter ? $this->printHooks($param) : '')
351351
. ($multiline ? ",\n" : ', ');
352352
}
@@ -368,7 +368,7 @@ private function printConstant(Constant $const): string
368368
return $this->printDocComment($const)
369369
. $this->printAttributes($const->getAttributes())
370370
. $def
371-
. $this->dump($const->getValue(), strlen($def)) . ";\n";
371+
. $this->dump($const->getValue(), strlen($def), DumpContext::Constant) . ";\n";
372372
}
373373

374374

@@ -387,7 +387,7 @@ private function printProperty(Property $property, bool $readOnlyClass = false,
387387

388388
$defaultValue = $property->getValue() === null && !$property->isInitialized()
389389
? ''
390-
: ' = ' . $this->dump($property->getValue(), strlen($def) + 3); // 3 = ' = '
390+
: ' = ' . $this->dump($property->getValue(), strlen($def) + 3, DumpContext::Property); // 3 = ' = '
391391

392392
return $this->printDocComment($property)
393393
. $this->printAttributes($property->getAttributes())
@@ -451,6 +451,7 @@ protected function printAttributes(array $attrs, bool $inline = false): string
451451
}
452452

453453
$this->dumper->indentation = $this->indentation;
454+
$this->dumper->context = DumpContext::Attribute;
454455
$items = [];
455456
foreach ($attrs as $attr) {
456457
$args = $this->dumper->format('...?:', $attr->getArguments());
@@ -510,10 +511,11 @@ protected function indent(string $s): string
510511
}
511512

512513

513-
protected function dump(mixed $var, int $column = 0): string
514+
protected function dump(mixed $var, int $column = 0, DumpContext $context = DumpContext::Expression): string
514515
{
515516
$this->dumper->indentation = $this->indentation;
516517
$this->dumper->wrapLength = $this->wrapLength;
518+
$this->dumper->context = $context;
517519
$s = $this->dumper->dump($var, $column);
518520
$s = Helpers::simplifyTaggedNames($s, $this->namespace);
519521
return $s;
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
<?php declare(strict_types=1);
2+
3+
/**
4+
* Test: Nette\PhpGenerator\Dumper context validation
5+
*/
6+
7+
use Nette\PhpGenerator\DumpContext;
8+
use Nette\PhpGenerator\Dumper;
9+
use Tester\Assert;
10+
11+
require __DIR__ . '/../bootstrap.php';
12+
13+
14+
// Constant context (class constant, property default)
15+
16+
test('Constant context rejects stdClass', function () {
17+
$dumper = new Dumper;
18+
$dumper->context = DumpContext::Constant;
19+
Assert::exception(
20+
fn() => $dumper->dump((object) ['a' => 1]),
21+
Nette\InvalidStateException::class,
22+
'%a% stdClass %a% Constant %a%',
23+
);
24+
});
25+
26+
27+
test('Constant context rejects DateTime', function () {
28+
$dumper = new Dumper;
29+
$dumper->context = DumpContext::Constant;
30+
Assert::exception(
31+
fn() => $dumper->dump(new DateTime('2024-01-01')),
32+
Nette\InvalidStateException::class,
33+
'%a% DateTime %a% Constant %a%',
34+
);
35+
});
36+
37+
38+
test('Constant context rejects custom objects', function () {
39+
$dumper = new Dumper;
40+
$dumper->context = DumpContext::Constant;
41+
Assert::exception(
42+
fn() => $dumper->dump(new ArrayObject),
43+
Nette\InvalidStateException::class,
44+
'%a% ArrayObject %a% Constant %a%',
45+
);
46+
});
47+
48+
49+
test('Constant context allows first-class callable', function () {
50+
$dumper = new Dumper;
51+
$dumper->context = DumpContext::Constant;
52+
Assert::contains('(...)', $dumper->dump(strlen(...)));
53+
});
54+
55+
56+
// Property context (same restrictions as Constant)
57+
58+
test('Property context rejects stdClass', function () {
59+
$dumper = new Dumper;
60+
$dumper->context = DumpContext::Property;
61+
Assert::exception(
62+
fn() => $dumper->dump((object) ['a' => 1]),
63+
Nette\InvalidStateException::class,
64+
'%a% Property %a%',
65+
);
66+
});
67+
68+
69+
test('Property context rejects DateTime', function () {
70+
$dumper = new Dumper;
71+
$dumper->context = DumpContext::Property;
72+
Assert::exception(
73+
fn() => $dumper->dump(new DateTime('2024-01-01')),
74+
Nette\InvalidStateException::class,
75+
'%a% Property %a%',
76+
);
77+
});
78+
79+
80+
test('Property context rejects custom objects', function () {
81+
$dumper = new Dumper;
82+
$dumper->context = DumpContext::Property;
83+
Assert::exception(
84+
fn() => $dumper->dump(new ArrayObject),
85+
Nette\InvalidStateException::class,
86+
'%a% Property %a%',
87+
);
88+
});
89+
90+
91+
// Parameter context (allows new, casts)
92+
93+
test('Parameter context allows stdClass', function () {
94+
$dumper = new Dumper;
95+
$dumper->context = DumpContext::Parameter;
96+
Assert::contains('(object)', $dumper->dump((object) ['a' => 1]));
97+
});
98+
99+
100+
test('Parameter context allows DateTime', function () {
101+
$dumper = new Dumper;
102+
$dumper->context = DumpContext::Parameter;
103+
Assert::contains('new \DateTime', $dumper->dump(new DateTime('2024-01-01')));
104+
});
105+
106+
107+
test('Parameter context rejects custom objects', function () {
108+
$dumper = new Dumper;
109+
$dumper->context = DumpContext::Parameter;
110+
Assert::exception(
111+
fn() => $dumper->dump(new ArrayObject),
112+
Nette\InvalidStateException::class,
113+
'%a% ArrayObject %a% Parameter %a%',
114+
);
115+
});
116+
117+
118+
// Attribute context (same as Parameter)
119+
120+
test('Attribute context allows stdClass', function () {
121+
$dumper = new Dumper;
122+
$dumper->context = DumpContext::Attribute;
123+
Assert::contains('(object)', $dumper->dump((object) ['a' => 1]));
124+
});
125+
126+
127+
test('Attribute context allows DateTime', function () {
128+
$dumper = new Dumper;
129+
$dumper->context = DumpContext::Attribute;
130+
Assert::contains('new \DateTime', $dumper->dump(new DateTime('2024-01-01')));
131+
});
132+
133+
134+
test('Attribute context rejects custom objects', function () {
135+
$dumper = new Dumper;
136+
$dumper->context = DumpContext::Attribute;
137+
Assert::exception(
138+
fn() => $dumper->dump(new ArrayObject),
139+
Nette\InvalidStateException::class,
140+
'%a% ArrayObject %a% Attribute %a%',
141+
);
142+
});
143+
144+
145+
// Expression context (no restrictions, default)
146+
147+
test('Expression context allows everything', function () {
148+
$dumper = new Dumper;
149+
Assert::same(DumpContext::Expression, $dumper->context);
150+
Assert::contains('(object)', $dumper->dump((object) ['a' => 1]));
151+
Assert::contains('new \DateTime', $dumper->dump(new DateTime('2024-01-01')));
152+
Assert::contains('createObject', $dumper->dump(new ArrayObject));
153+
});
154+
155+
156+
// Propagation through arrays
157+
158+
test('context propagates into array elements', function () {
159+
$dumper = new Dumper;
160+
$dumper->context = DumpContext::Constant;
161+
Assert::exception(
162+
fn() => $dumper->dump(['key' => (object) ['a' => 1]]),
163+
Nette\InvalidStateException::class,
164+
'%a% stdClass %a% Constant %a%',
165+
);
166+
});

0 commit comments

Comments
 (0)