github.com/orijtech/structslop@v0.0.9-0.20230520012622-069644583b8b/structslop.go (about)

     1  // Copyright 2020 Orijtech, Inc. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package structslop
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"go/ast"
    21  	"go/build"
    22  	"go/format"
    23  	"go/parser"
    24  	"go/token"
    25  	"go/types"
    26  	"os"
    27  	"sort"
    28  	"strings"
    29  
    30  	"github.com/dave/dst"
    31  	"github.com/dave/dst/decorator"
    32  	"golang.org/x/tools/go/analysis"
    33  	"golang.org/x/tools/go/analysis/passes/inspect"
    34  	"golang.org/x/tools/go/ast/inspector"
    35  )
    36  
    37  var (
    38  	includeTestFiles bool
    39  	verbose          bool
    40  	apply            bool
    41  	generated        bool
    42  )
    43  
    44  func init() {
    45  	Analyzer.Flags.BoolVar(&includeTestFiles, "include-test-files", includeTestFiles, "also check test files")
    46  	Analyzer.Flags.BoolVar(&verbose, "verbose", verbose, "print all information, even when struct is not sloppy")
    47  	Analyzer.Flags.BoolVar(&apply, "apply", apply, "apply suggested fixes (using -fix won't work)")
    48  	Analyzer.Flags.BoolVar(&generated, "generated", generated, "report issues in generated code")
    49  }
    50  
    51  const Doc = `check for structs that can be rearrange fields to provide for maximum space/allocation efficiency`
    52  
    53  // Analyzer describes struct slop analysis function detector.
    54  var Analyzer = &analysis.Analyzer{
    55  	Name:     "structslop",
    56  	Doc:      Doc,
    57  	Requires: []*analysis.Analyzer{inspect.Analyzer},
    58  	Run:      run,
    59  }
    60  
    61  func run(pass *analysis.Pass) (interface{}, error) {
    62  	// Use custom sizes instance, which implements types.Sizes for calculating struct size.
    63  	// go/types and gc does not agree about the struct size.
    64  	// See https://github.com/golang/go/issues/14909#issuecomment-199936232
    65  	pass.TypesSizes = &sizes{
    66  		stdSizes: types.SizesFor(build.Default.Compiler, build.Default.GOARCH),
    67  		maxAlign: pass.TypesSizes.Alignof(types.Typ[types.UnsafePointer]),
    68  	}
    69  
    70  	dec := decorator.NewDecorator(pass.Fset)
    71  	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    72  	nodeFilter := []ast.Node{
    73  		(*ast.File)(nil),
    74  		(*ast.StructType)(nil),
    75  	}
    76  
    77  	fileDiags := make(map[string][]byte)
    78  	var af *ast.File
    79  	var df *dst.File
    80  
    81  	// Track generated files unless -generated is set.
    82  	genFiles := make(map[*token.File]bool)
    83  	if !generated {
    84  	files:
    85  		for _, f := range pass.Files {
    86  			for _, c := range f.Comments {
    87  				for _, l := range c.List {
    88  					if strings.HasPrefix(l.Text, "// Code generated ") && strings.HasSuffix(l.Text, " DO NOT EDIT.") {
    89  						file := pass.Fset.File(f.Pos())
    90  						genFiles[file] = true
    91  						continue files
    92  					}
    93  				}
    94  			}
    95  		}
    96  	}
    97  	inspect.Preorder(nodeFilter, func(n ast.Node) {
    98  		file := pass.Fset.File(n.Pos())
    99  		if strings.HasSuffix(file.Name(), "_test.go") && !includeTestFiles {
   100  			return
   101  		}
   102  		// Skip generated structs if instructed.
   103  		if !generated && genFiles[file] {
   104  			return
   105  		}
   106  		if f, ok := n.(*ast.File); ok {
   107  			af = f
   108  			df, _ = dec.DecorateFile(af)
   109  			return
   110  		}
   111  		atyp := n.(*ast.StructType)
   112  		styp, ok := pass.TypesInfo.Types[atyp].Type.(*types.Struct)
   113  		// Type information may be incomplete.
   114  		if !ok {
   115  			return
   116  		}
   117  		if !verbose && styp.NumFields() < 2 {
   118  			return
   119  		}
   120  
   121  		r := checkSloppy(pass, styp)
   122  		if !verbose && !r.sloppy() {
   123  			return
   124  		}
   125  
   126  		var buf bytes.Buffer
   127  		expr, err := parser.ParseExpr(formatStruct(r.optStruct, pass.Pkg.Path()))
   128  		if err != nil {
   129  			return
   130  		}
   131  		if err := format.Node(&buf, token.NewFileSet(), expr.(*ast.StructType)); err != nil {
   132  			return
   133  		}
   134  
   135  		var msg string
   136  		switch {
   137  		case r.oldGcSize == r.newGcSize:
   138  			msg = fmt.Sprintf("struct has size %d (size class %d)", r.oldGcSize, r.oldRuntimeSize)
   139  		case r.oldGcSize != r.newGcSize:
   140  			msg = fmt.Sprintf(
   141  				"struct has size %d (size class %d), could be %d (size class %d), optimal fields order:\n%s\n",
   142  				r.oldGcSize,
   143  				r.oldRuntimeSize,
   144  				r.newGcSize,
   145  				r.newRuntimeSize,
   146  				buf.String(),
   147  			)
   148  			if r.sloppy() {
   149  				msg = fmt.Sprintf(
   150  					"struct has size %d (size class %d), could be %d (size class %d), you'll save %.2f%% if you rearrange it to:\n%s\n",
   151  					r.oldGcSize,
   152  					r.oldRuntimeSize,
   153  					r.newGcSize,
   154  					r.newRuntimeSize,
   155  					r.savings(),
   156  					buf.String(),
   157  				)
   158  			}
   159  		}
   160  
   161  		dtyp := dec.Dst.Nodes[atyp].(*dst.StructType)
   162  		fields := make([]*dst.Field, 0, len(r.optIdx))
   163  		dummy := &dst.Field{}
   164  		for _, f := range dtyp.Fields.List {
   165  			fields = append(fields, f)
   166  			if len(f.Names) == 0 {
   167  				continue
   168  			}
   169  			for range f.Names[1:] {
   170  				fields = append(fields, dummy)
   171  			}
   172  		}
   173  		optFields := make([]*dst.Field, 0, len(r.optIdx))
   174  		for _, i := range r.optIdx {
   175  			f := fields[i]
   176  			if f == dummy {
   177  				continue
   178  			}
   179  			optFields = append(optFields, f)
   180  		}
   181  		dtyp.Fields.List = optFields
   182  
   183  		var suggested bytes.Buffer
   184  		if err := decorator.Fprint(&suggested, df); err != nil {
   185  			return
   186  		}
   187  		pass.Report(analysis.Diagnostic{
   188  			Pos:            n.Pos(),
   189  			End:            n.End(),
   190  			Message:        msg,
   191  			SuggestedFixes: nil,
   192  		})
   193  		f := pass.Fset.File(n.Pos())
   194  		fileDiags[f.Name()] = suggested.Bytes()
   195  	})
   196  
   197  	if !apply {
   198  		return nil, nil
   199  	}
   200  	for fn, content := range fileDiags {
   201  		fi, err := os.Open(fn)
   202  		if err != nil {
   203  			_, _ = fmt.Fprintf(os.Stderr, "failed to open file: %v", err)
   204  		}
   205  		st, err := fi.Stat()
   206  		if err != nil {
   207  			_, _ = fmt.Fprintf(os.Stderr, "failed to get file stat: %v", err)
   208  		}
   209  		if err := fi.Close(); err != nil {
   210  			_, _ = fmt.Fprintf(os.Stderr, "failed to close file: %v", err)
   211  		}
   212  		if err := os.WriteFile(fn, content, st.Mode()); err != nil {
   213  			_, _ = fmt.Fprintf(os.Stderr, "failed to write suggested fix to file: %v", err)
   214  		}
   215  	}
   216  	return nil, nil
   217  }
   218  
   219  type result struct {
   220  	oldGcSize      int64
   221  	newGcSize      int64
   222  	oldRuntimeSize int64
   223  	newRuntimeSize int64
   224  	optStruct      *types.Struct
   225  	optIdx         []int
   226  }
   227  
   228  func (r result) sloppy() bool {
   229  	return r.oldRuntimeSize > r.newRuntimeSize
   230  }
   231  
   232  func (r result) savings() float64 {
   233  	return float64(r.oldRuntimeSize-r.newRuntimeSize) / float64(r.oldRuntimeSize) * 100
   234  }
   235  
   236  func mapFieldIdx(s *types.Struct) map[*types.Var]int {
   237  	m := make(map[*types.Var]int, s.NumFields())
   238  	for i := 0; i < s.NumFields(); i++ {
   239  		m[s.Field(i)] = i
   240  	}
   241  	return m
   242  }
   243  
   244  func checkSloppy(pass *analysis.Pass, origStruct *types.Struct) result {
   245  	m := mapFieldIdx(origStruct)
   246  	optStruct := optimalStructArrangement(pass.TypesSizes, m)
   247  	idx := make([]int, optStruct.NumFields())
   248  	for i := range idx {
   249  		idx[i] = m[optStruct.Field(i)]
   250  	}
   251  	r := result{
   252  		oldGcSize: pass.TypesSizes.Sizeof(origStruct),
   253  		newGcSize: pass.TypesSizes.Sizeof(optStruct),
   254  		optStruct: optStruct,
   255  		optIdx:    idx,
   256  	}
   257  	r.oldRuntimeSize = int64(roundUpSize(uintptr(r.oldGcSize)))
   258  	r.newRuntimeSize = int64(roundUpSize(uintptr(r.newGcSize)))
   259  	return r
   260  }
   261  
   262  func optimalStructArrangement(sizes types.Sizes, m map[*types.Var]int) *types.Struct {
   263  	fields := make([]*types.Var, len(m))
   264  	for v, i := range m {
   265  		fields[i] = v
   266  	}
   267  
   268  	sort.Slice(fields, func(i, j int) bool {
   269  		ti, tj := fields[i].Type(), fields[j].Type()
   270  		si, sj := sizes.Sizeof(ti), sizes.Sizeof(tj)
   271  
   272  		if si == 0 && sj != 0 {
   273  			return true
   274  		}
   275  		if sj == 0 && si != 0 {
   276  			return false
   277  		}
   278  
   279  		ai, aj := sizes.Alignof(ti), sizes.Alignof(tj)
   280  		if ai != aj {
   281  			return ai > aj
   282  		}
   283  
   284  		if si != sj {
   285  			return si > sj
   286  		}
   287  
   288  		return false
   289  	})
   290  
   291  	return types.NewStruct(fields, nil)
   292  }
   293  
   294  func formatStruct(styp *types.Struct, curPkgPath string) string {
   295  	qualifier := func(p *types.Package) string {
   296  		if p.Path() == curPkgPath {
   297  			return ""
   298  		}
   299  		return p.Name()
   300  	}
   301  	return types.TypeString(styp, qualifier)
   302  }