phplrt 4.0

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.