detached a3227f4

feat(expr): add the parser

commit
a3227f448517f66ea4a1b4d1b1dcfc2c9368090a
author
brice · · 2026-10-12 14:46 UTC
parent
1facd52
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +441 -0
A expr/ast.go +73 -0
@@ -0,0 +1,73 @@
1+// SPDX-License-Identifier: MIT
2+
3+package expr
4+
5+// Node is a node of a parsed expression. The concrete types are [*Literal],
6+// [*Unary], [*FunctionCall], [*Binary], [*Logical], [*Grouping],
7+// [*ContextAccess], [*IndexAccess] and [*Star].
8+type Node interface {
9+ isNode()
10+}
11+
12+// Literal is a null, boolean, number or string constant.
13+type Literal struct {
14+ Value Value
15+ Token Token
16+}
17+
18+// Unary is a negation: "!x".
19+type Unary struct {
20+ Operator Token
21+ Expr Node
22+}
23+
24+// FunctionCall is a call such as "contains(a, b)". Name is the identifier
25+// token as written; function names are case-insensitive.
26+type FunctionCall struct {
27+ Name Token
28+ Args []Node
29+}
30+
31+// Binary is a comparison: ==, !=, <, <=, > or >=.
32+type Binary struct {
33+ Left Node
34+ Operator Token
35+ Right Node
36+}
37+
38+// Logical is a chain of && or || operands. The chain is flat: "a || b || c"
39+// is one Logical with three Args. Operator is the first operator token.
40+type Logical struct {
41+ Operator Token
42+ Args []Node
43+}
44+
45+// Grouping is a parenthesized expression.
46+type Grouping struct {
47+ Group Node
48+}
49+
50+// ContextAccess is the name of a context, such as "github".
51+type ContextAccess struct {
52+ Name Token
53+}
54+
55+// IndexAccess is "expr[index]" or "expr.name" (Index is then a string
56+// [Literal]) or "expr.*" (Index is a [Star]).
57+type IndexAccess struct {
58+ Expr Node
59+ Index Node
60+}
61+
62+// Star is the "*" of "expr.*" and "expr[*]".
63+type Star struct{}
64+
65+func (*Literal) isNode() {}
66+func (*Unary) isNode() {}
67+func (*FunctionCall) isNode() {}
68+func (*Binary) isNode() {}
69+func (*Logical) isNode() {}
70+func (*Grouping) isNode() {}
71+func (*ContextAccess) isNode() {}
72+func (*IndexAccess) isNode() {}
73+func (*Star) isNode() {}
@@ -0,0 +1,73 @@
1+// SPDX-License-Identifier: MIT
2+
3+package expr
4+
5+// Node is a node of a parsed expression. The concrete types are [*Literal],
6+// [*Unary], [*FunctionCall], [*Binary], [*Logical], [*Grouping],
7+// [*ContextAccess], [*IndexAccess] and [*Star].
8+type Node interface {
9+ isNode()
10+}
11+
12+// Literal is a null, boolean, number or string constant.
13+type Literal struct {
14+ Value Value
15+ Token Token
16+}
17+
18+// Unary is a negation: "!x".
19+type Unary struct {
20+ Operator Token
21+ Expr Node
22+}
23+
24+// FunctionCall is a call such as "contains(a, b)". Name is the identifier
25+// token as written; function names are case-insensitive.
26+type FunctionCall struct {
27+ Name Token
28+ Args []Node
29+}
30+
31+// Binary is a comparison: ==, !=, <, <=, > or >=.
32+type Binary struct {
33+ Left Node
34+ Operator Token
35+ Right Node
36+}
37+
38+// Logical is a chain of && or || operands. The chain is flat: "a || b || c"
39+// is one Logical with three Args. Operator is the first operator token.
40+type Logical struct {
41+ Operator Token
42+ Args []Node
43+}
44+
45+// Grouping is a parenthesized expression.
46+type Grouping struct {
47+ Group Node
48+}
49+
50+// ContextAccess is the name of a context, such as "github".
51+type ContextAccess struct {
52+ Name Token
53+}
54+
55+// IndexAccess is "expr[index]" or "expr.name" (Index is then a string
56+// [Literal]) or "expr.*" (Index is a [Star]).
57+type IndexAccess struct {
58+ Expr Node
59+ Index Node
60+}
61+
62+// Star is the "*" of "expr.*" and "expr[*]".
63+type Star struct{}
64+
65+func (*Literal) isNode() {}
66+func (*Unary) isNode() {}
67+func (*FunctionCall) isNode() {}
68+func (*Binary) isNode() {}
69+func (*Logical) isNode() {}
70+func (*Grouping) isNode() {}
71+func (*ContextAccess) isNode() {}
72+func (*IndexAccess) isNode() {}
73+func (*Star) isNode() {}
A expr/parser.go +368 -0
@@ -0,0 +1,368 @@
1+// SPDX-License-Identifier: MIT
2+
3+package expr
4+
5+import "math"
6+
7+// FuncInfo describes a function that expressions may call besides the
8+// built-in ones (case, contains, endsWith, format, fromJSON, join,
9+// startsWith, toJSON). The parser checks names and argument counts against
10+// it; the implementation is supplied at evaluation time in
11+// [EvalContext.Funcs].
12+type FuncInfo struct {
13+ // Name is matched case-insensitively.
14+ Name string
15+ // MinArgs and MaxArgs bound the number of arguments. A negative MaxArgs
16+ // means no upper bound.
17+ MinArgs int
18+ MaxArgs int
19+}
20+
21+// builtinFuncs are the functions of the reference's wellKnownFunctions, keyed
22+// by lower-cased name. They take precedence over caller-supplied FuncInfo for
23+// validation.
24+var builtinFuncs = map[string]FuncInfo{
25+ "case": {"case", 3, math.MaxInt},
26+ "contains": {"contains", 2, 2},
27+ "endswith": {"endsWith", 2, 2},
28+ "format": {"format", 1, 255},
29+ "fromjson": {"fromJson", 1, 1},
30+ "join": {"join", 1, 2},
31+ "startswith": {"startsWith", 2, 2},
32+ "tojson": {"toJson", 1, 1},
33+}
34+
35+// Parse parses the text of one expression (the inside of ${{ }}).
36+//
37+// contexts lists the context names the expression may reference, such as
38+// "github" or "matrix"; funcs lists the extra functions it may call. Names
39+// are case-insensitive. A reference to any other context or function is an
40+// error, as is a call with a wrong number of arguments.
41+//
42+// The returned error is an [*Error]. An expression with no tokens, such as
43+// "" or " ", is not an error: the reference returns nothing for it, and Parse
44+// returns a nil [Node], which [Evaluate] rejects.
45+func Parse(expr string, contexts []string, funcs []FuncInfo) (Node, error) {
46+ tokens, err := Lex(expr)
47+ if err != nil {
48+ return nil, err
49+ }
50+ p := &parser{
51+ tokens: tokens,
52+ contexts: make(map[string]bool, len(contexts)),
53+ funcs: make(map[string]FuncInfo, len(funcs)),
54+ }
55+ for _, c := range contexts {
56+ p.contexts[foldKey(c)] = true
57+ }
58+ for _, f := range funcs {
59+ p.funcs[foldKey(f.Name)] = f
60+ }
61+ return p.parse()
62+}
63+
64+// parser is a port of Parser in parser.js: recursive descent, with the same
65+// precedence (lowest first): ||, &&, == !=, < <= > >=, !, then index and
66+// call.
67+type parser struct {
68+ tokens []Token
69+ offset int
70+ depth int
71+ contexts map[string]bool
72+ funcs map[string]FuncInfo
73+}
74+
75+func (p *parser) parse() (Node, error) {
76+ if p.atEnd() {
77+ return nil, nil
78+ }
79+ n, err := p.expression()
80+ if err != nil {
81+ return nil, err
82+ }
83+ if !p.atEnd() {
84+ return nil, newTokenError(ErrorUnexpectedSymbol, p.peek())
85+ }
86+ return n, nil
87+}
88+
89+func (p *parser) expression() (Node, error) {
90+ if err := p.incrDepth(); err != nil {
91+ return nil, err
92+ }
93+ n, err := p.logicalOr()
94+ p.depth--
95+ return n, err
96+}
97+
98+func (p *parser) logicalOr() (Node, error) {
99+ n, err := p.logicalAnd()
100+ if err != nil || !p.check(TokenOr) {
101+ return n, err
102+ }
103+ return p.logicalChain(TokenOr, n, p.logicalAnd)
104+}
105+
106+func (p *parser) logicalAnd() (Node, error) {
107+ n, err := p.equality()
108+ if err != nil || !p.check(TokenAnd) {
109+ return n, err
110+ }
111+ return p.logicalChain(TokenAnd, n, p.equality)
112+}
113+
114+// logicalChain parses the rest of a flat chain of operator operands, the
115+// first of which is already parsed.
116+func (p *parser) logicalChain(op TokenType, first Node, operand func() (Node, error)) (Node, error) {
117+ if err := p.incrDepth(); err != nil {
118+ return nil, err
119+ }
120+ logical := &Logical{Operator: p.peek(), Args: []Node{first}}
121+ for p.match(op) {
122+ right, err := operand()
123+ if err != nil {
124+ return nil, err
125+ }
126+ logical.Args = append(logical.Args, right)
127+ }
128+ p.depth--
129+ return logical, nil
130+}
131+
132+func (p *parser) equality() (Node, error) {
133+ n, err := p.comparison()
134+ if err != nil {
135+ return nil, err
136+ }
137+ for p.match(TokenBangEqual, TokenEqualEqual) {
138+ op := p.previous()
139+ right, err := p.comparison()
140+ if err != nil {
141+ return nil, err
142+ }
143+ n = &Binary{Left: n, Operator: op, Right: right}
144+ }
145+ return n, nil
146+}
147+
148+func (p *parser) comparison() (Node, error) {
149+ n, err := p.unary()
150+ if err != nil {
151+ return nil, err
152+ }
153+ for p.match(TokenGreater, TokenGreaterEqual, TokenLess, TokenLessEqual) {
154+ op := p.previous()
155+ right, err := p.unary()
156+ if err != nil {
157+ return nil, err
158+ }
159+ n = &Binary{Left: n, Operator: op, Right: right}
160+ }
161+ return n, nil
162+}
163+
164+func (p *parser) unary() (Node, error) {
165+ if p.match(TokenBang) {
166+ if err := p.incrDepth(); err != nil {
167+ return nil, err
168+ }
169+ op := p.previous()
170+ operand, err := p.unary()
171+ if err != nil {
172+ return nil, err
173+ }
174+ p.depth--
175+ return &Unary{Operator: op, Expr: operand}, nil
176+ }
177+ return p.index()
178+}
179+
180+func (p *parser) index() (Node, error) {
181+ n, err := p.call()
182+ if err != nil {
183+ return nil, err
184+ }
185+ depthIncreased := 0
186+ switch n.(type) {
187+ case *Grouping, *FunctionCall, *ContextAccess:
188+ loop:
189+ for {
190+ switch {
191+ case p.match(TokenLeftBracket):
192+ var idx Node
193+ if p.match(TokenStar) {
194+ idx = &Star{}
195+ } else if idx, err = p.expression(); err != nil {
196+ return nil, err
197+ }
198+ if err := p.consume(TokenRightBracket, ErrorUnexpectedSymbol); err != nil {
199+ return nil, err
200+ }
201+ if err := p.incrDepth(); err != nil {
202+ return nil, err
203+ }
204+ depthIncreased++
205+ n = &IndexAccess{Expr: n, Index: idx}
206+ case p.match(TokenDot):
207+ if err := p.incrDepth(); err != nil {
208+ return nil, err
209+ }
210+ depthIncreased++
211+ switch {
212+ case p.match(TokenIdentifier):
213+ prop := p.previous()
214+ n = &IndexAccess{Expr: n, Index: &Literal{Value: String(prop.Lexeme), Token: prop}}
215+ case p.match(TokenStar):
216+ n = &IndexAccess{Expr: n, Index: &Star{}}
217+ default:
218+ return nil, newTokenError(ErrorUnexpectedSymbol, p.peek())
219+ }
220+ default:
221+ break loop
222+ }
223+ }
224+ }
225+ p.depth -= depthIncreased
226+ return n, nil
227+}
228+
229+func (p *parser) call() (Node, error) {
230+ if !p.check(TokenIdentifier) {
231+ return p.primary()
232+ }
233+ ident := p.next()
234+ if !p.match(TokenLeftParen) {
235+ if !p.contexts[foldKey(ident.Lexeme)] {
236+ return nil, newTokenError(ErrorUnrecognizedContext, ident)
237+ }
238+ return &ContextAccess{Name: ident}, nil
239+ }
240+ var args []Node
241+ for !p.match(TokenRightParen) {
242+ arg, err := p.expression()
243+ if err != nil {
244+ return nil, err
245+ }
246+ args = append(args, arg)
247+ if !p.check(TokenRightParen) {
248+ // A trailing comma before ")" is accepted, as in the reference.
249+ if err := p.consume(TokenComma, ErrorUnexpectedSymbol); err != nil {
250+ return nil, err
251+ }
252+ }
253+ }
254+ if err := p.validateFunction(ident, len(args)); err != nil {
255+ return nil, err
256+ }
257+ return &FunctionCall{Name: ident, Args: args}, nil
258+}
259+
260+// validateFunction checks that the function exists and accepts argCount
261+// arguments (funcs.js validateFunction).
262+func (p *parser) validateFunction(ident Token, argCount int) error {
263+ name := foldKey(ident.Lexeme)
264+ f, ok := builtinFuncs[name]
265+ if !ok {
266+ f, ok = p.funcs[name]
267+ if !ok {
268+ return newTokenError(ErrorUnrecognizedFunction, ident)
269+ }
270+ }
271+ if argCount < f.MinArgs {
272+ return newTokenError(ErrorTooFewParameters, ident)
273+ }
274+ if f.MaxArgs >= 0 && argCount > f.MaxArgs {
275+ return newTokenError(ErrorTooManyParameters, ident)
276+ }
277+ if name == "case" && argCount%2 == 0 {
278+ return newTokenError(ErrorEvenParameters, ident)
279+ }
280+ return nil
281+}
282+
283+func (p *parser) primary() (Node, error) {
284+ switch {
285+ case p.match(TokenFalse):
286+ return &Literal{Value: Boolean(false), Token: p.previous()}, nil
287+ case p.match(TokenTrue):
288+ return &Literal{Value: Boolean(true), Token: p.previous()}, nil
289+ case p.match(TokenNull):
290+ return &Literal{Value: Null{}, Token: p.previous()}, nil
291+ case p.match(TokenNumber):
292+ return &Literal{Value: p.previous().Value, Token: p.previous()}, nil
293+ case p.match(TokenString):
294+ return &Literal{Value: p.previous().Value, Token: p.previous()}, nil
295+ case p.match(TokenLeftParen):
296+ n, err := p.expression()
297+ if err != nil {
298+ return nil, err
299+ }
300+ if p.atEnd() {
301+ // Back up to the last token before the EOF.
302+ return nil, newTokenError(ErrorUnexpectedEndOfExpression, p.previous())
303+ }
304+ if err := p.consume(TokenRightParen, ErrorUnexpectedSymbol); err != nil {
305+ return nil, err
306+ }
307+ return &Grouping{Group: n}, nil
308+ case p.atEnd():
309+ return nil, newTokenError(ErrorUnexpectedEndOfExpression, p.previous())
310+ }
311+ return nil, newTokenError(ErrorUnexpectedSymbol, p.peek())
312+}
313+
314+// match consumes the next token if it has one of the given types.
315+func (p *parser) match(types ...TokenType) bool {
316+ for _, t := range types {
317+ if p.check(t) {
318+ p.next()
319+ return true
320+ }
321+ }
322+ return false
323+}
324+
325+func (p *parser) check(t TokenType) bool {
326+ if p.atEnd() {
327+ return false
328+ }
329+ return p.peek().Type == t
330+}
331+
332+func (p *parser) atEnd() bool { return p.peek().Type == TokenEOF }
333+
334+func (p *parser) next() Token {
335+ if !p.atEnd() {
336+ p.offset++
337+ }
338+ return p.previous()
339+}
340+
341+func (p *parser) peek() Token { return p.tokens[p.offset] }
342+
343+// previous returns the token before the current one. At offset 0 the
344+// reference reads tokens[-1], which is undefined and crashes; here it
345+// returns the zero Token. That is unreachable: previous is only used after
346+// a token was consumed.
347+func (p *parser) previous() Token {
348+ if p.offset == 0 {
349+ return Token{}
350+ }
351+ return p.tokens[p.offset-1]
352+}
353+
354+func (p *parser) consume(t TokenType, errType ErrorType) error {
355+ if p.check(t) {
356+ p.next()
357+ return nil
358+ }
359+ return newTokenError(errType, p.peek())
360+}
361+
362+func (p *parser) incrDepth() error {
363+ p.depth++
364+ if p.depth > MaxParserDepth {
365+ return newTokenError(ErrorExceededMaxDepth, p.peek())
366+ }
367+ return nil
368+}
@@ -0,0 +1,368 @@
1+// SPDX-License-Identifier: MIT
2+
3+package expr
4+
5+import "math"
6+
7+// FuncInfo describes a function that expressions may call besides the
8+// built-in ones (case, contains, endsWith, format, fromJSON, join,
9+// startsWith, toJSON). The parser checks names and argument counts against
10+// it; the implementation is supplied at evaluation time in
11+// [EvalContext.Funcs].
12+type FuncInfo struct {
13+ // Name is matched case-insensitively.
14+ Name string
15+ // MinArgs and MaxArgs bound the number of arguments. A negative MaxArgs
16+ // means no upper bound.
17+ MinArgs int
18+ MaxArgs int
19+}
20+
21+// builtinFuncs are the functions of the reference's wellKnownFunctions, keyed
22+// by lower-cased name. They take precedence over caller-supplied FuncInfo for
23+// validation.
24+var builtinFuncs = map[string]FuncInfo{
25+ "case": {"case", 3, math.MaxInt},
26+ "contains": {"contains", 2, 2},
27+ "endswith": {"endsWith", 2, 2},
28+ "format": {"format", 1, 255},
29+ "fromjson": {"fromJson", 1, 1},
30+ "join": {"join", 1, 2},
31+ "startswith": {"startsWith", 2, 2},
32+ "tojson": {"toJson", 1, 1},
33+}
34+
35+// Parse parses the text of one expression (the inside of ${{ }}).
36+//
37+// contexts lists the context names the expression may reference, such as
38+// "github" or "matrix"; funcs lists the extra functions it may call. Names
39+// are case-insensitive. A reference to any other context or function is an
40+// error, as is a call with a wrong number of arguments.
41+//
42+// The returned error is an [*Error]. An expression with no tokens, such as
43+// "" or " ", is not an error: the reference returns nothing for it, and Parse
44+// returns a nil [Node], which [Evaluate] rejects.
45+func Parse(expr string, contexts []string, funcs []FuncInfo) (Node, error) {
46+ tokens, err := Lex(expr)
47+ if err != nil {
48+ return nil, err
49+ }
50+ p := &parser{
51+ tokens: tokens,
52+ contexts: make(map[string]bool, len(contexts)),
53+ funcs: make(map[string]FuncInfo, len(funcs)),
54+ }
55+ for _, c := range contexts {
56+ p.contexts[foldKey(c)] = true
57+ }
58+ for _, f := range funcs {
59+ p.funcs[foldKey(f.Name)] = f
60+ }
61+ return p.parse()
62+}
63+
64+// parser is a port of Parser in parser.js: recursive descent, with the same
65+// precedence (lowest first): ||, &&, == !=, < <= > >=, !, then index and
66+// call.
67+type parser struct {
68+ tokens []Token
69+ offset int
70+ depth int
71+ contexts map[string]bool
72+ funcs map[string]FuncInfo
73+}
74+
75+func (p *parser) parse() (Node, error) {
76+ if p.atEnd() {
77+ return nil, nil
78+ }
79+ n, err := p.expression()
80+ if err != nil {
81+ return nil, err
82+ }
83+ if !p.atEnd() {
84+ return nil, newTokenError(ErrorUnexpectedSymbol, p.peek())
85+ }
86+ return n, nil
87+}
88+
89+func (p *parser) expression() (Node, error) {
90+ if err := p.incrDepth(); err != nil {
91+ return nil, err
92+ }
93+ n, err := p.logicalOr()
94+ p.depth--
95+ return n, err
96+}
97+
98+func (p *parser) logicalOr() (Node, error) {
99+ n, err := p.logicalAnd()
100+ if err != nil || !p.check(TokenOr) {
101+ return n, err
102+ }
103+ return p.logicalChain(TokenOr, n, p.logicalAnd)
104+}
105+
106+func (p *parser) logicalAnd() (Node, error) {
107+ n, err := p.equality()
108+ if err != nil || !p.check(TokenAnd) {
109+ return n, err
110+ }
111+ return p.logicalChain(TokenAnd, n, p.equality)
112+}
113+
114+// logicalChain parses the rest of a flat chain of operator operands, the
115+// first of which is already parsed.
116+func (p *parser) logicalChain(op TokenType, first Node, operand func() (Node, error)) (Node, error) {
117+ if err := p.incrDepth(); err != nil {
118+ return nil, err
119+ }
120+ logical := &Logical{Operator: p.peek(), Args: []Node{first}}
121+ for p.match(op) {
122+ right, err := operand()
123+ if err != nil {
124+ return nil, err
125+ }
126+ logical.Args = append(logical.Args, right)
127+ }
128+ p.depth--
129+ return logical, nil
130+}
131+
132+func (p *parser) equality() (Node, error) {
133+ n, err := p.comparison()
134+ if err != nil {
135+ return nil, err
136+ }
137+ for p.match(TokenBangEqual, TokenEqualEqual) {
138+ op := p.previous()
139+ right, err := p.comparison()
140+ if err != nil {
141+ return nil, err
142+ }
143+ n = &Binary{Left: n, Operator: op, Right: right}
144+ }
145+ return n, nil
146+}
147+
148+func (p *parser) comparison() (Node, error) {
149+ n, err := p.unary()
150+ if err != nil {
151+ return nil, err
152+ }
153+ for p.match(TokenGreater, TokenGreaterEqual, TokenLess, TokenLessEqual) {
154+ op := p.previous()
155+ right, err := p.unary()
156+ if err != nil {
157+ return nil, err
158+ }
159+ n = &Binary{Left: n, Operator: op, Right: right}
160+ }
161+ return n, nil
162+}
163+
164+func (p *parser) unary() (Node, error) {
165+ if p.match(TokenBang) {
166+ if err := p.incrDepth(); err != nil {
167+ return nil, err
168+ }
169+ op := p.previous()
170+ operand, err := p.unary()
171+ if err != nil {
172+ return nil, err
173+ }
174+ p.depth--
175+ return &Unary{Operator: op, Expr: operand}, nil
176+ }
177+ return p.index()
178+}
179+
180+func (p *parser) index() (Node, error) {
181+ n, err := p.call()
182+ if err != nil {
183+ return nil, err
184+ }
185+ depthIncreased := 0
186+ switch n.(type) {
187+ case *Grouping, *FunctionCall, *ContextAccess:
188+ loop:
189+ for {
190+ switch {
191+ case p.match(TokenLeftBracket):
192+ var idx Node
193+ if p.match(TokenStar) {
194+ idx = &Star{}
195+ } else if idx, err = p.expression(); err != nil {
196+ return nil, err
197+ }
198+ if err := p.consume(TokenRightBracket, ErrorUnexpectedSymbol); err != nil {
199+ return nil, err
200+ }
201+ if err := p.incrDepth(); err != nil {
202+ return nil, err
203+ }
204+ depthIncreased++
205+ n = &IndexAccess{Expr: n, Index: idx}
206+ case p.match(TokenDot):
207+ if err := p.incrDepth(); err != nil {
208+ return nil, err
209+ }
210+ depthIncreased++
211+ switch {
212+ case p.match(TokenIdentifier):
213+ prop := p.previous()
214+ n = &IndexAccess{Expr: n, Index: &Literal{Value: String(prop.Lexeme), Token: prop}}
215+ case p.match(TokenStar):
216+ n = &IndexAccess{Expr: n, Index: &Star{}}
217+ default:
218+ return nil, newTokenError(ErrorUnexpectedSymbol, p.peek())
219+ }
220+ default:
221+ break loop
222+ }
223+ }
224+ }
225+ p.depth -= depthIncreased
226+ return n, nil
227+}
228+
229+func (p *parser) call() (Node, error) {
230+ if !p.check(TokenIdentifier) {
231+ return p.primary()
232+ }
233+ ident := p.next()
234+ if !p.match(TokenLeftParen) {
235+ if !p.contexts[foldKey(ident.Lexeme)] {
236+ return nil, newTokenError(ErrorUnrecognizedContext, ident)
237+ }
238+ return &ContextAccess{Name: ident}, nil
239+ }
240+ var args []Node
241+ for !p.match(TokenRightParen) {
242+ arg, err := p.expression()
243+ if err != nil {
244+ return nil, err
245+ }
246+ args = append(args, arg)
247+ if !p.check(TokenRightParen) {
248+ // A trailing comma before ")" is accepted, as in the reference.
249+ if err := p.consume(TokenComma, ErrorUnexpectedSymbol); err != nil {
250+ return nil, err
251+ }
252+ }
253+ }
254+ if err := p.validateFunction(ident, len(args)); err != nil {
255+ return nil, err
256+ }
257+ return &FunctionCall{Name: ident, Args: args}, nil
258+}
259+
260+// validateFunction checks that the function exists and accepts argCount
261+// arguments (funcs.js validateFunction).
262+func (p *parser) validateFunction(ident Token, argCount int) error {
263+ name := foldKey(ident.Lexeme)
264+ f, ok := builtinFuncs[name]
265+ if !ok {
266+ f, ok = p.funcs[name]
267+ if !ok {
268+ return newTokenError(ErrorUnrecognizedFunction, ident)
269+ }
270+ }
271+ if argCount < f.MinArgs {
272+ return newTokenError(ErrorTooFewParameters, ident)
273+ }
274+ if f.MaxArgs >= 0 && argCount > f.MaxArgs {
275+ return newTokenError(ErrorTooManyParameters, ident)
276+ }
277+ if name == "case" && argCount%2 == 0 {
278+ return newTokenError(ErrorEvenParameters, ident)
279+ }
280+ return nil
281+}
282+
283+func (p *parser) primary() (Node, error) {
284+ switch {
285+ case p.match(TokenFalse):
286+ return &Literal{Value: Boolean(false), Token: p.previous()}, nil
287+ case p.match(TokenTrue):
288+ return &Literal{Value: Boolean(true), Token: p.previous()}, nil
289+ case p.match(TokenNull):
290+ return &Literal{Value: Null{}, Token: p.previous()}, nil
291+ case p.match(TokenNumber):
292+ return &Literal{Value: p.previous().Value, Token: p.previous()}, nil
293+ case p.match(TokenString):
294+ return &Literal{Value: p.previous().Value, Token: p.previous()}, nil
295+ case p.match(TokenLeftParen):
296+ n, err := p.expression()
297+ if err != nil {
298+ return nil, err
299+ }
300+ if p.atEnd() {
301+ // Back up to the last token before the EOF.
302+ return nil, newTokenError(ErrorUnexpectedEndOfExpression, p.previous())
303+ }
304+ if err := p.consume(TokenRightParen, ErrorUnexpectedSymbol); err != nil {
305+ return nil, err
306+ }
307+ return &Grouping{Group: n}, nil
308+ case p.atEnd():
309+ return nil, newTokenError(ErrorUnexpectedEndOfExpression, p.previous())
310+ }
311+ return nil, newTokenError(ErrorUnexpectedSymbol, p.peek())
312+}
313+
314+// match consumes the next token if it has one of the given types.
315+func (p *parser) match(types ...TokenType) bool {
316+ for _, t := range types {
317+ if p.check(t) {
318+ p.next()
319+ return true
320+ }
321+ }
322+ return false
323+}
324+
325+func (p *parser) check(t TokenType) bool {
326+ if p.atEnd() {
327+ return false
328+ }
329+ return p.peek().Type == t
330+}
331+
332+func (p *parser) atEnd() bool { return p.peek().Type == TokenEOF }
333+
334+func (p *parser) next() Token {
335+ if !p.atEnd() {
336+ p.offset++
337+ }
338+ return p.previous()
339+}
340+
341+func (p *parser) peek() Token { return p.tokens[p.offset] }
342+
343+// previous returns the token before the current one. At offset 0 the
344+// reference reads tokens[-1], which is undefined and crashes; here it
345+// returns the zero Token. That is unreachable: previous is only used after
346+// a token was consumed.
347+func (p *parser) previous() Token {
348+ if p.offset == 0 {
349+ return Token{}
350+ }
351+ return p.tokens[p.offset-1]
352+}
353+
354+func (p *parser) consume(t TokenType, errType ErrorType) error {
355+ if p.check(t) {
356+ p.next()
357+ return nil
358+ }
359+ return newTokenError(errType, p.peek())
360+}
361+
362+func (p *parser) incrDepth() error {
363+ p.depth++
364+ if p.depth > MaxParserDepth {
365+ return newTokenError(ErrorExceededMaxDepth, p.peek())
366+ }
367+ return nil
368+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close