main 98594d0

reader.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package template
4
5import (
6	"fmt"
7	"sort"
8	"strings"
9
10	"bnema.dev/ermite-actions/expr"
11)
12
13// ReadYAML parses a YAML document and reads it as a template of the schema
14// definition rootType (for workflows, "workflow-root-strict"). Problems
15// found in the document, such as syntax errors, unexpected keys or
16// misplaced expressions, are recorded in ctx; the returned token holds what
17// could be read and is nil when the document is not valid YAML.
18//
19// An empty document is an error, as in the reference. A document larger than
20// [MaxFileBytes] is refused before it is parsed, with one error about the
21// file.
22func ReadYAML(ctx *Context, rootType, file string, content []byte) (root Token) {
23	if len(content) > MaxFileBytes {
24		e := &Error{File: file, fileOnly: true, Raw: fmt.Sprintf("The file is too large: %d bytes, the limit is %d bytes", len(content), MaxFileBytes)}
25		ctx.add(e)
26		return nil
27	}
28	res := readYAMLDocument(file, content, ctx.RunnerYAML)
29	for _, e := range res.errs {
30		ctx.add(e)
31	}
32	if len(res.errs) > 0 {
33		return nil
34	}
35	defer func() {
36		if r := recover(); r != nil {
37			f, ok := r.(fatal)
38			if !ok {
39				panic(r)
40			}
41			ctx.ErrorInFile(file, f.msg)
42		}
43	}()
44	rd := &reader{ctx: ctx, schema: ctx.schema, file: file}
45	if res.root == nil {
46		if ctx.RunnerYAML {
47			// YamlDotNet produces no DocumentStart event for an empty stream.
48			fatalf("Expected document start parse event")
49		}
50		fatalf("Expected a scalar value, a sequence, or a mapping")
51	}
52	def := newDefinitionInfo(ctx.schema, nil, ctx.schema.get(rootType))
53	root = rd.readValue(res.root, def)
54	for _, e := range res.after {
55		ctx.add(e)
56	}
57	return root
58}
59
60type reader struct {
61	ctx    *Context
62	schema *Schema
63	file   string
64	// noExpressions is set when reading the result of an evaluation, whose
65	// strings are text even if they contain ${{.
66	noExpressions bool
67	// depth is the nesting of the collections being read, see Context.MaxDepth.
68	depth int
69}
70
71// enter counts one more level of nesting. Like TemplateMemory.IncrementDepth
72// it fails when the levels already entered reach the limit, so a limit of 100
73// allows 100 nested collections.
74func (r *reader) enter() {
75	if r.ctx.MaxDepth <= 0 {
76		return
77	}
78	if r.depth >= r.ctx.MaxDepth {
79		fatalf("Maximum object depth exceeded")
80	}
81	r.depth++
82}
83
84// text is the string a literal becomes when the schema wants a string, and
85// the text of a key that is not a string: Token.String, except that a number
86// is shown as the runner's NumberToken shows it in runner mode.
87func (r *reader) text(t Token) string {
88	if n, ok := t.(*Number); ok && r.ctx.RunnerYAML {
89		return DotNetNumberString(n.Value)
90	}
91	return t.String()
92}
93
94// foldKey is the form of a key under which duplicates are detected: the
95// reference upper-cases like JavaScript, the runner compares keys with
96// .NET's OrdinalIgnoreCase.
97func (r *reader) foldKey(s string) string {
98	if r.ctx.RunnerYAML {
99		return expr.OrdinalFold(s)
100	}
101	return strings.ToUpper(s)
102}
103
104func (r *reader) leave() {
105	if r.ctx.MaxDepth > 0 {
106		r.depth--
107	}
108}
109
110// skip walks a value that is not kept (the value of an unexpected key, the
111// items of a collection the schema does not allow) and counts its nesting,
112// as the reference's SkipValue does.
113func (r *reader) skip(n *rnode) {
114	if n == nil {
115		return
116	}
117	if n.fault != "" {
118		fatalf("%s", n.fault)
119	}
120	if r.ctx.MaxDepth <= 0 {
121		return
122	}
123	switch n.kind {
124	case rSeq:
125		r.enter()
126		for _, it := range n.items {
127			r.skip(it)
128		}
129		r.leave()
130	case rMap:
131		r.enter()
132		for i := range n.keys {
133			r.skip(n.keys[i])
134			r.skip(n.vals[i])
135		}
136		r.leave()
137	}
138}
139
140func (r *reader) origin(n *rnode) Origin { return Origin{File: r.file, Pos: n.pos} }
141
142func (r *reader) readValue(n *rnode, def *DefinitionInfo) Token {
143	if n.fault != "" {
144		fatalf("%s", n.fault)
145	}
146	switch n.kind {
147	case rLiteral:
148		scalar := r.parseScalar(n, n.lit, def)
149		return r.validate(scalar, def)
150	case rSeq:
151		r.enter()
152		seq := NewSequence(r.origin(n))
153		defs := def.schema.definitionsOfType(def.Definition, DefSequence)
154		if len(defs) > 0 {
155			itemDef := def.child(defs[0].ItemType)
156			for _, it := range n.items {
157				seq.Items = append(seq.Items, r.readValue(it, itemDef))
158			}
159		} else {
160			r.ctx.ErrorAt(seq, "A sequence was not expected")
161			for _, it := range n.items {
162				r.skip(it)
163			}
164		}
165		seq.def = def
166		r.leave()
167		return seq
168	}
169	r.enter()
170	m := NewMapping(r.origin(n))
171	defs := def.schema.definitionsOfType(def.Definition, DefMapping)
172	if len(defs) > 0 {
173		if len(defs) > 1 || len(defs[0].Properties) > 0 || defs[0].LooseKeyType == "" {
174			r.mappingWithProperties(n, m, def, defs)
175		} else {
176			keyDef := def.child(defs[0].LooseKeyType)
177			valDef := def.child(defs[0].LooseValueType)
178			r.mappingAllLoose(n, m, def, keyDef, valDef, defs[0])
179		}
180	} else {
181		r.ctx.ErrorAt(m, "A mapping was not expected")
182		for i := range n.keys {
183			r.skip(n.keys[i])
184			r.skip(n.vals[i])
185		}
186	}
187	if m.def == nil {
188		m.def = def
189	}
190	r.leave()
191	return m
192}
193
194// key reads the key of a mapping entry; a key that is not a scalar is an
195// error and reads as an empty string.
196func (r *reader) key(n *rnode, i int, def *DefinitionInfo) Token {
197	k := n.keys[i]
198	if k.fault != "" {
199		fatalf("%s", k.fault)
200	}
201	if k.badKey {
202		if r.ctx.RunnerYAML {
203			// The runner's loop over the keys ends at the first one that is not
204			// a literal, and then expects the end of the mapping.
205			fatalf("Expected mapping end")
206		}
207		r.ctx.ErrorAt(NewString(r.origin(k), ""), "Unexpected value ''")
208	}
209	return r.parseScalar(k, k.lit, def)
210}
211
212func (r *reader) mappingWithProperties(n *rnode, m *Mapping, def *DefinitionInfo, defs []*Definition) {
213	var looseKey, looseVal string
214	if defs[0].LooseKeyType != "" {
215		looseKey, looseVal = defs[0].LooseKeyType, defs[0].LooseValueType
216	}
217	var looseKeyDef, looseValDef *DefinitionInfo
218	upperKeys := map[string]bool{}
219	hasExpressionKey := false
220	for i := range n.keys {
221		keyScalar := r.key(n, i, def)
222		val := n.vals[i]
223		if IsExpression(keyScalar) {
224			hasExpressionKey = true
225			if len(def.allowed) > 0 {
226				m.Add(keyScalar, r.readValue(val, def.child("any")))
227			} else {
228				r.ctx.ErrorAt(keyScalar, "A template expression is not allowed in this context")
229				r.skip(val)
230			}
231			continue
232		}
233		nextKey, ok := keyScalar.(*String)
234		if !ok {
235			nextKey = &String{tokenBase: *keyScalar.base(), Value: r.text(keyScalar)}
236		}
237		if nextKey.Value != "" {
238			up := r.foldKey(nextKey.Value)
239			if upperKeys[up] {
240				r.ctx.Errorf(nextKey, "'%s' is already defined", nextKey.Value)
241				r.skip(val)
242				continue
243			}
244			upperKeys[up] = true
245		}
246		var prop *Property
247		prop, defs = matchPropertyAndFilter(defs, nextKey.Value)
248		if prop != nil {
249			if backgroundStepProperties[nextKey.Value] {
250				r.ctx.Errorf(nextKey, "Unexpected value '%s'", nextKey.Value)
251				r.skip(val)
252				continue
253			}
254			nd := def.child(prop.Type)
255			nextKey.def = nd
256			m.Add(nextKey, r.readValue(val, nd))
257			continue
258		}
259		if looseKey != "" {
260			if looseKeyDef == nil {
261				looseKeyDef = def.child(looseKey)
262				looseValDef = def.child(looseVal)
263			}
264			r.validate(nextKey, looseKeyDef)
265			nextKey.def = def.child(defs[0].LooseValueType)
266			m.Add(nextKey, r.readValue(val, looseValDef))
267			continue
268		}
269		r.ctx.Errorf(nextKey, "Unexpected value '%s'", nextKey.Value)
270		r.skip(val)
271	}
272	if len(defs) == 1 {
273		m.def = newDefinitionInfo(def.schema, def, defs[0])
274	}
275	switch {
276	case len(defs) > 1:
277		hits := map[string]int{}
278		for _, d := range defs {
279			for _, k := range d.PropertyNames {
280				hits[k]++
281			}
282		}
283		var props []string
284		for k, c := range hits {
285			if c == 1 && !backgroundStepOnlyProperties[k] {
286				props = append(props, k)
287			}
288		}
289		sort.Strings(props)
290		r.ctx.Errorf(m, "There's not enough info to determine what you meant. Add one of these properties: %s", strings.Join(props, ", "))
291	case len(defs) == 1 && !hasExpressionKey:
292		for _, name := range defs[0].PropertyNames {
293			if defs[0].Properties[name].Required && !upperKeys[r.foldKey(name)] {
294				r.ctx.Errorf(m, "Required property is missing: %s", name)
295			}
296		}
297	}
298}
299
300var (
301	backgroundStepProperties     = map[string]bool{"background": true, "wait": true, "wait-all": true, "cancel": true}
302	backgroundStepOnlyProperties = map[string]bool{"wait": true, "wait-all": true, "cancel": true}
303)
304
305func (r *reader) mappingAllLoose(n *rnode, m *Mapping, def, keyDef, valDef *DefinitionInfo, md *Definition) {
306	upperKeys := map[string]bool{}
307	for i := range n.keys {
308		keyScalar := r.key(n, i, def)
309		keyScalar.base().def = keyDef
310		val := n.vals[i]
311		if IsExpression(keyScalar) {
312			if len(def.allowed) > 0 {
313				m.Add(keyScalar, r.readValue(val, valDef))
314			} else {
315				r.ctx.ErrorAt(keyScalar, "A template expression is not allowed in this context")
316				r.skip(val)
317			}
318			continue
319		}
320		nextKey, ok := keyScalar.(*String)
321		if !ok {
322			nextKey = &String{tokenBase: *keyScalar.base(), Value: r.text(keyScalar)}
323		}
324		if nextKey.Value != "" {
325			up := r.foldKey(nextKey.Value)
326			if upperKeys[up] {
327				r.ctx.Errorf(nextKey, "'%s' is already defined", nextKey.Value)
328				r.skip(val)
329				continue
330			}
331			upperKeys[up] = true
332		}
333		r.validate(nextKey, keyDef)
334		nextKey.def = def.child(md.LooseValueType)
335		m.Add(nextKey, r.readValue(val, valDef))
336	}
337}
338
339// validate checks a scalar against the scalar definitions of def. A literal
340// that is not a string may be converted to one.
341func (r *reader) validate(scalar Token, def *DefinitionInfo) Token {
342	switch scalar.Kind() {
343	case KindNull, KindBoolean, KindNumber, KindString:
344		for _, d := range def.schema.scalarDefinitions(def.Definition) {
345			if d.isMatch(scalar) {
346				scalar.base().def = newDefinitionInfo(def.schema, def, d)
347				return scalar
348			}
349		}
350		if scalar.Kind() != KindString {
351			sl := &String{tokenBase: *scalar.base(), Value: r.text(scalar)}
352			for _, d := range def.schema.scalarDefinitions(def.Definition) {
353				if d.isMatch(sl) {
354					sl.def = newDefinitionInfo(def.schema, def, d)
355					return sl
356				}
357			}
358		}
359		r.ctx.ErrorAt(scalar, fmt.Sprintf("Unexpected value '%s'", r.text(scalar)))
360	case KindBasicExpression:
361		if len(def.allowed) == 0 {
362			r.ctx.ErrorAt(scalar, "A template expression is not allowed in this context")
363		}
364	default:
365		r.ctx.ErrorAt(scalar, fmt.Sprintf("Unexpected value '%s'", scalar.String()))
366	}
367	return scalar
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