Skip to content

Commit 5734433

Browse files
committed
added LinterExtension for validating filters, functions, classes and methods
1 parent 205e8d6 commit 5734433

File tree

4 files changed

+222
-0
lines changed

4 files changed

+222
-0
lines changed

src/Tools/Linter.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ private function createEngine(): Latte\Engine
8080
$engine->addExtension(new Nette\Bridges\AssetsLatte\LatteExtension(new Nette\Assets\Registry));
8181
}
8282

83+
$engine->addExtension(new LinterExtension);
84+
8385
return $engine;
8486
}
8587

src/Tools/LinterExtension.php

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Latte (https://latte.nette.org)
5+
* Copyright (c) 2008 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Latte\Tools;
11+
12+
use Latte;
13+
use Latte\Compiler\Node;
14+
use Latte\Compiler\Nodes\Php;
15+
use Latte\Compiler\Nodes\Php\Expression;
16+
use Latte\Compiler\Nodes\TemplateNode;
17+
use Latte\Compiler\NodeTraverser;
18+
use function defined;
19+
20+
21+
/**
22+
* Linter extension for validating filters, functions, classes, methods, constants and more.
23+
*/
24+
final class LinterExtension extends Latte\Extension
25+
{
26+
private ?Latte\Engine $engine = null;
27+
28+
29+
public function beforeCompile(Latte\Engine $engine): void
30+
{
31+
$this->engine = $engine;
32+
}
33+
34+
35+
public function getPasses(): array
36+
{
37+
return [
38+
'linter' => $this->linterPass(...),
39+
];
40+
}
41+
42+
43+
private function linterPass(TemplateNode $node): void
44+
{
45+
(new NodeTraverser)->traverse($node, function (Node $node) {
46+
if ($node instanceof Php\FilterNode) {
47+
$this->validateFilter($node);
48+
49+
} elseif ($node instanceof Expression\FunctionCallNode && $node->name instanceof Php\NameNode) {
50+
$this->validateFunction($node);
51+
52+
} elseif ($node instanceof Expression\NewNode && $node->class instanceof Php\NameNode) {
53+
$this->validateClass($node);
54+
55+
} elseif ($node instanceof Expression\StaticMethodCallNode
56+
&& $node->class instanceof Php\NameNode
57+
&& $node->name instanceof Php\IdentifierNode
58+
) {
59+
$this->validateStaticMethod($node);
60+
61+
} elseif ($node instanceof Expression\ClassConstantFetchNode
62+
&& $node->class instanceof Php\NameNode
63+
&& $node->name instanceof Php\IdentifierNode
64+
) {
65+
$this->validateClassConstant($node);
66+
67+
} elseif ($node instanceof Expression\ConstantFetchNode) {
68+
$this->validateConstant($node);
69+
70+
} elseif ($node instanceof Expression\InstanceofNode && $node->class instanceof Php\NameNode) {
71+
$this->validateInstanceof($node);
72+
73+
} elseif ($node instanceof Expression\StaticPropertyFetchNode
74+
&& $node->class instanceof Php\NameNode
75+
&& $node->name instanceof Php\VarLikeIdentifierNode
76+
) {
77+
$this->validateStaticProperty($node);
78+
}
79+
});
80+
}
81+
82+
83+
private function validateFilter(Php\FilterNode $node): void
84+
{
85+
$name = $node->name->name;
86+
$filters = $this->engine->getFilters();
87+
if (!isset($filters[$name])) {
88+
trigger_error("Unknown filter |$name $node->position", E_USER_WARNING);
89+
}
90+
}
91+
92+
93+
private function validateFunction(Expression\FunctionCallNode $node): void
94+
{
95+
$name = (string) $node->name;
96+
if (!function_exists($name)) {
97+
trigger_error("Unknown function $name() $node->position", E_USER_WARNING);
98+
}
99+
}
100+
101+
102+
private function validateClass(Expression\NewNode $node): void
103+
{
104+
$className = (string) $node->class;
105+
if (!class_exists($className) && !interface_exists($className)) {
106+
trigger_error("Unknown class $className $node->position", E_USER_WARNING);
107+
}
108+
}
109+
110+
111+
private function validateStaticMethod(Expression\StaticMethodCallNode $node): void
112+
{
113+
$className = (string) $node->class;
114+
$methodName = $node->name->name;
115+
if (!method_exists($className, $methodName)) {
116+
trigger_error("Unknown method $className::$methodName() $node->position", E_USER_WARNING);
117+
}
118+
}
119+
120+
121+
private function validateClassConstant(Expression\ClassConstantFetchNode $node): void
122+
{
123+
$name = "{$node->class}::{$node->name->name}";
124+
if (!defined($name)) {
125+
trigger_error("Unknown class constant $name $node->position", E_USER_WARNING);
126+
}
127+
}
128+
129+
130+
private function validateConstant(Expression\ConstantFetchNode $node): void
131+
{
132+
$magic = ['__LINE__' => 1, '__FILE__' => 1, '__DIR__' => 1];
133+
$name = (string) $node->name;
134+
if (!defined($name) && !isset($magic[$name])) {
135+
trigger_error("Unknown constant $name $node->position", E_USER_WARNING);
136+
}
137+
}
138+
139+
140+
private function validateInstanceof(Expression\InstanceofNode $node): void
141+
{
142+
$className = (string) $node->class;
143+
if (!class_exists($className) && !interface_exists($className)) {
144+
trigger_error("Unknown class $className in instanceof $node->position", E_USER_WARNING);
145+
}
146+
}
147+
148+
149+
private function validateStaticProperty(Expression\StaticPropertyFetchNode $node): void
150+
{
151+
$className = (string) $node->class;
152+
$propertyName = $node->name->name;
153+
if (!property_exists($className, $propertyName)) {
154+
trigger_error("Unknown static property $className::\$$propertyName $node->position", E_USER_WARNING);
155+
}
156+
}
157+
}

tests/linter/LinterExtension.phpt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Latte\Tools\LinterExtension;
6+
use Tester\Assert;
7+
8+
require __DIR__ . '/../bootstrap.php';
9+
10+
11+
test('detects unknown filters, functions, classes, methods, constants', function () {
12+
$warnings = [];
13+
set_error_handler(function (int $severity, string $message) use (&$warnings) {
14+
if ($severity === E_USER_WARNING) {
15+
$warnings[] = $message;
16+
return true;
17+
}
18+
return false;
19+
});
20+
21+
try {
22+
$latte = createLatte();
23+
$latte->addExtension(new LinterExtension);
24+
$latte->compile(file_get_contents(__DIR__ . '/templates/unknown.latte'));
25+
} finally {
26+
restore_error_handler();
27+
}
28+
29+
Assert::same([
30+
'Unknown filter |unknownFilter on line 13 at column 7',
31+
'Unknown function unknownFunction() on line 14 at column 3',
32+
'Unknown function unknownFunction() on line 15 at column 3',
33+
'Unknown class UnknownClass on line 16 at column 13',
34+
'Unknown method DateTime::unknownMethod() on line 17 at column 3',
35+
'Unknown method DateTime::unknownMethod() on line 18 at column 3',
36+
'Unknown class constant DateTime::UNKNOWN_CONSTANT on line 19 at column 3',
37+
'Unknown constant UNKNOWN_GLOBAL_CONSTANT on line 20 at column 3',
38+
'Unknown class UnknownClass in instanceof on line 21 at column 5',
39+
'Unknown static property DateTime::$unknownProperty on line 22 at column 3',
40+
], $warnings);
41+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{* Valid constructs - should pass *}
2+
{$text|upper}
3+
{=count([1, 2, 3])}
4+
{=count(...)}
5+
{var $dt = new DateTime()}
6+
{=DateTime::createFromFormat('Y-m-d', '2024-01-01')}
7+
{=DateTime::createFromFormat(...)}
8+
{=DateTime::ATOM}
9+
{=\PHP_VERSION}
10+
{if $x instanceof DateTime}ok{/if}
11+
12+
{* Invalid constructs - should warn *}
13+
{$text|unknownFilter}
14+
{=unknownFunction($value)}
15+
{=unknownFunction(...)}
16+
{var $obj = new UnknownClass()}
17+
{=DateTime::unknownMethod()}
18+
{=DateTime::unknownMethod(...)}
19+
{=DateTime::UNKNOWN_CONSTANT}
20+
{=\UNKNOWN_GLOBAL_CONSTANT}
21+
{if $x instanceof UnknownClass}bad{/if}
22+
{=DateTime::$unknownProperty}

0 commit comments

Comments
 (0)