Browse Source

Added some comments to grammars and cleaned up exceptions

main
Dustin Wilson 3 years ago
parent
commit
2be674cd2a
  1. 78
      lib/Grammar.php
  2. 10
      lib/Grammar/CaptureList.php
  3. 89
      lib/Grammar/Exception.php
  4. 4
      lib/Grammar/GrammarInclude.php
  5. 51
      lib/Grammar/ImmutableList.php
  6. 5
      lib/Grammar/InjectionList.php
  7. 10
      lib/Grammar/NamedPatternListList.php
  8. 1
      lib/Grammar/Pattern.php
  9. 1
      lib/Grammar/PatternList.php
  10. 39
      lib/Grammar/Registry.php
  11. 4
      lib/Grammar/Repository.php

78
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;
}

10
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;

89
lib/Grammar/Exception.php

@ -0,0 +1,89 @@
<?php
/** @license MIT
* Copyright 2017 , Dustin Wilson, J. King et al.
* See LICENSE and AUTHORS files for details */
declare(strict_types=1);
namespace dW\Lit\Grammar;
class Exception extends \Exception {
const INVALID_CODE = 100;
const UNKNOWN_ERROR = 101;
const INCORRECT_PARAMETERS_FOR_MESSAGE = 102;
const UNREACHABLE_CODE = 103;
const JSON_INVALID_FILE = 200;
const JSON_ERROR_STATE_MISMATCH = 201;
const JSON_ERROR_CTRL_CHAR = 202;
const JSON_ERROR_SYNTAX = 203;
const JSON_ERROR_UTF8 = 204;
const JSON_ERROR_RECURSION = 205;
const JSON_ERROR_INF_OR_NAN = 206;
const JSON_ERROR_UNSUPPORTED_TYPE = 207;
const JSON_ERROR_INVALID_PROPERTY_NAME = 208;
const JSON_ERROR_UTF16 = 209;
const JSON_MISSING_PROPERTY = 210;
const JSON_INVALID_TYPE = 211;
const LIST_IMMUTABLE = 300;
const LIST_INVALID_INDEX = 301;
const LIST_INVALID_TYPE = 302;
protected static $messages = [
100 => '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);
}
}

4
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;

51
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);
}
}

5
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 {}

10
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;

1
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;

1
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);

39
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;
}
}

4
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 {}
Loading…
Cancel
Save