|
| 1 | +// Copyright (c) 2021 Uber Technologies, Inc. |
| 2 | +// |
| 3 | +// Permission is hereby granted, free of charge, to any person obtaining a copy |
| 4 | +// of this software and associated documentation files (the "Software"), to deal |
| 5 | +// in the Software without restriction, including without limitation the rights |
| 6 | +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 7 | +// copies of the Software, and to permit persons to whom the Software is |
| 8 | +// furnished to do so, subject to the following conditions: |
| 9 | +// |
| 10 | +// The above copyright notice and this permission notice shall be included in |
| 11 | +// all copies or substantial portions of the Software. |
| 12 | +// |
| 13 | +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 14 | +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 15 | +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 16 | +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 17 | +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 18 | +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| 19 | +// THE SOFTWARE. |
| 20 | + |
| 21 | +// Package allfxevents implements a Go analysis pass that verifies that an |
| 22 | +// fxevent.Logger implementation handles all known fxevent types. As a special |
| 23 | +// case for no-op or fake fxevent.Loggers, it ignores implementations that |
| 24 | +// handle none of the event types. |
| 25 | +// |
| 26 | +// This is meant for use within Fx only. |
| 27 | +package allfxevents |
| 28 | + |
| 29 | +import ( |
| 30 | + "fmt" |
| 31 | + "go/ast" |
| 32 | + "go/token" |
| 33 | + "go/types" |
| 34 | + "sort" |
| 35 | + "strings" |
| 36 | + |
| 37 | + "golang.org/x/tools/go/analysis" |
| 38 | + "golang.org/x/tools/go/analysis/passes/inspect" |
| 39 | + "golang.org/x/tools/go/ast/inspector" |
| 40 | + "golang.org/x/tools/go/types/typeutil" |
| 41 | +) |
| 42 | + |
| 43 | +// Analyzer is a go/analysis compatible analyzer that verifies that all |
| 44 | +// fxevent.Loggers shipped with Fx handle all known Fx event types. |
| 45 | +var Analyzer = &analysis.Analyzer{ |
| 46 | + Name: "allfxevents", |
| 47 | + Doc: "check for unhandled fxevent.Events", |
| 48 | + Run: run, |
| 49 | + Requires: []*analysis.Analyzer{ |
| 50 | + inspect.Analyzer, |
| 51 | + }, |
| 52 | +} |
| 53 | + |
| 54 | +var _filter = []ast.Node{ |
| 55 | + &ast.File{}, |
| 56 | + &ast.FuncDecl{}, |
| 57 | + &ast.CaseClause{}, |
| 58 | + &ast.TypeAssertExpr{}, |
| 59 | +} |
| 60 | + |
| 61 | +func run(pass *analysis.Pass) (interface{}, error) { |
| 62 | + fxeventPkg, ok := findPackage(pass.Pkg, "go.uber.org/fx/fxevent") |
| 63 | + if !ok { |
| 64 | + // If the package doesn't import fxevent, and itself isn't |
| 65 | + // fxevent, then we don't need to run this pass. |
| 66 | + return nil, nil |
| 67 | + } |
| 68 | + |
| 69 | + v := visitor{ |
| 70 | + Fxevent: inspectFxevent(fxeventPkg), |
| 71 | + Fset: pass.Fset, |
| 72 | + Info: pass.TypesInfo, |
| 73 | + Report: pass.Report, |
| 74 | + } |
| 75 | + |
| 76 | + pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes(_filter, v.Visit) |
| 77 | + return nil, nil |
| 78 | +} |
| 79 | + |
| 80 | +type visitor struct { |
| 81 | + Fset *token.FileSet |
| 82 | + Info *types.Info |
| 83 | + Fxevent fxevent |
| 84 | + Report func(analysis.Diagnostic) |
| 85 | + |
| 86 | + // types not yet referenced by this function |
| 87 | + loggerType types.Type |
| 88 | + funcEvents *typeSet |
| 89 | +} |
| 90 | + |
| 91 | +func (v *visitor) Visit(n ast.Node, push bool) (recurse bool) { |
| 92 | + switch n := n.(type) { |
| 93 | + case *ast.File: |
| 94 | + if !push { |
| 95 | + return false |
| 96 | + } |
| 97 | + |
| 98 | + // Don't run the linter on test files. |
| 99 | + fname := v.Fset.File(n.Pos()).Name() |
| 100 | + return !strings.HasSuffix(fname, "_test.go") |
| 101 | + |
| 102 | + case *ast.FuncDecl: |
| 103 | + if !push { |
| 104 | + return v.funcDeclExit(n) |
| 105 | + } |
| 106 | + return v.funcDeclEnter(n) |
| 107 | + |
| 108 | + case *ast.CaseClause: |
| 109 | + if !push { |
| 110 | + return false |
| 111 | + } |
| 112 | + for _, expr := range n.List { |
| 113 | + t := v.Info.Types[expr].Type |
| 114 | + if t != nil { |
| 115 | + v.funcEvents.Remove(t) |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + case *ast.TypeAssertExpr: |
| 120 | + if !push { |
| 121 | + return false |
| 122 | + } |
| 123 | + t := v.Info.Types[n.Type].Type |
| 124 | + if t != nil { |
| 125 | + v.funcEvents.Remove(t) |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + return false |
| 130 | +} |
| 131 | + |
| 132 | +func (v *visitor) funcDeclEnter(n *ast.FuncDecl) bool { |
| 133 | + // Skip top-level functions, and methods not named |
| 134 | + // LogEvent. |
| 135 | + if n.Recv == nil || n.Name.Name != "LogEvent" { |
| 136 | + return false |
| 137 | + } |
| 138 | + |
| 139 | + // Skip types that don't implement fxevent.Logger. |
| 140 | + t := v.Info.Types[n.Recv.List[0].Type].Type |
| 141 | + if t == nil || !types.Implements(t, v.Fxevent.LoggerInterface) { |
| 142 | + return false |
| 143 | + } |
| 144 | + |
| 145 | + // Each function declaration gets its own copy of the typeSet to track |
| 146 | + // events in. |
| 147 | + v.loggerType = t |
| 148 | + v.funcEvents = v.Fxevent.Events.Clone() |
| 149 | + return true |
| 150 | +} |
| 151 | + |
| 152 | +func (v *visitor) funcDeclExit(n *ast.FuncDecl) bool { |
| 153 | + nEvents := v.funcEvents.Len() |
| 154 | + if nEvents == 0 { |
| 155 | + return false |
| 156 | + } |
| 157 | + |
| 158 | + // If the logger doesn't handle *any* event type, it's probably a fake, |
| 159 | + // or a no-op implementation. Don't bother with it. |
| 160 | + if nEvents == v.Fxevent.Events.Len() { |
| 161 | + return false |
| 162 | + } |
| 163 | + |
| 164 | + missing := make([]string, 0, nEvents) |
| 165 | + v.funcEvents.Iterate(func(t types.Type) { |
| 166 | + // Use a fxevent qualifier so that event names don't include |
| 167 | + // the full import path of the fxevent package. |
| 168 | + missing = append(missing, types.TypeString(t, emptyQualifier)) |
| 169 | + }) |
| 170 | + sort.Strings(missing) |
| 171 | + |
| 172 | + v.Report(analysis.Diagnostic{ |
| 173 | + Pos: n.Pos(), |
| 174 | + Message: fmt.Sprintf("%v doesn't handle %v", |
| 175 | + types.TypeString(v.loggerType, emptyQualifier), |
| 176 | + missing, |
| 177 | + ), |
| 178 | + }) |
| 179 | + |
| 180 | + return false |
| 181 | +} |
| 182 | + |
| 183 | +// Find the package with the given import path. |
| 184 | +func findPackage(pkg *types.Package, importPath string) (_ *types.Package, ok bool) { |
| 185 | + if pkg.Path() == importPath { |
| 186 | + return pkg, true |
| 187 | + } |
| 188 | + |
| 189 | + for _, imp := range pkg.Imports() { |
| 190 | + if imp.Path() == importPath { |
| 191 | + return imp, true |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + return nil, false |
| 196 | +} |
| 197 | + |
| 198 | +// fxevent holds type information extracted from the fxevent package necessary |
| 199 | +// for inspection. |
| 200 | +type fxevent struct { |
| 201 | + Logger types.Type // fxevent.Logger |
| 202 | + LoggerInterface *types.Interface // raw type information for fxevent.Logger |
| 203 | + |
| 204 | + Event types.Type // fxevent.Type |
| 205 | + Events typeSet |
| 206 | +} |
| 207 | + |
| 208 | +func inspectFxevent(pkg *types.Package) fxevent { |
| 209 | + scope := pkg.Scope() |
| 210 | + event := scope.Lookup("Event").Type() |
| 211 | + |
| 212 | + var eventTypes typeSet |
| 213 | + for _, name := range scope.Names() { |
| 214 | + if name == "Event" { |
| 215 | + continue |
| 216 | + } |
| 217 | + |
| 218 | + obj := scope.Lookup(name) |
| 219 | + if !obj.Exported() { |
| 220 | + continue |
| 221 | + } |
| 222 | + |
| 223 | + typ := obj.Type() |
| 224 | + |
| 225 | + if !types.ConvertibleTo(typ, event) { |
| 226 | + typ = types.NewPointer(typ) |
| 227 | + if !types.ConvertibleTo(typ, event) { |
| 228 | + continue |
| 229 | + } |
| 230 | + } |
| 231 | + |
| 232 | + eventTypes.Put(typ) |
| 233 | + } |
| 234 | + |
| 235 | + logger := scope.Lookup("Logger").Type() |
| 236 | + return fxevent{ |
| 237 | + Logger: logger, |
| 238 | + LoggerInterface: logger.Underlying().(*types.Interface), |
| 239 | + Event: event, |
| 240 | + Events: eventTypes, |
| 241 | + } |
| 242 | +} |
| 243 | + |
| 244 | +// A set of types.Type objects. The zero value is valid. |
| 245 | +type typeSet struct{ m typeutil.Map } |
| 246 | + |
| 247 | +func (ts *typeSet) Len() int { |
| 248 | + return ts.m.Len() |
| 249 | +} |
| 250 | + |
| 251 | +// Put puts an item into the set. |
| 252 | +func (ts *typeSet) Put(t types.Type) { |
| 253 | + ts.m.Set(t, struct{}{}) |
| 254 | +} |
| 255 | + |
| 256 | +// Remove removes an item from the set, reporting whether it was found in the |
| 257 | +// set. |
| 258 | +func (ts *typeSet) Remove(t types.Type) (found bool) { |
| 259 | + return ts.m.Delete(t) |
| 260 | +} |
| 261 | + |
| 262 | +// Iterate iterates through the type set in an unspecified order. |
| 263 | +func (ts *typeSet) Iterate(f func(types.Type)) { |
| 264 | + ts.m.Iterate(func(t types.Type, _ interface{}) { |
| 265 | + f(t) |
| 266 | + }) |
| 267 | +} |
| 268 | + |
| 269 | +func (ts *typeSet) Clone() *typeSet { |
| 270 | + var out typeSet |
| 271 | + ts.Iterate(out.Put) |
| 272 | + return &out |
| 273 | +} |
| 274 | + |
| 275 | +// Use this as a types.Qualifier to print the name of an entity with |
| 276 | +// types.TypeString or similar without including their full package path. |
| 277 | +func emptyQualifier(*types.Package) string { |
| 278 | + return "" |
| 279 | +} |
0 commit comments