diff --git a/UPDATE.md b/UPDATE.md index 66812feaa6..c5c60d3c5e 100644 --- a/UPDATE.md +++ b/UPDATE.md @@ -9,7 +9,10 @@ timeline closely anyway. beta4 to beta5 -------------- -* `Yaml::load()` has been renamed to `Yaml::parse()` +* Yaml Component: + + * Exception classes have been moved to their own namespace + * `Yaml::load()` has been renamed to `Yaml::parse()` * The `extensions` setting for Twig has been removed. There is now only one way to register Twig extensions, via the `twig.extension` tag. diff --git a/src/Symfony/Component/Yaml/ParserException.php b/src/Symfony/Component/Yaml/Exception/DumpException.php similarity index 60% rename from src/Symfony/Component/Yaml/ParserException.php rename to src/Symfony/Component/Yaml/Exception/DumpException.php index 4689c8d7f0..53952ce1ad 100644 --- a/src/Symfony/Component/Yaml/ParserException.php +++ b/src/Symfony/Component/Yaml/Exception/DumpException.php @@ -9,13 +9,15 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Yaml; +namespace Symfony\Component\Yaml\Exception; /** - * Exception class used by all exceptions thrown by the component. + * Exception class thrown when an error occurs during dumping. * * @author Fabien Potencier + * + * @api */ -class ParserException extends Exception +class DumpException extends \RuntimeException implements ExceptionInterface { } diff --git a/src/Symfony/Component/Yaml/Exception.php b/src/Symfony/Component/Yaml/Exception/ExceptionInterface.php similarity index 67% rename from src/Symfony/Component/Yaml/Exception.php rename to src/Symfony/Component/Yaml/Exception/ExceptionInterface.php index 0c95e936f4..92e5c2ea4e 100644 --- a/src/Symfony/Component/Yaml/Exception.php +++ b/src/Symfony/Component/Yaml/Exception/ExceptionInterface.php @@ -9,13 +9,15 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\Yaml; +namespace Symfony\Component\Yaml\Exception; /** - * Exception class used by all exceptions thrown by the component. + * Exception interface for all exceptions thrown by the component. * * @author Fabien Potencier + * + * @api */ -class Exception extends \Exception +interface ExceptionInterface { } diff --git a/src/Symfony/Component/Yaml/Exception/ParseException.php b/src/Symfony/Component/Yaml/Exception/ParseException.php new file mode 100644 index 0000000000..8e935bc8a9 --- /dev/null +++ b/src/Symfony/Component/Yaml/Exception/ParseException.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Exception; + +/** + * Exception class thrown when an error occurs during parsing. + * + * @author Fabien Potencier + * + * @api + */ +class ParseException extends \RuntimeException implements ExceptionInterface +{ + private $parsedFile; + private $parsedLine; + private $snippet; + private $rawMessage; + + /** + * Constructor. + * + * @param string $message The error message + * @param integer $lineno The line where the error occurred + * @param integer $snippet The snippet of code near the problem + * @param string $filename The file name where the error occurred + * @param Exception $previous The previous exception + */ + public function __construct($message, $parsedLine = -1, $snippet = null, $parsedFile = null, Exception $previous = null) + { + $this->parsedFile = $parsedFile; + $this->parsedLine = $parsedLine; + $this->snippet = $snippet; + $this->rawMessage = $message; + + $this->updateRepr(); + + parent::__construct($this->message, 0, $previous); + } + + /** + * Gets the snippet of code near the error. + * + * @return string The snippet of code + */ + public function getSnippet() + { + return $this->snippet; + } + + /** + * Sets the snippet of code near the error. + * + * @param string $parsedFile The filename + */ + public function setSnippet($snippet) + { + $this->snippet = $snippet; + + $this->updateRepr(); + } + + /** + * Gets the filename where the error occurred. + * + * This method returns null if a string is parsed. + * + * @return string The filename + */ + public function getParsedFile() + { + return $this->parsedFile; + } + + /** + * Sets the filename where the error occurred. + * + * @param string $parsedFile The filename + */ + public function setParsedFile($parsedFile) + { + $this->parsedFile = $parsedFile; + + $this->updateRepr(); + } + + /** + * Gets the line where the error occurred. + * + * @return integer The file line + */ + public function getParsedLine() + { + return $this->parsedLine; + } + + /** + * Sets the line where the error occurred. + * + * @param integer $parsedLine The file line + */ + public function setParsedLine($parsedLine) + { + $this->parsedLine = $parsedLine; + + $this->updateRepr(); + } + + private function updateRepr() + { + $this->message = $this->rawMessage; + + $dot = false; + if ('.' === substr($this->message, -1)) { + $this->message = substr($this->message, 0, -1); + $dot = true; + } + + if (null !== $this->parsedFile) { + $this->message .= sprintf(' in %s', json_encode($this->parsedFile)); + } + + if ($this->parsedLine >= 0) { + $this->message .= sprintf(' at line %d', $this->parsedLine); + } + + if ($this->snippet) { + $this->message .= sprintf(' (near "%s")', $this->snippet); + } + + if ($dot) { + $this->message .= '.'; + } + } +} diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index 22aec93c2f..045a5eb747 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -10,6 +10,9 @@ namespace Symfony\Component\Yaml; +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Exception\DumpException; + /** * Inline implements a YAML parser/dumper for the YAML inline syntax. * @@ -64,13 +67,13 @@ class Inline * * @return string The YAML string representing the PHP array * - * @throws Exception When trying to dump PHP resource + * @throws DumpException When trying to dump PHP resource */ static public function dump($value) { switch (true) { case is_resource($value): - throw new Exception('Unable to dump PHP resources in a YAML file.'); + throw new DumpException('Unable to dump PHP resources in a YAML file.'); case is_object($value): return '!!php/object:'.serialize($value); case is_array($value): @@ -141,7 +144,7 @@ class Inline * * @return string A YAML string * - * @throws ParserException When malformed inline YAML string is parsed + * @throws ParseException When malformed inline YAML string is parsed */ static public function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true) { @@ -162,7 +165,7 @@ class Inline $output = $match[1]; $i += strlen($output); } else { - throw new ParserException(sprintf('Malformed inline YAML string (%s).', $scalar)); + throw new ParseException(sprintf('Malformed inline YAML string (%s).', $scalar)); } $output = $evaluate ? self::evaluateScalar($output) : $output; @@ -179,12 +182,12 @@ class Inline * * @return string A YAML string * - * @throws ParserException When malformed inline YAML string is parsed + * @throws ParseException When malformed inline YAML string is parsed */ static private function parseQuotedScalar($scalar, &$i) { if (!preg_match('/'.self::REGEX_QUOTED_STRING.'/Au', substr($scalar, $i), $match)) { - throw new ParserException(sprintf('Malformed inline YAML string (%s).', substr($scalar, $i))); + throw new ParseException(sprintf('Malformed inline YAML string (%s).', substr($scalar, $i))); } $output = substr($match[0], 1, strlen($match[0]) - 2); @@ -209,7 +212,7 @@ class Inline * * @return string A YAML string * - * @throws ParserException When malformed inline YAML string is parsed + * @throws ParseException When malformed inline YAML string is parsed */ static private function parseSequence($sequence, &$i = 0) { @@ -254,7 +257,7 @@ class Inline ++$i; } - throw new ParserException(sprintf('Malformed inline YAML string %s', $sequence)); + throw new ParseException(sprintf('Malformed inline YAML string %s', $sequence)); } /** @@ -265,7 +268,7 @@ class Inline * * @return string A YAML string * - * @throws ParserException When malformed inline YAML string is parsed + * @throws ParseException When malformed inline YAML string is parsed */ static private function parseMapping($mapping, &$i = 0) { @@ -318,7 +321,7 @@ class Inline } } - throw new ParserException(sprintf('Malformed inline YAML string %s', $mapping)); + throw new ParseException(sprintf('Malformed inline YAML string %s', $mapping)); } /** diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php index 8e08dc5c7f..c89d4b5064 100644 --- a/src/Symfony/Component/Yaml/Parser.php +++ b/src/Symfony/Component/Yaml/Parser.php @@ -10,6 +10,8 @@ namespace Symfony\Component\Yaml; +use Symfony\Component\Yaml\Exception\ParseException; + /** * Parser parses YAML strings to convert them to PHP arrays. * @@ -40,7 +42,7 @@ class Parser * * @return mixed A PHP value * - * @throws ParserException If the YAML is not valid + * @throws ParseException If the YAML is not valid */ public function parse($value) { @@ -49,7 +51,7 @@ class Parser $this->lines = explode("\n", $this->cleanup($value)); if (function_exists('mb_detect_encoding') && false === mb_detect_encoding($value, 'UTF-8', true)) { - throw new ParserException('The YAML value does not appear to be valid UTF-8.'); + throw new ParseException('The YAML value does not appear to be valid UTF-8.'); } if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) { @@ -65,7 +67,7 @@ class Parser // tab? if (preg_match('#^\t+#', $this->currentLine)) { - throw new ParserException(sprintf('A YAML file cannot contain tabs as indentation at line %d (%s).', $this->getRealCurrentLineNb() + 1, $this->currentLine)); + throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } $isRef = $isInPlace = $isProcessed = false; @@ -102,13 +104,20 @@ class Parser } } } else if (preg_match('#^(?P'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P.+?))?\s*$#u', $this->currentLine, $values)) { - $key = Inline::parseScalar($values['key']); + try { + $key = Inline::parseScalar($values['key']); + } catch (ParseException $e) { + $e->setParsedLine($this->getRealCurrentLineNb() + 1); + $e->setSnippet($this->currentLine); + + throw $e; + } if ('<<' === $key) { if (isset($values['value']) && '*' === substr($values['value'], 0, 1)) { $isInPlace = substr($values['value'], 1); if (!array_key_exists($isInPlace, $this->refs)) { - throw new ParserException(sprintf('Reference "%s" does not exist at line %s (%s).', $isInPlace, $this->getRealCurrentLineNb() + 1, $this->currentLine)); + throw new ParseException(sprintf('Reference "%s" does not exist.', $isInPlace), $this->getRealCurrentLineNb() + 1, $this->currentLine); } } else { if (isset($values['value']) && $values['value'] !== '') { @@ -123,12 +132,12 @@ class Parser $merged = array(); if (!is_array($parsed)) { - throw new ParserException(sprintf('YAML merge keys used with a scalar value instead of an array at line %s (%s)', $this->getRealCurrentLineNb() + 1, $this->currentLine)); + throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } else if (isset($parsed[0])) { // Numeric array, merge individual elements foreach (array_reverse($parsed) as $parsedItem) { if (!is_array($parsedItem)) { - throw new ParserException(sprintf('Merge items must be arrays at line %s (%s).', $this->getRealCurrentLineNb() + 1, $parsedItem)); + throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem); } $merged = array_merge($parsedItem, $merged); } @@ -168,7 +177,15 @@ class Parser } else { // 1-liner followed by newline if (2 == count($this->lines) && empty($this->lines[1])) { - $value = Inline::parse($this->lines[0]); + try { + $value = Inline::parse($this->lines[0]); + } catch (ParseException $e) { + $e->setParsedLine($this->getRealCurrentLineNb() + 1); + $e->setSnippet($this->currentLine); + + throw $e; + } + if (is_array($value)) { $first = reset($value); if (is_string($first) && '*' === substr($first, 0, 1)) { @@ -189,25 +206,25 @@ class Parser switch (preg_last_error()) { case PREG_INTERNAL_ERROR: - $error = 'Internal PCRE error on line'; + $error = 'Internal PCRE error.'; break; case PREG_BACKTRACK_LIMIT_ERROR: - $error = 'pcre.backtrack_limit reached on line'; + $error = 'pcre.backtrack_limit reached.'; break; case PREG_RECURSION_LIMIT_ERROR: - $error = 'pcre.recursion_limit reached on line'; + $error = 'pcre.recursion_limit reached.'; break; case PREG_BAD_UTF8_ERROR: - $error = 'Malformed UTF-8 data on line'; + $error = 'Malformed UTF-8 data.'; break; case PREG_BAD_UTF8_OFFSET_ERROR: - $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point on line'; + $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.'; break; default: - $error = 'Unable to parse line'; + $error = 'Unable to parse.'; } - throw new ParserException(sprintf('%s %d (%s).', $error, $this->getRealCurrentLineNb() + 1, $this->currentLine)); + throw new ParseException($error, $this->getRealCurrentLineNb() + 1, $this->currentLine); } if ($isRef) { @@ -249,7 +266,7 @@ class Parser * * @return string A YAML string * - * @throws ParserException When indentation problem are detected + * @throws ParseException When indentation problem are detected */ private function getNextEmbedBlock($indentation = null) { @@ -259,7 +276,7 @@ class Parser $newIndent = $this->getCurrentLineIndentation(); if (!$this->isCurrentLineEmpty() && 0 == $newIndent) { - throw new ParserException(sprintf('Indentation problem at line %d (%s)', $this->getRealCurrentLineNb() + 1, $this->currentLine)); + throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } } else { $newIndent = $indentation; @@ -288,7 +305,7 @@ class Parser break; } else { - throw new ParserException(sprintf('Indentation problem at line %d (%s)', $this->getRealCurrentLineNb() + 1, $this->currentLine)); + throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine); } } @@ -326,7 +343,7 @@ class Parser * * @return mixed A PHP value * - * @throws ParserException When reference does not exist + * @throws ParseException When reference does not exist */ private function parseValue($value) { @@ -338,7 +355,7 @@ class Parser } if (!array_key_exists($value, $this->refs)) { - throw new ParserException(sprintf('Reference "%s" does not exist (%s).', $value, $this->currentLine)); + throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLine); } return $this->refs[$value]; @@ -350,7 +367,14 @@ class Parser return $this->parseFoldedScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), intval(abs($modifiers))); } - return Inline::parse($value); + try { + return Inline::parse($value); + } catch (ParseException $e) { + $e->setParsedLine($this->getRealCurrentLineNb() + 1); + $e->setSnippet($this->currentLine); + + throw $e; + } } /** diff --git a/src/Symfony/Component/Yaml/Yaml.php b/src/Symfony/Component/Yaml/Yaml.php index d3124312e6..aad88fdc19 100644 --- a/src/Symfony/Component/Yaml/Yaml.php +++ b/src/Symfony/Component/Yaml/Yaml.php @@ -11,6 +11,8 @@ namespace Symfony\Component\Yaml; +use Symfony\Component\Yaml\Exception\ParseException; + /** * Yaml offers convenience methods to load and dump YAML. * @@ -64,12 +66,14 @@ class Yaml $yaml = new Parser(); try { - $ret = $yaml->parse($input); - } catch (\Exception $e) { - throw new \InvalidArgumentException(sprintf('Unable to parse %s: %s', $file ? sprintf('file "%s"', $file) : 'string', $e->getMessage()), 0, $e); - } + return $yaml->parse($input); + } catch (ParseException $e) { + if ($file) { + $e->setParsedFile($file); + } - return $ret; + throw $e; + } } /** diff --git a/tests/Symfony/Tests/Component/Yaml/ParserTest.php b/tests/Symfony/Tests/Component/Yaml/ParserTest.php index 23edc4330c..bd2716efe9 100644 --- a/tests/Symfony/Tests/Component/Yaml/ParserTest.php +++ b/tests/Symfony/Tests/Component/Yaml/ParserTest.php @@ -13,7 +13,7 @@ namespace Symfony\Tests\Component\Yaml; use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Parser; -use Symfony\Component\Yaml\ParserException; +use Symfony\Component\Yaml\Exception\ParseException; class ParserTest extends \PHPUnit_Framework_TestCase { @@ -80,7 +80,7 @@ class ParserTest extends \PHPUnit_Framework_TestCase $this->fail('YAML files must not contain tabs'); } catch (\Exception $e) { $this->assertInstanceOf('\Exception', $e, 'YAML files must not contain tabs'); - $this->assertEquals('A YAML file cannot contain tabs as indentation at line 2 ('.strpbrk($yaml, "\t").').', $e->getMessage(), 'YAML files must not contain tabs'); + $this->assertEquals('A YAML file cannot contain tabs as indentation at line 2 (near "'.strpbrk($yaml, "\t").'").', $e->getMessage(), 'YAML files must not contain tabs'); } } } @@ -126,7 +126,7 @@ EOF $this->fail('charsets other than UTF-8 are rejected.'); } catch (\Exception $e) { - $this->assertInstanceOf('Symfony\Component\Yaml\ParserException', $e, 'charsets other than UTF-8 are rejected.'); + $this->assertInstanceOf('Symfony\Component\Yaml\Exception\ParseException', $e, 'charsets other than UTF-8 are rejected.'); } } }