| index 5f6b8bba2..fde592418 100644 |
| --- a/approved_moves_copy_scan_test.go |
| +++ b/approved_moves_copy_scan_test.go |
| @@ -28,10 +28,45 @@ import ( |
| // unrecorded sentence: it is never silently dropped. Hitting a limit is a |
| // violation that names file:line. |
| // |
| -// The one place it does not look is a placeholder: an operand of a |
| -// concatenation or an argument of a Sprintf may be any value. The row it is |
| -// matched against must carry a <placeholder> at that spot, and the record is |
| -// where a human vouches for it. |
| +// A placeholder (an operand of a concatenation, an argument of a Sprintf) may |
| +// be data, such as a host name: the row it is matched against must carry a |
| +// <placeholder> at that spot, and the record is where a human vouches for it. |
| +// But a placeholder never carries an error's text. A call to Error(), a value |
| +// whose declared type is error or implements it, a value the gate cannot type |
| +// that is named err or ends in Err or err, a local or helper parameter that |
| +// holds any of these, and the argument of a %w verb are all violations there, |
| +// even when the format matches a row. |
| +// |
| +// What it is (ruling T15-scope): a lint against accidental drift into |
| +// operator copy, not a boundary against code written to evade it. The |
| +// whole-branch review and code review cover deliberate constructions. These |
| +// are its known limits: |
| +// |
| +// - Types are syntactic and same-package. It does not follow generics, |
| +// embedded structs, type aliases, or methods promoted through embedding. |
| +// A value of another package's type is an error only by its type's name |
| +// (one ending in Error) or, when its type is unknown, by its own name. |
| +// An error held in a variable of an unrelated name and an unknown type |
| +// (x := pkg.Do(); Sprintf("%v", x)) is read as data. |
| +// - Receivers are not lexically scoped. A method call whose receiver |
| +// identifier is declared more than once in the function is refused; a |
| +// receiver declared once is resolved by its one declaration. |
| +// - Struct fields written by JSON decoding, reflection, through a pointer |
| +// to the field (p := &x.Reason; *p = ...), with +=, or in files outside |
| +// movesScanFiles are not seen. A field read reflects the writes it can |
| +// see. A write that takes one result of a multi-value expression is a |
| +// non-sentence write. |
| +// - A Reason read as a placeholder whose writes fail only softly (a |
| +// variable the gate cannot read, not an error's text or a limit) is a |
| +// wildcard, as any other placeholder is. |
| +// - Function values: a direct reference to a sentence-flow function (a |
| +// package function, a method, or a closure configured in movesLocalSinks) |
| +// that is not a call is refused. A closure that is not configured is not |
| +// tracked as a value; its own parameters are unbound at its sinks, which |
| +// refuses them there. |
| +// - Anything that leaves the scanned files (a map or channel carrying text |
| +// from elsewhere, a global set at init) is read as far as the files say |
| +// and no further. |
| |
| // movesSentenceFields are the struct fields that carry a sentence from the |
| // function that makes it to the one that speaks it. A read of one as a |
| @@ -290,6 +325,7 @@ type fieldWrite struct { |
| expr ast.Expr |
| ctype string // the composite literal's struct type, when it names one |
| recv ast.Expr // for an assignment, the expression whose field is written |
| + multi bool // one result of a multi-value expression: not provable |
| } |
| |
| type fieldResult struct { |
| @@ -404,16 +440,25 @@ func newUniverse(t *testing.T, root string, files []string) *movesUniverse { |
| for i, el := range n.Elts { |
| if kv, ok := el.(*ast.KeyValueExpr); ok { |
| if k, ok := kv.Key.(*ast.Ident); ok { |
| - u.fieldWrites[k.Name] = append(u.fieldWrites[k.Name], &fieldWrite{f, fd, kv.Value, ctype, nil}) |
| + u.fieldWrites[k.Name] = append(u.fieldWrites[k.Name], &fieldWrite{file: f, fd: fd, expr: kv.Value, ctype: ctype}) |
| } |
| } else if i < len(order) { |
| - u.fieldWrites[order[i]] = append(u.fieldWrites[order[i]], &fieldWrite{f, fd, el, ctype, nil}) |
| + u.fieldWrites[order[i]] = append(u.fieldWrites[order[i]], &fieldWrite{file: f, fd: fd, expr: el, ctype: ctype}) |
| } |
| } |
| case *ast.AssignStmt: |
| for i, l := range n.Lhs { |
| - if s, ok := l.(*ast.SelectorExpr); ok && i < len(n.Rhs) && len(n.Rhs) == len(n.Lhs) { |
| - u.fieldWrites[s.Sel.Name] = append(u.fieldWrites[s.Sel.Name], &fieldWrite{f, fd, n.Rhs[i], "", s.X}) |
| + s, ok := l.(*ast.SelectorExpr) |
| + if !ok { |
| + continue |
| + } |
| + if len(n.Rhs) == len(n.Lhs) { |
| + u.fieldWrites[s.Sel.Name] = append(u.fieldWrites[s.Sel.Name], &fieldWrite{file: f, fd: fd, expr: n.Rhs[i], recv: s.X}) |
| + } else { |
| + // box.Reason, _ = detail(err) or box.Reason, ok = m[k]: one |
| + // result of a multi-value expression, which the gate cannot |
| + // prove is a sentence. The expression is the field written. |
| + u.fieldWrites[s.Sel.Name] = append(u.fieldWrites[s.Sel.Name], &fieldWrite{file: f, fd: fd, expr: s, recv: s.X, multi: true}) |
| } |
| } |
| } |
| @@ -454,6 +499,7 @@ type evalCtx struct { |
| bind map[string][]string |
| fwd map[string]bool |
| fwdFn map[string]bool |
| + errParams map[string]string // a helper's parameters bound to an error's text, and why |
| curReturn *ast.ReturnStmt |
| noCalls bool |
| visiting map[string]bool |
| @@ -461,7 +507,7 @@ type evalCtx struct { |
| |
| func (u *movesUniverse) newCtx(file string, fd *ast.FuncDecl, noCalls bool) *evalCtx { |
| c := &evalCtx{u: u, file: file, fd: fd, locals: localAssigns(fd), bind: map[string][]string{}, |
| - fwd: map[string]bool{}, fwdFn: map[string]bool{}, noCalls: noCalls, visiting: map[string]bool{}} |
| + fwd: map[string]bool{}, fwdFn: map[string]bool{}, errParams: map[string]string{}, noCalls: noCalls, visiting: map[string]bool{}} |
| c.pkg = loadMovesPkg(u.t, u.at(filepath.Dir(file))) |
| if fd == nil { |
| return c |
| @@ -540,17 +586,20 @@ func dedupe(in []string) []string { |
| |
| // operand reads a placeholder: any value is allowed, so what it cannot derive |
| // becomes one wildcard, which the record must bind to a <placeholder>. Only a |
| -// limit or a cycle (a hard problem) still counts. |
| +// hard problem (a limit, a cycle, an error's text) still counts. |
| func (c *evalCtx) operand(e ast.Expr, depth int) ([]string, []problem) { |
| if sel, isField := e.(*ast.SelectorExpr); isField && !c.noCalls { |
| // A field read as a placeholder is a wildcard for the record's |
| // <placeholder>, except a field that carries a sentence from one function |
| // to another (a Check's Reason): that is forwarded when every write to it |
| - // is a sentence. |
| + // is a sentence. A write's hard problem (a limit, an error's text, a |
| + // multi-value write) is never covered by the wildcard. |
| if movesSentenceFields[sel.Sel.Name] { |
| - if a, p := c.fieldRead(sel); len(p) == 0 { |
| + a, p := c.fieldRead(sel) |
| + if len(p) == 0 { |
| return a, nil |
| } |
| + return []string{wild}, hardOnly(p) |
| } |
| return []string{wild}, nil |
| } |
| @@ -561,6 +610,166 @@ func (c *evalCtx) operand(e ast.Expr, depth int) ([]string, []problem) { |
| return []string{wild}, hardOnly(probs) |
| } |
| |
| +// concatOperand reads one side of a +: a placeholder, unless it is an error's |
| +// text. A side that is itself a concatenation is checked by its own eval. |
| +func (c *evalCtx) concatOperand(e ast.Expr, depth int) ([]string, []problem) { |
| + if !isConcat(e) { |
| + if why := c.errorText(e, 0); why != "" { |
| + return []string{wild}, []problem{errorTextProblem(e, why)} |
| + } |
| + } |
| + return c.operand(e, depth) |
| +} |
| + |
| +func isConcat(e ast.Expr) bool { |
| + switch e := e.(type) { |
| + case *ast.ParenExpr: |
| + return isConcat(e.X) |
| + case *ast.BinaryExpr: |
| + return e.Op == token.ADD |
| + } |
| + return false |
| +} |
| + |
| +func errorTextProblem(e ast.Expr, why string) problem { |
| + return problem{fmt.Sprintf("%s puts an error's text in a sentence (%s)", render(e), why), true} |
| +} |
| + |
| +// errName is the name an error usually goes by: err, or one ending in Err or |
| +// err. It is used only when the gate cannot type the value. |
| +func errName(n string) bool { |
| + return strings.HasSuffix(n, "err") || strings.HasSuffix(n, "Err") |
| +} |
| + |
| +// isErrorType reports whether a declared type is error or implements it: the |
| +// error interface, a package type with an Error method, a package interface |
| +// that embeds error or declares Error, or another package's type whose name |
| +// ends in Error. |
| +func (c *evalCtx) isErrorType(t ast.Expr) bool { |
| + switch t := unstar(t).(type) { |
| + case *ast.Ident: |
| + if t.Name == "error" { |
| + return true |
| + } |
| + for _, d := range c.pkg.methods["Error"] { |
| + if recvBase(d) == t.Name { |
| + return true |
| + } |
| + } |
| + if it, ok := c.pkg.types[t.Name].(*ast.InterfaceType); ok && it.Methods != nil { |
| + for _, m := range it.Methods.List { |
| + if id, ok := m.Type.(*ast.Ident); ok && id.Name == "error" && len(m.Names) == 0 { |
| + return true |
| + } |
| + for _, n := range m.Names { |
| + if n.Name == "Error" { |
| + return true |
| + } |
| + } |
| + } |
| + } |
| + case *ast.SelectorExpr: |
| + return strings.HasSuffix(t.Sel.Name, "Error") |
| + } |
| + return false |
| +} |
| + |
| +// errorText says why e is an error's text, or "" when the gate has no reason to |
| +// think so. It reads the expression as written and the locals it names. |
| +func (c *evalCtx) errorText(e ast.Expr, depth int) string { |
| + if depth > maxEvalDepth { |
| + return "the gate ran out of depth deciding whether it is an error's text" |
| + } |
| + switch e := e.(type) { |
| + case *ast.ParenExpr: |
| + return c.errorText(e.X, depth+1) |
| + case *ast.BinaryExpr: |
| + if e.Op != token.ADD { |
| + return "" |
| + } |
| + if why := c.errorText(e.X, depth+1); why != "" { |
| + return why |
| + } |
| + return c.errorText(e.Y, depth+1) |
| + case *ast.CallExpr: |
| + if sel, ok := e.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "Error" && len(e.Args) == 0 { |
| + return "it calls Error" |
| + } |
| + if t := c.typeExprOf(e, 0); t != nil && c.isErrorType(t) { |
| + return "its result is an error" |
| + } |
| + // A call out of the package (fmt.Sprint, strings.TrimSpace) carries the |
| + // text of its arguments. A package helper is read by eval instead. |
| + if _, p := c.resolveCallees(e); p != nil { |
| + for _, a := range e.Args { |
| + if why := c.errorText(a, depth+1); why != "" { |
| + return fmt.Sprintf("it is made from %s, and %s", render(a), why) |
| + } |
| + } |
| + } |
| + case *ast.Ident: |
| + if why, ok := c.errParams[e.Name]; ok { |
| + return why |
| + } |
| + t := c.typeExprOf(e, 0) |
| + if t != nil && c.isErrorType(t) { |
| + return "its type is error" |
| + } |
| + key := fmt.Sprintf("errtext %p %s", c.fd, e.Name) |
| + if !c.visiting[key] { |
| + c.visiting[key] = true |
| + defer delete(c.visiting, key) |
| + for _, a := range c.locals[e.Name] { |
| + switch { |
| + case a.rhs != nil: |
| + if why := c.errorText(a.rhs, depth+1); why != "" { |
| + return fmt.Sprintf("it is assigned %s at %s, and %s", render(a.rhs), c.posOf(a.rhs), why) |
| + } |
| + case a.call != nil: |
| + if decls, p := c.resolveCallees(a.call); p == nil && len(decls) > 0 && decls[0].Type != nil { |
| + if rt := resultTypeAt(decls[0], a.pos); rt != nil && c.isErrorType(rt) { |
| + return fmt.Sprintf("it is assigned an error result at %s", c.posOf(a.call)) |
| + } |
| + } |
| + } |
| + } |
| + } |
| + if t == nil && errName(e.Name) { |
| + return "the gate cannot type it, and it is named as an error" |
| + } |
| + case *ast.SelectorExpr: |
| + t := c.typeExprOf(e, 0) |
| + if t != nil && c.isErrorType(t) { |
| + return "its type is error" |
| + } |
| + if t == nil && errName(e.Sel.Name) { |
| + return "the gate cannot type it, and it is named as an error" |
| + } |
| + } |
| + return "" |
| +} |
| + |
| +// wrapArgs returns the argument indexes (into the call's Args, the format at |
| +// 0) of a format's %w verbs. ok is false when the format's arguments cannot be |
| +// counted (an explicit argument index). |
| +func wrapArgs(format string) (idx []int, ok bool) { |
| + if strings.Contains(format, "%[") { |
| + return nil, !strings.Contains(format, "w") |
| + } |
| + arg := 1 |
| + for _, v := range formatVerb.FindAllString(format, -1) { |
| + if v == "%%" { |
| + continue |
| + } |
| + arg += strings.Count(v, "*") |
| + if strings.HasSuffix(v, "w") { |
| + idx = append(idx, arg) |
| + } |
| + arg++ |
| + } |
| + return idx, true |
| +} |
| + |
| // eval returns the texts e can be and why it cannot be read. |
| func (c *evalCtx) eval(e ast.Expr, depth int) ([]string, []problem) { |
| if depth > maxEvalDepth { |
| @@ -590,16 +799,15 @@ func (c *evalCtx) eval(e ast.Expr, depth int) ([]string, []problem) { |
| if e.Op != token.ADD { |
| return soft("%s is not a sentence", render(e)) |
| } |
| - l, lp := c.operand(e.X, depth+1) |
| - r, rp := c.operand(e.Y, depth+1) |
| - if len(lp)+len(rp) > 0 { |
| - return nil, append(lp, rp...) |
| - } |
| + l, lp := c.concatOperand(e.X, depth+1) |
| + r, rp := c.concatOperand(e.Y, depth+1) |
| out, ok := cross(l, r) |
| if !ok { |
| - return nil, limit(c, e, "too many alternative sentences") |
| + return nil, append(append(lp, rp...), limit(c, e, "too many alternative sentences")...) |
| } |
| - return dedupe(out), nil |
| + // The text is kept beside the problems, so a refused sentence is still |
| + // named by what it says. |
| + return dedupe(out), append(lp, rp...) |
| case *ast.SelectorExpr: |
| if c.noCalls { |
| return []string{wild}, nil |
| @@ -707,6 +915,10 @@ func (c *evalCtx) fieldRead(e *ast.SelectorExpr) ([]string, []problem) { |
| continue |
| } |
| n++ |
| + if w.multi { |
| + 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}) |
| + continue |
| + } |
| alts, probs := wc.eval(w.expr, 0) |
| for _, p := range probs { |
| 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}) |
| @@ -758,13 +970,34 @@ func (c *evalCtx) evalCall(e *ast.CallExpr, pos, depth int) ([]string, []problem |
| return nil, append([]problem{{fmt.Sprintf("%s has a format that is not a literal", render(e)), false}}, probs...) |
| } |
| var out []string |
| + var argProbs []problem |
| + flagged := map[int]bool{} |
| + for i, a := range e.Args[1:] { |
| + if why := c.errorText(a, 0); why != "" { |
| + flagged[i+1] = true |
| + argProbs = append(argProbs, errorTextProblem(a, why)) |
| + } |
| + } |
| for _, f := range fmts { |
| if strings.Contains(f, wild) || strings.Contains(f, fwdMark) { |
| return soft("%s has a format that is not a literal", render(e)) |
| } |
| out = append(out, formatPattern(f)) |
| + idx, ok := wrapArgs(f) |
| + if !ok { |
| + argProbs = append(argProbs, problem{fmt.Sprintf("%s has a %%w verb whose argument the gate cannot count", render(e)), true}) |
| + } |
| + for _, i := range idx { |
| + switch { |
| + case i >= len(e.Args): |
| + argProbs = append(argProbs, problem{fmt.Sprintf("%s has a %%w verb with no argument", render(e)), true}) |
| + case !flagged[i]: |
| + flagged[i] = true |
| + argProbs = append(argProbs, problem{fmt.Sprintf("%s is the argument of a %%w verb, an error", render(e.Args[i])), true}) |
| + } |
| + } |
| } |
| - return dedupe(out), nil |
| + return dedupe(out), argProbs |
| } |
| if c.noCalls { |
| return []string{wild}, nil |
| @@ -831,7 +1064,13 @@ func (c *evalCtx) resolveCallees(e *ast.CallExpr) ([]*ast.FuncDecl, *problem) { |
| return bad("%s(...) is not a function of this package", fn.Name) |
| case *ast.SelectorExpr: |
| if fn.Sel.Name == "Error" && len(e.Args) == 0 { |
| - return bad("%s forwards an error's text", render(e)) |
| + // Hard: an error's text is never a placeholder either. |
| + return nil, &problem{fmt.Sprintf("%s forwards an error's text", render(e)), true} |
| + } |
| + if x, ok := fn.X.(*ast.Ident); ok && c.fd != nil && declCount(c.fd, x.Name) > 1 { |
| + // The gate types a receiver by its first declaration. A receiver |
| + // declared again (if m := supplied; ...) may be another type. |
| + 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} |
| } |
| tx := c.typeExprOf(fn.X, 0) |
| t := baseTypeName(tx) |
| @@ -855,6 +1094,61 @@ func (c *evalCtx) resolveCallees(e *ast.CallExpr) ([]*ast.FuncDecl, *problem) { |
| return bad("%s is not a call the gate can read", render(e)) |
| } |
| |
| +// declCount counts the declarations of name in fd: its receiver, parameters |
| +// and named results, and in its body every := , var, range variable and |
| +// closure parameter or result. A := that redeclares in the same scope counts |
| +// too; the gate does not scope. |
| +func declCount(fd *ast.FuncDecl, name string) int { |
| + n := 0 |
| + fields := func(fl *ast.FieldList) { |
| + if fl == nil { |
| + return |
| + } |
| + for _, f := range fl.List { |
| + for _, id := range f.Names { |
| + if id.Name == name { |
| + n++ |
| + } |
| + } |
| + } |
| + } |
| + fields(fd.Recv) |
| + fields(fd.Type.Params) |
| + fields(fd.Type.Results) |
| + if fd.Body == nil { |
| + return n |
| + } |
| + ident := func(e ast.Expr) { |
| + if id, ok := e.(*ast.Ident); ok && id.Name == name { |
| + n++ |
| + } |
| + } |
| + ast.Inspect(fd.Body, func(x ast.Node) bool { |
| + switch x := x.(type) { |
| + case *ast.AssignStmt: |
| + if x.Tok == token.DEFINE { |
| + for _, l := range x.Lhs { |
| + ident(l) |
| + } |
| + } |
| + case *ast.ValueSpec: |
| + for _, id := range x.Names { |
| + ident(id) |
| + } |
| + case *ast.RangeStmt: |
| + if x.Tok == token.DEFINE { |
| + ident(x.Key) |
| + ident(x.Value) |
| + } |
| + case *ast.FuncLit: |
| + fields(x.Type.Params) |
| + fields(x.Type.Results) |
| + } |
| + return true |
| + }) |
| + return n |
| +} |
| + |
| // evalTrimPrefix reads strings.TrimPrefix(x, "lit"): the sentences x can be, |
| // with the prefix taken off those that have it. |
| func (c *evalCtx) evalTrimPrefix(e *ast.CallExpr, depth int) ([]string, []problem) { |
| @@ -1008,6 +1302,11 @@ func (c *evalCtx) callDecl(d *ast.FuncDecl, call *ast.CallExpr, pos, depth int) |
| a, pr := c.operand(call.Args[i], depth+1) |
| hc.bind[p] = a |
| probs = append(probs, pr...) |
| + // An argument that is an error's text may still only select among |
| + // sentences; it is refused where the helper puts it in one. |
| + if why := c.errorText(call.Args[i], 0); why != "" { |
| + hc.errParams[p] = fmt.Sprintf("it is bound to %s at %s, and %s", render(call.Args[i]), c.posOf(call.Args[i]), why) |
| + } |
| } else { |
| hc.bind[p] = []string{wild} |
| } |
| @@ -1537,12 +1836,52 @@ func funcValueRefs(t *testing.T, u *movesUniverse) []funcRef { |
| if err != nil { |
| t.Fatal(err) |
| } |
| + // A closure configured as a local sink (verifyArchive's fail) is a |
| + // sentence-flow function of this file: an alias of it (send := fail) |
| + // escapes the checks at its call sites. |
| + closures := map[string]bool{} |
| + ast.Inspect(af, func(n ast.Node) bool { |
| + var names []*ast.Ident |
| + var values []ast.Expr |
| + switch n := n.(type) { |
| + case *ast.AssignStmt: |
| + for _, l := range n.Lhs { |
| + id, _ := l.(*ast.Ident) |
| + names = append(names, id) |
| + } |
| + values = n.Rhs |
| + case *ast.ValueSpec: |
| + names, values = n.Names, n.Values |
| + } |
| + if len(names) != len(values) { |
| + return true |
| + } |
| + for i, id := range names { |
| + if _, lit := values[i].(*ast.FuncLit); lit && id != nil { |
| + if _, ok := movesLocalSinks[f][id.Name]; ok { |
| + closures[id.Name] = true |
| + } |
| + } |
| + } |
| + return true |
| + }) |
| callFun := map[ast.Node]bool{} |
| skip := map[ast.Node]bool{} |
| ast.Inspect(af, func(n ast.Node) bool { |
| switch n := n.(type) { |
| case *ast.CallExpr: |
| callFun[n.Fun] = true |
| + case *ast.AssignStmt: |
| + // The name being defined or assigned is not a use of its value. |
| + for _, l := range n.Lhs { |
| + if id, ok := l.(*ast.Ident); ok { |
| + skip[id] = true |
| + } |
| + } |
| + case *ast.ValueSpec: |
| + for _, id := range n.Names { |
| + skip[id] = true |
| + } |
| case *ast.FuncDecl: |
| skip[n.Name] = true |
| case *ast.SelectorExpr: |
| @@ -1558,7 +1897,7 @@ func funcValueRefs(t *testing.T, u *movesUniverse) []funcRef { |
| if skip[n] || callFun[n] { |
| return true |
| } |
| - if flow[n.Name] && declared[n.Name] { |
| + if flow[n.Name] && declared[n.Name] || closures[n.Name] { |
| 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)}) |
| } |
| } |