Composer Constraints
The little language the require section of every composer.json is written
in:
1^1.2.3 2>=2.0 <3.0.0-beta 3~1.2 || ^2.0 || 3.* 41.0 - 2.0 5dev-main as 1.0.x-dev
Two things make this harder than it looks, and the lexer settles both.
A version is a single token rather than a rule. 1.0.0-beta.1+build is full
of dots, dashes and pluses, and reading them separately would leave the parser
guessing which dash opens a range and which belongs to a pre-release. The one
dash that does open a range is told apart by what follows it - -(?=\s) -
which is the same rule Composer itself goes by.
The second is that a space means "and". >=2.0 <3.0.0-beta is one range built
of two comparisons, and Comparison()+ says so without an operator to read.
Grammar
1/** 2 * ----------------------------------------------------------------------------- 3 * Composer Version Constraint 4 * ----------------------------------------------------------------------------- 5 * 6 * The versions a package is allowed to be installed at, written in the 7 * "require" section of a "composer.json". 8 * 9 * @see https://getcomposer.org/doc/articles/versions.md 10 */ 11 12%pragma root Constraint 13 14%skip T_WHITESPACE \s++ 15 16%token T_OR \|\||\| 17%token T_COMMA , 18 19%token T_CARET \^ 20%token T_TILDE ~ 21%token T_GTE >= 22%token T_LTE <= 23%token T_NEQ != 24%token T_GT > 25%token T_LT < 26%token T_EQ ==|= 27 28%token T_AS (?i)as\b 29%token T_HYPHEN -(?=\s) 30 31// 1.2.3, v1.2, 1.*, 1.2.x, dev-main, master, 1.0.0-beta.1+build 32%token T_VERSION v?[0-9]++(?:\.(?:[0-9]++|[xX*]))*+(?:[.\-+][a-zA-Z0-9][a-zA-Z0-9.\-+]*+)?|[*]|[a-zA-Z][a-zA-Z0-9._\-/]*+ 33 34Constraint 35 : Range() ((::T_OR:: | ::T_COMMA::) Range())* 36 ; 37 38// A space between two constraints reads as "and" 39Range 40 : HyphenRange() 41 | Comparison()+ 42 ; 43 44// 1.0 - 2.0 45HyphenRange 46 : Version() ::T_HYPHEN:: Version() 47 ; 48 49Comparison 50 : Operator()? Version() 51 ; 52 53Operator 54 : <T_CARET> 55 | <T_TILDE> 56 | <T_GTE> 57 | <T_LTE> 58 | <T_NEQ> 59 | <T_GT> 60 | <T_LT> 61 | <T_EQ> 62 ; 63 64// A branch aliased to a version: "dev-main as 1.0.x-dev" 65Version 66 : <T_VERSION> (::T_AS:: <T_VERSION>)? 67 ;
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$constraint = $parser->parse(new Source('~1.2 || ^2.0 || 3.*'));
The grammar recognises the constraint and gives back its parts; deciding
whether a given version satisfies it is a job for PHP, not for a grammar. Hang
a reducer on Comparison and HyphenRange and you have
the matcher.
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.