Skip to content

Commit 3533521

Browse files
Merge branch 'main' into multiline-block
2 parents 4c059a4 + 81d2fe1 commit 3533521

File tree

19 files changed

+251
-35
lines changed

19 files changed

+251
-35
lines changed

.github/workflows/composer.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ jobs:
1515
fail-fast: false
1616
matrix:
1717
php: [ '8.0', '8.1', '8.2', '8.3' ]
18-
outdated-args: [ '--ignore=phpunit/phpunit' ]
18+
outdated-args: ['--ignore=phpunit/phpunit --ignore=nikic/php-parser']
1919
include:
2020
- php: '7.4'
21-
outdated-args: '--ignore=nette/finder --ignore=phpunit/phpunit'
21+
outdated-args: '--ignore=nette/finder --ignore=phpunit/phpunit --ignore=nikic/php-parser'
2222

2323
name: Composer outdated - PHP ${{ matrix.php }}
2424

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,19 @@
22

33
## [Unreleased][unreleased]
44

5+
## [0.17.1] - 2024-07-18
6+
### Updated
7+
- Coding standard
8+
### Fixed
9+
- RelatedFilesCollector collecting classes that were not present in vendor
10+
11+
## [0.17.0] - 2024-03-27
512
### Fixed
613
- Updated coding standard (Possible **BC break** - added `final` or `abstract` to (almost) all classes)
714
- Bleeding edge changes - updated typehints
815
- Fixed parameters parsing for multiline {define}
16+
- Functions handling with FunctionExecutor in new Latte
17+
- Removed unformatPresenterClass of new PresenterFactory
918

1019
## [0.16.3] - 2023-11-26
1120
### Added
@@ -247,7 +256,9 @@
247256
- Transform components to explicit calls
248257
- Error mapper for better DX
249258

250-
[unreleased]: https://github.com/efabrica-team/phpstan-latte/compare/0.16.3...HEAD
259+
[unreleased]: https://github.com/efabrica-team/phpstan-latte/compare/0.17.1...HEAD
260+
[0.17.1]: https://github.com/efabrica-team/phpstan-latte/compare/0.17.0...0.17.1
261+
[0.17.0]: https://github.com/efabrica-team/phpstan-latte/compare/0.16.3...0.17.0
251262
[0.16.3]: https://github.com/efabrica-team/phpstan-latte/compare/0.16.2...0.16.3
252263
[0.16.2]: https://github.com/efabrica-team/phpstan-latte/compare/0.16.1...0.16.2
253264
[0.16.1]: https://github.com/efabrica-team/phpstan-latte/compare/0.16.0...0.16.1

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
"phpunit/phpunit": "^9.5",
1818
"nette/application": "^3.1.6",
1919
"nette/forms": "^3.1.12",
20-
"nikic/php-parser": "^4.15",
21-
"efabrica/coding-standard": "^0.6",
20+
"nikic/php-parser": "^4.19",
21+
"efabrica/coding-standard": "^0.7",
2222
"phpstan/phpstan-strict-rules": "^1.4",
2323
"spaze/phpstan-disallowed-calls": "^2.11 | ^3.0"
2424
},

src/Compiler/Compiler/AbstractCompiler.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public function __construct(?Engine $engine = null, bool $strictMode = false, ar
4242
$this->strictMode = $strictMode;
4343
$this->filters = $filters;
4444
$this->functions = $functions;
45+
$this->installFunctions($functions);
4546
}
4647

4748
public function generateClassName(): string
@@ -95,5 +96,22 @@ protected function addTypes(string $phpContent, string $className, ?string $actu
9596
return $phpContent;
9697
}
9798

99+
/**
100+
* @param array<string, string|array{string, string}> $functions
101+
*/
102+
private function installFunctions(array $functions): void
103+
{
104+
foreach ($functions as $name => $function) {
105+
if (is_callable($function)) {
106+
$this->engine->addFunction($name, $function);
107+
} else {
108+
// add placeholder function
109+
$this->engine->addFunction($name, function (...$args) {
110+
return null;
111+
});
112+
}
113+
}
114+
}
115+
98116
abstract protected function createDefaultEngine(): Engine;
99117
}

src/Compiler/Compiler/Latte2Compiler.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function compile(string $templateContent, ?string $actualClass, string $c
5050
$latteTokens = $parser->parse($templateContent);
5151
$className = $this->generateClassName();
5252
$phpContent = $compiler
53-
->setFunctions(array_keys($this->getDefaultFunctions()))
53+
->setFunctions(array_keys($this->getFunctions()))
5454
->compile(
5555
$latteTokens,
5656
$className,
@@ -82,7 +82,12 @@ public function getFilters(): array
8282

8383
public function getFunctions(): array
8484
{
85-
return array_merge($this->getDefaultFunctions(), $this->functions);
85+
try {
86+
$engineFunctions = $this->getEngineFunctionsByReflection();
87+
} catch (ReflectionException $e) {
88+
$engineFunctions = $this->getDefaultFunctions();
89+
}
90+
return array_change_key_case(array_merge($engineFunctions, $this->functions));
8691
}
8792

8893
/**
@@ -109,6 +114,25 @@ private function getEngineFiltersByReflection(): array
109114
return $engineFilters;
110115
}
111116

117+
/**
118+
* @return array<string, callable>
119+
* @throws ReflectionException
120+
*/
121+
private function getEngineFunctionsByReflection(): array
122+
{
123+
// we must use try to use reflection to get to filter signatures in Latte 2 :-(
124+
$engineFunctionsPropertyReflection = (new ReflectionClass($this->engine))->getProperty('functions');
125+
$engineFunctionsPropertyReflection->setAccessible(true);
126+
/** @var array<string, callable> $engineFunctionsProperty */
127+
$engineFunctionsProperty = $engineFunctionsPropertyReflection->getValue($this->engine);
128+
129+
$engineFunctions = [];
130+
foreach ($engineFunctionsProperty as $functionName => $callable) {
131+
$engineFunctions[strtolower($functionName)] = $callable;
132+
}
133+
return $engineFunctions;
134+
}
135+
112136
/**
113137
* @return array<string, string|array{string, string}|callable>
114138
*/

src/Compiler/NodeVisitor/AddFormClassesNodeVisitor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,7 @@ private function createClassNode(string $parentClassName, ControlHolderInterface
510510
->setReturnType('?Nette\Forms\ControlGroup');
511511

512512
$getGroupComment = $this->createGetGroupConditionalReturnTypeComment($controlHolder->getGroups());
513-
$getGroupMethod->setDocComment('/** ' . $getGroupComment . ' */');
513+
$getGroupMethod->setDocComment('/** @param int|string $name' . "\n" . $getGroupComment . ' */');
514514
$methods[] = $getGroupMethod;
515515
}
516516

src/Compiler/NodeVisitor/ChangeFunctionsNodeVisitor.php

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Efabrica\PHPStanLatte\Compiler\NodeVisitor\Behavior\ScopeNodeVisitorInterface;
1515
use Efabrica\PHPStanLatte\Resolver\NameResolver\NameResolver;
1616
use Efabrica\PHPStanLatte\Template\Variable;
17+
use Latte\Runtime\Template;
1718
use PhpParser\Comment\Doc;
1819
use PhpParser\Node;
1920
use PhpParser\Node\Arg;
@@ -34,6 +35,11 @@
3435
use PhpParser\Node\Stmt\Expression;
3536
use PhpParser\Node\VariadicPlaceholder;
3637
use PhpParser\NodeVisitorAbstract;
38+
use PHPStan\BetterReflection\BetterReflection;
39+
use PHPStan\BetterReflection\Reflection\ReflectionFunction as BetterReflectionFunction;
40+
use PHPStan\BetterReflection\Reflection\ReflectionMethod as BetterReflectionMethod;
41+
use PHPStan\BetterReflection\Reflection\ReflectionNamedType as BetterReflectionNamedType;
42+
use PHPStan\BetterReflection\Reflection\ReflectionParameter as BetterReflectionParameter;
3743
use PHPStan\Broker\ClassNotFoundException;
3844
use PHPStan\PhpDoc\TypeStringResolver;
3945
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
@@ -44,6 +50,9 @@
4450
use PHPStan\Type\ClosureTypeFactory;
4551
use PHPStan\Type\ObjectType;
4652
use PHPStan\Type\ThisType;
53+
use ReflectionFunction;
54+
use ReflectionNamedType;
55+
use ReflectionParameter;
4756

4857
final class ChangeFunctionsNodeVisitor extends NodeVisitorAbstract implements FunctionsNodeVisitorInterface, ExprTypeNodeVisitorInterface, ScopeNodeVisitorInterface
4958
{
@@ -237,13 +246,17 @@ private function createFunctionCallNode(string $functionName, array $args): ?Nod
237246
}
238247

239248
if ($function instanceof Closure || $this->isCallableString($function)) {
249+
if ($function instanceof Closure) {
250+
$args = $this->updateArgs(new ReflectionFunction($function), $args);
251+
}
240252
return new FuncCall(new VariableExpr($this->createFunctionVariableName($functionName)), $args);
241253
}
242254

243255
if (is_string($function)) {
244256
if (str_contains($function, '::')) {
245257
$function = explode('::', $function);
246258
} else {
259+
$args = $this->updateArgs((new BetterReflection())->reflector()->reflectFunction($function), $args);
247260
return new FuncCall(new FullyQualified($function), $args);
248261
}
249262
}
@@ -257,19 +270,15 @@ private function createFunctionCallNode(string $functionName, array $args): ?Nod
257270
/** @var non-empty-string $methodName */
258271
$methodName = $function[1];
259272

260-
try {
261-
$classReflection = $this->reflectionProvider->getClass($className);
262-
} catch (ClassNotFoundException $e) {
263-
return null;
264-
}
273+
$reflectionClass = (new BetterReflection())->reflector()->reflectClass($className);
274+
$reflectionMethod = $reflectionClass->getMethod($methodName);
265275

266-
try {
267-
$methodReflection = $classReflection->getMethod($methodName, $this->getScope());
268-
} catch (MissingMethodFromReflectionException $e) {
276+
if ($reflectionMethod === null) {
269277
return null;
270278
}
271279

272-
if ($methodReflection->isStatic()) {
280+
$args = $this->updateArgs($reflectionMethod, $args);
281+
if ($reflectionMethod->isStatic()) {
273282
return new StaticCall(
274283
new FullyQualified($className),
275284
new Identifier($methodName),
@@ -284,4 +293,30 @@ private function createFunctionCallNode(string $functionName, array $args): ?Nod
284293
$args
285294
);
286295
}
296+
297+
/**
298+
* @param BetterReflectionFunction|BetterReflectionMethod|ReflectionFunction $reflection
299+
* @param Arg[]|VariadicPlaceholder[] $args
300+
* @return Arg[]|VariadicPlaceholder[]
301+
*/
302+
private function updateArgs($reflection, array $args): array
303+
{
304+
$parameter = $reflection->getParameters()[0] ?? null;
305+
if ($parameter instanceof BetterReflectionParameter && $parameter->getType() instanceof BetterReflectionNamedType) {
306+
$parameterType = $parameter->getType()->getName();
307+
} elseif ($parameter instanceof ReflectionParameter && $parameter->getType() instanceof ReflectionNamedType) {
308+
$parameterType = $parameter->getType()->getName();
309+
} else {
310+
$parameterType = null;
311+
}
312+
if ($parameterType !== Template::class &&
313+
isset($args[0]) &&
314+
$args[0] instanceof Arg &&
315+
$args[0]->value instanceof VariableExpr &&
316+
$args[0]->value->name === 'this'
317+
) {
318+
$args = array_slice($args, 1);
319+
}
320+
return $args;
321+
}
287322
}

src/LatteContext/Collector/RelatedFilesCollector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public function collectData(Node $node, Scope $scope): array
8989
$newClassNames = [$this->nameResolver->resolve($node->class)];
9090
}
9191
foreach ($newClassNames as $newClassName) {
92-
if ($newClassName !== null && !in_array($newClassName, ['this', 'self', 'static', 'parent'], true)) {
92+
if ($newClassName !== null && !in_array($newClassName, ['this', 'self', 'static', 'parent'], true) && class_exists($newClassName)) {
9393
$classReflection = $this->reflectionProvider->getClass($newClassName);
9494
if (!$classReflection->isInterface() && !$classReflection->isTrait()) {
9595
$filename = $this->getFilename($classReflection);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Efabrica\PHPStanLatte\LinkProcessor;
6+
7+
use Nette\Application\PresenterFactory;
8+
use Nette\InvalidStateException;
9+
10+
final class FakePresenterFactory extends PresenterFactory
11+
{
12+
/** @var array<string, array{string, string, string}> */
13+
private array $mapping = [];
14+
15+
/**
16+
* @param array<string, string|array{string, string}|array{string, string, string}> $mapping
17+
*/
18+
public function setFakeMapping(array $mapping): FakePresenterFactory
19+
{
20+
parent::setMapping($mapping);
21+
foreach ($mapping as $module => $mask) {
22+
if (is_string($mask)) {
23+
if (!preg_match('#^\\\\?([\w\\\\]*\\\\)?(\w*\*\w*?\\\\)?([\w\\\\]*\*\w*)$#D', $mask, $m)) {
24+
throw new InvalidStateException("Invalid mapping mask '$mask'.");
25+
}
26+
27+
$this->mapping[$module] = [$m[1], $m[2] ?: '*Module\\', $m[3]];
28+
} elseif (is_array($mask) && count($mask) === 3) {
29+
$this->mapping[$module] = [$mask[0] ? $mask[0] . '\\' : '', $mask[1] . '\\', $mask[2]];
30+
} else {
31+
throw new InvalidStateException("Invalid mapping mask for module $module.");
32+
}
33+
}
34+
35+
return $this;
36+
}
37+
38+
public function unformatPresenterClass(string $class): ?string
39+
{
40+
foreach ($this->mapping as $module => $mapping) {
41+
$mapping = str_replace(['\\', '*'], ['\\\\', '(\w+)'], $mapping);
42+
if (preg_match('#^\\\\?' . $mapping[0] . '((?:' . $mapping[1] . ')*)' . $mapping[2] . '$#Di', $class, $matches) !== 1) {
43+
continue;
44+
}
45+
46+
return ($module === '*' ? '' : $module . ':')
47+
. preg_replace('#' . $mapping[1] . '#iA', '$1:', $matches[1]) . $matches[3];
48+
}
49+
return null;
50+
}
51+
}

src/LinkProcessor/PresenterActionLinkProcessor.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ public function createLinkExpressions(string $targetName, array $linkParams, arr
7777
if ($this->actualClass === null) {
7878
return [];
7979
}
80+
if (!is_callable([$presenterFactory, 'unformatPresenterClass'])) {
81+
throw new LinkProcessorException('PresenterFactory does not have method unformatPresenterClass');
82+
}
8083
$actualClass = @$presenterFactory->unformatPresenterClass($this->actualClass);
8184
if ($actualClass === null) {
8285
return [];

0 commit comments

Comments
 (0)