Quellcode durchsuchen

Merge pull request #4827 from curry684/issue-4203

Added more graceful warning suppression utility
Jordi Boggiano vor 9 Jahren
Ursprung
Commit
5c944d45ac

+ 2 - 1
src/Composer/Autoload/ClassMapGenerator.php

@@ -18,6 +18,7 @@
 
 namespace Composer\Autoload;
 
+use Composer\Util\Silencer;
 use Symfony\Component\Finder\Finder;
 use Composer\IO\IOInterface;
 
@@ -122,7 +123,7 @@ class ClassMapGenerator
         }
 
         try {
-            $contents = @php_strip_whitespace($path);
+            $contents = Silencer::call('php_strip_whitespace', $path);
             if (!$contents) {
                 if (!file_exists($path)) {
                     throw new \Exception('File does not exist');

+ 2 - 1
src/Composer/Cache.php

@@ -14,6 +14,7 @@ namespace Composer;
 
 use Composer\IO\IOInterface;
 use Composer\Util\Filesystem;
+use Composer\Util\Silencer;
 use Symfony\Component\Finder\Finder;
 
 /**
@@ -44,7 +45,7 @@ class Cache
         $this->filesystem = $filesystem ?: new Filesystem();
 
         if (
-            (!is_dir($this->root) && !@mkdir($this->root, 0777, true))
+            (!is_dir($this->root) && !Silencer::call('mkdir', $this->root, 0777, true))
             || !is_writable($this->root)
         ) {
             $this->io->writeError('<warning>Cannot create cache directory ' . $this->root . ', or directory is not writable. Proceeding without cache</warning>');

+ 5 - 4
src/Composer/Command/ConfigCommand.php

@@ -12,6 +12,7 @@
 
 namespace Composer\Command;
 
+use Composer\Util\Silencer;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputOption;
@@ -142,7 +143,7 @@ EOT
             ? ($this->config->get('home') . '/config.json')
             : ($input->getOption('file') ?: trim(getenv('COMPOSER')) ?: 'composer.json');
 
-        // create global composer.json if this was invoked using `composer global config`
+        // Create global composer.json if this was invoked using `composer global config`
         if ($configFile === 'composer.json' && !file_exists($configFile) && realpath(getcwd()) === realpath($this->config->get('home'))) {
             file_put_contents($configFile, "{\n}\n");
         }
@@ -157,16 +158,16 @@ EOT
         $this->authConfigFile = new JsonFile($authConfigFile, null, $io);
         $this->authConfigSource = new JsonConfigSource($this->authConfigFile, true);
 
-        // initialize the global file if it's not there
+        // Initialize the global file if it's not there, ignoring any warnings or notices
         if ($input->getOption('global') && !$this->configFile->exists()) {
             touch($this->configFile->getPath());
             $this->configFile->write(array('config' => new \ArrayObject));
-            @chmod($this->configFile->getPath(), 0600);
+            Silencer::call('chmod', $this->configFile->getPath(), 0600);
         }
         if ($input->getOption('global') && !$this->authConfigFile->exists()) {
             touch($this->authConfigFile->getPath());
             $this->authConfigFile->write(array('http-basic' => new \ArrayObject, 'github-oauth' => new \ArrayObject, 'gitlab-oauth' => new \ArrayObject));
-            @chmod($this->authConfigFile->getPath(), 0600);
+            Silencer::call('chmod', $this->authConfigFile->getPath(), 0600);
         }
 
         if (!$this->configFile->exists()) {

+ 3 - 2
src/Composer/Command/CreateProjectCommand.php

@@ -27,6 +27,7 @@ use Composer\Repository\CompositeRepository;
 use Composer\Repository\FilesystemRepository;
 use Composer\Repository\InstalledFilesystemRepository;
 use Composer\Script\ScriptEvents;
+use Composer\Util\Silencer;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -224,10 +225,10 @@ EOT
         chdir($oldCwd);
         $vendorComposerDir = $composer->getConfig()->get('vendor-dir').'/composer';
         if (is_dir($vendorComposerDir) && $fs->isDirEmpty($vendorComposerDir)) {
-            @rmdir($vendorComposerDir);
+            Silencer::call('rmdir', $vendorComposerDir);
             $vendorDir = $composer->getConfig()->get('vendor-dir');
             if (is_dir($vendorDir) && $fs->isDirEmpty($vendorDir)) {
-                @rmdir($vendorDir);
+                Silencer::call('rmdir', $vendorDir);
             }
         }
 

+ 2 - 1
src/Composer/Config/JsonConfigSource.php

@@ -14,6 +14,7 @@ namespace Composer\Config;
 
 use Composer\Json\JsonFile;
 use Composer\Json\JsonManipulator;
+use Composer\Util\Silencer;
 
 /**
  * JSON Configuration Source
@@ -173,7 +174,7 @@ class JsonConfigSource implements ConfigSourceInterface
         }
 
         if ($newFile) {
-            @chmod($this->file->getPath(), 0600);
+            Silencer::call('chmod', $this->file->getPath(), 0600);
         }
     }
 

+ 7 - 4
src/Composer/Console/Application.php

@@ -12,6 +12,7 @@
 
 namespace Composer\Console;
 
+use Composer\Util\Silencer;
 use Symfony\Component\Console\Application as BaseApplication;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
@@ -64,7 +65,7 @@ class Application extends BaseApplication
         }
 
         if (function_exists('date_default_timezone_set') && function_exists('date_default_timezone_get')) {
-            date_default_timezone_set(@date_default_timezone_get());
+            date_default_timezone_set(Silencer::call('date_default_timezone_get'));
         }
 
         if (!$shutdownRegistered) {
@@ -203,21 +204,23 @@ class Application extends BaseApplication
     {
         $io = $this->getIO();
 
+        Silencer::suppress();
         try {
             $composer = $this->getComposer(false, true);
             if ($composer) {
                 $config = $composer->getConfig();
 
                 $minSpaceFree = 1024 * 1024;
-                if ((($df = @disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree)
-                    || (($df = @disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree)
-                    || (($df = @disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree)
+                if ((($df = disk_free_space($dir = $config->get('home'))) !== false && $df < $minSpaceFree)
+                    || (($df = disk_free_space($dir = $config->get('vendor-dir'))) !== false && $df < $minSpaceFree)
+                    || (($df = disk_free_space($dir = sys_get_temp_dir())) !== false && $df < $minSpaceFree)
                 ) {
                     $io->writeError('<error>The disk hosting '.$dir.' is full, this may be the cause of the following exception</error>');
                 }
             }
         } catch (\Exception $e) {
         }
+        Silencer::restore();
 
         if (defined('PHP_WINDOWS_VERSION_BUILD') && false !== strpos($exception->getMessage(), 'The system cannot find the path specified')) {
             $io->writeError('<error>The following exception may be caused by a stale entry in your cmd.exe AutoRun</error>');

+ 3 - 2
src/Composer/Factory.php

@@ -22,6 +22,7 @@ use Composer\Repository\WritableRepositoryInterface;
 use Composer\Util\Filesystem;
 use Composer\Util\ProcessExecutor;
 use Composer\Util\RemoteFilesystem;
+use Composer\Util\Silencer;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
 use Composer\EventDispatcher\EventDispatcher;
 use Composer\Autoload\AutoloadGenerator;
@@ -163,9 +164,9 @@ class Factory
         foreach ($dirs as $dir) {
             if (!file_exists($dir . '/.htaccess')) {
                 if (!is_dir($dir)) {
-                    @mkdir($dir, 0777, true);
+                    Silencer::call('mkdir', $dir, 0777, true);
                 }
-                @file_put_contents($dir . '/.htaccess', 'Deny from all');
+                Silencer::call('file_put_contents', $dir . '/.htaccess', 'Deny from all');
             }
         }
 

+ 5 - 4
src/Composer/Installer/LibraryInstaller.php

@@ -18,6 +18,7 @@ use Composer\Repository\InstalledRepositoryInterface;
 use Composer\Package\PackageInterface;
 use Composer\Util\Filesystem;
 use Composer\Util\ProcessExecutor;
+use Composer\Util\Silencer;
 
 /**
  * Package installation manager.
@@ -130,7 +131,7 @@ class LibraryInstaller implements InstallerInterface
         if (strpos($package->getName(), '/')) {
             $packageVendorDir = dirname($downloadPath);
             if (is_dir($packageVendorDir) && $this->filesystem->isDirEmpty($packageVendorDir)) {
-                @rmdir($packageVendorDir);
+                Silencer::call('rmdir', $packageVendorDir);
             }
         }
     }
@@ -233,7 +234,7 @@ class LibraryInstaller implements InstallerInterface
                     // likely leftover from a previous install, make sure
                     // that the target is still executable in case this
                     // is a fresh install of the vendor.
-                    @chmod($link, 0777 & ~umask());
+                    Silencer::call('chmod', $link, 0777 & ~umask());
                 }
                 $this->io->writeError('    Skipped installation of bin '.$bin.' for package '.$package->getName().': name conflicts with an existing file');
                 continue;
@@ -248,7 +249,7 @@ class LibraryInstaller implements InstallerInterface
             } elseif ($this->binCompat === "full") {
                 $this->installFullBinaries($binPath, $link, $bin, $package);
             }
-            @chmod($link, 0777 & ~umask());
+            Silencer::call('chmod', $link, 0777 & ~umask());
         }
     }
 
@@ -298,7 +299,7 @@ class LibraryInstaller implements InstallerInterface
 
         // attempt removing the bin dir in case it is left empty
         if ((is_dir($this->binDir)) && ($this->filesystem->isDirEmpty($this->binDir))) {
-            @rmdir($this->binDir);
+            Silencer::call('rmdir', $this->binDir);
         }
     }
 

+ 1 - 1
src/Composer/Util/RemoteFilesystem.php

@@ -886,7 +886,7 @@ class RemoteFilesystem
         );
 
         foreach ($caBundlePaths as $caBundle) {
-            if (@is_readable($caBundle) && $this->validateCaFile($caBundle)) {
+            if (Silencer::call('is_readable', $caBundle) && $this->validateCaFile($caBundle)) {
                 return $caPath = $caBundle;
             }
         }

+ 73 - 0
src/Composer/Util/Silencer.php

@@ -0,0 +1,73 @@
+<?php
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Util;
+
+/**
+ * Temporarily suppress PHP error reporting, usually warnings and below.
+ *
+ * @author Niels Keurentjes <niels.keurentjes@omines.com>
+ */
+class Silencer
+{
+    /**
+     * @var int[] Unpop stack
+     */
+    private static $stack = array();
+
+    /**
+     * Suppresses given mask or errors.
+     *
+     * @param int|null $mask Error levels to suppress, default value NULL indicates all warnings and below.
+     * @return int The old error reporting level.
+     */
+    public static function suppress($mask = null)
+    {
+        if (!isset($mask)) {
+            $mask = E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_DEPRECATED | E_USER_DEPRECATED | E_STRICT;
+        }
+        array_push(self::$stack, $old = error_reporting());
+        error_reporting($old & ~$mask);
+        return $old;
+    }
+
+    /**
+     * Restores a single state.
+     */
+    public static function restore()
+    {
+        if (!empty(self::$stack))
+            error_reporting(array_pop(self::$stack));
+    }
+
+    /**
+     * Calls a specified function while silencing warnings and below.
+     *
+     * Future improvement: when PHP requirements are raised add Callable type hint (5.4) and variadic parameters (5.6)
+     *
+     * @param callable $callable Function to execute.
+     * @return mixed Return value of the callback.
+     * @throws \Exception Any exceptions from the callback are rethrown.
+     */
+    public static function call($callable /*, ...$parameters */)
+    {
+        try {
+            self::suppress();
+            $result = call_user_func_array($callable, array_slice(func_get_args(), 1));
+            self::restore();
+            return $result;
+        } catch(\Exception $e) {
+            // Use a finally block for this when requirements are raised to PHP 5.5
+            self::restore();
+            throw $e;
+        }
+    }
+}

+ 57 - 0
tests/Composer/Test/Util/SilencerTest.php

@@ -0,0 +1,57 @@
+<?php
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Test\Util;
+
+use Composer\Util\Silencer;
+
+/**
+ * SilencerTest
+ *
+ * @author Niels Keurentjes <niels.keurentjes@omines.com>
+ */
+class SilencerTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * Test succeeds when no warnings are emitted externally, and original level is restored.
+     */
+    public function testSilencer()
+    {
+        $before = error_reporting();
+
+        // Check warnings are suppressed correctly
+        Silencer::suppress();
+        @trigger_error('Test', E_USER_WARNING);
+        Silencer::restore();
+
+        // Check all parameters and return values are passed correctly in a silenced call.
+        $result = Silencer::call(function($a, $b, $c) {
+            @trigger_error('Test', E_USER_WARNING);
+            return $a * $b * $c;
+        }, 2, 3, 4);
+        $this->assertEquals(24, $result);
+
+        // Check the error reporting setting was restored correctly
+        $this->assertEquals($before, error_reporting());
+    }
+
+    /**
+     * Test whether exception from silent callbacks are correctly forwarded.
+     */
+    public function testSilencedException()
+    {
+        $verification = microtime();
+        $this->setExpectedException('\RuntimeException', $verification);
+        Silencer::call(function() use ($verification) {
+            throw new \RuntimeException($verification);
+        });
+    }
+}