Skip to content

[PhpUnitBridge]allow outdated vendors mode #24867

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 108 additions & 34 deletions src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,22 @@ class DeprecationErrorHandler
{
const MODE_WEAK = 'weak';
const MODE_WEAK_VENDORS = 'weak_vendors';
const MODE_ALLOW_OUTDATED_VENDORS = 'allow_outdated_vendors';
const MODE_DISABLED = 'disabled';

private static $isRegistered = false;

/** @var string[] absolute paths to vendor directories */
private static $vendors;

/**
* Registers and configures the deprecation handler.
*
* The following reporting modes are supported:
* - use "weak" to hide the deprecation report but keep a global count;
* - use "weak_vendors" to act as "weak" but only for vendors;
* - use "allow_outdated_vendors" to act as "weak" but only for vendors that
* failed to keep up with their upstream dependencies deprecations;
* - use "/some-regexp/" to stop the test suite whenever a deprecation
* message matches the given regular expression;
* - use a number to define the upper bound of allowed deprecations,
Expand All @@ -54,53 +60,32 @@ public static function register($mode = 0)
if (false === $mode) {
$mode = getenv('SYMFONY_DEPRECATIONS_HELPER');
}
if (DeprecationErrorHandler::MODE_WEAK !== $mode && DeprecationErrorHandler::MODE_WEAK_VENDORS !== $mode && (!isset($mode[0]) || '/' !== $mode[0])) {
if (!in_array($mode, array(
DeprecationErrorHandler::MODE_WEAK,
DeprecationErrorHandler::MODE_WEAK_VENDORS,
DeprecationErrorHandler::MODE_ALLOW_OUTDATED_VENDORS,
), true) && (!isset($mode[0]) || '/' !== $mode[0])) {
$mode = preg_match('/^[1-9][0-9]*$/', $mode) ? (int) $mode : 0;
}

return $memoizedMode = $mode;
};

$inVendors = function ($path) {
/** @var string[] absolute paths to vendor directories */
static $vendors;
if (null === $vendors) {
foreach (get_declared_classes() as $class) {
if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) {
$r = new \ReflectionClass($class);
$v = dirname(dirname($r->getFileName()));
if (file_exists($v.'/composer/installed.json')) {
$vendors[] = $v;
}
}
}
}
$realPath = realpath($path);
if (false === $realPath && '-' !== $path && 'Standard input code' !== $path) {
return true;
}
foreach ($vendors as $vendor) {
if (0 === strpos($realPath, $vendor) && false !== strpbrk(substr($realPath, strlen($vendor), 1), '/'.DIRECTORY_SEPARATOR)) {
return true;
}
}

return false;
};

$deprecations = array(
'unsilencedCount' => 0,
'remainingCount' => 0,
'legacyCount' => 0,
'otherCount' => 0,
'outdated vendorCount' => 0,
'remaining vendorCount' => 0,
'unsilenced' => array(),
'remaining' => array(),
'legacy' => array(),
'other' => array(),
'outdated vendor' => array(),
'remaining vendor' => array(),
);
$deprecationHandler = function ($type, $msg, $file, $line, $context = array()) use (&$deprecations, $getMode, $UtilPrefix, $inVendors) {
$deprecationHandler = function ($type, $msg, $file, $line, $context = array()) use (&$deprecations, $getMode, $UtilPrefix) {
$mode = $getMode();
if ((E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) || DeprecationErrorHandler::MODE_DISABLED === $mode) {
$ErrorHandler = $UtilPrefix.'ErrorHandler';
Expand All @@ -111,8 +96,9 @@ public static function register($mode = 0)
$trace = debug_backtrace(true);
$group = 'other';
$isVendor = false;
$isWeak = DeprecationErrorHandler::MODE_WEAK === $mode || (DeprecationErrorHandler::MODE_WEAK_VENDORS === $mode && $isVendor = $inVendors($file));

$isWeak = DeprecationErrorHandler::MODE_WEAK === $mode ||
(DeprecationErrorHandler::MODE_WEAK_VENDORS === $mode && $isVendor = self::inVendors($file)) ||
(DeprecationErrorHandler::MODE_ALLOW_OUTDATED_VENDORS === $mode && $isOutdatedVendor = self::isOutdatedVendor($trace));
$i = count($trace);
while (1 < $i && (!isset($trace[--$i]['class']) || ('ReflectionMethod' === $trace[$i]['class'] || 0 === strpos($trace[$i]['class'], 'PHPUnit_') || 0 === strpos($trace[$i]['class'], 'PHPUnit\\')))) {
// No-op
Expand All @@ -128,7 +114,9 @@ public static function register($mode = 0)
// \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest()
// then we need to use the serialized information to determine
// if the error has been triggered from vendor code.
$isWeak = DeprecationErrorHandler::MODE_WEAK === $mode || (DeprecationErrorHandler::MODE_WEAK_VENDORS === $mode && $isVendor = isset($parsedMsg['triggering_file']) && $inVendors($parsedMsg['triggering_file']));
$isWeak = DeprecationErrorHandler::MODE_WEAK === $mode ||
(DeprecationErrorHandler::MODE_WEAK_VENDORS === $mode && $isVendor = isset($parsedMsg['triggering_file']) && self::inVendors($parsedMsg['triggering_file'])) ||
(DeprecationErrorHandler::MODE_ALLOW_OUTDATED_VENDORS === $mode) && $isOutdatedVendor = isset($parsedMsg['trace']) && self::isOutdatedVendor($parsedMsg['trace']);
} else {
$class = isset($trace[$i]['object']) ? get_class($trace[$i]['object']) : $trace[$i]['class'];
$method = $trace[$i]['function'];
Expand All @@ -145,6 +133,8 @@ public static function register($mode = 0)
|| in_array('legacy', $Test::getGroups($class, $method), true)
) {
$group = 'legacy';
} elseif (DeprecationErrorHandler::MODE_ALLOW_OUTDATED_VENDORS === $mode && $isOutdatedVendor) {
$group = 'outdated vendor';
} elseif (DeprecationErrorHandler::MODE_WEAK_VENDORS === $mode && $isVendor) {
$group = 'remaining vendor';
} else {
Expand Down Expand Up @@ -219,13 +209,16 @@ public static function register($mode = 0)
if (DeprecationErrorHandler::MODE_WEAK_VENDORS === $mode) {
$groups[] = 'remaining vendor';
}
if (DeprecationErrorHandler::MODE_ALLOW_OUTDATED_VENDORS === $mode) {
$groups[] = 'outdated vendor';
}
array_push($groups, 'legacy', 'other');

foreach ($groups as $group) {
if ($deprecations[$group.'Count']) {
echo "\n", $colorize(
sprintf('%s deprecation notices (%d)', ucfirst($group), $deprecations[$group.'Count']),
'legacy' !== $group && 'remaining vendor' !== $group
!in_array($group, array('legacy', 'remaining vendor', 'outdated vendor'), true)
), "\n";

uasort($deprecations[$group], $cmp);
Expand Down Expand Up @@ -254,6 +247,87 @@ public static function register($mode = 0)
}
}

private static function isOutdatedVendor(array $trace): bool
{
$erroringFile = $erroringPackage = null;
foreach ($trace as $line) {
if (!isset($line['file'])) {
continue;
}
$file = $line['file'];
if ('-' === $file || 'Standard input code' === $file || !realpath($file)) {
continue;
}
if (!self::inVendors($file)) {
return false;
}
if (null !== $erroringFile && null !== $erroringPackage) {
if (self::getPackage($file) !== $erroringPackage) {
return true;
}
} else {
$erroringFile = $file;
$erroringPackage = self::getPackage($file);
}
}

return false;
}

/**
* inVendors() should always be called prior to calling this method.
*/
private static function getPackage(string $path): string
{
$path = realpath($path) ?: $path;
foreach (self::getVendors() as $vendorRoot) {
if (0 === strpos($path, $vendorRoot)) {
$relativePath = substr($path, strlen($vendorRoot) + 1);
$vendor = strstr($relativePath, DIRECTORY_SEPARATOR, true);

return $vendor.'/'.strstr(substr($relativePath, strlen($vendor) + 1), DIRECTORY_SEPARATOR, true);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should throw a meaningful exception when reaching this place explaining that the provided path is not a vendor path, instead of letting PHP throw a fatal error due to null not respecting the string return type (which would not be clear at all).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof we're inside the error handler, that's why I refrained from doing that, I'm not sure how it will behave, but I will try.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, it's an error handler, so an exception should probably be fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reaching this place would indicate a bad usage of the private method inside the class anyway (as you are not supposed to pass a non-vendor path). So I think it would be fine (and would make it easier for contributors to see their mistakes)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried putting an exception unconditionally on the first line, and it worked as expected, so it's fine indeed 👍


throw new \RuntimeException(sprintf(
'No vendors found for path "%s"',
$path
));
}

private static function getVendors(): array
{
if (null === self::$vendors) {
self::$vendors = array();
foreach (get_declared_classes() as $class) {
if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) {
$r = new \ReflectionClass($class);
$v = dirname(dirname($r->getFileName()));
if (file_exists($v.'/composer/installed.json')) {
self::$vendors[] = $v;
}
}
}
}

return self::$vendors;
}

private static function inVendors(string $path): bool
{
$realPath = realpath($path);
if (false === $realPath && '-' !== $path && 'Standard input code' !== $path) {
return true;
}
foreach (self::getVendors() as $vendor) {
if (0 === strpos($realPath, $vendor) && false !== strpbrk(substr($realPath, strlen($vendor), 1), '/'.DIRECTORY_SEPARATOR)) {
return true;
}
}

return false;
}

public static function collectDeprecations($outputFile)
{
$deprecations = array();
Expand All @@ -269,7 +343,7 @@ public static function collectDeprecations($outputFile)

return $ErrorHandler::handleError($type, $msg, $file, $line, $context);
}
$deprecations[] = array(error_reporting(), $msg, $file);
$deprecations[] = array(error_reporting(), $msg, $file, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
});

register_shutdown_function(function () use ($outputFile, &$deprecations) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,13 @@ public function endTest($test, $time)
unlink($this->runsInSeparateProcess);
putenv('SYMFONY_DEPRECATIONS_SERIALIZE');
foreach ($deprecations ? unserialize($deprecations) : array() as $deprecation) {
$error = serialize(array('deprecation' => $deprecation[1], 'class' => $className, 'method' => $test->getName(false), 'triggering_file' => isset($deprecation[2]) ? $deprecation[2] : null));
$error = serialize(array(
'deprecation' => $deprecation[1],
'class' => $className,
'method' => $test->getName(false),
'triggering_file' => isset($deprecation[2]) ? $deprecation[2] : null,
'trace' => isset($deprecation[3]) ? $deprecation[3] : null,
));
if ($deprecation[0]) {
trigger_error($error, E_USER_DEPRECATED);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
--TEST--
Test DeprecationErrorHandler in weak vendors mode when calling deprecated api
--FILE--
<?php

putenv('SYMFONY_DEPRECATIONS_HELPER=allow_outdated_vendors');
putenv('ANSICON');
putenv('ConEmuANSI');
putenv('TERM');

$vendor = __DIR__;
while (!file_exists($vendor.'/vendor')) {
$vendor = dirname($vendor);
}
define('PHPUNIT_COMPOSER_INSTALL', $vendor.'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
require_once __DIR__.'/../../bootstrap.php';
eval(<<<'EOPHP'
namespace PHPUnit\Util;

class Test
{
public static function getGroups()
{
return array();
}
}
EOPHP
);
require __DIR__.'/fake_vendor/autoload.php';
require __DIR__.'/fake_vendor/acme/lib/SomeService.php';
$defraculator = new \Acme\Lib\SomeService();
$defraculator->deprecatedApi();


?>
--EXPECTF--
Remaining deprecation notices (1)

1x: deprecatedApi is deprecated! You should stop relying on it!
1x in SomeService::deprecatedApi from acme\lib

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
--TEST--
Test DeprecationErrorHandler in weak vendors mode on vendor file
--FILE--
<?php

putenv('SYMFONY_DEPRECATIONS_HELPER=allow_outdated_vendors');
putenv('ANSICON');
putenv('ConEmuANSI');
putenv('TERM');

$vendor = __DIR__;
while (!file_exists($vendor.'/vendor')) {
$vendor = dirname($vendor);
}
define('PHPUNIT_COMPOSER_INSTALL', $vendor.'/vendor/autoload.php');
require PHPUNIT_COMPOSER_INSTALL;
require_once __DIR__.'/../../bootstrap.php';
eval(<<<'EOPHP'
namespace PHPUnit\Util;

class Test
{
public static function getGroups()
{
return array();
}
}
EOPHP
);
require __DIR__.'/fake_vendor/autoload.php';
require __DIR__.'/fake_vendor/acme/outdated-lib/outdated_file.php';

?>
--EXPECTF--
Outdated vendor deprecation notices (1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace acme\lib;

class SomeService
{
public function deprecatedApi()
{
@trigger_error(
__FUNCTION__.' is deprecated! You should stop relying on it!',
E_USER_DEPRECATED
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php
/* We have not caught up on the deprecations yet and still call the other lib
in a deprecated way. */

include __DIR__.'/../lib/SomeService.php';
$defraculator = new \acme\lib\SomeService();
$defraculator->deprecatedApi();