From 2be674cd2ad0c5e09f53fcadb3c3b729fd669a5c Mon Sep 17 00:00:00 2001 From: Dustin Wilson Date: Fri, 2 Jul 2021 14:16:05 -0500 Subject: [PATCH] Added some comments to grammars and cleaned up exceptions --- lib/Grammar.php | 78 ++++++++++++------------ lib/Grammar/CaptureList.php | 10 +++- lib/Grammar/Exception.php | 89 ++++++++++++++++++++++++++++ lib/Grammar/GrammarInclude.php | 4 ++ lib/Grammar/ImmutableList.php | 51 +++++++++------- lib/Grammar/InjectionList.php | 5 ++ lib/Grammar/NamedPatternListList.php | 10 +++- lib/Grammar/Pattern.php | 1 + lib/Grammar/PatternList.php | 1 + lib/Grammar/Registry.php | 39 +----------- lib/Grammar/Repository.php | 4 ++ 11 files changed, 187 insertions(+), 105 deletions(-) create mode 100644 lib/Grammar/Exception.php diff --git a/lib/Grammar.php b/lib/Grammar.php index a8014c5..8599501 100644 --- a/lib/Grammar.php +++ b/lib/Grammar.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace dW\Lit; use dW\Lit\Grammar\CaptureList, + dW\Lit\Grammar\Exception, dW\Lit\Grammar\GrammarInclude, dW\Lit\Grammar\InjectionList, dW\Lit\Grammar\Pattern, @@ -34,52 +35,37 @@ class Grammar { $this->_repository = $repository; } - + /** Parses an Atom JSON grammar and converts to a Grammar object */ public static function fromJSON(string $jsonPath): self { - assert(is_file($jsonPath), new \Exception("\"$jsonPath\" is either not a file or you do not have permission to read the file\n")); + if (!is_file($jsonPath)) { + throw new Exception(Exception::JSON_INVALID_FILE, $jsonPath); + } $json = json_decode(file_get_contents($jsonPath), true); if ($json === null) { - $message = "Parsing \"$jsonPath\" failed with the following error: "; - switch (json_last_error()) { - case JSON_ERROR_DEPTH: - $message .= 'Maximum stack depth exceeded'; - break; - case JSON_ERROR_STATE_MISMATCH: - $message .= 'Underflow or mode mismatch'; - break; - case JSON_ERROR_CTRL_CHAR: - $message .= 'Unexpected control character found'; - break; - case JSON_ERROR_SYNTAX: - $message .= 'Syntax error, malformed JSON'; - break; - case JSON_ERROR_UTF8: - $message .= 'Malformed UTF-8 characters, possibly incorrectly encoded'; - break; - default: - $message .= 'Unknown error'; - break; - } + throw new Exception(json_last_error() + 200, $jsonPath); + } - throw new \Exception("$message\n"); + if (!isset($json['scopeName'])) { + throw new Exception(Exception::JSON_MISSING_PROPERTY, $jsonPath, 'scopeName'); } - assert(isset($json['scopeName']), new \Exception("\"$jsonPath\" does not have the required scopeName property")); - assert(isset($json['patterns']), new \Exception("\"$jsonPath\" does not have the required patterns property")); + if (!isset($json['patterns'])) { + throw new Exception(Exception::JSON_MISSING_PROPERTY, $jsonPath, 'patterns'); + } $name = $json['name'] ?? null; $scopeName = $json['scopeName']; $contentRegex = (isset($json['contentRegex'])) ? "/{$json['contentRegex']}/" : null; $firstLineMatch = (isset($json['firstLineMatch'])) ? "/{$json['firstLineMatch']}/" : null; - $patterns = self::parseJSONPatternList($json['patterns']); + $patterns = self::parseJSONPatternList($json['patterns'], $jsonPath); $injections = null; if (isset($json['injections'])) { $injections = []; foreach ($json['injections'] as $key => $injection) { - $injsections[$key] = (count($injection) === 1 && key($injection) === 'patterns') ? self::parseJSONPatternList($injection['patterns']) : self::parseJSONPattern($injection); + $injsections[$key] = (count($injection) === 1 && key($injection) === 'patterns') ? self::parseJSONPatternList($injection['patterns'], $jsonPath) : self::parseJSONPattern($injection, $jsonPath); } if (count($injections) > 0) { @@ -93,7 +79,7 @@ class Grammar { if (isset($json['repository'])) { $respository = []; foreach ($json['repository'] as $key => $r) { - $repository[$key] = (count($r) === 1 && key($r) === 'patterns') ? self::parseJSONPatternList($r['patterns']) : self::parseJSONPattern($r); + $repository[$key] = (count($r) === 1 && key($r) === 'patterns') ? self::parseJSONPatternList($r['patterns'], $jsonPath) : self::parseJSONPattern($r, $jsonPath); } if (count($repository) > 0) { @@ -107,7 +93,7 @@ class Grammar { } - protected static function parseJSONPattern(array $pattern): GrammarInclude|Pattern|null { + protected static function parseJSONPattern(array $pattern, string $jsonPath): GrammarInclude|Pattern|null { if (array_keys($pattern) === [ 'include' ]) { return new GrammarInclude($pattern['include']); } @@ -129,7 +115,10 @@ class Grammar { foreach ($pattern as $key => $value) { switch ($key) { case 'applyEndPatternLast': - assert(is_bool($value) || (is_int($value) && ($value === 0 || $value === 1)), new \Exception("The value for applyEndPatternLast must be either a boolean, 0, or 1\n")); + if (!is_bool($value) || (!is_int($value) && ($value !== 0 && $value !== 1))) { + throw new Exception(Exception::JSON_INVALID_TYPE, 'Boolean, 0, or 1', 'applyEndPatternLast', gettype($value), $jsonPath); + } + $value = (bool)$value; case 'name': case 'contentName': @@ -145,7 +134,10 @@ class Grammar { case 'captures': case 'beginCaptures': case 'endCaptures': - assert(is_array($value), new \Exception("Array value expected for '$key', found " . gettype($value) . "\n")); + if (!is_array($value)) { + throw new Exception(Exception::JSON_INVALID_TYPE, 'Array', $key, gettype($value), $jsonPath); + } + if (count($value) === 0) { continue 2; } @@ -159,20 +151,27 @@ class Grammar { continue; } - assert(strspn($kkkk, '0123456789') === strlen($kkkk), new \Exception("\"$kkkk\" is not castable to an integer for use in a capture list\n")); + if (strspn($kkkk, '0123456789') !== strlen($kkkk)) { + throw new Exception(Exception::JSON_INVALID_TYPE, 'Integer', 'capture list index', $kkkk, $jsonPath); + } + $kkk = (int)$kkkk; } - $v = array_map(function ($n) { - return (count($n) === 1 && key($n) === 'patterns') ? self::parseJSONPatternList($n['patterns']) : self::parseJSONPattern($n); + $v = array_map(function($n) use ($jsonPath) { + return (count($n) === 1 && key($n) === 'patterns') ? self::parseJSONPatternList($n['patterns'], $jsonPath) : self::parseJSONPattern($n, $jsonPath); }, $v); $p[$key] = new CaptureList(array_combine($kk, $v)); $modified = true; break; case 'patterns': - assert(is_array($value), new \Exception("Array value expected for '$key', found " . gettype($value) . "\n")); - $p[$key] = self::parseJSONPatternList($value); + if (!is_array($value)) { + // '%1$s expected for %2$s, found %3$s in "%4$s"' + throw new Exception(Exception::JSON_INVALID_TYPE, 'Array', $key, gettype($value), $jsonPath); + } + + $p[$key] = self::parseJSONPatternList($value, $jsonPath); $modified = true; break; } @@ -181,11 +180,10 @@ class Grammar { return ($modified) ? new Pattern(...$p) : null; } - - protected static function parseJSONPatternList(array $list): ?PatternList { + protected static function parseJSONPatternList(array $list, string $jsonPath): ?PatternList { $result = []; foreach ($list as $pattern) { - $p = self::parseJSONPattern($pattern); + $p = self::parseJSONPattern($pattern, $jsonPath); if ($p !== null) { $result[] = $p; } diff --git a/lib/Grammar/CaptureList.php b/lib/Grammar/CaptureList.php index f001f87..52c67b7 100644 --- a/lib/Grammar/CaptureList.php +++ b/lib/Grammar/CaptureList.php @@ -8,9 +8,15 @@ namespace dW\Lit\Grammar; class CaptureList extends ImmutableList { public function __construct(array $array) { + /* This shit is here because PHP doesn't have array types or generics :) */ foreach ($array as $k => $v) { - assert(is_int($k), new \Exception('Integer index expected for supplied array, found ' . gettype($k) . "\n")); - assert($v instanceof GrammarInclude || $v instanceof Pattern || $v instanceof PatternList, new \Exception(__NAMESPACE__ . '\GrammarInclude, ' . __NAMESPACE__ . '\Pattern, or ' . __NAMESPACE__ . '\PatternList value expected for supplied array, found ' . gettype($v) . "\n")); + if (!is_int($k)) { + throw new Exception(Exception::LIST_INVALID_TYPE, 'Integer', 'supplied array index', gettype($k)); + } + + if (!$v instanceof GrammarInclude && !$v instanceof Pattern && !$v instanceof PatternList) { + throw new Exception(Exception::LIST_INVALID_TYPE, __NAMESPACE__.'\GrammarInclude, '.__NAMESPACE__.'\Pattern, or '.__NAMESPACE__.'\PatternList', 'supplied array value', gettype($v)); + } } $this->storage = $array; diff --git a/lib/Grammar/Exception.php b/lib/Grammar/Exception.php new file mode 100644 index 0000000..73e22d6 --- /dev/null +++ b/lib/Grammar/Exception.php @@ -0,0 +1,89 @@ + 'Invalid error code', + 101 => 'Unknown error; escaping', + 102 => 'Incorrect number of parameters for Exception message; %s expected', + 103 => 'Unreachable code', + + 200 => '"%s" is either not a file or you do not have permission to read the file', + 201 => '"%s" is invalid or malformed JSON', + 202 => 'Invalid control character encountered when parsing "%s"', + 203 => 'Syntax error, malformed JSON when parsing "%s"', + 204 => 'Malformed UTF-8 characters, possibly incorrectly encoded when parsing "%s"', + 205 => 'One or more recursive references could not be encoded when parsing "%s"', + 206 => 'One or more NAN or INF values could not be encoded when parsing "%s"', + 207 => 'Unsupported type encountered when parsing "%s"', + 208 => 'Invalid property name encountered when parsing "%s"', + 209 => 'Malformed UTF-16 characters, possibly incorrectly encoded when parsing "%s"', + 210 => '"%1$s" does not have the required %2$s property', + 211 => '%1$s expected for %2$s, found %3$s in "%4$s"', + + 300 => '%s is immutable', + 301 => 'Invalid %1$s index at offset %2$s', + 302 => '%1$s expected for %2$s, found %3$s' + ]; + + public function __construct(int $code, ...$args) { + if (!isset(self::$messages[$code])) { + throw new self(self::INVALID_CODE); + } + + $message = self::$messages[$code]; + $previous = null; + + if ($args) { + // Grab a previous exception if there is one. + if ($args[0] instanceof \Throwable) { + $previous = array_shift($args); + } elseif (end($args) instanceof \Throwable) { + $previous = array_pop($args); + } + } + + // Count the number of replacements needed in the message. + preg_match_all('/(\%(?:\d+\$)?s)/', $message, $matches); + $count = count(array_unique($matches[1])); + + // If the number of replacements don't match the arguments then oops. + if (count($args) !== $count) { + throw new self(self::INCORRECT_PARAMETERS_FOR_MESSAGE, $count); + } + + if ($count > 0) { + // Go through each of the arguments and run sprintf on the strings. + $message = call_user_func_array('sprintf', [ $message, ...$args ]); + } + + parent::__construct("$message\n", $code, $previous); + } +} diff --git a/lib/Grammar/GrammarInclude.php b/lib/Grammar/GrammarInclude.php index 45bde0c..bf702f4 100644 --- a/lib/Grammar/GrammarInclude.php +++ b/lib/Grammar/GrammarInclude.php @@ -7,6 +7,10 @@ declare(strict_types=1); namespace dW\Lit\Grammar; use dW\Lit\FauxReadOnly; +/** + * This allows for referencing a different language, recursively referencing the + * grammar itself, or a rule declared in the file's repository. + */ class GrammarInclude { use FauxReadOnly; diff --git a/lib/Grammar/ImmutableList.php b/lib/Grammar/ImmutableList.php index d5a2ed1..b30cb0d 100644 --- a/lib/Grammar/ImmutableList.php +++ b/lib/Grammar/ImmutableList.php @@ -11,31 +11,15 @@ abstract class ImmutableList implements \ArrayAccess, \Countable, \Iterator { protected int|string|null $position; protected array $storage = []; + public function __construct(...$values) { $this->storage = $values; $this->count = count($this->storage); } - public function offsetSet($offset, $value) { - throw new \Exception(__CLASS__ . "s are immutable\n"); - } - public function offsetExists($offset) { - return isset($this->storage[$offset]); - } - - public function offsetUnset($offset) { - throw new \Exception(__CLASS__ . "s are immutable\n"); - } - - public function offsetGet($offset) { - assert(isset($this->storage[$offset]), new \Exception("Invalid ImmutableList index at $offset\n")); - return $this->storage[$offset]; - } - - public function rewind() { - reset($this->storage); - $this->position = key($this->storage); + public function count(): int { + return $this->count; } public function current() { @@ -52,11 +36,32 @@ abstract class ImmutableList implements \ArrayAccess, \Countable, \Iterator { $this->position = key($this->storage); } - public function valid() { - return $this->offsetExists($this->position); + public function offsetExists($offset) { + return isset($this->storage[$offset]); } - public function count(): int { - return $this->count; + public function offsetGet($offset) { + if (!isset($this->storage[$offset])) { + throw new Exception(Exception::LIST_INVALID_INDEX, __CLASS__, $offset); + } + + return $this->storage[$offset]; + } + + public function offsetSet($offset, $value) { + throw new Exception(Exception::LIST_IMMUTABLE, __CLASS__); + } + + public function offsetUnset($offset) { + throw new Exception(Exception::LIST_IMMUTABLE, __CLASS__); + } + + public function rewind() { + reset($this->storage); + $this->position = key($this->storage); + } + + public function valid() { + return $this->offsetExists($this->position); } } diff --git a/lib/Grammar/InjectionList.php b/lib/Grammar/InjectionList.php index 55da791..488a823 100644 --- a/lib/Grammar/InjectionList.php +++ b/lib/Grammar/InjectionList.php @@ -6,4 +6,9 @@ declare(strict_types=1); namespace dW\Lit\Grammar; +/** + * An immutable list of injection pattern rules which allows for creation of a + * new grammar; instead of applying to an entire file it's instead applied to a + * specific scope selector. + */ class InjectionList extends NamedPatternListList {} \ No newline at end of file diff --git a/lib/Grammar/NamedPatternListList.php b/lib/Grammar/NamedPatternListList.php index 0bf79e1..f56690e 100644 --- a/lib/Grammar/NamedPatternListList.php +++ b/lib/Grammar/NamedPatternListList.php @@ -8,9 +8,15 @@ namespace dW\Lit\Grammar; abstract class NamedPatternListList extends ImmutableList { public function __construct(array $array) { + /* This shit is here because PHP doesn't have array types or generics :) */ foreach ($array as $k => $v) { - assert(is_string($k), new \Exception('String index expected for supplied array, found ' . gettype($k) . "\n")); - assert($v instanceof GrammarInclude || $v instanceof Pattern || $v instanceof PatternList, new \Exception(__NAMESPACE__ . '\GrammarInclude, ' . __NAMESPACE__ . '\Pattern, or ' . __NAMESPACE__ . '\PatternList value expected for supplied array, found ' . gettype($v) . "\n")); + if (!is_string($k)) { + throw new Exception(Exception::LIST_INVALID_TYPE, 'String', 'supplied array index', gettype($k)); + } + + if (!$v instanceof GrammarInclude && !$v instanceof Pattern && !$v instanceof PatternList) { + throw new Exception(Exception::LIST_INVALID_TYPE, __NAMESPACE__.'\GrammarInclude, '.__NAMESPACE__.'\Pattern, or '.__NAMESPACE__.'\PatternList', 'supplied array value', gettype($v)); + } } $this->storage = $array; diff --git a/lib/Grammar/Pattern.php b/lib/Grammar/Pattern.php index 77a24dd..fcee171 100644 --- a/lib/Grammar/Pattern.php +++ b/lib/Grammar/Pattern.php @@ -8,6 +8,7 @@ namespace dW\Lit\Grammar; use dW\Lit\FauxReadOnly; use dW\Lit\Grammar; +/** Rule responsible for matching a portion of the document */ class Pattern { use FauxReadOnly; diff --git a/lib/Grammar/PatternList.php b/lib/Grammar/PatternList.php index 74b9538..1d40d49 100644 --- a/lib/Grammar/PatternList.php +++ b/lib/Grammar/PatternList.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace dW\Lit\Grammar; +/** Immutable list of pattern rules */ class PatternList extends ImmutableList { public function __construct(Pattern|GrammarInclude ...$values) { parent::__construct(...$values); diff --git a/lib/Grammar/Registry.php b/lib/Grammar/Registry.php index 6acb268..bc0a212 100644 --- a/lib/Grammar/Registry.php +++ b/lib/Grammar/Registry.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace dW\Lit\Grammar; +/** Static storage for grammars; a map of scope and a Grammar object */ class Registry { protected static array $grammars = []; @@ -33,42 +34,4 @@ class Registry { return false; } - - public static function validate(string $grammar): bool { - if ($grammar === null) { - throw new \Exception("\"$jsonPath\" is not a valid grammar JSON file.".\PHP_EOL); - } - - $requiredProperties = [ - 'name', - 'patterns', - 'scopeName' - ]; - - $missing = []; - foreach ($requiredProperties as $r) { - if (!array_key_exists($r, $grammar))) { - $missing = $r; - } - } - - $missingLen = count($missing); - if ($missingLen > 0) { - if ($missingLen > 1) { - if ($missingLen > 2) { - $last = array_pop($missing); - $missing = implode(', ', $missing); - $missing .= ", and $last"; - } else { - $missing = implode(' and ', $missing); - } - - throw new \Exception("\"$jsonPath\" is missing the required $missing properties.".\PHP_EOL); - } - - throw new \Exception("\"$jsonPath\" is missing the required {$missing[0]} property.".\PHP_EOL); - } - - return true; - } } \ No newline at end of file diff --git a/lib/Grammar/Repository.php b/lib/Grammar/Repository.php index 60ffb72..45a31dd 100644 --- a/lib/Grammar/Repository.php +++ b/lib/Grammar/Repository.php @@ -6,4 +6,8 @@ declare(strict_types=1); namespace dW\Lit\Grammar; +/** + * An immutable list of rules which can be included from other places in the + * grammar; The key is the name of the rule and the value is the actual rule. + */ class Repository extends NamedPatternListList {} \ No newline at end of file