diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bf77fdf..c5bf39ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [ 1.23.x, 1.24.x, tip ] + go-version: [1.24.x, 1.25.x, tip] steps: - name: Set up Go stable if: matrix.go-version != 'tip' @@ -32,17 +32,17 @@ jobs: - name: test run: make test - name: govulncheck - if: matrix.go-version != 'tip' + if: matrix.go-version == '1.25.x' uses: golang/govulncheck-action@v1 with: go-version-input: ${{ matrix.go-version }} check-latest: true go-package: ./... - name: golangci-lint - if: matrix.go-version == '1.24.x' - uses: golangci/golangci-lint-action@v8 + if: matrix.go-version == '1.25.x' + uses: golangci/golangci-lint-action@v9 with: - version: v2.1.6 + version: v2.6.1 - name: integration test - if: matrix.go-version == '1.24.x' + if: matrix.go-version == '1.25.x' run: tests/system_test.sh diff --git a/go.mod b/go.mod index 4d945e0f..a4c0cd1d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.0 require ( github.com/JaderDias/movingmedian v0.0.0-20220813210630-d8c6b6de8835 + github.com/alecthomas/assert/v2 v2.11.0 github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746 github.com/davecgh/go-spew v1.1.1 github.com/dgryski/go-expirecache v0.0.0-20170314133854-743ef98b2adb @@ -32,6 +33,7 @@ require ( require ( github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 // indirect + github.com/alecthomas/repr v0.4.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 // indirect @@ -44,6 +46,7 @@ require ( github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect diff --git a/go.sum b/go.sum index 19f41212..b1067728 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,10 @@ github.com/JaderDias/movingmedian v0.0.0-20220813210630-d8c6b6de8835 h1:mbxQnovj github.com/JaderDias/movingmedian v0.0.0-20220813210630-d8c6b6de8835/go.mod h1:zsfWLaDctbM7aV1TsQAwkVswuKQ0k7PK4rjC1VZqpbI= github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 h1:xlwdaKcTNVW4PtpQb8aKA4Pjy0CdJHEqvFbAnvR5m2g= github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794/go.mod h1:7e+I0LQFUI9AXWxOfsQROs9xPhoJtbsyWcjJqDd4KPY= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -100,6 +104,8 @@ github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/pkg/expr/functions/cairo/cairo_test.go b/pkg/expr/functions/cairo/cairo_test.go index ee3d8489..220a8bb6 100644 --- a/pkg/expr/functions/cairo/cairo_test.go +++ b/pkg/expr/functions/cairo/cairo_test.go @@ -1,5 +1,4 @@ //go:build cairo -// +build cairo package cairo diff --git a/pkg/expr/functions/cairo/png/cairo.go b/pkg/expr/functions/cairo/png/cairo.go index aac75e71..dff0f94b 100644 --- a/pkg/expr/functions/cairo/png/cairo.go +++ b/pkg/expr/functions/cairo/png/cairo.go @@ -1,5 +1,4 @@ //go:build cairo -// +build cairo package png diff --git a/pkg/expr/functions/cairo/png/pixel_ratio.go b/pkg/expr/functions/cairo/png/pixel_ratio.go index 75492634..f052efc3 100644 --- a/pkg/expr/functions/cairo/png/pixel_ratio.go +++ b/pkg/expr/functions/cairo/png/pixel_ratio.go @@ -1,5 +1,4 @@ //go:build cairo -// +build cairo package png diff --git a/pkg/expr/functions/cairo/png/png.go b/pkg/expr/functions/cairo/png/png.go index 87aec3bb..43d99bbd 100644 --- a/pkg/expr/functions/cairo/png/png.go +++ b/pkg/expr/functions/cairo/png/png.go @@ -1,5 +1,4 @@ //go:build !cairo -// +build !cairo package png diff --git a/pkg/expr/types/cairo.go b/pkg/expr/types/cairo.go index d6717160..e7bba61a 100644 --- a/pkg/expr/types/cairo.go +++ b/pkg/expr/types/cairo.go @@ -1,5 +1,4 @@ //go:build cairo -// +build cairo package types diff --git a/pkg/expr/types/png.go b/pkg/expr/types/png.go index 3e434ab0..683a0e66 100644 --- a/pkg/expr/types/png.go +++ b/pkg/expr/types/png.go @@ -1,4 +1,4 @@ -// +build !cairo +//go:build !cairo package types diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index cad7aab1..c123a95a 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -332,11 +332,19 @@ func (e *expr) insertFirstArg(exp *expr) error { return nil } -func parseExprWithoutPipe(e string) (Expr, string, error) { - // skip whitespace - for len(e) > 1 && unicode.IsSpace(rune(e[0])) { - e = e[1:] +func skipWhitespace(e string) string { + skipTo := len(e) + for i, r := range e { + if !unicode.IsSpace(r) { + skipTo = i + break + } } + return e[skipTo:] +} + +func parseExprWithoutPipe(e string) (Expr, string, error) { + e = skipWhitespace(e) if len(e) == 0 { return nil, "", ErrMissingExpr @@ -393,9 +401,7 @@ func ParseExpr(e string) (Expr, string, error) { } func pipe(exp *expr, e string) (*expr, string, error) { - for len(e) > 1 && unicode.IsSpace(rune(e[0])) { - e = e[1:] - } + e = skipWhitespace(e) if e == "" || e[0] != '|' { return exp, e, nil @@ -452,7 +458,7 @@ func parseArgList(e string) (string, []*expr, map[string]*expr, string, error) { e = e[1:] // check for empty args - t := strings.TrimLeftFunc(e, unicode.IsSpace) + t := skipWhitespace(e) if t != "" && t[0] == ')' { return "", posArgs, namedArgs, t[1:], nil } @@ -519,7 +525,7 @@ func parseArgList(e string) (string, []*expr, map[string]*expr, string, error) { } // after the argument, trim any trailing spaces - e = strings.TrimLeftFunc(e, unicode.IsSpace) + e = skipWhitespace(e) // We've consumed the entire buffer but the argument list isn't complete. if len(e) == 0 { @@ -676,7 +682,6 @@ FOR: } func parseString(s string) (string, string, error) { - if s[0] != '\'' && s[0] != '"' { panic("string should start with open quote") } diff --git a/pkg/parser/parser_fuzz.go b/pkg/parser/parser_fuzz.go index 8d4178a8..cda02ef4 100644 --- a/pkg/parser/parser_fuzz.go +++ b/pkg/parser/parser_fuzz.go @@ -1,5 +1,3 @@ -// +build gofuzz - package parser func Fuzz(data []byte) int { diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index 3acd6040..29cd51a5 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -4,9 +4,42 @@ import ( "reflect" "testing" + "github.com/alecthomas/assert/v2" "github.com/davecgh/go-spew/spew" ) +func TestSkipWhitespace(t *testing.T) { + testCases := []struct{ in, expected string }{ + { + in: " ", + expected: "", + }, + { + in: " foo", + expected: "foo", + }, + { + in: " foo ", + expected: "foo ", + }, + { + in: "\nfoo", + expected: "foo", + }, + { + in: "\tfoo", + expected: "foo", + }, + } + + for _, tc := range testCases { + t.Run(tc.in, func(t *testing.T) { + out := skipWhitespace(tc.in) + assert.Equal(t, tc.expected, out) + }) + } +} + func TestParseExpr(t *testing.T) { tests := []struct { @@ -427,6 +460,92 @@ func TestParseExpr(t *testing.T) { argString: "func(\ng\r), \ns\t", }, }, + { + s: "foo.bar\n.baz\t", + e: &expr{ + target: "foo.bar", + etype: EtName, + }, + }, + { + s: "absolute( baz )\n", + e: &expr{ + target: "absolute", + etype: EtFunc, + args: []*expr{ + {target: "baz"}, + }, + argString: " baz ", + }, + }, + { + s: "func1(\"example blah\")", + e: &expr{ + target: "func1", + etype: EtFunc, + args: []*expr{ + { + etype: EtString, + valStr: "example blah", + }, + }, + argString: "\"example blah\"", + }, + }, + { + s: "foobar(\n)", + e: &expr{ + target: "foobar", + etype: EtFunc, + }, + }, + { + s: "foobar(asdf,\n\tzxcv,\n\tqwer\n)", + e: &expr{ + target: "foobar", + etype: EtFunc, + args: []*expr{ + {target: "asdf"}, + {target: "zxcv"}, + {target: "qwer"}, + }, + argString: "asdf,\n\tzxcv,\n\tqwer\n", + }, + }, + { + s: "func1(foo.bar)\n| func2(foo.baz)|\n func3(\n\tfunc4(asdf.zxcv.qwer)\n)", + e: &expr{ + target: "func3", + etype: EtFunc, + args: []*expr{ + { + target: "func2", + etype: EtFunc, + args: []*expr{ + { + target: "func1", + etype: EtFunc, + args: []*expr{ + {target: "foo.bar"}, + }, + argString: "foo.bar", + }, + {target: "foo.baz"}, + }, + argString: "func1(foo.bar),foo.baz", + }, + { + target: "func4", + etype: EtFunc, + args: []*expr{ + {target: "asdf.zxcv.qwer"}, + }, + argString: "asdf.zxcv.qwer", + }, + }, + argString: "func2(func1(foo.bar),foo.baz),func4(asdf.zxcv.qwer)", + }, + }, } for _, ttr := range tests { diff --git a/vendor/github.com/alecthomas/assert/v2/.golangci.yml b/vendor/github.com/alecthomas/assert/v2/.golangci.yml new file mode 100644 index 00000000..de26bb06 --- /dev/null +++ b/vendor/github.com/alecthomas/assert/v2/.golangci.yml @@ -0,0 +1,85 @@ +run: + tests: true + skip-dirs: + - _examples + +output: + print-issued-lines: false + +linters: + enable-all: true + disable: + - maligned + - lll + - gocyclo + - gochecknoglobals + - wsl + - whitespace + - godox + - funlen + - gocognit + - gomnd + - goerr113 + - godot + - nestif + - testpackage + - nolintlint + - exhaustivestruct + - wrapcheck + - gci + - gofumpt + - gocritic + - nlreturn + - errorlint + - nakedret + - forbidigo + - revive + - cyclop + - ifshort + - paralleltest + - interfacer + - scopelint + - golint + - wastedassign + - forcetypeassert + - gomoddirectives + - thelper + - varnamelen + - depguard + - exhaustruct + - nonamedreturns + - varcheck + - structcheck + - deadcode + - nosnakecase + - mnd + - perfsprint + +linters-settings: + govet: + check-shadowing: true + gocyclo: + min-complexity: 10 + dupl: + threshold: 100 + goconst: + min-len: 8 + min-occurrences: 3 + exhaustive: + default-signifies-exhaustive: true + +issues: + max-per-linter: 0 + max-same: 0 + exclude-use-default: false + exclude: + # Captured by errcheck. + - '^(G104|G204|G307):' + # Very commonly not checked. + - 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked' + - 'exported method `(.*\.MarshalJSON|.*\.UnmarshalJSON|.*\.EntityURN|.*\.GoString|.*\.Pos)` should have comment or be unexported' + - 'composite literal uses unkeyed fields' + - 'declaration of "err" shadows declaration' + - 'bad syntax for struct tag key' + - 'bad syntax for struct tag pair' + - '^ST1012' diff --git a/vendor/github.com/alecthomas/assert/v2/COPYING b/vendor/github.com/alecthomas/assert/v2/COPYING new file mode 100644 index 00000000..d88bef06 --- /dev/null +++ b/vendor/github.com/alecthomas/assert/v2/COPYING @@ -0,0 +1,19 @@ +Copyright (C) 2021 Alec Thomas + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/alecthomas/assert/v2/README.md b/vendor/github.com/alecthomas/assert/v2/README.md new file mode 100644 index 00000000..9ffe5eef --- /dev/null +++ b/vendor/github.com/alecthomas/assert/v2/README.md @@ -0,0 +1,146 @@ +# A simple assertion library using Go generics + +[![PkgGoDev](https://pkg.go.dev/badge/github.com/alecthomas/assert/v2)](https://pkg.go.dev/github.com/alecthomas/assert/v2) [![CI](https://github.com/alecthomas/assert/actions/workflows/ci.yml/badge.svg)](https://github.com/alecthomas/assert/actions/workflows/ci.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/alecthomas/assert/v2)](https://goreportcard.com/report/github.com/alecthomas/assert/v2) [![Slack chat](https://img.shields.io/static/v1?logo=slack&style=flat&label=slack&color=green&message=gophers)](https://gophers.slack.com/messages/CN9DS8YF3) + + +This library is inspired by testify/require, but with a significantly reduced +API surface based on empirical use of that package. + +It also provides much nicer diff output, eg. + +``` +=== RUN TestFail + assert_test.go:14: Expected values to be equal: + assert.Data{ + - Str: "foo", + + Str: "far", + Num: 10, + } +--- FAIL: TestFail (0.00s) +``` + +## API + +Import then use as `assert`: + +```go +import "github.com/alecthomas/assert/v2" +``` + +This library has the following API. For all functions, `msgAndArgs` is used to +format error messages using the `fmt` package. + +```go +// Equal asserts that "expected" and "actual" are equal using google/go-cmp. +// +// If they are not, a diff of the Go representation of the values will be displayed. +func Equal[T comparable](t testing.TB, expected, actual T, msgAndArgs ...interface{}) + +// NotEqual asserts that "expected" is not equal to "actual" using google/go-cmp. +// +// If they are equal the expected value will be displayed. +func NotEqual[T comparable](t testing.TB, expected, actual T, msgAndArgs ...interface{}) + +// Zero asserts that a value is its zero value. +func Zero[T comparable](t testing.TB, value T, msgAndArgs ...interface{}) + +// NotZero asserts that a value is not its zero value. +func NotZero[T comparable](t testing.TB, value T, msgAndArgs ...interface{}) + +// Contains asserts that "haystack" contains "needle". +func Contains(t testing.TB, haystack string, needle string, msgAndArgs ...interface{}) + +// NotContains asserts that "haystack" does not contain "needle". +func NotContains(t testing.TB, haystack string, needle string, msgAndArgs ...interface{}) + +// EqualError asserts that either an error is non-nil and that its message is what is expected, +// or that error is nil if the expected message is empty. +func EqualError(t testing.TB, err error, errString string, msgAndArgs...interface{}) + +// Error asserts that an error is not nil. +func Error(t testing.TB, err error, msgAndArgs ...interface{}) + +// NoError asserts that an error is nil. +func NoError(t testing.TB, err error, msgAndArgs ...interface{}) + +// IsError asserts than any error in "err"'s tree matches "target". +func IsError(t testing.TB, err, target error, msgAndArgs ...interface{}) + +// NotIsError asserts than no error in "err"'s tree matches "target". +func NotIsError(t testing.TB, err, target error, msgAndArgs ...interface{}) + +// Panics asserts that the given function panics. +func Panics(t testing.TB, fn func(), msgAndArgs ...interface{}) + +// NotPanics asserts that the given function does not panic. +func NotPanics(t testing.TB, fn func(), msgAndArgs ...interface{}) + +// Compare two values for equality and return true or false. +func Compare[T any](t testing.TB, x, y T) bool + +// True asserts that an expression is true. +func True(t testing.TB, ok bool, msgAndArgs ...interface{}) + +// False asserts that an expression is false. +func False(t testing.TB, ok bool, msgAndArgs ...interface{}) +``` + +## Evaluation process + +Our empirical data of testify usage comes from a monorepo with around 50K lines +of tests. + +These are the usage counts for all testify functions, normalised to the base +(not `Printf()`) non-negative(not `No(t)?`) case for each core function. + +```text +2240 Error +1314 Equal + 219 True + 210 Nil + 167 Empty + 107 Contains + 79 Len + 61 False + 24 EqualValues + 20 EqualError + 17 Zero + 15 Fail + 15 ElementsMatch + 9 Panics + 7 IsType + 6 FileExists + 4 JSONEq + 3 PanicsWithValue + 3 Eventually +``` + +The decision for each function was: + +### Keep + +- `Error(t, err)` -> frequently used, keep +- `Equal(t, expected, actual)` -> frequently used, keep but make type safe +- `True(t, expr)` -> frequently used, keep +- `False(t, expr)` -> frequently used, keep +- `Empty(t, thing)` -> `require.Equal(t, len(thing), 0)` +- `Contains(t, haystack string, needle string)` - the only variant used in our codebase, keep as concrete type +- `Zero(t, value)` -> make type safe, keep +- `Panics(t, f)` -> useful, keep +- `EqualError(t, a, b)` -> useful, keep +- `Nil(t, value)` -> frequently used, keep + +### Not keeping, replace with ... + +- `ElementsMatch(t, a, b)` - use [peterrk/slices](https://github.com/peterrk/slices) or stdlib sort support once it lands. +- `IsType(t, a, b)` -> `require.Equal(t, reflect.TypeOf(a).String(), reflect.TypeOf(b).String())` +- `FileExists()` -> very little use, drop +- `JSONEq()` -> very little use, drop +- `PanicsWithValue()` -> very little use, drop +- `Eventually()` -> very little use, drop +- `Contains(t, haystack []T, needle T)` - very little use, replace with +- `Contains(t, haystack map[K]V, needle K)` - very little use, drop +- `Len(t, v, n)` -> cannot be implemented as a single function with generics`Equal(t, len(v), n)` +- `EqualValues()` - `Equal(t, TYPE(a), TYPE(b))` +- `Fail()` -> `t.Fatal()` diff --git a/vendor/github.com/alecthomas/assert/v2/assert.go b/vendor/github.com/alecthomas/assert/v2/assert.go new file mode 100644 index 00000000..261beafa --- /dev/null +++ b/vendor/github.com/alecthomas/assert/v2/assert.go @@ -0,0 +1,353 @@ +// Package assert provides type-safe assertions with clean error messages. +package assert + +import ( + "bytes" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "testing" + + "github.com/alecthomas/repr" + "github.com/hexops/gotextdiff" + "github.com/hexops/gotextdiff/myers" +) + +// A CompareOption modifies how object comparisons behave. +type CompareOption func() []repr.Option + +// Exclude fields of the given type from comparison. +func Exclude[T any]() CompareOption { + return func() []repr.Option { + return []repr.Option{repr.Hide[T]()} + } +} + +// OmitEmpty fields from comparison. +func OmitEmpty() CompareOption { + return func() []repr.Option { + return []repr.Option{repr.OmitEmpty(true)} + } +} + +// IgnoreGoStringer ignores GoStringer implementations when comparing. +func IgnoreGoStringer() CompareOption { + return func() []repr.Option { + return []repr.Option{repr.IgnoreGoStringer()} + } +} + +// Compare two values for equality and return true or false. +func Compare[T any](t testing.TB, x, y T, options ...CompareOption) bool { + return objectsAreEqual(x, y, options...) +} + +func extractCompareOptions(msgAndArgs ...any) ([]any, []CompareOption) { + compareOptions := []CompareOption{} + out := []any{} + for _, arg := range msgAndArgs { + if opt, ok := arg.(CompareOption); ok { + compareOptions = append(compareOptions, opt) + } else { + out = append(out, arg) + } + } + return out, compareOptions +} + +// HasPrefix asserts that the string s starts with prefix. +func HasPrefix(t testing.TB, s, prefix string, msgAndArgs ...any) { + if strings.HasPrefix(s, prefix) { + return + } + t.Helper() + msg := formatMsgAndArgs("Expected string to have prefix:", msgAndArgs...) + t.Fatalf("%s\nPrefix: %q\nString: %q\n", msg, prefix, s) +} + +// HasSuffix asserts that the string s ends with suffix. +func HasSuffix(t testing.TB, s, suffix string, msgAndArgs ...any) { + if strings.HasSuffix(s, suffix) { + return + } + t.Helper() + msg := formatMsgAndArgs("Expected string to have suffix:", msgAndArgs...) + t.Fatalf("%s\nSuffix: %q\nString: %q\n", msg, suffix, s) +} + +// Equal asserts that "expected" and "actual" are equal. +// +// If they are not, a diff of the Go representation of the values will be displayed. +func Equal[T any](t testing.TB, expected, actual T, msgArgsAndCompareOptions ...any) { + msgArgsAndCompareOptions, compareOptions := extractCompareOptions(msgArgsAndCompareOptions...) + if objectsAreEqual(expected, actual, compareOptions...) { + return + } + t.Helper() + msg := formatMsgAndArgs("Expected values to be equal:", msgArgsAndCompareOptions...) + t.Fatalf("%s\n%s", msg, Diff(expected, actual, compareOptions...)) +} + +// NotEqual asserts that "expected" is not equal to "actual". +// +// If they are equal the expected value will be displayed. +func NotEqual[T any](t testing.TB, expected, actual T, msgArgsAndCompareOptions ...any) { + msgArgsAndCompareOptions, compareOptions := extractCompareOptions(msgArgsAndCompareOptions...) + if !objectsAreEqual(expected, actual, compareOptions...) { + return + } + t.Helper() + msg := formatMsgAndArgs("Expected values to not be equal but both were:", msgArgsAndCompareOptions...) + t.Fatalf("%s\n%s", msg, repr.String(expected, repr.Indent(" "))) +} + +// Contains asserts that "haystack" contains "needle". +func Contains(t testing.TB, haystack string, needle string, msgAndArgs ...any) { + if strings.Contains(haystack, needle) { + return + } + t.Helper() + msg := formatMsgAndArgs("Haystack does not contain needle.", msgAndArgs...) + t.Fatalf("%s\nNeedle: %q\nHaystack: %q\n", msg, needle, haystack) +} + +// NotContains asserts that "haystack" does not contain "needle". +func NotContains(t testing.TB, haystack string, needle string, msgAndArgs ...any) { + if !strings.Contains(haystack, needle) { + return + } + t.Helper() + msg := formatMsgAndArgs("Haystack should not contain needle.", msgAndArgs...) + quotedHaystack, quotedNeedle, positions := needlePosition(haystack, needle) + t.Fatalf("%s\nNeedle: %s\nHaystack: %s\n %s\n", msg, quotedNeedle, quotedHaystack, positions) +} + +// SliceContains asserts that "haystack" contains "needle". +func SliceContains[T any](t testing.TB, haystack []T, needle T, msgAndArgs ...interface{}) { + t.Helper() + for _, item := range haystack { + if objectsAreEqual(item, needle) { + return + } + } + + msg := formatMsgAndArgs("Haystack does not contain needle.", msgAndArgs...) + needleRepr := repr.String(needle, repr.Indent(" ")) + haystackRepr := repr.String(haystack, repr.Indent(" ")) + t.Fatalf("%s\nNeedle: %s\nHaystack: %s\n", msg, needleRepr, haystackRepr) +} + +// NotSliceContains asserts that "haystack" does not contain "needle". +func NotSliceContains[T any](t testing.TB, haystack []T, needle T, msgAndArgs ...interface{}) { + t.Helper() + for _, item := range haystack { + if objectsAreEqual(item, needle) { + msg := formatMsgAndArgs("Haystack should not contain needle.", msgAndArgs...) + needleRepr := repr.String(needle, repr.Indent(" ")) + haystackRepr := repr.String(haystack, repr.Indent(" ")) + t.Fatalf("%s\nNeedle: %s\nHaystack: %s\n", msg, needleRepr, haystackRepr) + } + } +} + +// Zero asserts that a value is its zero value. +func Zero[T any](t testing.TB, value T, msgAndArgs ...any) { + var zero T + if objectsAreEqual(value, zero) { + return + } + val := reflect.ValueOf(value) + if (val.Kind() == reflect.Slice || val.Kind() == reflect.Map || val.Kind() == reflect.Array) && val.Len() == 0 { + return + } + t.Helper() + msg := formatMsgAndArgs("Expected a zero value but got:", msgAndArgs...) + t.Fatalf("%s\n%s", msg, repr.String(value, repr.Indent(" "))) +} + +// NotZero asserts that a value is not its zero value. +func NotZero[T any](t testing.TB, value T, msgAndArgs ...any) { + var zero T + if !objectsAreEqual(value, zero) { + val := reflect.ValueOf(value) + if !((val.Kind() == reflect.Slice || val.Kind() == reflect.Map || val.Kind() == reflect.Array) && val.Len() == 0) { + return + } + } + t.Helper() + msg := formatMsgAndArgs("Did not expect the zero value:", msgAndArgs...) + t.Fatalf("%s\n%s", msg, repr.String(value)) +} + +// EqualError asserts that either an error is non-nil and that its message is what is expected, +// or that error is nil if the expected message is empty. +func EqualError(t testing.TB, err error, errString string, msgAndArgs ...any) { + if err == nil && errString == "" { + return + } + t.Helper() + if err == nil { + t.Fatal(formatMsgAndArgs("Expected an error", msgAndArgs...)) + } + if err.Error() != errString { + msg := formatMsgAndArgs("Error message not as expected:", msgAndArgs...) + t.Fatalf("%s\n%s", msg, Diff(errString, err.Error())) + } +} + +// IsError asserts than any error in "err"'s tree matches "target". +func IsError(t testing.TB, err, target error, msgAndArgs ...any) { + if errors.Is(err, target) { + return + } + t.Helper() + t.Fatal(formatMsgAndArgs(fmt.Sprintf("Error tree %+v should contain error %q", err, target), msgAndArgs...)) +} + +// NotIsError asserts than no error in "err"'s tree matches "target". +func NotIsError(t testing.TB, err, target error, msgAndArgs ...any) { + if !errors.Is(err, target) { + return + } + t.Helper() + t.Fatal(formatMsgAndArgs(fmt.Sprintf("Error tree %+v should NOT contain error %q", err, target), msgAndArgs...)) +} + +// Error asserts that an error is not nil. +func Error(t testing.TB, err error, msgAndArgs ...any) { + if err != nil { + return + } + t.Helper() + t.Fatal(formatMsgAndArgs("Expected an error", msgAndArgs...)) +} + +// NoError asserts that an error is nil. +func NoError(t testing.TB, err error, msgAndArgs ...any) { + if err == nil { + return + } + t.Helper() + msg := formatMsgAndArgs("Did not expect an error but got:", msgAndArgs...) + t.Fatalf("%s\n%+v", msg, err) +} + +// True asserts that an expression is true. +func True(t testing.TB, ok bool, msgAndArgs ...any) { + if ok { + return + } + t.Helper() + t.Fatal(formatMsgAndArgs("Expected expression to be true", msgAndArgs...)) +} + +// False asserts that an expression is false. +func False(t testing.TB, ok bool, msgAndArgs ...any) { + if !ok { + return + } + t.Helper() + t.Fatal(formatMsgAndArgs("Expected expression to be false", msgAndArgs...)) +} + +// Panics asserts that the given function panics. +func Panics(t testing.TB, fn func(), msgAndArgs ...any) { + t.Helper() + defer func() { + if recover() == nil { + msg := formatMsgAndArgs("Expected function to panic", msgAndArgs...) + t.Fatal(msg) + } + }() + fn() +} + +// NotPanics asserts that the given function does not panic. +func NotPanics(t testing.TB, fn func(), msgAndArgs ...any) { + t.Helper() + defer func() { + if err := recover(); err != nil { + msg := formatMsgAndArgs("Expected function not to panic", msgAndArgs...) + t.Fatalf("%s\nPanic: %v", msg, err) + } + }() + fn() +} + +// Diff returns a unified diff of the string representation of two values. +func Diff[T any](before, after T, compareOptions ...CompareOption) string { + var lhss, rhss string + // Special case strings so we get nice diffs. + if l, ok := any(before).(string); ok { + lhss = l + "\n" + rhss = any(after).(string) + "\n" + } else { + ropts := expandCompareOptions(compareOptions...) + lhss = repr.String(before, ropts...) + "\n" + rhss = repr.String(after, ropts...) + "\n" + } + edits := myers.ComputeEdits("a.txt", lhss, rhss) + lines := strings.Split(fmt.Sprint(gotextdiff.ToUnified("expected.txt", "actual.txt", lhss, edits)), "\n") + if len(lines) < 3 { + return "" + } + return strings.Join(lines[3:], "\n") +} + +func formatMsgAndArgs(dflt string, msgAndArgs ...any) string { + if len(msgAndArgs) == 0 { + return dflt + } + format, ok := msgAndArgs[0].(string) + if !ok { + panic("message argument to assert function must be a fmt string") + } + return fmt.Sprintf(format, msgAndArgs[1:]...) +} + +func needlePosition(haystack, needle string) (quotedHaystack, quotedNeedle, positions string) { + quotedNeedle = strconv.Quote(needle) + quotedNeedle = quotedNeedle[1 : len(quotedNeedle)-1] + quotedHaystack = strconv.Quote(haystack) + rawPositions := strings.ReplaceAll(quotedHaystack, quotedNeedle, strings.Repeat("^", len(quotedNeedle))) + for _, rn := range rawPositions { + if rn != '^' { + positions += " " + } else { + positions += "^" + } + } + return +} + +func expandCompareOptions(options ...CompareOption) []repr.Option { + ropts := []repr.Option{repr.Indent(" ")} + for _, option := range options { + ropts = append(ropts, option()...) + } + return ropts +} + +func objectsAreEqual(expected, actual any, options ...CompareOption) bool { + if expected == nil || actual == nil { + return expected == actual + } + if exp, eok := expected.([]byte); eok { + if act, aok := actual.([]byte); aok { + return bytes.Equal(exp, act) + } + } + if exp, eok := expected.(string); eok { + if act, aok := actual.(string); aok { + return exp == act + } + } + + ropts := expandCompareOptions(options...) + expectedStr := repr.String(expected, ropts...) + actualStr := repr.String(actual, ropts...) + + return expectedStr == actualStr +} diff --git a/vendor/github.com/alecthomas/assert/v2/renovate.json5 b/vendor/github.com/alecthomas/assert/v2/renovate.json5 new file mode 100644 index 00000000..77c7b016 --- /dev/null +++ b/vendor/github.com/alecthomas/assert/v2/renovate.json5 @@ -0,0 +1,18 @@ +{ + $schema: "https://docs.renovatebot.com/renovate-schema.json", + extends: [ + "config:recommended", + ":semanticCommits", + ":semanticCommitTypeAll(chore)", + ":semanticCommitScope(deps)", + "group:allNonMajor", + "schedule:earlyMondays", // Run once a week. + ], + packageRules: [ + { + matchPackageNames: ["golangci-lint"], + matchManagers: ["hermit"], + enabled: false, + }, + ], +} diff --git a/vendor/github.com/alecthomas/repr/COPYING b/vendor/github.com/alecthomas/repr/COPYING new file mode 100644 index 00000000..e477b078 --- /dev/null +++ b/vendor/github.com/alecthomas/repr/COPYING @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Alec Thomas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/alecthomas/repr/README.md b/vendor/github.com/alecthomas/repr/README.md new file mode 100644 index 00000000..1957b2fc --- /dev/null +++ b/vendor/github.com/alecthomas/repr/README.md @@ -0,0 +1,90 @@ +# Python's repr() for Go [![](https://godoc.org/github.com/alecthomas/repr?status.svg)](http://godoc.org/github.com/alecthomas/repr) [![CircleCI](https://img.shields.io/circleci/project/github/alecthomas/repr.svg)](https://circleci.com/gh/alecthomas/repr) + +This package attempts to represent Go values in a form that can be used almost directly in Go source +code. + +Unfortunately, some values (such as pointers to basic types) can not be represented directly in Go. +These values will be represented as `&`. eg. `&23` + +## Example + +```go +type test struct { + S string + I int + A []int +} + +func main() { + repr.Print(&test{ + S: "String", + I: 123, + A: []int{1, 2, 3}, + }) +} +``` + +Outputs + +``` +&main.test{S: "String", I: 123, A: []int{1, 2, 3}} +``` + +## Why repr and not [pp](https://github.com/k0kubun/pp)? + +pp is designed for printing coloured output to consoles, with (seemingly?) no way to disable this. If you don't want coloured output (eg. for use in diffs, logs, etc.) repr is for you. + +## Why repr and not [go-spew](https://github.com/davecgh/go-spew)? + +Repr deliberately contains much less metadata about values. It is designed to (generally) be copyable directly into source code. + +Compare go-spew: + +```go +(parser.expression) (len=1 cap=1) { + (parser.alternative) (len=1 cap=1) { + ([]interface {}) (len=1 cap=1) { + (*parser.repitition)(0xc82000b220)({ + expression: (parser.expression) (len=2 cap=2) { + (parser.alternative) (len=1 cap=1) { + ([]interface {}) (len=1 cap=1) { + (parser.str) (len=1) "a" + } + }, + (parser.alternative) (len=1 cap=1) { + ([]interface {}) (len=1 cap=1) { + (*parser.self)(0x593ef0)({ + }) + } + } + } + }) + } + } +} +``` + +To repr: + +```go +parser.expression{ + parser.alternative{ + []interface {}{ + &parser.repitition{ + expression: parser.expression{ + parser.alternative{ + []interface {}{ + parser.str("a"), + }, + }, + parser.alternative{ + []interface {}{ + &parser.self{ }, + }, + }, + }, + }, + }, + }, +} +``` diff --git a/vendor/github.com/alecthomas/repr/renovate.json5 b/vendor/github.com/alecthomas/repr/renovate.json5 new file mode 100644 index 00000000..897864b8 --- /dev/null +++ b/vendor/github.com/alecthomas/repr/renovate.json5 @@ -0,0 +1,11 @@ +{ + $schema: "https://docs.renovatebot.com/renovate-schema.json", + extends: [ + "config:recommended", + ":semanticCommits", + ":semanticCommitTypeAll(chore)", + ":semanticCommitScope(deps)", + "group:allNonMajor", + "schedule:earlyMondays", // Run once a week. + ], +} diff --git a/vendor/github.com/alecthomas/repr/repr.go b/vendor/github.com/alecthomas/repr/repr.go new file mode 100644 index 00000000..c03eb067 --- /dev/null +++ b/vendor/github.com/alecthomas/repr/repr.go @@ -0,0 +1,442 @@ +// Package repr attempts to represent Go values in a form that can be copy-and-pasted into source +// code directly. +// +// Some values (such as pointers to basic types) can not be represented directly in +// Go. These values will be output as `&`. eg. `&23` +package repr + +import ( + "bytes" + "fmt" + "io" + "os" + "reflect" + "sort" + "strings" + "time" + "unsafe" +) + +var ( + // "Real" names of basic kinds, used to differentiate type aliases. + realKindName = map[reflect.Kind]string{ + reflect.Bool: "bool", + reflect.Int: "int", + reflect.Int8: "int8", + reflect.Int16: "int16", + reflect.Int32: "int32", + reflect.Int64: "int64", + reflect.Uint: "uint", + reflect.Uint8: "uint8", + reflect.Uint16: "uint16", + reflect.Uint32: "uint32", + reflect.Uint64: "uint64", + reflect.Uintptr: "uintptr", + reflect.Float32: "float32", + reflect.Float64: "float64", + reflect.Complex64: "complex64", + reflect.Complex128: "complex128", + reflect.Array: "array", + reflect.Chan: "chan", + reflect.Func: "func", + reflect.Map: "map", + reflect.Slice: "slice", + reflect.String: "string", + } + + goStringerType = reflect.TypeOf((*fmt.GoStringer)(nil)).Elem() + anyType = reflect.TypeOf((*any)(nil)).Elem() + + byteSliceType = reflect.TypeOf([]byte{}) +) + +// Default prints to os.Stdout with two space indentation. +var Default = New(os.Stdout, Indent(" ")) + +// An Option modifies the default behaviour of a Printer. +type Option func(o *Printer) + +// Indent output by this much. +func Indent(indent string) Option { return func(o *Printer) { o.indent = indent } } + +// NoIndent disables indenting. +func NoIndent() Option { return Indent("") } + +// OmitEmpty sets whether empty field members should be omitted from output. +func OmitEmpty(omitEmpty bool) Option { return func(o *Printer) { o.omitEmpty = omitEmpty } } + +// ExplicitTypes adds explicit typing to slice and map struct values that would normally be inferred by Go. +func ExplicitTypes(ok bool) Option { return func(o *Printer) { o.explicitTypes = true } } + +// IgnoreGoStringer disables use of the .GoString() method. +func IgnoreGoStringer() Option { return func(o *Printer) { o.ignoreGoStringer = true } } + +// IgnorePrivate disables private field members from output. +func IgnorePrivate() Option { return func(o *Printer) { o.ignorePrivate = true } } + +// ScalarLiterals forces the use of literals for scalars, rather than a string representation if available. +// +// For example, `time.Hour` will be printed as `time.Duration(3600000000000)` rather than `time.Duration(1h0m0s)`. +func ScalarLiterals() Option { return func(o *Printer) { o.useLiterals = true } } + +// Hide excludes fields of the given type from representation. +func Hide[T any]() Option { + return func(o *Printer) { + t := (*T)(nil) // A bit of skulduggery so we can Hide() interfaces. + rt := reflect.TypeOf(t).Elem() + o.exclude[rt] = true + } +} + +// AlwaysIncludeType always includes explicit type information for each item. +func AlwaysIncludeType() Option { return func(o *Printer) { o.alwaysIncludeType = true } } + +// Printer represents structs in a printable manner. +type Printer struct { + indent string + omitEmpty bool + ignoreGoStringer bool + ignorePrivate bool + alwaysIncludeType bool + explicitTypes bool + exclude map[reflect.Type]bool + w io.Writer + useLiterals bool +} + +// New creates a new Printer on w with the given Options. +func New(w io.Writer, options ...Option) *Printer { + p := &Printer{ + w: w, + indent: " ", + omitEmpty: true, + exclude: map[reflect.Type]bool{}, + } + for _, option := range options { + option(p) + } + return p +} + +func (p *Printer) nextIndent(indent string) string { + if p.indent != "" { + return indent + p.indent + } + return "" +} + +func (p *Printer) thisIndent(indent string) string { + if p.indent != "" { + return indent + } + return "" +} + +// Print the values. +func (p *Printer) Print(vs ...any) { + for i, v := range vs { + if i > 0 { + fmt.Fprint(p.w, " ") + } + p.reprValue(map[reflect.Value]bool{}, reflect.ValueOf(v), "", true, false) + } +} + +// Println prints each value on a new line. +func (p *Printer) Println(vs ...any) { + for i, v := range vs { + if i > 0 { + fmt.Fprint(p.w, " ") + } + p.reprValue(map[reflect.Value]bool{}, reflect.ValueOf(v), "", true, false) + } + fmt.Fprintln(p.w) +} + +// showType is true if struct types should be shown. isAnyValue is true if the containing value is an "any" type. +func (p *Printer) reprValue(seen map[reflect.Value]bool, v reflect.Value, indent string, showStructType bool, isAnyValue bool) { // nolint: gocyclo + if seen[v] { + fmt.Fprint(p.w, "...") + return + } + seen[v] = true + defer delete(seen, v) + + if v.Kind() == reflect.Invalid || (v.Kind() == reflect.Ptr || v.Kind() == reflect.Map || v.Kind() == reflect.Chan || v.Kind() == reflect.Slice || v.Kind() == reflect.Func || v.Kind() == reflect.Interface) && v.IsNil() { + fmt.Fprint(p.w, "nil") + return + } + t := v.Type() + + if t == byteSliceType { + fmt.Fprintf(p.w, "[]byte(%q)", v.Bytes()) + return + } + + // If we can't access a private field directly with reflection, try and do so via unsafe. + if !v.CanInterface() && v.CanAddr() { + uv := reflect.NewAt(t, unsafe.Pointer(v.UnsafeAddr())).Elem() + if uv.CanInterface() { + v = uv + } + } + // Attempt to use fmt.GoStringer interface. + if !p.ignoreGoStringer && t.Implements(goStringerType) && v.CanInterface() { + fmt.Fprint(p.w, v.Interface().(fmt.GoStringer).GoString()) + return + } + in := p.thisIndent(indent) + ni := p.nextIndent(indent) + switch v.Kind() { + case reflect.Slice, reflect.Array: + fmt.Fprintf(p.w, "%s{", substAny(v.Type())) + if v.Len() == 0 { + fmt.Fprint(p.w, "}") + } else { + if p.indent != "" { + fmt.Fprintf(p.w, "\n") + } + for i := 0; i < v.Len(); i++ { + e := v.Index(i) + fmt.Fprintf(p.w, "%s", ni) + p.reprValue(seen, e, ni, p.alwaysIncludeType || p.explicitTypes, v.Type().Elem() == anyType) + if p.indent != "" { + fmt.Fprintf(p.w, ",\n") + } else if i < v.Len()-1 { + fmt.Fprintf(p.w, ", ") + } + } + fmt.Fprintf(p.w, "%s}", in) + } + + case reflect.Chan: + fmt.Fprintf(p.w, "make(") + fmt.Fprintf(p.w, "%s", substAny(v.Type())) + fmt.Fprintf(p.w, ", %d)", v.Cap()) + + case reflect.Map: + fmt.Fprintf(p.w, "%s{", substAny(v.Type())) + if p.indent != "" && v.Len() != 0 { + fmt.Fprintf(p.w, "\n") + } + keys := v.MapKeys() + sort.Slice(keys, func(i, j int) bool { + return fmt.Sprint(keys[i]) < fmt.Sprint(keys[j]) + }) + for i, k := range keys { + kv := v.MapIndex(k) + fmt.Fprintf(p.w, "%s", ni) + p.reprValue(seen, k, ni, p.alwaysIncludeType || p.explicitTypes, v.Type().Key() == anyType) + fmt.Fprintf(p.w, ": ") + p.reprValue(seen, kv, ni, true, v.Type().Elem() == anyType) + if p.indent != "" { + fmt.Fprintf(p.w, ",\n") + } else if i < v.Len()-1 { + fmt.Fprintf(p.w, ", ") + } + } + fmt.Fprintf(p.w, "%s}", in) + + case reflect.Struct: + if td, ok := asTime(v); ok { + timeToGo(p.w, td) + } else { + if showStructType { + fmt.Fprintf(p.w, "%s{", substAny(v.Type())) + } else { + fmt.Fprint(p.w, "{") + } + if p.indent != "" && v.NumField() != 0 { + fmt.Fprintf(p.w, "\n") + } + previous := false + for i := 0; i < v.NumField(); i++ { + t := v.Type().Field(i) + if p.exclude[t.Type] { + continue + } + f := v.Field(i) + ft := f.Type() + // skip private fields + if p.ignorePrivate && !f.CanInterface() { + continue + } + if p.omitEmpty && (f.IsZero() || + ft.Kind() == reflect.Slice && f.Len() == 0 || + ft.Kind() == reflect.Map && f.Len() == 0) { + continue + } + if previous && p.indent == "" { + fmt.Fprintf(p.w, ", ") + } + previous = true + fmt.Fprintf(p.w, "%s%s: ", ni, t.Name) + p.reprValue(seen, f, ni, true, t.Type == anyType) + + // if private fields should be ignored, look up if a public + // field need to be displayed and breaks at the first public + // field found preventing from looping over all remaining + // fields. + // + // If no other field need to be displayed, continue and do + // not print a comma. + // + // This prevents from having a trailing comma if a private + // field ends a structure. + if p.ignorePrivate { + nc := false + for j := i + 1; j < v.NumField(); j++ { + if v.Field(j).CanInterface() { + nc = true + // exit for j loop + break + } + } + // Skip comma display if no remaining public field found. + if !nc { + continue + } + } + if p.indent != "" { + fmt.Fprintf(p.w, ",\n") + } + } + fmt.Fprintf(p.w, "%s}", indent) + } + case reflect.Ptr: + if v.IsNil() { + fmt.Fprintf(p.w, "nil") + return + } + if showStructType { + fmt.Fprintf(p.w, "&") + } + p.reprValue(seen, v.Elem(), indent, showStructType, false) + + case reflect.String: + if t.Name() != "string" || p.alwaysIncludeType { + fmt.Fprintf(p.w, "%s(%q)", t, v.String()) + } else { + fmt.Fprintf(p.w, "%q", v.String()) + } + + case reflect.Interface: + if v.IsNil() { + fmt.Fprintf(p.w, "%s(nil)", substAny(v.Type())) + } else { + p.reprValue(seen, v.Elem(), indent, true, true) + } + + case reflect.Func: + fmt.Fprint(p.w, substAny(v.Type())) + + default: + value := fmt.Sprintf("%v", v) + if p.useLiterals { + value = fmt.Sprintf("%#v", v) + } + if t.Name() != realKindName[t.Kind()] || p.alwaysIncludeType || isAnyValue { + fmt.Fprintf(p.w, "%s(%s)", t, value) + } else { + fmt.Fprintf(p.w, "%s", value) + } + } +} + +func asTime(v reflect.Value) (time.Time, bool) { + if !v.CanInterface() { + return time.Time{}, false + } + t, ok := v.Interface().(time.Time) + return t, ok +} + +// String returns a string representing v. +func String(v any, options ...Option) string { + w := bytes.NewBuffer(nil) + options = append([]Option{NoIndent()}, options...) + p := New(w, options...) + p.Print(v) + return w.String() +} + +func extractOptions(vs ...any) (args []any, options []Option) { + for _, v := range vs { + if o, ok := v.(Option); ok { + options = append(options, o) + } else { + args = append(args, v) + } + } + return +} + +// Println prints v to os.Stdout, one per line. +func Println(vs ...any) { + args, options := extractOptions(vs...) + New(os.Stdout, options...).Println(args...) +} + +// Print writes a representation of v to os.Stdout, separated by spaces. +func Print(vs ...any) { + args, options := extractOptions(vs...) + New(os.Stdout, options...).Print(args...) +} + +func timeToGo(w io.Writer, t time.Time) { + if t.IsZero() { + fmt.Fprint(w, "time.Time{}") + return + } + + var zone string + switch loc := t.Location(); loc { + case nil: + zone = "nil" + case time.UTC: + zone = "time.UTC" + case time.Local: + zone = "time.Local" + default: + n, off := t.Zone() + zone = fmt.Sprintf("time.FixedZone(%q, %d)", n, off) + } + y, m, d := t.Date() + fmt.Fprintf(w, `time.Date(%d, %d, %d, %d, %d, %d, %d, %s)`, y, m, d, t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), zone) +} + +// Replace "interface {}" with "any" +func substAny(t reflect.Type) string { + switch t.Kind() { + case reflect.Array: + return fmt.Sprintf("[%d]%s", t.Len(), substAny(t.Elem())) + + case reflect.Slice: + return "[]" + substAny(t.Elem()) + + case reflect.Map: + return "map[" + substAny(t.Key()) + "]" + substAny(t.Elem()) + + case reflect.Chan: + return fmt.Sprintf("%s %s", t.ChanDir(), substAny(t.Elem())) + + case reflect.Func: + in := []string{} + out := []string{} + for i := 0; i < t.NumIn(); i++ { + in = append(in, substAny(t.In(i))) + } + for i := 0; i < t.NumOut(); i++ { + out = append(out, substAny(t.Out(i))) + } + if len(out) == 0 { + return "func" + t.Name() + "(" + strings.Join(in, ", ") + ")" + } + return "func" + t.Name() + "(" + strings.Join(in, ", ") + ") (" + strings.Join(out, ", ") + ")" + } + + if t == anyType { + return "any" + } + return t.String() +} diff --git a/vendor/github.com/hexops/gotextdiff/LICENSE b/vendor/github.com/hexops/gotextdiff/LICENSE new file mode 100644 index 00000000..6a66aea5 --- /dev/null +++ b/vendor/github.com/hexops/gotextdiff/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/hexops/gotextdiff/README.md b/vendor/github.com/hexops/gotextdiff/README.md new file mode 100644 index 00000000..bfd49a0c --- /dev/null +++ b/vendor/github.com/hexops/gotextdiff/README.md @@ -0,0 +1,54 @@ +# gotextdiff - unified text diffing in Go Hexops logo + +This is a copy of the Go text diffing packages that [the official Go language server gopls uses internally](https://github.com/golang/tools/tree/master/internal/lsp/diff) to generate unified diffs. + +If you've previously tried to generate unified text diffs in Go (like the ones you see in Git and on GitHub), you may have found [github.com/sergi/go-diff](https://github.com/sergi/go-diff) which is a Go port of Neil Fraser's google-diff-match-patch code - however it [does not support unified diffs](https://github.com/sergi/go-diff/issues/57). + +This is arguably one of the best (and most maintained) unified text diffing packages in Go as of at least 2020. + +(All credit goes to [the Go authors](http://tip.golang.org/AUTHORS), I am merely re-publishing their work so others can use it.) + +## Example usage + +Import the packages: + +```Go +import ( + "github.com/hexops/gotextdiff" + "github.com/hexops/gotextdiff/myers" +) +``` + +Assuming you want to diff `a.txt` and `b.txt`, whose contents are stored in `aString` and `bString` then: + +```Go +edits := myers.ComputeEdits(span.URIFromPath("a.txt"), aString, bString) +diff := fmt.Sprint(gotextdiff.ToUnified("a.txt", "b.txt", aString, edits)) +``` + +`diff` will be a string like: + +```diff +--- a.txt ++++ b.txt +@@ -1,13 +1,28 @@ +-foo ++bar +``` + +## API compatability + +We will publish a new major version anytime the API changes in a backwards-incompatible way. Because the upstream is not being developed with this being a public package in mind, API breakages may occur more often than in other Go packages (but you can always continue using the old version thanks to Go modules.) + +## Alternatives + +- [github.com/andreyvit/diff](https://github.com/andreyvit/diff): Quick'n'easy string diffing functions for Golang based on github.com/sergi/go-diff. +- [github.com/kylelemons/godebug/diff](https://github.com/kylelemons/godebug/tree/master/diff): implements a linewise diff algorithm ([inactive](https://github.com/kylelemons/godebug/issues/22#issuecomment-524573477)). + +## Contributing + +We will only accept changes made [upstream](https://github.com/golang/tools/tree/master/internal/lsp/diff), please send any contributions to the upstream instead! Compared to the upstream, only import paths will be modified (to be non-`internal` so they are importable.) The only thing we add here is this README. + +## License + +See https://github.com/golang/tools/blob/master/LICENSE diff --git a/vendor/github.com/hexops/gotextdiff/diff.go b/vendor/github.com/hexops/gotextdiff/diff.go new file mode 100644 index 00000000..53e499bc --- /dev/null +++ b/vendor/github.com/hexops/gotextdiff/diff.go @@ -0,0 +1,159 @@ +// 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 gotextdiff supports a pluggable diff algorithm. +package gotextdiff + +import ( + "sort" + "strings" + + "github.com/hexops/gotextdiff/span" +) + +// TextEdit represents a change to a section of a document. +// The text within the specified span should be replaced by the supplied new text. +type TextEdit struct { + Span span.Span + NewText string +} + +// ComputeEdits is the type for a function that produces a set of edits that +// convert from the before content to the after content. +type ComputeEdits func(uri span.URI, before, after string) []TextEdit + +// SortTextEdits attempts to order all edits by their starting points. +// The sort is stable so that edits with the same starting point will not +// be reordered. +func SortTextEdits(d []TextEdit) { + // Use a stable sort to maintain the order of edits inserted at the same position. + sort.SliceStable(d, func(i int, j int) bool { + return span.Compare(d[i].Span, d[j].Span) < 0 + }) +} + +// ApplyEdits applies the set of edits to the before and returns the resulting +// content. +// It may panic or produce garbage if the edits are not valid for the provided +// before content. +func ApplyEdits(before string, edits []TextEdit) string { + // Preconditions: + // - all of the edits apply to before + // - and all the spans for each TextEdit have the same URI + if len(edits) == 0 { + return before + } + _, edits, _ = prepareEdits(before, edits) + after := strings.Builder{} + last := 0 + for _, edit := range edits { + start := edit.Span.Start().Offset() + if start > last { + after.WriteString(before[last:start]) + last = start + } + after.WriteString(edit.NewText) + last = edit.Span.End().Offset() + } + if last < len(before) { + after.WriteString(before[last:]) + } + return after.String() +} + +// LineEdits takes a set of edits and expands and merges them as necessary +// to ensure that there are only full line edits left when it is done. +func LineEdits(before string, edits []TextEdit) []TextEdit { + if len(edits) == 0 { + return nil + } + c, edits, partial := prepareEdits(before, edits) + if partial { + edits = lineEdits(before, c, edits) + } + return edits +} + +// prepareEdits returns a sorted copy of the edits +func prepareEdits(before string, edits []TextEdit) (*span.TokenConverter, []TextEdit, bool) { + partial := false + c := span.NewContentConverter("", []byte(before)) + copied := make([]TextEdit, len(edits)) + for i, edit := range edits { + edit.Span, _ = edit.Span.WithAll(c) + copied[i] = edit + partial = partial || + edit.Span.Start().Offset() >= len(before) || + edit.Span.Start().Column() > 1 || edit.Span.End().Column() > 1 + } + SortTextEdits(copied) + return c, copied, partial +} + +// lineEdits rewrites the edits to always be full line edits +func lineEdits(before string, c *span.TokenConverter, edits []TextEdit) []TextEdit { + adjusted := make([]TextEdit, 0, len(edits)) + current := TextEdit{Span: span.Invalid} + for _, edit := range edits { + if current.Span.IsValid() && edit.Span.Start().Line() <= current.Span.End().Line() { + // overlaps with the current edit, need to combine + // first get the gap from the previous edit + gap := before[current.Span.End().Offset():edit.Span.Start().Offset()] + // now add the text of this edit + current.NewText += gap + edit.NewText + // and then adjust the end position + current.Span = span.New(current.Span.URI(), current.Span.Start(), edit.Span.End()) + } else { + // does not overlap, add previous run (if there is one) + adjusted = addEdit(before, adjusted, current) + // and then remember this edit as the start of the next run + current = edit + } + } + // add the current pending run if there is one + return addEdit(before, adjusted, current) +} + +func addEdit(before string, edits []TextEdit, edit TextEdit) []TextEdit { + if !edit.Span.IsValid() { + return edits + } + // if edit is partial, expand it to full line now + start := edit.Span.Start() + end := edit.Span.End() + if start.Column() > 1 { + // prepend the text and adjust to start of line + delta := start.Column() - 1 + start = span.NewPoint(start.Line(), 1, start.Offset()-delta) + edit.Span = span.New(edit.Span.URI(), start, end) + edit.NewText = before[start.Offset():start.Offset()+delta] + edit.NewText + } + if start.Offset() >= len(before) && start.Line() > 1 && before[len(before)-1] != '\n' { + // after end of file that does not end in eol, so join to last line of file + // to do this we need to know where the start of the last line was + eol := strings.LastIndex(before, "\n") + if eol < 0 { + // file is one non terminated line + eol = 0 + } + delta := len(before) - eol + start = span.NewPoint(start.Line()-1, 1, start.Offset()-delta) + edit.Span = span.New(edit.Span.URI(), start, end) + edit.NewText = before[start.Offset():start.Offset()+delta] + edit.NewText + } + if end.Column() > 1 { + remains := before[end.Offset():] + eol := strings.IndexRune(remains, '\n') + if eol < 0 { + eol = len(remains) + } else { + eol++ + } + end = span.NewPoint(end.Line()+1, 1, end.Offset()+eol) + edit.Span = span.New(edit.Span.URI(), start, end) + edit.NewText = edit.NewText + remains[:eol] + } + edits = append(edits, edit) + return edits +} diff --git a/vendor/github.com/hexops/gotextdiff/myers/diff.go b/vendor/github.com/hexops/gotextdiff/myers/diff.go new file mode 100644 index 00000000..5e3e9236 --- /dev/null +++ b/vendor/github.com/hexops/gotextdiff/myers/diff.go @@ -0,0 +1,205 @@ +// 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" + + diff "github.com/hexops/gotextdiff" + "github.com/hexops/gotextdiff/span" +) + +// 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 + +func ComputeEdits(uri span.URI, before, after string) []diff.TextEdit { + ops := operations(splitLines(before), splitLines(after)) + edits := make([]diff.TextEdit, 0, len(ops)) + for _, op := range ops { + s := span.New(uri, span.NewPoint(op.I1+1, 1, 0), span.NewPoint(op.I2+1, 1, 0)) + switch op.Kind { + case diff.Delete: + // Delete: unformatted[i1:i2] is deleted. + edits = append(edits, diff.TextEdit{Span: s}) + case diff.Insert: + // Insert: formatted[j1:j2] is inserted at unformatted[i1:i1]. + if content := strings.Join(op.Content, ""); content != "" { + edits = append(edits, diff.TextEdit{Span: s, NewText: content}) + } + } + } + return edits +} + +type operation struct { + Kind diff.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 == diff.Insert { + 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: diff.Delete, + 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: diff.Insert, + 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 +} diff --git a/vendor/github.com/hexops/gotextdiff/span/parse.go b/vendor/github.com/hexops/gotextdiff/span/parse.go new file mode 100644 index 00000000..aa17c84e --- /dev/null +++ b/vendor/github.com/hexops/gotextdiff/span/parse.go @@ -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 span + +import ( + "strconv" + "strings" + "unicode/utf8" +) + +// Parse returns the location represented by the input. +// Only file paths are accepted, not URIs. +// The returned span will be normalized, and thus if printed may produce a +// different string. +func Parse(input string) Span { + // :0:0#0-0:0#0 + valid := input + var hold, offset int + hadCol := false + suf := rstripSuffix(input) + if suf.sep == "#" { + offset = suf.num + suf = rstripSuffix(suf.remains) + } + if suf.sep == ":" { + valid = suf.remains + hold = suf.num + hadCol = true + suf = rstripSuffix(suf.remains) + } + switch { + case suf.sep == ":": + return New(URIFromPath(suf.remains), NewPoint(suf.num, hold, offset), Point{}) + case suf.sep == "-": + // we have a span, fall out of the case to continue + default: + // separator not valid, rewind to either the : or the start + return New(URIFromPath(valid), NewPoint(hold, 0, offset), Point{}) + } + // only the span form can get here + // at this point we still don't know what the numbers we have mean + // if have not yet seen a : then we might have either a line or a column depending + // on whether start has a column or not + // we build an end point and will fix it later if needed + end := NewPoint(suf.num, hold, offset) + hold, offset = 0, 0 + suf = rstripSuffix(suf.remains) + if suf.sep == "#" { + offset = suf.num + suf = rstripSuffix(suf.remains) + } + if suf.sep != ":" { + // turns out we don't have a span after all, rewind + return New(URIFromPath(valid), end, Point{}) + } + valid = suf.remains + hold = suf.num + suf = rstripSuffix(suf.remains) + if suf.sep != ":" { + // line#offset only + return New(URIFromPath(valid), NewPoint(hold, 0, offset), end) + } + // we have a column, so if end only had one number, it is also the column + if !hadCol { + end = NewPoint(suf.num, end.v.Line, end.v.Offset) + } + return New(URIFromPath(suf.remains), NewPoint(suf.num, hold, offset), end) +} + +type suffix struct { + remains string + sep string + num int +} + +func rstripSuffix(input string) suffix { + if len(input) == 0 { + return suffix{"", "", -1} + } + remains := input + num := -1 + // first see if we have a number at the end + last := strings.LastIndexFunc(remains, func(r rune) bool { return r < '0' || r > '9' }) + if last >= 0 && last < len(remains)-1 { + number, err := strconv.ParseInt(remains[last+1:], 10, 64) + if err == nil { + num = int(number) + remains = remains[:last+1] + } + } + // now see if we have a trailing separator + r, w := utf8.DecodeLastRuneInString(remains) + if r != ':' && r != '#' && r == '#' { + return suffix{input, "", -1} + } + remains = remains[:len(remains)-w] + return suffix{remains, string(r), num} +} diff --git a/vendor/github.com/hexops/gotextdiff/span/span.go b/vendor/github.com/hexops/gotextdiff/span/span.go new file mode 100644 index 00000000..4d2ad098 --- /dev/null +++ b/vendor/github.com/hexops/gotextdiff/span/span.go @@ -0,0 +1,285 @@ +// 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 span contains support for representing with positions and ranges in +// text files. +package span + +import ( + "encoding/json" + "fmt" + "path" +) + +// Span represents a source code range in standardized form. +type Span struct { + v span +} + +// Point represents a single point within a file. +// In general this should only be used as part of a Span, as on its own it +// does not carry enough information. +type Point struct { + v point +} + +type span struct { + URI URI `json:"uri"` + Start point `json:"start"` + End point `json:"end"` +} + +type point struct { + Line int `json:"line"` + Column int `json:"column"` + Offset int `json:"offset"` +} + +// Invalid is a span that reports false from IsValid +var Invalid = Span{v: span{Start: invalidPoint.v, End: invalidPoint.v}} + +var invalidPoint = Point{v: point{Line: 0, Column: 0, Offset: -1}} + +// Converter is the interface to an object that can convert between line:column +// and offset forms for a single file. +type Converter interface { + //ToPosition converts from an offset to a line:column pair. + ToPosition(offset int) (int, int, error) + //ToOffset converts from a line:column pair to an offset. + ToOffset(line, col int) (int, error) +} + +func New(uri URI, start Point, end Point) Span { + s := Span{v: span{URI: uri, Start: start.v, End: end.v}} + s.v.clean() + return s +} + +func NewPoint(line, col, offset int) Point { + p := Point{v: point{Line: line, Column: col, Offset: offset}} + p.v.clean() + return p +} + +func Compare(a, b Span) int { + if r := CompareURI(a.URI(), b.URI()); r != 0 { + return r + } + if r := comparePoint(a.v.Start, b.v.Start); r != 0 { + return r + } + return comparePoint(a.v.End, b.v.End) +} + +func ComparePoint(a, b Point) int { + return comparePoint(a.v, b.v) +} + +func comparePoint(a, b point) int { + if !a.hasPosition() { + if a.Offset < b.Offset { + return -1 + } + if a.Offset > b.Offset { + return 1 + } + return 0 + } + if a.Line < b.Line { + return -1 + } + if a.Line > b.Line { + return 1 + } + if a.Column < b.Column { + return -1 + } + if a.Column > b.Column { + return 1 + } + return 0 +} + +func (s Span) HasPosition() bool { return s.v.Start.hasPosition() } +func (s Span) HasOffset() bool { return s.v.Start.hasOffset() } +func (s Span) IsValid() bool { return s.v.Start.isValid() } +func (s Span) IsPoint() bool { return s.v.Start == s.v.End } +func (s Span) URI() URI { return s.v.URI } +func (s Span) Start() Point { return Point{s.v.Start} } +func (s Span) End() Point { return Point{s.v.End} } +func (s *Span) MarshalJSON() ([]byte, error) { return json.Marshal(&s.v) } +func (s *Span) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &s.v) } + +func (p Point) HasPosition() bool { return p.v.hasPosition() } +func (p Point) HasOffset() bool { return p.v.hasOffset() } +func (p Point) IsValid() bool { return p.v.isValid() } +func (p *Point) MarshalJSON() ([]byte, error) { return json.Marshal(&p.v) } +func (p *Point) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &p.v) } +func (p Point) Line() int { + if !p.v.hasPosition() { + panic(fmt.Errorf("position not set in %v", p.v)) + } + return p.v.Line +} +func (p Point) Column() int { + if !p.v.hasPosition() { + panic(fmt.Errorf("position not set in %v", p.v)) + } + return p.v.Column +} +func (p Point) Offset() int { + if !p.v.hasOffset() { + panic(fmt.Errorf("offset not set in %v", p.v)) + } + return p.v.Offset +} + +func (p point) hasPosition() bool { return p.Line > 0 } +func (p point) hasOffset() bool { return p.Offset >= 0 } +func (p point) isValid() bool { return p.hasPosition() || p.hasOffset() } +func (p point) isZero() bool { + return (p.Line == 1 && p.Column == 1) || (!p.hasPosition() && p.Offset == 0) +} + +func (s *span) clean() { + //this presumes the points are already clean + if !s.End.isValid() || (s.End == point{}) { + s.End = s.Start + } +} + +func (p *point) clean() { + if p.Line < 0 { + p.Line = 0 + } + if p.Column <= 0 { + if p.Line > 0 { + p.Column = 1 + } else { + p.Column = 0 + } + } + if p.Offset == 0 && (p.Line > 1 || p.Column > 1) { + p.Offset = -1 + } +} + +// Format implements fmt.Formatter to print the Location in a standard form. +// The format produced is one that can be read back in using Parse. +func (s Span) Format(f fmt.State, c rune) { + fullForm := f.Flag('+') + preferOffset := f.Flag('#') + // we should always have a uri, simplify if it is file format + //TODO: make sure the end of the uri is unambiguous + uri := string(s.v.URI) + if c == 'f' { + uri = path.Base(uri) + } else if !fullForm { + uri = s.v.URI.Filename() + } + fmt.Fprint(f, uri) + if !s.IsValid() || (!fullForm && s.v.Start.isZero() && s.v.End.isZero()) { + return + } + // see which bits of start to write + printOffset := s.HasOffset() && (fullForm || preferOffset || !s.HasPosition()) + printLine := s.HasPosition() && (fullForm || !printOffset) + printColumn := printLine && (fullForm || (s.v.Start.Column > 1 || s.v.End.Column > 1)) + fmt.Fprint(f, ":") + if printLine { + fmt.Fprintf(f, "%d", s.v.Start.Line) + } + if printColumn { + fmt.Fprintf(f, ":%d", s.v.Start.Column) + } + if printOffset { + fmt.Fprintf(f, "#%d", s.v.Start.Offset) + } + // start is written, do we need end? + if s.IsPoint() { + return + } + // we don't print the line if it did not change + printLine = fullForm || (printLine && s.v.End.Line > s.v.Start.Line) + fmt.Fprint(f, "-") + if printLine { + fmt.Fprintf(f, "%d", s.v.End.Line) + } + if printColumn { + if printLine { + fmt.Fprint(f, ":") + } + fmt.Fprintf(f, "%d", s.v.End.Column) + } + if printOffset { + fmt.Fprintf(f, "#%d", s.v.End.Offset) + } +} + +func (s Span) WithPosition(c Converter) (Span, error) { + if err := s.update(c, true, false); err != nil { + return Span{}, err + } + return s, nil +} + +func (s Span) WithOffset(c Converter) (Span, error) { + if err := s.update(c, false, true); err != nil { + return Span{}, err + } + return s, nil +} + +func (s Span) WithAll(c Converter) (Span, error) { + if err := s.update(c, true, true); err != nil { + return Span{}, err + } + return s, nil +} + +func (s *Span) update(c Converter, withPos, withOffset bool) error { + if !s.IsValid() { + return fmt.Errorf("cannot add information to an invalid span") + } + if withPos && !s.HasPosition() { + if err := s.v.Start.updatePosition(c); err != nil { + return err + } + if s.v.End.Offset == s.v.Start.Offset { + s.v.End = s.v.Start + } else if err := s.v.End.updatePosition(c); err != nil { + return err + } + } + if withOffset && (!s.HasOffset() || (s.v.End.hasPosition() && !s.v.End.hasOffset())) { + if err := s.v.Start.updateOffset(c); err != nil { + return err + } + if s.v.End.Line == s.v.Start.Line && s.v.End.Column == s.v.Start.Column { + s.v.End.Offset = s.v.Start.Offset + } else if err := s.v.End.updateOffset(c); err != nil { + return err + } + } + return nil +} + +func (p *point) updatePosition(c Converter) error { + line, col, err := c.ToPosition(p.Offset) + if err != nil { + return err + } + p.Line = line + p.Column = col + return nil +} + +func (p *point) updateOffset(c Converter) error { + offset, err := c.ToOffset(p.Line, p.Column) + if err != nil { + return err + } + p.Offset = offset + return nil +} diff --git a/vendor/github.com/hexops/gotextdiff/span/token.go b/vendor/github.com/hexops/gotextdiff/span/token.go new file mode 100644 index 00000000..6f8b9b57 --- /dev/null +++ b/vendor/github.com/hexops/gotextdiff/span/token.go @@ -0,0 +1,194 @@ +// 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 span + +import ( + "fmt" + "go/token" +) + +// Range represents a source code range in token.Pos form. +// It also carries the FileSet that produced the positions, so that it is +// self contained. +type Range struct { + FileSet *token.FileSet + Start token.Pos + End token.Pos + Converter Converter +} + +type FileConverter struct { + file *token.File +} + +// TokenConverter is a Converter backed by a token file set and file. +// It uses the file set methods to work out the conversions, which +// makes it fast and does not require the file contents. +type TokenConverter struct { + FileConverter + fset *token.FileSet +} + +// NewRange creates a new Range from a FileSet and two positions. +// To represent a point pass a 0 as the end pos. +func NewRange(fset *token.FileSet, start, end token.Pos) Range { + return Range{ + FileSet: fset, + Start: start, + End: end, + } +} + +// NewTokenConverter returns an implementation of Converter backed by a +// token.File. +func NewTokenConverter(fset *token.FileSet, f *token.File) *TokenConverter { + return &TokenConverter{fset: fset, FileConverter: FileConverter{file: f}} +} + +// NewContentConverter returns an implementation of Converter for the +// given file content. +func NewContentConverter(filename string, content []byte) *TokenConverter { + fset := token.NewFileSet() + f := fset.AddFile(filename, -1, len(content)) + f.SetLinesForContent(content) + return NewTokenConverter(fset, f) +} + +// IsPoint returns true if the range represents a single point. +func (r Range) IsPoint() bool { + return r.Start == r.End +} + +// Span converts a Range to a Span that represents the Range. +// It will fill in all the members of the Span, calculating the line and column +// information. +func (r Range) Span() (Span, error) { + if !r.Start.IsValid() { + return Span{}, fmt.Errorf("start pos is not valid") + } + f := r.FileSet.File(r.Start) + if f == nil { + return Span{}, fmt.Errorf("file not found in FileSet") + } + return FileSpan(f, r.Converter, r.Start, r.End) +} + +// FileSpan returns a span within tok, using converter to translate between +// offsets and positions. +func FileSpan(tok *token.File, converter Converter, start, end token.Pos) (Span, error) { + var s Span + var err error + var startFilename string + startFilename, s.v.Start.Line, s.v.Start.Column, err = position(tok, start) + if err != nil { + return Span{}, err + } + s.v.URI = URIFromPath(startFilename) + if end.IsValid() { + var endFilename string + endFilename, s.v.End.Line, s.v.End.Column, err = position(tok, end) + if err != nil { + return Span{}, err + } + // In the presence of line directives, a single File can have sections from + // multiple file names. + if endFilename != startFilename { + return Span{}, fmt.Errorf("span begins in file %q but ends in %q", startFilename, endFilename) + } + } + s.v.Start.clean() + s.v.End.clean() + s.v.clean() + if converter != nil { + return s.WithOffset(converter) + } + if startFilename != tok.Name() { + return Span{}, fmt.Errorf("must supply Converter for file %q containing lines from %q", tok.Name(), startFilename) + } + return s.WithOffset(&FileConverter{tok}) +} + +func position(f *token.File, pos token.Pos) (string, int, int, error) { + off, err := offset(f, pos) + if err != nil { + return "", 0, 0, err + } + return positionFromOffset(f, off) +} + +func positionFromOffset(f *token.File, offset int) (string, int, int, error) { + if offset > f.Size() { + return "", 0, 0, fmt.Errorf("offset %v is past the end of the file %v", offset, f.Size()) + } + pos := f.Pos(offset) + p := f.Position(pos) + // TODO(golang/go#41029): Consider returning line, column instead of line+1, 1 if + // the file's last character is not a newline. + if offset == f.Size() { + return p.Filename, p.Line + 1, 1, nil + } + return p.Filename, p.Line, p.Column, nil +} + +// offset is a copy of the Offset function in go/token, but with the adjustment +// that it does not panic on invalid positions. +func offset(f *token.File, pos token.Pos) (int, error) { + if int(pos) < f.Base() || int(pos) > f.Base()+f.Size() { + return 0, fmt.Errorf("invalid pos") + } + return int(pos) - f.Base(), nil +} + +// Range converts a Span to a Range that represents the Span for the supplied +// File. +func (s Span) Range(converter *TokenConverter) (Range, error) { + s, err := s.WithOffset(converter) + if err != nil { + return Range{}, err + } + // go/token will panic if the offset is larger than the file's size, + // so check here to avoid panicking. + if s.Start().Offset() > converter.file.Size() { + return Range{}, fmt.Errorf("start offset %v is past the end of the file %v", s.Start(), converter.file.Size()) + } + if s.End().Offset() > converter.file.Size() { + return Range{}, fmt.Errorf("end offset %v is past the end of the file %v", s.End(), converter.file.Size()) + } + return Range{ + FileSet: converter.fset, + Start: converter.file.Pos(s.Start().Offset()), + End: converter.file.Pos(s.End().Offset()), + Converter: converter, + }, nil +} + +func (l *FileConverter) ToPosition(offset int) (int, int, error) { + _, line, col, err := positionFromOffset(l.file, offset) + return line, col, err +} + +func (l *FileConverter) ToOffset(line, col int) (int, error) { + if line < 0 { + return -1, fmt.Errorf("line is not valid") + } + lineMax := l.file.LineCount() + 1 + if line > lineMax { + return -1, fmt.Errorf("line is beyond end of file %v", lineMax) + } else if line == lineMax { + if col > 1 { + return -1, fmt.Errorf("column is beyond end of file") + } + // at the end of the file, allowing for a trailing eol + return l.file.Size(), nil + } + pos := lineStart(l.file, line) + if !pos.IsValid() { + return -1, fmt.Errorf("line is not in file") + } + // we assume that column is in bytes here, and that the first byte of a + // line is at column 1 + pos += token.Pos(col - 1) + return offset(l.file, pos) +} diff --git a/vendor/github.com/hexops/gotextdiff/span/token111.go b/vendor/github.com/hexops/gotextdiff/span/token111.go new file mode 100644 index 00000000..bf7a5406 --- /dev/null +++ b/vendor/github.com/hexops/gotextdiff/span/token111.go @@ -0,0 +1,39 @@ +// 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. + +// +build !go1.12 + +package span + +import ( + "go/token" +) + +// lineStart is the pre-Go 1.12 version of (*token.File).LineStart. For Go +// versions <= 1.11, we borrow logic from the analysisutil package. +// TODO(rstambler): Delete this file when we no longer support Go 1.11. +func lineStart(f *token.File, line int) token.Pos { + // Use binary search to find the start offset of this line. + + min := 0 // inclusive + max := f.Size() // exclusive + for { + offset := (min + max) / 2 + pos := f.Pos(offset) + posn := f.Position(pos) + if posn.Line == line { + return pos - (token.Pos(posn.Column) - 1) + } + + if min+1 >= max { + return token.NoPos + } + + if posn.Line < line { + min = offset + } else { + max = offset + } + } +} diff --git a/vendor/github.com/hexops/gotextdiff/span/token112.go b/vendor/github.com/hexops/gotextdiff/span/token112.go new file mode 100644 index 00000000..017aec9c --- /dev/null +++ b/vendor/github.com/hexops/gotextdiff/span/token112.go @@ -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. + +// +build go1.12 + +package span + +import ( + "go/token" +) + +// TODO(rstambler): Delete this file when we no longer support Go 1.11. +func lineStart(f *token.File, line int) token.Pos { + return f.LineStart(line) +} diff --git a/vendor/github.com/hexops/gotextdiff/span/uri.go b/vendor/github.com/hexops/gotextdiff/span/uri.go new file mode 100644 index 00000000..25049213 --- /dev/null +++ b/vendor/github.com/hexops/gotextdiff/span/uri.go @@ -0,0 +1,169 @@ +// 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 span + +import ( + "fmt" + "net/url" + "os" + "path" + "path/filepath" + "runtime" + "strings" + "unicode" +) + +const fileScheme = "file" + +// URI represents the full URI for a file. +type URI string + +func (uri URI) IsFile() bool { + return strings.HasPrefix(string(uri), "file://") +} + +// Filename returns the file path for the given URI. +// It is an error to call this on a URI that is not a valid filename. +func (uri URI) Filename() string { + filename, err := filename(uri) + if err != nil { + panic(err) + } + return filepath.FromSlash(filename) +} + +func filename(uri URI) (string, error) { + if uri == "" { + return "", nil + } + u, err := url.ParseRequestURI(string(uri)) + if err != nil { + return "", err + } + if u.Scheme != fileScheme { + return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri) + } + // If the URI is a Windows URI, we trim the leading "/" and lowercase + // the drive letter, which will never be case sensitive. + if isWindowsDriveURIPath(u.Path) { + u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:] + } + return u.Path, nil +} + +func URIFromURI(s string) URI { + if !strings.HasPrefix(s, "file://") { + return URI(s) + } + + if !strings.HasPrefix(s, "file:///") { + // VS Code sends URLs with only two slashes, which are invalid. golang/go#39789. + s = "file:///" + s[len("file://"):] + } + // Even though the input is a URI, it may not be in canonical form. VS Code + // in particular over-escapes :, @, etc. Unescape and re-encode to canonicalize. + path, err := url.PathUnescape(s[len("file://"):]) + if err != nil { + panic(err) + } + + // File URIs from Windows may have lowercase drive letters. + // Since drive letters are guaranteed to be case insensitive, + // we change them to uppercase to remain consistent. + // For example, file:///c:/x/y/z becomes file:///C:/x/y/z. + if isWindowsDriveURIPath(path) { + path = path[:1] + strings.ToUpper(string(path[1])) + path[2:] + } + u := url.URL{Scheme: fileScheme, Path: path} + return URI(u.String()) +} + +func CompareURI(a, b URI) int { + if equalURI(a, b) { + return 0 + } + if a < b { + return -1 + } + return 1 +} + +func equalURI(a, b URI) bool { + if a == b { + return true + } + // If we have the same URI basename, we may still have the same file URIs. + if !strings.EqualFold(path.Base(string(a)), path.Base(string(b))) { + return false + } + fa, err := filename(a) + if err != nil { + return false + } + fb, err := filename(b) + if err != nil { + return false + } + // Stat the files to check if they are equal. + infoa, err := os.Stat(filepath.FromSlash(fa)) + if err != nil { + return false + } + infob, err := os.Stat(filepath.FromSlash(fb)) + if err != nil { + return false + } + return os.SameFile(infoa, infob) +} + +// URIFromPath returns a span URI for the supplied file path. +// It will always have the file scheme. +func URIFromPath(path string) URI { + if path == "" { + return "" + } + // Handle standard library paths that contain the literal "$GOROOT". + // TODO(rstambler): The go/packages API should allow one to determine a user's $GOROOT. + const prefix = "$GOROOT" + if len(path) >= len(prefix) && strings.EqualFold(prefix, path[:len(prefix)]) { + suffix := path[len(prefix):] + path = runtime.GOROOT() + suffix + } + if !isWindowsDrivePath(path) { + if abs, err := filepath.Abs(path); err == nil { + path = abs + } + } + // Check the file path again, in case it became absolute. + if isWindowsDrivePath(path) { + path = "/" + strings.ToUpper(string(path[0])) + path[1:] + } + path = filepath.ToSlash(path) + u := url.URL{ + Scheme: fileScheme, + Path: path, + } + return URI(u.String()) +} + +// isWindowsDrivePath returns true if the file path is of the form used by +// Windows. We check if the path begins with a drive letter, followed by a ":". +// For example: C:/x/y/z. +func isWindowsDrivePath(path string) bool { + if len(path) < 3 { + return false + } + return unicode.IsLetter(rune(path[0])) && path[1] == ':' +} + +// isWindowsDriveURI returns true if the file URI is of the format used by +// Windows URIs. The url.Parse package does not specially handle Windows paths +// (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:"). +func isWindowsDriveURIPath(uri string) bool { + if len(uri) < 4 { + return false + } + return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' +} diff --git a/vendor/github.com/hexops/gotextdiff/span/utf16.go b/vendor/github.com/hexops/gotextdiff/span/utf16.go new file mode 100644 index 00000000..f06a2468 --- /dev/null +++ b/vendor/github.com/hexops/gotextdiff/span/utf16.go @@ -0,0 +1,91 @@ +// 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 span + +import ( + "fmt" + "unicode/utf16" + "unicode/utf8" +) + +// ToUTF16Column calculates the utf16 column expressed by the point given the +// supplied file contents. +// This is used to convert from the native (always in bytes) column +// representation and the utf16 counts used by some editors. +func ToUTF16Column(p Point, content []byte) (int, error) { + if !p.HasPosition() { + return -1, fmt.Errorf("ToUTF16Column: point is missing position") + } + if !p.HasOffset() { + return -1, fmt.Errorf("ToUTF16Column: point is missing offset") + } + offset := p.Offset() // 0-based + colZero := p.Column() - 1 // 0-based + if colZero == 0 { + // 0-based column 0, so it must be chr 1 + return 1, nil + } else if colZero < 0 { + return -1, fmt.Errorf("ToUTF16Column: column is invalid (%v)", colZero) + } + // work out the offset at the start of the line using the column + lineOffset := offset - colZero + if lineOffset < 0 || offset > len(content) { + return -1, fmt.Errorf("ToUTF16Column: offsets %v-%v outside file contents (%v)", lineOffset, offset, len(content)) + } + // Use the offset to pick out the line start. + // This cannot panic: offset > len(content) and lineOffset < offset. + start := content[lineOffset:] + + // Now, truncate down to the supplied column. + start = start[:colZero] + + // and count the number of utf16 characters + // in theory we could do this by hand more efficiently... + return len(utf16.Encode([]rune(string(start)))) + 1, nil +} + +// FromUTF16Column advances the point by the utf16 character offset given the +// supplied line contents. +// This is used to convert from the utf16 counts used by some editors to the +// native (always in bytes) column representation. +func FromUTF16Column(p Point, chr int, content []byte) (Point, error) { + if !p.HasOffset() { + return Point{}, fmt.Errorf("FromUTF16Column: point is missing offset") + } + // if chr is 1 then no adjustment needed + if chr <= 1 { + return p, nil + } + if p.Offset() >= len(content) { + return p, fmt.Errorf("FromUTF16Column: offset (%v) greater than length of content (%v)", p.Offset(), len(content)) + } + remains := content[p.Offset():] + // scan forward the specified number of characters + for count := 1; count < chr; count++ { + if len(remains) <= 0 { + return Point{}, fmt.Errorf("FromUTF16Column: chr goes beyond the content") + } + r, w := utf8.DecodeRune(remains) + if r == '\n' { + // Per the LSP spec: + // + // > If the character value is greater than the line length it + // > defaults back to the line length. + break + } + remains = remains[w:] + if r >= 0x10000 { + // a two point rune + count++ + // if we finished in a two point rune, do not advance past the first + if count >= chr { + break + } + } + p.v.Column += w + p.v.Offset += w + } + return p, nil +} diff --git a/vendor/github.com/hexops/gotextdiff/unified.go b/vendor/github.com/hexops/gotextdiff/unified.go new file mode 100644 index 00000000..b7d85cfc --- /dev/null +++ b/vendor/github.com/hexops/gotextdiff/unified.go @@ -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 gotextdiff + +import ( + "fmt" + "strings" +) + +// 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 ( + // Delete is the operation kind for a line that is present in the input + // but not in the output. + Delete OpKind = iota + // Insert is the operation kind for a line that is new in the output. + Insert + // Equal is the operation kind for a line that is the same in the input and + // output, often used to provide context around edited lines. + Equal +) + +// String returns a human readable representation of an OpKind. It is not +// intended for machine processing. +func (k OpKind) String() string { + switch k { + case Delete: + return "delete" + case Insert: + return "insert" + case Equal: + return "equal" + default: + panic("unknown operation kind") + } +} + +const ( + edge = 3 + gap = edge * 2 +) + +// ToUnified takes a file contents and a sequence of edits, and calculates +// a unified diff that represents those edits. +func ToUnified(from, to string, content string, edits []TextEdit) Unified { + u := Unified{ + From: from, + To: to, + } + if len(edits) == 0 { + return u + } + c, edits, partial := prepareEdits(content, edits) + if partial { + edits = lineEdits(content, c, edits) + } + lines := splitLines(content) + var h *Hunk + last := 0 + toLine := 0 + for _, edit := range edits { + start := edit.Span.Start().Line() - 1 + end := edit.Span.End().Line() - 1 + 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+edge) + 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-edge, start) + h.FromLine -= delta + h.ToLine -= delta + } + last = start + for i := start; i < end; i++ { + h.Lines = append(h.Lines, Line{Kind: Delete, Content: lines[i]}) + last++ + } + if edit.NewText != "" { + for _, line := range splitLines(edit.NewText) { + h.Lines = append(h.Lines, Line{Kind: Insert, Content: line}) + toLine++ + } + } + } + if h != nil { + // add the edge to the final hunk + addEqualLines(h, lines, last, last+edge) + u.Hunks = append(u.Hunks, h) + } + return u +} + +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: Equal, Content: lines[i]}) + delta++ + } + return delta +} + +// Format 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) Format(f fmt.State, r rune) { + if len(u.Hunks) == 0 { + return + } + fmt.Fprintf(f, "--- %s\n", u.From) + fmt.Fprintf(f, "+++ %s\n", u.To) + for _, hunk := range u.Hunks { + fromCount, toCount := 0, 0 + for _, l := range hunk.Lines { + switch l.Kind { + case Delete: + fromCount++ + case Insert: + toCount++ + default: + fromCount++ + toCount++ + } + } + fmt.Fprint(f, "@@") + if fromCount > 1 { + fmt.Fprintf(f, " -%d,%d", hunk.FromLine, fromCount) + } else { + fmt.Fprintf(f, " -%d", hunk.FromLine) + } + if toCount > 1 { + fmt.Fprintf(f, " +%d,%d", hunk.ToLine, toCount) + } else { + fmt.Fprintf(f, " +%d", hunk.ToLine) + } + fmt.Fprint(f, " @@\n") + for _, l := range hunk.Lines { + switch l.Kind { + case Delete: + fmt.Fprintf(f, "-%s", l.Content) + case Insert: + fmt.Fprintf(f, "+%s", l.Content) + default: + fmt.Fprintf(f, " %s", l.Content) + } + if !strings.HasSuffix(l.Content, "\n") { + fmt.Fprintf(f, "\n\\ No newline at end of file\n") + } + } + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index f78c7b51..c0b02a4e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -4,6 +4,12 @@ github.com/JaderDias/movingmedian # github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 ## explicit; go 1.12 github.com/aclements/go-moremath/mathx +# github.com/alecthomas/assert/v2 v2.11.0 +## explicit; go 1.18 +github.com/alecthomas/assert/v2 +# github.com/alecthomas/repr v0.4.0 +## explicit; go 1.18 +github.com/alecthomas/repr # github.com/beorn7/perks v1.0.1 ## explicit; go 1.11 github.com/beorn7/perks/quantile @@ -83,6 +89,11 @@ github.com/gorilla/handlers # github.com/gorilla/mux v1.8.1 ## explicit; go 1.20 github.com/gorilla/mux +# github.com/hexops/gotextdiff v1.0.3 +## explicit; go 1.16 +github.com/hexops/gotextdiff +github.com/hexops/gotextdiff/myers +github.com/hexops/gotextdiff/span # github.com/lomik/og-rek v0.0.0-20170411191824-628eefeb8d80 ## explicit github.com/lomik/og-rek