v0.2.1 98594d0

funcs.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package expr
4
5import (
6	"errors"
7	"strconv"
8	"strings"
9	"unicode"
10)
11
12// builtinFunc implements a built-in function. It runs inside the evaluator so
13// that it can charge the output budget and watch the context, and it receives
14// the call, not the evaluated arguments: like the runner's functions
15// (Sdk/Functions/*.cs) it evaluates only the arguments it needs, in its own
16// order, so an argument that is never reached cannot fail.
17type builtinFunc func(e *evaluator, c *FunctionCall) (Value, error)
18
19// builtinImpls are the implementations of the built-in functions, keyed by
20// upper-cased name (see foldKey). Arity is validated by the parser, so each
21// may assume its documented argument count.
22var builtinImpls map[string]builtinFunc
23
24// The map is filled in init: the implementations call back into the evaluator,
25// which would make a package-level initializer cyclic.
26func init() {
27	builtinImpls = map[string]builtinFunc{
28		"CASE":       funcCase,
29		"CONTAINS":   funcContains,
30		"ENDSWITH":   funcEndsWith,
31		"FORMAT":     funcFormat,
32		"FROMJSON":   funcFromJSON,
33		"JOIN":       funcJoin,
34		"STARTSWITH": funcStartsWith,
35		"TOJSON":     funcToJSON,
36	}
37}
38
39// funcCase is case(pred1, val1, pred2, val2, ..., default): the predicates are
40// evaluated in order up to the first true one, then only its value (Case.cs).
41// A predicate that is not a boolean is an error. The JavaScript reference
42// ships case behind an experimental feature flag that only affects editor
43// completion; the parser always accepts it, and so does the runner.
44func funcCase(e *evaluator, c *FunctionCall) (Value, error) {
45	n := len(c.Args)
46	if n%2 == 0 {
47		return nil, evalErrorf("case requires an odd number of arguments")
48	}
49	for i := 0; i < n-1; i += 2 {
50		p, err := e.eval(c.Args[i])
51		if err != nil {
52			return nil, err
53		}
54		pred, ok := normalize(p).(Boolean)
55		if !ok {
56			return nil, evalErrorf("case predicate must evaluate to a boolean value")
57		}
58		if pred {
59			return e.eval(c.Args[i+1])
60		}
61	}
62	return e.eval(c.Args[n-1])
63}
64
65// funcContains is contains(search, item) (Contains.cs): when search is a
66// primitive and so is item, a case-insensitive (OrdinalIgnoreCase) substring
67// test on their string forms; when search is a non-empty array, == against each
68// element; false otherwise. item is only evaluated when it can matter.
69func funcContains(e *evaluator, c *FunctionCall) (Value, error) {
70	l, err := e.eval(c.Args[0])
71	if err != nil {
72		return nil, err
73	}
74	left := normalize(l)
75	switch {
76	case isPrimitive(left):
77		r, err := e.eval(c.Args[1])
78		if err != nil {
79			return nil, err
80		}
81		if right := normalize(r); isPrimitive(right) {
82			return Boolean(jsIncludes(ordinalFold(CoerceString(left)), ordinalFold(CoerceString(right)))), nil
83		}
84	case left.Kind() == KindArray && len(left.(*Array).items) > 0:
85		right, err := e.eval(c.Args[1])
86		if err != nil {
87			return nil, err
88		}
89		for _, v := range left.(*Array).items {
90			if err := e.tick(); err != nil {
91				return nil, err
92			}
93			if equals(right, v) {
94				return Boolean(true), nil
95			}
96		}
97	}
98	return Boolean(false), nil
99}
100
101func funcStartsWith(e *evaluator, c *FunctionCall) (Value, error) {
102	ls, rs, ok, err := e.primitivePair(c)
103	return Boolean(ok && jsHasPrefix(ordinalFold(ls), ordinalFold(rs))), err
104}
105
106func funcEndsWith(e *evaluator, c *FunctionCall) (Value, error) {
107	ls, rs, ok, err := e.primitivePair(c)
108	return Boolean(ok && jsHasSuffix(ordinalFold(ls), ordinalFold(rs))), err
109}
110
111// primitivePair evaluates the two arguments, the second only if the first is
112// primitive, and returns their string forms; ok is false when either is an
113// array or a dictionary.
114func (e *evaluator) primitivePair(c *FunctionCall) (ls, rs string, ok bool, err error) {
115	l, err := e.eval(c.Args[0])
116	if err != nil || !isPrimitive(l) {
117		return "", "", false, err
118	}
119	r, err := e.eval(c.Args[1])
120	if err != nil || !isPrimitive(r) {
121		return "", "", false, err
122	}
123	return CoerceString(l), CoerceString(r), true, nil
124}
125
126// funcJoin is join(array, separator) (Join.cs). A primitive first argument is
127// just converted to a string, ignoring the separator; a dictionary or an empty
128// array gives "". The separator is evaluated only when there are at least two
129// items to separate, and a separator that is not primitive means ",".
130func funcJoin(e *evaluator, c *FunctionCall) (Value, error) {
131	f, err := e.eval(c.Args[0])
132	if err != nil {
133		return nil, err
134	}
135	first := normalize(f)
136	if isPrimitive(first) {
137		return String(CoerceString(first)), nil
138	}
139	arr, ok := first.(*Array)
140	if !ok || len(arr.items) == 0 {
141		return String(""), nil
142	}
143	sep := ","
144	if len(arr.items) > 1 && len(c.Args) > 1 {
145		sv, err := e.eval(c.Args[1])
146		if err != nil {
147			return nil, err
148		}
149		if isPrimitive(sv) {
150			sep = CoerceString(sv)
151		}
152	}
153	var out strings.Builder
154	for i, it := range arr.items {
155		if err := e.tick(); err != nil {
156			return nil, err
157		}
158		part := CoerceString(it)
159		n := len(part)
160		if i > 0 {
161			n += len(sep)
162		}
163		if err := e.charge(n); err != nil {
164			return nil, err
165		}
166		if i > 0 {
167			out.WriteString(sep)
168		}
169		out.WriteString(part)
170	}
171	return String(out.String()), nil
172}
173
174// funcToJSON is toJSON(value). The output counts against the budget and is
175// cut off as soon as it exceeds what is left.
176func funcToJSON(e *evaluator, c *FunctionCall) (Value, error) {
177	v, err := e.eval(c.Args[0])
178	if err != nil {
179		return nil, err
180	}
181	max := -1
182	if e.limit >= 0 {
183		max = e.limit - e.used
184	}
185	out, err := EncodeJSON(v, max)
186	switch {
187	case errors.Is(err, errJSONTooBig):
188		return nil, e.budgetError()
189	case err != nil:
190		return nil, &EvalError{Message: "toJSON: " + err.Error(), Err: err}
191	}
192	if err := e.charge(len(out)); err != nil {
193		return nil, err
194	}
195	return String(out), nil
196}
197
198// funcFromJSON is fromJSON(value). Anything that Json.NET cannot read, blank
199// input included, fails with the message of its JsonReaderException (a
200// [*JSONError]), which is what the runner reports.
201func funcFromJSON(e *evaluator, c *FunctionCall) (Value, error) {
202	a, err := e.eval(c.Args[0])
203	if err != nil {
204		return nil, err
205	}
206	v, err := FromJSON([]byte(CoerceString(a)))
207	if err != nil {
208		return nil, &EvalError{Message: err.Error(), Err: err}
209	}
210	return v, nil
211}
212
213// formatPiece is a segment of the result of format(): literal text, or the
214// index of an argument (evaluated when the result is assembled) with the
215// format specifiers written after the colon.
216type formatPiece struct {
217	text  string
218	arg   int
219	specs string
220	isArg bool
221}
222
223// funcFormat is format(fmt, args...) (Format.cs): "{N}" inserts argument N,
224// "{{" and "}}" are literal braces. The format string is scanned completely
225// first (so a syntax error or a reference to a missing argument is reported
226// before any argument is evaluated), then the arguments are evaluated, once
227// each, in the order they appear. N is at most 255 (a byte). Format specifiers
228// ("{0:x}") are not supported for any value: they are an error.
229func funcFormat(e *evaluator, c *FunctionCall) (Value, error) {
230	f, err := e.eval(c.Args[0])
231	if err != nil {
232		return nil, err
233	}
234	fs := CoerceString(f)
235	var pieces []formatPiece
236	text := func(t string) error {
237		if t == "" {
238			return nil
239		}
240		if err := e.charge(utf16Len(t)); err != nil {
241			return err
242		}
243		pieces = append(pieces, formatPiece{text: t})
244		return nil
245	}
246	index := 0
247	for index < len(fs) {
248		lbrace := indexByteFrom(fs, '{', index)
249		rbrace := indexByteFrom(fs, '}', index)
250		switch {
251		case lbrace >= 0 && (rbrace < 0 || rbrace > lbrace):
252			// Escaped left brace.
253			if charAt(fs, lbrace+1) == '{' {
254				if err := text(fs[index : lbrace+1]); err != nil {
255					return nil, err
256				}
257				index = lbrace + 2
258				continue
259			}
260			// Left brace, number, optional format specifiers, right brace.
261			if rbrace > lbrace+1 {
262				if argIndex, end, ok := readArgIndex(fs, lbrace+1); ok {
263					if specs, closing, ok := readFormatSpecifiers(fs, end+1); ok {
264						if argIndex > len(c.Args)-2 {
265							return nil, evalErrorf("The following format string references more arguments than were supplied: %s", fs)
266						}
267						if err := text(fs[index:lbrace]); err != nil {
268							return nil, err
269						}
270						pieces = append(pieces, formatPiece{arg: argIndex, specs: specs, isArg: true})
271						index = closing + 1
272						continue
273					}
274				}
275			}
276			return nil, evalErrorf("The following format string is invalid: %s", fs)
277		case rbrace >= 0:
278			// Escaped right brace.
279			if charAt(fs, rbrace+1) != '}' {
280				return nil, evalErrorf("The following format string is invalid: %s", fs)
281			}
282			if err := text(fs[index : rbrace+1]); err != nil {
283				return nil, err
284			}
285			index = rbrace + 2
286		default:
287			if err := text(fs[index:]); err != nil {
288				return nil, err
289			}
290			index = len(fs)
291		}
292	}
293	// Assemble: arguments are evaluated lazily, once each, in the order of
294	// their first appearance.
295	type argValue struct {
296		str  string
297		kind Kind
298		done bool
299	}
300	cache := make([]argValue, len(c.Args)-1)
301	var out strings.Builder
302	for _, p := range pieces {
303		if !p.isArg {
304			out.WriteString(p.text)
305			continue
306		}
307		av := &cache[p.arg]
308		if !av.done {
309			v, err := e.eval(c.Args[1+p.arg])
310			if err != nil {
311				return nil, err
312			}
313			*av = argValue{str: CoerceString(v), kind: normalize(v).Kind(), done: true}
314		}
315		if p.specs != "" {
316			return nil, evalErrorf("The format specifiers '%s' are not valid for objects of type '%s'", p.specs, av.kind)
317		}
318		if av.str != "" {
319			if err := e.charge(utf16Len(av.str)); err != nil {
320				return nil, err
321			}
322		}
323		out.WriteString(av.str)
324	}
325	return String(out.String()), nil
326}
327
328func indexByteFrom(s string, c byte, from int) int {
329	if from >= len(s) {
330		return -1
331	}
332	i := strings.IndexByte(s[from:], c)
333	if i < 0 {
334		return -1
335	}
336	return from + i
337}
338
339// charAt returns the byte at i, or 0 past the end (safeCharAt).
340func charAt(s string, i int) byte {
341	if i < len(s) {
342		return s[i]
343	}
344	return 0
345}
346
347// readArgIndex reads the digits at s[start:] as the index of a format
348// argument: a byte, so at most 255 (Byte.TryParse). It returns the index of the
349// last digit. Digits are Unicode decimal digits for the scan but only ASCII
350// ones parse, so any other digit makes the format string invalid.
351func readArgIndex(s string, start int) (value, end int, ok bool) {
352	n := 0
353	for i := start; i < len(s); {
354		r, w := decodeRune(s[i:])
355		if !unicode.IsDigit(r) {
356			break
357		}
358		i += w
359		n = i - start
360	}
361	if n == 0 {
362		return 0, 0, false
363	}
364	v, err := strconv.ParseUint(s[start:start+n], 10, 8)
365	if err != nil || !isASCII(s[start:start+n]) {
366		return 0, 0, false
367	}
368	return int(v), start + n - 1, true
369}
370
371// readFormatSpecifiers reads what follows the digits of "{N": either the
372// closing brace, or ":" and specifiers up to a closing brace, where "}}" is a
373// literal brace inside the specifiers. It returns the specifiers and the index
374// of the closing brace.
375func readFormatSpecifiers(s string, start int) (specs string, closing int, ok bool) {
376	switch charAt(s, start) {
377	case '}':
378		return "", start, true
379	case ':':
380	default:
381		return "", 0, false
382	}
383	var b strings.Builder
384	for i := start + 1; i < len(s); {
385		if s[i] != '}' {
386			b.WriteByte(s[i])
387			i++
388		} else if charAt(s, i+1) == '}' {
389			b.WriteByte('}')
390			i += 2
391		} else {
392			return b.String(), i, true
393		}
394	}
395	return "", 0, false
396}
397
398// utf16Len is the length of s in UTF-16 code units, the length of a .NET string.
399func utf16Len(s string) int {
400	n := 0
401	for _, r := range s {
402		n++
403		if r >= 0x10000 {
404			n++
405		}
406	}
407	return n
408}

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