Skip to content

Commit 2677e76

Browse files
authored
Merge pull request #220 from php-school/force-strict-types-patch
Force strict types patch
2 parents 57f5e9a + 140c5e4 commit 2677e76

File tree

7 files changed

+245
-19
lines changed

7 files changed

+245
-19
lines changed

src/CodePatcher.php

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77
use PhpParser\Error;
88
use PhpParser\Node\Stmt;
9+
use PhpParser\Node\Stmt\Declare_;
910
use PhpParser\Parser;
1011
use PhpParser\PrettyPrinter\Standard;
1112
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
1213
use PhpSchool\PhpWorkshop\Exercise\SubmissionPatchable;
14+
use PhpSchool\PhpWorkshop\Patch\Transformer;
1315

1416
/**
1517
* Service to apply patches to a student's solution. Accepts a default patch via the constructor.
@@ -90,30 +92,43 @@ private function applyPatch(Patch $patch, string $code): string
9092
$statements = [];
9193
}
9294

93-
$declare = null;
94-
if (isset($statements[0]) && $statements[0] instanceof \PhpParser\Node\Stmt\Declare_) {
95-
$declare = array_shift($statements);
96-
}
97-
9895
foreach ($patch->getModifiers() as $modifier) {
96+
$declare = null;
97+
if ($this->isFirstStatementStrictTypesDeclare($statements)) {
98+
$declare = array_shift($statements);
99+
}
100+
99101
if ($modifier instanceof CodeInsertion) {
100102
$statements = $this->applyCodeInsertion($modifier, $statements);
101-
continue;
102103
}
103104

104-
if (is_callable($modifier)) {
105+
if ($modifier instanceof \Closure) {
105106
$statements = $modifier($statements);
106-
continue;
107107
}
108-
}
109108

110-
if ($declare !== null) {
111-
array_unshift($statements, $declare);
109+
if ($modifier instanceof Transformer) {
110+
$statements = $modifier->transform($statements);
111+
}
112+
113+
if ($declare !== null && !$this->isFirstStatementStrictTypesDeclare($statements)) {
114+
array_unshift($statements, $declare);
115+
}
112116
}
113117

114118
return $this->printer->prettyPrintFile($statements);
115119
}
116120

121+
/**
122+
* @param array<Stmt> $statements
123+
*/
124+
public function isFirstStatementStrictTypesDeclare(array $statements): bool
125+
{
126+
return isset($statements[0])
127+
&& $statements[0] instanceof Declare_
128+
&& isset($statements[0]->declares[0])
129+
&& $statements[0]->declares[0]->key->name === 'strict_types';
130+
}
131+
117132
/**
118133
* @param CodeInsertion $codeInsertion
119134
* @param array<Stmt> $statements

src/Patch.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,23 @@
55
namespace PhpSchool\PhpWorkshop;
66

77
use Closure;
8+
use PhpSchool\PhpWorkshop\Patch\Transformer;
89

910
/**
1011
* This class is responsible for storing the modifications that should
1112
* be made to a PHP file. That includes insertions and transformers.
1213
* A transformer is a simple closure which should receives an AST
1314
* representation of the student's solution and it should return the modified AST.
15+
*
16+
* Transformers can also be class implementing `Transformer`.
17+
*
1418
* An insertion is a block of code that can be inserted at the top or bottom of
1519
* the students solution.
1620
*/
1721
final class Patch
1822
{
1923
/**
20-
* @var array<CodeInsertion|Closure>
24+
* @var array<CodeInsertion|Closure|Transformer>
2125
*/
2226
private $modifications = [];
2327

@@ -35,22 +39,23 @@ public function withInsertion(CodeInsertion $insertion): self
3539
}
3640

3741
/**
38-
* Add a new transformer (`Closure`). `Patch` is immutable so a new instance is returned.
42+
* Add a new transformer (`Closure` OR `Transformer`). `Patch` is immutable so a new instance is returned.
3943
*
40-
* @param Closure $closure
44+
* @param Closure|Transformer $closure
4145
* @return self
4246
*/
43-
public function withTransformer(Closure $closure): self
47+
public function withTransformer($closure): self
4448
{
4549
$new = clone $this;
4650
$new->modifications[] = $closure;
4751
return $new;
4852
}
4953

5054
/**
51-
* Retrieve all the modifications including insertions (`CodeInsertion`'s) & transformers (`Closure`'s)
55+
* Retrieve all the modifications including insertions (`CodeInsertion`'s)
56+
* & transformers (`Closure`'s OR `Transformer`'s)
5257
*
53-
* @return array<CodeInsertion|Closure>
58+
* @return array<CodeInsertion|Closure|Transformer>
5459
*/
5560
public function getModifiers(): array
5661
{

src/Patch/ForceStrictTypes.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshop\Patch;
4+
5+
use PhpParser\Node\Scalar\LNumber;
6+
use PhpParser\Node\Stmt;
7+
use PhpParser\Node\Stmt\Declare_;
8+
use PhpParser\Node\Stmt\DeclareDeclare;
9+
10+
class ForceStrictTypes implements Transformer
11+
{
12+
/**
13+
* @param array<Stmt> $statements
14+
* @return array<Stmt>
15+
*/
16+
public function transform(array $statements): array
17+
{
18+
if ($this->isFirstStatementStrictTypesDeclare($statements)) {
19+
return $statements;
20+
}
21+
22+
$declare = new \PhpParser\Node\Stmt\Declare_([
23+
new DeclareDeclare(
24+
new \PhpParser\Node\Identifier('strict_types'),
25+
new LNumber(1)
26+
)
27+
]);
28+
29+
return array_merge([$declare], $statements);
30+
}
31+
32+
/**
33+
* @param array<Stmt> $statements
34+
*/
35+
public function isFirstStatementStrictTypesDeclare(array $statements): bool
36+
{
37+
return isset($statements[0]) && $statements[0] instanceof Declare_;
38+
}
39+
}

src/Patch/Transformer.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshop\Patch;
4+
5+
use PhpParser\Node\Stmt;
6+
7+
interface Transformer
8+
{
9+
/**
10+
* @param array<Stmt> $statements
11+
* @return array<Stmt>
12+
*/
13+
public function transform(array $statements): array;
14+
}

test/CodePatcherTest.php

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44

55
use PhpParser\Node\Expr\Variable;
66
use PhpParser\Node\Name;
7+
use PhpParser\Node\Scalar\LNumber;
78
use PhpParser\Node\Stmt\Catch_;
9+
use PhpParser\Node\Stmt\DeclareDeclare;
810
use PhpParser\Node\Stmt\TryCatch;
911
use PhpParser\ParserFactory;
1012
use PhpParser\PrettyPrinter\Standard;
13+
use PhpParser\PrettyPrinterAbstract;
1114
use PhpSchool\PhpWorkshop\CodePatcher;
1215
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
1316
use PhpSchool\PhpWorkshop\Patch;
@@ -92,7 +95,7 @@ public function codeProvider(): array
9295
(new Patch())->withInsertion(new Insertion(Insertion::TYPE_BEFORE, ' <?php $before = "here";')),
9396
"<?php\n\n\$before = \"here\";\n\$original = true;"
9497
],
95-
'transformer' => [
98+
'transformer-closure' => [
9699
'<?php $original = true;',
97100
(new Patch())
98101
->withTransformer(function (array $statements) {
@@ -119,6 +122,22 @@ public function codeProvider(): array
119122
}),
120123
"<?php\n\ntry {\n \$before = \"here\";\n \$original = true;\n} catch (Exception \$e) {\n}"
121124
],
125+
'transformer-class' => [
126+
'<?php $original = true;',
127+
(new Patch())
128+
->withTransformer(new class implements Patch\Transformer {
129+
public function transform(array $statements): array
130+
{
131+
return [
132+
new TryCatch(
133+
$statements,
134+
[new Catch_([new Name(\Exception::class)], new Variable('e'), [])]
135+
)
136+
];
137+
}
138+
}),
139+
"<?php\n\ntry {\n \$original = true;\n} catch (Exception \$e) {\n}"
140+
],
122141
];
123142
}
124143

@@ -169,4 +188,61 @@ public function testTransformerWithStrictTypes(): void
169188
$patcher->patch($exercise, $code)
170189
);
171190
}
191+
192+
public function testTransformerWhichAddsStrictTypesDoesNotResultInDoubleStrictTypesStatement(): void
193+
{
194+
$code = '<?php declare(strict_types=1); $original = true;';
195+
$patch = (new Patch())
196+
->withTransformer(function (array $statements) {
197+
return [new \PhpParser\Node\Stmt\Declare_([
198+
new DeclareDeclare(
199+
new \PhpParser\Node\Identifier('strict_types'),
200+
new LNumber(1)
201+
)
202+
])];
203+
});
204+
205+
$patcher = new CodePatcher((new ParserFactory())->create(ParserFactory::PREFER_PHP7), new Standard());
206+
207+
$exercise = $this->createMock(PatchableExercise::class);
208+
209+
$exercise
210+
->expects($this->once())
211+
->method('getPatch')
212+
->willReturn($patch);
213+
214+
$this->assertEquals(
215+
"<?php\n\ndeclare (strict_types=1);",
216+
$patcher->patch($exercise, $code)
217+
);
218+
}
219+
220+
public function testAddingStrictTypesDeclareDoesNotBreakBeforeInsertion(): void
221+
{
222+
$code = '<?php $original = true;';
223+
$patch = (new Patch())
224+
->withTransformer(function (array $statements) {
225+
return array_merge([new \PhpParser\Node\Stmt\Declare_([
226+
new DeclareDeclare(
227+
new \PhpParser\Node\Identifier('strict_types'),
228+
new LNumber(1)
229+
)
230+
])], $statements);
231+
})
232+
->withInsertion(new Insertion(Insertion::TYPE_BEFORE, '$before = "here";'));
233+
234+
$patcher = new CodePatcher((new ParserFactory())->create(ParserFactory::PREFER_PHP7), new Standard());
235+
236+
$exercise = $this->createMock(PatchableExercise::class);
237+
238+
$exercise
239+
->expects($this->once())
240+
->method('getPatch')
241+
->willReturn($patch);
242+
243+
$this->assertEquals(
244+
"<?php\n\ndeclare (strict_types=1);\n\$before = \"here\";\n\$original = true;",
245+
$patcher->patch($exercise, $code)
246+
);
247+
}
172248
}

test/Patch/ForceStrictTypesTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace PhpSchool\PhpWorkshopTest\Patch;
4+
5+
use PhpParser\ParserFactory;
6+
use PhpParser\PrettyPrinter\Standard;
7+
use PhpSchool\PhpWorkshop\Patch\ForceStrictTypes;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class ForceStrictTypesTest extends TestCase
11+
{
12+
public function testStrictTypesDeclareIsAppended(): void
13+
{
14+
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
15+
$ast = $parser->parse("<?php echo 'Hello World';");
16+
17+
$transformer = new ForceStrictTypes();
18+
$ast = $transformer->transform($ast);
19+
20+
self::assertSame(
21+
"declare (strict_types=1);\necho 'Hello World';",
22+
(new Standard())->prettyPrint($ast)
23+
);
24+
}
25+
26+
public function testStrictTypesDeclareIsNotAppendedIfItAlreadyExists(): void
27+
{
28+
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
29+
$ast = $parser->parse("<?php declare (strict_types=1);\necho 'Hello World';");
30+
$transformer = new ForceStrictTypes();
31+
32+
$ast = $transformer->transform($ast);
33+
34+
self::assertSame(
35+
"declare (strict_types=1);\necho 'Hello World';",
36+
(new Standard())->prettyPrint($ast)
37+
);
38+
}
39+
}

test/PatchTest.php

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public function testWithInsertion(): void
1919
$this->assertEquals([$insertion], $new->getModifiers());
2020
}
2121

22-
public function testWithTransformer(): void
22+
public function testWithTransformerWithClosure(): void
2323
{
2424
$patch = new Patch();
2525
$transformer = function (array $statements) {
@@ -30,4 +30,42 @@ public function testWithTransformer(): void
3030
$this->assertEmpty($patch->getModifiers());
3131
$this->assertEquals([$transformer], $new->getModifiers());
3232
}
33+
34+
public function testWithTransformerWithTransformer(): void
35+
{
36+
$patch = new Patch();
37+
$transformer = new class implements Patch\Transformer {
38+
public function transform(array $ast): array
39+
{
40+
return $ast;
41+
}
42+
};
43+
44+
$new = $patch->withTransformer($transformer);
45+
$this->assertNotSame($patch, $new);
46+
$this->assertEmpty($patch->getModifiers());
47+
$this->assertEquals([$transformer], $new->getModifiers());
48+
}
49+
50+
public function testWithTransformerMultiple(): void
51+
{
52+
$transformer1 = new class implements Patch\Transformer {
53+
public function transform(array $ast): array
54+
{
55+
return $ast;
56+
}
57+
};
58+
$transformer2 = function (array $statements) {
59+
return $statements;
60+
};
61+
62+
$patch = new Patch();
63+
$new = $patch
64+
->withTransformer($transformer1)
65+
->withTransformer($transformer2);
66+
67+
$this->assertNotSame($patch, $new);
68+
$this->assertEmpty($patch->getModifiers());
69+
$this->assertEquals([$transformer1, $transformer2], $new->getModifiers());
70+
}
3371
}

0 commit comments

Comments
 (0)