go fix の裏側

modernize analyzer を読み解く

Go 1.26 Release Party

Dennis(デニス) Metzger(メッツガー)

Backend Engineer @ Finatext

  1. func ToPtr[T any](x T) *T {
  2. return &x
  3. }

Go 1.26: new(expr)

  1. type data struct {
  2. x *int
  3. y *int
  4. }
  5. d := data{
  6. x: ToPtr(10),
  7. y: ToPtr(10),
  8. }
  1. type data struct {
  2. x *int
  3. y *int
  4. }
  5. d := data{
  6. x: new(10),
  7. y: new(10),
  8. }

Go 1.26: 新しい go fix

  1. func ToPtr[T any](x T) *T {
  2. return &x
  3. }
  4. var _ = ToPtr(123)

go fix ./...

  1. //go:fix inline
  2. func ToPtr[T any](x T) *T {
  3. return new(x)
  4. }
  5. var _ = new(123)

なぜこれを理解する必要があるのか?

LLMの時代はLinterの時代

  • AGENTS.md
  • Linterが通るまで直して✅️

LLMの時代はLinterの時代

  • AGENTS.md
  • Linterが通るまで直して✅️

LLMの時代はLinterの時代

  • AGENTS.md
  • Linterが通るまで直して✅️

LLMの時代はLinterの時代

  • AGENTS.md
  • Linterが通るまで直して✅️

modernizer の仕組み

  1. package main
  2. import (
  3. "golang.org/x/tools/go/analysis/
    multichecker"
  4. "golang.org/x/tools/go/analysis/
    passes/
    modernize"
  5. )
  6. func main() {
  7. multichecker.Main(modernize.Suite...)
  8. }

go/analysis/passes/modernize/cmd/modernize/main.go

  1. package modernize
  2. var Suite = []*analysis.Analyzer{
  3. AnyAnalyzer,
  4. // AppendClippedAnalyzer, // not nil-preserving!
  5. // BLoopAnalyzer, // may skew benchmark results, see golang/go#74967
  6. FmtAppendfAnalyzer,
  7. ForVarAnalyzer,
  8. MapsLoopAnalyzer,
  9. MinMaxAnalyzer,
  10. NewExprAnalyzer,
  11. ...
  12. }
  1. package modernize
  2. var Suite = []*analysis.Analyzer{
  3. AnyAnalyzer,
  4. // AppendClippedAnalyzer, // not nil-preserving!
  5. // BLoopAnalyzer, // may skew benchmark results, see golang/go#74967
  6. FmtAppendfAnalyzer,
  7. ForVarAnalyzer,
  8. MapsLoopAnalyzer,
  9. MinMaxAnalyzer,
  10. NewExprAnalyzer,
  11. ...
  12. }
  1. var NewExprAnalyzer = &analysis.Analyzer{
  2. Name: "newexpr",
  3. Doc: ...,
  4. URL: ...,
  5. Requires: []*analysis.Analyzer{
  6. inspect.Analyzer,
  7. },
  8. Run: run,
  9. FactTypes: []analysis.Fact{&newLike{
    }
    },
  10. }
  1. var NewExprAnalyzer = &analysis.Analyzer{
  2. ...
  3. Requires: []*analysis.Analyzer{
  4. inspect.Analyzer,
  5. },
  6. Run: run,
  7. FactTypes: []analysis.Fact{&newLike{
    }
    },
  8. }
  9. func run(pass *analysis.Pass) (any, error) {
  10. var (
  11. inspect = pass.ResultOf[inspect.Analyzer].
    (*inspector.Inspector)
  12. info = pass.TypesInfo
  13. )
  14. ...
  1. var NewExprAnalyzer = &analysis.Analyzer{
  2. ...
  3. Requires: []*analysis.Analyzer{
  4. inspect.Analyzer,
  5. },
  6. Run: run,
  7. FactTypes: []analysis.Fact{&newLike{
    }
    },
  8. }
  9. func run(pass *analysis.Pass) (any, error) {
  10. var (
  11. inspect = pass.ResultOf[inspect.Analyzer].
    (*inspector.Inspector)
  12. info = pass.TypesInfo
  13. )
  14. ...
  1. var NewExprAnalyzer = &analysis.Analyzer{
  2. ...
  3. Requires: []*analysis.Analyzer{
  4. inspect.Analyzer,
  5. },
  6. Run: run,
  7. FactTypes: []analysis.Fact{&newLike{
    }
    },
  8. }
  9. func run(pass *analysis.Pass) (any, error) {
  10. var (
  11. inspect = pass.ResultOf[inspect.Analyzer].
    (*inspector.Inspector)
  12. info = pass.TypesInfo
  13. )
  14. ...
  1. var NewExprAnalyzer = &analysis.Analyzer{
  2. ...
  3. Requires: []*analysis.Analyzer{
  4. inspect.Analyzer,
  5. },
  6. Run: run,
  7. FactTypes: []analysis.Fact{&newLike{
    }
    },
  8. }
  9. func run(pass *analysis.Pass) (any, error) {
  10. var (
  11. inspect = pass.ResultOf[inspect.Analyzer].
    (*inspector.Inspector)
  12. info = pass.TypesInfo
  13. )
  14. ...
  1. package inspect
  2. var Analyzer = &analysis.Analyzer{
  3. Name: "inspect",
  4. Doc: ...,
  5. URL: ...,
  6. Run: run,
  7. RunDespiteErrors: true,
  8. ResultType: reflect.
    TypeFor[*inspector.Inspector](),
  9. }
  10. func run(pass *analysis.Pass) (any, error) {
  11. ...
  1. package inspect
  2. var Analyzer = &analysis.Analyzer{
  3. Name: "inspect",
  4. Doc: ...,
  5. URL: ...,
  6. Run: run,
  7. RunDespiteErrors: true,
  8. ResultType: reflect.
    TypeFor[*inspector.Inspector](),
  9. }
  10. func run(pass *analysis.Pass) (any, error) {
  11. ...
  1. var NewExprAnalyzer = &analysis.Analyzer{
  2. ...
  3. Requires: []*analysis.Analyzer{
  4. inspect.Analyzer,
  5. },
  6. Run: run,
  7. FactTypes: []analysis.Fact{&newLike{
    }
    },
  8. }
  9. func run(pass *analysis.Pass) (any, error) {
  10. var (
  11. inspect = pass.ResultOf[inspect.Analyzer].
    (*inspector.Inspector)
  12. info = pass.TypesInfo
  13. )
  14. ...

Part 1:

対象関数を見つける

  1. for curFuncDecl := range inspect.Root().
  2. Preorder((*ast.FuncDecl)(nil)) {
  3. decl := curFuncDecl.Node().(*ast.FuncDecl)
  4. fn := info.Defs[decl.Name].(*types.Func)

  1. func _(){}
  1. for curFuncDecl := range inspect.Root().
  2. Preorder((*ast.FuncDecl)(nil)) {
  3. decl := curFuncDecl.Node().(*ast.FuncDecl)
  4. fn := info.Defs[decl.Name].(*types.Func)

  1. func _(){}
  1. if decl.Body != nil &&
  2. len(decl.Body.List) == 1 {

  1. func _(){
  2. 〇〇
  3. }
  1. if ret, ok := decl.Body.List[0]. (*ast.ReturnStmt);
  2. ok && len(ret.Results) == 1 {

  1. func _(){
  2. return 〇〇
  3. }
  1. if unary, ok := ret.Results[0].(*ast.UnaryExpr);
  2. ok && unary.Op == token.AND {

  1. func _(){
  2. return &〇〇
  3. }
  1. if id, ok := unary.X.(*ast.Ident); ok {

  1. func _(){
  2. return &v
  3. }
  1. if v, ok := info.Uses[id].(*types.Var); ok {

  1. func _(){
  2. return &v
  3. }
  1. sig := fn.Signature()
  2. if sig.Results().Len() == 1 &&
  3. is[*types.Pointer](
    sig.Results().At(0).Type()) &&
  4. sig.Params().Len() == 1 &&
  5. sig.Params().At(0) == v {

  1. func _(){
  2. return &v
  3. }
  1. sig := fn.Signature()
  2. if sig.Results().Len() == 1 &&
  3. is[*types.Pointer](
    sig.Results().At(0).Type()) &&
  4. sig.Params().Len() == 1 &&
  5. sig.Params().At(0) == v {

  1. func _() T{
  2. return &v
  3. }
  1. sig := fn.Signature()
  2. if sig.Results().Len() == 1 &&
  3. is[*types.Pointer](
    sig.Results().At(0).Type()) &&
  4. sig.Params().Len() == 1 &&
  5. sig.Params().At(0) == v {

  1. func _() *T{
  2. return &v
  3. }
  1. sig := fn.Signature()
  2. if sig.Results().Len() == 1 &&
  3. is[*types.Pointer](
    sig.Results().At(0).Type()) &&
  4. sig.Params().Len() == 1 &&
  5. sig.Params().At(0) == v {

  1. func _(〇〇 〇〇) *T{
  2. return &v
  3. }
  1. sig := fn.Signature()
  2. if sig.Results().Len() == 1 &&
  3. is[*types.Pointer](
    sig.Results().At(0).Type()) &&
  4. sig.Params().Len() == 1 &&
  5. sig.Params().At(0) == v {

  1. func _(v T) *T{
  2. return &v
  3. }
  1. pass.ExportObjectFact(fn, &newLike{})
  1. var NewExprAnalyzer = &analysis.Analyzer{
  2. ...
  3. FactTypes: []analysis.Fact{&newLike{
    }
    },
  4. }
  5. func run(pass *analysis.Pass) (any, error) {
  6. ...
  7. pass.ExportObjectFact(fn, &newLike{})
  8. ...
  9. }
  1. var NewExprAnalyzer = &analysis.Analyzer{
  2. ...
  3. FactTypes: []analysis.Fact{&newLike{
    }
    },
  4. }
  5. func run(pass *analysis.Pass) (any, error) {
  6. ...
  7. pass.ExportObjectFact(fn, &newLike{})
  8. ...
  9. }
  10. type newLike struct{}
  11. func (*newLike) AFact() {}
  12. func (*newLike) String() string {
  13. return "newlike"
  14. }

Part 2:

関数内のnew対応

  1. file := astutil.EnclosingFile(curFuncDecl)
  2. if !analyzerutil.FileUsesGoVersion(
  3. pass, file, versions.Go1_26,
  4. ) {
  5. continue
  6. }
  1. var edits []analysis.TextEdit
  2. curRet, _ := curFuncDecl.FindNode(ret)
  3. if lookup(info, curRet, "new") == builtinNew {
  4. edits = []analysis.TextEdit{
  5. {
  6. Pos: unary.OpPos,
  7. End: unary.OpPos + token.Pos(len("&")),
  8. NewText: []byte("new("),
  9. },
  10. {
  11. Pos: unary.X.End(),
  12. End: unary.X.End(),
  13. NewText: []byte(")"),
  14. },
  15. }
  16. }

Part 3:

アノテーション

  1. if !strings.Contains(
  2. decl.Doc.Text(),
  3. "go:fix inline",
  4. ) {
  5. edits = append(edits, analysis.TextEdit{
  6. Pos: decl.Pos(),
  7. End: decl.Pos(),
  8. NewText: []byte("//go:fix inline\n"),
  9. })
  10. }
  1. if len(edits) > 0 {
  2. pass.Report(analysis.Diagnostic{
  3. Pos: decl.Name.Pos(),
  4. End: decl.Name.End(),
  5. Message: fmt.Sprintf(
  6. "%s can be an inlinable"+
  7. " wrapper around new(expr)",
  8. decl.Name),
  9. SuggestedFixes: []analysis.SuggestedFix{
    {
  10. Message: "Make %s an inlinable" +
  11. " wrapper around new(expr)",
  12. TextEdits: edits,
  13. }
    }
    ,
  14. })
  15. }

Part 3:

呼び出し箇所の対応

  1. }
  2. }
  3. }
  4. }
  5. }
  6. }
  7. }
  1. for curCall := range inspect.Root().
  2. Preorder((*ast.CallExpr)(nil)) {
  3. call := curCall.Node().(*ast.CallExpr)
  4. var fact newLike
  5. if fn, ok := typeutil.Callee(info, call).
    (*types.Func); ok &&
  6. pass.ImportObjectFact(fn, &fact) {
  1. file := astutil.EnclosingFile(curCall)
  2. if !analyzerutil.FileUsesGoVersion(
  3. pass, file, versions.Go1_26,
  4. ) {
  5. continue
  6. }
  7. if lookup(info, curCall, "new") != builtinNew {
  8. continue
  9. }
  1. var targ types.Type
  2. {
  3. arg := call.Args[0]
  4. tvarg := info.Types[arg]
  1. if tvarg.Value != nil {
  2. info2 := &types.Info{
  3. Types: make(map[ast.Expr]types.TypeAndValue),
  4. }
  5. if err := types.CheckExpr(
  6. token.NewFileSet(), pass.Pkg,
  7. token.NoPos, arg, info2); err != nil {
  8. continue
  9. }
  10. tvarg = info2.Types[arg]
  11. }
  1. targ = types.Default(tvarg.Type)
  2. }
  1. if !types.Identical(
  2. types.NewPointer(targ),
  3. info.TypeOf(call),
  4. ) {
  5. continue
  6. }
  1. pass.Report(analysis.Diagnostic{
  2. Pos: call.Pos(),
  3. End: call.End(),
  4. Message: fmt.Sprintf(
  5. "call of %s(x) can be simplified to new(x)",
  6. fn.Name()),
  7. SuggestedFixes: []analysis.SuggestedFix{
    {
  8. Message: fmt.Sprintf(
  9. "Simplify %s(x) to new(x)", fn.Name()),
  10. TextEdits: []analysis.TextEdit{
    {
  11. Pos: call.Fun.Pos(),
  12. End: call.Fun.End(),
  13. NewText: []byte("new"),
  14. }
    }
    ,
  15. }
    }
    ,
  16. })
  1. }
  2. }
  3. return nil, nil
  4. }

覚えてほしいこと

  • リンターは1パッケージずつのASTしか見れない
  • Requirespass.ResultOf で別のリンターの ResultType を受けることができる
  • pass.ExportObjectFactpass.ImportObjectFact
    事実を共有することができる

ご清聴ありがとうございました!

Dennis Metzger
Go 1.26 Release Party