Как обновить смещения файлов при смене анализируемых файлов с помощью Go singlechecker * analysis.Pass? - PullRequest
0 голосов
/ 25 февраля 2020

Я использую https://github.com/gin-gonic/gin, и я заметил, что мне нужно использовать c.Request.Context() вместо c *gin.Context в моих обработчиках, чтобы обеспечить распространение и отмену контекста.

Я написал следующий анализатор для поиска этих случаев:

package main

import (
    "bytes"
    "fmt"
    "go/ast"
    "go/printer"
    "go/token"
    "go/types"
    "io/ioutil"
    "os"

    "golang.org/x/tools/go/analysis"
    "golang.org/x/tools/go/analysis/singlechecker"
)

var Analyzer = &analysis.Analyzer{
    Name: "gincontext",
    Doc: "This analyzer detects usage of *gin.Context as context.Context function " +
        " parameter, which doesn't properly communicate cancellation downstream.",
    Run: run,
}

func main() {
    singlechecker.Main(Analyzer)
}

func run(pass *analysis.Pass) (interface{}, error) {
    for _, file := range pass.Files {
        ast.Inspect(file, func(n ast.Node) bool {
            be, ok := n.(*ast.CallExpr)
            if !ok {
                return true
            }

            if !funcHasContextContextAsFirstParam(pass, be.Fun) {
                return true
            }

            if len(be.Args) < 1 {
                return true
            }

            arg := be.Args[0]
            if !exprArgIsGinContext(pass, arg) {
                return true
            }

            // TODO: how to update file contents?

            pass.Reportf(
                be.Pos(),
                "calling function that takes context.Context with *gin.Context which might not work as expected %q",
                render(pass.Fset, be))

            return true
        })

    }

    return nil, nil
}

// exprArgIsGinContext checks if provided expression is a pointer of type *gin.Context.
func exprArgIsGinContext(pass *analysis.Pass, expr ast.Expr) bool {
    t := pass.TypesInfo.TypeOf(expr)
    if t == nil {
        return false
    }

    p, ok := t.Underlying().(*types.Pointer)
    if !ok {
        return false
    }

    named, ok := p.Elem().(*types.Named)
    if !ok {
        return false
    }

    namedObj := named.Obj()
    if namedObj.Name() != "Context" || namedObj.Pkg().Name() != "gin" || namedObj.Pkg().Path() != "github.com/gin-gonic/gin" {
        return false
    }

    return true
}

// funcHasContextContextAsFirstParam checks if provided expression is a function
// call site with context.Context as first parameter.
func funcHasContextContextAsFirstParam(pass *analysis.Pass, expr ast.Expr) bool {
    t := pass.TypesInfo.TypeOf(expr)
    if t == nil {
        return false
    }

    bt, ok := t.Underlying().(*types.Signature)
    if !ok {
        return false
    }

    params := bt.Params()

    if params.Len() < 1 {
        return false
    }

    param := params.At(0)
    named, ok := param.Type().(*types.Named)
    if !ok {
        return false
    }

    namedObj := named.Obj()
    if namedObj.Name() != "Context" || namedObj.Pkg().Name() != "context" {
        return false
    }

    return true
}

// render returns the pretty-print of the given node
func render(fset *token.FileSet, x interface{}) string {
    var buf bytes.Buffer
    if err := printer.Fprint(&buf, fset, x); err != nil {
        panic(err)
    }
    return buf.String()
}

В разделе TODO я пытаюсь обновить содержимое файла, чтобы изменить:

func goo(ctx context.Context) {
    // ...
}

// this ...
func handler(c *gin.Context) {
    fmt.Println("handler")
    foo(c, 123, "dummy")
}

func handler2(c *gin.Context) {
    fmt.Println("handler2")
    foo(c, 10, "xxx")
}

// ... to this ...
func handler(c *gin.Context) {
    fmt.Println("handler")
    foo(c.Request.Context(), 123, "dummy")
}
func handler2(c *gin.Context) {
    fmt.Println("handler2")
    foo(c.Request.Context(), 10, "xxx")
}

Мне удалось написать что-то вроде этого:

position := pass.Fset.Position(arg.Pos())
filename := position.Filename

f, err := os.OpenFile(filename, os.O_RDWR, 0600)
if err != nil {
    panic(err)
}
defer func() {
    if err := f.Close(); err != nil {
        panic(err)
    }
}()

// TODO: change +1 to length of *gin.Context variable name
if _, err := f.Seek(int64(position.Offset)+1, 0); err != nil {
    panic(err)
}

remainder, err := ioutil.ReadAll(f)
if err != nil {
    panic(err)
}

if _, err := f.Seek(int64(position.Offset)+1, 0); err != nil {
    panic(err)
}

const requestContextStr = ".Request.Context()"
if _, err := f.Write([]byte(requestContextStr)); err != nil {
    panic(err)
}

if _, err := f.Write(remainder); err != nil {
    panic(err)
}

, но это только правильно обновит первое вхождение, и будут зашифрованы следующие:

func handler(c *gin.Context) {
    fmt.Println("handler")
    // OK
    foo(c.Request.Context(), 123, "dummy")
}

func handler2(c *gin.Context) {
    // Scrambled...
    fmt.Println(.Request.Context()"handler2")
    foo(c, 10, "xxx")
}

Как разрешить анализатору знаете, что мы изменили файл и как обновить текущие смещения файла?

...