detached d978b07

Merge 2c9c8ea83388495f96efe4a0831d582e03e03e6f into ba4ad521151d3500de111a037937f8e1dd3922d2

commit
d978b07b2a8b0f87e35e894cdd012bcfe768db25
author
ermite · · 2026-10-22 06:13 UTC
merge of
ba4ad52 + 2c9c8ea · diff against the first parent
signature
unsigned
3 files · +397 -1
M cmd/xtgen/main.go +31 -1
@@ -1,4 +1,34 @@
11 // Command xtgen generates bindings from the pinned xcb-proto XML.
22 package main
33
4-func main() {}
4+import (
5+ "flag"
6+ "fmt"
7+ "os"
8+ "path/filepath"
9+)
10+
11+func main() {
12+ all := flag.Bool("all", false, "process all vendored modules")
13+ flag.Parse()
14+ paths := flag.Args()
15+ if *all {
16+ var err error
17+ paths, err = filepath.Glob("spec/xml/*.xml")
18+ if err != nil {
19+ fmt.Fprintln(os.Stderr, err)
20+ os.Exit(1)
21+ }
22+ }
23+ if len(paths) == 0 {
24+ fmt.Fprintln(os.Stderr, "xtgen: supply XML paths or -all")
25+ os.Exit(1)
26+ }
27+ // T2.1 validates and builds the AST. Resolution and emission follow in T2.2.
28+ for _, path := range paths {
29+ if _, err := parseFile(path); err != nil {
30+ fmt.Fprintln(os.Stderr, err)
31+ os.Exit(1)
32+ }
33+ }
34+}
@@ -1,4 +1,34 @@
1 // Command xtgen generates bindings from the pinned xcb-proto XML.1 // Command xtgen generates bindings from the pinned xcb-proto XML.
2 package main2 package main
3 3
4-func main() {}4+import (
5+ "flag"
6+ "fmt"
7+ "os"
8+ "path/filepath"
9+)
10+
11+func main() {
12+ all := flag.Bool("all", false, "process all vendored modules")
13+ flag.Parse()
14+ paths := flag.Args()
15+ if *all {
16+ var err error
17+ paths, err = filepath.Glob("spec/xml/*.xml")
18+ if err != nil {
19+ fmt.Fprintln(os.Stderr, err)
20+ os.Exit(1)
21+ }
22+ }
23+ if len(paths) == 0 {
24+ fmt.Fprintln(os.Stderr, "xtgen: supply XML paths or -all")
25+ os.Exit(1)
26+ }
27+ // T2.1 validates and builds the AST. Resolution and emission follow in T2.2.
28+ for _, path := range paths {
29+ if _, err := parseFile(path); err != nil {
30+ fmt.Fprintln(os.Stderr, err)
31+ os.Exit(1)
32+ }
33+ }
34+}
A cmd/xtgen/parse.go +292 -0
@@ -0,0 +1,292 @@
1+package main
2+
3+import (
4+ "encoding/xml"
5+ "fmt"
6+ "io"
7+ "os"
8+ "strconv"
9+ "strings"
10+)
11+
12+// Node retains XML order and source locations, including expressions. Resolution
13+// turns this syntax tree into the flat, typed emission IR; parsing makes no Go
14+// naming or layout choices.
15+type Node struct {
16+ Kind string
17+ Attr map[string]string
18+ Text string
19+ Children []*Node
20+ File string
21+ Line int
22+}
23+
24+func (n *Node) fail(format string, args ...any) error {
25+ return fmt.Errorf("%s:%d: %s", n.File, n.Line, fmt.Sprintf(format, args...))
26+}
27+
28+// Attribute and child whitelists are per element, not global: a known attribute
29+// or element used in the wrong place is also an error. Documentation is ignored.
30+var attributes = map[string]string{
31+ "xcb": "header extension-name extension-xname extension-multiword major-version minor-version",
32+ "import": "", "typedef": "oldname newname", "xidtype": "name", "xidunion": "name", "type": "",
33+ "enum": "name", "item": "name", "value": "", "bit": "", "struct": "name", "union": "name",
34+ "request": "name opcode combine-adjacent", "reply": "", "event": "name number no-sequence-number xge",
35+ "eventcopy": "name number ref", "error": "name number type", "errorcopy": "name number ref",
36+ "field": "name type enum altenum mask", "pad": "bytes align", "list": "name type enum",
37+ "exprfield": "name type", "switch": "name", "bitcase": "", "fieldref": "", "enumref": "ref",
38+ "op": "op", "unop": "op", "popcount": "", "doc": "",
39+}
40+var children = map[string]string{
41+ "xcb": "import typedef xidtype xidunion enum struct union request event eventcopy error errorcopy doc",
42+ "xidunion": "type", "enum": "item doc", "item": "value bit doc",
43+ "struct": "field pad list doc", "union": "field pad list doc",
44+ "request": "field pad list exprfield switch reply doc", "reply": "field pad list exprfield doc",
45+ "event": "field pad list exprfield doc", "error": "field pad doc",
46+ "list": "value fieldref op unop popcount enumref", "exprfield": "value fieldref op unop popcount enumref",
47+ "switch": "fieldref op unop popcount enumref bitcase", "bitcase": "enumref value field pad doc",
48+ "op": "value fieldref op unop popcount enumref", "unop": "value fieldref op unop popcount enumref",
49+ "popcount": "value fieldref op unop popcount enumref",
50+}
51+
52+func contains(words, word string) bool {
53+ for _, s := range strings.Fields(words) {
54+ if s == word {
55+ return true
56+ }
57+ }
58+ return false
59+}
60+
61+func parseFile(path string) (*Node, error) {
62+ f, err := os.Open(path)
63+ if err != nil {
64+ return nil, err
65+ }
66+ defer f.Close()
67+ return parseXML(path, f)
68+}
69+func parseXML(file string, r io.Reader) (*Node, error) {
70+ d := xml.NewDecoder(r)
71+ var root *Node
72+ var stack []*Node
73+ for {
74+ token, err := d.Token()
75+ if err == io.EOF {
76+ break
77+ }
78+ if err != nil {
79+ line, _ := d.InputPos()
80+ return nil, fmt.Errorf("%s:%d: %w", file, line, err)
81+ }
82+ switch t := token.(type) {
83+ case xml.StartElement:
84+ line, _ := d.InputPos()
85+ n := &Node{Kind: t.Name.Local, Attr: make(map[string]string), File: file, Line: line}
86+ allowed, ok := attributes[n.Kind]
87+ if !ok || t.Name.Space != "" {
88+ return nil, n.fail("unsupported element <%s>", t.Name.Local)
89+ }
90+ if len(stack) == 0 {
91+ if root != nil || n.Kind != "xcb" {
92+ return nil, n.fail("expected a single <xcb> root")
93+ }
94+ root = n
95+ } else {
96+ parent := stack[len(stack)-1]
97+ if !contains(children[parent.Kind], n.Kind) {
98+ return nil, n.fail("unsupported <%s> in <%s>", n.Kind, parent.Kind)
99+ }
100+ parent.Children = append(parent.Children, n)
101+ }
102+ if n.Kind == "doc" {
103+ if err := d.Skip(); err != nil {
104+ return nil, n.fail("invalid doc: %v", err)
105+ }
106+ continue
107+ }
108+ for _, a := range t.Attr {
109+ if a.Name.Space != "" || !contains(allowed, a.Name.Local) {
110+ return nil, n.fail("unknown attribute %q on <%s>", a.Name.Local, n.Kind)
111+ }
112+ if _, ok := n.Attr[a.Name.Local]; ok {
113+ return nil, n.fail("duplicate attribute %q", a.Name.Local)
114+ }
115+ n.Attr[a.Name.Local] = a.Value
116+ }
117+ stack = append(stack, n)
118+ case xml.EndElement:
119+ if len(stack) == 0 {
120+ return nil, fmt.Errorf("%s: unexpected closing element", file)
121+ }
122+ n := stack[len(stack)-1]
123+ n.Text = strings.TrimSpace(n.Text)
124+ if err := validateNode(n); err != nil {
125+ return nil, err
126+ }
127+ stack = stack[:len(stack)-1]
128+ case xml.CharData:
129+ if len(stack) > 0 {
130+ stack[len(stack)-1].Text += string(t)
131+ } else if strings.TrimSpace(string(t)) != "" {
132+ return nil, fmt.Errorf("%s: text outside root", file)
133+ }
134+ case xml.Directive:
135+ return nil, fmt.Errorf("%s: XML directives are unsupported", file)
136+ }
137+ }
138+ if root == nil {
139+ return nil, fmt.Errorf("%s:1: missing <xcb> root", file)
140+ }
141+ return root, nil
142+}
143+func require(n *Node, names string) error {
144+ for _, name := range strings.Fields(names) {
145+ if n.Attr[name] == "" {
146+ return n.fail("missing %s on <%s>", name, n.Kind)
147+ }
148+ }
149+ return nil
150+}
151+func number(n *Node, attr string, bits int) error {
152+ _, err := strconv.ParseUint(n.Attr[attr], 0, bits)
153+ if err != nil {
154+ return n.fail("invalid %s %q", attr, n.Attr[attr])
155+ }
156+ return nil
157+}
158+func exprCount(n *Node) int {
159+ count := 0
160+ for _, c := range n.Children {
161+ if c.Kind != "doc" {
162+ count++
163+ }
164+ }
165+ return count
166+}
167+func validateNode(n *Node) error {
168+ required := map[string]string{
169+ "xcb": "header", "typedef": "oldname newname", "xidtype": "name", "xidunion": "name",
170+ "enum": "name", "item": "name", "struct": "name", "union": "name", "request": "name opcode",
171+ "event": "name number", "eventcopy": "name number ref", "error": "name number", "errorcopy": "name number ref",
172+ "field": "name type", "list": "name type", "exprfield": "name type", "switch": "name", "enumref": "ref", "op": "op", "unop": "op",
173+ }
174+ if err := require(n, required[n.Kind]); err != nil {
175+ return err
176+ }
177+ switch n.Kind {
178+ case "import", "type", "fieldref", "enumref":
179+ if n.Text == "" {
180+ return n.fail("empty <%s>", n.Kind)
181+ }
182+ case "value", "bit":
183+ if _, err := strconv.ParseInt(n.Text, 0, 64); err != nil {
184+ return n.fail("invalid <%s> integer %q", n.Kind, n.Text)
185+ }
186+ case "pad":
187+ _, b := n.Attr["bytes"]
188+ _, a := n.Attr["align"]
189+ if b == a {
190+ return n.fail("pad requires exactly one of bytes or align")
191+ }
192+ key := "bytes"
193+ if a {
194+ key = "align"
195+ }
196+ if err := number(n, key, 31); err != nil {
197+ return err
198+ }
199+ if a {
200+ v, _ := strconv.Atoi(n.Attr[key])
201+ if v == 0 || v&(v-1) != 0 {
202+ return n.fail("pad alignment must be a positive power of two")
203+ }
204+ }
205+ case "request":
206+ if err := number(n, "opcode", 8); err != nil {
207+ return err
208+ }
209+ case "event", "eventcopy", "error", "errorcopy":
210+ if err := number(n, "number", 8); err != nil {
211+ return err
212+ }
213+ if n.Attr["xge"] == "true" {
214+ v, _ := strconv.Atoi(n.Attr["number"])
215+ if v >= 128 {
216+ return n.fail("xge evtype must be < 128")
217+ }
218+ }
219+ case "op":
220+ if !contains("+ - * / & | << >>", n.Attr["op"]) {
221+ return n.fail("unsupported operator %q", n.Attr["op"])
222+ }
223+ if exprCount(n) != 2 {
224+ return n.fail("op requires two operands")
225+ }
226+ case "unop":
227+ if n.Attr["op"] != "~" {
228+ return n.fail("unsupported unary operator %q", n.Attr["op"])
229+ }
230+ if exprCount(n) != 1 {
231+ return n.fail("unop requires one operand")
232+ }
233+ case "popcount", "exprfield", "item":
234+ if exprCount(n) != 1 {
235+ return n.fail("<%s> requires one expression", n.Kind)
236+ }
237+ case "list":
238+ if exprCount(n) > 1 {
239+ return n.fail("list has multiple length expressions")
240+ }
241+ case "bitcase":
242+ fields := 0
243+ for _, c := range n.Children {
244+ if c.Kind == "field" {
245+ fields++
246+ if contains("CARD8 CARD16 INT8 INT16 BOOL char BYTE void", c.Attr["type"]) {
247+ return c.fail("value-list bitcase requires a 4-byte field")
248+ }
249+ }
250+ }
251+ if fields != 1 {
252+ return n.fail("value-list bitcase requires exactly one field")
253+ }
254+ }
255+ for _, key := range []string{"xge", "no-sequence-number", "combine-adjacent", "extension-multiword"} {
256+ if v, ok := n.Attr[key]; ok && v != "true" && v != "false" {
257+ return n.fail("invalid boolean %s=%q", key, v)
258+ }
259+ }
260+ // Forward references cannot be laid out in one pass. Reject at the length
261+ // expression's location rather than failing later while emitting Go.
262+ declared := map[string]bool{}
263+ for _, c := range n.Children {
264+ if c.Kind == "field" || c.Kind == "exprfield" {
265+ declared[c.Attr["name"]] = true
266+ }
267+ }
268+ seen := map[string]bool{}
269+ for _, c := range n.Children {
270+ if c.Kind == "list" {
271+ var visit func(*Node) error
272+ visit = func(e *Node) error {
273+ if e.Kind == "fieldref" && declared[e.Text] && !seen[e.Text] {
274+ return e.fail("list length references later field %q", e.Text)
275+ }
276+ for _, child := range e.Children {
277+ if err := visit(child); err != nil {
278+ return err
279+ }
280+ }
281+ return nil
282+ }
283+ if err := visit(c); err != nil {
284+ return err
285+ }
286+ }
287+ if c.Kind == "field" || c.Kind == "exprfield" {
288+ seen[c.Attr["name"]] = true
289+ }
290+ }
291+ return nil
292+}
@@ -0,0 +1,292 @@
1+package main
2+
3+import (
4+ "encoding/xml"
5+ "fmt"
6+ "io"
7+ "os"
8+ "strconv"
9+ "strings"
10+)
11+
12+// Node retains XML order and source locations, including expressions. Resolution
13+// turns this syntax tree into the flat, typed emission IR; parsing makes no Go
14+// naming or layout choices.
15+type Node struct {
16+ Kind string
17+ Attr map[string]string
18+ Text string
19+ Children []*Node
20+ File string
21+ Line int
22+}
23+
24+func (n *Node) fail(format string, args ...any) error {
25+ return fmt.Errorf("%s:%d: %s", n.File, n.Line, fmt.Sprintf(format, args...))
26+}
27+
28+// Attribute and child whitelists are per element, not global: a known attribute
29+// or element used in the wrong place is also an error. Documentation is ignored.
30+var attributes = map[string]string{
31+ "xcb": "header extension-name extension-xname extension-multiword major-version minor-version",
32+ "import": "", "typedef": "oldname newname", "xidtype": "name", "xidunion": "name", "type": "",
33+ "enum": "name", "item": "name", "value": "", "bit": "", "struct": "name", "union": "name",
34+ "request": "name opcode combine-adjacent", "reply": "", "event": "name number no-sequence-number xge",
35+ "eventcopy": "name number ref", "error": "name number type", "errorcopy": "name number ref",
36+ "field": "name type enum altenum mask", "pad": "bytes align", "list": "name type enum",
37+ "exprfield": "name type", "switch": "name", "bitcase": "", "fieldref": "", "enumref": "ref",
38+ "op": "op", "unop": "op", "popcount": "", "doc": "",
39+}
40+var children = map[string]string{
41+ "xcb": "import typedef xidtype xidunion enum struct union request event eventcopy error errorcopy doc",
42+ "xidunion": "type", "enum": "item doc", "item": "value bit doc",
43+ "struct": "field pad list doc", "union": "field pad list doc",
44+ "request": "field pad list exprfield switch reply doc", "reply": "field pad list exprfield doc",
45+ "event": "field pad list exprfield doc", "error": "field pad doc",
46+ "list": "value fieldref op unop popcount enumref", "exprfield": "value fieldref op unop popcount enumref",
47+ "switch": "fieldref op unop popcount enumref bitcase", "bitcase": "enumref value field pad doc",
48+ "op": "value fieldref op unop popcount enumref", "unop": "value fieldref op unop popcount enumref",
49+ "popcount": "value fieldref op unop popcount enumref",
50+}
51+
52+func contains(words, word string) bool {
53+ for _, s := range strings.Fields(words) {
54+ if s == word {
55+ return true
56+ }
57+ }
58+ return false
59+}
60+
61+func parseFile(path string) (*Node, error) {
62+ f, err := os.Open(path)
63+ if err != nil {
64+ return nil, err
65+ }
66+ defer f.Close()
67+ return parseXML(path, f)
68+}
69+func parseXML(file string, r io.Reader) (*Node, error) {
70+ d := xml.NewDecoder(r)
71+ var root *Node
72+ var stack []*Node
73+ for {
74+ token, err := d.Token()
75+ if err == io.EOF {
76+ break
77+ }
78+ if err != nil {
79+ line, _ := d.InputPos()
80+ return nil, fmt.Errorf("%s:%d: %w", file, line, err)
81+ }
82+ switch t := token.(type) {
83+ case xml.StartElement:
84+ line, _ := d.InputPos()
85+ n := &Node{Kind: t.Name.Local, Attr: make(map[string]string), File: file, Line: line}
86+ allowed, ok := attributes[n.Kind]
87+ if !ok || t.Name.Space != "" {
88+ return nil, n.fail("unsupported element <%s>", t.Name.Local)
89+ }
90+ if len(stack) == 0 {
91+ if root != nil || n.Kind != "xcb" {
92+ return nil, n.fail("expected a single <xcb> root")
93+ }
94+ root = n
95+ } else {
96+ parent := stack[len(stack)-1]
97+ if !contains(children[parent.Kind], n.Kind) {
98+ return nil, n.fail("unsupported <%s> in <%s>", n.Kind, parent.Kind)
99+ }
100+ parent.Children = append(parent.Children, n)
101+ }
102+ if n.Kind == "doc" {
103+ if err := d.Skip(); err != nil {
104+ return nil, n.fail("invalid doc: %v", err)
105+ }
106+ continue
107+ }
108+ for _, a := range t.Attr {
109+ if a.Name.Space != "" || !contains(allowed, a.Name.Local) {
110+ return nil, n.fail("unknown attribute %q on <%s>", a.Name.Local, n.Kind)
111+ }
112+ if _, ok := n.Attr[a.Name.Local]; ok {
113+ return nil, n.fail("duplicate attribute %q", a.Name.Local)
114+ }
115+ n.Attr[a.Name.Local] = a.Value
116+ }
117+ stack = append(stack, n)
118+ case xml.EndElement:
119+ if len(stack) == 0 {
120+ return nil, fmt.Errorf("%s: unexpected closing element", file)
121+ }
122+ n := stack[len(stack)-1]
123+ n.Text = strings.TrimSpace(n.Text)
124+ if err := validateNode(n); err != nil {
125+ return nil, err
126+ }
127+ stack = stack[:len(stack)-1]
128+ case xml.CharData:
129+ if len(stack) > 0 {
130+ stack[len(stack)-1].Text += string(t)
131+ } else if strings.TrimSpace(string(t)) != "" {
132+ return nil, fmt.Errorf("%s: text outside root", file)
133+ }
134+ case xml.Directive:
135+ return nil, fmt.Errorf("%s: XML directives are unsupported", file)
136+ }
137+ }
138+ if root == nil {
139+ return nil, fmt.Errorf("%s:1: missing <xcb> root", file)
140+ }
141+ return root, nil
142+}
143+func require(n *Node, names string) error {
144+ for _, name := range strings.Fields(names) {
145+ if n.Attr[name] == "" {
146+ return n.fail("missing %s on <%s>", name, n.Kind)
147+ }
148+ }
149+ return nil
150+}
151+func number(n *Node, attr string, bits int) error {
152+ _, err := strconv.ParseUint(n.Attr[attr], 0, bits)
153+ if err != nil {
154+ return n.fail("invalid %s %q", attr, n.Attr[attr])
155+ }
156+ return nil
157+}
158+func exprCount(n *Node) int {
159+ count := 0
160+ for _, c := range n.Children {
161+ if c.Kind != "doc" {
162+ count++
163+ }
164+ }
165+ return count
166+}
167+func validateNode(n *Node) error {
168+ required := map[string]string{
169+ "xcb": "header", "typedef": "oldname newname", "xidtype": "name", "xidunion": "name",
170+ "enum": "name", "item": "name", "struct": "name", "union": "name", "request": "name opcode",
171+ "event": "name number", "eventcopy": "name number ref", "error": "name number", "errorcopy": "name number ref",
172+ "field": "name type", "list": "name type", "exprfield": "name type", "switch": "name", "enumref": "ref", "op": "op", "unop": "op",
173+ }
174+ if err := require(n, required[n.Kind]); err != nil {
175+ return err
176+ }
177+ switch n.Kind {
178+ case "import", "type", "fieldref", "enumref":
179+ if n.Text == "" {
180+ return n.fail("empty <%s>", n.Kind)
181+ }
182+ case "value", "bit":
183+ if _, err := strconv.ParseInt(n.Text, 0, 64); err != nil {
184+ return n.fail("invalid <%s> integer %q", n.Kind, n.Text)
185+ }
186+ case "pad":
187+ _, b := n.Attr["bytes"]
188+ _, a := n.Attr["align"]
189+ if b == a {
190+ return n.fail("pad requires exactly one of bytes or align")
191+ }
192+ key := "bytes"
193+ if a {
194+ key = "align"
195+ }
196+ if err := number(n, key, 31); err != nil {
197+ return err
198+ }
199+ if a {
200+ v, _ := strconv.Atoi(n.Attr[key])
201+ if v == 0 || v&(v-1) != 0 {
202+ return n.fail("pad alignment must be a positive power of two")
203+ }
204+ }
205+ case "request":
206+ if err := number(n, "opcode", 8); err != nil {
207+ return err
208+ }
209+ case "event", "eventcopy", "error", "errorcopy":
210+ if err := number(n, "number", 8); err != nil {
211+ return err
212+ }
213+ if n.Attr["xge"] == "true" {
214+ v, _ := strconv.Atoi(n.Attr["number"])
215+ if v >= 128 {
216+ return n.fail("xge evtype must be < 128")
217+ }
218+ }
219+ case "op":
220+ if !contains("+ - * / & | << >>", n.Attr["op"]) {
221+ return n.fail("unsupported operator %q", n.Attr["op"])
222+ }
223+ if exprCount(n) != 2 {
224+ return n.fail("op requires two operands")
225+ }
226+ case "unop":
227+ if n.Attr["op"] != "~" {
228+ return n.fail("unsupported unary operator %q", n.Attr["op"])
229+ }
230+ if exprCount(n) != 1 {
231+ return n.fail("unop requires one operand")
232+ }
233+ case "popcount", "exprfield", "item":
234+ if exprCount(n) != 1 {
235+ return n.fail("<%s> requires one expression", n.Kind)
236+ }
237+ case "list":
238+ if exprCount(n) > 1 {
239+ return n.fail("list has multiple length expressions")
240+ }
241+ case "bitcase":
242+ fields := 0
243+ for _, c := range n.Children {
244+ if c.Kind == "field" {
245+ fields++
246+ if contains("CARD8 CARD16 INT8 INT16 BOOL char BYTE void", c.Attr["type"]) {
247+ return c.fail("value-list bitcase requires a 4-byte field")
248+ }
249+ }
250+ }
251+ if fields != 1 {
252+ return n.fail("value-list bitcase requires exactly one field")
253+ }
254+ }
255+ for _, key := range []string{"xge", "no-sequence-number", "combine-adjacent", "extension-multiword"} {
256+ if v, ok := n.Attr[key]; ok && v != "true" && v != "false" {
257+ return n.fail("invalid boolean %s=%q", key, v)
258+ }
259+ }
260+ // Forward references cannot be laid out in one pass. Reject at the length
261+ // expression's location rather than failing later while emitting Go.
262+ declared := map[string]bool{}
263+ for _, c := range n.Children {
264+ if c.Kind == "field" || c.Kind == "exprfield" {
265+ declared[c.Attr["name"]] = true
266+ }
267+ }
268+ seen := map[string]bool{}
269+ for _, c := range n.Children {
270+ if c.Kind == "list" {
271+ var visit func(*Node) error
272+ visit = func(e *Node) error {
273+ if e.Kind == "fieldref" && declared[e.Text] && !seen[e.Text] {
274+ return e.fail("list length references later field %q", e.Text)
275+ }
276+ for _, child := range e.Children {
277+ if err := visit(child); err != nil {
278+ return err
279+ }
280+ }
281+ return nil
282+ }
283+ if err := visit(c); err != nil {
284+ return err
285+ }
286+ }
287+ if c.Kind == "field" || c.Kind == "exprfield" {
288+ seen[c.Attr["name"]] = true
289+ }
290+ }
291+ return nil
292+}
A cmd/xtgen/parse_test.go +74 -0
@@ -0,0 +1,74 @@
1+package main
2+
3+import (
4+ "path/filepath"
5+ "strings"
6+ "testing"
7+)
8+
9+func TestParseVendored(t *testing.T) {
10+ files, err := filepath.Glob("../../spec/xml/*.xml")
11+ if err != nil {
12+ t.Fatal(err)
13+ }
14+ if len(files) != 9 {
15+ t.Fatalf("got %d XML files, want 9", len(files))
16+ }
17+ for _, file := range files {
18+ t.Run(filepath.Base(file), func(t *testing.T) {
19+ n, err := parseFile(file)
20+ if err != nil {
21+ t.Fatal(err)
22+ }
23+ if n.Kind != "xcb" || len(n.Children) == 0 {
24+ t.Fatal("missing module AST")
25+ }
26+ })
27+ }
28+}
29+func TestParseRejects(t *testing.T) {
30+ cases := []struct{ name, body, want string }{
31+ {"unknown element", "<surprise/>", "unsupported element"},
32+ {"unknown attribute", `<xidtype name="WINDOW" surprise="yes"/>`, "unknown attribute"},
33+ {"misplaced known element", `<struct name="S"><reply/></struct>`, "unsupported <reply>"},
34+ {"sumof", `<request name="R" opcode="1"><list name="a" type="CARD8"><sumof ref="a"/></list></request>`, "sumof"},
35+ {"paramref", `<request name="R" opcode="1"><list name="a" type="CARD8"><paramref type="CARD8">a</paramref></list></request>`, "paramref"},
36+ {"case", `<request name="R" opcode="1"><switch name="s"><case/></switch></request>`, "case"},
37+ {"struct switch", `<struct name="S"><switch name="s"/></struct>`, "unsupported <switch>"},
38+ {"reply switch", `<request name="R" opcode="1"><reply><switch name="s"/></reply></request>`, "unsupported <switch>"},
39+ {"nested switch", `<request name="R" opcode="1"><switch name="s"><bitcase><switch name="s"/></bitcase></switch></request>`, "unsupported <switch>"},
40+ {"fd", `<request name="R" opcode="1"><fd name="fd"/></request>`, "fd"},
41+ {"eventstruct", `<eventstruct name="E"/>`, "eventstruct"},
42+ {"required alignment", `<struct name="S"><required_start_align align="4"/></struct>`, "required_start_align"},
43+ {"forward length", `<struct name="S"><list name="a" type="CARD8"><fieldref>n</fieldref></list><field name="n" type="CARD8"/></struct>`, "later field"},
44+ {"xge code", `<event name="E" number="128" xge="true"/>`, "evtype"},
45+ {"bad opcode", `<request name="R" opcode="256"/>`, "invalid opcode"},
46+ {"bad operator", `<request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="%"><value>1</value><value>2</value></op></exprfield></request>`, "unsupported operator"},
47+ {"wrong operands", `<request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="+"><value>1</value></op></exprfield></request>`, "two operands"},
48+ {"bad pad", `<struct name="S"><pad align="3"/></struct>`, "power of two"},
49+ {"namespace", `<xidtype xmlns="other" name="X"/>`, "unsupported element"},
50+ {"duplicate attr", `<xidtype name="X" name="Y"/>`, "duplicate attribute"},
51+ }
52+ for _, tc := range cases {
53+ t.Run(tc.name, func(t *testing.T) {
54+ _, err := parseXML("bad.xml", strings.NewReader("<xcb header=\"test\">\n"+tc.body+"\n</xcb>"))
55+ if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "bad.xml:2:") {
56+ t.Fatalf("got %v, want file:line and %q", err, tc.want)
57+ }
58+ })
59+ }
60+}
61+func TestParseAST(t *testing.T) {
62+ n, err := parseXML("ok.xml", strings.NewReader(`<xcb header="a"><doc><arbitrary/></doc><request name="R" opcode="1"><field type="CARD32" name="n"/><list type="CARD8" name="data"><op op="*"><fieldref>n</fieldref><value>4</value></op></list></request></xcb>`))
63+ if err != nil {
64+ t.Fatal(err)
65+ }
66+ r := n.Children[1]
67+ if r.Attr["name"] != "R" || r.Children[1].Children[0].Children[0].Text != "n" {
68+ t.Fatal("AST lost order or expression")
69+ }
70+}
71+func FuzzParseXML(f *testing.F) {
72+ f.Add(`<xcb header="test"/>`)
73+ f.Fuzz(func(t *testing.T, raw string) { _, _ = parseXML("fuzz.xml", strings.NewReader(raw)) })
74+}
@@ -0,0 +1,74 @@
1+package main
2+
3+import (
4+ "path/filepath"
5+ "strings"
6+ "testing"
7+)
8+
9+func TestParseVendored(t *testing.T) {
10+ files, err := filepath.Glob("../../spec/xml/*.xml")
11+ if err != nil {
12+ t.Fatal(err)
13+ }
14+ if len(files) != 9 {
15+ t.Fatalf("got %d XML files, want 9", len(files))
16+ }
17+ for _, file := range files {
18+ t.Run(filepath.Base(file), func(t *testing.T) {
19+ n, err := parseFile(file)
20+ if err != nil {
21+ t.Fatal(err)
22+ }
23+ if n.Kind != "xcb" || len(n.Children) == 0 {
24+ t.Fatal("missing module AST")
25+ }
26+ })
27+ }
28+}
29+func TestParseRejects(t *testing.T) {
30+ cases := []struct{ name, body, want string }{
31+ {"unknown element", "<surprise/>", "unsupported element"},
32+ {"unknown attribute", `<xidtype name="WINDOW" surprise="yes"/>`, "unknown attribute"},
33+ {"misplaced known element", `<struct name="S"><reply/></struct>`, "unsupported <reply>"},
34+ {"sumof", `<request name="R" opcode="1"><list name="a" type="CARD8"><sumof ref="a"/></list></request>`, "sumof"},
35+ {"paramref", `<request name="R" opcode="1"><list name="a" type="CARD8"><paramref type="CARD8">a</paramref></list></request>`, "paramref"},
36+ {"case", `<request name="R" opcode="1"><switch name="s"><case/></switch></request>`, "case"},
37+ {"struct switch", `<struct name="S"><switch name="s"/></struct>`, "unsupported <switch>"},
38+ {"reply switch", `<request name="R" opcode="1"><reply><switch name="s"/></reply></request>`, "unsupported <switch>"},
39+ {"nested switch", `<request name="R" opcode="1"><switch name="s"><bitcase><switch name="s"/></bitcase></switch></request>`, "unsupported <switch>"},
40+ {"fd", `<request name="R" opcode="1"><fd name="fd"/></request>`, "fd"},
41+ {"eventstruct", `<eventstruct name="E"/>`, "eventstruct"},
42+ {"required alignment", `<struct name="S"><required_start_align align="4"/></struct>`, "required_start_align"},
43+ {"forward length", `<struct name="S"><list name="a" type="CARD8"><fieldref>n</fieldref></list><field name="n" type="CARD8"/></struct>`, "later field"},
44+ {"xge code", `<event name="E" number="128" xge="true"/>`, "evtype"},
45+ {"bad opcode", `<request name="R" opcode="256"/>`, "invalid opcode"},
46+ {"bad operator", `<request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="%"><value>1</value><value>2</value></op></exprfield></request>`, "unsupported operator"},
47+ {"wrong operands", `<request name="R" opcode="1"><exprfield name="x" type="CARD32"><op op="+"><value>1</value></op></exprfield></request>`, "two operands"},
48+ {"bad pad", `<struct name="S"><pad align="3"/></struct>`, "power of two"},
49+ {"namespace", `<xidtype xmlns="other" name="X"/>`, "unsupported element"},
50+ {"duplicate attr", `<xidtype name="X" name="Y"/>`, "duplicate attribute"},
51+ }
52+ for _, tc := range cases {
53+ t.Run(tc.name, func(t *testing.T) {
54+ _, err := parseXML("bad.xml", strings.NewReader("<xcb header=\"test\">\n"+tc.body+"\n</xcb>"))
55+ if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "bad.xml:2:") {
56+ t.Fatalf("got %v, want file:line and %q", err, tc.want)
57+ }
58+ })
59+ }
60+}
61+func TestParseAST(t *testing.T) {
62+ n, err := parseXML("ok.xml", strings.NewReader(`<xcb header="a"><doc><arbitrary/></doc><request name="R" opcode="1"><field type="CARD32" name="n"/><list type="CARD8" name="data"><op op="*"><fieldref>n</fieldref><value>4</value></op></list></request></xcb>`))
63+ if err != nil {
64+ t.Fatal(err)
65+ }
66+ r := n.Children[1]
67+ if r.Attr["name"] != "R" || r.Children[1].Children[0].Children[0].Text != "n" {
68+ t.Fatal("AST lost order or expression")
69+ }
70+}
71+func FuzzParseXML(f *testing.F) {
72+ f.Add(`<xcb header="test"/>`)
73+ f.Fuzz(func(t *testing.T, raw string) { _, _ = parseXML("fuzz.xml", strings.NewReader(raw)) })
74+}

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 1

↑↓ moveenter openesc close