8000 feature #21465 [Debug] Support `@final` on methods (GuilhemN) · symfony/symfony@6d3d17d · GitHub
[go: up one dir, main page]

Skip to content

Commit 6d3d17d

Browse files
committed
feature #21465 [Debug] Support @final on methods (GuilhemN)
This PR was squashed before being merged into the 3.3-dev branch (closes #21465). Discussion ---------- [Debug] Support `@final` on methods | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #21461 (comment) | License | MIT | Doc PR | This adds support for `@final` on methods: ```php class Foo { /** * @Final since version 2.0. */ public function bar() { } } ``` ping @nicolas-grekas Commits ------- 1b0a6b6 [Debug] Support on methods
2 parents 033c41a + 1b0a6b6 commit 6d3d17d

File tree

6 files changed

+94
-70
lines changed

6 files changed

+94
-70
lines changed

src/Symfony/Component/Debug/DebugClassLoader.php

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class DebugClassLoader
2828
private $isFinder;
2929
private static $caseCheck;
3030
private static $final = array();
31+
private static $finalMethods = array();
3132
private static $deprecated = array();
3233
private static $php7Reserved = array('int', 'float', 'bool', 'string', 'true', 'false', 'null');
3334
private static $darwinCache = array('/' => array('/', array()));
@@ -164,13 +165,40 @@ public function loadClass($class)
164165
throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: %s vs %s', $class, $name));
165166
}
166167

167-
if (preg_match('#\n \* @final(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) {
168-
self::$final[$name] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
169-
}
170-
171168
$parent = get_parent_class($class);
172-
if ($parent && isset(self::$final[$parent])) {
173-
@trigger_error(sprintf('The %s class is considered final%s. It may change without further notice as of its next major version. You should not extend it from %s.', $parent, self::$final[$parent], $name), E_USER_DEPRECATED);
169+
170+
// Not an interface nor a trait
171+
if (class_exists($name, false)) {
172+
if (preg_match('#\n \* @final(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) {
173+
self::$final[$name] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
174+
}
175+
176+
if ($parent && isset(self::$final[$parent])) {
177+
@trigger_error(sprintf('The %s class is considered final%s. It may change without further notice as of its next major version. You should not extend it from %s.', $parent, self::$final[$parent], $name), E_USER_DEPRECATED);
178+
}
179+
180+
// Inherit @final annotations
181+
self::$finalMethods[$name] = $parent && isset(self::$finalMethods[$parent]) ? self::$finalMethods[$parent] : array();
182+
183+
foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
184+
if ($method->class !== $name) {
185+
continue;
186+
}
187+
188+
if ($parent && isset(self::$finalMethods[$parent][$method->name])) {
189+
@trigger_error(sprintf('%s It may change without further notice as of its next major version. You should not extend it from %s.', self::$finalMethods[$parent][$method->name], $name), E_USER_DEPRECATED);
190+
}
191+
192+
$doc = $method->getDocComment();
193+
if (false === $doc || false === strpos($doc, '@final')) {
194+
continue;
195+
}
196+
197+
if (preg_match('#\n\s+\* @final(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) {
198+
$message = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
199+
self::$finalMethods[$name][$method->name] = sprintf('The %s::%s() method is considered final%s.', $name, $method->name, $message);
200+
}
201+
}
174202
}
175203

176204
if (in_array(strtolower($refl->getShortName()), self::$php7Reserved)) {

src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,28 @@ class_exists('Test\\'.__NAMESPACE__.'\\ExtendsFinalClass', true);
289289

290290
$this->assertSame($xError, $lastError);
291291
}
292+
293+
public function testExtendedFinalMethod()
294+
{
295+
set_error_handler(function () { return false; });
296+
$e = error_reporting(0);
297+
trigger_error('', E_USER_NOTICE);
298+
299+
class_exists(__NAMESPACE__.'\\Fixtures\\ExtendedFinalMethod', true);
300+
301+
error_reporting($e);
302+
restore_error_handler();
303+
304+
$lastError = error_get_last();
305+
unset($lastError['file'], $lastError['line']);
306+
307+
$xError = array(
308+
'type' => E_USER_DEPRECATED,
309+
'message' => 'The Symfony\Component\Debug\Tests\Fixtures\FinalMethod::finalMethod() method is considered final since version 3.3. It may change without further notice as of its next major version. You should not extend it from Symfony\Component\Debug\Tests\Fixtures\ExtendedFinalMethod.',
310+
);
311+
312+
$this->assertSame($xError, $lastError);
313+
}
292314
}
293315

294316
class ClassLoader
@@ -324,6 +346,10 @@ public function findFile($class)
324346
return $fixtureDir.'DeprecatedInterface.php';
325347
} elseif (__NAMESPACE__.'\Fixtures\FinalClass' === $class) {
326348
return $fixtureDir.'FinalClass.php';
349+
} elseif (__NAMESPACE__.'\Fixtures\FinalMethod' === $class) {
350+
return $fixtureDir.'FinalMethod.php';
351+
} elseif (__NAMESPACE__.'\Fixtures\ExtendedFinalMethod' === $class) {
352+
return $fixtureDir.'ExtendedFinalMethod.php';
327353
} elseif ('Symfony\Bridge\Debug\Tests\Fixtures\ExtendsDeprecatedParent' === $class) {
328354
eval('namespace Symfony\Bridge\Debug\Tests\Fixtures; class ExtendsDeprecatedParent extends \\'.__NAMESPACE__.'\Fixtures\DeprecatedClass {}');
329355
} elseif ('Test\\'.__NAMESPACE__.'\DeprecatedParentClass' === $class) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
class ExtendedFinalMethod extends FinalMethod
6+
{
7+
/**
8+
* {@inheritdoc}
9+
*/
10+
public function finalMethod()
11+
{
12+
}
13+
14+
public function anotherMethod()
15+
{
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
class FinalMethod
6+
{
7+
/**
8+
* @final since version 3.3.
9+
*/
10+
public function finalMethod()
11+
{
12+
}
13+
14+
public function anotherMethod()
15+
{
16+
}
17+
}

src/Symfony/Component/HttpFoundation/Response.php

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -186,34 +186,6 @@ class Response
186186
511 => 'Network Authentication Required', // RFC6585
187187
);
188188

189-
private static $deprecatedMethods = array(
190-
'setDate', 'getDate',
191-
'setExpires', 'getExpires',
192-
'setLastModified', 'getLastModified',
193-
'setProtocolVersion', 'getProtocolVersion',
194-
'setStatusCode', 'getStatusCode',
195-
'setCharset', 'getCharset',
196-
'setPrivate', 'setPublic',
197-
'getAge', 'getMaxAge', 'setMaxAge', 'setSharedMaxAge',
198-
'getTtl', 'setTtl', 'setClientTtl',
199-
'getEtag', 'setEtag',
200-
'hasVary', 'getVary', 'setVary',
201-
'isInvalid', 'isSuccessful', 'isRedirection',
202-
'isClientError', 'isOk', 'isForbidden',
203-
'isNotFound', 'isRedirect', 'isEmpty',
204-
'isCacheable', 'isFresh', 'isValidateable',
205-
'mustRevalidate', 'setCache', 'setNotModified',
206-
'isNotModified', 'isInformational', 'isServerError',
207-
'closeOutputBuffers', 'ensureIEOverSSLCompatibility',
208-
);
209-
private static $deprecationsTriggered = array(
210-
__CLASS__ => true,
211-
BinaryFileResponse::class => true,
212-
JsonResponse::class => true,
213-
RedirectResponse::class => true,
214-
StreamedResponse::class => true,
215-
);
216-
217189
/**
218190
* Constructor.
219191
*
@@ -229,23 +201,6 @@ public function __construct($content = '', $status = 200, $headers = array())
229201
$this->setContent($content);
230202
$this->setStatusCode($status);
231203
$this->setProtocolVersion('1.0');
232-
233-
// Deprecations
234-
$class = get_class($this);
235-
if ($this instanceof \PHPUnit_Framework_MockObject_MockObject || $this instanceof \Prophecy\Doubler\DoubleInterface) {
236-
$class = get_parent_class($class);
237-
}
238-
if (isset(self::$deprecationsTriggered[$class])) {
239-
return;
240-
}
241-
242-
self::$deprecationsTriggered[$class] = true;
243-
foreach (self::$deprecatedMethods as $method) {
244-
$r = new \ReflectionMethod($class, $method);
245-
if (__CLASS__ !== $r->getDeclaringClass()->getName()) {
246-
@trigger_error(sprintf('Extending %s::%s() in %s is deprecated since version 3.2 and won\'t be supported anymore in 4.0 as it will be final.', __CLASS__, $method, $class), E_USER_DEPRECATED);
247-
}
248-
}
249204
}
250205

251206
/**

src/Symfony/Component/HttpFoundation/Tests/ResponseTest.php

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -843,25 +843,6 @@ public function testSettersAreChainable()
843843
}
844844
}
845845

846-
public function testNoDeprecationsAreTriggered()
847-
{
848-
new DefaultResponse();
849-
$this->getMockBuilder(Response::class)->getMock();
850-
}
851-
852-
/**
853-
* @group legacy
854-
* @expectedDeprecation Extending Symfony\Component\HttpFoundation\Response::getDate() in Symfony\Component\HttpFoundation\Tests\ExtendedResponse is deprecated %s.
855-
* @expectedDeprecation Extending Symfony\Component\HttpFoundation\Response::setLastModified() in Symfony\Component\HttpFoundation\Tests\ExtendedResponse is deprecated %s.
856-
*/
857-
public function testDeprecations()
858-
{
859-
new ExtendedResponse();
860-
861-
// Deprecations should not be triggered twice
862-
new ExtendedResponse();
863-
}
864-
865846
public function validContentProvider()
866847
{
867848
return array(

0 commit comments

Comments
 (0)
0