carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// approved_moves_copy_scan_test.go
2package main
3
4import (
5 "fmt"
6 "go/ast"
7 "go/parser"
8 "go/token"
9 "os"
10 "path/filepath"
11 "slices"
12 "sort"
13 "strconv"
14 "strings"
15 "testing"
16)
17
18// The analyser behind TestMovesStringsAreRecorded and TestMovesSinksAreSentences.
19//
20// It fails closed. A value that reaches an operator sink (a jsonError message,
21// a Check's Reason, a Phase 1 copy table, a sentence-returning function) must
22// be one the analyser can positively derive to text: a literal, a const, a
23// literal-format Sprintf or concatenation, a local whose every assignment is
24// such text, a call to a package helper whose every return is, a parameter
25// bound to such an argument, or a field whose every write in the scanned files
26// is. The text it derives is matched against the record. Anything it cannot
27// derive is a violation, and a pattern with no text of its own (a bare error
28// forwarded through "%v" or "" + err.Error()) is a violation too and an
29// unrecorded sentence: it is never silently dropped. Hitting a limit is a
30// violation that names file:line.
31//
32// A placeholder (an operand of a concatenation, an argument of a Sprintf) may
33// be data, such as a host name: the row it is matched against must carry a
34// <placeholder> at that spot, and the record is where a human vouches for it.
35// A placeholder must not carry an error's text that the gate can see. These
36// are violations there, even when the format matches a row (errorText):
37//
38// - a call to Error();
39// - a value whose declared type is error or implements it;
40// - a value the gate cannot type that is named err or ends in Err or err;
41// - a local, or a helper parameter, that holds any of these;
42// - the argument of a %w verb;
43// - a call to a package helper with a string result, any of whose returns
44// carries an error's text, or that the gate cannot read (a return of a
45// shape it does not know, a receiver it cannot resolve, a call to
46// itself);
47// - a field, any of whose writes in the scanned files carries an error's
48// text or takes one result of a multi-value expression;
49// - any of these hitting the analysis depth limit.
50//
51// Data the gate cannot derive (a region read from a request, a list joined by
52// strings.Join, a host name in a field) may stand for a placeholder. That
53// includes a value made from a helper result the gate cannot read (a local
54// assigned it, a field written from it, an argument bound to it) when the
55// result carries no error's text the gate can see. The real tree's region
56// and host placeholders are read that way. The strict reading, where any
57// such value is a violation, refused 13 real Phase 1 sinks whose
58// placeholders are all data.
59//
60// What it is (ruling T15-scope): a lint against accidental drift into
61// operator copy, not a boundary against code written to evade it. The
62// whole-branch review and code review cover deliberate constructions. These
63// are its known limits:
64//
65// - Types are syntactic and same-package. It does not follow generics,
66// embedded structs, type aliases, or methods promoted through embedding.
67// A value of another package's type is an error only by its type's name
68// (one ending in Error) or, when its type is unknown, by its own name.
69// An error held in a variable of an unrelated name and an unknown type
70// (x := pkg.Do(); Sprintf("%v", x)) is read as data. So is a helper
71// result of any type other than string or error (a Stringer formatted by
72// %v).
73// - A helper reads its parameters by their call sites only where it is
74// called as a placeholder. A field written from the writer's own
75// parameter (target{Name: n}) is read by n's type, not by what the
76// writer's callers pass, so mk(err.Error()) through such a field is not
77// seen.
78// - Receivers are not lexically scoped. A method call whose receiver
79// identifier is declared more than once in the function is refused; a
80// receiver declared once is resolved by its one declaration.
81// - Struct fields written by JSON decoding, reflection, through a pointer
82// to the field (p := &x.Reason; *p = ...), with +=, or in files outside
83// movesScanFiles are not seen. A field read reflects the writes it can
84// see. A write that takes one result of a multi-value expression is a
85// non-sentence write.
86// - A Reason read as a placeholder whose writes fail only softly (a
87// variable the gate cannot read, not an error's text or a limit) is a
88// wildcard, as any other placeholder is.
89// - Function values: a direct reference to a sentence-flow function (a
90// package function, a method, or a closure configured in movesLocalSinks)
91// that is not a call is refused. A closure that is not configured is not
92// tracked as a value; its own parameters are unbound at its sinks, which
93// refuses them there.
94// - Anything that leaves the scanned files (a map or channel carrying text
95// from elsewhere, a global set at init) is read as far as the files say
96// and no further.
97
98// movesSentenceFields are the struct fields that carry a sentence from the
99// function that makes it to the one that speaks it. A read of one as a
100// placeholder is forwarded, not wild, when every write to it is a sentence.
101var movesSentenceFields = map[string]bool{"Reason": true, "StuckReason": true}
102
103// fwdMark stands for a value that is a sink somewhere else (a forwarded
104// parameter, a field read): it is checked where it is made, not here.
105const fwdMark = "\x01"
106
107const (
108 maxEvalDepth = 10
109 maxAlts = 64
110 maxEvalSteps = 3000000
111)
112
113type problem struct {
114 msg string
115 hard bool // survives a placeholder: a limit, an ambiguity
116}
117
118func (p problem) String() string { return p.msg }
119
120// ---- the package index ----
121
122type movesPkgIndex struct {
123 vars map[string]ast.Expr
124 funcs map[string]*ast.FuncDecl
125 methods map[string][]*ast.FuncDecl
126 types map[string]ast.Expr
127 decls []*ast.FuncDecl
128}
129
130var movesPkgCache = map[string]*movesPkgIndex{}
131
132// movesFset positions every file the scan parses, so a violation inside a
133// helper in a sibling file can be named file:line.
134var movesFset = token.NewFileSet()
135
136func newPkgIndex() *movesPkgIndex {
137 return &movesPkgIndex{vars: map[string]ast.Expr{}, funcs: map[string]*ast.FuncDecl{},
138 methods: map[string][]*ast.FuncDecl{}, types: map[string]ast.Expr{}}
139}
140
141func (p *movesPkgIndex) addFile(f *ast.File) {
142 for _, d := range f.Decls {
143 switch d := d.(type) {
144 case *ast.FuncDecl:
145 p.decls = append(p.decls, d)
146 if d.Recv == nil {
147 p.funcs[d.Name.Name] = d
148 } else {
149 p.methods[d.Name.Name] = append(p.methods[d.Name.Name], d)
150 }
151 case *ast.GenDecl:
152 for _, s := range d.Specs {
153 switch s := s.(type) {
154 case *ast.ValueSpec:
155 if len(s.Names) == len(s.Values) {
156 for i, nm := range s.Names {
157 p.vars[nm.Name] = s.Values[i]
158 }
159 }
160 case *ast.TypeSpec:
161 p.types[s.Name.Name] = s.Type
162 }
163 }
164 }
165 }
166}
167
168func loadMovesPkg(t *testing.T, dir string) *movesPkgIndex {
169 t.Helper()
170 if p, ok := movesPkgCache[dir]; ok {
171 return p
172 }
173 p := newPkgIndex()
174 movesPkgCache[dir] = p
175 ents, err := os.ReadDir(dir)
176 if err != nil {
177 t.Fatal(err)
178 }
179 for _, e := range ents {
180 n := e.Name()
181 if e.IsDir() || !strings.HasSuffix(n, ".go") || strings.HasSuffix(n, "_test.go") {
182 continue
183 }
184 f, err := parser.ParseFile(movesFset, filepath.Join(dir, n), nil, 0)
185 if err != nil {
186 t.Fatal(err)
187 }
188 p.addFile(f)
189 }
190 return p
191}
192
193func recvBase(d *ast.FuncDecl) string {
194 if d.Recv == nil || len(d.Recv.List) == 0 {
195 return ""
196 }
197 return baseTypeName(d.Recv.List[0].Type)
198}
199
200func baseTypeName(e ast.Expr) string {
201 switch e := e.(type) {
202 case *ast.Ident:
203 return e.Name
204 case *ast.StarExpr:
205 return baseTypeName(e.X)
206 case *ast.ParenExpr:
207 return baseTypeName(e.X)
208 case *ast.SelectorExpr:
209 return baseTypeName(e.X) + "." + e.Sel.Name // another package's type: never a package method
210 }
211 return ""
212}
213
214func paramNames(ft *ast.FuncType) []string {
215 var out []string
216 if ft.Params == nil {
217 return out
218 }
219 for _, f := range ft.Params.List {
220 if len(f.Names) == 0 {
221 out = append(out, "_")
222 }
223 for _, n := range f.Names {
224 out = append(out, n.Name)
225 }
226 }
227 return out
228}
229
230func resultCount(d *ast.FuncDecl) int {
231 if d.Type.Results == nil {
232 return 0
233 }
234 n := 0
235 for _, f := range d.Type.Results.List {
236 if len(f.Names) == 0 {
237 n++
238 }
239 n += len(f.Names)
240 }
241 return n
242}
243
244// stringResults is the set of a function's result positions that are strings.
245func stringResults(d *ast.FuncDecl) map[int]bool {
246 out := map[int]bool{}
247 if d.Type.Results == nil {
248 return out
249 }
250 i := 0
251 for _, f := range d.Type.Results.List {
252 n := len(f.Names)
253 if n == 0 {
254 n = 1
255 }
256 if id, ok := f.Type.(*ast.Ident); ok && id.Name == "string" {
257 for k := 0; k < n; k++ {
258 out[i+k] = true
259 }
260 }
261 i += n
262 }
263 return out
264}
265
266func namedResults(d *ast.FuncDecl) []string {
267 var out []string
268 if d.Type.Results == nil {
269 return out
270 }
271 for _, f := range d.Type.Results.List {
272 for _, n := range f.Names {
273 out = append(out, n.Name)
274 }
275 }
276 return out
277}
278
279// ---- local assignments ----
280
281// localAssign is one assignment to a variable of a function: an expression, a
282// position of a multi-value call, a zero value (a declaration with no value),
283// or something the gate cannot read (a range variable).
284type localAssign struct {
285 rhs ast.Expr
286 call *ast.CallExpr
287 pos int
288 zero bool
289 unreadable bool
290 typ ast.Expr // a declared type, when there is one
291 typeFrom ast.Expr // for an unreadable value, the expression whose type it has
292}
293
294func localAssigns(fd *ast.FuncDecl) map[string][]localAssign {
295 out := map[string][]localAssign{}
296 if fd == nil || fd.Body == nil {
297 return out
298 }
299 ast.Inspect(fd.Body, func(n ast.Node) bool {
300 switch n := n.(type) {
301 case *ast.AssignStmt:
302 for i, l := range n.Lhs {
303 id, ok := l.(*ast.Ident)
304 if !ok || id.Name == "_" {
305 continue
306 }
307 switch {
308 case len(n.Rhs) == len(n.Lhs):
309 out[id.Name] = append(out[id.Name], localAssign{rhs: n.Rhs[i]})
310 case len(n.Rhs) == 1:
311 if c, ok := n.Rhs[0].(*ast.CallExpr); ok {
312 out[id.Name] = append(out[id.Name], localAssign{call: c, pos: i})
313 } else if ix, ok := n.Rhs[0].(*ast.IndexExpr); ok && i == 0 {
314 out[id.Name] = append(out[id.Name], localAssign{unreadable: true, typeFrom: ix})
315 } else {
316 out[id.Name] = append(out[id.Name], localAssign{unreadable: true})
317 }
318 }
319 }
320 case *ast.ValueSpec:
321 for i, id := range n.Names {
322 switch {
323 case len(n.Values) == len(n.Names):
324 out[id.Name] = append(out[id.Name], localAssign{rhs: n.Values[i], typ: n.Type})
325 case len(n.Values) == 0:
326 out[id.Name] = append(out[id.Name], localAssign{zero: true, typ: n.Type})
327 case len(n.Values) == 1:
328 if c, ok := n.Values[0].(*ast.CallExpr); ok {
329 out[id.Name] = append(out[id.Name], localAssign{call: c, pos: i, typ: n.Type})
330 } else {
331 out[id.Name] = append(out[id.Name], localAssign{unreadable: true})
332 }
333 }
334 }
335 case *ast.RangeStmt:
336 for _, x := range []ast.Expr{n.Key, n.Value} {
337 if id, ok := x.(*ast.Ident); ok && id.Name != "_" {
338 out[id.Name] = append(out[id.Name], localAssign{unreadable: true})
339 }
340 }
341 }
342 return true
343 })
344 return out
345}
346
347// ---- the universe: every struct field and every write to it ----
348
349type fieldWrite struct {
350 file string
351 fd *ast.FuncDecl
352 expr ast.Expr
353 ctype string // the composite literal's struct type, when it names one
354 recv ast.Expr // for an assignment, the expression whose field is written
355 multi bool // one result of a multi-value expression: not provable
356}
357
358type fieldResult struct {
359 done bool
360 probs []problem
361}
362
363type movesUniverse struct {
364 t *testing.T
365 root string
366 fieldWrites map[string][]*fieldWrite
367 fieldTypes map[string]map[string]bool
368 structFields map[string][]string
369 fieldTypeOf map[string]map[string]ast.Expr
370 results map[string]*fieldResult
371 fieldTexts map[string]string // fieldText's answer per field and struct
372 used map[string]bool // package functions evaluated as sentence helpers
373 paramSinks map[string]bool // functions whose parameter reaches a sink
374 files []string
375 steps int
376 // diagSkipped counts, per movesDiagnosticWrites entry, the writes the
377 // universe left out of the field's writes.
378 diagSkipped map[string]int
379}
380
381// callsError reports whether e calls an Error method: the write carries an
382// error's text. Only such writes can be listed in movesDiagnosticWrites.
383func callsError(e ast.Expr) bool {
384 found := false
385 ast.Inspect(e, func(n ast.Node) bool {
386 if c, ok := n.(*ast.CallExpr); ok {
387 if sel, ok := c.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "Error" {
388 found = true
389 }
390 }
391 return !found
392 })
393 return found
394}
395
396func (u *movesUniverse) at(p string) string {
397 if u.root == "." {
398 return p
399 }
400 return filepath.Join(u.root, p)
401}
402
403func newUniverse(t *testing.T, root string, files []string) *movesUniverse {
404 t.Helper()
405 u := &movesUniverse{t: t, root: root, fieldWrites: map[string][]*fieldWrite{}, fieldTypes: map[string]map[string]bool{},
406 structFields: map[string][]string{}, fieldTypeOf: map[string]map[string]ast.Expr{}, results: map[string]*fieldResult{}, fieldTexts: map[string]string{}, used: map[string]bool{},
407 paramSinks: map[string]bool{}, files: files, diagSkipped: map[string]int{}}
408 parsed := map[string]*ast.File{}
409 for _, f := range files {
410 af, err := parser.ParseFile(movesFset, u.at(f), nil, 0)
411 if err != nil {
412 t.Fatal(err)
413 }
414 parsed[f] = af
415 }
416 addType := func(field, typ string) {
417 if u.fieldTypes[field] == nil {
418 u.fieldTypes[field] = map[string]bool{}
419 }
420 u.fieldTypes[field][typ] = true
421 }
422 structNames := map[*ast.StructType]string{}
423 for _, af := range parsed {
424 ast.Inspect(af, func(n ast.Node) bool {
425 if ts, ok := n.(*ast.TypeSpec); ok {
426 if st, ok := ts.Type.(*ast.StructType); ok {
427 structNames[st] = ts.Name.Name
428 }
429 }
430 return true
431 })
432 }
433 for _, af := range parsed {
434 ast.Inspect(af, func(n ast.Node) bool {
435 st, ok := n.(*ast.StructType)
436 if !ok {
437 return true
438 }
439 name, named := structNames[st]
440 if !named {
441 name = fmt.Sprintf("anonymous struct at %s", movesFset.Position(st.Pos()))
442 structNames[st] = name
443 }
444 var order []string
445 if u.fieldTypeOf[name] == nil {
446 u.fieldTypeOf[name] = map[string]ast.Expr{}
447 }
448 for _, f := range st.Fields.List {
449 for _, nm := range f.Names {
450 order = append(order, nm.Name)
451 addType(nm.Name, name)
452 u.fieldTypeOf[name][nm.Name] = f.Type
453 }
454 }
455 u.structFields[name] = order
456 return true
457 })
458 }
459 for _, f := range files {
460 af := parsed[f]
461 var fd *ast.FuncDecl
462 var visit func(n ast.Node) bool
463 visit = func(n ast.Node) bool {
464 switch n := n.(type) {
465 case *ast.FuncDecl:
466 fd = n
467 if n.Body != nil {
468 ast.Inspect(n.Body, visit)
469 }
470 fd = nil
471 return false
472 case *ast.CompositeLit:
473 var order []string
474 ctype := ""
475 switch ty := n.Type.(type) {
476 case *ast.Ident:
477 order = u.structFields[ty.Name]
478 ctype = ty.Name
479 case *ast.SelectorExpr:
480 order = u.structFields[ty.Sel.Name]
481 ctype = ty.Sel.Name
482 case *ast.StructType:
483 order = u.structFields[structNames[ty]]
484 ctype = structNames[ty]
485 }
486 for i, el := range n.Elts {
487 if kv, ok := el.(*ast.KeyValueExpr); ok {
488 if k, ok := kv.Key.(*ast.Ident); ok {
489 if fd != nil {
490 if key := fd.Name.Name + "|" + ctype + "." + k.Name; movesDiagnosticWrites[f][key].Why != "" && callsError(kv.Value) {
491 u.diagSkipped[f+"|"+key]++
492 continue
493 }
494 }
495 u.fieldWrites[k.Name] = append(u.fieldWrites[k.Name], &fieldWrite{file: f, fd: fd, expr: kv.Value, ctype: ctype})
496 }
497 } else if i < len(order) {
498 u.fieldWrites[order[i]] = append(u.fieldWrites[order[i]], &fieldWrite{file: f, fd: fd, expr: el, ctype: ctype})
499 }
500 }
501 case *ast.AssignStmt:
502 for i, l := range n.Lhs {
503 s, ok := l.(*ast.SelectorExpr)
504 if !ok {
505 continue
506 }
507 if len(n.Rhs) == len(n.Lhs) {
508 u.fieldWrites[s.Sel.Name] = append(u.fieldWrites[s.Sel.Name], &fieldWrite{file: f, fd: fd, expr: n.Rhs[i], recv: s.X})
509 } else {
510 // box.Reason, _ = detail(err) or box.Reason, ok = m[k]: one
511 // result of a multi-value expression, which the gate cannot
512 // prove is a sentence. The expression is the field written.
513 u.fieldWrites[s.Sel.Name] = append(u.fieldWrites[s.Sel.Name], &fieldWrite{file: f, fd: fd, expr: s, recv: s.X, multi: true})
514 }
515 }
516 }
517 return true
518 }
519 ast.Inspect(af, visit)
520 }
521 return u
522}
523
524var movesDefaultUniverse *movesUniverse
525
526// universeFor picks the universe a file is read in: the real tree's scanned
527// files, or just the file itself (a fixture, or a base-commit copy).
528func universeFor(t *testing.T, root, file string) *movesUniverse {
529 t.Helper()
530 if root == "." {
531 for _, f := range movesScanFiles {
532 if f == file {
533 if movesDefaultUniverse == nil {
534 movesDefaultUniverse = newUniverse(t, ".", movesScanFiles)
535 }
536 return movesDefaultUniverse
537 }
538 }
539 }
540 return newUniverse(t, root, []string{file})
541}
542
543// ---- evaluation ----
544
545type evalCtx struct {
546 u *movesUniverse
547 file string
548 fd *ast.FuncDecl
549 pkg *movesPkgIndex
550 locals map[string][]localAssign
551 bind map[string][]string
552 fwd map[string]bool
553 fwdFn map[string]bool
554 errParams map[string]string // a helper's parameters bound to an error's text, and why
555 curReturn *ast.ReturnStmt
556 noCalls bool
557 visiting map[string]bool
558}
559
560func (u *movesUniverse) newCtx(file string, fd *ast.FuncDecl, noCalls bool) *evalCtx {
561 c := &evalCtx{u: u, file: file, fd: fd, locals: localAssigns(fd), bind: map[string][]string{},
562 fwd: map[string]bool{}, fwdFn: map[string]bool{}, errParams: map[string]string{}, noCalls: noCalls, visiting: map[string]bool{}}
563 c.pkg = loadMovesPkg(u.t, u.at(filepath.Dir(file)))
564 if fd == nil {
565 return c
566 }
567 if i, ok := movesLocalSinks[file][fd.Name.Name]; ok {
568 if ps := paramNames(fd.Type); i < len(ps) {
569 c.fwd[ps[i]] = true
570 }
571 }
572 if i, ok := movesFuncSinks[file][fd.Name.Name]; ok {
573 if ps := paramNames(fd.Type); i < len(ps) {
574 c.fwdFn[ps[i]] = true
575 }
576 }
577 // A closure that is a local sink (fail := func(why string)) forwards its parameter.
578 ast.Inspect(fd.Body, func(n ast.Node) bool {
579 as, ok := n.(*ast.AssignStmt)
580 if !ok || len(as.Lhs) != 1 || len(as.Rhs) != 1 {
581 return true
582 }
583 id, ok := as.Lhs[0].(*ast.Ident)
584 lit, isLit := as.Rhs[0].(*ast.FuncLit)
585 if !ok || !isLit {
586 return true
587 }
588 if i, ok := movesLocalSinks[file][id.Name]; ok {
589 if ps := paramNames(lit.Type); i < len(ps) {
590 c.fwd[ps[i]] = true
591 }
592 }
593 return true
594 })
595 return c
596}
597
598func (c *evalCtx) posOf(n ast.Node) string { return movesFset.Position(n.Pos()).String() }
599
600func hardOnly(ps []problem) []problem {
601 var out []problem
602 for _, p := range ps {
603 if p.hard {
604 out = append(out, p)
605 }
606 }
607 return out
608}
609
610func limit(c *evalCtx, n ast.Node, what string) []problem {
611 return []problem{{fmt.Sprintf("analysis limit reached at %s: %s", c.posOf(n), what), true}}
612}
613
614func cross(l, r []string) ([]string, bool) {
615 if len(l)*len(r) > maxAlts {
616 return nil, false
617 }
618 out := make([]string, 0, len(l)*len(r))
619 for _, a := range l {
620 for _, b := range r {
621 out = append(out, a+b)
622 }
623 }
624 return out, true
625}
626
627func dedupe(in []string) []string {
628 seen := map[string]bool{}
629 var out []string
630 for _, s := range in {
631 if !seen[s] {
632 seen[s] = true
633 out = append(out, s)
634 }
635 }
636 return out
637}
638
639// operand reads a placeholder: any value is allowed, so what it cannot derive
640// becomes one wildcard, which the record must bind to a <placeholder>. Only a
641// hard problem (a limit, a cycle, an error's text) still counts.
642func (c *evalCtx) operand(e ast.Expr, depth int) ([]string, []problem) {
643 if sel, isField := e.(*ast.SelectorExpr); isField && !c.noCalls {
644 // A field read as a placeholder is a wildcard for the record's
645 // <placeholder>, except a field that carries a sentence from one function
646 // to another (a Check's Reason): that is forwarded when every write to it
647 // is a sentence. A write's hard problem (a limit, an error's text, a
648 // multi-value write) is never covered by the wildcard.
649 if movesSentenceFields[sel.Sel.Name] {
650 a, p := c.fieldRead(sel)
651 if len(p) == 0 {
652 return a, nil
653 }
654 return []string{wild}, hardOnly(p)
655 }
656 return []string{wild}, nil
657 }
658 alts, probs := c.eval(e, depth)
659 if len(probs) == 0 {
660 return alts, nil
661 }
662 return []string{wild}, hardOnly(probs)
663}
664
665// concatOperand reads one side of a +: a placeholder, unless it is an error's
666// text. A side that is itself a concatenation is checked by its own eval.
667func (c *evalCtx) concatOperand(e ast.Expr, depth int) ([]string, []problem) {
668 if !isConcat(e) {
669 if why := c.errorText(e, 0); why != "" {
670 return []string{wild}, []problem{errorTextProblem(e, why)}
671 }
672 }
673 return c.operand(e, depth)
674}
675
676func isConcat(e ast.Expr) bool {
677 switch e := e.(type) {
678 case *ast.ParenExpr:
679 return isConcat(e.X)
680 case *ast.BinaryExpr:
681 return e.Op == token.ADD
682 }
683 return false
684}
685
686func errorTextProblem(e ast.Expr, why string) problem {
687 what := "an error's text"
688 m, rest := whyClass(why)
689 if m != "" {
690 what = "text the gate cannot derive"
691 }
692 return problem{fmt.Sprintf("%s puts %s in a sentence (%s)", render(e), what, rest), true}
693}
694
695// errName is the name an error usually goes by: err, or one ending in Err or
696// err. It is used only when the gate cannot type the value.
697func errName(n string) bool {
698 return strings.HasSuffix(n, "err") || strings.HasSuffix(n, "Err")
699}
700
701// isErrorType reports whether a declared type is error or implements it: the
702// error interface, a package type with an Error method, a package interface
703// that embeds error or declares Error, or another package's type whose name
704// ends in Error.
705func (c *evalCtx) isErrorType(t ast.Expr) bool {
706 switch t := unstar(t).(type) {
707 case *ast.Ident:
708 if t.Name == "error" {
709 return true
710 }
711 for _, d := range c.pkg.methods["Error"] {
712 if recvBase(d) == t.Name {
713 return true
714 }
715 }
716 if it, ok := c.pkg.types[t.Name].(*ast.InterfaceType); ok && it.Methods != nil {
717 for _, m := range it.Methods.List {
718 if id, ok := m.Type.(*ast.Ident); ok && id.Name == "error" && len(m.Names) == 0 {
719 return true
720 }
721 for _, n := range m.Names {
722 if n.Name == "Error" {
723 return true
724 }
725 }
726 }
727 }
728 case *ast.SelectorExpr:
729 return strings.HasSuffix(t.Sel.Name, "Error")
730 }
731 return false
732}
733
734// The classes of a reason errorText gives, carried as a mark at its front.
735// An unmarked reason is an error's text, and it counts wherever the value
736// goes. An unprovable one (an analysis limit, a multi-value field write) counts
737// wherever the value goes too: the gate does not know what the text is. An
738// underivable one (a helper result the gate cannot read: a return of a shape
739// it does not know, a receiver it cannot resolve, a call to itself) counts only
740// for the value that is that result. A value merely made from it (a local
741// assigned it, an argument bound to it, a field written from it) is data, as
742// a placeholder may be: the real tree's region and host names are read
743// through helpers the gate cannot fully derive.
744const (
745 underivable = "\x03"
746 unprovable = "\x04"
747)
748
749func whyClass(why string) (mark, rest string) {
750 if strings.HasPrefix(why, underivable) || strings.HasPrefix(why, unprovable) {
751 return why[:1], why[1:]
752 }
753 return "", why
754}
755
756// rewrap wraps why in format (why is its last verb), keeping its class.
757func rewrap(why, format string, a ...any) string {
758 m, rest := whyClass(why)
759 return m + fmt.Sprintf(format, append(a, rest)...)
760}
761
762// dataWhy is why as it passes through data: an underivable result drops out.
763func dataWhy(why string) string {
764 if m, _ := whyClass(why); m == underivable {
765 return ""
766 }
767 return why
768}
769
770// errorText says why e is an error's text, or text the gate cannot derive
771// (see the classes above), or "" when the gate has no reason to think either.
772// It reads the expression as written, the locals it names, the bodies of the
773// package helpers it calls, and the writes of the fields it reads.
774func (c *evalCtx) errorText(e ast.Expr, depth int) string {
775 if depth > maxEvalDepth {
776 return fmt.Sprintf("%sanalysis limit reached at %s: the gate ran out of depth reading what text it carries", unprovable, c.posOf(e))
777 }
778 switch e := e.(type) {
779 case *ast.ParenExpr:
780 return c.errorText(e.X, depth+1)
781 case *ast.BinaryExpr:
782 if e.Op != token.ADD {
783 return ""
784 }
785 if why := c.errorText(e.X, depth+1); why != "" {
786 return why
787 }
788 return c.errorText(e.Y, depth+1)
789 case *ast.CallExpr:
790 if sel, ok := e.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "Error" && len(e.Args) == 0 {
791 return "it calls Error"
792 }
793 if t := c.typeExprOf(e, 0); t != nil && c.isErrorType(t) {
794 return "its result is an error"
795 }
796 // A call out of the package (fmt.Sprint, strings.TrimSpace) carries the
797 // text of its arguments. A package helper is read through its body.
798 decls, p := c.resolveCallees(e)
799 if p != nil && p.hard {
800 return underivable + p.msg // a receiver the gate cannot resolve
801 }
802 if p != nil {
803 for _, a := range e.Args {
804 if why := dataWhy(c.errorText(a, depth+1)); why != "" {
805 return rewrap(why, "it is made from %s, and %s", render(a))
806 }
807 }
808 return ""
809 }
810 return c.helperText(e, decls, 0, depth)
811 case *ast.Ident:
812 if why, ok := c.errParams[e.Name]; ok {
813 return why
814 }
815 t := c.typeExprOf(e, 0)
816 if t != nil && c.isErrorType(t) {
817 return "its type is error"
818 }
819 key := fmt.Sprintf("errtext %p %s", c.fd, e.Name)
820 if !c.visiting[key] {
821 c.visiting[key] = true
822 defer delete(c.visiting, key)
823 for _, a := range c.locals[e.Name] {
824 switch {
825 case a.rhs != nil:
826 if why := dataWhy(c.errorText(a.rhs, depth+1)); why != "" {
827 if _, rest := whyClass(why); strings.HasPrefix(rest, "it is assigned ") {
828 return why // an alias chain: its innermost step says it
829 }
830 return rewrap(why, "it is assigned %s at %s, and %s", render(a.rhs), c.posOf(a.rhs))
831 }
832 case a.call != nil:
833 if decls, p := c.resolveCallees(a.call); p == nil && len(decls) > 0 && decls[0].Type != nil {
834 if rt := resultTypeAt(decls[0], a.pos); rt != nil && c.isErrorType(rt) {
835 return fmt.Sprintf("it is assigned an error result at %s", c.posOf(a.call))
836 }
837 if why := dataWhy(c.helperText(a.call, decls, a.pos, depth)); why != "" {
838 return rewrap(why, "it is assigned %s at %s, and %s", render(a.call), c.posOf(a.call))
839 }
840 }
841 }
842 }
843 }
844 if t == nil && errName(e.Name) {
845 return "the gate cannot type it, and it is named as an error"
846 }
847 case *ast.SelectorExpr:
848 t := c.typeExprOf(e, 0)
849 if t != nil && c.isErrorType(t) {
850 return "its type is error"
851 }
852 if t == nil && errName(e.Sel.Name) {
853 return "the gate cannot type it, and it is named as an error"
854 }
855 return c.fieldText(e)
856 }
857 return ""
858}
859
860func isStringType(t ast.Expr) bool {
861 id, ok := t.(*ast.Ident)
862 return ok && id.Name == "string"
863}
864
865// bindErrParams marks hc's parameters that call binds to an error's text (or
866// to text the gate cannot prove). They are refused where the helper speaks
867// them, not where it only compares them.
868func (c *evalCtx) bindErrParams(hc *evalCtx, d *ast.FuncDecl, call *ast.CallExpr, depth int) {
869 for i, p := range paramNames(d.Type) {
870 if i < len(call.Args) {
871 if why := dataWhy(c.errorText(call.Args[i], depth)); why != "" {
872 hc.errParams[p] = rewrap(why, "it is bound to %s at %s, and %s", render(call.Args[i]), c.posOf(call.Args[i]))
873 }
874 }
875 }
876}
877
878// helperText reads a call to a package helper whose result at pos is a string,
879// used as a placeholder, through its body: each return is read with errorText
880// in the helper's own context, its parameters marked where the call binds
881// them to an error's text. A return that carries an error's text, or one the
882// gate cannot read, makes the call a violation. A return of data (a parameter,
883// a region read from a request, a list joined by strings.Join) may stand for
884// a <placeholder>, as any placeholder may. A helper with another result type
885// (an int for %d) is not text.
886func (c *evalCtx) helperText(e *ast.CallExpr, decls []*ast.FuncDecl, pos, depth int) string {
887 if c.noCalls || len(decls) == 0 {
888 return ""
889 }
890 for _, d := range decls {
891 if d.Type == nil || d.Body == nil || !isStringType(resultTypeAt(d, pos)) {
892 return ""
893 }
894 }
895 if depth > maxEvalDepth {
896 return fmt.Sprintf("%sanalysis limit reached at %s: helpers nested too deeply", unprovable, c.posOf(e))
897 }
898 for _, d := range decls {
899 key := fmt.Sprintf("errhelper %p %d", d, pos)
900 if c.visiting[key] {
901 return fmt.Sprintf("%shelper %s calls itself at %s", underivable, d.Name.Name, c.posOf(e))
902 }
903 dfile := strings.TrimPrefix(movesFset.Position(d.Pos()).Filename, c.u.root+"/")
904 hc := c.u.newCtx(dfile, d, c.noCalls)
905 hc.visiting = c.visiting
906 c.bindErrParams(hc, d, e, depth+1)
907 c.visiting[key] = true
908 why := hc.returnsText(d, pos, depth+1)
909 delete(c.visiting, key)
910 switch m, _ := whyClass(why); {
911 case why == "":
912 case m == "":
913 return fmt.Sprintf("helper %s returns an error's text: %s", d.Name.Name, why)
914 default:
915 return rewrap(why, "helper %s returns text the gate cannot derive: %s", d.Name.Name)
916 }
917 }
918 return ""
919}
920
921// returnsText reads each return of d at pos with errorText (c is d's own
922// context), as callDecl reads them for a sentence.
923func (c *evalCtx) returnsText(d *ast.FuncDecl, pos, depth int) string {
924 nres := resultCount(d)
925 named := namedResults(d)
926 why := ""
927 ast.Inspect(d.Body, func(n ast.Node) bool {
928 if why != "" {
929 return false
930 }
931 switch n := n.(type) {
932 case *ast.FuncLit:
933 return false
934 case *ast.ReturnStmt:
935 var r string
936 switch {
937 case len(n.Results) == nres && pos < len(n.Results):
938 r = c.errorText(n.Results[pos], depth)
939 case len(n.Results) == 1 && nres > 1:
940 cl, ok := n.Results[0].(*ast.CallExpr)
941 if !ok {
942 r = fmt.Sprintf("%sreturn at %s has a shape the gate cannot read", underivable, c.posOf(n))
943 break
944 }
945 decls, p := c.resolveCallees(cl)
946 switch {
947 case p != nil && p.hard:
948 r = underivable + p.msg
949 case p != nil:
950 r = fmt.Sprintf("%sreturn at %s forwards %s, which the gate cannot read", underivable, c.posOf(n), render(cl))
951 default:
952 r = c.helperText(cl, decls, pos, depth)
953 }
954 case len(n.Results) == 0 && pos < len(named):
955 r = c.errorText(&ast.Ident{Name: named[pos], NamePos: n.Pos()}, depth)
956 default:
957 r = fmt.Sprintf("%sreturn at %s has a shape the gate cannot read", underivable, c.posOf(n))
958 }
959 if r != "" {
960 why = rewrap(r, "at %s: %s", c.posOf(n))
961 }
962 }
963 return true
964 })
965 return why
966}
967
968// fieldText reads a field used as a placeholder through its writes, as
969// fieldRead does for a Reason: a write that carries an error's text, or takes
970// one result of a multi-value expression, makes the read one too. A write of
971// data (a parameter, a variable, a helper result the gate cannot fully read)
972// may stand for a <placeholder>. Writes to a same-named field of another known
973// struct are not this field's.
974func (c *evalCtx) fieldText(e *ast.SelectorExpr) string {
975 if c.noCalls {
976 return ""
977 }
978 f := e.Sel.Name
979 rt := lastName(baseTypeName(c.typeExprOf(e.X, 0)))
980 key := f + "|" + rt
981 if why, ok := c.u.fieldTexts[key]; ok {
982 return why // a field read while its own writes are read is clean here
983 }
984 c.u.fieldTexts[key] = ""
985 why := ""
986 for _, w := range c.u.fieldWrites[f] {
987 wc := c.u.newCtx(w.file, w.fd, c.noCalls)
988 wt := lastName(w.ctype)
989 if w.recv != nil {
990 wt = lastName(baseTypeName(wc.typeExprOf(w.recv, 0)))
991 }
992 if rt != "" && wt != "" && wt != rt {
993 continue
994 }
995 if w.multi {
996 why = fmt.Sprintf("%sa write to field %s at %s takes one result of a multi-value expression, which the gate cannot prove is a sentence", unprovable, f, wc.posOf(w.expr))
997 break
998 }
999 // From depth 0: the answer is cached per field, so it must not depend
1000 // on how deep the first reader was. A field cycle reads as clean (the
1001 // entry above), and the step limit still bounds the work.
1002 if r := dataWhy(wc.errorText(w.expr, 0)); r != "" {
1003 why = rewrap(r, "a write to field %s at %s: %s", f, wc.posOf(w.expr))
1004 break
1005 }
1006 }
1007 c.u.fieldTexts[key] = why
1008 return why
1009}
1010
1011// wrapArgs returns the argument indexes (into the call's Args, the format at
1012// 0) of a format's %w verbs. ok is false when the format's arguments cannot be
1013// counted (an explicit argument index).
1014func wrapArgs(format string) (idx []int, ok bool) {
1015 if strings.Contains(format, "%[") {
1016 return nil, !strings.Contains(format, "w")
1017 }
1018 arg := 1
1019 for _, v := range formatVerb.FindAllString(format, -1) {
1020 if v == "%%" {
1021 continue
1022 }
1023 arg += strings.Count(v, "*")
1024 if strings.HasSuffix(v, "w") {
1025 idx = append(idx, arg)
1026 }
1027 arg++
1028 }
1029 return idx, true
1030}
1031
1032// eval returns the texts e can be and why it cannot be read.
1033func (c *evalCtx) eval(e ast.Expr, depth int) ([]string, []problem) {
1034 if depth > maxEvalDepth {
1035 return nil, limit(c, e, "expression nested too deeply")
1036 }
1037 if c.u.steps++; c.u.steps > maxEvalSteps {
1038 return nil, limit(c, e, "too much work reading this value")
1039 }
1040 soft := func(format string, a ...any) ([]string, []problem) {
1041 return nil, []problem{{fmt.Sprintf(format, a...), false}}
1042 }
1043 switch e := e.(type) {
1044 case *ast.ParenExpr:
1045 return c.eval(e.X, depth)
1046 case *ast.BasicLit:
1047 if e.Kind != token.STRING {
1048 return soft("%s is not a string", e.Value)
1049 }
1050 v, err := strconv.Unquote(e.Value)
1051 if err != nil {
1052 return soft("%s is not a string literal", e.Value)
1053 }
1054 return []string{v}, nil
1055 case *ast.Ident:
1056 return c.evalIdent(e, depth)
1057 case *ast.BinaryExpr:
1058 if e.Op != token.ADD {
1059 return soft("%s is not a sentence", render(e))
1060 }
1061 l, lp := c.concatOperand(e.X, depth+1)
1062 r, rp := c.concatOperand(e.Y, depth+1)
1063 out, ok := cross(l, r)
1064 if !ok {
1065 return nil, append(append(lp, rp...), limit(c, e, "too many alternative sentences")...)
1066 }
1067 // The text is kept beside the problems, so a refused sentence is still
1068 // named by what it says.
1069 return dedupe(out), append(lp, rp...)
1070 case *ast.SelectorExpr:
1071 if c.noCalls {
1072 return []string{wild}, nil
1073 }
1074 return c.fieldRead(e)
1075 case *ast.CallExpr:
1076 return c.evalCall(e, 0, depth)
1077 }
1078 return soft("%s is not a sentence the gate can read", render(e))
1079}
1080
1081func (c *evalCtx) evalIdent(id *ast.Ident, depth int) ([]string, []problem) {
1082 if depth > maxEvalDepth {
1083 return nil, limit(c, id, "alias chain too long")
1084 }
1085 if c.guardedEcho(id) {
1086 return []string{fwdMark}, nil
1087 }
1088 var alts []string
1089 var probs []problem
1090 known := false
1091 if why, ok := c.errParams[id.Name]; ok {
1092 // Spoken, not merely compared: the helper returns or builds text from a
1093 // parameter bound to an error's text.
1094 what := "an error's text"
1095 m, rest := whyClass(why)
1096 if m != "" {
1097 what = "text the gate cannot derive"
1098 }
1099 return nil, []problem{{fmt.Sprintf("%s is %s (%s)", id.Name, what, rest), true}}
1100 }
1101 if b, ok := c.bind[id.Name]; ok {
1102 known = true
1103 alts = append(alts, b...)
1104 } else if c.fwd[id.Name] {
1105 known = true
1106 alts = append(alts, fwdMark)
1107 }
1108 if as, ok := c.locals[id.Name]; ok {
1109 known = true
1110 key := fmt.Sprintf("local %p %s", c.fd, id.Name)
1111 if c.visiting[key] {
1112 return nil, []problem{{fmt.Sprintf("%s at %s is assigned from itself", id.Name, c.posOf(id)), true}}
1113 }
1114 c.visiting[key] = true
1115 for _, a := range as {
1116 switch {
1117 case a.zero:
1118 alts = append(alts, "")
1119 case a.rhs != nil:
1120 x, p := c.eval(a.rhs, depth+1)
1121 alts, probs = append(alts, x...), append(probs, p...)
1122 case a.call != nil:
1123 x, p := c.evalCall(a.call, a.pos, depth+1)
1124 alts, probs = append(alts, x...), append(probs, p...)
1125 default:
1126 probs = append(probs, problem{fmt.Sprintf("%s is assigned a value the gate cannot read (a range variable or a destructured value)", id.Name), false})
1127 }
1128 }
1129 delete(c.visiting, key)
1130 }
1131 if known {
1132 if len(alts) > maxAlts {
1133 return nil, limit(c, id, "too many alternative sentences")
1134 }
1135 return dedupe(alts), probs
1136 }
1137 if x, ok := c.pkg.vars[id.Name]; ok {
1138 pc := c.u.newCtx(c.file, nil, c.noCalls)
1139 pc.visiting = c.visiting
1140 key := "pkgvar " + id.Name
1141 if c.visiting[key] {
1142 return nil, []problem{{fmt.Sprintf("%s is defined in terms of itself", id.Name), true}}
1143 }
1144 c.visiting[key] = true
1145 defer delete(c.visiting, key)
1146 return pc.eval(x, depth+1)
1147 }
1148 if c.fd != nil {
1149 for _, n := range paramNames(c.fd.Type) {
1150 if n == id.Name {
1151 return nil, []problem{{fmt.Sprintf("parameter %s is not bound to an argument (%s)", id.Name, c.posOf(id)), false}}
1152 }
1153 }
1154 }
1155 return nil, []problem{{fmt.Sprintf("%s is a variable the gate cannot read", id.Name), false}}
1156}
1157
1158// fieldRead reads X.F: a sentence only if every write to a field named F, in
1159// any struct of the scanned files (a keyed or positional composite literal, or
1160// an assignment), is. The text of each write is checked where it is written,
1161// so the read is forwarded.
1162// fieldWriteAlts is the sentences written to the field e reads, in the struct
1163// type it reads (every write, when the type is unknown). A write that is
1164// itself forwarded keeps its mark: it is checked where it is made.
1165func (c *evalCtx) fieldWriteAlts(e *ast.SelectorExpr) []string {
1166 f := e.Sel.Name
1167 rt := lastName(baseTypeName(c.typeExprOf(e.X, 0)))
1168 var alts []string
1169 for _, w := range c.u.fieldWrites[f] {
1170 wc := c.u.newCtx(w.file, w.fd, c.noCalls)
1171 wt := lastName(w.ctype)
1172 if w.recv != nil {
1173 wt = lastName(baseTypeName(wc.typeExprOf(w.recv, 0)))
1174 }
1175 if rt != "" && wt != "" && wt != rt {
1176 continue
1177 }
1178 x, _ := wc.eval(w.expr, 0)
1179 alts = append(alts, x...)
1180 }
1181 return dedupe(alts)
1182}
1183
1184func (c *evalCtx) fieldRead(e *ast.SelectorExpr) ([]string, []problem) {
1185 f := e.Sel.Name
1186 types := c.u.fieldTypes[f]
1187 if len(types) == 0 {
1188 return nil, []problem{{fmt.Sprintf("%s: no struct in the scanned files declares a field %s", render(e), f), false}}
1189 }
1190 rt := lastName(baseTypeName(c.typeExprOf(e.X, 0)))
1191 key := f + "|" + rt
1192 res := c.u.results[key]
1193 if res == nil {
1194 res = &fieldResult{}
1195 c.u.results[key] = res
1196 n := 0
1197 for _, w := range c.u.fieldWrites[f] {
1198 wc := c.u.newCtx(w.file, w.fd, c.noCalls)
1199 // A write whose struct type is known and is not the one read is another
1200 // field that happens to share the name.
1201 wt := lastName(w.ctype)
1202 if w.recv != nil {
1203 wt = lastName(baseTypeName(wc.typeExprOf(w.recv, 0)))
1204 }
1205 if rt != "" && wt != "" && wt != rt {
1206 continue
1207 }
1208 n++
1209 if w.multi {
1210 res.probs = append(res.probs, problem{fmt.Sprintf("a write to field %s at %s takes one result of a multi-value expression, which the gate cannot prove is a sentence", f, wc.posOf(w.expr)), true})
1211 continue
1212 }
1213 alts, probs := wc.eval(w.expr, 0)
1214 for _, p := range probs {
1215 res.probs = append(res.probs, problem{fmt.Sprintf("a write to field %s at %s is not a sentence: %s", f, wc.posOf(w.expr), p.msg), p.hard})
1216 }
1217 for _, a := range alts {
1218 if a != "" && !strings.Contains(a, fwdMark) && !informative(a) {
1219 res.probs = append(res.probs, problem{fmt.Sprintf("a write to field %s at %s has no text of its own (%q)", f, wc.posOf(w.expr), strings.ReplaceAll(a, wild, "%")), false})
1220 }
1221 }
1222 }
1223 if n == 0 {
1224 res.probs = append(res.probs, problem{fmt.Sprintf("field %s is never written in the scanned files", f), false})
1225 }
1226 }
1227 if len(res.probs) > 0 {
1228 return nil, res.probs
1229 }
1230 return []string{fwdMark}, nil
1231}
1232
1233func lastName(s string) string {
1234 if i := strings.LastIndex(s, "."); i >= 0 {
1235 return s[i+1:]
1236 }
1237 return s
1238}
1239
1240func isFormatCall(e *ast.CallExpr) bool {
1241 sel, ok := e.Fun.(*ast.SelectorExpr)
1242 if !ok || len(e.Args) == 0 {
1243 return false
1244 }
1245 id, ok := sel.X.(*ast.Ident)
1246 return ok && ((id.Name == "fmt" && (sel.Sel.Name == "Sprintf" || sel.Sel.Name == "Errorf")) ||
1247 (id.Name == "errors" && sel.Sel.Name == "New"))
1248}
1249
1250// evalCall reads result pos of a call.
1251func (c *evalCtx) evalCall(e *ast.CallExpr, pos, depth int) ([]string, []problem) {
1252 if depth > maxEvalDepth {
1253 return nil, limit(c, e, "call nesting too deep")
1254 }
1255 soft := func(format string, a ...any) ([]string, []problem) {
1256 return nil, []problem{{fmt.Sprintf(format, a...), false}}
1257 }
1258 if isFormatCall(e) {
1259 fmts, probs := c.eval(e.Args[0], depth+1)
1260 if len(probs) > 0 {
1261 return nil, append([]problem{{fmt.Sprintf("%s has a format that is not a literal", render(e)), false}}, probs...)
1262 }
1263 var out []string
1264 var argProbs []problem
1265 flagged := map[int]bool{}
1266 for i, a := range e.Args[1:] {
1267 if why := c.errorText(a, 0); why != "" {
1268 flagged[i+1] = true
1269 argProbs = append(argProbs, errorTextProblem(a, why))
1270 }
1271 }
1272 for _, f := range fmts {
1273 if strings.Contains(f, wild) || strings.Contains(f, fwdMark) {
1274 return soft("%s has a format that is not a literal", render(e))
1275 }
1276 out = append(out, formatPattern(f))
1277 idx, ok := wrapArgs(f)
1278 if !ok {
1279 argProbs = append(argProbs, problem{fmt.Sprintf("%s has a %%w verb whose argument the gate cannot count", render(e)), true})
1280 }
1281 for _, i := range idx {
1282 switch {
1283 case i >= len(e.Args):
1284 argProbs = append(argProbs, problem{fmt.Sprintf("%s has a %%w verb with no argument", render(e)), true})
1285 case !flagged[i]:
1286 flagged[i] = true
1287 argProbs = append(argProbs, problem{fmt.Sprintf("%s is the argument of a %%w verb, an error", render(e.Args[i])), true})
1288 }
1289 }
1290 }
1291 return dedupe(out), argProbs
1292 }
1293 if c.noCalls {
1294 return []string{wild}, nil
1295 }
1296 if sel, ok := e.Fun.(*ast.SelectorExpr); ok && len(e.Args) == 2 {
1297 if x, ok := sel.X.(*ast.Ident); ok && x.Name == "strings" && sel.Sel.Name == "TrimPrefix" {
1298 return c.evalTrimPrefix(e, depth)
1299 }
1300 }
1301 decls, prob := c.resolveCallees(e)
1302 if prob != nil {
1303 return nil, []problem{*prob}
1304 }
1305 if len(decls) == 1 && c.fwdFn[renderName(e.Fun)] {
1306 return []string{fwdMark}, nil
1307 }
1308 var alts []string
1309 var probs []problem
1310 for _, d := range decls {
1311 a, p := c.callDecl(d, e, pos, depth)
1312 alts, probs = append(alts, a...), append(probs, p...)
1313 }
1314 if len(alts) > maxAlts {
1315 return nil, limit(c, e, "too many alternative sentences")
1316 }
1317 return dedupe(alts), probs
1318}
1319
1320func renderName(e ast.Expr) string {
1321 if id, ok := e.(*ast.Ident); ok {
1322 return id.Name
1323 }
1324 return ""
1325}
1326
1327// resolveCallees finds the package function or methods a call reaches, by
1328// name for a function and by the receiver's declared type for a method.
1329func (c *evalCtx) resolveCallees(e *ast.CallExpr) ([]*ast.FuncDecl, *problem) {
1330 bad := func(format string, a ...any) ([]*ast.FuncDecl, *problem) {
1331 return nil, &problem{fmt.Sprintf(format, a...), false}
1332 }
1333 switch fn := e.Fun.(type) {
1334 case *ast.Ident:
1335 _, isLocal := c.locals[fn.Name]
1336 isParam := false
1337 if c.fd != nil {
1338 for _, p := range paramNames(c.fd.Type) {
1339 if p == fn.Name {
1340 isParam = true
1341 }
1342 }
1343 }
1344 d, isPkg := c.pkg.funcs[fn.Name]
1345 switch {
1346 case (isLocal || isParam) && isPkg:
1347 return bad("%s at %s names both a local function value and a package function", fn.Name, c.posOf(fn))
1348 case c.fwdFn[fn.Name]:
1349 return []*ast.FuncDecl{{Name: fn}}, nil
1350 case isLocal || isParam:
1351 return bad("%s(...) calls a function value the gate cannot read", fn.Name)
1352 case isPkg:
1353 return []*ast.FuncDecl{d}, nil
1354 }
1355 return bad("%s(...) is not a function of this package", fn.Name)
1356 case *ast.SelectorExpr:
1357 if fn.Sel.Name == "Error" && len(e.Args) == 0 {
1358 // Hard: an error's text is never a placeholder either.
1359 return nil, &problem{fmt.Sprintf("%s forwards an error's text", render(e)), true}
1360 }
1361 if d, ok, prob := c.importedCallee(fn); ok {
1362 if prob != nil {
1363 return nil, prob
1364 }
1365 return []*ast.FuncDecl{d}, nil
1366 }
1367 if x, ok := fn.X.(*ast.Ident); ok && c.fd != nil && declCount(c.fd, x.Name) > 1 {
1368 // The gate types a receiver by its first declaration. A receiver
1369 // declared again (if m := supplied; ...) may be another type.
1370 return nil, &problem{fmt.Sprintf("%s at %s: the receiver %s is declared more than once in %s, so the gate cannot tell which one this is", render(e), c.posOf(e), x.Name, c.fd.Name.Name), true}
1371 }
1372 tx := c.typeExprOf(fn.X, 0)
1373 t := baseTypeName(tx)
1374 if t == "" {
1375 return bad("%s calls out of the package or on a value the gate cannot type", render(e))
1376 }
1377 if _, iface := c.pkg.types[t].(*ast.InterfaceType); iface {
1378 return bad("%s at %s is a call through the interface %s, whose implementation the gate cannot see", render(e), c.posOf(e), t)
1379 }
1380 var out []*ast.FuncDecl
1381 for _, d := range c.pkg.methods[fn.Sel.Name] {
1382 if recvBase(d) == t {
1383 out = append(out, d)
1384 }
1385 }
1386 if len(out) == 0 {
1387 return bad("%s: no method %s on %s in this package", render(e), fn.Sel.Name, t)
1388 }
1389 return out, nil
1390 }
1391 return bad("%s is not a call the gate can read", render(e))
1392}
1393
1394// movesModule is the module path a cross-package call must stay inside.
1395const movesModule = "github.com/carlosframework/platform/"
1396
1397// movesImportCache holds each scanned file's imports, name to path.
1398var movesImportCache = map[string]map[string]string{}
1399
1400// fileImports reads file's imports: each one's name (its alias, or its
1401// path's last element) to its path.
1402func (c *evalCtx) fileImports() map[string]string {
1403 path := c.u.at(c.file)
1404 if m, ok := movesImportCache[path]; ok {
1405 return m
1406 }
1407 m := map[string]string{}
1408 movesImportCache[path] = m
1409 f, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
1410 if err != nil {
1411 c.u.t.Fatal(err)
1412 }
1413 for _, im := range f.Imports {
1414 p := strings.Trim(im.Path.Value, `"`)
1415 name := p[strings.LastIndex(p, "/")+1:]
1416 if im.Name != nil {
1417 name = im.Name.Name
1418 }
1419 m[name] = p
1420 }
1421 return m
1422}
1423
1424// importedCallee resolves pkg.Func, a call into another package of this
1425// module (Task 3 fix round 1: the shared templates of
1426// internal/moves/operatorcopy.go). ok is false when fn is not such a call:
1427// its X names something this function or package declares, or no import.
1428// The callee's file must itself be scanned, so its sentences answer to the
1429// record like any other; a callee anywhere else is refused, as a call out of
1430// the package always was; so is one the record does not register as
1431// returning sentences (movesReturnedSentences), so a call into the module's
1432// other code stays out of the package. The callee is read in its own package's context
1433// (callDecl's newCtx), with this call's arguments bound.
1434func (c *evalCtx) importedCallee(fn *ast.SelectorExpr) (d *ast.FuncDecl, ok bool, prob *problem) {
1435 x, isIdent := fn.X.(*ast.Ident)
1436 if !isIdent {
1437 return nil, false, nil
1438 }
1439 if _, local := c.locals[x.Name]; local || (c.fd != nil && declCount(c.fd, x.Name) > 0) {
1440 return nil, false, nil
1441 }
1442 if _, v := c.pkg.vars[x.Name]; v {
1443 return nil, false, nil
1444 }
1445 path, imported := c.fileImports()[x.Name]
1446 if !imported || !strings.HasPrefix(path, movesModule) {
1447 return nil, false, nil
1448 }
1449 dir := strings.TrimPrefix(path, movesModule)
1450 registered := false
1451 for f, fns := range movesReturnedSentences {
1452 if filepath.Dir(f) == dir && fns[fn.Sel.Name] {
1453 registered = true
1454 }
1455 }
1456 if !registered {
1457 return nil, false, nil
1458 }
1459 if _, err := os.Stat(c.u.at(dir)); err != nil {
1460 return nil, false, nil
1461 }
1462 pkg := loadMovesPkg(c.u.t, c.u.at(dir))
1463 d, found := pkg.funcs[fn.Sel.Name]
1464 if !found {
1465 return nil, false, nil
1466 }
1467 dfile := strings.TrimPrefix(movesFset.Position(d.Pos()).Filename, c.u.root+"/")
1468 if !slices.Contains(c.u.files, dfile) || !movesReturnedSentences[dfile][fn.Sel.Name] {
1469 return nil, false, nil
1470 }
1471 return d, true, nil
1472}
1473
1474// declCount counts the declarations of name in fd: its receiver, parameters
1475// and named results, and in its body every := , var, range variable and
1476// closure parameter or result. A := that redeclares in the same scope counts
1477// too; the gate does not scope.
1478func declCount(fd *ast.FuncDecl, name string) int {
1479 n := 0
1480 fields := func(fl *ast.FieldList) {
1481 if fl == nil {
1482 return
1483 }
1484 for _, f := range fl.List {
1485 for _, id := range f.Names {
1486 if id.Name == name {
1487 n++
1488 }
1489 }
1490 }
1491 }
1492 fields(fd.Recv)
1493 fields(fd.Type.Params)
1494 fields(fd.Type.Results)
1495 if fd.Body == nil {
1496 return n
1497 }
1498 ident := func(e ast.Expr) {
1499 if id, ok := e.(*ast.Ident); ok && id.Name == name {
1500 n++
1501 }
1502 }
1503 ast.Inspect(fd.Body, func(x ast.Node) bool {
1504 switch x := x.(type) {
1505 case *ast.AssignStmt:
1506 if x.Tok == token.DEFINE {
1507 for _, l := range x.Lhs {
1508 ident(l)
1509 }
1510 }
1511 case *ast.ValueSpec:
1512 for _, id := range x.Names {
1513 ident(id)
1514 }
1515 case *ast.RangeStmt:
1516 if x.Tok == token.DEFINE {
1517 ident(x.Key)
1518 ident(x.Value)
1519 }
1520 case *ast.FuncLit:
1521 fields(x.Type.Params)
1522 fields(x.Type.Results)
1523 }
1524 return true
1525 })
1526 return n
1527}
1528
1529// evalTrimPrefix reads strings.TrimPrefix(x, "lit"): the sentences x can be,
1530// with the prefix taken off those that have it.
1531func (c *evalCtx) evalTrimPrefix(e *ast.CallExpr, depth int) ([]string, []problem) {
1532 xs, xp := c.eval(e.Args[0], depth+1)
1533 ps, pp := c.eval(e.Args[1], depth+1)
1534 if len(xp)+len(pp) > 0 {
1535 return nil, append(xp, pp...)
1536 }
1537 var out []string
1538 for _, x := range xs {
1539 for _, p := range ps {
1540 if strings.Contains(p, wild) || strings.Contains(p, fwdMark) {
1541 return nil, []problem{{fmt.Sprintf("%s trims a prefix that is not a literal", render(e)), false}}
1542 }
1543 out = append(out, strings.TrimPrefix(x, p))
1544 }
1545 }
1546 return dedupe(out), nil
1547}
1548
1549func unstar(e ast.Expr) ast.Expr {
1550 for {
1551 switch x := e.(type) {
1552 case *ast.StarExpr:
1553 e = x.X
1554 case *ast.ParenExpr:
1555 e = x.X
1556 default:
1557 return e
1558 }
1559 }
1560}
1561
1562// resultTypeAt is the declared type of result pos of d.
1563func resultTypeAt(d *ast.FuncDecl, pos int) ast.Expr {
1564 if d.Type.Results == nil {
1565 return nil
1566 }
1567 i := 0
1568 for _, f := range d.Type.Results.List {
1569 n := len(f.Names)
1570 if n == 0 {
1571 n = 1
1572 }
1573 if pos < i+n {
1574 return f.Type
1575 }
1576 i += n
1577 }
1578 return nil
1579}
1580
1581// typeExprOf finds the declared type of an expression, as far as the source
1582// says: a receiver, parameter or typed variable, a composite literal, a field
1583// of a known struct, an element of a known map or slice, a call's result.
1584func (c *evalCtx) typeExprOf(e ast.Expr, depth int) ast.Expr {
1585 if depth > 6 {
1586 return nil
1587 }
1588 switch e := e.(type) {
1589 case *ast.ParenExpr:
1590 return c.typeExprOf(e.X, depth+1)
1591 case *ast.UnaryExpr:
1592 if e.Op == token.AND {
1593 return c.typeExprOf(e.X, depth+1)
1594 }
1595 case *ast.CompositeLit:
1596 return e.Type
1597 case *ast.Ident:
1598 if c.fd != nil {
1599 if c.fd.Recv != nil {
1600 for _, f := range c.fd.Recv.List {
1601 for _, n := range f.Names {
1602 if n.Name == e.Name {
1603 return f.Type
1604 }
1605 }
1606 }
1607 }
1608 if c.fd.Type.Params != nil {
1609 for _, f := range c.fd.Type.Params.List {
1610 for _, n := range f.Names {
1611 if n.Name == e.Name {
1612 return f.Type
1613 }
1614 }
1615 }
1616 }
1617 }
1618 for _, a := range c.locals[e.Name] {
1619 switch {
1620 case a.typ != nil:
1621 return a.typ
1622 case a.call != nil:
1623 if decls, p := c.resolveCallees(a.call); p == nil && len(decls) > 0 && decls[0].Type != nil {
1624 if tx := resultTypeAt(decls[0], a.pos); tx != nil {
1625 return tx
1626 }
1627 }
1628 case a.rhs != nil:
1629 if tx := c.typeExprOf(a.rhs, depth+1); tx != nil {
1630 return tx
1631 }
1632 case a.typeFrom != nil:
1633 if tx := c.typeExprOf(a.typeFrom, depth+1); tx != nil {
1634 return tx
1635 }
1636 }
1637 }
1638 case *ast.SelectorExpr:
1639 base := lastName(baseTypeName(c.typeExprOf(e.X, depth+1)))
1640 if ft, ok := c.u.fieldTypeOf[base][e.Sel.Name]; ok {
1641 return ft
1642 }
1643 case *ast.IndexExpr:
1644 switch x := unstar(c.typeExprOf(e.X, depth+1)).(type) {
1645 case *ast.MapType:
1646 return x.Value
1647 case *ast.ArrayType:
1648 return x.Elt
1649 }
1650 case *ast.CallExpr:
1651 if decls, p := c.resolveCallees(e); p == nil && len(decls) > 0 && decls[0].Type != nil {
1652 return resultTypeAt(decls[0], 0)
1653 }
1654 }
1655 return nil
1656}
1657
1658// callDecl evaluates result pos of d called as call: its parameters bound to
1659// the arguments (read as placeholders), then every return.
1660func (c *evalCtx) callDecl(d *ast.FuncDecl, call *ast.CallExpr, pos, depth int) ([]string, []problem) {
1661 if d.Body == nil || !stringResults(d)[pos] {
1662 return nil, []problem{{fmt.Sprintf("%s(...) does not return a sentence at result %d", d.Name.Name, pos), false}}
1663 }
1664 key := fmt.Sprintf("helper %p %d", d, pos)
1665 if c.visiting[key] {
1666 return nil, []problem{{fmt.Sprintf("%s calls itself at %s", d.Name.Name, c.posOf(call)), true}}
1667 }
1668 c.visiting[key] = true
1669 defer delete(c.visiting, key)
1670 c.u.used[d.Name.Name] = true
1671
1672 dfile := strings.TrimPrefix(movesFset.Position(d.Pos()).Filename, c.u.root+"/")
1673 hc := c.u.newCtx(dfile, d, c.noCalls)
1674 hc.visiting = c.visiting
1675 params := paramNames(d.Type)
1676 var probs []problem
1677 for i, p := range params {
1678 if i < len(call.Args) {
1679 if onlyFormatArg(d, p) {
1680 // A parameter the helper only passes to a format call as an
1681 // argument is a placeholder there, read as a direct format
1682 // call reads its arguments: for an error's text (below), not
1683 // as a sentence.
1684 hc.bind[p] = []string{wild}
1685 } else {
1686 a, pr := c.operand(call.Args[i], depth+1)
1687 hc.bind[p] = a
1688 probs = append(probs, pr...)
1689 }
1690 // An argument that is an error's text may still only select among
1691 // sentences; it is refused where the helper puts it in one.
1692 if why := dataWhy(c.errorText(call.Args[i], 0)); why != "" {
1693 hc.errParams[p] = rewrap(why, "it is bound to %s at %s, and %s", render(call.Args[i]), c.posOf(call.Args[i]))
1694 }
1695 } else {
1696 hc.bind[p] = []string{wild}
1697 }
1698 }
1699 nres := resultCount(d)
1700 named := namedResults(d)
1701 var alts []string
1702 ast.Inspect(d.Body, func(n ast.Node) bool {
1703 switch n := n.(type) {
1704 case *ast.FuncLit:
1705 return false
1706 case *ast.ReturnStmt:
1707 hc.curReturn = n
1708 var a []string
1709 var p []problem
1710 switch {
1711 case len(n.Results) == nres && pos < len(n.Results):
1712 a, p = hc.eval(n.Results[pos], depth+1)
1713 case len(n.Results) == 1 && nres > 1:
1714 if cl, ok := n.Results[0].(*ast.CallExpr); ok {
1715 a, p = hc.evalCall(cl, pos, depth+1)
1716 }
1717 case len(n.Results) == 0 && pos < len(named):
1718 a, p = hc.evalIdent(&ast.Ident{Name: named[pos], NamePos: n.Pos()}, depth+1)
1719 default:
1720 p = []problem{{fmt.Sprintf("return at %s has a shape the gate cannot read", hc.posOf(n)), false}}
1721 }
1722 hc.curReturn = nil
1723 for _, x := range p {
1724 probs = append(probs, problem{fmt.Sprintf("helper %s, at %s: %s", d.Name.Name, hc.posOf(n), x.msg), x.hard})
1725 }
1726 alts = append(alts, a...)
1727 }
1728 return true
1729 })
1730 return alts, probs
1731}
1732
1733// onlyFormatArg is true when every use of the parameter p in d's body is an
1734// argument, not the format, of a format call (operatorcopy.go's templates).
1735func onlyFormatArg(d *ast.FuncDecl, p string) bool {
1736 uses, asArg := 0, 0
1737 ast.Inspect(d.Body, func(n ast.Node) bool {
1738 switch n := n.(type) {
1739 case *ast.Ident:
1740 if n.Name == p {
1741 uses++
1742 }
1743 case *ast.CallExpr:
1744 if isFormatCall(n) {
1745 for _, a := range n.Args[1:] {
1746 if id, ok := a.(*ast.Ident); ok && id.Name == p {
1747 asArg++
1748 }
1749 }
1750 }
1751 }
1752 return true
1753 })
1754 return uses > 0 && uses == asArg && declCount(d, p) == 1
1755}
1756
1757// ---- guarded echo (a mapping helper) ----
1758
1759func (c *evalCtx) guardedEcho(id *ast.Ident) bool {
1760 if c.curReturn == nil || c.fd == nil || !movesMappingHelpers[c.file][c.fd.Name.Name] {
1761 return false
1762 }
1763 direct := false
1764 for _, r := range c.curReturn.Results {
1765 if r == ast.Expr(id) {
1766 direct = true
1767 }
1768 }
1769 if !direct {
1770 return false
1771 }
1772 var cc *ast.CaseClause
1773 var stack []ast.Node
1774 ast.Inspect(c.fd.Body, func(n ast.Node) bool {
1775 if n == nil {
1776 stack = stack[:len(stack)-1]
1777 return true
1778 }
1779 stack = append(stack, n)
1780 if n == ast.Node(c.curReturn) {
1781 for i := len(stack) - 1; i >= 0; i-- {
1782 if k, ok := stack[i].(*ast.CaseClause); ok {
1783 cc = k
1784 break
1785 }
1786 }
1787 }
1788 return cc == nil
1789 })
1790 if cc == nil || len(cc.List) == 0 {
1791 return false
1792 }
1793 for _, g := range cc.List {
1794 if !guardsLiteral(g, id.Name, c) {
1795 return false
1796 }
1797 }
1798 return true
1799}
1800
1801func (c *evalCtx) isLiteralText(e ast.Expr) bool {
1802 switch e := e.(type) {
1803 case *ast.ParenExpr:
1804 return c.isLiteralText(e.X)
1805 case *ast.BasicLit:
1806 return e.Kind == token.STRING
1807 case *ast.Ident:
1808 if _, local := c.locals[e.Name]; local {
1809 return false
1810 }
1811 _, ok := c.pkg.vars[e.Name]
1812 return ok
1813 }
1814 return false
1815}
1816
1817func guardsLiteral(g ast.Expr, name string, c *evalCtx) bool {
1818 switch g := g.(type) {
1819 case *ast.ParenExpr:
1820 return guardsLiteral(g.X, name, c)
1821 case *ast.BinaryExpr:
1822 switch g.Op {
1823 case token.LAND:
1824 return guardsLiteral(g.X, name, c) && guardsLiteral(g.Y, name, c)
1825 case token.EQL:
1826 l, lok := g.X.(*ast.Ident)
1827 r, rok := g.Y.(*ast.Ident)
1828 switch {
1829 case lok && l.Name == name:
1830 return c.isLiteralText(g.Y) && !(rok && r.Name == name)
1831 case rok && r.Name == name:
1832 return c.isLiteralText(g.X)
1833 }
1834 }
1835 case *ast.CallExpr:
1836 sel, ok := g.Fun.(*ast.SelectorExpr)
1837 if !ok || len(g.Args) != 2 {
1838 return false
1839 }
1840 x, isID := sel.X.(*ast.Ident)
1841 a0, a0ok := g.Args[0].(*ast.Ident)
1842 if !isID || x.Name != "strings" || !a0ok || a0.Name != name {
1843 return false
1844 }
1845 switch sel.Sel.Name {
1846 case "HasPrefix", "HasSuffix", "Contains":
1847 return c.isLiteralText(g.Args[1])
1848 }
1849 }
1850 return false
1851}
1852
1853func render(e ast.Expr) string {
1854 var b strings.Builder
1855 ast.Inspect(e, func(n ast.Node) bool {
1856 switch n := n.(type) {
1857 case *ast.BinaryExpr:
1858 b.WriteString(render(n.X) + " " + n.Op.String() + " " + render(n.Y))
1859 return false
1860 case *ast.Ident:
1861 b.WriteString(n.Name)
1862 case *ast.SelectorExpr:
1863 b.WriteString(render(n.X) + "." + n.Sel.Name)
1864 return false
1865 case *ast.CallExpr:
1866 b.WriteString(render(n.Fun) + "(...)")
1867 return false
1868 }
1869 return true
1870 })
1871 return b.String()
1872}
1873
1874// ---- emissions and violations ----
1875
1876type movesEmission struct {
1877 file string
1878 line int // where the call or literal starts
1879 endLine int
1880 fn string // the enclosing function
1881 kind string
1882 pattern string
1883 fragment bool // a piece of a sentence, matched by containment
1884 underived bool // the gate could not derive a sentence: it matches no row
1885 fromParam bool // the value is made from a string or error parameter of fn
1886}
1887
1888func (e movesEmission) String() string {
1889 if e.underived {
1890 return fmt.Sprintf("%s:%d (%s) a value the gate could not derive to a sentence", e.file, e.line, e.kind)
1891 }
1892 return fmt.Sprintf("%s:%d (%s) %q", e.file, e.line, e.kind, strings.ReplaceAll(e.pattern, wild, "%"))
1893}
1894
1895// informative reports whether a pattern says anything: at least three letters
1896// outside the parts the code fills in (a bare "a " or "S2: " is a piece of
1897// a sentence, not one).
1898func informative(p string) bool {
1899 n := 0
1900 for _, r := range strings.ReplaceAll(strings.ReplaceAll(p, wild, ""), fwdMark, "") {
1901 if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' {
1902 if n++; n == 3 {
1903 return true
1904 }
1905 }
1906 }
1907 return false
1908}
1909
1910// movesViolation is a sink that does not take a sentence the gate can derive.
1911type movesViolation struct {
1912 file string
1913 line int
1914 fn string
1915 kind string
1916 why string
1917}
1918
1919func (v movesViolation) String() string {
1920 return fmt.Sprintf("%s:%d (%s in %s): %s", v.file, v.line, v.kind, v.fn, v.why)
1921}
1922
1923// scanMovesFile returns the emissions of one file.
1924func scanMovesFile(t *testing.T, file string) []movesEmission {
1925 t.Helper()
1926 return scanMovesFileWith(t, ".", file, false)
1927}
1928
1929// scanMovesFileWith is scanMovesFile; noCalls reads calls to package
1930// functions and field reads as wildcards, so a legacy function's sentence is
1931// only the text written in that function.
1932func scanMovesFileWith(t *testing.T, root, file string, noCalls bool) []movesEmission {
1933 t.Helper()
1934 ems, _ := scanMovesFileFull(t, root, file, noCalls)
1935 return ems
1936}
1937
1938func mentions(e ast.Expr, locals map[string][]localAssign, params map[string]bool, out map[string]bool, depth int) {
1939 ast.Inspect(e, func(n ast.Node) bool {
1940 if id, ok := n.(*ast.Ident); ok {
1941 if params[id.Name] {
1942 out[id.Name] = true
1943 }
1944 if depth < 3 {
1945 for _, a := range locals[id.Name] {
1946 if a.rhs != nil {
1947 mentions(a.rhs, locals, params, out, depth+1)
1948 }
1949 if a.call != nil {
1950 mentions(a.call, locals, params, out, depth+1)
1951 }
1952 }
1953 }
1954 }
1955 return true
1956 })
1957}
1958
1959// scanMovesFileFull returns a file's emissions and its sink violations.
1960func scanMovesFileFull(t *testing.T, root, file string, noCalls bool) ([]movesEmission, []movesViolation) {
1961 t.Helper()
1962 return scanMovesFileIn(t, universeFor(t, root, file), root, file, noCalls)
1963}
1964
1965// scanMovesFileIn scans a file in the given universe of struct fields.
1966func scanMovesFileIn(t *testing.T, u *movesUniverse, root, file string, noCalls bool) ([]movesEmission, []movesViolation) {
1967 t.Helper()
1968 at := func(p string) string {
1969 if root == "." {
1970 return p
1971 }
1972 return filepath.Join(root, p)
1973 }
1974 fset := movesFset
1975 f, err := parser.ParseFile(fset, at(file), nil, 0)
1976 if err != nil {
1977 t.Fatal(err)
1978 }
1979 var out []movesEmission
1980 var vios []movesViolation
1981 seenVio := map[string]bool{}
1982 var fn string
1983 var ctx *evalCtx
1984 violate := func(line int, kind, why string) {
1985 v := movesViolation{file, line, fn, kind, why}
1986 if !seenVio[v.String()] {
1987 seenVio[v.String()] = true
1988 vios = append(vios, v)
1989 }
1990 }
1991 emit := func(pos token.Pos, kind string, e ast.Expr, fragment bool) {
1992 line := fset.Position(pos).Line
1993 end := fset.Position(e.End()).Line
1994 alts, probs := ctx.eval(e, 0)
1995 // A sentence field read whole at a sink speaks what was written to it:
1996 // each write's sentence needs its row here (fix round 2 of Plan 6
1997 // Task 1a: a StuckReason written with a literal was otherwise passed
1998 // as checked where it is made, which only checked it was a sentence).
1999 if sel, ok := e.(*ast.SelectorExpr); ok && movesSentenceFields[sel.Sel.Name] && len(probs) == 0 {
2000 alts = ctx.fieldWriteAlts(sel)
2001 }
2002 fromParam := false
2003 if ctx.fd != nil {
2004 params := map[string]bool{}
2005 if ctx.fd.Type.Params != nil {
2006 for _, f := range ctx.fd.Type.Params.List {
2007 if id, ok := f.Type.(*ast.Ident); ok && (id.Name == "string" || id.Name == "error") {
2008 for _, n := range f.Names {
2009 params[n.Name] = true
2010 }
2011 }
2012 }
2013 }
2014 hit := map[string]bool{}
2015 mentions(e, ctx.locals, params, hit, 0)
2016 if len(hit) > 0 {
2017 u.paramSinks[ctx.fd.Name.Name] = true
2018 fromParam = true
2019 }
2020 }
2021 underived := len(probs) > 0
2022 for _, p := range probs {
2023 violate(line, kind, p.msg)
2024 }
2025 for _, a := range dedupe(alts) {
2026 switch {
2027 case a == "":
2028 case strings.Contains(a, fwdMark) && !informative(a):
2029 // forwarded text with no sentence of its own around it: checked where it is made
2030 case informative(a):
2031 out = append(out, movesEmission{file, line, end, fn, kind, strings.ReplaceAll(a, fwdMark, wild), fragment, false, fromParam})
2032 default:
2033 underived = true
2034 if !fragment {
2035 violate(line, kind, fmt.Sprintf("a value with no text of its own reaches this sink (it derives only to %q)", strings.ReplaceAll(a, wild, "%")))
2036 }
2037 }
2038 }
2039 if underived && !fragment {
2040 out = append(out, movesEmission{file, line, end, fn, kind, "", false, true, fromParam})
2041 }
2042 }
2043 isCheck := func(x ast.Expr) bool {
2044 switch x := x.(type) {
2045 case *ast.Ident:
2046 return x.Name == "Check"
2047 case *ast.SelectorExpr:
2048 return x.Sel.Name == "Check"
2049 }
2050 return false
2051 }
2052 tables := map[string]bool{}
2053 for _, n := range movesCopyTables[file] {
2054 tables[n] = true
2055 }
2056 literals := map[string]bool{}
2057 for _, n := range movesLiteralFuncs[file] {
2058 literals[n] = true
2059 }
2060 for _, d := range f.Decls {
2061 fd, ok := d.(*ast.FuncDecl)
2062 if !ok || fd.Body == nil {
2063 continue
2064 }
2065 fn = fd.Name.Name
2066 ctx = u.newCtx(file, fd, noCalls)
2067 inTable := tables[fn]
2068 strs := stringResults(fd)
2069 named := namedResults(fd)
2070 returnsSentence := movesReturnedSentences[file][fn] && len(strs) > 0
2071 var stack []ast.Node
2072 litDepth := 0
2073 ast.Inspect(fd.Body, func(n ast.Node) bool {
2074 if n == nil {
2075 if _, ok := stack[len(stack)-1].(*ast.FuncLit); ok {
2076 litDepth--
2077 }
2078 stack = stack[:len(stack)-1]
2079 return true
2080 }
2081 stack = append(stack, n)
2082 switch n := n.(type) {
2083 case *ast.FuncLit:
2084 litDepth++
2085 case *ast.BasicLit:
2086 // A literal with an em dash is main's wording, which phase1Conflict
2087 // matches and rewrites; it is never spoken. A literal here is a
2088 // piece of a sentence unless it is compared whole or returned.
2089 if literals[fn] && n.Kind == token.STRING && !strings.Contains(n.Value, `\u2014`) && !strings.ContainsRune(n.Value, '\u2014') {
2090 emit(n.Pos(), fn, n, true)
2091 }
2092 case *ast.CallExpr:
2093 name := ""
2094 switch f := n.Fun.(type) {
2095 case *ast.Ident:
2096 name = f.Name
2097 case *ast.SelectorExpr:
2098 name = f.Sel.Name // a method: only local sinks, below
2099 }
2100 i, sink := movesLocalSinks[file][name]
2101 if id, ok := n.Fun.(*ast.Ident); ok && id.Name == "jsonError" {
2102 i, sink = 2, true
2103 }
2104 if sink && i < len(n.Args) {
2105 emit(n.Pos(), name, n.Args[i], false)
2106 }
2107 if j, fsink := movesFuncSinks[file][name]; fsink && j < len(n.Args) {
2108 if why := ctx.checkFuncArg(n.Args[j]); why != "" {
2109 violate(fset.Position(n.Pos()).Line, name, why)
2110 }
2111 }
2112 case *ast.CompositeLit:
2113 if n.Type != nil && isCheck(n.Type) {
2114 for _, el := range n.Elts {
2115 if kv, ok := el.(*ast.KeyValueExpr); ok {
2116 if k, ok := kv.Key.(*ast.Ident); ok && k.Name == "Reason" {
2117 emit(kv.Pos(), "Check.Reason", kv.Value, false)
2118 }
2119 }
2120 }
2121 }
2122 if inTable {
2123 for _, el := range n.Elts {
2124 if kv, ok := el.(*ast.KeyValueExpr); ok {
2125 switch kv.Value.(type) {
2126 case *ast.FuncLit, *ast.CompositeLit:
2127 // closures are read through their returns; nested tables by descent
2128 default:
2129 emit(kv.Pos(), fn, kv.Value, false)
2130 }
2131 }
2132 }
2133 }
2134 case *ast.AssignStmt:
2135 if len(n.Lhs) == 1 && len(n.Rhs) == 1 {
2136 if s, ok := n.Lhs[0].(*ast.SelectorExpr); ok && s.Sel.Name == "Reason" && isCheck(s.X) {
2137 emit(n.Pos(), "Check.Reason", n.Rhs[0], false)
2138 }
2139 }
2140 case *ast.ReturnStmt:
2141 ctx.curReturn = n
2142 switch {
2143 case inTable:
2144 for _, r := range n.Results {
2145 if _, nested := r.(*ast.CompositeLit); !nested {
2146 emit(r.Pos(), fn, r, false)
2147 }
2148 }
2149 case literals[fn]:
2150 // phase1Conflict's returns are sentences too.
2151 for _, r := range n.Results {
2152 emit(r.Pos(), fn, r, false)
2153 }
2154 case returnsSentence && litDepth == 0:
2155 if len(n.Results) == 0 {
2156 for i, nm := range named {
2157 if strs[i] {
2158 emit(n.Pos(), "returned sentence", &ast.Ident{Name: nm, NamePos: n.Pos()}, false)
2159 }
2160 }
2161 }
2162 for i, r := range n.Results {
2163 if strs[i] {
2164 emit(r.Pos(), "returned sentence", r, false)
2165 }
2166 }
2167 }
2168 ctx.curReturn = nil
2169 }
2170 return true
2171 })
2172 }
2173 return out, vios
2174}
2175
2176// movesServerTextSpec names a file's server text: the type that carries text
2177// the console wrote, its one field, the one function that makes it (Maker,
2178// which must read the body of a response the HTTP client returned), and the
2179// one sink that says it (Sink, which takes the type). Nothing else in the
2180// package may make the type, write its field or read it.
2181type movesServerTextSpec struct{ Type, Field, Maker, Sink string }
2182
2183type serverTextProblem struct{ fn, msg string }
2184
2185func (p serverTextProblem) String() string { return p.fn + ": " + p.msg }
2186
2187// checkServerText holds a server text type to its spec, across every non-test
2188// Go file of file's package directory.
2189func checkServerText(t *testing.T, file string, spec movesServerTextSpec) []serverTextProblem {
2190 t.Helper()
2191 dir := filepath.Dir(file)
2192 names, err := filepath.Glob(filepath.Join(dir, "*.go"))
2193 if err != nil {
2194 t.Fatal(err)
2195 }
2196 var out []serverTextProblem
2197 maker, sink := false, false
2198 var files []*ast.File
2199 consts := map[string]bool{} // the package's named constants
2200 for _, name := range names {
2201 if strings.HasSuffix(name, "_test.go") {
2202 continue
2203 }
2204 f, err := parser.ParseFile(token.NewFileSet(), name, nil, 0)
2205 if err != nil {
2206 t.Fatal(err)
2207 }
2208 files = append(files, f)
2209 for _, d := range f.Decls {
2210 if g, ok := d.(*ast.GenDecl); ok && g.Tok == token.CONST {
2211 for _, sp := range g.Specs {
2212 for _, n := range sp.(*ast.ValueSpec).Names {
2213 consts[n.Name] = true
2214 }
2215 }
2216 }
2217 }
2218 }
2219 mentions := func(n ast.Node) bool {
2220 found := false
2221 ast.Inspect(n, func(x ast.Node) bool {
2222 if id, ok := x.(*ast.Ident); ok && id.Name == spec.Type {
2223 found = true
2224 }
2225 return !found
2226 })
2227 return found
2228 }
2229 for _, f := range files {
2230 for _, d := range f.Decls {
2231 // Server text is never a package-level value: a var or const whose
2232 // type is or holds the type, or a composite literal of it outside
2233 // a function body (fix round 3, hole 1).
2234 if g, ok := d.(*ast.GenDecl); ok && (g.Tok == token.VAR || g.Tok == token.CONST) {
2235 for _, sp := range g.Specs {
2236 if vs := sp.(*ast.ValueSpec); mentions(vs) {
2237 for _, n := range vs.Names {
2238 out = append(out, serverTextProblem{"<package>", fmt.Sprintf("declares %s, a package-level %s", n.Name, spec.Type)})
2239 }
2240 }
2241 }
2242 continue
2243 }
2244 fd, ok := d.(*ast.FuncDecl)
2245 if !ok || fd.Body == nil {
2246 continue
2247 }
2248 fn := fd.Name.Name
2249 if fd.Recv != nil {
2250 fn = recvBase(fd) + "." + fn
2251 }
2252 writes := map[ast.Node]bool{}
2253 readsBody, callsDo := false, false
2254 localConsts := map[string]bool{}
2255 ast.Inspect(fd.Body, func(n ast.Node) bool {
2256 if g, ok := n.(*ast.GenDecl); ok && g.Tok == token.CONST {
2257 for _, sp := range g.Specs {
2258 for _, nm := range sp.(*ast.ValueSpec).Names {
2259 localConsts[nm.Name] = true
2260 }
2261 }
2262 }
2263 return true
2264 })
2265 ast.Inspect(fd.Body, func(n ast.Node) bool {
2266 switch n := n.(type) {
2267 case *ast.AssignStmt:
2268 for _, l := range n.Lhs {
2269 if s, ok := l.(*ast.SelectorExpr); ok && s.Sel.Name == spec.Field {
2270 writes[s] = true
2271 if fn != spec.Maker {
2272 out = append(out, serverTextProblem{fd.Name.Name, fmt.Sprintf("writes %s.%s outside %s", spec.Type, spec.Field, spec.Maker)})
2273 }
2274 }
2275 }
2276 case *ast.CompositeLit:
2277 id, ok := n.Type.(*ast.Ident)
2278 if !ok || id.Name != spec.Type {
2279 break
2280 }
2281 if fn != spec.Maker {
2282 out = append(out, serverTextProblem{fd.Name.Name, fmt.Sprintf("makes a %s outside %s", spec.Type, spec.Maker)})
2283 break
2284 }
2285 // In the maker the text is a value read from the decoded
2286 // response: never a string literal or a named constant
2287 // (fix round 3, hole 2).
2288 for _, el := range n.Elts {
2289 v := el
2290 if kv, ok := el.(*ast.KeyValueExpr); ok {
2291 v = kv.Value
2292 }
2293 for p, ok := v.(*ast.ParenExpr); ok; p, ok = v.(*ast.ParenExpr) {
2294 v = p.X
2295 }
2296 what := ""
2297 switch v := v.(type) {
2298 case *ast.BasicLit:
2299 what = v.Value
2300 case *ast.Ident:
2301 if consts[v.Name] || localConsts[v.Name] {
2302 what = v.Name
2303 }
2304 }
2305 if what != "" {
2306 out = append(out, serverTextProblem{fd.Name.Name, fmt.Sprintf("makes a %s from %s, not a value read from the response", spec.Type, what)})
2307 }
2308 }
2309 case *ast.SelectorExpr:
2310 switch {
2311 case n.Sel.Name == "Body":
2312 readsBody = true
2313 case n.Sel.Name == "Do":
2314 callsDo = true
2315 case n.Sel.Name == spec.Field && !writes[n] && fn != spec.Maker && fn != spec.Sink:
2316 out = append(out, serverTextProblem{fd.Name.Name, fmt.Sprintf("reads %s.%s outside %s", spec.Type, spec.Field, spec.Sink)})
2317 }
2318 }
2319 return true
2320 })
2321 if fn == spec.Maker {
2322 maker = true
2323 if !readsBody || !callsDo {
2324 out = append(out, serverTextProblem{fn, "makes server text but reads no body of a response the HTTP client returned"})
2325 }
2326 }
2327 if fn == spec.Sink {
2328 for _, fld := range fd.Type.Params.List {
2329 if id, ok := fld.Type.(*ast.Ident); ok && id.Name == spec.Type {
2330 sink = true
2331 }
2332 }
2333 }
2334 }
2335 }
2336 if !maker && !strings.Contains(dir, "testdata") {
2337 out = append(out, serverTextProblem{spec.Maker, "the maker is not declared"})
2338 }
2339 if !sink {
2340 out = append(out, serverTextProblem{spec.Sink, "the sink does not take " + spec.Type})
2341 }
2342 return out
2343}
2344
2345// checkFuncArg checks the function a call hands to a sentence-function
2346// parameter: a package function that is a sentence helper.
2347func (c *evalCtx) checkFuncArg(e ast.Expr) string {
2348 id, ok := e.(*ast.Ident)
2349 if !ok {
2350 return fmt.Sprintf("%s is passed where a sentence helper is expected", render(e))
2351 }
2352 d, isFn := c.pkg.funcs[id.Name]
2353 if !isFn {
2354 return fmt.Sprintf("%s is passed where a sentence helper is expected", render(e))
2355 }
2356 call := &ast.CallExpr{Fun: id, Args: nil}
2357 _, probs := c.callDecl(d, call, 0, 0)
2358 for _, p := range probs {
2359 return p.msg
2360 }
2361 return ""
2362}
2363
2364// ---- function values ----
2365
2366// movesFuncValueAllow lists functions that take a sentence parameter and are
2367// referenced other than as a direct call, each with the reason that is safe.
2368// The file is where the reference is.
2369var movesFuncValueAllow = map[string]map[string]string{
2370 "internal/console/apiinstancemove.go": {
2371 "phase1Conflict": "handed only to moveTransitionSaying's conflict parameter; movesFuncSinks makes the gate read every return of it at that call site, and its err parameter only selects among recorded sentences",
2372 },
2373}
2374
2375type funcRef struct {
2376 file string
2377 name string
2378 msg string
2379}
2380
2381// funcValueRefs finds references to sentence-flow functions that are not
2382// direct calls: `f := send`, a method value, a function passed as an
2383// argument. A call through such a value escapes the argument checks.
2384func funcValueRefs(t *testing.T, u *movesUniverse) []funcRef {
2385 t.Helper()
2386 flow := map[string]bool{}
2387 for n := range u.used {
2388 flow[n] = true
2389 }
2390 for n := range u.paramSinks {
2391 flow[n] = true
2392 }
2393 // Only names that are declared functions or methods of the scanned files.
2394 declared := map[string]bool{}
2395 var out []funcRef
2396 for _, f := range u.files {
2397 af, err := parser.ParseFile(movesFset, u.at(f), nil, 0)
2398 if err != nil {
2399 t.Fatal(err)
2400 }
2401 for _, d := range af.Decls {
2402 fd, ok := d.(*ast.FuncDecl)
2403 if !ok {
2404 continue
2405 }
2406 declared[fd.Name.Name] = true
2407 // The forwarders configured as sinks are sentence-flow functions too.
2408 if _, ok := movesLocalSinks[f][fd.Name.Name]; ok {
2409 flow[fd.Name.Name] = true
2410 }
2411 if _, ok := movesFuncSinks[f][fd.Name.Name]; ok {
2412 flow[fd.Name.Name] = true
2413 }
2414 // Main's own functions are not the series' concern.
2415 if _, ok := movesLegacyFiles[f]; ok {
2416 delete(flow, fd.Name.Name)
2417 } else if _, ok := movesLegacyFuncs[f][fd.Name.Name]; ok {
2418 delete(flow, fd.Name.Name)
2419 }
2420 }
2421 }
2422 for _, f := range u.files {
2423 af, err := parser.ParseFile(movesFset, u.at(f), nil, 0)
2424 if err != nil {
2425 t.Fatal(err)
2426 }
2427 // A closure configured as a local sink (verifyArchive's fail) is a
2428 // sentence-flow function of this file: an alias of it (send := fail)
2429 // escapes the checks at its call sites.
2430 closures := map[string]bool{}
2431 ast.Inspect(af, func(n ast.Node) bool {
2432 var names []*ast.Ident
2433 var values []ast.Expr
2434 switch n := n.(type) {
2435 case *ast.AssignStmt:
2436 for _, l := range n.Lhs {
2437 id, _ := l.(*ast.Ident)
2438 names = append(names, id)
2439 }
2440 values = n.Rhs
2441 case *ast.ValueSpec:
2442 names, values = n.Names, n.Values
2443 }
2444 if len(names) != len(values) {
2445 return true
2446 }
2447 for i, id := range names {
2448 if _, lit := values[i].(*ast.FuncLit); lit && id != nil {
2449 if _, ok := movesLocalSinks[f][id.Name]; ok {
2450 closures[id.Name] = true
2451 }
2452 }
2453 }
2454 return true
2455 })
2456 callFun := map[ast.Node]bool{}
2457 skip := map[ast.Node]bool{}
2458 ast.Inspect(af, func(n ast.Node) bool {
2459 switch n := n.(type) {
2460 case *ast.CallExpr:
2461 callFun[n.Fun] = true
2462 case *ast.AssignStmt:
2463 // The name being defined or assigned is not a use of its value.
2464 for _, l := range n.Lhs {
2465 if id, ok := l.(*ast.Ident); ok {
2466 skip[id] = true
2467 }
2468 }
2469 case *ast.ValueSpec:
2470 for _, id := range n.Names {
2471 skip[id] = true
2472 }
2473 case *ast.FuncDecl:
2474 skip[n.Name] = true
2475 case *ast.SelectorExpr:
2476 skip[n.Sel] = true
2477 if flow[n.Sel.Name] && declared[n.Sel.Name] && !callFun[n] {
2478 out = append(out, funcRef{f, n.Sel.Name, fmt.Sprintf("%s: %s is used as a value, not called, so a call through it escapes the argument checks", movesFset.Position(n.Pos()), render(n))})
2479 }
2480 case *ast.KeyValueExpr:
2481 if id, ok := n.Key.(*ast.Ident); ok {
2482 skip[id] = true
2483 }
2484 case *ast.Ident:
2485 if skip[n] || callFun[n] {
2486 return true
2487 }
2488 if flow[n.Name] && declared[n.Name] || closures[n.Name] {
2489 out = append(out, funcRef{f, n.Name, fmt.Sprintf("%s: %s is used as a value, not called, so a call through it escapes the argument checks", movesFset.Position(n.Pos()), n.Name)})
2490 }
2491 }
2492 return true
2493 })
2494 }
2495 sort.Slice(out, func(i, j int) bool { return out[i].msg < out[j].msg })
2496 return out
2497}
2498