phplrt 4.0

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.