blob: 5783278d2c29b635e40bc3740980abe9b6a4a12f [file] [log] [blame]
Dan Willemsen6ff23252015-09-15 13:49:18 -07001// Copyright 2010 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// This file contains the code to check canonical methods.
6
7package main
8
9import (
10 "fmt"
11 "go/ast"
12 "go/printer"
13 "strings"
14)
15
16func init() {
17 register("methods",
18 "check that canonically named methods are canonically defined",
19 checkCanonicalMethod,
20 funcDecl, interfaceType)
21}
22
23type MethodSig struct {
24 args []string
25 results []string
26}
27
28// canonicalMethods lists the input and output types for Go methods
Dan Willemsen0c157092016-07-08 13:57:52 -070029// that are checked using dynamic interface checks. Because the
Dan Willemsen6ff23252015-09-15 13:49:18 -070030// checks are dynamic, such methods would not cause a compile error
31// if they have the wrong signature: instead the dynamic check would
Dan Willemsen0c157092016-07-08 13:57:52 -070032// fail, sometimes mysteriously. If a method is found with a name listed
Dan Willemsen6ff23252015-09-15 13:49:18 -070033// here but not the input/output types listed here, vet complains.
34//
35// A few of the canonical methods have very common names.
36// For example, a type might implement a Scan method that
37// has nothing to do with fmt.Scanner, but we still want to check
38// the methods that are intended to implement fmt.Scanner.
39// To do that, the arguments that have a = prefix are treated as
40// signals that the canonical meaning is intended: if a Scan
41// method doesn't have a fmt.ScanState as its first argument,
Dan Willemsen0c157092016-07-08 13:57:52 -070042// we let it go. But if it does have a fmt.ScanState, then the
Dan Willemsen6ff23252015-09-15 13:49:18 -070043// rest has to match.
44var canonicalMethods = map[string]MethodSig{
45 // "Flush": {{}, {"error"}}, // http.Flusher and jpeg.writer conflict
46 "Format": {[]string{"=fmt.State", "rune"}, []string{}}, // fmt.Formatter
47 "GobDecode": {[]string{"[]byte"}, []string{"error"}}, // gob.GobDecoder
48 "GobEncode": {[]string{}, []string{"[]byte", "error"}}, // gob.GobEncoder
49 "MarshalJSON": {[]string{}, []string{"[]byte", "error"}}, // json.Marshaler
50 "MarshalXML": {[]string{"*xml.Encoder", "xml.StartElement"}, []string{"error"}}, // xml.Marshaler
Dan Willemsen6ff23252015-09-15 13:49:18 -070051 "ReadByte": {[]string{}, []string{"byte", "error"}}, // io.ByteReader
52 "ReadFrom": {[]string{"=io.Reader"}, []string{"int64", "error"}}, // io.ReaderFrom
53 "ReadRune": {[]string{}, []string{"rune", "int", "error"}}, // io.RuneReader
54 "Scan": {[]string{"=fmt.ScanState", "rune"}, []string{"error"}}, // fmt.Scanner
55 "Seek": {[]string{"=int64", "int"}, []string{"int64", "error"}}, // io.Seeker
56 "UnmarshalJSON": {[]string{"[]byte"}, []string{"error"}}, // json.Unmarshaler
57 "UnmarshalXML": {[]string{"*xml.Decoder", "xml.StartElement"}, []string{"error"}}, // xml.Unmarshaler
58 "UnreadByte": {[]string{}, []string{"error"}},
59 "UnreadRune": {[]string{}, []string{"error"}},
60 "WriteByte": {[]string{"byte"}, []string{"error"}}, // jpeg.writer (matching bufio.Writer)
61 "WriteTo": {[]string{"=io.Writer"}, []string{"int64", "error"}}, // io.WriterTo
62}
63
64func checkCanonicalMethod(f *File, node ast.Node) {
65 switch n := node.(type) {
66 case *ast.FuncDecl:
67 if n.Recv != nil {
68 canonicalMethod(f, n.Name, n.Type)
69 }
70 case *ast.InterfaceType:
71 for _, field := range n.Methods.List {
72 for _, id := range field.Names {
73 canonicalMethod(f, id, field.Type.(*ast.FuncType))
74 }
75 }
76 }
77}
78
79func canonicalMethod(f *File, id *ast.Ident, t *ast.FuncType) {
80 // Expected input/output.
81 expect, ok := canonicalMethods[id.Name]
82 if !ok {
83 return
84 }
85
86 // Actual input/output
87 args := typeFlatten(t.Params.List)
88 var results []ast.Expr
89 if t.Results != nil {
90 results = typeFlatten(t.Results.List)
91 }
92
93 // Do the =s (if any) all match?
94 if !f.matchParams(expect.args, args, "=") || !f.matchParams(expect.results, results, "=") {
95 return
96 }
97
98 // Everything must match.
99 if !f.matchParams(expect.args, args, "") || !f.matchParams(expect.results, results, "") {
100 expectFmt := id.Name + "(" + argjoin(expect.args) + ")"
101 if len(expect.results) == 1 {
102 expectFmt += " " + argjoin(expect.results)
103 } else if len(expect.results) > 1 {
104 expectFmt += " (" + argjoin(expect.results) + ")"
105 }
106
107 f.b.Reset()
108 if err := printer.Fprint(&f.b, f.fset, t); err != nil {
109 fmt.Fprintf(&f.b, "<%s>", err)
110 }
111 actual := f.b.String()
112 actual = strings.TrimPrefix(actual, "func")
113 actual = id.Name + actual
114
115 f.Badf(id.Pos(), "method %s should have signature %s", actual, expectFmt)
116 }
117}
118
119func argjoin(x []string) string {
120 y := make([]string, len(x))
121 for i, s := range x {
122 if s[0] == '=' {
123 s = s[1:]
124 }
125 y[i] = s
126 }
127 return strings.Join(y, ", ")
128}
129
130// Turn parameter list into slice of types
131// (in the ast, types are Exprs).
132// Have to handle f(int, bool) and f(x, y, z int)
133// so not a simple 1-to-1 conversion.
134func typeFlatten(l []*ast.Field) []ast.Expr {
135 var t []ast.Expr
136 for _, f := range l {
137 if len(f.Names) == 0 {
138 t = append(t, f.Type)
139 continue
140 }
Dan Willemsene1b3b182018-02-27 19:36:27 -0800141 for range f.Names {
Dan Willemsen6ff23252015-09-15 13:49:18 -0700142 t = append(t, f.Type)
143 }
144 }
145 return t
146}
147
148// Does each type in expect with the given prefix match the corresponding type in actual?
149func (f *File) matchParams(expect []string, actual []ast.Expr, prefix string) bool {
150 for i, x := range expect {
151 if !strings.HasPrefix(x, prefix) {
152 continue
153 }
154 if i >= len(actual) {
155 return false
156 }
157 if !f.matchParamType(x, actual[i]) {
158 return false
159 }
160 }
161 if prefix == "" && len(actual) > len(expect) {
162 return false
163 }
164 return true
165}
166
167// Does this one type match?
168func (f *File) matchParamType(expect string, actual ast.Expr) bool {
Dan Willemsenf3f2eb62018-08-28 11:28:58 -0700169 expect = strings.TrimPrefix(expect, "=")
Dan Willemsen6ff23252015-09-15 13:49:18 -0700170 // Strip package name if we're in that package.
171 if n := len(f.file.Name.Name); len(expect) > n && expect[:n] == f.file.Name.Name && expect[n] == '.' {
172 expect = expect[n+1:]
173 }
174
175 // Overkill but easy.
176 f.b.Reset()
177 printer.Fprint(&f.b, f.fset, actual)
178 return f.b.String() == expect
179}