Skip to content

Commit 4c8a26c

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 2398d01 commit 4c8a26c

4 files changed

Lines changed: 209 additions & 5 deletions

File tree

src/PhpGenerator/DumpContext.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
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+
declare(strict_types=1);
9+
10+
namespace Nette\PhpGenerator;
11+
12+
13+
/**
14+
* Context in which a dumped value will be used.
15+
*/
16+
enum DumpContext
17+
{
18+
case Expression;
19+
case Constant;
20+
case Property;
21+
case Parameter;
22+
case Attribute;
23+
}

src/PhpGenerator/Dumper.php

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

2930

3031
/**
@@ -144,10 +145,17 @@ private function dumpObject(object $var, array $parents, int $level, int $column
144145
$parents[] = $var;
145146

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

150155
} elseif ($class === \DateTime::class || $class === \DateTimeImmutable::class) {
156+
if (!in_array($this->context, [DumpContext::Expression, DumpContext::Parameter, DumpContext::Attribute], strict: true)) {
157+
throw new Nette\InvalidStateException("Cannot dump object of type $class in {$this->context->name} context.");
158+
}
151159
assert($var instanceof \DateTimeInterface);
152160
return $this->format(
153161
"new \\$class(?, new \\DateTimeZone(?))",
@@ -167,6 +175,9 @@ private function dumpObject(object $var, array $parents, int $level, int $column
167175
throw new Nette\InvalidStateException('Cannot dump object of type Closure.');
168176

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

172183
} else {

src/PhpGenerator/Printer.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ public function printClass(
171171
$cases[] = $this->printDocComment($case)
172172
. $this->printAttributes($case->getAttributes())
173173
. 'case ' . $case->getName()
174-
. ($case->getValue() === null ? '' : ' = ' . $this->dump($case->getValue()))
174+
. ($case->getValue() === null ? '' : ' = ' . $this->dump($case->getValue(), context: DumpContext::Constant))
175175
. ";\n";
176176
}
177177
}
@@ -348,7 +348,7 @@ private function formatParameters(Closure|GlobalFunction|Method|PropertyHook $fu
348348
. ($param->isReference() ? '&' : '')
349349
. ($variadic ? '...' : '')
350350
. '$' . $param->getName()
351-
. ($param->hasDefaultValue() && !$variadic ? ' = ' . $this->dump($param->getDefaultValue()) : '')
351+
. ($param->hasDefaultValue() && !$variadic ? ' = ' . $this->dump($param->getDefaultValue(), context: DumpContext::Parameter) : '')
352352
. ($param instanceof PromotedParameter ? $this->printHooks($param) : '')
353353
. ($multiline ? ",\n" : ', ');
354354
}
@@ -370,7 +370,7 @@ private function printConstant(Constant $const): string
370370
return $this->printDocComment($const)
371371
. $this->printAttributes($const->getAttributes())
372372
. $def
373-
. $this->dump($const->getValue(), strlen($def)) . ";\n";
373+
. $this->dump($const->getValue(), strlen($def), DumpContext::Constant) . ";\n";
374374
}
375375

376376

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

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

394394
return $this->printDocComment($property)
395395
. $this->printAttributes($property->getAttributes())
@@ -453,6 +453,7 @@ protected function printAttributes(array $attrs, bool $inline = false): string
453453
}
454454

455455
$this->dumper->indentation = $this->indentation;
456+
$this->dumper->context = DumpContext::Attribute;
456457
$items = [];
457458
foreach ($attrs as $attr) {
458459
$args = $this->dumper->format('...?:', $attr->getArguments());
@@ -512,10 +513,11 @@ protected function indent(string $s): string
512513
}
513514

514515

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

0 commit comments

Comments
 (0)