From 4c9281c6c4d67801c9b425cb4c90551ab8780222 Mon Sep 17 00:00:00 2001 From: xhd2015 Date: Sun, 21 Jul 2024 18:39:46 +0800 Subject: [PATCH] add test for go1.23rc1 --- .github/workflows/go-next.yml | 32 ++ cmd/xgo/go_build.go | 10 + cmd/xgo/main.go | 4 +- cmd/xgo/patch.go | 2 +- cmd/xgo/patch/runtime_def.go | 7 +- cmd/xgo/patch_compiler.go | 46 +-- cmd/xgo/patch_compiler_ast_type_check.go | 6 +- cmd/xgo/patch_runtime.go | 71 ++++- cmd/xgo/version.go | 4 +- patch/adapter_go1.22.go | 123 -------- patch/adapter_go1.22_23.go | 127 ++++++++ patch/adapter_go1.23.go | 7 + patch/trap.go | 4 +- support/assert/diff.go | 124 ++++++++ support/assert/err.go | 44 +++ support/goinfo/goinfo.go | 12 +- support/transform/astdiff/astdiff.go | 195 ++++++++++++ support/transform/astdiff/astdiff_test.go | 71 +++++ support/transform/astdiff/expr.go | 126 ++++++++ support/transform/astdiff/expr_go1.17.go | 19 ++ support/transform/astdiff/expr_go1.18.go | 22 ++ support/transform/astdiff/spec.go | 39 +++ support/transform/astdiff/stmt.go | 49 +++ support/transform/astgen/astgen.go | 8 + support/transform/edit/line/line.go | 91 ++++++ support/transform/patch/README.md | 44 +++ support/transform/patch/parse.go | 169 ++++++++++ support/transform/patch/patch.go | 292 ++++++++++++++++++ support/transform/patch/patch_test.go | 73 +++++ .../patch/testdata/const_decl/expected.go | 15 + .../patch/testdata/const_decl/original.go | 9 + .../testdata/const_decl/original.go.patch | 7 + .../patch/testdata/hello_world/expected.go | 8 + .../patch/testdata/hello_world/original.go | 7 + .../testdata/hello_world/original.go.patch | 6 + .../patch/testdata/import/expected.go | 9 + .../patch/testdata/import/original.go | 7 + .../patch/testdata/import/original.go.patch | 4 + .../patch/testdata/prepend_func/expected.go | 9 + .../patch/testdata/prepend_func/original.go | 7 + .../testdata/prepend_func/original.go.patch | 6 + .../patch/testdata/replace/expected.go | 12 + .../patch/testdata/replace/original.go | 12 + .../patch/testdata/replace/original.go.patch | 6 + 44 files changed, 1775 insertions(+), 170 deletions(-) create mode 100644 .github/workflows/go-next.yml create mode 100644 patch/adapter_go1.22_23.go create mode 100644 patch/adapter_go1.23.go create mode 100644 support/assert/diff.go create mode 100644 support/assert/err.go create mode 100644 support/transform/astdiff/astdiff.go create mode 100644 support/transform/astdiff/astdiff_test.go create mode 100644 support/transform/astdiff/expr.go create mode 100644 support/transform/astdiff/expr_go1.17.go create mode 100644 support/transform/astdiff/expr_go1.18.go create mode 100644 support/transform/astdiff/spec.go create mode 100644 support/transform/astdiff/stmt.go create mode 100644 support/transform/astgen/astgen.go create mode 100644 support/transform/edit/line/line.go create mode 100644 support/transform/patch/README.md create mode 100644 support/transform/patch/parse.go create mode 100644 support/transform/patch/patch.go create mode 100644 support/transform/patch/patch_test.go create mode 100644 support/transform/patch/testdata/const_decl/expected.go create mode 100644 support/transform/patch/testdata/const_decl/original.go create mode 100644 support/transform/patch/testdata/const_decl/original.go.patch create mode 100644 support/transform/patch/testdata/hello_world/expected.go create mode 100644 support/transform/patch/testdata/hello_world/original.go create mode 100644 support/transform/patch/testdata/hello_world/original.go.patch create mode 100644 support/transform/patch/testdata/import/expected.go create mode 100644 support/transform/patch/testdata/import/original.go create mode 100644 support/transform/patch/testdata/import/original.go.patch create mode 100644 support/transform/patch/testdata/prepend_func/expected.go create mode 100644 support/transform/patch/testdata/prepend_func/original.go create mode 100644 support/transform/patch/testdata/prepend_func/original.go.patch create mode 100644 support/transform/patch/testdata/replace/expected.go create mode 100644 support/transform/patch/testdata/replace/original.go create mode 100644 support/transform/patch/testdata/replace/original.go.patch diff --git a/.github/workflows/go-next.yml b/.github/workflows/go-next.yml new file mode 100644 index 00000000..31742e78 --- /dev/null +++ b/.github/workflows/go-next.yml @@ -0,0 +1,32 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go Next + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + + tests-with-go-next: + strategy: + matrix: + os: [ ubuntu-latest] + go: [ '1.23rc1' ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + run: | + curl -fsSL -o go.tar.gz "https://go.dev/dl/go${{matrix.go}}.linux-amd64.tar.gz" + mkdir setup + tar -C setup -xzf go.tar.gz + ls setup + GOROOT=$PWD/setup/go PATH=$PWD/setup/go/bin:$PATH go version + + - name: Test + run: GOROOT=$PWD/setup/go PATH=$PWD/setup/go/bin:$PATH go run ./script/run-test --reset-instrument --debug -v \ No newline at end of file diff --git a/cmd/xgo/go_build.go b/cmd/xgo/go_build.go index 7589c952..0805fd64 100644 --- a/cmd/xgo/go_build.go +++ b/cmd/xgo/go_build.go @@ -6,6 +6,16 @@ import ( "path/filepath" ) +const GO_MAJOR_1 = 1 +const GO_VERSION_16 = 16 // unspported +const GO_VERSION_17 = 17 +const GO_VERSION_18 = 18 +const GO_VERSION_19 = 19 +const GO_VERSION_20 = 20 +const GO_VERSION_21 = 21 +const GO_VERSION_22 = 22 +const GO_VERSION_23 = 23 + func buildCompiler(goroot string, output string) error { args := []string{"build"} if isDevelopment { diff --git a/cmd/xgo/main.go b/cmd/xgo/main.go index 490959bc..e4f806be 100644 --- a/cmd/xgo/main.go +++ b/cmd/xgo/main.go @@ -711,8 +711,8 @@ func checkGoVersion(goroot string, noInstrument bool) (*goinfo.GoVersion, error) } if !noInstrument { minor := goVersion.Minor - if goVersion.Major != 1 || (minor < 17 || minor > 22) { - return nil, fmt.Errorf("only supports go1.17.0 ~ go1.22.1, current: %s", goVersionStr) + if goVersion.Major != 1 || (minor < 17 || minor > 23) { + return nil, fmt.Errorf("only supports go1.17 ~ go1.23, current: %s", goVersionStr) } } return goVersion, nil diff --git a/cmd/xgo/patch.go b/cmd/xgo/patch.go index 64cdb772..2169427e 100644 --- a/cmd/xgo/patch.go +++ b/cmd/xgo/patch.go @@ -53,7 +53,7 @@ func patchRuntimeAndCompiler(origGoroot string, goroot string, xgoSrc string, go } // runtime - err := patchRuntimeAndTesting(goroot, goVersion) + err := patchRuntimeAndTesting(origGoroot, goroot, goVersion) if err != nil { return err } diff --git a/cmd/xgo/patch/runtime_def.go b/cmd/xgo/patch/runtime_def.go index 6d8f6db4..a1470090 100644 --- a/cmd/xgo/patch/runtime_def.go +++ b/cmd/xgo/patch/runtime_def.go @@ -44,8 +44,10 @@ func __xgo_get_pc_name_impl(pc uintptr) string { ` // start with go1.21, the runtime.FuncForPC(pc).Name() -// was wrapped in funcNameForPrint(...), we unwrap it -// NOTE: when upgrading to go1.23, should check +// is wrapped in funcNameForPrint(...), we unwrap it. +// it is confirmed that in go1.21,go1.22 and go1.23, +// the name is wrapped. +// NOTE: when upgrading to go1.24, should check // the implementation again const RuntimeGetFuncName_Go121 = ` func __xgo_get_pc_name_impl(pc uintptr) string { @@ -200,6 +202,7 @@ if os.Getenv("XGO_COMPILER_ENABLE")=="true" { } ` +// only missing in go1.21 and below const NodesGen = ` func (n *node) SetPos(p Pos) { n.pos = p diff --git a/cmd/xgo/patch_compiler.go b/cmd/xgo/patch_compiler.go index 08b10a6c..e76a34f5 100644 --- a/cmd/xgo/patch_compiler.go +++ b/cmd/xgo/patch_compiler.go @@ -90,7 +90,7 @@ func patchCompilerInternal(goroot string, goVersion *goinfo.GoVersion) error { if err != nil { return fmt.Errorf("patching noder: %w", err) } - if goVersion.Major == 1 && (goVersion.Minor == 18 || goVersion.Minor == 19) { + if goVersion.Major == GO_MAJOR_1 && (goVersion.Minor == GO_VERSION_18 || goVersion.Minor == GO_VERSION_19) { err := poatchIRGenericGen(goroot, goVersion) if err != nil { return fmt.Errorf("patching generic trap: %w", err) @@ -120,16 +120,16 @@ func getInternalPatch(goroot string, subDirs ...string) string { } func patchSyntaxNode(goroot string, goVersion *goinfo.GoVersion) error { - if goVersion.Major > 1 || goVersion.Minor >= 22 { + if goVersion.Major > 1 || goVersion.Minor >= GO_VERSION_22 { return nil } var fragments []string if goVersion.Major == 1 { - if goVersion.Minor < 22 { + if goVersion.Minor <= GO_VERSION_21 { fragments = append(fragments, patch.NodesGen) } - if goVersion.Minor <= 17 { + if goVersion.Minor <= GO_VERSION_17 { fragments = append(fragments, patch.Nodes_Inspect_117) } } @@ -142,14 +142,15 @@ func patchSyntaxNode(goroot string, goVersion *goinfo.GoVersion) error { func patchGcMain(goroot string, goVersion *goinfo.GoVersion) error { file := filepath.Join(goroot, filepath.Join(gcMain...)) - go116AndUnder := goVersion.Major == 1 && goVersion.Minor <= 16 - go117 := goVersion.Major == 1 && goVersion.Minor == 17 - go118 := goVersion.Major == 1 && goVersion.Minor == 18 - go119 := goVersion.Major == 1 && goVersion.Minor == 19 - go119AndUnder := goVersion.Major == 1 && goVersion.Minor <= 19 - go120 := goVersion.Major == 1 && goVersion.Minor == 20 - go121 := goVersion.Major == 1 && goVersion.Minor == 21 - go122 := goVersion.Major == 1 && goVersion.Minor == 22 + go116AndUnder := goVersion.Major == 1 && goVersion.Minor <= GO_VERSION_16 + go117 := goVersion.Major == 1 && goVersion.Minor == GO_VERSION_17 + go118 := goVersion.Major == 1 && goVersion.Minor == GO_VERSION_18 + go119 := goVersion.Major == 1 && goVersion.Minor == GO_VERSION_19 + go119AndUnder := goVersion.Major == 1 && goVersion.Minor <= GO_VERSION_19 + go120 := goVersion.Major == GO_MAJOR_1 && goVersion.Minor == GO_VERSION_20 + go121 := goVersion.Major == GO_MAJOR_1 && goVersion.Minor == GO_VERSION_21 + go122 := goVersion.Major == GO_MAJOR_1 && goVersion.Minor == GO_VERSION_22 + go123 := goVersion.Major == GO_MAJOR_1 && goVersion.Minor == GO_VERSION_23 return editFile(file, func(content string) (string, error) { imports := []string{ @@ -247,7 +248,7 @@ func patchGcMain(goroot string, goVersion *goinfo.GoVersion) error { }else{`+flagNSwitch+` } `) - } else if go122 { + } else if go122 || go123 { // go1.22 also does not respect rewritten content when inlined // NOTE: the override of LowerL is inserted after xgo_patch.Patch() content = addContentAfter(content, @@ -269,22 +270,25 @@ func patchGcMain(goroot string, goVersion *goinfo.GoVersion) error { func patchCompilerNoder(goroot string, goVersion *goinfo.GoVersion) error { files := []string(noderFile) var noderFiles string - if goVersion.Major == 1 { + if goVersion.Major == GO_MAJOR_1 { minor := goVersion.Minor - if minor == 16 { + if minor == GO_VERSION_16 { files = []string(noderFile16) noderFiles = patch.NoderFiles_1_17 - } else if minor == 17 { + } else if minor == GO_VERSION_17 { noderFiles = patch.NoderFiles_1_17 - } else if minor == 18 { + } else if minor == GO_VERSION_18 { noderFiles = patch.NoderFiles_1_17 - } else if minor == 19 { + } else if minor == GO_VERSION_19 { noderFiles = patch.NoderFiles_1_17 - } else if minor == 20 { + } else if minor == GO_VERSION_20 { noderFiles = patch.NoderFiles_1_20 - } else if minor == 21 { + } else if minor == GO_VERSION_21 { + noderFiles = patch.NoderFiles_1_21 + } else if minor == GO_VERSION_22 { noderFiles = patch.NoderFiles_1_21 - } else if minor == 22 { + } else if minor == GO_VERSION_23 { + // TODO: verify noderFiles = patch.NoderFiles_1_21 } } diff --git a/cmd/xgo/patch_compiler_ast_type_check.go b/cmd/xgo/patch_compiler_ast_type_check.go index e8660564..8873f866 100644 --- a/cmd/xgo/patch_compiler_ast_type_check.go +++ b/cmd/xgo/patch_compiler_ast_type_check.go @@ -322,7 +322,7 @@ var syntaxPrinterPatch = &FilePatch{ p.printRawNode(n.X) `, CheckGoVersion: func(goVersion *goinfo.GoVersion) bool { - return goVersion.Major == 1 && goVersion.Minor <= 22 + return goVersion.Major == GO_MAJOR_1 && goVersion.Minor <= GO_VERSION_23 }, }, }, @@ -383,13 +383,13 @@ func patchCompilerForConstTrap(goroot string, goVersion *goinfo.GoVersion) error if err != nil { return err } - if goVersion.Major == 1 && goVersion.Minor <= 21 { + if goVersion.Major == GO_MAJOR_1 && goVersion.Minor <= GO_VERSION_21 { err = noderExprPatch.Apply(goroot, goVersion) if err != nil { return err } } - if goVersion.Major == 1 && goVersion.Minor <= 22 { + if goVersion.Major == GO_MAJOR_1 && goVersion.Minor <= GO_VERSION_23 { err = syntaxPrinterPatch.Apply(goroot, goVersion) if err != nil { return err diff --git a/cmd/xgo/patch_runtime.go b/cmd/xgo/patch_runtime.go index 5e1b2322..aa271fa9 100644 --- a/cmd/xgo/patch_runtime.go +++ b/cmd/xgo/patch_runtime.go @@ -9,7 +9,9 @@ import ( "github.com/xhd2015/xgo/cmd/xgo/patch" "github.com/xhd2015/xgo/support/filecopy" + "github.com/xhd2015/xgo/support/fileutil" "github.com/xhd2015/xgo/support/goinfo" + ast_patch "github.com/xhd2015/xgo/support/transform/patch" ) var xgoAutoGenRegisterFuncHelper = _FilePath{"src", "runtime", "__xgo_autogen_register_func_helper.go"} @@ -57,12 +59,12 @@ var runtimeFiles = []_FilePath{ timeSleep, } -func patchRuntimeAndTesting(goroot string, goVersion *goinfo.GoVersion) error { +func patchRuntimeAndTesting(origGoroot string, goroot string, goVersion *goinfo.GoVersion) error { err := patchRuntimeProc(goroot, goVersion) if err != nil { return err } - err = patchRuntimeTesting(goroot) + err = patchRuntimeTesting(origGoroot, goroot, goVersion) if err != nil { return err } @@ -114,8 +116,8 @@ func addRuntimeFunctions(goroot string, goVersion *goinfo.GoVersion, xgoSrc stri } // func name patch - if goVersion.Major > 1 || goVersion.Minor > 22 { - panic("should check the implementation of runtime.FuncForPC(pc).Name() to ensure __xgo_get_pc_name is not wrapped in print format above go1.22") + if goVersion.Major > GO_MAJOR_1 || goVersion.Minor > GO_VERSION_23 { + panic("should check the implementation of runtime.FuncForPC(pc).Name() to ensure __xgo_get_pc_name is not wrapped in print format above go1.23,it is confirmed that in go1.21,go1.22 and go1.23 the name is wrapped in funcNameForPrint(...).") } if goVersion.Major > 1 || goVersion.Minor >= 21 { content = append(content, []byte(patch.RuntimeGetFuncName_Go121)...) @@ -149,14 +151,19 @@ func patchRuntimeProc(goroot string, goVersion *goinfo.GoVersion) error { ) procDecl := `func newproc(fn` - newProc := `newg := newproc1(fn, gp, pc)` - if goVersion.Major == 1 && goVersion.Minor <= 17 { - // to avoid typo check - const size = "s" + "i" + "z" - procDecl = `func newproc(` + size + ` int32` - newProc = `newg := newproc1(fn, argp, ` + size + `, gp, pc)` + newProc := `newg := newproc1(fn, gp, pc, false, waitReasonZero)` + if goVersion.Major == GO_MAJOR_1 { + if goVersion.Minor <= GO_VERSION_17 { + // to avoid typo check + const size = "s" + "i" + "z" + procDecl = `func newproc(` + size + ` int32` + newProc = `newg := newproc1(fn, argp, ` + size + `, gp, pc)` + } else if goVersion.Minor <= GO_VERSION_22 { + newProc = `newg := newproc1(fn, gp, pc)` + } else if goVersion.Minor <= GO_VERSION_23 { + newProc = `newg := newproc1(fn, gp, pc, false, waitReasonZero)` + } } - // see https://github.com/xhd2015/xgo/issues/67 content = addContentAtIndex( content, @@ -206,8 +213,46 @@ func patchRuntimeProc(goroot string, goVersion *goinfo.GoVersion) error { return nil } -func patchRuntimeTesting(goroot string) error { - return testingFilePatch.Apply(goroot, nil) +func patchRuntimeTesting(origGoroot string, goroot string, goVersion *goinfo.GoVersion) error { + if goVersion.Major == GO_MAJOR_1 && goVersion.Minor <= GO_VERSION_22 { + return testingFilePatch.Apply(goroot, nil) + } + // go 1.23 + srcFile := testingFilePatch.FilePath.Join(origGoroot) + srcCode, err := fileutil.ReadFile(srcFile) + if err != nil { + return err + } + newCode, err := ast_patch.Patch(string(srcCode), `package testing +//prepend `+toInsertCode(patch.TestingCallbackDeclarations+patch.TestingEndCallbackDeclarations)+` +func tRunner(t *T, fn func(t *T)) { + //... + t.start = highPrecisionTimeNow() + + //prepend `+toInsertCode(patch.TestingStart+patch.TestingEnd)+` + fn(t) +} + `) + if err != nil { + return err + } + err = fileutil.WriteFile(testingFilePatch.FilePath.Join(goroot), []byte(newCode)) + if err != nil { + return err + } + return nil +} + +func toInsertCode(code string) string { + lines := strings.Split(code, "\n") + for i, line := range lines { + if i == 0 { + lines[i] = " " + line + } else { + lines[i] = "// " + line + } + } + return strings.Join(lines, "\n") } // only required if need to mock time.Sleep diff --git a/cmd/xgo/version.go b/cmd/xgo/version.go index 2eaae839..f3140cb0 100644 --- a/cmd/xgo/version.go +++ b/cmd/xgo/version.go @@ -4,8 +4,8 @@ import "fmt" // auto updated const VERSION = "1.0.46" -const REVISION = "b2c1918871f0a8bc3ea4bb9cf46e297d21c5f433+1" -const NUMBER = 295 +const REVISION = "644da2d2357b7ad0c9fb4b72995bf0b526fc4cf4+1" +const NUMBER = 296 // manually updated const CORE_VERSION = "1.0.43" diff --git a/patch/adapter_go1.22.go b/patch/adapter_go1.22.go index 2fba1a6f..0e356348 100644 --- a/patch/adapter_go1.22.go +++ b/patch/adapter_go1.22.go @@ -3,128 +3,5 @@ package patch -import ( - "go/constant" - - "cmd/compile/internal/base" - "cmd/compile/internal/ir" - "cmd/compile/internal/reflectdata" - "cmd/compile/internal/typecheck" - "cmd/compile/internal/types" - "cmd/internal/src" -) - const goMajor = 1 const goMinor = 22 - -const genericTrapNeedsWorkaround = true -const closureMayBeEliminatedDueToIfConst = false - -func forEachFunc(callback func(fn *ir.Func) bool) { - for _, fn := range typecheck.Target.Funcs { - if !callback(fn) { - return - } - } -} - -// go 1.20 does not require type -func NewNilExpr(pos src.XPos, t *types.Type) *ir.NilExpr { - return ir.NewNilExpr(pos, t) -} - -func AddFuncs(fn *ir.Func) { - typecheck.Target.Funcs = append(typecheck.Target.Funcs, fn) -} - -func NewFunc(fpos, npos src.XPos, sym *types.Sym, typ *types.Type) *ir.Func { - return ir.NewFunc(fpos, npos, sym, typ) -} - -func NewSignature(pkg *types.Pkg, recv *types.Field, tparams, params, results []*types.Field) *types.Type { - return types.NewSignature(recv, params, results) -} - -func NewBasicLit(pos src.XPos, t *types.Type, val constant.Value) ir.Node { - return ir.NewBasicLit(pos, t, val) -} - -// NOTE: []*types.Field instead of *types.Type(go1.21) -func takeAddrs(fn *ir.Func, t []*types.Field, nameOnly bool) ir.Expr { - if len(t) == 0 { - return NewNilExpr(fn.Pos(), intfSlice) - } - paramList := make([]ir.Node, len(t)) - i := 0 - ForEachField(t, func(field *types.Field) bool { - paramList[i] = takeAddr(fn, field, nameOnly) - i++ - return true - }) - return ir.NewCompLitExpr(fn.Pos(), ir.OCOMPLIT, intfSlice, paramList) -} - -// NOTE: []*types.Field instead of *types.Type(go1.21) -func getFieldNames(pos src.XPos, fn *ir.Func, t []*types.Field) ir.Expr { - if len(t) == 0 { - return NewNilExpr(pos, strSlice) - } - paramList := make([]ir.Node, len(t)) - i := 0 - ForEachField(t, func(field *types.Field) bool { - fieldName := getFieldName(fn, field) - paramList[i] = NewStringLit(pos, fieldName) - i++ - return true - }) - return ir.NewCompLitExpr(pos, ir.OCOMPLIT, strSlice, paramList) -} - -func getTypeNames(params []*types.Field) []ir.Node { - paramNames := make([]ir.Node, 0, len(params)) - for _, p := range params { - paramNames = append(paramNames, p.Nname.(*ir.Name)) - } - return paramNames -} - -func ForEachField(params []*types.Field, callback func(field *types.Field) bool) { - n := len(params) - for i := 0; i < n; i++ { - if !callback(params[i]) { - return - } - } -} - -func GetFieldIndex(fields []*types.Field, i int) *types.Field { - return fields[i] -} - -func getCallee(callNode *ir.CallExpr) ir.Node { - return callNode.Fun -} - -// TODO: maybe go1.22 does not need this -func SetConvTypeWordPtr(conv *ir.ConvExpr, t *types.Type) { - conv.TypeWord = reflectdata.TypePtrAt(base.Pos, types.NewPtr(t)) -} -func SetConvTypeWord(conv *ir.ConvExpr, t *types.Type) { - conv.TypeWord = reflectdata.TypePtrAt(base.Pos, t) -} - -func getFuncResultsType(funcType *types.Type) *types.Type { - panic("getFuncResultsType should not be called above go1.19") -} - -func canInsertTrap(fn *ir.Func) bool { - return true -} - -func NewNameAt(pos src.XPos, sym *types.Sym, typ *types.Type) *ir.Name { - return ir.NewNameAt(pos, sym, typ) -} - -func isClosureWrapperForGeneric(fn *ir.Func) bool { - return false -} diff --git a/patch/adapter_go1.22_23.go b/patch/adapter_go1.22_23.go new file mode 100644 index 00000000..d7d5fc7b --- /dev/null +++ b/patch/adapter_go1.22_23.go @@ -0,0 +1,127 @@ +//go:build go1.22 && !go1.24 +// +build go1.22,!go1.24 + +package patch + +import ( + "go/constant" + + "cmd/compile/internal/base" + "cmd/compile/internal/ir" + "cmd/compile/internal/reflectdata" + "cmd/compile/internal/typecheck" + "cmd/compile/internal/types" + "cmd/internal/src" +) + +const genericTrapNeedsWorkaround = true +const closureMayBeEliminatedDueToIfConst = false + +func forEachFunc(callback func(fn *ir.Func) bool) { + for _, fn := range typecheck.Target.Funcs { + if !callback(fn) { + return + } + } +} + +// go 1.20 does not require type +func NewNilExpr(pos src.XPos, t *types.Type) *ir.NilExpr { + return ir.NewNilExpr(pos, t) +} + +func AddFuncs(fn *ir.Func) { + typecheck.Target.Funcs = append(typecheck.Target.Funcs, fn) +} + +func NewFunc(fpos, npos src.XPos, sym *types.Sym, typ *types.Type) *ir.Func { + return ir.NewFunc(fpos, npos, sym, typ) +} + +func NewSignature(pkg *types.Pkg, recv *types.Field, tparams, params, results []*types.Field) *types.Type { + return types.NewSignature(recv, params, results) +} + +func NewBasicLit(pos src.XPos, t *types.Type, val constant.Value) ir.Node { + return ir.NewBasicLit(pos, t, val) +} + +// NOTE: []*types.Field instead of *types.Type(go1.21) +func takeAddrs(fn *ir.Func, t []*types.Field, nameOnly bool) ir.Expr { + if len(t) == 0 { + return NewNilExpr(fn.Pos(), intfSlice) + } + paramList := make([]ir.Node, len(t)) + i := 0 + ForEachField(t, func(field *types.Field) bool { + paramList[i] = takeAddr(fn, field, nameOnly) + i++ + return true + }) + return ir.NewCompLitExpr(fn.Pos(), ir.OCOMPLIT, intfSlice, paramList) +} + +// NOTE: []*types.Field instead of *types.Type(go1.21) +func getFieldNames(pos src.XPos, fn *ir.Func, t []*types.Field) ir.Expr { + if len(t) == 0 { + return NewNilExpr(pos, strSlice) + } + paramList := make([]ir.Node, len(t)) + i := 0 + ForEachField(t, func(field *types.Field) bool { + fieldName := getFieldName(fn, field) + paramList[i] = NewStringLit(pos, fieldName) + i++ + return true + }) + return ir.NewCompLitExpr(pos, ir.OCOMPLIT, strSlice, paramList) +} + +func getTypeNames(params []*types.Field) []ir.Node { + paramNames := make([]ir.Node, 0, len(params)) + for _, p := range params { + paramNames = append(paramNames, p.Nname.(*ir.Name)) + } + return paramNames +} + +func ForEachField(params []*types.Field, callback func(field *types.Field) bool) { + n := len(params) + for i := 0; i < n; i++ { + if !callback(params[i]) { + return + } + } +} + +func GetFieldIndex(fields []*types.Field, i int) *types.Field { + return fields[i] +} + +func getCallee(callNode *ir.CallExpr) ir.Node { + return callNode.Fun +} + +// TODO: maybe go1.22 does not need this +func SetConvTypeWordPtr(conv *ir.ConvExpr, t *types.Type) { + conv.TypeWord = reflectdata.TypePtrAt(base.Pos, types.NewPtr(t)) +} +func SetConvTypeWord(conv *ir.ConvExpr, t *types.Type) { + conv.TypeWord = reflectdata.TypePtrAt(base.Pos, t) +} + +func getFuncResultsType(funcType *types.Type) *types.Type { + panic("getFuncResultsType should not be called above go1.19") +} + +func canInsertTrap(fn *ir.Func) bool { + return true +} + +func NewNameAt(pos src.XPos, sym *types.Sym, typ *types.Type) *ir.Name { + return ir.NewNameAt(pos, sym, typ) +} + +func isClosureWrapperForGeneric(fn *ir.Func) bool { + return false +} diff --git a/patch/adapter_go1.23.go b/patch/adapter_go1.23.go new file mode 100644 index 00000000..7f365a6c --- /dev/null +++ b/patch/adapter_go1.23.go @@ -0,0 +1,7 @@ +//go:build go1.23 && !go1.24 +// +build go1.23,!go1.24 + +package patch + +const goMajor = 1 +const goMinor = 23 diff --git a/patch/trap.go b/patch/trap.go index 19b2817a..092c0fda 100644 --- a/patch/trap.go +++ b/patch/trap.go @@ -26,8 +26,8 @@ func init() { panic(fmt.Errorf("expect goMajor to be 1, actual:%d", goMajor)) } - if goMinor < 17 || goMinor > 22 { - panic(fmt.Errorf("expect goMinor 17~22, actual:%d", goMinor)) + if goMinor < 17 || goMinor > 23 { + panic(fmt.Errorf("expect goMinor 17~23, actual:%d", goMinor)) } } diff --git a/support/assert/diff.go b/support/assert/diff.go new file mode 100644 index 00000000..0b4b7fb8 --- /dev/null +++ b/support/assert/diff.go @@ -0,0 +1,124 @@ +package assert + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "reflect" + + "github.com/xhd2015/xgo/support/cmd" + "github.com/xhd2015/xgo/support/fileutil" +) + +// Diff compares two values, resulting in a human readable diff format. +func Diff(expected interface{}, actual interface{}) string { + if res, ok := tryDiffError(expected, actual); ok { + return res + } + // check if any is error type + expectedTxt, err := toDiffableText(expected) + if err != nil { + return err.Error() + } + actualTxt, err := toDiffableText(actual) + if err != nil { + return err.Error() + } + diff, err := diffText([]byte(expectedTxt), []byte(actualTxt)) + if err != nil { + sep := "" + if diff != "" { + sep = ", " + } + diff += sep + "err: " + err.Error() + } + return diff +} + +func toDiffableText(v interface{}) (string, error) { + if v == nil { + return "nil", nil + } + if v, ok := toPlainString(v); ok { + return tryPrettyJSONText(v) + } + text, err := prettyObj(v) + if err != nil { + return "", fmt.Errorf("marshal %T: %v", v, err) + } + return string(text), nil +} + +func prettyObj(v interface{}) ([]byte, error) { + return json.MarshalIndent(v, "", " ") +} + +func toPlainString(v interface{}) (string, bool) { + switch v := v.(type) { + case string: + return v, true + case []byte: + return string(v), true + case json.RawMessage: + return string(v), true + } + + // slow + rv := reflect.ValueOf(v) + switch rv.Kind() { + case reflect.String: + return rv.String(), true + case reflect.Slice: + if rv.Elem().Kind() == reflect.Uint8 { + // []byte + return string(rv.Bytes()), true + } + } + return "", false +} + +func tryPrettyJSONText(s string) (string, error) { + if s == "" || s == "null" { + return s, nil + } + var v interface{} + err := json.Unmarshal([]byte(s), &v) + if err != nil { + return s, nil + } + text, err := json.MarshalIndent(v, "", " ") + if err != nil { + return "", err + } + return string(text), nil +} + +func diffText(expected []byte, actual []byte) (string, error) { + tmpDir, err := os.MkdirTemp("", "diff") + if err != nil { + return "", err + } + defer os.RemoveAll(tmpDir) + + // var jsonExpected []byte + // var jsonActual []byte + + err = fileutil.WriteFile(filepath.Join(tmpDir, "expected"), expected) + if err != nil { + return "", err + } + err = fileutil.WriteFile(filepath.Join(tmpDir, "actual"), actual) + if err != nil { + return "", err + } + + diff, err := cmd.Dir(tmpDir).Output("git", "diff", "--no-index", "--no-color", "--", "expected", "actual") + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() != 0 { + err = nil + } + } + return diff, err +} diff --git a/support/assert/err.go b/support/assert/err.go new file mode 100644 index 00000000..9e20fb36 --- /dev/null +++ b/support/assert/err.go @@ -0,0 +1,44 @@ +package assert + +import "fmt" + +func tryDiffError(expected interface{}, actual interface{}) (string, bool) { + expectedErr, expectedOK := expected.(error) + actualErr, actualOK := actual.(error) + if expectedOK != actualOK { + if expectedOK { + return fmt.Sprintf("expect: error %s, actual: %T %s", expectedErr.Error(), actual, actual), true + } + return fmt.Sprintf("expect: %T %s, actual: error %s", expectedErr, expectedErr, actualErr.Error()), true + } + if !expectedOK { + return "", false + } + res := diffError(expectedErr, actualErr) + if res == nil { + return "", true + } + return res.Error(), true +} + +func diffError(expected error, actual error) error { + if expected == actual { + return nil + } + if expected == nil { + if actual != nil { + return fmt.Errorf("expect no error, actual: %v", actual) + } + return nil + } + if actual == nil { + return fmt.Errorf("expect error: %v, actual nil", expected) + } + + e := expected.Error() + a := actual.Error() + if e != a { + return fmt.Errorf("expect err: %v, actual: %v", e, a) + } + return nil +} diff --git a/support/goinfo/goinfo.go b/support/goinfo/goinfo.go index ad068bdb..8e89463c 100644 --- a/support/goinfo/goinfo.go +++ b/support/goinfo/goinfo.go @@ -66,7 +66,17 @@ func ParseGoVersionNumber(version string) (*GoVersion, error) { verList := strings.Split(version, ".") for i := 0; i < 3; i++ { if i < len(verList) { - verInt, err := strconv.ParseInt(verList[i], 10, 64) + num := verList[i] + if i == 1 { + // 1.23rc1 + idx := strings.IndexFunc(num, func(r rune) bool { + return r < '0' || r > '9' + }) + if idx > 0 { + num = num[:idx] + } + } + verInt, err := strconv.ParseInt(num, 10, 64) if err != nil { return nil, fmt.Errorf("unrecognized version, expect number, found: %s", version) } diff --git a/support/transform/astdiff/astdiff.go b/support/transform/astdiff/astdiff.go new file mode 100644 index 00000000..4d36d63b --- /dev/null +++ b/support/transform/astdiff/astdiff.go @@ -0,0 +1,195 @@ +package astdiff + +import ( + "go/ast" +) + +// TODO: generate using template + +func NodeSame(a ast.Node, b ast.Node) bool { + if (a == nil) != (b == nil) { + return false + } + switch a := a.(type) { + case ast.Stmt: + b, ok := b.(ast.Stmt) + if !ok { + return false + } + return StmtSame(a, b) + case ast.Expr: + b, ok := b.(ast.Expr) + if !ok { + return false + } + return ExprSame(a, b) + case ast.Spec: + b, ok := b.(ast.Spec) + if !ok { + return false + } + return SpecSame(a, b) + case *ast.File: + b, ok := b.(*ast.File) + if !ok { + return false + } + return FileSame(a, b) + } + return false +} + +func FileSame(a *ast.File, b *ast.File) bool { + if (a == nil) != (b == nil) { + return false + } + if !identSame(a.Name, b.Name) { + return false + } + if !DeclsSame(a.Decls, b.Decls) { + return false + } + return true +} + +func DeclsSame(a []ast.Decl, b []ast.Decl) bool { + if (a == nil) != (b == nil) { + return false + } + if len(a) != len(b) { + return false + } + for i, d := range a { + if !DeclSame(d, b[i]) { + return false + } + } + return true +} + +func DeclSame(a ast.Decl, b ast.Decl) bool { + if (a == nil) != (b == nil) { + return false + } + switch a := a.(type) { + case *ast.GenDecl: + b, ok := b.(*ast.GenDecl) + if !ok { + return false + } + if a.Tok != b.Tok { + return false + } + return SpecsSame(a.Specs, b.Specs) + case *ast.FuncDecl: + b, ok := b.(*ast.FuncDecl) + if !ok { + return false + } + return FuncDeclSame(a, b) + case *ast.BadDecl: + _, ok := b.(*ast.BadDecl) + if !ok { + return false + } + return true + } + return false +} + +func SpecsSame(a []ast.Spec, b []ast.Spec) bool { + if (a == nil) != (b == nil) { + return false + } + if len(a) != len(b) { + return false + } + for i, e := range a { + if !SpecSame(e, b[i]) { + return false + } + } + return true +} +func fieldListSame(a *ast.FieldList, b *ast.FieldList) bool { + if (a == nil) != (b == nil) { + return false + } + if a == nil { + return true + } + return fieldsSame(a.List, b.List) +} + +func fieldsSame(a []*ast.Field, b []*ast.Field) bool { + if (a == nil) != (b == nil) { + return false + } + if len(a) != len(b) { + return false + } + for i, e := range a { + if !fieldSame(e, b[i]) { + return false + } + } + return true +} + +func fieldSame(a *ast.Field, b *ast.Field) bool { + if (a == nil) != (b == nil) { + return false + } + if !identsSame(a.Names, b.Names) { + return false + } + if !ExprSame(a.Type, b.Type) { + return false + } + if !basicLitSame(a.Tag, b.Tag) { + return false + } + return true +} + +func blockStmtSame(a *ast.BlockStmt, b *ast.BlockStmt) bool { + if (a == nil) != (b == nil) { + return false + } + return StmtsSame(a.List, b.List) +} + +func basicLitSame(a *ast.BasicLit, b *ast.BasicLit) bool { + if (a == nil) != (b == nil) { + return false + } + if a == nil { + return true + } + if a.Kind != b.Kind { + return false + } + return a.Value == b.Value +} + +func identsSame(a []*ast.Ident, b []*ast.Ident) bool { + if (a == nil) != (b == nil) { + return false + } + if len(a) != len(b) { + return false + } + for i, e := range a { + if !identSame(e, b[i]) { + return false + } + } + return true +} + +func identSame(a *ast.Ident, b *ast.Ident) bool { + if (a == nil) != (b == nil) { + return false + } + return a == nil || a.Name == b.Name +} diff --git a/support/transform/astdiff/astdiff_test.go b/support/transform/astdiff/astdiff_test.go new file mode 100644 index 00000000..1f5fb28e --- /dev/null +++ b/support/transform/astdiff/astdiff_test.go @@ -0,0 +1,71 @@ +package astdiff + +import ( + "testing" + + "github.com/xhd2015/xgo/support/goparse" +) + +func TestNodeSame(t *testing.T) { + tests := []struct { + name string + a string + b string + want bool + }{ + { + name: "empty", + want: true, + }, + { + name: "func with comment", + a: `func main(){}`, + b: "func main(){ /**/}", + want: true, + }, + { + name: "func with space", + a: `func main() string {}`, + b: `func main() string { + }`, + want: true, + }, + { + name: "func signature changed", + a: `func greet(s string){}`, + b: "func greet(s string,version int) { return \"\" }", + want: false, + }, + { + name: "return statement", + a: `func greet(s string) string{ return "hello " + s }`, + b: `func greet(s string,) string{ + return "hello " + s + }`, + want: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + testNodeSame(t, tt.a, tt.b, tt.want) + }) + } +} + +func testNodeSame(t *testing.T, a string, b string, want bool) { + fileA, _, err := goparse.ParseFileCode("a.go", []byte(goparse.AddMissingPackage(a, "main"))) + if err != nil { + t.Error(err) + return + } + fileB, _, err := goparse.ParseFileCode("b.go", []byte(goparse.AddMissingPackage(b, "main"))) + if err != nil { + t.Error(err) + return + } + got := NodeSame(fileA, fileB) + if got != want { + t.Errorf("expect node same: %v, actual: %v", want, got) + } +} diff --git a/support/transform/astdiff/expr.go b/support/transform/astdiff/expr.go new file mode 100644 index 00000000..4601a94f --- /dev/null +++ b/support/transform/astdiff/expr.go @@ -0,0 +1,126 @@ +package astdiff + +import ( + "fmt" + "go/ast" + "go/token" +) + +func ExprSame(a ast.Expr, b ast.Expr) bool { + if (a == nil) != (b == nil) { + return false + } + if a == nil { + return true + } + switch a := a.(type) { + case *ast.Ident: + b, ok := b.(*ast.Ident) + if !ok { + return false + } + return a.Name == b.Name + case *ast.BasicLit: + b, ok := b.(*ast.BasicLit) + if !ok { + return false + } + return basicLitSame(a, b) + case *ast.SelectorExpr: + b, ok := b.(*ast.SelectorExpr) + if !ok { + return false + } + return identSame(a.Sel, b.Sel) && ExprSame(a.X, b.X) + case *ast.CallExpr: + b, ok := b.(*ast.CallExpr) + if !ok { + return false + } + if !ExprSame(a.Fun, b.Fun) { + return false + } + if !ExprsSame(a.Args, b.Args) { + return false + } + if (a.Ellipsis == token.NoPos) != (b.Ellipsis == token.NoPos) { + return false + } + return true + case *ast.BinaryExpr: + b, ok := b.(*ast.BinaryExpr) + if !ok { + return false + } + if a.Op != b.Op { + return false + } + return ExprSame(a.X, b.X) && ExprSame(a.Y, b.Y) + case *ast.StarExpr: + b, ok := b.(*ast.StarExpr) + if !ok { + return false + } + return ExprSame(a.X, b.X) + case *ast.FuncLit: + b, ok := b.(*ast.FuncLit) + if !ok { + return false + } + return funcTypeSame(a.Type, b.Type) && blockStmtSame(a.Body, b.Body) + case *ast.FuncType: + b, ok := b.(*ast.FuncType) + if !ok { + return false + } + return funcTypeSame(a, b) + default: + panic(fmt.Errorf("unrecognized: %T", a)) + } + return false +} + +func ExprsSame(a []ast.Expr, b []ast.Expr) bool { + if (a == nil) != (b == nil) { + return false + } + if len(a) != len(b) { + return false + } + for i, e := range a { + if !ExprSame(e, b[i]) { + return false + } + } + return true +} + +func FuncDeclSame(a *ast.FuncDecl, b *ast.FuncDecl) bool { + return funcDeclSame(a, b, false) +} + +func FuncDeclSameIgnoreBody(a *ast.FuncDecl, b *ast.FuncDecl) bool { + return funcDeclSame(a, b, true) +} + +func funcDeclSame(a *ast.FuncDecl, b *ast.FuncDecl, ignoreBody bool) bool { + if (a == nil) != (b == nil) { + return false + } + if a == nil { + return true + } + if !fieldListSame(a.Recv, b.Recv) { + return false + } + if !identSame(a.Name, b.Name) { + return false + } + if !funcTypeSame(a.Type, b.Type) { + return false + } + if !ignoreBody && !blockStmtSame(a.Body, b.Body) { + return false + } + return true +} diff --git a/support/transform/astdiff/expr_go1.17.go b/support/transform/astdiff/expr_go1.17.go new file mode 100644 index 00000000..a1b82437 --- /dev/null +++ b/support/transform/astdiff/expr_go1.17.go @@ -0,0 +1,19 @@ +//go:build !go1.18 +// +build !go1.18 + +package astdiff + +import "go/ast" + +func funcTypeSame(a *ast.FuncType, b *ast.FuncType) bool { + if (a == nil) != (b == nil) { + return false + } + if !fieldListSame(a.Params, b.Params) { + return false + } + if !fieldListSame(a.Results, b.Results) { + return false + } + return true +} diff --git a/support/transform/astdiff/expr_go1.18.go b/support/transform/astdiff/expr_go1.18.go new file mode 100644 index 00000000..f2671ade --- /dev/null +++ b/support/transform/astdiff/expr_go1.18.go @@ -0,0 +1,22 @@ +//go:build go1.18 +// +build go1.18 + +package astdiff + +import "go/ast" + +func funcTypeSame(a *ast.FuncType, b *ast.FuncType) bool { + if (a == nil) != (b == nil) { + return false + } + if !fieldListSame(a.TypeParams, b.TypeParams) { + return false + } + if !fieldListSame(a.Params, b.Params) { + return false + } + if !fieldListSame(a.Results, b.Results) { + return false + } + return true +} diff --git a/support/transform/astdiff/spec.go b/support/transform/astdiff/spec.go new file mode 100644 index 00000000..a884aaa5 --- /dev/null +++ b/support/transform/astdiff/spec.go @@ -0,0 +1,39 @@ +package astdiff + +import "go/ast" + +func SpecSame(a ast.Spec, b ast.Spec) bool { + if (a == nil) != (b == nil) { + return false + } + switch a := a.(type) { + case *ast.ImportSpec: + b, ok := b.(*ast.ImportSpec) + if !ok { + return false + } + if !identSame(a.Name, b.Name) { + return false + } + if !basicLitSame(a.Path, b.Path) { + return false + } + return true + case *ast.ValueSpec: + b, ok := b.(*ast.ValueSpec) + if !ok { + return false + } + if !identsSame(a.Names, b.Names) { + return false + } + if !ExprSame(a.Type, b.Type) { + return false + } + if !ExprsSame(a.Values, b.Values) { + return false + } + return true + } + return false +} diff --git a/support/transform/astdiff/stmt.go b/support/transform/astdiff/stmt.go new file mode 100644 index 00000000..17fed8d7 --- /dev/null +++ b/support/transform/astdiff/stmt.go @@ -0,0 +1,49 @@ +package astdiff + +import "go/ast" + +func StmtsSame(a []ast.Stmt, b []ast.Stmt) bool { + if (a == nil) != (b == nil) { + return false + } + if len(a) != len(b) { + return false + } + for i, e := range a { + if !StmtSame(e, b[i]) { + return false + } + } + return true +} + +func StmtSame(a ast.Stmt, b ast.Stmt) bool { + if (a == nil) != (b == nil) { + return false + } + switch a := a.(type) { + case *ast.ReturnStmt: + b, ok := b.(*ast.ReturnStmt) + if !ok { + return false + } + return ExprsSame(a.Results, b.Results) + case *ast.AssignStmt: + b, ok := b.(*ast.AssignStmt) + if !ok { + return false + } + + if a.Tok != b.Tok { + return false + } + return ExprsSame(a.Lhs, b.Lhs) && ExprsSame(a.Rhs, b.Rhs) + case *ast.ExprStmt: + b, ok := b.(*ast.ExprStmt) + if !ok { + return false + } + return ExprSame(a.X, b.X) + } + return false +} diff --git a/support/transform/astgen/astgen.go b/support/transform/astgen/astgen.go new file mode 100644 index 00000000..8050b2dd --- /dev/null +++ b/support/transform/astgen/astgen.go @@ -0,0 +1,8 @@ +package astgen + +type Config struct { +} + +func Gen(config *Config) { + +} diff --git a/support/transform/edit/line/line.go b/support/transform/edit/line/line.go new file mode 100644 index 00000000..51f7b23e --- /dev/null +++ b/support/transform/edit/line/line.go @@ -0,0 +1,91 @@ +package line + +import ( + "fmt" + "sort" +) + +type Edit struct { + edits []*edit +} + +type edit struct { + line int + prepend []string + append []string + replace []string +} + +func (c *Edit) Prepend(lineNum int, lines []string) { + c.edits = append(c.edits, &edit{ + line: lineNum, + prepend: lines, + }) +} + +func (c *Edit) Append(lineNum int, lines []string) { + c.edits = append(c.edits, &edit{ + line: lineNum, + append: lines, + }) +} + +func (c *Edit) Replace(lineNum int, lines []string) { + c.edits = append(c.edits, &edit{ + line: lineNum, + replace: lines, + }) +} + +func (c *Edit) Apply(lines []string) ([]string, error) { + // sort edits to apply sequentially + sort.Slice(c.edits, func(i, j int) bool { + d := c.edits[i].line - c.edits[j].line + if d == 0 { + return i < j + } + return d < 0 + }) + n := len(lines) + newLines := make([]string, 0, n) + + cursor := 1 + + m := len(c.edits) + for i := 0; i < m; i++ { + e := c.edits[i] + line := e.line + if line > n { + return nil, fmt.Errorf("bad line: %d", line) + } + if cursor < line { + newLines = append(newLines, lines[cursor-1:line-1]...) + cursor = line + } + // cursor == line + j := i + 1 + for ; j < m && c.edits[j].line == line; j++ { + } + // i -> j are same line + for k := i; k < j; k++ { + newLines = append(newLines, c.edits[k].prepend...) + } + var hasReplaced bool + for k := i; k < j; k++ { + if len(c.edits[k].replace) > 0 { + newLines = append(newLines, c.edits[k].replace...) + hasReplaced = true + } + } + if !hasReplaced { + newLines = append(newLines, lines[cursor-1]) + } + cursor++ + for k := i; k < j; k++ { + newLines = append(newLines, c.edits[k].append...) + } + i = j - 1 + } + newLines = append(newLines, lines[cursor-1:]...) + return newLines, nil +} diff --git a/support/transform/patch/README.md b/support/transform/patch/README.md new file mode 100644 index 00000000..a5cff3d0 --- /dev/null +++ b/support/transform/patch/README.md @@ -0,0 +1,44 @@ +# Syntax-aware Patch +This package provides an intuitive way to patch the go code. + +The purpose is to power xgo's patching mechanism. + +See [#169](https://github.com/xhd2015/xgo/issues/169#issuecomment-2241407305). + +# Examples +You can find more examples in the [testdata](./testdata/) directory. + +The following example is taken from [./testdata/hello_world/](./testdata/hello_world/). + +`original.go`: +```go +package main + +import "fmt" + +func main() { + fmt.Printf("hello world\n") +} +``` + +`original.go.patch`: +```go +package main + +func main(){ + //append fmt.Printf("the world is patched\n") + fmt.Printf("hello world\n") +} +``` + +`result.go`: +```go +package main + +import "fmt" + +func main() { + fmt.Printf("hello world\n") + fmt.Printf("the world is patched\n") +} +``` \ No newline at end of file diff --git a/support/transform/patch/parse.go b/support/transform/patch/parse.go new file mode 100644 index 00000000..db784815 --- /dev/null +++ b/support/transform/patch/parse.go @@ -0,0 +1,169 @@ +package patch + +import ( + "fmt" + "go/ast" + "go/token" + "strings" +) + +type PatchContent struct { + ID string + PrependLines []string + AppendLines []string + ReplaceLine []string +} + +func parsePatch(lines []string, node ast.Node, fset *token.FileSet) map[ast.Node]*PatchContent { + assignedLine := make(map[int]bool) + mapping := make(map[ast.Node]*PatchContent) + ast.Inspect(node, func(n ast.Node) bool { + if n == nil { + return true + } + switch n := n.(type) { + case *ast.Comment: + return false + case *ast.CommentGroup: + return false + case *ast.GenDecl: + if n.Lparen == token.NoPos { + // if the decl does not include a ( + // then the comment will be assigned to specific node + return true + } + } + comments := getCommentLines(lines, n, fset, assignedLine) + if len(comments) == 0 { + return true + } + p, _ := parseComments(comments) + if p != nil { + mapping[n] = p + } + return true + }) + return mapping +} + +const DOUBLE_SLASH = "//" + +func getCommentLines(lines []string, node ast.Node, fset *token.FileSet, assignedLine map[int]bool) []string { + line := fset.Position(node.Pos()).Line + + if line <= 1 || assignedLine[line-1] { + return nil + } + + var comments []string + // previous line + for i := line - 1; i > 0; i-- { + if assignedLine[i] { + continue + } + trimLine := strings.TrimSpace(lines[i-1]) + if !strings.HasPrefix(trimLine, DOUBLE_SLASH) { + comments = make([]string, 0, line-i-1) + for j := i + 1; j < line; j++ { + assignedLine[j] = true + comments = append(comments, strings.TrimSpace(lines[j-1])[len(DOUBLE_SLASH):]) + } + break + } + } + return comments +} + +type cmd struct { + command string + lines []string + id string +} + +func parseComments(comments []string) (*PatchContent, error) { + appendCmd := &cmd{ + command: "append", + } + prependCmd := &cmd{ + command: "prepend", + } + replaceCmd := &cmd{ + command: "replace", + } + cmds := []*cmd{appendCmd, prependCmd, replaceCmd} + + var lastCmd *cmd + + n := len(comments) + for i := 0; i < n; i++ { + line := comments[i] + trimmedLine := strings.TrimSpace(line) + if trimmedLine == "" || trimmedLine == "..." { + continue + } + if strings.HasPrefix(line, " ") { + // start with space + if lastCmd == nil { + return nil, fmt.Errorf("unknown text: %s", line) + } + lastCmd.lines = append(lastCmd.lines, strings.TrimSpace(line)) + continue + } + var match bool + for _, cmd := range cmds { + id, content, ok, err := tryCommand(line, cmd.command) + if err != nil { + return nil, err + } + if !ok { + continue + } + lastCmd = cmd + match = true + if cmd.id == "" && id != "" { + cmd.id = id + } + cmd.lines = append(cmd.lines, content) + break + } + if !match { + return nil, fmt.Errorf("bad instruction: %s", line) + } + } + var id string + for _, cmd := range cmds { + if id == "" && cmd.id != "" { + id = cmd.id + break + } + } + + return &PatchContent{ + ID: id, + PrependLines: prependCmd.lines, + AppendLines: appendCmd.lines, + ReplaceLine: replaceCmd.lines, + }, nil +} + +func tryCommand(line string, cmd string) (id string, content string, ok bool, err error) { + prefix := cmd + " " + if !strings.HasPrefix(line, prefix) { + return + } + ok = true + remain := strings.TrimSpace(line[len(prefix):]) + + if strings.HasPrefix(remain, "<") { + idx := strings.Index(remain[1:], ">") + if idx < 0 { + err = fmt.Errorf("missing >") + return + } + id = remain[1 : idx+1] + remain = remain[idx+2:] + } + + content = remain + return +} diff --git a/support/transform/patch/patch.go b/support/transform/patch/patch.go new file mode 100644 index 00000000..3db6e1f6 --- /dev/null +++ b/support/transform/patch/patch.go @@ -0,0 +1,292 @@ +package patch + +import ( + "fmt" + "go/ast" + "go/token" + "strings" + + "github.com/xhd2015/xgo/support/goparse" + "github.com/xhd2015/xgo/support/transform/astdiff" + "github.com/xhd2015/xgo/support/transform/edit/line" +) + +// see https://github.com/xhd2015/xgo/issues/169#issuecomment-2241407305 +func Patch(old string, patch string) (string, error) { + srcAST, srcFset, err := goparse.ParseFileCode("old.go", []byte(old)) + if err != nil { + return "", err + } + + patchCodeAST, patchFset, err := goparse.ParseFileCode("patch.go", []byte(patch)) + if err != nil { + return "", err + } + return patchAST(old, srcAST, srcFset, patch, patchCodeAST, patchFset) +} + +func PatchFile(srcFile string, patchFile string) (string, error) { + srcCode, srcAST, srcFset, err := goparse.Parse(srcFile) + if err != nil { + return "", err + } + + patchCode, patchCodeAST, patchFset, err := goparse.Parse(patchFile) + if err != nil { + return "", err + } + + return patchAST(string(srcCode), srcAST, srcFset, string(patchCode), patchCodeAST, patchFset) +} + +func patchAST(srcCode string, srcAST *ast.File, srcFset *token.FileSet, patchCode string, patchAST *ast.File, patchFset *token.FileSet) (string, error) { + patchLines := strings.Split(patchCode, "\n") + + patchMapping := parsePatch(patchLines, patchAST, patchFset) + + var srcSpecs []ast.Spec + var patchSpecs []ast.Spec + + var srcFuncDecls []*ast.FuncDecl + var patchFuncDecls []*ast.FuncDecl + + var srcImports []*ast.ImportSpec + var patchImports []*ast.ImportSpec + + patchFuncMapping := make(map[string]*ast.FuncDecl) + for _, decl := range patchAST.Decls { + switch decl := decl.(type) { + case *ast.FuncDecl: + patchFuncMapping[decl.Name.Name] = decl + patchFuncDecls = append(patchFuncDecls, decl) + case *ast.GenDecl: + for _, spec := range decl.Specs { + switch spec := spec.(type) { + case *ast.ImportSpec: + patchImports = append(patchImports, spec) + default: + patchSpecs = append(patchSpecs, spec) + } + } + default: + return "", fmt.Errorf("unhandled %T", decl) + } + } + + for _, decl := range srcAST.Decls { + switch decl := decl.(type) { + case *ast.FuncDecl: + srcFuncDecls = append(srcFuncDecls, decl) + case *ast.GenDecl: + for _, spec := range decl.Specs { + switch spec := spec.(type) { + case *ast.ImportSpec: + srcImports = append(srcImports, spec) + default: + srcSpecs = append(srcSpecs, spec) + } + } + } + } + edit := &line.Edit{} + // imports + patchNodeStrict(edit, srcFset, patchMapping, importSpecs(srcImports), importSpecs(patchImports)) + + // specs + patchNodeStrict(edit, srcFset, patchMapping, specs(srcSpecs), specs(patchSpecs)) + + // decls + patchNodes(edit, srcFset, patchMapping, funcDecls(srcFuncDecls), funcDecls(patchFuncDecls), true) + + // inside funcs + for _, decl := range srcAST.Decls { + fdecl, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + fnPatch := patchFuncMapping[fdecl.Name.Name] + if fnPatch == nil { + continue + } + + // check name and type + if !astdiff.FuncDeclSameIgnoreBody(fdecl, fnPatch) { + return "", fmt.Errorf("func not match %s: %s -> %s", fdecl.Name.Name, getText(srcCode, fdecl.Type, srcFset), getText(patchCode, fnPatch.Type, patchFset)) + } + patchFunc(edit, fdecl, srcFset, fnPatch, patchMapping) + } + + srcLines := strings.Split(srcCode, "\n") + newLines, err := edit.Apply(srcLines) + if err != nil { + return "", err + } + newCode := strings.Join(newLines, "\n") + return newCode, nil +} + +type nodeList interface { + Len() int + Index(i int) ast.Node + Slice(i int, j int) nodeList +} + +type stmtList []ast.Stmt + +func (c stmtList) Len() int { + return len(c) +} +func (c stmtList) Index(i int) ast.Node { + stmt := c[i] + if stmt == nil { + return nil + } + return stmt +} +func (c stmtList) Slice(i int, j int) nodeList { + if j == -1 { + j = len(c) + } + return stmtList(c[i:j]) +} + +type importSpecs []*ast.ImportSpec + +func (c importSpecs) Len() int { + return len(c) +} +func (c importSpecs) Index(i int) ast.Node { + spec := c[i] + if spec == nil { + return nil + } + return spec +} +func (c importSpecs) Slice(i int, j int) nodeList { + if j == -1 { + j = len(c) + } + return importSpecs(c[i:j]) +} + +type specs []ast.Spec + +func (c specs) Len() int { + return len(c) +} +func (c specs) Index(i int) ast.Node { + spec := c[i] + if spec == nil { + return nil + } + return spec +} +func (c specs) Slice(i int, j int) nodeList { + if j == -1 { + j = len(c) + } + return specs(c[i:j]) +} + +type funcDecls []*ast.FuncDecl + +func (c funcDecls) Len() int { + return len(c) +} +func (c funcDecls) Index(i int) ast.Node { + spec := c[i] + if spec == nil { + return nil + } + return spec +} +func (c funcDecls) Slice(i int, j int) nodeList { + if j == -1 { + j = len(c) + } + return funcDecls(c[i:j]) +} + +func patchFunc(edit *line.Edit, srcFunc *ast.FuncDecl, srcFset *token.FileSet, patchFunc *ast.FuncDecl, patchMapping map[ast.Node]*PatchContent) { + var srcStmts []ast.Stmt + var patchStmts []ast.Stmt + + if srcFunc.Body != nil { + srcStmts = srcFunc.Body.List + } + if patchFunc.Body != nil { + patchStmts = patchFunc.Body.List + } + + patchNodeStrict(edit, srcFset, patchMapping, stmtList(srcStmts), stmtList(patchStmts)) + +} +func patchNodeStrict(edit *line.Edit, srcFset *token.FileSet, patchMapping map[ast.Node]*PatchContent, srcNodes nodeList, patchNodeList nodeList) { + patchNodes(edit, srcFset, patchMapping, srcNodes, patchNodeList, false) +} + +func patchNodes(edit *line.Edit, srcFset *token.FileSet, patchMapping map[ast.Node]*PatchContent, srcNodes nodeList, patchNodes nodeList, funcDeclIgnoreBody bool) { + var srcIndex int + var patchIndex int + for { + if patchIndex >= patchNodes.Len() { + break + } + if srcIndex >= srcNodes.Len() { + panic(fmt.Errorf("bad patch")) + } + patchHead := patchNodes.Index(patchIndex) + m := findMatch(srcNodes.Slice(srcIndex, -1), patchHead, funcDeclIgnoreBody) + if m < 0 { + panic(fmt.Errorf("patch not found: %v", patchHead)) + } + srcNode := srcNodes.Index(srcIndex + m) + srcIndex += m + patchIndex++ + patch := patchMapping[patchHead] + if patch == nil { + continue + } + + line := srcFset.Position(srcNode.Pos()).Line + if len(patch.AppendLines) > 0 { + edit.Append(line, patch.AppendLines) + } + if len(patch.PrependLines) > 0 { + edit.Prepend(line, patch.PrependLines) + } + if len(patch.ReplaceLine) > 0 { + edit.Replace(line, patch.ReplaceLine) + } + } +} + +func getText(code string, node ast.Node, fset *token.FileSet) string { + start := fset.Position(node.Pos()).Offset + end := fset.Position(node.End()).Offset + return code[start:end] +} + +func findMatch(list nodeList, node ast.Node, funcDeclIgnoreBody bool) int { + n := list.Len() + for i := 0; i < n; i++ { + srcNode := list.Index(i) + if funcDeclIgnoreBody { + srcFuncDecl, ok := srcNode.(*ast.FuncDecl) + if ok { + patchFuncDecl, ok := node.(*ast.FuncDecl) + if !ok { + continue + } + if astdiff.FuncDeclSameIgnoreBody(srcFuncDecl, patchFuncDecl) { + return i + } + continue + } + } + if astdiff.NodeSame(srcNode, node) { + return i + } + } + return -1 +} diff --git a/support/transform/patch/patch_test.go b/support/transform/patch/patch_test.go new file mode 100644 index 00000000..e46e6db4 --- /dev/null +++ b/support/transform/patch/patch_test.go @@ -0,0 +1,73 @@ +package patch + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/xhd2015/xgo/support/assert" + "github.com/xhd2015/xgo/support/fileutil" + "github.com/xhd2015/xgo/support/goparse" + "github.com/xhd2015/xgo/support/transform/astdiff" +) + +func TestPatchFile(t *testing.T) { + tests := []struct { + dir string + }{ + // { + // dir: "./testdata/hello_world", + // }, + // { + // dir: "./testdata/replace", + // }, + // { + // dir: "./testdata/import", + // }, + // { + // dir: "./testdata/const_decl", + // }, + { + dir: "./testdata/prepend_func", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.dir, func(t *testing.T) { + testPatchDir(t, tt.dir) + }) + } +} + +func testPatchDir(t *testing.T, dir string) { + result, err := PatchFile(filepath.Join(dir, "original.go"), filepath.Join(dir, "original.go.patch")) + if err != nil { + t.Error(err) + return + } + expectedBytes, err := fileutil.ReadFile(filepath.Join(dir, "expected.go")) + if err != nil { + t.Error(err) + return + } + + resultCode, _, err := goparse.ParseFileCode("result.go", []byte(result)) + if err != nil { + t.Error(err) + return + } + expectedCode, _, err := goparse.ParseFileCode("expected.go", expectedBytes) + if err != nil { + t.Error(err) + return + } + + // assert they are syntically the same + if !astdiff.FileSame(resultCode, expectedCode) { + result = strings.TrimSuffix(result, "\n") + expected := strings.TrimSuffix(string(expectedBytes), "\n") + if diff := assert.Diff(expected, result); diff != "" { + t.Errorf("PatchFile(): %s", diff) + } + } +} diff --git a/support/transform/patch/testdata/const_decl/expected.go b/support/transform/patch/testdata/const_decl/expected.go new file mode 100644 index 00000000..b9c9706a --- /dev/null +++ b/support/transform/patch/testdata/const_decl/expected.go @@ -0,0 +1,15 @@ +package main + +import "fmt" + +/**/ +const a = 100 + +// this is just a comment +/**/ +const _ = 10 +const b = 10 + +func main() { + fmt.Println("hello world") +} diff --git a/support/transform/patch/testdata/const_decl/original.go b/support/transform/patch/testdata/const_decl/original.go new file mode 100644 index 00000000..7521968d --- /dev/null +++ b/support/transform/patch/testdata/const_decl/original.go @@ -0,0 +1,9 @@ +package main + +import "fmt" + +const _ = 10 + +func main() { + fmt.Println("hello world") +} diff --git a/support/transform/patch/testdata/const_decl/original.go.patch b/support/transform/patch/testdata/const_decl/original.go.patch new file mode 100644 index 00000000..bc49fd30 --- /dev/null +++ b/support/transform/patch/testdata/const_decl/original.go.patch @@ -0,0 +1,7 @@ +package main + +//prepend const a = 100 +// // this is just a comment +// +//append const b = 10 +const _ = 10 diff --git a/support/transform/patch/testdata/hello_world/expected.go b/support/transform/patch/testdata/hello_world/expected.go new file mode 100644 index 00000000..1c2e3882 --- /dev/null +++ b/support/transform/patch/testdata/hello_world/expected.go @@ -0,0 +1,8 @@ +package main + +import "fmt" + +func main() { + fmt.Printf("hello world\n") + fmt.Printf("the world is patched\n") +} diff --git a/support/transform/patch/testdata/hello_world/original.go b/support/transform/patch/testdata/hello_world/original.go new file mode 100644 index 00000000..a36de8e3 --- /dev/null +++ b/support/transform/patch/testdata/hello_world/original.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Printf("hello world\n") +} diff --git a/support/transform/patch/testdata/hello_world/original.go.patch b/support/transform/patch/testdata/hello_world/original.go.patch new file mode 100644 index 00000000..bb5b664e --- /dev/null +++ b/support/transform/patch/testdata/hello_world/original.go.patch @@ -0,0 +1,6 @@ +package main + +func main(){ + //append fmt.Printf("the world is patched\n") + fmt.Printf("hello world\n") +} \ No newline at end of file diff --git a/support/transform/patch/testdata/import/expected.go b/support/transform/patch/testdata/import/expected.go new file mode 100644 index 00000000..47909d9e --- /dev/null +++ b/support/transform/patch/testdata/import/expected.go @@ -0,0 +1,9 @@ +package main + +import _ "embed" +import "fmt" + + +func main() { + fmt.Println("hello world") +} diff --git a/support/transform/patch/testdata/import/original.go b/support/transform/patch/testdata/import/original.go new file mode 100644 index 00000000..c0481191 --- /dev/null +++ b/support/transform/patch/testdata/import/original.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("hello world") +} diff --git a/support/transform/patch/testdata/import/original.go.patch b/support/transform/patch/testdata/import/original.go.patch new file mode 100644 index 00000000..484888e0 --- /dev/null +++ b/support/transform/patch/testdata/import/original.go.patch @@ -0,0 +1,4 @@ +package main + +//prepend import _ "embed" +import "fmt" diff --git a/support/transform/patch/testdata/prepend_func/expected.go b/support/transform/patch/testdata/prepend_func/expected.go new file mode 100644 index 00000000..facdfede --- /dev/null +++ b/support/transform/patch/testdata/prepend_func/expected.go @@ -0,0 +1,9 @@ +package main + +import "fmt" + +var a int + +func main() { + fmt.Println("hello world") +} diff --git a/support/transform/patch/testdata/prepend_func/original.go b/support/transform/patch/testdata/prepend_func/original.go new file mode 100644 index 00000000..c0481191 --- /dev/null +++ b/support/transform/patch/testdata/prepend_func/original.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("hello world") +} diff --git a/support/transform/patch/testdata/prepend_func/original.go.patch b/support/transform/patch/testdata/prepend_func/original.go.patch new file mode 100644 index 00000000..a1eebbfc --- /dev/null +++ b/support/transform/patch/testdata/prepend_func/original.go.patch @@ -0,0 +1,6 @@ +package main + +//prepend var a int +func main(){ + +} diff --git a/support/transform/patch/testdata/replace/expected.go b/support/transform/patch/testdata/replace/expected.go new file mode 100644 index 00000000..f285d839 --- /dev/null +++ b/support/transform/patch/testdata/replace/expected.go @@ -0,0 +1,12 @@ +package main + +import "fmt" + +func main() { + fmt.Println(greet("world")) +} + +// greet +func greet(s string) string { + return "patched " + s +} diff --git a/support/transform/patch/testdata/replace/original.go b/support/transform/patch/testdata/replace/original.go new file mode 100644 index 00000000..62f975cb --- /dev/null +++ b/support/transform/patch/testdata/replace/original.go @@ -0,0 +1,12 @@ +package main + +import "fmt" + +func main() { + fmt.Println(greet("world")) +} + +// greet +func greet(s string) string { + return "hello " + s +} diff --git a/support/transform/patch/testdata/replace/original.go.patch b/support/transform/patch/testdata/replace/original.go.patch new file mode 100644 index 00000000..183821ba --- /dev/null +++ b/support/transform/patch/testdata/replace/original.go.patch @@ -0,0 +1,6 @@ +package main + +func greet(s string) string { + //replace return "patched " +s + return "hello " + s +} \ No newline at end of file