1//go:build link
2
3package main
4
5// Read source files and RFC and errata files, and cross-link them.
6
7// todo: also cross-reference typescript and possibly other files. switch from go parser to just reading the source as text.
8
9import (
10 "bytes"
11 "flag"
12 "fmt"
13 "go/parser"
14 "go/token"
15 "log"
16 "os"
17 "path/filepath"
18 "regexp"
19 "strconv"
20 "strings"
21)
22
23func usage() {
24 log.Println("usage: link ../*.go ../*/*.go")
25 flag.PrintDefaults()
26 os.Exit(2)
27}
28
29func main() {
30 log.SetFlags(0)
31 flag.Usage = usage
32 flag.Parse()
33 args := flag.Args()
34 if len(args) == 0 {
35 usage()
36 }
37
38 type ref struct {
39 srcpath string
40 srclineno int
41 dstpath string
42 dstlineno int
43 dstisrfc bool
44 dstrfc string // e.g. "5322" or "6376-eid4810"
45 comment string // e.g. "todo" or "todo spec"
46 }
47
48 // RFC-file to RFC-line to references to list of file+line (possibly RFCs).
49 rfcLineSources := map[string]map[int][]ref{}
50
51 // Source-file to source-line to references of RFCs.
52 sourceLineRFCs := map[string]map[int][]ref{}
53
54 re := regexp.MustCompile(`((../)*)rfc/([0-9]{4,5})(-eid([1-9][0-9]*))?(:([1-9][0-9]*))?`)
55
56 addRef := func(m map[string]map[int][]ref, rfc string, lineno int, r ref) {
57 lineRefs := m[rfc]
58 if lineRefs == nil {
59 lineRefs = map[int][]ref{}
60 m[rfc] = lineRefs
61 }
62 lineRefs[lineno] = append(lineRefs[lineno], r)
63 }
64
65 // Parse all .go files on the cli, assumed to be relative to current dir.
66 fset := token.NewFileSet()
67 for _, arg := range args {
68 f, err := parser.ParseFile(fset, arg, nil, parser.ParseComments|parser.SkipObjectResolution)
69 if err != nil {
70 log.Fatalf("parse file %q: %s", arg, err)
71 }
72 for _, cg := range f.Comments {
73 for _, c := range cg.List {
74 lines := strings.Split(c.Text, "\n")
75 for i, line := range lines {
76 matches := re.FindAllStringSubmatch(line, -1)
77 if len(matches) == 0 {
78 continue
79 }
80
81 var comment string
82 if strings.HasPrefix(line, "// todo") {
83 s, _, have := strings.Cut(strings.TrimPrefix(line, "// "), ":")
84 if have {
85 comment = s
86 } else {
87 comment = "todo"
88 }
89 }
90
91 srcpath := arg
92 srclineno := fset.Position(c.Pos()).Line + i
93 dir := filepath.Dir(srcpath)
94 for _, m := range matches {
95 pre := m[1]
96 rfc := m[3]
97 eid := m[5]
98 lineStr := m[7]
99 if eid != "" && lineStr != "" {
100 log.Fatalf("%s:%d: cannot reference both errata (eid %q) to specified line number", srcpath, srclineno, eid)
101 }
102 var dstlineno int
103 if lineStr != "" {
104 v, err := strconv.ParseInt(lineStr, 10, 32)
105 if err != nil {
106 log.Fatalf("%s:%d: bad linenumber %q: %v", srcpath, srclineno, lineStr, err)
107 }
108 dstlineno = int(v)
109 }
110 if dstlineno <= 0 {
111 dstlineno = 1
112 }
113 if eid != "" {
114 rfc += "-eid" + eid
115 }
116 dstpath := filepath.Join(dir, pre+"rfc", rfc)
117 if _, err := os.Stat(dstpath); err != nil {
118 log.Fatalf("%s:%d: references %s: %v", srcpath, srclineno, dstpath, err)
119 }
120 r := ref{srcpath, srclineno, dstpath, dstlineno, true, rfc, comment}
121 addRef(sourceLineRFCs, r.srcpath, r.srclineno, r)
122 addRef(rfcLineSources, r.dstrfc, r.dstlineno, ref{r.dstrfc, r.dstlineno, r.srcpath, r.srclineno, false, "", comment})
123 }
124 }
125 }
126 }
127 }
128
129 files, err := os.ReadDir(".")
130 if err != nil {
131 log.Fatalf("readdir: %v", err)
132 }
133 for _, de := range files {
134 name := de.Name()
135 isrfc := isRFC(name)
136 iserrata := isErrata(name)
137 if !isrfc && !iserrata {
138 continue
139 }
140 oldBuf, err := os.ReadFile(name)
141 if err != nil {
142 log.Fatalf("readdir: %v", err)
143 }
144 old := string(oldBuf)
145 b := &bytes.Buffer{}
146 lineRefs := rfcLineSources[name]
147 lines := strings.Split(old, "\n")
148 if len(lines) > 0 && lines[len(lines)-1] == "" {
149 lines = lines[:len(lines)-1]
150 }
151 for i, line := range lines {
152 if !(iserrata && i > 0) && len(line) > 80 {
153 line = strings.TrimRight(line[:80], " ")
154 }
155 refs := lineRefs[i+1]
156 if len(refs) > 0 {
157 line = fmt.Sprintf("%-80s", line)
158
159 // Lookup source files for rfc:line, so we can cross-link the rfcs.
160 done := map[string]bool{}
161 for _, r := range refs {
162 for _, xr := range sourceLineRFCs[r.dstpath][r.dstlineno] {
163 sref := fmt.Sprintf(" %s:%d", xr.dstrfc, xr.dstlineno)
164 if xr.dstrfc == name && xr.dstlineno == i+1 || done[sref] {
165 continue
166 }
167 line += sref
168 done[sref] = true
169 }
170 }
171
172 // Add link from rfc to source code.
173 for _, r := range refs {
174 comment := r.comment
175 if comment != "" {
176 comment += ": "
177 }
178 line += fmt.Sprintf(" %s%s:%d", comment, r.dstpath, r.dstlineno)
179 }
180 }
181 line += "\n"
182 b.WriteString(line)
183 }
184 newBuf := b.Bytes()
185 if !bytes.Equal(oldBuf, newBuf) {
186 if err := os.WriteFile(name, newBuf, 0660); err != nil {
187 log.Printf("writefile %q: %s", name, err)
188 }
189 log.Print(name)
190 }
191 }
192}
193
194func isRFC(name string) bool {
195 if len(name) < 4 || len(name) > 5 {
196 return false
197 }
198 for _, c := range name {
199 if c < '0' || c > '9' {
200 return false
201 }
202 }
203 return true
204}
205
206func isErrata(name string) bool {
207 t := strings.Split(name, "-")
208 return len(t) == 2 && isRFC(t[0]) && strings.HasPrefix(t[1], "eid")
209}
210