Skip to content

Commit 7312770

Browse files
committed
Improved version constraint parser
1 parent bfd216a commit 7312770

File tree

2 files changed

+155
-47
lines changed

2 files changed

+155
-47
lines changed

Diff for: constraints.go

+108-27
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ type Constraint interface {
2121

2222
// ParseConstraint converts a string into a Constraint. The resulting Constraint
2323
// may be converted back to string using the String() method.
24-
// WIP: only simple constraint (like ""=1.2.0" or ">=2.0.0) are parsed for now
25-
// a full parser will be deployed in the future
2624
func ParseConstraint(in string) (Constraint, error) {
2725
in = strings.TrimSpace(in)
2826
curr := 0
@@ -37,14 +35,19 @@ func ParseConstraint(in string) (Constraint, error) {
3735
}
3836
return 0
3937
}
38+
skipSpace := func() {
39+
for curr < l && in[curr] == ' ' {
40+
curr++
41+
}
42+
}
4043
peek := func() byte {
4144
if curr < l {
4245
return in[curr]
4346
}
4447
return 0
4548
}
4649

47-
ver := func() (*Version, error) {
50+
version := func() (*Version, error) {
4851
start := curr
4952
for {
5053
n := peek()
@@ -58,56 +61,134 @@ func ParseConstraint(in string) (Constraint, error) {
5861
}
5962
}
6063

61-
stack := []Constraint{}
62-
for {
64+
var terminal func() (Constraint, error)
65+
var constraint func() (Constraint, error)
66+
67+
terminal = func() (Constraint, error) {
68+
skipSpace()
6369
switch next() {
70+
case '!':
71+
expr, err := terminal()
72+
if err != nil {
73+
return nil, err
74+
}
75+
return &Not{expr}, nil
76+
case '(':
77+
expr, err := constraint()
78+
if err != nil {
79+
return nil, err
80+
}
81+
skipSpace()
82+
if c := next(); c != ')' {
83+
return nil, fmt.Errorf("unexpected char at: %s", in[curr-1:])
84+
}
85+
return expr, nil
6486
case '=':
65-
if v, err := ver(); err == nil {
66-
stack = append(stack, &Equals{v})
67-
} else {
87+
v, err := version()
88+
if err != nil {
6889
return nil, err
6990
}
91+
return &Equals{v}, nil
7092
case '>':
7193
if peek() == '=' {
7294
next()
73-
if v, err := ver(); err == nil {
74-
stack = append(stack, &GreaterThanOrEqual{v})
75-
} else {
95+
v, err := version()
96+
if err != nil {
7697
return nil, err
7798
}
99+
return &GreaterThanOrEqual{v}, nil
78100
} else {
79-
if v, err := ver(); err == nil {
80-
stack = append(stack, &GreaterThan{v})
81-
} else {
101+
v, err := version()
102+
if err != nil {
82103
return nil, err
83104
}
105+
return &GreaterThan{v}, nil
84106
}
85107
case '<':
86108
if peek() == '=' {
87109
next()
88-
if v, err := ver(); err == nil {
89-
stack = append(stack, &LessThanOrEqual{v})
90-
} else {
110+
v, err := version()
111+
if err != nil {
91112
return nil, err
92113
}
114+
return &LessThanOrEqual{v}, nil
93115
} else {
94-
if v, err := ver(); err == nil {
95-
stack = append(stack, &LessThan{v})
96-
} else {
116+
v, err := version()
117+
if err != nil {
97118
return nil, err
98119
}
120+
return &LessThan{v}, nil
99121
}
100-
case ' ':
101-
// ignore
102122
default:
103123
return nil, fmt.Errorf("unexpected char at: %s", in[curr-1:])
104-
case 0:
105-
if len(stack) != 1 {
106-
return nil, fmt.Errorf("invalid constraint: %s", in)
124+
}
125+
}
126+
127+
andExpr := func() (Constraint, error) {
128+
t1, err := terminal()
129+
if err != nil {
130+
return nil, err
131+
}
132+
stack := []Constraint{t1}
133+
134+
for {
135+
skipSpace()
136+
if peek() != '&' {
137+
if len(stack) == 1 {
138+
return stack[0], nil
139+
}
140+
return &And{stack}, nil
141+
}
142+
next()
143+
if peek() != '&' {
144+
return nil, fmt.Errorf("unexpected char at: %s", in[curr-1:])
145+
}
146+
next()
147+
148+
t2, err := terminal()
149+
if err != nil {
150+
return nil, err
107151
}
108-
return stack[0], nil
152+
stack = append(stack, t2)
109153
}
110154
}
155+
156+
constraint = func() (Constraint, error) {
157+
t1, err := andExpr()
158+
if err != nil {
159+
return nil, err
160+
}
161+
stack := []Constraint{t1}
162+
163+
for {
164+
skipSpace()
165+
switch peek() {
166+
case '|':
167+
next()
168+
if peek() != '|' {
169+
return nil, fmt.Errorf("unexpected char at: %s", in[curr-1:])
170+
}
171+
next()
172+
173+
t2, err := andExpr()
174+
if err != nil {
175+
return nil, err
176+
}
177+
stack = append(stack, t2)
178+
179+
case 0, ')':
180+
if len(stack) == 1 {
181+
return stack[0], nil
182+
}
183+
return &Or{stack}, nil
184+
185+
default:
186+
return nil, fmt.Errorf("unexpected char at: %s", in[curr-1:])
187+
}
188+
}
189+
}
190+
191+
return constraint()
111192
}
112193

113194
// True is the empty constraint
@@ -259,7 +340,7 @@ func (not *Not) Match(v *Version) bool {
259340

260341
func (not *Not) String() string {
261342
op := not.Operand.String()
262-
if op[0] != '(' {
343+
if op == "" || op[0] != '(' {
263344
return "!(" + op + ")"
264345
}
265346
return "!" + op

Diff for: constraints_test.go

+47-20
Original file line numberDiff line numberDiff line change
@@ -80,23 +80,41 @@ func TestConstraints(t *testing.T) {
8080
}
8181

8282
func TestConstraintsParser(t *testing.T) {
83-
good := map[string]string{
84-
"": "",
85-
"=1.3.0": "=1.3.0",
86-
" =1.3.0 ": "=1.3.0",
87-
"=1.3.0 ": "=1.3.0",
88-
" =1.3.0": "=1.3.0",
89-
">=1.3.0": ">=1.3.0",
90-
">1.3.0": ">1.3.0",
91-
"<=1.3.0": "<=1.3.0",
92-
"<1.3.0": "<1.3.0",
83+
type goodStringTest struct {
84+
In, Out string
9385
}
94-
for s, r := range good {
95-
p, err := ParseConstraint(s)
96-
require.NoError(t, err)
97-
require.Equal(t, r, p.String())
98-
fmt.Printf("'%s' parsed as %s\n", s, p.String())
86+
good := []goodStringTest{
87+
{"", ""}, // always true
88+
{"=1.3.0", "=1.3.0"},
89+
{" =1.3.0 ", "=1.3.0"},
90+
{"=1.3.0 ", "=1.3.0"},
91+
{" =1.3.0", "=1.3.0"},
92+
{">=1.3.0", ">=1.3.0"},
93+
{">1.3.0", ">1.3.0"},
94+
{"<=1.3.0", "<=1.3.0"},
95+
{"<1.3.0", "<1.3.0"},
96+
{"(=1.4.0)", "=1.4.0"},
97+
{"!(=1.4.0)", "!(=1.4.0)"},
98+
{"!(((=1.4.0)))", "!(=1.4.0)"},
99+
{"=1.2.4 && =1.3.0", "(=1.2.4 && =1.3.0)"},
100+
{"=1.2.4 && =1.3.0 && =1.2.0", "(=1.2.4 && =1.3.0 && =1.2.0)"},
101+
{"=1.2.4 && =1.3.0 || =1.2.0", "((=1.2.4 && =1.3.0) || =1.2.0)"},
102+
{"=1.2.4 || =1.3.0 && =1.2.0", "(=1.2.4 || (=1.3.0 && =1.2.0))"},
103+
{"(=1.2.4 || =1.3.0) && =1.2.0", "((=1.2.4 || =1.3.0) && =1.2.0)"},
104+
{"(=1.2.4 || !>1.3.0) && =1.2.0", "((=1.2.4 || !(>1.3.0)) && =1.2.0)"},
105+
{"!(=1.2.4 || >1.3.0) && =1.2.0", "(!(=1.2.4 || >1.3.0) && =1.2.0)"},
99106
}
107+
for i, test := range good {
108+
in := test.In
109+
out := test.Out
110+
t.Run(fmt.Sprintf("GoodString%03d", i), func(t *testing.T) {
111+
p, err := ParseConstraint(in)
112+
require.NoError(t, err, "error parsing %s", in)
113+
require.Equal(t, out, p.String())
114+
fmt.Printf("'%s' parsed as %s\n", in, p.String())
115+
})
116+
}
117+
100118
bad := []string{
101119
"1.0.0",
102120
"= 1.0.0",
@@ -107,11 +125,20 @@ func TestConstraintsParser(t *testing.T) {
107125
">>1.0.0",
108126
">1.0.0 =2.0.0",
109127
">1.0.0 &",
128+
"!1.0.0",
129+
">1.0.0 && 2.0.0",
130+
">1.0.0 | =2.0.0",
131+
"(>1.0.0 | =2.0.0)",
132+
"(>1.0.0 || =2.0.0",
133+
">1.0.0 || 2.0.0",
110134
}
111-
for _, s := range bad {
112-
p, err := ParseConstraint(s)
113-
require.Nil(t, p)
114-
require.Error(t, err)
115-
fmt.Printf("'%s' parse error: %s\n", s, err)
135+
for i, s := range bad {
136+
in := s
137+
t.Run(fmt.Sprintf("BadString%03d", i), func(t *testing.T) {
138+
p, err := ParseConstraint(in)
139+
require.Nil(t, p)
140+
require.Error(t, err)
141+
fmt.Printf("'%s' parse error: %s\n", in, err)
142+
})
116143
}
117144
}

0 commit comments

Comments
 (0)