Эх сурвалжийг харах

added schema/syntax validation for composer.json

digitalkaoz 13 жил өмнө
parent
commit
6929c42848

+ 0 - 151
bin/installer

@@ -1,151 +0,0 @@
-#!/usr/bin/env php
-<?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.
- */
-
-
-process($argv);
-
-/**
- * processes the installer
- */
-function process($argv)
-{
-    $check = in_array('--check', $argv);
-    $help = in_array('--help', $argv);
-    $force = in_array('--force', $argv);
-
-    if ($help) {
-        displayHelp();
-        exit(0);
-    }
-
-    $ok = checkPlatform();
-
-    if ($check && !$ok) {
-        exit(1);
-    }
-
-    if ($ok || $force) {
-        installComposer();
-    }
-
-    exit(0);
-}
-
-/**
- * displays the help
- */
-function displayHelp()
-{
-    echo <<<EOF
-Composer Installer
-------------------
-Options
---help   this help
---check  for checking environment only
---force  forces the installation
-
-EOF;
-}
-
-/**
- * check the platform for possible issues on running composer
- */
-function checkPlatform()
-{
-    $errors = array();
-    if (false !== ini_get('detect_unicode')) {
-        $errors['unicode'] = 'On';
-    }
-
-    if (ini_get('phar.readonly')) {
-        $errors['readonly'] = 'On';
-    }
-
-    if (ini_get('phar.require_hash')) {
-        $errors['require_hash'] = 'On';
-    }
-
-    if ($suhosin = ini_get('suhosin.executor.include.whitelist') && (isset($suhosin) && false === stripos($suhosin, 'phar'))) {
-        $errors['suhosin'] = $suhosin;
-    }
-
-    if (PHP_VERSION < '5.3.2') {
-        $errors['php'] = PHP_VERSION;
-    }
-
-    if (!empty($errors)) {
-        out("Composer detected that you have enabled some settings in your `php.ini` file that can make Composer unable to work properly.".PHP_EOL, 'error');
-
-        echo PHP_EOL.'Make sure that you have changed options listed below:'.PHP_EOL;
-        foreach ($errors as $error => $actual) {
-            switch ($error) {
-                case 'unicode':
-                    $text = "    detect_unicode = Off (actual: {$actual})".PHP_EOL;
-                    break;
-
-                case 'readonly':
-                    $text = "    phar.readonly = Off (actual: {$actual})".PHP_EOL;
-                    break;
-
-                case 'require_hash':
-                    $text = "    phar.require_hash = Off (actual: {$actual})".PHP_EOL;
-                    break;
-
-                case 'suhosin':
-                    $text = "    suhosin.executor.include.whitelist = phar (actual: {$actual})".PHP_EOL;
-                    break;
-                case 'php':
-                    $text = "    PHP_VERSION (actual: {$actual})".PHP_EOL;
-                    break;
-            }
-            out($text, 'info');
-        }
-        echo PHP_EOL;
-        return false;
-    }
-
-    out("All settings correct for using Composer".PHP_EOL,'success');
-    return true;
-}
-
-/**
- * installs composer to the current working directory
- */
-function installComposer()
-{
-    $installDir = getcwd();
-    $file = $installDir . DIRECTORY_SEPARATOR . 'composer.phar';
-
-    if (is_readable($file)) {
-        @unlink($file);
-    }
-
-    $download = copy('http://getcomposer.org/composer.phar', $installDir.DIRECTORY_SEPARATOR.'composer.phar');
-
-    out(PHP_EOL."Composer successfully installed to: " . $file, 'success');
-    out(PHP_EOL."Use it: php composer.phar".PHP_EOL, 'info');
-}
-
-/**
- * colorize output
- */
-function out($text, $color = null)
-{
-    $styles = array(
-        'success' => "\033[0;32m%s\033[0m",
-        'error' => "\033[31;31m%s\033[0m",
-        'info' => "\033[33;33m%s\033[0m"
-    );
-
-    echo sprintf(isset($styles[$color]) ? $styles[$color] : "%s", $text);
-}

+ 3 - 1
composer.json

@@ -18,7 +18,9 @@
         }
     ],
     "require": {
-        "php": ">=5.3.0",
+        "php": ">=5.3.0",       
+        "justinrainbow/json-schema": ">=1.1.0",
+        "seld/jsonlint": "*",
         "symfony/console": "dev-master",
         "symfony/finder": "dev-master",
         "symfony/process": "dev-master"

+ 1 - 2
res/composer-schema.json

@@ -35,8 +35,7 @@
         },
         "version": {
             "type": "string",
-            "description": "Package version, see http://packagist.org/about for more info on valid schemes.",
-            "required": true
+            "description": "Package version, see http://packagist.org/about for more info on valid schemes."
         },
         "time": {
             "type": "string",

+ 1 - 1
src/Composer/Command/ValidateCommand.php

@@ -53,7 +53,7 @@ EOT
         }
 
         try {
-            JsonFile::parseJson(file_get_contents($file));
+            JsonFile::parseJson(file_get_contents($file), true);
         } catch (\Exception $e) {
             $output->writeln('<error>'.$e->getMessage().'</error>');
             return 1;

+ 1 - 1
src/Composer/Console/Application.php

@@ -128,4 +128,4 @@ class Application extends BaseApplication
 
         return $helperSet;
     }
-}
+}

+ 1 - 1
src/Composer/Factory.php

@@ -54,7 +54,7 @@ class Factory
             'vendor-dir' => 'vendor',
         );
 
-        $packageConfig = $file->read();
+        $packageConfig = $file->read(true);
 
         if (isset($packageConfig['config']) && is_array($packageConfig['config'])) {
             $packageConfig['config'] = array_merge($composerConfig, $packageConfig['config']);

+ 56 - 49
src/Composer/Json/JsonFile.php

@@ -14,6 +14,8 @@ namespace Composer\Json;
 
 use Composer\Repository\RepositoryManager;
 use Composer\Composer;
+use JsonSchema\Validator;
+use Seld\JsonLint\JsonParser;
 use Composer\Util\StreamContextFactory;
 
 if (!defined('JSON_UNESCAPED_SLASHES')) {
@@ -68,7 +70,7 @@ class JsonFile
      *
      * @return  array
      */
-    public function read()
+    public function read($validate = false)
     {
         $ctx = StreamContextFactory::getContext(array(
             'http' => array(
@@ -80,7 +82,7 @@ class JsonFile
             throw new \RuntimeException('Could not read '.$this->path.', you are probably offline');
         }
 
-        return static::parseJson($json);
+        return static::parseJson($json, $validate);
     }
 
     /**
@@ -215,59 +217,64 @@ class JsonFile
     /**
      * Parses json string and returns hash.
      *
-     * @param   string  $json   json string
+     * @param string $json json string
+     * @param boolean $validateSchema wether to validate the json schema
      *
      * @return  mixed
      */
-    static public function parseJson($json)
+    public static function parseJson($json, $validateSchema=false)
+    {        
+        $data = static::validateSyntax($json);
+
+        if ($validateSchema) {
+            static::validateSchema($json);
+        }
+
+        return $data;
+    }
+
+    /**
+     * validates a composer.json against the schema
+     * 
+     * @param string $json
+     * @return boolean
+     * @throws \UnexpectedValueException
+     */
+    public static function validateSchema($json)
     {
-        $data = json_decode($json, true);
-
-        if (null === $data && 'null' !== strtolower($json)) {
-            switch (json_last_error()) {
-            case JSON_ERROR_NONE:
-                $msg = 'No error has occurred, is your composer.json file empty?';
-                break;
-            case JSON_ERROR_DEPTH:
-                $msg = 'The maximum stack depth has been exceeded';
-                break;
-            case JSON_ERROR_STATE_MISMATCH:
-                $msg = 'Invalid or malformed JSON';
-                break;
-            case JSON_ERROR_CTRL_CHAR:
-                $msg = 'Control character error, possibly incorrectly encoded';
-                break;
-            case JSON_ERROR_SYNTAX:
-                $msg = 'Syntax error';
-                $charOffset = 0;
-                if (preg_match('#["}\]]\s*(,)\s*[}\]]#', $json, $match, PREG_OFFSET_CAPTURE)) {
-                    $msg .= ', extra comma';
-                } elseif (preg_match('#((?<=[^\\\\])\\\\(?!["\\\\/bfnrt]|u[a-f0-9]{4}))#i', $json, $match, PREG_OFFSET_CAPTURE)) {
-                    $msg .= ', unescaped backslash (\\)';
-                } elseif (preg_match('#(["}\]])(?: *\r?\n *)+"#', $json, $match, PREG_OFFSET_CAPTURE)) {
-                    $msg .= ', missing comma';
-                    $charOffset = 1;
-                } elseif (preg_match('#^ *([a-z0-9_-]+) *:#mi', $json, $match, PREG_OFFSET_CAPTURE)) {
-                    $msg .= ', you must use double quotes (") around keys';
-                } elseif (preg_match('#(\'.+?\' *:|: *\'.+?\')#', $json, $match, PREG_OFFSET_CAPTURE)) {
-                    $msg .= ', use double quotes (") instead of single quotes (\')';
-                } elseif (preg_match('#(\[".*?":.*?\])#', $json, $match, PREG_OFFSET_CAPTURE)) {
-                    $msg .= ', you must use the hash syntax (e.g. {"foo": "bar"}) instead of array syntax (e.g. ["foo", "bar"])';
-                } elseif (preg_match('#".*?"( *["{\[])#', $json, $match, PREG_OFFSET_CAPTURE)) {
-                    $msg .= ', missing colon';
-                }
-                if (isset($match[1][1])) {
-                    $preError = substr($json, 0, $match[1][1]);
-                    $msg .= ' on line '.(substr_count($preError, "\n")+1).', char '.abs(strrpos($preError, "\n") - strlen($preError) - $charOffset);
-                }
-                break;
-            case JSON_ERROR_UTF8:
-                $msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
-                break;
+        $data = json_decode($json);
+        $schema = json_decode(file_get_contents(__DIR__ . '/../../../res/composer-schema.json'));
+
+        $validator = new Validator();
+
+        $validator->check($data, $schema);
+
+        if (!$validator->isValid()) {
+            $msg = "\n";
+            foreach ((array) $validator->getErrors() as $error) {
+                $msg .= ($error['property'] ? $error['property'].' : ' : '').$error['message']."\n";
             }
-            throw new \UnexpectedValueException('JSON Parse Error: '.$msg);
+
+            throw new \UnexpectedValueException('Your composer.json did not validate against the schema. The following mistakes were found:'.$msg);
         }
+    }
 
-        return $data;
+    /**
+     * validates the json syntax
+     * 
+     * @param string $json
+     * @return array
+     * @throws \UnexpectedValueException
+     */
+    public static function validateSyntax($json)
+    {
+        $parser = new JsonParser();
+        $result = $parser->lint($json);
+
+        if (null === $result) {
+           return json_decode($json, true);
+        }
+
+        throw $result;
     }
 }

+ 23 - 11
tests/Composer/Test/Json/JsonFileTest.php

@@ -12,6 +12,7 @@
 
 namespace Composer\Test\Json;
 
+use Seld\JsonLint\ParsingException;
 use Composer\Json\JsonFile;
 
 class JsonFileTest extends \PHPUnit_Framework_TestCase
@@ -21,7 +22,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
         $json = '{
         "foo": "bar",
 }';
-        $this->expectParseException('extra comma on line 2, char 21', $json);
+        $this->expectParseException('Parse error on line 2', $json);
     }
 
     public function testParseErrorDetectExtraCommaInArray()
@@ -31,7 +32,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
             "bar",
         ]
 }';
-        $this->expectParseException('extra comma on line 3, char 18', $json);
+        $this->expectParseException('Parse error on line 3', $json);
     }
 
     public function testParseErrorDetectUnescapedBackslash()
@@ -39,7 +40,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
         $json = '{
         "fo\o": "bar"
 }';
-        $this->expectParseException('unescaped backslash (\\) on line 2, char 12', $json);
+        $this->expectParseException('Parse error on line 1', $json);
     }
 
     public function testParseErrorSkipsEscapedBackslash()
@@ -48,7 +49,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
         "fo\\\\o": "bar"
         "a": "b"
 }';
-        $this->expectParseException('missing comma on line 2, char 23', $json);
+        $this->expectParseException('Parse error on line 2', $json);
     }
 
     public function testParseErrorDetectSingleQuotes()
@@ -56,7 +57,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
         $json = '{
         \'foo\': "bar"
 }';
-        $this->expectParseException('use double quotes (") instead of single quotes (\') on line 2, char 9', $json);
+        $this->expectParseException('Parse error on line 1', $json);
     }
 
     public function testParseErrorDetectMissingQuotes()
@@ -64,7 +65,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
         $json = '{
         foo: "bar"
 }';
-        $this->expectParseException('must use double quotes (") around keys on line 2, char 9', $json);
+        $this->expectParseException('Parse error on line 1', $json);
     }
 
     public function testParseErrorDetectArrayAsHash()
@@ -72,7 +73,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
         $json = '{
         "foo": ["bar": "baz"]
 }';
-        $this->expectParseException('you must use the hash syntax (e.g. {"foo": "bar"}) instead of array syntax (e.g. ["foo", "bar"]) on line 2, char 16', $json);
+        $this->expectParseException('Parse error on line 2', $json);
     }
 
     public function testParseErrorDetectMissingComma()
@@ -81,7 +82,18 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
         "foo": "bar"
         "bar": "foo"
 }';
-        $this->expectParseException('missing comma on line 2, char 21', $json);
+        $this->expectParseException('Parse error on line 2', $json);
+    }
+
+    public function testSchemaValidation()
+    {
+        $json = file_get_contents(__DIR__.'/../../../../composer.json');
+
+        try {
+            $this->assertNull(JsonFile::validateSchema($json));
+        } catch (\UnexpectedValueException $e) {
+            $this->fail('invalid schema');
+        }
     }
 
     public function testParseErrorDetectMissingCommaMultiline()
@@ -91,7 +103,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
 
         "bar": "foo"
 }';
-        $this->expectParseException('missing comma on line 2, char 24', $json);
+        $this->expectParseException('Parse error on line 2', $json);
     }
 
     public function testParseErrorDetectMissingColon()
@@ -100,7 +112,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
         "foo": "bar",
         "bar" "foo"
 }';
-        $this->expectParseException('missing colon on line 3, char 14', $json);
+        $this->expectParseException('Parse error on line 3', $json);
     }
 
     public function testSimpleJsonString()
@@ -166,7 +178,7 @@ class JsonFileTest extends \PHPUnit_Framework_TestCase
         try {
             JsonFile::parseJson($json);
             $this->fail();
-        } catch (\UnexpectedValueException $e) {
+        } catch (ParsingException $e) {
             $this->assertContains($text, $e->getMessage());
         }
     }