PhpDoc Types
The type language of phpstan and
psalm - the one hiding inside @param, @return and
@var, which has quietly grown into a language with generics, shapes and
conditionals:
1array{id: positive-int, nickname?: ?non-empty-string, handler: callable(int): void} 2 3\Closure<TKey of array-key, TValue super Demo\Entity\User>(TKey $key, int ...$rest): TValue 4 5($size is positive-int ? non-empty-array<int, User> : Demo\Status::ACTIVE)
A fully qualified name is one token, not a rule. \Demo\Entity\User and
non-empty-string are both read by T_IDENTIFIER in a single step, so the
backslash and the dash never reach the parser and never have to be told apart
from an operator.
Above it, Type reads an Atomic and then a chain of unions or a chain of
intersections, never both. A|B|C and A&B are read; A|B&C is refused
until the brackets say how it groups, which is the reading
phpstan/phpdoc-parser insists on as well.
The line to steal is at the top, though: %pragma lexer.pcre.disable u turns
the u modifier off for the whole lexer. Identifiers here are written of
bytes rather than of code points, which is what lets \x80-\xff stand for
"any character a name outside ASCII is built from".
Grammar
1/** 2 * ----------------------------------------------------------------------------- 3 * PHPDoc Type Expression Of PHPStan 4 * ----------------------------------------------------------------------------- 5 * 6 * The type language read by "phpstan/phpdoc-parser". 7 * 8 * @see https://github.com/phpstan/phpdoc-parser/blob/2.3.x/doc/grammars/type.abnf 9 */ 10 11// An identifier is written of octets, so "\x80-\xff" names every byte a name 12// outside ASCII is written of 13%pragma lexer.pcre.disable u 14 15%pragma root Type 16 17/** 18 * ----------------------------------------------------------------------------- 19 * Lexemes 20 * ----------------------------------------------------------------------------- 21 */ 22 23%skip T_WHITESPACE \s++ 24 25%token T_CONSTANT_FLOAT [+\-]?(?:[0-9]++(?:_[0-9]++)*+\.(?:[0-9]++(?:_[0-9]++)*+)?(?:[eE][+\-]?[0-9]++(?:_[0-9]++)*+)?|[0-9]++(?:_[0-9]++)*+(?:[eE][+\-]?[0-9]++(?:_[0-9]++)*+)|\.[0-9]++(?:_[0-9]++)*+(?:[eE][+\-]?[0-9]++(?:_[0-9]++)*+)?) 26%token T_CONSTANT_INT [+\-]?(?:0[bB][01]++(?:_[01]++)*+|0[oO][0-7]++(?:_[0-7]++)*+|0[xX][0-9a-fA-F]++(?:_[0-9a-fA-F]++)*+|[0-9]++(?:_[0-9]++)*+) 27%token T_CONSTANT_STRING '(?:\\[^\r\n]|[^\r\n\\'])*+'|"(?:\\[^\r\n]|[^\r\n\\"])*+" 28 29%token T_THIS_VARIABLE \$this(?![a-zA-Z0-9_\x80-\xff]) 30%token T_VARIABLE \$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*+ 31 32// The words a template argument, a variance and a conditional are written of: 33// "T of User", "covariant T", "T is not int ? A : B" 34%token T_CONTRAVARIANT contravariant(?![a-zA-Z0-9_\-\\\x80-\xff]) 35%token T_COVARIANT covariant(?![a-zA-Z0-9_\-\\\x80-\xff]) 36%token T_SUPER super(?![a-zA-Z0-9_\-\\\x80-\xff]) 37%token T_NOT not(?![a-zA-Z0-9_\-\\\x80-\xff]) 38%token T_IS is(?![a-zA-Z0-9_\-\\\x80-\xff]) 39%token T_OF of(?![a-zA-Z0-9_\-\\\x80-\xff]) 40 41// Every part of a name at once: "\Demo\Entity\User", "non-empty-string" 42%token T_IDENTIFIER \\?[a-zA-Z_\x80-\xff][a-zA-Z0-9_\-\x80-\xff]*+(?:\\[a-zA-Z_\x80-\xff][a-zA-Z0-9_\-\x80-\xff]*+)*+ 43 44%token T_VARIADIC \.\.\. 45%token T_DOUBLE_COLON :: 46%token T_COLON : 47%token T_EQUAL_SIGN = 48%token T_UNION \| 49%token T_INTERSECTION & 50%token T_NULLABLE \? 51%token T_WILDCARD \* 52%token T_COMMA , 53 54%token T_PARENTHESES_OPEN \( 55%token T_PARENTHESES_CLOSE \) 56%token T_ANGLE_BRACKET_OPEN < 57%token T_ANGLE_BRACKET_CLOSE > 58%token T_SQUARE_BRACKET_OPEN \[ 59%token T_SQUARE_BRACKET_CLOSE \] 60%token T_CURLY_BRACKET_OPEN \{ 61%token T_CURLY_BRACKET_CLOSE \} 62 63/** 64 * ----------------------------------------------------------------------------- 65 * Type 66 * ----------------------------------------------------------------------------- 67 */ 68 69Type 70 : Atomic() (Union() | Intersection())? 71 | Nullable() 72 ; 73 74// A type written inside a pair of parentheses, where a conditional is written 75ParenthesizedType 76 : <T_VARIABLE> Conditional() 77 | Atomic() (Union() | Intersection() | Conditional())? 78 | Nullable() 79 ; 80 81Union 82 : (::T_UNION:: Atomic())+ 83 ; 84 85Intersection 86 : (::T_INTERSECTION:: Atomic())+ 87 ; 88 89// is int ? string : bool 90Conditional 91 : ::T_IS:: <T_NOT>? Atomic() 92 ::T_NULLABLE:: Type() 93 ::T_COLON:: ParenthesizedType() 94 ; 95 96// ?int 97Nullable 98 : ::T_NULLABLE:: Atomic() 99 ; 100 101Atomic 102 : <T_CONSTANT_FLOAT> 103 | <T_CONSTANT_INT> 104 | <T_CONSTANT_STRING> 105 | <T_THIS_VARIABLE> 106 | ::T_PARENTHESES_OPEN:: ParenthesizedType() ::T_PARENTHESES_CLOSE:: Array()? 107 | Identifier() ( 108 Callable() 109 | Generic() 110 | ArrayShape() 111 | Array() 112 | ClassConstant() 113 )? 114 ; 115 116Identifier 117 : <T_IDENTIFIER> 118 | <T_IS> 119 | <T_NOT> 120 | <T_OF> 121 | <T_SUPER> 122 | <T_COVARIANT> 123 | <T_CONTRAVARIANT> 124 ; 125 126// Demo\Status::ACTIVE 127ClassConstant 128 : ::T_DOUBLE_COLON:: Identifier() 129 ; 130 131/** 132 * ----------------------------------------------------------------------------- 133 * Generic 134 * ----------------------------------------------------------------------------- 135 */ 136 137// <int, non-empty-string> 138Generic 139 : ::T_ANGLE_BRACKET_OPEN:: 140 GenericTypeArgument() (::T_COMMA:: GenericTypeArgument())* 141 ::T_ANGLE_BRACKET_CLOSE:: 142 ; 143 144GenericTypeArgument 145 : (<T_CONTRAVARIANT> | <T_COVARIANT>)? Type() 146 | <T_WILDCARD> 147 ; 148 149/** 150 * ----------------------------------------------------------------------------- 151 * Callable 152 * ----------------------------------------------------------------------------- 153 */ 154 155// <T of User>(T $value, int ...$rest): T 156Callable 157 : CallableTemplate()? 158 ::T_PARENTHESES_OPEN:: CallableParameters()? ::T_PARENTHESES_CLOSE:: 159 ::T_COLON:: CallableReturnType() 160 ; 161 162CallableTemplate 163 : ::T_ANGLE_BRACKET_OPEN:: 164 CallableTemplateArgument() (::T_COMMA:: CallableTemplateArgument())* 165 ::T_ANGLE_BRACKET_CLOSE:: 166 ; 167 168CallableTemplateArgument 169 : Identifier() 170 (::T_OF:: Type())? 171 (::T_SUPER:: Type())? 172 (::T_EQUAL_SIGN:: Type())? 173 ; 174 175CallableParameters 176 : CallableParameter() (::T_COMMA:: CallableParameter())* 177 ; 178 179// The "&" of a parameter read by reference, the "..." of a variadic one, the 180// name it is called by and the "=" of an optional one 181CallableParameter 182 : Type() 183 <T_INTERSECTION>? 184 <T_VARIADIC>? 185 <T_VARIABLE>? 186 <T_EQUAL_SIGN>? 187 ; 188 189CallableReturnType 190 : Identifier() Generic()? 191 | Nullable() 192 | ::T_PARENTHESES_OPEN:: Type() ::T_PARENTHESES_CLOSE:: 193 ; 194 195/** 196 * ----------------------------------------------------------------------------- 197 * Array And Shape 198 * ----------------------------------------------------------------------------- 199 */ 200 201// [][] 202Array 203 : (::T_SQUARE_BRACKET_OPEN:: ::T_SQUARE_BRACKET_CLOSE::)+ 204 ; 205 206// {id: int, name?: string, 0: bool} 207ArrayShape 208 : ::T_CURLY_BRACKET_OPEN:: 209 ArrayShapeItem() (::T_COMMA:: ArrayShapeItem())* 210 ::T_CURLY_BRACKET_CLOSE:: 211 ; 212 213ArrayShapeItem 214 : ArrayShapeKey() <T_NULLABLE>? ::T_COLON:: Type() 215 | Type() 216 ; 217 218ArrayShapeKey 219 : <T_CONSTANT_STRING> 220 | <T_CONSTANT_INT> 221 | Identifier() 222 ;
Usage
1use Phplrt\Compiler\Compiler; 2use Phplrt\Source\File; 3use Phplrt\Source\Source; 4 5$parser = new Compiler() 6 ->load(new File(__DIR__ . '/grammar.pp3')) 7 ->getParser(); 8 9$type = $parser->parse(new Source('list<non-empty-string>'));
This is the grammar to copy when a docblock has to be understood rather than matched with a regular expression - by a static analyser, a serializer, a mapper or an IDE plugin. The grammars repository also carries the PSR-5 and TypeLang readings of the same idea, which are smaller and stricter.
25+ more grammars. phplrt/grammars collects ready to read grammars for real languages - JSON5, TSV, semantic versions, DQL, PHQL, JMS types, PSR-5 and Doctrine annotations, Symfony expressions, Go! AOP pointcuts, Praspel contracts and more - each with sample inputs and a test that keeps it honest.