Skip to content

Commit b7d4723

Browse files
committed
wip
1 parent 36565f1 commit b7d4723

File tree

16 files changed

+193
-66
lines changed

16 files changed

+193
-66
lines changed

phpstan.neon

Lines changed: 128 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ parameters:
1111
- src/Bridges/ApplicationLatte/UIMacros.php
1212
- src/Bridges/ApplicationLatte/SnippetBridge.php
1313

14+
fileExtensions:
15+
- php
16+
- phtml
17+
18+
# Cross-version compatible project (Latte 2/3, Tracy 2/3) — some rules only match with specific versions
19+
reportUnmatchedIgnoredErrors: false
20+
1421
ignoreErrors:
1522
# Latte 2/3 compatibility - classes/methods/properties differ between versions
1623
-
@@ -40,6 +47,43 @@ parameters:
4047
- property.nonObject
4148
- if.alwaysFalse
4249

50+
# Latte 2/3 compatibility - Template::addFilter(?string) and version_compare
51+
-
52+
path: src/Bridges/ApplicationLatte/Template.php
53+
identifiers:
54+
- argument.type
55+
- if.alwaysFalse
56+
57+
# Latte 3 node compatibility - properties/methods may differ between minor versions
58+
-
59+
paths:
60+
- src/Bridges/ApplicationLatte/Nodes/ControlNode.php
61+
- src/Bridges/ApplicationLatte/Nodes/LinkNode.php
62+
- src/Bridges/ApplicationLatte/Nodes/NNonceNode.php
63+
- src/Bridges/ApplicationLatte/Nodes/SnippetNode.php
64+
- src/Bridges/ApplicationLatte/Nodes/LinkBaseNode.php
65+
- src/Bridges/ApplicationLatte/Nodes/IfCurrentNode.php
66+
- src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php
67+
identifiers:
68+
- method.nonObject
69+
- property.nonObject
70+
- property.notFound
71+
- argument.type
72+
- variable.undefined
73+
- postDec.type
74+
- generator.valueType
75+
- booleanAnd.leftAlwaysFalse
76+
- function.alreadyNarrowedType
77+
78+
# SnippetRuntime: Latte Block class and Renderable interface compatibility
79+
-
80+
path: src/Bridges/ApplicationLatte/SnippetRuntime.php
81+
identifiers:
82+
- class.notFound
83+
- property.notFound
84+
- method.notFound
85+
- offsetAccess.nonArray
86+
4387
# Nette DI config access pattern - $config is array|object depending on DI version
4488
-
4589
identifier: property.nonObject
@@ -81,47 +125,104 @@ parameters:
81125
identifier: property.readOnlyAssignNotInConstructor
82126
path: src/Application/UI/Presenter.php
83127

84-
# Latte 3 node compatibility - properties/methods may differ between minor versions
128+
# Nette Forms compatibility - methods differ between versions
85129
-
130+
identifier: method.notFound
131+
path: src/Application/UI/Form.php
132+
133+
# TemplateFactory::createTemplate() accepts optional $class parameter via phpDoc
134+
-
135+
identifier: arguments.count
86136
paths:
87-
- src/Bridges/ApplicationLatte/Nodes/ControlNode.php
88-
- src/Bridges/ApplicationLatte/Nodes/LinkNode.php
89-
- src/Bridges/ApplicationLatte/Nodes/NNonceNode.php
90-
- src/Bridges/ApplicationLatte/Nodes/SnippetNode.php
91-
- src/Bridges/ApplicationLatte/Nodes/LinkBaseNode.php
92-
- src/Bridges/ApplicationLatte/Nodes/IfCurrentNode.php
93-
- src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php
137+
- src/Application/UI/Presenter.php
138+
- src/Application/UI/Control.php
139+
140+
# MicroPresenter uses duck typing for template objects and nullable container
141+
-
142+
path: src/Application/MicroPresenter.php
94143
identifiers:
144+
- argument.type
145+
- argument.templateType
146+
- method.notFound
95147
- method.nonObject
96-
- property.nonObject
97148
- property.notFound
98-
- argument.type
99-
- variable.undefined
100-
- postDec.type
101-
- generator.valueType
102-
- booleanAnd.leftAlwaysFalse
149+
- return.type
150+
151+
# PresenterFactory/Callback: `new $class` returns object, not IPresenter (verified at runtime)
152+
-
153+
identifier: return.type
154+
paths:
155+
- src/Application/PresenterFactory.php
156+
- src/Bridges/ApplicationDI/PresenterFactoryCallback.php
157+
158+
# PresenterFactory: defensive type checks after regex validation — always true but kept for safety
159+
-
160+
path: src/Application/PresenterFactory.php
161+
identifiers:
103162
- function.alreadyNarrowedType
163+
- booleanAnd.alwaysTrue
164+
- identical.alwaysTrue
104165

105-
# SnippetRuntime: Latte Block class and Renderable interface compatibility
166+
# Route/RouteList: defensive type checks — always true after prior validation
106167
-
107-
path: src/Bridges/ApplicationLatte/SnippetRuntime.php
168+
identifier: function.alreadyNarrowedType
169+
paths:
170+
- src/Application/Routers/Route.php
171+
- src/Application/Routers/RouteList.php
172+
-
173+
identifier: nullCoalesce.offset
174+
path: src/Application/Routers/Route.php
175+
176+
# ComponentReflection: cache reference pattern causes always-true/unreachable false positives
177+
-
178+
path: src/Application/UI/ComponentReflection.php
108179
identifiers:
109-
- class.notFound
110-
- property.notFound
180+
- notIdentical.alwaysTrue
181+
- deadCode.unreachable
182+
- booleanAnd.rightAlwaysTrue
183+
184+
# ComponentReflection: static methods exist on Presenter subclass, called via $class:: dynamic dispatch
185+
-
186+
path: src/Application/UI/ComponentReflection.php
187+
identifiers:
188+
- staticMethod.notFound
111189
- method.notFound
112-
- offsetAccess.nonArray
190+
- return.type
113191

114-
# Nette Forms compatibility - methods differ between versions
192+
# Control::$template->flashes is set dynamically for flash messages
115193
-
116-
identifier: method.notFound
117-
path: src/Application/UI/Form.php
194+
identifier: property.notFound
195+
path: src/Application/UI/Control.php
118196

119-
# TemplateFactory::createTemplate() accepts optional $class parameter via phpDoc
197+
# Presenter::detectedCsrf() redirect() throws AbortException, not InvalidLinkException
120198
-
121-
identifier: arguments.count
122-
paths:
123-
- src/Application/UI/Presenter.php
124-
- src/Application/UI/Control.php
199+
identifier: catch.neverThrown
200+
path: src/Application/UI/Presenter.php
201+
202+
# Exception::$code cannot have native type in override, phpDoc int differs from parent mixed
203+
-
204+
identifier: property.phpDocType
205+
path: src/Application/exceptions.php
206+
207+
# parse_str() native type is wider than actual query string result
208+
-
209+
identifier: varTag.nativeType
210+
path: src/Application/LinkGenerator.php
211+
212+
# Tracy version_compare conditions and BlueScreen closure parameter counts
213+
-
214+
path: src/Bridges/ApplicationDI/ApplicationExtension.php
215+
identifiers:
216+
- booleanAnd.rightAlwaysTrue
217+
- booleanAnd.leftAlwaysTrue
218+
- arguments.count
219+
220+
# RoutingPanel::analyse() return type and ReflectionClass with runtime class-string
221+
-
222+
path: src/Bridges/ApplicationTracy/RoutingPanel.php
223+
identifiers:
224+
- return.type
225+
- argument.type
125226

126227
includes:
127228
- phpstan-baseline.neon

src/Application/LinkGenerator.php

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function link(
4646
$parts = self::parseDestination($destination);
4747
$args = $parts['args'] ?? $args;
4848
$request = $this->createRequest($component, $parts['path'] . ($parts['signal'] ? '!' : ''), $args, $mode ?? 'link');
49-
$relative = $mode === 'link' && !$parts['absolute'] && !$component?->getPresenter()->absoluteUrls;
49+
$relative = $mode === 'link' && !$parts['absolute'] && !$component?->getPresenter()?->absoluteUrls;
5050
return $mode === 'forward' || $mode === 'test'
5151
? null
5252
: $this->requestToUrl($request, $relative) . $parts['fragment'];
@@ -76,7 +76,9 @@ public function createRequest(
7676
if (($component && !$component instanceof UI\Presenter) || str_ends_with($destination, '!')) {
7777
[$cname, $signal] = Helpers::splitName(rtrim($destination, '!'));
7878
if ($cname !== '') {
79+
assert($component !== null);
7980
$component = $component->getComponent(strtr($cname, ':', '-'));
81+
assert($component instanceof UI\Component);
8082
}
8183

8284
if ($signal === '') {
@@ -100,7 +102,7 @@ public function createRequest(
100102
throw new \LogicException("Presenter must be specified in '$destination'.");
101103
}
102104
$action = $path === 'this' ? $refPresenter->getAction() : $action;
103-
$presenter = $refPresenter->getName();
105+
$presenter = (string) $refPresenter->getName();
104106
$presenterClass = $refPresenter::class;
105107

106108
} else {
@@ -110,7 +112,7 @@ public function createRequest(
110112
throw new UI\InvalidLinkException("Missing presenter name in '$destination'.");
111113
}
112114
} elseif ($refPresenter) { // relative
113-
[$module, , $sep] = Helpers::splitName($refPresenter->getName());
115+
[$module, , $sep] = Helpers::splitName((string) $refPresenter->getName());
114116
$presenter = $module . $sep . $presenter;
115117
}
116118

@@ -123,6 +125,7 @@ public function createRequest(
123125

124126
// PROCESS SIGNAL ARGUMENTS
125127
if (isset($signal)) { // $component must be StatePersistent
128+
assert($component instanceof UI\Component);
126129
$reflection = new UI\ComponentReflection($component::class);
127130
if ($signal === 'this') { // means "no signal"
128131
$signal = '';
@@ -158,7 +161,7 @@ public function createRequest(
158161
}
159162

160163
// PROCESS ARGUMENTS
161-
if (is_subclass_of($presenterClass, UI\Presenter::class)) {
164+
if ($presenterClass !== null && is_subclass_of($presenterClass, UI\Presenter::class)) {
162165
if ($action === '') {
163166
$action = UI\Presenter::DefaultAction;
164167
}
@@ -178,7 +181,7 @@ public function createRequest(
178181
if ($method = $reflection->getActionRenderMethod($action)) {
179182
$this->validateLinkTarget($refPresenter, $method, "action '$presenter:$action'", $mode);
180183

181-
UI\ParameterConverter::toParameters($method, $args, $path === 'this' ? $refPresenter->getParameters() : [], $missing);
184+
UI\ParameterConverter::toParameters($method, $args, $path === 'this' && $refPresenter ? $refPresenter->getParameters() : [], $missing);
182185

183186
} elseif (array_key_exists(0, $args)) {
184187
throw new UI\InvalidLinkException("Unable to pass parameters to action '$presenter:$action', missing corresponding method $presenterClass::{$presenterClass::formatRenderMethod($action)}().");
@@ -218,12 +221,12 @@ public function createRequest(
218221
$args[UI\Presenter::ActionKey] = $action;
219222
}
220223

221-
if (!empty($signal)) {
224+
if (!empty($signal) && $component instanceof UI\Component) {
222225
$args[UI\Presenter::SignalKey] = $component->getParameterId($signal);
223-
$current = $current && $args[UI\Presenter::SignalKey] === $refPresenter->getParameter(UI\Presenter::SignalKey);
226+
$current = $current && $args[UI\Presenter::SignalKey] === $refPresenter?->getParameter(UI\Presenter::SignalKey);
224227
}
225228

226-
if (($mode === 'redirect' || $mode === 'forward') && $refPresenter->hasFlashSession()) {
229+
if (($mode === 'redirect' || $mode === 'forward') && $refPresenter && $refPresenter->hasFlashSession()) {
227230
$flashKey = $refPresenter->getParameter(UI\Presenter::FlashKey);
228231
$args[UI\Presenter::FlashKey] = is_string($flashKey) && $flashKey !== '' ? $flashKey : null;
229232
}
@@ -246,6 +249,7 @@ public static function parseDestination(string $destination): array
246249

247250
if (!empty($matches['query'])) {
248251
parse_str(substr($matches['query'], 1), $args);
252+
/** @var array<string, mixed> $args */
249253
}
250254

251255
return [
@@ -301,7 +305,7 @@ private function validateLinkTarget(
301305
): void
302306
{
303307
if ($mode !== 'forward' && !(new UI\AccessPolicy($element))->isLinkable()) {
304-
throw new UI\InvalidLinkException("Link to forbidden $message from '{$presenter->getName()}:{$presenter->getAction()}'.");
308+
throw new UI\InvalidLinkException("Link to forbidden $message from '{$presenter?->getName()}:{$presenter?->getAction()}'.");
305309
} elseif ($presenter?->invalidLinkMode
306310
&& (UI\ComponentReflection::parseAnnotation($element, 'deprecated') || $element->getAttributes(Attributes\Deprecated::class))
307311
) {

src/Application/Request.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public function getPost(?string $key = null): mixed
117117
{
118118
return func_num_args() === 0
119119
? $this->post
120-
: ($this->post[$key] ?? null);
120+
: ($this->post[(string) $key] ?? null);
121121
}
122122

123123

src/Application/Responses/FileResponse.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,22 +85,24 @@ public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $htt
8585
if (preg_match('#^bytes=(\d*)-(\d*)$#D', (string) $httpRequest->getHeader('Range'), $matches)) {
8686
[, $start, $end] = $matches;
8787
if ($start === '') {
88-
$start = max(0, $filesize - $end);
88+
$start = max(0, $filesize - (int) $end);
8989
$end = $filesize - 1;
9090

91-
} elseif ($end === '' || $end > $filesize - 1) {
91+
} elseif ($end === '' || (int) $end > $filesize - 1) {
9292
$end = $filesize - 1;
9393
}
9494

95-
if ($end < $start) {
95+
if ((int) $end < (int) $start) {
9696
$httpResponse->setCode(416); // requested range not satisfiable
9797
return;
9898
}
9999

100100
$httpResponse->setCode(206);
101+
$start = (int) $start;
102+
$end = (int) $end;
101103
$httpResponse->setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $filesize);
102104
$length = $end - $start + 1;
103-
fseek($handle, (int) $start);
105+
fseek($handle, $start);
104106

105107
} else {
106108
$httpResponse->setHeader('Content-Range', 'bytes 0-' . ($filesize - 1) . '/' . $filesize);
@@ -109,7 +111,7 @@ public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $htt
109111

110112
$httpResponse->setHeader('Content-Length', (string) $length);
111113
while (!feof($handle) && $length > 0) {
112-
$s = fread($handle, (int) min(4_000_000, $length));
114+
$s = fread($handle, max(1, (int) min(4_000_000, $length)));
113115
if ($s === false) {
114116
break;
115117
}

src/Application/UI/Component.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,10 +299,11 @@ public function link(string $destination, $args = []): string
299299
? $args
300300
: array_slice(func_get_args(), 1);
301301
$presenter = $this->getPresenter();
302+
assert($presenter !== null);
302303
return (string) $presenter->getLinkGenerator()->link($destination, $args, $this, 'link');
303304

304305
} catch (InvalidLinkException $e) {
305-
return $this->getPresenter()->handleInvalidLink($e);
306+
return $presenter->handleInvalidLink($e);
306307
}
307308
}
308309

@@ -330,6 +331,7 @@ public function lazyLink(string $destination, $args = []): Link
330331
public function isLinkCurrent(?string $destination = null, $args = []): bool
331332
{
332333
$presenter = $this->getPresenter();
334+
assert($presenter !== null);
333335
if ($destination !== null) {
334336
$args = func_num_args() < 3 && is_array($args)
335337
? $args
@@ -354,6 +356,7 @@ public function redirect(string $destination, $args = []): void
354356
? $args
355357
: array_slice(func_get_args(), 1);
356358
$presenter = $this->getPresenter();
359+
assert($presenter !== null);
357360
$presenter->saveGlobalState();
358361
$presenter->redirectUrl((string) $presenter->getLinkGenerator()->link($destination, $args, $this, 'redirect'));
359362
}
@@ -372,6 +375,7 @@ public function redirectPermanent(string $destination, $args = []): void
372375
? $args
373376
: array_slice(func_get_args(), 1);
374377
$presenter = $this->getPresenter();
378+
assert($presenter !== null);
375379
$presenter->redirectUrl(
376380
(string) $presenter->getLinkGenerator()->link($destination, $args, $this, 'redirect'),
377381
Nette\Http\IResponse::S301_MovedPermanently,

src/Application/UI/Control.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ final public function getTemplate(): Template
5454
protected function createTemplate(?string $class = null): Template
5555
{
5656
$class ??= $this->formatTemplateClass();
57-
$templateFactory = $this->templateFactory ?? $this->getPresenter()->getTemplateFactory();
57+
$presenter = $this->getPresenter();
58+
assert($presenter !== null);
59+
$templateFactory = $this->templateFactory ?? $presenter->getTemplateFactory();
5860
return $templateFactory->createTemplate($this, $class);
5961
}
6062

@@ -103,14 +105,16 @@ public function templatePrepareFilters(Template $template): void
103105
public function flashMessage(string|\stdClass|\Stringable $message, string $type = 'info'): \stdClass
104106
{
105107
$id = $this->getParameterId('flash');
108+
$presenter = $this->getPresenter();
109+
assert($presenter !== null);
106110
$flash = $message instanceof \stdClass ? $message : (object) [
107111
'message' => $message,
108112
'type' => $type,
109113
];
110-
$messages = $this->getPresenter()->getFlashSession()->get($id);
114+
$messages = $presenter->getFlashSession()->get($id);
111115
$messages[] = $flash;
112116
$this->getTemplate()->flashes = $messages;
113-
$this->getPresenter()->getFlashSession()->set($id, $messages);
117+
$presenter->getFlashSession()->set($id, $messages);
114118
return $flash;
115119
}
116120

0 commit comments

Comments
 (0)