whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package aliases
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
"go/types"
|
||||
)
|
||||
|
||||
// Package aliases defines backward compatible shims
|
||||
// for the types.Alias type representation added in 1.22.
|
||||
// This defines placeholders for x/tools until 1.26.
|
||||
|
||||
// NewAlias creates a new TypeName in Package pkg that
|
||||
// is an alias for the type rhs.
|
||||
//
|
||||
// The enabled parameter determines whether the resulting [TypeName]'s
|
||||
// type is an [types.Alias]. Its value must be the result of a call to
|
||||
// [Enabled], which computes the effective value of
|
||||
// GODEBUG=gotypesalias=... by invoking the type checker. The Enabled
|
||||
// function is expensive and should be called once per task (e.g.
|
||||
// package import), not once per call to NewAlias.
|
||||
//
|
||||
// Precondition: enabled || len(tparams)==0.
|
||||
// If materialized aliases are disabled, there must not be any type parameters.
|
||||
func NewAlias(enabled bool, pos token.Pos, pkg *types.Package, name string, rhs types.Type, tparams []*types.TypeParam) *types.TypeName {
|
||||
if enabled {
|
||||
tname := types.NewTypeName(pos, pkg, name, nil)
|
||||
newAlias(tname, rhs, tparams)
|
||||
return tname
|
||||
}
|
||||
if len(tparams) > 0 {
|
||||
panic("cannot create an alias with type parameters when gotypesalias is not enabled")
|
||||
}
|
||||
return types.NewTypeName(pos, pkg, name, rhs)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !go1.22
|
||||
// +build !go1.22
|
||||
|
||||
package aliases
|
||||
|
||||
import (
|
||||
"go/types"
|
||||
)
|
||||
|
||||
// Alias is a placeholder for a go/types.Alias for <=1.21.
|
||||
// It will never be created by go/types.
|
||||
type Alias struct{}
|
||||
|
||||
func (*Alias) String() string { panic("unreachable") }
|
||||
func (*Alias) Underlying() types.Type { panic("unreachable") }
|
||||
func (*Alias) Obj() *types.TypeName { panic("unreachable") }
|
||||
func Rhs(alias *Alias) types.Type { panic("unreachable") }
|
||||
func TypeParams(alias *Alias) *types.TypeParamList { panic("unreachable") }
|
||||
func SetTypeParams(alias *Alias, tparams []*types.TypeParam) { panic("unreachable") }
|
||||
func TypeArgs(alias *Alias) *types.TypeList { panic("unreachable") }
|
||||
func Origin(alias *Alias) *Alias { panic("unreachable") }
|
||||
|
||||
// Unalias returns the type t for go <=1.21.
|
||||
func Unalias(t types.Type) types.Type { return t }
|
||||
|
||||
func newAlias(name *types.TypeName, rhs types.Type, tparams []*types.TypeParam) *Alias {
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// Enabled reports whether [NewAlias] should create [types.Alias] types.
|
||||
//
|
||||
// Before go1.22, this function always returns false.
|
||||
func Enabled() bool { return false }
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.22
|
||||
// +build go1.22
|
||||
|
||||
package aliases
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
)
|
||||
|
||||
// Alias is an alias of types.Alias.
|
||||
type Alias = types.Alias
|
||||
|
||||
// Rhs returns the type on the right-hand side of the alias declaration.
|
||||
func Rhs(alias *Alias) types.Type {
|
||||
if alias, ok := any(alias).(interface{ Rhs() types.Type }); ok {
|
||||
return alias.Rhs() // go1.23+
|
||||
}
|
||||
|
||||
// go1.22's Alias didn't have the Rhs method,
|
||||
// so Unalias is the best we can do.
|
||||
return Unalias(alias)
|
||||
}
|
||||
|
||||
// TypeParams returns the type parameter list of the alias.
|
||||
func TypeParams(alias *Alias) *types.TypeParamList {
|
||||
if alias, ok := any(alias).(interface{ TypeParams() *types.TypeParamList }); ok {
|
||||
return alias.TypeParams() // go1.23+
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTypeParams sets the type parameters of the alias type.
|
||||
func SetTypeParams(alias *Alias, tparams []*types.TypeParam) {
|
||||
if alias, ok := any(alias).(interface {
|
||||
SetTypeParams(tparams []*types.TypeParam)
|
||||
}); ok {
|
||||
alias.SetTypeParams(tparams) // go1.23+
|
||||
} else if len(tparams) > 0 {
|
||||
panic("cannot set type parameters of an Alias type in go1.22")
|
||||
}
|
||||
}
|
||||
|
||||
// TypeArgs returns the type arguments used to instantiate the Alias type.
|
||||
func TypeArgs(alias *Alias) *types.TypeList {
|
||||
if alias, ok := any(alias).(interface{ TypeArgs() *types.TypeList }); ok {
|
||||
return alias.TypeArgs() // go1.23+
|
||||
}
|
||||
return nil // empty (go1.22)
|
||||
}
|
||||
|
||||
// Origin returns the generic Alias type of which alias is an instance.
|
||||
// If alias is not an instance of a generic alias, Origin returns alias.
|
||||
func Origin(alias *Alias) *Alias {
|
||||
if alias, ok := any(alias).(interface{ Origin() *types.Alias }); ok {
|
||||
return alias.Origin() // go1.23+
|
||||
}
|
||||
return alias // not an instance of a generic alias (go1.22)
|
||||
}
|
||||
|
||||
// Unalias is a wrapper of types.Unalias.
|
||||
func Unalias(t types.Type) types.Type { return types.Unalias(t) }
|
||||
|
||||
// newAlias is an internal alias around types.NewAlias.
|
||||
// Direct usage is discouraged as the moment.
|
||||
// Try to use NewAlias instead.
|
||||
func newAlias(tname *types.TypeName, rhs types.Type, tparams []*types.TypeParam) *Alias {
|
||||
a := types.NewAlias(tname, rhs)
|
||||
SetTypeParams(a, tparams)
|
||||
return a
|
||||
}
|
||||
|
||||
// Enabled reports whether [NewAlias] should create [types.Alias] types.
|
||||
//
|
||||
// This function is expensive! Call it sparingly.
|
||||
func Enabled() bool {
|
||||
// The only reliable way to compute the answer is to invoke go/types.
|
||||
// We don't parse the GODEBUG environment variable, because
|
||||
// (a) it's tricky to do so in a manner that is consistent
|
||||
// with the godebug package; in particular, a simple
|
||||
// substring check is not good enough. The value is a
|
||||
// rightmost-wins list of options. But more importantly:
|
||||
// (b) it is impossible to detect changes to the effective
|
||||
// setting caused by os.Setenv("GODEBUG"), as happens in
|
||||
// many tests. Therefore any attempt to cache the result
|
||||
// is just incorrect.
|
||||
fset := token.NewFileSet()
|
||||
f, _ := parser.ParseFile(fset, "a.go", "package p; type A = int", 0)
|
||||
pkg, _ := new(types.Config).Check("p", fset, []*ast.File{f}, nil)
|
||||
_, enabled := pkg.Scope().Lookup("A").Type().(*types.Alias)
|
||||
return enabled
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package aliases_test
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/aliases"
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
)
|
||||
|
||||
// Assert that Obj exists on Alias.
|
||||
var _ func(*aliases.Alias) *types.TypeName = (*aliases.Alias).Obj
|
||||
|
||||
// TestNewAlias tests that alias.NewAlias creates an alias of a type
|
||||
// whose underlying and Unaliased type is *Named.
|
||||
// When gotypesalias=1 (or unset) and GoVersion >= 1.22, the type will
|
||||
// be an *aliases.Alias.
|
||||
func TestNewAlias(t *testing.T) {
|
||||
const source = `
|
||||
package p
|
||||
|
||||
type Named int
|
||||
`
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "hello.go", source, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var conf types.Config
|
||||
pkg, err := conf.Check("p", fset, []*ast.File{f}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expr := `*Named`
|
||||
tv, err := types.Eval(fset, pkg, 0, expr)
|
||||
if err != nil {
|
||||
t.Fatalf("Eval(%s) failed: %v", expr, err)
|
||||
}
|
||||
|
||||
for _, godebug := range []string{
|
||||
// The default gotypesalias value follows the x/tools/go.mod version
|
||||
// The go.mod is at 1.19 so the default is gotypesalias=0.
|
||||
// "", // Use the default GODEBUG value.
|
||||
"gotypesalias=0",
|
||||
"gotypesalias=1",
|
||||
} {
|
||||
t.Run(godebug, func(t *testing.T) {
|
||||
t.Setenv("GODEBUG", godebug)
|
||||
|
||||
enabled := aliases.Enabled()
|
||||
|
||||
A := aliases.NewAlias(enabled, token.NoPos, pkg, "A", tv.Type, nil)
|
||||
if got, want := A.Name(), "A"; got != want {
|
||||
t.Errorf("Expected A.Name()==%q. got %q", want, got)
|
||||
}
|
||||
|
||||
if got, want := A.Type().Underlying(), tv.Type; got != want {
|
||||
t.Errorf("Expected A.Type().Underlying()==%q. got %q", want, got)
|
||||
}
|
||||
if got, want := aliases.Unalias(A.Type()), tv.Type; got != want {
|
||||
t.Errorf("Expected Unalias(A)==%q. got %q", want, got)
|
||||
}
|
||||
|
||||
if testenv.Go1Point() >= 22 && godebug != "gotypesalias=0" {
|
||||
if _, ok := A.Type().(*aliases.Alias); !ok {
|
||||
t.Errorf("Expected A.Type() to be a types.Alias(). got %q", A.Type())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewAlias tests that alias.NewAlias can create a parameterized alias
|
||||
// A[T] of a type whose underlying and Unaliased type is *T. The test then
|
||||
// instantiates A[Named] and checks that the underlying and Unaliased type
|
||||
// of A[Named] is *Named.
|
||||
//
|
||||
// Requires gotypesalias GODEBUG and aliastypeparams GOEXPERIMENT.
|
||||
func TestNewParameterizedAlias(t *testing.T) {
|
||||
testenv.NeedsGoExperiment(t, "aliastypeparams")
|
||||
|
||||
t.Setenv("GODEBUG", "gotypesalias=1") // needed until gotypesalias is removed (1.27).
|
||||
enabled := aliases.Enabled()
|
||||
if !enabled {
|
||||
t.Fatal("Need materialized aliases enabled")
|
||||
}
|
||||
|
||||
const source = `
|
||||
package p
|
||||
|
||||
type Named int
|
||||
`
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "hello.go", source, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var conf types.Config
|
||||
pkg, err := conf.Check("p", fset, []*ast.File{f}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// type A[T ~int] = *T
|
||||
tparam := types.NewTypeParam(
|
||||
types.NewTypeName(token.NoPos, pkg, "T", nil),
|
||||
types.NewUnion([]*types.Term{types.NewTerm(true, types.Typ[types.Int])}),
|
||||
)
|
||||
ptrT := types.NewPointer(tparam)
|
||||
A := aliases.NewAlias(enabled, token.NoPos, pkg, "A", ptrT, []*types.TypeParam{tparam})
|
||||
if got, want := A.Name(), "A"; got != want {
|
||||
t.Errorf("NewAlias: got %q, want %q", got, want)
|
||||
}
|
||||
|
||||
if got, want := A.Type().Underlying(), ptrT; !types.Identical(got, want) {
|
||||
t.Errorf("A.Type().Underlying (%q) is not identical to %q", got, want)
|
||||
}
|
||||
if got, want := aliases.Unalias(A.Type()), ptrT; !types.Identical(got, want) {
|
||||
t.Errorf("Unalias(A)==%q is not identical to %q", got, want)
|
||||
}
|
||||
|
||||
if _, ok := A.Type().(*aliases.Alias); !ok {
|
||||
t.Errorf("Expected A.Type() to be a types.Alias(). got %q", A.Type())
|
||||
}
|
||||
|
||||
pkg.Scope().Insert(A) // Add A to pkg so it is available to types.Eval.
|
||||
|
||||
named, ok := pkg.Scope().Lookup("Named").(*types.TypeName)
|
||||
if !ok {
|
||||
t.Fatalf("Failed to Lookup(%q) in package %s", "Named", pkg)
|
||||
}
|
||||
ptrNamed := types.NewPointer(named.Type())
|
||||
|
||||
const expr = `A[Named]`
|
||||
tv, err := types.Eval(fset, pkg, 0, expr)
|
||||
if err != nil {
|
||||
t.Fatalf("Eval(%s) failed: %v", expr, err)
|
||||
}
|
||||
|
||||
if got, want := tv.Type.Underlying(), ptrNamed; !types.Identical(got, want) {
|
||||
t.Errorf("A[Named].Type().Underlying (%q) is not identical to %q", got, want)
|
||||
}
|
||||
if got, want := aliases.Unalias(tv.Type), ptrNamed; !types.Identical(got, want) {
|
||||
t.Errorf("Unalias(A[Named])==%q is not identical to %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package analysisinternal_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/importer"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"golang.org/x/tools/internal/analysisinternal"
|
||||
)
|
||||
|
||||
func TestAddImport(t *testing.T) {
|
||||
descr := func(s string) string {
|
||||
if _, _, line, ok := runtime.Caller(1); ok {
|
||||
return fmt.Sprintf("L%d %s", line, s)
|
||||
}
|
||||
panic("runtime.Caller failed")
|
||||
}
|
||||
|
||||
// Each test case contains a «name pkgpath»
|
||||
// section to be replaced with a reference
|
||||
// to a valid import of pkgpath,
|
||||
// ideally of the specified name.
|
||||
for _, test := range []struct {
|
||||
descr, src, want string
|
||||
}{
|
||||
{
|
||||
descr: descr("simple add import"),
|
||||
src: `package a
|
||||
func _() {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
import "fmt"
|
||||
|
||||
func _() {
|
||||
fmt
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("existing import"),
|
||||
src: `package a
|
||||
|
||||
import "fmt"
|
||||
|
||||
func _(fmt.Stringer) {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import "fmt"
|
||||
|
||||
func _(fmt.Stringer) {
|
||||
fmt
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("existing blank import"),
|
||||
src: `package a
|
||||
|
||||
import _ "fmt"
|
||||
|
||||
func _() {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import "fmt"
|
||||
|
||||
import _ "fmt"
|
||||
|
||||
func _() {
|
||||
fmt
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("existing renaming import"),
|
||||
src: `package a
|
||||
|
||||
import fmtpkg "fmt"
|
||||
|
||||
var fmt int
|
||||
|
||||
func _(fmtpkg.Stringer) {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import fmtpkg "fmt"
|
||||
|
||||
var fmt int
|
||||
|
||||
func _(fmtpkg.Stringer) {
|
||||
fmtpkg
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("existing import is shadowed"),
|
||||
src: `package a
|
||||
|
||||
import "fmt"
|
||||
|
||||
var _ fmt.Stringer
|
||||
|
||||
func _(fmt int) {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import fmt0 "fmt"
|
||||
|
||||
import "fmt"
|
||||
|
||||
var _ fmt.Stringer
|
||||
|
||||
func _(fmt int) {
|
||||
fmt0
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("preferred name is shadowed"),
|
||||
src: `package a
|
||||
|
||||
import "fmt"
|
||||
|
||||
func _(fmt fmt.Stringer) {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import fmt0 "fmt"
|
||||
|
||||
import "fmt"
|
||||
|
||||
func _(fmt fmt.Stringer) {
|
||||
fmt0
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("import inserted before doc comments"),
|
||||
src: `package a
|
||||
|
||||
// hello
|
||||
import ()
|
||||
|
||||
// world
|
||||
func _() {
|
||||
«fmt fmt»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import "fmt"
|
||||
|
||||
// hello
|
||||
import ()
|
||||
|
||||
// world
|
||||
func _() {
|
||||
fmt
|
||||
}`,
|
||||
},
|
||||
{
|
||||
descr: descr("arbitrary preferred name => renaming import"),
|
||||
src: `package a
|
||||
|
||||
func _() {
|
||||
«foo encoding/json»
|
||||
}`,
|
||||
want: `package a
|
||||
|
||||
import foo "encoding/json"
|
||||
|
||||
func _() {
|
||||
foo
|
||||
}`,
|
||||
},
|
||||
} {
|
||||
t.Run(test.descr, func(t *testing.T) {
|
||||
// splice marker
|
||||
before, mid, ok1 := strings.Cut(test.src, "«")
|
||||
mid, after, ok2 := strings.Cut(mid, "»")
|
||||
if !ok1 || !ok2 {
|
||||
t.Fatal("no «name path» marker")
|
||||
}
|
||||
src := before + "/*!*/" + after
|
||||
name, path, _ := strings.Cut(mid, " ")
|
||||
|
||||
// parse
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "a.go", src, parser.ParseComments)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
pos := fset.File(f.Pos()).Pos(len(before))
|
||||
|
||||
// type-check
|
||||
info := &types.Info{
|
||||
Types: make(map[ast.Expr]types.TypeAndValue),
|
||||
Scopes: make(map[ast.Node]*types.Scope),
|
||||
Defs: make(map[*ast.Ident]types.Object),
|
||||
Implicits: make(map[ast.Node]types.Object),
|
||||
}
|
||||
conf := &types.Config{
|
||||
Error: func(err error) { t.Log(err) },
|
||||
Importer: importer.Default(),
|
||||
}
|
||||
conf.Check(f.Name.Name, fset, []*ast.File{f}, info)
|
||||
|
||||
// add import
|
||||
name, edits := analysisinternal.AddImport(info, f, pos, path, name)
|
||||
|
||||
var edit analysis.TextEdit
|
||||
switch len(edits) {
|
||||
case 0:
|
||||
case 1:
|
||||
edit = edits[0]
|
||||
default:
|
||||
t.Fatalf("expected at most one edit, got %d", len(edits))
|
||||
}
|
||||
|
||||
// apply patch
|
||||
start := fset.Position(edit.Pos)
|
||||
end := fset.Position(edit.End)
|
||||
output := src[:start.Offset] + string(edit.NewText) + src[end.Offset:]
|
||||
output = strings.ReplaceAll(output, "/*!*/", name)
|
||||
if output != test.want {
|
||||
t.Errorf("\n--got--\n%s\n--want--\n%s\n--diff--\n%s",
|
||||
output, test.want, cmp.Diff(test.want, output))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package analysisinternal provides gopls' internal analyses with a
|
||||
// number of helper functions that operate on typed syntax trees.
|
||||
package analysisinternal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"os"
|
||||
pathpkg "path"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"golang.org/x/tools/internal/aliases"
|
||||
)
|
||||
|
||||
func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos {
|
||||
// Get the end position for the type error.
|
||||
offset, end := fset.PositionFor(start, false).Offset, start
|
||||
if offset >= len(src) {
|
||||
return end
|
||||
}
|
||||
if width := bytes.IndexAny(src[offset:], " \n,():;[]+-*"); width > 0 {
|
||||
end = start + token.Pos(width)
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
func ZeroValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr {
|
||||
// TODO(adonovan): think about generics, and also generic aliases.
|
||||
under := aliases.Unalias(typ)
|
||||
// Don't call Underlying unconditionally: although it removes
|
||||
// Named and Alias, it also removes TypeParam.
|
||||
if n, ok := under.(*types.Named); ok {
|
||||
under = n.Underlying()
|
||||
}
|
||||
switch under := under.(type) {
|
||||
case *types.Basic:
|
||||
switch {
|
||||
case under.Info()&types.IsNumeric != 0:
|
||||
return &ast.BasicLit{Kind: token.INT, Value: "0"}
|
||||
case under.Info()&types.IsBoolean != 0:
|
||||
return &ast.Ident{Name: "false"}
|
||||
case under.Info()&types.IsString != 0:
|
||||
return &ast.BasicLit{Kind: token.STRING, Value: `""`}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown basic type %v", under))
|
||||
}
|
||||
case *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Signature, *types.Slice, *types.Array:
|
||||
return ast.NewIdent("nil")
|
||||
case *types.Struct:
|
||||
texpr := TypeExpr(f, pkg, typ) // typ because we want the name here.
|
||||
if texpr == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.CompositeLit{
|
||||
Type: texpr,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsZeroValue checks whether the given expression is a 'zero value' (as determined by output of
|
||||
// analysisinternal.ZeroValue)
|
||||
func IsZeroValue(expr ast.Expr) bool {
|
||||
switch e := expr.(type) {
|
||||
case *ast.BasicLit:
|
||||
return e.Value == "0" || e.Value == `""`
|
||||
case *ast.Ident:
|
||||
return e.Name == "nil" || e.Name == "false"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// TypeExpr returns syntax for the specified type. References to
|
||||
// named types from packages other than pkg are qualified by an appropriate
|
||||
// package name, as defined by the import environment of file.
|
||||
func TypeExpr(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr {
|
||||
switch t := typ.(type) {
|
||||
case *types.Basic:
|
||||
switch t.Kind() {
|
||||
case types.UnsafePointer:
|
||||
return &ast.SelectorExpr{X: ast.NewIdent("unsafe"), Sel: ast.NewIdent("Pointer")}
|
||||
default:
|
||||
return ast.NewIdent(t.Name())
|
||||
}
|
||||
case *types.Pointer:
|
||||
x := TypeExpr(f, pkg, t.Elem())
|
||||
if x == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.UnaryExpr{
|
||||
Op: token.MUL,
|
||||
X: x,
|
||||
}
|
||||
case *types.Array:
|
||||
elt := TypeExpr(f, pkg, t.Elem())
|
||||
if elt == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.ArrayType{
|
||||
Len: &ast.BasicLit{
|
||||
Kind: token.INT,
|
||||
Value: fmt.Sprintf("%d", t.Len()),
|
||||
},
|
||||
Elt: elt,
|
||||
}
|
||||
case *types.Slice:
|
||||
elt := TypeExpr(f, pkg, t.Elem())
|
||||
if elt == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.ArrayType{
|
||||
Elt: elt,
|
||||
}
|
||||
case *types.Map:
|
||||
key := TypeExpr(f, pkg, t.Key())
|
||||
value := TypeExpr(f, pkg, t.Elem())
|
||||
if key == nil || value == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.MapType{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
case *types.Chan:
|
||||
dir := ast.ChanDir(t.Dir())
|
||||
if t.Dir() == types.SendRecv {
|
||||
dir = ast.SEND | ast.RECV
|
||||
}
|
||||
value := TypeExpr(f, pkg, t.Elem())
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.ChanType{
|
||||
Dir: dir,
|
||||
Value: value,
|
||||
}
|
||||
case *types.Signature:
|
||||
var params []*ast.Field
|
||||
for i := 0; i < t.Params().Len(); i++ {
|
||||
p := TypeExpr(f, pkg, t.Params().At(i).Type())
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
params = append(params, &ast.Field{
|
||||
Type: p,
|
||||
Names: []*ast.Ident{
|
||||
{
|
||||
Name: t.Params().At(i).Name(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
if t.Variadic() {
|
||||
last := params[len(params)-1]
|
||||
last.Type = &ast.Ellipsis{Elt: last.Type.(*ast.ArrayType).Elt}
|
||||
}
|
||||
var returns []*ast.Field
|
||||
for i := 0; i < t.Results().Len(); i++ {
|
||||
r := TypeExpr(f, pkg, t.Results().At(i).Type())
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
returns = append(returns, &ast.Field{
|
||||
Type: r,
|
||||
})
|
||||
}
|
||||
return &ast.FuncType{
|
||||
Params: &ast.FieldList{
|
||||
List: params,
|
||||
},
|
||||
Results: &ast.FieldList{
|
||||
List: returns,
|
||||
},
|
||||
}
|
||||
case interface{ Obj() *types.TypeName }: // *types.{Alias,Named,TypeParam}
|
||||
if t.Obj().Pkg() == nil {
|
||||
return ast.NewIdent(t.Obj().Name())
|
||||
}
|
||||
if t.Obj().Pkg() == pkg {
|
||||
return ast.NewIdent(t.Obj().Name())
|
||||
}
|
||||
pkgName := t.Obj().Pkg().Name()
|
||||
|
||||
// If the file already imports the package under another name, use that.
|
||||
for _, cand := range f.Imports {
|
||||
if path, _ := strconv.Unquote(cand.Path.Value); path == t.Obj().Pkg().Path() {
|
||||
if cand.Name != nil && cand.Name.Name != "" {
|
||||
pkgName = cand.Name.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
if pkgName == "." {
|
||||
return ast.NewIdent(t.Obj().Name())
|
||||
}
|
||||
return &ast.SelectorExpr{
|
||||
X: ast.NewIdent(pkgName),
|
||||
Sel: ast.NewIdent(t.Obj().Name()),
|
||||
}
|
||||
case *types.Struct:
|
||||
return ast.NewIdent(t.String())
|
||||
case *types.Interface:
|
||||
return ast.NewIdent(t.String())
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// StmtToInsertVarBefore returns the ast.Stmt before which we can safely insert a new variable.
|
||||
// Some examples:
|
||||
//
|
||||
// Basic Example:
|
||||
// z := 1
|
||||
// y := z + x
|
||||
// If x is undeclared, then this function would return `y := z + x`, so that we
|
||||
// can insert `x := ` on the line before `y := z + x`.
|
||||
//
|
||||
// If stmt example:
|
||||
// if z == 1 {
|
||||
// } else if z == y {}
|
||||
// If y is undeclared, then this function would return `if z == 1 {`, because we cannot
|
||||
// insert a statement between an if and an else if statement. As a result, we need to find
|
||||
// the top of the if chain to insert `y := ` before.
|
||||
func StmtToInsertVarBefore(path []ast.Node) ast.Stmt {
|
||||
enclosingIndex := -1
|
||||
for i, p := range path {
|
||||
if _, ok := p.(ast.Stmt); ok {
|
||||
enclosingIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if enclosingIndex == -1 {
|
||||
return nil
|
||||
}
|
||||
enclosingStmt := path[enclosingIndex]
|
||||
switch enclosingStmt.(type) {
|
||||
case *ast.IfStmt:
|
||||
// The enclosingStmt is inside of the if declaration,
|
||||
// We need to check if we are in an else-if stmt and
|
||||
// get the base if statement.
|
||||
return baseIfStmt(path, enclosingIndex)
|
||||
case *ast.CaseClause:
|
||||
// Get the enclosing switch stmt if the enclosingStmt is
|
||||
// inside of the case statement.
|
||||
for i := enclosingIndex + 1; i < len(path); i++ {
|
||||
if node, ok := path[i].(*ast.SwitchStmt); ok {
|
||||
return node
|
||||
} else if node, ok := path[i].(*ast.TypeSwitchStmt); ok {
|
||||
return node
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(path) <= enclosingIndex+1 {
|
||||
return enclosingStmt.(ast.Stmt)
|
||||
}
|
||||
// Check if the enclosing statement is inside another node.
|
||||
switch expr := path[enclosingIndex+1].(type) {
|
||||
case *ast.IfStmt:
|
||||
// Get the base if statement.
|
||||
return baseIfStmt(path, enclosingIndex+1)
|
||||
case *ast.ForStmt:
|
||||
if expr.Init == enclosingStmt || expr.Post == enclosingStmt {
|
||||
return expr
|
||||
}
|
||||
case *ast.SwitchStmt, *ast.TypeSwitchStmt:
|
||||
return expr.(ast.Stmt)
|
||||
}
|
||||
return enclosingStmt.(ast.Stmt)
|
||||
}
|
||||
|
||||
// baseIfStmt walks up the if/else-if chain until we get to
|
||||
// the top of the current if chain.
|
||||
func baseIfStmt(path []ast.Node, index int) ast.Stmt {
|
||||
stmt := path[index]
|
||||
for i := index + 1; i < len(path); i++ {
|
||||
if node, ok := path[i].(*ast.IfStmt); ok && node.Else == stmt {
|
||||
stmt = node
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return stmt.(ast.Stmt)
|
||||
}
|
||||
|
||||
// WalkASTWithParent walks the AST rooted at n. The semantics are
|
||||
// similar to ast.Inspect except it does not call f(nil).
|
||||
func WalkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) {
|
||||
var ancestors []ast.Node
|
||||
ast.Inspect(n, func(n ast.Node) (recurse bool) {
|
||||
if n == nil {
|
||||
ancestors = ancestors[:len(ancestors)-1]
|
||||
return false
|
||||
}
|
||||
|
||||
var parent ast.Node
|
||||
if len(ancestors) > 0 {
|
||||
parent = ancestors[len(ancestors)-1]
|
||||
}
|
||||
ancestors = append(ancestors, n)
|
||||
return f(n, parent)
|
||||
})
|
||||
}
|
||||
|
||||
// MatchingIdents finds the names of all identifiers in 'node' that match any of the given types.
|
||||
// 'pos' represents the position at which the identifiers may be inserted. 'pos' must be within
|
||||
// the scope of each of identifier we select. Otherwise, we will insert a variable at 'pos' that
|
||||
// is unrecognized.
|
||||
func MatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *types.Info, pkg *types.Package) map[types.Type][]string {
|
||||
|
||||
// Initialize matches to contain the variable types we are searching for.
|
||||
matches := make(map[types.Type][]string)
|
||||
for _, typ := range typs {
|
||||
if typ == nil {
|
||||
continue // TODO(adonovan): is this reachable?
|
||||
}
|
||||
matches[typ] = nil // create entry
|
||||
}
|
||||
|
||||
seen := map[types.Object]struct{}{}
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
// Prevent circular definitions. If 'pos' is within an assignment statement, do not
|
||||
// allow any identifiers in that assignment statement to be selected. Otherwise,
|
||||
// we could do the following, where 'x' satisfies the type of 'f0':
|
||||
//
|
||||
// x := fakeStruct{f0: x}
|
||||
//
|
||||
if assign, ok := n.(*ast.AssignStmt); ok && pos > assign.Pos() && pos <= assign.End() {
|
||||
return false
|
||||
}
|
||||
if n.End() > pos {
|
||||
return n.Pos() <= pos
|
||||
}
|
||||
ident, ok := n.(*ast.Ident)
|
||||
if !ok || ident.Name == "_" {
|
||||
return true
|
||||
}
|
||||
obj := info.Defs[ident]
|
||||
if obj == nil || obj.Type() == nil {
|
||||
return true
|
||||
}
|
||||
if _, ok := obj.(*types.TypeName); ok {
|
||||
return true
|
||||
}
|
||||
// Prevent duplicates in matches' values.
|
||||
if _, ok = seen[obj]; ok {
|
||||
return true
|
||||
}
|
||||
seen[obj] = struct{}{}
|
||||
// Find the scope for the given position. Then, check whether the object
|
||||
// exists within the scope.
|
||||
innerScope := pkg.Scope().Innermost(pos)
|
||||
if innerScope == nil {
|
||||
return true
|
||||
}
|
||||
_, foundObj := innerScope.LookupParent(ident.Name, pos)
|
||||
if foundObj != obj {
|
||||
return true
|
||||
}
|
||||
// The object must match one of the types that we are searching for.
|
||||
// TODO(adonovan): opt: use typeutil.Map?
|
||||
if names, ok := matches[obj.Type()]; ok {
|
||||
matches[obj.Type()] = append(names, ident.Name)
|
||||
} else {
|
||||
// If the object type does not exactly match
|
||||
// any of the target types, greedily find the first
|
||||
// target type that the object type can satisfy.
|
||||
for typ := range matches {
|
||||
if equivalentTypes(obj.Type(), typ) {
|
||||
matches[typ] = append(matches[typ], ident.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return matches
|
||||
}
|
||||
|
||||
func equivalentTypes(want, got types.Type) bool {
|
||||
if types.Identical(want, got) {
|
||||
return true
|
||||
}
|
||||
// Code segment to help check for untyped equality from (golang/go#32146).
|
||||
if rhs, ok := want.(*types.Basic); ok && rhs.Info()&types.IsUntyped > 0 {
|
||||
if lhs, ok := got.Underlying().(*types.Basic); ok {
|
||||
return rhs.Info()&types.IsConstType == lhs.Info()&types.IsConstType
|
||||
}
|
||||
}
|
||||
return types.AssignableTo(want, got)
|
||||
}
|
||||
|
||||
// MakeReadFile returns a simple implementation of the Pass.ReadFile function.
|
||||
func MakeReadFile(pass *analysis.Pass) func(filename string) ([]byte, error) {
|
||||
return func(filename string) ([]byte, error) {
|
||||
if err := CheckReadable(pass, filename); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.ReadFile(filename)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckReadable enforces the access policy defined by the ReadFile field of [analysis.Pass].
|
||||
func CheckReadable(pass *analysis.Pass, filename string) error {
|
||||
if slicesContains(pass.OtherFiles, filename) ||
|
||||
slicesContains(pass.IgnoredFiles, filename) {
|
||||
return nil
|
||||
}
|
||||
for _, f := range pass.Files {
|
||||
// TODO(adonovan): use go1.20 f.FileStart
|
||||
if pass.Fset.File(f.Pos()).Name() == filename {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("Pass.ReadFile: %s is not among OtherFiles, IgnoredFiles, or names of Files", filename)
|
||||
}
|
||||
|
||||
// TODO(adonovan): use go1.21 slices.Contains.
|
||||
func slicesContains[S ~[]E, E comparable](slice S, x E) bool {
|
||||
for _, elem := range slice {
|
||||
if elem == x {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AddImport checks whether this file already imports pkgpath and
|
||||
// that import is in scope at pos. If so, it returns the name under
|
||||
// which it was imported and a zero edit. Otherwise, it adds a new
|
||||
// import of pkgpath, using a name derived from the preferred name,
|
||||
// and returns the chosen name along with the edit for the new import.
|
||||
//
|
||||
// It does not mutate its arguments.
|
||||
func AddImport(info *types.Info, file *ast.File, pos token.Pos, pkgpath, preferredName string) (name string, newImport []analysis.TextEdit) {
|
||||
// Find innermost enclosing lexical block.
|
||||
scope := info.Scopes[file].Innermost(pos)
|
||||
if scope == nil {
|
||||
panic("no enclosing lexical block")
|
||||
}
|
||||
|
||||
// Is there an existing import of this package?
|
||||
// If so, are we in its scope? (not shadowed)
|
||||
for _, spec := range file.Imports {
|
||||
pkgname, ok := importedPkgName(info, spec)
|
||||
if ok && pkgname.Imported().Path() == pkgpath {
|
||||
if _, obj := scope.LookupParent(pkgname.Name(), pos); obj == pkgname {
|
||||
return pkgname.Name(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We must add a new import.
|
||||
// Ensure we have a fresh name.
|
||||
newName := preferredName
|
||||
for i := 0; ; i++ {
|
||||
if _, obj := scope.LookupParent(newName, pos); obj == nil {
|
||||
break // fresh
|
||||
}
|
||||
newName = fmt.Sprintf("%s%d", preferredName, i)
|
||||
}
|
||||
|
||||
// For now, keep it real simple: create a new import
|
||||
// declaration before the first existing declaration (which
|
||||
// must exist), including its comments, and let goimports tidy it up.
|
||||
//
|
||||
// Use a renaming import whenever the preferred name is not
|
||||
// available, or the chosen name does not match the last
|
||||
// segment of its path.
|
||||
newText := fmt.Sprintf("import %q\n\n", pkgpath)
|
||||
if newName != preferredName || newName != pathpkg.Base(pkgpath) {
|
||||
newText = fmt.Sprintf("import %s %q\n\n", newName, pkgpath)
|
||||
}
|
||||
decl0 := file.Decls[0]
|
||||
var before ast.Node = decl0
|
||||
switch decl0 := decl0.(type) {
|
||||
case *ast.GenDecl:
|
||||
if decl0.Doc != nil {
|
||||
before = decl0.Doc
|
||||
}
|
||||
case *ast.FuncDecl:
|
||||
if decl0.Doc != nil {
|
||||
before = decl0.Doc
|
||||
}
|
||||
}
|
||||
return newName, []analysis.TextEdit{{
|
||||
Pos: before.Pos(),
|
||||
End: before.Pos(),
|
||||
NewText: []byte(newText),
|
||||
}}
|
||||
}
|
||||
|
||||
// importedPkgName returns the PkgName object declared by an ImportSpec.
|
||||
// TODO(adonovan): use go1.22's Info.PkgNameOf.
|
||||
func importedPkgName(info *types.Info, imp *ast.ImportSpec) (*types.PkgName, bool) {
|
||||
var obj types.Object
|
||||
if imp.Name != nil {
|
||||
obj = info.Defs[imp.Name]
|
||||
} else {
|
||||
obj = info.Implicits[imp]
|
||||
}
|
||||
pkgname, ok := obj.(*types.PkgName)
|
||||
return pkgname, ok
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package analysisinternal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MustExtractDoc is like [ExtractDoc] but it panics on error.
|
||||
//
|
||||
// To use, define a doc.go file such as:
|
||||
//
|
||||
// // Package halting defines an analyzer of program termination.
|
||||
// //
|
||||
// // # Analyzer halting
|
||||
// //
|
||||
// // halting: reports whether execution will halt.
|
||||
// //
|
||||
// // The halting analyzer reports a diagnostic for functions
|
||||
// // that run forever. To suppress the diagnostics, try inserting
|
||||
// // a 'break' statement into each loop.
|
||||
// package halting
|
||||
//
|
||||
// import _ "embed"
|
||||
//
|
||||
// //go:embed doc.go
|
||||
// var doc string
|
||||
//
|
||||
// And declare your analyzer as:
|
||||
//
|
||||
// var Analyzer = &analysis.Analyzer{
|
||||
// Name: "halting",
|
||||
// Doc: analysisutil.MustExtractDoc(doc, "halting"),
|
||||
// ...
|
||||
// }
|
||||
func MustExtractDoc(content, name string) string {
|
||||
doc, err := ExtractDoc(content, name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
// ExtractDoc extracts a section of a package doc comment from the
|
||||
// provided contents of an analyzer package's doc.go file.
|
||||
//
|
||||
// A section is a portion of the comment between one heading and
|
||||
// the next, using this form:
|
||||
//
|
||||
// # Analyzer NAME
|
||||
//
|
||||
// NAME: SUMMARY
|
||||
//
|
||||
// Full description...
|
||||
//
|
||||
// where NAME matches the name argument, and SUMMARY is a brief
|
||||
// verb-phrase that describes the analyzer. The following lines, up
|
||||
// until the next heading or the end of the comment, contain the full
|
||||
// description. ExtractDoc returns the portion following the colon,
|
||||
// which is the form expected by Analyzer.Doc.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// # Analyzer printf
|
||||
//
|
||||
// printf: checks consistency of calls to printf
|
||||
//
|
||||
// The printf analyzer checks consistency of calls to printf.
|
||||
// Here is the complete description...
|
||||
//
|
||||
// This notation allows a single doc comment to provide documentation
|
||||
// for multiple analyzers, each in its own section.
|
||||
// The HTML anchors generated for each heading are predictable.
|
||||
//
|
||||
// It returns an error if the content was not a valid Go source file
|
||||
// containing a package doc comment with a heading of the required
|
||||
// form.
|
||||
//
|
||||
// This machinery enables the package documentation (typically
|
||||
// accessible via the web at https://pkg.go.dev/) and the command
|
||||
// documentation (typically printed to a terminal) to be derived from
|
||||
// the same source and formatted appropriately.
|
||||
func ExtractDoc(content, name string) (string, error) {
|
||||
if content == "" {
|
||||
return "", fmt.Errorf("empty Go source file")
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "", content, parser.ParseComments|parser.PackageClauseOnly)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("not a Go source file")
|
||||
}
|
||||
if f.Doc == nil {
|
||||
return "", fmt.Errorf("Go source file has no package doc comment")
|
||||
}
|
||||
for _, section := range strings.Split(f.Doc.Text(), "\n# ") {
|
||||
if body := strings.TrimPrefix(section, "Analyzer "+name); body != section &&
|
||||
body != "" &&
|
||||
body[0] == '\r' || body[0] == '\n' {
|
||||
body = strings.TrimSpace(body)
|
||||
rest := strings.TrimPrefix(body, name+":")
|
||||
if rest == body {
|
||||
return "", fmt.Errorf("'Analyzer %s' heading not followed by '%s: summary...' line", name, name)
|
||||
}
|
||||
return strings.TrimSpace(rest), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("package doc comment contains no 'Analyzer %s' heading", name)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package analysisinternal_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/analysisinternal"
|
||||
)
|
||||
|
||||
func TestExtractDoc(t *testing.T) {
|
||||
const multi = `// Copyright
|
||||
|
||||
//+build tag
|
||||
|
||||
// Package foo
|
||||
//
|
||||
// # Irrelevant heading
|
||||
//
|
||||
// This is irrelevant doc.
|
||||
//
|
||||
// # Analyzer nocolon
|
||||
//
|
||||
// This one has the wrong form for this line.
|
||||
//
|
||||
// # Analyzer food
|
||||
//
|
||||
// food: reports dining opportunities
|
||||
//
|
||||
// This is the doc for analyzer 'food'.
|
||||
//
|
||||
// # Analyzer foo
|
||||
//
|
||||
// foo: reports diagnostics
|
||||
//
|
||||
// This is the doc for analyzer 'foo'.
|
||||
//
|
||||
// # Analyzer bar
|
||||
//
|
||||
// bar: reports drinking opportunities
|
||||
//
|
||||
// This is the doc for analyzer 'bar'.
|
||||
package blah
|
||||
|
||||
var x = syntax error
|
||||
`
|
||||
|
||||
for _, test := range []struct {
|
||||
content, name string
|
||||
want string // doc or "error: %w" string
|
||||
}{
|
||||
{"", "foo",
|
||||
"error: empty Go source file"},
|
||||
{"//foo", "foo",
|
||||
"error: not a Go source file"},
|
||||
{"//foo\npackage foo", "foo",
|
||||
"error: package doc comment contains no 'Analyzer foo' heading"},
|
||||
{multi, "foo",
|
||||
"reports diagnostics\n\nThis is the doc for analyzer 'foo'."},
|
||||
{multi, "bar",
|
||||
"reports drinking opportunities\n\nThis is the doc for analyzer 'bar'."},
|
||||
{multi, "food",
|
||||
"reports dining opportunities\n\nThis is the doc for analyzer 'food'."},
|
||||
{multi, "nope",
|
||||
"error: package doc comment contains no 'Analyzer nope' heading"},
|
||||
{multi, "nocolon",
|
||||
"error: 'Analyzer nocolon' heading not followed by 'nocolon: summary...' line"},
|
||||
} {
|
||||
got, err := analysisinternal.ExtractDoc(test.content, test.name)
|
||||
if err != nil {
|
||||
got = "error: " + err.Error()
|
||||
}
|
||||
if test.want != got {
|
||||
t.Errorf("ExtractDoc(%q) returned <<%s>>, want <<%s>>, given input <<%s>>",
|
||||
test.name, got, test.want, test.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
# Checking Go Package API Compatibility
|
||||
|
||||
The `apidiff` tool in this directory determines whether two versions of the same
|
||||
package are compatible. The goal is to help the developer make an informed
|
||||
choice of semantic version after they have changed the code of their module.
|
||||
|
||||
`apidiff` reports two kinds of changes: incompatible ones, which require
|
||||
incrementing the major part of the semantic version, and compatible ones, which
|
||||
require a minor version increment. If no API changes are reported but there are
|
||||
code changes that could affect client code, then the patch version should
|
||||
be incremented.
|
||||
|
||||
Because `apidiff` ignores package import paths, it may be used to display API
|
||||
differences between any two packages, not just different versions of the same
|
||||
package.
|
||||
|
||||
The current version of `apidiff` compares only packages, not modules.
|
||||
|
||||
|
||||
## Compatibility Desiderata
|
||||
|
||||
Any tool that checks compatibility can offer only an approximation. No tool can
|
||||
detect behavioral changes; and even if it could, whether a behavioral change is
|
||||
a breaking change or not depends on many factors, such as whether it closes a
|
||||
security hole or fixes a bug. Even a change that causes some code to fail to
|
||||
compile may not be considered a breaking change by the developers or their
|
||||
users. It may only affect code marked as experimental or unstable, for
|
||||
example, or the break may only manifest in unlikely cases.
|
||||
|
||||
For a tool to be useful, its notion of compatibility must be relaxed enough to
|
||||
allow reasonable changes, like adding a field to a struct, but strict enough to
|
||||
catch significant breaking changes. A tool that is too lax will miss important
|
||||
incompatibilities, and users will stop trusting it; one that is too strict may
|
||||
generate so much noise that users will ignore it.
|
||||
|
||||
To a first approximation, this tool reports a change as incompatible if it could
|
||||
cause client code to stop compiling. But `apidiff` ignores five ways in which
|
||||
code may fail to compile after a change. Three of them are mentioned in the
|
||||
[Go 1 Compatibility Guarantee](https://golang.org/doc/go1compat).
|
||||
|
||||
### Unkeyed Struct Literals
|
||||
|
||||
Code that uses an unkeyed struct literal would fail to compile if a field was
|
||||
added to the struct, making any such addition an incompatible change. An example:
|
||||
|
||||
```
|
||||
// old
|
||||
type Point struct { X, Y int }
|
||||
|
||||
// new
|
||||
type Point struct { X, Y, Z int }
|
||||
|
||||
// client
|
||||
p := pkg.Point{1, 2} // fails in new because there are more fields than expressions
|
||||
```
|
||||
Here and below, we provide three snippets: the code in the old version of the
|
||||
package, the code in the new version, and the code written in a client of the package,
|
||||
which refers to it by the name `pkg`. The client code compiles against the old
|
||||
code but not the new.
|
||||
|
||||
### Embedding and Shadowing
|
||||
|
||||
Adding an exported field to a struct can break code that embeds that struct,
|
||||
because the newly added field may conflict with an identically named field
|
||||
at the same struct depth. A selector referring to the latter would become
|
||||
ambiguous and thus erroneous.
|
||||
|
||||
|
||||
```
|
||||
// old
|
||||
type Point struct { X, Y int }
|
||||
|
||||
// new
|
||||
type Point struct { X, Y, Z int }
|
||||
|
||||
// client
|
||||
type z struct { Z int }
|
||||
|
||||
var v struct {
|
||||
pkg.Point
|
||||
z
|
||||
}
|
||||
|
||||
_ = v.Z // fails in new
|
||||
```
|
||||
In the new version, the last line fails to compile because there are two embedded `Z`
|
||||
fields at the same depth, one from `z` and one from `pkg.Point`.
|
||||
|
||||
|
||||
### Using an Identical Type Externally
|
||||
|
||||
If it is possible for client code to write a type expression representing the
|
||||
underlying type of a defined type in a package, then external code can use it in
|
||||
assignments involving the package type, making any change to that type incompatible.
|
||||
```
|
||||
// old
|
||||
type Point struct { X, Y int }
|
||||
|
||||
// new
|
||||
type Point struct { X, Y, Z int }
|
||||
|
||||
// client
|
||||
var p struct { X, Y int } = pkg.Point{} // fails in new because of Point's extra field
|
||||
```
|
||||
Here, the external code could have used the provided name `Point`, but chose not
|
||||
to. I'll have more to say about this and related examples later.
|
||||
|
||||
### unsafe.Sizeof and Friends
|
||||
|
||||
Since `unsafe.Sizeof`, `unsafe.Offsetof` and `unsafe.Alignof` are constant
|
||||
expressions, they can be used in an array type literal:
|
||||
|
||||
```
|
||||
// old
|
||||
type S struct{ X int }
|
||||
|
||||
// new
|
||||
type S struct{ X, y int }
|
||||
|
||||
// client
|
||||
var a [unsafe.Sizeof(pkg.S{})]int = [8]int{} // fails in new because S's size is not 8
|
||||
```
|
||||
Use of these operations could make many changes to a type potentially incompatible.
|
||||
|
||||
|
||||
### Type Switches
|
||||
|
||||
A package change that merges two different types (with same underlying type)
|
||||
into a single new type may break type switches in clients that refer to both
|
||||
original types:
|
||||
|
||||
```
|
||||
// old
|
||||
type T1 int
|
||||
type T2 int
|
||||
|
||||
// new
|
||||
type T1 int
|
||||
type T2 = T1
|
||||
|
||||
// client
|
||||
switch x.(type) {
|
||||
case T1:
|
||||
case T2:
|
||||
} // fails with new because two cases have the same type
|
||||
```
|
||||
This sort of incompatibility is sufficiently esoteric to ignore; the tool allows
|
||||
merging types.
|
||||
|
||||
## First Attempt at a Definition
|
||||
|
||||
Our first attempt at defining compatibility captures the idea that all the
|
||||
exported names in the old package must have compatible equivalents in the new
|
||||
package.
|
||||
|
||||
A new package is compatible with an old one if and only if:
|
||||
- For every exported package-level name in the old package, the same name is
|
||||
declared in the new at package level, and
|
||||
- the names denote the same kind of object (e.g. both are variables), and
|
||||
- the types of the objects are compatible.
|
||||
|
||||
We will work out the details (and make some corrections) below, but it is clear
|
||||
already that we will need to determine what makes two types compatible. And
|
||||
whatever the definition of type compatibility, it's certainly true that if two
|
||||
types are the same, they are compatible. So we will need to decide what makes an
|
||||
old and new type the same. We will call this sameness relation _correspondence_.
|
||||
|
||||
## Type Correspondence
|
||||
|
||||
Go already has a definition of when two types are the same:
|
||||
[type identity](https://golang.org/ref/spec#Type_identity).
|
||||
But identity isn't adequate for our purpose: it says that two defined
|
||||
types are identical if they arise from the same definition, but it's unclear
|
||||
what "same" means when talking about two different packages (or two versions of
|
||||
a single package).
|
||||
|
||||
The obvious change to the definition of identity is to require that old and new
|
||||
[defined types](https://golang.org/ref/spec#Type_definitions)
|
||||
have the same name instead. But that doesn't work either, for two
|
||||
reasons. First, type aliases can equate two defined types with different names:
|
||||
|
||||
```
|
||||
// old
|
||||
type E int
|
||||
|
||||
// new
|
||||
type t int
|
||||
type E = t
|
||||
```
|
||||
Second, an unexported type can be renamed:
|
||||
|
||||
```
|
||||
// old
|
||||
type u1 int
|
||||
var V u1
|
||||
|
||||
// new
|
||||
type u2 int
|
||||
var V u2
|
||||
```
|
||||
Here, even though `u1` and `u2` are unexported, their exported fields and
|
||||
methods are visible to clients, so they are part of the API. But since the name
|
||||
`u1` is not visible to clients, it can be changed compatibly. We say that `u1`
|
||||
and `u2` are _exposed_: a type is exposed if a client package can declare variables of that type.
|
||||
|
||||
We will say that an old defined type _corresponds_ to a new one if they have the
|
||||
same name, or one can be renamed to the other without otherwise changing the
|
||||
API. In the first example above, old `E` and new `t` correspond. In the second,
|
||||
old `u1` and new `u2` correspond.
|
||||
|
||||
Two or more old defined types can correspond to a single new type: we consider
|
||||
"merging" two types into one to be a compatible change. As mentioned above,
|
||||
code that uses both names in a type switch will fail, but we deliberately ignore
|
||||
this case. However, a single old type can correspond to only one new type.
|
||||
|
||||
So far, we've explained what correspondence means for defined types. To extend
|
||||
the definition to all types, we parallel the language's definition of type
|
||||
identity. So, for instance, an old and a new slice type correspond if their
|
||||
element types correspond.
|
||||
|
||||
## Definition of Compatibility
|
||||
|
||||
We can now present the definition of compatibility used by `apidiff`.
|
||||
|
||||
### Package Compatibility
|
||||
|
||||
> A new package is compatible with an old one if:
|
||||
>1. Each exported name in the old package's scope also appears in the new
|
||||
>package's scope, and the object (constant, variable, function or type) denoted
|
||||
>by that name in the old package is compatible with the object denoted by the
|
||||
>name in the new package, and
|
||||
>2. For every exposed type that implements an exposed interface in the old package,
|
||||
> its corresponding type should implement the corresponding interface in the new package.
|
||||
>
|
||||
>Otherwise the packages are incompatible.
|
||||
|
||||
As an aside, the tool also finds exported names in the new package that are not
|
||||
exported in the old, and marks them as compatible changes.
|
||||
|
||||
Clause 2 is discussed further in "Whole-Package Compatibility."
|
||||
|
||||
### Object Compatibility
|
||||
|
||||
This section provides compatibility rules for constants, variables, functions
|
||||
and types.
|
||||
|
||||
#### Constants
|
||||
|
||||
>A new exported constant is compatible with an old one of the same name if and only if
|
||||
>1. Their types correspond, and
|
||||
>2. Their values are identical.
|
||||
|
||||
It is tempting to allow changing a typed constant to an untyped one. That may
|
||||
seem harmless, but it can break code like this:
|
||||
|
||||
```
|
||||
// old
|
||||
const C int64 = 1
|
||||
|
||||
// new
|
||||
const C = 1
|
||||
|
||||
// client
|
||||
var x = C // old type is int64, new is int
|
||||
var y int64 = x // fails with new: different types in assignment
|
||||
```
|
||||
|
||||
A change to the value of a constant can break compatibility if the value is used
|
||||
in an array type:
|
||||
|
||||
```
|
||||
// old
|
||||
const C = 1
|
||||
|
||||
// new
|
||||
const C = 2
|
||||
|
||||
// client
|
||||
var a [C]int = [1]int{} // fails with new because [2]int and [1]int are different types
|
||||
```
|
||||
Changes to constant values are rare, and determining whether they are compatible
|
||||
or not is better left to the user, so the tool reports them.
|
||||
|
||||
#### Variables
|
||||
|
||||
>A new exported variable is compatible with an old one of the same name if and
|
||||
>only if their types correspond.
|
||||
|
||||
Correspondence doesn't look past names, so this rule does not prevent adding a
|
||||
field to `MyStruct` if the package declares `var V MyStruct`. It does, however, mean that
|
||||
|
||||
```
|
||||
var V struct { X int }
|
||||
```
|
||||
is incompatible with
|
||||
```
|
||||
var V struct { X, Y int }
|
||||
```
|
||||
I discuss this at length below in the section "Compatibility, Types and Names."
|
||||
|
||||
#### Functions
|
||||
|
||||
>A new exported function or variable is compatible with an old function of the
|
||||
>same name if and only if their types (signatures) correspond.
|
||||
|
||||
This rule captures the fact that, although many signature changes are compatible
|
||||
for all call sites, none are compatible for assignment:
|
||||
|
||||
```
|
||||
var v func(int) = pkg.F
|
||||
```
|
||||
Here, `F` must be of type `func(int)` and not, for instance, `func(...int)` or `func(interface{})`.
|
||||
|
||||
Note that the rule permits changing a function to a variable. This is a common
|
||||
practice, usually done for test stubbing, and cannot break any code at compile
|
||||
time.
|
||||
|
||||
#### Exported Types
|
||||
|
||||
> A new exported type is compatible with an old one if and only if their
|
||||
> names are the same and their types correspond.
|
||||
|
||||
This rule seems far too strict. But, ignoring aliases for the moment, it demands only
|
||||
that the old and new _defined_ types correspond. Consider:
|
||||
```
|
||||
// old
|
||||
type T struct { X int }
|
||||
|
||||
// new
|
||||
type T struct { X, Y int }
|
||||
```
|
||||
The addition of `Y` is a compatible change, because this rule does not require
|
||||
that the struct literals have to correspond, only that the defined types
|
||||
denoted by `T` must correspond. (Remember that correspondence stops at type
|
||||
names.)
|
||||
|
||||
If one type is an alias that refers to the corresponding defined type, the
|
||||
situation is the same:
|
||||
|
||||
```
|
||||
// old
|
||||
type T struct { X int }
|
||||
|
||||
// new
|
||||
type u struct { X, Y int }
|
||||
type T = u
|
||||
```
|
||||
Here, the only requirement is that old `T` corresponds to new `u`, not that the
|
||||
struct types correspond. (We can't tell from this snippet that the old `T` and
|
||||
the new `u` do correspond; that depends on whether `u` replaces `T` throughout
|
||||
the API.)
|
||||
|
||||
However, the following change is incompatible, because the names do not
|
||||
denote corresponding types:
|
||||
|
||||
```
|
||||
// old
|
||||
type T = struct { X int }
|
||||
|
||||
// new
|
||||
type T = struct { X, Y int }
|
||||
```
|
||||
### Type Literal Compatibility
|
||||
|
||||
Only five kinds of types can differ compatibly: defined types, structs,
|
||||
interfaces, channels and numeric types. We only consider the compatibility of
|
||||
the last four when they are the underlying type of a defined type. See
|
||||
"Compatibility, Types and Names" for a rationale.
|
||||
|
||||
We justify the compatibility rules by enumerating all the ways a type
|
||||
can be used, and by showing that the allowed changes cannot break any code that
|
||||
uses values of the type in those ways.
|
||||
|
||||
Values of all types can be used in assignments (including argument passing and
|
||||
function return), but we do not require that old and new types are assignment
|
||||
compatible. That is because we assume that the old and new packages are never
|
||||
used together: any given binary will link in either the old package or the new.
|
||||
So in describing how a type can be used in the sections below, we omit
|
||||
assignment.
|
||||
|
||||
Any type can also be used in a type assertion or conversion. The changes we allow
|
||||
below may affect the run-time behavior of these operations, but they cannot affect
|
||||
whether they compile. The only such breaking change would be to change
|
||||
the type `T` in an assertion `x.T` so that it no longer implements the interface
|
||||
type of `x`; but the rules for interfaces below disallow that.
|
||||
|
||||
> A new type is compatible with an old one if and only if they correspond, or
|
||||
> one of the cases below applies.
|
||||
|
||||
#### Defined Types
|
||||
|
||||
Other than assignment, the only ways to use a defined type are to access its
|
||||
methods, or to make use of the properties of its underlying type. Rule 2 below
|
||||
covers the latter, and rules 3 and 4 cover the former.
|
||||
|
||||
> A new defined type is compatible with an old one if and only if all of the
|
||||
> following hold:
|
||||
>1. They correspond.
|
||||
>2. Their underlying types are compatible.
|
||||
>3. The new exported value method set is a superset of the old.
|
||||
>4. The new exported pointer method set is a superset of the old.
|
||||
|
||||
An exported method set is a method set with all unexported methods removed.
|
||||
When comparing methods of a method set, we require identical names and
|
||||
corresponding signatures.
|
||||
|
||||
Removing an exported method is clearly a breaking change. But removing an
|
||||
unexported one (or changing its signature) can be breaking as well, if it
|
||||
results in the type no longer implementing an interface. See "Whole-Package
|
||||
Compatibility," below.
|
||||
|
||||
#### Channels
|
||||
|
||||
> A new channel type is compatible with an old one if
|
||||
> 1. The element types correspond, and
|
||||
> 2. Either the directions are the same, or the new type has no direction.
|
||||
|
||||
Other than assignment, the only ways to use values of a channel type are to send
|
||||
and receive on them, to close them, and to use them as map keys. Changes to a
|
||||
channel type cannot cause code that closes a channel or uses it as a map key to
|
||||
fail to compile, so we need not consider those operations.
|
||||
|
||||
Rule 1 ensures that any operations on the values sent or received will compile.
|
||||
Rule 2 captures the fact that any program that compiles with a directed channel
|
||||
must use either only sends, or only receives, so allowing the other operation
|
||||
by removing the channel direction cannot break any code.
|
||||
|
||||
|
||||
#### Interfaces
|
||||
|
||||
> A new interface is compatible with an old one if and only if:
|
||||
> 1. The old interface does not have an unexported method, and it corresponds
|
||||
> to the new interfaces (i.e. they have the same method set), or
|
||||
> 2. The old interface has an unexported method and the new exported method set is a
|
||||
> superset of the old.
|
||||
|
||||
Other than assignment, the only ways to use an interface are to implement it,
|
||||
embed it, or call one of its methods. (Interface values can also be used as map
|
||||
keys, but that cannot cause a compile-time error.)
|
||||
|
||||
Certainly, removing an exported method from an interface could break a client
|
||||
call, so neither rule allows it.
|
||||
|
||||
Rule 1 also disallows adding a method to an interface without an existing unexported
|
||||
method. Such an interface can be implemented in client code. If adding a method
|
||||
were allowed, a type that implements the old interface could fail to implement
|
||||
the new one:
|
||||
|
||||
```
|
||||
type I interface { M1() } // old
|
||||
type I interface { M1(); M2() } // new
|
||||
|
||||
// client
|
||||
type t struct{}
|
||||
func (t) M1() {}
|
||||
var i pkg.I = t{} // fails with new, because t lacks M2
|
||||
```
|
||||
|
||||
Rule 2 is based on the observation that if an interface has an unexported
|
||||
method, the only way a client can implement it is to embed it.
|
||||
Adding a method is compatible in this case, because the embedding struct will
|
||||
continue to implement the interface. Adding a method also cannot break any call
|
||||
sites, since no program that compiles could have any such call sites.
|
||||
|
||||
#### Structs
|
||||
|
||||
> A new struct is compatible with an old one if all of the following hold:
|
||||
> 1. The new set of top-level exported fields is a superset of the old.
|
||||
> 2. The new set of _selectable_ exported fields is a superset of the old.
|
||||
> 3. If the old struct is comparable, so is the new one.
|
||||
|
||||
The set of selectable exported fields is the set of exported fields `F`
|
||||
such that `x.F` is a valid selector expression for a value `x` of the struct
|
||||
type. `F` may be at the top level of the struct, or it may be a field of an
|
||||
embedded struct.
|
||||
|
||||
Two fields are the same if they have the same name and corresponding types.
|
||||
|
||||
Other than assignment, there are only four ways to use a struct: write a struct
|
||||
literal, select a field, use a value of the struct as a map key, or compare two
|
||||
values for equality. The first clause ensures that struct literals compile; the
|
||||
second, that selections compile; and the third, that equality expressions and
|
||||
map index expressions compile.
|
||||
|
||||
#### Numeric Types
|
||||
|
||||
> A new numeric type is compatible with an old one if and only if they are
|
||||
> both unsigned integers, both signed integers, both floats or both complex
|
||||
> types, and the new one is at least as large as the old on both 32-bit and
|
||||
> 64-bit architectures.
|
||||
|
||||
Other than in assignments, numeric types appear in arithmetic and comparison
|
||||
expressions. Since all arithmetic operations but shifts (see below) require that
|
||||
operand types be identical, and by assumption the old and new types underly
|
||||
defined types (see "Compatibility, Types and Names," below), there is no way for
|
||||
client code to write an arithmetic expression that compiles with operands of the
|
||||
old type but not the new.
|
||||
|
||||
Numeric types can also appear in type switches and type assertions. Again, since
|
||||
the old and new types underly defined types, type switches and type assertions
|
||||
that compiled using the old defined type will continue to compile with the new
|
||||
defined type.
|
||||
|
||||
Going from an unsigned to a signed integer type is an incompatible change for
|
||||
the sole reason that only an unsigned type can appear as the right operand of a
|
||||
shift. If this rule is relaxed, then changes from an unsigned type to a larger
|
||||
signed type would be compatible. See [this
|
||||
issue](https://github.com/golang/go/issues/19113).
|
||||
|
||||
Only integer types can be used in bitwise and shift operations, and for indexing
|
||||
slices and arrays. That is why switching from an integer to a floating-point
|
||||
type--even one that can represent all values of the integer type--is an
|
||||
incompatible change.
|
||||
|
||||
|
||||
Conversions from floating-point to complex types or vice versa are not permitted
|
||||
(the predeclared functions real, imag, and complex must be used instead). To
|
||||
prevent valid floating-point or complex conversions from becoming invalid,
|
||||
changing a floating-point type to a complex type or vice versa is considered an
|
||||
incompatible change.
|
||||
|
||||
Although conversions between any two integer types are valid, assigning a
|
||||
constant value to a variable of integer type that is too small to represent the
|
||||
constant is not permitted. That is why the only compatible changes are to
|
||||
a new type whose values are a superset of the old. The requirement that the new
|
||||
set of values must include the old on both 32-bit and 64-bit machines allows
|
||||
conversions from `int32` to `int` and from `int` to `int64`, but not the other
|
||||
direction; and similarly for `uint`.
|
||||
|
||||
Changing a type to or from `uintptr` is considered an incompatible change. Since
|
||||
its size is not specified, there is no way to know whether the new type's values
|
||||
are a superset of the old type's.
|
||||
|
||||
## Whole-Package Compatibility
|
||||
|
||||
Some changes that are compatible for a single type are not compatible when the
|
||||
package is considered as a whole. For example, if you remove an unexported
|
||||
method on a defined type, it may no longer implement an interface of the
|
||||
package. This can break client code:
|
||||
|
||||
```
|
||||
// old
|
||||
type T int
|
||||
func (T) m() {}
|
||||
type I interface { m() }
|
||||
|
||||
// new
|
||||
type T int // no method m anymore
|
||||
|
||||
// client
|
||||
var i pkg.I = pkg.T{} // fails with new because T lacks m
|
||||
```
|
||||
|
||||
Similarly, adding a method to an interface can cause defined types
|
||||
in the package to stop implementing it.
|
||||
|
||||
The second clause in the definition for package compatibility handles these
|
||||
cases. To repeat:
|
||||
> 2. For every exposed type that implements an exposed interface in the old package,
|
||||
> its corresponding type should implement the corresponding interface in the new package.
|
||||
Recall that a type is exposed if it is part of the package's API, even if it is
|
||||
unexported.
|
||||
|
||||
Other incompatibilities that involve more than one type in the package can arise
|
||||
whenever two types with identical underlying types exist in the old or new
|
||||
package. Here, a change "splits" an identical underlying type into two, breaking
|
||||
conversions:
|
||||
|
||||
```
|
||||
// old
|
||||
type B struct { X int }
|
||||
type C struct { X int }
|
||||
|
||||
// new
|
||||
type B struct { X int }
|
||||
type C struct { X, Y int }
|
||||
|
||||
// client
|
||||
var b B
|
||||
_ = C(b) // fails with new: cannot convert B to C
|
||||
```
|
||||
Finally, changes that are compatible for the package in which they occur can
|
||||
break downstream packages. That can happen even if they involve unexported
|
||||
methods, thanks to embedding.
|
||||
|
||||
The definitions given here don't account for these sorts of problems.
|
||||
|
||||
|
||||
## Compatibility, Types and Names
|
||||
|
||||
The above definitions state that the only types that can differ compatibly are
|
||||
defined types and the types that underly them. Changes to other type literals
|
||||
are considered incompatible. For instance, it is considered an incompatible
|
||||
change to add a field to the struct in this variable declaration:
|
||||
|
||||
```
|
||||
var V struct { X int }
|
||||
```
|
||||
or this alias definition:
|
||||
```
|
||||
type T = struct { X int }
|
||||
```
|
||||
|
||||
We make this choice to keep the definition of compatibility (relatively) simple.
|
||||
A more precise definition could, for instance, distinguish between
|
||||
|
||||
```
|
||||
func F(struct { X int })
|
||||
```
|
||||
where any changes to the struct are incompatible, and
|
||||
|
||||
```
|
||||
func F(struct { X, u int })
|
||||
```
|
||||
where adding a field is compatible (since clients cannot write the signature,
|
||||
and thus cannot assign `F` to a variable of the signature type). The definition
|
||||
should then also allow other function signature changes that only require
|
||||
call-site compatibility, like
|
||||
|
||||
```
|
||||
func F(struct { X, u int }, ...int)
|
||||
```
|
||||
The result would be a much more complex definition with little benefit, since
|
||||
the examples in this section rarely arise in practice.
|
||||
@@ -0,0 +1,227 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// TODO: test swap corresponding types (e.g. u1 <-> u2 and u2 <-> u1)
|
||||
// TODO: test exported alias refers to something in another package -- does correspondence work then?
|
||||
// TODO: CODE COVERAGE
|
||||
// TODO: note that we may miss correspondences because we bail early when we compare a signature (e.g. when lengths differ; we could do up to the shorter)
|
||||
// TODO: if you add an unexported method to an exposed interface, you have to check that
|
||||
// every exposed type that previously implemented the interface still does. Otherwise
|
||||
// an external assignment of the exposed type to the interface type could fail.
|
||||
// TODO: check constant values: large values aren't representable by some types.
|
||||
// TODO: Document all the incompatibilities we don't check for.
|
||||
|
||||
package apidiff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/constant"
|
||||
"go/token"
|
||||
"go/types"
|
||||
|
||||
"golang.org/x/tools/internal/aliases"
|
||||
)
|
||||
|
||||
// Changes reports on the differences between the APIs of the old and new packages.
|
||||
// It classifies each difference as either compatible or incompatible (breaking.) For
|
||||
// a detailed discussion of what constitutes an incompatible change, see the package
|
||||
// documentation.
|
||||
func Changes(old, new *types.Package) Report {
|
||||
d := newDiffer(old, new)
|
||||
d.checkPackage()
|
||||
r := Report{}
|
||||
for _, m := range d.incompatibles.collect() {
|
||||
r.Changes = append(r.Changes, Change{Message: m, Compatible: false})
|
||||
}
|
||||
for _, m := range d.compatibles.collect() {
|
||||
r.Changes = append(r.Changes, Change{Message: m, Compatible: true})
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type differ struct {
|
||||
old, new *types.Package
|
||||
// Correspondences between named types.
|
||||
// Even though it is the named types (*types.Named) that correspond, we use
|
||||
// *types.TypeName as a map key because they are canonical.
|
||||
// The values can be either named types or basic types.
|
||||
correspondMap map[*types.TypeName]types.Type
|
||||
|
||||
// Messages.
|
||||
incompatibles messageSet
|
||||
compatibles messageSet
|
||||
}
|
||||
|
||||
func newDiffer(old, new *types.Package) *differ {
|
||||
return &differ{
|
||||
old: old,
|
||||
new: new,
|
||||
correspondMap: map[*types.TypeName]types.Type{},
|
||||
incompatibles: messageSet{},
|
||||
compatibles: messageSet{},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *differ) incompatible(obj types.Object, part, format string, args ...interface{}) {
|
||||
addMessage(d.incompatibles, obj, part, format, args)
|
||||
}
|
||||
|
||||
func (d *differ) compatible(obj types.Object, part, format string, args ...interface{}) {
|
||||
addMessage(d.compatibles, obj, part, format, args)
|
||||
}
|
||||
|
||||
func addMessage(ms messageSet, obj types.Object, part, format string, args []interface{}) {
|
||||
ms.add(obj, part, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (d *differ) checkPackage() {
|
||||
// Old changes.
|
||||
for _, name := range d.old.Scope().Names() {
|
||||
oldobj := d.old.Scope().Lookup(name)
|
||||
if !oldobj.Exported() {
|
||||
continue
|
||||
}
|
||||
newobj := d.new.Scope().Lookup(name)
|
||||
if newobj == nil {
|
||||
d.incompatible(oldobj, "", "removed")
|
||||
continue
|
||||
}
|
||||
d.checkObjects(oldobj, newobj)
|
||||
}
|
||||
// New additions.
|
||||
for _, name := range d.new.Scope().Names() {
|
||||
newobj := d.new.Scope().Lookup(name)
|
||||
if newobj.Exported() && d.old.Scope().Lookup(name) == nil {
|
||||
d.compatible(newobj, "", "added")
|
||||
}
|
||||
}
|
||||
|
||||
// Whole-package satisfaction.
|
||||
// For every old exposed interface oIface and its corresponding new interface nIface...
|
||||
for otn1, nt1 := range d.correspondMap {
|
||||
oIface, ok := otn1.Type().Underlying().(*types.Interface)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
nIface, ok := nt1.Underlying().(*types.Interface)
|
||||
if !ok {
|
||||
// If nt1 isn't an interface but otn1 is, then that's an incompatibility that
|
||||
// we've already noticed, so there's no need to do anything here.
|
||||
continue
|
||||
}
|
||||
// For every old type that implements oIface, its corresponding new type must implement
|
||||
// nIface.
|
||||
for otn2, nt2 := range d.correspondMap {
|
||||
if otn1 == otn2 {
|
||||
continue
|
||||
}
|
||||
if types.Implements(otn2.Type(), oIface) && !types.Implements(nt2, nIface) {
|
||||
d.incompatible(otn2, "", "no longer implements %s", objectString(otn1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *differ) checkObjects(old, new types.Object) {
|
||||
switch old := old.(type) {
|
||||
case *types.Const:
|
||||
if new, ok := new.(*types.Const); ok {
|
||||
d.constChanges(old, new)
|
||||
return
|
||||
}
|
||||
case *types.Var:
|
||||
if new, ok := new.(*types.Var); ok {
|
||||
d.checkCorrespondence(old, "", old.Type(), new.Type())
|
||||
return
|
||||
}
|
||||
case *types.Func:
|
||||
switch new := new.(type) {
|
||||
case *types.Func:
|
||||
d.checkCorrespondence(old, "", old.Type(), new.Type())
|
||||
return
|
||||
case *types.Var:
|
||||
d.compatible(old, "", "changed from func to var")
|
||||
d.checkCorrespondence(old, "", old.Type(), new.Type())
|
||||
return
|
||||
|
||||
}
|
||||
case *types.TypeName:
|
||||
if new, ok := new.(*types.TypeName); ok {
|
||||
d.checkCorrespondence(old, "", old.Type(), new.Type())
|
||||
return
|
||||
}
|
||||
default:
|
||||
panic("unexpected obj type")
|
||||
}
|
||||
// Here if kind of type changed.
|
||||
d.incompatible(old, "", "changed from %s to %s",
|
||||
objectKindString(old), objectKindString(new))
|
||||
}
|
||||
|
||||
// Compare two constants.
|
||||
func (d *differ) constChanges(old, new *types.Const) {
|
||||
ot := old.Type()
|
||||
nt := new.Type()
|
||||
// Check for change of type.
|
||||
if !d.correspond(ot, nt) {
|
||||
d.typeChanged(old, "", ot, nt)
|
||||
return
|
||||
}
|
||||
// Check for change of value.
|
||||
// We know the types are the same, so constant.Compare shouldn't panic.
|
||||
if !constant.Compare(old.Val(), token.EQL, new.Val()) {
|
||||
d.incompatible(old, "", "value changed from %s to %s", old.Val(), new.Val())
|
||||
}
|
||||
}
|
||||
|
||||
func objectKindString(obj types.Object) string {
|
||||
switch obj.(type) {
|
||||
case *types.Const:
|
||||
return "const"
|
||||
case *types.Var:
|
||||
return "var"
|
||||
case *types.Func:
|
||||
return "func"
|
||||
case *types.TypeName:
|
||||
return "type"
|
||||
default:
|
||||
return "???"
|
||||
}
|
||||
}
|
||||
|
||||
func (d *differ) checkCorrespondence(obj types.Object, part string, old, new types.Type) {
|
||||
if !d.correspond(old, new) {
|
||||
d.typeChanged(obj, part, old, new)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *differ) typeChanged(obj types.Object, part string, old, new types.Type) {
|
||||
old = removeNamesFromSignature(old)
|
||||
new = removeNamesFromSignature(new)
|
||||
olds := types.TypeString(old, types.RelativeTo(d.old))
|
||||
news := types.TypeString(new, types.RelativeTo(d.new))
|
||||
d.incompatible(obj, part, "changed from %s to %s", olds, news)
|
||||
}
|
||||
|
||||
// go/types always includes the argument and result names when formatting a signature.
|
||||
// Since these can change without affecting compatibility, we don't want users to
|
||||
// be distracted by them, so we remove them.
|
||||
func removeNamesFromSignature(t types.Type) types.Type {
|
||||
t = aliases.Unalias(t)
|
||||
sig, ok := t.(*types.Signature)
|
||||
if !ok {
|
||||
return t
|
||||
}
|
||||
|
||||
dename := func(p *types.Tuple) *types.Tuple {
|
||||
var vars []*types.Var
|
||||
for i := 0; i < p.Len(); i++ {
|
||||
v := p.At(i)
|
||||
vars = append(vars, types.NewVar(v.Pos(), v.Pkg(), "", aliases.Unalias(v.Type())))
|
||||
}
|
||||
return types.NewTuple(vars...)
|
||||
}
|
||||
|
||||
return types.NewSignature(sig.Recv(), dename(sig.Params()), dename(sig.Results()), sig.Variadic())
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package apidiff
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"go/types"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/tools/go/packages"
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
)
|
||||
|
||||
func TestChanges(t *testing.T) {
|
||||
dir, err := os.MkdirTemp("", "apidiff_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir = filepath.Join(dir, "go")
|
||||
wanti, wantc := splitIntoPackages(t, dir)
|
||||
defer os.RemoveAll(dir)
|
||||
sort.Strings(wanti)
|
||||
sort.Strings(wantc)
|
||||
|
||||
oldpkg, err := load(t, "apidiff/old", dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
newpkg, err := load(t, "apidiff/new", dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
report := Changes(oldpkg.Types, newpkg.Types)
|
||||
|
||||
got := report.messages(false)
|
||||
if diff := cmp.Diff(wanti, got); diff != "" {
|
||||
t.Errorf("incompatibles (-want +got):\n%s", diff)
|
||||
}
|
||||
got = report.messages(true)
|
||||
if diff := cmp.Diff(wantc, got); diff != "" {
|
||||
t.Errorf("compatibles (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func splitIntoPackages(t *testing.T, dir string) (incompatibles, compatibles []string) {
|
||||
// Read the input file line by line.
|
||||
// Write a line into the old or new package,
|
||||
// dependent on comments.
|
||||
// Also collect expected messages.
|
||||
f, err := os.Open("testdata/tests.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(dir, "src", "apidiff"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "src", "apidiff", "go.mod"), []byte("module apidiff\n"), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oldd := filepath.Join(dir, "src/apidiff/old")
|
||||
newd := filepath.Join(dir, "src/apidiff/new")
|
||||
if err := os.MkdirAll(oldd, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(newd, 0700); err != nil && !os.IsExist(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
oldf, err := os.Create(filepath.Join(oldd, "old.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
newf, err := os.Create(filepath.Join(newd, "new.go"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wl := func(f *os.File, line string) {
|
||||
if _, err := fmt.Fprintln(f, line); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
writeBoth := func(line string) { wl(oldf, line); wl(newf, line) }
|
||||
writeln := writeBoth
|
||||
s := bufio.NewScanner(f)
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
tl := strings.TrimSpace(line)
|
||||
switch {
|
||||
case tl == "// old":
|
||||
writeln = func(line string) { wl(oldf, line) }
|
||||
case tl == "// new":
|
||||
writeln = func(line string) { wl(newf, line) }
|
||||
case tl == "// both":
|
||||
writeln = writeBoth
|
||||
case strings.HasPrefix(tl, "// i "):
|
||||
incompatibles = append(incompatibles, strings.TrimSpace(tl[4:]))
|
||||
case strings.HasPrefix(tl, "// c "):
|
||||
compatibles = append(compatibles, strings.TrimSpace(tl[4:]))
|
||||
default:
|
||||
writeln(line)
|
||||
}
|
||||
}
|
||||
if s.Err() != nil {
|
||||
t.Fatal(s.Err())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func load(t *testing.T, importPath, goPath string) (*packages.Package, error) {
|
||||
testenv.NeedsGoPackages(t)
|
||||
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.LoadTypes,
|
||||
}
|
||||
if goPath != "" {
|
||||
cfg.Env = append(os.Environ(), "GOPATH="+goPath)
|
||||
cfg.Dir = filepath.Join(goPath, "src", filepath.FromSlash(importPath))
|
||||
}
|
||||
pkgs, err := packages.Load(cfg, importPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pkgs[0].Errors) > 0 {
|
||||
return nil, pkgs[0].Errors[0]
|
||||
}
|
||||
return pkgs[0], nil
|
||||
}
|
||||
|
||||
func TestExportedFields(t *testing.T) {
|
||||
pkg, err := load(t, "golang.org/x/tools/internal/apidiff/testdata/exported_fields", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
typeof := func(name string) types.Type {
|
||||
return pkg.Types.Scope().Lookup(name).Type()
|
||||
}
|
||||
|
||||
s := typeof("S")
|
||||
su := s.(*types.Named).Underlying().(*types.Struct)
|
||||
|
||||
ef := exportedSelectableFields(su)
|
||||
wants := []struct {
|
||||
name string
|
||||
typ types.Type
|
||||
}{
|
||||
{"A1", typeof("A1")},
|
||||
{"D", types.Typ[types.Bool]},
|
||||
{"E", types.Typ[types.Int]},
|
||||
{"F", typeof("F")},
|
||||
{"S", types.NewPointer(s)},
|
||||
}
|
||||
|
||||
if got, want := len(ef), len(wants); got != want {
|
||||
t.Errorf("got %d fields, want %d\n%+v", got, want, ef)
|
||||
}
|
||||
for _, w := range wants {
|
||||
if got := ef[w.name]; got != nil && !types.Identical(got.Type(), w.typ) {
|
||||
t.Errorf("%s: got %v, want %v", w.name, got.Type(), w.typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package apidiff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/types"
|
||||
"reflect"
|
||||
|
||||
"golang.org/x/tools/internal/aliases"
|
||||
"golang.org/x/tools/internal/typesinternal"
|
||||
)
|
||||
|
||||
func (d *differ) checkCompatible(otn *types.TypeName, old, new types.Type) {
|
||||
old = aliases.Unalias(old)
|
||||
new = aliases.Unalias(new)
|
||||
switch old := old.(type) {
|
||||
case *types.Interface:
|
||||
if new, ok := new.(*types.Interface); ok {
|
||||
d.checkCompatibleInterface(otn, old, new)
|
||||
return
|
||||
}
|
||||
|
||||
case *types.Struct:
|
||||
if new, ok := new.(*types.Struct); ok {
|
||||
d.checkCompatibleStruct(otn, old, new)
|
||||
return
|
||||
}
|
||||
|
||||
case *types.Chan:
|
||||
if new, ok := new.(*types.Chan); ok {
|
||||
d.checkCompatibleChan(otn, old, new)
|
||||
return
|
||||
}
|
||||
|
||||
case *types.Basic:
|
||||
if new, ok := new.(*types.Basic); ok {
|
||||
d.checkCompatibleBasic(otn, old, new)
|
||||
return
|
||||
}
|
||||
|
||||
case *types.Named:
|
||||
panic("unreachable")
|
||||
|
||||
default:
|
||||
d.checkCorrespondence(otn, "", old, new)
|
||||
return
|
||||
|
||||
}
|
||||
// Here if old and new are different kinds of types.
|
||||
d.typeChanged(otn, "", old, new)
|
||||
}
|
||||
|
||||
func (d *differ) checkCompatibleChan(otn *types.TypeName, old, new *types.Chan) {
|
||||
d.checkCorrespondence(otn, ", element type", old.Elem(), new.Elem())
|
||||
if old.Dir() != new.Dir() {
|
||||
if new.Dir() == types.SendRecv {
|
||||
d.compatible(otn, "", "removed direction")
|
||||
} else {
|
||||
d.incompatible(otn, "", "changed direction")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *differ) checkCompatibleBasic(otn *types.TypeName, old, new *types.Basic) {
|
||||
// Certain changes to numeric types are compatible. Approximately, the info must
|
||||
// be the same, and the new values must be a superset of the old.
|
||||
if old.Kind() == new.Kind() {
|
||||
// old and new are identical
|
||||
return
|
||||
}
|
||||
if compatibleBasics[[2]types.BasicKind{old.Kind(), new.Kind()}] {
|
||||
d.compatible(otn, "", "changed from %s to %s", old, new)
|
||||
} else {
|
||||
d.typeChanged(otn, "", old, new)
|
||||
}
|
||||
}
|
||||
|
||||
// All pairs (old, new) of compatible basic types.
|
||||
var compatibleBasics = map[[2]types.BasicKind]bool{
|
||||
{types.Uint8, types.Uint16}: true,
|
||||
{types.Uint8, types.Uint32}: true,
|
||||
{types.Uint8, types.Uint}: true,
|
||||
{types.Uint8, types.Uint64}: true,
|
||||
{types.Uint16, types.Uint32}: true,
|
||||
{types.Uint16, types.Uint}: true,
|
||||
{types.Uint16, types.Uint64}: true,
|
||||
{types.Uint32, types.Uint}: true,
|
||||
{types.Uint32, types.Uint64}: true,
|
||||
{types.Uint, types.Uint64}: true,
|
||||
{types.Int8, types.Int16}: true,
|
||||
{types.Int8, types.Int32}: true,
|
||||
{types.Int8, types.Int}: true,
|
||||
{types.Int8, types.Int64}: true,
|
||||
{types.Int16, types.Int32}: true,
|
||||
{types.Int16, types.Int}: true,
|
||||
{types.Int16, types.Int64}: true,
|
||||
{types.Int32, types.Int}: true,
|
||||
{types.Int32, types.Int64}: true,
|
||||
{types.Int, types.Int64}: true,
|
||||
{types.Float32, types.Float64}: true,
|
||||
{types.Complex64, types.Complex128}: true,
|
||||
}
|
||||
|
||||
// Interface compatibility:
|
||||
// If the old interface has an unexported method, the new interface is compatible
|
||||
// if its exported method set is a superset of the old. (Users could not implement,
|
||||
// only embed.)
|
||||
//
|
||||
// If the old interface did not have an unexported method, the new interface is
|
||||
// compatible if its exported method set is the same as the old, and it has no
|
||||
// unexported methods. (Adding an unexported method makes the interface
|
||||
// unimplementable outside the package.)
|
||||
//
|
||||
// TODO: must also check that if any methods were added or removed, every exposed
|
||||
// type in the package that implemented the interface in old still implements it in
|
||||
// new. Otherwise external assignments could fail.
|
||||
func (d *differ) checkCompatibleInterface(otn *types.TypeName, old, new *types.Interface) {
|
||||
// Method sets are checked in checkCompatibleDefined.
|
||||
|
||||
// Does the old interface have an unexported method?
|
||||
if unexportedMethod(old) != nil {
|
||||
d.checkMethodSet(otn, old, new, additionsCompatible)
|
||||
} else {
|
||||
// Perform an equivalence check, but with more information.
|
||||
d.checkMethodSet(otn, old, new, additionsIncompatible)
|
||||
if u := unexportedMethod(new); u != nil {
|
||||
d.incompatible(otn, u.Name(), "added unexported method")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return an unexported method from the method set of t, or nil if there are none.
|
||||
func unexportedMethod(t *types.Interface) *types.Func {
|
||||
for i := 0; i < t.NumMethods(); i++ {
|
||||
if m := t.Method(i); !m.Exported() {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// We need to check three things for structs:
|
||||
// 1. The set of exported fields must be compatible. This ensures that keyed struct
|
||||
// literals continue to compile. (There is no compatibility guarantee for unkeyed
|
||||
// struct literals.)
|
||||
// 2. The set of exported *selectable* fields must be compatible. This includes the exported
|
||||
// fields of all embedded structs. This ensures that selections continue to compile.
|
||||
// 3. If the old struct is comparable, so must the new one be. This ensures that equality
|
||||
// expressions and uses of struct values as map keys continue to compile.
|
||||
//
|
||||
// An unexported embedded struct can't appear in a struct literal outside the
|
||||
// package, so it doesn't have to be present, or have the same name, in the new
|
||||
// struct.
|
||||
//
|
||||
// Field tags are ignored: they have no compile-time implications.
|
||||
func (d *differ) checkCompatibleStruct(obj types.Object, old, new *types.Struct) {
|
||||
d.checkCompatibleObjectSets(obj, exportedFields(old), exportedFields(new))
|
||||
d.checkCompatibleObjectSets(obj, exportedSelectableFields(old), exportedSelectableFields(new))
|
||||
// Removing comparability from a struct is an incompatible change.
|
||||
if types.Comparable(old) && !types.Comparable(new) {
|
||||
d.incompatible(obj, "", "old is comparable, new is not")
|
||||
}
|
||||
}
|
||||
|
||||
// exportedFields collects all the immediate fields of the struct that are exported.
|
||||
// This is also the set of exported keys for keyed struct literals.
|
||||
func exportedFields(s *types.Struct) map[string]types.Object {
|
||||
m := map[string]types.Object{}
|
||||
for i := 0; i < s.NumFields(); i++ {
|
||||
f := s.Field(i)
|
||||
if f.Exported() {
|
||||
m[f.Name()] = f
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// exportedSelectableFields collects all the exported fields of the struct, including
|
||||
// exported fields of embedded structs.
|
||||
//
|
||||
// We traverse the struct breadth-first, because of the rule that a lower-depth field
|
||||
// shadows one at a higher depth.
|
||||
func exportedSelectableFields(s *types.Struct) map[string]types.Object {
|
||||
var (
|
||||
m = map[string]types.Object{}
|
||||
next []*types.Struct // embedded structs at the next depth
|
||||
seen []*types.Struct // to handle recursive embedding
|
||||
)
|
||||
for cur := []*types.Struct{s}; len(cur) > 0; cur, next = next, nil {
|
||||
seen = append(seen, cur...)
|
||||
// We only want to consider unambiguous fields. Ambiguous fields (where there
|
||||
// is more than one field of the same name at the same level) are legal, but
|
||||
// cannot be selected.
|
||||
for name, f := range unambiguousFields(cur) {
|
||||
// Record an exported field we haven't seen before. If we have seen it,
|
||||
// it occurred a lower depth, so it shadows this field.
|
||||
if f.Exported() && m[name] == nil {
|
||||
m[name] = f
|
||||
}
|
||||
// Remember embedded structs for processing at the next depth,
|
||||
// but only if we haven't seen the struct at this depth or above.
|
||||
if !f.Anonymous() {
|
||||
continue
|
||||
}
|
||||
t := f.Type().Underlying()
|
||||
if p, ok := t.(*types.Pointer); ok {
|
||||
t = p.Elem().Underlying()
|
||||
}
|
||||
if t, ok := t.(*types.Struct); ok && !contains(seen, t) {
|
||||
next = append(next, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func contains(ts []*types.Struct, t *types.Struct) bool {
|
||||
for _, s := range ts {
|
||||
if types.Identical(s, t) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Given a set of structs at the same depth, the unambiguous fields are the ones whose
|
||||
// names appear exactly once.
|
||||
func unambiguousFields(structs []*types.Struct) map[string]*types.Var {
|
||||
fields := map[string]*types.Var{}
|
||||
seen := map[string]bool{}
|
||||
for _, s := range structs {
|
||||
for i := 0; i < s.NumFields(); i++ {
|
||||
f := s.Field(i)
|
||||
name := f.Name()
|
||||
if seen[name] {
|
||||
delete(fields, name)
|
||||
} else {
|
||||
seen[name] = true
|
||||
fields[name] = f
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// Anything removed or change from the old set is an incompatible change.
|
||||
// Anything added to the new set is a compatible change.
|
||||
func (d *differ) checkCompatibleObjectSets(obj types.Object, old, new map[string]types.Object) {
|
||||
for name, oldo := range old {
|
||||
newo := new[name]
|
||||
if newo == nil {
|
||||
d.incompatible(obj, name, "removed")
|
||||
} else {
|
||||
d.checkCorrespondence(obj, name, oldo.Type(), newo.Type())
|
||||
}
|
||||
}
|
||||
for name := range new {
|
||||
if old[name] == nil {
|
||||
d.compatible(obj, name, "added")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *differ) checkCompatibleDefined(otn *types.TypeName, old *types.Named, new types.Type) {
|
||||
// We've already checked that old and new correspond.
|
||||
d.checkCompatible(otn, old.Underlying(), new.Underlying())
|
||||
// If there are different kinds of types (e.g. struct and interface), don't bother checking
|
||||
// the method sets.
|
||||
if reflect.TypeOf(old.Underlying()) != reflect.TypeOf(new.Underlying()) {
|
||||
return
|
||||
}
|
||||
// Interface method sets are checked in checkCompatibleInterface.
|
||||
if types.IsInterface(old) {
|
||||
return
|
||||
}
|
||||
|
||||
// A new method set is compatible with an old if the new exported methods are a superset of the old.
|
||||
d.checkMethodSet(otn, old, new, additionsCompatible)
|
||||
d.checkMethodSet(otn, types.NewPointer(old), types.NewPointer(new), additionsCompatible)
|
||||
}
|
||||
|
||||
const (
|
||||
additionsCompatible = true
|
||||
additionsIncompatible = false
|
||||
)
|
||||
|
||||
func (d *differ) checkMethodSet(otn *types.TypeName, oldt, newt types.Type, addcompat bool) {
|
||||
// TODO: find a way to use checkCompatibleObjectSets for this.
|
||||
oldMethodSet := exportedMethods(oldt)
|
||||
newMethodSet := exportedMethods(newt)
|
||||
msname := otn.Name()
|
||||
if _, ok := aliases.Unalias(oldt).(*types.Pointer); ok {
|
||||
msname = "*" + msname
|
||||
}
|
||||
for name, oldMethod := range oldMethodSet {
|
||||
newMethod := newMethodSet[name]
|
||||
if newMethod == nil {
|
||||
var part string
|
||||
// Due to embedding, it's possible that the method's receiver type is not
|
||||
// the same as the defined type whose method set we're looking at. So for
|
||||
// a type T with removed method M that is embedded in some other type U,
|
||||
// we will generate two "removed" messages for T.M, one for its own type
|
||||
// T and one for the embedded type U. We want both messages to appear,
|
||||
// but the messageSet dedup logic will allow only one message for a given
|
||||
// object. So use the part string to distinguish them.
|
||||
recv := oldMethod.Type().(*types.Signature).Recv()
|
||||
if _, named := typesinternal.ReceiverNamed(recv); named.Obj() != otn {
|
||||
part = fmt.Sprintf(", method set of %s", msname)
|
||||
}
|
||||
d.incompatible(oldMethod, part, "removed")
|
||||
} else {
|
||||
obj := oldMethod
|
||||
// If a value method is changed to a pointer method and has a signature
|
||||
// change, then we can get two messages for the same method definition: one
|
||||
// for the value method set that says it's removed, and another for the
|
||||
// pointer method set that says it changed. To keep both messages (since
|
||||
// messageSet dedups), use newMethod for the second. (Slight hack.)
|
||||
if !hasPointerReceiver(oldMethod) && hasPointerReceiver(newMethod) {
|
||||
obj = newMethod
|
||||
}
|
||||
d.checkCorrespondence(obj, "", oldMethod.Type(), newMethod.Type())
|
||||
}
|
||||
}
|
||||
|
||||
// Check for added methods.
|
||||
for name, newMethod := range newMethodSet {
|
||||
if oldMethodSet[name] == nil {
|
||||
if addcompat {
|
||||
d.compatible(newMethod, "", "added")
|
||||
} else {
|
||||
d.incompatible(newMethod, "", "added")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// exportedMethods collects all the exported methods of type's method set.
|
||||
func exportedMethods(t types.Type) map[string]*types.Func {
|
||||
m := make(map[string]*types.Func)
|
||||
ms := types.NewMethodSet(t)
|
||||
for i := 0; i < ms.Len(); i++ {
|
||||
obj := ms.At(i).Obj().(*types.Func)
|
||||
if obj.Exported() {
|
||||
m[obj.Name()] = obj
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func hasPointerReceiver(method *types.Func) bool {
|
||||
isptr, _ := typesinternal.ReceiverNamed(method.Type().(*types.Signature).Recv())
|
||||
return isptr
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package apidiff
|
||||
|
||||
import (
|
||||
"go/types"
|
||||
"sort"
|
||||
|
||||
"golang.org/x/tools/internal/aliases"
|
||||
)
|
||||
|
||||
// Two types are correspond if they are identical except for defined types,
|
||||
// which must correspond.
|
||||
//
|
||||
// Two defined types correspond if they can be interchanged in the old and new APIs,
|
||||
// possibly after a renaming.
|
||||
//
|
||||
// This is not a pure function. If we come across named types while traversing,
|
||||
// we establish correspondence.
|
||||
func (d *differ) correspond(old, new types.Type) bool {
|
||||
return d.corr(old, new, nil)
|
||||
}
|
||||
|
||||
// corr determines whether old and new correspond. The argument p is a list of
|
||||
// known interface identities, to avoid infinite recursion.
|
||||
//
|
||||
// corr calls itself recursively as much as possible, to establish more
|
||||
// correspondences and so check more of the API. E.g. if the new function has more
|
||||
// parameters than the old, compare all the old ones before returning false.
|
||||
//
|
||||
// Compare this to the implementation of go/types.Identical.
|
||||
func (d *differ) corr(old, new types.Type, p *ifacePair) bool {
|
||||
// Structure copied from types.Identical.
|
||||
old = aliases.Unalias(old)
|
||||
new = aliases.Unalias(new)
|
||||
switch old := old.(type) {
|
||||
case *types.Basic:
|
||||
return types.Identical(old, new)
|
||||
|
||||
case *types.Array:
|
||||
if new, ok := new.(*types.Array); ok {
|
||||
return d.corr(old.Elem(), new.Elem(), p) && old.Len() == new.Len()
|
||||
}
|
||||
|
||||
case *types.Slice:
|
||||
if new, ok := new.(*types.Slice); ok {
|
||||
return d.corr(old.Elem(), new.Elem(), p)
|
||||
}
|
||||
|
||||
case *types.Map:
|
||||
if new, ok := new.(*types.Map); ok {
|
||||
return d.corr(old.Key(), new.Key(), p) && d.corr(old.Elem(), new.Elem(), p)
|
||||
}
|
||||
|
||||
case *types.Chan:
|
||||
if new, ok := new.(*types.Chan); ok {
|
||||
return d.corr(old.Elem(), new.Elem(), p) && old.Dir() == new.Dir()
|
||||
}
|
||||
|
||||
case *types.Pointer:
|
||||
if new, ok := new.(*types.Pointer); ok {
|
||||
return d.corr(old.Elem(), new.Elem(), p)
|
||||
}
|
||||
|
||||
case *types.Signature:
|
||||
if new, ok := new.(*types.Signature); ok {
|
||||
pe := d.corr(old.Params(), new.Params(), p)
|
||||
re := d.corr(old.Results(), new.Results(), p)
|
||||
return old.Variadic() == new.Variadic() && pe && re
|
||||
}
|
||||
|
||||
case *types.Tuple:
|
||||
if new, ok := new.(*types.Tuple); ok {
|
||||
for i := 0; i < old.Len(); i++ {
|
||||
if i >= new.Len() || !d.corr(old.At(i).Type(), new.At(i).Type(), p) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return old.Len() == new.Len()
|
||||
}
|
||||
|
||||
case *types.Struct:
|
||||
if new, ok := new.(*types.Struct); ok {
|
||||
for i := 0; i < old.NumFields(); i++ {
|
||||
if i >= new.NumFields() {
|
||||
return false
|
||||
}
|
||||
of := old.Field(i)
|
||||
nf := new.Field(i)
|
||||
if of.Anonymous() != nf.Anonymous() ||
|
||||
old.Tag(i) != new.Tag(i) ||
|
||||
!d.corr(of.Type(), nf.Type(), p) ||
|
||||
!d.corrFieldNames(of, nf) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return old.NumFields() == new.NumFields()
|
||||
}
|
||||
|
||||
case *types.Interface:
|
||||
if new, ok := new.(*types.Interface); ok {
|
||||
// Deal with circularity. See the comment in types.Identical.
|
||||
q := &ifacePair{old, new, p}
|
||||
for p != nil {
|
||||
if p.identical(q) {
|
||||
return true // same pair was compared before
|
||||
}
|
||||
p = p.prev
|
||||
}
|
||||
oldms := d.sortedMethods(old)
|
||||
newms := d.sortedMethods(new)
|
||||
for i, om := range oldms {
|
||||
if i >= len(newms) {
|
||||
return false
|
||||
}
|
||||
nm := newms[i]
|
||||
if d.methodID(om) != d.methodID(nm) || !d.corr(om.Type(), nm.Type(), q) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return old.NumMethods() == new.NumMethods()
|
||||
}
|
||||
|
||||
case *types.Named:
|
||||
if new, ok := new.(*types.Named); ok {
|
||||
return d.establishCorrespondence(old, new)
|
||||
}
|
||||
if new, ok := new.(*types.Basic); ok {
|
||||
// Basic types are defined types, too, so we have to support them.
|
||||
|
||||
return d.establishCorrespondence(old, new)
|
||||
}
|
||||
|
||||
default:
|
||||
panic("unknown type kind")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare old and new field names. We are determining correspondence across packages,
|
||||
// so just compare names, not packages. For an unexported, embedded field of named
|
||||
// type (non-named embedded fields are possible with aliases), we check that the type
|
||||
// names correspond. We check the types for correspondence before this is called, so
|
||||
// we've established correspondence.
|
||||
func (d *differ) corrFieldNames(of, nf *types.Var) bool {
|
||||
if of.Anonymous() && nf.Anonymous() && !of.Exported() && !nf.Exported() {
|
||||
if on, ok := of.Type().(*types.Named); ok {
|
||||
nn := nf.Type().(*types.Named)
|
||||
return d.establishCorrespondence(on, nn)
|
||||
}
|
||||
}
|
||||
return of.Name() == nf.Name()
|
||||
}
|
||||
|
||||
// Establish that old corresponds with new if it does not already
|
||||
// correspond to something else.
|
||||
func (d *differ) establishCorrespondence(old *types.Named, new types.Type) bool {
|
||||
oldname := old.Obj()
|
||||
oldc := d.correspondMap[oldname]
|
||||
if oldc == nil {
|
||||
// For now, assume the types don't correspond unless they are from the old
|
||||
// and new packages, respectively.
|
||||
//
|
||||
// This is too conservative. For instance,
|
||||
// [old] type A = q.B; [new] type A q.C
|
||||
// could be OK if in package q, B is an alias for C.
|
||||
// Or, using p as the name of the current old/new packages:
|
||||
// [old] type A = q.B; [new] type A int
|
||||
// could be OK if in q,
|
||||
// [old] type B int; [new] type B = p.A
|
||||
// In this case, p.A and q.B name the same type in both old and new worlds.
|
||||
// Note that this case doesn't imply circular package imports: it's possible
|
||||
// that in the old world, p imports q, but in the new, q imports p.
|
||||
//
|
||||
// However, if we didn't do something here, then we'd incorrectly allow cases
|
||||
// like the first one above in which q.B is not an alias for q.C
|
||||
//
|
||||
// What we should do is check that the old type, in the new world's package
|
||||
// of the same path, doesn't correspond to something other than the new type.
|
||||
// That is a bit hard, because there is no easy way to find a new package
|
||||
// matching an old one.
|
||||
if newn, ok := new.(*types.Named); ok {
|
||||
if old.Obj().Pkg() != d.old || newn.Obj().Pkg() != d.new {
|
||||
return old.Obj().Id() == newn.Obj().Id()
|
||||
}
|
||||
}
|
||||
// If there is no correspondence, create one.
|
||||
d.correspondMap[oldname] = new
|
||||
// Check that the corresponding types are compatible.
|
||||
d.checkCompatibleDefined(oldname, old, new)
|
||||
return true
|
||||
}
|
||||
return types.Identical(oldc, new)
|
||||
}
|
||||
|
||||
func (d *differ) sortedMethods(iface *types.Interface) []*types.Func {
|
||||
ms := make([]*types.Func, iface.NumMethods())
|
||||
for i := 0; i < iface.NumMethods(); i++ {
|
||||
ms[i] = iface.Method(i)
|
||||
}
|
||||
sort.Slice(ms, func(i, j int) bool { return d.methodID(ms[i]) < d.methodID(ms[j]) })
|
||||
return ms
|
||||
}
|
||||
|
||||
func (d *differ) methodID(m *types.Func) string {
|
||||
// If the method belongs to one of the two packages being compared, use
|
||||
// just its name even if it's unexported. That lets us treat unexported names
|
||||
// from the old and new packages as equal.
|
||||
if m.Pkg() == d.old || m.Pkg() == d.new {
|
||||
return m.Name()
|
||||
}
|
||||
return m.Id()
|
||||
}
|
||||
|
||||
// Copied from the go/types package:
|
||||
|
||||
// An ifacePair is a node in a stack of interface type pairs compared for identity.
|
||||
type ifacePair struct {
|
||||
x, y *types.Interface
|
||||
prev *ifacePair
|
||||
}
|
||||
|
||||
func (p *ifacePair) identical(q *ifacePair) bool {
|
||||
return p.x == q.x && p.y == q.y || p.x == q.y && p.y == q.x
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// TODO: show that two-non-empty dotjoin can happen, by using an anon struct as a field type
|
||||
// TODO: don't report removed/changed methods for both value and pointer method sets?
|
||||
|
||||
package apidiff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/types"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// There can be at most one message for each object or part thereof.
|
||||
// Parts include interface methods and struct fields.
|
||||
//
|
||||
// The part thing is necessary. Method (Func) objects have sufficient info, but field
|
||||
// Vars do not: they just have a field name and a type, without the enclosing struct.
|
||||
type messageSet map[types.Object]map[string]string
|
||||
|
||||
// Add a message for obj and part, overwriting a previous message
|
||||
// (shouldn't happen).
|
||||
// obj is required but part can be empty.
|
||||
func (m messageSet) add(obj types.Object, part, msg string) {
|
||||
s := m[obj]
|
||||
if s == nil {
|
||||
s = map[string]string{}
|
||||
m[obj] = s
|
||||
}
|
||||
if f, ok := s[part]; ok && f != msg {
|
||||
fmt.Printf("! second, different message for obj %s, part %q\n", obj, part)
|
||||
fmt.Printf(" first: %s\n", f)
|
||||
fmt.Printf(" second: %s\n", msg)
|
||||
}
|
||||
s[part] = msg
|
||||
}
|
||||
|
||||
func (m messageSet) collect() []string {
|
||||
var s []string
|
||||
for obj, parts := range m {
|
||||
// Format each object name relative to its own package.
|
||||
objstring := objectString(obj)
|
||||
for part, msg := range parts {
|
||||
var p string
|
||||
|
||||
if strings.HasPrefix(part, ",") {
|
||||
p = objstring + part
|
||||
} else {
|
||||
p = dotjoin(objstring, part)
|
||||
}
|
||||
s = append(s, p+": "+msg)
|
||||
}
|
||||
}
|
||||
sort.Strings(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func objectString(obj types.Object) string {
|
||||
if f, ok := obj.(*types.Func); ok {
|
||||
sig := f.Type().(*types.Signature)
|
||||
if recv := sig.Recv(); recv != nil {
|
||||
tn := types.TypeString(recv.Type(), types.RelativeTo(obj.Pkg()))
|
||||
if tn[0] == '*' {
|
||||
tn = "(" + tn + ")"
|
||||
}
|
||||
return fmt.Sprintf("%s.%s", tn, obj.Name())
|
||||
}
|
||||
}
|
||||
return obj.Name()
|
||||
}
|
||||
|
||||
func dotjoin(s1, s2 string) string {
|
||||
if s1 == "" {
|
||||
return s2
|
||||
}
|
||||
if s2 == "" {
|
||||
return s1
|
||||
}
|
||||
return s1 + "." + s2
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package apidiff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Report describes the changes detected by Changes.
|
||||
type Report struct {
|
||||
Changes []Change
|
||||
}
|
||||
|
||||
// A Change describes a single API change.
|
||||
type Change struct {
|
||||
Message string
|
||||
Compatible bool
|
||||
}
|
||||
|
||||
func (r Report) messages(compatible bool) []string {
|
||||
var msgs []string
|
||||
for _, c := range r.Changes {
|
||||
if c.Compatible == compatible {
|
||||
msgs = append(msgs, c.Message)
|
||||
}
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
func (r Report) String() string {
|
||||
var buf bytes.Buffer
|
||||
if err := r.Text(&buf); err != nil {
|
||||
return fmt.Sprintf("!!%v", err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (r Report) Text(w io.Writer) error {
|
||||
if err := r.TextIncompatible(w, true); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.TextCompatible(w)
|
||||
}
|
||||
|
||||
func (r Report) TextIncompatible(w io.Writer, withHeader bool) error {
|
||||
if withHeader {
|
||||
return r.writeMessages(w, "Incompatible changes:", r.messages(false))
|
||||
}
|
||||
return r.writeMessages(w, "", r.messages(false))
|
||||
}
|
||||
|
||||
func (r Report) TextCompatible(w io.Writer) error {
|
||||
return r.writeMessages(w, "Compatible changes:", r.messages(true))
|
||||
}
|
||||
|
||||
func (r Report) writeMessages(w io.Writer, header string, msgs []string) error {
|
||||
if len(msgs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if header != "" {
|
||||
if _, err := fmt.Fprintf(w, "%s\n", header); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, m := range msgs {
|
||||
if _, err := fmt.Fprintf(w, "- %s\n", m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package exported_fields
|
||||
|
||||
// Used for testing exportedFields.
|
||||
// Its exported fields are:
|
||||
// A1 [1]int
|
||||
// D bool
|
||||
// E int
|
||||
// F F
|
||||
// S *S
|
||||
type (
|
||||
S struct {
|
||||
int
|
||||
*embed2
|
||||
embed
|
||||
E int // shadows embed.E
|
||||
alias
|
||||
A1
|
||||
*S
|
||||
}
|
||||
|
||||
A1 [1]int
|
||||
|
||||
embed struct {
|
||||
E string
|
||||
}
|
||||
|
||||
embed2 struct {
|
||||
embed3
|
||||
F // shadows embed3.F
|
||||
}
|
||||
embed3 struct {
|
||||
F bool
|
||||
}
|
||||
alias = struct{ D bool }
|
||||
|
||||
F int
|
||||
)
|
||||
@@ -0,0 +1,924 @@
|
||||
// This file is split into two packages, old and new.
|
||||
// It is syntactically valid Go so that gofmt can process it.
|
||||
//
|
||||
// If a comment begins with: Then:
|
||||
// old write subsequent lines to the "old" package
|
||||
// new write subsequent lines to the "new" package
|
||||
// both write subsequent lines to both packages
|
||||
// c expect a compatible error with the following text
|
||||
// i expect an incompatible error with the following text
|
||||
package ignore
|
||||
|
||||
// both
|
||||
import "io"
|
||||
|
||||
//////////////// Basics
|
||||
|
||||
//// Same type in both: OK.
|
||||
// both
|
||||
type A int
|
||||
|
||||
//// Changing the type is an incompatible change.
|
||||
// old
|
||||
type B int
|
||||
|
||||
// new
|
||||
// i B: changed from int to string
|
||||
type B string
|
||||
|
||||
//// Adding a new type, whether alias or not, is a compatible change.
|
||||
// new
|
||||
// c AA: added
|
||||
type AA = A
|
||||
|
||||
// c B1: added
|
||||
type B1 bool
|
||||
|
||||
//// Change of type for an unexported name doesn't matter...
|
||||
// old
|
||||
type t int
|
||||
|
||||
// new
|
||||
type t string // OK: t isn't part of the API
|
||||
|
||||
//// ...unless it is exposed.
|
||||
// both
|
||||
var V2 u
|
||||
|
||||
// old
|
||||
type u string
|
||||
|
||||
// new
|
||||
// i u: changed from string to int
|
||||
type u int
|
||||
|
||||
//// An exposed, unexported type can be renamed.
|
||||
// both
|
||||
type u2 int
|
||||
|
||||
// old
|
||||
type u1 int
|
||||
|
||||
var V5 u1
|
||||
|
||||
// new
|
||||
var V5 u2 // OK: V5 has changed type, but old u1 corresponds to new u2
|
||||
|
||||
//// Splitting a single type into two is an incompatible change.
|
||||
// both
|
||||
type u3 int
|
||||
|
||||
// old
|
||||
type (
|
||||
Split1 = u1
|
||||
Split2 = u1
|
||||
)
|
||||
|
||||
// new
|
||||
type (
|
||||
Split1 = u2 // OK, since old u1 corresponds to new u2
|
||||
|
||||
// This tries to make u1 correspond to u3
|
||||
// i Split2: changed from u1 to u3
|
||||
Split2 = u3
|
||||
)
|
||||
|
||||
//// Merging two types into one is OK.
|
||||
// old
|
||||
type (
|
||||
GoodMerge1 = u2
|
||||
GoodMerge2 = u3
|
||||
)
|
||||
|
||||
// new
|
||||
type (
|
||||
GoodMerge1 = u3
|
||||
GoodMerge2 = u3
|
||||
)
|
||||
|
||||
//// Merging isn't OK here because a method is lost.
|
||||
// both
|
||||
type u4 int
|
||||
|
||||
func (u4) M() {}
|
||||
|
||||
// old
|
||||
type (
|
||||
BadMerge1 = u3
|
||||
BadMerge2 = u4
|
||||
)
|
||||
|
||||
// new
|
||||
type (
|
||||
BadMerge1 = u3
|
||||
// i u4.M: removed
|
||||
// What's really happening here is that old u4 corresponds to new u3,
|
||||
// and new u3's method set is not a superset of old u4's.
|
||||
BadMerge2 = u3
|
||||
)
|
||||
|
||||
// old
|
||||
type Rem int
|
||||
|
||||
// new
|
||||
// i Rem: removed
|
||||
|
||||
//////////////// Constants
|
||||
|
||||
//// type changes
|
||||
// old
|
||||
const (
|
||||
C1 = 1
|
||||
C2 int = 2
|
||||
C3 = 3
|
||||
C4 u1 = 4
|
||||
)
|
||||
|
||||
var V8 int
|
||||
|
||||
// new
|
||||
const (
|
||||
// i C1: changed from untyped int to untyped string
|
||||
C1 = "1"
|
||||
// i C2: changed from int to untyped int
|
||||
C2 = -1
|
||||
// i C3: changed from untyped int to int
|
||||
C3 int = 3
|
||||
// i V8: changed from var to const
|
||||
V8 int = 1
|
||||
C4 u2 = 4 // OK: u1 corresponds to u2
|
||||
)
|
||||
|
||||
// value change
|
||||
// old
|
||||
const (
|
||||
Cr1 = 1
|
||||
Cr2 = "2"
|
||||
Cr3 = 3.5
|
||||
Cr4 = complex(0, 4.1)
|
||||
)
|
||||
|
||||
// new
|
||||
const (
|
||||
// i Cr1: value changed from 1 to -1
|
||||
Cr1 = -1
|
||||
// i Cr2: value changed from "2" to "3"
|
||||
Cr2 = "3"
|
||||
// i Cr3: value changed from 3.5 to 3.8
|
||||
Cr3 = 3.8
|
||||
// i Cr4: value changed from (0 + 4.1i) to (4.1 + 0i)
|
||||
Cr4 = complex(4.1, 0)
|
||||
)
|
||||
|
||||
//////////////// Variables
|
||||
|
||||
//// simple type changes
|
||||
// old
|
||||
var (
|
||||
V1 string
|
||||
V3 A
|
||||
V7 <-chan int
|
||||
)
|
||||
|
||||
// new
|
||||
var (
|
||||
// i V1: changed from string to []string
|
||||
V1 []string
|
||||
V3 A // OK: same
|
||||
// i V7: changed from <-chan int to chan int
|
||||
V7 chan int
|
||||
)
|
||||
|
||||
//// interface type changes
|
||||
// old
|
||||
var (
|
||||
V9 interface{ M() }
|
||||
V10 interface{ M() }
|
||||
V11 interface{ M() }
|
||||
)
|
||||
|
||||
// new
|
||||
var (
|
||||
// i V9: changed from interface{M()} to interface{}
|
||||
V9 interface{}
|
||||
// i V10: changed from interface{M()} to interface{M(); M2()}
|
||||
V10 interface {
|
||||
M2()
|
||||
M()
|
||||
}
|
||||
// i V11: changed from interface{M()} to interface{M(int)}
|
||||
V11 interface{ M(int) }
|
||||
)
|
||||
|
||||
//// struct type changes
|
||||
// old
|
||||
var (
|
||||
VS1 struct{ A, B int }
|
||||
VS2 struct{ A, B int }
|
||||
VS3 struct{ A, B int }
|
||||
VS4 struct {
|
||||
A int
|
||||
u1
|
||||
}
|
||||
)
|
||||
|
||||
// new
|
||||
var (
|
||||
// i VS1: changed from struct{A int; B int} to struct{B int; A int}
|
||||
VS1 struct{ B, A int }
|
||||
// i VS2: changed from struct{A int; B int} to struct{A int}
|
||||
VS2 struct{ A int }
|
||||
// i VS3: changed from struct{A int; B int} to struct{A int; B int; C int}
|
||||
VS3 struct{ A, B, C int }
|
||||
VS4 struct {
|
||||
A int
|
||||
u2
|
||||
}
|
||||
)
|
||||
|
||||
//////////////// Types
|
||||
|
||||
// old
|
||||
const C5 = 3
|
||||
|
||||
type (
|
||||
A1 [1]int
|
||||
A2 [2]int
|
||||
A3 [C5]int
|
||||
)
|
||||
|
||||
// new
|
||||
// i C5: value changed from 3 to 4
|
||||
const C5 = 4
|
||||
|
||||
type (
|
||||
A1 [1]int
|
||||
// i A2: changed from [2]int to [2]bool
|
||||
A2 [2]bool
|
||||
// i A3: changed from [3]int to [4]int
|
||||
A3 [C5]int
|
||||
)
|
||||
|
||||
// old
|
||||
type (
|
||||
Sl []int
|
||||
P1 *int
|
||||
P2 *u1
|
||||
)
|
||||
|
||||
// new
|
||||
type (
|
||||
// i Sl: changed from []int to []string
|
||||
Sl []string
|
||||
// i P1: changed from *int to **bool
|
||||
P1 **bool
|
||||
P2 *u2 // OK: u1 corresponds to u2
|
||||
)
|
||||
|
||||
// old
|
||||
type Bc1 int32
|
||||
type Bc2 uint
|
||||
type Bc3 float32
|
||||
type Bc4 complex64
|
||||
|
||||
// new
|
||||
// c Bc1: changed from int32 to int
|
||||
type Bc1 int
|
||||
|
||||
// c Bc2: changed from uint to uint64
|
||||
type Bc2 uint64
|
||||
|
||||
// c Bc3: changed from float32 to float64
|
||||
type Bc3 float64
|
||||
|
||||
// c Bc4: changed from complex64 to complex128
|
||||
type Bc4 complex128
|
||||
|
||||
// old
|
||||
type Bi1 int32
|
||||
type Bi2 uint
|
||||
type Bi3 float64
|
||||
type Bi4 complex128
|
||||
|
||||
// new
|
||||
// i Bi1: changed from int32 to int16
|
||||
type Bi1 int16
|
||||
|
||||
// i Bi2: changed from uint to uint32
|
||||
type Bi2 uint32
|
||||
|
||||
// i Bi3: changed from float64 to float32
|
||||
type Bi3 float32
|
||||
|
||||
// i Bi4: changed from complex128 to complex64
|
||||
type Bi4 complex64
|
||||
|
||||
// old
|
||||
type (
|
||||
M1 map[string]int
|
||||
M2 map[string]int
|
||||
M3 map[string]int
|
||||
)
|
||||
|
||||
// new
|
||||
type (
|
||||
M1 map[string]int
|
||||
// i M2: changed from map[string]int to map[int]int
|
||||
M2 map[int]int
|
||||
// i M3: changed from map[string]int to map[string]string
|
||||
M3 map[string]string
|
||||
)
|
||||
|
||||
// old
|
||||
type (
|
||||
Ch1 chan int
|
||||
Ch2 <-chan int
|
||||
Ch3 chan int
|
||||
Ch4 <-chan int
|
||||
)
|
||||
|
||||
// new
|
||||
type (
|
||||
// i Ch1, element type: changed from int to bool
|
||||
Ch1 chan bool
|
||||
// i Ch2: changed direction
|
||||
Ch2 chan<- int
|
||||
// i Ch3: changed direction
|
||||
Ch3 <-chan int
|
||||
// c Ch4: removed direction
|
||||
Ch4 chan int
|
||||
)
|
||||
|
||||
// old
|
||||
type I1 interface {
|
||||
M1()
|
||||
M2()
|
||||
}
|
||||
|
||||
// new
|
||||
type I1 interface {
|
||||
// M1()
|
||||
// i I1.M1: removed
|
||||
M2(int)
|
||||
// i I1.M2: changed from func() to func(int)
|
||||
M3()
|
||||
// i I1.M3: added
|
||||
m()
|
||||
// i I1.m: added unexported method
|
||||
}
|
||||
|
||||
// old
|
||||
type I2 interface {
|
||||
M1()
|
||||
m()
|
||||
}
|
||||
|
||||
// new
|
||||
type I2 interface {
|
||||
M1()
|
||||
// m() Removing an unexported method is OK.
|
||||
m2() // OK, because old already had an unexported method
|
||||
// c I2.M2: added
|
||||
M2()
|
||||
}
|
||||
|
||||
// old
|
||||
type I3 interface {
|
||||
io.Reader
|
||||
M()
|
||||
}
|
||||
|
||||
// new
|
||||
// OK: what matters is the method set; the name of the embedded
|
||||
// interface isn't important.
|
||||
type I3 interface {
|
||||
M()
|
||||
Read([]byte) (int, error)
|
||||
}
|
||||
|
||||
// old
|
||||
type I4 io.Writer
|
||||
|
||||
// new
|
||||
// OK: in both, I4 is a distinct type from io.Writer, and
|
||||
// the old and new I4s have the same method set.
|
||||
type I4 interface {
|
||||
Write([]byte) (int, error)
|
||||
}
|
||||
|
||||
// old
|
||||
type I5 = io.Writer
|
||||
|
||||
// new
|
||||
// i I5: changed from io.Writer to I5
|
||||
// In old, I5 and io.Writer are the same type; in new,
|
||||
// they are different. That can break something like:
|
||||
// var _ func(io.Writer) = func(pkg.I6) {}
|
||||
type I5 io.Writer
|
||||
|
||||
// old
|
||||
type I6 interface{ Write([]byte) (int, error) }
|
||||
|
||||
// new
|
||||
// i I6: changed from I6 to io.Writer
|
||||
// Similar to the above.
|
||||
type I6 = io.Writer
|
||||
|
||||
//// correspondence with a basic type
|
||||
// Basic types are technically defined types, but they aren't
|
||||
// represented that way in go/types, so the cases below are special.
|
||||
|
||||
// both
|
||||
type T1 int
|
||||
|
||||
// old
|
||||
var VT1 T1
|
||||
|
||||
// new
|
||||
// i VT1: changed from T1 to int
|
||||
// This fails because old T1 corresponds to both int and new T1.
|
||||
var VT1 int
|
||||
|
||||
// old
|
||||
type t2 int
|
||||
|
||||
var VT2 t2
|
||||
|
||||
// new
|
||||
// OK: t2 corresponds to int. It's fine that old t2
|
||||
// doesn't exist in new.
|
||||
var VT2 int
|
||||
|
||||
// both
|
||||
type t3 int
|
||||
|
||||
func (t3) M() {}
|
||||
|
||||
// old
|
||||
var VT3 t3
|
||||
|
||||
// new
|
||||
// i t3.M: removed
|
||||
// Here the change from t3 to int is incompatible
|
||||
// because old t3 has an exported method.
|
||||
var VT3 int
|
||||
|
||||
// old
|
||||
var VT4 int
|
||||
|
||||
// new
|
||||
type t4 int
|
||||
|
||||
// i VT4: changed from int to t4
|
||||
// This is incompatible because of code like
|
||||
// VT4 + int(1)
|
||||
// which works in old but fails in new.
|
||||
// The difference from the above cases is that
|
||||
// in those, we were merging two types into one;
|
||||
// here, we are splitting int into t4 and int.
|
||||
var VT4 t4
|
||||
|
||||
//////////////// Functions
|
||||
|
||||
// old
|
||||
func F1(a int, b string) map[u1]A { return nil }
|
||||
func F2(int) {}
|
||||
func F3(int) {}
|
||||
func F4(int) int { return 0 }
|
||||
func F5(int) int { return 0 }
|
||||
func F6(int) {}
|
||||
func F7(interface{}) {}
|
||||
|
||||
// new
|
||||
func F1(c int, d string) map[u2]AA { return nil } //OK: same (since u1 corresponds to u2)
|
||||
|
||||
// i F2: changed from func(int) to func(int) bool
|
||||
func F2(int) bool { return true }
|
||||
|
||||
// i F3: changed from func(int) to func(int, int)
|
||||
func F3(int, int) {}
|
||||
|
||||
// i F4: changed from func(int) int to func(bool) int
|
||||
func F4(bool) int { return 0 }
|
||||
|
||||
// i F5: changed from func(int) int to func(int) string
|
||||
func F5(int) string { return "" }
|
||||
|
||||
// i F6: changed from func(int) to func(...int)
|
||||
func F6(...int) {}
|
||||
|
||||
// i F7: changed from func(interface{}) to func(interface{x()})
|
||||
func F7(a interface{ x() }) {}
|
||||
|
||||
// old
|
||||
func F8(bool) {}
|
||||
|
||||
// new
|
||||
// c F8: changed from func to var
|
||||
var F8 func(bool)
|
||||
|
||||
// old
|
||||
var F9 func(int)
|
||||
|
||||
// new
|
||||
// i F9: changed from var to func
|
||||
func F9(int) {}
|
||||
|
||||
// both
|
||||
// OK, even though new S1 is incompatible with old S1 (see below)
|
||||
func F10(S1) {}
|
||||
|
||||
//////////////// Structs
|
||||
|
||||
// old
|
||||
type S1 struct {
|
||||
A int
|
||||
B string
|
||||
C bool
|
||||
d float32
|
||||
}
|
||||
|
||||
// new
|
||||
type S1 = s1
|
||||
|
||||
type s1 struct {
|
||||
C chan int
|
||||
// i S1.C: changed from bool to chan int
|
||||
A int
|
||||
// i S1.B: removed
|
||||
// i S1: old is comparable, new is not
|
||||
x []int
|
||||
d float32
|
||||
E bool
|
||||
// c S1.E: added
|
||||
}
|
||||
|
||||
// old
|
||||
type embed struct {
|
||||
E string
|
||||
}
|
||||
|
||||
type S2 struct {
|
||||
A int
|
||||
embed
|
||||
}
|
||||
|
||||
// new
|
||||
type embedx struct {
|
||||
E string
|
||||
}
|
||||
|
||||
type S2 struct {
|
||||
embedx // OK: the unexported embedded field changed names, but the exported field didn't
|
||||
A int
|
||||
}
|
||||
|
||||
// both
|
||||
type F int
|
||||
|
||||
// old
|
||||
type S3 struct {
|
||||
A int
|
||||
embed
|
||||
}
|
||||
|
||||
// new
|
||||
type embed struct{ F int }
|
||||
|
||||
type S3 struct {
|
||||
// i S3.E: removed
|
||||
embed
|
||||
// c S3.F: added
|
||||
A int
|
||||
}
|
||||
|
||||
// old
|
||||
type embed2 struct {
|
||||
embed3
|
||||
F // shadows embed3.F
|
||||
}
|
||||
|
||||
type embed3 struct {
|
||||
F bool
|
||||
}
|
||||
|
||||
type alias = struct{ D bool }
|
||||
|
||||
type S4 struct {
|
||||
int
|
||||
*embed2
|
||||
embed
|
||||
E int // shadows embed.E
|
||||
alias
|
||||
A1
|
||||
*S4
|
||||
}
|
||||
|
||||
// new
|
||||
type S4 struct {
|
||||
// OK: removed unexported fields
|
||||
// D and F marked as added because they are now part of the immediate fields
|
||||
D bool
|
||||
// c S4.D: added
|
||||
E int // OK: same as in old
|
||||
F F
|
||||
// c S4.F: added
|
||||
A1 // OK: same
|
||||
*S4 // OK: same (recursive embedding)
|
||||
}
|
||||
|
||||
//// Difference between exported selectable fields and exported immediate fields.
|
||||
// both
|
||||
type S5 struct{ A int }
|
||||
|
||||
// old
|
||||
// Exported immediate fields: A, S5
|
||||
// Exported selectable fields: A int, S5 S5
|
||||
type S6 struct {
|
||||
S5 S5
|
||||
A int
|
||||
}
|
||||
|
||||
// new
|
||||
// Exported immediate fields: S5
|
||||
// Exported selectable fields: A int, S5 S5.
|
||||
|
||||
// i S6.A: removed
|
||||
type S6 struct {
|
||||
S5
|
||||
}
|
||||
|
||||
//// Ambiguous fields can exist; they just can't be selected.
|
||||
// both
|
||||
type (
|
||||
embed7a struct{ E int }
|
||||
embed7b struct{ E bool }
|
||||
)
|
||||
|
||||
// old
|
||||
type S7 struct { // legal, but no selectable fields
|
||||
embed7a
|
||||
embed7b
|
||||
}
|
||||
|
||||
// new
|
||||
type S7 struct {
|
||||
embed7a
|
||||
embed7b
|
||||
// c S7.E: added
|
||||
E string
|
||||
}
|
||||
|
||||
//////////////// Method sets
|
||||
|
||||
// old
|
||||
type SM struct {
|
||||
embedm
|
||||
Embedm
|
||||
}
|
||||
|
||||
func (SM) V1() {}
|
||||
func (SM) V2() {}
|
||||
func (SM) V3() {}
|
||||
func (SM) V4() {}
|
||||
func (SM) v() {}
|
||||
|
||||
func (*SM) P1() {}
|
||||
func (*SM) P2() {}
|
||||
func (*SM) P3() {}
|
||||
func (*SM) P4() {}
|
||||
func (*SM) p() {}
|
||||
|
||||
type embedm int
|
||||
|
||||
func (embedm) EV1() {}
|
||||
func (embedm) EV2() {}
|
||||
func (embedm) EV3() {}
|
||||
func (*embedm) EP1() {}
|
||||
func (*embedm) EP2() {}
|
||||
func (*embedm) EP3() {}
|
||||
|
||||
type Embedm struct {
|
||||
A int
|
||||
}
|
||||
|
||||
func (Embedm) FV() {}
|
||||
func (*Embedm) FP() {}
|
||||
|
||||
type RepeatEmbedm struct {
|
||||
Embedm
|
||||
}
|
||||
|
||||
// new
|
||||
type SM struct {
|
||||
embedm2
|
||||
embedm3
|
||||
Embedm
|
||||
// i SM.A: changed from int to bool
|
||||
}
|
||||
|
||||
// c SMa: added
|
||||
type SMa = SM
|
||||
|
||||
func (SM) V1() {} // OK: same
|
||||
|
||||
// func (SM) V2() {}
|
||||
// i SM.V2: removed
|
||||
|
||||
// i SM.V3: changed from func() to func(int)
|
||||
func (SM) V3(int) {}
|
||||
|
||||
// c SM.V5: added
|
||||
func (SM) V5() {}
|
||||
|
||||
func (SM) v(int) {} // OK: unexported method change
|
||||
func (SM) v2() {} // OK: unexported method added
|
||||
|
||||
func (*SM) P1() {} // OK: same
|
||||
//func (*SM) P2() {}
|
||||
// i (*SM).P2: removed
|
||||
|
||||
// i (*SM).P3: changed from func() to func(int)
|
||||
func (*SMa) P3(int) {}
|
||||
|
||||
// c (*SM).P5: added
|
||||
func (*SM) P5() {}
|
||||
|
||||
// func (*SM) p() {} // OK: unexported method removed
|
||||
|
||||
// Changing from a value to a pointer receiver or vice versa
|
||||
// just looks like adding and removing a method.
|
||||
|
||||
// i SM.V4: removed
|
||||
// i (*SM).V4: changed from func() to func(int)
|
||||
func (*SM) V4(int) {}
|
||||
|
||||
// c SM.P4: added
|
||||
// P4 is not removed from (*SM) because value methods
|
||||
// are in the pointer method set.
|
||||
func (SM) P4() {}
|
||||
|
||||
type embedm2 int
|
||||
|
||||
// i embedm.EV1: changed from func() to func(int)
|
||||
func (embedm2) EV1(int) {}
|
||||
|
||||
// i embedm.EV2, method set of SM: removed
|
||||
// i embedm.EV2, method set of *SM: removed
|
||||
|
||||
// i (*embedm).EP2, method set of *SM: removed
|
||||
func (*embedm2) EP1() {}
|
||||
|
||||
type embedm3 int
|
||||
|
||||
func (embedm3) EV3() {} // OK: compatible with old embedm.EV3
|
||||
func (*embedm3) EP3() {} // OK: compatible with old (*embedm).EP3
|
||||
|
||||
type Embedm struct {
|
||||
// i Embedm.A: changed from int to bool
|
||||
A bool
|
||||
}
|
||||
|
||||
// i Embedm.FV: changed from func() to func(int)
|
||||
func (Embedm) FV(int) {}
|
||||
func (*Embedm) FP() {}
|
||||
|
||||
type RepeatEmbedm struct {
|
||||
// i RepeatEmbedm.A: changed from int to bool
|
||||
Embedm
|
||||
}
|
||||
|
||||
//////////////// Whole-package interface satisfaction
|
||||
|
||||
// old
|
||||
type WI1 interface {
|
||||
M1()
|
||||
m1()
|
||||
}
|
||||
|
||||
type WI2 interface {
|
||||
M2()
|
||||
m2()
|
||||
}
|
||||
|
||||
type WS1 int
|
||||
|
||||
func (WS1) M1() {}
|
||||
func (WS1) m1() {}
|
||||
|
||||
type WS2 int
|
||||
|
||||
func (WS2) M2() {}
|
||||
func (WS2) m2() {}
|
||||
|
||||
// new
|
||||
type WI1 interface {
|
||||
M1()
|
||||
m()
|
||||
}
|
||||
|
||||
type WS1 int
|
||||
|
||||
func (WS1) M1() {}
|
||||
|
||||
// i WS1: no longer implements WI1
|
||||
//func (WS1) m1() {}
|
||||
|
||||
type WI2 interface {
|
||||
M2()
|
||||
m2()
|
||||
// i WS2: no longer implements WI2
|
||||
m3()
|
||||
}
|
||||
|
||||
type WS2 int
|
||||
|
||||
func (WS2) M2() {}
|
||||
func (WS2) m2() {}
|
||||
|
||||
//////////////// Miscellany
|
||||
|
||||
// This verifies that the code works even through
|
||||
// multiple levels of unexported typed.
|
||||
|
||||
// old
|
||||
var Z w
|
||||
|
||||
type w []x
|
||||
type x []z
|
||||
type z int
|
||||
|
||||
// new
|
||||
var Z w
|
||||
|
||||
type w []x
|
||||
type x []z
|
||||
|
||||
// i z: changed from int to bool
|
||||
type z bool
|
||||
|
||||
// old
|
||||
type H struct{}
|
||||
|
||||
func (H) M() {}
|
||||
|
||||
// new
|
||||
// i H: changed from struct{} to interface{M()}
|
||||
type H interface {
|
||||
M()
|
||||
}
|
||||
|
||||
//// Splitting types
|
||||
|
||||
//// OK: in both old and new, {J1, K1, L1} name the same type.
|
||||
// old
|
||||
type (
|
||||
J1 = K1
|
||||
K1 = L1
|
||||
L1 int
|
||||
)
|
||||
|
||||
// new
|
||||
type (
|
||||
J1 = K1
|
||||
K1 int
|
||||
L1 = J1
|
||||
)
|
||||
|
||||
//// Old has one type, K2; new has J2 and K2.
|
||||
// both
|
||||
type K2 int
|
||||
|
||||
// old
|
||||
type J2 = K2
|
||||
|
||||
// new
|
||||
// i K2: changed from K2 to K2
|
||||
type J2 K2 // old K2 corresponds with new J2
|
||||
// old K2 also corresponds with new K2: problem
|
||||
|
||||
// both
|
||||
type k3 int
|
||||
|
||||
var Vj3 j3 // expose j3
|
||||
|
||||
// old
|
||||
type j3 = k3
|
||||
|
||||
// new
|
||||
// OK: k3 isn't exposed
|
||||
type j3 k3
|
||||
|
||||
// both
|
||||
type k4 int
|
||||
|
||||
var Vj4 j4 // expose j4
|
||||
var VK4 k4 // expose k4
|
||||
|
||||
// old
|
||||
type j4 = k4
|
||||
|
||||
// new
|
||||
// i Vj4: changed from k4 to j4
|
||||
// e.g. p.Vj4 = p.Vk4
|
||||
type j4 k4
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package astutil
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// CloneNode returns a deep copy of a Node.
|
||||
// It omits pointers to ast.{Scope,Object} variables.
|
||||
func CloneNode[T ast.Node](n T) T {
|
||||
return cloneNode(n).(T)
|
||||
}
|
||||
|
||||
func cloneNode(n ast.Node) ast.Node {
|
||||
var clone func(x reflect.Value) reflect.Value
|
||||
set := func(dst, src reflect.Value) {
|
||||
src = clone(src)
|
||||
if src.IsValid() {
|
||||
dst.Set(src)
|
||||
}
|
||||
}
|
||||
clone = func(x reflect.Value) reflect.Value {
|
||||
switch x.Kind() {
|
||||
case reflect.Ptr:
|
||||
if x.IsNil() {
|
||||
return x
|
||||
}
|
||||
// Skip fields of types potentially involved in cycles.
|
||||
switch x.Interface().(type) {
|
||||
case *ast.Object, *ast.Scope:
|
||||
return reflect.Zero(x.Type())
|
||||
}
|
||||
y := reflect.New(x.Type().Elem())
|
||||
set(y.Elem(), x.Elem())
|
||||
return y
|
||||
|
||||
case reflect.Struct:
|
||||
y := reflect.New(x.Type()).Elem()
|
||||
for i := 0; i < x.Type().NumField(); i++ {
|
||||
set(y.Field(i), x.Field(i))
|
||||
}
|
||||
return y
|
||||
|
||||
case reflect.Slice:
|
||||
if x.IsNil() {
|
||||
return x
|
||||
}
|
||||
y := reflect.MakeSlice(x.Type(), x.Len(), x.Cap())
|
||||
for i := 0; i < x.Len(); i++ {
|
||||
set(y.Index(i), x.Index(i))
|
||||
}
|
||||
return y
|
||||
|
||||
case reflect.Interface:
|
||||
y := reflect.New(x.Type()).Elem()
|
||||
set(y, x.Elem())
|
||||
return y
|
||||
|
||||
case reflect.Array, reflect.Chan, reflect.Func, reflect.Map, reflect.UnsafePointer:
|
||||
panic(x) // unreachable in AST
|
||||
|
||||
default:
|
||||
return x // bool, string, number
|
||||
}
|
||||
}
|
||||
return clone(reflect.ValueOf(n)).Interface().(ast.Node)
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package bisect can be used by compilers and other programs
|
||||
// to serve as a target for the bisect debugging tool.
|
||||
// See [golang.org/x/tools/cmd/bisect] for details about using the tool.
|
||||
//
|
||||
// To be a bisect target, allowing bisect to help determine which of a set of independent
|
||||
// changes provokes a failure, a program needs to:
|
||||
//
|
||||
// 1. Define a way to accept a change pattern on its command line or in its environment.
|
||||
// The most common mechanism is a command-line flag.
|
||||
// The pattern can be passed to [New] to create a [Matcher], the compiled form of a pattern.
|
||||
//
|
||||
// 2. Assign each change a unique ID. One possibility is to use a sequence number,
|
||||
// but the most common mechanism is to hash some kind of identifying information
|
||||
// like the file and line number where the change might be applied.
|
||||
// [Hash] hashes its arguments to compute an ID.
|
||||
//
|
||||
// 3. Enable each change that the pattern says should be enabled.
|
||||
// The [Matcher.Enable] method answers this question for a given change ID.
|
||||
//
|
||||
// 4. Report each change that the pattern says should be reported.
|
||||
// The [Matcher.Report] method answers this question for a given change ID.
|
||||
// The report consists of one more lines on standard error or standard output
|
||||
// that contain a “match marker”. [Marker] returns the match marker for a given ID.
|
||||
// When bisect reports a change as causing the failure, it identifies the change
|
||||
// by printing those report lines, with the match marker removed.
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// A program starts by defining how it receives the pattern. In this example, we will assume a flag.
|
||||
// The next step is to compile the pattern:
|
||||
//
|
||||
// m, err := bisect.New(patternFlag)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// Then, each time a potential change is considered, the program computes
|
||||
// a change ID by hashing identifying information (source file and line, in this case)
|
||||
// and then calls m.ShouldEnable and m.ShouldReport to decide whether to
|
||||
// enable and report the change, respectively:
|
||||
//
|
||||
// for each change {
|
||||
// h := bisect.Hash(file, line)
|
||||
// if m.ShouldEnable(h) {
|
||||
// enableChange()
|
||||
// }
|
||||
// if m.ShouldReport(h) {
|
||||
// log.Printf("%v %s:%d", bisect.Marker(h), file, line)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Note that the two return different values when bisect is searching for a
|
||||
// minimal set of changes to disable to provoke a failure.
|
||||
//
|
||||
// Finally, note that New returns a nil Matcher when there is no pattern,
|
||||
// meaning that the target is not running under bisect at all.
|
||||
// In that common case, the computation of the hash can be avoided entirely
|
||||
// by checking for m == nil first:
|
||||
//
|
||||
// for each change {
|
||||
// if m == nil {
|
||||
// enableChange()
|
||||
// } else {
|
||||
// h := bisect.Hash(file, line)
|
||||
// if m.ShouldEnable(h) {
|
||||
// enableChange()
|
||||
// }
|
||||
// if m.ShouldReport(h) {
|
||||
// log.Printf("%v %s:%d", bisect.Marker(h), file, line)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # Pattern Syntax
|
||||
//
|
||||
// Patterns are generated by the bisect tool and interpreted by [New].
|
||||
// Users should not have to understand the patterns except when
|
||||
// debugging a target's bisect support or debugging the bisect tool itself.
|
||||
//
|
||||
// The pattern syntax selecting a change is a sequence of bit strings
|
||||
// separated by + and - operators. Each bit string denotes the set of
|
||||
// changes with IDs ending in those bits, + is set addition, - is set subtraction,
|
||||
// and the expression is evaluated in the usual left-to-right order.
|
||||
// The special binary number “y” denotes the set of all changes,
|
||||
// standing in for the empty bit string.
|
||||
// In the expression, all the + operators must appear before all the - operators.
|
||||
// A leading + adds to an empty set. A leading - subtracts from the set of all
|
||||
// possible suffixes.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// - “01+10” and “+01+10” both denote the set of changes
|
||||
// with IDs ending with the bits 01 or 10.
|
||||
//
|
||||
// - “01+10-1001” denotes the set of changes with IDs
|
||||
// ending with the bits 01 or 10, but excluding those ending in 1001.
|
||||
//
|
||||
// - “-01-1000” and “y-01-1000 both denote the set of all changes
|
||||
// with IDs not ending in 01 nor 1000.
|
||||
//
|
||||
// - “0+1-01+001” is not a valid pattern, because all the + operators do not
|
||||
// appear before all the - operators.
|
||||
//
|
||||
// In the syntaxes described so far, the pattern specifies the changes to
|
||||
// enable and report. If a pattern is prefixed by a “!”, the meaning
|
||||
// changes: the pattern specifies the changes to DISABLE and report. This
|
||||
// mode of operation is needed when a program passes with all changes
|
||||
// enabled but fails with no changes enabled. In this case, bisect
|
||||
// searches for minimal sets of changes to disable.
|
||||
// Put another way, the leading “!” inverts the result from [Matcher.ShouldEnable]
|
||||
// but does not invert the result from [Matcher.ShouldReport].
|
||||
//
|
||||
// As a convenience for manual debugging, “n” is an alias for “!y”,
|
||||
// meaning to disable and report all changes.
|
||||
//
|
||||
// Finally, a leading “v” in the pattern indicates that the reports will be shown
|
||||
// to the user of bisect to describe the changes involved in a failure.
|
||||
// At the API level, the leading “v” causes [Matcher.Verbose] to return true.
|
||||
// See the next section for details.
|
||||
//
|
||||
// # Match Reports
|
||||
//
|
||||
// The target program must enable only those changed matched
|
||||
// by the pattern, and it must print a match report for each such change.
|
||||
// A match report consists of one or more lines of text that will be
|
||||
// printed by the bisect tool to describe a change implicated in causing
|
||||
// a failure. Each line in the report for a given change must contain a
|
||||
// match marker with that change ID, as returned by [Marker].
|
||||
// The markers are elided when displaying the lines to the user.
|
||||
//
|
||||
// A match marker has the form “[bisect-match 0x1234]” where
|
||||
// 0x1234 is the change ID in hexadecimal.
|
||||
// An alternate form is “[bisect-match 010101]”, giving the change ID in binary.
|
||||
//
|
||||
// When [Matcher.Verbose] returns false, the match reports are only
|
||||
// being processed by bisect to learn the set of enabled changes,
|
||||
// not shown to the user, meaning that each report can be a match
|
||||
// marker on a line by itself, eliding the usual textual description.
|
||||
// When the textual description is expensive to compute,
|
||||
// checking [Matcher.Verbose] can help the avoid that expense
|
||||
// in most runs.
|
||||
package bisect
|
||||
|
||||
// New creates and returns a new Matcher implementing the given pattern.
|
||||
// The pattern syntax is defined in the package doc comment.
|
||||
//
|
||||
// In addition to the pattern syntax syntax, New("") returns nil, nil.
|
||||
// The nil *Matcher is valid for use: it returns true from ShouldEnable
|
||||
// and false from ShouldReport for all changes. Callers can avoid calling
|
||||
// [Hash], [Matcher.ShouldEnable], and [Matcher.ShouldPrint] entirely
|
||||
// when they recognize the nil Matcher.
|
||||
func New(pattern string) (*Matcher, error) {
|
||||
if pattern == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m := new(Matcher)
|
||||
|
||||
// Allow multiple v, so that “bisect cmd vPATTERN” can force verbose all the time.
|
||||
p := pattern
|
||||
for len(p) > 0 && p[0] == 'v' {
|
||||
m.verbose = true
|
||||
p = p[1:]
|
||||
if p == "" {
|
||||
return nil, &parseError{"invalid pattern syntax: " + pattern}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow multiple !, each negating the last, so that “bisect cmd !PATTERN” works
|
||||
// even when bisect chooses to add its own !.
|
||||
m.enable = true
|
||||
for len(p) > 0 && p[0] == '!' {
|
||||
m.enable = !m.enable
|
||||
p = p[1:]
|
||||
if p == "" {
|
||||
return nil, &parseError{"invalid pattern syntax: " + pattern}
|
||||
}
|
||||
}
|
||||
|
||||
if p == "n" {
|
||||
// n is an alias for !y.
|
||||
m.enable = !m.enable
|
||||
p = "y"
|
||||
}
|
||||
|
||||
// Parse actual pattern syntax.
|
||||
result := true
|
||||
bits := uint64(0)
|
||||
start := 0
|
||||
wid := 1 // 1-bit (binary); sometimes 4-bit (hex)
|
||||
for i := 0; i <= len(p); i++ {
|
||||
// Imagine a trailing - at the end of the pattern to flush final suffix
|
||||
c := byte('-')
|
||||
if i < len(p) {
|
||||
c = p[i]
|
||||
}
|
||||
if i == start && wid == 1 && c == 'x' { // leading x for hex
|
||||
start = i + 1
|
||||
wid = 4
|
||||
continue
|
||||
}
|
||||
switch c {
|
||||
default:
|
||||
return nil, &parseError{"invalid pattern syntax: " + pattern}
|
||||
case '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
if wid != 4 {
|
||||
return nil, &parseError{"invalid pattern syntax: " + pattern}
|
||||
}
|
||||
fallthrough
|
||||
case '0', '1':
|
||||
bits <<= wid
|
||||
bits |= uint64(c - '0')
|
||||
case 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F':
|
||||
if wid != 4 {
|
||||
return nil, &parseError{"invalid pattern syntax: " + pattern}
|
||||
}
|
||||
bits <<= 4
|
||||
bits |= uint64(c&^0x20 - 'A' + 10)
|
||||
case 'y':
|
||||
if i+1 < len(p) && (p[i+1] == '0' || p[i+1] == '1') {
|
||||
return nil, &parseError{"invalid pattern syntax: " + pattern}
|
||||
}
|
||||
bits = 0
|
||||
case '+', '-':
|
||||
if c == '+' && result == false {
|
||||
// Have already seen a -. Should be - from here on.
|
||||
return nil, &parseError{"invalid pattern syntax (+ after -): " + pattern}
|
||||
}
|
||||
if i > 0 {
|
||||
n := (i - start) * wid
|
||||
if n > 64 {
|
||||
return nil, &parseError{"pattern bits too long: " + pattern}
|
||||
}
|
||||
if n <= 0 {
|
||||
return nil, &parseError{"invalid pattern syntax: " + pattern}
|
||||
}
|
||||
if p[start] == 'y' {
|
||||
n = 0
|
||||
}
|
||||
mask := uint64(1)<<n - 1
|
||||
m.list = append(m.list, cond{mask, bits, result})
|
||||
} else if c == '-' {
|
||||
// leading - subtracts from complete set
|
||||
m.list = append(m.list, cond{0, 0, true})
|
||||
}
|
||||
bits = 0
|
||||
result = c == '+'
|
||||
start = i + 1
|
||||
wid = 1
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// A Matcher is the parsed, compiled form of a PATTERN string.
|
||||
// The nil *Matcher is valid: it has all changes enabled but none reported.
|
||||
type Matcher struct {
|
||||
verbose bool
|
||||
enable bool // when true, list is for “enable and report” (when false, “disable and report”)
|
||||
list []cond // conditions; later ones win over earlier ones
|
||||
}
|
||||
|
||||
// A cond is a single condition in the matcher.
|
||||
// Given an input id, if id&mask == bits, return the result.
|
||||
type cond struct {
|
||||
mask uint64
|
||||
bits uint64
|
||||
result bool
|
||||
}
|
||||
|
||||
// Verbose reports whether the reports will be shown to users
|
||||
// and need to include a human-readable change description.
|
||||
// If not, the target can print just the Marker on a line by itself
|
||||
// and perhaps save some computation.
|
||||
func (m *Matcher) Verbose() bool {
|
||||
return m.verbose
|
||||
}
|
||||
|
||||
// ShouldEnable reports whether the change with the given id should be enabled.
|
||||
func (m *Matcher) ShouldEnable(id uint64) bool {
|
||||
if m == nil {
|
||||
return true
|
||||
}
|
||||
for i := len(m.list) - 1; i >= 0; i-- {
|
||||
c := &m.list[i]
|
||||
if id&c.mask == c.bits {
|
||||
return c.result == m.enable
|
||||
}
|
||||
}
|
||||
return false == m.enable
|
||||
}
|
||||
|
||||
// ShouldReport reports whether the change with the given id should be reported.
|
||||
func (m *Matcher) ShouldReport(id uint64) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
for i := len(m.list) - 1; i >= 0; i-- {
|
||||
c := &m.list[i]
|
||||
if id&c.mask == c.bits {
|
||||
return c.result
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Marker returns the match marker text to use on any line reporting details
|
||||
// about a match of the given ID.
|
||||
// It always returns the hexadecimal format.
|
||||
func Marker(id uint64) string {
|
||||
return string(AppendMarker(nil, id))
|
||||
}
|
||||
|
||||
// AppendMarker is like [Marker] but appends the marker to dst.
|
||||
func AppendMarker(dst []byte, id uint64) []byte {
|
||||
const prefix = "[bisect-match 0x"
|
||||
var buf [len(prefix) + 16 + 1]byte
|
||||
copy(buf[:], prefix)
|
||||
for i := 0; i < 16; i++ {
|
||||
buf[len(prefix)+i] = "0123456789abcdef"[id>>60]
|
||||
id <<= 4
|
||||
}
|
||||
buf[len(prefix)+16] = ']'
|
||||
return append(dst, buf[:]...)
|
||||
}
|
||||
|
||||
// CutMarker finds the first match marker in line and removes it,
|
||||
// returning the shortened line (with the marker removed),
|
||||
// the ID from the match marker,
|
||||
// and whether a marker was found at all.
|
||||
// If there is no marker, CutMarker returns line, 0, false.
|
||||
func CutMarker(line string) (short string, id uint64, ok bool) {
|
||||
// Find first instance of prefix.
|
||||
prefix := "[bisect-match "
|
||||
i := 0
|
||||
for ; ; i++ {
|
||||
if i >= len(line)-len(prefix) {
|
||||
return line, 0, false
|
||||
}
|
||||
if line[i] == '[' && line[i:i+len(prefix)] == prefix {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Scan to ].
|
||||
j := i + len(prefix)
|
||||
for j < len(line) && line[j] != ']' {
|
||||
j++
|
||||
}
|
||||
if j >= len(line) {
|
||||
return line, 0, false
|
||||
}
|
||||
|
||||
// Parse id.
|
||||
idstr := line[i+len(prefix) : j]
|
||||
if len(idstr) >= 3 && idstr[:2] == "0x" {
|
||||
// parse hex
|
||||
if len(idstr) > 2+16 { // max 0x + 16 digits
|
||||
return line, 0, false
|
||||
}
|
||||
for i := 2; i < len(idstr); i++ {
|
||||
id <<= 4
|
||||
switch c := idstr[i]; {
|
||||
case '0' <= c && c <= '9':
|
||||
id |= uint64(c - '0')
|
||||
case 'a' <= c && c <= 'f':
|
||||
id |= uint64(c - 'a' + 10)
|
||||
case 'A' <= c && c <= 'F':
|
||||
id |= uint64(c - 'A' + 10)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if idstr == "" || len(idstr) > 64 { // min 1 digit, max 64 digits
|
||||
return line, 0, false
|
||||
}
|
||||
// parse binary
|
||||
for i := 0; i < len(idstr); i++ {
|
||||
id <<= 1
|
||||
switch c := idstr[i]; c {
|
||||
default:
|
||||
return line, 0, false
|
||||
case '0', '1':
|
||||
id |= uint64(c - '0')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Construct shortened line.
|
||||
// Remove at most one space from around the marker,
|
||||
// so that "foo [marker] bar" shortens to "foo bar".
|
||||
j++ // skip ]
|
||||
if i > 0 && line[i-1] == ' ' {
|
||||
i--
|
||||
} else if j < len(line) && line[j] == ' ' {
|
||||
j++
|
||||
}
|
||||
short = line[:i] + line[j:]
|
||||
return short, id, true
|
||||
}
|
||||
|
||||
// Hash computes a hash of the data arguments,
|
||||
// each of which must be of type string, byte, int, uint, int32, uint32, int64, uint64, uintptr, or a slice of one of those types.
|
||||
func Hash(data ...any) uint64 {
|
||||
h := offset64
|
||||
for _, v := range data {
|
||||
switch v := v.(type) {
|
||||
default:
|
||||
// Note: Not printing the type, because reflect.ValueOf(v)
|
||||
// would make the interfaces prepared by the caller escape
|
||||
// and therefore allocate. This way, Hash(file, line) runs
|
||||
// without any allocation. It should be clear from the
|
||||
// source code calling Hash what the bad argument was.
|
||||
panic("bisect.Hash: unexpected argument type")
|
||||
case string:
|
||||
h = fnvString(h, v)
|
||||
case byte:
|
||||
h = fnv(h, v)
|
||||
case int:
|
||||
h = fnvUint64(h, uint64(v))
|
||||
case uint:
|
||||
h = fnvUint64(h, uint64(v))
|
||||
case int32:
|
||||
h = fnvUint32(h, uint32(v))
|
||||
case uint32:
|
||||
h = fnvUint32(h, v)
|
||||
case int64:
|
||||
h = fnvUint64(h, uint64(v))
|
||||
case uint64:
|
||||
h = fnvUint64(h, v)
|
||||
case uintptr:
|
||||
h = fnvUint64(h, uint64(v))
|
||||
case []string:
|
||||
for _, x := range v {
|
||||
h = fnvString(h, x)
|
||||
}
|
||||
case []byte:
|
||||
for _, x := range v {
|
||||
h = fnv(h, x)
|
||||
}
|
||||
case []int:
|
||||
for _, x := range v {
|
||||
h = fnvUint64(h, uint64(x))
|
||||
}
|
||||
case []uint:
|
||||
for _, x := range v {
|
||||
h = fnvUint64(h, uint64(x))
|
||||
}
|
||||
case []int32:
|
||||
for _, x := range v {
|
||||
h = fnvUint32(h, uint32(x))
|
||||
}
|
||||
case []uint32:
|
||||
for _, x := range v {
|
||||
h = fnvUint32(h, x)
|
||||
}
|
||||
case []int64:
|
||||
for _, x := range v {
|
||||
h = fnvUint64(h, uint64(x))
|
||||
}
|
||||
case []uint64:
|
||||
for _, x := range v {
|
||||
h = fnvUint64(h, x)
|
||||
}
|
||||
case []uintptr:
|
||||
for _, x := range v {
|
||||
h = fnvUint64(h, uint64(x))
|
||||
}
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Trivial error implementation, here to avoid importing errors.
|
||||
|
||||
type parseError struct{ text string }
|
||||
|
||||
func (e *parseError) Error() string { return e.text }
|
||||
|
||||
// FNV-1a implementation. See Go's hash/fnv/fnv.go.
|
||||
// Copied here for simplicity (can handle uints directly)
|
||||
// and to avoid the dependency.
|
||||
|
||||
const (
|
||||
offset64 uint64 = 14695981039346656037
|
||||
prime64 uint64 = 1099511628211
|
||||
)
|
||||
|
||||
func fnv(h uint64, x byte) uint64 {
|
||||
h ^= uint64(x)
|
||||
h *= prime64
|
||||
return h
|
||||
}
|
||||
|
||||
func fnvString(h uint64, x string) uint64 {
|
||||
for i := 0; i < len(x); i++ {
|
||||
h ^= uint64(x[i])
|
||||
h *= prime64
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func fnvUint64(h uint64, x uint64) uint64 {
|
||||
for i := 0; i < 8; i++ {
|
||||
h ^= uint64(x & 0xFF)
|
||||
x >>= 8
|
||||
h *= prime64
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func fnvUint32(h uint64, x uint32) uint64 {
|
||||
for i := 0; i < 4; i++ {
|
||||
h ^= uint64(x & 0xFF)
|
||||
x >>= 8
|
||||
h *= prime64
|
||||
}
|
||||
return h
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package bisect
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// In order for package bisect to be copied into the standard library
|
||||
// and used by very low-level packages such as internal/godebug,
|
||||
// it needs to have no imports at all.
|
||||
func TestNoImports(t *testing.T) {
|
||||
files, err := filepath.Glob("*.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, file := range files {
|
||||
if strings.HasSuffix(file, "_test.go") {
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
continue
|
||||
}
|
||||
if strings.Contains(string(data), "\nimport") {
|
||||
t.Errorf("%s contains imports; package bisect must not import other packages", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package diff computes differences between text files or strings.
|
||||
package diff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// An Edit describes the replacement of a portion of a text file.
|
||||
type Edit struct {
|
||||
Start, End int // byte offsets of the region to replace
|
||||
New string // the replacement
|
||||
}
|
||||
|
||||
func (e Edit) String() string {
|
||||
return fmt.Sprintf("{Start:%d,End:%d,New:%q}", e.Start, e.End, e.New)
|
||||
}
|
||||
|
||||
// Apply applies a sequence of edits to the src buffer and returns the
|
||||
// result. Edits are applied in order of start offset; edits with the
|
||||
// same start offset are applied in they order they were provided.
|
||||
//
|
||||
// Apply returns an error if any edit is out of bounds,
|
||||
// or if any pair of edits is overlapping.
|
||||
func Apply(src string, edits []Edit) (string, error) {
|
||||
edits, size, err := validate(src, edits)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Apply edits.
|
||||
out := make([]byte, 0, size)
|
||||
lastEnd := 0
|
||||
for _, edit := range edits {
|
||||
if lastEnd < edit.Start {
|
||||
out = append(out, src[lastEnd:edit.Start]...)
|
||||
}
|
||||
out = append(out, edit.New...)
|
||||
lastEnd = edit.End
|
||||
}
|
||||
out = append(out, src[lastEnd:]...)
|
||||
|
||||
if len(out) != size {
|
||||
panic("wrong size")
|
||||
}
|
||||
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// ApplyBytes is like Apply, but it accepts a byte slice.
|
||||
// The result is always a new array.
|
||||
func ApplyBytes(src []byte, edits []Edit) ([]byte, error) {
|
||||
res, err := Apply(string(src), edits)
|
||||
return []byte(res), err
|
||||
}
|
||||
|
||||
// validate checks that edits are consistent with src,
|
||||
// and returns the size of the patched output.
|
||||
// It may return a different slice.
|
||||
func validate(src string, edits []Edit) ([]Edit, int, error) {
|
||||
if !sort.IsSorted(editsSort(edits)) {
|
||||
edits = append([]Edit(nil), edits...)
|
||||
SortEdits(edits)
|
||||
}
|
||||
|
||||
// Check validity of edits and compute final size.
|
||||
size := len(src)
|
||||
lastEnd := 0
|
||||
for _, edit := range edits {
|
||||
if !(0 <= edit.Start && edit.Start <= edit.End && edit.End <= len(src)) {
|
||||
return nil, 0, fmt.Errorf("diff has out-of-bounds edits")
|
||||
}
|
||||
if edit.Start < lastEnd {
|
||||
return nil, 0, fmt.Errorf("diff has overlapping edits")
|
||||
}
|
||||
size += len(edit.New) + edit.Start - edit.End
|
||||
lastEnd = edit.End
|
||||
}
|
||||
|
||||
return edits, size, nil
|
||||
}
|
||||
|
||||
// SortEdits orders a slice of Edits by (start, end) offset.
|
||||
// This ordering puts insertions (end = start) before deletions
|
||||
// (end > start) at the same point, but uses a stable sort to preserve
|
||||
// the order of multiple insertions at the same point.
|
||||
// (Apply detects multiple deletions at the same point as an error.)
|
||||
func SortEdits(edits []Edit) {
|
||||
sort.Stable(editsSort(edits))
|
||||
}
|
||||
|
||||
type editsSort []Edit
|
||||
|
||||
func (a editsSort) Len() int { return len(a) }
|
||||
func (a editsSort) Less(i, j int) bool {
|
||||
if cmp := a[i].Start - a[j].Start; cmp != 0 {
|
||||
return cmp < 0
|
||||
}
|
||||
return a[i].End < a[j].End
|
||||
}
|
||||
func (a editsSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
||||
// lineEdits expands and merges a sequence of edits so that each
|
||||
// resulting edit replaces one or more complete lines.
|
||||
// See ApplyEdits for preconditions.
|
||||
func lineEdits(src string, edits []Edit) ([]Edit, error) {
|
||||
edits, _, err := validate(src, edits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Do all deletions begin and end at the start of a line,
|
||||
// and all insertions end with a newline?
|
||||
// (This is merely a fast path.)
|
||||
for _, edit := range edits {
|
||||
if edit.Start >= len(src) || // insertion at EOF
|
||||
edit.Start > 0 && src[edit.Start-1] != '\n' || // not at line start
|
||||
edit.End > 0 && src[edit.End-1] != '\n' || // not at line start
|
||||
edit.New != "" && edit.New[len(edit.New)-1] != '\n' { // partial insert
|
||||
goto expand // slow path
|
||||
}
|
||||
}
|
||||
return edits, nil // aligned
|
||||
|
||||
expand:
|
||||
if len(edits) == 0 {
|
||||
return edits, nil // no edits (unreachable due to fast path)
|
||||
}
|
||||
expanded := make([]Edit, 0, len(edits)) // a guess
|
||||
prev := edits[0]
|
||||
// TODO(adonovan): opt: start from the first misaligned edit.
|
||||
// TODO(adonovan): opt: avoid quadratic cost of string += string.
|
||||
for _, edit := range edits[1:] {
|
||||
between := src[prev.End:edit.Start]
|
||||
if !strings.Contains(between, "\n") {
|
||||
// overlapping lines: combine with previous edit.
|
||||
prev.New += between + edit.New
|
||||
prev.End = edit.End
|
||||
} else {
|
||||
// non-overlapping lines: flush previous edit.
|
||||
expanded = append(expanded, expandEdit(prev, src))
|
||||
prev = edit
|
||||
}
|
||||
}
|
||||
return append(expanded, expandEdit(prev, src)), nil // flush final edit
|
||||
}
|
||||
|
||||
// expandEdit returns edit expanded to complete whole lines.
|
||||
func expandEdit(edit Edit, src string) Edit {
|
||||
// Expand start left to start of line.
|
||||
// (delta is the zero-based column number of start.)
|
||||
start := edit.Start
|
||||
if delta := start - 1 - strings.LastIndex(src[:start], "\n"); delta > 0 {
|
||||
edit.Start -= delta
|
||||
edit.New = src[start-delta:start] + edit.New
|
||||
}
|
||||
|
||||
// Expand end right to end of line.
|
||||
end := edit.End
|
||||
if end > 0 && src[end-1] != '\n' ||
|
||||
edit.New != "" && edit.New[len(edit.New)-1] != '\n' {
|
||||
if nl := strings.IndexByte(src[end:], '\n'); nl < 0 {
|
||||
edit.End = len(src) // extend to EOF
|
||||
} else {
|
||||
edit.End = end + nl + 1 // extend beyond \n
|
||||
}
|
||||
}
|
||||
edit.New += src[end:edit.End]
|
||||
|
||||
return edit
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package diff_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/tools/internal/diff"
|
||||
"golang.org/x/tools/internal/diff/difftest"
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
)
|
||||
|
||||
func TestApply(t *testing.T) {
|
||||
for _, tc := range difftest.TestCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
got, err := diff.Apply(tc.In, tc.Edits)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply(Edits) failed: %v", err)
|
||||
}
|
||||
if got != tc.Out {
|
||||
t.Errorf("Apply(Edits): got %q, want %q", got, tc.Out)
|
||||
}
|
||||
if tc.LineEdits != nil {
|
||||
got, err := diff.Apply(tc.In, tc.LineEdits)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply(LineEdits) failed: %v", err)
|
||||
}
|
||||
if got != tc.Out {
|
||||
t.Errorf("Apply(LineEdits): got %q, want %q", got, tc.Out)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNEdits(t *testing.T) {
|
||||
for _, tc := range difftest.TestCases {
|
||||
edits := diff.Strings(tc.In, tc.Out)
|
||||
got, err := diff.Apply(tc.In, edits)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply failed: %v", err)
|
||||
}
|
||||
if got != tc.Out {
|
||||
t.Fatalf("%s: got %q wanted %q", tc.Name, got, tc.Out)
|
||||
}
|
||||
if len(edits) < len(tc.Edits) { // should find subline edits
|
||||
t.Errorf("got %v, expected %v for %#v", edits, tc.Edits, tc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNRandom(t *testing.T) {
|
||||
rand.Seed(1)
|
||||
for i := 0; i < 1000; i++ {
|
||||
a := randstr("abω", 16)
|
||||
b := randstr("abωc", 16)
|
||||
edits := diff.Strings(a, b)
|
||||
got, err := diff.Apply(a, edits)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply failed: %v", err)
|
||||
}
|
||||
if got != b {
|
||||
t.Fatalf("%d: got %q, wanted %q, starting with %q", i, got, b, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// $ go test -fuzz=FuzzRoundTrip ./internal/diff
|
||||
func FuzzRoundTrip(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, a, b string) {
|
||||
if !utf8.ValidString(a) || !utf8.ValidString(b) {
|
||||
return // inputs must be text
|
||||
}
|
||||
edits := diff.Strings(a, b)
|
||||
got, err := diff.Apply(a, edits)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply failed: %v", err)
|
||||
}
|
||||
if got != b {
|
||||
t.Fatalf("applying diff(%q, %q) gives %q; edits=%v", a, b, got, edits)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLineEdits(t *testing.T) {
|
||||
for _, tc := range difftest.TestCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
want := tc.LineEdits
|
||||
if want == nil {
|
||||
want = tc.Edits // already line-aligned
|
||||
}
|
||||
got, err := diff.LineEdits(tc.In, tc.Edits)
|
||||
if err != nil {
|
||||
t.Fatalf("LineEdits: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("in=<<%s>>\nout=<<%s>>\nraw edits=%s\nline edits=%s\nwant: %s",
|
||||
tc.In, tc.Out, tc.Edits, got, want)
|
||||
}
|
||||
// make sure that applying the edits gives the expected result
|
||||
fixed, err := diff.Apply(tc.In, got)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if fixed != tc.Out {
|
||||
t.Errorf("Apply(LineEdits): got %q, want %q", fixed, tc.Out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToUnified(t *testing.T) {
|
||||
testenv.NeedsTool(t, "patch")
|
||||
for _, tc := range difftest.TestCases {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
unified, err := diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.Edits, diff.DefaultContextLines)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if unified == "" {
|
||||
return
|
||||
}
|
||||
orig := filepath.Join(t.TempDir(), "original")
|
||||
err = os.WriteFile(orig, []byte(tc.In), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
temp := filepath.Join(t.TempDir(), "patched")
|
||||
err = os.WriteFile(temp, []byte(tc.In), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command("patch", "-p0", "-u", "-s", "-o", temp, orig)
|
||||
cmd.Stdin = strings.NewReader(unified)
|
||||
cmd.Stdout = new(bytes.Buffer)
|
||||
cmd.Stderr = new(bytes.Buffer)
|
||||
if err = cmd.Run(); err != nil {
|
||||
t.Fatalf("%v: %q (%q) (%q)", err, cmd.String(),
|
||||
cmd.Stderr, cmd.Stdout)
|
||||
}
|
||||
got, err := os.ReadFile(temp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != tc.Out {
|
||||
t.Errorf("applying unified failed: got\n%q, wanted\n%q unified\n%q",
|
||||
got, tc.Out, unified)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegressionOld001(t *testing.T) {
|
||||
a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n"
|
||||
|
||||
b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n"
|
||||
diffs := diff.Strings(a, b)
|
||||
got, err := diff.Apply(a, diffs)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply failed: %v", err)
|
||||
}
|
||||
if got != b {
|
||||
i := 0
|
||||
for ; i < len(a) && i < len(b) && got[i] == b[i]; i++ {
|
||||
}
|
||||
t.Errorf("oops %vd\n%q\n%q", diffs, got, b)
|
||||
t.Errorf("\n%q\n%q", got[i:], b[i:])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegressionOld002(t *testing.T) {
|
||||
a := "n\"\n)\n"
|
||||
b := "n\"\n\t\"golang.org/x//nnal/stack\"\n)\n"
|
||||
diffs := diff.Strings(a, b)
|
||||
got, err := diff.Apply(a, diffs)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply failed: %v", err)
|
||||
}
|
||||
if got != b {
|
||||
i := 0
|
||||
for ; i < len(a) && i < len(b) && got[i] == b[i]; i++ {
|
||||
}
|
||||
t.Errorf("oops %vd\n%q\n%q", diffs, got, b)
|
||||
t.Errorf("\n%q\n%q", got[i:], b[i:])
|
||||
}
|
||||
}
|
||||
|
||||
// return a random string of length n made of characters from s
|
||||
func randstr(s string, n int) string {
|
||||
src := []rune(s)
|
||||
x := make([]rune, n)
|
||||
for i := 0; i < n; i++ {
|
||||
x[i] = src[rand.Intn(len(src))]
|
||||
}
|
||||
return string(x)
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package difftest supplies a set of tests that will operate on any
|
||||
// implementation of a diff algorithm as exposed by
|
||||
// "golang.org/x/tools/internal/diff"
|
||||
package difftest
|
||||
|
||||
// There are two kinds of tests, semantic tests, and 'golden data' tests.
|
||||
// The semantic tests check that the computed diffs transform the input to
|
||||
// the output, and that 'patch' accepts the computed unified diffs.
|
||||
// The other tests just check that Edits and LineEdits haven't changed
|
||||
// unexpectedly. These fields may need to be changed when the diff algorithm
|
||||
// changes.
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/diff"
|
||||
)
|
||||
|
||||
const (
|
||||
FileA = "from"
|
||||
FileB = "to"
|
||||
UnifiedPrefix = "--- " + FileA + "\n+++ " + FileB + "\n"
|
||||
)
|
||||
|
||||
var TestCases = []struct {
|
||||
Name, In, Out, Unified string
|
||||
Edits, LineEdits []diff.Edit // expectation (LineEdits=nil => already line-aligned)
|
||||
NoDiff bool
|
||||
}{{
|
||||
Name: "empty",
|
||||
In: "",
|
||||
Out: "",
|
||||
}, {
|
||||
Name: "no_diff",
|
||||
In: "gargantuan\n",
|
||||
Out: "gargantuan\n",
|
||||
}, {
|
||||
Name: "replace_all",
|
||||
In: "fruit\n",
|
||||
Out: "cheese\n",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1 +1 @@
|
||||
-fruit
|
||||
+cheese
|
||||
`[1:],
|
||||
Edits: []diff.Edit{{Start: 0, End: 5, New: "cheese"}},
|
||||
LineEdits: []diff.Edit{{Start: 0, End: 6, New: "cheese\n"}},
|
||||
}, {
|
||||
Name: "insert_rune",
|
||||
In: "gord\n",
|
||||
Out: "gourd\n",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1 +1 @@
|
||||
-gord
|
||||
+gourd
|
||||
`[1:],
|
||||
Edits: []diff.Edit{{Start: 2, End: 2, New: "u"}},
|
||||
LineEdits: []diff.Edit{{Start: 0, End: 5, New: "gourd\n"}},
|
||||
}, {
|
||||
Name: "delete_rune",
|
||||
In: "groat\n",
|
||||
Out: "goat\n",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1 +1 @@
|
||||
-groat
|
||||
+goat
|
||||
`[1:],
|
||||
Edits: []diff.Edit{{Start: 1, End: 2, New: ""}},
|
||||
LineEdits: []diff.Edit{{Start: 0, End: 6, New: "goat\n"}},
|
||||
}, {
|
||||
Name: "replace_rune",
|
||||
In: "loud\n",
|
||||
Out: "lord\n",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1 +1 @@
|
||||
-loud
|
||||
+lord
|
||||
`[1:],
|
||||
Edits: []diff.Edit{{Start: 2, End: 3, New: "r"}},
|
||||
LineEdits: []diff.Edit{{Start: 0, End: 5, New: "lord\n"}},
|
||||
}, {
|
||||
Name: "replace_partials",
|
||||
In: "blanket\n",
|
||||
Out: "bunker\n",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1 +1 @@
|
||||
-blanket
|
||||
+bunker
|
||||
`[1:],
|
||||
Edits: []diff.Edit{
|
||||
{Start: 1, End: 3, New: "u"},
|
||||
{Start: 6, End: 7, New: "r"},
|
||||
},
|
||||
LineEdits: []diff.Edit{{Start: 0, End: 8, New: "bunker\n"}},
|
||||
}, {
|
||||
Name: "insert_line",
|
||||
In: "1: one\n3: three\n",
|
||||
Out: "1: one\n2: two\n3: three\n",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1,2 +1,3 @@
|
||||
1: one
|
||||
+2: two
|
||||
3: three
|
||||
`[1:],
|
||||
Edits: []diff.Edit{{Start: 7, End: 7, New: "2: two\n"}},
|
||||
}, {
|
||||
Name: "replace_no_newline",
|
||||
In: "A",
|
||||
Out: "B",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1 +1 @@
|
||||
-A
|
||||
\ No newline at end of file
|
||||
+B
|
||||
\ No newline at end of file
|
||||
`[1:],
|
||||
Edits: []diff.Edit{{Start: 0, End: 1, New: "B"}},
|
||||
}, {
|
||||
Name: "delete_empty",
|
||||
In: "meow",
|
||||
Out: "", // GNU diff -u special case: +0,0
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1 +0,0 @@
|
||||
-meow
|
||||
\ No newline at end of file
|
||||
`[1:],
|
||||
Edits: []diff.Edit{{Start: 0, End: 4, New: ""}},
|
||||
LineEdits: []diff.Edit{{Start: 0, End: 4, New: ""}},
|
||||
}, {
|
||||
Name: "append_empty",
|
||||
In: "", // GNU diff -u special case: -0,0
|
||||
Out: "AB\nC",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -0,0 +1,2 @@
|
||||
+AB
|
||||
+C
|
||||
\ No newline at end of file
|
||||
`[1:],
|
||||
Edits: []diff.Edit{{Start: 0, End: 0, New: "AB\nC"}},
|
||||
LineEdits: []diff.Edit{{Start: 0, End: 0, New: "AB\nC"}},
|
||||
},
|
||||
// TODO(adonovan): fix this test: GNU diff -u prints "+1,2", Unifies prints "+1,3".
|
||||
// {
|
||||
// Name: "add_start",
|
||||
// In: "A",
|
||||
// Out: "B\nCA",
|
||||
// Unified: UnifiedPrefix + `
|
||||
// @@ -1 +1,2 @@
|
||||
// -A
|
||||
// \ No newline at end of file
|
||||
// +B
|
||||
// +CA
|
||||
// \ No newline at end of file
|
||||
// `[1:],
|
||||
// Edits: []diff.TextEdit{{Span: newSpan(0, 0), NewText: "B\nC"}},
|
||||
// LineEdits: []diff.TextEdit{{Span: newSpan(0, 0), NewText: "B\nC"}},
|
||||
// },
|
||||
{
|
||||
Name: "add_end",
|
||||
In: "A",
|
||||
Out: "AB",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1 +1 @@
|
||||
-A
|
||||
\ No newline at end of file
|
||||
+AB
|
||||
\ No newline at end of file
|
||||
`[1:],
|
||||
Edits: []diff.Edit{{Start: 1, End: 1, New: "B"}},
|
||||
LineEdits: []diff.Edit{{Start: 0, End: 1, New: "AB"}},
|
||||
}, {
|
||||
Name: "add_empty",
|
||||
In: "",
|
||||
Out: "AB\nC",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -0,0 +1,2 @@
|
||||
+AB
|
||||
+C
|
||||
\ No newline at end of file
|
||||
`[1:],
|
||||
Edits: []diff.Edit{{Start: 0, End: 0, New: "AB\nC"}},
|
||||
LineEdits: []diff.Edit{{Start: 0, End: 0, New: "AB\nC"}},
|
||||
}, {
|
||||
Name: "add_newline",
|
||||
In: "A",
|
||||
Out: "A\n",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1 +1 @@
|
||||
-A
|
||||
\ No newline at end of file
|
||||
+A
|
||||
`[1:],
|
||||
Edits: []diff.Edit{{Start: 1, End: 1, New: "\n"}},
|
||||
LineEdits: []diff.Edit{{Start: 0, End: 1, New: "A\n"}},
|
||||
}, {
|
||||
Name: "delete_front",
|
||||
In: "A\nB\nC\nA\nB\nB\nA\n",
|
||||
Out: "C\nB\nA\nB\nA\nC\n",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1,7 +1,6 @@
|
||||
-A
|
||||
-B
|
||||
C
|
||||
+B
|
||||
A
|
||||
B
|
||||
-B
|
||||
A
|
||||
+C
|
||||
`[1:],
|
||||
NoDiff: true, // unified diff is different but valid
|
||||
Edits: []diff.Edit{
|
||||
{Start: 0, End: 4, New: ""},
|
||||
{Start: 6, End: 6, New: "B\n"},
|
||||
{Start: 10, End: 12, New: ""},
|
||||
{Start: 14, End: 14, New: "C\n"},
|
||||
},
|
||||
LineEdits: []diff.Edit{
|
||||
{Start: 0, End: 4, New: ""},
|
||||
{Start: 6, End: 6, New: "B\n"},
|
||||
{Start: 10, End: 12, New: ""},
|
||||
{Start: 14, End: 14, New: "C\n"},
|
||||
},
|
||||
}, {
|
||||
Name: "replace_last_line",
|
||||
In: "A\nB\n",
|
||||
Out: "A\nC\n\n",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1,2 +1,3 @@
|
||||
A
|
||||
-B
|
||||
+C
|
||||
+
|
||||
`[1:],
|
||||
Edits: []diff.Edit{{Start: 2, End: 3, New: "C\n"}},
|
||||
LineEdits: []diff.Edit{{Start: 2, End: 4, New: "C\n\n"}},
|
||||
},
|
||||
{
|
||||
Name: "multiple_replace",
|
||||
In: "A\nB\nC\nD\nE\nF\nG\n",
|
||||
Out: "A\nH\nI\nJ\nE\nF\nK\n",
|
||||
Unified: UnifiedPrefix + `
|
||||
@@ -1,7 +1,7 @@
|
||||
A
|
||||
-B
|
||||
-C
|
||||
-D
|
||||
+H
|
||||
+I
|
||||
+J
|
||||
E
|
||||
F
|
||||
-G
|
||||
+K
|
||||
`[1:],
|
||||
Edits: []diff.Edit{
|
||||
{Start: 2, End: 8, New: "H\nI\nJ\n"},
|
||||
{Start: 12, End: 14, New: "K\n"},
|
||||
},
|
||||
NoDiff: true, // diff algorithm produces different delete/insert pattern
|
||||
},
|
||||
{
|
||||
Name: "extra_newline",
|
||||
In: "\nA\n",
|
||||
Out: "A\n",
|
||||
Edits: []diff.Edit{{Start: 0, End: 1, New: ""}},
|
||||
Unified: UnifiedPrefix + `@@ -1,2 +1 @@
|
||||
-
|
||||
A
|
||||
`,
|
||||
}, {
|
||||
Name: "unified_lines",
|
||||
In: "aaa\nccc\n",
|
||||
Out: "aaa\nbbb\nccc\n",
|
||||
Edits: []diff.Edit{{Start: 3, End: 3, New: "\nbbb"}},
|
||||
LineEdits: []diff.Edit{{Start: 0, End: 4, New: "aaa\nbbb\n"}},
|
||||
Unified: UnifiedPrefix + "@@ -1,2 +1,3 @@\n aaa\n+bbb\n ccc\n",
|
||||
}, {
|
||||
Name: "60379",
|
||||
In: `package a
|
||||
|
||||
type S struct {
|
||||
s fmt.Stringer
|
||||
}
|
||||
`,
|
||||
Out: `package a
|
||||
|
||||
type S struct {
|
||||
s fmt.Stringer
|
||||
}
|
||||
`,
|
||||
Edits: []diff.Edit{{Start: 27, End: 27, New: "\t"}},
|
||||
LineEdits: []diff.Edit{{Start: 27, End: 42, New: "\ts fmt.Stringer\n"}},
|
||||
Unified: UnifiedPrefix + "@@ -1,5 +1,5 @@\n package a\n \n type S struct {\n-s fmt.Stringer\n+\ts fmt.Stringer\n }\n",
|
||||
},
|
||||
}
|
||||
|
||||
func DiffTest(t *testing.T, compute func(before, after string) []diff.Edit) {
|
||||
for _, test := range TestCases {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
edits := compute(test.In, test.Out)
|
||||
got, err := diff.Apply(test.In, edits)
|
||||
if err != nil {
|
||||
t.Fatalf("Apply failed: %v", err)
|
||||
}
|
||||
unified, err := diff.ToUnified(FileA, FileB, test.In, edits, diff.DefaultContextLines)
|
||||
if err != nil {
|
||||
t.Fatalf("ToUnified: %v", err)
|
||||
}
|
||||
if got != test.Out {
|
||||
t.Errorf("Apply: got patched:\n%v\nfrom diff:\n%v\nexpected:\n%v",
|
||||
got, unified, test.Out)
|
||||
}
|
||||
if !test.NoDiff && unified != test.Unified {
|
||||
t.Errorf("Unified: got diff:\n%q\nexpected:\n%q diffs:%v",
|
||||
unified, test.Unified, edits)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package difftest supplies a set of tests that will operate on any
|
||||
// implementation of a diff algorithm as exposed by
|
||||
// "golang.org/x/tools/internal/diff"
|
||||
package difftest_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/diff/difftest"
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
)
|
||||
|
||||
func TestVerifyUnified(t *testing.T) {
|
||||
testenv.NeedsTool(t, "diff")
|
||||
for _, test := range difftest.TestCases {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
if test.NoDiff {
|
||||
t.Skip("diff tool produces expected different results")
|
||||
}
|
||||
diff, err := getDiffOutput(test.In, test.Out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(diff) > 0 {
|
||||
diff = difftest.UnifiedPrefix + diff
|
||||
}
|
||||
if diff != test.Unified {
|
||||
t.Errorf("unified:\n%s\ndiff -u:\n%s", test.Unified, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getDiffOutput(a, b string) (string, error) {
|
||||
fileA, err := os.CreateTemp("", "myers.in")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.Remove(fileA.Name())
|
||||
if _, err := fileA.Write([]byte(a)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := fileA.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
fileB, err := os.CreateTemp("", "myers.in")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer os.Remove(fileB.Name())
|
||||
if _, err := fileB.Write([]byte(b)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := fileB.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
cmd := exec.Command("diff", "-u", fileA.Name(), fileB.Name())
|
||||
cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
exit, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("can't exec %s: %v", cmd, err)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
// Nonzero exit with no output: terminated by signal?
|
||||
return "", fmt.Errorf("%s failed: %v; stderr:\n%s", cmd, err, exit.Stderr)
|
||||
}
|
||||
// nonzero exit + output => files differ
|
||||
}
|
||||
diff := string(out)
|
||||
if len(diff) <= 0 {
|
||||
return diff, nil
|
||||
}
|
||||
bits := strings.SplitN(diff, "\n", 3)
|
||||
if len(bits) != 3 {
|
||||
return "", fmt.Errorf("diff output did not have file prefix:\n%s", diff)
|
||||
}
|
||||
return bits[2], nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package diff
|
||||
|
||||
// This file exports some private declarations to tests.
|
||||
|
||||
var LineEdits = lineEdits
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package lcs
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// lcs is a longest common sequence
|
||||
type lcs []diag
|
||||
|
||||
// A diag is a piece of the edit graph where A[X+i] == B[Y+i], for 0<=i<Len.
|
||||
// All computed diagonals are parts of a longest common subsequence.
|
||||
type diag struct {
|
||||
X, Y int
|
||||
Len int
|
||||
}
|
||||
|
||||
// sort sorts in place, by lowest X, and if tied, inversely by Len
|
||||
func (l lcs) sort() lcs {
|
||||
sort.Slice(l, func(i, j int) bool {
|
||||
if l[i].X != l[j].X {
|
||||
return l[i].X < l[j].X
|
||||
}
|
||||
return l[i].Len > l[j].Len
|
||||
})
|
||||
return l
|
||||
}
|
||||
|
||||
// validate that the elements of the lcs do not overlap
|
||||
// (can only happen when the two-sided algorithm ends early)
|
||||
// expects the lcs to be sorted
|
||||
func (l lcs) valid() bool {
|
||||
for i := 1; i < len(l); i++ {
|
||||
if l[i-1].X+l[i-1].Len > l[i].X {
|
||||
return false
|
||||
}
|
||||
if l[i-1].Y+l[i-1].Len > l[i].Y {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// repair overlapping lcs
|
||||
// only called if two-sided stops early
|
||||
func (l lcs) fix() lcs {
|
||||
// from the set of diagonals in l, find a maximal non-conflicting set
|
||||
// this problem may be NP-complete, but we use a greedy heuristic,
|
||||
// which is quadratic, but with a better data structure, could be D log D.
|
||||
// indepedent is not enough: {0,3,1} and {3,0,2} can't both occur in an lcs
|
||||
// which has to have monotone x and y
|
||||
if len(l) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Slice(l, func(i, j int) bool { return l[i].Len > l[j].Len })
|
||||
tmp := make(lcs, 0, len(l))
|
||||
tmp = append(tmp, l[0])
|
||||
for i := 1; i < len(l); i++ {
|
||||
var dir direction
|
||||
nxt := l[i]
|
||||
for _, in := range tmp {
|
||||
if dir, nxt = overlap(in, nxt); dir == empty || dir == bad {
|
||||
break
|
||||
}
|
||||
}
|
||||
if nxt.Len > 0 && dir != bad {
|
||||
tmp = append(tmp, nxt)
|
||||
}
|
||||
}
|
||||
tmp.sort()
|
||||
if false && !tmp.valid() { // debug checking
|
||||
log.Fatalf("here %d", len(tmp))
|
||||
}
|
||||
return tmp
|
||||
}
|
||||
|
||||
type direction int
|
||||
|
||||
const (
|
||||
empty direction = iota // diag is empty (so not in lcs)
|
||||
leftdown // proposed acceptably to the left and below
|
||||
rightup // proposed diag is acceptably to the right and above
|
||||
bad // proposed diag is inconsistent with the lcs so far
|
||||
)
|
||||
|
||||
// overlap trims the proposed diag prop so it doesn't overlap with
|
||||
// the existing diag that has already been added to the lcs.
|
||||
func overlap(exist, prop diag) (direction, diag) {
|
||||
if prop.X <= exist.X && exist.X < prop.X+prop.Len {
|
||||
// remove the end of prop where it overlaps with the X end of exist
|
||||
delta := prop.X + prop.Len - exist.X
|
||||
prop.Len -= delta
|
||||
if prop.Len <= 0 {
|
||||
return empty, prop
|
||||
}
|
||||
}
|
||||
if exist.X <= prop.X && prop.X < exist.X+exist.Len {
|
||||
// remove the beginning of prop where overlaps with exist
|
||||
delta := exist.X + exist.Len - prop.X
|
||||
prop.Len -= delta
|
||||
if prop.Len <= 0 {
|
||||
return empty, prop
|
||||
}
|
||||
prop.X += delta
|
||||
prop.Y += delta
|
||||
}
|
||||
if prop.Y <= exist.Y && exist.Y < prop.Y+prop.Len {
|
||||
// remove the end of prop that overlaps (in Y) with exist
|
||||
delta := prop.Y + prop.Len - exist.Y
|
||||
prop.Len -= delta
|
||||
if prop.Len <= 0 {
|
||||
return empty, prop
|
||||
}
|
||||
}
|
||||
if exist.Y <= prop.Y && prop.Y < exist.Y+exist.Len {
|
||||
// remove the beginning of peop that overlaps with exist
|
||||
delta := exist.Y + exist.Len - prop.Y
|
||||
prop.Len -= delta
|
||||
if prop.Len <= 0 {
|
||||
return empty, prop
|
||||
}
|
||||
prop.X += delta // no test reaches this code
|
||||
prop.Y += delta
|
||||
}
|
||||
if prop.X+prop.Len <= exist.X && prop.Y+prop.Len <= exist.Y {
|
||||
return leftdown, prop
|
||||
}
|
||||
if exist.X+exist.Len <= prop.X && exist.Y+exist.Len <= prop.Y {
|
||||
return rightup, prop
|
||||
}
|
||||
// prop can't be in an lcs that contains exist
|
||||
return bad, prop
|
||||
}
|
||||
|
||||
// manipulating Diag and lcs
|
||||
|
||||
// prepend a diagonal (x,y)-(x+1,y+1) segment either to an empty lcs
|
||||
// or to its first Diag. prepend is only called to extend diagonals
|
||||
// the backward direction.
|
||||
func (lcs lcs) prepend(x, y int) lcs {
|
||||
if len(lcs) > 0 {
|
||||
d := &lcs[0]
|
||||
if int(d.X) == x+1 && int(d.Y) == y+1 {
|
||||
// extend the diagonal down and to the left
|
||||
d.X, d.Y = int(x), int(y)
|
||||
d.Len++
|
||||
return lcs
|
||||
}
|
||||
}
|
||||
|
||||
r := diag{X: int(x), Y: int(y), Len: 1}
|
||||
lcs = append([]diag{r}, lcs...)
|
||||
return lcs
|
||||
}
|
||||
|
||||
// append appends a diagonal, or extends the existing one.
|
||||
// by adding the edge (x,y)-(x+1.y+1). append is only called
|
||||
// to extend diagonals in the forward direction.
|
||||
func (lcs lcs) append(x, y int) lcs {
|
||||
if len(lcs) > 0 {
|
||||
last := &lcs[len(lcs)-1]
|
||||
// Expand last element if adjoining.
|
||||
if last.X+last.Len == x && last.Y+last.Len == y {
|
||||
last.Len++
|
||||
return lcs
|
||||
}
|
||||
}
|
||||
|
||||
return append(lcs, diag{X: x, Y: y, Len: 1})
|
||||
}
|
||||
|
||||
// enforce constraint on d, k
|
||||
func ok(d, k int) bool {
|
||||
return d >= 0 && -d <= k && k <= d
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package lcs
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type Btest struct {
|
||||
a, b string
|
||||
lcs []string
|
||||
}
|
||||
|
||||
var Btests = []Btest{
|
||||
{"aaabab", "abaab", []string{"abab", "aaab"}},
|
||||
{"aabbba", "baaba", []string{"aaba"}},
|
||||
{"cabbx", "cbabx", []string{"cabx", "cbbx"}},
|
||||
{"c", "cb", []string{"c"}},
|
||||
{"aaba", "bbb", []string{"b"}},
|
||||
{"bbaabb", "b", []string{"b"}},
|
||||
{"baaabb", "bbaba", []string{"bbb", "baa", "bab"}},
|
||||
{"baaabb", "abbab", []string{"abb", "bab", "aab"}},
|
||||
{"baaba", "aaabba", []string{"aaba"}},
|
||||
{"ca", "cba", []string{"ca"}},
|
||||
{"ccbcbc", "abba", []string{"bb"}},
|
||||
{"ccbcbc", "aabba", []string{"bb"}},
|
||||
{"ccb", "cba", []string{"cb"}},
|
||||
{"caef", "axe", []string{"ae"}},
|
||||
{"bbaabb", "baabb", []string{"baabb"}},
|
||||
// Example from Myers:
|
||||
{"abcabba", "cbabac", []string{"caba", "baba", "cbba"}},
|
||||
{"3456aaa", "aaa", []string{"aaa"}},
|
||||
{"aaa", "aaa123", []string{"aaa"}},
|
||||
{"aabaa", "aacaa", []string{"aaaa"}},
|
||||
{"1a", "a", []string{"a"}},
|
||||
{"abab", "bb", []string{"bb"}},
|
||||
{"123", "ab", []string{""}},
|
||||
{"a", "b", []string{""}},
|
||||
{"abc", "123", []string{""}},
|
||||
{"aa", "aa", []string{"aa"}},
|
||||
{"abcde", "12345", []string{""}},
|
||||
{"aaa3456", "aaa", []string{"aaa"}},
|
||||
{"abcde", "12345a", []string{"a"}},
|
||||
{"ab", "123", []string{""}},
|
||||
{"1a2", "a", []string{"a"}},
|
||||
// for two-sided
|
||||
{"babaab", "cccaba", []string{"aba"}},
|
||||
{"aabbab", "cbcabc", []string{"bab"}},
|
||||
{"abaabb", "bcacab", []string{"baab"}},
|
||||
{"abaabb", "abaaaa", []string{"abaa"}},
|
||||
{"bababb", "baaabb", []string{"baabb"}},
|
||||
{"abbbaa", "cabacc", []string{"aba"}},
|
||||
{"aabbaa", "aacaba", []string{"aaaa", "aaba"}},
|
||||
}
|
||||
|
||||
func init() {
|
||||
log.SetFlags(log.Lshortfile)
|
||||
}
|
||||
|
||||
func check(t *testing.T, str string, lcs lcs, want []string) {
|
||||
t.Helper()
|
||||
if !lcs.valid() {
|
||||
t.Errorf("bad lcs %v", lcs)
|
||||
}
|
||||
var got strings.Builder
|
||||
for _, dd := range lcs {
|
||||
got.WriteString(str[dd.X : dd.X+dd.Len])
|
||||
}
|
||||
ans := got.String()
|
||||
for _, w := range want {
|
||||
if ans == w {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("str=%q lcs=%v want=%q got=%q", str, lcs, want, ans)
|
||||
}
|
||||
|
||||
func checkDiffs(t *testing.T, before string, diffs []Diff, after string) {
|
||||
t.Helper()
|
||||
var ans strings.Builder
|
||||
sofar := 0 // index of position in before
|
||||
for _, d := range diffs {
|
||||
if sofar < d.Start {
|
||||
ans.WriteString(before[sofar:d.Start])
|
||||
}
|
||||
ans.WriteString(after[d.ReplStart:d.ReplEnd])
|
||||
sofar = d.End
|
||||
}
|
||||
ans.WriteString(before[sofar:])
|
||||
if ans.String() != after {
|
||||
t.Fatalf("diff %v took %q to %q, not to %q", diffs, before, ans.String(), after)
|
||||
}
|
||||
}
|
||||
|
||||
func lcslen(l lcs) int {
|
||||
ans := 0
|
||||
for _, d := range l {
|
||||
ans += int(d.Len)
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
// return a random string of length n made of characters from s
|
||||
func randstr(s string, n int) string {
|
||||
src := []rune(s)
|
||||
x := make([]rune, n)
|
||||
for i := 0; i < n; i++ {
|
||||
x[i] = src[rand.Intn(len(src))]
|
||||
}
|
||||
return string(x)
|
||||
}
|
||||
|
||||
func TestLcsFix(t *testing.T) {
|
||||
tests := []struct{ before, after lcs }{
|
||||
{lcs{diag{0, 0, 3}, diag{2, 2, 5}, diag{3, 4, 5}, diag{8, 9, 4}}, lcs{diag{0, 0, 2}, diag{2, 2, 1}, diag{3, 4, 5}, diag{8, 9, 4}}},
|
||||
{lcs{diag{1, 1, 6}, diag{6, 12, 3}}, lcs{diag{1, 1, 5}, diag{6, 12, 3}}},
|
||||
{lcs{diag{0, 0, 4}, diag{3, 5, 4}}, lcs{diag{0, 0, 3}, diag{3, 5, 4}}},
|
||||
{lcs{diag{0, 20, 1}, diag{0, 0, 3}, diag{1, 20, 4}}, lcs{diag{0, 0, 3}, diag{3, 22, 2}}},
|
||||
{lcs{diag{0, 0, 4}, diag{1, 1, 2}}, lcs{diag{0, 0, 4}}},
|
||||
{lcs{diag{0, 0, 4}}, lcs{diag{0, 0, 4}}},
|
||||
{lcs{}, lcs{}},
|
||||
{lcs{diag{0, 0, 4}, diag{1, 1, 6}, diag{3, 3, 2}}, lcs{diag{0, 0, 1}, diag{1, 1, 6}}},
|
||||
}
|
||||
for n, x := range tests {
|
||||
got := x.before.fix()
|
||||
if len(got) != len(x.after) {
|
||||
t.Errorf("got %v, expected %v, for %v", got, x.after, x.before)
|
||||
}
|
||||
olen := lcslen(x.after)
|
||||
glen := lcslen(got)
|
||||
if olen != glen {
|
||||
t.Errorf("%d: lens(%d,%d) differ, %v, %v, %v", n, glen, olen, got, x.after, x.before)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// package lcs contains code to find longest-common-subsequences
|
||||
// (and diffs)
|
||||
package lcs
|
||||
|
||||
/*
|
||||
Compute longest-common-subsequences of two slices A, B using
|
||||
algorithms from Myers' paper. A longest-common-subsequence
|
||||
(LCS from now on) of A and B is a maximal set of lexically increasing
|
||||
pairs of subscripts (x,y) with A[x]==B[y]. There may be many LCS, but
|
||||
they all have the same length. An LCS determines a sequence of edits
|
||||
that changes A into B.
|
||||
|
||||
The key concept is the edit graph of A and B.
|
||||
If A has length N and B has length M, then the edit graph has
|
||||
vertices v[i][j] for 0 <= i <= N, 0 <= j <= M. There is a
|
||||
horizontal edge from v[i][j] to v[i+1][j] whenever both are in
|
||||
the graph, and a vertical edge from v[i][j] to f[i][j+1] similarly.
|
||||
When A[i] == B[j] there is a diagonal edge from v[i][j] to v[i+1][j+1].
|
||||
|
||||
A path between in the graph between (0,0) and (N,M) determines a sequence
|
||||
of edits converting A into B: each horizontal edge corresponds to removing
|
||||
an element of A, and each vertical edge corresponds to inserting an
|
||||
element of B.
|
||||
|
||||
A vertex (x,y) is on (forward) diagonal k if x-y=k. A path in the graph
|
||||
is of length D if it has D non-diagonal edges. The algorithms generate
|
||||
forward paths (in which at least one of x,y increases at each edge),
|
||||
or backward paths (in which at least one of x,y decreases at each edge),
|
||||
or a combination. (Note that the orientation is the traditional mathematical one,
|
||||
with the origin in the lower-left corner.)
|
||||
|
||||
Here is the edit graph for A:"aabbaa", B:"aacaba". (I know the diagonals look weird.)
|
||||
⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙
|
||||
a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ |
|
||||
⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙
|
||||
b | | | ___/‾‾‾ | ___/‾‾‾ | | |
|
||||
⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙
|
||||
a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ |
|
||||
⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙
|
||||
c | | | | | | |
|
||||
⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙
|
||||
a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ |
|
||||
⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙
|
||||
a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ |
|
||||
⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙
|
||||
a a b b a a
|
||||
|
||||
|
||||
The algorithm labels a vertex (x,y) with D,k if it is on diagonal k and at
|
||||
the end of a maximal path of length D. (Because x-y=k it suffices to remember
|
||||
only the x coordinate of the vertex.)
|
||||
|
||||
The forward algorithm: Find the longest diagonal starting at (0,0) and
|
||||
label its end with D=0,k=0. From that vertex take a vertical step and
|
||||
then follow the longest diagonal (up and to the right), and label that vertex
|
||||
with D=1,k=-1. From the D=0,k=0 point take a horizontal step and the follow
|
||||
the longest diagonal (up and to the right) and label that vertex
|
||||
D=1,k=1. In the same way, having labelled all the D vertices,
|
||||
from a vertex labelled D,k find two vertices
|
||||
tentatively labelled D+1,k-1 and D+1,k+1. There may be two on the same
|
||||
diagonal, in which case take the one with the larger x.
|
||||
|
||||
Eventually the path gets to (N,M), and the diagonals on it are the LCS.
|
||||
|
||||
Here is the edit graph with the ends of D-paths labelled. (So, for instance,
|
||||
0/2,2 indicates that x=2,y=2 is labelled with 0, as it should be, since the first
|
||||
step is to go up the longest diagonal from (0,0).)
|
||||
A:"aabbaa", B:"aacaba"
|
||||
⊙ ------- ⊙ ------- ⊙ -------(3/3,6)------- ⊙ -------(3/5,6)-------(4/6,6)
|
||||
a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ |
|
||||
⊙ ------- ⊙ ------- ⊙ -------(2/3,5)------- ⊙ ------- ⊙ ------- ⊙
|
||||
b | | | ___/‾‾‾ | ___/‾‾‾ | | |
|
||||
⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ -------(3/5,4)------- ⊙
|
||||
a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ |
|
||||
⊙ ------- ⊙ -------(1/2,3)-------(2/3,3)------- ⊙ ------- ⊙ ------- ⊙
|
||||
c | | | | | | |
|
||||
⊙ ------- ⊙ -------(0/2,2)-------(1/3,2)-------(2/4,2)-------(3/5,2)-------(4/6,2)
|
||||
a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ |
|
||||
⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙
|
||||
a | ___/‾‾‾ | ___/‾‾‾ | | | ___/‾‾‾ | ___/‾‾‾ |
|
||||
⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙ ------- ⊙
|
||||
a a b b a a
|
||||
|
||||
The 4-path is reconstructed starting at (4/6,6), horizontal to (3/5,6), diagonal to (3,4), vertical
|
||||
to (2/3,3), horizontal to (1/2,3), vertical to (0/2,2), and diagonal to (0,0). As expected,
|
||||
there are 4 non-diagonal steps, and the diagonals form an LCS.
|
||||
|
||||
There is a symmetric backward algorithm, which gives (backwards labels are prefixed with a colon):
|
||||
A:"aabbaa", B:"aacaba"
|
||||
⊙ -------- ⊙ -------- ⊙ -------- ⊙ -------- ⊙ -------- ⊙ -------- ⊙
|
||||
a | ____/‾‾‾ | ____/‾‾‾ | | | ____/‾‾‾ | ____/‾‾‾ |
|
||||
⊙ -------- ⊙ -------- ⊙ -------- ⊙ -------- ⊙ --------(:0/5,5)-------- ⊙
|
||||
b | | | ____/‾‾‾ | ____/‾‾‾ | | |
|
||||
⊙ -------- ⊙ -------- ⊙ --------(:1/3,4)-------- ⊙ -------- ⊙ -------- ⊙
|
||||
a | ____/‾‾‾ | ____/‾‾‾ | | | ____/‾‾‾ | ____/‾‾‾ |
|
||||
(:3/0,3)--------(:2/1,3)-------- ⊙ --------(:2/3,3)--------(:1/4,3)-------- ⊙ -------- ⊙
|
||||
c | | | | | | |
|
||||
⊙ -------- ⊙ -------- ⊙ --------(:3/3,2)--------(:2/4,2)-------- ⊙ -------- ⊙
|
||||
a | ____/‾‾‾ | ____/‾‾‾ | | | ____/‾‾‾ | ____/‾‾‾ |
|
||||
(:3/0,1)-------- ⊙ -------- ⊙ -------- ⊙ --------(:3/4,1)-------- ⊙ -------- ⊙
|
||||
a | ____/‾‾‾ | ____/‾‾‾ | | | ____/‾‾‾ | ____/‾‾‾ |
|
||||
(:4/0,0)-------- ⊙ -------- ⊙ -------- ⊙ --------(:4/4,0)-------- ⊙ -------- ⊙
|
||||
a a b b a a
|
||||
|
||||
Neither of these is ideal for use in an editor, where it is undesirable to send very long diffs to the
|
||||
front end. It's tricky to decide exactly what 'very long diffs' means, as "replace A by B" is very short.
|
||||
We want to control how big D can be, by stopping when it gets too large. The forward algorithm then
|
||||
privileges common prefixes, and the backward algorithm privileges common suffixes. Either is an undesirable
|
||||
asymmetry.
|
||||
|
||||
Fortunately there is a two-sided algorithm, implied by results in Myers' paper. Here's what the labels in
|
||||
the edit graph look like.
|
||||
A:"aabbaa", B:"aacaba"
|
||||
⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙
|
||||
a | ____/‾‾‾‾ | ____/‾‾‾‾ | | | ____/‾‾‾‾ | ____/‾‾‾‾ |
|
||||
⊙ --------- ⊙ --------- ⊙ --------- (2/3,5) --------- ⊙ --------- (:0/5,5)--------- ⊙
|
||||
b | | | ____/‾‾‾‾ | ____/‾‾‾‾ | | |
|
||||
⊙ --------- ⊙ --------- ⊙ --------- (:1/3,4)--------- ⊙ --------- ⊙ --------- ⊙
|
||||
a | ____/‾‾‾‾ | ____/‾‾‾‾ | | | ____/‾‾‾‾ | ____/‾‾‾‾ |
|
||||
⊙ --------- (:2/1,3)--------- (1/2,3) ---------(2:2/3,3)--------- (:1/4,3)--------- ⊙ --------- ⊙
|
||||
c | | | | | | |
|
||||
⊙ --------- ⊙ --------- (0/2,2) --------- (1/3,2) ---------(2:2/4,2)--------- ⊙ --------- ⊙
|
||||
a | ____/‾‾‾‾ | ____/‾‾‾‾ | | | ____/‾‾‾‾ | ____/‾‾‾‾ |
|
||||
⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙
|
||||
a | ____/‾‾‾‾ | ____/‾‾‾‾ | | | ____/‾‾‾‾ | ____/‾‾‾‾ |
|
||||
⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙ --------- ⊙
|
||||
a a b b a a
|
||||
|
||||
The algorithm stopped when it saw the backwards 2-path ending at (1,3) and the forwards 2-path ending at (3,5). The criterion
|
||||
is a backwards path ending at (u,v) and a forward path ending at (x,y), where u <= x and the two points are on the same
|
||||
diagonal. (Here the edgegraph has a diagonal, but the criterion is x-y=u-v.) Myers proves there is a forward
|
||||
2-path from (0,0) to (1,3), and that together with the backwards 2-path ending at (1,3) gives the expected 4-path.
|
||||
Unfortunately the forward path has to be constructed by another run of the forward algorithm; it can't be found from the
|
||||
computed labels. That is the worst case. Had the code noticed (x,y)=(u,v)=(3,3) the whole path could be reconstructed
|
||||
from the edgegraph. The implementation looks for a number of special cases to try to avoid computing an extra forward path.
|
||||
|
||||
If the two-sided algorithm has stop early (because D has become too large) it will have found a forward LCS and a
|
||||
backwards LCS. Ideally these go with disjoint prefixes and suffixes of A and B, but disjointness may fail and the two
|
||||
computed LCS may conflict. (An easy example is where A is a suffix of B, and shares a short prefix. The backwards LCS
|
||||
is all of A, and the forward LCS is a prefix of A.) The algorithm combines the two
|
||||
to form a best-effort LCS. In the worst case the forward partial LCS may have to
|
||||
be recomputed.
|
||||
*/
|
||||
|
||||
/* Eugene Myers paper is titled
|
||||
"An O(ND) Difference Algorithm and Its Variations"
|
||||
and can be found at
|
||||
http://www.xmailserver.org/diff2.pdf
|
||||
|
||||
(There is a generic implementation of the algorithm the repository with git hash
|
||||
b9ad7e4ade3a686d608e44475390ad428e60e7fc)
|
||||
*/
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2022 The Go Authors. All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
#
|
||||
# Creates a zip file containing all numbered versions
|
||||
# of the commit history of a large source file, for use
|
||||
# as input data for the tests of the diff algorithm.
|
||||
#
|
||||
# Run script from root of the x/tools repo.
|
||||
|
||||
set -eu
|
||||
|
||||
# WARNING: This script will install the latest version of $file
|
||||
# The largest real source file in the x/tools repo.
|
||||
# file=internal/golang/completion/completion.go
|
||||
# file=internal/golang/diagnostics.go
|
||||
file=internal/protocol/tsprotocol.go
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
git log $file |
|
||||
awk '/^commit / {print $2}' |
|
||||
nl -ba -nrz |
|
||||
while read n hash; do
|
||||
git checkout --quiet $hash $file
|
||||
cp -f $file $tmp/$n
|
||||
done
|
||||
(cd $tmp && zip -q - *) > testdata.zip
|
||||
rm -fr $tmp
|
||||
git restore --staged $file
|
||||
git restore $file
|
||||
echo "Created testdata.zip"
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package lcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// For each D, vec[D] has length D+1,
|
||||
// and the label for (D, k) is stored in vec[D][(D+k)/2].
|
||||
type label struct {
|
||||
vec [][]int
|
||||
}
|
||||
|
||||
// Temporary checking DO NOT COMMIT true TO PRODUCTION CODE
|
||||
const debug = false
|
||||
|
||||
// debugging. check that the (d,k) pair is valid
|
||||
// (that is, -d<=k<=d and d+k even)
|
||||
func checkDK(D, k int) {
|
||||
if k >= -D && k <= D && (D+k)%2 == 0 {
|
||||
return
|
||||
}
|
||||
panic(fmt.Sprintf("out of range, d=%d,k=%d", D, k))
|
||||
}
|
||||
|
||||
func (t *label) set(D, k, x int) {
|
||||
if debug {
|
||||
checkDK(D, k)
|
||||
}
|
||||
for len(t.vec) <= D {
|
||||
t.vec = append(t.vec, nil)
|
||||
}
|
||||
if t.vec[D] == nil {
|
||||
t.vec[D] = make([]int, D+1)
|
||||
}
|
||||
t.vec[D][(D+k)/2] = x // known that D+k is even
|
||||
}
|
||||
|
||||
func (t *label) get(d, k int) int {
|
||||
if debug {
|
||||
checkDK(d, k)
|
||||
}
|
||||
return int(t.vec[d][(d+k)/2])
|
||||
}
|
||||
|
||||
func newtriang(limit int) label {
|
||||
if limit < 100 {
|
||||
// Preallocate if limit is not large.
|
||||
return label{vec: make([][]int, limit)}
|
||||
}
|
||||
return label{}
|
||||
}
|
||||
@@ -0,0 +1,480 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package lcs
|
||||
|
||||
// TODO(adonovan): remove unclear references to "old" in this package.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// A Diff is a replacement of a portion of A by a portion of B.
|
||||
type Diff struct {
|
||||
Start, End int // offsets of portion to delete in A
|
||||
ReplStart, ReplEnd int // offset of replacement text in B
|
||||
}
|
||||
|
||||
// DiffStrings returns the differences between two strings.
|
||||
// It does not respect rune boundaries.
|
||||
func DiffStrings(a, b string) []Diff { return diff(stringSeqs{a, b}) }
|
||||
|
||||
// DiffBytes returns the differences between two byte sequences.
|
||||
// It does not respect rune boundaries.
|
||||
func DiffBytes(a, b []byte) []Diff { return diff(bytesSeqs{a, b}) }
|
||||
|
||||
// DiffRunes returns the differences between two rune sequences.
|
||||
func DiffRunes(a, b []rune) []Diff { return diff(runesSeqs{a, b}) }
|
||||
|
||||
func diff(seqs sequences) []Diff {
|
||||
// A limit on how deeply the LCS algorithm should search. The value is just a guess.
|
||||
const maxDiffs = 100
|
||||
diff, _ := compute(seqs, twosided, maxDiffs/2)
|
||||
return diff
|
||||
}
|
||||
|
||||
// compute computes the list of differences between two sequences,
|
||||
// along with the LCS. It is exercised directly by tests.
|
||||
// The algorithm is one of {forward, backward, twosided}.
|
||||
func compute(seqs sequences, algo func(*editGraph) lcs, limit int) ([]Diff, lcs) {
|
||||
if limit <= 0 {
|
||||
limit = 1 << 25 // effectively infinity
|
||||
}
|
||||
alen, blen := seqs.lengths()
|
||||
g := &editGraph{
|
||||
seqs: seqs,
|
||||
vf: newtriang(limit),
|
||||
vb: newtriang(limit),
|
||||
limit: limit,
|
||||
ux: alen,
|
||||
uy: blen,
|
||||
delta: alen - blen,
|
||||
}
|
||||
lcs := algo(g)
|
||||
diffs := lcs.toDiffs(alen, blen)
|
||||
return diffs, lcs
|
||||
}
|
||||
|
||||
// editGraph carries the information for computing the lcs of two sequences.
|
||||
type editGraph struct {
|
||||
seqs sequences
|
||||
vf, vb label // forward and backward labels
|
||||
|
||||
limit int // maximal value of D
|
||||
// the bounding rectangle of the current edit graph
|
||||
lx, ly, ux, uy int
|
||||
delta int // common subexpression: (ux-lx)-(uy-ly)
|
||||
}
|
||||
|
||||
// toDiffs converts an LCS to a list of edits.
|
||||
func (lcs lcs) toDiffs(alen, blen int) []Diff {
|
||||
var diffs []Diff
|
||||
var pa, pb int // offsets in a, b
|
||||
for _, l := range lcs {
|
||||
if pa < l.X || pb < l.Y {
|
||||
diffs = append(diffs, Diff{pa, l.X, pb, l.Y})
|
||||
}
|
||||
pa = l.X + l.Len
|
||||
pb = l.Y + l.Len
|
||||
}
|
||||
if pa < alen || pb < blen {
|
||||
diffs = append(diffs, Diff{pa, alen, pb, blen})
|
||||
}
|
||||
return diffs
|
||||
}
|
||||
|
||||
// --- FORWARD ---
|
||||
|
||||
// fdone decides if the forward path has reached the upper right
|
||||
// corner of the rectangle. If so, it also returns the computed lcs.
|
||||
func (e *editGraph) fdone(D, k int) (bool, lcs) {
|
||||
// x, y, k are relative to the rectangle
|
||||
x := e.vf.get(D, k)
|
||||
y := x - k
|
||||
if x == e.ux && y == e.uy {
|
||||
return true, e.forwardlcs(D, k)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// run the forward algorithm, until success or up to the limit on D.
|
||||
func forward(e *editGraph) lcs {
|
||||
e.setForward(0, 0, e.lx)
|
||||
if ok, ans := e.fdone(0, 0); ok {
|
||||
return ans
|
||||
}
|
||||
// from D to D+1
|
||||
for D := 0; D < e.limit; D++ {
|
||||
e.setForward(D+1, -(D + 1), e.getForward(D, -D))
|
||||
if ok, ans := e.fdone(D+1, -(D + 1)); ok {
|
||||
return ans
|
||||
}
|
||||
e.setForward(D+1, D+1, e.getForward(D, D)+1)
|
||||
if ok, ans := e.fdone(D+1, D+1); ok {
|
||||
return ans
|
||||
}
|
||||
for k := -D + 1; k <= D-1; k += 2 {
|
||||
// these are tricky and easy to get backwards
|
||||
lookv := e.lookForward(k, e.getForward(D, k-1)+1)
|
||||
lookh := e.lookForward(k, e.getForward(D, k+1))
|
||||
if lookv > lookh {
|
||||
e.setForward(D+1, k, lookv)
|
||||
} else {
|
||||
e.setForward(D+1, k, lookh)
|
||||
}
|
||||
if ok, ans := e.fdone(D+1, k); ok {
|
||||
return ans
|
||||
}
|
||||
}
|
||||
}
|
||||
// D is too large
|
||||
// find the D path with maximal x+y inside the rectangle and
|
||||
// use that to compute the found part of the lcs
|
||||
kmax := -e.limit - 1
|
||||
diagmax := -1
|
||||
for k := -e.limit; k <= e.limit; k += 2 {
|
||||
x := e.getForward(e.limit, k)
|
||||
y := x - k
|
||||
if x+y > diagmax && x <= e.ux && y <= e.uy {
|
||||
diagmax, kmax = x+y, k
|
||||
}
|
||||
}
|
||||
return e.forwardlcs(e.limit, kmax)
|
||||
}
|
||||
|
||||
// recover the lcs by backtracking from the farthest point reached
|
||||
func (e *editGraph) forwardlcs(D, k int) lcs {
|
||||
var ans lcs
|
||||
for x := e.getForward(D, k); x != 0 || x-k != 0; {
|
||||
if ok(D-1, k-1) && x-1 == e.getForward(D-1, k-1) {
|
||||
// if (x-1,y) is labelled D-1, x--,D--,k--,continue
|
||||
D, k, x = D-1, k-1, x-1
|
||||
continue
|
||||
} else if ok(D-1, k+1) && x == e.getForward(D-1, k+1) {
|
||||
// if (x,y-1) is labelled D-1, x, D--,k++, continue
|
||||
D, k = D-1, k+1
|
||||
continue
|
||||
}
|
||||
// if (x-1,y-1)--(x,y) is a diagonal, prepend,x--,y--, continue
|
||||
y := x - k
|
||||
ans = ans.prepend(x+e.lx-1, y+e.ly-1)
|
||||
x--
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
// start at (x,y), go up the diagonal as far as possible,
|
||||
// and label the result with d
|
||||
func (e *editGraph) lookForward(k, relx int) int {
|
||||
rely := relx - k
|
||||
x, y := relx+e.lx, rely+e.ly
|
||||
if x < e.ux && y < e.uy {
|
||||
x += e.seqs.commonPrefixLen(x, e.ux, y, e.uy)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
func (e *editGraph) setForward(d, k, relx int) {
|
||||
x := e.lookForward(k, relx)
|
||||
e.vf.set(d, k, x-e.lx)
|
||||
}
|
||||
|
||||
func (e *editGraph) getForward(d, k int) int {
|
||||
x := e.vf.get(d, k)
|
||||
return x
|
||||
}
|
||||
|
||||
// --- BACKWARD ---
|
||||
|
||||
// bdone decides if the backward path has reached the lower left corner
|
||||
func (e *editGraph) bdone(D, k int) (bool, lcs) {
|
||||
// x, y, k are relative to the rectangle
|
||||
x := e.vb.get(D, k)
|
||||
y := x - (k + e.delta)
|
||||
if x == 0 && y == 0 {
|
||||
return true, e.backwardlcs(D, k)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// run the backward algorithm, until success or up to the limit on D.
|
||||
func backward(e *editGraph) lcs {
|
||||
e.setBackward(0, 0, e.ux)
|
||||
if ok, ans := e.bdone(0, 0); ok {
|
||||
return ans
|
||||
}
|
||||
// from D to D+1
|
||||
for D := 0; D < e.limit; D++ {
|
||||
e.setBackward(D+1, -(D + 1), e.getBackward(D, -D)-1)
|
||||
if ok, ans := e.bdone(D+1, -(D + 1)); ok {
|
||||
return ans
|
||||
}
|
||||
e.setBackward(D+1, D+1, e.getBackward(D, D))
|
||||
if ok, ans := e.bdone(D+1, D+1); ok {
|
||||
return ans
|
||||
}
|
||||
for k := -D + 1; k <= D-1; k += 2 {
|
||||
// these are tricky and easy to get wrong
|
||||
lookv := e.lookBackward(k, e.getBackward(D, k-1))
|
||||
lookh := e.lookBackward(k, e.getBackward(D, k+1)-1)
|
||||
if lookv < lookh {
|
||||
e.setBackward(D+1, k, lookv)
|
||||
} else {
|
||||
e.setBackward(D+1, k, lookh)
|
||||
}
|
||||
if ok, ans := e.bdone(D+1, k); ok {
|
||||
return ans
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// D is too large
|
||||
// find the D path with minimal x+y inside the rectangle and
|
||||
// use that to compute the part of the lcs found
|
||||
kmax := -e.limit - 1
|
||||
diagmin := 1 << 25
|
||||
for k := -e.limit; k <= e.limit; k += 2 {
|
||||
x := e.getBackward(e.limit, k)
|
||||
y := x - (k + e.delta)
|
||||
if x+y < diagmin && x >= 0 && y >= 0 {
|
||||
diagmin, kmax = x+y, k
|
||||
}
|
||||
}
|
||||
if kmax < -e.limit {
|
||||
panic(fmt.Sprintf("no paths when limit=%d?", e.limit))
|
||||
}
|
||||
return e.backwardlcs(e.limit, kmax)
|
||||
}
|
||||
|
||||
// recover the lcs by backtracking
|
||||
func (e *editGraph) backwardlcs(D, k int) lcs {
|
||||
var ans lcs
|
||||
for x := e.getBackward(D, k); x != e.ux || x-(k+e.delta) != e.uy; {
|
||||
if ok(D-1, k-1) && x == e.getBackward(D-1, k-1) {
|
||||
// D--, k--, x unchanged
|
||||
D, k = D-1, k-1
|
||||
continue
|
||||
} else if ok(D-1, k+1) && x+1 == e.getBackward(D-1, k+1) {
|
||||
// D--, k++, x++
|
||||
D, k, x = D-1, k+1, x+1
|
||||
continue
|
||||
}
|
||||
y := x - (k + e.delta)
|
||||
ans = ans.append(x+e.lx, y+e.ly)
|
||||
x++
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
// start at (x,y), go down the diagonal as far as possible,
|
||||
func (e *editGraph) lookBackward(k, relx int) int {
|
||||
rely := relx - (k + e.delta) // forward k = k + e.delta
|
||||
x, y := relx+e.lx, rely+e.ly
|
||||
if x > 0 && y > 0 {
|
||||
x -= e.seqs.commonSuffixLen(0, x, 0, y)
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// convert to rectangle, and label the result with d
|
||||
func (e *editGraph) setBackward(d, k, relx int) {
|
||||
x := e.lookBackward(k, relx)
|
||||
e.vb.set(d, k, x-e.lx)
|
||||
}
|
||||
|
||||
func (e *editGraph) getBackward(d, k int) int {
|
||||
x := e.vb.get(d, k)
|
||||
return x
|
||||
}
|
||||
|
||||
// -- TWOSIDED ---
|
||||
|
||||
func twosided(e *editGraph) lcs {
|
||||
// The termination condition could be improved, as either the forward
|
||||
// or backward pass could succeed before Myers' Lemma applies.
|
||||
// Aside from questions of efficiency (is the extra testing cost-effective)
|
||||
// this is more likely to matter when e.limit is reached.
|
||||
e.setForward(0, 0, e.lx)
|
||||
e.setBackward(0, 0, e.ux)
|
||||
|
||||
// from D to D+1
|
||||
for D := 0; D < e.limit; D++ {
|
||||
// just finished a backwards pass, so check
|
||||
if got, ok := e.twoDone(D, D); ok {
|
||||
return e.twolcs(D, D, got)
|
||||
}
|
||||
// do a forwards pass (D to D+1)
|
||||
e.setForward(D+1, -(D + 1), e.getForward(D, -D))
|
||||
e.setForward(D+1, D+1, e.getForward(D, D)+1)
|
||||
for k := -D + 1; k <= D-1; k += 2 {
|
||||
// these are tricky and easy to get backwards
|
||||
lookv := e.lookForward(k, e.getForward(D, k-1)+1)
|
||||
lookh := e.lookForward(k, e.getForward(D, k+1))
|
||||
if lookv > lookh {
|
||||
e.setForward(D+1, k, lookv)
|
||||
} else {
|
||||
e.setForward(D+1, k, lookh)
|
||||
}
|
||||
}
|
||||
// just did a forward pass, so check
|
||||
if got, ok := e.twoDone(D+1, D); ok {
|
||||
return e.twolcs(D+1, D, got)
|
||||
}
|
||||
// do a backward pass, D to D+1
|
||||
e.setBackward(D+1, -(D + 1), e.getBackward(D, -D)-1)
|
||||
e.setBackward(D+1, D+1, e.getBackward(D, D))
|
||||
for k := -D + 1; k <= D-1; k += 2 {
|
||||
// these are tricky and easy to get wrong
|
||||
lookv := e.lookBackward(k, e.getBackward(D, k-1))
|
||||
lookh := e.lookBackward(k, e.getBackward(D, k+1)-1)
|
||||
if lookv < lookh {
|
||||
e.setBackward(D+1, k, lookv)
|
||||
} else {
|
||||
e.setBackward(D+1, k, lookh)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// D too large. combine a forward and backward partial lcs
|
||||
// first, a forward one
|
||||
kmax := -e.limit - 1
|
||||
diagmax := -1
|
||||
for k := -e.limit; k <= e.limit; k += 2 {
|
||||
x := e.getForward(e.limit, k)
|
||||
y := x - k
|
||||
if x+y > diagmax && x <= e.ux && y <= e.uy {
|
||||
diagmax, kmax = x+y, k
|
||||
}
|
||||
}
|
||||
if kmax < -e.limit {
|
||||
panic(fmt.Sprintf("no forward paths when limit=%d?", e.limit))
|
||||
}
|
||||
lcs := e.forwardlcs(e.limit, kmax)
|
||||
// now a backward one
|
||||
// find the D path with minimal x+y inside the rectangle and
|
||||
// use that to compute the lcs
|
||||
diagmin := 1 << 25 // infinity
|
||||
for k := -e.limit; k <= e.limit; k += 2 {
|
||||
x := e.getBackward(e.limit, k)
|
||||
y := x - (k + e.delta)
|
||||
if x+y < diagmin && x >= 0 && y >= 0 {
|
||||
diagmin, kmax = x+y, k
|
||||
}
|
||||
}
|
||||
if kmax < -e.limit {
|
||||
panic(fmt.Sprintf("no backward paths when limit=%d?", e.limit))
|
||||
}
|
||||
lcs = append(lcs, e.backwardlcs(e.limit, kmax)...)
|
||||
// These may overlap (e.forwardlcs and e.backwardlcs return sorted lcs)
|
||||
ans := lcs.fix()
|
||||
return ans
|
||||
}
|
||||
|
||||
// Does Myers' Lemma apply?
|
||||
func (e *editGraph) twoDone(df, db int) (int, bool) {
|
||||
if (df+db+e.delta)%2 != 0 {
|
||||
return 0, false // diagonals cannot overlap
|
||||
}
|
||||
kmin := -db + e.delta
|
||||
if -df > kmin {
|
||||
kmin = -df
|
||||
}
|
||||
kmax := db + e.delta
|
||||
if df < kmax {
|
||||
kmax = df
|
||||
}
|
||||
for k := kmin; k <= kmax; k += 2 {
|
||||
x := e.vf.get(df, k)
|
||||
u := e.vb.get(db, k-e.delta)
|
||||
if u <= x {
|
||||
// is it worth looking at all the other k?
|
||||
for l := k; l <= kmax; l += 2 {
|
||||
x := e.vf.get(df, l)
|
||||
y := x - l
|
||||
u := e.vb.get(db, l-e.delta)
|
||||
v := u - l
|
||||
if x == u || u == 0 || v == 0 || y == e.uy || x == e.ux {
|
||||
return l, true
|
||||
}
|
||||
}
|
||||
return k, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (e *editGraph) twolcs(df, db, kf int) lcs {
|
||||
// db==df || db+1==df
|
||||
x := e.vf.get(df, kf)
|
||||
y := x - kf
|
||||
kb := kf - e.delta
|
||||
u := e.vb.get(db, kb)
|
||||
v := u - kf
|
||||
|
||||
// Myers proved there is a df-path from (0,0) to (u,v)
|
||||
// and a db-path from (x,y) to (N,M).
|
||||
// In the first case the overall path is the forward path
|
||||
// to (u,v) followed by the backward path to (N,M).
|
||||
// In the second case the path is the backward path to (x,y)
|
||||
// followed by the forward path to (x,y) from (0,0).
|
||||
|
||||
// Look for some special cases to avoid computing either of these paths.
|
||||
if x == u {
|
||||
// "babaab" "cccaba"
|
||||
// already patched together
|
||||
lcs := e.forwardlcs(df, kf)
|
||||
lcs = append(lcs, e.backwardlcs(db, kb)...)
|
||||
return lcs.sort()
|
||||
}
|
||||
|
||||
// is (u-1,v) or (u,v-1) labelled df-1?
|
||||
// if so, that forward df-1-path plus a horizontal or vertical edge
|
||||
// is the df-path to (u,v), then plus the db-path to (N,M)
|
||||
if u > 0 && ok(df-1, u-1-v) && e.vf.get(df-1, u-1-v) == u-1 {
|
||||
// "aabbab" "cbcabc"
|
||||
lcs := e.forwardlcs(df-1, u-1-v)
|
||||
lcs = append(lcs, e.backwardlcs(db, kb)...)
|
||||
return lcs.sort()
|
||||
}
|
||||
if v > 0 && ok(df-1, (u-(v-1))) && e.vf.get(df-1, u-(v-1)) == u {
|
||||
// "abaabb" "bcacab"
|
||||
lcs := e.forwardlcs(df-1, u-(v-1))
|
||||
lcs = append(lcs, e.backwardlcs(db, kb)...)
|
||||
return lcs.sort()
|
||||
}
|
||||
|
||||
// The path can't possibly contribute to the lcs because it
|
||||
// is all horizontal or vertical edges
|
||||
if u == 0 || v == 0 || x == e.ux || y == e.uy {
|
||||
// "abaabb" "abaaaa"
|
||||
if u == 0 || v == 0 {
|
||||
return e.backwardlcs(db, kb)
|
||||
}
|
||||
return e.forwardlcs(df, kf)
|
||||
}
|
||||
|
||||
// is (x+1,y) or (x,y+1) labelled db-1?
|
||||
if x+1 <= e.ux && ok(db-1, x+1-y-e.delta) && e.vb.get(db-1, x+1-y-e.delta) == x+1 {
|
||||
// "bababb" "baaabb"
|
||||
lcs := e.backwardlcs(db-1, kb+1)
|
||||
lcs = append(lcs, e.forwardlcs(df, kf)...)
|
||||
return lcs.sort()
|
||||
}
|
||||
if y+1 <= e.uy && ok(db-1, x-(y+1)-e.delta) && e.vb.get(db-1, x-(y+1)-e.delta) == x {
|
||||
// "abbbaa" "cabacc"
|
||||
lcs := e.backwardlcs(db-1, kb-1)
|
||||
lcs = append(lcs, e.forwardlcs(df, kf)...)
|
||||
return lcs.sort()
|
||||
}
|
||||
|
||||
// need to compute another path
|
||||
// "aabbaa" "aacaba"
|
||||
lcs := e.backwardlcs(db, kb)
|
||||
oldx, oldy := e.ux, e.uy
|
||||
e.ux = u
|
||||
e.uy = v
|
||||
lcs = append(lcs, forward(e)...)
|
||||
e.ux, e.uy = oldx, oldy
|
||||
return lcs.sort()
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package lcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAlgosOld(t *testing.T) {
|
||||
for i, algo := range []func(*editGraph) lcs{forward, backward, twosided} {
|
||||
t.Run(strings.Fields("forward backward twosided")[i], func(t *testing.T) {
|
||||
for _, tx := range Btests {
|
||||
lim := len(tx.a) + len(tx.b)
|
||||
|
||||
diffs, lcs := compute(stringSeqs{tx.a, tx.b}, algo, lim)
|
||||
check(t, tx.a, lcs, tx.lcs)
|
||||
checkDiffs(t, tx.a, diffs, tx.b)
|
||||
|
||||
diffs, lcs = compute(stringSeqs{tx.b, tx.a}, algo, lim)
|
||||
check(t, tx.b, lcs, tx.lcs)
|
||||
checkDiffs(t, tx.b, diffs, tx.a)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntOld(t *testing.T) {
|
||||
// need to avoid any characters in btests
|
||||
lfill, rfill := "AAAAAAAAAAAA", "BBBBBBBBBBBB"
|
||||
for _, tx := range Btests {
|
||||
if len(tx.a) < 2 || len(tx.b) < 2 {
|
||||
continue
|
||||
}
|
||||
left := tx.a + lfill
|
||||
right := tx.b + rfill
|
||||
lim := len(tx.a) + len(tx.b)
|
||||
diffs, lcs := compute(stringSeqs{left, right}, twosided, lim)
|
||||
check(t, left, lcs, tx.lcs)
|
||||
checkDiffs(t, left, diffs, right)
|
||||
diffs, lcs = compute(stringSeqs{right, left}, twosided, lim)
|
||||
check(t, right, lcs, tx.lcs)
|
||||
checkDiffs(t, right, diffs, left)
|
||||
|
||||
left = lfill + tx.a
|
||||
right = rfill + tx.b
|
||||
diffs, lcs = compute(stringSeqs{left, right}, twosided, lim)
|
||||
check(t, left, lcs, tx.lcs)
|
||||
checkDiffs(t, left, diffs, right)
|
||||
diffs, lcs = compute(stringSeqs{right, left}, twosided, lim)
|
||||
check(t, right, lcs, tx.lcs)
|
||||
checkDiffs(t, right, diffs, left)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecialOld(t *testing.T) { // exercises lcs.fix
|
||||
a := "golang.org/x/tools/intern"
|
||||
b := "github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/intern"
|
||||
diffs, lcs := compute(stringSeqs{a, b}, twosided, 4)
|
||||
if !lcs.valid() {
|
||||
t.Errorf("%d,%v", len(diffs), lcs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegressionOld001(t *testing.T) {
|
||||
a := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n"
|
||||
|
||||
b := "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage diff_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/google/safehtml/template\"\n\t\"golang.org/x/tools/gopls/internal/lsp/diff\"\n\t\"golang.org/x/tools/internal/diff/difftest\"\n\t\"golang.org/x/tools/gopls/internal/span\"\n)\n"
|
||||
for i := 1; i < len(b); i++ {
|
||||
diffs, lcs := compute(stringSeqs{a, b}, twosided, i) // 14 from gopls
|
||||
if !lcs.valid() {
|
||||
t.Errorf("%d,%v", len(diffs), lcs)
|
||||
}
|
||||
checkDiffs(t, a, diffs, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegressionOld002(t *testing.T) {
|
||||
a := "n\"\n)\n"
|
||||
b := "n\"\n\t\"golang.org/x//nnal/stack\"\n)\n"
|
||||
for i := 1; i <= len(b); i++ {
|
||||
diffs, lcs := compute(stringSeqs{a, b}, twosided, i)
|
||||
if !lcs.valid() {
|
||||
t.Errorf("%d,%v", len(diffs), lcs)
|
||||
}
|
||||
checkDiffs(t, a, diffs, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegressionOld003(t *testing.T) {
|
||||
a := "golang.org/x/hello v1.0.0\nrequire golang.org/x/unused v1"
|
||||
b := "golang.org/x/hello v1"
|
||||
for i := 1; i <= len(a); i++ {
|
||||
diffs, lcs := compute(stringSeqs{a, b}, twosided, i)
|
||||
if !lcs.valid() {
|
||||
t.Errorf("%d,%v", len(diffs), lcs)
|
||||
}
|
||||
checkDiffs(t, a, diffs, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandOld(t *testing.T) {
|
||||
rand.Seed(1)
|
||||
for i := 0; i < 1000; i++ {
|
||||
// TODO(adonovan): use ASCII and bytesSeqs here? The use of
|
||||
// non-ASCII isn't relevant to the property exercised by the test.
|
||||
a := []rune(randstr("abω", 16))
|
||||
b := []rune(randstr("abωc", 16))
|
||||
seq := runesSeqs{a, b}
|
||||
|
||||
const lim = 24 // large enough to get true lcs
|
||||
_, forw := compute(seq, forward, lim)
|
||||
_, back := compute(seq, backward, lim)
|
||||
_, two := compute(seq, twosided, lim)
|
||||
if lcslen(two) != lcslen(forw) || lcslen(forw) != lcslen(back) {
|
||||
t.Logf("\n%v\n%v\n%v", forw, back, two)
|
||||
t.Fatalf("%d forw:%d back:%d two:%d", i, lcslen(forw), lcslen(back), lcslen(two))
|
||||
}
|
||||
if !two.valid() || !forw.valid() || !back.valid() {
|
||||
t.Errorf("check failure")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDiffAPI tests the public API functions (Diff{Bytes,Strings,Runes})
|
||||
// to ensure at least minimal parity of the three representations.
|
||||
func TestDiffAPI(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
a, b string
|
||||
wantStrings, wantBytes, wantRunes string
|
||||
}{
|
||||
{"abcXdef", "abcxdef", "[{3 4 3 4}]", "[{3 4 3 4}]", "[{3 4 3 4}]"}, // ASCII
|
||||
{"abcωdef", "abcΩdef", "[{3 5 3 5}]", "[{3 5 3 5}]", "[{3 4 3 4}]"}, // non-ASCII
|
||||
} {
|
||||
|
||||
gotStrings := fmt.Sprint(DiffStrings(test.a, test.b))
|
||||
if gotStrings != test.wantStrings {
|
||||
t.Errorf("DiffStrings(%q, %q) = %v, want %v",
|
||||
test.a, test.b, gotStrings, test.wantStrings)
|
||||
}
|
||||
gotBytes := fmt.Sprint(DiffBytes([]byte(test.a), []byte(test.b)))
|
||||
if gotBytes != test.wantBytes {
|
||||
t.Errorf("DiffBytes(%q, %q) = %v, want %v",
|
||||
test.a, test.b, gotBytes, test.wantBytes)
|
||||
}
|
||||
gotRunes := fmt.Sprint(DiffRunes([]rune(test.a), []rune(test.b)))
|
||||
if gotRunes != test.wantRunes {
|
||||
t.Errorf("DiffRunes(%q, %q) = %v, want %v",
|
||||
test.a, test.b, gotRunes, test.wantRunes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTwoOld(b *testing.B) {
|
||||
tests := genBench("abc", 96)
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, tt := range tests {
|
||||
_, two := compute(stringSeqs{tt.before, tt.after}, twosided, 100)
|
||||
if !two.valid() {
|
||||
b.Error("check failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkForwOld(b *testing.B) {
|
||||
tests := genBench("abc", 96)
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, tt := range tests {
|
||||
_, two := compute(stringSeqs{tt.before, tt.after}, forward, 100)
|
||||
if !two.valid() {
|
||||
b.Error("check failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func genBench(set string, n int) []struct{ before, after string } {
|
||||
// before and after for benchmarks. 24 strings of length n with
|
||||
// before and after differing at least once, and about 5%
|
||||
rand.Seed(3)
|
||||
var ans []struct{ before, after string }
|
||||
for i := 0; i < 24; i++ {
|
||||
// maybe b should have an approximately known number of diffs
|
||||
a := randstr(set, n)
|
||||
cnt := 0
|
||||
bb := make([]rune, 0, n)
|
||||
for _, r := range a {
|
||||
if rand.Float64() < .05 {
|
||||
cnt++
|
||||
r = 'N'
|
||||
}
|
||||
bb = append(bb, r)
|
||||
}
|
||||
if cnt == 0 {
|
||||
// avoid == shortcut
|
||||
bb[n/2] = 'N'
|
||||
}
|
||||
ans = append(ans, struct{ before, after string }{a, string(bb)})
|
||||
}
|
||||
return ans
|
||||
}
|
||||
|
||||
// This benchmark represents a common case for a diff command:
|
||||
// large file with a single relatively small diff in the middle.
|
||||
// (It's not clear whether this is representative of gopls workloads
|
||||
// or whether it is important to gopls diff performance.)
|
||||
//
|
||||
// TODO(adonovan) opt: it could be much faster. For example,
|
||||
// comparing a file against itself is about 10x faster than with the
|
||||
// small deletion in the middle. Strangely, comparing a file against
|
||||
// itself minus the last byte is faster still; I don't know why.
|
||||
// There is much low-hanging fruit here for further improvement.
|
||||
func BenchmarkLargeFileSmallDiff(b *testing.B) {
|
||||
data, err := os.ReadFile("old.go") // large file
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
n := len(data)
|
||||
|
||||
src := string(data)
|
||||
dst := src[:n*49/100] + src[n*51/100:] // remove 2% from the middle
|
||||
b.Run("string", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
compute(stringSeqs{src, dst}, twosided, len(src)+len(dst))
|
||||
}
|
||||
})
|
||||
|
||||
srcBytes := []byte(src)
|
||||
dstBytes := []byte(dst)
|
||||
b.Run("bytes", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
compute(bytesSeqs{srcBytes, dstBytes}, twosided, len(srcBytes)+len(dstBytes))
|
||||
}
|
||||
})
|
||||
|
||||
srcRunes := []rune(src)
|
||||
dstRunes := []rune(dst)
|
||||
b.Run("runes", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
compute(runesSeqs{srcRunes, dstRunes}, twosided, len(srcRunes)+len(dstRunes))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package lcs
|
||||
|
||||
// This file defines the abstract sequence over which the LCS algorithm operates.
|
||||
|
||||
// sequences abstracts a pair of sequences, A and B.
|
||||
type sequences interface {
|
||||
lengths() (int, int) // len(A), len(B)
|
||||
commonPrefixLen(ai, aj, bi, bj int) int // len(commonPrefix(A[ai:aj], B[bi:bj]))
|
||||
commonSuffixLen(ai, aj, bi, bj int) int // len(commonSuffix(A[ai:aj], B[bi:bj]))
|
||||
}
|
||||
|
||||
type stringSeqs struct{ a, b string }
|
||||
|
||||
func (s stringSeqs) lengths() (int, int) { return len(s.a), len(s.b) }
|
||||
func (s stringSeqs) commonPrefixLen(ai, aj, bi, bj int) int {
|
||||
return commonPrefixLenString(s.a[ai:aj], s.b[bi:bj])
|
||||
}
|
||||
func (s stringSeqs) commonSuffixLen(ai, aj, bi, bj int) int {
|
||||
return commonSuffixLenString(s.a[ai:aj], s.b[bi:bj])
|
||||
}
|
||||
|
||||
// The explicit capacity in s[i:j:j] leads to more efficient code.
|
||||
|
||||
type bytesSeqs struct{ a, b []byte }
|
||||
|
||||
func (s bytesSeqs) lengths() (int, int) { return len(s.a), len(s.b) }
|
||||
func (s bytesSeqs) commonPrefixLen(ai, aj, bi, bj int) int {
|
||||
return commonPrefixLenBytes(s.a[ai:aj:aj], s.b[bi:bj:bj])
|
||||
}
|
||||
func (s bytesSeqs) commonSuffixLen(ai, aj, bi, bj int) int {
|
||||
return commonSuffixLenBytes(s.a[ai:aj:aj], s.b[bi:bj:bj])
|
||||
}
|
||||
|
||||
type runesSeqs struct{ a, b []rune }
|
||||
|
||||
func (s runesSeqs) lengths() (int, int) { return len(s.a), len(s.b) }
|
||||
func (s runesSeqs) commonPrefixLen(ai, aj, bi, bj int) int {
|
||||
return commonPrefixLenRunes(s.a[ai:aj:aj], s.b[bi:bj:bj])
|
||||
}
|
||||
func (s runesSeqs) commonSuffixLen(ai, aj, bi, bj int) int {
|
||||
return commonSuffixLenRunes(s.a[ai:aj:aj], s.b[bi:bj:bj])
|
||||
}
|
||||
|
||||
// TODO(adonovan): optimize these functions using ideas from:
|
||||
// - https://go.dev/cl/408116 common.go
|
||||
// - https://go.dev/cl/421435 xor_generic.go
|
||||
|
||||
// TODO(adonovan): factor using generics when available,
|
||||
// but measure performance impact.
|
||||
|
||||
// commonPrefixLen* returns the length of the common prefix of a[ai:aj] and b[bi:bj].
|
||||
func commonPrefixLenBytes(a, b []byte) int {
|
||||
n := min(len(a), len(b))
|
||||
i := 0
|
||||
for i < n && a[i] == b[i] {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
func commonPrefixLenRunes(a, b []rune) int {
|
||||
n := min(len(a), len(b))
|
||||
i := 0
|
||||
for i < n && a[i] == b[i] {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
func commonPrefixLenString(a, b string) int {
|
||||
n := min(len(a), len(b))
|
||||
i := 0
|
||||
for i < n && a[i] == b[i] {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// commonSuffixLen* returns the length of the common suffix of a[ai:aj] and b[bi:bj].
|
||||
func commonSuffixLenBytes(a, b []byte) int {
|
||||
n := min(len(a), len(b))
|
||||
i := 0
|
||||
for i < n && a[len(a)-1-i] == b[len(b)-1-i] {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
func commonSuffixLenRunes(a, b []rune) int {
|
||||
n := min(len(a), len(b))
|
||||
i := 0
|
||||
for i < n && a[len(a)-1-i] == b[len(b)-1-i] {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
func commonSuffixLenString(a, b string) int {
|
||||
n := min(len(a), len(b))
|
||||
i := 0
|
||||
for i < n && a[len(a)-1-i] == b[len(b)-1-i] {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func min(x, y int) int {
|
||||
if x < y {
|
||||
return x
|
||||
} else {
|
||||
return y
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package myers implements the Myers diff algorithm.
|
||||
package myers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/internal/diff"
|
||||
)
|
||||
|
||||
// Sources:
|
||||
// https://blog.jcoglan.com/2017/02/17/the-myers-diff-algorithm-part-3/
|
||||
// https://www.codeproject.com/Articles/42279/%2FArticles%2F42279%2FInvestigating-Myers-diff-algorithm-Part-1-of-2
|
||||
|
||||
// ComputeEdits returns the diffs of two strings using a simple
|
||||
// line-based implementation, like [diff.Strings].
|
||||
//
|
||||
// Deprecated: this implementation is moribund. However, when diffs
|
||||
// appear in marker test expectations, they are the particular diffs
|
||||
// produced by this implementation. The marker test framework
|
||||
// asserts diff(orig, got)==wantDiff, but ideally it would compute
|
||||
// got==apply(orig, wantDiff) so that the notation of the diff
|
||||
// is immaterial.
|
||||
func ComputeEdits(before, after string) []diff.Edit {
|
||||
beforeLines := splitLines(before)
|
||||
ops := operations(beforeLines, splitLines(after))
|
||||
|
||||
// Build a table mapping line number to offset.
|
||||
lineOffsets := make([]int, 0, len(beforeLines)+1)
|
||||
total := 0
|
||||
for i := range beforeLines {
|
||||
lineOffsets = append(lineOffsets, total)
|
||||
total += len(beforeLines[i])
|
||||
}
|
||||
lineOffsets = append(lineOffsets, total) // EOF
|
||||
|
||||
edits := make([]diff.Edit, 0, len(ops))
|
||||
for _, op := range ops {
|
||||
start, end := lineOffsets[op.I1], lineOffsets[op.I2]
|
||||
switch op.Kind {
|
||||
case opDelete:
|
||||
// Delete: before[I1:I2] is deleted.
|
||||
edits = append(edits, diff.Edit{Start: start, End: end})
|
||||
case opInsert:
|
||||
// Insert: after[J1:J2] is inserted at before[I1:I1].
|
||||
if content := strings.Join(op.Content, ""); content != "" {
|
||||
edits = append(edits, diff.Edit{Start: start, End: end, New: content})
|
||||
}
|
||||
}
|
||||
}
|
||||
return edits
|
||||
}
|
||||
|
||||
// opKind is used to denote the type of operation a line represents.
|
||||
type opKind int
|
||||
|
||||
const (
|
||||
opDelete opKind = iota // line deleted from input (-)
|
||||
opInsert // line inserted into output (+)
|
||||
opEqual // line present in input and output
|
||||
)
|
||||
|
||||
func (kind opKind) String() string {
|
||||
switch kind {
|
||||
case opDelete:
|
||||
return "delete"
|
||||
case opInsert:
|
||||
return "insert"
|
||||
case opEqual:
|
||||
return "equal"
|
||||
default:
|
||||
panic("unknown opKind")
|
||||
}
|
||||
}
|
||||
|
||||
type operation struct {
|
||||
Kind opKind
|
||||
Content []string // content from b
|
||||
I1, I2 int // indices of the line in a
|
||||
J1 int // indices of the line in b, J2 implied by len(Content)
|
||||
}
|
||||
|
||||
// operations returns the list of operations to convert a into b, consolidating
|
||||
// operations for multiple lines and not including equal lines.
|
||||
func operations(a, b []string) []*operation {
|
||||
if len(a) == 0 && len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
trace, offset := shortestEditSequence(a, b)
|
||||
snakes := backtrack(trace, len(a), len(b), offset)
|
||||
|
||||
M, N := len(a), len(b)
|
||||
|
||||
var i int
|
||||
solution := make([]*operation, len(a)+len(b))
|
||||
|
||||
add := func(op *operation, i2, j2 int) {
|
||||
if op == nil {
|
||||
return
|
||||
}
|
||||
op.I2 = i2
|
||||
if op.Kind == opInsert {
|
||||
op.Content = b[op.J1:j2]
|
||||
}
|
||||
solution[i] = op
|
||||
i++
|
||||
}
|
||||
x, y := 0, 0
|
||||
for _, snake := range snakes {
|
||||
if len(snake) < 2 {
|
||||
continue
|
||||
}
|
||||
var op *operation
|
||||
// delete (horizontal)
|
||||
for snake[0]-snake[1] > x-y {
|
||||
if op == nil {
|
||||
op = &operation{
|
||||
Kind: opDelete,
|
||||
I1: x,
|
||||
J1: y,
|
||||
}
|
||||
}
|
||||
x++
|
||||
if x == M {
|
||||
break
|
||||
}
|
||||
}
|
||||
add(op, x, y)
|
||||
op = nil
|
||||
// insert (vertical)
|
||||
for snake[0]-snake[1] < x-y {
|
||||
if op == nil {
|
||||
op = &operation{
|
||||
Kind: opInsert,
|
||||
I1: x,
|
||||
J1: y,
|
||||
}
|
||||
}
|
||||
y++
|
||||
}
|
||||
add(op, x, y)
|
||||
op = nil
|
||||
// equal (diagonal)
|
||||
for x < snake[0] {
|
||||
x++
|
||||
y++
|
||||
}
|
||||
if x >= M && y >= N {
|
||||
break
|
||||
}
|
||||
}
|
||||
return solution[:i]
|
||||
}
|
||||
|
||||
// backtrack uses the trace for the edit sequence computation and returns the
|
||||
// "snakes" that make up the solution. A "snake" is a single deletion or
|
||||
// insertion followed by zero or diagonals.
|
||||
func backtrack(trace [][]int, x, y, offset int) [][]int {
|
||||
snakes := make([][]int, len(trace))
|
||||
d := len(trace) - 1
|
||||
for ; x > 0 && y > 0 && d > 0; d-- {
|
||||
V := trace[d]
|
||||
if len(V) == 0 {
|
||||
continue
|
||||
}
|
||||
snakes[d] = []int{x, y}
|
||||
|
||||
k := x - y
|
||||
|
||||
var kPrev int
|
||||
if k == -d || (k != d && V[k-1+offset] < V[k+1+offset]) {
|
||||
kPrev = k + 1
|
||||
} else {
|
||||
kPrev = k - 1
|
||||
}
|
||||
|
||||
x = V[kPrev+offset]
|
||||
y = x - kPrev
|
||||
}
|
||||
if x < 0 || y < 0 {
|
||||
return snakes
|
||||
}
|
||||
snakes[d] = []int{x, y}
|
||||
return snakes
|
||||
}
|
||||
|
||||
// shortestEditSequence returns the shortest edit sequence that converts a into b.
|
||||
func shortestEditSequence(a, b []string) ([][]int, int) {
|
||||
M, N := len(a), len(b)
|
||||
V := make([]int, 2*(N+M)+1)
|
||||
offset := N + M
|
||||
trace := make([][]int, N+M+1)
|
||||
|
||||
// Iterate through the maximum possible length of the SES (N+M).
|
||||
for d := 0; d <= N+M; d++ {
|
||||
copyV := make([]int, len(V))
|
||||
// k lines are represented by the equation y = x - k. We move in
|
||||
// increments of 2 because end points for even d are on even k lines.
|
||||
for k := -d; k <= d; k += 2 {
|
||||
// At each point, we either go down or to the right. We go down if
|
||||
// k == -d, and we go to the right if k == d. We also prioritize
|
||||
// the maximum x value, because we prefer deletions to insertions.
|
||||
var x int
|
||||
if k == -d || (k != d && V[k-1+offset] < V[k+1+offset]) {
|
||||
x = V[k+1+offset] // down
|
||||
} else {
|
||||
x = V[k-1+offset] + 1 // right
|
||||
}
|
||||
|
||||
y := x - k
|
||||
|
||||
// Diagonal moves while we have equal contents.
|
||||
for x < M && y < N && a[x] == b[y] {
|
||||
x++
|
||||
y++
|
||||
}
|
||||
|
||||
V[k+offset] = x
|
||||
|
||||
// Return if we've exceeded the maximum values.
|
||||
if x == M && y == N {
|
||||
// Makes sure to save the state of the array before returning.
|
||||
copy(copyV, V)
|
||||
trace[d] = copyV
|
||||
return trace, offset
|
||||
}
|
||||
}
|
||||
|
||||
// Save the state of the array.
|
||||
copy(copyV, V)
|
||||
trace[d] = copyV
|
||||
}
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
func splitLines(text string) []string {
|
||||
lines := strings.SplitAfter(text, "\n")
|
||||
if lines[len(lines)-1] == "" {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
return lines
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package myers_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/diff/difftest"
|
||||
"golang.org/x/tools/internal/diff/myers"
|
||||
)
|
||||
|
||||
func TestDiff(t *testing.T) {
|
||||
difftest.DiffTest(t, myers.ComputeEdits)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package diff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/tools/internal/diff/lcs"
|
||||
)
|
||||
|
||||
// Strings computes the differences between two strings.
|
||||
// The resulting edits respect rune boundaries.
|
||||
func Strings(before, after string) []Edit {
|
||||
if before == after {
|
||||
return nil // common case
|
||||
}
|
||||
|
||||
if isASCII(before) && isASCII(after) {
|
||||
// TODO(adonovan): opt: specialize diffASCII for strings.
|
||||
return diffASCII([]byte(before), []byte(after))
|
||||
}
|
||||
return diffRunes([]rune(before), []rune(after))
|
||||
}
|
||||
|
||||
// Bytes computes the differences between two byte slices.
|
||||
// The resulting edits respect rune boundaries.
|
||||
func Bytes(before, after []byte) []Edit {
|
||||
if bytes.Equal(before, after) {
|
||||
return nil // common case
|
||||
}
|
||||
|
||||
if isASCII(before) && isASCII(after) {
|
||||
return diffASCII(before, after)
|
||||
}
|
||||
return diffRunes(runes(before), runes(after))
|
||||
}
|
||||
|
||||
func diffASCII(before, after []byte) []Edit {
|
||||
diffs := lcs.DiffBytes(before, after)
|
||||
|
||||
// Convert from LCS diffs.
|
||||
res := make([]Edit, len(diffs))
|
||||
for i, d := range diffs {
|
||||
res[i] = Edit{d.Start, d.End, string(after[d.ReplStart:d.ReplEnd])}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func diffRunes(before, after []rune) []Edit {
|
||||
diffs := lcs.DiffRunes(before, after)
|
||||
|
||||
// The diffs returned by the lcs package use indexes
|
||||
// into whatever slice was passed in.
|
||||
// Convert rune offsets to byte offsets.
|
||||
res := make([]Edit, len(diffs))
|
||||
lastEnd := 0
|
||||
utf8Len := 0
|
||||
for i, d := range diffs {
|
||||
utf8Len += runesLen(before[lastEnd:d.Start]) // text between edits
|
||||
start := utf8Len
|
||||
utf8Len += runesLen(before[d.Start:d.End]) // text deleted by this edit
|
||||
res[i] = Edit{start, utf8Len, string(after[d.ReplStart:d.ReplEnd])}
|
||||
lastEnd = d.End
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// runes is like []rune(string(bytes)) without the duplicate allocation.
|
||||
func runes(bytes []byte) []rune {
|
||||
n := utf8.RuneCount(bytes)
|
||||
runes := make([]rune, n)
|
||||
for i := 0; i < n; i++ {
|
||||
r, sz := utf8.DecodeRune(bytes)
|
||||
bytes = bytes[sz:]
|
||||
runes[i] = r
|
||||
}
|
||||
return runes
|
||||
}
|
||||
|
||||
// runesLen returns the length in bytes of the UTF-8 encoding of runes.
|
||||
func runesLen(runes []rune) (len int) {
|
||||
for _, r := range runes {
|
||||
len += utf8.RuneLen(r)
|
||||
}
|
||||
return len
|
||||
}
|
||||
|
||||
// isASCII reports whether s contains only ASCII.
|
||||
func isASCII[S string | []byte](s S) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] >= utf8.RuneSelf {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package diff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DefaultContextLines is the number of unchanged lines of surrounding
|
||||
// context displayed by Unified. Use ToUnified to specify a different value.
|
||||
const DefaultContextLines = 3
|
||||
|
||||
// Unified returns a unified diff of the old and new strings.
|
||||
// The old and new labels are the names of the old and new files.
|
||||
// If the strings are equal, it returns the empty string.
|
||||
func Unified(oldLabel, newLabel, old, new string) string {
|
||||
edits := Strings(old, new)
|
||||
unified, err := ToUnified(oldLabel, newLabel, old, edits, DefaultContextLines)
|
||||
if err != nil {
|
||||
// Can't happen: edits are consistent.
|
||||
log.Fatalf("internal error in diff.Unified: %v", err)
|
||||
}
|
||||
return unified
|
||||
}
|
||||
|
||||
// ToUnified applies the edits to content and returns a unified diff,
|
||||
// with contextLines lines of (unchanged) context around each diff hunk.
|
||||
// The old and new labels are the names of the content and result files.
|
||||
// It returns an error if the edits are inconsistent; see ApplyEdits.
|
||||
func ToUnified(oldLabel, newLabel, content string, edits []Edit, contextLines int) (string, error) {
|
||||
u, err := toUnified(oldLabel, newLabel, content, edits, contextLines)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// unified represents a set of edits as a unified diff.
|
||||
type unified struct {
|
||||
// from is the name of the original file.
|
||||
from string
|
||||
// to is the name of the modified file.
|
||||
to string
|
||||
// hunks is the set of edit hunks needed to transform the file content.
|
||||
hunks []*hunk
|
||||
}
|
||||
|
||||
// Hunk represents a contiguous set of line edits to apply.
|
||||
type hunk struct {
|
||||
// The line in the original source where the hunk starts.
|
||||
fromLine int
|
||||
// The line in the original source where the hunk finishes.
|
||||
toLine int
|
||||
// The set of line based edits to apply.
|
||||
lines []line
|
||||
}
|
||||
|
||||
// Line represents a single line operation to apply as part of a Hunk.
|
||||
type line struct {
|
||||
// kind is the type of line this represents, deletion, insertion or copy.
|
||||
kind opKind
|
||||
// content is the content of this line.
|
||||
// For deletion it is the line being removed, for all others it is the line
|
||||
// to put in the output.
|
||||
content string
|
||||
}
|
||||
|
||||
// opKind is used to denote the type of operation a line represents.
|
||||
type opKind int
|
||||
|
||||
const (
|
||||
// opDelete is the operation kind for a line that is present in the input
|
||||
// but not in the output.
|
||||
opDelete opKind = iota
|
||||
// opInsert is the operation kind for a line that is new in the output.
|
||||
opInsert
|
||||
// opEqual is the operation kind for a line that is the same in the input and
|
||||
// output, often used to provide context around edited lines.
|
||||
opEqual
|
||||
)
|
||||
|
||||
// String returns a human readable representation of an OpKind. It is not
|
||||
// intended for machine processing.
|
||||
func (k opKind) String() string {
|
||||
switch k {
|
||||
case opDelete:
|
||||
return "delete"
|
||||
case opInsert:
|
||||
return "insert"
|
||||
case opEqual:
|
||||
return "equal"
|
||||
default:
|
||||
panic("unknown operation kind")
|
||||
}
|
||||
}
|
||||
|
||||
// toUnified takes a file contents and a sequence of edits, and calculates
|
||||
// a unified diff that represents those edits.
|
||||
func toUnified(fromName, toName string, content string, edits []Edit, contextLines int) (unified, error) {
|
||||
gap := contextLines * 2
|
||||
u := unified{
|
||||
from: fromName,
|
||||
to: toName,
|
||||
}
|
||||
if len(edits) == 0 {
|
||||
return u, nil
|
||||
}
|
||||
var err error
|
||||
edits, err = lineEdits(content, edits) // expand to whole lines
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
lines := splitLines(content)
|
||||
var h *hunk
|
||||
last := 0
|
||||
toLine := 0
|
||||
for _, edit := range edits {
|
||||
// Compute the zero-based line numbers of the edit start and end.
|
||||
// TODO(adonovan): opt: compute incrementally, avoid O(n^2).
|
||||
start := strings.Count(content[:edit.Start], "\n")
|
||||
end := strings.Count(content[:edit.End], "\n")
|
||||
if edit.End == len(content) && len(content) > 0 && content[len(content)-1] != '\n' {
|
||||
end++ // EOF counts as an implicit newline
|
||||
}
|
||||
|
||||
switch {
|
||||
case h != nil && start == last:
|
||||
//direct extension
|
||||
case h != nil && start <= last+gap:
|
||||
//within range of previous lines, add the joiners
|
||||
addEqualLines(h, lines, last, start)
|
||||
default:
|
||||
//need to start a new hunk
|
||||
if h != nil {
|
||||
// add the edge to the previous hunk
|
||||
addEqualLines(h, lines, last, last+contextLines)
|
||||
u.hunks = append(u.hunks, h)
|
||||
}
|
||||
toLine += start - last
|
||||
h = &hunk{
|
||||
fromLine: start + 1,
|
||||
toLine: toLine + 1,
|
||||
}
|
||||
// add the edge to the new hunk
|
||||
delta := addEqualLines(h, lines, start-contextLines, start)
|
||||
h.fromLine -= delta
|
||||
h.toLine -= delta
|
||||
}
|
||||
last = start
|
||||
for i := start; i < end; i++ {
|
||||
h.lines = append(h.lines, line{kind: opDelete, content: lines[i]})
|
||||
last++
|
||||
}
|
||||
if edit.New != "" {
|
||||
for _, content := range splitLines(edit.New) {
|
||||
h.lines = append(h.lines, line{kind: opInsert, content: content})
|
||||
toLine++
|
||||
}
|
||||
}
|
||||
}
|
||||
if h != nil {
|
||||
// add the edge to the final hunk
|
||||
addEqualLines(h, lines, last, last+contextLines)
|
||||
u.hunks = append(u.hunks, h)
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func splitLines(text string) []string {
|
||||
lines := strings.SplitAfter(text, "\n")
|
||||
if lines[len(lines)-1] == "" {
|
||||
lines = lines[:len(lines)-1]
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func addEqualLines(h *hunk, lines []string, start, end int) int {
|
||||
delta := 0
|
||||
for i := start; i < end; i++ {
|
||||
if i < 0 {
|
||||
continue
|
||||
}
|
||||
if i >= len(lines) {
|
||||
return delta
|
||||
}
|
||||
h.lines = append(h.lines, line{kind: opEqual, content: lines[i]})
|
||||
delta++
|
||||
}
|
||||
return delta
|
||||
}
|
||||
|
||||
// String converts a unified diff to the standard textual form for that diff.
|
||||
// The output of this function can be passed to tools like patch.
|
||||
func (u unified) String() string {
|
||||
if len(u.hunks) == 0 {
|
||||
return ""
|
||||
}
|
||||
b := new(strings.Builder)
|
||||
fmt.Fprintf(b, "--- %s\n", u.from)
|
||||
fmt.Fprintf(b, "+++ %s\n", u.to)
|
||||
for _, hunk := range u.hunks {
|
||||
fromCount, toCount := 0, 0
|
||||
for _, l := range hunk.lines {
|
||||
switch l.kind {
|
||||
case opDelete:
|
||||
fromCount++
|
||||
case opInsert:
|
||||
toCount++
|
||||
default:
|
||||
fromCount++
|
||||
toCount++
|
||||
}
|
||||
}
|
||||
fmt.Fprint(b, "@@")
|
||||
if fromCount > 1 {
|
||||
fmt.Fprintf(b, " -%d,%d", hunk.fromLine, fromCount)
|
||||
} else if hunk.fromLine == 1 && fromCount == 0 {
|
||||
// Match odd GNU diff -u behavior adding to empty file.
|
||||
fmt.Fprintf(b, " -0,0")
|
||||
} else {
|
||||
fmt.Fprintf(b, " -%d", hunk.fromLine)
|
||||
}
|
||||
if toCount > 1 {
|
||||
fmt.Fprintf(b, " +%d,%d", hunk.toLine, toCount)
|
||||
} else if hunk.toLine == 1 && toCount == 0 {
|
||||
// Match odd GNU diff -u behavior adding to empty file.
|
||||
fmt.Fprintf(b, " +0,0")
|
||||
} else {
|
||||
fmt.Fprintf(b, " +%d", hunk.toLine)
|
||||
}
|
||||
fmt.Fprint(b, " @@\n")
|
||||
for _, l := range hunk.lines {
|
||||
switch l.kind {
|
||||
case opDelete:
|
||||
fmt.Fprintf(b, "-%s", l.content)
|
||||
case opInsert:
|
||||
fmt.Fprintf(b, "+%s", l.content)
|
||||
default:
|
||||
fmt.Fprintf(b, " %s", l.content)
|
||||
}
|
||||
if !strings.HasSuffix(l.content, "\n") {
|
||||
fmt.Fprintf(b, "\n\\ No newline at end of file\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package diffp implements a basic diff algorithm equivalent to patience diff.
|
||||
// It is a copy of internal/diff from the main Go repo, renamed to diffp to avoid
|
||||
// conflict with the existing golang.org/x/tools/internal/diff.
|
||||
package diffp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A pair is a pair of values tracked for both the x and y side of a diff.
|
||||
// It is typically a pair of line indexes.
|
||||
type pair struct{ x, y int }
|
||||
|
||||
// Diff returns an anchored diff of the two texts old and new
|
||||
// in the “unified diff” format. If old and new are identical,
|
||||
// Diff returns a nil slice (no output).
|
||||
//
|
||||
// Unix diff implementations typically look for a diff with
|
||||
// the smallest number of lines inserted and removed,
|
||||
// which can in the worst case take time quadratic in the
|
||||
// number of lines in the texts. As a result, many implementations
|
||||
// either can be made to run for a long time or cut off the search
|
||||
// after a predetermined amount of work.
|
||||
//
|
||||
// In contrast, this implementation looks for a diff with the
|
||||
// smallest number of “unique” lines inserted and removed,
|
||||
// where unique means a line that appears just once in both old and new.
|
||||
// We call this an “anchored diff” because the unique lines anchor
|
||||
// the chosen matching regions. An anchored diff is usually clearer
|
||||
// than a standard diff, because the algorithm does not try to
|
||||
// reuse unrelated blank lines or closing braces.
|
||||
// The algorithm also guarantees to run in O(n log n) time
|
||||
// instead of the standard O(n²) time.
|
||||
//
|
||||
// Some systems call this approach a “patience diff,” named for
|
||||
// the “patience sorting” algorithm, itself named for a solitaire card game.
|
||||
// We avoid that name for two reasons. First, the name has been used
|
||||
// for a few different variants of the algorithm, so it is imprecise.
|
||||
// Second, the name is frequently interpreted as meaning that you have
|
||||
// to wait longer (to be patient) for the diff, meaning that it is a slower algorithm,
|
||||
// when in fact the algorithm is faster than the standard one.
|
||||
func Diff(oldName string, old []byte, newName string, new []byte) []byte {
|
||||
if bytes.Equal(old, new) {
|
||||
return nil
|
||||
}
|
||||
x := lines(old)
|
||||
y := lines(new)
|
||||
|
||||
// Print diff header.
|
||||
var out bytes.Buffer
|
||||
fmt.Fprintf(&out, "diff %s %s\n", oldName, newName)
|
||||
fmt.Fprintf(&out, "--- %s\n", oldName)
|
||||
fmt.Fprintf(&out, "+++ %s\n", newName)
|
||||
|
||||
// Loop over matches to consider,
|
||||
// expanding each match to include surrounding lines,
|
||||
// and then printing diff chunks.
|
||||
// To avoid setup/teardown cases outside the loop,
|
||||
// tgs returns a leading {0,0} and trailing {len(x), len(y)} pair
|
||||
// in the sequence of matches.
|
||||
var (
|
||||
done pair // printed up to x[:done.x] and y[:done.y]
|
||||
chunk pair // start lines of current chunk
|
||||
count pair // number of lines from each side in current chunk
|
||||
ctext []string // lines for current chunk
|
||||
)
|
||||
for _, m := range tgs(x, y) {
|
||||
if m.x < done.x {
|
||||
// Already handled scanning forward from earlier match.
|
||||
continue
|
||||
}
|
||||
|
||||
// Expand matching lines as far possible,
|
||||
// establishing that x[start.x:end.x] == y[start.y:end.y].
|
||||
// Note that on the first (or last) iteration we may (or definitey do)
|
||||
// have an empty match: start.x==end.x and start.y==end.y.
|
||||
start := m
|
||||
for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] {
|
||||
start.x--
|
||||
start.y--
|
||||
}
|
||||
end := m
|
||||
for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] {
|
||||
end.x++
|
||||
end.y++
|
||||
}
|
||||
|
||||
// Emit the mismatched lines before start into this chunk.
|
||||
// (No effect on first sentinel iteration, when start = {0,0}.)
|
||||
for _, s := range x[done.x:start.x] {
|
||||
ctext = append(ctext, "-"+s)
|
||||
count.x++
|
||||
}
|
||||
for _, s := range y[done.y:start.y] {
|
||||
ctext = append(ctext, "+"+s)
|
||||
count.y++
|
||||
}
|
||||
|
||||
// If we're not at EOF and have too few common lines,
|
||||
// the chunk includes all the common lines and continues.
|
||||
const C = 3 // number of context lines
|
||||
if (end.x < len(x) || end.y < len(y)) &&
|
||||
(end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) {
|
||||
for _, s := range x[start.x:end.x] {
|
||||
ctext = append(ctext, " "+s)
|
||||
count.x++
|
||||
count.y++
|
||||
}
|
||||
done = end
|
||||
continue
|
||||
}
|
||||
|
||||
// End chunk with common lines for context.
|
||||
if len(ctext) > 0 {
|
||||
n := end.x - start.x
|
||||
if n > C {
|
||||
n = C
|
||||
}
|
||||
for _, s := range x[start.x : start.x+n] {
|
||||
ctext = append(ctext, " "+s)
|
||||
count.x++
|
||||
count.y++
|
||||
}
|
||||
done = pair{start.x + n, start.y + n}
|
||||
|
||||
// Format and emit chunk.
|
||||
// Convert line numbers to 1-indexed.
|
||||
// Special case: empty file shows up as 0,0 not 1,0.
|
||||
if count.x > 0 {
|
||||
chunk.x++
|
||||
}
|
||||
if count.y > 0 {
|
||||
chunk.y++
|
||||
}
|
||||
fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y)
|
||||
for _, s := range ctext {
|
||||
out.WriteString(s)
|
||||
}
|
||||
count.x = 0
|
||||
count.y = 0
|
||||
ctext = ctext[:0]
|
||||
}
|
||||
|
||||
// If we reached EOF, we're done.
|
||||
if end.x >= len(x) && end.y >= len(y) {
|
||||
break
|
||||
}
|
||||
|
||||
// Otherwise start a new chunk.
|
||||
chunk = pair{end.x - C, end.y - C}
|
||||
for _, s := range x[chunk.x:end.x] {
|
||||
ctext = append(ctext, " "+s)
|
||||
count.x++
|
||||
count.y++
|
||||
}
|
||||
done = end
|
||||
}
|
||||
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
// lines returns the lines in the file x, including newlines.
|
||||
// If the file does not end in a newline, one is supplied
|
||||
// along with a warning about the missing newline.
|
||||
func lines(x []byte) []string {
|
||||
l := strings.SplitAfter(string(x), "\n")
|
||||
if l[len(l)-1] == "" {
|
||||
l = l[:len(l)-1]
|
||||
} else {
|
||||
// Treat last line as having a message about the missing newline attached,
|
||||
// using the same text as BSD/GNU diff (including the leading backslash).
|
||||
l[len(l)-1] += "\n\\ No newline at end of file\n"
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// tgs returns the pairs of indexes of the longest common subsequence
|
||||
// of unique lines in x and y, where a unique line is one that appears
|
||||
// once in x and once in y.
|
||||
//
|
||||
// The longest common subsequence algorithm is as described in
|
||||
// Thomas G. Szymanski, “A Special Case of the Maximal Common
|
||||
// Subsequence Problem,” Princeton TR #170 (January 1975),
|
||||
// available at https://research.swtch.com/tgs170.pdf.
|
||||
func tgs(x, y []string) []pair {
|
||||
// Count the number of times each string appears in a and b.
|
||||
// We only care about 0, 1, many, counted as 0, -1, -2
|
||||
// for the x side and 0, -4, -8 for the y side.
|
||||
// Using negative numbers now lets us distinguish positive line numbers later.
|
||||
m := make(map[string]int)
|
||||
for _, s := range x {
|
||||
if c := m[s]; c > -2 {
|
||||
m[s] = c - 1
|
||||
}
|
||||
}
|
||||
for _, s := range y {
|
||||
if c := m[s]; c > -8 {
|
||||
m[s] = c - 4
|
||||
}
|
||||
}
|
||||
|
||||
// Now unique strings can be identified by m[s] = -1+-4.
|
||||
//
|
||||
// Gather the indexes of those strings in x and y, building:
|
||||
// xi[i] = increasing indexes of unique strings in x.
|
||||
// yi[i] = increasing indexes of unique strings in y.
|
||||
// inv[i] = index j such that x[xi[i]] = y[yi[j]].
|
||||
var xi, yi, inv []int
|
||||
for i, s := range y {
|
||||
if m[s] == -1+-4 {
|
||||
m[s] = len(yi)
|
||||
yi = append(yi, i)
|
||||
}
|
||||
}
|
||||
for i, s := range x {
|
||||
if j, ok := m[s]; ok && j >= 0 {
|
||||
xi = append(xi, i)
|
||||
inv = append(inv, j)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply Algorithm A from Szymanski's paper.
|
||||
// In those terms, A = J = inv and B = [0, n).
|
||||
// We add sentinel pairs {0,0}, and {len(x),len(y)}
|
||||
// to the returned sequence, to help the processing loop.
|
||||
J := inv
|
||||
n := len(xi)
|
||||
T := make([]int, n)
|
||||
L := make([]int, n)
|
||||
for i := range T {
|
||||
T[i] = n + 1
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
k := sort.Search(n, func(k int) bool {
|
||||
return T[k] >= J[i]
|
||||
})
|
||||
T[k] = J[i]
|
||||
L[i] = k + 1
|
||||
}
|
||||
k := 0
|
||||
for _, v := range L {
|
||||
if k < v {
|
||||
k = v
|
||||
}
|
||||
}
|
||||
seq := make([]pair, 2+k)
|
||||
seq[1+k] = pair{len(x), len(y)} // sentinel at end
|
||||
lastj := n
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
if L[i] == k && J[i] < lastj {
|
||||
seq[k] = pair{xi[i], yi[J[i]]}
|
||||
k--
|
||||
}
|
||||
}
|
||||
seq[0] = pair{0, 0} // sentinel at start
|
||||
return seq
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package diffp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/txtar"
|
||||
)
|
||||
|
||||
func clean(text []byte) []byte {
|
||||
text = bytes.ReplaceAll(text, []byte("$\n"), []byte("\n"))
|
||||
text = bytes.TrimSuffix(text, []byte("^D\n"))
|
||||
return text
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
files, _ := filepath.Glob("testdata/*.txt")
|
||||
if len(files) == 0 {
|
||||
t.Fatalf("no testdata")
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
t.Run(filepath.Base(file), func(t *testing.T) {
|
||||
a, err := txtar.ParseFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(a.Files) != 3 || a.Files[2].Name != "diff" {
|
||||
t.Fatalf("%s: want three files, third named \"diff\"", file)
|
||||
}
|
||||
diffs := Diff(a.Files[0].Name, clean(a.Files[0].Data), a.Files[1].Name, clean(a.Files[1].Data))
|
||||
want := clean(a.Files[2].Data)
|
||||
if !bytes.Equal(diffs, want) {
|
||||
t.Fatalf("%s: have:\n%s\nwant:\n%s\n%s", file,
|
||||
diffs, want, Diff("have", diffs, "want", want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
-- old --
|
||||
-- new --
|
||||
a
|
||||
b
|
||||
c
|
||||
-- diff --
|
||||
diff old new
|
||||
--- old
|
||||
+++ new
|
||||
@@ -0,0 +1,3 @@
|
||||
+a
|
||||
+b
|
||||
+c
|
||||
@@ -0,0 +1,13 @@
|
||||
-- old --
|
||||
a
|
||||
b
|
||||
c
|
||||
-- new --
|
||||
-- diff --
|
||||
diff old new
|
||||
--- old
|
||||
+++ new
|
||||
@@ -1,3 +0,0 @@
|
||||
-a
|
||||
-b
|
||||
-c
|
||||
@@ -0,0 +1,35 @@
|
||||
Example from Hunt and McIlroy, “An Algorithm for Differential File Comparison.”
|
||||
https://www.cs.dartmouth.edu/~doug/diff.pdf
|
||||
|
||||
-- old --
|
||||
a
|
||||
b
|
||||
c
|
||||
d
|
||||
e
|
||||
f
|
||||
g
|
||||
-- new --
|
||||
w
|
||||
a
|
||||
b
|
||||
x
|
||||
y
|
||||
z
|
||||
e
|
||||
-- diff --
|
||||
diff old new
|
||||
--- old
|
||||
+++ new
|
||||
@@ -1,7 +1,7 @@
|
||||
+w
|
||||
a
|
||||
b
|
||||
-c
|
||||
-d
|
||||
+x
|
||||
+y
|
||||
+z
|
||||
e
|
||||
-f
|
||||
-g
|
||||
@@ -0,0 +1,40 @@
|
||||
-- old --
|
||||
a
|
||||
|
||||
b
|
||||
|
||||
c
|
||||
|
||||
d
|
||||
|
||||
e
|
||||
|
||||
f
|
||||
-- new --
|
||||
a
|
||||
|
||||
B
|
||||
|
||||
C
|
||||
|
||||
d
|
||||
|
||||
e
|
||||
|
||||
f
|
||||
-- diff --
|
||||
diff old new
|
||||
--- old
|
||||
+++ new
|
||||
@@ -1,8 +1,8 @@
|
||||
a
|
||||
$
|
||||
-b
|
||||
-
|
||||
-c
|
||||
+B
|
||||
+
|
||||
+C
|
||||
$
|
||||
d
|
||||
$
|
||||
@@ -0,0 +1,38 @@
|
||||
-- old --
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
eleven
|
||||
-- new --
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
-- diff --
|
||||
diff old new
|
||||
--- old
|
||||
+++ new
|
||||
@@ -5,7 +5,6 @@
|
||||
5
|
||||
6
|
||||
7
|
||||
-eight
|
||||
-nine
|
||||
-ten
|
||||
-eleven
|
||||
+8
|
||||
+9
|
||||
+10
|
||||
@@ -0,0 +1,9 @@
|
||||
-- old --
|
||||
a
|
||||
b
|
||||
c^D
|
||||
-- new --
|
||||
a
|
||||
b
|
||||
c^D
|
||||
-- diff --
|
||||
@@ -0,0 +1,18 @@
|
||||
-- old --
|
||||
a
|
||||
b
|
||||
c
|
||||
-- new --
|
||||
a
|
||||
b
|
||||
c^D
|
||||
-- diff --
|
||||
diff old new
|
||||
--- old
|
||||
+++ new
|
||||
@@ -1,3 +1,3 @@
|
||||
a
|
||||
b
|
||||
-c
|
||||
+c
|
||||
\ No newline at end of file
|
||||
@@ -0,0 +1,18 @@
|
||||
-- old --
|
||||
a
|
||||
b
|
||||
c^D
|
||||
-- new --
|
||||
a
|
||||
b
|
||||
c
|
||||
-- diff --
|
||||
diff old new
|
||||
--- old
|
||||
+++ new
|
||||
@@ -1,3 +1,3 @@
|
||||
a
|
||||
b
|
||||
-c
|
||||
\ No newline at end of file
|
||||
+c
|
||||
@@ -0,0 +1,62 @@
|
||||
-- old --
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
14½
|
||||
15
|
||||
16
|
||||
17
|
||||
18
|
||||
19
|
||||
20
|
||||
-- new --
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
13
|
||||
14
|
||||
17
|
||||
18
|
||||
19
|
||||
20
|
||||
-- diff --
|
||||
diff old new
|
||||
--- old
|
||||
+++ new
|
||||
@@ -4,7 +4,6 @@
|
||||
4
|
||||
5
|
||||
6
|
||||
-7
|
||||
8
|
||||
9
|
||||
10
|
||||
@@ -12,9 +11,6 @@
|
||||
12
|
||||
13
|
||||
14
|
||||
-14½
|
||||
-15
|
||||
-16
|
||||
17
|
||||
18
|
||||
19
|
||||
@@ -0,0 +1,5 @@
|
||||
-- old --
|
||||
hello world
|
||||
-- new --
|
||||
hello world
|
||||
-- diff --
|
||||
@@ -0,0 +1,34 @@
|
||||
-- old --
|
||||
e
|
||||
pi
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
-- new --
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
-- diff --
|
||||
diff old new
|
||||
--- old
|
||||
+++ new
|
||||
@@ -1,5 +1,6 @@
|
||||
-e
|
||||
-pi
|
||||
+1
|
||||
+2
|
||||
+3
|
||||
4
|
||||
5
|
||||
6
|
||||
@@ -0,0 +1,40 @@
|
||||
Another example from Hunt and McIlroy,
|
||||
“An Algorithm for Differential File Comparison.”
|
||||
https://www.cs.dartmouth.edu/~doug/diff.pdf
|
||||
|
||||
Anchored diff gives up on finding anything,
|
||||
since there are no unique lines.
|
||||
|
||||
-- old --
|
||||
a
|
||||
b
|
||||
c
|
||||
a
|
||||
b
|
||||
b
|
||||
a
|
||||
-- new --
|
||||
c
|
||||
a
|
||||
b
|
||||
a
|
||||
b
|
||||
c
|
||||
-- diff --
|
||||
diff old new
|
||||
--- old
|
||||
+++ new
|
||||
@@ -1,7 +1,6 @@
|
||||
-a
|
||||
-b
|
||||
-c
|
||||
-a
|
||||
-b
|
||||
-b
|
||||
-a
|
||||
+c
|
||||
+a
|
||||
+b
|
||||
+a
|
||||
+b
|
||||
+c
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The drivertest package provides a fake implementation of the go/packages
|
||||
// driver protocol that delegates to the go list driver. It may be used to test
|
||||
// programs such as gopls that specialize behavior when a go/packages driver is
|
||||
// in use.
|
||||
//
|
||||
// The driver is run as a child of the current process, by calling [RunIfChild]
|
||||
// at process start, and running go/packages with the environment variables set
|
||||
// by [Env].
|
||||
package drivertest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
const runAsDriverEnv = "DRIVERTEST_RUN_AS_DRIVER"
|
||||
|
||||
// RunIfChild runs the current process as a go/packages driver, if configured
|
||||
// to do so by the current environment (see [Env]).
|
||||
//
|
||||
// Otherwise, RunIfChild is a no op.
|
||||
func RunIfChild() {
|
||||
if os.Getenv(runAsDriverEnv) != "" {
|
||||
main()
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// Env returns additional environment variables for use in [packages.Config]
|
||||
// to enable the use of drivertest as the driver.
|
||||
//
|
||||
// t abstracts a *testing.T or log.Default().
|
||||
func Env(t interface{ Fatal(...any) }) []string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return []string{"GOPACKAGESDRIVER=" + exe, runAsDriverEnv + "=1"}
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
dec := json.NewDecoder(os.Stdin)
|
||||
var request packages.DriverRequest
|
||||
if err := dec.Decode(&request); err != nil {
|
||||
log.Fatalf("decoding request: %v", err)
|
||||
}
|
||||
|
||||
config := packages.Config{
|
||||
Mode: request.Mode,
|
||||
Env: append(request.Env, "GOPACKAGESDRIVER=off"), // avoid recursive invocation
|
||||
BuildFlags: request.BuildFlags,
|
||||
Tests: request.Tests,
|
||||
Overlay: request.Overlay,
|
||||
}
|
||||
pkgs, err := packages.Load(&config, flag.Args()...)
|
||||
if err != nil {
|
||||
log.Fatalf("load failed: %v", err)
|
||||
}
|
||||
|
||||
var roots []string
|
||||
for _, pkg := range pkgs {
|
||||
roots = append(roots, pkg.ID)
|
||||
}
|
||||
var allPackages []*packages.Package
|
||||
packages.Visit(pkgs, nil, func(pkg *packages.Package) {
|
||||
newImports := make(map[string]*packages.Package)
|
||||
for path, imp := range pkg.Imports {
|
||||
newImports[path] = &packages.Package{ID: imp.ID}
|
||||
}
|
||||
pkg.Imports = newImports
|
||||
allPackages = append(allPackages, pkg)
|
||||
})
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
response := packages.DriverResponse{
|
||||
Roots: roots,
|
||||
Packages: allPackages,
|
||||
}
|
||||
if err := enc.Encode(response); err != nil {
|
||||
log.Fatalf("encoding response: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package drivertest_test
|
||||
|
||||
// This file is both a test of drivertest and an example of how to use it in your own tests.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
"golang.org/x/tools/internal/diff"
|
||||
"golang.org/x/tools/internal/diff/myers"
|
||||
"golang.org/x/tools/internal/drivertest"
|
||||
"golang.org/x/tools/internal/packagesinternal"
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
"golang.org/x/tools/internal/testfiles"
|
||||
"golang.org/x/tools/txtar"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
drivertest.RunIfChild()
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestDriverConformance(t *testing.T) {
|
||||
testenv.NeedsExec(t)
|
||||
|
||||
const workspace = `
|
||||
-- go.mod --
|
||||
module example.com/m
|
||||
|
||||
go 1.20
|
||||
|
||||
-- m.go --
|
||||
package m
|
||||
|
||||
-- lib/lib.go --
|
||||
package lib
|
||||
`
|
||||
|
||||
fs, err := txtar.FS(txtar.Parse([]byte(workspace)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := testfiles.CopyToTmp(t, fs)
|
||||
|
||||
// TODO(rfindley): on mac, this is required to fix symlink path mismatches.
|
||||
// But why? Where is the symlink being evaluated in go/packages?
|
||||
dir, err = filepath.EvalSymlinks(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
baseConfig := packages.Config{
|
||||
Dir: dir,
|
||||
Mode: packages.NeedName |
|
||||
packages.NeedFiles |
|
||||
packages.NeedCompiledGoFiles |
|
||||
packages.NeedImports |
|
||||
packages.NeedDeps |
|
||||
packages.NeedTypesSizes |
|
||||
packages.NeedModule |
|
||||
packages.NeedEmbedFiles |
|
||||
packages.LoadMode(packagesinternal.DepsErrors) |
|
||||
packages.LoadMode(packagesinternal.ForTest),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
overlay string
|
||||
}{
|
||||
{
|
||||
name: "load all",
|
||||
query: "./...",
|
||||
},
|
||||
{
|
||||
name: "overlays",
|
||||
query: "./...",
|
||||
overlay: `
|
||||
-- m.go --
|
||||
package m
|
||||
|
||||
import . "lib"
|
||||
-- a/a.go --
|
||||
package a
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "std",
|
||||
query: "std",
|
||||
},
|
||||
{
|
||||
name: "builtin",
|
||||
query: "builtin",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
cfg := baseConfig
|
||||
if test.overlay != "" {
|
||||
cfg.Overlay = make(map[string][]byte)
|
||||
for _, file := range txtar.Parse([]byte(test.overlay)).Files {
|
||||
name := filepath.Join(dir, filepath.FromSlash(file.Name))
|
||||
cfg.Overlay[name] = file.Data
|
||||
}
|
||||
}
|
||||
|
||||
// Compare JSON-encoded packages with and without GOPACKAGESDRIVER.
|
||||
//
|
||||
// Note that this does not guarantee that the go/packages results
|
||||
// themselves are equivalent, only that their encoded JSON is equivalent.
|
||||
// Certain fields such as Module are intentionally omitted from external
|
||||
// drivers, because they don't make sense for an arbitrary build system.
|
||||
var jsons []string
|
||||
for _, env := range [][]string{
|
||||
{"GOPACKAGESDRIVER=off"},
|
||||
drivertest.Env(t),
|
||||
} {
|
||||
cfg.Env = append(os.Environ(), env...)
|
||||
pkgs, err := packages.Load(&cfg, test.query)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load (env: %v): %v", env, err)
|
||||
}
|
||||
data, err := json.MarshalIndent(pkgs, "", "\t")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal (env: %v): %v", env, err)
|
||||
}
|
||||
jsons = append(jsons, string(data))
|
||||
}
|
||||
|
||||
listJSON := jsons[0]
|
||||
driverJSON := jsons[1]
|
||||
|
||||
// Use the myers package for better line diffs.
|
||||
edits := myers.ComputeEdits(listJSON, driverJSON)
|
||||
d, err := diff.ToUnified("go list", "driver", listJSON, edits, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d != "" {
|
||||
t.Errorf("mismatching JSON:\n%s", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2017 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package edit implements buffered position-based editing of byte slices.
|
||||
package edit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// A Buffer is a queue of edits to apply to a given byte slice.
|
||||
type Buffer struct {
|
||||
old []byte
|
||||
q edits
|
||||
}
|
||||
|
||||
// An edit records a single text modification: change the bytes in [start,end) to new.
|
||||
type edit struct {
|
||||
start int
|
||||
end int
|
||||
new string
|
||||
}
|
||||
|
||||
// An edits is a list of edits that is sortable by start offset, breaking ties by end offset.
|
||||
type edits []edit
|
||||
|
||||
func (x edits) Len() int { return len(x) }
|
||||
func (x edits) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
func (x edits) Less(i, j int) bool {
|
||||
if x[i].start != x[j].start {
|
||||
return x[i].start < x[j].start
|
||||
}
|
||||
return x[i].end < x[j].end
|
||||
}
|
||||
|
||||
// NewBuffer returns a new buffer to accumulate changes to an initial data slice.
|
||||
// The returned buffer maintains a reference to the data, so the caller must ensure
|
||||
// the data is not modified until after the Buffer is done being used.
|
||||
func NewBuffer(old []byte) *Buffer {
|
||||
return &Buffer{old: old}
|
||||
}
|
||||
|
||||
// Insert inserts the new string at old[pos:pos].
|
||||
func (b *Buffer) Insert(pos int, new string) {
|
||||
if pos < 0 || pos > len(b.old) {
|
||||
panic("invalid edit position")
|
||||
}
|
||||
b.q = append(b.q, edit{pos, pos, new})
|
||||
}
|
||||
|
||||
// Delete deletes the text old[start:end].
|
||||
func (b *Buffer) Delete(start, end int) {
|
||||
if end < start || start < 0 || end > len(b.old) {
|
||||
panic("invalid edit position")
|
||||
}
|
||||
b.q = append(b.q, edit{start, end, ""})
|
||||
}
|
||||
|
||||
// Replace replaces old[start:end] with new.
|
||||
func (b *Buffer) Replace(start, end int, new string) {
|
||||
if end < start || start < 0 || end > len(b.old) {
|
||||
panic("invalid edit position")
|
||||
}
|
||||
b.q = append(b.q, edit{start, end, new})
|
||||
}
|
||||
|
||||
// Bytes returns a new byte slice containing the original data
|
||||
// with the queued edits applied.
|
||||
func (b *Buffer) Bytes() []byte {
|
||||
// Sort edits by starting position and then by ending position.
|
||||
// Breaking ties by ending position allows insertions at point x
|
||||
// to be applied before a replacement of the text at [x, y).
|
||||
sort.Stable(b.q)
|
||||
|
||||
var new []byte
|
||||
offset := 0
|
||||
for i, e := range b.q {
|
||||
if e.start < offset {
|
||||
e0 := b.q[i-1]
|
||||
panic(fmt.Sprintf("overlapping edits: [%d,%d)->%q, [%d,%d)->%q", e0.start, e0.end, e0.new, e.start, e.end, e.new))
|
||||
}
|
||||
new = append(new, b.old[offset:e.start]...)
|
||||
offset = e.end
|
||||
new = append(new, e.new...)
|
||||
}
|
||||
new = append(new, b.old[offset:]...)
|
||||
return new
|
||||
}
|
||||
|
||||
// String returns a string containing the original data
|
||||
// with the queued edits applied.
|
||||
func (b *Buffer) String() string {
|
||||
return string(b.Bytes())
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2017 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package edit
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestEdit(t *testing.T) {
|
||||
b := NewBuffer([]byte("0123456789"))
|
||||
b.Insert(8, ",7½,")
|
||||
b.Replace(9, 10, "the-end")
|
||||
b.Insert(10, "!")
|
||||
b.Insert(4, "3.14,")
|
||||
b.Insert(4, "π,")
|
||||
b.Insert(4, "3.15,")
|
||||
b.Replace(3, 4, "three,")
|
||||
want := "012three,3.14,π,3.15,4567,7½,8the-end!"
|
||||
|
||||
s := b.String()
|
||||
if s != want {
|
||||
t.Errorf("b.String() = %q, want %q", s, want)
|
||||
}
|
||||
sb := b.Bytes()
|
||||
if string(sb) != want {
|
||||
t.Errorf("b.Bytes() = %q, want %q", sb, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package event_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/event/core"
|
||||
"golang.org/x/tools/internal/event/export"
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
type Hooks struct {
|
||||
A func(ctx context.Context, a int) (context.Context, func())
|
||||
B func(ctx context.Context, b string) (context.Context, func())
|
||||
}
|
||||
|
||||
var (
|
||||
aValue = keys.NewInt("a", "")
|
||||
bValue = keys.NewString("b", "")
|
||||
aCount = keys.NewInt64("aCount", "Count of time A is called.")
|
||||
aStat = keys.NewInt("aValue", "A value.")
|
||||
bCount = keys.NewInt64("B", "Count of time B is called.")
|
||||
bLength = keys.NewInt("BLen", "B length.")
|
||||
|
||||
Baseline = Hooks{
|
||||
A: func(ctx context.Context, a int) (context.Context, func()) {
|
||||
return ctx, func() {}
|
||||
},
|
||||
B: func(ctx context.Context, b string) (context.Context, func()) {
|
||||
return ctx, func() {}
|
||||
},
|
||||
}
|
||||
|
||||
StdLog = Hooks{
|
||||
A: func(ctx context.Context, a int) (context.Context, func()) {
|
||||
log.Printf("A where a=%d", a)
|
||||
return ctx, func() {}
|
||||
},
|
||||
B: func(ctx context.Context, b string) (context.Context, func()) {
|
||||
log.Printf("B where b=%q", b)
|
||||
return ctx, func() {}
|
||||
},
|
||||
}
|
||||
|
||||
Log = Hooks{
|
||||
A: func(ctx context.Context, a int) (context.Context, func()) {
|
||||
core.Log1(ctx, "A", aValue.Of(a))
|
||||
return ctx, func() {}
|
||||
},
|
||||
B: func(ctx context.Context, b string) (context.Context, func()) {
|
||||
core.Log1(ctx, "B", bValue.Of(b))
|
||||
return ctx, func() {}
|
||||
},
|
||||
}
|
||||
|
||||
Trace = Hooks{
|
||||
A: func(ctx context.Context, a int) (context.Context, func()) {
|
||||
return core.Start1(ctx, "A", aValue.Of(a))
|
||||
},
|
||||
B: func(ctx context.Context, b string) (context.Context, func()) {
|
||||
return core.Start1(ctx, "B", bValue.Of(b))
|
||||
},
|
||||
}
|
||||
|
||||
Stats = Hooks{
|
||||
A: func(ctx context.Context, a int) (context.Context, func()) {
|
||||
core.Metric1(ctx, aStat.Of(a))
|
||||
core.Metric1(ctx, aCount.Of(1))
|
||||
return ctx, func() {}
|
||||
},
|
||||
B: func(ctx context.Context, b string) (context.Context, func()) {
|
||||
core.Metric1(ctx, bLength.Of(len(b)))
|
||||
core.Metric1(ctx, bCount.Of(1))
|
||||
return ctx, func() {}
|
||||
},
|
||||
}
|
||||
|
||||
initialList = []int{0, 1, 22, 333, 4444, 55555, 666666, 7777777}
|
||||
stringList = []string{
|
||||
"A value",
|
||||
"Some other value",
|
||||
"A nice longer value but not too long",
|
||||
"V",
|
||||
"",
|
||||
"ı",
|
||||
"prime count of values",
|
||||
}
|
||||
)
|
||||
|
||||
type namedBenchmark struct {
|
||||
name string
|
||||
test func(*testing.B)
|
||||
}
|
||||
|
||||
func Benchmark(b *testing.B) {
|
||||
b.Run("Baseline", Baseline.runBenchmark)
|
||||
b.Run("StdLog", StdLog.runBenchmark)
|
||||
benchmarks := []namedBenchmark{
|
||||
{"Log", Log.runBenchmark},
|
||||
{"Trace", Trace.runBenchmark},
|
||||
{"Stats", Stats.runBenchmark},
|
||||
}
|
||||
|
||||
event.SetExporter(nil)
|
||||
for _, t := range benchmarks {
|
||||
b.Run(t.name+"NoExporter", t.test)
|
||||
}
|
||||
|
||||
event.SetExporter(noopExporter)
|
||||
for _, t := range benchmarks {
|
||||
b.Run(t.name+"Noop", t.test)
|
||||
}
|
||||
|
||||
event.SetExporter(export.Spans(export.LogWriter(io.Discard, false)))
|
||||
for _, t := range benchmarks {
|
||||
b.Run(t.name, t.test)
|
||||
}
|
||||
}
|
||||
|
||||
func A(ctx context.Context, hooks Hooks, a int) int {
|
||||
ctx, done := hooks.A(ctx, a)
|
||||
defer done()
|
||||
return B(ctx, hooks, a, stringList[a%len(stringList)])
|
||||
}
|
||||
|
||||
func B(ctx context.Context, hooks Hooks, a int, b string) int {
|
||||
_, done := hooks.B(ctx, b)
|
||||
defer done()
|
||||
return a + len(b)
|
||||
}
|
||||
|
||||
func (hooks Hooks) runBenchmark(b *testing.B) {
|
||||
ctx := context.Background()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
var acc int
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, value := range initialList {
|
||||
acc += A(ctx, hooks, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
log.SetOutput(io.Discard)
|
||||
}
|
||||
|
||||
func noopExporter(ctx context.Context, ev core.Event, lm label.Map) context.Context {
|
||||
return ctx
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package core provides support for event based telemetry.
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
// Event holds the information about an event of note that occurred.
|
||||
type Event struct {
|
||||
at time.Time
|
||||
|
||||
// As events are often on the stack, storing the first few labels directly
|
||||
// in the event can avoid an allocation at all for the very common cases of
|
||||
// simple events.
|
||||
// The length needs to be large enough to cope with the majority of events
|
||||
// but no so large as to cause undue stack pressure.
|
||||
// A log message with two values will use 3 labels (one for each value and
|
||||
// one for the message itself).
|
||||
|
||||
static [3]label.Label // inline storage for the first few labels
|
||||
dynamic []label.Label // dynamically sized storage for remaining labels
|
||||
}
|
||||
|
||||
// eventLabelMap implements label.Map for a the labels of an Event.
|
||||
type eventLabelMap struct {
|
||||
event Event
|
||||
}
|
||||
|
||||
func (ev Event) At() time.Time { return ev.at }
|
||||
|
||||
func (ev Event) Format(f fmt.State, r rune) {
|
||||
if !ev.at.IsZero() {
|
||||
fmt.Fprint(f, ev.at.Format("2006/01/02 15:04:05 "))
|
||||
}
|
||||
for index := 0; ev.Valid(index); index++ {
|
||||
if l := ev.Label(index); l.Valid() {
|
||||
fmt.Fprintf(f, "\n\t%v", l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ev Event) Valid(index int) bool {
|
||||
return index >= 0 && index < len(ev.static)+len(ev.dynamic)
|
||||
}
|
||||
|
||||
func (ev Event) Label(index int) label.Label {
|
||||
if index < len(ev.static) {
|
||||
return ev.static[index]
|
||||
}
|
||||
return ev.dynamic[index-len(ev.static)]
|
||||
}
|
||||
|
||||
func (ev Event) Find(key label.Key) label.Label {
|
||||
for _, l := range ev.static {
|
||||
if l.Key() == key {
|
||||
return l
|
||||
}
|
||||
}
|
||||
for _, l := range ev.dynamic {
|
||||
if l.Key() == key {
|
||||
return l
|
||||
}
|
||||
}
|
||||
return label.Label{}
|
||||
}
|
||||
|
||||
func MakeEvent(static [3]label.Label, labels []label.Label) Event {
|
||||
return Event{
|
||||
static: static,
|
||||
dynamic: labels,
|
||||
}
|
||||
}
|
||||
|
||||
// CloneEvent event returns a copy of the event with the time adjusted to at.
|
||||
func CloneEvent(ev Event, at time.Time) Event {
|
||||
ev.at = at
|
||||
return ev
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
// Exporter is a function that handles events.
|
||||
// It may return a modified context and event.
|
||||
type Exporter func(context.Context, Event, label.Map) context.Context
|
||||
|
||||
var (
|
||||
exporter unsafe.Pointer
|
||||
)
|
||||
|
||||
// SetExporter sets the global exporter function that handles all events.
|
||||
// The exporter is called synchronously from the event call site, so it should
|
||||
// return quickly so as not to hold up user code.
|
||||
func SetExporter(e Exporter) {
|
||||
p := unsafe.Pointer(&e)
|
||||
if e == nil {
|
||||
// &e is always valid, and so p is always valid, but for the early abort
|
||||
// of ProcessEvent to be efficient it needs to make the nil check on the
|
||||
// pointer without having to dereference it, so we make the nil function
|
||||
// also a nil pointer
|
||||
p = nil
|
||||
}
|
||||
atomic.StorePointer(&exporter, p)
|
||||
}
|
||||
|
||||
// deliver is called to deliver an event to the supplied exporter.
|
||||
// it will fill in the time.
|
||||
func deliver(ctx context.Context, exporter Exporter, ev Event) context.Context {
|
||||
// add the current time to the event
|
||||
ev.at = time.Now()
|
||||
// hand the event off to the current exporter
|
||||
return exporter(ctx, ev, ev)
|
||||
}
|
||||
|
||||
// Export is called to deliver an event to the global exporter if set.
|
||||
func Export(ctx context.Context, ev Event) context.Context {
|
||||
// get the global exporter and abort early if there is not one
|
||||
exporterPtr := (*Exporter)(atomic.LoadPointer(&exporter))
|
||||
if exporterPtr == nil {
|
||||
return ctx
|
||||
}
|
||||
return deliver(ctx, *exporterPtr, ev)
|
||||
}
|
||||
|
||||
// ExportPair is called to deliver a start event to the supplied exporter.
|
||||
// It also returns a function that will deliver the end event to the same
|
||||
// exporter.
|
||||
// It will fill in the time.
|
||||
func ExportPair(ctx context.Context, begin, end Event) (context.Context, func()) {
|
||||
// get the global exporter and abort early if there is not one
|
||||
exporterPtr := (*Exporter)(atomic.LoadPointer(&exporter))
|
||||
if exporterPtr == nil {
|
||||
return ctx, func() {}
|
||||
}
|
||||
ctx = deliver(ctx, *exporterPtr, begin)
|
||||
return ctx, func() { deliver(ctx, *exporterPtr, end) }
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
// Log1 takes a message and one label delivers a log event to the exporter.
|
||||
// It is a customized version of Print that is faster and does no allocation.
|
||||
func Log1(ctx context.Context, message string, t1 label.Label) {
|
||||
Export(ctx, MakeEvent([3]label.Label{
|
||||
keys.Msg.Of(message),
|
||||
t1,
|
||||
}, nil))
|
||||
}
|
||||
|
||||
// Log2 takes a message and two labels and delivers a log event to the exporter.
|
||||
// It is a customized version of Print that is faster and does no allocation.
|
||||
func Log2(ctx context.Context, message string, t1 label.Label, t2 label.Label) {
|
||||
Export(ctx, MakeEvent([3]label.Label{
|
||||
keys.Msg.Of(message),
|
||||
t1,
|
||||
t2,
|
||||
}, nil))
|
||||
}
|
||||
|
||||
// Metric1 sends a label event to the exporter with the supplied labels.
|
||||
func Metric1(ctx context.Context, t1 label.Label) context.Context {
|
||||
return Export(ctx, MakeEvent([3]label.Label{
|
||||
keys.Metric.New(),
|
||||
t1,
|
||||
}, nil))
|
||||
}
|
||||
|
||||
// Metric2 sends a label event to the exporter with the supplied labels.
|
||||
func Metric2(ctx context.Context, t1, t2 label.Label) context.Context {
|
||||
return Export(ctx, MakeEvent([3]label.Label{
|
||||
keys.Metric.New(),
|
||||
t1,
|
||||
t2,
|
||||
}, nil))
|
||||
}
|
||||
|
||||
// Start1 sends a span start event with the supplied label list to the exporter.
|
||||
// It also returns a function that will end the span, which should normally be
|
||||
// deferred.
|
||||
func Start1(ctx context.Context, name string, t1 label.Label) (context.Context, func()) {
|
||||
return ExportPair(ctx,
|
||||
MakeEvent([3]label.Label{
|
||||
keys.Start.Of(name),
|
||||
t1,
|
||||
}, nil),
|
||||
MakeEvent([3]label.Label{
|
||||
keys.End.New(),
|
||||
}, nil))
|
||||
}
|
||||
|
||||
// Start2 sends a span start event with the supplied label list to the exporter.
|
||||
// It also returns a function that will end the span, which should normally be
|
||||
// deferred.
|
||||
func Start2(ctx context.Context, name string, t1, t2 label.Label) (context.Context, func()) {
|
||||
return ExportPair(ctx,
|
||||
MakeEvent([3]label.Label{
|
||||
keys.Start.Of(name),
|
||||
t1,
|
||||
t2,
|
||||
}, nil),
|
||||
MakeEvent([3]label.Label{
|
||||
keys.End.New(),
|
||||
}, nil))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package event provides a set of packages that cover the main
|
||||
// concepts of telemetry in an implementation agnostic way.
|
||||
package event
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package event
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/tools/internal/event/core"
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
// Exporter is a function that handles events.
|
||||
// It may return a modified context and event.
|
||||
type Exporter func(context.Context, core.Event, label.Map) context.Context
|
||||
|
||||
// SetExporter sets the global exporter function that handles all events.
|
||||
// The exporter is called synchronously from the event call site, so it should
|
||||
// return quickly so as not to hold up user code.
|
||||
func SetExporter(e Exporter) {
|
||||
core.SetExporter(core.Exporter(e))
|
||||
}
|
||||
|
||||
// Log takes a message and a label list and combines them into a single event
|
||||
// before delivering them to the exporter.
|
||||
func Log(ctx context.Context, message string, labels ...label.Label) {
|
||||
core.Export(ctx, core.MakeEvent([3]label.Label{
|
||||
keys.Msg.Of(message),
|
||||
}, labels))
|
||||
}
|
||||
|
||||
// IsLog returns true if the event was built by the Log function.
|
||||
// It is intended to be used in exporters to identify the semantics of the
|
||||
// event when deciding what to do with it.
|
||||
func IsLog(ev core.Event) bool {
|
||||
return ev.Label(0).Key() == keys.Msg
|
||||
}
|
||||
|
||||
// Error takes a message and a label list and combines them into a single event
|
||||
// before delivering them to the exporter. It captures the error in the
|
||||
// delivered event.
|
||||
func Error(ctx context.Context, message string, err error, labels ...label.Label) {
|
||||
core.Export(ctx, core.MakeEvent([3]label.Label{
|
||||
keys.Msg.Of(message),
|
||||
keys.Err.Of(err),
|
||||
}, labels))
|
||||
}
|
||||
|
||||
// IsError returns true if the event was built by the Error function.
|
||||
// It is intended to be used in exporters to identify the semantics of the
|
||||
// event when deciding what to do with it.
|
||||
func IsError(ev core.Event) bool {
|
||||
return ev.Label(0).Key() == keys.Msg &&
|
||||
ev.Label(1).Key() == keys.Err
|
||||
}
|
||||
|
||||
// Metric sends a label event to the exporter with the supplied labels.
|
||||
func Metric(ctx context.Context, labels ...label.Label) {
|
||||
core.Export(ctx, core.MakeEvent([3]label.Label{
|
||||
keys.Metric.New(),
|
||||
}, labels))
|
||||
}
|
||||
|
||||
// IsMetric returns true if the event was built by the Metric function.
|
||||
// It is intended to be used in exporters to identify the semantics of the
|
||||
// event when deciding what to do with it.
|
||||
func IsMetric(ev core.Event) bool {
|
||||
return ev.Label(0).Key() == keys.Metric
|
||||
}
|
||||
|
||||
// Label sends a label event to the exporter with the supplied labels.
|
||||
func Label(ctx context.Context, labels ...label.Label) context.Context {
|
||||
return core.Export(ctx, core.MakeEvent([3]label.Label{
|
||||
keys.Label.New(),
|
||||
}, labels))
|
||||
}
|
||||
|
||||
// IsLabel returns true if the event was built by the Label function.
|
||||
// It is intended to be used in exporters to identify the semantics of the
|
||||
// event when deciding what to do with it.
|
||||
func IsLabel(ev core.Event) bool {
|
||||
return ev.Label(0).Key() == keys.Label
|
||||
}
|
||||
|
||||
// Start sends a span start event with the supplied label list to the exporter.
|
||||
// It also returns a function that will end the span, which should normally be
|
||||
// deferred.
|
||||
func Start(ctx context.Context, name string, labels ...label.Label) (context.Context, func()) {
|
||||
return core.ExportPair(ctx,
|
||||
core.MakeEvent([3]label.Label{
|
||||
keys.Start.Of(name),
|
||||
}, labels),
|
||||
core.MakeEvent([3]label.Label{
|
||||
keys.End.New(),
|
||||
}, nil))
|
||||
}
|
||||
|
||||
// IsStart returns true if the event was built by the Start function.
|
||||
// It is intended to be used in exporters to identify the semantics of the
|
||||
// event when deciding what to do with it.
|
||||
func IsStart(ev core.Event) bool {
|
||||
return ev.Label(0).Key() == keys.Start
|
||||
}
|
||||
|
||||
// IsEnd returns true if the event was built by the End function.
|
||||
// It is intended to be used in exporters to identify the semantics of the
|
||||
// event when deciding what to do with it.
|
||||
func IsEnd(ev core.Event) bool {
|
||||
return ev.Label(0).Key() == keys.End
|
||||
}
|
||||
|
||||
// Detach returns a context without an associated span.
|
||||
// This allows the creation of spans that are not children of the current span.
|
||||
func Detach(ctx context.Context) context.Context {
|
||||
return core.Export(ctx, core.MakeEvent([3]label.Label{
|
||||
keys.Detach.New(),
|
||||
}, nil))
|
||||
}
|
||||
|
||||
// IsDetach returns true if the event was built by the Detach function.
|
||||
// It is intended to be used in exporters to identify the semantics of the
|
||||
// event when deciding what to do with it.
|
||||
func IsDetach(ev core.Event) bool {
|
||||
return ev.Label(0).Key() == keys.Detach
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package eventtest supports logging events to a test.
|
||||
// You can use NewContext to create a context that knows how to deliver
|
||||
// telemetry events back to the test.
|
||||
// You must use this context or a derived one anywhere you want telemetry to be
|
||||
// correctly routed back to the test it was constructed with.
|
||||
// Any events delivered to a background context will be dropped.
|
||||
//
|
||||
// Importing this package will cause it to register a new global telemetry
|
||||
// exporter that understands the special contexts returned by NewContext.
|
||||
// This means you should not import this package if you are not going to call
|
||||
// NewContext.
|
||||
package eventtest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/event/core"
|
||||
"golang.org/x/tools/internal/event/export"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
func init() {
|
||||
e := &testExporter{buffer: &bytes.Buffer{}}
|
||||
e.logger = export.LogWriter(e.buffer, false)
|
||||
|
||||
event.SetExporter(export.Spans(e.processEvent))
|
||||
}
|
||||
|
||||
type testingKeyType int
|
||||
|
||||
const testingKey = testingKeyType(0)
|
||||
|
||||
// NewContext returns a context you should use for the active test.
|
||||
func NewContext(ctx context.Context, t testing.TB) context.Context {
|
||||
return context.WithValue(ctx, testingKey, t)
|
||||
}
|
||||
|
||||
type testExporter struct {
|
||||
mu sync.Mutex
|
||||
buffer *bytes.Buffer
|
||||
logger event.Exporter
|
||||
}
|
||||
|
||||
func (w *testExporter) processEvent(ctx context.Context, ev core.Event, tm label.Map) context.Context {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
// build our log message in buffer
|
||||
result := w.logger(ctx, ev, tm)
|
||||
v := ctx.Value(testingKey)
|
||||
// get the testing.TB
|
||||
if w.buffer.Len() > 0 && v != nil {
|
||||
v.(testing.TB).Log(w.buffer)
|
||||
}
|
||||
w.buffer.Truncate(0)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package export
|
||||
|
||||
import (
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type TraceID [16]byte
|
||||
type SpanID [8]byte
|
||||
|
||||
func (t TraceID) String() string {
|
||||
return fmt.Sprintf("%02x", t[:])
|
||||
}
|
||||
|
||||
func (s SpanID) String() string {
|
||||
return fmt.Sprintf("%02x", s[:])
|
||||
}
|
||||
|
||||
func (s SpanID) IsValid() bool {
|
||||
return s != SpanID{}
|
||||
}
|
||||
|
||||
var (
|
||||
generationMu sync.Mutex
|
||||
nextSpanID uint64
|
||||
spanIDInc uint64
|
||||
|
||||
traceIDAdd [2]uint64
|
||||
traceIDRand *rand.Rand
|
||||
)
|
||||
|
||||
func initGenerator() {
|
||||
var rngSeed int64
|
||||
for _, p := range []interface{}{
|
||||
&rngSeed, &traceIDAdd, &nextSpanID, &spanIDInc,
|
||||
} {
|
||||
binary.Read(crand.Reader, binary.LittleEndian, p)
|
||||
}
|
||||
traceIDRand = rand.New(rand.NewSource(rngSeed))
|
||||
spanIDInc |= 1
|
||||
}
|
||||
|
||||
func newTraceID() TraceID {
|
||||
generationMu.Lock()
|
||||
defer generationMu.Unlock()
|
||||
if traceIDRand == nil {
|
||||
initGenerator()
|
||||
}
|
||||
var tid [16]byte
|
||||
binary.LittleEndian.PutUint64(tid[0:8], traceIDRand.Uint64()+traceIDAdd[0])
|
||||
binary.LittleEndian.PutUint64(tid[8:16], traceIDRand.Uint64()+traceIDAdd[1])
|
||||
return tid
|
||||
}
|
||||
|
||||
func newSpanID() SpanID {
|
||||
var id uint64
|
||||
for id == 0 {
|
||||
id = atomic.AddUint64(&nextSpanID, spanIDInc)
|
||||
}
|
||||
var sid [8]byte
|
||||
binary.LittleEndian.PutUint64(sid[:], id)
|
||||
return sid
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package export
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/event/core"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
// Labels builds an exporter that manipulates the context using the event.
|
||||
// If the event is type IsLabel or IsStartSpan then it returns a context updated
|
||||
// with label values from the event.
|
||||
// For all other event types the event labels will be updated with values from the
|
||||
// context if they are missing.
|
||||
func Labels(output event.Exporter) event.Exporter {
|
||||
return func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
|
||||
stored, _ := ctx.Value(labelContextKey).(label.Map)
|
||||
if event.IsLabel(ev) || event.IsStart(ev) {
|
||||
// update the label map stored in the context
|
||||
fromEvent := label.Map(ev)
|
||||
if stored == nil {
|
||||
stored = fromEvent
|
||||
} else {
|
||||
stored = label.MergeMaps(fromEvent, stored)
|
||||
}
|
||||
ctx = context.WithValue(ctx, labelContextKey, stored)
|
||||
}
|
||||
// add the stored label context to the label map
|
||||
lm = label.MergeMaps(lm, stored)
|
||||
return output(ctx, ev, lm)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package export
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/event/core"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
// LogWriter returns an Exporter that logs events to the supplied writer.
|
||||
// If onlyErrors is true it does not log any event that did not have an
|
||||
// associated error.
|
||||
// It ignores all telemetry other than log events.
|
||||
func LogWriter(w io.Writer, onlyErrors bool) event.Exporter {
|
||||
lw := &logWriter{writer: w, onlyErrors: onlyErrors}
|
||||
return lw.ProcessEvent
|
||||
}
|
||||
|
||||
type logWriter struct {
|
||||
mu sync.Mutex
|
||||
printer Printer
|
||||
writer io.Writer
|
||||
onlyErrors bool
|
||||
}
|
||||
|
||||
func (w *logWriter) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context {
|
||||
switch {
|
||||
case event.IsLog(ev):
|
||||
if w.onlyErrors && !event.IsError(ev) {
|
||||
return ctx
|
||||
}
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.printer.WriteEvent(w.writer, ev, lm)
|
||||
|
||||
case event.IsStart(ev):
|
||||
if span := GetSpan(ctx); span != nil {
|
||||
fmt.Fprintf(w.writer, "start: %v %v", span.Name, span.ID)
|
||||
if span.ParentID.IsValid() {
|
||||
fmt.Fprintf(w.writer, "[%v]", span.ParentID)
|
||||
}
|
||||
}
|
||||
case event.IsEnd(ev):
|
||||
if span := GetSpan(ctx); span != nil {
|
||||
fmt.Fprintf(w.writer, "finish: %v %v", span.Name, span.ID)
|
||||
}
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package export_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/event/core"
|
||||
"golang.org/x/tools/internal/event/export"
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
func ExampleLog() {
|
||||
ctx := context.Background()
|
||||
event.SetExporter(timeFixer(export.LogWriter(os.Stdout, false)))
|
||||
anInt := keys.NewInt("myInt", "an integer")
|
||||
aString := keys.NewString("myString", "a string")
|
||||
event.Log(ctx, "my event", anInt.Of(6))
|
||||
event.Error(ctx, "error event", errors.New("an error"), aString.Of("some string value"))
|
||||
// Output:
|
||||
// 2020/03/05 14:27:48 my event
|
||||
// myInt=6
|
||||
// 2020/03/05 14:27:48 error event: an error
|
||||
// myString="some string value"
|
||||
}
|
||||
|
||||
func timeFixer(output event.Exporter) event.Exporter {
|
||||
at, _ := time.Parse(time.RFC3339Nano, "2020-03-05T14:27:48Z")
|
||||
return func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
|
||||
copy := core.CloneEvent(ev, at)
|
||||
return output(ctx, copy, lm)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package metric
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
// Data represents a single point in the time series of a metric.
|
||||
// This provides the common interface to all metrics no matter their data
|
||||
// format.
|
||||
// To get the actual values for the metric you must type assert to a concrete
|
||||
// metric type.
|
||||
type Data interface {
|
||||
// Handle returns the metric handle this data is for.
|
||||
//TODO: rethink the concept of metric handles
|
||||
Handle() string
|
||||
// Groups reports the rows that currently exist for this metric.
|
||||
Groups() [][]label.Label
|
||||
}
|
||||
|
||||
// Int64Data is a concrete implementation of Data for int64 scalar metrics.
|
||||
type Int64Data struct {
|
||||
// Info holds the original construction information.
|
||||
Info *Scalar
|
||||
// IsGauge is true for metrics that track values, rather than increasing over time.
|
||||
IsGauge bool
|
||||
// Rows holds the per group values for the metric.
|
||||
Rows []int64
|
||||
// End is the last time this metric was updated.
|
||||
EndTime time.Time
|
||||
|
||||
groups [][]label.Label
|
||||
key *keys.Int64
|
||||
}
|
||||
|
||||
// Float64Data is a concrete implementation of Data for float64 scalar metrics.
|
||||
type Float64Data struct {
|
||||
// Info holds the original construction information.
|
||||
Info *Scalar
|
||||
// IsGauge is true for metrics that track values, rather than increasing over time.
|
||||
IsGauge bool
|
||||
// Rows holds the per group values for the metric.
|
||||
Rows []float64
|
||||
// End is the last time this metric was updated.
|
||||
EndTime time.Time
|
||||
|
||||
groups [][]label.Label
|
||||
key *keys.Float64
|
||||
}
|
||||
|
||||
// HistogramInt64Data is a concrete implementation of Data for int64 histogram metrics.
|
||||
type HistogramInt64Data struct {
|
||||
// Info holds the original construction information.
|
||||
Info *HistogramInt64
|
||||
// Rows holds the per group values for the metric.
|
||||
Rows []*HistogramInt64Row
|
||||
// End is the last time this metric was updated.
|
||||
EndTime time.Time
|
||||
|
||||
groups [][]label.Label
|
||||
key *keys.Int64
|
||||
}
|
||||
|
||||
// HistogramInt64Row holds the values for a single row of a HistogramInt64Data.
|
||||
type HistogramInt64Row struct {
|
||||
// Values is the counts per bucket.
|
||||
Values []int64
|
||||
// Count is the total count.
|
||||
Count int64
|
||||
// Sum is the sum of all the values recorded.
|
||||
Sum int64
|
||||
// Min is the smallest recorded value.
|
||||
Min int64
|
||||
// Max is the largest recorded value.
|
||||
Max int64
|
||||
}
|
||||
|
||||
// HistogramFloat64Data is a concrete implementation of Data for float64 histogram metrics.
|
||||
type HistogramFloat64Data struct {
|
||||
// Info holds the original construction information.
|
||||
Info *HistogramFloat64
|
||||
// Rows holds the per group values for the metric.
|
||||
Rows []*HistogramFloat64Row
|
||||
// End is the last time this metric was updated.
|
||||
EndTime time.Time
|
||||
|
||||
groups [][]label.Label
|
||||
key *keys.Float64
|
||||
}
|
||||
|
||||
// HistogramFloat64Row holds the values for a single row of a HistogramFloat64Data.
|
||||
type HistogramFloat64Row struct {
|
||||
// Values is the counts per bucket.
|
||||
Values []int64
|
||||
// Count is the total count.
|
||||
Count int64
|
||||
// Sum is the sum of all the values recorded.
|
||||
Sum float64
|
||||
// Min is the smallest recorded value.
|
||||
Min float64
|
||||
// Max is the largest recorded value.
|
||||
Max float64
|
||||
}
|
||||
|
||||
func labelListEqual(a, b []label.Label) bool {
|
||||
//TODO: make this more efficient
|
||||
return fmt.Sprint(a) == fmt.Sprint(b)
|
||||
}
|
||||
|
||||
func labelListLess(a, b []label.Label) bool {
|
||||
//TODO: make this more efficient
|
||||
return fmt.Sprint(a) < fmt.Sprint(b)
|
||||
}
|
||||
|
||||
func getGroup(lm label.Map, g *[][]label.Label, keys []label.Key) (int, bool) {
|
||||
group := make([]label.Label, len(keys))
|
||||
for i, key := range keys {
|
||||
l := lm.Find(key)
|
||||
if l.Valid() {
|
||||
group[i] = l
|
||||
}
|
||||
}
|
||||
old := *g
|
||||
index := sort.Search(len(old), func(i int) bool {
|
||||
return !labelListLess(old[i], group)
|
||||
})
|
||||
if index < len(old) && labelListEqual(group, old[index]) {
|
||||
// not a new group
|
||||
return index, false
|
||||
}
|
||||
*g = make([][]label.Label, len(old)+1)
|
||||
copy(*g, old[:index])
|
||||
copy((*g)[index+1:], old[index:])
|
||||
(*g)[index] = group
|
||||
return index, true
|
||||
}
|
||||
|
||||
func (data *Int64Data) Handle() string { return data.Info.Name }
|
||||
func (data *Int64Data) Groups() [][]label.Label { return data.groups }
|
||||
|
||||
func (data *Int64Data) modify(at time.Time, lm label.Map, f func(v int64) int64) Data {
|
||||
index, insert := getGroup(lm, &data.groups, data.Info.Keys)
|
||||
old := data.Rows
|
||||
if insert {
|
||||
data.Rows = make([]int64, len(old)+1)
|
||||
copy(data.Rows, old[:index])
|
||||
copy(data.Rows[index+1:], old[index:])
|
||||
} else {
|
||||
data.Rows = make([]int64, len(old))
|
||||
copy(data.Rows, old)
|
||||
}
|
||||
data.Rows[index] = f(data.Rows[index])
|
||||
data.EndTime = at
|
||||
frozen := *data
|
||||
return &frozen
|
||||
}
|
||||
|
||||
func (data *Int64Data) count(at time.Time, lm label.Map, l label.Label) Data {
|
||||
return data.modify(at, lm, func(v int64) int64 {
|
||||
return v + 1
|
||||
})
|
||||
}
|
||||
|
||||
func (data *Int64Data) sum(at time.Time, lm label.Map, l label.Label) Data {
|
||||
return data.modify(at, lm, func(v int64) int64 {
|
||||
return v + data.key.From(l)
|
||||
})
|
||||
}
|
||||
|
||||
func (data *Int64Data) latest(at time.Time, lm label.Map, l label.Label) Data {
|
||||
return data.modify(at, lm, func(v int64) int64 {
|
||||
return data.key.From(l)
|
||||
})
|
||||
}
|
||||
|
||||
func (data *Float64Data) Handle() string { return data.Info.Name }
|
||||
func (data *Float64Data) Groups() [][]label.Label { return data.groups }
|
||||
|
||||
func (data *Float64Data) modify(at time.Time, lm label.Map, f func(v float64) float64) Data {
|
||||
index, insert := getGroup(lm, &data.groups, data.Info.Keys)
|
||||
old := data.Rows
|
||||
if insert {
|
||||
data.Rows = make([]float64, len(old)+1)
|
||||
copy(data.Rows, old[:index])
|
||||
copy(data.Rows[index+1:], old[index:])
|
||||
} else {
|
||||
data.Rows = make([]float64, len(old))
|
||||
copy(data.Rows, old)
|
||||
}
|
||||
data.Rows[index] = f(data.Rows[index])
|
||||
data.EndTime = at
|
||||
frozen := *data
|
||||
return &frozen
|
||||
}
|
||||
|
||||
func (data *Float64Data) sum(at time.Time, lm label.Map, l label.Label) Data {
|
||||
return data.modify(at, lm, func(v float64) float64 {
|
||||
return v + data.key.From(l)
|
||||
})
|
||||
}
|
||||
|
||||
func (data *Float64Data) latest(at time.Time, lm label.Map, l label.Label) Data {
|
||||
return data.modify(at, lm, func(v float64) float64 {
|
||||
return data.key.From(l)
|
||||
})
|
||||
}
|
||||
|
||||
func (data *HistogramInt64Data) Handle() string { return data.Info.Name }
|
||||
func (data *HistogramInt64Data) Groups() [][]label.Label { return data.groups }
|
||||
|
||||
func (data *HistogramInt64Data) modify(at time.Time, lm label.Map, f func(v *HistogramInt64Row)) Data {
|
||||
index, insert := getGroup(lm, &data.groups, data.Info.Keys)
|
||||
old := data.Rows
|
||||
var v HistogramInt64Row
|
||||
if insert {
|
||||
data.Rows = make([]*HistogramInt64Row, len(old)+1)
|
||||
copy(data.Rows, old[:index])
|
||||
copy(data.Rows[index+1:], old[index:])
|
||||
} else {
|
||||
data.Rows = make([]*HistogramInt64Row, len(old))
|
||||
copy(data.Rows, old)
|
||||
v = *data.Rows[index]
|
||||
}
|
||||
oldValues := v.Values
|
||||
v.Values = make([]int64, len(data.Info.Buckets))
|
||||
copy(v.Values, oldValues)
|
||||
f(&v)
|
||||
data.Rows[index] = &v
|
||||
data.EndTime = at
|
||||
frozen := *data
|
||||
return &frozen
|
||||
}
|
||||
|
||||
func (data *HistogramInt64Data) record(at time.Time, lm label.Map, l label.Label) Data {
|
||||
return data.modify(at, lm, func(v *HistogramInt64Row) {
|
||||
value := data.key.From(l)
|
||||
v.Sum += value
|
||||
if v.Min > value || v.Count == 0 {
|
||||
v.Min = value
|
||||
}
|
||||
if v.Max < value || v.Count == 0 {
|
||||
v.Max = value
|
||||
}
|
||||
v.Count++
|
||||
for i, b := range data.Info.Buckets {
|
||||
if value <= b {
|
||||
v.Values[i]++
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (data *HistogramFloat64Data) Handle() string { return data.Info.Name }
|
||||
func (data *HistogramFloat64Data) Groups() [][]label.Label { return data.groups }
|
||||
|
||||
func (data *HistogramFloat64Data) modify(at time.Time, lm label.Map, f func(v *HistogramFloat64Row)) Data {
|
||||
index, insert := getGroup(lm, &data.groups, data.Info.Keys)
|
||||
old := data.Rows
|
||||
var v HistogramFloat64Row
|
||||
if insert {
|
||||
data.Rows = make([]*HistogramFloat64Row, len(old)+1)
|
||||
copy(data.Rows, old[:index])
|
||||
copy(data.Rows[index+1:], old[index:])
|
||||
} else {
|
||||
data.Rows = make([]*HistogramFloat64Row, len(old))
|
||||
copy(data.Rows, old)
|
||||
v = *data.Rows[index]
|
||||
}
|
||||
oldValues := v.Values
|
||||
v.Values = make([]int64, len(data.Info.Buckets))
|
||||
copy(v.Values, oldValues)
|
||||
f(&v)
|
||||
data.Rows[index] = &v
|
||||
data.EndTime = at
|
||||
frozen := *data
|
||||
return &frozen
|
||||
}
|
||||
|
||||
func (data *HistogramFloat64Data) record(at time.Time, lm label.Map, l label.Label) Data {
|
||||
return data.modify(at, lm, func(v *HistogramFloat64Row) {
|
||||
value := data.key.From(l)
|
||||
v.Sum += value
|
||||
if v.Min > value || v.Count == 0 {
|
||||
v.Min = value
|
||||
}
|
||||
if v.Max < value || v.Count == 0 {
|
||||
v.Max = value
|
||||
}
|
||||
v.Count++
|
||||
for i, b := range data.Info.Buckets {
|
||||
if value <= b {
|
||||
v.Values[i]++
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package metric aggregates events into metrics that can be exported.
|
||||
package metric
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/event/core"
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
var Entries = keys.New("metric_entries", "The set of metrics calculated for an event")
|
||||
|
||||
type Config struct {
|
||||
subscribers map[interface{}][]subscriber
|
||||
}
|
||||
|
||||
type subscriber func(time.Time, label.Map, label.Label) Data
|
||||
|
||||
func (e *Config) subscribe(key label.Key, s subscriber) {
|
||||
if e.subscribers == nil {
|
||||
e.subscribers = make(map[interface{}][]subscriber)
|
||||
}
|
||||
e.subscribers[key] = append(e.subscribers[key], s)
|
||||
}
|
||||
|
||||
func (e *Config) Exporter(output event.Exporter) event.Exporter {
|
||||
var mu sync.Mutex
|
||||
return func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
|
||||
if !event.IsMetric(ev) {
|
||||
return output(ctx, ev, lm)
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
var metrics []Data
|
||||
for index := 0; ev.Valid(index); index++ {
|
||||
l := ev.Label(index)
|
||||
if !l.Valid() {
|
||||
continue
|
||||
}
|
||||
id := l.Key()
|
||||
if list := e.subscribers[id]; len(list) > 0 {
|
||||
for _, s := range list {
|
||||
metrics = append(metrics, s(ev.At(), lm, l))
|
||||
}
|
||||
}
|
||||
}
|
||||
lm = label.MergeMaps(label.NewMap(Entries.Of(metrics)), lm)
|
||||
return output(ctx, ev, lm)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package metric
|
||||
|
||||
import (
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
// Scalar represents the construction information for a scalar metric.
|
||||
type Scalar struct {
|
||||
// Name is the unique name of this metric.
|
||||
Name string
|
||||
// Description can be used by observers to describe the metric to users.
|
||||
Description string
|
||||
// Keys is the set of labels that collectively describe rows of the metric.
|
||||
Keys []label.Key
|
||||
}
|
||||
|
||||
// HistogramInt64 represents the construction information for an int64 histogram metric.
|
||||
type HistogramInt64 struct {
|
||||
// Name is the unique name of this metric.
|
||||
Name string
|
||||
// Description can be used by observers to describe the metric to users.
|
||||
Description string
|
||||
// Keys is the set of labels that collectively describe rows of the metric.
|
||||
Keys []label.Key
|
||||
// Buckets holds the inclusive upper bound of each bucket in the histogram.
|
||||
Buckets []int64
|
||||
}
|
||||
|
||||
// HistogramFloat64 represents the construction information for an float64 histogram metric.
|
||||
type HistogramFloat64 struct {
|
||||
// Name is the unique name of this metric.
|
||||
Name string
|
||||
// Description can be used by observers to describe the metric to users.
|
||||
Description string
|
||||
// Keys is the set of labels that collectively describe rows of the metric.
|
||||
Keys []label.Key
|
||||
// Buckets holds the inclusive upper bound of each bucket in the histogram.
|
||||
Buckets []float64
|
||||
}
|
||||
|
||||
// Count creates a new metric based on the Scalar information that counts
|
||||
// the number of times the supplied int64 measure is set.
|
||||
// Metrics of this type will use Int64Data.
|
||||
func (info Scalar) Count(e *Config, key label.Key) {
|
||||
data := &Int64Data{Info: &info, key: nil}
|
||||
e.subscribe(key, data.count)
|
||||
}
|
||||
|
||||
// SumInt64 creates a new metric based on the Scalar information that sums all
|
||||
// the values recorded on the int64 measure.
|
||||
// Metrics of this type will use Int64Data.
|
||||
func (info Scalar) SumInt64(e *Config, key *keys.Int64) {
|
||||
data := &Int64Data{Info: &info, key: key}
|
||||
e.subscribe(key, data.sum)
|
||||
}
|
||||
|
||||
// LatestInt64 creates a new metric based on the Scalar information that tracks
|
||||
// the most recent value recorded on the int64 measure.
|
||||
// Metrics of this type will use Int64Data.
|
||||
func (info Scalar) LatestInt64(e *Config, key *keys.Int64) {
|
||||
data := &Int64Data{Info: &info, IsGauge: true, key: key}
|
||||
e.subscribe(key, data.latest)
|
||||
}
|
||||
|
||||
// SumFloat64 creates a new metric based on the Scalar information that sums all
|
||||
// the values recorded on the float64 measure.
|
||||
// Metrics of this type will use Float64Data.
|
||||
func (info Scalar) SumFloat64(e *Config, key *keys.Float64) {
|
||||
data := &Float64Data{Info: &info, key: key}
|
||||
e.subscribe(key, data.sum)
|
||||
}
|
||||
|
||||
// LatestFloat64 creates a new metric based on the Scalar information that tracks
|
||||
// the most recent value recorded on the float64 measure.
|
||||
// Metrics of this type will use Float64Data.
|
||||
func (info Scalar) LatestFloat64(e *Config, key *keys.Float64) {
|
||||
data := &Float64Data{Info: &info, IsGauge: true, key: key}
|
||||
e.subscribe(key, data.latest)
|
||||
}
|
||||
|
||||
// Record creates a new metric based on the HistogramInt64 information that
|
||||
// tracks the bucketized counts of values recorded on the int64 measure.
|
||||
// Metrics of this type will use HistogramInt64Data.
|
||||
func (info HistogramInt64) Record(e *Config, key *keys.Int64) {
|
||||
data := &HistogramInt64Data{Info: &info, key: key}
|
||||
e.subscribe(key, data.record)
|
||||
}
|
||||
|
||||
// Record creates a new metric based on the HistogramFloat64 information that
|
||||
// tracks the bucketized counts of values recorded on the float64 measure.
|
||||
// Metrics of this type will use HistogramFloat64Data.
|
||||
func (info HistogramFloat64) Record(e *Config, key *keys.Float64) {
|
||||
data := &HistogramFloat64Data{Info: &info, key: key}
|
||||
e.subscribe(key, data.record)
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
# Exporting Metrics and Traces with OpenCensus, Zipkin, and Prometheus
|
||||
|
||||
This tutorial provides a minimum example to verify that metrics and traces
|
||||
can be exported to OpenCensus from Go tools.
|
||||
|
||||
## Setting up oragent
|
||||
|
||||
1. Ensure you have [docker](https://www.docker.com/get-started) and [docker-compose](https://docs.docker.com/compose/install/).
|
||||
2. Clone [oragent](https://github.com/orijtech/oragent).
|
||||
3. In the oragent directory, start the services:
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
If everything goes well, you should see output resembling the following:
|
||||
```
|
||||
Starting oragent_zipkin_1 ... done
|
||||
Starting oragent_oragent_1 ... done
|
||||
Starting oragent_prometheus_1 ... done
|
||||
...
|
||||
```
|
||||
* You can check the status of the OpenCensus agent using zPages at http://localhost:55679/debug/tracez.
|
||||
* You can now access the Prometheus UI at http://localhost:9445.
|
||||
* You can now access the Zipkin UI at http://localhost:9444.
|
||||
4. To shut down oragent, hit Ctrl+C in the terminal.
|
||||
5. You can also start oragent in detached mode by running `docker-compose up -d`. To stop oragent while detached, run `docker-compose down`.
|
||||
|
||||
## Exporting Metrics and Traces
|
||||
1. Clone the [tools](https://golang.org/x/tools) subrepository.
|
||||
1. Inside `internal`, create a file named `main.go` with the following contents:
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/event/export"
|
||||
"golang.org/x/tools/internal/event/export/metric"
|
||||
"golang.org/x/tools/internal/event/export/ocagent"
|
||||
)
|
||||
|
||||
type testExporter struct {
|
||||
metrics metric.Exporter
|
||||
ocagent *ocagent.Exporter
|
||||
}
|
||||
|
||||
func (e *testExporter) ProcessEvent(ctx context.Context, ev event.Event) (context.Context, event.Event) {
|
||||
ctx, ev = export.Tag(ctx, ev)
|
||||
ctx, ev = export.ContextSpan(ctx, ev)
|
||||
ctx, ev = e.metrics.ProcessEvent(ctx, ev)
|
||||
ctx, ev = e.ocagent.ProcessEvent(ctx, ev)
|
||||
return ctx, ev
|
||||
}
|
||||
|
||||
func main() {
|
||||
exporter := &testExporter{}
|
||||
|
||||
exporter.ocagent = ocagent.Connect(&ocagent.Config{
|
||||
Start: time.Now(),
|
||||
Address: "http://127.0.0.1:55678",
|
||||
Service: "go-tools-test",
|
||||
Rate: 5 * time.Second,
|
||||
Client: &http.Client{},
|
||||
})
|
||||
event.SetExporter(exporter)
|
||||
|
||||
ctx := context.TODO()
|
||||
mLatency := event.NewFloat64Key("latency", "the latency in milliseconds")
|
||||
distribution := metric.HistogramFloat64Data{
|
||||
Info: &metric.HistogramFloat64{
|
||||
Name: "latencyDistribution",
|
||||
Description: "the various latencies",
|
||||
Buckets: []float64{0, 10, 50, 100, 200, 400, 800, 1000, 1400, 2000, 5000, 10000, 15000},
|
||||
},
|
||||
}
|
||||
|
||||
distribution.Info.Record(&exporter.metrics, mLatency)
|
||||
|
||||
for {
|
||||
sleep := randomSleep()
|
||||
_, end := event.StartSpan(ctx, "main.randomSleep()")
|
||||
time.Sleep(time.Duration(sleep) * time.Millisecond)
|
||||
end()
|
||||
event.Record(ctx, mLatency.Of(float64(sleep)))
|
||||
|
||||
fmt.Println("Latency: ", float64(sleep))
|
||||
}
|
||||
}
|
||||
|
||||
func randomSleep() int64 {
|
||||
var max int64
|
||||
switch modulus := time.Now().Unix() % 5; modulus {
|
||||
case 0:
|
||||
max = 17001
|
||||
case 1:
|
||||
max = 8007
|
||||
case 2:
|
||||
max = 917
|
||||
case 3:
|
||||
max = 87
|
||||
case 4:
|
||||
max = 1173
|
||||
}
|
||||
return rand.Int63n(max)
|
||||
}
|
||||
|
||||
```
|
||||
3. Run the new file from within the tools repository:
|
||||
```bash
|
||||
go run internal/main.go
|
||||
```
|
||||
4. After about 5 seconds, OpenCensus should start receiving your new metrics, which you can see at http://localhost:8844/metrics. This page will look similar to the following:
|
||||
```
|
||||
# HELP promdemo_latencyDistribution the various latencies
|
||||
# TYPE promdemo_latencyDistribution histogram
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="0"} 0
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="10"} 2
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="50"} 9
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="100"} 22
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="200"} 35
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="400"} 49
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="800"} 63
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="1000"} 78
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="1400"} 93
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="2000"} 108
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="5000"} 123
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="10000"} 138
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="15000"} 153
|
||||
promdemo_latencyDistribution_bucket{vendor="otc",le="+Inf"} 15
|
||||
promdemo_latencyDistribution_sum{vendor="otc"} 1641
|
||||
promdemo_latencyDistribution_count{vendor="otc"} 15
|
||||
```
|
||||
5. After a few more seconds, Prometheus should start displaying your new metrics. You can view the distribution at http://localhost:9445/graph?g0.range_input=5m&g0.stacked=1&g0.expr=rate(oragent_latencyDistribution_bucket%5B5m%5D)&g0.tab=0.
|
||||
|
||||
6. Zipkin should also start displaying traces. You can view them at http://localhost:9444/zipkin/?limit=10&lookback=300000&serviceName=go-tools-test.
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ocagent
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/internal/event/export/metric"
|
||||
"golang.org/x/tools/internal/event/export/ocagent/wire"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
// dataToMetricDescriptor return a *wire.MetricDescriptor based on data.
|
||||
func dataToMetricDescriptor(data metric.Data) *wire.MetricDescriptor {
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
descriptor := &wire.MetricDescriptor{
|
||||
Name: data.Handle(),
|
||||
Description: getDescription(data),
|
||||
// TODO: Unit?
|
||||
Type: dataToMetricDescriptorType(data),
|
||||
LabelKeys: getLabelKeys(data),
|
||||
}
|
||||
|
||||
return descriptor
|
||||
}
|
||||
|
||||
// getDescription returns the description of data.
|
||||
func getDescription(data metric.Data) string {
|
||||
switch d := data.(type) {
|
||||
case *metric.Int64Data:
|
||||
return d.Info.Description
|
||||
|
||||
case *metric.Float64Data:
|
||||
return d.Info.Description
|
||||
|
||||
case *metric.HistogramInt64Data:
|
||||
return d.Info.Description
|
||||
|
||||
case *metric.HistogramFloat64Data:
|
||||
return d.Info.Description
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// getLabelKeys returns a slice of *wire.LabelKeys based on the keys
|
||||
// in data.
|
||||
func getLabelKeys(data metric.Data) []*wire.LabelKey {
|
||||
switch d := data.(type) {
|
||||
case *metric.Int64Data:
|
||||
return infoKeysToLabelKeys(d.Info.Keys)
|
||||
|
||||
case *metric.Float64Data:
|
||||
return infoKeysToLabelKeys(d.Info.Keys)
|
||||
|
||||
case *metric.HistogramInt64Data:
|
||||
return infoKeysToLabelKeys(d.Info.Keys)
|
||||
|
||||
case *metric.HistogramFloat64Data:
|
||||
return infoKeysToLabelKeys(d.Info.Keys)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// dataToMetricDescriptorType returns a wire.MetricDescriptor_Type based on the
|
||||
// underlying type of data.
|
||||
func dataToMetricDescriptorType(data metric.Data) wire.MetricDescriptor_Type {
|
||||
switch d := data.(type) {
|
||||
case *metric.Int64Data:
|
||||
if d.IsGauge {
|
||||
return wire.MetricDescriptor_GAUGE_INT64
|
||||
}
|
||||
return wire.MetricDescriptor_CUMULATIVE_INT64
|
||||
|
||||
case *metric.Float64Data:
|
||||
if d.IsGauge {
|
||||
return wire.MetricDescriptor_GAUGE_DOUBLE
|
||||
}
|
||||
return wire.MetricDescriptor_CUMULATIVE_DOUBLE
|
||||
|
||||
case *metric.HistogramInt64Data:
|
||||
return wire.MetricDescriptor_CUMULATIVE_DISTRIBUTION
|
||||
|
||||
case *metric.HistogramFloat64Data:
|
||||
return wire.MetricDescriptor_CUMULATIVE_DISTRIBUTION
|
||||
}
|
||||
|
||||
return wire.MetricDescriptor_UNSPECIFIED
|
||||
}
|
||||
|
||||
// dataToTimeseries returns a slice of *wire.TimeSeries based on the
|
||||
// points in data.
|
||||
func dataToTimeseries(data metric.Data, start time.Time) []*wire.TimeSeries {
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
numRows := numRows(data)
|
||||
startTimestamp := convertTimestamp(start)
|
||||
timeseries := make([]*wire.TimeSeries, 0, numRows)
|
||||
|
||||
for i := 0; i < numRows; i++ {
|
||||
timeseries = append(timeseries, &wire.TimeSeries{
|
||||
StartTimestamp: &startTimestamp,
|
||||
// TODO: labels?
|
||||
Points: dataToPoints(data, i),
|
||||
})
|
||||
}
|
||||
|
||||
return timeseries
|
||||
}
|
||||
|
||||
// numRows returns the number of rows in data.
|
||||
func numRows(data metric.Data) int {
|
||||
switch d := data.(type) {
|
||||
case *metric.Int64Data:
|
||||
return len(d.Rows)
|
||||
case *metric.Float64Data:
|
||||
return len(d.Rows)
|
||||
case *metric.HistogramInt64Data:
|
||||
return len(d.Rows)
|
||||
case *metric.HistogramFloat64Data:
|
||||
return len(d.Rows)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// dataToPoints returns an array of *wire.Points based on the point(s)
|
||||
// in data at index i.
|
||||
func dataToPoints(data metric.Data, i int) []*wire.Point {
|
||||
switch d := data.(type) {
|
||||
case *metric.Int64Data:
|
||||
timestamp := convertTimestamp(d.EndTime)
|
||||
return []*wire.Point{
|
||||
{
|
||||
Value: wire.PointInt64Value{
|
||||
Int64Value: d.Rows[i],
|
||||
},
|
||||
Timestamp: ×tamp,
|
||||
},
|
||||
}
|
||||
case *metric.Float64Data:
|
||||
timestamp := convertTimestamp(d.EndTime)
|
||||
return []*wire.Point{
|
||||
{
|
||||
Value: wire.PointDoubleValue{
|
||||
DoubleValue: d.Rows[i],
|
||||
},
|
||||
Timestamp: ×tamp,
|
||||
},
|
||||
}
|
||||
case *metric.HistogramInt64Data:
|
||||
row := d.Rows[i]
|
||||
bucketBounds := make([]float64, len(d.Info.Buckets))
|
||||
for i, val := range d.Info.Buckets {
|
||||
bucketBounds[i] = float64(val)
|
||||
}
|
||||
return distributionToPoints(row.Values, row.Count, float64(row.Sum), bucketBounds, d.EndTime)
|
||||
case *metric.HistogramFloat64Data:
|
||||
row := d.Rows[i]
|
||||
return distributionToPoints(row.Values, row.Count, row.Sum, d.Info.Buckets, d.EndTime)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// distributionToPoints returns an array of *wire.Points containing a
|
||||
// wire.PointDistributionValue representing a distribution with the
|
||||
// supplied counts, count, and sum.
|
||||
func distributionToPoints(counts []int64, count int64, sum float64, bucketBounds []float64, end time.Time) []*wire.Point {
|
||||
buckets := make([]*wire.Bucket, len(counts))
|
||||
for i := 0; i < len(counts); i++ {
|
||||
buckets[i] = &wire.Bucket{
|
||||
Count: counts[i],
|
||||
}
|
||||
}
|
||||
timestamp := convertTimestamp(end)
|
||||
return []*wire.Point{
|
||||
{
|
||||
Value: wire.PointDistributionValue{
|
||||
DistributionValue: &wire.DistributionValue{
|
||||
Count: count,
|
||||
Sum: sum,
|
||||
// TODO: SumOfSquaredDeviation?
|
||||
Buckets: buckets,
|
||||
BucketOptions: &wire.BucketOptionsExplicit{
|
||||
Bounds: bucketBounds,
|
||||
},
|
||||
},
|
||||
},
|
||||
Timestamp: ×tamp,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// infoKeysToLabelKeys returns an array of *wire.LabelKeys containing the
|
||||
// string values of the elements of labelKeys.
|
||||
func infoKeysToLabelKeys(infoKeys []label.Key) []*wire.LabelKey {
|
||||
labelKeys := make([]*wire.LabelKey, 0, len(infoKeys))
|
||||
for _, key := range infoKeys {
|
||||
labelKeys = append(labelKeys, &wire.LabelKey{
|
||||
Key: key.Name(),
|
||||
})
|
||||
}
|
||||
|
||||
return labelKeys
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ocagent_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
)
|
||||
|
||||
func TestEncodeMetric(t *testing.T) {
|
||||
exporter := registerExporter()
|
||||
const prefix = testNodeStr + `
|
||||
"metrics":[`
|
||||
const suffix = `]}`
|
||||
tests := []struct {
|
||||
name string
|
||||
run func(ctx context.Context)
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "HistogramFloat64, HistogramInt64",
|
||||
run: func(ctx context.Context) {
|
||||
ctx = event.Label(ctx, keyMethod.Of("godoc.ServeHTTP"))
|
||||
event.Metric(ctx, latencyMs.Of(96.58))
|
||||
ctx = event.Label(ctx, keys.Err.Of(errors.New("panic: fatal signal")))
|
||||
event.Metric(ctx, bytesIn.Of(97e2))
|
||||
},
|
||||
want: prefix + `
|
||||
{
|
||||
"metric_descriptor": {
|
||||
"name": "latency_ms",
|
||||
"description": "The latency of calls in milliseconds",
|
||||
"type": 6,
|
||||
"label_keys": [
|
||||
{
|
||||
"key": "method"
|
||||
},
|
||||
{
|
||||
"key": "route"
|
||||
}
|
||||
]
|
||||
},
|
||||
"timeseries": [
|
||||
{
|
||||
"start_timestamp": "1970-01-01T00:00:00Z",
|
||||
"points": [
|
||||
{
|
||||
"timestamp": "1970-01-01T00:00:40Z",
|
||||
"distributionValue": {
|
||||
"count": 1,
|
||||
"sum": 96.58,
|
||||
"bucket_options": {
|
||||
"explicit": {
|
||||
"bounds": [
|
||||
0,
|
||||
5,
|
||||
10,
|
||||
25,
|
||||
50
|
||||
]
|
||||
}
|
||||
},
|
||||
"buckets": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"metric_descriptor": {
|
||||
"name": "latency_ms",
|
||||
"description": "The latency of calls in milliseconds",
|
||||
"type": 6,
|
||||
"label_keys": [
|
||||
{
|
||||
"key": "method"
|
||||
},
|
||||
{
|
||||
"key": "route"
|
||||
}
|
||||
]
|
||||
},
|
||||
"timeseries": [
|
||||
{
|
||||
"start_timestamp": "1970-01-01T00:00:00Z",
|
||||
"points": [
|
||||
{
|
||||
"timestamp": "1970-01-01T00:00:40Z",
|
||||
"distributionValue": {
|
||||
"count": 1,
|
||||
"sum": 9700,
|
||||
"bucket_options": {
|
||||
"explicit": {
|
||||
"bounds": [
|
||||
0,
|
||||
10,
|
||||
50,
|
||||
100,
|
||||
500,
|
||||
1000,
|
||||
2000
|
||||
]
|
||||
}
|
||||
},
|
||||
"buckets": [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}` + suffix,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.TODO()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.run(ctx)
|
||||
got := exporter.Output("/v1/metrics")
|
||||
checkJSON(t, got, []byte(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package ocagent adds the ability to export all telemetry to an ocagent.
|
||||
// This keeps the compile time dependencies to zero and allows the agent to
|
||||
// have the exporters needed for telemetry aggregation and viewing systems.
|
||||
package ocagent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/event/core"
|
||||
"golang.org/x/tools/internal/event/export"
|
||||
"golang.org/x/tools/internal/event/export/metric"
|
||||
"golang.org/x/tools/internal/event/export/ocagent/wire"
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Start time.Time
|
||||
Host string
|
||||
Process uint32
|
||||
Client *http.Client
|
||||
Service string
|
||||
Address string
|
||||
Rate time.Duration
|
||||
}
|
||||
|
||||
var (
|
||||
connectMu sync.Mutex
|
||||
exporters = make(map[Config]*Exporter)
|
||||
)
|
||||
|
||||
// Discover finds the local agent to export to, it will return nil if there
|
||||
// is not one running.
|
||||
// TODO: Actually implement a discovery protocol rather than a hard coded address
|
||||
func Discover() *Config {
|
||||
return &Config{
|
||||
Address: "http://localhost:55678",
|
||||
}
|
||||
}
|
||||
|
||||
type Exporter struct {
|
||||
mu sync.Mutex
|
||||
config Config
|
||||
spans []*export.Span
|
||||
metrics []metric.Data
|
||||
}
|
||||
|
||||
// Connect creates a process specific exporter with the specified
|
||||
// serviceName and the address of the ocagent to which it will upload
|
||||
// its telemetry.
|
||||
func Connect(config *Config) *Exporter {
|
||||
if config == nil || config.Address == "off" {
|
||||
return nil
|
||||
}
|
||||
resolved := *config
|
||||
if resolved.Host == "" {
|
||||
hostname, _ := os.Hostname()
|
||||
resolved.Host = hostname
|
||||
}
|
||||
if resolved.Process == 0 {
|
||||
resolved.Process = uint32(os.Getpid())
|
||||
}
|
||||
if resolved.Client == nil {
|
||||
resolved.Client = http.DefaultClient
|
||||
}
|
||||
if resolved.Service == "" {
|
||||
resolved.Service = filepath.Base(os.Args[0])
|
||||
}
|
||||
if resolved.Rate == 0 {
|
||||
resolved.Rate = 2 * time.Second
|
||||
}
|
||||
|
||||
connectMu.Lock()
|
||||
defer connectMu.Unlock()
|
||||
if exporter, found := exporters[resolved]; found {
|
||||
return exporter
|
||||
}
|
||||
exporter := &Exporter{config: resolved}
|
||||
exporters[resolved] = exporter
|
||||
if exporter.config.Start.IsZero() {
|
||||
exporter.config.Start = time.Now()
|
||||
}
|
||||
go func() {
|
||||
for range time.Tick(exporter.config.Rate) {
|
||||
exporter.Flush()
|
||||
}
|
||||
}()
|
||||
return exporter
|
||||
}
|
||||
|
||||
func (e *Exporter) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context {
|
||||
switch {
|
||||
case event.IsEnd(ev):
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
span := export.GetSpan(ctx)
|
||||
if span != nil {
|
||||
e.spans = append(e.spans, span)
|
||||
}
|
||||
case event.IsMetric(ev):
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
data := metric.Entries.Get(lm).([]metric.Data)
|
||||
e.metrics = append(e.metrics, data...)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (e *Exporter) Flush() {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
spans := make([]*wire.Span, len(e.spans))
|
||||
for i, s := range e.spans {
|
||||
spans[i] = convertSpan(s)
|
||||
}
|
||||
e.spans = nil
|
||||
metrics := make([]*wire.Metric, len(e.metrics))
|
||||
for i, m := range e.metrics {
|
||||
metrics[i] = convertMetric(m, e.config.Start)
|
||||
}
|
||||
e.metrics = nil
|
||||
|
||||
if len(spans) > 0 {
|
||||
e.send("/v1/trace", &wire.ExportTraceServiceRequest{
|
||||
Node: e.config.buildNode(),
|
||||
Spans: spans,
|
||||
//TODO: Resource?
|
||||
})
|
||||
}
|
||||
if len(metrics) > 0 {
|
||||
e.send("/v1/metrics", &wire.ExportMetricsServiceRequest{
|
||||
Node: e.config.buildNode(),
|
||||
Metrics: metrics,
|
||||
//TODO: Resource?
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *Config) buildNode() *wire.Node {
|
||||
return &wire.Node{
|
||||
Identifier: &wire.ProcessIdentifier{
|
||||
HostName: cfg.Host,
|
||||
Pid: cfg.Process,
|
||||
StartTimestamp: convertTimestamp(cfg.Start),
|
||||
},
|
||||
LibraryInfo: &wire.LibraryInfo{
|
||||
Language: wire.LanguageGo,
|
||||
ExporterVersion: "0.0.1",
|
||||
CoreLibraryVersion: "x/tools",
|
||||
},
|
||||
ServiceInfo: &wire.ServiceInfo{
|
||||
Name: cfg.Service,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Exporter) send(endpoint string, message interface{}) {
|
||||
blob, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
errorInExport("ocagent failed to marshal message for %v: %v", endpoint, err)
|
||||
return
|
||||
}
|
||||
uri := e.config.Address + endpoint
|
||||
req, err := http.NewRequest("POST", uri, bytes.NewReader(blob))
|
||||
if err != nil {
|
||||
errorInExport("ocagent failed to build request for %v: %v", uri, err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res, err := e.config.Client.Do(req)
|
||||
if err != nil {
|
||||
errorInExport("ocagent failed to send message: %v \n", err)
|
||||
return
|
||||
}
|
||||
if res.Body != nil {
|
||||
res.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func errorInExport(message string, args ...interface{}) {
|
||||
// This function is useful when debugging the exporter, but in general we
|
||||
// want to just drop any export
|
||||
}
|
||||
|
||||
func convertTimestamp(t time.Time) wire.Timestamp {
|
||||
return t.Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
func toTruncatableString(s string) *wire.TruncatableString {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &wire.TruncatableString{Value: s}
|
||||
}
|
||||
|
||||
func convertSpan(span *export.Span) *wire.Span {
|
||||
result := &wire.Span{
|
||||
TraceID: span.ID.TraceID[:],
|
||||
SpanID: span.ID.SpanID[:],
|
||||
TraceState: nil, //TODO?
|
||||
ParentSpanID: span.ParentID[:],
|
||||
Name: toTruncatableString(span.Name),
|
||||
Kind: wire.UnspecifiedSpanKind,
|
||||
StartTime: convertTimestamp(span.Start().At()),
|
||||
EndTime: convertTimestamp(span.Finish().At()),
|
||||
Attributes: convertAttributes(span.Start(), 1),
|
||||
TimeEvents: convertEvents(span.Events()),
|
||||
SameProcessAsParentSpan: true,
|
||||
//TODO: StackTrace?
|
||||
//TODO: Links?
|
||||
//TODO: Status?
|
||||
//TODO: Resource?
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func convertMetric(data metric.Data, start time.Time) *wire.Metric {
|
||||
descriptor := dataToMetricDescriptor(data)
|
||||
timeseries := dataToTimeseries(data, start)
|
||||
|
||||
if descriptor == nil && timeseries == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: handle Histogram metrics
|
||||
return &wire.Metric{
|
||||
MetricDescriptor: descriptor,
|
||||
Timeseries: timeseries,
|
||||
// TODO: attach Resource?
|
||||
}
|
||||
}
|
||||
|
||||
func skipToValidLabel(list label.List, index int) (int, label.Label) {
|
||||
// skip to the first valid label
|
||||
for ; list.Valid(index); index++ {
|
||||
l := list.Label(index)
|
||||
if !l.Valid() || l.Key() == keys.Label {
|
||||
continue
|
||||
}
|
||||
return index, l
|
||||
}
|
||||
return -1, label.Label{}
|
||||
}
|
||||
|
||||
func convertAttributes(list label.List, index int) *wire.Attributes {
|
||||
index, l := skipToValidLabel(list, index)
|
||||
if !l.Valid() {
|
||||
return nil
|
||||
}
|
||||
attributes := make(map[string]wire.Attribute)
|
||||
for {
|
||||
if l.Valid() {
|
||||
attributes[l.Key().Name()] = convertAttribute(l)
|
||||
}
|
||||
index++
|
||||
if !list.Valid(index) {
|
||||
return &wire.Attributes{AttributeMap: attributes}
|
||||
}
|
||||
l = list.Label(index)
|
||||
}
|
||||
}
|
||||
|
||||
func convertAttribute(l label.Label) wire.Attribute {
|
||||
switch key := l.Key().(type) {
|
||||
case *keys.Int:
|
||||
return wire.IntAttribute{IntValue: int64(key.From(l))}
|
||||
case *keys.Int8:
|
||||
return wire.IntAttribute{IntValue: int64(key.From(l))}
|
||||
case *keys.Int16:
|
||||
return wire.IntAttribute{IntValue: int64(key.From(l))}
|
||||
case *keys.Int32:
|
||||
return wire.IntAttribute{IntValue: int64(key.From(l))}
|
||||
case *keys.Int64:
|
||||
return wire.IntAttribute{IntValue: int64(key.From(l))}
|
||||
case *keys.UInt:
|
||||
return wire.IntAttribute{IntValue: int64(key.From(l))}
|
||||
case *keys.UInt8:
|
||||
return wire.IntAttribute{IntValue: int64(key.From(l))}
|
||||
case *keys.UInt16:
|
||||
return wire.IntAttribute{IntValue: int64(key.From(l))}
|
||||
case *keys.UInt32:
|
||||
return wire.IntAttribute{IntValue: int64(key.From(l))}
|
||||
case *keys.UInt64:
|
||||
return wire.IntAttribute{IntValue: int64(key.From(l))}
|
||||
case *keys.Float32:
|
||||
return wire.DoubleAttribute{DoubleValue: float64(key.From(l))}
|
||||
case *keys.Float64:
|
||||
return wire.DoubleAttribute{DoubleValue: key.From(l)}
|
||||
case *keys.Boolean:
|
||||
return wire.BoolAttribute{BoolValue: key.From(l)}
|
||||
case *keys.String:
|
||||
return wire.StringAttribute{StringValue: toTruncatableString(key.From(l))}
|
||||
case *keys.Error:
|
||||
return wire.StringAttribute{StringValue: toTruncatableString(key.From(l).Error())}
|
||||
case *keys.Value:
|
||||
return wire.StringAttribute{StringValue: toTruncatableString(fmt.Sprint(key.From(l)))}
|
||||
default:
|
||||
return wire.StringAttribute{StringValue: toTruncatableString(fmt.Sprintf("%T", key))}
|
||||
}
|
||||
}
|
||||
|
||||
func convertEvents(events []core.Event) *wire.TimeEvents {
|
||||
//TODO: MessageEvents?
|
||||
result := make([]wire.TimeEvent, len(events))
|
||||
for i, event := range events {
|
||||
result[i] = convertEvent(event)
|
||||
}
|
||||
return &wire.TimeEvents{TimeEvent: result}
|
||||
}
|
||||
|
||||
func convertEvent(ev core.Event) wire.TimeEvent {
|
||||
return wire.TimeEvent{
|
||||
Time: convertTimestamp(ev.At()),
|
||||
Annotation: convertAnnotation(ev),
|
||||
}
|
||||
}
|
||||
|
||||
func getAnnotationDescription(ev core.Event) (string, int) {
|
||||
l := ev.Label(0)
|
||||
if l.Key() != keys.Msg {
|
||||
return "", 0
|
||||
}
|
||||
if msg := keys.Msg.From(l); msg != "" {
|
||||
return msg, 1
|
||||
}
|
||||
l = ev.Label(1)
|
||||
if l.Key() != keys.Err {
|
||||
return "", 1
|
||||
}
|
||||
if err := keys.Err.From(l); err != nil {
|
||||
return err.Error(), 2
|
||||
}
|
||||
return "", 2
|
||||
}
|
||||
|
||||
func convertAnnotation(ev core.Event) *wire.Annotation {
|
||||
description, index := getAnnotationDescription(ev)
|
||||
if _, l := skipToValidLabel(ev, index); !l.Valid() && description == "" {
|
||||
return nil
|
||||
}
|
||||
return &wire.Annotation{
|
||||
Description: toTruncatableString(description),
|
||||
Attributes: convertAttributes(ev, index),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ocagent_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/event/core"
|
||||
"golang.org/x/tools/internal/event/export"
|
||||
"golang.org/x/tools/internal/event/export/metric"
|
||||
"golang.org/x/tools/internal/event/export/ocagent"
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
const testNodeStr = `{
|
||||
"node":{
|
||||
"identifier":{
|
||||
"host_name":"tester",
|
||||
"pid":1,
|
||||
"start_timestamp":"1970-01-01T00:00:00Z"
|
||||
},
|
||||
"library_info":{
|
||||
"language":4,
|
||||
"exporter_version":"0.0.1",
|
||||
"core_library_version":"x/tools"
|
||||
},
|
||||
"service_info":{
|
||||
"name":"ocagent-tests"
|
||||
}
|
||||
},`
|
||||
|
||||
var (
|
||||
keyDB = keys.NewString("db", "the database name")
|
||||
keyMethod = keys.NewString("method", "a metric grouping key")
|
||||
keyRoute = keys.NewString("route", "another metric grouping key")
|
||||
|
||||
key1DB = keys.NewString("1_db", "A test string key")
|
||||
|
||||
key2aAge = keys.NewFloat64("2a_age", "A test float64 key")
|
||||
key2bTTL = keys.NewFloat32("2b_ttl", "A test float32 key")
|
||||
key2cExpiryMS = keys.NewFloat64("2c_expiry_ms", "A test float64 key")
|
||||
|
||||
key3aRetry = keys.NewBoolean("3a_retry", "A test boolean key")
|
||||
key3bStale = keys.NewBoolean("3b_stale", "Another test boolean key")
|
||||
|
||||
key4aMax = keys.NewInt("4a_max", "A test int key")
|
||||
key4bOpcode = keys.NewInt8("4b_opcode", "A test int8 key")
|
||||
key4cBase = keys.NewInt16("4c_base", "A test int16 key")
|
||||
key4eChecksum = keys.NewInt32("4e_checksum", "A test int32 key")
|
||||
key4fMode = keys.NewInt64("4f_mode", "A test int64 key")
|
||||
|
||||
key5aMin = keys.NewUInt("5a_min", "A test uint key")
|
||||
key5bMix = keys.NewUInt8("5b_mix", "A test uint8 key")
|
||||
key5cPort = keys.NewUInt16("5c_port", "A test uint16 key")
|
||||
key5dMinHops = keys.NewUInt32("5d_min_hops", "A test uint32 key")
|
||||
key5eMaxHops = keys.NewUInt64("5e_max_hops", "A test uint64 key")
|
||||
|
||||
recursiveCalls = keys.NewInt64("recursive_calls", "Number of recursive calls")
|
||||
bytesIn = keys.NewInt64("bytes_in", "Number of bytes in") //, unit.Bytes)
|
||||
latencyMs = keys.NewFloat64("latency", "The latency in milliseconds") //, unit.Milliseconds)
|
||||
|
||||
metricLatency = metric.HistogramFloat64{
|
||||
Name: "latency_ms",
|
||||
Description: "The latency of calls in milliseconds",
|
||||
Keys: []label.Key{keyMethod, keyRoute},
|
||||
Buckets: []float64{0, 5, 10, 25, 50},
|
||||
}
|
||||
|
||||
metricBytesIn = metric.HistogramInt64{
|
||||
Name: "latency_ms",
|
||||
Description: "The latency of calls in milliseconds",
|
||||
Keys: []label.Key{keyMethod, keyRoute},
|
||||
Buckets: []int64{0, 10, 50, 100, 500, 1000, 2000},
|
||||
}
|
||||
|
||||
metricRecursiveCalls = metric.Scalar{
|
||||
Name: "latency_ms",
|
||||
Description: "The latency of calls in milliseconds",
|
||||
Keys: []label.Key{keyMethod, keyRoute},
|
||||
}
|
||||
)
|
||||
|
||||
type testExporter struct {
|
||||
ocagent *ocagent.Exporter
|
||||
sent fakeSender
|
||||
}
|
||||
|
||||
func registerExporter() *testExporter {
|
||||
exporter := &testExporter{}
|
||||
cfg := ocagent.Config{
|
||||
Host: "tester",
|
||||
Process: 1,
|
||||
Service: "ocagent-tests",
|
||||
Client: &http.Client{Transport: &exporter.sent},
|
||||
}
|
||||
cfg.Start, _ = time.Parse(time.RFC3339Nano, "1970-01-01T00:00:00Z")
|
||||
exporter.ocagent = ocagent.Connect(&cfg)
|
||||
|
||||
metrics := metric.Config{}
|
||||
metricLatency.Record(&metrics, latencyMs)
|
||||
metricBytesIn.Record(&metrics, bytesIn)
|
||||
metricRecursiveCalls.SumInt64(&metrics, recursiveCalls)
|
||||
|
||||
e := exporter.ocagent.ProcessEvent
|
||||
e = metrics.Exporter(e)
|
||||
e = spanFixer(e)
|
||||
e = export.Spans(e)
|
||||
e = export.Labels(e)
|
||||
e = timeFixer(e)
|
||||
event.SetExporter(e)
|
||||
return exporter
|
||||
}
|
||||
|
||||
func timeFixer(output event.Exporter) event.Exporter {
|
||||
start, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:30Z")
|
||||
at, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:40Z")
|
||||
end, _ := time.Parse(time.RFC3339Nano, "1970-01-01T00:00:50Z")
|
||||
return func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
|
||||
switch {
|
||||
case event.IsStart(ev):
|
||||
ev = core.CloneEvent(ev, start)
|
||||
case event.IsEnd(ev):
|
||||
ev = core.CloneEvent(ev, end)
|
||||
default:
|
||||
ev = core.CloneEvent(ev, at)
|
||||
}
|
||||
return output(ctx, ev, lm)
|
||||
}
|
||||
}
|
||||
|
||||
func spanFixer(output event.Exporter) event.Exporter {
|
||||
return func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
|
||||
if event.IsStart(ev) {
|
||||
span := export.GetSpan(ctx)
|
||||
span.ID = export.SpanContext{}
|
||||
}
|
||||
return output(ctx, ev, lm)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *testExporter) Output(route string) []byte {
|
||||
e.ocagent.Flush()
|
||||
return e.sent.get(route)
|
||||
}
|
||||
|
||||
func checkJSON(t *testing.T, got, want []byte) {
|
||||
// compare the compact form, to allow for formatting differences
|
||||
g := &bytes.Buffer{}
|
||||
if err := json.Compact(g, got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := &bytes.Buffer{}
|
||||
if err := json.Compact(w, want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if g.String() != w.String() {
|
||||
t.Fatalf("Got:\n%s\nWant:\n%s", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeSender struct {
|
||||
mu sync.Mutex
|
||||
data map[string][]byte
|
||||
}
|
||||
|
||||
func (s *fakeSender) get(route string) []byte {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
data, found := s.data[route]
|
||||
if found {
|
||||
delete(s.data, route)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *fakeSender) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.data == nil {
|
||||
s.data = make(map[string][]byte)
|
||||
}
|
||||
data, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := req.URL.EscapedPath()
|
||||
if _, found := s.data[path]; found {
|
||||
return nil, fmt.Errorf("duplicate delivery to %v", path)
|
||||
}
|
||||
s.data[path] = data
|
||||
return &http.Response{
|
||||
Status: "200 OK",
|
||||
StatusCode: 200,
|
||||
Proto: "HTTP/1.0",
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 0,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ocagent_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
)
|
||||
|
||||
func TestTrace(t *testing.T) {
|
||||
exporter := registerExporter()
|
||||
const prefix = testNodeStr + `
|
||||
"spans":[{
|
||||
"trace_id":"AAAAAAAAAAAAAAAAAAAAAA==",
|
||||
"span_id":"AAAAAAAAAAA=",
|
||||
"parent_span_id":"AAAAAAAAAAA=",
|
||||
"name":{"value":"event span"},
|
||||
"start_time":"1970-01-01T00:00:30Z",
|
||||
"end_time":"1970-01-01T00:00:50Z",
|
||||
"time_events":{
|
||||
`
|
||||
const suffix = `
|
||||
},
|
||||
"same_process_as_parent_span":true
|
||||
}]
|
||||
}`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
run func(ctx context.Context)
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no labels",
|
||||
run: func(ctx context.Context) {
|
||||
event.Label(ctx)
|
||||
},
|
||||
want: prefix + `
|
||||
"timeEvent":[{"time":"1970-01-01T00:00:40Z"}]
|
||||
` + suffix,
|
||||
},
|
||||
{
|
||||
name: "description no error",
|
||||
run: func(ctx context.Context) {
|
||||
event.Log(ctx, "cache miss", keyDB.Of("godb"))
|
||||
},
|
||||
want: prefix + `"timeEvent":[{"time":"1970-01-01T00:00:40Z","annotation":{
|
||||
"description": { "value": "cache miss" },
|
||||
"attributes": {
|
||||
"attributeMap": {
|
||||
"db": { "stringValue": { "value": "godb" } }
|
||||
}
|
||||
}
|
||||
}}]` + suffix,
|
||||
},
|
||||
|
||||
{
|
||||
name: "description and error",
|
||||
run: func(ctx context.Context) {
|
||||
event.Error(ctx, "cache miss",
|
||||
errors.New("no network connectivity"),
|
||||
keyDB.Of("godb"),
|
||||
)
|
||||
},
|
||||
want: prefix + `"timeEvent":[{"time":"1970-01-01T00:00:40Z","annotation":{
|
||||
"description": { "value": "cache miss" },
|
||||
"attributes": {
|
||||
"attributeMap": {
|
||||
"db": { "stringValue": { "value": "godb" } },
|
||||
"error": { "stringValue": { "value": "no network connectivity" } }
|
||||
}
|
||||
}
|
||||
}}]` + suffix,
|
||||
},
|
||||
{
|
||||
name: "no description, but error",
|
||||
run: func(ctx context.Context) {
|
||||
event.Error(ctx, "",
|
||||
errors.New("no network connectivity"),
|
||||
keyDB.Of("godb"),
|
||||
)
|
||||
},
|
||||
want: prefix + `"timeEvent":[{"time":"1970-01-01T00:00:40Z","annotation":{
|
||||
"description": { "value": "no network connectivity" },
|
||||
"attributes": {
|
||||
"attributeMap": {
|
||||
"db": { "stringValue": { "value": "godb" } }
|
||||
}
|
||||
}
|
||||
}}]` + suffix,
|
||||
},
|
||||
{
|
||||
name: "enumerate all attribute types",
|
||||
run: func(ctx context.Context) {
|
||||
event.Log(ctx, "cache miss",
|
||||
key1DB.Of("godb"),
|
||||
|
||||
key2aAge.Of(0.456), // Constant converted into "float64"
|
||||
key2bTTL.Of(float32(5000)),
|
||||
key2cExpiryMS.Of(float64(1e3)),
|
||||
|
||||
key3aRetry.Of(false),
|
||||
key3bStale.Of(true),
|
||||
|
||||
key4aMax.Of(0x7fff), // Constant converted into "int"
|
||||
key4bOpcode.Of(int8(0x7e)),
|
||||
key4cBase.Of(int16(1<<9)),
|
||||
key4eChecksum.Of(int32(0x11f7e294)),
|
||||
key4fMode.Of(int64(0644)),
|
||||
|
||||
key5aMin.Of(uint(1)),
|
||||
key5bMix.Of(uint8(44)),
|
||||
key5cPort.Of(uint16(55678)),
|
||||
key5dMinHops.Of(uint32(1<<9)),
|
||||
key5eMaxHops.Of(uint64(0xffffff)),
|
||||
)
|
||||
},
|
||||
want: prefix + `"timeEvent":[{"time":"1970-01-01T00:00:40Z","annotation":{
|
||||
"description": { "value": "cache miss" },
|
||||
"attributes": {
|
||||
"attributeMap": {
|
||||
"1_db": { "stringValue": { "value": "godb" } },
|
||||
"2a_age": { "doubleValue": 0.456 },
|
||||
"2b_ttl": { "doubleValue": 5000 },
|
||||
"2c_expiry_ms": { "doubleValue": 1000 },
|
||||
"3a_retry": {},
|
||||
"3b_stale": { "boolValue": true },
|
||||
"4a_max": { "intValue": 32767 },
|
||||
"4b_opcode": { "intValue": 126 },
|
||||
"4c_base": { "intValue": 512 },
|
||||
"4e_checksum": { "intValue": 301458068 },
|
||||
"4f_mode": { "intValue": 420 },
|
||||
"5a_min": { "intValue": 1 },
|
||||
"5b_mix": { "intValue": 44 },
|
||||
"5c_port": { "intValue": 55678 },
|
||||
"5d_min_hops": { "intValue": 512 },
|
||||
"5e_max_hops": { "intValue": 16777215 }
|
||||
}
|
||||
}
|
||||
}}]` + suffix,
|
||||
},
|
||||
}
|
||||
ctx := context.TODO()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx, done := event.Start(ctx, "event span")
|
||||
tt.run(ctx)
|
||||
done()
|
||||
got := exporter.Output("/v1/trace")
|
||||
checkJSON(t, got, []byte(tt.want))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wire
|
||||
|
||||
// This file holds common ocagent types
|
||||
|
||||
type Node struct {
|
||||
Identifier *ProcessIdentifier `json:"identifier,omitempty"`
|
||||
LibraryInfo *LibraryInfo `json:"library_info,omitempty"`
|
||||
ServiceInfo *ServiceInfo `json:"service_info,omitempty"`
|
||||
Attributes map[string]string `json:"attributes,omitempty"`
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
Type string `json:"type,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
type TruncatableString struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
TruncatedByteCount int32 `json:"truncated_byte_count,omitempty"`
|
||||
}
|
||||
|
||||
type Attributes struct {
|
||||
AttributeMap map[string]Attribute `json:"attributeMap,omitempty"`
|
||||
DroppedAttributesCount int32 `json:"dropped_attributes_count,omitempty"`
|
||||
}
|
||||
|
||||
type StringAttribute struct {
|
||||
StringValue *TruncatableString `json:"stringValue,omitempty"`
|
||||
}
|
||||
|
||||
type IntAttribute struct {
|
||||
IntValue int64 `json:"intValue,omitempty"`
|
||||
}
|
||||
|
||||
type BoolAttribute struct {
|
||||
BoolValue bool `json:"boolValue,omitempty"`
|
||||
}
|
||||
|
||||
type DoubleAttribute struct {
|
||||
DoubleValue float64 `json:"doubleValue,omitempty"`
|
||||
}
|
||||
|
||||
type Attribute interface {
|
||||
labelAttribute()
|
||||
}
|
||||
|
||||
func (StringAttribute) labelAttribute() {}
|
||||
func (IntAttribute) labelAttribute() {}
|
||||
func (BoolAttribute) labelAttribute() {}
|
||||
func (DoubleAttribute) labelAttribute() {}
|
||||
|
||||
type StackTrace struct {
|
||||
StackFrames *StackFrames `json:"stack_frames,omitempty"`
|
||||
StackTraceHashID uint64 `json:"stack_trace_hash_id,omitempty"`
|
||||
}
|
||||
|
||||
type StackFrames struct {
|
||||
Frame []*StackFrame `json:"frame,omitempty"`
|
||||
DroppedFramesCount int32 `json:"dropped_frames_count,omitempty"`
|
||||
}
|
||||
|
||||
type StackFrame struct {
|
||||
FunctionName *TruncatableString `json:"function_name,omitempty"`
|
||||
OriginalFunctionName *TruncatableString `json:"original_function_name,omitempty"`
|
||||
FileName *TruncatableString `json:"file_name,omitempty"`
|
||||
LineNumber int64 `json:"line_number,omitempty"`
|
||||
ColumnNumber int64 `json:"column_number,omitempty"`
|
||||
LoadModule *Module `json:"load_module,omitempty"`
|
||||
SourceVersion *TruncatableString `json:"source_version,omitempty"`
|
||||
}
|
||||
|
||||
type Module struct {
|
||||
Module *TruncatableString `json:"module,omitempty"`
|
||||
BuildID *TruncatableString `json:"build_id,omitempty"`
|
||||
}
|
||||
|
||||
type ProcessIdentifier struct {
|
||||
HostName string `json:"host_name,omitempty"`
|
||||
Pid uint32 `json:"pid,omitempty"`
|
||||
StartTimestamp Timestamp `json:"start_timestamp,omitempty"`
|
||||
}
|
||||
|
||||
type LibraryInfo struct {
|
||||
Language Language `json:"language,omitempty"`
|
||||
ExporterVersion string `json:"exporter_version,omitempty"`
|
||||
CoreLibraryVersion string `json:"core_library_version,omitempty"`
|
||||
}
|
||||
|
||||
type Language int32
|
||||
|
||||
const (
|
||||
LanguageGo Language = 4
|
||||
)
|
||||
|
||||
type ServiceInfo struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wire
|
||||
|
||||
// This file contains type that match core proto types
|
||||
|
||||
type Timestamp = string
|
||||
|
||||
type Int64Value struct {
|
||||
Value int64 `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type DoubleValue struct {
|
||||
Value float64 `json:"value,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wire
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ExportMetricsServiceRequest struct {
|
||||
Node *Node `json:"node,omitempty"`
|
||||
Metrics []*Metric `json:"metrics,omitempty"`
|
||||
Resource *Resource `json:"resource,omitempty"`
|
||||
}
|
||||
|
||||
type Metric struct {
|
||||
MetricDescriptor *MetricDescriptor `json:"metric_descriptor,omitempty"`
|
||||
Timeseries []*TimeSeries `json:"timeseries,omitempty"`
|
||||
Resource *Resource `json:"resource,omitempty"`
|
||||
}
|
||||
|
||||
type MetricDescriptor struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Type MetricDescriptor_Type `json:"type,omitempty"`
|
||||
LabelKeys []*LabelKey `json:"label_keys,omitempty"`
|
||||
}
|
||||
|
||||
type MetricDescriptor_Type int32
|
||||
|
||||
const (
|
||||
MetricDescriptor_UNSPECIFIED MetricDescriptor_Type = 0
|
||||
MetricDescriptor_GAUGE_INT64 MetricDescriptor_Type = 1
|
||||
MetricDescriptor_GAUGE_DOUBLE MetricDescriptor_Type = 2
|
||||
MetricDescriptor_GAUGE_DISTRIBUTION MetricDescriptor_Type = 3
|
||||
MetricDescriptor_CUMULATIVE_INT64 MetricDescriptor_Type = 4
|
||||
MetricDescriptor_CUMULATIVE_DOUBLE MetricDescriptor_Type = 5
|
||||
MetricDescriptor_CUMULATIVE_DISTRIBUTION MetricDescriptor_Type = 6
|
||||
MetricDescriptor_SUMMARY MetricDescriptor_Type = 7
|
||||
)
|
||||
|
||||
type LabelKey struct {
|
||||
Key string `json:"key,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type TimeSeries struct {
|
||||
StartTimestamp *Timestamp `json:"start_timestamp,omitempty"`
|
||||
LabelValues []*LabelValue `json:"label_values,omitempty"`
|
||||
Points []*Point `json:"points,omitempty"`
|
||||
}
|
||||
|
||||
type LabelValue struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
HasValue bool `json:"has_value,omitempty"`
|
||||
}
|
||||
|
||||
type Point struct {
|
||||
Timestamp *Timestamp `json:"timestamp,omitempty"`
|
||||
Value PointValue `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type PointInt64Value struct {
|
||||
Int64Value int64 `json:"int64Value,omitempty"`
|
||||
}
|
||||
|
||||
// MarshalJSON creates JSON formatted the same way as jsonpb so that the
|
||||
// OpenCensus service can correctly determine the underlying value type.
|
||||
// This custom MarshalJSON exists because,
|
||||
// by default *Point is JSON marshalled as:
|
||||
//
|
||||
// {"value": {"int64Value": 1}}
|
||||
//
|
||||
// but it should be marshalled as:
|
||||
//
|
||||
// {"int64Value": 1}
|
||||
func (p *Point) MarshalJSON() ([]byte, error) {
|
||||
if p == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
|
||||
switch d := p.Value.(type) {
|
||||
case PointInt64Value:
|
||||
return json.Marshal(&struct {
|
||||
Timestamp *Timestamp `json:"timestamp,omitempty"`
|
||||
Value int64 `json:"int64Value,omitempty"`
|
||||
}{
|
||||
Timestamp: p.Timestamp,
|
||||
Value: d.Int64Value,
|
||||
})
|
||||
case PointDoubleValue:
|
||||
return json.Marshal(&struct {
|
||||
Timestamp *Timestamp `json:"timestamp,omitempty"`
|
||||
Value float64 `json:"doubleValue,omitempty"`
|
||||
}{
|
||||
Timestamp: p.Timestamp,
|
||||
Value: d.DoubleValue,
|
||||
})
|
||||
case PointDistributionValue:
|
||||
return json.Marshal(&struct {
|
||||
Timestamp *Timestamp `json:"timestamp,omitempty"`
|
||||
Value *DistributionValue `json:"distributionValue,omitempty"`
|
||||
}{
|
||||
Timestamp: p.Timestamp,
|
||||
Value: d.DistributionValue,
|
||||
})
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown point type %T", p.Value)
|
||||
}
|
||||
}
|
||||
|
||||
type PointDoubleValue struct {
|
||||
DoubleValue float64 `json:"doubleValue,omitempty"`
|
||||
}
|
||||
|
||||
type PointDistributionValue struct {
|
||||
DistributionValue *DistributionValue `json:"distributionValue,omitempty"`
|
||||
}
|
||||
|
||||
type PointSummaryValue struct {
|
||||
SummaryValue *SummaryValue `json:"summaryValue,omitempty"`
|
||||
}
|
||||
|
||||
type PointValue interface {
|
||||
labelPointValue()
|
||||
}
|
||||
|
||||
func (PointInt64Value) labelPointValue() {}
|
||||
func (PointDoubleValue) labelPointValue() {}
|
||||
func (PointDistributionValue) labelPointValue() {}
|
||||
func (PointSummaryValue) labelPointValue() {}
|
||||
|
||||
type DistributionValue struct {
|
||||
Count int64 `json:"count,omitempty"`
|
||||
Sum float64 `json:"sum,omitempty"`
|
||||
SumOfSquaredDeviation float64 `json:"sum_of_squared_deviation,omitempty"`
|
||||
BucketOptions BucketOptions `json:"bucket_options,omitempty"`
|
||||
Buckets []*Bucket `json:"buckets,omitempty"`
|
||||
}
|
||||
|
||||
type BucketOptionsExplicit struct {
|
||||
Bounds []float64 `json:"bounds,omitempty"`
|
||||
}
|
||||
|
||||
type BucketOptions interface {
|
||||
labelBucketOptions()
|
||||
}
|
||||
|
||||
func (*BucketOptionsExplicit) labelBucketOptions() {}
|
||||
|
||||
var _ BucketOptions = (*BucketOptionsExplicit)(nil)
|
||||
var _ json.Marshaler = (*BucketOptionsExplicit)(nil)
|
||||
|
||||
// Declared for the purpose of custom JSON marshaling without cycles.
|
||||
type bucketOptionsExplicitAlias BucketOptionsExplicit
|
||||
|
||||
// MarshalJSON creates JSON formatted the same way as jsonpb so that the
|
||||
// OpenCensus service can correctly determine the underlying value type.
|
||||
// This custom MarshalJSON exists because,
|
||||
// by default BucketOptionsExplicit is JSON marshalled as:
|
||||
//
|
||||
// {"bounds":[1,2,3]}
|
||||
//
|
||||
// but it should be marshalled as:
|
||||
//
|
||||
// {"explicit":{"bounds":[1,2,3]}}
|
||||
func (be *BucketOptionsExplicit) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&struct {
|
||||
Explicit *bucketOptionsExplicitAlias `json:"explicit,omitempty"`
|
||||
}{
|
||||
Explicit: (*bucketOptionsExplicitAlias)(be),
|
||||
})
|
||||
}
|
||||
|
||||
type Bucket struct {
|
||||
Count int64 `json:"count,omitempty"`
|
||||
Exemplar *Exemplar `json:"exemplar,omitempty"`
|
||||
}
|
||||
|
||||
type Exemplar struct {
|
||||
Value float64 `json:"value,omitempty"`
|
||||
Timestamp *Timestamp `json:"timestamp,omitempty"`
|
||||
Attachments map[string]string `json:"attachments,omitempty"`
|
||||
}
|
||||
|
||||
type SummaryValue struct {
|
||||
Count *Int64Value `json:"count,omitempty"`
|
||||
Sum *DoubleValue `json:"sum,omitempty"`
|
||||
Snapshot *Snapshot `json:"snapshot,omitempty"`
|
||||
}
|
||||
|
||||
type Snapshot struct {
|
||||
Count *Int64Value `json:"count,omitempty"`
|
||||
Sum *DoubleValue `json:"sum,omitempty"`
|
||||
PercentileValues []*SnapshotValueAtPercentile `json:"percentile_values,omitempty"`
|
||||
}
|
||||
|
||||
type SnapshotValueAtPercentile struct {
|
||||
Percentile float64 `json:"percentile,omitempty"`
|
||||
Value float64 `json:"value,omitempty"`
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wire
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pt *Point
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"PointInt64",
|
||||
&Point{
|
||||
Value: PointInt64Value{
|
||||
Int64Value: 5,
|
||||
},
|
||||
},
|
||||
`{"int64Value":5}`,
|
||||
},
|
||||
{
|
||||
"PointDouble",
|
||||
&Point{
|
||||
Value: PointDoubleValue{
|
||||
DoubleValue: 3.14,
|
||||
},
|
||||
},
|
||||
`{"doubleValue":3.14}`,
|
||||
},
|
||||
{
|
||||
"PointDistribution",
|
||||
&Point{
|
||||
Value: PointDistributionValue{
|
||||
DistributionValue: &DistributionValue{
|
||||
Count: 3,
|
||||
Sum: 10,
|
||||
Buckets: []*Bucket{
|
||||
{
|
||||
Count: 1,
|
||||
},
|
||||
{
|
||||
Count: 2,
|
||||
},
|
||||
},
|
||||
BucketOptions: &BucketOptionsExplicit{
|
||||
Bounds: []float64{
|
||||
0, 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
`{"distributionValue":{"count":3,"sum":10,"bucket_options":{"explicit":{"bounds":[0,5]}},"buckets":[{"count":1},{"count":2}]}}`,
|
||||
},
|
||||
{
|
||||
"nil point",
|
||||
nil,
|
||||
`null`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buf, err := tt.pt.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("Got:\n%v\nWant:\n%v", err, nil)
|
||||
}
|
||||
got := string(buf)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Fatalf("Got:\n%s\nWant:\n%s", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wire
|
||||
|
||||
type ExportTraceServiceRequest struct {
|
||||
Node *Node `json:"node,omitempty"`
|
||||
Spans []*Span `json:"spans,omitempty"`
|
||||
Resource *Resource `json:"resource,omitempty"`
|
||||
}
|
||||
|
||||
type Span struct {
|
||||
TraceID []byte `json:"trace_id,omitempty"`
|
||||
SpanID []byte `json:"span_id,omitempty"`
|
||||
TraceState *TraceState `json:"tracestate,omitempty"`
|
||||
ParentSpanID []byte `json:"parent_span_id,omitempty"`
|
||||
Name *TruncatableString `json:"name,omitempty"`
|
||||
Kind SpanKind `json:"kind,omitempty"`
|
||||
StartTime Timestamp `json:"start_time,omitempty"`
|
||||
EndTime Timestamp `json:"end_time,omitempty"`
|
||||
Attributes *Attributes `json:"attributes,omitempty"`
|
||||
StackTrace *StackTrace `json:"stack_trace,omitempty"`
|
||||
TimeEvents *TimeEvents `json:"time_events,omitempty"`
|
||||
Links *Links `json:"links,omitempty"`
|
||||
Status *Status `json:"status,omitempty"`
|
||||
Resource *Resource `json:"resource,omitempty"`
|
||||
SameProcessAsParentSpan bool `json:"same_process_as_parent_span,omitempty"`
|
||||
ChildSpanCount bool `json:"child_span_count,omitempty"`
|
||||
}
|
||||
|
||||
type TraceState struct {
|
||||
Entries []*TraceStateEntry `json:"entries,omitempty"`
|
||||
}
|
||||
|
||||
type TraceStateEntry struct {
|
||||
Key string `json:"key,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type SpanKind int32
|
||||
|
||||
const (
|
||||
UnspecifiedSpanKind SpanKind = 0
|
||||
ServerSpanKind SpanKind = 1
|
||||
ClientSpanKind SpanKind = 2
|
||||
)
|
||||
|
||||
type TimeEvents struct {
|
||||
TimeEvent []TimeEvent `json:"timeEvent,omitempty"`
|
||||
DroppedAnnotationsCount int32 `json:"dropped_annotations_count,omitempty"`
|
||||
DroppedMessageEventsCount int32 `json:"dropped_message_events_count,omitempty"`
|
||||
}
|
||||
|
||||
type TimeEvent struct {
|
||||
Time Timestamp `json:"time,omitempty"`
|
||||
MessageEvent *MessageEvent `json:"messageEvent,omitempty"`
|
||||
Annotation *Annotation `json:"annotation,omitempty"`
|
||||
}
|
||||
|
||||
type Annotation struct {
|
||||
Description *TruncatableString `json:"description,omitempty"`
|
||||
Attributes *Attributes `json:"attributes,omitempty"`
|
||||
}
|
||||
|
||||
type MessageEvent struct {
|
||||
Type MessageEventType `json:"type,omitempty"`
|
||||
ID uint64 `json:"id,omitempty"`
|
||||
UncompressedSize uint64 `json:"uncompressed_size,omitempty"`
|
||||
CompressedSize uint64 `json:"compressed_size,omitempty"`
|
||||
}
|
||||
|
||||
type MessageEventType int32
|
||||
|
||||
const (
|
||||
UnspecifiedMessageEvent MessageEventType = iota
|
||||
SentMessageEvent
|
||||
ReceivedMessageEvent
|
||||
)
|
||||
|
||||
type TimeEventValue interface {
|
||||
labelTimeEventValue()
|
||||
}
|
||||
|
||||
func (Annotation) labelTimeEventValue() {}
|
||||
func (MessageEvent) labelTimeEventValue() {}
|
||||
|
||||
type Links struct {
|
||||
Link []*Link `json:"link,omitempty"`
|
||||
DroppedLinksCount int32 `json:"dropped_links_count,omitempty"`
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
TraceID []byte `json:"trace_id,omitempty"`
|
||||
SpanID []byte `json:"span_id,omitempty"`
|
||||
Type LinkType `json:"type,omitempty"`
|
||||
Attributes *Attributes `json:"attributes,omitempty"`
|
||||
TraceState *TraceState `json:"tracestate,omitempty"`
|
||||
}
|
||||
|
||||
type LinkType int32
|
||||
|
||||
const (
|
||||
UnspecifiedLinkType LinkType = 0
|
||||
ChildLinkType LinkType = 1
|
||||
ParentLinkType LinkType = 2
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
Code int32 `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package export
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"golang.org/x/tools/internal/event/core"
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
type Printer struct {
|
||||
buffer [128]byte
|
||||
}
|
||||
|
||||
func (p *Printer) WriteEvent(w io.Writer, ev core.Event, lm label.Map) {
|
||||
buf := p.buffer[:0]
|
||||
if !ev.At().IsZero() {
|
||||
w.Write(ev.At().AppendFormat(buf, "2006/01/02 15:04:05 "))
|
||||
}
|
||||
msg := keys.Msg.Get(lm)
|
||||
io.WriteString(w, msg)
|
||||
if err := keys.Err.Get(lm); err != nil {
|
||||
if msg != "" {
|
||||
io.WriteString(w, ": ")
|
||||
}
|
||||
io.WriteString(w, err.Error())
|
||||
}
|
||||
for index := 0; ev.Valid(index); index++ {
|
||||
l := ev.Label(index)
|
||||
if !l.Valid() || l.Key() == keys.Msg || l.Key() == keys.Err {
|
||||
continue
|
||||
}
|
||||
io.WriteString(w, "\n\t")
|
||||
io.WriteString(w, l.Key().Name())
|
||||
io.WriteString(w, "=")
|
||||
l.Key().Format(w, buf, l)
|
||||
}
|
||||
io.WriteString(w, "\n")
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/event/core"
|
||||
"golang.org/x/tools/internal/event/export/metric"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
func New() *Exporter {
|
||||
return &Exporter{}
|
||||
}
|
||||
|
||||
type Exporter struct {
|
||||
mu sync.Mutex
|
||||
metrics []metric.Data
|
||||
}
|
||||
|
||||
func (e *Exporter) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context {
|
||||
if !event.IsMetric(ev) {
|
||||
return ctx
|
||||
}
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
metrics := metric.Entries.Get(lm).([]metric.Data)
|
||||
for _, data := range metrics {
|
||||
name := data.Handle()
|
||||
// We keep the metrics in name sorted order so the page is stable and easy
|
||||
// to read. We do this with an insertion sort rather than sorting the list
|
||||
// each time
|
||||
index := sort.Search(len(e.metrics), func(i int) bool {
|
||||
return e.metrics[i].Handle() >= name
|
||||
})
|
||||
if index >= len(e.metrics) || e.metrics[index].Handle() != name {
|
||||
// we have a new metric, so we need to make a space for it
|
||||
old := e.metrics
|
||||
e.metrics = make([]metric.Data, len(old)+1)
|
||||
copy(e.metrics, old[:index])
|
||||
copy(e.metrics[index+1:], old[index:])
|
||||
}
|
||||
e.metrics[index] = data
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (e *Exporter) header(w http.ResponseWriter, name, description string, isGauge, isHistogram bool) {
|
||||
kind := "counter"
|
||||
if isGauge {
|
||||
kind = "gauge"
|
||||
}
|
||||
if isHistogram {
|
||||
kind = "histogram"
|
||||
}
|
||||
fmt.Fprintf(w, "# HELP %s %s\n", name, description)
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n", name, kind)
|
||||
}
|
||||
|
||||
func (e *Exporter) row(w http.ResponseWriter, name string, group []label.Label, extra string, value interface{}) {
|
||||
fmt.Fprint(w, name)
|
||||
buf := &bytes.Buffer{}
|
||||
fmt.Fprint(buf, group)
|
||||
if extra != "" {
|
||||
if buf.Len() > 0 {
|
||||
fmt.Fprint(buf, ",")
|
||||
}
|
||||
fmt.Fprint(buf, extra)
|
||||
}
|
||||
if buf.Len() > 0 {
|
||||
fmt.Fprint(w, "{")
|
||||
buf.WriteTo(w)
|
||||
fmt.Fprint(w, "}")
|
||||
}
|
||||
fmt.Fprintf(w, " %v\n", value)
|
||||
}
|
||||
|
||||
func (e *Exporter) Serve(w http.ResponseWriter, r *http.Request) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
for _, data := range e.metrics {
|
||||
switch data := data.(type) {
|
||||
case *metric.Int64Data:
|
||||
e.header(w, data.Info.Name, data.Info.Description, data.IsGauge, false)
|
||||
for i, group := range data.Groups() {
|
||||
e.row(w, data.Info.Name, group, "", data.Rows[i])
|
||||
}
|
||||
|
||||
case *metric.Float64Data:
|
||||
e.header(w, data.Info.Name, data.Info.Description, data.IsGauge, false)
|
||||
for i, group := range data.Groups() {
|
||||
e.row(w, data.Info.Name, group, "", data.Rows[i])
|
||||
}
|
||||
|
||||
case *metric.HistogramInt64Data:
|
||||
e.header(w, data.Info.Name, data.Info.Description, false, true)
|
||||
for i, group := range data.Groups() {
|
||||
row := data.Rows[i]
|
||||
for j, b := range data.Info.Buckets {
|
||||
e.row(w, data.Info.Name+"_bucket", group, fmt.Sprintf(`le="%v"`, b), row.Values[j])
|
||||
}
|
||||
e.row(w, data.Info.Name+"_bucket", group, `le="+Inf"`, row.Count)
|
||||
e.row(w, data.Info.Name+"_count", group, "", row.Count)
|
||||
e.row(w, data.Info.Name+"_sum", group, "", row.Sum)
|
||||
}
|
||||
|
||||
case *metric.HistogramFloat64Data:
|
||||
e.header(w, data.Info.Name, data.Info.Description, false, true)
|
||||
for i, group := range data.Groups() {
|
||||
row := data.Rows[i]
|
||||
for j, b := range data.Info.Buckets {
|
||||
e.row(w, data.Info.Name+"_bucket", group, fmt.Sprintf(`le="%v"`, b), row.Values[j])
|
||||
}
|
||||
e.row(w, data.Info.Name+"_bucket", group, `le="+Inf"`, row.Count)
|
||||
e.row(w, data.Info.Name+"_count", group, "", row.Count)
|
||||
e.row(w, data.Info.Name+"_sum", group, "", row.Sum)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package export
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/tools/internal/event"
|
||||
"golang.org/x/tools/internal/event/core"
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
type SpanContext struct {
|
||||
TraceID TraceID
|
||||
SpanID SpanID
|
||||
}
|
||||
|
||||
type Span struct {
|
||||
Name string
|
||||
ID SpanContext
|
||||
ParentID SpanID
|
||||
mu sync.Mutex
|
||||
start core.Event
|
||||
finish core.Event
|
||||
events []core.Event
|
||||
}
|
||||
|
||||
type contextKeyType int
|
||||
|
||||
const (
|
||||
spanContextKey = contextKeyType(iota)
|
||||
labelContextKey
|
||||
)
|
||||
|
||||
func GetSpan(ctx context.Context) *Span {
|
||||
v := ctx.Value(spanContextKey)
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
return v.(*Span)
|
||||
}
|
||||
|
||||
// Spans creates an exporter that maintains hierarchical span structure in the
|
||||
// context.
|
||||
// It creates new spans on start events, adds events to the current span on
|
||||
// log or label, and closes the span on end events.
|
||||
// The span structure can then be used by other exporters.
|
||||
func Spans(output event.Exporter) event.Exporter {
|
||||
return func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
|
||||
switch {
|
||||
case event.IsLog(ev), event.IsLabel(ev):
|
||||
if span := GetSpan(ctx); span != nil {
|
||||
span.mu.Lock()
|
||||
span.events = append(span.events, ev)
|
||||
span.mu.Unlock()
|
||||
}
|
||||
case event.IsStart(ev):
|
||||
span := &Span{
|
||||
Name: keys.Start.Get(lm),
|
||||
start: ev,
|
||||
}
|
||||
if parent := GetSpan(ctx); parent != nil {
|
||||
span.ID.TraceID = parent.ID.TraceID
|
||||
span.ParentID = parent.ID.SpanID
|
||||
} else {
|
||||
span.ID.TraceID = newTraceID()
|
||||
}
|
||||
span.ID.SpanID = newSpanID()
|
||||
ctx = context.WithValue(ctx, spanContextKey, span)
|
||||
case event.IsEnd(ev):
|
||||
if span := GetSpan(ctx); span != nil {
|
||||
span.mu.Lock()
|
||||
span.finish = ev
|
||||
span.mu.Unlock()
|
||||
}
|
||||
case event.IsDetach(ev):
|
||||
ctx = context.WithValue(ctx, spanContextKey, nil)
|
||||
}
|
||||
return output(ctx, ev, lm)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SpanContext) Format(f fmt.State, r rune) {
|
||||
fmt.Fprintf(f, "%v:%v", s.TraceID, s.SpanID)
|
||||
}
|
||||
|
||||
func (s *Span) Start() core.Event {
|
||||
// start never changes after construction, so we don't need to hold the mutex
|
||||
return s.start
|
||||
}
|
||||
|
||||
func (s *Span) Finish() core.Event {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.finish
|
||||
}
|
||||
|
||||
func (s *Span) Events() []core.Event {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.events
|
||||
}
|
||||
|
||||
func (s *Span) Format(f fmt.State, r rune) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
fmt.Fprintf(f, "%v %v", s.Name, s.ID)
|
||||
if s.ParentID.IsValid() {
|
||||
fmt.Fprintf(f, "[%v]", s.ParentID)
|
||||
}
|
||||
fmt.Fprintf(f, " %v->%v", s.start, s.finish)
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package keys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
// Value represents a key for untyped values.
|
||||
type Value struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// New creates a new Key for untyped values.
|
||||
func New(name, description string) *Value {
|
||||
return &Value{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *Value) Name() string { return k.name }
|
||||
func (k *Value) Description() string { return k.description }
|
||||
|
||||
func (k *Value) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
fmt.Fprint(w, k.From(l))
|
||||
}
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *Value) Get(lm label.Map) interface{} {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *Value) From(t label.Label) interface{} { return t.UnpackValue() }
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *Value) Of(value interface{}) label.Label { return label.OfValue(k, value) }
|
||||
|
||||
// Tag represents a key for tagging labels that have no value.
|
||||
// These are used when the existence of the label is the entire information it
|
||||
// carries, such as marking events to be of a specific kind, or from a specific
|
||||
// package.
|
||||
type Tag struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewTag creates a new Key for tagging labels.
|
||||
func NewTag(name, description string) *Tag {
|
||||
return &Tag{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *Tag) Name() string { return k.name }
|
||||
func (k *Tag) Description() string { return k.description }
|
||||
|
||||
func (k *Tag) Format(w io.Writer, buf []byte, l label.Label) {}
|
||||
|
||||
// New creates a new Label with this key.
|
||||
func (k *Tag) New() label.Label { return label.OfValue(k, nil) }
|
||||
|
||||
// Int represents a key
|
||||
type Int struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewInt creates a new Key for int values.
|
||||
func NewInt(name, description string) *Int {
|
||||
return &Int{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *Int) Name() string { return k.name }
|
||||
func (k *Int) Description() string { return k.description }
|
||||
|
||||
func (k *Int) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendInt(buf, int64(k.From(l)), 10))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *Int) Of(v int) label.Label { return label.Of64(k, uint64(v)) }
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *Int) Get(lm label.Map) int {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *Int) From(t label.Label) int { return int(t.Unpack64()) }
|
||||
|
||||
// Int8 represents a key
|
||||
type Int8 struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewInt8 creates a new Key for int8 values.
|
||||
func NewInt8(name, description string) *Int8 {
|
||||
return &Int8{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *Int8) Name() string { return k.name }
|
||||
func (k *Int8) Description() string { return k.description }
|
||||
|
||||
func (k *Int8) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendInt(buf, int64(k.From(l)), 10))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *Int8) Of(v int8) label.Label { return label.Of64(k, uint64(v)) }
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *Int8) Get(lm label.Map) int8 {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *Int8) From(t label.Label) int8 { return int8(t.Unpack64()) }
|
||||
|
||||
// Int16 represents a key
|
||||
type Int16 struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewInt16 creates a new Key for int16 values.
|
||||
func NewInt16(name, description string) *Int16 {
|
||||
return &Int16{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *Int16) Name() string { return k.name }
|
||||
func (k *Int16) Description() string { return k.description }
|
||||
|
||||
func (k *Int16) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendInt(buf, int64(k.From(l)), 10))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *Int16) Of(v int16) label.Label { return label.Of64(k, uint64(v)) }
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *Int16) Get(lm label.Map) int16 {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *Int16) From(t label.Label) int16 { return int16(t.Unpack64()) }
|
||||
|
||||
// Int32 represents a key
|
||||
type Int32 struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewInt32 creates a new Key for int32 values.
|
||||
func NewInt32(name, description string) *Int32 {
|
||||
return &Int32{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *Int32) Name() string { return k.name }
|
||||
func (k *Int32) Description() string { return k.description }
|
||||
|
||||
func (k *Int32) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendInt(buf, int64(k.From(l)), 10))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *Int32) Of(v int32) label.Label { return label.Of64(k, uint64(v)) }
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *Int32) Get(lm label.Map) int32 {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *Int32) From(t label.Label) int32 { return int32(t.Unpack64()) }
|
||||
|
||||
// Int64 represents a key
|
||||
type Int64 struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewInt64 creates a new Key for int64 values.
|
||||
func NewInt64(name, description string) *Int64 {
|
||||
return &Int64{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *Int64) Name() string { return k.name }
|
||||
func (k *Int64) Description() string { return k.description }
|
||||
|
||||
func (k *Int64) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendInt(buf, k.From(l), 10))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *Int64) Of(v int64) label.Label { return label.Of64(k, uint64(v)) }
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *Int64) Get(lm label.Map) int64 {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *Int64) From(t label.Label) int64 { return int64(t.Unpack64()) }
|
||||
|
||||
// UInt represents a key
|
||||
type UInt struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewUInt creates a new Key for uint values.
|
||||
func NewUInt(name, description string) *UInt {
|
||||
return &UInt{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *UInt) Name() string { return k.name }
|
||||
func (k *UInt) Description() string { return k.description }
|
||||
|
||||
func (k *UInt) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendUint(buf, uint64(k.From(l)), 10))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *UInt) Of(v uint) label.Label { return label.Of64(k, uint64(v)) }
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *UInt) Get(lm label.Map) uint {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *UInt) From(t label.Label) uint { return uint(t.Unpack64()) }
|
||||
|
||||
// UInt8 represents a key
|
||||
type UInt8 struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewUInt8 creates a new Key for uint8 values.
|
||||
func NewUInt8(name, description string) *UInt8 {
|
||||
return &UInt8{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *UInt8) Name() string { return k.name }
|
||||
func (k *UInt8) Description() string { return k.description }
|
||||
|
||||
func (k *UInt8) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendUint(buf, uint64(k.From(l)), 10))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *UInt8) Of(v uint8) label.Label { return label.Of64(k, uint64(v)) }
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *UInt8) Get(lm label.Map) uint8 {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *UInt8) From(t label.Label) uint8 { return uint8(t.Unpack64()) }
|
||||
|
||||
// UInt16 represents a key
|
||||
type UInt16 struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewUInt16 creates a new Key for uint16 values.
|
||||
func NewUInt16(name, description string) *UInt16 {
|
||||
return &UInt16{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *UInt16) Name() string { return k.name }
|
||||
func (k *UInt16) Description() string { return k.description }
|
||||
|
||||
func (k *UInt16) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendUint(buf, uint64(k.From(l)), 10))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *UInt16) Of(v uint16) label.Label { return label.Of64(k, uint64(v)) }
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *UInt16) Get(lm label.Map) uint16 {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *UInt16) From(t label.Label) uint16 { return uint16(t.Unpack64()) }
|
||||
|
||||
// UInt32 represents a key
|
||||
type UInt32 struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewUInt32 creates a new Key for uint32 values.
|
||||
func NewUInt32(name, description string) *UInt32 {
|
||||
return &UInt32{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *UInt32) Name() string { return k.name }
|
||||
func (k *UInt32) Description() string { return k.description }
|
||||
|
||||
func (k *UInt32) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendUint(buf, uint64(k.From(l)), 10))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *UInt32) Of(v uint32) label.Label { return label.Of64(k, uint64(v)) }
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *UInt32) Get(lm label.Map) uint32 {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *UInt32) From(t label.Label) uint32 { return uint32(t.Unpack64()) }
|
||||
|
||||
// UInt64 represents a key
|
||||
type UInt64 struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewUInt64 creates a new Key for uint64 values.
|
||||
func NewUInt64(name, description string) *UInt64 {
|
||||
return &UInt64{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *UInt64) Name() string { return k.name }
|
||||
func (k *UInt64) Description() string { return k.description }
|
||||
|
||||
func (k *UInt64) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendUint(buf, k.From(l), 10))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *UInt64) Of(v uint64) label.Label { return label.Of64(k, v) }
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *UInt64) Get(lm label.Map) uint64 {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *UInt64) From(t label.Label) uint64 { return t.Unpack64() }
|
||||
|
||||
// Float32 represents a key
|
||||
type Float32 struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewFloat32 creates a new Key for float32 values.
|
||||
func NewFloat32(name, description string) *Float32 {
|
||||
return &Float32{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *Float32) Name() string { return k.name }
|
||||
func (k *Float32) Description() string { return k.description }
|
||||
|
||||
func (k *Float32) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendFloat(buf, float64(k.From(l)), 'E', -1, 32))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *Float32) Of(v float32) label.Label {
|
||||
return label.Of64(k, uint64(math.Float32bits(v)))
|
||||
}
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *Float32) Get(lm label.Map) float32 {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *Float32) From(t label.Label) float32 {
|
||||
return math.Float32frombits(uint32(t.Unpack64()))
|
||||
}
|
||||
|
||||
// Float64 represents a key
|
||||
type Float64 struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewFloat64 creates a new Key for int64 values.
|
||||
func NewFloat64(name, description string) *Float64 {
|
||||
return &Float64{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *Float64) Name() string { return k.name }
|
||||
func (k *Float64) Description() string { return k.description }
|
||||
|
||||
func (k *Float64) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendFloat(buf, k.From(l), 'E', -1, 64))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *Float64) Of(v float64) label.Label {
|
||||
return label.Of64(k, math.Float64bits(v))
|
||||
}
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *Float64) Get(lm label.Map) float64 {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *Float64) From(t label.Label) float64 {
|
||||
return math.Float64frombits(t.Unpack64())
|
||||
}
|
||||
|
||||
// String represents a key
|
||||
type String struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewString creates a new Key for int64 values.
|
||||
func NewString(name, description string) *String {
|
||||
return &String{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *String) Name() string { return k.name }
|
||||
func (k *String) Description() string { return k.description }
|
||||
|
||||
func (k *String) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendQuote(buf, k.From(l)))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *String) Of(v string) label.Label { return label.OfString(k, v) }
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *String) Get(lm label.Map) string {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *String) From(t label.Label) string { return t.UnpackString() }
|
||||
|
||||
// Boolean represents a key
|
||||
type Boolean struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewBoolean creates a new Key for bool values.
|
||||
func NewBoolean(name, description string) *Boolean {
|
||||
return &Boolean{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *Boolean) Name() string { return k.name }
|
||||
func (k *Boolean) Description() string { return k.description }
|
||||
|
||||
func (k *Boolean) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
w.Write(strconv.AppendBool(buf, k.From(l)))
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *Boolean) Of(v bool) label.Label {
|
||||
if v {
|
||||
return label.Of64(k, 1)
|
||||
}
|
||||
return label.Of64(k, 0)
|
||||
}
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *Boolean) Get(lm label.Map) bool {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *Boolean) From(t label.Label) bool { return t.Unpack64() > 0 }
|
||||
|
||||
// Error represents a key
|
||||
type Error struct {
|
||||
name string
|
||||
description string
|
||||
}
|
||||
|
||||
// NewError creates a new Key for int64 values.
|
||||
func NewError(name, description string) *Error {
|
||||
return &Error{name: name, description: description}
|
||||
}
|
||||
|
||||
func (k *Error) Name() string { return k.name }
|
||||
func (k *Error) Description() string { return k.description }
|
||||
|
||||
func (k *Error) Format(w io.Writer, buf []byte, l label.Label) {
|
||||
io.WriteString(w, k.From(l).Error())
|
||||
}
|
||||
|
||||
// Of creates a new Label with this key and the supplied value.
|
||||
func (k *Error) Of(v error) label.Label { return label.OfValue(k, v) }
|
||||
|
||||
// Get can be used to get a label for the key from a label.Map.
|
||||
func (k *Error) Get(lm label.Map) error {
|
||||
if t := lm.Find(k); t.Valid() {
|
||||
return k.From(t)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// From can be used to get a value from a Label.
|
||||
func (k *Error) From(t label.Label) error {
|
||||
err, _ := t.UnpackValue().(error)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package keys
|
||||
|
||||
var (
|
||||
// Msg is a key used to add message strings to label lists.
|
||||
Msg = NewString("message", "a readable message")
|
||||
// Label is a key used to indicate an event adds labels to the context.
|
||||
Label = NewTag("label", "a label context marker")
|
||||
// Start is used for things like traces that have a name.
|
||||
Start = NewString("start", "span start")
|
||||
// Metric is a key used to indicate an event records metrics.
|
||||
End = NewTag("end", "a span end marker")
|
||||
// Metric is a key used to indicate an event records metrics.
|
||||
Detach = NewTag("detach", "a span detach marker")
|
||||
// Err is a key used to add error values to label lists.
|
||||
Err = NewError("error", "an error that occurred")
|
||||
// Metric is a key used to indicate an event records metrics.
|
||||
Metric = NewTag("metric", "a metric event marker")
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package keys
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Join returns a canonical join of the keys in S:
|
||||
// a sorted comma-separated string list.
|
||||
func Join[S ~[]T, T ~string](s S) string {
|
||||
strs := make([]string, 0, len(s))
|
||||
for _, v := range s {
|
||||
strs = append(strs, string(v))
|
||||
}
|
||||
sort.Strings(strs)
|
||||
return strings.Join(strs, ",")
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package keys
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestJoin(t *testing.T) {
|
||||
type T string
|
||||
type S []T
|
||||
|
||||
tests := []struct {
|
||||
data S
|
||||
want string
|
||||
}{
|
||||
{S{"a", "b", "c"}, "a,b,c"},
|
||||
{S{"b", "a", "c"}, "a,b,c"},
|
||||
{S{"c", "a", "b"}, "a,b,c"},
|
||||
{nil, ""},
|
||||
{S{}, ""},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if got := Join(test.data); got != test.want {
|
||||
t.Errorf("Join(%v) = %q, want %q", test.data, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package label
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Key is used as the identity of a Label.
|
||||
// Keys are intended to be compared by pointer only, the name should be unique
|
||||
// for communicating with external systems, but it is not required or enforced.
|
||||
type Key interface {
|
||||
// Name returns the key name.
|
||||
Name() string
|
||||
// Description returns a string that can be used to describe the value.
|
||||
Description() string
|
||||
|
||||
// Format is used in formatting to append the value of the label to the
|
||||
// supplied buffer.
|
||||
// The formatter may use the supplied buf as a scratch area to avoid
|
||||
// allocations.
|
||||
Format(w io.Writer, buf []byte, l Label)
|
||||
}
|
||||
|
||||
// Label holds a key and value pair.
|
||||
// It is normally used when passing around lists of labels.
|
||||
type Label struct {
|
||||
key Key
|
||||
packed uint64
|
||||
untyped interface{}
|
||||
}
|
||||
|
||||
// Map is the interface to a collection of Labels indexed by key.
|
||||
type Map interface {
|
||||
// Find returns the label that matches the supplied key.
|
||||
Find(key Key) Label
|
||||
}
|
||||
|
||||
// List is the interface to something that provides an iterable
|
||||
// list of labels.
|
||||
// Iteration should start from 0 and continue until Valid returns false.
|
||||
type List interface {
|
||||
// Valid returns true if the index is within range for the list.
|
||||
// It does not imply the label at that index will itself be valid.
|
||||
Valid(index int) bool
|
||||
// Label returns the label at the given index.
|
||||
Label(index int) Label
|
||||
}
|
||||
|
||||
// list implements LabelList for a list of Labels.
|
||||
type list struct {
|
||||
labels []Label
|
||||
}
|
||||
|
||||
// filter wraps a LabelList filtering out specific labels.
|
||||
type filter struct {
|
||||
keys []Key
|
||||
underlying List
|
||||
}
|
||||
|
||||
// listMap implements LabelMap for a simple list of labels.
|
||||
type listMap struct {
|
||||
labels []Label
|
||||
}
|
||||
|
||||
// mapChain implements LabelMap for a list of underlying LabelMap.
|
||||
type mapChain struct {
|
||||
maps []Map
|
||||
}
|
||||
|
||||
// OfValue creates a new label from the key and value.
|
||||
// This method is for implementing new key types, label creation should
|
||||
// normally be done with the Of method of the key.
|
||||
func OfValue(k Key, value interface{}) Label { return Label{key: k, untyped: value} }
|
||||
|
||||
// UnpackValue assumes the label was built using LabelOfValue and returns the value
|
||||
// that was passed to that constructor.
|
||||
// This method is for implementing new key types, for type safety normal
|
||||
// access should be done with the From method of the key.
|
||||
func (t Label) UnpackValue() interface{} { return t.untyped }
|
||||
|
||||
// Of64 creates a new label from a key and a uint64. This is often
|
||||
// used for non uint64 values that can be packed into a uint64.
|
||||
// This method is for implementing new key types, label creation should
|
||||
// normally be done with the Of method of the key.
|
||||
func Of64(k Key, v uint64) Label { return Label{key: k, packed: v} }
|
||||
|
||||
// Unpack64 assumes the label was built using LabelOf64 and returns the value that
|
||||
// was passed to that constructor.
|
||||
// This method is for implementing new key types, for type safety normal
|
||||
// access should be done with the From method of the key.
|
||||
func (t Label) Unpack64() uint64 { return t.packed }
|
||||
|
||||
type stringptr unsafe.Pointer
|
||||
|
||||
// OfString creates a new label from a key and a string.
|
||||
// This method is for implementing new key types, label creation should
|
||||
// normally be done with the Of method of the key.
|
||||
func OfString(k Key, v string) Label {
|
||||
hdr := (*reflect.StringHeader)(unsafe.Pointer(&v))
|
||||
return Label{
|
||||
key: k,
|
||||
packed: uint64(hdr.Len),
|
||||
untyped: stringptr(hdr.Data),
|
||||
}
|
||||
}
|
||||
|
||||
// UnpackString assumes the label was built using LabelOfString and returns the
|
||||
// value that was passed to that constructor.
|
||||
// This method is for implementing new key types, for type safety normal
|
||||
// access should be done with the From method of the key.
|
||||
func (t Label) UnpackString() string {
|
||||
var v string
|
||||
hdr := (*reflect.StringHeader)(unsafe.Pointer(&v))
|
||||
hdr.Data = uintptr(t.untyped.(stringptr))
|
||||
hdr.Len = int(t.packed)
|
||||
return v
|
||||
}
|
||||
|
||||
// Valid returns true if the Label is a valid one (it has a key).
|
||||
func (t Label) Valid() bool { return t.key != nil }
|
||||
|
||||
// Key returns the key of this Label.
|
||||
func (t Label) Key() Key { return t.key }
|
||||
|
||||
// Format is used for debug printing of labels.
|
||||
func (t Label) Format(f fmt.State, r rune) {
|
||||
if !t.Valid() {
|
||||
io.WriteString(f, `nil`)
|
||||
return
|
||||
}
|
||||
io.WriteString(f, t.Key().Name())
|
||||
io.WriteString(f, "=")
|
||||
var buf [128]byte
|
||||
t.Key().Format(f, buf[:0], t)
|
||||
}
|
||||
|
||||
func (l *list) Valid(index int) bool {
|
||||
return index >= 0 && index < len(l.labels)
|
||||
}
|
||||
|
||||
func (l *list) Label(index int) Label {
|
||||
return l.labels[index]
|
||||
}
|
||||
|
||||
func (f *filter) Valid(index int) bool {
|
||||
return f.underlying.Valid(index)
|
||||
}
|
||||
|
||||
func (f *filter) Label(index int) Label {
|
||||
l := f.underlying.Label(index)
|
||||
for _, f := range f.keys {
|
||||
if l.Key() == f {
|
||||
return Label{}
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (lm listMap) Find(key Key) Label {
|
||||
for _, l := range lm.labels {
|
||||
if l.Key() == key {
|
||||
return l
|
||||
}
|
||||
}
|
||||
return Label{}
|
||||
}
|
||||
|
||||
func (c mapChain) Find(key Key) Label {
|
||||
for _, src := range c.maps {
|
||||
l := src.Find(key)
|
||||
if l.Valid() {
|
||||
return l
|
||||
}
|
||||
}
|
||||
return Label{}
|
||||
}
|
||||
|
||||
var emptyList = &list{}
|
||||
|
||||
func NewList(labels ...Label) List {
|
||||
if len(labels) == 0 {
|
||||
return emptyList
|
||||
}
|
||||
return &list{labels: labels}
|
||||
}
|
||||
|
||||
func Filter(l List, keys ...Key) List {
|
||||
if len(keys) == 0 {
|
||||
return l
|
||||
}
|
||||
return &filter{keys: keys, underlying: l}
|
||||
}
|
||||
|
||||
func NewMap(labels ...Label) Map {
|
||||
return listMap{labels: labels}
|
||||
}
|
||||
|
||||
func MergeMaps(srcs ...Map) Map {
|
||||
var nonNil []Map
|
||||
for _, src := range srcs {
|
||||
if src != nil {
|
||||
nonNil = append(nonNil, src)
|
||||
}
|
||||
}
|
||||
if len(nonNil) == 1 {
|
||||
return nonNil[0]
|
||||
}
|
||||
return mapChain{maps: nonNil}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package label_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/tools/internal/event/keys"
|
||||
"golang.org/x/tools/internal/event/label"
|
||||
)
|
||||
|
||||
var (
|
||||
AKey = keys.NewString("A", "")
|
||||
BKey = keys.NewString("B", "")
|
||||
CKey = keys.NewString("C", "")
|
||||
A = AKey.Of("a")
|
||||
B = BKey.Of("b")
|
||||
C = CKey.Of("c")
|
||||
all = []label.Label{A, B, C}
|
||||
)
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
labels []label.Label
|
||||
expect string
|
||||
}{{
|
||||
name: "empty",
|
||||
}, {
|
||||
name: "single",
|
||||
labels: []label.Label{A},
|
||||
expect: `A="a"`,
|
||||
}, {
|
||||
name: "invalid",
|
||||
labels: []label.Label{{}},
|
||||
expect: ``,
|
||||
}, {
|
||||
name: "two",
|
||||
labels: []label.Label{A, B},
|
||||
expect: `A="a", B="b"`,
|
||||
}, {
|
||||
name: "three",
|
||||
labels: []label.Label{A, B, C},
|
||||
expect: `A="a", B="b", C="c"`,
|
||||
}, {
|
||||
name: "missing A",
|
||||
labels: []label.Label{{}, B, C},
|
||||
expect: `B="b", C="c"`,
|
||||
}, {
|
||||
name: "missing B",
|
||||
labels: []label.Label{A, {}, C},
|
||||
expect: `A="a", C="c"`,
|
||||
}, {
|
||||
name: "missing C",
|
||||
labels: []label.Label{A, B, {}},
|
||||
expect: `A="a", B="b"`,
|
||||
}, {
|
||||
name: "missing AB",
|
||||
labels: []label.Label{{}, {}, C},
|
||||
expect: `C="c"`,
|
||||
}, {
|
||||
name: "missing AC",
|
||||
labels: []label.Label{{}, B, {}},
|
||||
expect: `B="b"`,
|
||||
}, {
|
||||
name: "missing BC",
|
||||
labels: []label.Label{A, {}, {}},
|
||||
expect: `A="a"`,
|
||||
}} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := printList(label.NewList(test.labels...))
|
||||
if got != test.expect {
|
||||
t.Errorf("got %q want %q", got, test.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
labels []label.Label
|
||||
filters []label.Key
|
||||
expect string
|
||||
}{{
|
||||
name: "no filters",
|
||||
labels: all,
|
||||
expect: `A="a", B="b", C="c"`,
|
||||
}, {
|
||||
name: "no labels",
|
||||
filters: []label.Key{AKey},
|
||||
expect: ``,
|
||||
}, {
|
||||
name: "filter A",
|
||||
labels: all,
|
||||
filters: []label.Key{AKey},
|
||||
expect: `B="b", C="c"`,
|
||||
}, {
|
||||
name: "filter B",
|
||||
labels: all,
|
||||
filters: []label.Key{BKey},
|
||||
expect: `A="a", C="c"`,
|
||||
}, {
|
||||
name: "filter C",
|
||||
labels: all,
|
||||
filters: []label.Key{CKey},
|
||||
expect: `A="a", B="b"`,
|
||||
}, {
|
||||
name: "filter AC",
|
||||
labels: all,
|
||||
filters: []label.Key{AKey, CKey},
|
||||
expect: `B="b"`,
|
||||
}} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
labels := label.NewList(test.labels...)
|
||||
got := printList(label.Filter(labels, test.filters...))
|
||||
if got != test.expect {
|
||||
t.Errorf("got %q want %q", got, test.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMap(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
labels []label.Label
|
||||
keys []label.Key
|
||||
expect string
|
||||
}{{
|
||||
name: "no labels",
|
||||
keys: []label.Key{AKey},
|
||||
expect: `nil`,
|
||||
}, {
|
||||
name: "match A",
|
||||
labels: all,
|
||||
keys: []label.Key{AKey},
|
||||
expect: `A="a"`,
|
||||
}, {
|
||||
name: "match B",
|
||||
labels: all,
|
||||
keys: []label.Key{BKey},
|
||||
expect: `B="b"`,
|
||||
}, {
|
||||
name: "match C",
|
||||
labels: all,
|
||||
keys: []label.Key{CKey},
|
||||
expect: `C="c"`,
|
||||
}, {
|
||||
name: "match ABC",
|
||||
labels: all,
|
||||
keys: []label.Key{AKey, BKey, CKey},
|
||||
expect: `A="a", B="b", C="c"`,
|
||||
}, {
|
||||
name: "missing A",
|
||||
labels: []label.Label{{}, B, C},
|
||||
keys: []label.Key{AKey, BKey, CKey},
|
||||
expect: `nil, B="b", C="c"`,
|
||||
}, {
|
||||
name: "missing B",
|
||||
labels: []label.Label{A, {}, C},
|
||||
keys: []label.Key{AKey, BKey, CKey},
|
||||
expect: `A="a", nil, C="c"`,
|
||||
}, {
|
||||
name: "missing C",
|
||||
labels: []label.Label{A, B, {}},
|
||||
keys: []label.Key{AKey, BKey, CKey},
|
||||
expect: `A="a", B="b", nil`,
|
||||
}} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
lm := label.NewMap(test.labels...)
|
||||
got := printMap(lm, test.keys)
|
||||
if got != test.expect {
|
||||
t.Errorf("got %q want %q", got, test.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapMerge(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
maps []label.Map
|
||||
keys []label.Key
|
||||
expect string
|
||||
}{{
|
||||
name: "no maps",
|
||||
keys: []label.Key{AKey},
|
||||
expect: `nil`,
|
||||
}, {
|
||||
name: "one map",
|
||||
maps: []label.Map{label.NewMap(all...)},
|
||||
keys: []label.Key{AKey},
|
||||
expect: `A="a"`,
|
||||
}, {
|
||||
name: "invalid map",
|
||||
maps: []label.Map{label.NewMap()},
|
||||
keys: []label.Key{AKey},
|
||||
expect: `nil`,
|
||||
}, {
|
||||
name: "two maps",
|
||||
maps: []label.Map{label.NewMap(B, C), label.NewMap(A)},
|
||||
keys: []label.Key{AKey, BKey, CKey},
|
||||
expect: `A="a", B="b", C="c"`,
|
||||
}, {
|
||||
name: "invalid start map",
|
||||
maps: []label.Map{label.NewMap(), label.NewMap(B, C)},
|
||||
keys: []label.Key{AKey, BKey, CKey},
|
||||
expect: `nil, B="b", C="c"`,
|
||||
}, {
|
||||
name: "invalid mid map",
|
||||
maps: []label.Map{label.NewMap(A), label.NewMap(), label.NewMap(C)},
|
||||
keys: []label.Key{AKey, BKey, CKey},
|
||||
expect: `A="a", nil, C="c"`,
|
||||
}, {
|
||||
name: "invalid end map",
|
||||
maps: []label.Map{label.NewMap(A, B), label.NewMap()},
|
||||
keys: []label.Key{AKey, BKey, CKey},
|
||||
expect: `A="a", B="b", nil`,
|
||||
}, {
|
||||
name: "three maps one nil",
|
||||
maps: []label.Map{label.NewMap(A), label.NewMap(B), nil},
|
||||
keys: []label.Key{AKey, BKey, CKey},
|
||||
expect: `A="a", B="b", nil`,
|
||||
}, {
|
||||
name: "two maps one nil",
|
||||
maps: []label.Map{label.NewMap(A, B), nil},
|
||||
keys: []label.Key{AKey, BKey, CKey},
|
||||
expect: `A="a", B="b", nil`,
|
||||
}} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
tagMap := label.MergeMaps(test.maps...)
|
||||
got := printMap(tagMap, test.keys)
|
||||
if got != test.expect {
|
||||
t.Errorf("got %q want %q", got, test.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func printList(list label.List) string {
|
||||
buf := &bytes.Buffer{}
|
||||
for index := 0; list.Valid(index); index++ {
|
||||
l := list.Label(index)
|
||||
if !l.Valid() {
|
||||
continue
|
||||
}
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
fmt.Fprint(buf, l)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func printMap(lm label.Map, keys []label.Key) string {
|
||||
buf := &bytes.Buffer{}
|
||||
for _, key := range keys {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
fmt.Fprint(buf, lm.Find(key))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func TestAttemptedStringCorruption(t *testing.T) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if _, ok := r.(*runtime.TypeAssertionError); !ok {
|
||||
t.Fatalf("wanted to recover TypeAssertionError, got %T", r)
|
||||
}
|
||||
}()
|
||||
|
||||
var x uint64 = 12390
|
||||
p := unsafe.Pointer(&x)
|
||||
l := label.OfValue(AKey, p)
|
||||
_ = l.UnpackString()
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package facts defines a serializable set of analysis.Fact.
|
||||
//
|
||||
// It provides a partial implementation of the Fact-related parts of the
|
||||
// analysis.Pass interface for use in analysis drivers such as "go vet"
|
||||
// and other build systems.
|
||||
//
|
||||
// The serial format is unspecified and may change, so the same version
|
||||
// of this package must be used for reading and writing serialized facts.
|
||||
//
|
||||
// The handling of facts in the analysis system parallels the handling
|
||||
// of type information in the compiler: during compilation of package P,
|
||||
// the compiler emits an export data file that describes the type of
|
||||
// every object (named thing) defined in package P, plus every object
|
||||
// indirectly reachable from one of those objects. Thus the downstream
|
||||
// compiler of package Q need only load one export data file per direct
|
||||
// import of Q, and it will learn everything about the API of package P
|
||||
// and everything it needs to know about the API of P's dependencies.
|
||||
//
|
||||
// Similarly, analysis of package P emits a fact set containing facts
|
||||
// about all objects exported from P, plus additional facts about only
|
||||
// those objects of P's dependencies that are reachable from the API of
|
||||
// package P; the downstream analysis of Q need only load one fact set
|
||||
// per direct import of Q.
|
||||
//
|
||||
// The notion of "exportedness" that matters here is that of the
|
||||
// compiler. According to the language spec, a method pkg.T.f is
|
||||
// unexported simply because its name starts with lowercase. But the
|
||||
// compiler must nonetheless export f so that downstream compilations can
|
||||
// accurately ascertain whether pkg.T implements an interface pkg.I
|
||||
// defined as interface{f()}. Exported thus means "described in export
|
||||
// data".
|
||||
package facts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"go/types"
|
||||
"io"
|
||||
"log"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/tools/go/analysis"
|
||||
"golang.org/x/tools/go/types/objectpath"
|
||||
)
|
||||
|
||||
const debug = false
|
||||
|
||||
// A Set is a set of analysis.Facts.
|
||||
//
|
||||
// Decode creates a Set of facts by reading from the imports of a given
|
||||
// package, and Encode writes out the set. Between these operation,
|
||||
// the Import and Export methods will query and update the set.
|
||||
//
|
||||
// All of Set's methods except String are safe to call concurrently.
|
||||
type Set struct {
|
||||
pkg *types.Package
|
||||
mu sync.Mutex
|
||||
m map[key]analysis.Fact
|
||||
}
|
||||
|
||||
type key struct {
|
||||
pkg *types.Package
|
||||
obj types.Object // (object facts only)
|
||||
t reflect.Type
|
||||
}
|
||||
|
||||
// ImportObjectFact implements analysis.Pass.ImportObjectFact.
|
||||
func (s *Set) ImportObjectFact(obj types.Object, ptr analysis.Fact) bool {
|
||||
if obj == nil {
|
||||
panic("nil object")
|
||||
}
|
||||
key := key{pkg: obj.Pkg(), obj: obj, t: reflect.TypeOf(ptr)}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if v, ok := s.m[key]; ok {
|
||||
reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ExportObjectFact implements analysis.Pass.ExportObjectFact.
|
||||
func (s *Set) ExportObjectFact(obj types.Object, fact analysis.Fact) {
|
||||
if obj.Pkg() != s.pkg {
|
||||
log.Panicf("in package %s: ExportObjectFact(%s, %T): can't set fact on object belonging another package",
|
||||
s.pkg, obj, fact)
|
||||
}
|
||||
key := key{pkg: obj.Pkg(), obj: obj, t: reflect.TypeOf(fact)}
|
||||
s.mu.Lock()
|
||||
s.m[key] = fact // clobber any existing entry
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Set) AllObjectFacts(filter map[reflect.Type]bool) []analysis.ObjectFact {
|
||||
var facts []analysis.ObjectFact
|
||||
s.mu.Lock()
|
||||
for k, v := range s.m {
|
||||
if k.obj != nil && filter[k.t] {
|
||||
facts = append(facts, analysis.ObjectFact{Object: k.obj, Fact: v})
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return facts
|
||||
}
|
||||
|
||||
// ImportPackageFact implements analysis.Pass.ImportPackageFact.
|
||||
func (s *Set) ImportPackageFact(pkg *types.Package, ptr analysis.Fact) bool {
|
||||
if pkg == nil {
|
||||
panic("nil package")
|
||||
}
|
||||
key := key{pkg: pkg, t: reflect.TypeOf(ptr)}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if v, ok := s.m[key]; ok {
|
||||
reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ExportPackageFact implements analysis.Pass.ExportPackageFact.
|
||||
func (s *Set) ExportPackageFact(fact analysis.Fact) {
|
||||
key := key{pkg: s.pkg, t: reflect.TypeOf(fact)}
|
||||
s.mu.Lock()
|
||||
s.m[key] = fact // clobber any existing entry
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Set) AllPackageFacts(filter map[reflect.Type]bool) []analysis.PackageFact {
|
||||
var facts []analysis.PackageFact
|
||||
s.mu.Lock()
|
||||
for k, v := range s.m {
|
||||
if k.obj == nil && filter[k.t] {
|
||||
facts = append(facts, analysis.PackageFact{Package: k.pkg, Fact: v})
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return facts
|
||||
}
|
||||
|
||||
// gobFact is the Gob declaration of a serialized fact.
|
||||
type gobFact struct {
|
||||
PkgPath string // path of package
|
||||
Object objectpath.Path // optional path of object relative to package itself
|
||||
Fact analysis.Fact // type and value of user-defined Fact
|
||||
}
|
||||
|
||||
// A Decoder decodes the facts from the direct imports of the package
|
||||
// provided to NewEncoder. A single decoder may be used to decode
|
||||
// multiple fact sets (e.g. each for a different set of fact types)
|
||||
// for the same package. Each call to Decode returns an independent
|
||||
// fact set.
|
||||
type Decoder struct {
|
||||
pkg *types.Package
|
||||
getPackage GetPackageFunc
|
||||
}
|
||||
|
||||
// NewDecoder returns a fact decoder for the specified package.
|
||||
//
|
||||
// It uses a brute-force recursive approach to enumerate all objects
|
||||
// defined by dependencies of pkg, so that it can learn the set of
|
||||
// package paths that may be mentioned in the fact encoding. This does
|
||||
// not scale well; use [NewDecoderFunc] where possible.
|
||||
func NewDecoder(pkg *types.Package) *Decoder {
|
||||
// Compute the import map for this package.
|
||||
// See the package doc comment.
|
||||
m := importMap(pkg.Imports())
|
||||
getPackageFunc := func(path string) *types.Package { return m[path] }
|
||||
return NewDecoderFunc(pkg, getPackageFunc)
|
||||
}
|
||||
|
||||
// NewDecoderFunc returns a fact decoder for the specified package.
|
||||
//
|
||||
// It calls the getPackage function for the package path string of
|
||||
// each dependency (perhaps indirect) that it encounters in the
|
||||
// encoding. If the function returns nil, the fact is discarded.
|
||||
//
|
||||
// This function is preferred over [NewDecoder] when the client is
|
||||
// capable of efficient look-up of packages by package path.
|
||||
func NewDecoderFunc(pkg *types.Package, getPackage GetPackageFunc) *Decoder {
|
||||
return &Decoder{
|
||||
pkg: pkg,
|
||||
getPackage: getPackage,
|
||||
}
|
||||
}
|
||||
|
||||
// A GetPackageFunc function returns the package denoted by a package path.
|
||||
type GetPackageFunc = func(pkgPath string) *types.Package
|
||||
|
||||
// Decode decodes all the facts relevant to the analysis of package
|
||||
// pkgPath. The read function reads serialized fact data from an external
|
||||
// source for one of pkg's direct imports, identified by package path.
|
||||
// The empty file is a valid encoding of an empty fact set.
|
||||
//
|
||||
// It is the caller's responsibility to call gob.Register on all
|
||||
// necessary fact types.
|
||||
//
|
||||
// Concurrent calls to Decode are safe, so long as the
|
||||
// [GetPackageFunc] (if any) is also concurrency-safe.
|
||||
func (d *Decoder) Decode(read func(pkgPath string) ([]byte, error)) (*Set, error) {
|
||||
// Read facts from imported packages.
|
||||
// Facts may describe indirectly imported packages, or their objects.
|
||||
m := make(map[key]analysis.Fact) // one big bucket
|
||||
for _, imp := range d.pkg.Imports() {
|
||||
logf := func(format string, args ...interface{}) {
|
||||
if debug {
|
||||
prefix := fmt.Sprintf("in %s, importing %s: ",
|
||||
d.pkg.Path(), imp.Path())
|
||||
log.Print(prefix, fmt.Sprintf(format, args...))
|
||||
}
|
||||
}
|
||||
|
||||
// Read the gob-encoded facts.
|
||||
data, err := read(imp.Path())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("in %s, can't import facts for package %q: %v",
|
||||
d.pkg.Path(), imp.Path(), err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
continue // no facts
|
||||
}
|
||||
var gobFacts []gobFact
|
||||
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&gobFacts); err != nil {
|
||||
return nil, fmt.Errorf("decoding facts for %q: %v", imp.Path(), err)
|
||||
}
|
||||
logf("decoded %d facts: %v", len(gobFacts), gobFacts)
|
||||
|
||||
// Parse each one into a key and a Fact.
|
||||
for _, f := range gobFacts {
|
||||
factPkg := d.getPackage(f.PkgPath) // possibly an indirect dependency
|
||||
if factPkg == nil {
|
||||
// Fact relates to a dependency that was
|
||||
// unused in this translation unit. Skip.
|
||||
logf("no package %q; discarding %v", f.PkgPath, f.Fact)
|
||||
continue
|
||||
}
|
||||
key := key{pkg: factPkg, t: reflect.TypeOf(f.Fact)}
|
||||
if f.Object != "" {
|
||||
// object fact
|
||||
obj, err := objectpath.Object(factPkg, f.Object)
|
||||
if err != nil {
|
||||
// (most likely due to unexported object)
|
||||
// TODO(adonovan): audit for other possibilities.
|
||||
logf("no object for path: %v; discarding %s", err, f.Fact)
|
||||
continue
|
||||
}
|
||||
key.obj = obj
|
||||
logf("read %T fact %s for %v", f.Fact, f.Fact, key.obj)
|
||||
} else {
|
||||
// package fact
|
||||
logf("read %T fact %s for %v", f.Fact, f.Fact, factPkg)
|
||||
}
|
||||
m[key] = f.Fact
|
||||
}
|
||||
}
|
||||
|
||||
return &Set{pkg: d.pkg, m: m}, nil
|
||||
}
|
||||
|
||||
// Encode encodes a set of facts to a memory buffer.
|
||||
//
|
||||
// It may fail if one of the Facts could not be gob-encoded, but this is
|
||||
// a sign of a bug in an Analyzer.
|
||||
func (s *Set) Encode() []byte {
|
||||
encoder := new(objectpath.Encoder)
|
||||
|
||||
// TODO(adonovan): opt: use a more efficient encoding
|
||||
// that avoids repeating PkgPath for each fact.
|
||||
|
||||
// Gather all facts, including those from imported packages.
|
||||
var gobFacts []gobFact
|
||||
|
||||
s.mu.Lock()
|
||||
for k, fact := range s.m {
|
||||
if debug {
|
||||
log.Printf("%v => %s\n", k, fact)
|
||||
}
|
||||
|
||||
// Don't export facts that we imported from another
|
||||
// package, unless they represent fields or methods,
|
||||
// or package-level types.
|
||||
// (Facts about packages, and other package-level
|
||||
// objects, are only obtained from direct imports so
|
||||
// they needn't be reexported.)
|
||||
//
|
||||
// This is analogous to the pruning done by "deep"
|
||||
// export data for types, but not as precise because
|
||||
// we aren't careful about which structs or methods
|
||||
// we rexport: it should be only those referenced
|
||||
// from the API of s.pkg.
|
||||
// TODO(adonovan): opt: be more precise. e.g.
|
||||
// intersect with the set of objects computed by
|
||||
// importMap(s.pkg.Imports()).
|
||||
// TODO(adonovan): opt: implement "shallow" facts.
|
||||
if k.pkg != s.pkg {
|
||||
if k.obj == nil {
|
||||
continue // imported package fact
|
||||
}
|
||||
if _, isType := k.obj.(*types.TypeName); !isType &&
|
||||
k.obj.Parent() == k.obj.Pkg().Scope() {
|
||||
continue // imported fact about package-level non-type object
|
||||
}
|
||||
}
|
||||
|
||||
var object objectpath.Path
|
||||
if k.obj != nil {
|
||||
path, err := encoder.For(k.obj)
|
||||
if err != nil {
|
||||
if debug {
|
||||
log.Printf("discarding fact %s about %s\n", fact, k.obj)
|
||||
}
|
||||
continue // object not accessible from package API; discard fact
|
||||
}
|
||||
object = path
|
||||
}
|
||||
gobFacts = append(gobFacts, gobFact{
|
||||
PkgPath: k.pkg.Path(),
|
||||
Object: object,
|
||||
Fact: fact,
|
||||
})
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
// Sort facts by (package, object, type) for determinism.
|
||||
sort.Slice(gobFacts, func(i, j int) bool {
|
||||
x, y := gobFacts[i], gobFacts[j]
|
||||
if x.PkgPath != y.PkgPath {
|
||||
return x.PkgPath < y.PkgPath
|
||||
}
|
||||
if x.Object != y.Object {
|
||||
return x.Object < y.Object
|
||||
}
|
||||
tx := reflect.TypeOf(x.Fact)
|
||||
ty := reflect.TypeOf(y.Fact)
|
||||
if tx != ty {
|
||||
return tx.String() < ty.String()
|
||||
}
|
||||
return false // equal
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
if len(gobFacts) > 0 {
|
||||
if err := gob.NewEncoder(&buf).Encode(gobFacts); err != nil {
|
||||
// Fact encoding should never fail. Identify the culprit.
|
||||
for _, gf := range gobFacts {
|
||||
if err := gob.NewEncoder(io.Discard).Encode(gf); err != nil {
|
||||
fact := gf.Fact
|
||||
pkgpath := reflect.TypeOf(fact).Elem().PkgPath()
|
||||
log.Panicf("internal error: gob encoding of analysis fact %s failed: %v; please report a bug against fact %T in package %q",
|
||||
fact, err, fact, pkgpath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Printf("package %q: encode %d facts, %d bytes\n",
|
||||
s.pkg.Path(), len(gobFacts), buf.Len())
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// String is provided only for debugging, and must not be called
|
||||
// concurrent with any Import/Export method.
|
||||
func (s *Set) String() string {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("{")
|
||||
for k, f := range s.m {
|
||||
if buf.Len() > 1 {
|
||||
buf.WriteString(", ")
|
||||
}
|
||||
if k.obj != nil {
|
||||
buf.WriteString(k.obj.String())
|
||||
} else {
|
||||
buf.WriteString(k.pkg.Path())
|
||||
}
|
||||
fmt.Fprintf(&buf, ": %v", f)
|
||||
}
|
||||
buf.WriteString("}")
|
||||
return buf.String()
|
||||
}
|
||||
@@ -0,0 +1,560 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package facts_test
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/go/analysis/analysistest"
|
||||
"golang.org/x/tools/go/packages"
|
||||
"golang.org/x/tools/internal/aliases"
|
||||
"golang.org/x/tools/internal/facts"
|
||||
"golang.org/x/tools/internal/testenv"
|
||||
)
|
||||
|
||||
type myFact struct {
|
||||
S string
|
||||
}
|
||||
|
||||
func (f *myFact) String() string { return fmt.Sprintf("myFact(%s)", f.S) }
|
||||
func (f *myFact) AFact() {}
|
||||
|
||||
func init() {
|
||||
gob.Register(new(myFact))
|
||||
}
|
||||
|
||||
func TestEncodeDecode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
typeparams bool // requires typeparams to be enabled
|
||||
files map[string]string
|
||||
plookups []pkgLookups // see testEncodeDecode for details
|
||||
}{
|
||||
{
|
||||
name: "loading-order",
|
||||
// c -> b -> a, a2
|
||||
// c does not directly depend on a, but it indirectly uses a.T.
|
||||
//
|
||||
// Package a2 is never loaded directly so it is incomplete.
|
||||
//
|
||||
// We use only types in this example because we rely on
|
||||
// types.Eval to resolve the lookup expressions, and it only
|
||||
// works for types. This is a definite gap in the typechecker API.
|
||||
files: map[string]string{
|
||||
"a/a.go": `package a; type A int; type T int`,
|
||||
"a2/a.go": `package a2; type A2 int; type Unneeded int`,
|
||||
"b/b.go": `package b; import ("a"; "a2"); type B chan a2.A2; type F func() a.T`,
|
||||
"c/c.go": `package c; import "b"; type C []b.B`,
|
||||
},
|
||||
// In the following table, we analyze packages (a, b, c) in order,
|
||||
// look up various objects accessible within each package,
|
||||
// and see if they have a fact. The "analysis" exports a fact
|
||||
// for every object at package level.
|
||||
//
|
||||
// Note: Loop iterations are not independent test cases;
|
||||
// order matters, as we populate factmap.
|
||||
plookups: []pkgLookups{
|
||||
{"a", []lookup{
|
||||
{"A", "myFact(a.A)"},
|
||||
}},
|
||||
{"b", []lookup{
|
||||
{"a.A", "myFact(a.A)"},
|
||||
{"a.T", "myFact(a.T)"},
|
||||
{"B", "myFact(b.B)"},
|
||||
{"F", "myFact(b.F)"},
|
||||
{"F(nil)()", "myFact(a.T)"}, // (result type of b.F)
|
||||
}},
|
||||
{"c", []lookup{
|
||||
{"b.B", "myFact(b.B)"},
|
||||
{"b.F", "myFact(b.F)"},
|
||||
{"b.F(nil)()", "myFact(a.T)"},
|
||||
{"C", "myFact(c.C)"},
|
||||
{"C{}[0]", "myFact(b.B)"},
|
||||
{"<-(C{}[0])", "no fact"}, // object but no fact (we never "analyze" a2)
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "underlying",
|
||||
// c->b->a
|
||||
// c does not import a directly or use any of its types, but it does use
|
||||
// the types within a indirectly. c.q has the type a.a so package a should
|
||||
// be included by importMap.
|
||||
files: map[string]string{
|
||||
"a/a.go": `package a; type a int; type T *a`,
|
||||
"b/b.go": `package b; import "a"; type B a.T`,
|
||||
"c/c.go": `package c; import "b"; type C b.B; var q = *C(nil)`,
|
||||
},
|
||||
plookups: []pkgLookups{
|
||||
{"a", []lookup{
|
||||
{"a", "myFact(a.a)"},
|
||||
{"T", "myFact(a.T)"},
|
||||
}},
|
||||
{"b", []lookup{
|
||||
{"B", "myFact(b.B)"},
|
||||
{"B(nil)", "myFact(b.B)"},
|
||||
{"*(B(nil))", "myFact(a.a)"},
|
||||
}},
|
||||
{"c", []lookup{
|
||||
{"C", "myFact(c.C)"},
|
||||
{"C(nil)", "myFact(c.C)"},
|
||||
{"*C(nil)", "myFact(a.a)"},
|
||||
{"q", "myFact(a.a)"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "methods",
|
||||
// c->b->a
|
||||
// c does not import a directly or use any of its types, but it does use
|
||||
// the types within a indirectly via a method.
|
||||
files: map[string]string{
|
||||
"a/a.go": `package a; type T int`,
|
||||
"b/b.go": `package b; import "a"; type B struct{}; func (_ B) M() a.T { return 0 }`,
|
||||
"c/c.go": `package c; import "b"; var C b.B`,
|
||||
},
|
||||
plookups: []pkgLookups{
|
||||
{"a", []lookup{
|
||||
{"T", "myFact(a.T)"},
|
||||
}},
|
||||
{"b", []lookup{
|
||||
{"B{}", "myFact(b.B)"},
|
||||
{"B{}.M()", "myFact(a.T)"},
|
||||
}},
|
||||
{"c", []lookup{
|
||||
{"C", "myFact(b.B)"},
|
||||
{"C.M()", "myFact(a.T)"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "globals",
|
||||
files: map[string]string{
|
||||
"a/a.go": `package a;
|
||||
type T1 int
|
||||
type T2 int
|
||||
type T3 int
|
||||
type T4 int
|
||||
type T5 int
|
||||
type K int; type V string
|
||||
`,
|
||||
"b/b.go": `package b
|
||||
import "a"
|
||||
var (
|
||||
G1 []a.T1
|
||||
G2 [7]a.T2
|
||||
G3 chan a.T3
|
||||
G4 *a.T4
|
||||
G5 struct{ F a.T5 }
|
||||
G6 map[a.K]a.V
|
||||
)
|
||||
`,
|
||||
"c/c.go": `package c; import "b";
|
||||
var (
|
||||
v1 = b.G1
|
||||
v2 = b.G2
|
||||
v3 = b.G3
|
||||
v4 = b.G4
|
||||
v5 = b.G5
|
||||
v6 = b.G6
|
||||
)
|
||||
`,
|
||||
},
|
||||
plookups: []pkgLookups{
|
||||
{"a", []lookup{}},
|
||||
{"b", []lookup{}},
|
||||
{"c", []lookup{
|
||||
{"v1[0]", "myFact(a.T1)"},
|
||||
{"v2[0]", "myFact(a.T2)"},
|
||||
{"<-v3", "myFact(a.T3)"},
|
||||
{"*v4", "myFact(a.T4)"},
|
||||
{"v5.F", "myFact(a.T5)"},
|
||||
{"v6[0]", "myFact(a.V)"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "typeparams",
|
||||
typeparams: true,
|
||||
files: map[string]string{
|
||||
"a/a.go": `package a
|
||||
type T1 int
|
||||
type T2 int
|
||||
type T3 interface{Foo()}
|
||||
type T4 int
|
||||
type T5 int
|
||||
type T6 interface{Foo()}
|
||||
`,
|
||||
"b/b.go": `package b
|
||||
import "a"
|
||||
type N1[T a.T1|int8] func() T
|
||||
type N2[T any] struct{ F T }
|
||||
type N3[T a.T3] func() T
|
||||
type N4[T a.T4|int8] func() T
|
||||
type N5[T interface{Bar() a.T5} ] func() T
|
||||
|
||||
type t5 struct{}; func (t5) Bar() a.T5 { return 0 }
|
||||
|
||||
var G1 N1[a.T1]
|
||||
var G2 func() N2[a.T2]
|
||||
var G3 N3[a.T3]
|
||||
var G4 N4[a.T4]
|
||||
var G5 N5[t5]
|
||||
|
||||
func F6[T a.T6]() T { var x T; return x }
|
||||
`,
|
||||
"c/c.go": `package c; import "b";
|
||||
var (
|
||||
v1 = b.G1
|
||||
v2 = b.G2
|
||||
v3 = b.G3
|
||||
v4 = b.G4
|
||||
v5 = b.G5
|
||||
v6 = b.F6[t6]
|
||||
)
|
||||
|
||||
type t6 struct{}; func (t6) Foo() {}
|
||||
`,
|
||||
},
|
||||
plookups: []pkgLookups{
|
||||
{"a", []lookup{}},
|
||||
{"b", []lookup{}},
|
||||
{"c", []lookup{
|
||||
{"v1", "myFact(b.N1)"},
|
||||
{"v1()", "myFact(a.T1)"},
|
||||
{"v2()", "myFact(b.N2)"},
|
||||
{"v2().F", "myFact(a.T2)"},
|
||||
{"v3", "myFact(b.N3)"},
|
||||
{"v4", "myFact(b.N4)"},
|
||||
{"v4()", "myFact(a.T4)"},
|
||||
{"v5", "myFact(b.N5)"},
|
||||
{"v5()", "myFact(b.t5)"},
|
||||
{"v6()", "myFact(c.t6)"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
test := tests[i]
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testEncodeDecode(t, test.files, test.plookups)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type lookup struct {
|
||||
objexpr string
|
||||
want string
|
||||
}
|
||||
|
||||
type pkgLookups struct {
|
||||
path string
|
||||
lookups []lookup
|
||||
}
|
||||
|
||||
// testEncodeDecode tests fact encoding and decoding and simulates how package facts
|
||||
// are passed during analysis. It operates on a group of Go file contents. Then
|
||||
// for each <package, []lookup> in tests it does the following:
|
||||
// 1. loads and type checks the package,
|
||||
// 2. calls (*facts.Decoder).Decode to load the facts exported by its imports,
|
||||
// 3. exports a myFact Fact for all of package level objects,
|
||||
// 4. For each lookup for the current package:
|
||||
// 4.a) lookup the types.Object for a Go source expression in the current package
|
||||
// (or confirms one is not expected want=="no object"),
|
||||
// 4.b) finds a Fact for the object (or confirms one is not expected want=="no fact"),
|
||||
// 4.c) compares the content of the Fact to want.
|
||||
// 5. encodes the Facts of the package.
|
||||
//
|
||||
// Note: tests are not independent test cases; order matters (as does a package being
|
||||
// skipped). It changes what Facts can be imported.
|
||||
//
|
||||
// Failures are reported on t.
|
||||
func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) {
|
||||
dir, cleanup, err := analysistest.WriteFiles(files)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// factmap represents the passing of encoded facts from one
|
||||
// package to another. In practice one would use the file system.
|
||||
factmap := make(map[string][]byte)
|
||||
read := func(pkgPath string) ([]byte, error) { return factmap[pkgPath], nil }
|
||||
|
||||
// Analyze packages in order, look up various objects accessible within
|
||||
// each package, and see if they have a fact. The "analysis" exports a
|
||||
// fact for every object at package level.
|
||||
//
|
||||
// Note: Loop iterations are not independent test cases;
|
||||
// order matters, as we populate factmap.
|
||||
for _, test := range tests {
|
||||
// load package
|
||||
pkg, err := load(t, dir, test.path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// decode
|
||||
facts, err := facts.NewDecoder(pkg).Decode(read)
|
||||
if err != nil {
|
||||
t.Fatalf("Decode failed: %v", err)
|
||||
}
|
||||
t.Logf("decode %s facts = %v", pkg.Path(), facts) // show all facts
|
||||
|
||||
// export
|
||||
// (one fact for each package-level object)
|
||||
for _, name := range pkg.Scope().Names() {
|
||||
obj := pkg.Scope().Lookup(name)
|
||||
fact := &myFact{obj.Pkg().Name() + "." + obj.Name()}
|
||||
facts.ExportObjectFact(obj, fact)
|
||||
}
|
||||
t.Logf("exported %s facts = %v", pkg.Path(), facts) // show all facts
|
||||
|
||||
// import
|
||||
// (after export, because an analyzer may import its own facts)
|
||||
for _, lookup := range test.lookups {
|
||||
fact := new(myFact)
|
||||
var got string
|
||||
if obj := find(pkg, lookup.objexpr); obj == nil {
|
||||
got = "no object"
|
||||
} else if facts.ImportObjectFact(obj, fact) {
|
||||
got = fact.String()
|
||||
} else {
|
||||
got = "no fact"
|
||||
}
|
||||
if got != lookup.want {
|
||||
t.Errorf("in %s, ImportObjectFact(%s, %T) = %s, want %s",
|
||||
pkg.Path(), lookup.objexpr, fact, got, lookup.want)
|
||||
}
|
||||
}
|
||||
|
||||
// encode
|
||||
factmap[pkg.Path()] = facts.Encode()
|
||||
}
|
||||
}
|
||||
|
||||
func find(p *types.Package, expr string) types.Object {
|
||||
// types.Eval only allows us to compute a TypeName object for an expression.
|
||||
// TODO(adonovan): support other expressions that denote an object:
|
||||
// - an identifier (or qualified ident) for a func, const, or var
|
||||
// - new(T).f for a field or method
|
||||
// I've added CheckExpr in https://go-review.googlesource.com/c/go/+/144677.
|
||||
// If that becomes available, use it.
|
||||
|
||||
// Choose an arbitrary position within the (single-file) package
|
||||
// so that we are within the scope of its import declarations.
|
||||
somepos := p.Scope().Lookup(p.Scope().Names()[0]).Pos()
|
||||
tv, err := types.Eval(token.NewFileSet(), p, somepos, expr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if n, ok := aliases.Unalias(tv.Type).(*types.Named); ok {
|
||||
return n.Obj()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func load(t *testing.T, dir string, path string) (*types.Package, error) {
|
||||
cfg := &packages.Config{
|
||||
Mode: packages.LoadSyntax,
|
||||
Dir: dir,
|
||||
Env: append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"),
|
||||
}
|
||||
testenv.NeedsGoPackagesEnv(t, cfg.Env)
|
||||
pkgs, err := packages.Load(cfg, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if packages.PrintErrors(pkgs) > 0 {
|
||||
return nil, fmt.Errorf("packages had errors")
|
||||
}
|
||||
if len(pkgs) == 0 {
|
||||
return nil, fmt.Errorf("no package matched %s", path)
|
||||
}
|
||||
return pkgs[0].Types, nil
|
||||
}
|
||||
|
||||
type otherFact struct {
|
||||
S string
|
||||
}
|
||||
|
||||
func (f *otherFact) String() string { return fmt.Sprintf("otherFact(%s)", f.S) }
|
||||
func (f *otherFact) AFact() {}
|
||||
|
||||
func TestFactFilter(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"a/a.go": `package a; type A int`,
|
||||
}
|
||||
dir, cleanup, err := analysistest.WriteFiles(files)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
pkg, err := load(t, dir, "a")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
obj := pkg.Scope().Lookup("A")
|
||||
s, err := facts.NewDecoder(pkg).Decode(func(pkgPath string) ([]byte, error) { return nil, nil })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.ExportObjectFact(obj, &myFact{"good object fact"})
|
||||
s.ExportPackageFact(&myFact{"good package fact"})
|
||||
s.ExportObjectFact(obj, &otherFact{"bad object fact"})
|
||||
s.ExportPackageFact(&otherFact{"bad package fact"})
|
||||
|
||||
filter := map[reflect.Type]bool{
|
||||
reflect.TypeOf(&myFact{}): true,
|
||||
}
|
||||
|
||||
pkgFacts := s.AllPackageFacts(filter)
|
||||
wantPkgFacts := `[{package a ("a") myFact(good package fact)}]`
|
||||
if got := fmt.Sprintf("%v", pkgFacts); got != wantPkgFacts {
|
||||
t.Errorf("AllPackageFacts: got %v, want %v", got, wantPkgFacts)
|
||||
}
|
||||
|
||||
objFacts := s.AllObjectFacts(filter)
|
||||
wantObjFacts := "[{type a.A int myFact(good object fact)}]"
|
||||
if got := fmt.Sprintf("%v", objFacts); got != wantObjFacts {
|
||||
t.Errorf("AllObjectFacts: got %v, want %v", got, wantObjFacts)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMalformed checks that facts can be encoded and decoded *despite*
|
||||
// types.Config.Check returning an error. Importing facts is expected to
|
||||
// happen when Analyzers have RunDespiteErrors set to true. So this
|
||||
// needs to robust, e.g. no infinite loops.
|
||||
func TestMalformed(t *testing.T) {
|
||||
var findPkg func(*types.Package, string) *types.Package
|
||||
findPkg = func(p *types.Package, name string) *types.Package {
|
||||
if p.Name() == name {
|
||||
return p
|
||||
}
|
||||
for _, o := range p.Imports() {
|
||||
if f := findPkg(o, name); f != nil {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type pkgTest struct {
|
||||
content string
|
||||
err string // if non-empty, expected substring of err.Error() from conf.Check().
|
||||
wants map[string]string // package path to expected name
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
pkgs []pkgTest
|
||||
}{
|
||||
{
|
||||
name: "initialization-cycle",
|
||||
pkgs: []pkgTest{
|
||||
// Notation: myFact(a.[N]) means: package a has members {N}.
|
||||
{
|
||||
content: `package a; type N[T any] struct { F *N[N[T]] }`,
|
||||
err: "instantiation cycle:",
|
||||
wants: map[string]string{"a": "myFact(a.[N])", "b": "no package", "c": "no package"},
|
||||
},
|
||||
{
|
||||
content: `package b; import "a"; type B a.N[int]`,
|
||||
wants: map[string]string{"a": "myFact(a.[N])", "b": "myFact(b.[B])", "c": "no package"},
|
||||
},
|
||||
{
|
||||
content: `package c; import "b"; var C b.B`,
|
||||
wants: map[string]string{"a": "no fact", "b": "myFact(b.[B])", "c": "myFact(c.[C])"},
|
||||
// package fact myFact(a.[N]) not reexported
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
test := tests[i]
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// setup for test wide variables.
|
||||
packages := make(map[string]*types.Package)
|
||||
conf := types.Config{
|
||||
Importer: closure(packages),
|
||||
Error: func(err error) {}, // do not stop on first type checking error
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
factmap := make(map[string][]byte)
|
||||
read := func(pkgPath string) ([]byte, error) { return factmap[pkgPath], nil }
|
||||
|
||||
// Processes the pkgs in order. For package, export a package fact,
|
||||
// and use this fact to verify which package facts are reachable via Decode.
|
||||
// We allow for packages to have type checking errors.
|
||||
for i, pkgTest := range test.pkgs {
|
||||
// parse
|
||||
f, err := parser.ParseFile(fset, fmt.Sprintf("%d.go", i), pkgTest.content, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// typecheck
|
||||
pkg, err := conf.Check(f.Name.Name, fset, []*ast.File{f}, nil)
|
||||
var got string
|
||||
if err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
if !strings.Contains(got, pkgTest.err) {
|
||||
t.Fatalf("%s: type checking error %q did not match pattern %q", pkg.Path(), err.Error(), pkgTest.err)
|
||||
}
|
||||
packages[pkg.Path()] = pkg
|
||||
|
||||
// decode facts
|
||||
facts, err := facts.NewDecoder(pkg).Decode(read)
|
||||
if err != nil {
|
||||
t.Fatalf("Decode failed: %v", err)
|
||||
}
|
||||
|
||||
// export facts
|
||||
fact := &myFact{fmt.Sprintf("%s.%s", pkg.Name(), pkg.Scope().Names())}
|
||||
facts.ExportPackageFact(fact)
|
||||
|
||||
// import facts
|
||||
for other, want := range pkgTest.wants {
|
||||
fact := new(myFact)
|
||||
var got string
|
||||
if found := findPkg(pkg, other); found == nil {
|
||||
got = "no package"
|
||||
} else if facts.ImportPackageFact(found, fact) {
|
||||
got = fact.String()
|
||||
} else {
|
||||
got = "no fact"
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("in %s, ImportPackageFact(%s, %T) = %s, want %s",
|
||||
pkg.Path(), other, fact, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// encode facts
|
||||
factmap[pkg.Path()] = facts.Encode()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type closure map[string]*types.Package
|
||||
|
||||
func (c closure) Import(path string) (*types.Package, error) { return c[path], nil }
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package facts
|
||||
|
||||
import (
|
||||
"go/types"
|
||||
|
||||
"golang.org/x/tools/internal/aliases"
|
||||
)
|
||||
|
||||
// importMap computes the import map for a package by traversing the
|
||||
// entire exported API each of its imports.
|
||||
//
|
||||
// This is a workaround for the fact that we cannot access the map used
|
||||
// internally by the types.Importer returned by go/importer. The entries
|
||||
// in this map are the packages and objects that may be relevant to the
|
||||
// current analysis unit.
|
||||
//
|
||||
// Packages in the map that are only indirectly imported may be
|
||||
// incomplete (!pkg.Complete()).
|
||||
//
|
||||
// This function scales very poorly with packages' transitive object
|
||||
// references, which can be more than a million for each package near
|
||||
// the top of a large project. (This was a significant contributor to
|
||||
// #60621.)
|
||||
// TODO(adonovan): opt: compute this information more efficiently
|
||||
// by obtaining it from the internals of the gcexportdata decoder.
|
||||
func importMap(imports []*types.Package) map[string]*types.Package {
|
||||
objects := make(map[types.Object]bool)
|
||||
typs := make(map[types.Type]bool) // Named and TypeParam
|
||||
packages := make(map[string]*types.Package)
|
||||
|
||||
var addObj func(obj types.Object)
|
||||
var addType func(T types.Type)
|
||||
|
||||
addObj = func(obj types.Object) {
|
||||
if !objects[obj] {
|
||||
objects[obj] = true
|
||||
addType(obj.Type())
|
||||
if pkg := obj.Pkg(); pkg != nil {
|
||||
packages[pkg.Path()] = pkg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addType = func(T types.Type) {
|
||||
switch T := T.(type) {
|
||||
case *aliases.Alias:
|
||||
addType(aliases.Unalias(T))
|
||||
case *types.Basic:
|
||||
// nop
|
||||
case *types.Named:
|
||||
// Remove infinite expansions of *types.Named by always looking at the origin.
|
||||
// Some named types with type parameters [that will not type check] have
|
||||
// infinite expansions:
|
||||
// type N[T any] struct { F *N[N[T]] }
|
||||
// importMap() is called on such types when Analyzer.RunDespiteErrors is true.
|
||||
T = T.Origin()
|
||||
if !typs[T] {
|
||||
typs[T] = true
|
||||
addObj(T.Obj())
|
||||
addType(T.Underlying())
|
||||
for i := 0; i < T.NumMethods(); i++ {
|
||||
addObj(T.Method(i))
|
||||
}
|
||||
if tparams := T.TypeParams(); tparams != nil {
|
||||
for i := 0; i < tparams.Len(); i++ {
|
||||
addType(tparams.At(i))
|
||||
}
|
||||
}
|
||||
if targs := T.TypeArgs(); targs != nil {
|
||||
for i := 0; i < targs.Len(); i++ {
|
||||
addType(targs.At(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
case *types.Pointer:
|
||||
addType(T.Elem())
|
||||
case *types.Slice:
|
||||
addType(T.Elem())
|
||||
case *types.Array:
|
||||
addType(T.Elem())
|
||||
case *types.Chan:
|
||||
addType(T.Elem())
|
||||
case *types.Map:
|
||||
addType(T.Key())
|
||||
addType(T.Elem())
|
||||
case *types.Signature:
|
||||
addType(T.Params())
|
||||
addType(T.Results())
|
||||
if tparams := T.TypeParams(); tparams != nil {
|
||||
for i := 0; i < tparams.Len(); i++ {
|
||||
addType(tparams.At(i))
|
||||
}
|
||||
}
|
||||
case *types.Struct:
|
||||
for i := 0; i < T.NumFields(); i++ {
|
||||
addObj(T.Field(i))
|
||||
}
|
||||
case *types.Tuple:
|
||||
for i := 0; i < T.Len(); i++ {
|
||||
addObj(T.At(i))
|
||||
}
|
||||
case *types.Interface:
|
||||
for i := 0; i < T.NumMethods(); i++ {
|
||||
addObj(T.Method(i))
|
||||
}
|
||||
for i := 0; i < T.NumEmbeddeds(); i++ {
|
||||
addType(T.EmbeddedType(i)) // walk Embedded for implicits
|
||||
}
|
||||
case *types.Union:
|
||||
for i := 0; i < T.Len(); i++ {
|
||||
addType(T.Term(i).Type())
|
||||
}
|
||||
case *types.TypeParam:
|
||||
if !typs[T] {
|
||||
typs[T] = true
|
||||
addObj(T.Obj())
|
||||
addType(T.Constraint())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, imp := range imports {
|
||||
packages[imp.Path()] = imp
|
||||
|
||||
scope := imp.Scope()
|
||||
for _, name := range scope.Names() {
|
||||
addObj(scope.Lookup(name))
|
||||
}
|
||||
}
|
||||
|
||||
return packages
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package fakenet
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewConn returns a net.Conn built on top of the supplied reader and writer.
|
||||
// It decouples the read and write on the conn from the underlying stream
|
||||
// to enable Close to abort ones that are in progress.
|
||||
// It's primary use is to fake a network connection from stdin and stdout.
|
||||
func NewConn(name string, in io.ReadCloser, out io.WriteCloser) net.Conn {
|
||||
c := &fakeConn{
|
||||
name: name,
|
||||
reader: newFeeder(in.Read),
|
||||
writer: newFeeder(out.Write),
|
||||
in: in,
|
||||
out: out,
|
||||
}
|
||||
go c.reader.run()
|
||||
go c.writer.run()
|
||||
return c
|
||||
}
|
||||
|
||||
type fakeConn struct {
|
||||
name string
|
||||
reader *connFeeder
|
||||
writer *connFeeder
|
||||
in io.ReadCloser
|
||||
out io.WriteCloser
|
||||
}
|
||||
|
||||
type fakeAddr string
|
||||
|
||||
// connFeeder serializes calls to the source function (io.Reader.Read or
|
||||
// io.Writer.Write) by delegating them to a channel. This also allows calls to
|
||||
// be intercepted when the connection is closed, and cancelled early if the
|
||||
// connection is closed while the calls are still outstanding.
|
||||
type connFeeder struct {
|
||||
source func([]byte) (int, error)
|
||||
input chan []byte
|
||||
result chan feedResult
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
type feedResult struct {
|
||||
n int
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *fakeConn) Close() error {
|
||||
c.reader.close()
|
||||
c.writer.close()
|
||||
c.in.Close()
|
||||
c.out.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *fakeConn) Read(b []byte) (n int, err error) { return c.reader.do(b) }
|
||||
func (c *fakeConn) Write(b []byte) (n int, err error) { return c.writer.do(b) }
|
||||
func (c *fakeConn) LocalAddr() net.Addr { return fakeAddr(c.name) }
|
||||
func (c *fakeConn) RemoteAddr() net.Addr { return fakeAddr(c.name) }
|
||||
func (c *fakeConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (c *fakeConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (c *fakeConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
func (a fakeAddr) Network() string { return "fake" }
|
||||
func (a fakeAddr) String() string { return string(a) }
|
||||
|
||||
func newFeeder(source func([]byte) (int, error)) *connFeeder {
|
||||
return &connFeeder{
|
||||
source: source,
|
||||
input: make(chan []byte),
|
||||
result: make(chan feedResult),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *connFeeder) close() {
|
||||
f.mu.Lock()
|
||||
if !f.closed {
|
||||
f.closed = true
|
||||
close(f.done)
|
||||
}
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
func (f *connFeeder) do(b []byte) (n int, err error) {
|
||||
// send the request to the worker
|
||||
select {
|
||||
case f.input <- b:
|
||||
case <-f.done:
|
||||
return 0, io.EOF
|
||||
}
|
||||
// get the result from the worker
|
||||
select {
|
||||
case r := <-f.result:
|
||||
return r.n, r.err
|
||||
case <-f.done:
|
||||
return 0, io.EOF
|
||||
}
|
||||
}
|
||||
|
||||
func (f *connFeeder) run() {
|
||||
var b []byte
|
||||
for {
|
||||
// wait for an input request
|
||||
select {
|
||||
case b = <-f.input:
|
||||
case <-f.done:
|
||||
return
|
||||
}
|
||||
// invoke the underlying method
|
||||
n, err := f.source(b)
|
||||
// send the result back to the requester
|
||||
select {
|
||||
case f.result <- feedResult{n: n, err: err}:
|
||||
case <-f.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gcimporter_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/internal/aliases"
|
||||
"golang.org/x/tools/internal/gcimporter"
|
||||
)
|
||||
|
||||
var isRace = false
|
||||
|
||||
func fileLine(fset *token.FileSet, obj types.Object) string {
|
||||
posn := fset.Position(obj.Pos())
|
||||
filename := filepath.Clean(strings.ReplaceAll(posn.Filename, "$GOROOT", runtime.GOROOT()))
|
||||
return fmt.Sprintf("%s:%d", filename, posn.Line)
|
||||
}
|
||||
|
||||
func equalType(x, y types.Type) error {
|
||||
x = aliases.Unalias(x)
|
||||
y = aliases.Unalias(y)
|
||||
if reflect.TypeOf(x) != reflect.TypeOf(y) {
|
||||
return fmt.Errorf("unequal kinds: %T vs %T", x, y)
|
||||
}
|
||||
switch x := x.(type) {
|
||||
case *types.Interface:
|
||||
y := y.(*types.Interface)
|
||||
// TODO(gri): enable separate emission of Embedded interfaces
|
||||
// and ExplicitMethods then use this logic.
|
||||
// if x.NumEmbeddeds() != y.NumEmbeddeds() {
|
||||
// return fmt.Errorf("unequal number of embedded interfaces: %d vs %d",
|
||||
// x.NumEmbeddeds(), y.NumEmbeddeds())
|
||||
// }
|
||||
// for i := 0; i < x.NumEmbeddeds(); i++ {
|
||||
// xi := x.Embedded(i)
|
||||
// yi := y.Embedded(i)
|
||||
// if xi.String() != yi.String() {
|
||||
// return fmt.Errorf("mismatched %th embedded interface: %s vs %s",
|
||||
// i, xi, yi)
|
||||
// }
|
||||
// }
|
||||
// if x.NumExplicitMethods() != y.NumExplicitMethods() {
|
||||
// return fmt.Errorf("unequal methods: %d vs %d",
|
||||
// x.NumExplicitMethods(), y.NumExplicitMethods())
|
||||
// }
|
||||
// for i := 0; i < x.NumExplicitMethods(); i++ {
|
||||
// xm := x.ExplicitMethod(i)
|
||||
// ym := y.ExplicitMethod(i)
|
||||
// if xm.Name() != ym.Name() {
|
||||
// return fmt.Errorf("mismatched %th method: %s vs %s", i, xm, ym)
|
||||
// }
|
||||
// if err := equalType(xm.Type(), ym.Type()); err != nil {
|
||||
// return fmt.Errorf("mismatched %s method: %s", xm.Name(), err)
|
||||
// }
|
||||
// }
|
||||
if x.NumMethods() != y.NumMethods() {
|
||||
return fmt.Errorf("unequal methods: %d vs %d",
|
||||
x.NumMethods(), y.NumMethods())
|
||||
}
|
||||
for i := 0; i < x.NumMethods(); i++ {
|
||||
xm := x.Method(i)
|
||||
ym := y.Method(i)
|
||||
if xm.Name() != ym.Name() {
|
||||
return fmt.Errorf("mismatched %dth method: %s vs %s", i, xm, ym)
|
||||
}
|
||||
if err := equalType(xm.Type(), ym.Type()); err != nil {
|
||||
return fmt.Errorf("mismatched %s method: %s", xm.Name(), err)
|
||||
}
|
||||
}
|
||||
// Constraints are handled explicitly in the *TypeParam case below, so we
|
||||
// don't yet need to consider embeddeds here.
|
||||
// TODO(rfindley): consider the type set here.
|
||||
case *types.Array:
|
||||
y := y.(*types.Array)
|
||||
if x.Len() != y.Len() {
|
||||
return fmt.Errorf("unequal array lengths: %d vs %d", x.Len(), y.Len())
|
||||
}
|
||||
if err := equalType(x.Elem(), y.Elem()); err != nil {
|
||||
return fmt.Errorf("array elements: %s", err)
|
||||
}
|
||||
case *types.Basic:
|
||||
y := y.(*types.Basic)
|
||||
if x.Kind() != y.Kind() {
|
||||
return fmt.Errorf("unequal basic types: %s vs %s", x, y)
|
||||
}
|
||||
case *types.Chan:
|
||||
y := y.(*types.Chan)
|
||||
if x.Dir() != y.Dir() {
|
||||
return fmt.Errorf("unequal channel directions: %d vs %d", x.Dir(), y.Dir())
|
||||
}
|
||||
if err := equalType(x.Elem(), y.Elem()); err != nil {
|
||||
return fmt.Errorf("channel elements: %s", err)
|
||||
}
|
||||
case *types.Map:
|
||||
y := y.(*types.Map)
|
||||
if err := equalType(x.Key(), y.Key()); err != nil {
|
||||
return fmt.Errorf("map keys: %s", err)
|
||||
}
|
||||
if err := equalType(x.Elem(), y.Elem()); err != nil {
|
||||
return fmt.Errorf("map values: %s", err)
|
||||
}
|
||||
case *types.Named:
|
||||
y := y.(*types.Named)
|
||||
return cmpNamed(x, y)
|
||||
case *types.Pointer:
|
||||
y := y.(*types.Pointer)
|
||||
if err := equalType(x.Elem(), y.Elem()); err != nil {
|
||||
return fmt.Errorf("pointer elements: %s", err)
|
||||
}
|
||||
case *types.Signature:
|
||||
y := y.(*types.Signature)
|
||||
if err := equalType(x.Params(), y.Params()); err != nil {
|
||||
return fmt.Errorf("parameters: %s", err)
|
||||
}
|
||||
if err := equalType(x.Results(), y.Results()); err != nil {
|
||||
return fmt.Errorf("results: %s", err)
|
||||
}
|
||||
if x.Variadic() != y.Variadic() {
|
||||
return fmt.Errorf("unequal variadicity: %t vs %t",
|
||||
x.Variadic(), y.Variadic())
|
||||
}
|
||||
if (x.Recv() != nil) != (y.Recv() != nil) {
|
||||
return fmt.Errorf("unequal receivers: %s vs %s", x.Recv(), y.Recv())
|
||||
}
|
||||
if x.Recv() != nil {
|
||||
// TODO(adonovan): fix: this assertion fires for interface methods.
|
||||
// The type of the receiver of an interface method is a named type
|
||||
// if the Package was loaded from export data, or an unnamed (interface)
|
||||
// type if the Package was produced by type-checking ASTs.
|
||||
// if err := equalType(x.Recv().Type(), y.Recv().Type()); err != nil {
|
||||
// return fmt.Errorf("receiver: %s", err)
|
||||
// }
|
||||
}
|
||||
if err := equalTypeParams(x.TypeParams(), y.TypeParams()); err != nil {
|
||||
return fmt.Errorf("type params: %s", err)
|
||||
}
|
||||
if err := equalTypeParams(x.RecvTypeParams(), y.RecvTypeParams()); err != nil {
|
||||
return fmt.Errorf("recv type params: %s", err)
|
||||
}
|
||||
case *types.Slice:
|
||||
y := y.(*types.Slice)
|
||||
if err := equalType(x.Elem(), y.Elem()); err != nil {
|
||||
return fmt.Errorf("slice elements: %s", err)
|
||||
}
|
||||
case *types.Struct:
|
||||
y := y.(*types.Struct)
|
||||
if x.NumFields() != y.NumFields() {
|
||||
return fmt.Errorf("unequal struct fields: %d vs %d",
|
||||
x.NumFields(), y.NumFields())
|
||||
}
|
||||
for i := 0; i < x.NumFields(); i++ {
|
||||
xf := x.Field(i)
|
||||
yf := y.Field(i)
|
||||
if xf.Name() != yf.Name() {
|
||||
return fmt.Errorf("mismatched fields: %s vs %s", xf, yf)
|
||||
}
|
||||
if err := equalType(xf.Type(), yf.Type()); err != nil {
|
||||
return fmt.Errorf("struct field %s: %s", xf.Name(), err)
|
||||
}
|
||||
if x.Tag(i) != y.Tag(i) {
|
||||
return fmt.Errorf("struct field %s has unequal tags: %q vs %q",
|
||||
xf.Name(), x.Tag(i), y.Tag(i))
|
||||
}
|
||||
}
|
||||
case *types.Tuple:
|
||||
y := y.(*types.Tuple)
|
||||
if x.Len() != y.Len() {
|
||||
return fmt.Errorf("unequal tuple lengths: %d vs %d", x.Len(), y.Len())
|
||||
}
|
||||
for i := 0; i < x.Len(); i++ {
|
||||
if err := equalType(x.At(i).Type(), y.At(i).Type()); err != nil {
|
||||
return fmt.Errorf("tuple element %d: %s", i, err)
|
||||
}
|
||||
}
|
||||
case *types.TypeParam:
|
||||
y := y.(*types.TypeParam)
|
||||
if x.String() != y.String() {
|
||||
return fmt.Errorf("unequal named types: %s vs %s", x, y)
|
||||
}
|
||||
// For now, just compare constraints by type string to short-circuit
|
||||
// cycles. We have to make interfaces explicit as export data currently
|
||||
// doesn't support marking interfaces as implicit.
|
||||
// TODO(rfindley): remove makeExplicit once export data contains an
|
||||
// implicit bit.
|
||||
xc := makeExplicit(x.Constraint()).String()
|
||||
yc := makeExplicit(y.Constraint()).String()
|
||||
if xc != yc {
|
||||
return fmt.Errorf("unequal constraints: %s vs %s", xc, yc)
|
||||
}
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected %T type", x))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cmpNamed compares two named types x and y, returning an error for any
|
||||
// discrepancies. It does not compare their underlying types.
|
||||
func cmpNamed(x, y *types.Named) error {
|
||||
xOrig := x.Origin()
|
||||
yOrig := y.Origin()
|
||||
if xOrig.String() != yOrig.String() {
|
||||
return fmt.Errorf("unequal named types: %s vs %s", x, y)
|
||||
}
|
||||
if err := equalTypeParams(x.TypeParams(), y.TypeParams()); err != nil {
|
||||
return fmt.Errorf("type parameters: %s", err)
|
||||
}
|
||||
if err := equalTypeArgs(x.TypeArgs(), y.TypeArgs()); err != nil {
|
||||
return fmt.Errorf("type arguments: %s", err)
|
||||
}
|
||||
if x.NumMethods() != y.NumMethods() {
|
||||
return fmt.Errorf("unequal methods: %d vs %d",
|
||||
x.NumMethods(), y.NumMethods())
|
||||
}
|
||||
// Unfortunately method sorting is not canonical, so sort before comparing.
|
||||
var xms, yms []*types.Func
|
||||
for i := 0; i < x.NumMethods(); i++ {
|
||||
xms = append(xms, x.Method(i))
|
||||
yms = append(yms, y.Method(i))
|
||||
}
|
||||
for _, ms := range [][]*types.Func{xms, yms} {
|
||||
sort.Slice(ms, func(i, j int) bool {
|
||||
return ms[i].Name() < ms[j].Name()
|
||||
})
|
||||
}
|
||||
for i, xm := range xms {
|
||||
ym := yms[i]
|
||||
if xm.Name() != ym.Name() {
|
||||
return fmt.Errorf("mismatched %dth method: %s vs %s", i, xm, ym)
|
||||
}
|
||||
// Calling equalType here leads to infinite recursion, so just compare
|
||||
// strings.
|
||||
if xm.String() != ym.String() {
|
||||
return fmt.Errorf("unequal methods: %s vs %s", x, y)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// makeExplicit returns an explicit version of typ, if typ is an implicit
|
||||
// interface. Otherwise it returns typ unmodified.
|
||||
func makeExplicit(typ types.Type) types.Type {
|
||||
if iface, _ := typ.(*types.Interface); iface != nil && iface.IsImplicit() {
|
||||
var methods []*types.Func
|
||||
for i := 0; i < iface.NumExplicitMethods(); i++ {
|
||||
methods = append(methods, iface.Method(i))
|
||||
}
|
||||
var embeddeds []types.Type
|
||||
for i := 0; i < iface.NumEmbeddeds(); i++ {
|
||||
embeddeds = append(embeddeds, iface.EmbeddedType(i))
|
||||
}
|
||||
return types.NewInterfaceType(methods, embeddeds)
|
||||
}
|
||||
return typ
|
||||
}
|
||||
|
||||
func equalTypeArgs(x, y *types.TypeList) error {
|
||||
if x.Len() != y.Len() {
|
||||
return fmt.Errorf("unequal lengths: %d vs %d", x.Len(), y.Len())
|
||||
}
|
||||
for i := 0; i < x.Len(); i++ {
|
||||
if err := equalType(x.At(i), y.At(i)); err != nil {
|
||||
return fmt.Errorf("type %d: %s", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func equalTypeParams(x, y *types.TypeParamList) error {
|
||||
if x.Len() != y.Len() {
|
||||
return fmt.Errorf("unequal lengths: %d vs %d", x.Len(), y.Len())
|
||||
}
|
||||
for i := 0; i < x.Len(); i++ {
|
||||
if err := equalType(x.At(i), y.At(i)); err != nil {
|
||||
return fmt.Errorf("type parameter %d: %s", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestVeryLongFile tests the position of an import object declared in
|
||||
// a very long input file. Line numbers greater than maxlines are
|
||||
// reported as line 1, not garbage or token.NoPos.
|
||||
func TestVeryLongFile(t *testing.T) {
|
||||
// parse and typecheck
|
||||
longFile := "package foo" + strings.Repeat("\n", 123456) + "var X int"
|
||||
fset1 := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset1, "foo.go", longFile, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var conf types.Config
|
||||
pkg, err := conf.Check("foo", fset1, []*ast.File{f}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// export
|
||||
var out bytes.Buffer
|
||||
if err := gcimporter.IExportData(&out, fset1, pkg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exportdata := out.Bytes()
|
||||
|
||||
// import
|
||||
imports := make(map[string]*types.Package)
|
||||
fset2 := token.NewFileSet()
|
||||
_, pkg2, err := gcimporter.IImportData(fset2, imports, exportdata, pkg.Path())
|
||||
if err != nil {
|
||||
t.Fatalf("BImportData(%s): %v", pkg.Path(), err)
|
||||
}
|
||||
|
||||
// compare
|
||||
posn1 := fset1.Position(pkg.Scope().Lookup("X").Pos())
|
||||
posn2 := fset2.Position(pkg2.Scope().Lookup("X").Pos())
|
||||
if want := "foo.go:1:1"; posn2.String() != want {
|
||||
t.Errorf("X position = %s, want %s (orig was %s)",
|
||||
posn2, want, posn1)
|
||||
}
|
||||
}
|
||||
|
||||
const src = `
|
||||
package p
|
||||
|
||||
type (
|
||||
T0 = int32
|
||||
T1 = struct{}
|
||||
T2 = struct{ T1 }
|
||||
Invalid = foo // foo is undeclared
|
||||
)
|
||||
`
|
||||
|
||||
func checkPkg(t *testing.T, pkg *types.Package, label string) {
|
||||
T1 := types.NewStruct(nil, nil)
|
||||
T2 := types.NewStruct([]*types.Var{types.NewField(0, pkg, "T1", T1, true)}, nil)
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
typ types.Type
|
||||
}{
|
||||
{"T0", types.Typ[types.Int32]},
|
||||
{"T1", T1},
|
||||
{"T2", T2},
|
||||
{"Invalid", types.Typ[types.Invalid]},
|
||||
} {
|
||||
obj := pkg.Scope().Lookup(test.name)
|
||||
if obj == nil {
|
||||
t.Errorf("%s: %s not found", label, test.name)
|
||||
continue
|
||||
}
|
||||
tname, _ := obj.(*types.TypeName)
|
||||
if tname == nil {
|
||||
t.Errorf("%s: %v not a type name", label, obj)
|
||||
continue
|
||||
}
|
||||
if !tname.IsAlias() {
|
||||
t.Errorf("%s: %v: not marked as alias", label, tname)
|
||||
continue
|
||||
}
|
||||
if got := tname.Type(); !types.Identical(got, test.typ) {
|
||||
t.Errorf("%s: %v: got %v; want %v", label, tname, got, test.typ)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file contains the remaining vestiges of
|
||||
// $GOROOT/src/go/internal/gcimporter/bimport.go.
|
||||
|
||||
package gcimporter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func errorf(format string, args ...interface{}) {
|
||||
panic(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
const deltaNewFile = -64 // see cmd/compile/internal/gc/bexport.go
|
||||
|
||||
// Synthesize a token.Pos
|
||||
type fakeFileSet struct {
|
||||
fset *token.FileSet
|
||||
files map[string]*fileInfo
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
file *token.File
|
||||
lastline int
|
||||
}
|
||||
|
||||
const maxlines = 64 * 1024
|
||||
|
||||
func (s *fakeFileSet) pos(file string, line, column int) token.Pos {
|
||||
// TODO(mdempsky): Make use of column.
|
||||
|
||||
// Since we don't know the set of needed file positions, we reserve maxlines
|
||||
// positions per file. We delay calling token.File.SetLines until all
|
||||
// positions have been calculated (by way of fakeFileSet.setLines), so that
|
||||
// we can avoid setting unnecessary lines. See also golang/go#46586.
|
||||
f := s.files[file]
|
||||
if f == nil {
|
||||
f = &fileInfo{file: s.fset.AddFile(file, -1, maxlines)}
|
||||
s.files[file] = f
|
||||
}
|
||||
if line > maxlines {
|
||||
line = 1
|
||||
}
|
||||
if line > f.lastline {
|
||||
f.lastline = line
|
||||
}
|
||||
|
||||
// Return a fake position assuming that f.file consists only of newlines.
|
||||
return token.Pos(f.file.Base() + line - 1)
|
||||
}
|
||||
|
||||
func (s *fakeFileSet) setLines() {
|
||||
fakeLinesOnce.Do(func() {
|
||||
fakeLines = make([]int, maxlines)
|
||||
for i := range fakeLines {
|
||||
fakeLines[i] = i
|
||||
}
|
||||
})
|
||||
for _, f := range s.files {
|
||||
f.file.SetLines(fakeLines[:f.lastline])
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
fakeLines []int
|
||||
fakeLinesOnce sync.Once
|
||||
)
|
||||
|
||||
func chanDir(d int) types.ChanDir {
|
||||
// tag values must match the constants in cmd/compile/internal/gc/go.go
|
||||
switch d {
|
||||
case 1 /* Crecv */ :
|
||||
return types.RecvOnly
|
||||
case 2 /* Csend */ :
|
||||
return types.SendOnly
|
||||
case 3 /* Cboth */ :
|
||||
return types.SendRecv
|
||||
default:
|
||||
errorf("unexpected channel dir %d", d)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
var predeclOnce sync.Once
|
||||
var predecl []types.Type // initialized lazily
|
||||
|
||||
func predeclared() []types.Type {
|
||||
predeclOnce.Do(func() {
|
||||
// initialize lazily to be sure that all
|
||||
// elements have been initialized before
|
||||
predecl = []types.Type{ // basic types
|
||||
types.Typ[types.Bool],
|
||||
types.Typ[types.Int],
|
||||
types.Typ[types.Int8],
|
||||
types.Typ[types.Int16],
|
||||
types.Typ[types.Int32],
|
||||
types.Typ[types.Int64],
|
||||
types.Typ[types.Uint],
|
||||
types.Typ[types.Uint8],
|
||||
types.Typ[types.Uint16],
|
||||
types.Typ[types.Uint32],
|
||||
types.Typ[types.Uint64],
|
||||
types.Typ[types.Uintptr],
|
||||
types.Typ[types.Float32],
|
||||
types.Typ[types.Float64],
|
||||
types.Typ[types.Complex64],
|
||||
types.Typ[types.Complex128],
|
||||
types.Typ[types.String],
|
||||
|
||||
// basic type aliases
|
||||
types.Universe.Lookup("byte").Type(),
|
||||
types.Universe.Lookup("rune").Type(),
|
||||
|
||||
// error
|
||||
types.Universe.Lookup("error").Type(),
|
||||
|
||||
// untyped types
|
||||
types.Typ[types.UntypedBool],
|
||||
types.Typ[types.UntypedInt],
|
||||
types.Typ[types.UntypedRune],
|
||||
types.Typ[types.UntypedFloat],
|
||||
types.Typ[types.UntypedComplex],
|
||||
types.Typ[types.UntypedString],
|
||||
types.Typ[types.UntypedNil],
|
||||
|
||||
// package unsafe
|
||||
types.Typ[types.UnsafePointer],
|
||||
|
||||
// invalid type
|
||||
types.Typ[types.Invalid], // only appears in packages with errors
|
||||
|
||||
// used internally by gc; never used by this package or in .a files
|
||||
anyType{},
|
||||
}
|
||||
predecl = append(predecl, additionalPredeclared()...)
|
||||
})
|
||||
return predecl
|
||||
}
|
||||
|
||||
type anyType struct{}
|
||||
|
||||
func (t anyType) Underlying() types.Type { return t }
|
||||
func (t anyType) String() string { return "any" }
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file is a copy of $GOROOT/src/go/internal/gcimporter/exportdata.go.
|
||||
|
||||
// This file implements FindExportData.
|
||||
|
||||
package gcimporter
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func readGopackHeader(r *bufio.Reader) (name string, size int64, err error) {
|
||||
// See $GOROOT/include/ar.h.
|
||||
hdr := make([]byte, 16+12+6+6+8+10+2)
|
||||
_, err = io.ReadFull(r, hdr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// leave for debugging
|
||||
if false {
|
||||
fmt.Printf("header: %s", hdr)
|
||||
}
|
||||
s := strings.TrimSpace(string(hdr[16+12+6+6+8:][:10]))
|
||||
length, err := strconv.Atoi(s)
|
||||
size = int64(length)
|
||||
if err != nil || hdr[len(hdr)-2] != '`' || hdr[len(hdr)-1] != '\n' {
|
||||
err = fmt.Errorf("invalid archive header")
|
||||
return
|
||||
}
|
||||
name = strings.TrimSpace(string(hdr[:16]))
|
||||
return
|
||||
}
|
||||
|
||||
// FindExportData positions the reader r at the beginning of the
|
||||
// export data section of an underlying GC-created object/archive
|
||||
// file by reading from it. The reader must be positioned at the
|
||||
// start of the file before calling this function. The hdr result
|
||||
// is the string before the export data, either "$$" or "$$B".
|
||||
// The size result is the length of the export data in bytes, or -1 if not known.
|
||||
func FindExportData(r *bufio.Reader) (hdr string, size int64, err error) {
|
||||
// Read first line to make sure this is an object file.
|
||||
line, err := r.ReadSlice('\n')
|
||||
if err != nil {
|
||||
err = fmt.Errorf("can't find export data (%v)", err)
|
||||
return
|
||||
}
|
||||
|
||||
if string(line) == "!<arch>\n" {
|
||||
// Archive file. Scan to __.PKGDEF.
|
||||
var name string
|
||||
if name, size, err = readGopackHeader(r); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// First entry should be __.PKGDEF.
|
||||
if name != "__.PKGDEF" {
|
||||
err = fmt.Errorf("go archive is missing __.PKGDEF")
|
||||
return
|
||||
}
|
||||
|
||||
// Read first line of __.PKGDEF data, so that line
|
||||
// is once again the first line of the input.
|
||||
if line, err = r.ReadSlice('\n'); err != nil {
|
||||
err = fmt.Errorf("can't find export data (%v)", err)
|
||||
return
|
||||
}
|
||||
size -= int64(len(line))
|
||||
}
|
||||
|
||||
// Now at __.PKGDEF in archive or still at beginning of file.
|
||||
// Either way, line should begin with "go object ".
|
||||
if !strings.HasPrefix(string(line), "go object ") {
|
||||
err = fmt.Errorf("not a Go object file")
|
||||
return
|
||||
}
|
||||
|
||||
// Skip over object header to export data.
|
||||
// Begins after first line starting with $$.
|
||||
for line[0] != '$' {
|
||||
if line, err = r.ReadSlice('\n'); err != nil {
|
||||
err = fmt.Errorf("can't find export data (%v)", err)
|
||||
return
|
||||
}
|
||||
size -= int64(len(line))
|
||||
}
|
||||
hdr = string(line)
|
||||
if size < 0 {
|
||||
size = -1
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file is a reduced copy of $GOROOT/src/go/internal/gcimporter/gcimporter.go.
|
||||
|
||||
// Package gcimporter provides various functions for reading
|
||||
// gc-generated object files that can be used to implement the
|
||||
// Importer interface defined by the Go 1.5 standard library package.
|
||||
//
|
||||
// The encoding is deterministic: if the encoder is applied twice to
|
||||
// the same types.Package data structure, both encodings are equal.
|
||||
// This property may be important to avoid spurious changes in
|
||||
// applications such as build systems.
|
||||
//
|
||||
// However, the encoder is not necessarily idempotent. Importing an
|
||||
// exported package may yield a types.Package that, while it
|
||||
// represents the same set of Go types as the original, may differ in
|
||||
// the details of its internal representation. Because of these
|
||||
// differences, re-encoding the imported package may yield a
|
||||
// different, but equally valid, encoding of the package.
|
||||
package gcimporter // import "golang.org/x/tools/internal/gcimporter"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Enable debug during development: it adds some additional checks, and
|
||||
// prevents errors from being recovered.
|
||||
debug = false
|
||||
|
||||
// If trace is set, debugging output is printed to std out.
|
||||
trace = false
|
||||
)
|
||||
|
||||
var exportMap sync.Map // package dir → func() (string, bool)
|
||||
|
||||
// lookupGorootExport returns the location of the export data
|
||||
// (normally found in the build cache, but located in GOROOT/pkg
|
||||
// in prior Go releases) for the package located in pkgDir.
|
||||
//
|
||||
// (We use the package's directory instead of its import path
|
||||
// mainly to simplify handling of the packages in src/vendor
|
||||
// and cmd/vendor.)
|
||||
func lookupGorootExport(pkgDir string) (string, bool) {
|
||||
f, ok := exportMap.Load(pkgDir)
|
||||
if !ok {
|
||||
var (
|
||||
listOnce sync.Once
|
||||
exportPath string
|
||||
)
|
||||
f, _ = exportMap.LoadOrStore(pkgDir, func() (string, bool) {
|
||||
listOnce.Do(func() {
|
||||
cmd := exec.Command("go", "list", "-export", "-f", "{{.Export}}", pkgDir)
|
||||
cmd.Dir = build.Default.GOROOT
|
||||
var output []byte
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
exports := strings.Split(string(bytes.TrimSpace(output)), "\n")
|
||||
if len(exports) != 1 {
|
||||
return
|
||||
}
|
||||
|
||||
exportPath = exports[0]
|
||||
})
|
||||
|
||||
return exportPath, exportPath != ""
|
||||
})
|
||||
}
|
||||
|
||||
return f.(func() (string, bool))()
|
||||
}
|
||||
|
||||
var pkgExts = [...]string{".a", ".o"}
|
||||
|
||||
// FindPkg returns the filename and unique package id for an import
|
||||
// path based on package information provided by build.Import (using
|
||||
// the build.Default build.Context). A relative srcDir is interpreted
|
||||
// relative to the current working directory.
|
||||
// If no file was found, an empty filename is returned.
|
||||
func FindPkg(path, srcDir string) (filename, id string) {
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var noext string
|
||||
switch {
|
||||
default:
|
||||
// "x" -> "$GOPATH/pkg/$GOOS_$GOARCH/x.ext", "x"
|
||||
// Don't require the source files to be present.
|
||||
if abs, err := filepath.Abs(srcDir); err == nil { // see issue 14282
|
||||
srcDir = abs
|
||||
}
|
||||
bp, _ := build.Import(path, srcDir, build.FindOnly|build.AllowBinary)
|
||||
if bp.PkgObj == "" {
|
||||
var ok bool
|
||||
if bp.Goroot && bp.Dir != "" {
|
||||
filename, ok = lookupGorootExport(bp.Dir)
|
||||
}
|
||||
if !ok {
|
||||
id = path // make sure we have an id to print in error message
|
||||
return
|
||||
}
|
||||
} else {
|
||||
noext = strings.TrimSuffix(bp.PkgObj, ".a")
|
||||
id = bp.ImportPath
|
||||
}
|
||||
|
||||
case build.IsLocalImport(path):
|
||||
// "./x" -> "/this/directory/x.ext", "/this/directory/x"
|
||||
noext = filepath.Join(srcDir, path)
|
||||
id = noext
|
||||
|
||||
case filepath.IsAbs(path):
|
||||
// for completeness only - go/build.Import
|
||||
// does not support absolute imports
|
||||
// "/x" -> "/x.ext", "/x"
|
||||
noext = path
|
||||
id = path
|
||||
}
|
||||
|
||||
if false { // for debugging
|
||||
if path != id {
|
||||
fmt.Printf("%s -> %s\n", path, id)
|
||||
}
|
||||
}
|
||||
|
||||
if filename != "" {
|
||||
if f, err := os.Stat(filename); err == nil && !f.IsDir() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// try extensions
|
||||
for _, ext := range pkgExts {
|
||||
filename = noext + ext
|
||||
if f, err := os.Stat(filename); err == nil && !f.IsDir() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
filename = "" // not found
|
||||
return
|
||||
}
|
||||
|
||||
// Import imports a gc-generated package given its import path and srcDir, adds
|
||||
// the corresponding package object to the packages map, and returns the object.
|
||||
// The packages map must contain all packages already imported.
|
||||
func Import(packages map[string]*types.Package, path, srcDir string, lookup func(path string) (io.ReadCloser, error)) (pkg *types.Package, err error) {
|
||||
var rc io.ReadCloser
|
||||
var filename, id string
|
||||
if lookup != nil {
|
||||
// With custom lookup specified, assume that caller has
|
||||
// converted path to a canonical import path for use in the map.
|
||||
if path == "unsafe" {
|
||||
return types.Unsafe, nil
|
||||
}
|
||||
id = path
|
||||
|
||||
// No need to re-import if the package was imported completely before.
|
||||
if pkg = packages[id]; pkg != nil && pkg.Complete() {
|
||||
return
|
||||
}
|
||||
f, err := lookup(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rc = f
|
||||
} else {
|
||||
filename, id = FindPkg(path, srcDir)
|
||||
if filename == "" {
|
||||
if path == "unsafe" {
|
||||
return types.Unsafe, nil
|
||||
}
|
||||
return nil, fmt.Errorf("can't find import: %q", id)
|
||||
}
|
||||
|
||||
// no need to re-import if the package was imported completely before
|
||||
if pkg = packages[id]; pkg != nil && pkg.Complete() {
|
||||
return
|
||||
}
|
||||
|
||||
// open file
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// add file name to error
|
||||
err = fmt.Errorf("%s: %v", filename, err)
|
||||
}
|
||||
}()
|
||||
rc = f
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
var hdr string
|
||||
var size int64
|
||||
buf := bufio.NewReader(rc)
|
||||
if hdr, size, err = FindExportData(buf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch hdr {
|
||||
case "$$B\n":
|
||||
var data []byte
|
||||
data, err = io.ReadAll(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// TODO(gri): allow clients of go/importer to provide a FileSet.
|
||||
// Or, define a new standard go/types/gcexportdata package.
|
||||
fset := token.NewFileSet()
|
||||
|
||||
// Select appropriate importer.
|
||||
if len(data) > 0 {
|
||||
switch data[0] {
|
||||
case 'v', 'c', 'd': // binary, till go1.10
|
||||
return nil, fmt.Errorf("binary (%c) import format is no longer supported", data[0])
|
||||
|
||||
case 'i': // indexed, till go1.19
|
||||
_, pkg, err := IImportData(fset, packages, data[1:], id)
|
||||
return pkg, err
|
||||
|
||||
case 'u': // unified, from go1.20
|
||||
_, pkg, err := UImportData(fset, packages, data[1:size], id)
|
||||
return pkg, err
|
||||
|
||||
default:
|
||||
l := len(data)
|
||||
if l > 10 {
|
||||
l = 10
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected export data with prefix %q for path %s", string(data[:l]), id)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("unknown export data header: %q", hdr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type byPath []*types.Package
|
||||
|
||||
func (a byPath) Len() int { return len(a) }
|
||||
func (a byPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byPath) Less(i, j int) bool { return a[i].Path() < a[j].Path() }
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
// Copyright 2021 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package gcimporter
|
||||
|
||||
// Temporarily expose version-related functionality so that we can test at
|
||||
// specific export data versions.
|
||||
|
||||
var IExportCommon = iexportCommon
|
||||
|
||||
const (
|
||||
IExportVersion = iexportVersion
|
||||
IExportVersionGenerics = iexportVersionGenerics
|
||||
IExportVersionGo1_18 = iexportVersionGo1_18
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user