diff --git a/src/lexer/PhutilTypeLexer.php b/src/lexer/PhutilTypeLexer.php index c977bf1..0ebe88a 100644 --- a/src/lexer/PhutilTypeLexer.php +++ b/src/lexer/PhutilTypeLexer.php @@ -1,32 +1,32 @@ array( array('\s+', ' '), array('\\|', '|'), array('<', '<'), array('>', '>'), array(',', ','), array('\\?', '?'), array('optional', 'opt'), array('map', 'map'), array('list', 'list'), array('int|float|bool|string|null|callable|wild|regex', 'k'), - array('[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*', 'k'), + array('\\\\?[a-zA-Z_\x7f-\xff]+(\\\\[a-zA-Z_\x7f-\xff]+)*', 'k'), array('\\(', '(', 'comment'), ), 'comment' => array( array('\\)', ')', '!pop'), array('[^\\)]+', 'cm'), ), ); } } diff --git a/src/parser/__tests__/PhutilTypeSpecTestCase.php b/src/parser/__tests__/PhutilTypeSpecTestCase.php index 2863c52..c294c5b 100644 --- a/src/parser/__tests__/PhutilTypeSpecTestCase.php +++ b/src/parser/__tests__/PhutilTypeSpecTestCase.php @@ -1,312 +1,320 @@ ', 'int | null', 'list < string >', 'int (must be even)', 'optional int', 'int?', 'int|null?', 'optional int? (minimum 300)', 'list', 'list>>> (easy)', + '\\SomeClass', + '\\Namespace\\SomeClass', + '\\NamespaceA\\NamespaceB\\NamespaceC', + 'NamespaceA\\NamespaceB\\NamespaceC', ); $bad = array( '', 'list<>', 'list', 'map|map', 'int optional', '(derp)', 'list', 'int?|string', + '\\', + '\\\\', + '\\SomeClass\\', + 'SomeClass\\', ); $good = array_fill_keys($good, true); $bad = array_fill_keys($bad, false); foreach ($good + $bad as $input => $expect) { $caught = null; try { PhutilTypeSpec::newFromString($input); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual( $expect, ($caught === null), $input); } } public function testTypeSpecStringify() { $types = array( 'int', 'list', 'map', 'list>', 'map>', 'int|null', 'int|string|null', 'list', 'list', 'optional int', 'int (even)', ); foreach ($types as $type) { $this->assertEqual( $type, PhutilTypeSpec::newFromString($type)->toString()); } } public function testCanonicalize() { $tests = array( 'int?' => 'optional int', 'int | null' => 'int|null', 'list < map < int , string > > ?' => 'optional list>', 'int ( x )' => 'int ( x )', ); foreach ($tests as $input => $expect) { $this->assertEqual( $expect, PhutilTypeSpec::newFromString($input)->toString(), $input); } } public function testGetCommonParentClass() { $map = array( 'stdClass' => array( array('stdClass', 'stdClass'), ), false => array( array('Exception', 'stdClass'), ), 'Exception' => array( array('Exception', 'RuntimeException'), array('LogicException', 'RuntimeException'), array('BadMethodCallException', 'OutOfBoundsException'), ), ); foreach ($map as $expect => $tests) { if (is_int($expect)) { $expect = (bool)$expect; } foreach ($tests as $input) { list($class_a, $class_b) = $input; $this->assertEqual( $expect, PhutilTypeSpec::getCommonParentClass($class_a, $class_b), print_r($input, true)); } } } public function testGetTypeOf() { $map = array( 'int' => 1, 'string' => 'asdf', 'float' => 1.5, 'bool' => true, 'null' => null, 'map' => array(), 'list' => array('a', 'b'), 'list' => array(1, 2, 3), 'map' => array('x' => 3), 'map>' => array(1 => array('x', 'y')), 'stdClass' => new stdClass(), 'list' => array( new Exception(), new LogicException(), new RuntimeException(), ), 'map' => array('x' => new stdClass()), ); foreach ($map as $expect => $input) { $this->assertEqual( $expect, PhutilTypeSpec::getTypeOf($input), print_r($input, true)); PhutilTypeSpec::newFromString($expect)->check($input); } } public function testTypeCheckFailures() { $map = array( 'int' => 'string', 'string' => 32, 'null' => true, 'bool' => null, 'map' => 16, 'list' => array('y' => 'z'), 'int|null' => 'ducks', 'stdClass' => new Exception(), 'list' => array(new Exception()), ); foreach ($map as $type => $value) { $caught = null; try { PhutilTypeSpec::newFromString($type)->check($value); } catch (PhutilTypeCheckException $ex) { $caught = $ex; } $this->assertTrue($caught instanceof PhutilTypeCheckException); } } public function testCheckMap() { $spec = array( 'count' => 'int', 'color' => 'optional string', ); // Valid PhutilTypeSpec::checkMap( array( 'count' => 1, ), $spec); // Valid, with optional parameter. PhutilTypeSpec::checkMap( array( 'count' => 3, 'color' => 'red', ), $spec); // Parameter "count" is required but missing. $caught = null; try { PhutilTypeSpec::checkMap( array(), $spec); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof PhutilTypeMissingParametersException); // Parameter "size" is specified but does not exist. $caught = null; try { PhutilTypeSpec::checkMap( array( 'count' => 4, 'size' => 'large', ), $spec); } catch (Exception $ex) { $caught = $ex; } $this->assertTrue($caught instanceof PhutilTypeExtraParametersException); } public function testRegexValidation() { PhutilTypeSpec::checkMap( array( 'regex' => '/.*/', ), array( 'regex' => 'regex', )); $caught = null; try { PhutilTypeSpec::checkMap( array( 'regex' => '.*', ), array( 'regex' => 'regex', )); } catch (PhutilTypeCheckException $ex) { $caught = $ex; } $this->assertTrue($caught instanceof PhutilTypeCheckException); } public function testScalarOrListRegexp() { PhutilTypeSpec::checkMap( array( 'regex' => '/.*/', ), array( 'regex' => 'regex | list', )); PhutilTypeSpec::checkMap( array( 'regex' => array('/.*/'), ), array( 'regex' => 'regex | list', )); PhutilTypeSpec::checkMap( array( 'regex' => '/.*/', ), array( 'regex' => 'list | regex', )); PhutilTypeSpec::checkMap( array( 'regex' => array('/.*/'), ), array( 'regex' => 'list | regex', )); $this->assertTrue(true); } public function testMixedVector() { // This is a test case for an issue where we would not infer the type // of a vector containing a mixture of scalar and nonscalar elements // correctly. $caught = null; try { PhutilTypeSpec::checkMap( array( 'key' => array('!', (object)array()), ), array( 'key' => 'list', )); } catch (PhutilTypeCheckException $ex) { $caught = $ex; } $this->assertTrue($caught instanceof PhutilTypeCheckException); } }