Repository: expr-lang/expr
Branch: master
Commit: b90e77c64fb9
Files: 230
Total size: 2.8 MB
Directory structure:
gitextract_ewtnex4r/
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── images/
│ │ └── demo.tape
│ ├── scripts/
│ │ └── coverage.mjs
│ └── workflows/
│ ├── build.yml
│ ├── check.yml
│ ├── diff.yml
│ ├── fuzz.yml
│ └── test.yml
├── .gitignore
├── LICENSE
├── README.md
├── SECURITY.md
├── ast/
│ ├── dump.go
│ ├── find.go
│ ├── find_test.go
│ ├── node.go
│ ├── print.go
│ ├── print_test.go
│ ├── visitor.go
│ └── visitor_test.go
├── bench_test.go
├── builtin/
│ ├── builtin.go
│ ├── builtin_test.go
│ ├── function.go
│ ├── lib.go
│ ├── utils.go
│ └── validation.go
├── checker/
│ ├── checker.go
│ ├── checker_bench_test.go
│ ├── checker_test.go
│ ├── info.go
│ ├── info_test.go
│ └── nature/
│ ├── nature.go
│ └── utils.go
├── compiler/
│ ├── compiler.go
│ └── compiler_test.go
├── conf/
│ ├── config.go
│ └── env.go
├── debug/
│ ├── debugger.go
│ ├── go.mod
│ └── go.sum
├── docgen/
│ ├── README.md
│ ├── docgen.go
│ ├── docgen_test.go
│ └── markdown.go
├── docs/
│ ├── configuration.md
│ ├── environment.md
│ ├── functions.md
│ ├── getting-started.md
│ ├── language-definition.md
│ ├── patch.md
│ └── visitor.md
├── expr.go
├── expr_test.go
├── file/
│ ├── error.go
│ ├── location.go
│ ├── source.go
│ └── source_test.go
├── go.mod
├── internal/
│ ├── deref/
│ │ ├── deref.go
│ │ └── deref_test.go
│ ├── difflib/
│ │ ├── difflib.go
│ │ └── difflib_test.go
│ ├── ring/
│ │ ├── ring.go
│ │ └── ring_test.go
│ ├── spew/
│ │ ├── bypass.go
│ │ ├── bypasssafe.go
│ │ ├── common.go
│ │ ├── common_test.go
│ │ ├── config.go
│ │ ├── doc.go
│ │ ├── dump.go
│ │ ├── dump_test.go
│ │ ├── dumpcgo_test.go
│ │ ├── dumpnocgo_test.go
│ │ ├── example_test.go
│ │ ├── format.go
│ │ ├── format_test.go
│ │ ├── internal_test.go
│ │ ├── internalunsafe_test.go
│ │ ├── spew.go
│ │ ├── spew_test.go
│ │ └── testdata/
│ │ └── dumpcgo.go
│ └── testify/
│ ├── assert/
│ │ ├── assertion_compare.go
│ │ ├── assertion_compare_test.go
│ │ ├── assertion_format.go
│ │ ├── assertion_format.go.tmpl
│ │ ├── assertion_forward.go
│ │ ├── assertion_forward.go.tmpl
│ │ ├── assertion_order.go
│ │ ├── assertion_order_test.go
│ │ ├── assertions.go
│ │ ├── assertions_test.go
│ │ ├── doc.go
│ │ ├── errors.go
│ │ ├── forward_assertions.go
│ │ ├── forward_assertions_test.go
│ │ ├── http_assertions.go
│ │ ├── http_assertions_test.go
│ │ └── internal/
│ │ └── unsafetests/
│ │ ├── doc.go
│ │ └── unsafetests_test.go
│ └── require/
│ ├── doc.go
│ ├── forward_requirements.go
│ ├── forward_requirements_test.go
│ ├── require.go
│ ├── require.go.tmpl
│ ├── require_forward.go
│ ├── require_forward.go.tmpl
│ ├── requirements.go
│ └── requirements_test.go
├── optimizer/
│ ├── const_expr.go
│ ├── count_any.go
│ ├── count_any_test.go
│ ├── count_threshold.go
│ ├── count_threshold_test.go
│ ├── filter_first.go
│ ├── filter_last.go
│ ├── filter_len.go
│ ├── filter_map.go
│ ├── filter_map_test.go
│ ├── fold.go
│ ├── fold_test.go
│ ├── in_array.go
│ ├── in_range.go
│ ├── optimizer.go
│ ├── optimizer_test.go
│ ├── predicate_combination.go
│ ├── sum_array.go
│ ├── sum_array_test.go
│ ├── sum_map.go
│ ├── sum_map_test.go
│ ├── sum_range.go
│ └── sum_range_test.go
├── parser/
│ ├── bench_test.go
│ ├── lexer/
│ │ ├── lexer.go
│ │ ├── lexer_test.go
│ │ ├── state.go
│ │ ├── token.go
│ │ └── utils.go
│ ├── operator/
│ │ └── operator.go
│ ├── parser.go
│ ├── parser_test.go
│ └── utils/
│ └── utils.go
├── patcher/
│ ├── operator_override.go
│ ├── value/
│ │ ├── bench_test.go
│ │ ├── value.go
│ │ ├── value_example_test.go
│ │ └── value_test.go
│ ├── with_context.go
│ ├── with_context_test.go
│ ├── with_timezone.go
│ └── with_timezone_test.go
├── repl/
│ ├── go.mod
│ ├── go.sum
│ └── repl.go
├── test/
│ ├── bench/
│ │ └── bench_call_test.go
│ ├── coredns/
│ │ ├── coredns.go
│ │ └── coredns_test.go
│ ├── crowdsec/
│ │ ├── crowdsec.go
│ │ ├── crowdsec_test.go
│ │ └── funcs.go
│ ├── deref/
│ │ └── deref_test.go
│ ├── examples/
│ │ ├── examples_test.go
│ │ └── markdown.go
│ ├── fuzz/
│ │ ├── fuzz_corpus.sh
│ │ ├── fuzz_corpus.txt
│ │ ├── fuzz_env.go
│ │ ├── fuzz_expr.dict
│ │ └── fuzz_test.go
│ ├── gen/
│ │ ├── env.go
│ │ ├── gen.go
│ │ ├── gen_test.go
│ │ └── utils.go
│ ├── interface/
│ │ ├── interface_method_test.go
│ │ └── interface_test.go
│ ├── issues/
│ │ ├── 461/
│ │ │ └── issue_test.go
│ │ ├── 567/
│ │ │ └── issue_test.go
│ │ ├── 688/
│ │ │ └── issue_test.go
│ │ ├── 723/
│ │ │ └── issue_test.go
│ │ ├── 730/
│ │ │ └── issue_test.go
│ │ ├── 739/
│ │ │ └── issue_test.go
│ │ ├── 756/
│ │ │ └── issue_test.go
│ │ ├── 785/
│ │ │ └── issue_test.go
│ │ ├── 817/
│ │ │ └── issue_test.go
│ │ ├── 819/
│ │ │ └── issue_test.go
│ │ ├── 823/
│ │ │ └── issue_test.go
│ │ ├── 830/
│ │ │ └── issue_test.go
│ │ ├── 836/
│ │ │ └── issue_test.go
│ │ ├── 840/
│ │ │ └── issue_test.go
│ │ ├── 844/
│ │ │ └── issue_test.go
│ │ ├── 854/
│ │ │ └── issue_test.go
│ │ ├── 857/
│ │ │ └── issue_test.go
│ │ ├── 888/
│ │ │ └── issue_test.go
│ │ ├── 924/
│ │ │ └── issue_test.go
│ │ └── 934/
│ │ └── issue_test.go
│ ├── mock/
│ │ └── mock.go
│ ├── operator/
│ │ ├── issues584/
│ │ │ └── issues584_test.go
│ │ └── operator_test.go
│ ├── patch/
│ │ ├── change_ident_test.go
│ │ ├── patch_count_test.go
│ │ ├── patch_test.go
│ │ └── set_type/
│ │ └── set_type_test.go
│ ├── pipes/
│ │ └── pipes_test.go
│ ├── playground/
│ │ ├── data.go
│ │ └── env.go
│ └── time/
│ └── time_test.go
├── testdata/
│ ├── crash.txt
│ ├── crowdsec.json
│ ├── examples.md
│ └── generated.txt
├── types/
│ ├── types.go
│ └── types_test.go
└── vm/
├── debug.go
├── debug_off.go
├── debug_test.go
├── func_types/
│ └── main.go
├── func_types[generated].go
├── opcodes.go
├── program.go
├── program_test.go
├── runtime/
│ ├── helpers/
│ │ └── main.go
│ ├── helpers[generated].go
│ ├── helpers_test.go
│ ├── runtime.go
│ └── sort.go
├── utils.go
├── vm.go
├── vm_bench_test.go
└── vm_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
*\[generated\].go linguist-language=txt
================================================
FILE: .github/FUNDING.yml
================================================
github: antonmedv
================================================
FILE: .github/images/demo.tape
================================================
Set Shell zsh
Sleep 500ms
Type "repl"
Enter
Sleep 500ms
Type "1..9 | filter("
Sleep 500ms
Type "# "
Sleep 500ms
Type "% 2 == 0) | map("
Sleep 500ms
Type "# ^ 2"
Sleep 500ms
Type ")"
Enter
Sleep 1s
Type "de"
Sleep 500ms
Type "bug"
Enter
Sleep 1.5s
Enter 50
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Enter
Sleep 1.5s
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Escape
Type "OB"
Enter
Sleep 2s
Escape
Type "OB"
Escape
Type "OB"
Ctrl+C
Sleep 1s
Ctrl+D
Ctrl+D
================================================
FILE: .github/scripts/coverage.mjs
================================================
#!/usr/bin/env zx
const expected = 90
const exclude = [
'expr/test', // We do not need to test the test package.
'checker/mock', // Mocks only used for testing.
'vm/func_types', // Generated files.
'vm/runtime/helpers', // Generated files.
'internal/difflib', // Test dependency. This is vendored dependency, and ideally we also have good tests for it.
'internal/spew', // Test dependency.
'internal/testify', // Test dependency.
'patcher/value', // Contains a lot of repeating code. Ideally we should have a test for it.
'pro', // Expr Pro is not a part of the main codebase.
]
cd(path.resolve(__dirname, '..', '..'))
await spinner('Running tests', async () => {
await $`go test -coverprofile=coverage.out -coverpkg=github.com/expr-lang/expr/... ./...`
const coverage = fs.readFileSync('coverage.out').toString()
.split('\n')
.filter(line => {
for (const ex of exclude)
if (line.includes(ex)) return false
return true
})
.join('\n')
fs.writeFileSync('coverage.out', coverage)
await $`go tool cover -html=coverage.out -o coverage.html`
})
const cover = await $({verbose: true})`go tool cover -func=coverage.out`
const total = +cover.stdout.match(/total:\s+\(statements\)\s+(\d+\.\d+)%/)[1]
if (total < expected) {
echo(chalk.red(`Coverage is too low: ${total}% < ${expected}% (expected)`))
process.exit(1)
} else {
echo(`Coverage is good: ${chalk.green(total + '%')} >= ${expected}% (expected)`)
}
================================================
FILE: .github/workflows/build.yml
================================================
name: build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-versions: [ '1.18', '1.22', '1.24', '1.25', '1.26' ]
go-arch: [ '386' ]
steps:
- uses: actions/checkout@v3
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Build
run: GOARCH=${{ matrix.go-arch }} go build
================================================
FILE: .github/workflows/check.yml
================================================
name: check
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Go 1.18
uses: actions/setup-go@v4
with:
go-version: 1.18
- name: Test
run: npx zx .github/scripts/coverage.mjs
================================================
FILE: .github/workflows/diff.yml
================================================
name: diff
on:
pull_request:
branches: [ master ]
jobs:
bench:
runs-on: ubuntu-latest
steps:
- name: Setup Go 1.18
uses: actions/setup-go@v4
with:
go-version: 1.18
- name: Install benchstat
# NOTE: benchstat@latest requires go 1.23 since 2025-02-14 - this is the last go 1.18 ref
# https://cs.opensource.google/go/x/perf/+/c95ad7d5b636f67d322a7e4832e83103d0fdd292
run: go install golang.org/x/perf/cmd/benchstat@884df5810d2850d775c2cb4885a7ea339128a17d
- uses: actions/checkout@v3
- name: Benchmark new code
run: go test -bench=. -benchmem -run=^$ -count=10 -timeout=30m | tee /tmp/new.txt
- name: Checkout master
uses: actions/checkout@v3
with:
ref: master
- name: Benchmark master
run: go test -bench=. -benchmem -run=^$ -count=10 -timeout=30m | tee /tmp/old.txt
- name: Diff
run: benchstat /tmp/old.txt /tmp/new.txt
================================================
FILE: .github/workflows/fuzz.yml
================================================
name: fuzz
on: [pull_request]
permissions: {}
jobs:
fuzzing:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Build Fuzzers
id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
with:
oss-fuzz-project-name: 'expr'
language: 'go'
- name: Run Fuzzers
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
oss-fuzz-project-name: 'expr'
language: 'go'
fuzz-seconds: 600
output-sarif: true
- name: Upload Crash
uses: actions/upload-artifact@v4
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
path: ./out/artifacts
- name: Upload Sarif
if: always() && steps.build.outcome == 'success'
uses: github/codeql-action/upload-sarif@v3
with:
# Path to SARIF file relative to the root of the repository
sarif_file: cifuzz-sarif/results.sarif
checkout_path: cifuzz-sarif
================================================
FILE: .github/workflows/test.yml
================================================
name: test
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-versions: [ '1.18', '1.19', '1.20', '1.21', '1.22', '1.23', '1.24', '1.25', '1.26' ]
steps:
- uses: actions/checkout@v3
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Test
run: go test ./...
debug:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Go 1.18
uses: actions/setup-go@v4
with:
go-version: 1.18
- name: Test
run: go test -tags=expr_debug -run=TestDebugger -v ./vm
race:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Go 1.21
uses: actions/setup-go@v4
with:
go-version: 1.21
- name: Test
run: go test -race .
================================================
FILE: .gitignore
================================================
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
*.html
custom_tests.json
pro/
test/avs/
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Anton Medvedev
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.
================================================
FILE: README.md
================================================
")
}
assert.Equal(t, strings.Trim(tt.err, "\n"), err.Error())
})
}
}
func TestCheck_FloatVsInt(t *testing.T) {
tree, err := parser.Parse(`Int + Float`)
require.NoError(t, err)
typ, err := checker.Check(tree, conf.New(mock.Env{}))
assert.NoError(t, err)
assert.Equal(t, typ.Kind(), reflect.Float64)
}
func TestCheck_IntSums(t *testing.T) {
tree, err := parser.Parse(`Uint32 + Int32`)
require.NoError(t, err)
typ, err := checker.Check(tree, conf.New(mock.Env{}))
assert.NoError(t, err)
assert.Equal(t, typ.Kind(), reflect.Int)
}
func TestVisitor_ConstantNode(t *testing.T) {
tree, err := parser.Parse(`re("[a-z]")`)
require.NoError(t, err)
regexValue := regexp.MustCompile("[a-z]")
constNode := &ast.ConstantNode{Value: regexValue}
ast.Patch(&tree.Node, constNode)
_, err = checker.Check(tree, nil)
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(regexValue), tree.Node.Type())
}
func TestCheck_AsBool(t *testing.T) {
tree, err := parser.Parse(`1+2`)
require.NoError(t, err)
config := &conf.Config{}
expr.AsBool()(config)
_, err = checker.Check(tree, config)
assert.Error(t, err)
assert.Equal(t, "expected bool, but got int", err.Error())
}
func TestCheck_AsInt64(t *testing.T) {
tree, err := parser.Parse(`true`)
require.NoError(t, err)
config := &conf.Config{}
expr.AsInt64()(config)
_, err = checker.Check(tree, config)
assert.Error(t, err)
assert.Equal(t, "expected int64, but got bool", err.Error())
}
func TestCheck_TaggedFieldName(t *testing.T) {
tree, err := parser.Parse(`foo.bar`)
require.NoError(t, err)
config := conf.CreateNew()
expr.Env(struct {
X struct {
Y bool `expr:"bar"`
} `expr:"foo"`
}{})(config)
expr.AsBool()(config)
_, err = checker.Check(tree, config)
assert.NoError(t, err)
}
func TestCheck_NoConfig(t *testing.T) {
tree, err := parser.Parse(`any`)
require.NoError(t, err)
_, err = checker.Check(tree, conf.CreateNew())
assert.NoError(t, err)
}
func TestCheck_AllowUndefinedVariables(t *testing.T) {
type Env struct {
A int
}
tree, err := parser.Parse(`Any + fn()`)
require.NoError(t, err)
config := conf.New(Env{})
expr.AllowUndefinedVariables()(config)
_, err = checker.Check(tree, config)
assert.NoError(t, err)
}
func TestCheck_AllowUndefinedVariables_DefaultType(t *testing.T) {
env := map[string]bool{}
tree, err := parser.Parse(`Any`)
require.NoError(t, err)
config := conf.New(env)
expr.AllowUndefinedVariables()(config)
expr.AsBool()(config)
_, err = checker.Check(tree, config)
assert.NoError(t, err)
}
func TestCheck_AllowUndefinedVariables_OptionalChaining(t *testing.T) {
type Env struct{}
tree, err := parser.Parse("Not?.A.B == nil")
require.NoError(t, err)
config := conf.New(Env{})
expr.AllowUndefinedVariables()(config)
_, err = checker.Check(tree, config)
assert.NoError(t, err)
}
func TestCheck_PointerNode(t *testing.T) {
_, err := checker.Check(&parser.Tree{Node: &ast.PointerNode{}}, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot use pointer accessor outside predicate")
}
func TestCheck_TypeWeights(t *testing.T) {
types := map[string]any{
"Uint": uint(1),
"Uint8": uint8(2),
"Uint16": uint16(3),
"Uint32": uint32(4),
"Uint64": uint64(5),
"Int": 6,
"Int8": int8(7),
"Int16": int16(8),
"Int32": int32(9),
"Int64": int64(10),
"Float32": float32(11),
"Float64": float64(12),
}
c := new(checker.Checker)
for a := range types {
for b := range types {
tree, err := parser.Parse(fmt.Sprintf("%s + %s", a, b))
require.NoError(t, err)
config := conf.New(types)
_, err = c.Check(tree, config)
require.NoError(t, err)
}
}
}
func TestCheck_works_with_nil_types(t *testing.T) {
env := map[string]any{
"null": nil,
}
tree, err := parser.Parse("null")
require.NoError(t, err)
_, err = checker.Check(tree, conf.New(env))
require.NoError(t, err)
}
func TestCheck_cast_to_expected_works_with_interface(t *testing.T) {
t.Run("float64", func(t *testing.T) {
type Env struct {
Any any
}
tree, err := parser.Parse("Any")
require.NoError(t, err)
config := conf.New(Env{})
expr.AsFloat64()(config)
expr.AsAny()(config)
_, err = checker.Check(tree, config)
require.NoError(t, err)
})
t.Run("kind", func(t *testing.T) {
env := map[string]any{
"Any": any("foo"),
}
tree, err := parser.Parse("Any")
require.NoError(t, err)
config := conf.New(env)
expr.AsKind(reflect.String)(config)
_, err = checker.Check(tree, config)
require.NoError(t, err)
})
}
func TestCheck_operator_in_works_with_interfaces(t *testing.T) {
tree, err := parser.Parse(`'Tom' in names`)
require.NoError(t, err)
config := conf.New(nil)
expr.AllowUndefinedVariables()(config)
_, err = checker.Check(tree, config)
require.NoError(t, err)
}
func TestCheck_Function_types_are_checked(t *testing.T) {
add := expr.Function(
"add",
func(p ...any) (any, error) {
out := 0
for _, each := range p {
out += each.(int)
}
return out, nil
},
new(func(int) int),
new(func(int, int) int),
new(func(int, int, int) int),
new(func(...int) int),
)
config := conf.CreateNew()
add(config)
c := new(checker.Checker)
tests := []string{
"add(1)",
"add(1, 2)",
"add(1, 2, 3)",
"add(1, 2, 3, 4)",
}
for _, test := range tests {
t.Run(test, func(t *testing.T) {
tree, err := parser.Parse(test)
require.NoError(t, err)
_, err = c.Check(tree, config)
require.NoError(t, err)
require.Equal(t, reflect.Int, tree.Node.Type().Kind())
})
}
t.Run("errors", func(t *testing.T) {
tree, err := parser.Parse("add(1, '2')")
require.NoError(t, err)
_, err = c.Check(tree, config)
require.Error(t, err)
require.Equal(t, "cannot use string as argument (type int) to call add (1:8)\n | add(1, '2')\n | .......^", err.Error())
})
}
func TestCheck_Function_without_types(t *testing.T) {
add := expr.Function(
"add",
func(p ...any) (any, error) {
out := 0
for _, each := range p {
out += each.(int)
}
return out, nil
},
)
tree, err := parser.Parse("add(1, 2, 3)")
require.NoError(t, err)
config := conf.CreateNew()
add(config)
_, err = checker.Check(tree, config)
require.NoError(t, err)
require.Equal(t, reflect.Interface, tree.Node.Type().Kind())
}
func TestCheck_dont_panic_on_nil_arguments_for_builtins(t *testing.T) {
tests := []string{
"len(nil)",
"abs(nil)",
"int(nil)",
"float(nil)",
}
for _, test := range tests {
t.Run(test, func(t *testing.T) {
tree, err := parser.Parse(test)
require.NoError(t, err)
_, err = checker.Check(tree, conf.New(nil))
require.Error(t, err)
})
}
}
func TestCheck_do_not_override_params_for_functions(t *testing.T) {
env := map[string]any{
"foo": func(p string) string {
return "foo"
},
}
config := conf.New(env)
expr.Function(
"bar",
func(p ...any) (any, error) {
return p[0].(string), nil
},
new(func(string) string),
)(config)
config.Check()
t.Run("func from env", func(t *testing.T) {
tree, err := parser.Parse("foo(1)")
require.NoError(t, err)
_, err = checker.Check(tree, config)
require.Error(t, err)
require.Contains(t, err.Error(), "cannot use int as argument")
})
t.Run("func from function", func(t *testing.T) {
tree, err := parser.Parse("bar(1)")
require.NoError(t, err)
_, err = checker.Check(tree, config)
require.Error(t, err)
require.Contains(t, err.Error(), "cannot use int as argument")
})
}
func TestCheck_env_keyword(t *testing.T) {
env := map[string]any{
"num": 42,
"str": "foo",
"name": "str",
}
tests := []struct {
input string
want reflect.Kind
}{
{`$env['str']`, reflect.String},
{`$env['num']`, reflect.Int},
{`$env[name]`, reflect.Interface},
}
c := new(checker.Checker)
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
tree, err := parser.Parse(test.input)
require.NoError(t, err)
rtype, err := c.Check(tree, conf.New(env))
require.NoError(t, err)
require.True(t, rtype.Kind() == test.want, fmt.Sprintf("expected %s, got %s", test.want, rtype.Kind()))
})
}
}
func TestCheck_builtin_without_call(t *testing.T) {
tests := []struct {
input string
err string
}{
{`len + 1`, "invalid operation: + (mismatched types func(...interface {}) (interface {}, error) and int) (1:5)\n | len + 1\n | ....^"},
{`string.A`, "type func(interface {}) string has no field A (1:8)\n | string.A\n | .......^"},
}
c := new(checker.Checker)
for _, test := range tests {
t.Run(test.input, func(t *testing.T) {
tree, err := parser.Parse(test.input)
require.NoError(t, err)
_, err = c.Check(tree, conf.New(nil))
require.Error(t, err)
require.Equal(t, test.err, err.Error())
})
}
}
func TestCheck_EmbeddedInterface(t *testing.T) {
t.Run("embedded interface lookup returns compile-error not panic", func(t *testing.T) {
type Env struct {
context.Context
Country string
}
type Wrapper struct {
Ctx Env
}
config := conf.New(Wrapper{
Ctx: Env{
Context: context.Background(),
Country: "TR",
},
})
expr.WithContext("Ctx")(config)
_, err := checker.ParseCheck("Ctx.C", config)
require.Error(t, err)
require.Contains(t, err.Error(), "has no field C")
})
}
func TestCheck_types(t *testing.T) {
env := types.Map{
"foo": types.Map{
"bar": types.Map{
"baz": types.String,
types.Extra: types.String,
},
},
"arr": types.Array(types.Map{
"value": types.String,
}),
types.Extra: types.Any,
}
noerr := "no error"
tests := []struct {
code string
err string
}{
{`unknown`, noerr},
{`[unknown + 42, another_unknown + "foo"]`, noerr},
{`foo.bar.baz > 0`, `invalid operation: > (mismatched types string and int)`},
{`foo.unknown.baz`, `unknown field unknown (1:5)`},
{`foo.bar.unknown`, noerr},
{`foo.bar.unknown + 42`, `invalid operation: + (mismatched types string and int)`},
{`[foo] | map(.unknown)`, `unknown field unknown`},
{`[foo] | map(.bar) | filter(.baz)`, `predicate should return boolean (got string)`},
{`arr | filter(.value > 0)`, `invalid operation: > (mismatched types string and int)`},
{`arr | filter(.value contains "a") | filter(.value == 0)`, `invalid operation: == (mismatched types string and int)`},
}
c := new(checker.Checker)
for _, test := range tests {
t.Run(test.code, func(t *testing.T) {
tree, err := parser.Parse(test.code)
require.NoError(t, err)
config := conf.New(env)
_, err = c.Check(tree, config)
if test.err == noerr {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), test.err)
}
})
}
}
================================================
FILE: checker/info.go
================================================
package checker
import (
"reflect"
"github.com/expr-lang/expr/ast"
. "github.com/expr-lang/expr/checker/nature"
"github.com/expr-lang/expr/vm"
)
func FieldIndex(c *Cache, env Nature, node ast.Node) (bool, []int, string) {
switch n := node.(type) {
case *ast.IdentifierNode:
if idx, ok := env.FieldIndex(c, n.Value); ok {
return true, idx, n.Value
}
case *ast.MemberNode:
base := n.Node.Nature().Deref(c)
if base.Kind == reflect.Struct {
if prop, ok := n.Property.(*ast.StringNode); ok {
if idx, ok := base.FieldIndex(c, prop.Value); ok {
return true, idx, prop.Value
}
}
}
}
return false, nil, ""
}
func MethodIndex(c *Cache, env Nature, node ast.Node) (bool, int, string) {
switch n := node.(type) {
case *ast.IdentifierNode:
if env.Kind == reflect.Struct {
if m, ok := env.Get(c, n.Value); ok && m.TypeData != nil {
return m.Method, m.MethodIndex, n.Value
}
}
case *ast.MemberNode:
if name, ok := n.Property.(*ast.StringNode); ok {
base := n.Node.Type()
if base != nil && base.Kind() != reflect.Interface {
if m, ok := base.MethodByName(name.Value); ok {
return true, m.Index, name.Value
}
}
}
}
return false, 0, ""
}
func TypedFuncIndex(fn reflect.Type, method bool) (int, bool) {
if fn == nil {
return 0, false
}
if fn.Kind() != reflect.Func {
return 0, false
}
// OnCallTyped doesn't work for functions with variadic arguments.
if fn.IsVariadic() {
return 0, false
}
// OnCallTyped doesn't work named function, like `type MyFunc func() int`.
if fn.PkgPath() != "" { // If PkgPath() is not empty, it means that function is named.
return 0, false
}
fnNumIn := fn.NumIn()
fnInOffset := 0
if method {
fnNumIn--
fnInOffset = 1
}
funcTypes:
for i := range vm.FuncTypes {
if i == 0 {
continue
}
typed := reflect.ValueOf(vm.FuncTypes[i]).Elem().Type()
if typed.Kind() != reflect.Func {
continue
}
if typed.NumOut() != fn.NumOut() {
continue
}
for j := 0; j < typed.NumOut(); j++ {
if typed.Out(j) != fn.Out(j) {
continue funcTypes
}
}
if typed.NumIn() != fnNumIn {
continue
}
for j := 0; j < typed.NumIn(); j++ {
if typed.In(j) != fn.In(j+fnInOffset) {
continue funcTypes
}
}
return i, true
}
return 0, false
}
func IsFastFunc(fn reflect.Type, method bool) bool {
if fn == nil {
return false
}
if fn.Kind() != reflect.Func {
return false
}
numIn := 1
if method {
numIn = 2
}
if fn.IsVariadic() &&
fn.NumIn() == numIn &&
fn.NumOut() == 1 &&
fn.Out(0).Kind() == reflect.Interface {
rest := fn.In(fn.NumIn() - 1) // function has only one param for functions and two for methods
if rest != nil && rest.Kind() == reflect.Slice && rest.Elem().Kind() == reflect.Interface {
return true
}
}
return false
}
================================================
FILE: checker/info_test.go
================================================
package checker_test
import (
"reflect"
"testing"
"time"
"github.com/expr-lang/expr/internal/testify/require"
"github.com/expr-lang/expr/checker"
"github.com/expr-lang/expr/test/mock"
)
func TestTypedFuncIndex(t *testing.T) {
fn := func() time.Duration {
return 1 * time.Second
}
index, ok := checker.TypedFuncIndex(reflect.TypeOf(fn), false)
require.True(t, ok)
require.Equal(t, 1, index)
}
func TestTypedFuncIndex_excludes_named_functions(t *testing.T) {
var fn mock.MyFunc
_, ok := checker.TypedFuncIndex(reflect.TypeOf(fn), false)
require.False(t, ok)
}
================================================
FILE: checker/nature/nature.go
================================================
package nature
import (
"fmt"
"reflect"
"time"
"github.com/expr-lang/expr/builtin"
"github.com/expr-lang/expr/internal/deref"
)
var (
intType = reflect.TypeOf(0)
floatType = reflect.TypeOf(float64(0))
arrayType = reflect.TypeOf([]any{})
byteSliceType = reflect.TypeOf([]byte{})
timeType = reflect.TypeOf(time.Time{})
durationType = reflect.TypeOf(time.Duration(0))
builtinInt = map[reflect.Type]struct{}{
reflect.TypeOf(int(0)): {},
reflect.TypeOf(int8(0)): {},
reflect.TypeOf(int16(0)): {},
reflect.TypeOf(int32(0)): {},
reflect.TypeOf(int64(0)): {},
reflect.TypeOf(uintptr(0)): {},
reflect.TypeOf(uint(0)): {},
reflect.TypeOf(uint8(0)): {},
reflect.TypeOf(uint16(0)): {},
reflect.TypeOf(uint32(0)): {},
reflect.TypeOf(uint64(0)): {},
}
builtinFloat = map[reflect.Type]struct{}{
reflect.TypeOf(float32(0)): {},
reflect.TypeOf(float64(0)): {},
}
)
type NatureCheck int
const (
_ NatureCheck = iota
BoolCheck
StringCheck
IntegerCheck
NumberCheck
MapCheck
ArrayCheck
TimeCheck
DurationCheck
)
type Nature struct {
// The order of the fields matter, check alignment before making changes.
Type reflect.Type // Type of the value. If nil, then value is unknown.
Kind reflect.Kind // Kind of the value.
*TypeData
// Ref is a reference used for multiple, disjoint purposes. When the Nature
// is for a:
// - Predicate: then Ref is the nature of the Out of the predicate.
// - Array-like types: then Ref is the Elem nature of array type (usually Type is []any, but ArrayOf can be any nature).
Ref *Nature
Nil bool // If value is nil.
Strict bool // If map is types.StrictMap.
Method bool // If value retrieved from method. Usually used to determine amount of in arguments.
IsInteger bool // If it's a builtin integer or unsigned integer type.
IsFloat bool // If it's a builtin float type.
}
type TypeData struct {
methodset *methodset // optional to avoid the map in *Cache
*structData
// map-only data
Fields map[string]Nature // Fields of map type.
DefaultMapValue *Nature // Default value of map type.
// callable-only data
Func *builtin.Function // Used to pass function type from callee to CallNode.
MethodIndex int // Index of method in type.
inElem, outZero *Nature
numIn, numOut int
isVariadic bool
isVariadicSet bool
numInSet bool
numOutSet bool
}
// Cache is a shared cache of type information. It is only used in the stages
// where type information becomes relevant, so packages like ast, parser, types,
// and lexer do not need to use the cache because they don't need any service
// from the Nature type, they only describe. However, when receiving a Nature
// from one of those packages, the cache must be set immediately.
type Cache struct {
methods map[reflect.Type]*methodset
structs map[reflect.Type]Nature
}
// NatureOf returns a Nature describing "i". If "i" is nil then it returns a
// Nature describing the value "nil".
func (c *Cache) NatureOf(i any) Nature {
// reflect.TypeOf(nil) returns nil, but in FromType we want to differentiate
// what nil means for us
if i == nil {
return Nature{Nil: true}
}
return c.FromType(reflect.TypeOf(i))
}
// FromType returns a Nature describing a value of type "t". If "t" is nil then
// it returns a Nature describing an unknown value.
func (c *Cache) FromType(t reflect.Type) Nature {
if t == nil {
return Nature{}
}
var td *TypeData
var isInteger, isFloat bool
k := t.Kind()
switch k {
case reflect.Struct:
// c can be nil when we call the package function FromType, which uses a
// nil *Cache to call this method.
if c != nil {
return c.getStruct(t)
}
fallthrough
case reflect.Func:
td = new(TypeData)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
_, isInteger = builtinInt[t]
case reflect.Float32, reflect.Float64:
_, isFloat = builtinFloat[t]
}
return Nature{
Type: t,
Kind: k,
TypeData: td,
IsInteger: isInteger,
IsFloat: isFloat,
}
}
func (c *Cache) getStruct(t reflect.Type) Nature {
if c.structs == nil {
c.structs = map[reflect.Type]Nature{}
} else if nt, ok := c.structs[t]; ok {
return nt
}
nt := Nature{
Type: t,
Kind: reflect.Struct,
TypeData: &TypeData{
structData: &structData{
rType: t,
numField: t.NumField(),
anonIdx: -1, // do not lookup embedded fields yet
},
},
}
c.structs[t] = nt
return nt
}
func (c *Cache) getMethodset(t reflect.Type, k reflect.Kind) *methodset {
if t == nil || c == nil {
return nil
}
if c.methods == nil {
c.methods = map[reflect.Type]*methodset{
t: nil,
}
} else if s, ok := c.methods[t]; ok {
return s
}
numMethod := t.NumMethod()
if numMethod < 1 {
c.methods[t] = nil // negative cache
return nil
}
s := &methodset{
rType: t,
kind: k,
numMethod: numMethod,
}
c.methods[t] = s
return s
}
// NatureOf calls NatureOf on a nil *Cache. See the comment on Cache.
func NatureOf(i any) Nature {
var c *Cache
return c.NatureOf(i)
}
// FromType calls FromType on a nil *Cache. See the comment on Cache.
func FromType(t reflect.Type) Nature {
var c *Cache
return c.FromType(t)
}
func ArrayFromType(c *Cache, t reflect.Type) Nature {
elem := c.FromType(t)
nt := c.FromType(arrayType)
nt.Ref = &elem
return nt
}
func (n *Nature) IsAny(c *Cache) bool {
return n.Type != nil && n.Kind == reflect.Interface && n.NumMethods(c) == 0
}
func (n *Nature) IsUnknown(c *Cache) bool {
return n.Type == nil && !n.Nil || n.IsAny(c)
}
func (n *Nature) String() string {
if n.Type != nil {
return n.Type.String()
}
return "unknown"
}
func (n *Nature) Deref(c *Cache) Nature {
t, _, changed := deref.TypeKind(n.Type, n.Kind)
if !changed {
return *n
}
return c.FromType(t)
}
func (n *Nature) Key(c *Cache) Nature {
if n.Kind == reflect.Map {
return c.FromType(n.Type.Key())
}
return Nature{}
}
func (n *Nature) Elem(c *Cache) Nature {
switch n.Kind {
case reflect.Ptr:
return c.FromType(n.Type.Elem())
case reflect.Map:
if n.TypeData != nil && n.DefaultMapValue != nil {
return *n.DefaultMapValue
}
return c.FromType(n.Type.Elem())
case reflect.Slice, reflect.Array:
if n.Ref != nil {
return *n.Ref
}
return c.FromType(n.Type.Elem())
}
return Nature{}
}
func (n *Nature) AssignableTo(nt Nature) bool {
if n.Nil {
switch nt.Kind {
case reflect.Pointer, reflect.Interface, reflect.Chan, reflect.Func,
reflect.Map, reflect.Slice:
// nil can be assigned to these kinds
return true
}
}
if n.Type == nil || nt.Type == nil ||
n.Kind != nt.Kind && nt.Kind != reflect.Interface {
return false
}
return n.Type.AssignableTo(nt.Type)
}
func (n *Nature) getMethodset(c *Cache) *methodset {
if n.TypeData != nil && n.TypeData.methodset != nil {
return n.TypeData.methodset
}
s := c.getMethodset(n.Type, n.Kind)
if n.TypeData != nil {
n.TypeData.methodset = s // cache locally if possible
}
return s
}
func (n *Nature) NumMethods(c *Cache) int {
if s := n.getMethodset(c); s != nil {
return s.numMethod
}
return 0
}
func (n *Nature) MethodByName(c *Cache, name string) (Nature, bool) {
if s := n.getMethodset(c); s != nil {
if m := s.method(c, name); m != nil {
return m.nature, true
}
}
return Nature{}, false
}
func (n *Nature) NumIn() int {
if n.numInSet {
return n.numIn
}
n.numInSet = true
n.numIn = n.Type.NumIn()
return n.numIn
}
func (n *Nature) InElem(c *Cache, i int) Nature {
if n.inElem == nil {
n2 := c.FromType(n.Type.In(i))
n2 = n2.Elem(c)
n.inElem = &n2
}
return *n.inElem
}
func (n *Nature) In(c *Cache, i int) Nature {
return c.FromType(n.Type.In(i))
}
func (n *Nature) IsFirstArgUnknown(c *Cache) bool {
if n.Type != nil {
n2 := c.FromType(n.Type.In(0))
return n2.IsUnknown(c)
}
return false
}
func (n *Nature) NumOut() int {
if n.numOutSet {
return n.numOut
}
n.numOutSet = true
n.numOut = n.Type.NumOut()
return n.numOut
}
func (n *Nature) Out(c *Cache, i int) Nature {
if i != 0 {
return n.out(c, i)
}
if n.outZero != nil {
return *n.outZero
}
nt := n.out(c, 0)
n.outZero = &nt
return nt
}
func (n *Nature) out(c *Cache, i int) Nature {
if n.Type == nil {
return Nature{}
}
return c.FromType(n.Type.Out(i))
}
func (n *Nature) IsVariadic() bool {
if n.isVariadicSet {
return n.isVariadic
}
n.isVariadicSet = true
n.isVariadic = n.Type.IsVariadic()
return n.isVariadic
}
func (n *Nature) FieldByName(c *Cache, name string) (Nature, bool) {
if n.Kind != reflect.Struct {
return Nature{}, false
}
var sd *structData
if n.TypeData != nil && n.structData != nil {
sd = n.structData
} else {
sd = c.getStruct(n.Type).structData
}
if sf := sd.structField(c, nil, name); sf != nil {
return sf.Nature, true
}
return Nature{}, false
}
func (n *Nature) IsFastMap() bool {
return n.Type != nil &&
n.Type.Kind() == reflect.Map &&
n.Type.Key().Kind() == reflect.String &&
n.Type.Elem().Kind() == reflect.Interface
}
func (n *Nature) Get(c *Cache, name string) (Nature, bool) {
if n.Kind == reflect.Map && n.TypeData != nil {
f, ok := n.Fields[name]
return f, ok
}
return n.getSlow(c, name)
}
func (n *Nature) getSlow(c *Cache, name string) (Nature, bool) {
if nt, ok := n.MethodByName(c, name); ok {
return nt, true
}
t, k, changed := deref.TypeKind(n.Type, n.Kind)
if k == reflect.Struct {
var sd *structData
if changed {
sd = c.getStruct(t).structData
} else {
sd = n.structData
}
if sf := sd.structField(c, nil, name); sf != nil {
return sf.Nature, true
}
}
return Nature{}, false
}
func (n *Nature) FieldIndex(c *Cache, name string) ([]int, bool) {
if n.Kind != reflect.Struct {
return nil, false
}
if sf := n.structField(c, nil, name); sf != nil {
return sf.Index, true
}
return nil, false
}
func (n *Nature) All(c *Cache) map[string]Nature {
table := make(map[string]Nature)
if n.Type == nil {
return table
}
for i := 0; i < n.NumMethods(c); i++ {
method := n.Type.Method(i)
nt := c.FromType(method.Type)
if nt.TypeData == nil {
nt.TypeData = new(TypeData)
}
nt.Method = true
nt.MethodIndex = method.Index
table[method.Name] = nt
}
t := deref.Type(n.Type)
switch t.Kind() {
case reflect.Struct:
for name, nt := range StructFields(c, t) {
if _, ok := table[name]; ok {
continue
}
table[name] = nt
}
case reflect.Map:
if n.TypeData != nil {
for key, nt := range n.Fields {
if _, ok := table[key]; ok {
continue
}
table[key] = nt
}
}
}
return table
}
func (n *Nature) IsNumber() bool {
return n.IsInteger || n.IsFloat
}
func (n *Nature) PromoteNumericNature(c *Cache, rhs Nature) Nature {
if n.IsUnknown(c) || rhs.IsUnknown(c) {
return Nature{}
}
if n.IsFloat || rhs.IsFloat {
return c.FromType(floatType)
}
return c.FromType(intType)
}
func (n *Nature) IsTime() bool {
return n.Type == timeType
}
func (n *Nature) IsDuration() bool {
return n.Type == durationType
}
func (n *Nature) IsBool() bool {
return n.Kind == reflect.Bool
}
func (n *Nature) IsString() bool {
return n.Kind == reflect.String
}
func (n *Nature) IsByteSlice() bool {
return n.Type == byteSliceType
}
func (n *Nature) IsArray() bool {
return n.Kind == reflect.Slice || n.Kind == reflect.Array
}
func (n *Nature) IsMap() bool {
return n.Kind == reflect.Map
}
func (n *Nature) IsStruct() bool {
return n.Kind == reflect.Struct
}
func (n *Nature) IsFunc() bool {
return n.Kind == reflect.Func
}
func (n *Nature) IsPointer() bool {
return n.Kind == reflect.Ptr
}
func (n *Nature) IsAnyOf(cs ...NatureCheck) bool {
var result bool
for i := 0; i < len(cs) && !result; i++ {
switch cs[i] {
case BoolCheck:
result = n.IsBool()
case StringCheck:
result = n.IsString()
case IntegerCheck:
result = n.IsInteger
case NumberCheck:
result = n.IsNumber()
case MapCheck:
result = n.IsMap()
case ArrayCheck:
result = n.IsArray()
case TimeCheck:
result = n.IsTime()
case DurationCheck:
result = n.IsDuration()
default:
panic(fmt.Sprintf("unknown check value %d", cs[i]))
}
}
return result
}
func (n *Nature) ComparableTo(c *Cache, rhs Nature) bool {
return n.IsUnknown(c) || rhs.IsUnknown(c) ||
n.Nil || rhs.Nil ||
n.IsNumber() && rhs.IsNumber() ||
n.IsDuration() && rhs.IsDuration() ||
n.IsTime() && rhs.IsTime() ||
n.IsArray() && rhs.IsArray() ||
n.AssignableTo(rhs)
}
func (n *Nature) MaybeCompatible(c *Cache, rhs Nature, cs ...NatureCheck) bool {
nIsUnknown := n.IsUnknown(c)
rshIsUnknown := rhs.IsUnknown(c)
return nIsUnknown && rshIsUnknown ||
nIsUnknown && rhs.IsAnyOf(cs...) ||
rshIsUnknown && n.IsAnyOf(cs...)
}
func (n *Nature) MakeArrayOf(c *Cache) Nature {
nt := c.FromType(arrayType)
nt.Ref = n
return nt
}
================================================
FILE: checker/nature/utils.go
================================================
package nature
import (
"reflect"
"github.com/expr-lang/expr/internal/deref"
)
func fieldName(fieldName string, tag reflect.StructTag) (string, bool) {
switch taggedName := tag.Get("expr"); taggedName {
case "-":
return "", false
case "":
return fieldName, true
default:
return taggedName, true
}
}
type structData struct {
rType reflect.Type
fields map[string]*structField
numField, ownIdx, anonIdx int
curParent, curChild *structData
curChildIndex []int
}
type structField struct {
Nature
Index []int
}
func (s *structData) finished() bool {
return s.ownIdx >= s.numField && // no own fields left to visit
s.anonIdx >= s.numField && // no embedded fields to visit
s.curChild == nil // no child in process of visiting
}
func (s *structData) structField(c *Cache, parentEmbed *structData, name string) *structField {
if s.fields == nil {
if s.numField > 0 {
s.fields = make(map[string]*structField, s.numField)
}
} else if f := s.fields[name]; f != nil {
return f
}
if s.finished() {
return nil
}
// Lookup own fields first.
for ; s.ownIdx < s.numField; s.ownIdx++ {
field := s.rType.Field(s.ownIdx)
if field.Anonymous && s.anonIdx < 0 {
// start iterating anon fields on the first instead of zero
s.anonIdx = s.ownIdx
}
if !field.IsExported() {
continue
}
fName, ok := fieldName(field.Name, field.Tag)
if !ok || fName == "" {
// name can still be empty for a type created at runtime with
// reflect
continue
}
nt := c.FromType(field.Type)
sf := &structField{
Nature: nt,
Index: field.Index,
}
s.fields[fName] = sf
if parentEmbed != nil {
parentEmbed.trySet(fName, sf)
}
if fName == name {
return sf
}
}
if s.curChild != nil {
sf := s.findInEmbedded(c, parentEmbed, s.curChild, s.curChildIndex, name)
if sf != nil {
return sf
}
}
// Lookup embedded fields through anon own fields
for ; s.anonIdx >= 0 && s.anonIdx < s.numField; s.anonIdx++ {
field := s.rType.Field(s.anonIdx)
// we do enter embedded non-exported types because they could contain
// exported fields
if !field.Anonymous {
continue
}
t, k, _ := deref.TypeKind(field.Type, field.Type.Kind())
if k != reflect.Struct {
continue
}
childEmbed := c.getStruct(t).structData
sf := s.findInEmbedded(c, parentEmbed, childEmbed, field.Index, name)
if sf != nil {
return sf
}
}
return nil
}
func (s *structData) findInEmbedded(
c *Cache,
parentEmbed, childEmbed *structData,
childIndex []int,
name string,
) *structField {
// Set current parent/child data. This allows trySet to handle child fields
// and add them to our struct and to the parent as well if needed
s.curParent = parentEmbed
s.curChild = childEmbed
s.curChildIndex = childIndex
defer func() {
// Ensure to cleanup references
s.curParent = nil
if childEmbed.finished() {
// If the child can still have more fields to explore then keep it
// referened to look it up again if we need to
s.curChild = nil
s.curChildIndex = nil
}
}()
// See if the child has already cached its fields. This is still important
// to check even if it's the s.unfinishedEmbedded because it may have
// explored new fields since the last time we visited it
for name, sf := range childEmbed.fields {
s.trySet(name, sf)
}
// Recheck if we have what we needed from the above sync
if sf := s.fields[name]; sf != nil {
return sf
}
// Try finding in the child again in case it hasn't finished
if !childEmbed.finished() {
if childEmbed.structField(c, s, name) != nil {
return s.fields[name]
}
}
return nil
}
func (s *structData) trySet(name string, sf *structField) {
if _, ok := s.fields[name]; ok {
return
}
sf = &structField{
Nature: sf.Nature,
Index: append(s.curChildIndex, sf.Index...),
}
s.fields[name] = sf
if s.curParent != nil {
s.curParent.trySet(name, sf)
}
}
func StructFields(c *Cache, t reflect.Type) map[string]Nature {
table := make(map[string]Nature)
if t == nil {
return table
}
t, k, _ := deref.TypeKind(t, t.Kind())
if k == reflect.Struct {
// lookup for a field with an empty name, which will cause to never find a
// match, meaning everything will have been cached.
sd := c.getStruct(t).structData
sd.structField(c, nil, "")
for name, sf := range sd.fields {
table[name] = sf.Nature
}
}
return table
}
type methodset struct {
rType reflect.Type
kind reflect.Kind
methods map[string]*method
numMethod, idx int
}
type method struct {
reflect.Method
nature Nature
}
func (s *methodset) method(c *Cache, name string) *method {
if s.methods == nil {
s.methods = make(map[string]*method, s.numMethod)
} else if m := s.methods[name]; m != nil {
return m
}
for ; s.idx < s.numMethod; s.idx++ {
rm := s.rType.Method(s.idx)
if !rm.IsExported() {
continue
}
nt := c.FromType(rm.Type)
if s.rType.Kind() != reflect.Interface {
nt.Method = true
nt.MethodIndex = rm.Index
// In case of interface type method will not have a receiver,
// and to prevent checker decreasing numbers of in arguments
// return method type as not method (second argument is false).
// Also, we can not use m.Index here, because it will be
// different indexes for different types which implement
// the same interface.
}
m := &method{
Method: rm,
nature: nt,
}
s.methods[rm.Name] = m
if rm.Name == name {
return m
}
}
return nil
}
================================================
FILE: compiler/compiler.go
================================================
package compiler
import (
"fmt"
"math"
"reflect"
"regexp"
"runtime/debug"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/builtin"
"github.com/expr-lang/expr/checker"
. "github.com/expr-lang/expr/checker/nature"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/file"
"github.com/expr-lang/expr/parser"
. "github.com/expr-lang/expr/vm"
"github.com/expr-lang/expr/vm/runtime"
)
const (
placeholder = 12345
)
func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v\n%s", r, debug.Stack())
}
}()
c := &compiler{
config: config,
locations: make([]file.Location, 0),
constantsIndex: make(map[any]int),
functionsIndex: make(map[string]int),
debugInfo: make(map[string]string),
}
if config != nil {
c.ntCache = &c.config.NtCache
} else {
c.ntCache = new(Cache)
}
c.compile(tree.Node)
if c.config != nil {
switch c.config.Expect {
case reflect.Int:
c.emit(OpCast, 0)
case reflect.Int64:
c.emit(OpCast, 1)
case reflect.Float64:
c.emit(OpCast, 2)
case reflect.Bool:
c.emit(OpCast, 3)
}
if c.config.Optimize {
c.optimize()
}
}
var span *Span
if len(c.spans) > 0 {
span = c.spans[0]
}
program = NewProgram(
tree.Source,
tree.Node,
c.locations,
c.variables,
c.constants,
c.bytecode,
c.arguments,
c.functions,
c.debugInfo,
span,
)
return
}
type compiler struct {
config *conf.Config
ntCache *Cache
locations []file.Location
bytecode []Opcode
variables int
scopes []scope
constants []any
constantsIndex map[any]int
functions []Function
functionsIndex map[string]int
debugInfo map[string]string
nodes []ast.Node
spans []*Span
chains [][]int
arguments []int
}
type scope struct {
variableName string
index int
}
func (c *compiler) nodeParent() ast.Node {
if len(c.nodes) > 1 {
return c.nodes[len(c.nodes)-2]
}
return nil
}
func (c *compiler) emitLocation(loc file.Location, op Opcode, arg int) int {
c.bytecode = append(c.bytecode, op)
current := len(c.bytecode)
c.arguments = append(c.arguments, arg)
c.locations = append(c.locations, loc)
return current
}
func (c *compiler) emit(op Opcode, args ...int) int {
arg := 0
if len(args) > 1 {
panic("too many arguments")
}
if len(args) == 1 {
arg = args[0]
}
var loc file.Location
if len(c.nodes) > 0 {
loc = c.nodes[len(c.nodes)-1].Location()
}
return c.emitLocation(loc, op, arg)
}
func (c *compiler) emitPush(value any) int {
return c.emit(OpPush, c.addConstant(value))
}
func (c *compiler) addConstant(constant any) int {
indexable := true
hash := constant
switch reflect.TypeOf(constant).Kind() {
case reflect.Slice, reflect.Map, reflect.Struct, reflect.Func:
indexable = false
}
if field, ok := constant.(*runtime.Field); ok {
indexable = true
hash = fmt.Sprintf("%v", field)
}
if method, ok := constant.(*runtime.Method); ok {
indexable = true
hash = fmt.Sprintf("%v", method)
}
if indexable {
if p, ok := c.constantsIndex[hash]; ok {
return p
}
}
c.constants = append(c.constants, constant)
p := len(c.constants) - 1
if indexable {
c.constantsIndex[hash] = p
}
return p
}
func (c *compiler) addVariable(name string) int {
c.variables++
c.debugInfo[fmt.Sprintf("var_%d", c.variables-1)] = name
return c.variables - 1
}
// emitFunction adds builtin.Function.Func to the program.functions and emits call opcode.
func (c *compiler) emitFunction(fn *builtin.Function, argsLen int) {
switch argsLen {
case 0:
c.emit(OpCall0, c.addFunction(fn.Name, fn.Func))
case 1:
c.emit(OpCall1, c.addFunction(fn.Name, fn.Func))
case 2:
c.emit(OpCall2, c.addFunction(fn.Name, fn.Func))
case 3:
c.emit(OpCall3, c.addFunction(fn.Name, fn.Func))
default:
c.emit(OpLoadFunc, c.addFunction(fn.Name, fn.Func))
c.emit(OpCallN, argsLen)
}
}
// addFunction adds builtin.Function.Func to the program.functions and returns its index.
func (c *compiler) addFunction(name string, fn Function) int {
if fn == nil {
panic("function is nil")
}
if p, ok := c.functionsIndex[name]; ok {
return p
}
p := len(c.functions)
c.functions = append(c.functions, fn)
c.functionsIndex[name] = p
c.debugInfo[fmt.Sprintf("func_%d", p)] = name
return p
}
func (c *compiler) patchJump(placeholder int) {
offset := len(c.bytecode) - placeholder
c.arguments[placeholder-1] = offset
}
func (c *compiler) calcBackwardJump(to int) int {
return len(c.bytecode) + 1 - to
}
func (c *compiler) compile(node ast.Node) {
c.nodes = append(c.nodes, node)
defer func() {
c.nodes = c.nodes[:len(c.nodes)-1]
}()
if c.config != nil && c.config.Profile {
span := &Span{
Name: reflect.TypeOf(node).String(),
Expression: node.String(),
}
if len(c.spans) > 0 {
prev := c.spans[len(c.spans)-1]
prev.Children = append(prev.Children, span)
}
c.spans = append(c.spans, span)
defer func() {
if len(c.spans) > 1 {
c.spans = c.spans[:len(c.spans)-1]
}
}()
c.emit(OpProfileStart, c.addConstant(span))
defer func() {
c.emit(OpProfileEnd, c.addConstant(span))
}()
}
switch n := node.(type) {
case *ast.NilNode:
c.NilNode(n)
case *ast.IdentifierNode:
c.IdentifierNode(n)
case *ast.IntegerNode:
c.IntegerNode(n)
case *ast.FloatNode:
c.FloatNode(n)
case *ast.BoolNode:
c.BoolNode(n)
case *ast.StringNode:
c.StringNode(n)
case *ast.BytesNode:
c.BytesNode(n)
case *ast.ConstantNode:
c.ConstantNode(n)
case *ast.UnaryNode:
c.UnaryNode(n)
case *ast.BinaryNode:
c.BinaryNode(n)
case *ast.ChainNode:
c.ChainNode(n)
case *ast.MemberNode:
c.MemberNode(n)
case *ast.SliceNode:
c.SliceNode(n)
case *ast.CallNode:
c.CallNode(n)
case *ast.BuiltinNode:
c.BuiltinNode(n)
case *ast.PredicateNode:
c.PredicateNode(n)
case *ast.PointerNode:
c.PointerNode(n)
case *ast.VariableDeclaratorNode:
c.VariableDeclaratorNode(n)
case *ast.SequenceNode:
c.SequenceNode(n)
case *ast.ConditionalNode:
c.ConditionalNode(n)
case *ast.ArrayNode:
c.ArrayNode(n)
case *ast.MapNode:
c.MapNode(n)
case *ast.PairNode:
c.PairNode(n)
default:
panic(fmt.Sprintf("undefined node type (%T)", node))
}
}
func (c *compiler) NilNode(_ *ast.NilNode) {
c.emit(OpNil)
}
func (c *compiler) IdentifierNode(node *ast.IdentifierNode) {
if index, ok := c.lookupVariable(node.Value); ok {
c.emit(OpLoadVar, index)
return
}
if node.Value == "$env" {
c.emit(OpLoadEnv)
return
}
var env Nature
if c.config != nil {
env = c.config.Env
}
if env.IsFastMap() {
c.emit(OpLoadFast, c.addConstant(node.Value))
} else if ok, index, name := checker.FieldIndex(c.ntCache, env, node); ok {
c.emit(OpLoadField, c.addConstant(&runtime.Field{
Index: index,
Path: []string{name},
}))
} else if ok, index, name := checker.MethodIndex(c.ntCache, env, node); ok {
c.emit(OpLoadMethod, c.addConstant(&runtime.Method{
Name: name,
Index: index,
}))
} else {
c.emit(OpLoadConst, c.addConstant(node.Value))
}
}
func (c *compiler) IntegerNode(node *ast.IntegerNode) {
t := node.Type()
if t == nil {
c.emitPush(node.Value)
return
}
switch t.Kind() {
case reflect.Float32:
c.emitPush(float32(node.Value))
case reflect.Float64:
c.emitPush(float64(node.Value))
case reflect.Int:
c.emitPush(node.Value)
case reflect.Int8:
if node.Value > math.MaxInt8 || node.Value < math.MinInt8 {
panic(fmt.Sprintf("constant %d overflows int8", node.Value))
}
c.emitPush(int8(node.Value))
case reflect.Int16:
if node.Value > math.MaxInt16 || node.Value < math.MinInt16 {
panic(fmt.Sprintf("constant %d overflows int16", node.Value))
}
c.emitPush(int16(node.Value))
case reflect.Int32:
if node.Value > math.MaxInt32 || node.Value < math.MinInt32 {
panic(fmt.Sprintf("constant %d overflows int32", node.Value))
}
c.emitPush(int32(node.Value))
case reflect.Int64:
c.emitPush(int64(node.Value))
case reflect.Uint:
if node.Value < 0 {
panic(fmt.Sprintf("constant %d overflows uint", node.Value))
}
c.emitPush(uint(node.Value))
case reflect.Uint8:
if node.Value > math.MaxUint8 || node.Value < 0 {
panic(fmt.Sprintf("constant %d overflows uint8", node.Value))
}
c.emitPush(uint8(node.Value))
case reflect.Uint16:
if node.Value > math.MaxUint16 || node.Value < 0 {
panic(fmt.Sprintf("constant %d overflows uint16", node.Value))
}
c.emitPush(uint16(node.Value))
case reflect.Uint32:
if node.Value < 0 {
panic(fmt.Sprintf("constant %d overflows uint32", node.Value))
}
c.emitPush(uint32(node.Value))
case reflect.Uint64:
if node.Value < 0 {
panic(fmt.Sprintf("constant %d overflows uint64", node.Value))
}
c.emitPush(uint64(node.Value))
default:
c.emitPush(node.Value)
}
}
func (c *compiler) FloatNode(node *ast.FloatNode) {
switch node.Type().Kind() {
case reflect.Float32:
c.emitPush(float32(node.Value))
case reflect.Float64:
c.emitPush(node.Value)
default:
c.emitPush(node.Value)
}
}
func (c *compiler) BoolNode(node *ast.BoolNode) {
if node.Value {
c.emit(OpTrue)
} else {
c.emit(OpFalse)
}
}
func (c *compiler) StringNode(node *ast.StringNode) {
c.emitPush(node.Value)
}
func (c *compiler) BytesNode(node *ast.BytesNode) {
c.emitPush(node.Value)
}
func (c *compiler) ConstantNode(node *ast.ConstantNode) {
if node.Value == nil {
c.emit(OpNil)
return
}
c.emitPush(node.Value)
}
func (c *compiler) UnaryNode(node *ast.UnaryNode) {
c.compile(node.Node)
c.derefInNeeded(node.Node)
switch node.Operator {
case "!", "not":
c.emit(OpNot)
case "+":
// Do nothing
case "-":
c.emit(OpNegate)
default:
panic(fmt.Sprintf("unknown operator (%v)", node.Operator))
}
}
func (c *compiler) BinaryNode(node *ast.BinaryNode) {
switch node.Operator {
case "==":
c.equalBinaryNode(node)
case "!=":
c.equalBinaryNode(node)
c.emit(OpNot)
case "or", "||":
if c.config != nil && !c.config.ShortCircuit {
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpOr)
break
}
c.compile(node.Left)
c.derefInNeeded(node.Left)
end := c.emit(OpJumpIfTrue, placeholder)
c.emit(OpPop)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.patchJump(end)
case "and", "&&":
if c.config != nil && !c.config.ShortCircuit {
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpAnd)
break
}
c.compile(node.Left)
c.derefInNeeded(node.Left)
end := c.emit(OpJumpIfFalse, placeholder)
c.emit(OpPop)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.patchJump(end)
case "<":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpLess)
case ">":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpMore)
case "<=":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpLessOrEqual)
case ">=":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpMoreOrEqual)
case "+":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpAdd)
case "-":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpSubtract)
case "*":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpMultiply)
case "/":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpDivide)
case "%":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpModulo)
case "**", "^":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpExponent)
case "in":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpIn)
case "matches":
if str, ok := node.Right.(*ast.StringNode); ok {
re, err := regexp.Compile(str.Value)
if err != nil {
panic(err)
}
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.emit(OpMatchesConst, c.addConstant(re))
} else {
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpMatches)
}
case "contains":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpContains)
case "startsWith":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpStartsWith)
case "endsWith":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpEndsWith)
case "..":
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.emit(OpRange)
case "??":
c.compile(node.Left)
c.derefInNeeded(node.Left)
end := c.emit(OpJumpIfNotNil, placeholder)
c.emit(OpPop)
c.compile(node.Right)
c.derefInNeeded(node.Right)
c.patchJump(end)
default:
panic(fmt.Sprintf("unknown operator (%v)", node.Operator))
}
}
func (c *compiler) equalBinaryNode(node *ast.BinaryNode) {
l := kind(node.Left.Type())
r := kind(node.Right.Type())
leftIsSimple := isSimpleType(node.Left)
rightIsSimple := isSimpleType(node.Right)
leftAndRightAreSimple := leftIsSimple && rightIsSimple
c.compile(node.Left)
c.derefInNeeded(node.Left)
c.compile(node.Right)
c.derefInNeeded(node.Right)
if l == r && l == reflect.Int && leftAndRightAreSimple {
c.emit(OpEqualInt)
} else if l == r && l == reflect.String && leftAndRightAreSimple {
c.emit(OpEqualString)
} else {
c.emit(OpEqual)
}
}
func isSimpleType(node ast.Node) bool {
if node == nil {
return false
}
t := node.Type()
if t == nil {
return false
}
return t.PkgPath() == ""
}
func (c *compiler) ChainNode(node *ast.ChainNode) {
c.chains = append(c.chains, []int{})
c.compile(node.Node)
for _, ph := range c.chains[len(c.chains)-1] {
c.patchJump(ph) // If chain activated jump here (got nit somewhere).
}
parent := c.nodeParent()
if binary, ok := parent.(*ast.BinaryNode); ok && binary.Operator == "??" {
// If chain is used in nil coalescing operator, we can omit
// nil push at the end of the chain. The ?? operator will
// handle it.
} else {
// We need to put the nil on the stack, otherwise "typed"
// nil will be used as a result of the chain.
j := c.emit(OpJumpIfNotNil, placeholder)
c.emit(OpPop)
c.emit(OpNil)
c.patchJump(j)
}
c.chains = c.chains[:len(c.chains)-1]
}
func (c *compiler) MemberNode(node *ast.MemberNode) {
var env Nature
if c.config != nil {
env = c.config.Env
}
if ok, index, name := checker.MethodIndex(c.ntCache, env, node); ok {
c.compile(node.Node)
c.emit(OpMethod, c.addConstant(&runtime.Method{
Name: name,
Index: index,
}))
return
}
op := OpFetch
base := node.Node
ok, index, nodeName := checker.FieldIndex(c.ntCache, env, node)
path := []string{nodeName}
if ok {
op = OpFetchField
for !node.Optional {
if ident, isIdent := base.(*ast.IdentifierNode); isIdent {
if ok, identIndex, name := checker.FieldIndex(c.ntCache, env, ident); ok {
index = append(identIndex, index...)
path = append([]string{name}, path...)
c.emitLocation(ident.Location(), OpLoadField, c.addConstant(
&runtime.Field{Index: index, Path: path},
))
return
}
}
if member, isMember := base.(*ast.MemberNode); isMember {
if ok, memberIndex, name := checker.FieldIndex(c.ntCache, env, member); ok {
index = append(memberIndex, index...)
path = append([]string{name}, path...)
node = member
base = member.Node
} else {
break
}
} else {
break
}
}
}
c.compile(base)
// If the field is optional, we need to jump over the fetch operation.
// If no ChainNode (none c.chains) is used, do not compile the optional fetch.
if node.Optional && len(c.chains) > 0 {
ph := c.emit(OpJumpIfNil, placeholder)
c.chains[len(c.chains)-1] = append(c.chains[len(c.chains)-1], ph)
}
if op == OpFetch {
c.compile(node.Property)
deref := true
// If the map key is a pointer, we should not dereference the property.
if node.Node.Type() != nil && node.Node.Type().Kind() == reflect.Map {
keyType := node.Node.Type().Key()
propType := node.Property.Type()
if propType != nil && propType.AssignableTo(keyType) {
deref = false
}
}
if deref {
c.derefInNeeded(node.Property)
}
c.emit(OpFetch)
} else {
c.emitLocation(node.Location(), op, c.addConstant(
&runtime.Field{Index: index, Path: path},
))
}
}
func (c *compiler) SliceNode(node *ast.SliceNode) {
c.compile(node.Node)
if node.To != nil {
c.compile(node.To)
c.derefInNeeded(node.To)
} else {
c.emit(OpLen)
}
if node.From != nil {
c.compile(node.From)
c.derefInNeeded(node.From)
} else {
c.emitPush(0)
}
c.emit(OpSlice)
}
func (c *compiler) CallNode(node *ast.CallNode) {
fn := node.Callee.Type()
if fn.Kind() == reflect.Func {
fnInOffset := 0
fnNumIn := fn.NumIn()
switch callee := node.Callee.(type) {
case *ast.MemberNode:
if prop, ok := callee.Property.(*ast.StringNode); ok {
if _, ok = callee.Node.Type().MethodByName(prop.Value); ok && callee.Node.Type().Kind() != reflect.Interface {
fnInOffset = 1
fnNumIn--
}
}
case *ast.IdentifierNode:
if t, ok := c.config.Env.MethodByName(c.ntCache, callee.Value); ok && t.Method {
fnInOffset = 1
fnNumIn--
}
}
for i, arg := range node.Arguments {
c.compile(arg)
var in reflect.Type
if fn.IsVariadic() && i >= fnNumIn-1 {
in = fn.In(fn.NumIn() - 1).Elem()
} else {
in = fn.In(i + fnInOffset)
}
c.derefParam(in, arg)
}
} else {
for _, arg := range node.Arguments {
c.compile(arg)
}
}
if ident, ok := node.Callee.(*ast.IdentifierNode); ok {
if c.config != nil {
if fn, ok := c.config.Functions[ident.Value]; ok {
c.emitFunction(fn, len(node.Arguments))
return
}
}
}
c.compile(node.Callee)
if c.config != nil {
isMethod, _, _ := checker.MethodIndex(c.ntCache, c.config.Env, node.Callee)
if index, ok := checker.TypedFuncIndex(node.Callee.Type(), isMethod); ok {
c.emit(OpCallTyped, index)
return
} else if checker.IsFastFunc(node.Callee.Type(), isMethod) {
c.emit(OpCallFast, len(node.Arguments))
} else {
c.emit(OpCall, len(node.Arguments))
}
} else {
c.emit(OpCall, len(node.Arguments))
}
}
func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
switch node.Name {
case "all":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
var loopBreak int
c.emitLoop(func() {
c.compile(node.Arguments[1])
loopBreak = c.emit(OpJumpIfFalse, placeholder)
c.emit(OpPop)
})
c.emit(OpTrue)
c.patchJump(loopBreak)
c.emit(OpEnd)
return
case "none":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
var loopBreak int
c.emitLoop(func() {
c.compile(node.Arguments[1])
c.emit(OpNot)
loopBreak = c.emit(OpJumpIfFalse, placeholder)
c.emit(OpPop)
})
c.emit(OpTrue)
c.patchJump(loopBreak)
c.emit(OpEnd)
return
case "any":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
var loopBreak int
c.emitLoop(func() {
c.compile(node.Arguments[1])
loopBreak = c.emit(OpJumpIfTrue, placeholder)
c.emit(OpPop)
})
c.emit(OpFalse)
c.patchJump(loopBreak)
c.emit(OpEnd)
return
case "one":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
c.emitLoop(func() {
c.compile(node.Arguments[1])
c.emitCond(func() {
c.emit(OpIncrementCount)
})
})
c.emit(OpGetCount)
c.emitPush(1)
c.emit(OpEqual)
c.emit(OpEnd)
return
case "filter":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
c.emitLoop(func() {
c.compile(node.Arguments[1])
c.emitCond(func() {
c.emit(OpIncrementCount)
if node.Map != nil {
c.compile(node.Map)
} else {
c.emit(OpPointer)
}
})
})
c.emit(OpGetCount)
c.emit(OpEnd)
c.emit(OpArray)
return
case "map":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
c.emitLoop(func() {
c.compile(node.Arguments[1])
})
c.emit(OpGetLen)
c.emit(OpEnd)
c.emit(OpArray)
return
case "count":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
var loopBreak int
c.emitLoop(func() {
if len(node.Arguments) == 2 {
c.compile(node.Arguments[1])
} else {
c.emit(OpPointer)
}
c.emitCond(func() {
c.emit(OpIncrementCount)
// Early termination if threshold is set
if node.Threshold != nil {
c.emit(OpGetCount)
c.emit(OpInt, *node.Threshold)
c.emit(OpMoreOrEqual)
loopBreak = c.emit(OpJumpIfTrue, placeholder)
c.emit(OpPop)
}
})
})
c.emit(OpGetCount)
if node.Threshold != nil {
end := c.emit(OpJump, placeholder)
c.patchJump(loopBreak)
// Early exit path: pop the bool comparison result, push count
c.emit(OpPop)
c.emit(OpGetCount)
c.patchJump(end)
}
c.emit(OpEnd)
return
case "sum":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
c.emit(OpInt, 0)
c.emit(OpSetAcc)
c.emitLoop(func() {
if len(node.Arguments) == 2 {
c.compile(node.Arguments[1])
} else {
c.emit(OpPointer)
}
c.emit(OpGetAcc)
c.emit(OpAdd)
c.emit(OpSetAcc)
})
c.emit(OpGetAcc)
c.emit(OpEnd)
return
case "find":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
var loopBreak int
c.emitLoop(func() {
c.compile(node.Arguments[1])
noop := c.emit(OpJumpIfFalse, placeholder)
c.emit(OpPop)
if node.Map != nil {
c.compile(node.Map)
} else {
c.emit(OpPointer)
}
loopBreak = c.emit(OpJump, placeholder)
c.patchJump(noop)
c.emit(OpPop)
})
if node.Throws {
c.emit(OpPush, c.addConstant(fmt.Errorf("reflect: slice index out of range")))
c.emit(OpThrow)
} else {
c.emit(OpNil)
}
c.patchJump(loopBreak)
c.emit(OpEnd)
return
case "findIndex":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
var loopBreak int
c.emitLoop(func() {
c.compile(node.Arguments[1])
noop := c.emit(OpJumpIfFalse, placeholder)
c.emit(OpPop)
c.emit(OpGetIndex)
loopBreak = c.emit(OpJump, placeholder)
c.patchJump(noop)
c.emit(OpPop)
})
c.emit(OpNil)
c.patchJump(loopBreak)
c.emit(OpEnd)
return
case "findLast":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
var loopBreak int
c.emitLoopBackwards(func() {
c.compile(node.Arguments[1])
noop := c.emit(OpJumpIfFalse, placeholder)
c.emit(OpPop)
if node.Map != nil {
c.compile(node.Map)
} else {
c.emit(OpPointer)
}
loopBreak = c.emit(OpJump, placeholder)
c.patchJump(noop)
c.emit(OpPop)
})
if node.Throws {
c.emit(OpPush, c.addConstant(fmt.Errorf("reflect: slice index out of range")))
c.emit(OpThrow)
} else {
c.emit(OpNil)
}
c.patchJump(loopBreak)
c.emit(OpEnd)
return
case "findLastIndex":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
var loopBreak int
c.emitLoopBackwards(func() {
c.compile(node.Arguments[1])
noop := c.emit(OpJumpIfFalse, placeholder)
c.emit(OpPop)
c.emit(OpGetIndex)
loopBreak = c.emit(OpJump, placeholder)
c.patchJump(noop)
c.emit(OpPop)
})
c.emit(OpNil)
c.patchJump(loopBreak)
c.emit(OpEnd)
return
case "groupBy":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
c.emit(OpCreate, 1)
c.emit(OpSetAcc)
c.emitLoop(func() {
c.compile(node.Arguments[1])
c.emit(OpGroupBy)
})
c.emit(OpGetAcc)
c.emit(OpEnd)
return
case "sortBy":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
if len(node.Arguments) == 3 {
c.compile(node.Arguments[2])
} else {
c.emit(OpPush, c.addConstant("asc"))
}
c.emit(OpCreate, 2)
c.emit(OpSetAcc)
c.emitLoop(func() {
c.compile(node.Arguments[1])
c.emit(OpSortBy)
})
c.emit(OpSort)
c.emit(OpEnd)
return
case "reduce":
c.compile(node.Arguments[0])
c.derefInNeeded(node.Arguments[0])
c.emit(OpBegin)
if len(node.Arguments) == 3 {
c.compile(node.Arguments[2])
c.derefInNeeded(node.Arguments[2])
c.emit(OpSetAcc)
} else {
// When no initial value is provided, we use the first element as the
// accumulator. But first we must check if the array is empty to avoid
// an index out of range panic.
empty := c.emit(OpJumpIfEnd, placeholder)
c.emit(OpPointer)
c.emit(OpIncrementIndex)
c.emit(OpSetAcc)
jumpPastError := c.emit(OpJump, placeholder)
c.patchJump(empty)
c.emit(OpPush, c.addConstant(fmt.Errorf("reduce of empty array with no initial value")))
c.emit(OpThrow)
c.patchJump(jumpPastError)
}
c.emitLoop(func() {
c.compile(node.Arguments[1])
c.emit(OpSetAcc)
})
c.emit(OpGetAcc)
c.emit(OpEnd)
return
}
if id, ok := builtin.Index[node.Name]; ok {
f := builtin.Builtins[id]
for i, arg := range node.Arguments {
c.compile(arg)
argType := arg.Type()
if argType.Kind() == reflect.Ptr || arg.Nature().IsUnknown(c.ntCache) {
if f.Deref == nil {
// By default, builtins expect arguments to be dereferenced.
c.emit(OpDeref)
} else {
if f.Deref(i, argType) {
c.emit(OpDeref)
}
}
}
}
if f.Fast != nil {
c.emit(OpCallBuiltin1, id)
} else if f.Safe != nil {
id := c.addConstant(f.Safe)
c.emit(OpPush, id)
c.debugInfo[fmt.Sprintf("const_%d", id)] = node.Name
c.emit(OpCallSafe, len(node.Arguments))
} else if f.Func != nil {
c.emitFunction(f, len(node.Arguments))
}
return
}
panic(fmt.Sprintf("unknown builtin %v", node.Name))
}
func (c *compiler) emitCond(body func()) {
noop := c.emit(OpJumpIfFalse, placeholder)
c.emit(OpPop)
body()
jmp := c.emit(OpJump, placeholder)
c.patchJump(noop)
c.emit(OpPop)
c.patchJump(jmp)
}
func (c *compiler) emitLoop(body func()) {
begin := len(c.bytecode)
end := c.emit(OpJumpIfEnd, placeholder)
body()
c.emit(OpIncrementIndex)
c.emit(OpJumpBackward, c.calcBackwardJump(begin))
c.patchJump(end)
}
func (c *compiler) emitLoopBackwards(body func()) {
c.emit(OpGetLen)
c.emit(OpInt, 1)
c.emit(OpSubtract)
c.emit(OpSetIndex)
begin := len(c.bytecode)
c.emit(OpGetIndex)
c.emit(OpInt, 0)
c.emit(OpMoreOrEqual)
end := c.emit(OpJumpIfFalse, placeholder)
body()
c.emit(OpDecrementIndex)
c.emit(OpJumpBackward, c.calcBackwardJump(begin))
c.patchJump(end)
}
func (c *compiler) PredicateNode(node *ast.PredicateNode) {
c.compile(node.Node)
}
func (c *compiler) PointerNode(node *ast.PointerNode) {
switch node.Name {
case "index":
c.emit(OpGetIndex)
case "acc":
c.emit(OpGetAcc)
case "":
c.emit(OpPointer)
default:
panic(fmt.Sprintf("unknown pointer %v", node.Name))
}
}
func (c *compiler) VariableDeclaratorNode(node *ast.VariableDeclaratorNode) {
c.compile(node.Value)
index := c.addVariable(node.Name)
c.emit(OpStore, index)
c.beginScope(node.Name, index)
c.compile(node.Expr)
c.endScope()
}
func (c *compiler) SequenceNode(node *ast.SequenceNode) {
for i, n := range node.Nodes {
c.compile(n)
if i < len(node.Nodes)-1 {
c.emit(OpPop)
}
}
}
func (c *compiler) beginScope(name string, index int) {
c.scopes = append(c.scopes, scope{name, index})
}
func (c *compiler) endScope() {
c.scopes = c.scopes[:len(c.scopes)-1]
}
func (c *compiler) lookupVariable(name string) (int, bool) {
for i := len(c.scopes) - 1; i >= 0; i-- {
if c.scopes[i].variableName == name {
return c.scopes[i].index, true
}
}
return 0, false
}
func (c *compiler) ConditionalNode(node *ast.ConditionalNode) {
c.compile(node.Cond)
c.derefInNeeded(node.Cond)
otherwise := c.emit(OpJumpIfFalse, placeholder)
c.emit(OpPop)
c.compile(node.Exp1)
end := c.emit(OpJump, placeholder)
c.patchJump(otherwise)
c.emit(OpPop)
c.compile(node.Exp2)
c.patchJump(end)
}
func (c *compiler) ArrayNode(node *ast.ArrayNode) {
for _, node := range node.Nodes {
c.compile(node)
}
c.emitPush(len(node.Nodes))
c.emit(OpArray)
}
func (c *compiler) MapNode(node *ast.MapNode) {
for _, pair := range node.Pairs {
c.compile(pair)
}
c.emitPush(len(node.Pairs))
c.emit(OpMap)
}
func (c *compiler) PairNode(node *ast.PairNode) {
c.compile(node.Key)
c.compile(node.Value)
}
func (c *compiler) derefInNeeded(node ast.Node) {
if node.Nature().Nil {
return
}
switch node.Type().Kind() {
case reflect.Ptr, reflect.Interface:
c.emit(OpDeref)
}
}
func (c *compiler) derefParam(in reflect.Type, param ast.Node) {
if param.Nature().Nil {
return
}
if param.Type().AssignableTo(in) {
return
}
if in.Kind() != reflect.Ptr && param.Type().Kind() == reflect.Ptr {
c.emit(OpDeref)
}
}
func (c *compiler) optimize() {
for i, op := range c.bytecode {
switch op {
case OpJumpIfTrue, OpJumpIfFalse, OpJumpIfNil, OpJumpIfNotNil:
target := i + c.arguments[i] + 1
for target < len(c.bytecode) && c.bytecode[target] == op {
target += c.arguments[target] + 1
}
c.arguments[i] = target - i - 1
}
}
}
func kind(t reflect.Type) reflect.Kind {
if t == nil {
return reflect.Invalid
}
return t.Kind()
}
================================================
FILE: compiler/compiler_test.go
================================================
package compiler_test
import (
"math"
"reflect"
"testing"
"github.com/expr-lang/expr/internal/testify/assert"
"github.com/expr-lang/expr/internal/testify/require"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/test/mock"
"github.com/expr-lang/expr/test/playground"
"github.com/expr-lang/expr/vm"
"github.com/expr-lang/expr/vm/runtime"
)
type B struct {
_ byte
_ byte
C struct {
_ byte
_ byte
_ byte
D int
}
}
func (B) FuncInB() int {
return 0
}
type Env struct {
A struct {
_ byte
B B
Map map[string]B
Ptr *int
}
}
// AFunc is a method what goes before Func in the alphabet.
func (e Env) AFunc() int {
return 0
}
func (e Env) Func() B {
return B{}
}
func TestCompile(t *testing.T) {
var tests = []struct {
code string
want vm.Program
}{
{
`65535`,
vm.Program{
Constants: []any{
math.MaxUint16,
},
Bytecode: []vm.Opcode{
vm.OpPush,
},
Arguments: []int{0},
},
},
{
`.5`,
vm.Program{
Constants: []any{
.5,
},
Bytecode: []vm.Opcode{
vm.OpPush,
},
Arguments: []int{0},
},
},
{
`true`,
vm.Program{
Bytecode: []vm.Opcode{
vm.OpTrue,
},
Arguments: []int{0},
},
},
{
`"string"`,
vm.Program{
Constants: []any{
"string",
},
Bytecode: []vm.Opcode{
vm.OpPush,
},
Arguments: []int{0},
},
},
{
`"string" == "string"`,
vm.Program{
Constants: []any{
"string",
},
Bytecode: []vm.Opcode{
vm.OpPush,
vm.OpPush,
vm.OpEqualString,
},
Arguments: []int{0, 0, 0},
},
},
{
`1000000 == 1000000`,
vm.Program{
Constants: []any{
int64(1000000),
},
Bytecode: []vm.Opcode{
vm.OpPush,
vm.OpPush,
vm.OpEqualInt,
},
Arguments: []int{0, 0, 0},
},
},
{
`-1`,
vm.Program{
Constants: []any{1},
Bytecode: []vm.Opcode{
vm.OpPush,
vm.OpNegate,
},
Arguments: []int{0, 0},
},
},
{
`true && true || true`,
vm.Program{
Bytecode: []vm.Opcode{
vm.OpTrue,
vm.OpJumpIfFalse,
vm.OpPop,
vm.OpTrue,
vm.OpJumpIfTrue,
vm.OpPop,
vm.OpTrue,
},
Arguments: []int{0, 2, 0, 0, 2, 0, 0},
},
},
{
`true && (true || true)`,
vm.Program{
Bytecode: []vm.Opcode{
vm.OpTrue,
vm.OpJumpIfFalse,
vm.OpPop,
vm.OpTrue,
vm.OpJumpIfTrue,
vm.OpPop,
vm.OpTrue,
},
Arguments: []int{0, 5, 0, 0, 2, 0, 0},
},
},
{
`A.B.C.D`,
vm.Program{
Constants: []any{
&runtime.Field{
Index: []int{0, 1, 2, 3},
Path: []string{"A", "B", "C", "D"},
},
},
Bytecode: []vm.Opcode{
vm.OpLoadField,
},
Arguments: []int{0},
},
},
{
`A?.B.C.D`,
vm.Program{
Constants: []any{
&runtime.Field{
Index: []int{0},
Path: []string{"A"},
},
&runtime.Field{
Index: []int{1, 2, 3},
Path: []string{"B", "C", "D"},
},
},
Bytecode: []vm.Opcode{
vm.OpLoadField,
vm.OpJumpIfNil,
vm.OpFetchField,
vm.OpJumpIfNotNil,
vm.OpPop,
vm.OpNil,
},
Arguments: []int{0, 1, 1, 2, 0, 0},
},
},
{
`A.B?.C.D`,
vm.Program{
Constants: []any{
&runtime.Field{
Index: []int{0, 1},
Path: []string{"A", "B"},
},
&runtime.Field{
Index: []int{2, 3},
Path: []string{"C", "D"},
},
},
Bytecode: []vm.Opcode{
vm.OpLoadField,
vm.OpJumpIfNil,
vm.OpFetchField,
vm.OpJumpIfNotNil,
vm.OpPop,
vm.OpNil,
},
Arguments: []int{0, 1, 1, 2, 0, 0},
},
},
{
`A?.B`,
vm.Program{
Constants: []any{
&runtime.Field{
Index: []int{0},
Path: []string{"A"},
},
&runtime.Field{
Index: []int{1},
Path: []string{"B"},
},
},
Bytecode: []vm.Opcode{
vm.OpLoadField,
vm.OpJumpIfNil,
vm.OpFetchField,
vm.OpJumpIfNotNil,
vm.OpPop,
vm.OpNil,
},
Arguments: []int{0, 1, 1, 2, 0, 0},
},
},
{
`A?.B ?? 42`,
vm.Program{
Constants: []any{
&runtime.Field{
Index: []int{0},
Path: []string{"A"},
},
&runtime.Field{
Index: []int{1},
Path: []string{"B"},
},
42,
},
Bytecode: []vm.Opcode{
vm.OpLoadField,
vm.OpJumpIfNil,
vm.OpFetchField,
vm.OpJumpIfNotNil,
vm.OpPop,
vm.OpPush,
},
Arguments: []int{0, 1, 1, 2, 0, 2},
},
},
{
`A.Map["B"].C.D`,
vm.Program{
Constants: []any{
&runtime.Field{
Index: []int{0, 2},
Path: []string{"A", "Map"},
},
"B",
&runtime.Field{
Index: []int{2, 3},
Path: []string{"C", "D"},
},
},
Bytecode: []vm.Opcode{
vm.OpLoadField,
vm.OpPush,
vm.OpFetch,
vm.OpFetchField,
},
Arguments: []int{0, 1, 0, 2},
},
},
{
`A ?? 1`,
vm.Program{
Constants: []any{
&runtime.Field{
Index: []int{0},
Path: []string{"A"},
},
1,
},
Bytecode: []vm.Opcode{
vm.OpLoadField,
vm.OpJumpIfNotNil,
vm.OpPop,
vm.OpPush,
},
Arguments: []int{0, 2, 0, 1},
},
},
{
`A.Ptr + 1`,
vm.Program{
Constants: []any{
&runtime.Field{
Index: []int{0, 3},
Path: []string{"A", "Ptr"},
},
1,
},
Bytecode: []vm.Opcode{
vm.OpLoadField,
vm.OpDeref,
vm.OpPush,
vm.OpAdd,
},
Arguments: []int{0, 0, 1, 0},
},
},
{
`Func()`,
vm.Program{
Constants: []any{
&runtime.Method{
Index: 1,
Name: "Func",
},
},
Bytecode: []vm.Opcode{
vm.OpLoadMethod,
vm.OpCall,
},
Arguments: []int{0, 0},
},
},
{
`Func().FuncInB()`,
vm.Program{
Constants: []any{
&runtime.Method{
Index: 1,
Name: "Func",
},
&runtime.Method{
Index: 0,
Name: "FuncInB",
},
},
Bytecode: []vm.Opcode{
vm.OpLoadMethod,
vm.OpCall,
vm.OpMethod,
vm.OpCallTyped,
},
Arguments: []int{0, 0, 1, 12},
},
},
{
`1; 2; 3`,
vm.Program{
Constants: []any{
1,
2,
3,
},
Bytecode: []vm.Opcode{
vm.OpPush,
vm.OpPop,
vm.OpPush,
vm.OpPop,
vm.OpPush,
},
Arguments: []int{0, 0, 1, 0, 2},
},
},
}
for _, test := range tests {
t.Run(test.code, func(t *testing.T) {
program, err := expr.Compile(test.code, expr.Env(Env{}), expr.Optimize(false))
require.NoError(t, err)
assert.Equal(t, test.want.Disassemble(), program.Disassemble())
})
}
}
func TestCompile_panic(t *testing.T) {
tests := []string{
`(TotalPosts.Profile[Authors > TotalPosts == get(nil, TotalLikes)] > Authors) ^ (TotalLikes / (Posts?.PublishDate[TotalPosts] < Posts))`,
`one(Posts, nil)`,
`trim(TotalViews, Posts) <= get(Authors, nil)`,
`Authors.IsZero(nil * Authors) - (TotalViews && Posts ? nil : nil)[TotalViews.IsZero(false, " ").IsZero(Authors)]`,
}
for _, test := range tests {
t.Run(test, func(t *testing.T) {
_, err := expr.Compile(test, expr.Env(playground.Blog{}))
require.Error(t, err)
})
}
}
func TestCompile_FuncTypes(t *testing.T) {
env := map[string]any{
"fn": func([]any, string) string {
return "foo"
},
}
program, err := expr.Compile("fn([1, 2], 'bar')", expr.Env(env))
require.NoError(t, err)
require.Equal(t, vm.OpCallTyped, program.Bytecode[3])
require.Equal(t, 32, program.Arguments[3])
}
func TestCompile_FuncTypes_with_Method(t *testing.T) {
env := mock.Env{}
program, err := expr.Compile("FuncTyped('bar')", expr.Env(env))
require.NoError(t, err)
require.Equal(t, vm.OpCallTyped, program.Bytecode[2])
require.Equal(t, 76, program.Arguments[2])
}
func TestCompile_FuncTypes_excludes_named_functions(t *testing.T) {
env := mock.Env{}
program, err := expr.Compile("FuncNamed('bar')", expr.Env(env))
require.NoError(t, err)
require.Equal(t, vm.OpCall, program.Bytecode[2])
require.Equal(t, 1, program.Arguments[2])
}
func TestCompile_OpCallFast(t *testing.T) {
env := mock.Env{}
program, err := expr.Compile("Fast(3, 2, 1)", expr.Env(env))
require.NoError(t, err)
require.Equal(t, vm.OpCallFast, program.Bytecode[4])
require.Equal(t, 3, program.Arguments[4])
}
func TestCompile_optimizes_jumps(t *testing.T) {
env := map[string]any{
"a": true,
"b": true,
"c": true,
"d": true,
"i64": int64(1),
}
tests := []struct {
code string
want string
}{
{
`let foo = true; let bar = false; let baz = true; foo || bar || baz`,
`0 OpTrue
1 OpStore <0> foo
2 OpFalse
3 OpStore <1> bar
4 OpTrue
5 OpStore <2> baz
6 OpLoadVar <0> foo
7 OpJumpIfTrue <5> (13)
8 OpPop
9 OpLoadVar <1> bar
10 OpJumpIfTrue <2> (13)
11 OpPop
12 OpLoadVar <2> baz
`,
},
{
`a && b && c`,
`0 OpLoadFast <0> a
1 OpJumpIfFalse <5> (7)
2 OpPop
3 OpLoadFast <1> b
4 OpJumpIfFalse <2> (7)
5 OpPop
6 OpLoadFast <2> c
`,
},
{
`a && b || c && d`,
`0 OpLoadFast <0> a
1 OpJumpIfFalse <2> (4)
2 OpPop
3 OpLoadFast <1> b
4 OpJumpIfTrue <5> (10)
5 OpPop
6 OpLoadFast <2> c
7 OpJumpIfFalse <2> (10)
8 OpPop
9 OpLoadFast <3> d
`,
},
{
`filter([1, 2, 3, 4, 5], # > 3 && # != 4 && # != 5)`,
`0 OpPush <0> [1 2 3 4 5]
1 OpBegin
2 OpJumpIfEnd <23> (26)
3 OpPointer
4 OpPush <1> 3
5 OpMore
6 OpJumpIfFalse <16> (23)
7 OpPop
8 OpPointer
9 OpPush <2> 4
10 OpEqualInt
11 OpNot
12 OpJumpIfFalse <10> (23)
13 OpPop
14 OpPointer
15 OpPush <3> 5
16 OpEqualInt
17 OpNot
18 OpJumpIfFalse <4> (23)
19 OpPop
20 OpIncrementCount
21 OpPointer
22 OpJump <1> (24)
23 OpPop
24 OpIncrementIndex
25 OpJumpBackward <24> (2)
26 OpGetCount
27 OpEnd
28 OpArray
`,
},
{
`let foo = true; let bar = false; let baz = true; foo && bar || baz`,
`0 OpTrue
1 OpStore <0> foo
2 OpFalse
3 OpStore <1> bar
4 OpTrue
5 OpStore <2> baz
6 OpLoadVar <0> foo
7 OpJumpIfFalse <2> (10)
8 OpPop
9 OpLoadVar <1> bar
10 OpJumpIfTrue <2> (13)
11 OpPop
12 OpLoadVar <2> baz
`,
},
{
`true ?? nil ?? nil ?? nil`,
`0 OpTrue
1 OpJumpIfNotNil <8> (10)
2 OpPop
3 OpNil
4 OpJumpIfNotNil <5> (10)
5 OpPop
6 OpNil
7 OpJumpIfNotNil <2> (10)
8 OpPop
9 OpNil
`,
},
{
`let m = {"a": {"b": {"c": 1}}}; m?.a?.b?.c`,
`0 OpPush <0> a
1 OpPush <1> b
2 OpPush <2> c
3 OpPush <3> 1
4 OpPush <3> 1
5 OpMap
6 OpPush <3> 1
7 OpMap
8 OpPush <3> 1
9 OpMap
10 OpStore <0> m
11 OpLoadVar <0> m
12 OpJumpIfNil <8> (21)
13 OpPush <0> a
14 OpFetch
15 OpJumpIfNil <5> (21)
16 OpPush <1> b
17 OpFetch
18 OpJumpIfNil <2> (21)
19 OpPush <2> c
20 OpFetch
21 OpJumpIfNotNil <2> (24)
22 OpPop
23 OpNil
`,
},
{
`-1 not in [1, 2, 5]`,
`0 OpPush <0> -1
1 OpPush <1> map[1:{} 2:{} 5:{}]
2 OpIn
3 OpNot
`,
},
{
`1 + 8 not in [1, 2, 5]`,
`0 OpPush <0> 9
1 OpPush <1> map[1:{} 2:{} 5:{}]
2 OpIn
3 OpNot
`,
},
{
`true ? false : 8 not in [1, 2, 5]`,
`0 OpTrue
1 OpJumpIfFalse <3> (5)
2 OpPop
3 OpFalse
4 OpJump <5> (10)
5 OpPop
6 OpPush <0> 8
7 OpPush <1> map[1:{} 2:{} 5:{}]
8 OpIn
9 OpNot
`,
},
}
for _, test := range tests {
t.Run(test.code, func(t *testing.T) {
program, err := expr.Compile(test.code, expr.Env(env))
require.NoError(t, err)
require.Equal(t, test.want, program.Disassemble())
})
}
}
func TestCompile_IntegerArgsFunc(t *testing.T) {
env := mock.Env{}
tests := []struct{ code string }{
{"FuncInt(0)"},
{"FuncInt8(0)"},
{"FuncInt16(0)"},
{"FuncInt32(0)"},
{"FuncInt64(0)"},
{"FuncUint(0)"},
{"FuncUint8(0)"},
{"FuncUint16(0)"},
{"FuncUint32(0)"},
{"FuncUint64(0)"},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
_, err := expr.Compile(tt.code, expr.Env(env))
require.NoError(t, err)
})
}
}
func TestCompile_call_on_nil(t *testing.T) {
env := map[string]any{
"foo": nil,
}
_, err := expr.Compile(`foo()`, expr.Env(env))
require.Error(t, err)
require.Contains(t, err.Error(), "foo is nil; cannot call nil as function")
}
func TestCompile_Expect(t *testing.T) {
tests := []struct {
input string
option expr.Option
op vm.Opcode
arg int
}{
{
input: "1",
option: expr.AsKind(reflect.Int),
op: vm.OpCast,
arg: 0,
},
{
input: "1",
option: expr.AsInt64(),
op: vm.OpCast,
arg: 1,
},
{
input: "1",
option: expr.AsFloat64(),
op: vm.OpCast,
arg: 2,
},
{
input: "true",
option: expr.AsBool(),
op: vm.OpCast,
arg: 3,
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
program, err := expr.Compile(tt.input, tt.option)
require.NoError(t, err)
lastOp := program.Bytecode[len(program.Bytecode)-1]
lastArg := program.Arguments[len(program.Arguments)-1]
assert.Equal(t, tt.op, lastOp)
assert.Equal(t, tt.arg, lastArg)
})
}
}
================================================
FILE: conf/config.go
================================================
package conf
import (
"fmt"
"reflect"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/builtin"
"github.com/expr-lang/expr/checker/nature"
"github.com/expr-lang/expr/vm/runtime"
)
var (
// DefaultMemoryBudget represents default maximum allowed memory usage by the vm.VM.
DefaultMemoryBudget uint = 1e6
// DefaultMaxNodes represents default maximum allowed AST nodes by the compiler.
DefaultMaxNodes uint = 1e4
)
type FunctionsTable map[string]*builtin.Function
type Config struct {
EnvObject any
Env nature.Nature
Expect reflect.Kind
ExpectAny bool
Optimize bool
Strict bool
ShortCircuit bool
Profile bool
MaxNodes uint
ConstFns map[string]reflect.Value
Visitors []ast.Visitor
Functions FunctionsTable
Builtins FunctionsTable
Disabled map[string]bool // disabled builtins
NtCache nature.Cache
// DisableIfOperator disables the built-in `if ... { } else { }` operator syntax
// so that users can use a custom function named `if(...)` without conflicts.
// When enabled, the lexer treats `if`/`else` as identifiers and the parser
// will not parse `if` statements.
DisableIfOperator bool
}
// CreateNew creates new config with default values.
func CreateNew() *Config {
c := &Config{
Optimize: true,
ShortCircuit: true,
MaxNodes: DefaultMaxNodes,
ConstFns: make(map[string]reflect.Value),
Functions: make(map[string]*builtin.Function),
Builtins: make(map[string]*builtin.Function),
Disabled: make(map[string]bool),
}
for _, f := range builtin.Builtins {
c.Builtins[f.Name] = f
}
return c
}
// New creates new config with environment.
func New(env any) *Config {
c := CreateNew()
c.WithEnv(env)
return c
}
func (c *Config) WithEnv(env any) {
c.EnvObject = env
c.Env = EnvWithCache(&c.NtCache, env)
c.Strict = c.Env.Strict
}
func (c *Config) ConstExpr(name string) {
if c.EnvObject == nil {
panic("no environment is specified for ConstExpr()")
}
fn := reflect.ValueOf(runtime.Fetch(c.EnvObject, name))
if fn.Kind() != reflect.Func {
panic(fmt.Errorf("const expression %q must be a function", name))
}
c.ConstFns[name] = fn
}
type Checker interface {
Check()
}
func (c *Config) Check() {
for _, v := range c.Visitors {
if c, ok := v.(Checker); ok {
c.Check()
}
}
}
func (c *Config) IsOverridden(name string) bool {
if _, ok := c.Functions[name]; ok {
return true
}
if _, ok := c.Env.Get(&c.NtCache, name); ok {
return true
}
return false
}
================================================
FILE: conf/env.go
================================================
package conf
import (
"fmt"
"reflect"
. "github.com/expr-lang/expr/checker/nature"
"github.com/expr-lang/expr/internal/deref"
"github.com/expr-lang/expr/types"
)
// Env returns the Nature of the given environment.
//
// Deprecated: use EnvWithCache instead.
func Env(env any) Nature {
return EnvWithCache(new(Cache), env)
}
func EnvWithCache(c *Cache, env any) Nature {
if env == nil {
n := c.NatureOf(map[string]any{})
n.Strict = true
return n
}
switch env := env.(type) {
case types.Map:
nt := env.Nature()
return nt
}
v := reflect.ValueOf(env)
t := v.Type()
switch deref.Value(v).Kind() {
case reflect.Struct:
n := c.FromType(t)
n.Strict = true
return n
case reflect.Map:
n := c.FromType(v.Type())
if n.TypeData == nil {
n.TypeData = new(TypeData)
}
n.Strict = true
n.Fields = make(map[string]Nature, v.Len())
for _, key := range v.MapKeys() {
elem := v.MapIndex(key)
if !elem.IsValid() || !elem.CanInterface() {
panic(fmt.Sprintf("invalid map value: %s", key))
}
face := elem.Interface()
switch face := face.(type) {
case types.Map:
nt := face.Nature()
n.Fields[key.String()] = nt
default:
if face == nil {
n.Fields[key.String()] = c.NatureOf(nil)
continue
}
n.Fields[key.String()] = c.NatureOf(face)
}
}
return n
}
panic(fmt.Sprintf("unknown type %T", env))
}
================================================
FILE: debug/debugger.go
================================================
package debug
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
. "github.com/expr-lang/expr/vm"
)
func StartDebugger(program *Program, env any) {
vm := Debug()
app := tview.NewApplication()
table := tview.NewTable()
stack := tview.NewTable()
stack.
SetBorder(true).
SetTitle("Stack")
scope := tview.NewTable()
scope.
SetBorder(true).
SetTitle("Scope")
sub := tview.NewFlex().
SetDirection(tview.FlexRow).
AddItem(stack, 0, 3, false).
AddItem(scope, 0, 1, false)
flex := tview.NewFlex().
AddItem(table, 0, 1, true).
AddItem(sub, 0, 1, false)
app.SetRoot(flex, true)
done := false
go func() {
out, err := vm.Run(program, env)
done = true
app.QueueUpdateDraw(func() {
sub.RemoveItem(stack)
sub.RemoveItem(scope)
result := tview.NewTextView()
result.
SetBorder(true).
SetTitle("Output")
result.SetText(fmt.Sprintf("%#v", out))
sub.AddItem(result, 0, 1, false)
if err != nil {
errorView := tview.NewTextView()
errorView.
SetBorder(true).
SetTitle("Error")
errorView.SetText(err.Error())
sub.AddItem(errorView, 0, 1, false)
}
})
}()
index := make(map[int]int)
var buf strings.Builder
program.DisassembleWriter(&buf)
for row, line := range strings.Split(buf.String(), "\n") {
if line == "" {
continue
}
parts := strings.Split(line, "\t")
ip, err := strconv.Atoi(parts[0])
check(err)
index[ip] = row
table.SetCellSimple(row, 0, fmt.Sprintf("% *d", 5, ip))
for col := 1; col < len(parts); col++ {
table.SetCellSimple(row, col, parts[col])
}
for col := len(parts); col < 4; col++ {
table.SetCellSimple(row, col, "")
}
table.SetCell(row, 4, tview.NewTableCell("").SetExpansion(1))
}
draw := func(ip int) {
app.QueueUpdateDraw(func() {
for row := 0; row < table.GetRowCount(); row++ {
for col := 0; col < table.GetColumnCount(); col++ {
table.GetCell(row, col).SetBackgroundColor(tcell.ColorBlack)
}
}
if row, ok := index[ip]; ok {
table.Select(row, 0)
for col := 0; col < 5; col++ {
table.GetCell(row, col).SetBackgroundColor(tcell.ColorMediumBlue)
}
table.SetOffset(row-10, 0)
opcode := table.GetCell(row, 1).Text
if strings.HasPrefix(opcode, "OpJump") {
jump := table.GetCell(row, 3).Text
jump = strings.Trim(jump, "()")
ip, err := strconv.Atoi(jump)
if err == nil {
if row, ok := index[ip]; ok {
for col := 0; col < 5; col++ {
table.GetCell(row, col).SetBackgroundColor(tcell.ColorDimGrey)
}
}
}
}
}
stack.Clear()
for i, value := range vm.Stack {
stack.SetCellSimple(i, 0, fmt.Sprintf("% *d: ", 2, i))
stack.SetCellSimple(i, 1, fmt.Sprintf("%#v", value))
}
stack.ScrollToEnd()
scope.Clear()
var s *Scope
if len(vm.Scopes) > 0 {
s = vm.Scopes[len(vm.Scopes)-1]
}
if s != nil {
type pair struct {
key string
value any
}
var keys []pair
keys = append(keys, pair{"Array", s.Array})
keys = append(keys, pair{"Index", s.Index})
keys = append(keys, pair{"Len", s.Len})
keys = append(keys, pair{"Count", s.Count})
if s.Acc != nil {
keys = append(keys, pair{"Acc", s.Acc})
}
row := 0
for _, pair := range keys {
scope.SetCellSimple(row, 0, fmt.Sprintf("%v: ", pair.key))
scope.SetCellSimple(row, 1, fmt.Sprintf("%v", pair.value))
row++
}
}
})
}
getSelectedPosition := func() int {
row, _ := table.GetSelection()
ip, err := strconv.Atoi(strings.TrimSpace(table.GetCell(row, 0).Text))
check(err)
return ip
}
autostep := false
var breakpoint int
go func() {
draw(0)
for ip := range vm.Position() {
draw(ip)
if autostep {
if breakpoint != ip {
time.Sleep(20 * time.Millisecond)
if !done {
vm.Step()
}
} else {
autostep = false
}
}
}
}()
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyDown || event.Key() == tcell.KeyUp {
table.SetSelectable(true, false)
}
if event.Key() == tcell.KeyEnter {
selectable, _ := table.GetSelectable()
if selectable {
table.SetSelectable(false, false)
breakpoint = getSelectedPosition()
autostep = true
}
if !done {
vm.Step()
}
}
return event
})
err := app.Run()
check(err)
}
func check(err error) {
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
================================================
FILE: debug/go.mod
================================================
module github.com/expr-lang/expr/debug
go 1.18
require (
github.com/expr-lang/expr v0.0.0
github.com/gdamore/tcell/v2 v2.6.0
github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703
)
require (
github.com/gdamore/encoding v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/term v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
)
replace github.com/expr-lang/expr => ../
================================================
FILE: debug/go.sum
================================================
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg=
github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703 h1:ZyM/+FYnpbZsFWuCohniM56kRoHRB4r5EuIzXEYkpxo=
github.com/rivo/tview v0.0.0-20230814110005-ccc2c8119703/go.mod h1:nVwGv4MP47T0jvlk7KuTTjjuSmrGO4JF0iaiNt4bufE=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
================================================
FILE: docgen/README.md
================================================
# DocGen
This package provides documentation generator with JSON or Markdown output.
## Usage
Create a file and put next code into it.
```go
package main
import (
"encoding/json"
"fmt"
"github.com/expr-lang/expr/docgen"
)
func main() {
// TODO: Replace env with your own types.
doc := docgen.CreateDoc(env)
buf, err := json.MarshalIndent(doc, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(buf))
}
```
Run `go run your_file.go`. Documentation will be printed in JSON format.
## Markdown
To generate markdown documentation:
```go
package main
import "github.com/expr-lang/expr/docgen"
func main() {
// TODO: Replace env with your own types.
doc := docgen.CreateDoc(env)
print(doc.Markdown())
}
```
================================================
FILE: docgen/docgen.go
================================================
package docgen
import (
"reflect"
"regexp"
"strings"
"github.com/expr-lang/expr/checker/nature"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/internal/deref"
)
// Kind can be any of array, map, struct, func, string, int, float, bool or any.
type Kind string
// Identifier represents variable names and field names.
type Identifier string
// TypeName is a name of type in types map.
type TypeName string
type Context struct {
Variables map[Identifier]*Type `json:"variables"`
Types map[TypeName]*Type `json:"types"`
PkgPath string
}
type Type struct {
Name TypeName `json:"name,omitempty"`
Kind Kind `json:"kind,omitempty"`
Type *Type `json:"type,omitempty"`
Key *Type `json:"key_type,omitempty"`
Fields map[Identifier]*Type `json:"fields,omitempty"`
Arguments []*Type `json:"arguments,omitempty"`
Return *Type `json:"return,omitempty"`
}
var (
Operators = []string{"matches", "contains", "startsWith", "endsWith"}
Builtins = map[Identifier]*Type{
"true": {Kind: "bool"},
"false": {Kind: "bool"},
"len": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}}, Return: &Type{Kind: "int"}},
"all": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}},
"none": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}},
"any": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}},
"one": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}},
"filter": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "array", Type: &Type{Kind: "any"}}},
"map": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "array", Type: &Type{Kind: "any"}}},
"count": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "int"}},
"trim": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "cutstr", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},
"trimPrefix": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "cutstr", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},
"trimSuffix": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "cutstr", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},
"upper": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},
"lower": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},
"repeat": {Kind: "func", Arguments: []*Type{{Name: "n", Kind: "int"}}, Return: &Type{Name: "string", Kind: "string"}},
"join": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Name: "glue", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},
"indexOf": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "substr", Kind: "string"}}, Return: &Type{Name: "index", Kind: "int"}},
"lastIndexOf": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "substr", Kind: "string"}}, Return: &Type{Name: "index", Kind: "int"}},
"hasPrefix": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "prefix", Kind: "string"}}, Return: &Type{Kind: "bool"}},
"hasSuffix": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "prefix", Kind: "string"}}, Return: &Type{Kind: "bool"}},
"toJSON": {Kind: "func", Arguments: []*Type{{Kind: "any"}}, Return: &Type{Kind: "string"}},
"fromJSON": {Kind: "func", Arguments: []*Type{{Kind: "string"}}, Return: &Type{Kind: "any"}},
"toBase64": {Kind: "func", Arguments: []*Type{{Kind: "string"}}, Return: &Type{Kind: "string"}},
"fromBase64": {Kind: "func", Arguments: []*Type{{Kind: "string"}}, Return: &Type{Kind: "string"}},
"first": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}}, Return: &Type{Kind: "any"}},
"last": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}}, Return: &Type{Kind: "any"}},
"now": {Kind: "func", Return: &Type{Name: "time.Time", Kind: "struct"}},
"duration": {Kind: "func", Arguments: []*Type{{Kind: "string"}}, Return: &Type{Kind: "time.Duration"}},
}
)
func CreateDoc(i any) *Context {
c := &Context{
Variables: make(map[Identifier]*Type),
Types: make(map[TypeName]*Type),
PkgPath: deref.Type(reflect.TypeOf(i)).PkgPath(),
}
cache := new(nature.Cache)
env := conf.EnvWithCache(cache, i)
for name, t := range env.All(cache) {
if _, ok := c.Variables[Identifier(name)]; ok {
continue
}
c.Variables[Identifier(name)] = c.use(t.Type, fromMethod(t.Method))
}
for _, op := range Operators {
c.Variables[Identifier(op)] = &Type{
Kind: "operator",
}
}
for builtin, t := range Builtins {
c.Variables[builtin] = t
}
return c
}
type config struct {
method bool
}
type option func(c *config)
func fromMethod(b bool) option {
return func(c *config) {
c.method = b
}
}
func (c *Context) use(t reflect.Type, ops ...option) *Type {
config := &config{}
for _, op := range ops {
op(config)
}
methods := make([]reflect.Method, 0)
// Methods of struct should be gathered from original struct with pointer,
// as methods maybe declared on pointer receiver. Also this method retrieves
// all embedded structs methods as well, no need to recursion.
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
if isPrivate(m.Name) || isProtobuf(m.Name) {
continue
}
methods = append(methods, m)
}
t = deref.Type(t)
// Only named types will have methods defined on them.
// It maybe not even struct, but we gonna call then
// structs in appendix anyway.
if len(methods) > 0 {
goto appendix
}
// This switch only for "simple" types.
switch t.Kind() {
case reflect.Bool:
return &Type{Kind: "bool"}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fallthrough
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return &Type{Kind: "int"}
case reflect.Float32, reflect.Float64:
return &Type{Kind: "float"}
case reflect.String:
return &Type{Kind: "string"}
case reflect.Interface:
return &Type{Kind: "any"}
case reflect.Array, reflect.Slice:
return &Type{
Kind: "array",
Type: c.use(t.Elem()),
}
case reflect.Map:
return &Type{
Kind: "map",
Key: c.use(t.Key()),
Type: c.use(t.Elem()),
}
case reflect.Struct:
goto appendix
case reflect.Func:
arguments := make([]*Type, 0)
start := 0
if config.method {
start = 1
}
for i := start; i < t.NumIn(); i++ {
arguments = append(arguments, c.use(t.In(i)))
}
f := &Type{
Kind: "func",
Arguments: arguments,
}
if t.NumOut() > 0 {
f.Return = c.use(t.Out(0))
}
return f
}
appendix:
name := TypeName(t.String())
if c.PkgPath == t.PkgPath() {
name = TypeName(t.Name())
}
anonymous := t.Name() == ""
a, ok := c.Types[name]
if !ok {
a = &Type{
Kind: "struct",
Fields: make(map[Identifier]*Type),
}
// baseNode a should be saved before starting recursion, or it will never end.
if !anonymous {
c.Types[name] = a
}
ntCache := new(nature.Cache)
for name, field := range nature.StructFields(ntCache, t) {
if isPrivate(name) || isProtobuf(name) {
continue
}
if _, ok := a.Fields[Identifier(name)]; ok {
continue
}
a.Fields[Identifier(name)] = c.use(field.Type)
}
for _, m := range methods {
if isPrivate(m.Name) || isProtobuf(m.Name) {
continue
}
a.Fields[Identifier(m.Name)] = c.use(m.Type, fromMethod(true))
}
}
if anonymous {
return a
}
return &Type{
Kind: "struct",
Name: name,
}
}
var isCapital = regexp.MustCompile("^[A-Z]")
func isPrivate(s string) bool {
return !isCapital.Match([]byte(s))
}
func isProtobuf(s string) bool {
return strings.HasPrefix(s, "XXX_")
}
================================================
FILE: docgen/docgen_test.go
================================================
package docgen_test
import (
"math"
"testing"
"time"
"github.com/expr-lang/expr/internal/testify/assert"
"github.com/expr-lang/expr/internal/testify/require"
. "github.com/expr-lang/expr/docgen"
)
type Tweet struct {
Size int
Message string
}
type Env struct {
Tweets []Tweet
Config struct {
MaxSize int32
}
Env map[string]any
// NOTE: conflicting type name
TimeWeekday time.Weekday
Weekday Weekday
}
type Weekday int
func (Weekday) String() string {
return ""
}
type Duration int
func (Duration) String() string {
return ""
}
func (*Env) Duration(s string) Duration {
return Duration(0)
}
func TestCreateDoc(t *testing.T) {
Operators = nil
Builtins = nil
doc := CreateDoc(&Env{})
expected := &Context{
Variables: map[Identifier]*Type{
"Tweets": {
Kind: "array",
Type: &Type{
Kind: "struct",
Name: "Tweet",
},
},
"Config": {
Kind: "struct",
Fields: map[Identifier]*Type{
"MaxSize": {Kind: "int"},
},
},
"Env": {
Kind: "map",
Key: &Type{Kind: "string"},
Type: &Type{Kind: "any"},
},
"Duration": {
Kind: "func",
Arguments: []*Type{
{Kind: "string"},
},
Return: &Type{Kind: "struct", Name: "Duration"},
},
"TimeWeekday": {
Name: "time.Weekday",
Kind: "struct",
},
"Weekday": {
Name: "Weekday",
Kind: "struct",
},
},
Types: map[TypeName]*Type{
"Tweet": {
Kind: "struct",
Fields: map[Identifier]*Type{
"Size": {Kind: "int"},
"Message": {Kind: "string"},
},
},
"Duration": {
Kind: "struct",
Fields: map[Identifier]*Type{
"String": {
Kind: "func",
Arguments: []*Type{},
Return: &Type{
Kind: "string",
},
},
},
},
"time.Weekday": {
Kind: "struct",
Fields: map[Identifier]*Type{
"String": {
Kind: "func",
Arguments: []*Type{},
Return: &Type{
Kind: "string",
},
},
},
},
"Weekday": {
Kind: "struct",
Fields: map[Identifier]*Type{
"String": {
Kind: "func",
Arguments: []*Type{},
Return: &Type{
Kind: "string",
},
},
},
},
},
PkgPath: "github.com/expr-lang/expr/docgen_test",
}
assert.Equal(t, expected.Markdown(), doc.Markdown())
}
type A struct {
AmbiguousField int
OkField int
}
type B struct {
AmbiguousField string
}
type C struct {
A
B
}
type EnvAmbiguous struct {
A
B
C C
}
func TestCreateDoc_Ambiguous(t *testing.T) {
doc := CreateDoc(&EnvAmbiguous{})
expected := &Context{
Variables: map[Identifier]*Type{
"A": {
Kind: "struct",
Name: "A",
},
"AmbiguousField": {
Kind: "int",
},
"B": {
Kind: "struct",
Name: "B",
},
"OkField": {
Kind: "int",
},
"C": {
Kind: "struct",
Name: "C",
},
},
Types: map[TypeName]*Type{
"A": {
Kind: "struct",
Fields: map[Identifier]*Type{
"AmbiguousField": {Kind: "int"},
"OkField": {Kind: "int"},
},
},
"B": {
Kind: "struct",
Fields: map[Identifier]*Type{
"AmbiguousField": {Kind: "string"},
},
},
"C": {
Kind: "struct",
Fields: map[Identifier]*Type{
"A": {Kind: "struct", Name: "A"},
"AmbiguousField": {Kind: "int"},
"B": {Kind: "struct", Name: "B"},
"OkField": {Kind: "int"},
},
},
},
PkgPath: "github.com/expr-lang/expr/docgen_test",
}
assert.Equal(t, expected.Markdown(), doc.Markdown())
}
func TestCreateDoc_FromMap(t *testing.T) {
env := map[string]any{
"Tweets": []*Tweet{},
"Config": struct {
MaxSize int
}{},
"Max": math.Max,
}
Operators = nil
Builtins = nil
doc := CreateDoc(env)
expected := &Context{
Variables: map[Identifier]*Type{
"Tweets": {
Kind: "array",
Type: &Type{
Kind: "struct",
Name: "docgen_test.Tweet",
},
},
"Config": {
Kind: "struct",
Fields: map[Identifier]*Type{
"MaxSize": {Kind: "int"},
},
},
"Max": {
Kind: "func",
Arguments: []*Type{
{Kind: "float"},
{Kind: "float"},
},
Return: &Type{Kind: "float"},
},
},
Types: map[TypeName]*Type{
"docgen_test.Tweet": {
Kind: "struct",
Fields: map[Identifier]*Type{
"Size": {Kind: "int"},
"Message": {Kind: "string"},
},
},
},
}
require.EqualValues(t, expected.Markdown(), doc.Markdown())
}
func TestContext_Markdown(t *testing.T) {
doc := CreateDoc(&Env{})
md := doc.Markdown()
require.True(t, len(md) > 0)
}
================================================
FILE: docgen/markdown.go
================================================
package docgen
import (
"fmt"
"sort"
"strings"
)
func (c *Context) Markdown() string {
var variables []string
for name := range c.Variables {
variables = append(variables, string(name))
}
var types []string
for name := range c.Types {
types = append(types, string(name))
}
sort.Strings(variables)
sort.Strings(types)
out := `### Variables
| Name | Type |
|------|------|
`
for _, name := range variables {
v := c.Variables[Identifier(name)]
if v.Kind == "func" {
continue
}
if v.Kind == "operator" {
continue
}
out += fmt.Sprintf("| %v | %v |\n", name, link(v))
}
out += `
### Functions
| Name | Return type |
|------|-------------|
`
for _, name := range variables {
v := c.Variables[Identifier(name)]
if v.Kind == "func" {
args := make([]string, len(v.Arguments))
for i, arg := range v.Arguments {
args[i] = link(arg)
}
out += fmt.Sprintf("| %v(%v) | %v |\n", name, strings.Join(args, ", "), link(v.Return))
}
}
out += "\n### Types\n"
for _, name := range types {
t := c.Types[TypeName(name)]
out += fmt.Sprintf("#### %v\n", name)
out += fields(t)
out += "\n"
}
return out
}
func link(t *Type) string {
if t == nil {
return "nil"
}
if t.Name != "" {
return fmt.Sprintf("[%v](#%v)", t.Name, t.Name)
}
if t.Kind == "array" {
return fmt.Sprintf("array(%v)", link(t.Type))
}
if t.Kind == "map" {
return fmt.Sprintf("map(%v => %v)", link(t.Key), link(t.Type))
}
return fmt.Sprintf("`%v`", t.Kind)
}
func fields(t *Type) string {
var fields []string
for field := range t.Fields {
fields = append(fields, string(field))
}
sort.Strings(fields)
out := ""
foundFields := false
for _, name := range fields {
v := t.Fields[Identifier(name)]
if v.Kind != "func" {
if !foundFields {
out += "| Field | Type |\n|---|---|\n"
}
foundFields = true
out += fmt.Sprintf("| %v | %v |\n", name, link(v))
}
}
foundMethod := false
for _, name := range fields {
v := t.Fields[Identifier(name)]
if v.Kind == "func" {
if !foundMethod {
out += "\n| Method | Returns |\n|---|---|\n"
}
foundMethod = true
args := make([]string, len(v.Arguments))
for i, arg := range v.Arguments {
args[i] = link(arg)
}
out += fmt.Sprintf("| %v(%v) | %v |\n", name, strings.Join(args, ", "), link(v.Return))
}
}
return out
}
================================================
FILE: docs/configuration.md
================================================
# Configuration
## Return type
Usually, the return type of expression is anything. But we can instruct type checker to verify the return type of the
expression.
For example, in filter expressions, we expect the return type to be a boolean.
```go
program, err := expr.Compile(code, expr.AsBool())
if err != nil {
panic(err)
}
output, err := expr.Run(program, env)
if err != nil {
panic(err)
}
ok := output.(bool) // It is safe to assert the output to bool, if the expression is type checked as bool.
```
If `code` variable for example returns a string, the compiler will return an error.
Expr has a few options to specify the return type:
- [expr.AsBool()](https://pkg.go.dev/github.com/expr-lang/expr#AsBool) - expects the return type to be a bool.
- [expr.AsInt()](https://pkg.go.dev/github.com/expr-lang/expr#AsInt) - expects the return type to be an int (float64,
uint, int32, and other will be cast to int).
- [expr.AsInt64()](https://pkg.go.dev/github.com/expr-lang/expr#AsInt64) - expects the return type to be an int64 (
float64, uint, int32, and other will be cast to int64).
- [expr.AsFloat64()](https://pkg.go.dev/github.com/expr-lang/expr#AsFloat64) - expects the return type to be a float64 (
float32 will be cast to float64).
- [expr.AsAny()](https://pkg.go.dev/github.com/expr-lang/expr#AsAny) - expects the return type to be anything.
- [expr.AsKind(reflect.Kind)](https://pkg.go.dev/github.com/expr-lang/expr#AsKind) - expects the return type to be a
specific kind.
:::tip Warn on any
By default, type checker will accept any type, even if the return type is specified. Consider following examples:
```expr
let arr = [1, 2, 3]; arr[0]
```
The return type of the expression is `any`. Arrays created in Expr are of type `[]any`. The type checker will not return
an error if the return type is specified as `expr.AsInt()`. The output of the expression is `1`, which is an int, but the
type checker will not return an error.
But we can instruct the type checker to warn us if the return type is `any`. Use [`expr.WarnOnAny()`](https://pkg.go.dev/github.com/expr-lang/expr#WarnOnAny) to enable this behavior.
```go
program, err := expr.Compile(code, expr.AsInt(), expr.WarnOnAny())
```
The type checker will return an error if the return type is `any`. We need to modify the expression to return a specific
type.
```expr
let arr = [1, 2, 3]; int(arr[0])
```
:::
## WithContext
Although the compiled program is guaranteed to be terminated, some user defined functions may not be. For example, if a
user defined function calls a remote service, we may want to pass a context to the function.
This is possible via the [`WithContext`](https://pkg.go.dev/github.com/expr-lang/expr#WithContext) option.
This option will modify function calls to include the context as the first argument (only if the function signature
accepts a context).
```expr
customFunc(42)
// will be transformed to
customFunc(ctx, 42)
```
Function `expr.WithContext()` takes the name of context variable. The context variable must be defined in the environment.
```go
env := map[string]any{
"ctx": context.Background(),
"customFunc": func(ctx context.Context, a int) int {
return a
},
}
program, err := expr.Compile(code, expr.Env(env), expr.WithContext("ctx"))
```
## ConstExpr
For some user defined functions, we may want to evaluate the expression at compile time. This is possible via the
[`ConstExpr`](https://pkg.go.dev/github.com/expr-lang/expr#ConstExpr) option.
```go
func fib(n int) int {
if n <= 1 {
return n
}
return fib(n-1) + fib(n-2)
}
env := map[string]any{
"fib": fib,
}
program, err := expr.Compile(`fib(10)`, expr.Env(env), expr.ConstExpr("fib"))
```
If all arguments of the function are constants, the function will be evaluated at compile time. The result of the function
will be used as a constant in the expression.
```expr
fib(10) // will be transformed to 55 during the compilation
fib(12+12) // will be transformed to 267914296 during the compilation
fib(x) // will **not** be transformed and will be evaluated at runtime
```
## Timezone
By default, the timezone is set to `time.Local`. We can change the timezone via the [`Timezone`](https://pkg.go.dev/github.com/expr-lang/expr#Timezone) option.
```go
program, err := expr.Compile(code, expr.Timezone(time.UTC))
```
The timezone is used for the following functions:
```expr
date("2024-11-23 12:00:00") // parses the date in the specified timezone
now() // returns the current time in the specified timezone
```
================================================
FILE: docs/environment.md
================================================
# Environment
The environment is a map or a struct that contains the variables and functions that the expression can access.
## Struct as Environment
Let's consider the following example:
```go
type Env struct {
UpdatedAt time.Time
Posts []Post
Map map[string]string `expr:"tags"`
}
```
The `Env` struct contains 3 variables that the expression can access: `UpdatedAt`, `Posts`, and `tags`.
:::info
The `expr` tag is used to rename the `Map` field to `tags` variable in the expression.
:::
The `Env` struct can also contain methods. The methods defined on the struct become functions that the expression can
call.
```go
func (Env) Format(t time.Time) string {
return t.Format(time.RFC822)
}
```
:::tip
Methods defined on embedded structs are also accessible.
```go
type Env struct {
Helpers
}
type Helpers struct{}
func (Helpers) Format(t time.Time) string {
return t.Format(time.RFC822)
}
```
:::
We can use an empty struct `Env{}` to with [expr.Env](https://pkg.go.dev/github.com/expr-lang/expr#Env) to create an environment. Expr will use reflection to find
the fields and methods of the struct.
```go
program, err := expr.Compile(code, expr.Env(Env{}))
```
Compiler will type check the expression against the environment. After the compilation, we can run the program with the environment.
You should use the same type of environment that you passed to the `expr.Env` function.
```go
output, err := expr.Run(program, Env{
UpdatedAt: time.Now(),
Posts: []Post{{Title: "Hello, World!"}},
Map: map[string]string{"tag1": "value1"},
})
```
## Map as Environment
You can also use a map as an environment.
```go
env := map[string]any{
"UpdatedAt": time.Time{},
"Posts": []Post{},
"tags": map[string]string{},
"sprintf": fmt.Sprintf,
}
program, err := expr.Compile(code, expr.Env(env))
```
A map defines variables and functions that the expression can access. The key is the variable name, and the type
is the value's type.
```go
env := map[string]any{
"object": map[string]any{
"field": 42,
},
"struct": struct {
Field int `expr:"field"`
}{42},
}
```
Expr will infer the type of the `object` variable as `map[string]any`.
Accessing fields of the `object` and `struct` variables will return the following results.
```expr
object.field // 42
object.unknown // nil (no error)
struct.field // 42
struct.unknown // error (unknown field)
foobar // error (unknown variable)
```
:::note
The `foobar` variable is not defined in the environment.
By default, Expr will return an error if unknown variables are used in the expression.
You can disable this behavior by passing [`AllowUndefinedVariables`](https://pkg.go.dev/github.com/expr-lang/expr#AllowUndefinedVariables) option to the compiler.
:::
================================================
FILE: docs/functions.md
================================================
# Functions
Expr comes with a set of [builtin](language-definition.md) functions, but you can also define your own functions.
The easiest way to define a custom function is to add it to the environment.
```go
env := map[string]any{
"add": func(a, b int) int {
return a + b
},
}
```
Or you can use functions defined on a struct:
```go
type Env struct{}
func (Env) Add(a, b int) int {
return a + b
}
```
:::info
If functions are marked with [`ConstExpr`](./configuration.md#constexpr) option, they will be evaluated at compile time.
:::
The best way to define a function from a performance perspective is to use a [`Function`](https://pkg.go.dev/github.com/expr-lang/expr#Function) option.
```go
atoi := expr.Function(
"atoi",
func(params ...any) (any, error) {
return strconv.Atoi(params[0].(string))
},
)
program, err := expr.Compile(`atoi("42")`, atoi)
```
Type checker sees the `atoi` function as a function with a variadic number of arguments of type `any`, and returns
a value of type `any`. But, we can specify the types of arguments and the return value by adding the correct function
signature or multiple signatures.
```go
atoi := expr.Function(
"atoi",
func(params ...any) (any, error) {
return strconv.Atoi(params[0].(string))
},
// highlight-next-line
new(func(string) int),
)
```
Or we can simply reuse the strconv.Atoi function as a type:
```go
atoi := expr.Function(
"atoi",
func(params ...any) (any, error) {
return strconv.Atoi(params[0].(string))
},
// highlight-next-line
strconv.Atoi,
)
```
It is possible to define multiple signatures for a function:
```go
toInt := expr.Function(
"toInt",
func(params ...any) (any, error) {
switch params[0].(type) {
case float64:
return int(params[0].(float64)), nil
case string:
return strconv.Atoi(params[0].(string))
}
return nil, fmt.Errorf("invalid type")
},
// highlight-start
new(func(float64) int),
new(func(string) int),
// highlight-end
)
```
================================================
FILE: docs/getting-started.md
================================================
# Getting Started
**Expr** is a simple, fast and extensible expression language for Go. It is
designed to be easy to use and integrate into your Go application. Let's delve
deeper into its core features:
- **Memory safe** - Designed to prevent vulnerabilities like buffer overflows and memory leaks.
- **Type safe** - Enforces strict type rules, aligning with Go's type system.
- **Terminating** - Ensures every expression evaluation cannot loop indefinitely.
- **Side effects free** - Evaluations won't modify global states or variables.
Let's start with a simple example:
```go
program, err := expr.Compile(`2 + 2`)
if err != nil {
panic(err)
}
output, err := expr.Run(program, nil)
if err != nil {
panic(err)
}
fmt.Print(output) // 4
```
Expr compiles the expression `2 + 2` into a bytecode program. Then we run
the program and get the output.
:::tip
In performance-critical applications, you can reuse the compiled program. Compiled programs are safe for concurrent use.
**Compile once** and run **multiple** times.
:::
The `expr.Compile` function returns a `*vm.Program` and an error. The `expr.Run` function takes a program and an
environment. The environment is a map of variables that can be used in the expression. In this example, we use `nil` as
an environment because we don't need any variables.
Now let's pass some variables to the expression:
```go
env := map[string]any{
"foo": 100,
"bar": 200,
}
program, err := expr.Compile(`foo + bar`, expr.Env(env))
if err != nil {
panic(err)
}
output, err := expr.Run(program, env)
if err != nil {
panic(err)
}
fmt.Print(output) // 300
```
Why do we need to pass the environment to the `expr.Compile` function? Expr can be used as a type-safe language.
Expr can infer the type of the expression and check it against the environment.
Here is an example:
```go
env := map[string]any{
"name": "Anton",
"age": 35,
}
program, err := expr.Compile(`name + age`, expr.Env(env))
if err != nil {
// highlight-next-line
panic(err) // Will panic with "invalid operation: string + int"
}
```
Expr can work with any Go types:
```go
env := map[string]any{
"greet": "Hello, %v!",
"names": []string{"world", "you"},
"sprintf": fmt.Sprintf,
}
program, err := expr.Compile(`sprintf(greet, names[0])`, expr.Env(env))
if err != nil {
panic(err)
}
output, err := expr.Run(program, env)
if err != nil {
panic(err)
}
fmt.Print(output) // Hello, world!
```
Also, Expr can use a struct as an environment. Here is an example:
```go
type Env struct {
Posts []Post `expr:"posts"`
}
func (Env) Format(t time.Time) string { // Methods defined on the struct become functions.
return t.Format(time.RFC822)
}
type Post struct {
Body string
Date time.Time
}
func main() {
code := `map(posts, Format(.Date) + ": " + .Body)`
program, err := expr.Compile(code, expr.Env(Env{})) // Pass the struct as an environment.
if err != nil {
panic(err)
}
env := Env{
Posts: []Post{
{"Oh My God!", time.Now()},
{"How you doin?", time.Now()},
{"Could I be wearing any more clothes?", time.Now()},
},
}
output, err := expr.Run(program, env)
if err != nil {
panic(err)
}
fmt.Print(output)
}
```
The compiled program can be reused between runs.
```go
type Env struct {
X int
Y int
}
program, err := expr.Compile(`X + Y`, expr.Env(Env{}))
if err != nil {
panic(err)
}
output, err := expr.Run(program, Env{1, 2})
if err != nil {
panic(err)
}
fmt.Print(output) // 3
output, err = expr.Run(program, Env{3, 4})
if err != nil {
panic(err)
}
fmt.Print(output) // 7
```
:::info Eval = Compile + Run
For one-off expressions, you can use the `expr.Eval` function. It compiles and runs the expression in one step.
```go
output, err := expr.Eval(`2 + 2`, env)
```
:::
================================================
FILE: docs/language-definition.md
================================================
# Language Definition
**Expr** is a simple expression language that can be used to evaluate expressions.
## Literals
| Comment |
/* */ or //
|
| Boolean |
true, false
|
| Integer |
42, 0x2A, 0o52, 0b101010
|
| Float |
0.5, .5
|
| String |
"foo", 'bar'
|
| Array |
[1, 2, 3]
|
| Map |
{a: 1, b: 2, c: 3}
|
| Nil |
nil
|
| Bytes |
b"hello", b'\xff\x00'
|
### Strings
Strings can be enclosed in single quotes or double quotes. Strings can contain escape sequences, like `\n` for newline,
`\t` for tab, `\uXXXX` for Unicode code points.
```expr
"Hello\nWorld"
```
For multiline strings, use backticks:
```expr
`Hello
World`
```
Backticks strings are raw strings, they do not support escape sequences.
### Bytes
Bytes literals are represented by string literals preceded by a `b` or `B` character.
The bytes literal returns a `[]byte` value.
```expr
b"abc" // []byte{97, 98, 99}
```
Non-ASCII characters are UTF-8 encoded:
```expr
b"ÿ" // []byte{195, 191} - UTF-8 encoding of ÿ
```
Bytes literals support escape sequences for specifying arbitrary byte values:
- `\xNN` - hexadecimal escape (2 hex digits, value 0-255)
- `\NNN` - octal escape (3 octal digits, value 0-377)
- `\n`, `\t`, `\r`, etc. - standard escape sequences
```expr
b"\xff" // []byte{255}
b"\x00\x01" // []byte{0, 1}
b"\101" // []byte{65} - octal for 'A'
```
:::note
Unlike string literals, bytes literals do not support `\u` or `\U` Unicode escapes.
Use `\x` escapes for arbitrary byte values.
:::
## Operators
| Arithmetic |
+, -, *, /, % (modulus), ^ or ** (exponent)
|
| Comparison |
==, !=, <, >, <=, >=
|
| Logical |
not or !, and or &&, or or ||
|
| Conditional |
?: (ternary), ?? (nil coalescing), if {} else {} (multiline)
|
| Membership |
[], ., ?., in
|
| String |
+ (concatenation), contains, startsWith, endsWith
|
| Regex |
matches
|
| Range |
..
|
| Slice |
[:]
|
| Pipe |
|
|
### Membership Operator
Fields of structs and items of maps can be accessed with `.` operator
or `[]` operator. Next two expressions are equivalent:
```expr
user.Name
user["Name"]
```
Elements of arrays and slices can be accessed with
`[]` operator. Negative indices are supported with `-1` being
the last element.
```expr
array[0] // first element
array[-1] // last element
```
The `in` operator can be used to check if an item is in an array or a map.
```expr
"John" in ["John", "Jane"]
"name" in {"name": "John", "age": 30}
```
#### Optional chaining
The `?.` operator can be used to access a field of a struct or an item of a map
without checking if the struct or the map is `nil`. If the struct or the map is
`nil`, the result of the expression is `nil`.
```expr
author.User?.Name
```
Is equivalent to:
```expr
author.User != nil ? author.User.Name : nil
```
#### Nil coalescing
The `??` operator can be used to return the left-hand side if it is not `nil`,
otherwise the right-hand side is returned.
```expr
author.User?.Name ?? "Anonymous"
```
Is equivalent to:
```expr
author.User != nil ? author.User.Name : "Anonymous"
```
### Slice Operator
The slice operator `[:]` can be used to access a slice of an array.
For example, variable **array** is `[1, 2, 3, 4, 5]`:
```expr
array[1:4] == [2, 3, 4]
array[1:-1] == [2, 3, 4]
array[:3] == [1, 2, 3]
array[3:] == [4, 5]
array[:] == array
```
### Pipe Operator
The pipe operator `|` can be used to pass the result of the left-hand side
expression as the first argument of the right-hand side expression.
```expr
user.Name | lower() | split(" ")
```
Is equivalent to:
```expr
split(lower(user.Name), " ")
```
### Range Operator
The range operator `..` can be used to create a range of integers.
```expr
1..3 == [1, 2, 3]
```
## Variables
Variables can be declared with the `let` keyword. The variable name must start with a letter or an underscore.
The variable name can contain letters, digits and underscores. After the variable is declared, it can be used in the
expression.
```expr
let x = 42; x * 2
```
A few variables can be declared by a few `let` statements separated by a semicolon.
```expr
let x = 42;
let y = 2;
x * y
```
Here is an example of variable with pipe operator:
```expr
let name = user.Name | lower() | split(" ");
"Hello, " + name[0] + "!"
```
### $env
The `$env` variable is a map of all variables passed to the expression.
```expr
foo.Name == $env["foo"].Name
$env["var with spaces"]
```
Think of `$env` as a global variable that contains all variables.
The `$env` can be used to check if a variable is defined:
```expr
'foo' in $env
```
## Predicate
The predicate is an expression. Predicates can be used in functions like `filter`, `all`, `any`, `one`, `none`, etc.
For example, next expression creates a new array from 0 to 9 and then filters it by even numbers:
```expr
filter(0..9, {# % 2 == 0})
```
If items of the array is a struct or a map, it is possible to access fields with
omitted `#` symbol (`#.Value` becomes `.Value`).
```expr
filter(tweets, {len(.Content) > 240})
```
Braces `{` `}` can be omitted:
```expr
filter(tweets, len(.Content) > 240)
```
:::tip
In nested predicates, to access the outer variable, use [variables](#variables).
```expr
filter(posts, {
let post = #;
any(.Comments, .Author == post.Author)
})
```
:::
## String Functions
### trim(str[, chars]) {#trim}
Removes white spaces from both ends of a string `str`.
If the optional `chars` argument is given, it is a string specifying the set of characters to be removed.
```expr
trim(" Hello ") == "Hello"
trim("__Hello__", "_") == "Hello"
```
### trimPrefix(str, prefix) {#trimPrefix}
Removes the specified prefix from the string `str` if it starts with that prefix.
```expr
trimPrefix("HelloWorld", "Hello") == "World"
```
### trimSuffix(str, suffix) {#trimSuffix}
Removes the specified suffix from the string `str` if it ends with that suffix.
```expr
trimSuffix("HelloWorld", "World") == "Hello"
```
### upper(str) {#upper}
Converts all the characters in string `str` to uppercase.
```expr
upper("hello") == "HELLO"
```
### lower(str) {#lower}
Converts all the characters in string `str` to lowercase.
```expr
lower("HELLO") == "hello"
```
### split(str, delimiter[, n]) {#split}
Splits the string `str` at each instance of the delimiter and returns an array of substrings.
```expr
split("apple,orange,grape", ",") == ["apple", "orange", "grape"]
split("apple,orange,grape", ",", 2) == ["apple", "orange,grape"]
```
### splitAfter(str, delimiter[, n]) {#splitAfter}
Splits the string `str` after each instance of the delimiter.
```expr
splitAfter("apple,orange,grape", ",") == ["apple,", "orange,", "grape"]
splitAfter("apple,orange,grape", ",", 2) == ["apple,", "orange,grape"]
```
### replace(str, old, new) {#replace}
Replaces all occurrences of `old` in string `str` with `new`.
```expr
replace("Hello World", "World", "Universe") == "Hello Universe"
```
### repeat(str, n) {#repeat}
Repeats the string `str` `n` times.
```expr
repeat("Hi", 3) == "HiHiHi"
```
### indexOf(str, substring) {#indexOf}
Returns the index of the first occurrence of the substring in string `str` or -1 if not found.
```expr
indexOf("apple pie", "pie") == 6
```
### lastIndexOf(str, substring) {#lastIndexOf}
Returns the index of the last occurrence of the substring in string `str` or -1 if not found.
```expr
lastIndexOf("apple pie apple", "apple") == 10
```
### hasPrefix(str, prefix) {#hasPrefix}
Returns `true` if string `str` starts with the given prefix.
```expr
hasPrefix("HelloWorld", "Hello") == true
```
### hasSuffix(str, suffix) {#hasSuffix}
Returns `true` if string `str` ends with the given suffix.
```expr
hasSuffix("HelloWorld", "World") == true
```
## Date Functions
Expr has a built-in support for Go's [time package](https://pkg.go.dev/time).
It is possible to subtract two dates and get the duration between them:
```expr
createdAt - now()
```
It is possible to add a duration to a date:
```expr
createdAt + duration("1h")
```
And it is possible to compare dates:
```expr
createdAt > now() - duration("1h")
```
### now() {#now}
Returns the current date as a [time.Time](https://pkg.go.dev/time#Time) value.
```expr
now().Year() == 2024
```
### duration(str) {#duration}
Returns [time.Duration](https://pkg.go.dev/time#Duration) value of the given string `str`.
Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
```expr
duration("1h").Seconds() == 3600
```
### date(str[, format[, timezone]]) {#date}
Converts the given string `str` into a date representation.
If the optional `format` argument is given, it is a string specifying the format of the date.
The format string uses the same formatting rules as the standard
Go [time package](https://pkg.go.dev/time#pkg-constants).
If the optional `timezone` argument is given, it is a string specifying the timezone of the date.
If the `format` argument is not given, the `v` argument must be in one of the following formats:
- 2006-01-02
- 15:04:05
- 2006-01-02 15:04:05
- RFC3339
- RFC822,
- RFC850,
- RFC1123,
```expr
date("2023-08-14")
date("15:04:05")
date("2023-08-14T00:00:00Z")
date("2023-08-14 00:00:00", "2006-01-02 15:04:05", "Europe/Zurich")
```
Available methods on the date:
- `Year()` - returns the year
- `Month()` - returns the month (starting from 1)
- `Day()` - returns the day of the month
- `Hour()` - returns the hour
- `Minute()` - returns the minute
- `Second()` - returns the second
- `Weekday()` - returns the day of the week
- `YearDay()` - returns the day of the year
- and [more](https://pkg.go.dev/time#Time).
```expr
date("2023-08-14").Year() == 2023
```
### timezone(str) {#timezone}
Returns the timezone of the given string `str`. List of available timezones can be
found [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
```expr
timezone("Europe/Zurich")
timezone("UTC")
```
To convert a date to a different timezone, use the [`In()`](https://pkg.go.dev/time#Time.In) method:
```expr
date("2023-08-14 00:00:00").In(timezone("Europe/Zurich"))
```
## Number Functions
### max(n1, n2) {#max}
Returns the maximum of the two numbers `n1` and `n2`.
```expr
max(5, 7) == 7
```
### min(n1, n2) {#min}
Returns the minimum of the two numbers `n1` and `n2`.
```expr
min(5, 7) == 5
```
### abs(n) {#abs}
Returns the absolute value of a number.
```expr
abs(-5) == 5
```
### ceil(n) {#ceil}
Returns the least integer value greater than or equal to x.
```expr
ceil(1.5) == 2.0
```
### floor(n) {#floor}
Returns the greatest integer value less than or equal to x.
```expr
floor(1.5) == 1.0
```
### round(n) {#round}
Returns the nearest integer, rounding half away from zero.
```expr
round(1.5) == 2.0
```
## Array Functions
### all(array, predicate) {#all}
Returns **true** if all elements satisfies the [predicate](#predicate).
If the array is empty, returns **true**.
```expr
all(tweets, {.Size < 280})
```
### any(array, predicate) {#any}
Returns **true** if any elements satisfies the [predicate](#predicate).
If the array is empty, returns **false**.
```expr
any(tweets, {.Size > 280})
```
### one(array, predicate) {#one}
Returns **true** if _exactly one_ element satisfies the [predicate](#predicate).
If the array is empty, returns **false**.
```expr
one(participants, {.Winner})
```
### none(array, predicate) {#none}
Returns **true** if _all elements does not_ satisfy the [predicate](#predicate).
If the array is empty, returns **true**.
```expr
none(tweets, {.Size > 280})
```
### map(array, predicate) {#map}
Returns new array by applying the [predicate](#predicate) to each element of
the array.
```expr
map(tweets, {.Size})
```
### filter(array, predicate) {#filter}
Returns new array by filtering elements of the array by [predicate](#predicate).
```expr
filter(users, .Name startsWith "J")
```
### find(array, predicate) {#find}
Finds the first element in an array that satisfies the [predicate](#predicate).
```expr
find([1, 2, 3, 4], # > 2) == 3
```
### findIndex(array, predicate) {#findIndex}
Finds the index of the first element in an array that satisfies the [predicate](#predicate).
```expr
findIndex([1, 2, 3, 4], # > 2) == 2
```
### findLast(array, predicate) {#findLast}
Finds the last element in an array that satisfies the [predicate](#predicate).
```expr
findLast([1, 2, 3, 4], # > 2) == 4
```
### findLastIndex(array, predicate) {#findLastIndex}
Finds the index of the last element in an array that satisfies the [predicate](#predicate).
```expr
findLastIndex([1, 2, 3, 4], # > 2) == 3
```
### groupBy(array, predicate) {#groupBy}
Groups the elements of an array by the result of the [predicate](#predicate).
```expr
groupBy(users, .Age)
```
### count(array[, predicate]) {#count}
Returns the number of elements what satisfies the [predicate](#predicate).
```expr
count(users, .Age > 18)
```
Equivalent to:
```expr
len(filter(users, .Age > 18))
```
If the predicate is not given, returns the number of `true` elements in the array.
```expr
count([true, false, true]) == 2
```
### concat(array1, array2[, ...]) {#concat}
Concatenates two or more arrays.
```expr
concat([1, 2], [3, 4]) == [1, 2, 3, 4]
```
### flatten(array) {#flatten}
Flattens given array into one-dimensional array.
```expr
flatten([1, 2, [3, 4]]) == [1, 2, 3, 4]
```
### uniq(array) {#uniq}
Removes duplicates from an array.
```expr
uniq([1, 2, 3, 2, 1]) == [1, 2, 3]
```
### join(array[, delimiter]) {#join}
Joins an array of strings into a single string with the given delimiter.
If no delimiter is given, an empty string is used.
```expr
join(["apple", "orange", "grape"], ",") == "apple,orange,grape"
join(["apple", "orange", "grape"]) == "appleorangegrape"
```
### reduce(array, predicate[, initialValue]) {#reduce}
Applies a predicate to each element in the array, reducing the array to a single value.
Optional `initialValue` argument can be used to specify the initial value of the accumulator.
If `initialValue` is not given, the first element of the array is used as the initial value.
Following variables are available in the predicate:
- `#` - the current element
- `#acc` - the accumulator
- `#index` - the index of the current element
```expr
reduce(1..9, #acc + #)
reduce(1..9, #acc + #, 0)
```
### sum(array[, predicate]) {#sum}
Returns the sum of all numbers in the array.
```expr
sum([1, 2, 3]) == 6
```
If the optional `predicate` argument is given, it is a predicate that is applied on each element
of the array before summing.
```expr
sum(accounts, .Balance)
```
Equivalent to:
```expr
reduce(accounts, #acc + .Balance, 0)
// or
sum(map(accounts, .Balance))
```
### mean(array) {#mean}
Returns the average of all numbers in the array.
```expr
mean([1, 2, 3]) == 2.0
```
### median(array) {#median}
Returns the median of all numbers in the array.
```expr
median([1, 2, 3]) == 2.0
```
### first(array) {#first}
Returns the first element from an array. If the array is empty, returns `nil`.
```expr
first([1, 2, 3]) == 1
```
### last(array) {#last}
Returns the last element from an array. If the array is empty, returns `nil`.
```expr
last([1, 2, 3]) == 3
```
### take(array, n) {#take}
Returns the first `n` elements from an array. If the array has fewer than `n` elements, returns the whole array.
```expr
take([1, 2, 3, 4], 2) == [1, 2]
```
### reverse(array) {#reverse}
Return new reversed copy of the array.
```expr
reverse([3, 1, 4]) == [4, 1, 3]
reverse(reverse([3, 1, 4])) == [3, 1, 4]
```
### sort(array[, order]) {#sort}
Sorts an array in ascending order. Optional `order` argument can be used to specify the order of sorting: `asc`
or `desc`.
```expr
sort([3, 1, 4]) == [1, 3, 4]
sort([3, 1, 4], "desc") == [4, 3, 1]
```
### sortBy(array[, predicate, order]) {#sortBy}
Sorts an array by the result of the [predicate](#predicate). Optional `order` argument can be used to specify the order
of sorting: `asc` or `desc`.
```expr
sortBy(users, .Age)
sortBy(users, .Age, "desc")
```
## Map Functions
### keys(map) {#keys}
Returns an array containing the keys of the map.
```expr
keys({"name": "John", "age": 30}) == ["name", "age"]
```
### values(map) {#values}
Returns an array containing the values of the map.
```expr
values({"name": "John", "age": 30}) == ["John", 30]
```
## Type Conversion Functions
### type(v) {#type}
Returns the type of the given value `v`.
Returns on of the following types:
- `nil`
- `bool`
- `int`
- `uint`
- `float`
- `string`
- `array`
- `map`.
For named types and structs, the type name is returned.
```expr
type(42) == "int"
type("hello") == "string"
type(now()) == "time.Time"
```
### int(v) {#int}
Returns the integer value of a number or a string.
```expr
int("123") == 123
```
### float(v) {#float}
Returns the float value of a number or a string.
```expr
float("123.45") == 123.45
```
### string(v) {#string}
Converts the given value `v` into a string representation.
```expr
string(123) == "123"
```
### toJSON(v) {#toJSON}
Converts the given value `v` to its JSON string representation.
```expr
toJSON({"name": "John", "age": 30})
```
### fromJSON(v) {#fromJSON}
Parses the given JSON string `v` and returns the corresponding value.
```expr
fromJSON('{"name": "John", "age": 30}')
```
### toBase64(v) {#toBase64}
Encodes the string `v` into Base64 format.
```expr
toBase64("Hello World") == "SGVsbG8gV29ybGQ="
```
### fromBase64(v) {#fromBase64}
Decodes the Base64 encoded string `v` back to its original form.
```expr
fromBase64("SGVsbG8gV29ybGQ=") == "Hello World"
```
### toPairs(map) {#toPairs}
Converts a map to an array of key-value pairs.
```expr
toPairs({"name": "John", "age": 30}) == [["name", "John"], ["age", 30]]
```
### fromPairs(array) {#fromPairs}
Converts an array of key-value pairs to a map.
```expr
fromPairs([["name", "John"], ["age", 30]]) == {"name": "John", "age": 30}
```
## Miscellaneous Functions
### len(v) {#len}
Returns the length of an array, a map or a string.
```expr
len([1, 2, 3]) == 3
len({"name": "John", "age": 30}) == 2
len("Hello") == 5
```
### get(v, index) {#get}
Retrieves the element at the specified index from an array or map `v`. If the index is out of range, returns `nil`.
Or the key does not exist, returns `nil`.
```expr
get([1, 2, 3], 1) == 2
get({"name": "John", "age": 30}, "name") == "John"
```
## Bitwise Functions
### bitand(int, int) {#bitand}
Returns the values resulting from the bitwise AND operation.
```expr
bitand(0b1010, 0b1100) == 0b1000
```
### bitor(int, int) {#bitor}
Returns the values resulting from the bitwise OR operation.
```expr
bitor(0b1010, 0b1100) == 0b1110
```
### bitxor(int, int) {#bitxor}
Returns the values resulting from the bitwise XOR operation.
```expr
bitxor(0b1010, 0b1100) == 0b110
```
### bitnand(int, int) {#bitnand}
Returns the values resulting from the bitwise AND NOT operation.
```expr
bitnand(0b1010, 0b1100) == 0b10
```
### bitnot(int) {#bitnot}
Returns the values resulting from the bitwise NOT operation.
```expr
bitnot(0b1010) == -0b1011
```
### bitshl(int, int) {#bitshl}
Returns the values resulting from the Left Shift operation.
```expr
bitshl(0b101101, 2) == 0b10110100
```
### bitshr(int, int) {#bitshr}
Returns the values resulting from the Right Shift operation.
```expr
bitshr(0b101101, 2) == 0b1011
```
### bitushr(int, int) {#bitushr}
Returns the values resulting from the unsigned Right Shift operation.
```expr
bitushr(-0b101, 2) == 4611686018427387902
```
================================================
FILE: docs/patch.md
================================================
# Patch
Sometimes it may be necessary to modify an expression before the compilation.
For example, you may want to replace a variable with a constant, transform an expression into a function call,
or even modify the expression to use a different operator.
## Simple example
Let's start with a simple example. We have an expression that uses a variable `foo`:
```go
program, err := expr.Compile(`foo + bar`)
```
We want to replace the `foo` variable with a constant `42`. First, we need to implement a [visitor](./visitor.md):
```go
type FooPatcher struct{}
func (FooPatcher) Visit(node *ast.Node) {
if n, ok := (*node).(*ast.IdentifierNode); ok && n.Value == "foo" {
// highlight-next-line
ast.Patch(node, &ast.IntegerNode{Value: 42})
}
}
```
We used the [ast.Patch](https://pkg.go.dev/github.com/expr-lang/expr/ast#Patch) function to replace the `foo` variable with an integer node.
Now we can use the `FooPatcher` to modify the expression on compilation via the [expr.Patch](https://pkg.go.dev/github.com/expr-lang/expr#Patch) option:
```go
program, err := expr.Compile(`foo + bar`, expr.Patch(FooPatcher{}))
```
## Advanced example
Let's consider a more complex example. We have an expression that uses variables `foo` and `bar` of type `Decimal`:
```go
type Decimal struct {
Value int
}
```
And we want to transform the following expression:
```expr
a + b + c
```
Into functions calls that accept `Decimal` arguments:
```expr
add(add(a, b), c)
```
First, we need to implement a visitor that will transform the expression:
```go
type DecimalPatcher struct{}
var decimalType = reflect.TypeOf(Decimal{})
func (DecimalPatcher) Visit(node *ast.Node) {
if n, ok := (*node).(*ast.BinaryNode); ok && n.Operator == "+" {
if !n.Left.Type().AssignableTo(decimalType) {
return // skip, left side is not a Decimal
}
if !n.Right.Type().AssignableTo(decimalType) {
return // skip, right side is not a Decimal
}
// highlight-start
callNode := &ast.CallNode{
Callee: &ast.IdentifierNode{Value: "add"},
Arguments: []ast.Node{n.Left, n.Right},
}
ast.Patch(node, callNode)
// highlight-end
(*node).SetType(decimalType) // set the type, so the patcher can be applied recursively
}
}
```
We used [Type()](https://pkg.go.dev/github.com/expr-lang/expr/ast#Node.Type) method to get the type of the expression node.
The `AssignableTo` method is used to check if the type is `Decimal`. If both sides are `Decimal`, we replace the expression with a function call.
The important part of this patcher is to set correct types for the nodes. As we constructed a new `CallNode`, it lacks the type information.
So after the first patcher run, if we want the patcher to be applied recursively, we need to set the type of the node.
Now we can use the `DecimalPatcher` to modify the expression:
```go
env := map[string]interface{}{
"a": Decimal{1},
"b": Decimal{2},
"c": Decimal{3},
"add": func(x, y Decimal) Decimal {
return Decimal{x.Value + y.Value}
},
}
code := `a + b + c`
// highlight-next-line
program, err := expr.Compile(code, expr.Env(env), expr.Patch(DecimalPatcher{}))
if err != nil {
panic(err)
}
output, err := expr.Run(program, env)
if err != nil {
panic(err)
}
fmt.Println(output) // Decimal{6}
```
:::info
Expr comes with already implemented patcher that simplifies operator overloading.
The `DecimalPatcher` can be replaced with the [Operator](https://pkg.go.dev/github.com/expr-lang/expr#Operator) option.
```go
program, err := expr.Compile(code, expr.Env(env), expr.Operator("+", "add"))
```
Operator overloading patcher will check if provided functions (`"add"`) satisfy the operator (`"+"`), and
replace the operator with the function call.
:::
================================================
FILE: docs/visitor.md
================================================
# Visitor
Expr provides an interface to traverse the AST of the expression before the compilation.
The `Visitor` interface allows you to collect information about the expression, modify the expression, or even generate
a new expression.
Let's start with an [ast.Visitor](https://pkg.go.dev/github.com/expr-lang/expr/ast#Visitor) implementation which will
collect all variables used in the expression.
Visitor must implement a single method `Visit(*ast.Node)`, which will be called for each node in the AST.
```go
type Visitor struct {
Identifiers []string
}
func (v *Visitor) Visit(node *ast.Node) {
if n, ok := (*node).(*ast.IdentifierNode); ok {
v.Identifiers = append(v.Identifiers, n.Value)
}
}
```
Full list of available AST nodes can be found in the [ast](https://pkg.go.dev/github.com/expr-lang/expr/ast) documentation.
Let's parse the expression and use [ast.Walk](https://pkg.go.dev/github.com/expr-lang/expr/ast#Walk) to traverse the AST:
```go
tree, err := parser.Parse(`foo + bar`)
if err != nil {
panic(err)
}
v := &Visitor{}
// highlight-next-line
ast.Walk(&tree.Node, v)
fmt.Println(v.Identifiers) // [foo, bar]
```
:::note
Although it is possible to access the AST of compiled program, it may be already be modified by patchers, optimizers, etc.
```go
program, err := expr.Compile(`foo + bar`)
if err != nil {
panic(err)
}
// highlight-next-line
node := program.Node()
v := &Visitor{}
ast.Walk(&node, v)
```
:::
================================================
FILE: expr.go
================================================
package expr
import (
"errors"
"fmt"
"reflect"
"time"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/builtin"
"github.com/expr-lang/expr/checker"
"github.com/expr-lang/expr/compiler"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/file"
"github.com/expr-lang/expr/optimizer"
"github.com/expr-lang/expr/parser"
"github.com/expr-lang/expr/patcher"
"github.com/expr-lang/expr/vm"
)
// Option for configuring config.
type Option func(c *conf.Config)
// Env specifies expected input of env for type checks.
// If struct is passed, all fields will be treated as variables,
// as well as all fields of embedded structs and struct itself.
// If map is passed, all items will be treated as variables.
// Methods defined on this type will be available as functions.
func Env(env any) Option {
return func(c *conf.Config) {
c.WithEnv(env)
}
}
// AllowUndefinedVariables allows to use undefined variables inside expressions.
// This can be used with expr.Env option to partially define a few variables.
func AllowUndefinedVariables() Option {
return func(c *conf.Config) {
c.Strict = false
}
}
// Operator allows to replace a binary operator with a function.
func Operator(operator string, fn ...string) Option {
return func(c *conf.Config) {
p := &patcher.OperatorOverloading{
Operator: operator,
Overloads: fn,
Env: &c.Env,
Functions: c.Functions,
NtCache: &c.NtCache,
}
c.Visitors = append(c.Visitors, p)
}
}
// ConstExpr defines func expression as constant. If all argument to this function is constants,
// then it can be replaced by result of this func call on compile step.
func ConstExpr(fn string) Option {
return func(c *conf.Config) {
c.ConstExpr(fn)
}
}
// AsAny tells the compiler to expect any result.
func AsAny() Option {
return func(c *conf.Config) {
c.ExpectAny = true
}
}
// AsKind tells the compiler to expect kind of the result.
func AsKind(kind reflect.Kind) Option {
return func(c *conf.Config) {
c.Expect = kind
c.ExpectAny = true
}
}
// AsBool tells the compiler to expect a boolean result.
func AsBool() Option {
return func(c *conf.Config) {
c.Expect = reflect.Bool
c.ExpectAny = true
}
}
// AsInt tells the compiler to expect an int result.
func AsInt() Option {
return func(c *conf.Config) {
c.Expect = reflect.Int
c.ExpectAny = true
}
}
// AsInt64 tells the compiler to expect an int64 result.
func AsInt64() Option {
return func(c *conf.Config) {
c.Expect = reflect.Int64
c.ExpectAny = true
}
}
// AsFloat64 tells the compiler to expect a float64 result.
func AsFloat64() Option {
return func(c *conf.Config) {
c.Expect = reflect.Float64
c.ExpectAny = true
}
}
// DisableIfOperator disables the `if ... else ...` operator syntax so a custom
// function named `if(...)` can be used without conflicts.
func DisableIfOperator() Option {
return func(c *conf.Config) {
c.DisableIfOperator = true
}
}
// WarnOnAny tells the compiler to warn if expression return any type.
func WarnOnAny() Option {
return func(c *conf.Config) {
if c.Expect == reflect.Invalid {
panic("WarnOnAny() works only with combination with AsInt(), AsBool(), etc. options")
}
c.ExpectAny = false
}
}
// Optimize turns optimizations on or off.
func Optimize(b bool) Option {
return func(c *conf.Config) {
c.Optimize = b
}
}
// DisableShortCircuit turns short circuit off.
func DisableShortCircuit() Option {
return func(c *conf.Config) {
c.ShortCircuit = false
}
}
// Patch adds visitor to list of visitors what will be applied before compiling AST to bytecode.
func Patch(visitor ast.Visitor) Option {
return func(c *conf.Config) {
c.Visitors = append(c.Visitors, visitor)
}
}
// Function adds function to list of functions what will be available in expressions.
func Function(name string, fn func(params ...any) (any, error), types ...any) Option {
return func(c *conf.Config) {
ts := make([]reflect.Type, len(types))
for i, t := range types {
t := reflect.TypeOf(t)
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Func {
panic(fmt.Sprintf("expr: type of %s is not a function", name))
}
ts[i] = t
}
c.Functions[name] = &builtin.Function{
Name: name,
Func: fn,
Types: ts,
}
}
}
// DisableAllBuiltins disables all builtins.
func DisableAllBuiltins() Option {
return func(c *conf.Config) {
for name := range c.Builtins {
c.Disabled[name] = true
}
}
}
// DisableBuiltin disables builtin function.
func DisableBuiltin(name string) Option {
return func(c *conf.Config) {
c.Disabled[name] = true
}
}
// EnableBuiltin enables builtin function.
func EnableBuiltin(name string) Option {
return func(c *conf.Config) {
delete(c.Disabled, name)
}
}
// WithContext passes context to all functions calls with a context.Context argument.
func WithContext(name string) Option {
return func(c *conf.Config) {
c.Visitors = append(c.Visitors, patcher.WithContext{
Name: name,
Functions: c.Functions,
Env: &c.Env,
NtCache: &c.NtCache,
})
}
}
// Timezone sets default timezone for date() and now() builtin functions.
func Timezone(name string) Option {
tz, err := time.LoadLocation(name)
if err != nil {
panic(err)
}
return Patch(patcher.WithTimezone{
Location: tz,
})
}
// MaxNodes sets the maximum number of nodes allowed in the expression.
// By default, the maximum number of nodes is conf.DefaultMaxNodes.
// If MaxNodes is set to 0, the node budget check is disabled.
func MaxNodes(n uint) Option {
return func(c *conf.Config) {
c.MaxNodes = n
}
}
// Compile parses and compiles given input expression to bytecode program.
func Compile(input string, ops ...Option) (*vm.Program, error) {
config := conf.CreateNew()
for _, op := range ops {
op(config)
}
for name := range config.Disabled {
delete(config.Builtins, name)
}
config.Check()
tree, err := checker.ParseCheck(input, config)
if err != nil {
return nil, err
}
if config.Optimize {
err = optimizer.Optimize(&tree.Node, config)
if err != nil {
var fileError *file.Error
if errors.As(err, &fileError) {
return nil, fileError.Bind(tree.Source)
}
return nil, err
}
}
program, err := compiler.Compile(tree, config)
if err != nil {
return nil, err
}
return program, nil
}
// Run evaluates given bytecode program.
func Run(program *vm.Program, env any) (any, error) {
return vm.Run(program, env)
}
// Eval parses, compiles and runs given input.
func Eval(input string, env any) (any, error) {
if _, ok := env.(Option); ok {
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
}
tree, err := parser.Parse(input)
if err != nil {
return nil, err
}
program, err := compiler.Compile(tree, nil)
if err != nil {
return nil, err
}
output, err := Run(program, env)
if err != nil {
return nil, err
}
return output, nil
}
================================================
FILE: expr_test.go
================================================
package expr_test
import (
"context"
"encoding/json"
"fmt"
"os"
"reflect"
"strings"
"sync"
"testing"
"time"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/internal/testify/assert"
"github.com/expr-lang/expr/internal/testify/require"
"github.com/expr-lang/expr/types"
"github.com/expr-lang/expr/vm"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/file"
"github.com/expr-lang/expr/test/mock"
)
func ExampleEval() {
output, err := expr.Eval("greet + name", map[string]any{
"greet": "Hello, ",
"name": "world!",
})
if err != nil {
fmt.Printf("err: %v", err)
return
}
fmt.Printf("%v", output)
// Output: Hello, world!
}
func ExampleEval_runtime_error() {
_, err := expr.Eval(`map(1..3, {1 % (# - 3)})`, nil)
fmt.Print(err)
// Output: runtime error: integer divide by zero (1:14)
// | map(1..3, {1 % (# - 3)})
// | .............^
}
func ExampleCompile() {
env := map[string]any{
"foo": 1,
"bar": 99,
}
program, err := expr.Compile("foo in 1..99 and bar in 1..99", expr.Env(env))
if err != nil {
fmt.Printf("%v", err)
return
}
output, err := expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output)
// Output: true
}
func ExampleEval_bytes_literal() {
// Bytes literal returns []byte.
output, err := expr.Eval(`b"abc"`, nil)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output)
// Output: [97 98 99]
}
func TestDisableIfOperator_AllowsIfFunction(t *testing.T) {
env := map[string]any{
"if": func(x int) int { return x + 1 },
}
program, err := expr.Compile("if(41)", expr.Env(env), expr.DisableIfOperator())
require.NoError(t, err)
out, err := expr.Run(program, env)
require.NoError(t, err)
assert.Equal(t, 42, out)
}
func ExampleEnv() {
type Segment struct {
Origin string
}
type Passengers struct {
Adults int
}
type Meta struct {
Tags map[string]string
}
type Env struct {
Meta
Segments []*Segment
Passengers *Passengers
Marker string
}
code := `all(Segments, {.Origin == "MOW"}) && Passengers.Adults > 0 && Tags["foo"] startsWith "bar"`
program, err := expr.Compile(code, expr.Env(Env{}))
if err != nil {
fmt.Printf("%v", err)
return
}
env := Env{
Meta: Meta{
Tags: map[string]string{
"foo": "bar",
},
},
Segments: []*Segment{
{Origin: "MOW"},
},
Passengers: &Passengers{
Adults: 2,
},
Marker: "test",
}
output, err := expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output)
// Output: true
}
func ExampleEnv_tagged_field_names() {
env := struct {
FirstWord string
Separator string `expr:"Space"`
SecondWord string `expr:"second_word"`
}{
FirstWord: "Hello",
Separator: " ",
SecondWord: "World",
}
output, err := expr.Eval(`FirstWord + Space + second_word`, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output)
// Output: Hello World
}
func ExampleEnv_hidden_tagged_field_names() {
type Internal struct {
Visible string
Hidden string `expr:"-"`
}
type environment struct {
Visible string
Hidden string `expr:"-"`
HiddenInternal Internal `expr:"-"`
VisibleInternal Internal
}
env := environment{
Hidden: "First level secret",
HiddenInternal: Internal{
Visible: "Second level secret",
Hidden: "Also hidden",
},
VisibleInternal: Internal{
Visible: "Not a secret",
Hidden: "Hidden too",
},
}
hiddenValues := []string{
`Hidden`,
`HiddenInternal`,
`HiddenInternal.Visible`,
`HiddenInternal.Hidden`,
`VisibleInternal["Hidden"]`,
}
for _, expression := range hiddenValues {
output, err := expr.Eval(expression, env)
if err == nil || !strings.Contains(err.Error(), "cannot fetch") {
fmt.Printf("unexpected output: %v; err: %v\n", output, err)
return
}
fmt.Printf("%q is hidden as expected\n", expression)
}
visibleValues := []string{
`Visible`,
`VisibleInternal`,
`VisibleInternal["Visible"]`,
}
for _, expression := range visibleValues {
_, err := expr.Eval(expression, env)
if err != nil {
fmt.Printf("unexpected error: %v\n", err)
return
}
fmt.Printf("%q is visible as expected\n", expression)
}
testWithIn := []string{
`not ("Hidden" in $env)`,
`"Visible" in $env`,
`not ("Hidden" in VisibleInternal)`,
`"Visible" in VisibleInternal`,
}
for _, expression := range testWithIn {
val, err := expr.Eval(expression, env)
shouldBeTrue, ok := val.(bool)
if err != nil || !ok || !shouldBeTrue {
fmt.Printf("unexpected result; value: %v; error: %v\n", val, err)
return
}
}
// Output: "Hidden" is hidden as expected
// "HiddenInternal" is hidden as expected
// "HiddenInternal.Visible" is hidden as expected
// "HiddenInternal.Hidden" is hidden as expected
// "VisibleInternal[\"Hidden\"]" is hidden as expected
// "Visible" is visible as expected
// "VisibleInternal" is visible as expected
// "VisibleInternal[\"Visible\"]" is visible as expected
}
func ExampleAsKind() {
program, err := expr.Compile("{a: 1, b: 2}", expr.AsKind(reflect.Map))
if err != nil {
fmt.Printf("%v", err)
return
}
output, err := expr.Run(program, nil)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output)
// Output: map[a:1 b:2]
}
func ExampleAsBool() {
env := map[string]int{
"foo": 0,
}
program, err := expr.Compile("foo >= 0", expr.Env(env), expr.AsBool())
if err != nil {
fmt.Printf("%v", err)
return
}
output, err := expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output.(bool))
// Output: true
}
func ExampleAsBool_error() {
env := map[string]any{
"foo": 0,
}
_, err := expr.Compile("foo + 42", expr.Env(env), expr.AsBool())
fmt.Printf("%v", err)
// Output: expected bool, but got int
}
func ExampleAsInt() {
program, err := expr.Compile("42", expr.AsInt())
if err != nil {
fmt.Printf("%v", err)
return
}
output, err := expr.Run(program, nil)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%T(%v)", output, output)
// Output: int(42)
}
func ExampleAsInt64() {
env := map[string]any{
"rating": 5.5,
}
program, err := expr.Compile("rating", expr.Env(env), expr.AsInt64())
if err != nil {
fmt.Printf("%v", err)
return
}
output, err := expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output.(int64))
// Output: 5
}
func ExampleAsFloat64() {
program, err := expr.Compile("42", expr.AsFloat64())
if err != nil {
fmt.Printf("%v", err)
return
}
output, err := expr.Run(program, nil)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output.(float64))
// Output: 42
}
func ExampleAsFloat64_error() {
_, err := expr.Compile(`!!true`, expr.AsFloat64())
fmt.Printf("%v", err)
// Output: expected float64, but got bool
}
func ExampleWarnOnAny() {
// Arrays always have []any type. The expression return type is any.
// AsInt() instructs compiler to expect int or any, and cast to int,
// if possible. WarnOnAny() instructs to return an error on any type.
_, err := expr.Compile(`[42, true, "yes"][0]`, expr.AsInt(), expr.WarnOnAny())
fmt.Printf("%v", err)
// Output: expected int, but got interface {}
}
func ExampleOperator() {
code := `
Now() > CreatedAt &&
(Now() - CreatedAt).Hours() > 24
`
type Env struct {
CreatedAt time.Time
Now func() time.Time
Sub func(a, b time.Time) time.Duration
After func(a, b time.Time) bool
}
options := []expr.Option{
expr.Env(Env{}),
expr.Operator(">", "After"),
expr.Operator("-", "Sub"),
}
program, err := expr.Compile(code, options...)
if err != nil {
fmt.Printf("%v", err)
return
}
env := Env{
CreatedAt: time.Date(2018, 7, 14, 0, 0, 0, 0, time.UTC),
Now: func() time.Time { return time.Now() },
Sub: func(a, b time.Time) time.Duration { return a.Sub(b) },
After: func(a, b time.Time) bool { return a.After(b) },
}
output, err := expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output)
// Output: true
}
func ExampleOperator_with_decimal() {
type Decimal struct{ N float64 }
code := `A + B - C`
type Env struct {
A, B, C Decimal
Sub func(a, b Decimal) Decimal
Add func(a, b Decimal) Decimal
}
options := []expr.Option{
expr.Env(Env{}),
expr.Operator("+", "Add"),
expr.Operator("-", "Sub"),
}
program, err := expr.Compile(code, options...)
if err != nil {
fmt.Printf("Compile error: %v", err)
return
}
env := Env{
A: Decimal{3},
B: Decimal{2},
C: Decimal{1},
Sub: func(a, b Decimal) Decimal { return Decimal{a.N - b.N} },
Add: func(a, b Decimal) Decimal { return Decimal{a.N + b.N} },
}
output, err := expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output)
// Output: {4}
}
func fib(n int) int {
if n <= 1 {
return n
}
return fib(n-1) + fib(n-2)
}
func ExampleConstExpr() {
code := `[fib(5), fib(3+3), fib(dyn)]`
env := map[string]any{
"fib": fib,
"dyn": 0,
}
options := []expr.Option{
expr.Env(env),
expr.ConstExpr("fib"), // Mark fib func as constant expression.
}
program, err := expr.Compile(code, options...)
if err != nil {
fmt.Printf("%v", err)
return
}
// Only fib(5) and fib(6) calculated on Compile, fib(dyn) can be called at runtime.
env["dyn"] = 7
output, err := expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v\n", output)
// Output: [5 8 13]
}
func ExampleAllowUndefinedVariables() {
code := `name == nil ? "Hello, world!" : sprintf("Hello, %v!", name)`
env := map[string]any{
"sprintf": fmt.Sprintf,
}
options := []expr.Option{
expr.Env(env),
expr.AllowUndefinedVariables(), // Allow to use undefined variables.
}
program, err := expr.Compile(code, options...)
if err != nil {
fmt.Printf("%v", err)
return
}
output, err := expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v\n", output)
env["name"] = "you" // Define variables later on.
output, err = expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v\n", output)
// Output: Hello, world!
// Hello, you!
}
func ExampleAllowUndefinedVariables_zero_value() {
code := `name == "" ? foo + bar : foo + name`
// If environment has different zero values, then undefined variables
// will have it as default value.
env := map[string]string{}
options := []expr.Option{
expr.Env(env),
expr.AllowUndefinedVariables(), // Allow to use undefined variables.
}
program, err := expr.Compile(code, options...)
if err != nil {
fmt.Printf("%v", err)
return
}
env = map[string]string{
"foo": "Hello, ",
"bar": "world!",
}
output, err := expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output)
// Output: Hello, world!
}
func ExampleAllowUndefinedVariables_zero_value_functions() {
code := `words == "" ? Split("foo,bar", ",") : Split(words, ",")`
// Env is map[string]string type on which methods are defined.
env := mock.MapStringStringEnv{}
options := []expr.Option{
expr.Env(env),
expr.AllowUndefinedVariables(), // Allow to use undefined variables.
}
program, err := expr.Compile(code, options...)
if err != nil {
fmt.Printf("%v", err)
return
}
output, err := expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output)
// Output: [foo bar]
}
type patcher struct{}
func (p *patcher) Visit(node *ast.Node) {
switch n := (*node).(type) {
case *ast.MemberNode:
ast.Patch(node, &ast.CallNode{
Callee: &ast.IdentifierNode{Value: "get"},
Arguments: []ast.Node{n.Node, n.Property},
})
}
}
func ExamplePatch() {
program, err := expr.Compile(
`greet.you.world + "!"`,
expr.Patch(&patcher{}),
)
if err != nil {
fmt.Printf("%v", err)
return
}
env := map[string]any{
"greet": "Hello",
"get": func(a, b string) string {
return a + ", " + b
},
}
output, err := expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output)
// Output: Hello, you, world!
}
func ExampleWithContext() {
env := map[string]any{
"fn": func(ctx context.Context, _, _ int) int {
// An infinite loop that can be canceled by context.
for {
select {
case <-ctx.Done():
return 42
}
}
},
"ctx": context.TODO(), // Context should be passed as a variable.
}
program, err := expr.Compile(`fn(1, 2)`,
expr.Env(env),
expr.WithContext("ctx"), // Pass context variable name.
)
if err != nil {
fmt.Printf("%v", err)
return
}
// Cancel context after 100 milliseconds.
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
defer cancel()
// After program is compiled, context can be passed to Run.
env["ctx"] = ctx
// Run will return 42 after 100 milliseconds.
output, err := expr.Run(program, env)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output)
// Output: 42
}
func ExampleTimezone() {
program, err := expr.Compile(`now().Location().String()`, expr.Timezone("Asia/Kamchatka"))
if err != nil {
fmt.Printf("%v", err)
return
}
output, err := expr.Run(program, nil)
if err != nil {
fmt.Printf("%v", err)
return
}
fmt.Printf("%v", output)
// Output: Asia/Kamchatka
}
func TestExpr_readme_example(t *testing.T) {
env := map[string]any{
"greet": "Hello, %v!",
"names": []string{"world", "you"},
"sprintf": fmt.Sprintf,
}
code := `sprintf(greet, names[0])`
program, err := expr.Compile(code, expr.Env(env))
require.NoError(t, err)
output, err := expr.Run(program, env)
require.NoError(t, err)
require.Equal(t, "Hello, world!", output)
}
func TestExpr(t *testing.T) {
date := time.Date(2017, time.October, 23, 18, 30, 0, 0, time.UTC)
oneDay, _ := time.ParseDuration("24h")
timeNowPlusOneDay := date.Add(oneDay)
env := mock.Env{
Embed: mock.Embed{},
Ambiguous: "",
Any: nil,
Bool: true,
Float: 0,
Int64: 0,
Int32: 0,
Int: 0,
One: 1,
Two: 2,
Uint32: 0,
String: "string",
BoolPtr: nil,
FloatPtr: nil,
IntPtr: nil,
IntPtrPtr: nil,
StringPtr: nil,
Foo: mock.Foo{
Value: "foo",
Bar: mock.Bar{
Baz: "baz",
},
},
Abstract: nil,
ArrayOfAny: nil,
ArrayOfInt: []int{1, 2, 3, 4, 5},
ArrayOfFoo: []*mock.Foo{{Value: "foo"}, {Value: "bar"}, {Value: "baz"}},
MapOfFoo: nil,
MapOfAny: nil,
FuncParam: nil,
FuncParamAny: nil,
FuncTooManyReturns: nil,
FuncNamed: nil,
NilAny: nil,
NilFn: nil,
NilStruct: nil,
Variadic: func(head int, xs ...int) bool {
sum := 0
for _, x := range xs {
sum += x
}
return head == sum
},
Fast: nil,
Time: date,
TimePlusDay: timeNowPlusOneDay,
Duration: oneDay,
}
tests := []struct {
code string
want any
}{
{
`1`,
1,
},
{
`-.5`,
-.5,
},
{
`true && false || false`,
false,
},
{
`Int == 0 && Int32 == 0 && Int64 == 0 && Float64 == 0 && Bool && String == "string"`,
true,
},
{
`-Int64 == 0`,
true,
},
{
`"a" != "b"`,
true,
},
{
`"a" != "b" || 1 == 2`,
true,
},
{
`Int + 0`,
0,
},
{
`Uint64 + 0`,
0,
},
{
`Uint64 + Int64`,
0,
},
{
`Int32 + Int64`,
0,
},
{
`Float64 + 0`,
float64(0),
},
{
`0 + Float64`,
float64(0),
},
{
`0 <= Float64`,
true,
},
{
`Float64 < 1`,
true,
},
{
`Int < 1`,
true,
},
{
`2 + 2 == 4`,
true,
},
{
`8 % 3`,
2,
},
{
`2 ** 8`,
float64(256),
},
{
`2 ^ 8`,
float64(256),
},
{
`-(2-5)**3-2/(+4-3)+-2`,
float64(23),
},
{
`"hello" + " " + "world"`,
"hello world",
},
{
`0 in -1..1 and 1 in 1..1`,
true,
},
{
`Int in 0..1`,
true,
},
{
`Int32 in 0..1`,
true,
},
{
`Int64 in 0..1`,
true,
},
{
`1 in [1, 2, 3] && "foo" in {foo: 0, bar: 1} && "Bar" in Foo`,
true,
},
{
`1 in [1.5] || 1 not in [1]`,
false,
},
{
`One in 0..1 && Two not in 0..1`,
true,
},
{
`Two not in 0..1`,
true,
},
{
`Two not in 0..1`,
true,
},
{
`-1 not in [1]`,
true,
},
{
`Int32 in [10, 20]`,
false,
},
{
`String matches "s.+"`,
true,
},
{
`String matches ("^" + String + "$")`,
true,
},
{
`'foo' + 'bar' not matches 'foobar'`,
false,
},
{
`"foobar" contains "bar"`,
true,
},
{
`"foobar" startsWith "foo"`,
true,
},
{
`"foobar" endsWith "bar"`,
true,
},
{
`(0..10)[5]`,
5,
},
{
`Foo.Bar.Baz`,
"baz",
},
{
`Add(10, 5) + GetInt()`,
15,
},
{
`Foo.Method().Baz`,
`baz (from Foo.Method)`,
},
{
`Foo.MethodWithArgs("prefix ")`,
"prefix foo",
},
{
`len([1, 2, 3])`,
3,
},
{
`len([1, Two, 3])`,
3,
},
{
`len(["hello", "world"])`,
2,
},
{
`len("hello, world")`,
12,
},
{
`len('北京')`,
2,
},
{
`len('👍🏻')`, // one grapheme cluster, two code points
2,
},
{
`len('👍')`, // one grapheme cluster, one code point
1,
},
{
`len(ArrayOfInt)`,
5,
},
{
`len({a: 1, b: 2, c: 2})`,
3,
},
{
`max([1, 2, 3])`,
3,
},
{
`max(1, 2, 3)`,
3,
},
{
`min([1, 2, 3])`,
1,
},
{
`min(1, 2, 3)`,
1,
},
{
`{foo: 0, bar: 1}`,
map[string]any{"foo": 0, "bar": 1},
},
{
`{foo: 0, bar: 1}`,
map[string]any{"foo": 0, "bar": 1},
},
{
`(true ? 0+1 : 2+3) + (false ? -1 : -2)`,
-1,
},
{
`filter(1..9, {# > 7})`,
[]any{8, 9},
},
{
`map(1..3, {# * #})`,
[]any{1, 4, 9},
},
{
`all(1..3, {# > 0})`,
true,
},
{
`count(1..30, {# % 3 == 0})`,
10,
},
{
`count([true, true, false])`,
2,
},
{
`"a" < "b"`,
true,
},
{
`Time.Sub(Time).String() == "0s"`,
true,
},
{
`1 + 1`,
2,
},
{
`(One * Two) * 3 == One * (Two * 3)`,
true,
},
{
`ArrayOfInt[1]`,
2,
},
{
`ArrayOfInt[0] < ArrayOfInt[1]`,
true,
},
{
`ArrayOfInt[-1]`,
5,
},
{
`ArrayOfInt[1:2]`,
[]int{2},
},
{
`ArrayOfInt[1:4]`,
[]int{2, 3, 4},
},
{
`ArrayOfInt[-4:-1]`,
[]int{2, 3, 4},
},
{
`ArrayOfInt[:3]`,
[]int{1, 2, 3},
},
{
`ArrayOfInt[3:]`,
[]int{4, 5},
},
{
`ArrayOfInt[0:5] == ArrayOfInt`,
true,
},
{
`ArrayOfInt[0:] == ArrayOfInt`,
true,
},
{
`ArrayOfInt[:5] == ArrayOfInt`,
true,
},
{
`ArrayOfInt[:] == ArrayOfInt`,
true,
},
{
`4 in 5..1`,
false,
},
{
`4..0`,
[]int{},
},
{
`NilStruct`,
(*mock.Foo)(nil),
},
{
`NilAny == nil && nil == NilAny && nil == nil && NilAny == NilAny && NilInt == nil && NilSlice == nil && NilStruct == nil`,
true,
},
{
`0 == nil || "str" == nil || true == nil`,
false,
},
{
`Variadic(6, 1, 2, 3)`,
true,
},
{
`Variadic(0)`,
true,
},
{
`String[:]`,
"string",
},
{
`String[:3]`,
"str",
},
{
`String[:9]`,
"string",
},
{
`String[3:9]`,
"ing",
},
{
`String[7:9]`,
"",
},
{
`map(filter(ArrayOfInt, # >= 3), # + 1)`,
[]any{4, 5, 6},
},
{
`Time < Time + Duration`,
true,
},
{
`Time + Duration > Time`,
true,
},
{
`Time == Time`,
true,
},
{
`Time >= Time`,
true,
},
{
`Time <= Time`,
true,
},
{
`Time == Time + Duration`,
false,
},
{
`Time != Time`,
false,
},
{
`TimePlusDay - Duration`,
date,
},
{
`duration("1h") == duration("1h")`,
true,
},
{
`TimePlusDay - Time >= duration("24h")`,
true,
},
{
`duration("1h") > duration("1m")`,
true,
},
{
`duration("1h") < duration("1m")`,
false,
},
{
`duration("1h") >= duration("1m")`,
true,
},
{
`duration("1h") <= duration("1m")`,
false,
},
{
`duration("1h") > duration("1m")`,
true,
},
{
`duration("1h") + duration("1m")`,
time.Hour + time.Minute,
},
{
`duration("1h") - duration("1m")`,
time.Hour - time.Minute,
},
{
`7 * duration("1h")`,
7 * time.Hour,
},
{
`duration("1h") * 7`,
7 * time.Hour,
},
{
`duration("1s") * .5`,
5e8,
},
{
`1 /* one */ + 2 // two`,
3,
},
{
`let x = 1; x + 2`,
3,
},
{
`map(1..3, let x = #; let y = x * x; y * y)`,
[]any{1, 16, 81},
},
{
`map(1..2, let x = #; map(2..3, let y = #; x + y))`,
[]any{[]any{3, 4}, []any{4, 5}},
},
{
`len(filter(1..99, # % 7 == 0))`,
14,
},
{
`find(ArrayOfFoo, .Value == "baz")`,
env.ArrayOfFoo[2],
},
{
`findIndex(ArrayOfFoo, .Value == "baz")`,
2,
},
{
`filter(ArrayOfFoo, .Value == "baz")[0]`,
env.ArrayOfFoo[2],
},
{
`first(filter(ArrayOfFoo, .Value == "baz"))`,
env.ArrayOfFoo[2],
},
{
`first(filter(ArrayOfFoo, false))`,
nil,
},
{
`findLast(1..9, # % 2 == 0)`,
8,
},
{
`findLastIndex(1..9, # % 2 == 0)`,
7,
},
{
`filter(1..9, # % 2 == 0)[-1]`,
8,
},
{
`last(filter(1..9, # % 2 == 0))`,
8,
},
{
`map(filter(1..9, # % 2 == 0), # * 2)`,
[]any{4, 8, 12, 16},
},
{
`map(map(filter(1..9, # % 2 == 0), # * 2), # * 2)`,
[]any{8, 16, 24, 32},
},
{
`first(map(filter(1..9, # % 2 == 0), # * 2))`,
4,
},
{
`map(filter(1..9, # % 2 == 0), # * 2)[-1]`,
16,
},
{
`len(map(filter(1..9, # % 2 == 0), # * 2))`,
4,
},
{
`len(filter(map(1..9, # * 2), # % 2 == 0))`,
9,
},
{
`first(filter(map(1..9, # * 2), # % 2 == 0))`,
2,
},
{
`first(map(filter(1..9, # % 2 == 0), # * 2))`,
4,
},
{
`2^3 == 8`,
true,
},
{
`4/2 == 2`,
true,
},
{
`.5 in 0..1`,
false,
},
{
`.5 in ArrayOfInt`,
false,
},
{
`bitnot(10)`,
-11,
},
{
`bitxor(15, 32)`,
47,
},
{
`bitand(90, 34)`,
2,
},
{
`bitnand(35, 9)`,
34,
},
{
`bitor(10, 5)`,
15,
},
{
`bitshr(7, 2)`,
1,
},
{
`bitshl(7, 2)`,
28,
},
{
`bitushr(-100, 5)`,
576460752303423484,
},
{
`"hello"[1:3]`,
"el",
},
{
`[1, 2, 3]?.[0]`,
1,
},
{
`[[1, 2], 3, 4]?.[0]?.[1]`,
2,
},
{
`[nil, 3, 4]?.[0]?.[1]`,
nil,
},
{
`1 > 2 < 3`,
false,
},
{
`1 < 2 < 3`,
true,
},
{
`1 < 2 < 3 > 4`,
false,
},
{
`1 < 2 < 3 > 2`,
true,
},
{
`1 < 2 < 3 == true`,
true,
},
{
`if 1 > 2 { 333 * 2 + 1 } else { 444 }`,
444,
},
{
`let a = 3;
let b = 2;
if a>b {let c = Add(a, b); c+1} else {Add(10, b)}
`,
6,
},
{
`if "a" < "b" {let x = "a"; x} else {"abc"}`,
"a",
},
{
`if 1 == 2 { "no" } else if 1 == 1 { "yes" } else { "maybe" }`,
"yes",
},
{
`1; 2; 3`,
3,
},
{
`let a = 1; Add(2, 2); let b = 2; a + b`,
3,
},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
{
program, err := expr.Compile(tt.code, expr.Env(mock.Env{}))
require.NoError(t, err, "compile error")
got, err := expr.Run(program, env)
require.NoError(t, err, "run error")
assert.Equal(t, tt.want, got)
}
{
program, err := expr.Compile(tt.code, expr.Optimize(false))
require.NoError(t, err, "unoptimized")
got, err := expr.Run(program, env)
require.NoError(t, err, "unoptimized")
assert.Equal(t, tt.want, got, "unoptimized")
}
{
got, err := expr.Eval(tt.code, env)
require.NoError(t, err, "eval")
assert.Equal(t, tt.want, got, "eval")
}
{
program, err := expr.Compile(tt.code, expr.Env(mock.Env{}), expr.Optimize(false))
require.NoError(t, err)
code := program.Node().String()
got, err := expr.Eval(code, env)
require.NoError(t, err, code)
assert.Equal(t, tt.want, got, code)
}
})
}
}
func TestExpr_error(t *testing.T) {
env := mock.Env{
ArrayOfAny: []any{1, "2", 3, true},
}
tests := []struct {
code string
want string
}{
{
`filter(1..9, # > 9)[0]`,
`reflect: slice index out of range (1:20)
| filter(1..9, # > 9)[0]
| ...................^`,
},
{
`ArrayOfAny[-7]`,
`index out of range: -3 (array length is 4) (1:11)
| ArrayOfAny[-7]
| ..........^`,
},
{
`reduce(10..1, # + #acc)`,
`reduce of empty array with no initial value (1:1)
| reduce(10..1, # + #acc)
| ^`,
},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
program, err := expr.Compile(tt.code, expr.Env(mock.Env{}))
require.NoError(t, err)
_, err = expr.Run(program, env)
require.Error(t, err)
assert.Equal(t, tt.want, err.Error())
})
}
}
func TestExpr_optional_chaining(t *testing.T) {
env := map[string]any{}
program, err := expr.Compile("foo?.bar.baz", expr.Env(env), expr.AllowUndefinedVariables())
require.NoError(t, err)
got, err := expr.Run(program, env)
require.NoError(t, err)
assert.Equal(t, nil, got)
}
func TestExpr_optional_chaining_property(t *testing.T) {
env := map[string]any{
"foo": map[string]any{},
}
program, err := expr.Compile("foo.bar?.baz", expr.Env(env))
require.NoError(t, err)
got, err := expr.Run(program, env)
require.NoError(t, err)
assert.Equal(t, nil, got)
}
func TestExpr_optional_chaining_nested_chains(t *testing.T) {
env := map[string]any{
"foo": map[string]any{
"id": 1,
"bar": []map[string]any{
1: {
"baz": "baz",
},
},
},
}
program, err := expr.Compile("foo?.bar[foo?.id]?.baz", expr.Env(env))
require.NoError(t, err)
got, err := expr.Run(program, env)
require.NoError(t, err)
assert.Equal(t, "baz", got)
}
func TestExpr_optional_chaining_array(t *testing.T) {
env := map[string]any{}
program, err := expr.Compile("foo?.[1]?.[2]?.[3]", expr.Env(env), expr.AllowUndefinedVariables())
require.NoError(t, err)
got, err := expr.Run(program, env)
require.NoError(t, err)
assert.Equal(t, nil, got)
}
func TestExpr_eval_with_env(t *testing.T) {
_, err := expr.Eval("true", expr.Env(map[string]any{}))
assert.Error(t, err)
assert.Contains(t, err.Error(), "misused")
}
func TestExpr_fetch_from_func(t *testing.T) {
_, err := expr.Eval("foo.Value", map[string]any{
"foo": func() {},
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot fetch Value from func()")
}
func TestExpr_map_default_values(t *testing.T) {
env := map[string]any{
"foo": map[string]string{},
"bar": map[string]*string{},
}
input := `foo['missing'] == '' && bar['missing'] == nil`
program, err := expr.Compile(input, expr.Env(env))
require.NoError(t, err)
output, err := expr.Run(program, env)
require.NoError(t, err)
require.Equal(t, true, output)
}
func TestExpr_map_default_values_compile_check(t *testing.T) {
tests := []struct {
env any
input string
}{
{
mock.MapStringStringEnv{"foo": "bar"},
`Split(foo, sep)`,
},
{
mock.MapStringIntEnv{"foo": 1},
`foo / bar`,
},
}
for _, tt := range tests {
_, err := expr.Compile(tt.input, expr.Env(tt.env), expr.AllowUndefinedVariables())
require.NoError(t, err)
}
}
func TestExpr_calls_with_nil(t *testing.T) {
env := map[string]any{
"equals": func(a, b any) any {
assert.Nil(t, a, "a is not nil")
assert.Nil(t, b, "b is not nil")
return a == b
},
"is": mock.Is{},
}
p, err := expr.Compile(
"a == nil && equals(b, nil) && is.Nil(c)",
expr.Env(env),
expr.Operator("==", "equals"),
expr.AllowUndefinedVariables(),
)
require.NoError(t, err)
out, err := expr.Run(p, env)
require.NoError(t, err)
require.Equal(t, true, out)
}
func TestExpr_call_float_arg_func_with_int(t *testing.T) {
env := map[string]any{
"cnv": func(f float64) any {
return f
},
}
tests := []struct {
input string
expected float64
}{
{"-1", -1.0},
{"1+1", 2.0},
{"+1", 1.0},
{"1-1", 0.0},
{"1/1", 1.0},
{"1*1", 1.0},
{"1^1", 1.0},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
p, err := expr.Compile(fmt.Sprintf("cnv(%s)", tt.input), expr.Env(env))
require.NoError(t, err)
out, err := expr.Run(p, env)
require.NoError(t, err)
require.Equal(t, tt.expected, out)
})
}
}
func TestConstExpr_error_panic(t *testing.T) {
env := map[string]any{
"divide": func(a, b int) int { return a / b },
}
_, err := expr.Compile(
`1 + divide(1, 0)`,
expr.Env(env),
expr.ConstExpr("divide"),
)
require.Error(t, err)
require.Equal(t, "compile error: integer divide by zero (1:5)\n | 1 + divide(1, 0)\n | ....^", err.Error())
}
type divideError struct{ Message string }
func (e divideError) Error() string {
return e.Message
}
func TestConstExpr_error_as_error(t *testing.T) {
env := map[string]any{
"divide": func(a, b int) (int, error) {
if b == 0 {
return 0, divideError{"integer divide by zero"}
}
return a / b, nil
},
}
_, err := expr.Compile(
`1 + divide(1, 0)`,
expr.Env(env),
expr.ConstExpr("divide"),
)
require.Error(t, err)
require.Equal(t, "integer divide by zero", err.Error())
require.IsType(t, divideError{}, err)
}
func TestConstExpr_error_wrong_type(t *testing.T) {
env := map[string]any{
"divide": 0,
}
assert.Panics(t, func() {
_, _ = expr.Compile(
`1 + divide(1, 0)`,
expr.Env(env),
expr.ConstExpr("divide"),
)
})
}
func TestConstExpr_error_no_env(t *testing.T) {
assert.Panics(t, func() {
_, _ = expr.Compile(
`1 + divide(1, 0)`,
expr.ConstExpr("divide"),
)
})
}
var stringer = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
type stringerPatcher struct{}
func (p *stringerPatcher) Visit(node *ast.Node) {
t := (*node).Type()
if t == nil {
return
}
if t.Implements(stringer) {
ast.Patch(node, &ast.CallNode{
Callee: &ast.MemberNode{
Node: *node,
Property: &ast.StringNode{Value: "String"},
},
})
}
}
func TestPatch(t *testing.T) {
program, err := expr.Compile(
`Foo == "Foo.String"`,
expr.Env(mock.Env{}),
expr.Patch(&mock.StringerPatcher{}),
)
require.NoError(t, err)
output, err := expr.Run(program, mock.Env{})
require.NoError(t, err)
require.Equal(t, true, output)
}
func TestCompile_exposed_error(t *testing.T) {
_, err := expr.Compile(`1 == true`)
require.Error(t, err)
fileError, ok := err.(*file.Error)
require.True(t, ok, "error should be of type *file.Error")
require.Equal(t, "invalid operation: == (mismatched types int and bool) (1:3)\n | 1 == true\n | ..^", fileError.Error())
require.Equal(t, 2, fileError.Column)
require.Equal(t, 1, fileError.Line)
b, err := json.Marshal(err)
require.NoError(t, err)
require.Equal(t,
`{"from":2,"to":4,"line":1,"column":2,"message":"invalid operation: == (mismatched types int and bool)","snippet":"\n | 1 == true\n | ..^","prev":null}`,
string(b),
)
}
func TestAsBool_exposed_error(t *testing.T) {
_, err := expr.Compile(`42`, expr.AsBool())
require.Error(t, err)
_, ok := err.(*file.Error)
require.False(t, ok, "error must not be of type *file.Error")
require.Equal(t, "expected bool, but got int", err.Error())
}
func TestEval_exposed_error(t *testing.T) {
_, err := expr.Eval(`1 % 0`, nil)
require.Error(t, err)
fileError, ok := err.(*file.Error)
require.True(t, ok, "error should be of type *file.Error")
require.Equal(t, "runtime error: integer divide by zero (1:3)\n | 1 % 0\n | ..^", fileError.Error())
require.Equal(t, 2, fileError.Column)
require.Equal(t, 1, fileError.Line)
}
func TestCompile_exposed_error_with_multiline_script(t *testing.T) {
_, err := expr.Compile("{\n\ta: 1,\n\tb: #,\n\tc: 3,\n}")
require.Error(t, err)
fileError, ok := err.(*file.Error)
require.True(t, ok, "error should be of type *file.Error")
require.Equal(t, "unexpected token Operator(\"#\") (3:5)\n | b: #,\n | ....^", fileError.Error())
require.Equal(t, 4, fileError.Column)
require.Equal(t, 3, fileError.Line)
}
func TestIssue105(t *testing.T) {
type A struct {
Field string
}
type B struct {
Field int
}
type C struct {
A
B
}
type Env struct {
C
}
code := `
A.Field == '' &&
C.A.Field == '' &&
B.Field == 0 &&
C.B.Field == 0
`
_, err := expr.Compile(code, expr.Env(Env{}))
require.NoError(t, err)
}
func TestIssue_nested_closures(t *testing.T) {
code := `all(1..3, { all(1..3, { # > 0 }) and # > 0 })`
program, err := expr.Compile(code)
require.NoError(t, err)
output, err := expr.Run(program, nil)
require.NoError(t, err)
require.True(t, output.(bool))
}
func TestIssue138(t *testing.T) {
env := map[string]any{}
_, err := expr.Compile(`1 / (1 - 1)`, expr.Env(env))
require.NoError(t, err)
_, err = expr.Compile(`1 % 0`, expr.Env(env))
require.Error(t, err)
require.Equal(t, "integer divide by zero (1:3)\n | 1 % 0\n | ..^", err.Error())
}
func TestIssue154(t *testing.T) {
type Data struct {
Array *[2]any
Slice *[]any
Map *map[string]any
String *string
}
type Env struct {
Data *Data
}
b := true
i := 10
s := "value"
Array := [2]any{
&b,
&i,
}
Slice := []any{
&b,
&i,
}
Map := map[string]any{
"Bool": &b,
"Int": &i,
}
env := Env{
Data: &Data{
Array: &Array,
Slice: &Slice,
Map: &Map,
String: &s,
},
}
tests := []string{
`Data.Array[0] == true`,
`Data.Array[1] == 10`,
`Data.Slice[0] == true`,
`Data.Slice[1] == 10`,
`Data.Map["Bool"] == true`,
`Data.Map["Int"] == 10`,
`Data.String == "value"`,
}
for _, input := range tests {
program, err := expr.Compile(input, expr.Env(env))
require.NoError(t, err, input)
output, err := expr.Run(program, env)
require.NoError(t, err)
assert.True(t, output.(bool), input)
}
}
func TestIssue270(t *testing.T) {
env := map[string]any{
"int8": int8(1),
"int16": int16(3),
"int32": int32(5),
"int64": int64(7),
"uint8": uint8(11),
"uint16": uint16(13),
"uint32": uint32(17),
"uint64": uint64(19),
"int8a": uint(23),
"int8b": uint(29),
"int16a": uint(31),
"int16b": uint(37),
"int32a": uint(41),
"int32b": uint(43),
"int64a": uint(47),
"int64b": uint(53),
"uint8a": uint(59),
"uint8b": uint(61),
"uint16a": uint(67),
"uint16b": uint(71),
"uint32a": uint(73),
"uint32b": uint(79),
"uint64a": uint(83),
"uint64b": uint(89),
"float32a": float32(97),
"float32b": float32(101),
"float64a": float64(103),
"float64b": float64(107),
}
for _, each := range []struct {
input string
}{
{"int8 / int16"},
{"int32 / int64"},
{"uint8 / uint16"},
{"uint32 / uint64"},
{"int8 / uint64"},
{"int64 / uint8"},
{"int8a / int8b"},
{"int16a / int16b"},
{"int32a / int32b"},
{"int64a / int64b"},
{"uint8a / uint8b"},
{"uint16a / uint16b"},
{"uint32a / uint32b"},
{"uint64a / uint64b"},
{"float32a / float32b"},
{"float64a / float64b"},
} {
p, err := expr.Compile(each.input, expr.Env(env))
require.NoError(t, err)
out, err := expr.Run(p, env)
require.NoError(t, err)
require.IsType(t, float64(0), out)
}
}
func TestIssue271(t *testing.T) {
type BarArray []float64
type Foo struct {
Bar BarArray
Baz int
}
type Env struct {
Foo Foo
}
code := `Foo.Bar[0]`
program, err := expr.Compile(code, expr.Env(Env{}))
require.NoError(t, err)
output, err := expr.Run(program, Env{
Foo: Foo{
Bar: BarArray{1.0, 2.0, 3.0},
},
})
require.NoError(t, err)
require.Equal(t, 1.0, output)
}
type Issue346Array []Issue346Type
type Issue346Type struct {
Bar string
}
func (i Issue346Array) Len() int {
return len(i)
}
func TestIssue346(t *testing.T) {
code := `Foo[0].Bar`
env := map[string]any{
"Foo": Issue346Array{
{Bar: "bar"},
},
}
program, err := expr.Compile(code, expr.Env(env))
require.NoError(t, err)
output, err := expr.Run(program, env)
require.NoError(t, err)
require.Equal(t, "bar", output)
}
func TestCompile_allow_to_use_interface_to_get_an_element_from_map(t *testing.T) {
code := `{"value": "ok"}[vars.key]`
env := map[string]any{
"vars": map[string]any{
"key": "value",
},
}
program, err := expr.Compile(code, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(program, env)
assert.NoError(t, err)
assert.Equal(t, "ok", out)
t.Run("with allow undefined variables", func(t *testing.T) {
code := `{'key': 'value'}[Key]`
env := mock.MapStringStringEnv{}
options := []expr.Option{
expr.AllowUndefinedVariables(),
}
program, err := expr.Compile(code, options...)
assert.NoError(t, err)
out, err := expr.Run(program, env)
assert.NoError(t, err)
assert.Equal(t, nil, out)
})
}
func TestFastCall(t *testing.T) {
env := map[string]any{
"func": func(in any) float64 {
return 8
},
}
code := `func("8")`
program, err := expr.Compile(code, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(program, env)
assert.NoError(t, err)
assert.Equal(t, float64(8), out)
}
func TestFastCall_OpCallFastErr(t *testing.T) {
env := map[string]any{
"func": func(...any) (any, error) {
return 8, nil
},
}
code := `func("8")`
program, err := expr.Compile(code, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(program, env)
assert.NoError(t, err)
assert.Equal(t, 8, out)
}
func TestRun_custom_func_returns_an_error_as_second_arg(t *testing.T) {
env := map[string]any{
"semver": func(value string, cmp string) (bool, error) { return true, nil },
}
p, err := expr.Compile(`semver("1.2.3", "= 1.2.3")`, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(p, env)
assert.NoError(t, err)
assert.Equal(t, true, out)
}
func TestFunction(t *testing.T) {
add := expr.Function(
"add",
func(p ...any) (any, error) {
out := 0
for _, each := range p {
out += each.(int)
}
return out, nil
},
new(func(...int) int),
)
p, err := expr.Compile(`add() + add(1) + add(1, 2) + add(1, 2, 3) + add(1, 2, 3, 4)`, add)
assert.NoError(t, err)
out, err := expr.Run(p, nil)
assert.NoError(t, err)
assert.Equal(t, 20, out)
}
// Nil coalescing operator
func TestRun_NilCoalescingOperator(t *testing.T) {
env := map[string]any{
"foo": map[string]any{
"bar": "value",
},
}
t.Run("value", func(t *testing.T) {
p, err := expr.Compile(`foo.bar ?? "default"`, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(p, env)
assert.NoError(t, err)
assert.Equal(t, "value", out)
})
t.Run("default", func(t *testing.T) {
p, err := expr.Compile(`foo.baz ?? "default"`, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(p, env)
assert.NoError(t, err)
assert.Equal(t, "default", out)
})
t.Run("default with chain", func(t *testing.T) {
p, err := expr.Compile(`foo?.bar ?? "default"`, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(p, map[string]any{})
assert.NoError(t, err)
assert.Equal(t, "default", out)
})
}
func TestEval_nil_in_maps(t *testing.T) {
env := map[string]any{
"m": map[any]any{nil: "bar"},
"empty": map[any]any{},
}
t.Run("nil key exists", func(t *testing.T) {
p, err := expr.Compile(`m[nil]`, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(p, env)
assert.NoError(t, err)
assert.Equal(t, "bar", out)
})
t.Run("no nil key", func(t *testing.T) {
p, err := expr.Compile(`empty[nil]`, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(p, env)
assert.NoError(t, err)
assert.Equal(t, nil, out)
})
t.Run("nil in m", func(t *testing.T) {
p, err := expr.Compile(`nil in m`, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(p, env)
assert.NoError(t, err)
assert.Equal(t, true, out)
})
t.Run("nil in empty", func(t *testing.T) {
p, err := expr.Compile(`nil in empty`, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(p, env)
assert.NoError(t, err)
assert.Equal(t, false, out)
})
}
// Test the use of env keyword. Forms env[] and env[”] are valid.
// The enclosed identifier must be in the expression env.
func TestEnv_keyword(t *testing.T) {
env := map[string]any{
"space test": "ok",
"space_test": "not ok", // Seems to be some underscore substituting happening, check that.
"Section 1-2a": "ok",
`c:\ndrive\2015 Information Table`: "ok",
"%*worst function name ever!!": func() string {
return "ok"
}(),
"1": "o",
"2": "k",
"num": 10,
"mylist": []int{1, 2, 3, 4, 5},
"MIN": func(a, b int) int {
if a < b {
return a
} else {
return b
}
},
"red": "n",
"irect": "um",
"String Map": map[string]string{
"one": "two",
"three": "four",
},
"OtherMap": map[string]string{
"a": "b",
"c": "d",
},
}
// No error cases
var tests = []struct {
code string
want any
}{
{"$env['space test']", "ok"},
{"$env['Section 1-2a']", "ok"},
{`$env["c:\\ndrive\\2015 Information Table"]`, "ok"},
{"$env['%*worst function name ever!!']", "ok"},
{"$env['String Map'].one", "two"},
{"$env['1'] + $env['2']", "ok"},
{"1 + $env['num'] + $env['num']", 21},
{"MIN($env['num'],0)", 0},
{"$env['nu' + 'm']", 10},
{"$env[red + irect]", 10},
{"$env['String Map']?.five", ""},
{"$env.red", "n"},
{"$env?.unknown", nil},
{"$env.mylist[1]", 2},
{"$env?.OtherMap?.a", "b"},
{"$env?.OtherMap?.d", ""},
{"'num' in $env", true},
{"get($env, 'num')", 10},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
program, err := expr.Compile(tt.code, expr.Env(env))
require.NoError(t, err, "compile error")
got, err := expr.Run(program, env)
require.NoError(t, err, "execution error")
assert.Equal(t, tt.want, got, tt.code)
})
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
got, err := expr.Eval(tt.code, env)
require.NoError(t, err, "eval error: "+tt.code)
assert.Equal(t, tt.want, got, "eval: "+tt.code)
})
}
// error cases
tests = []struct {
code string
want any
}{
{"env()", "bad"},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
_, err := expr.Eval(tt.code, expr.Env(env))
require.Error(t, err, "compile error")
})
}
}
func TestEnv_keyword_with_custom_functions(t *testing.T) {
fn := expr.Function("fn", func(params ...any) (any, error) {
return "ok", nil
})
var tests = []struct {
code string
error bool
}{
{`fn()`, false},
{`$env.fn()`, true},
{`$env["fn"]`, true},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
_, err := expr.Compile(tt.code, expr.Env(mock.Env{}), fn)
if tt.error {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestIssue401(t *testing.T) {
program, err := expr.Compile("(a - b + c) / d", expr.AllowUndefinedVariables())
require.NoError(t, err, "compile error")
output, err := expr.Run(program, map[string]any{
"a": 1,
"b": 2,
"c": 3,
"d": 4,
})
require.NoError(t, err, "run error")
require.Equal(t, 0.5, output)
}
func TestEval_slices_out_of_bound(t *testing.T) {
tests := []struct {
code string
want any
}{
{"[1, 2, 3][:99]", []any{1, 2, 3}},
{"[1, 2, 3][99:]", []any{}},
{"[1, 2, 3][:-99]", []any{}},
{"[1, 2, 3][-99:]", []any{1, 2, 3}},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
got, err := expr.Eval(tt.code, nil)
require.NoError(t, err, "eval error: "+tt.code)
assert.Equal(t, tt.want, got, "eval: "+tt.code)
})
}
}
func TestExpr_timeout(t *testing.T) {
tests := []struct{ code string }{
{`-999999..999999`},
{`map(1..999999, 1..999999)`},
{`map(1..999999, repeat('a', #))`},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
program, err := expr.Compile(tt.code)
require.NoError(t, err)
timeout := make(chan bool, 1)
go func() {
time.Sleep(time.Second)
timeout <- true
}()
done := make(chan bool, 1)
go func() {
out, err := expr.Run(program, nil)
// Make sure out is used.
_ = fmt.Sprintf("%v", out)
assert.Error(t, err)
done <- true
}()
select {
case <-done:
// Success.
case <-timeout:
t.Fatal("timeout")
}
})
}
}
func TestIssue432(t *testing.T) {
env := map[string]any{
"func": func(
paramUint32 uint32,
paramUint16 uint16,
paramUint8 uint8,
paramUint uint,
paramInt32 int32,
paramInt16 int16,
paramInt8 int8,
paramInt int,
paramFloat64 float64,
paramFloat32 float32,
) float64 {
return float64(paramUint32) + float64(paramUint16) + float64(paramUint8) + float64(paramUint) +
float64(paramInt32) + float64(paramInt16) + float64(paramInt8) + float64(paramInt) +
float64(paramFloat64) + float64(paramFloat32)
},
}
code := `func(1,1,1,1,1,1,1,1,1,1)`
program, err := expr.Compile(code, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(program, env)
assert.NoError(t, err)
assert.Equal(t, float64(10), out)
}
func TestIssue462(t *testing.T) {
env := map[string]any{
"foo": func() (string, error) {
return "bar", nil
},
}
_, err := expr.Compile(`$env.unknown(int())`, expr.Env(env))
require.Error(t, err)
}
func TestIssue_embedded_pointer_struct(t *testing.T) {
var tests = []struct {
input string
env mock.Env
want any
}{
{
input: "EmbedPointerEmbedInt > 0",
env: mock.Env{
Embed: mock.Embed{
EmbedPointerEmbed: &mock.EmbedPointerEmbed{
EmbedPointerEmbedInt: 123,
},
},
},
want: true,
},
{
input: "(Embed).EmbedPointerEmbedInt > 0",
env: mock.Env{
Embed: mock.Embed{
EmbedPointerEmbed: &mock.EmbedPointerEmbed{
EmbedPointerEmbedInt: 123,
},
},
},
want: true,
},
{
input: "(Embed).EmbedPointerEmbedInt > 0",
env: mock.Env{
Embed: mock.Embed{
EmbedPointerEmbed: &mock.EmbedPointerEmbed{
EmbedPointerEmbedInt: 0,
},
},
},
want: false,
},
{
input: "(Embed).EmbedPointerEmbedMethod(0)",
env: mock.Env{
Embed: mock.Embed{
EmbedPointerEmbed: &mock.EmbedPointerEmbed{
EmbedPointerEmbedInt: 0,
},
},
},
want: "",
},
{
input: "(Embed).EmbedPointerEmbedPointerReceiverMethod(0)",
env: mock.Env{
Embed: mock.Embed{
EmbedPointerEmbed: nil,
},
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
program, err := expr.Compile(tt.input, expr.Env(tt.env))
require.NoError(t, err)
out, err := expr.Run(program, tt.env)
require.NoError(t, err)
require.Equal(t, tt.want, out)
})
}
}
func TestIssue474(t *testing.T) {
testCases := []struct {
code string
fail bool
}{
{
code: `func("invalid")`,
fail: true,
},
{
code: `func(true)`,
fail: true,
},
{
code: `func([])`,
fail: true,
},
{
code: `func({})`,
fail: true,
},
{
code: `func(1)`,
fail: false,
},
{
code: `func(1.5)`,
fail: false,
},
}
for _, tc := range testCases {
ltc := tc
t.Run(ltc.code, func(t *testing.T) {
t.Parallel()
function := expr.Function("func", func(params ...any) (any, error) {
return true, nil
}, new(func(float64) bool))
_, err := expr.Compile(ltc.code, function)
if ltc.fail {
if err == nil {
t.Error("expected an error, but it was nil")
t.FailNow()
}
} else {
if err != nil {
t.Errorf("expected nil, but it was %v", err)
t.FailNow()
}
}
})
}
}
func TestRaceCondition_variables(t *testing.T) {
program, err := expr.Compile(`let foo = 1; foo + 1`, expr.Env(mock.Env{}))
require.NoError(t, err)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
out, err := expr.Run(program, mock.Env{})
require.NoError(t, err)
require.Equal(t, 2, out)
}()
}
wg.Wait()
}
func TestOperatorDependsOnEnv(t *testing.T) {
env := map[string]any{
"plus": func(a, b int) int {
return 42
},
}
program, err := expr.Compile(`1 + 2`, expr.Operator("+", "plus"), expr.Env(env))
require.NoError(t, err)
out, err := expr.Run(program, env)
require.NoError(t, err)
assert.Equal(t, 42, out)
}
func TestIssue624(t *testing.T) {
type tag struct {
Name string
}
type item struct {
Tags []tag
}
i := item{
Tags: []tag{
{Name: "one"},
{Name: "two"},
},
}
rule := `[
true && true,
one(Tags, .Name in ["one"]),
one(Tags, .Name in ["two"]),
one(Tags, .Name in ["one"]) && one(Tags, .Name in ["two"])
]`
resp, err := expr.Eval(rule, i)
require.NoError(t, err)
require.Equal(t, []interface{}{true, true, true, true}, resp)
}
func TestPredicateCombination(t *testing.T) {
tests := []struct {
code1 string
code2 string
}{
{"all(1..3, {# > 0}) && all(1..3, {# < 4})", "all(1..3, {# > 0 && # < 4})"},
{"all(1..3, {# > 1}) && all(1..3, {# < 4})", "all(1..3, {# > 1 && # < 4})"},
{"all(1..3, {# > 0}) && all(1..3, {# < 2})", "all(1..3, {# > 0 && # < 2})"},
{"all(1..3, {# > 1}) && all(1..3, {# < 2})", "all(1..3, {# > 1 && # < 2})"},
{"any(1..3, {# > 0}) || any(1..3, {# < 4})", "any(1..3, {# > 0 || # < 4})"},
{"any(1..3, {# > 1}) || any(1..3, {# < 4})", "any(1..3, {# > 1 || # < 4})"},
{"any(1..3, {# > 0}) || any(1..3, {# < 2})", "any(1..3, {# > 0 || # < 2})"},
{"any(1..3, {# > 1}) || any(1..3, {# < 2})", "any(1..3, {# > 1 || # < 2})"},
{"none(1..3, {# > 0}) && none(1..3, {# < 4})", "none(1..3, {# > 0 || # < 4})"},
{"none(1..3, {# > 1}) && none(1..3, {# < 4})", "none(1..3, {# > 1 || # < 4})"},
{"none(1..3, {# > 0}) && none(1..3, {# < 2})", "none(1..3, {# > 0 || # < 2})"},
{"none(1..3, {# > 1}) && none(1..3, {# < 2})", "none(1..3, {# > 1 || # < 2})"},
}
for _, tt := range tests {
t.Run(tt.code1, func(t *testing.T) {
out1, err := expr.Eval(tt.code1, nil)
require.NoError(t, err)
out2, err := expr.Eval(tt.code2, nil)
require.NoError(t, err)
require.Equal(t, out1, out2)
})
}
}
func TestArrayComparison(t *testing.T) {
tests := []struct {
env any
code string
}{
{[]string{"A", "B"}, "foo == ['A', 'B']"},
{[]int{1, 2}, "foo == [1, 2]"},
{[]uint8{1, 2}, "foo == [1, 2]"},
{[]float64{1.1, 2.2}, "foo == [1.1, 2.2]"},
{[]any{"A", 1, 1.1, true}, "foo == ['A', 1, 1.1, true]"},
{[]string{"A", "B"}, "foo != [1, 2]"},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
env := map[string]any{"foo": tt.env}
program, err := expr.Compile(tt.code, expr.Env(env))
require.NoError(t, err)
out, err := expr.Run(program, env)
require.NoError(t, err)
require.Equal(t, true, out)
})
}
}
func TestIssue_570(t *testing.T) {
type Student struct {
Name string
}
env := map[string]any{
"student": (*Student)(nil),
}
program, err := expr.Compile("student?.Name", expr.Env(env))
require.NoError(t, err)
out, err := expr.Run(program, env)
require.NoError(t, err)
require.IsType(t, nil, out)
}
func TestIssue_integer_truncated_by_compiler(t *testing.T) {
env := map[string]any{
"fn": func(x byte) byte {
return x
},
}
_, err := expr.Compile("fn(255)", expr.Env(env))
require.NoError(t, err)
_, err = expr.Compile("fn(256)", expr.Env(env))
require.Error(t, err)
}
func TestExpr_crash(t *testing.T) {
content, err := os.ReadFile("testdata/crash.txt")
require.NoError(t, err)
_, err = expr.Compile(string(content))
require.Error(t, err)
}
func TestExpr_crash_with_zero(t *testing.T) {
code := "if\x00"
_, err := expr.Compile(code)
require.Error(t, err)
}
func TestExpr_nil_op_str(t *testing.T) {
// Let's test operators, which do `.(string)` in VM, also check for nil.
var str *string = nil
env := map[string]any{
"nilString": str,
}
tests := []struct{ code string }{
{`nilString == "str"`},
{`nilString contains "str"`},
{`nilString matches "str"`},
{`nilString startsWith "str"`},
{`nilString endsWith "str"`},
{`"str" == nilString`},
{`"str" contains nilString`},
{`"str" matches nilString`},
{`"str" startsWith nilString`},
{`"str" endsWith nilString`},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
program, err := expr.Compile(tt.code)
require.NoError(t, err)
output, err := expr.Run(program, env)
require.NoError(t, err)
require.Equal(t, false, output)
})
}
}
func TestExpr_env_types_map(t *testing.T) {
envTypes := types.Map{
"foo": types.Map{
"bar": types.String,
},
}
program, err := expr.Compile(`foo.bar`, expr.Env(envTypes))
require.NoError(t, err)
env := map[string]any{
"foo": map[string]any{
"bar": "value",
},
}
output, err := expr.Run(program, env)
require.NoError(t, err)
require.Equal(t, "value", output)
}
func TestExpr_env_types_map_error(t *testing.T) {
envTypes := types.Map{
"foo": types.Map{
"bar": types.String,
},
}
program, err := expr.Compile(`foo.bar`, expr.Env(envTypes))
require.NoError(t, err)
_, err = expr.Run(program, envTypes)
require.Error(t, err)
}
func TestIssue758_filter_map_index(t *testing.T) {
env := map[string]interface{}{}
exprStr := `
let a_map = 0..5 | filter(# % 2 == 0) | map(#index);
let b_filter = 0..5 | filter(# % 2 == 0);
let b_map = b_filter | map(#index);
[a_map, b_map]
`
result, err := expr.Eval(exprStr, env)
require.NoError(t, err)
expected := []interface{}{
[]interface{}{0, 1, 2},
[]interface{}{0, 1, 2},
}
require.Equal(t, expected, result)
}
func TestExpr_wierd_cases(t *testing.T) {
env := map[string]any{}
_, err := expr.Compile(`A(A)`, expr.Env(env))
require.Error(t, err)
require.Contains(t, err.Error(), "unknown name A")
}
func TestIssue785_get_nil(t *testing.T) {
exprStrs := []string{
`get(nil, "a")`,
`get({}, "a")`,
`get(nil, "a")`,
`get({}, "a")`,
`({} | get("a") | get("b"))`,
}
for _, exprStr := range exprStrs {
t.Run("get returns nil", func(t *testing.T) {
env := map[string]interface{}{}
result, err := expr.Eval(exprStr, env)
require.NoError(t, err)
require.Equal(t, nil, result)
})
}
}
func TestMaxNodes(t *testing.T) {
maxNodes := uint(100)
code := ""
for i := 0; i < int(maxNodes); i++ {
code += "1; "
}
_, err := expr.Compile(code, expr.MaxNodes(maxNodes))
require.Error(t, err)
assert.Contains(t, err.Error(), "exceeds maximum allowed nodes")
_, err = expr.Compile(code, expr.MaxNodes(maxNodes+1))
require.NoError(t, err)
}
func TestMaxNodesDisabled(t *testing.T) {
code := ""
for i := 0; i < 2*int(conf.DefaultMaxNodes); i++ {
code += "1; "
}
_, err := expr.Compile(code, expr.MaxNodes(0))
require.NoError(t, err)
}
func TestMemoryBudget(t *testing.T) {
tests := []struct {
code string
max int
}{
{`map(1..100, {map(1..100, {map(1..100, {0})})})`, -1},
{`len(1..10000000)`, -1},
{`1..100`, 100},
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
program, err := expr.Compile(tt.code)
require.NoError(t, err, "compile error")
vm := vm.VM{}
if tt.max > 0 {
vm.MemoryBudget = uint(tt.max)
}
_, err = vm.Run(program, nil)
require.Error(t, err, "run error")
assert.Contains(t, err.Error(), "memory budget exceeded")
})
}
}
func TestIssue802(t *testing.T) {
prog, err := expr.Compile(`arr[1:2][0]`)
if err != nil {
t.Fatalf("error compiling program: %v", err)
}
val, err := expr.Run(prog, map[string]any{
"arr": [5]int{0, 1, 2, 3, 4},
})
if err != nil {
t.Fatalf("error running program: %v", err)
}
valInt, ok := val.(int)
if !ok || valInt != 1 {
t.Fatalf("invalid result, expected 1, got %v", val)
}
}
func TestIssue807(t *testing.T) {
type MyStruct struct {
nonExported string
}
out, err := expr.Eval(` "nonExported" in $env `, MyStruct{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
b, ok := out.(bool)
if !ok {
t.Fatalf("expected boolean type, got %T: %v", b, b)
}
if b {
t.Fatalf("expected 'in' operator to return false for unexported field")
}
}
func TestDisableShortCircuit(t *testing.T) {
count := 0
exprStr := "foo() or bar()"
env := map[string]any{
"foo": func() bool {
count++
return true
},
"bar": func() bool {
count++
return true
},
}
program, _ := expr.Compile(exprStr, expr.DisableShortCircuit())
got, _ := expr.Run(program, env)
assert.Equal(t, 2, count)
assert.True(t, got.(bool))
program, _ = expr.Compile(exprStr)
got, _ = expr.Run(program, env)
assert.Equal(t, 3, count)
assert.True(t, got.(bool))
}
func TestBytesLiteral(t *testing.T) {
tests := []struct {
code string
want []byte
}{
{`b"hello"`, []byte("hello")},
{`b'world'`, []byte("world")},
{`b""`, []byte{}},
{`b'\x00\xff'`, []byte{0, 255}},
{`b"\x41\x42\x43"`, []byte("ABC")},
{`b'\101\102\103'`, []byte("ABC")},
{`b'\n\t\r'`, []byte{'\n', '\t', '\r'}},
{`b'hello\x00world'`, []byte("hello\x00world")},
{`b"ÿ"`, []byte{0xc3, 0xbf}}, // UTF-8 encoding of ÿ
}
for _, tt := range tests {
t.Run(tt.code, func(t *testing.T) {
program, err := expr.Compile(tt.code)
require.NoError(t, err)
output, err := expr.Run(program, nil)
require.NoError(t, err)
assert.Equal(t, tt.want, output)
})
}
}
func TestBytesLiteral_type(t *testing.T) {
env := map[string]any{
"data": []byte("test"),
}
// Verify bytes literal has []byte type and can be compared with []byte
program, err := expr.Compile(`data == b"test"`, expr.Env(env))
require.NoError(t, err)
output, err := expr.Run(program, env)
require.NoError(t, err)
assert.Equal(t, true, output)
}
func TestBytesLiteral_errors(t *testing.T) {
// \u and \U escapes should not be allowed in bytes literals
errorCases := []string{
`b'\u0041'`,
`b"\U00000041"`,
}
for _, code := range errorCases {
t.Run(code, func(t *testing.T) {
_, err := expr.Compile(code)
require.Error(t, err)
})
}
}
================================================
FILE: file/error.go
================================================
package file
import (
"fmt"
"strings"
)
type Error struct {
Location
Line int `json:"line"`
Column int `json:"column"`
Message string `json:"message"`
Snippet string `json:"snippet"`
Prev error `json:"prev"`
}
func (e *Error) Error() string {
return e.format()
}
var tabReplacer = strings.NewReplacer("\t", " ")
func (e *Error) Bind(source Source) *Error {
src := source.String()
var runeCount, lineStart int
e.Line = 1
e.Column = 0
for i, r := range src {
if runeCount == e.From {
break
}
if r == '\n' {
lineStart = i + 1
e.Line++
e.Column = 0
} else {
e.Column++
}
runeCount++
}
lineEnd := lineStart + strings.IndexByte(src[lineStart:], '\n')
if lineEnd < lineStart {
lineEnd = len(src)
}
if lineStart == lineEnd {
return e
}
const prefix = "\n | "
line := src[lineStart:lineEnd]
snippet := new(strings.Builder)
snippet.Grow(2*len(prefix) + len(line) + e.Column + 1)
snippet.WriteString(prefix)
tabReplacer.WriteString(snippet, line)
snippet.WriteString(prefix)
for i := 0; i < e.Column; i++ {
snippet.WriteByte('.')
}
snippet.WriteByte('^')
e.Snippet = snippet.String()
return e
}
func (e *Error) Unwrap() error {
return e.Prev
}
func (e *Error) Wrap(err error) {
e.Prev = err
}
func (e *Error) format() string {
if e.Snippet == "" {
return e.Message
}
return fmt.Sprintf(
"%s (%d:%d)%s",
e.Message,
e.Line,
e.Column+1, // add one to the 0-based column for display
e.Snippet,
)
}
================================================
FILE: file/location.go
================================================
package file
type Location struct {
From int `json:"from"`
To int `json:"to"`
}
================================================
FILE: file/source.go
================================================
package file
import "strings"
type Source struct {
raw string
}
func NewSource(contents string) Source {
return Source{
raw: contents,
}
}
func (s Source) String() string {
return s.raw
}
func (s Source) Snippet(line int) (string, bool) {
if s.raw == "" {
return "", false
}
var start int
for i := 1; i < line; i++ {
pos := strings.IndexByte(s.raw[start:], '\n')
if pos < 0 {
return "", false
}
start += pos + 1
}
end := start + strings.IndexByte(s.raw[start:], '\n')
if end < start {
end = len(s.raw)
}
return s.raw[start:end], true
}
================================================
FILE: file/source_test.go
================================================
package file
import (
"testing"
)
const (
unexpectedSnippet = "%s got snippet '%s', want '%v'"
snippetNotFound = "%s snippet not found, wanted '%v'"
snippetFound = "%s snippet found at Line %d, wanted none"
)
func TestStringSource_SnippetMultiLine(t *testing.T) {
source := NewSource("hello\nworld\nmy\nbub\n")
if str, found := source.Snippet(1); !found {
t.Errorf(snippetNotFound, t.Name(), 1)
} else if str != "hello" {
t.Errorf(unexpectedSnippet, t.Name(), str, "hello")
}
if str2, found := source.Snippet(2); !found {
t.Errorf(snippetNotFound, t.Name(), 2)
} else if str2 != "world" {
t.Errorf(unexpectedSnippet, t.Name(), str2, "world")
}
if str3, found := source.Snippet(3); !found {
t.Errorf(snippetNotFound, t.Name(), 3)
} else if str3 != "my" {
t.Errorf(unexpectedSnippet, t.Name(), str3, "my")
}
if str4, found := source.Snippet(4); !found {
t.Errorf(snippetNotFound, t.Name(), 4)
} else if str4 != "bub" {
t.Errorf(unexpectedSnippet, t.Name(), str4, "bub")
}
if str5, found := source.Snippet(5); !found {
t.Errorf(snippetNotFound, t.Name(), 5)
} else if str5 != "" {
t.Errorf(unexpectedSnippet, t.Name(), str5, "")
}
}
func TestStringSource_SnippetSingleLine(t *testing.T) {
source := NewSource("hello, world")
if str, found := source.Snippet(1); !found {
t.Errorf(snippetNotFound, t.Name(), 1)
} else if str != "hello, world" {
t.Errorf(unexpectedSnippet, t.Name(), str, "hello, world")
}
if str2, found := source.Snippet(2); found {
t.Errorf(snippetFound, t.Name(), 2)
} else if str2 != "" {
t.Errorf(unexpectedSnippet, t.Name(), str2, "")
}
}
================================================
FILE: go.mod
================================================
module github.com/expr-lang/expr
go 1.18
================================================
FILE: internal/deref/deref.go
================================================
package deref
import (
"fmt"
"reflect"
)
func Interface(p any) any {
if p == nil {
return nil
}
v := reflect.ValueOf(p)
for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
if v.IsNil() {
return nil
}
v = v.Elem()
}
if v.IsValid() {
return v.Interface()
}
panic(fmt.Sprintf("cannot dereference %v", p))
}
func Type(t reflect.Type) reflect.Type {
if t == nil {
return nil
}
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t
}
func Value(v reflect.Value) reflect.Value {
for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
if v.IsNil() {
return v
}
v = v.Elem()
}
return v
}
func TypeKind(t reflect.Type, k reflect.Kind) (_ reflect.Type, _ reflect.Kind, changed bool) {
for k == reflect.Pointer {
changed = true
t = t.Elem()
k = t.Kind()
}
return t, k, changed
}
================================================
FILE: internal/deref/deref_test.go
================================================
package deref_test
import (
"reflect"
"testing"
"github.com/expr-lang/expr/internal/testify/assert"
"github.com/expr-lang/expr/internal/deref"
)
func TestDeref(t *testing.T) {
a := uint(42)
b := &a
c := &b
d := &c
got := deref.Interface(d)
assert.Equal(t, uint(42), got)
}
func TestDeref_mix_ptr_with_interface(t *testing.T) {
a := uint(42)
var b any = &a
var c any = &b
d := &c
got := deref.Interface(d)
assert.Equal(t, uint(42), got)
}
func TestDeref_nil(t *testing.T) {
var a *int
assert.Nil(t, deref.Interface(a))
assert.Nil(t, deref.Interface(nil))
}
func TestType(t *testing.T) {
a := uint(42)
b := &a
c := &b
d := &c
dt := deref.Type(reflect.TypeOf(d))
assert.Equal(t, reflect.Uint, dt.Kind())
}
func TestType_two_ptr_with_interface(t *testing.T) {
a := uint(42)
var b any = &a
dt := deref.Type(reflect.TypeOf(b))
assert.Equal(t, reflect.Uint, dt.Kind())
}
func TestType_three_ptr_with_interface(t *testing.T) {
a := uint(42)
var b any = &a
var c any = &b
dt := deref.Type(reflect.TypeOf(c))
assert.Equal(t, reflect.Interface, dt.Kind())
}
func TestType_nil(t *testing.T) {
assert.Nil(t, deref.Type(nil))
}
func TestValue(t *testing.T) {
a := uint(42)
b := &a
c := &b
d := &c
got := deref.Value(reflect.ValueOf(d))
assert.Equal(t, uint(42), got.Interface())
}
func TestValue_two_ptr_with_interface(t *testing.T) {
a := uint(42)
var b any = &a
got := deref.Value(reflect.ValueOf(b))
assert.Equal(t, uint(42), got.Interface())
}
func TestValue_three_ptr_with_interface(t *testing.T) {
a := uint(42)
var b any = &a
c := &b
got := deref.Value(reflect.ValueOf(c))
assert.Equal(t, uint(42), got.Interface())
}
func TestValue_nil(t *testing.T) {
got := deref.Value(reflect.ValueOf(nil))
assert.False(t, got.IsValid())
}
func TestValue_nil_in_chain(t *testing.T) {
var a any = nil
var b any = &a
c := &b
got := deref.Value(reflect.ValueOf(c))
assert.True(t, got.IsValid())
assert.True(t, got.IsNil())
assert.Nil(t, got.Interface())
}
================================================
FILE: internal/difflib/difflib.go
================================================
// Package difflib is a partial port of Python difflib module.
//
// It provides tools to compare sequences of strings and generate textual diffs.
//
// The following class and functions have been ported:
//
// - SequenceMatcher
//
// - unified_diff
//
// - context_diff
//
// Getting unified diffs was the main goal of the port. Keep in mind this code
// is mostly suitable to output text differences in a human friendly way, there
// are no guarantees generated diffs are consumable by patch(1).
package difflib
import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
)
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func calculateRatio(matches, length int) float64 {
if length > 0 {
return 2.0 * float64(matches) / float64(length)
}
return 1.0
}
type Match struct {
A int
B int
Size int
}
type OpCode struct {
Tag byte
I1 int
I2 int
J1 int
J2 int
}
// SequenceMatcher compares sequence of strings. The basic
// algorithm predates, and is a little fancier than, an algorithm
// published in the late 1980's by Ratcliff and Obershelp under the
// hyperbolic name "gestalt pattern matching". The basic idea is to find
// the longest contiguous matching subsequence that contains no "junk"
// elements (R-O doesn't address junk). The same idea is then applied
// recursively to the pieces of the sequences to the left and to the right
// of the matching subsequence. This does not yield minimal edit
// sequences, but does tend to yield matches that "look right" to people.
//
// SequenceMatcher tries to compute a "human-friendly diff" between two
// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the
// longest *contiguous* & junk-free matching subsequence. That's what
// catches peoples' eyes. The Windows(tm) windiff has another interesting
// notion, pairing up elements that appear uniquely in each sequence.
// That, and the method here, appear to yield more intuitive difference
// reports than does diff. This method appears to be the least vulnerable
// to synching up on blocks of "junk lines", though (like blank lines in
// ordinary text files, or maybe "" lines in HTML files). That may be
// because this is the only method of the 3 that has a *concept* of
// "junk" .
//
// Timing: Basic R-O is cubic time worst case and quadratic time expected
// case. SequenceMatcher is quadratic time for the worst case and has
// expected-case behavior dependent in a complicated way on how many
// elements the sequences have in common; best case time is linear.
type SequenceMatcher struct {
a []string
b []string
b2j map[string][]int
IsJunk func(string) bool
autoJunk bool
bJunk map[string]struct{}
matchingBlocks []Match
fullBCount map[string]int
bPopular map[string]struct{}
opCodes []OpCode
}
func NewMatcher(a, b []string) *SequenceMatcher {
m := SequenceMatcher{autoJunk: true}
m.SetSeqs(a, b)
return &m
}
func NewMatcherWithJunk(a, b []string, autoJunk bool,
isJunk func(string) bool) *SequenceMatcher {
m := SequenceMatcher{IsJunk: isJunk, autoJunk: autoJunk}
m.SetSeqs(a, b)
return &m
}
// Set two sequences to be compared.
func (m *SequenceMatcher) SetSeqs(a, b []string) {
m.SetSeq1(a)
m.SetSeq2(b)
}
// Set the first sequence to be compared. The second sequence to be compared is
// not changed.
//
// SequenceMatcher computes and caches detailed information about the second
// sequence, so if you want to compare one sequence S against many sequences,
// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other
// sequences.
//
// See also SetSeqs() and SetSeq2().
func (m *SequenceMatcher) SetSeq1(a []string) {
if &a == &m.a {
return
}
m.a = a
m.matchingBlocks = nil
m.opCodes = nil
}
// Set the second sequence to be compared. The first sequence to be compared is
// not changed.
func (m *SequenceMatcher) SetSeq2(b []string) {
if &b == &m.b {
return
}
m.b = b
m.matchingBlocks = nil
m.opCodes = nil
m.fullBCount = nil
m.chainB()
}
func (m *SequenceMatcher) chainB() {
// Populate line -> index mapping
b2j := map[string][]int{}
for i, s := range m.b {
indices := b2j[s]
indices = append(indices, i)
b2j[s] = indices
}
// Purge junk elements
m.bJunk = map[string]struct{}{}
if m.IsJunk != nil {
junk := m.bJunk
for s, _ := range b2j {
if m.IsJunk(s) {
junk[s] = struct{}{}
}
}
for s, _ := range junk {
delete(b2j, s)
}
}
// Purge remaining popular elements
popular := map[string]struct{}{}
n := len(m.b)
if m.autoJunk && n >= 200 {
ntest := n/100 + 1
for s, indices := range b2j {
if len(indices) > ntest {
popular[s] = struct{}{}
}
}
for s, _ := range popular {
delete(b2j, s)
}
}
m.bPopular = popular
m.b2j = b2j
}
func (m *SequenceMatcher) isBJunk(s string) bool {
_, ok := m.bJunk[s]
return ok
}
// Find longest matching block in a[alo:ahi] and b[blo:bhi].
//
// If IsJunk is not defined:
//
// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where
//
// alo <= i <= i+k <= ahi
// blo <= j <= j+k <= bhi
//
// and for all (i',j',k') meeting those conditions,
//
// k >= k'
// i <= i'
// and if i == i', j <= j'
//
// In other words, of all maximal matching blocks, return one that
// starts earliest in a, and of all those maximal matching blocks that
// start earliest in a, return the one that starts earliest in b.
//
// If IsJunk is defined, first the longest matching block is
// determined as above, but with the additional restriction that no
// junk element appears in the block. Then that block is extended as
// far as possible by matching (only) junk elements on both sides. So
// the resulting block never matches on junk except as identical junk
// happens to be adjacent to an "interesting" match.
//
// If no blocks match, return (alo, blo, 0).
func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match {
// CAUTION: stripping common prefix or suffix would be incorrect.
// E.g.,
// ab
// acab
// Longest matching block is "ab", but if common prefix is
// stripped, it's "a" (tied with "b"). UNIX(tm) diff does so
// strip, so ends up claiming that ab is changed to acab by
// inserting "ca" in the middle. That's minimal but unintuitive:
// "it's obvious" that someone inserted "ac" at the front.
// Windiff ends up at the same place as diff, but by pairing up
// the unique 'b's and then matching the first two 'a's.
besti, bestj, bestsize := alo, blo, 0
// find longest junk-free match
// during an iteration of the loop, j2len[j] = length of longest
// junk-free match ending with a[i-1] and b[j]
j2len := map[int]int{}
for i := alo; i != ahi; i++ {
// look at all instances of a[i] in b; note that because
// b2j has no junk keys, the loop is skipped if a[i] is junk
newj2len := map[int]int{}
for _, j := range m.b2j[m.a[i]] {
// a[i] matches b[j]
if j < blo {
continue
}
if j >= bhi {
break
}
k := j2len[j-1] + 1
newj2len[j] = k
if k > bestsize {
besti, bestj, bestsize = i-k+1, j-k+1, k
}
}
j2len = newj2len
}
// Extend the best by non-junk elements on each end. In particular,
// "popular" non-junk elements aren't in b2j, which greatly speeds
// the inner loop above, but also means "the best" match so far
// doesn't contain any junk *or* popular non-junk elements.
for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) &&
m.a[besti-1] == m.b[bestj-1] {
besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
}
for besti+bestsize < ahi && bestj+bestsize < bhi &&
!m.isBJunk(m.b[bestj+bestsize]) &&
m.a[besti+bestsize] == m.b[bestj+bestsize] {
bestsize += 1
}
// Now that we have a wholly interesting match (albeit possibly
// empty!), we may as well suck up the matching junk on each
// side of it too. Can't think of a good reason not to, and it
// saves post-processing the (possibly considerable) expense of
// figuring out what to do with it. In the case of an empty
// interesting match, this is clearly the right thing to do,
// because no other kind of match is possible in the regions.
for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) &&
m.a[besti-1] == m.b[bestj-1] {
besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
}
for besti+bestsize < ahi && bestj+bestsize < bhi &&
m.isBJunk(m.b[bestj+bestsize]) &&
m.a[besti+bestsize] == m.b[bestj+bestsize] {
bestsize += 1
}
return Match{A: besti, B: bestj, Size: bestsize}
}
// Return list of triples describing matching subsequences.
//
// Each triple is of the form (i, j, n), and means that
// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in
// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are
// adjacent triples in the list, and the second is not the last triple in the
// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe
// adjacent equal blocks.
//
// The last triple is a dummy, (len(a), len(b), 0), and is the only
// triple with n==0.
func (m *SequenceMatcher) GetMatchingBlocks() []Match {
if m.matchingBlocks != nil {
return m.matchingBlocks
}
var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match
matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match {
match := m.findLongestMatch(alo, ahi, blo, bhi)
i, j, k := match.A, match.B, match.Size
if match.Size > 0 {
if alo < i && blo < j {
matched = matchBlocks(alo, i, blo, j, matched)
}
matched = append(matched, match)
if i+k < ahi && j+k < bhi {
matched = matchBlocks(i+k, ahi, j+k, bhi, matched)
}
}
return matched
}
matched := matchBlocks(0, len(m.a), 0, len(m.b), nil)
// It's possible that we have adjacent equal blocks in the
// matching_blocks list now.
nonAdjacent := []Match{}
i1, j1, k1 := 0, 0, 0
for _, b := range matched {
// Is this block adjacent to i1, j1, k1?
i2, j2, k2 := b.A, b.B, b.Size
if i1+k1 == i2 && j1+k1 == j2 {
// Yes, so collapse them -- this just increases the length of
// the first block by the length of the second, and the first
// block so lengthened remains the block to compare against.
k1 += k2
} else {
// Not adjacent. Remember the first block (k1==0 means it's
// the dummy we started with), and make the second block the
// new block to compare against.
if k1 > 0 {
nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
}
i1, j1, k1 = i2, j2, k2
}
}
if k1 > 0 {
nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
}
nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0})
m.matchingBlocks = nonAdjacent
return m.matchingBlocks
}
// Return list of 5-tuples describing how to turn a into b.
//
// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple
// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the
// tuple preceding it, and likewise for j1 == the previous j2.
//
// The tags are characters, with these meanings:
//
// 'r' (replace): a[i1:i2] should be replaced by b[j1:j2]
//
// 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case.
//
// 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case.
//
// 'e' (equal): a[i1:i2] == b[j1:j2]
func (m *SequenceMatcher) GetOpCodes() []OpCode {
if m.opCodes != nil {
return m.opCodes
}
i, j := 0, 0
matching := m.GetMatchingBlocks()
opCodes := make([]OpCode, 0, len(matching))
for _, m := range matching {
// invariant: we've pumped out correct diffs to change
// a[:i] into b[:j], and the next matching block is
// a[ai:ai+size] == b[bj:bj+size]. So we need to pump
// out a diff to change a[i:ai] into b[j:bj], pump out
// the matching block, and move (i,j) beyond the match
ai, bj, size := m.A, m.B, m.Size
tag := byte(0)
if i < ai && j < bj {
tag = 'r'
} else if i < ai {
tag = 'd'
} else if j < bj {
tag = 'i'
}
if tag > 0 {
opCodes = append(opCodes, OpCode{tag, i, ai, j, bj})
}
i, j = ai+size, bj+size
// the list of matching blocks is terminated by a
// sentinel with size 0
if size > 0 {
opCodes = append(opCodes, OpCode{'e', ai, i, bj, j})
}
}
m.opCodes = opCodes
return m.opCodes
}
// Isolate change clusters by eliminating ranges with no changes.
//
// Return a generator of groups with up to n lines of context.
// Each group is in the same format as returned by GetOpCodes().
func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
if n < 0 {
n = 3
}
codes := m.GetOpCodes()
if len(codes) == 0 {
codes = []OpCode{OpCode{'e', 0, 1, 0, 1}}
}
// Fixup leading and trailing groups if they show no changes.
if codes[0].Tag == 'e' {
c := codes[0]
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2}
}
if codes[len(codes)-1].Tag == 'e' {
c := codes[len(codes)-1]
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)}
}
nn := n + n
groups := [][]OpCode{}
group := []OpCode{}
for _, c := range codes {
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
// End the current group and start a new one whenever
// there is a large range with no changes.
if c.Tag == 'e' && i2-i1 > nn {
group = append(group, OpCode{c.Tag, i1, min(i2, i1+n),
j1, min(j2, j1+n)})
groups = append(groups, group)
group = []OpCode{}
i1, j1 = max(i1, i2-n), max(j1, j2-n)
}
group = append(group, OpCode{c.Tag, i1, i2, j1, j2})
}
if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') {
groups = append(groups, group)
}
return groups
}
// Return a measure of the sequences' similarity (float in [0,1]).
//
// Where T is the total number of elements in both sequences, and
// M is the number of matches, this is 2.0*M / T.
// Note that this is 1 if the sequences are identical, and 0 if
// they have nothing in common.
//
// .Ratio() is expensive to compute if you haven't already computed
// .GetMatchingBlocks() or .GetOpCodes(), in which case you may
// want to try .QuickRatio() or .RealQuickRation() first to get an
// upper bound.
func (m *SequenceMatcher) Ratio() float64 {
matches := 0
for _, m := range m.GetMatchingBlocks() {
matches += m.Size
}
return calculateRatio(matches, len(m.a)+len(m.b))
}
// Return an upper bound on ratio() relatively quickly.
//
// This isn't defined beyond that it is an upper bound on .Ratio(), and
// is faster to compute.
func (m *SequenceMatcher) QuickRatio() float64 {
// viewing a and b as multisets, set matches to the cardinality
// of their intersection; this counts the number of matches
// without regard to order, so is clearly an upper bound
if m.fullBCount == nil {
m.fullBCount = map[string]int{}
for _, s := range m.b {
m.fullBCount[s] = m.fullBCount[s] + 1
}
}
// avail[x] is the number of times x appears in 'b' less the
// number of times we've seen it in 'a' so far ... kinda
avail := map[string]int{}
matches := 0
for _, s := range m.a {
n, ok := avail[s]
if !ok {
n = m.fullBCount[s]
}
avail[s] = n - 1
if n > 0 {
matches += 1
}
}
return calculateRatio(matches, len(m.a)+len(m.b))
}
// Return an upper bound on ratio() very quickly.
//
// This isn't defined beyond that it is an upper bound on .Ratio(), and
// is faster to compute than either .Ratio() or .QuickRatio().
func (m *SequenceMatcher) RealQuickRatio() float64 {
la, lb := len(m.a), len(m.b)
return calculateRatio(min(la, lb), la+lb)
}
// Convert range to the "ed" format
func formatRangeUnified(start, stop int) string {
// Per the diff spec at http://www.unix.org/single_unix_specification/
beginning := start + 1 // lines start numbering with one
length := stop - start
if length == 1 {
return fmt.Sprintf("%d", beginning)
}
if length == 0 {
beginning -= 1 // empty ranges begin at line just before the range
}
return fmt.Sprintf("%d,%d", beginning, length)
}
// Unified diff parameters
type UnifiedDiff struct {
A []string // First sequence lines
FromFile string // First file name
FromDate string // First file time
B []string // Second sequence lines
ToFile string // Second file name
ToDate string // Second file time
Eol string // Headers end of line, defaults to LF
Context int // Number of context lines
}
// Compare two sequences of lines; generate the delta as a unified diff.
//
// Unified diffs are a compact way of showing line changes and a few
// lines of context. The number of context lines is set by 'n' which
// defaults to three.
//
// By default, the diff control lines (those with ---, +++, or @@) are
// created with a trailing newline. This is helpful so that inputs
// created from file.readlines() result in diffs that are suitable for
// file.writelines() since both the inputs and outputs have trailing
// newlines.
//
// For inputs that do not have trailing newlines, set the lineterm
// argument to "" so that the output will be uniformly newline free.
//
// The unidiff format normally has a header for filenames and modification
// times. Any or all of these may be specified using strings for
// 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.
// The modification times are normally expressed in the ISO 8601 format.
func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error {
buf := bufio.NewWriter(writer)
defer buf.Flush()
wf := func(format string, args ...interface{}) error {
_, err := buf.WriteString(fmt.Sprintf(format, args...))
return err
}
ws := func(s string) error {
_, err := buf.WriteString(s)
return err
}
if len(diff.Eol) == 0 {
diff.Eol = "\n"
}
started := false
m := NewMatcher(diff.A, diff.B)
for _, g := range m.GetGroupedOpCodes(diff.Context) {
if !started {
started = true
fromDate := ""
if len(diff.FromDate) > 0 {
fromDate = "\t" + diff.FromDate
}
toDate := ""
if len(diff.ToDate) > 0 {
toDate = "\t" + diff.ToDate
}
if diff.FromFile != "" || diff.ToFile != "" {
err := wf("--- %s%s%s", diff.FromFile, fromDate, diff.Eol)
if err != nil {
return err
}
err = wf("+++ %s%s%s", diff.ToFile, toDate, diff.Eol)
if err != nil {
return err
}
}
}
first, last := g[0], g[len(g)-1]
range1 := formatRangeUnified(first.I1, last.I2)
range2 := formatRangeUnified(first.J1, last.J2)
if err := wf("@@ -%s +%s @@%s", range1, range2, diff.Eol); err != nil {
return err
}
for _, c := range g {
i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
if c.Tag == 'e' {
for _, line := range diff.A[i1:i2] {
if err := ws(" " + line); err != nil {
return err
}
}
continue
}
if c.Tag == 'r' || c.Tag == 'd' {
for _, line := range diff.A[i1:i2] {
if err := ws("-" + line); err != nil {
return err
}
}
}
if c.Tag == 'r' || c.Tag == 'i' {
for _, line := range diff.B[j1:j2] {
if err := ws("+" + line); err != nil {
return err
}
}
}
}
}
return nil
}
// Like WriteUnifiedDiff but returns the diff a string.
func GetUnifiedDiffString(diff UnifiedDiff) (string, error) {
w := &bytes.Buffer{}
err := WriteUnifiedDiff(w, diff)
return string(w.Bytes()), err
}
// Convert range to the "ed" format.
func formatRangeContext(start, stop int) string {
// Per the diff spec at http://www.unix.org/single_unix_specification/
beginning := start + 1 // lines start numbering with one
length := stop - start
if length == 0 {
beginning -= 1 // empty ranges begin at line just before the range
}
if length <= 1 {
return fmt.Sprintf("%d", beginning)
}
return fmt.Sprintf("%d,%d", beginning, beginning+length-1)
}
type ContextDiff UnifiedDiff
// Compare two sequences of lines; generate the delta as a context diff.
//
// Context diffs are a compact way of showing line changes and a few
// lines of context. The number of context lines is set by diff.Context
// which defaults to three.
//
// By default, the diff control lines (those with *** or ---) are
// created with a trailing newline.
//
// For inputs that do not have trailing newlines, set the diff.Eol
// argument to "" so that the output will be uniformly newline free.
//
// The context diff format normally has a header for filenames and
// modification times. Any or all of these may be specified using
// strings for diff.FromFile, diff.ToFile, diff.FromDate, diff.ToDate.
// The modification times are normally expressed in the ISO 8601 format.
// If not specified, the strings default to blanks.
func WriteContextDiff(writer io.Writer, diff ContextDiff) error {
buf := bufio.NewWriter(writer)
defer buf.Flush()
var diffErr error
wf := func(format string, args ...interface{}) {
_, err := buf.WriteString(fmt.Sprintf(format, args...))
if diffErr == nil && err != nil {
diffErr = err
}
}
ws := func(s string) {
_, err := buf.WriteString(s)
if diffErr == nil && err != nil {
diffErr = err
}
}
if len(diff.Eol) == 0 {
diff.Eol = "\n"
}
prefix := map[byte]string{
'i': "+ ",
'd': "- ",
'r': "! ",
'e': " ",
}
started := false
m := NewMatcher(diff.A, diff.B)
for _, g := range m.GetGroupedOpCodes(diff.Context) {
if !started {
started = true
fromDate := ""
if len(diff.FromDate) > 0 {
fromDate = "\t" + diff.FromDate
}
toDate := ""
if len(diff.ToDate) > 0 {
toDate = "\t" + diff.ToDate
}
if diff.FromFile != "" || diff.ToFile != "" {
wf("*** %s%s%s", diff.FromFile, fromDate, diff.Eol)
wf("--- %s%s%s", diff.ToFile, toDate, diff.Eol)
}
}
first, last := g[0], g[len(g)-1]
ws("***************" + diff.Eol)
range1 := formatRangeContext(first.I1, last.I2)
wf("*** %s ****%s", range1, diff.Eol)
for _, c := range g {
if c.Tag == 'r' || c.Tag == 'd' {
for _, cc := range g {
if cc.Tag == 'i' {
continue
}
for _, line := range diff.A[cc.I1:cc.I2] {
ws(prefix[cc.Tag] + line)
}
}
break
}
}
range2 := formatRangeContext(first.J1, last.J2)
wf("--- %s ----%s", range2, diff.Eol)
for _, c := range g {
if c.Tag == 'r' || c.Tag == 'i' {
for _, cc := range g {
if cc.Tag == 'd' {
continue
}
for _, line := range diff.B[cc.J1:cc.J2] {
ws(prefix[cc.Tag] + line)
}
}
break
}
}
}
return diffErr
}
// Like WriteContextDiff but returns the diff a string.
func GetContextDiffString(diff ContextDiff) (string, error) {
w := &bytes.Buffer{}
err := WriteContextDiff(w, diff)
return string(w.Bytes()), err
}
// Split a string on "\n" while preserving them. The output can be used
// as input for UnifiedDiff and ContextDiff structures.
func SplitLines(s string) []string {
lines := strings.SplitAfter(s, "\n")
lines[len(lines)-1] += "\n"
return lines
}
================================================
FILE: internal/difflib/difflib_test.go
================================================
package difflib
import (
"bytes"
"fmt"
"math"
"reflect"
"strings"
"testing"
)
func assertAlmostEqual(t *testing.T, a, b float64, places int) {
if math.Abs(a-b) > math.Pow10(-places) {
t.Errorf("%.7f != %.7f", a, b)
}
}
func assertEqual(t *testing.T, a, b interface{}) {
if !reflect.DeepEqual(a, b) {
t.Errorf("%v != %v", a, b)
}
}
func splitChars(s string) []string {
chars := make([]string, 0, len(s))
// Assume ASCII inputs
for i := 0; i != len(s); i++ {
chars = append(chars, string(s[i]))
}
return chars
}
func TestSequenceMatcherRatio(t *testing.T) {
s := NewMatcher(splitChars("abcd"), splitChars("bcde"))
assertEqual(t, s.Ratio(), 0.75)
assertEqual(t, s.QuickRatio(), 0.75)
assertEqual(t, s.RealQuickRatio(), 1.0)
}
func TestGetOptCodes(t *testing.T) {
a := "qabxcd"
b := "abycdf"
s := NewMatcher(splitChars(a), splitChars(b))
w := &bytes.Buffer{}
for _, op := range s.GetOpCodes() {
fmt.Fprintf(w, "%s a[%d:%d], (%s) b[%d:%d] (%s)\n", string(op.Tag),
op.I1, op.I2, a[op.I1:op.I2], op.J1, op.J2, b[op.J1:op.J2])
}
result := string(w.Bytes())
expected := `d a[0:1], (q) b[0:0] ()
e a[1:3], (ab) b[0:2] (ab)
r a[3:4], (x) b[2:3] (y)
e a[4:6], (cd) b[3:5] (cd)
i a[6:6], () b[5:6] (f)
`
if expected != result {
t.Errorf("unexpected op codes: \n%s", result)
}
}
func TestGroupedOpCodes(t *testing.T) {
a := []string{}
for i := 0; i != 39; i++ {
a = append(a, fmt.Sprintf("%02d", i))
}
b := []string{}
b = append(b, a[:8]...)
b = append(b, " i")
b = append(b, a[8:19]...)
b = append(b, " x")
b = append(b, a[20:22]...)
b = append(b, a[27:34]...)
b = append(b, " y")
b = append(b, a[35:]...)
s := NewMatcher(a, b)
w := &bytes.Buffer{}
for _, g := range s.GetGroupedOpCodes(-1) {
fmt.Fprintf(w, "group\n")
for _, op := range g {
fmt.Fprintf(w, " %s, %d, %d, %d, %d\n", string(op.Tag),
op.I1, op.I2, op.J1, op.J2)
}
}
result := string(w.Bytes())
expected := `group
e, 5, 8, 5, 8
i, 8, 8, 8, 9
e, 8, 11, 9, 12
group
e, 16, 19, 17, 20
r, 19, 20, 20, 21
e, 20, 22, 21, 23
d, 22, 27, 23, 23
e, 27, 30, 23, 26
group
e, 31, 34, 27, 30
r, 34, 35, 30, 31
e, 35, 38, 31, 34
`
if expected != result {
t.Errorf("unexpected op codes: \n%s", result)
}
}
func ExampleGetUnifiedDiffString() {
a := `one
two
three
four
fmt.Printf("%s,%T",a,b)`
b := `zero
one
three
four`
diff := UnifiedDiff{
A: SplitLines(a),
B: SplitLines(b),
FromFile: "Original",
FromDate: "2005-01-26 23:30:50",
ToFile: "Current",
ToDate: "2010-04-02 10:20:52",
Context: 3,
}
result, _ := GetUnifiedDiffString(diff)
fmt.Println(strings.Replace(result, "\t", " ", -1))
// Output:
// --- Original 2005-01-26 23:30:50
// +++ Current 2010-04-02 10:20:52
// @@ -1,5 +1,4 @@
// +zero
// one
// -two
// three
// four
// -fmt.Printf("%s,%T",a,b)
}
func ExampleGetContextDiffString() {
a := `one
two
three
four
fmt.Printf("%s,%T",a,b)`
b := `zero
one
tree
four`
diff := ContextDiff{
A: SplitLines(a),
B: SplitLines(b),
FromFile: "Original",
ToFile: "Current",
Context: 3,
Eol: "\n",
}
result, _ := GetContextDiffString(diff)
fmt.Print(strings.Replace(result, "\t", " ", -1))
// Output:
// *** Original
// --- Current
// ***************
// *** 1,5 ****
// one
// ! two
// ! three
// four
// - fmt.Printf("%s,%T",a,b)
// --- 1,4 ----
// + zero
// one
// ! tree
// four
}
func ExampleGetContextDiffString_second() {
a := `one
two
three
four`
b := `zero
one
tree
four`
diff := ContextDiff{
A: SplitLines(a),
B: SplitLines(b),
FromFile: "Original",
ToFile: "Current",
Context: 3,
Eol: "\n",
}
result, _ := GetContextDiffString(diff)
fmt.Printf(strings.Replace(result, "\t", " ", -1))
// Output:
// *** Original
// --- Current
// ***************
// *** 1,4 ****
// one
// ! two
// ! three
// four
// --- 1,4 ----
// + zero
// one
// ! tree
// four
}
func rep(s string, count int) string {
return strings.Repeat(s, count)
}
func TestWithAsciiOneInsert(t *testing.T) {
sm := NewMatcher(splitChars(rep("b", 100)),
splitChars("a"+rep("b", 100)))
assertAlmostEqual(t, sm.Ratio(), 0.995, 3)
assertEqual(t, sm.GetOpCodes(),
[]OpCode{{'i', 0, 0, 0, 1}, {'e', 0, 100, 1, 101}})
assertEqual(t, len(sm.bPopular), 0)
sm = NewMatcher(splitChars(rep("b", 100)),
splitChars(rep("b", 50)+"a"+rep("b", 50)))
assertAlmostEqual(t, sm.Ratio(), 0.995, 3)
assertEqual(t, sm.GetOpCodes(),
[]OpCode{{'e', 0, 50, 0, 50}, {'i', 50, 50, 50, 51}, {'e', 50, 100, 51, 101}})
assertEqual(t, len(sm.bPopular), 0)
}
func TestWithAsciiOnDelete(t *testing.T) {
sm := NewMatcher(splitChars(rep("a", 40)+"c"+rep("b", 40)),
splitChars(rep("a", 40)+rep("b", 40)))
assertAlmostEqual(t, sm.Ratio(), 0.994, 3)
assertEqual(t, sm.GetOpCodes(),
[]OpCode{{'e', 0, 40, 0, 40}, {'d', 40, 41, 40, 40}, {'e', 41, 81, 40, 80}})
}
func TestWithAsciiBJunk(t *testing.T) {
isJunk := func(s string) bool {
return s == " "
}
sm := NewMatcherWithJunk(splitChars(rep("a", 40)+rep("b", 40)),
splitChars(rep("a", 44)+rep("b", 40)), true, isJunk)
assertEqual(t, sm.bJunk, map[string]struct{}{})
sm = NewMatcherWithJunk(splitChars(rep("a", 40)+rep("b", 40)),
splitChars(rep("a", 44)+rep("b", 40)+rep(" ", 20)), false, isJunk)
assertEqual(t, sm.bJunk, map[string]struct{}{" ": struct{}{}})
isJunk = func(s string) bool {
return s == " " || s == "b"
}
sm = NewMatcherWithJunk(splitChars(rep("a", 40)+rep("b", 40)),
splitChars(rep("a", 44)+rep("b", 40)+rep(" ", 20)), false, isJunk)
assertEqual(t, sm.bJunk, map[string]struct{}{" ": struct{}{}, "b": struct{}{}})
}
func TestSFBugsRatioForNullSeqn(t *testing.T) {
sm := NewMatcher(nil, nil)
assertEqual(t, sm.Ratio(), 1.0)
assertEqual(t, sm.QuickRatio(), 1.0)
assertEqual(t, sm.RealQuickRatio(), 1.0)
}
func TestSFBugsComparingEmptyLists(t *testing.T) {
groups := NewMatcher(nil, nil).GetGroupedOpCodes(-1)
assertEqual(t, len(groups), 0)
diff := UnifiedDiff{
FromFile: "Original",
ToFile: "Current",
Context: 3,
}
result, err := GetUnifiedDiffString(diff)
assertEqual(t, err, nil)
assertEqual(t, result, "")
}
func TestOutputFormatRangeFormatUnified(t *testing.T) {
// Per the diff spec at http://www.unix.org/single_unix_specification/
//
// Each field shall be of the form:
// %1d", if the range contains exactly one line,
// and:
// "%1d,%1d", , otherwise.
// If a range is empty, its beginning line number shall be the number of
// the line just before the range, or 0 if the empty range starts the file.
fm := formatRangeUnified
assertEqual(t, fm(3, 3), "3,0")
assertEqual(t, fm(3, 4), "4")
assertEqual(t, fm(3, 5), "4,2")
assertEqual(t, fm(3, 6), "4,3")
assertEqual(t, fm(0, 0), "0,0")
}
func TestOutputFormatRangeFormatContext(t *testing.T) {
// Per the diff spec at http://www.unix.org/single_unix_specification/
//
// The range of lines in file1 shall be written in the following format
// if the range contains two or more lines:
// "*** %d,%d ****\n", ,
// and the following format otherwise:
// "*** %d ****\n",
// The ending line number of an empty range shall be the number of the preceding line,
// or 0 if the range is at the start of the file.
//
// Next, the range of lines in file2 shall be written in the following format
// if the range contains two or more lines:
// "--- %d,%d ----\n", ,
// and the following format otherwise:
// "--- %d ----\n",
fm := formatRangeContext
assertEqual(t, fm(3, 3), "3")
assertEqual(t, fm(3, 4), "4")
assertEqual(t, fm(3, 5), "4,5")
assertEqual(t, fm(3, 6), "4,6")
assertEqual(t, fm(0, 0), "0")
}
func TestOutputFormatTabDelimiter(t *testing.T) {
diff := UnifiedDiff{
A: splitChars("one"),
B: splitChars("two"),
FromFile: "Original",
FromDate: "2005-01-26 23:30:50",
ToFile: "Current",
ToDate: "2010-04-12 10:20:52",
Eol: "\n",
}
ud, err := GetUnifiedDiffString(diff)
assertEqual(t, err, nil)
assertEqual(t, SplitLines(ud)[:2], []string{
"--- Original\t2005-01-26 23:30:50\n",
"+++ Current\t2010-04-12 10:20:52\n",
})
cd, err := GetContextDiffString(ContextDiff(diff))
assertEqual(t, err, nil)
assertEqual(t, SplitLines(cd)[:2], []string{
"*** Original\t2005-01-26 23:30:50\n",
"--- Current\t2010-04-12 10:20:52\n",
})
}
func TestOutputFormatNoTrailingTabOnEmptyFiledate(t *testing.T) {
diff := UnifiedDiff{
A: splitChars("one"),
B: splitChars("two"),
FromFile: "Original",
ToFile: "Current",
Eol: "\n",
}
ud, err := GetUnifiedDiffString(diff)
assertEqual(t, err, nil)
assertEqual(t, SplitLines(ud)[:2], []string{"--- Original\n", "+++ Current\n"})
cd, err := GetContextDiffString(ContextDiff(diff))
assertEqual(t, err, nil)
assertEqual(t, SplitLines(cd)[:2], []string{"*** Original\n", "--- Current\n"})
}
func TestOmitFilenames(t *testing.T) {
diff := UnifiedDiff{
A: SplitLines("o\nn\ne\n"),
B: SplitLines("t\nw\no\n"),
Eol: "\n",
}
ud, err := GetUnifiedDiffString(diff)
assertEqual(t, err, nil)
assertEqual(t, SplitLines(ud), []string{
"@@ -0,0 +1,2 @@\n",
"+t\n",
"+w\n",
"@@ -2,2 +3,0 @@\n",
"-n\n",
"-e\n",
"\n",
})
cd, err := GetContextDiffString(ContextDiff(diff))
assertEqual(t, err, nil)
assertEqual(t, SplitLines(cd), []string{
"***************\n",
"*** 0 ****\n",
"--- 1,2 ----\n",
"+ t\n",
"+ w\n",
"***************\n",
"*** 2,3 ****\n",
"- n\n",
"- e\n",
"--- 3 ----\n",
"\n",
})
}
func TestSplitLines(t *testing.T) {
allTests := []struct {
input string
want []string
}{
{"foo", []string{"foo\n"}},
{"foo\nbar", []string{"foo\n", "bar\n"}},
{"foo\nbar\n", []string{"foo\n", "bar\n", "\n"}},
}
for _, test := range allTests {
assertEqual(t, SplitLines(test.input), test.want)
}
}
func benchmarkSplitLines(b *testing.B, count int) {
str := strings.Repeat("foo\n", count)
b.ResetTimer()
n := 0
for i := 0; i < b.N; i++ {
n += len(SplitLines(str))
}
}
func BenchmarkSplitLines100(b *testing.B) {
benchmarkSplitLines(b, 100)
}
func BenchmarkSplitLines10000(b *testing.B) {
benchmarkSplitLines(b, 10000)
}
================================================
FILE: internal/ring/ring.go
================================================
package ring
// Ring is a very simple ring buffer implementation that uses a slice. The
// internal slice will only grow, never shrink. When it grows, it grows in
// chunks of "chunkSize" (given as argument in the [New] function). Pointer and
// reference types can be safely used because memory is cleared.
type Ring[T any] struct {
data []T
back, len, chunkSize int
}
func New[T any](chunkSize int) *Ring[T] {
if chunkSize < 1 {
panic("chunkSize must be greater than zero")
}
return &Ring[T]{
chunkSize: chunkSize,
}
}
func (r *Ring[T]) Len() int {
return r.len
}
func (r *Ring[T]) Cap() int {
return len(r.data)
}
func (r *Ring[T]) Reset() {
var zero T
for i := range r.data {
r.data[i] = zero // clear mem, optimized by the compiler, in Go 1.21 the "clear" builtin can be used
}
r.back = 0
r.len = 0
}
// Nth returns the n-th oldest value (zero-based) in the ring without making
// any change.
func (r *Ring[T]) Nth(n int) (v T, ok bool) {
if n < 0 || n >= r.len || len(r.data) == 0 {
return v, false
}
n = (n + r.back) % len(r.data)
return r.data[n], true
}
// Dequeue returns the oldest value.
func (r *Ring[T]) Dequeue() (v T, ok bool) {
if r.len == 0 {
return v, false
}
v, r.data[r.back] = r.data[r.back], v // retrieve and clear mem
r.len--
r.back = (r.back + 1) % len(r.data)
return v, true
}
// Enqueue adds an item to the ring.
func (r *Ring[T]) Enqueue(v T) {
if r.len == len(r.data) {
r.grow()
}
writePos := (r.back + r.len) % len(r.data)
r.data[writePos] = v
r.len++
}
func (r *Ring[T]) grow() {
s := make([]T, len(r.data)+r.chunkSize)
if r.len > 0 {
chunk1 := r.back + r.len
if chunk1 > len(r.data) {
chunk1 = len(r.data)
}
copied := copy(s, r.data[r.back:chunk1])
if copied < r.len { // wrapped slice
chunk2 := r.len - copied
copy(s[copied:], r.data[:chunk2])
}
}
r.back = 0
r.data = s
}
================================================
FILE: internal/ring/ring_test.go
================================================
package ring
import (
"fmt"
"testing"
)
func TestRing(t *testing.T) {
type op = ringOp[int]
testRing(t, New[int](3),
// noops on empty ring
op{cap: 0, opType: opRst, value: 0, items: []int{}},
op{cap: 0, opType: opDeq, value: 0, items: []int{}},
// basic
op{cap: 3, opType: opEnq, value: 1, items: []int{1}},
op{cap: 3, opType: opDeq, value: 1, items: []int{}},
// wrapping
op{cap: 3, opType: opEnq, value: 2, items: []int{2}},
op{cap: 3, opType: opEnq, value: 3, items: []int{2, 3}},
op{cap: 3, opType: opEnq, value: 4, items: []int{2, 3, 4}},
op{cap: 3, opType: opDeq, value: 2, items: []int{3, 4}},
op{cap: 3, opType: opDeq, value: 3, items: []int{4}},
op{cap: 3, opType: opDeq, value: 4, items: []int{}},
// resetting
op{cap: 3, opType: opEnq, value: 2, items: []int{2}},
op{cap: 3, opType: opRst, value: 0, items: []int{}},
op{cap: 3, opType: opDeq, value: 0, items: []int{}},
// growing without wrapping
op{cap: 3, opType: opEnq, value: 5, items: []int{5}},
op{cap: 3, opType: opEnq, value: 6, items: []int{5, 6}},
op{cap: 3, opType: opEnq, value: 7, items: []int{5, 6, 7}},
op{cap: 6, opType: opEnq, value: 8, items: []int{5, 6, 7, 8}},
op{cap: 6, opType: opRst, value: 0, items: []int{}},
op{cap: 6, opType: opDeq, value: 0, items: []int{}},
// growing and wrapping
op{cap: 6, opType: opEnq, value: 9, items: []int{9}},
op{cap: 6, opType: opEnq, value: 10, items: []int{9, 10}},
op{cap: 6, opType: opEnq, value: 11, items: []int{9, 10, 11}},
op{cap: 6, opType: opEnq, value: 12, items: []int{9, 10, 11, 12}},
op{cap: 6, opType: opEnq, value: 13, items: []int{9, 10, 11, 12, 13}},
op{cap: 6, opType: opEnq, value: 14, items: []int{9, 10, 11, 12, 13, 14}},
op{cap: 6, opType: opDeq, value: 9, items: []int{10, 11, 12, 13, 14}},
op{cap: 6, opType: opDeq, value: 10, items: []int{11, 12, 13, 14}},
op{cap: 6, opType: opEnq, value: 15, items: []int{11, 12, 13, 14, 15}},
op{cap: 6, opType: opEnq, value: 16, items: []int{11, 12, 13, 14, 15, 16}},
op{cap: 9, opType: opEnq, value: 17, items: []int{11, 12, 13, 14, 15, 16, 17}}, // grows wrapped
op{cap: 9, opType: opDeq, value: 11, items: []int{12, 13, 14, 15, 16, 17}},
op{cap: 9, opType: opDeq, value: 12, items: []int{13, 14, 15, 16, 17}},
op{cap: 9, opType: opDeq, value: 13, items: []int{14, 15, 16, 17}},
op{cap: 9, opType: opDeq, value: 14, items: []int{15, 16, 17}},
op{cap: 9, opType: opDeq, value: 15, items: []int{16, 17}},
op{cap: 9, opType: opDeq, value: 16, items: []int{17}},
op{cap: 9, opType: opDeq, value: 17, items: []int{}},
op{cap: 9, opType: opDeq, value: 0, items: []int{}},
)
t.Run("should panic on invalid chunkSize", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("should have panicked")
}
}()
New[int](0)
})
}
const (
opEnq = iota // enqueue an item
opDeq // dequeue an item and an item was available
opRst // reset
)
type ringOp[T comparable] struct {
cap int // expected values
opType int // opEnq or opDeq
value T // value to enqueue or value expected for dequeue; ignored for opRst
items []T // items left
}
func testRing[T comparable](t *testing.T, r *Ring[T], ops ...ringOp[T]) {
for i, op := range ops {
testOK := t.Run(fmt.Sprintf("opIndex=%v", i), func(t *testing.T) {
testRingOp(t, r, op)
})
if !testOK {
return
}
}
}
func testRingOp[T comparable](t *testing.T, r *Ring[T], op ringOp[T]) {
var zero T
switch op.opType {
case opEnq:
r.Enqueue(op.value)
case opDeq:
shouldSucceed := r.Len() > 0
v, ok := r.Dequeue()
switch {
case ok != shouldSucceed:
t.Fatalf("should have succeeded: %v", shouldSucceed)
case ok && v != op.value:
t.Fatalf("expected value: %v; got: %v", op.value, v)
case !ok && v != zero:
t.Fatalf("expected zero value; got: %v", v)
}
case opRst:
r.Reset()
}
if c := r.Cap(); c != op.cap {
t.Fatalf("expected cap: %v; got: %v", op.cap, c)
}
if l := r.Len(); l != len(op.items) {
t.Errorf("expected Len(): %v; got: %v", len(op.items), l)
}
var got []T
for i := 0; ; i++ {
v, ok := r.Nth(i)
if !ok {
break
}
got = append(got, v)
}
if l := len(got); l != len(op.items) {
t.Errorf("expected items: %v\ngot items: %v", op.items, got)
}
for i := range op.items {
if op.items[i] != got[i] {
t.Fatalf("expected items: %v\ngot items: %v", op.items, got)
}
}
if v, ok := r.Nth(len(op.items)); ok || v != zero {
t.Fatalf("expected no more items, got: v=%v; ok=%v", v, ok)
}
}
================================================
FILE: internal/spew/bypass.go
================================================
// Copyright (c) 2015-2016 Dave Collins
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is not running on Google App Engine, compiled by GopherJS, and
// "-tags safe" is not added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
// Go versions prior to 1.4 are disabled because they use a different layout
// for interfaces which make the implementation of unsafeReflectValue more complex.
//go:build !js && !appengine && !safe && !disableunsafe && go1.4
// +build !js,!appengine,!safe,!disableunsafe,go1.4
package spew
import (
"reflect"
"unsafe"
)
const (
// UnsafeDisabled is a build-time constant which specifies whether or
// not access to the unsafe package is available.
UnsafeDisabled = false
// ptrSize is the size of a pointer on the current arch.
ptrSize = unsafe.Sizeof((*byte)(nil))
)
type flag uintptr
var (
// flagRO indicates whether the value field of a reflect.Value
// is read-only.
flagRO flag
// flagAddr indicates whether the address of the reflect.Value's
// value may be taken.
flagAddr flag
)
// flagKindMask holds the bits that make up the kind
// part of the flags field. In all the supported versions,
// it is in the lower 5 bits.
const flagKindMask = flag(0x1f)
// Different versions of Go have used different
// bit layouts for the flags type. This table
// records the known combinations.
var okFlags = []struct {
ro, addr flag
}{{
// From Go 1.4 to 1.5
ro: 1 << 5,
addr: 1 << 7,
}, {
// Up to Go tip.
ro: 1<<5 | 1<<6,
addr: 1 << 8,
}}
var flagValOffset = func() uintptr {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
return field.Offset
}()
// flagField returns a pointer to the flag field of a reflect.Value.
func flagField(v *reflect.Value) *flag {
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
}
// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
// the typical safety restrictions preventing access to unaddressable and
// unexported data. It works by digging the raw pointer to the underlying
// value out of the protected value and generating a new unprotected (unsafe)
// reflect.Value to it.
//
// This allows us to check for implementations of the Stringer and error
// interfaces to be used for pretty printing ordinarily unaddressable and
// inaccessible values such as unexported struct fields.
func unsafeReflectValue(v reflect.Value) reflect.Value {
if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
return v
}
flagFieldPtr := flagField(&v)
*flagFieldPtr &^= flagRO
*flagFieldPtr |= flagAddr
return v
}
// Sanity checks against future reflect package changes
// to the type or semantics of the Value.flag field.
func init() {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
panic("reflect.Value flag field has changed kind")
}
type t0 int
var t struct {
A t0
// t0 will have flagEmbedRO set.
t0
// a will have flagStickyRO set
a t0
}
vA := reflect.ValueOf(t).FieldByName("A")
va := reflect.ValueOf(t).FieldByName("a")
vt0 := reflect.ValueOf(t).FieldByName("t0")
// Infer flagRO from the difference between the flags
// for the (otherwise identical) fields in t.
flagPublic := *flagField(&vA)
flagWithRO := *flagField(&va) | *flagField(&vt0)
flagRO = flagPublic ^ flagWithRO
// Infer flagAddr from the difference between a value
// taken from a pointer and not.
vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A")
flagNoPtr := *flagField(&vA)
flagPtr := *flagField(&vPtrA)
flagAddr = flagNoPtr ^ flagPtr
// Check that the inferred flags tally with one of the known versions.
for _, f := range okFlags {
if flagRO == f.ro && flagAddr == f.addr {
return
}
}
panic("reflect.Value read-only flag has changed semantics")
}
================================================
FILE: internal/spew/bypasssafe.go
================================================
// Copyright (c) 2015-2016 Dave Collins
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when the code is running on Google App Engine, compiled by GopherJS, or
// "-tags safe" is added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
//go:build js || appengine || safe || disableunsafe || !go1.4
// +build js appengine safe disableunsafe !go1.4
package spew
import "reflect"
const (
// UnsafeDisabled is a build-time constant which specifies whether or
// not access to the unsafe package is available.
UnsafeDisabled = true
)
// unsafeReflectValue typically converts the passed reflect.Value into a one
// that bypasses the typical safety restrictions preventing access to
// unaddressable and unexported data. However, doing this relies on access to
// the unsafe package. This is a stub version which simply returns the passed
// reflect.Value when the unsafe package is not available.
func unsafeReflectValue(v reflect.Value) reflect.Value {
return v
}
================================================
FILE: internal/spew/common.go
================================================
/*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"fmt"
"io"
"reflect"
"sort"
"strconv"
)
// Some constants in the form of bytes to avoid string overhead. This mirrors
// the technique used in the fmt package.
var (
panicBytes = []byte("(PANIC=")
plusBytes = []byte("+")
iBytes = []byte("i")
trueBytes = []byte("true")
falseBytes = []byte("false")
interfaceBytes = []byte("(interface {})")
commaNewlineBytes = []byte(",\n")
newlineBytes = []byte("\n")
openBraceBytes = []byte("{")
openBraceNewlineBytes = []byte("{\n")
closeBraceBytes = []byte("}")
asteriskBytes = []byte("*")
colonBytes = []byte(":")
colonSpaceBytes = []byte(": ")
openParenBytes = []byte("(")
closeParenBytes = []byte(")")
spaceBytes = []byte(" ")
pointerChainBytes = []byte("->")
nilAngleBytes = []byte("")
maxNewlineBytes = []byte("\n")
maxShortBytes = []byte("")
circularBytes = []byte("")
circularShortBytes = []byte("")
invalidAngleBytes = []byte("")
openBracketBytes = []byte("[")
closeBracketBytes = []byte("]")
percentBytes = []byte("%")
precisionBytes = []byte(".")
openAngleBytes = []byte("<")
closeAngleBytes = []byte(">")
openMapBytes = []byte("map[")
closeMapBytes = []byte("]")
lenEqualsBytes = []byte("len=")
capEqualsBytes = []byte("cap=")
)
// hexDigits is used to map a decimal value to a hex digit.
var hexDigits = "0123456789abcdef"
// catchPanic handles any panics that might occur during the handleMethods
// calls.
func catchPanic(w io.Writer, v reflect.Value) {
if err := recover(); err != nil {
w.Write(panicBytes)
fmt.Fprintf(w, "%v", err)
w.Write(closeParenBytes)
}
}
// handleMethods attempts to call the Error and String methods on the underlying
// type the passed reflect.Value represents and outputs the result to Writer w.
//
// It handles panics in any called methods by catching and displaying the error
// as the formatted value.
func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) {
// We need an interface to check if the type implements the error or
// Stringer interface. However, the reflect package won't give us an
// interface on certain things like unexported struct fields in order
// to enforce visibility rules. We use unsafe, when it's available,
// to bypass these restrictions since this package does not mutate the
// values.
if !v.CanInterface() {
if UnsafeDisabled {
return false
}
v = unsafeReflectValue(v)
}
// Choose whether or not to do error and Stringer interface lookups against
// the base type or a pointer to the base type depending on settings.
// Technically calling one of these methods with a pointer receiver can
// mutate the value, however, types which choose to satisfy an error or
// Stringer interface with a pointer receiver should not be mutating their
// state inside these interface methods.
if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
v = unsafeReflectValue(v)
}
if v.CanAddr() {
v = v.Addr()
}
// Is it an error or Stringer?
switch iface := v.Interface().(type) {
case error:
defer catchPanic(w, v)
if cs.ContinueOnMethod {
w.Write(openParenBytes)
w.Write([]byte(iface.Error()))
w.Write(closeParenBytes)
w.Write(spaceBytes)
return false
}
w.Write([]byte(iface.Error()))
return true
case fmt.Stringer:
defer catchPanic(w, v)
if cs.ContinueOnMethod {
w.Write(openParenBytes)
w.Write([]byte(iface.String()))
w.Write(closeParenBytes)
w.Write(spaceBytes)
return false
}
w.Write([]byte(iface.String()))
return true
}
return false
}
// printBool outputs a boolean value as true or false to Writer w.
func printBool(w io.Writer, val bool) {
if val {
w.Write(trueBytes)
} else {
w.Write(falseBytes)
}
}
// printInt outputs a signed integer value to Writer w.
func printInt(w io.Writer, val int64, base int) {
w.Write([]byte(strconv.FormatInt(val, base)))
}
// printUint outputs an unsigned integer value to Writer w.
func printUint(w io.Writer, val uint64, base int) {
w.Write([]byte(strconv.FormatUint(val, base)))
}
// printFloat outputs a floating point value using the specified precision,
// which is expected to be 32 or 64bit, to Writer w.
func printFloat(w io.Writer, val float64, precision int) {
w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision)))
}
// printComplex outputs a complex value using the specified float precision
// for the real and imaginary parts to Writer w.
func printComplex(w io.Writer, c complex128, floatPrecision int) {
r := real(c)
w.Write(openParenBytes)
w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision)))
i := imag(c)
if i >= 0 {
w.Write(plusBytes)
}
w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision)))
w.Write(iBytes)
w.Write(closeParenBytes)
}
// printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
// prefix to Writer w.
func printHexPtr(w io.Writer, p uintptr) {
// Null pointer.
num := uint64(p)
if num == 0 {
w.Write(nilAngleBytes)
return
}
// Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix
buf := make([]byte, 18)
// It's simpler to construct the hex string right to left.
base := uint64(16)
i := len(buf) - 1
for num >= base {
buf[i] = hexDigits[num%base]
num /= base
i--
}
buf[i] = hexDigits[num]
// Add '0x' prefix.
i--
buf[i] = 'x'
i--
buf[i] = '0'
// Strip unused leading bytes.
buf = buf[i:]
w.Write(buf)
}
// valuesSorter implements sort.Interface to allow a slice of reflect.Value
// elements to be sorted.
type valuesSorter struct {
values []reflect.Value
strings []string // either nil or same len and values
cs *ConfigState
}
// newValuesSorter initializes a valuesSorter instance, which holds a set of
// surrogate keys on which the data should be sorted. It uses flags in
// ConfigState to decide if and how to populate those surrogate keys.
func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
vs := &valuesSorter{values: values, cs: cs}
if canSortSimply(vs.values[0].Kind()) {
return vs
}
if !cs.DisableMethods {
vs.strings = make([]string, len(values))
for i := range vs.values {
b := bytes.Buffer{}
if !handleMethods(cs, &b, vs.values[i]) {
vs.strings = nil
break
}
vs.strings[i] = b.String()
}
}
if vs.strings == nil && cs.SpewKeys {
vs.strings = make([]string, len(values))
for i := range vs.values {
vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
}
}
return vs
}
// canSortSimply tests whether a reflect.Kind is a primitive that can be sorted
// directly, or whether it should be considered for sorting by surrogate keys
// (if the ConfigState allows it).
func canSortSimply(kind reflect.Kind) bool {
// This switch parallels valueSortLess, except for the default case.
switch kind {
case reflect.Bool:
return true
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return true
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
return true
case reflect.Float32, reflect.Float64:
return true
case reflect.String:
return true
case reflect.Uintptr:
return true
case reflect.Array:
return true
}
return false
}
// Len returns the number of values in the slice. It is part of the
// sort.Interface implementation.
func (s *valuesSorter) Len() int {
return len(s.values)
}
// Swap swaps the values at the passed indices. It is part of the
// sort.Interface implementation.
func (s *valuesSorter) Swap(i, j int) {
s.values[i], s.values[j] = s.values[j], s.values[i]
if s.strings != nil {
s.strings[i], s.strings[j] = s.strings[j], s.strings[i]
}
}
// valueSortLess returns whether the first value should sort before the second
// value. It is used by valueSorter.Less as part of the sort.Interface
// implementation.
func valueSortLess(a, b reflect.Value) bool {
switch a.Kind() {
case reflect.Bool:
return !a.Bool() && b.Bool()
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
return a.Int() < b.Int()
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
return a.Uint() < b.Uint()
case reflect.Float32, reflect.Float64:
return a.Float() < b.Float()
case reflect.String:
return a.String() < b.String()
case reflect.Uintptr:
return a.Uint() < b.Uint()
case reflect.Array:
// Compare the contents of both arrays.
l := a.Len()
for i := 0; i < l; i++ {
av := a.Index(i)
bv := b.Index(i)
if av.Interface() == bv.Interface() {
continue
}
return valueSortLess(av, bv)
}
}
return a.String() < b.String()
}
// Less returns whether the value at index i should sort before the
// value at index j. It is part of the sort.Interface implementation.
func (s *valuesSorter) Less(i, j int) bool {
if s.strings == nil {
return valueSortLess(s.values[i], s.values[j])
}
return s.strings[i] < s.strings[j]
}
// sortValues is a sort function that handles both native types and any type that
// can be converted to error or Stringer. Other inputs are sorted according to
// their Value.String() value to ensure display stability.
func sortValues(values []reflect.Value, cs *ConfigState) {
if len(values) == 0 {
return
}
sort.Sort(newValuesSorter(values, cs))
}
================================================
FILE: internal/spew/common_test.go
================================================
/*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew_test
import (
"fmt"
"reflect"
"testing"
"github.com/expr-lang/expr/internal/spew"
)
// custom type to test Stinger interface on non-pointer receiver.
type stringer string
// String implements the Stringer interface for testing invocation of custom
// stringers on types with non-pointer receivers.
func (s stringer) String() string {
return "stringer " + string(s)
}
// custom type to test Stinger interface on pointer receiver.
type pstringer string
// String implements the Stringer interface for testing invocation of custom
// stringers on types with only pointer receivers.
func (s *pstringer) String() string {
return "stringer " + string(*s)
}
// xref1 and xref2 are cross referencing structs for testing circular reference
// detection.
type xref1 struct {
ps2 *xref2
}
type xref2 struct {
ps1 *xref1
}
// indirCir1, indirCir2, and indirCir3 are used to generate an indirect circular
// reference for testing detection.
type indirCir1 struct {
ps2 *indirCir2
}
type indirCir2 struct {
ps3 *indirCir3
}
type indirCir3 struct {
ps1 *indirCir1
}
// embed is used to test embedded structures.
type embed struct {
a string
}
// embedwrap is used to test embedded structures.
type embedwrap struct {
*embed
e *embed
}
// panicer is used to intentionally cause a panic for testing spew properly
// handles them
type panicer int
func (p panicer) String() string {
panic("test panic")
}
// customError is used to test custom error interface invocation.
type customError int
func (e customError) Error() string {
return fmt.Sprintf("error: %d", int(e))
}
// stringizeWants converts a slice of wanted test output into a format suitable
// for a test error message.
func stringizeWants(wants []string) string {
s := ""
for i, want := range wants {
if i > 0 {
s += fmt.Sprintf("want%d: %s", i+1, want)
} else {
s += "want: " + want
}
}
return s
}
// testFailed returns whether or not a test failed by checking if the result
// of the test is in the slice of wanted strings.
func testFailed(result string, wants []string) bool {
for _, want := range wants {
if result == want {
return false
}
}
return true
}
type sortableStruct struct {
x int
}
func (ss sortableStruct) String() string {
return fmt.Sprintf("ss.%d", ss.x)
}
type unsortableStruct struct {
x int
}
type sortTestCase struct {
input []reflect.Value
expected []reflect.Value
}
func helpTestSortValues(tests []sortTestCase, cs *spew.ConfigState, t *testing.T) {
getInterfaces := func(values []reflect.Value) []interface{} {
interfaces := []interface{}{}
for _, v := range values {
interfaces = append(interfaces, v.Interface())
}
return interfaces
}
for _, test := range tests {
spew.SortValues(test.input, cs)
// reflect.DeepEqual cannot really make sense of reflect.Value,
// probably because of all the pointer tricks. For instance,
// v(2.0) != v(2.0) on a 32-bits system. Turn them into interface{}
// instead.
input := getInterfaces(test.input)
expected := getInterfaces(test.expected)
if !reflect.DeepEqual(input, expected) {
t.Errorf("Sort mismatch:\n %v != %v", input, expected)
}
}
}
// TestSortValues ensures the sort functionality for reflect.Value based sorting
// works as intended.
func TestSortValues(t *testing.T) {
v := reflect.ValueOf
a := v("a")
b := v("b")
c := v("c")
embedA := v(embed{"a"})
embedB := v(embed{"b"})
embedC := v(embed{"c"})
tests := []sortTestCase{
// No values.
{
[]reflect.Value{},
[]reflect.Value{},
},
// Bools.
{
[]reflect.Value{v(false), v(true), v(false)},
[]reflect.Value{v(false), v(false), v(true)},
},
// Ints.
{
[]reflect.Value{v(2), v(1), v(3)},
[]reflect.Value{v(1), v(2), v(3)},
},
// Uints.
{
[]reflect.Value{v(uint8(2)), v(uint8(1)), v(uint8(3))},
[]reflect.Value{v(uint8(1)), v(uint8(2)), v(uint8(3))},
},
// Floats.
{
[]reflect.Value{v(2.0), v(1.0), v(3.0)},
[]reflect.Value{v(1.0), v(2.0), v(3.0)},
},
// Strings.
{
[]reflect.Value{b, a, c},
[]reflect.Value{a, b, c},
},
// Array
{
[]reflect.Value{v([3]int{3, 2, 1}), v([3]int{1, 3, 2}), v([3]int{1, 2, 3})},
[]reflect.Value{v([3]int{1, 2, 3}), v([3]int{1, 3, 2}), v([3]int{3, 2, 1})},
},
// Uintptrs.
{
[]reflect.Value{v(uintptr(2)), v(uintptr(1)), v(uintptr(3))},
[]reflect.Value{v(uintptr(1)), v(uintptr(2)), v(uintptr(3))},
},
// SortableStructs.
{
// Note: not sorted - DisableMethods is set.
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
},
// UnsortableStructs.
{
// Note: not sorted - SpewKeys is false.
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
},
// Invalid.
{
[]reflect.Value{embedB, embedA, embedC},
[]reflect.Value{embedB, embedA, embedC},
},
}
cs := spew.ConfigState{DisableMethods: true, SpewKeys: false}
helpTestSortValues(tests, &cs, t)
}
// TestSortValuesWithMethods ensures the sort functionality for reflect.Value
// based sorting works as intended when using string methods.
func TestSortValuesWithMethods(t *testing.T) {
v := reflect.ValueOf
a := v("a")
b := v("b")
c := v("c")
tests := []sortTestCase{
// Ints.
{
[]reflect.Value{v(2), v(1), v(3)},
[]reflect.Value{v(1), v(2), v(3)},
},
// Strings.
{
[]reflect.Value{b, a, c},
[]reflect.Value{a, b, c},
},
// SortableStructs.
{
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
[]reflect.Value{v(sortableStruct{1}), v(sortableStruct{2}), v(sortableStruct{3})},
},
// UnsortableStructs.
{
// Note: not sorted - SpewKeys is false.
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
},
}
cs := spew.ConfigState{DisableMethods: false, SpewKeys: false}
helpTestSortValues(tests, &cs, t)
}
// TestSortValuesWithSpew ensures the sort functionality for reflect.Value
// based sorting works as intended when using spew to stringify keys.
func TestSortValuesWithSpew(t *testing.T) {
v := reflect.ValueOf
a := v("a")
b := v("b")
c := v("c")
tests := []sortTestCase{
// Ints.
{
[]reflect.Value{v(2), v(1), v(3)},
[]reflect.Value{v(1), v(2), v(3)},
},
// Strings.
{
[]reflect.Value{b, a, c},
[]reflect.Value{a, b, c},
},
// SortableStructs.
{
[]reflect.Value{v(sortableStruct{2}), v(sortableStruct{1}), v(sortableStruct{3})},
[]reflect.Value{v(sortableStruct{1}), v(sortableStruct{2}), v(sortableStruct{3})},
},
// UnsortableStructs.
{
[]reflect.Value{v(unsortableStruct{2}), v(unsortableStruct{1}), v(unsortableStruct{3})},
[]reflect.Value{v(unsortableStruct{1}), v(unsortableStruct{2}), v(unsortableStruct{3})},
},
}
cs := spew.ConfigState{DisableMethods: true, SpewKeys: true}
helpTestSortValues(tests, &cs, t)
}
================================================
FILE: internal/spew/config.go
================================================
/*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"fmt"
"io"
"os"
)
// ConfigState houses the configuration options used by spew to format and
// display values. There is a global instance, Config, that is used to control
// all top-level Formatter and Dump functionality. Each ConfigState instance
// provides methods equivalent to the top-level functions.
//
// The zero value for ConfigState provides no indentation. You would typically
// want to set it to a space or a tab.
//
// Alternatively, you can use NewDefaultConfig to get a ConfigState instance
// with default settings. See the documentation of NewDefaultConfig for default
// values.
type ConfigState struct {
// Indent specifies the string to use for each indentation level. The
// global config instance that all top-level functions use set this to a
// single space by default. If you would like more indentation, you might
// set this to a tab with "\t" or perhaps two spaces with " ".
Indent string
// MaxDepth controls the maximum number of levels to descend into nested
// data structures. The default, 0, means there is no limit.
//
// NOTE: Circular data structures are properly detected, so it is not
// necessary to set this value unless you specifically want to limit deeply
// nested data structures.
MaxDepth int
// DisableMethods specifies whether or not error and Stringer interfaces are
// invoked for types that implement them.
DisableMethods bool
// DisablePointerMethods specifies whether or not to check for and invoke
// error and Stringer interfaces on types which only accept a pointer
// receiver when the current type is not a pointer.
//
// NOTE: This might be an unsafe action since calling one of these methods
// with a pointer receiver could technically mutate the value, however,
// in practice, types which choose to satisfy an error or Stringer
// interface with a pointer receiver should not be mutating their state
// inside these interface methods. As a result, this option relies on
// access to the unsafe package, so it will not have any effect when
// running in environments without access to the unsafe package such as
// Google App Engine or with the "safe" build tag specified.
DisablePointerMethods bool
// DisablePointerAddresses specifies whether to disable the printing of
// pointer addresses. This is useful when diffing data structures in tests.
DisablePointerAddresses bool
// DisableCapacities specifies whether to disable the printing of capacities
// for arrays, slices, maps and channels. This is useful when diffing
// data structures in tests.
DisableCapacities bool
// ContinueOnMethod specifies whether or not recursion should continue once
// a custom error or Stringer interface is invoked. The default, false,
// means it will print the results of invoking the custom error or Stringer
// interface and return immediately instead of continuing to recurse into
// the internals of the data type.
//
// NOTE: This flag does not have any effect if method invocation is disabled
// via the DisableMethods or DisablePointerMethods options.
ContinueOnMethod bool
// SortKeys specifies map keys should be sorted before being printed. Use
// this to have a more deterministic, diffable output. Note that only
// native types (bool, int, uint, floats, uintptr and string) and types
// that support the error or Stringer interfaces (if methods are
// enabled) are supported, with other types sorted according to the
// reflect.Value.String() output which guarantees display stability.
SortKeys bool
// SpewKeys specifies that, as a last resort attempt, map keys should
// be spewed to strings and sorted by those strings. This is only
// considered if SortKeys is true.
SpewKeys bool
}
// Config is the active configuration of the top-level functions.
// The configuration can be changed by modifying the contents of spew.Config.
var Config = ConfigState{Indent: " "}
// Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the formatted string as a value that satisfies error. See NewFormatter
// for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) {
return fmt.Errorf(format, c.convertArgs(a)...)
}
// Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprint(w, c.convertArgs(a)...)
}
// Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(w, format, c.convertArgs(a)...)
}
// Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
// passed with a Formatter interface returned by c.NewFormatter. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, c.convertArgs(a)...)
}
// Print is a wrapper for fmt.Print that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Print(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Print(a ...interface{}) (n int, err error) {
return fmt.Print(c.convertArgs(a)...)
}
// Printf is a wrapper for fmt.Printf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) {
return fmt.Printf(format, c.convertArgs(a)...)
}
// Println is a wrapper for fmt.Println that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the number of bytes written and any write error encountered. See
// NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Println(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Println(a ...interface{}) (n int, err error) {
return fmt.Println(c.convertArgs(a)...)
}
// Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprint(a ...interface{}) string {
return fmt.Sprint(c.convertArgs(a)...)
}
// Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
// passed with a Formatter interface returned by c.NewFormatter. It returns
// the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprintf(format string, a ...interface{}) string {
return fmt.Sprintf(format, c.convertArgs(a)...)
}
// Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
// were passed with a Formatter interface returned by c.NewFormatter. It
// returns the resulting string. See NewFormatter for formatting details.
//
// This function is shorthand for the following syntax:
//
// fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b))
func (c *ConfigState) Sprintln(a ...interface{}) string {
return fmt.Sprintln(c.convertArgs(a)...)
}
/*
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
interface. As a result, it integrates cleanly with standard fmt package
printing functions. The formatter is useful for inline printing of smaller data
types similar to the standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
Typically this function shouldn't be called directly. It is much easier to make
use of the custom formatter by calling one of the convenience functions such as
c.Printf, c.Println, or c.Printf.
*/
func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter {
return newFormatter(c, v)
}
// Fdump formats and displays the passed arguments to io.Writer w. It formats
// exactly the same as Dump.
func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) {
fdump(c, w, a...)
}
/*
Dump displays the passed parameters to standard out with newlines, customizable
indentation, and additional debug information such as complete types and all
pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt
package:
- Pointers are dereferenced and followed
- Circular data structures are detected and handled properly
- Custom Stringer/error interfaces are optionally invoked, including
on unexported types
- Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
- Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by modifying the public members
of c. See ConfigState for options documentation.
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
get the formatted result as a string.
*/
func (c *ConfigState) Dump(a ...interface{}) {
fdump(c, os.Stdout, a...)
}
// Sdump returns a string with the passed arguments formatted exactly the same
// as Dump.
func (c *ConfigState) Sdump(a ...interface{}) string {
var buf bytes.Buffer
fdump(c, &buf, a...)
return buf.String()
}
// convertArgs accepts a slice of arguments and returns a slice of the same
// length with each argument converted to a spew Formatter interface using
// the ConfigState associated with s.
func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) {
formatters = make([]interface{}, len(args))
for index, arg := range args {
formatters[index] = newFormatter(c, arg)
}
return formatters
}
// NewDefaultConfig returns a ConfigState with the following default settings.
//
// Indent: " "
// MaxDepth: 0
// DisableMethods: false
// DisablePointerMethods: false
// ContinueOnMethod: false
// SortKeys: false
func NewDefaultConfig() *ConfigState {
return &ConfigState{Indent: " "}
}
================================================
FILE: internal/spew/doc.go
================================================
/*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
Package spew implements a deep pretty printer for Go data structures to aid in
debugging.
A quick overview of the additional features spew provides over the built-in
printing facilities for Go data types are as follows:
- Pointers are dereferenced and followed
- Circular data structures are detected and handled properly
- Custom Stringer/error interfaces are optionally invoked, including
on unexported types
- Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
- Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output (only when using
Dump style)
There are two different approaches spew allows for dumping Go data structures:
- Dump style which prints with newlines, customizable indentation,
and additional debug information such as types and all pointer addresses
used to indirect to the final value
- A custom Formatter interface that integrates cleanly with the standard fmt
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
similar to the default %v while providing the additional functionality
outlined above and passing unsupported format verbs such as %x and %q
along to fmt
# Quick Start
This section demonstrates how to quickly get started with spew. See the
sections below for further details on formatting and configuration options.
To dump a variable with full newlines, indentation, type, and pointer
information use Dump, Fdump, or Sdump:
spew.Dump(myVar1, myVar2, ...)
spew.Fdump(someWriter, myVar1, myVar2, ...)
str := spew.Sdump(myVar1, myVar2, ...)
Alternatively, if you would prefer to use format strings with a compacted inline
printing style, use the convenience wrappers Printf, Fprintf, etc with
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
%#+v (adds types and pointer addresses):
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
# Configuration Options
Configuration of spew is handled by fields in the ConfigState type. For
convenience, all of the top-level functions use a global state available
via the spew.Config global.
It is also possible to create a ConfigState instance that provides methods
equivalent to the top-level functions. This allows concurrent configuration
options. See the ConfigState documentation for more details.
The following configuration options are available:
- Indent
String to use for each indentation level for Dump functions.
It is a single space by default. A popular alternative is "\t".
- MaxDepth
Maximum number of levels to descend into nested data structures.
There is no limit by default.
- DisableMethods
Disables invocation of error and Stringer interface methods.
Method invocation is enabled by default.
- DisablePointerMethods
Disables invocation of error and Stringer interface methods on types
which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default.
- DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
- DisableCapacities
DisableCapacities specifies whether to disable the printing of
capacities for arrays, slices, maps and channels. This is useful when
diffing data structures in tests.
- ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.
- SortKeys
Specifies map keys should be sorted before being printed. Use
this to have a more deterministic, diffable output. Note that
only native types (bool, int, uint, floats, uintptr and string)
and types which implement error or Stringer interfaces are
supported with other types sorted according to the
reflect.Value.String() output which guarantees display
stability. Natural map order is used by default.
- SpewKeys
Specifies that, as a last resort attempt, map keys should be
spewed to strings and sorted by those strings. This is only
considered if SortKeys is true.
# Dump Usage
Simply call spew.Dump with a list of variables you want to dump:
spew.Dump(myVar1, myVar2, ...)
You may also call spew.Fdump if you would prefer to output to an arbitrary
io.Writer. For example, to dump to standard error:
spew.Fdump(os.Stderr, myVar1, myVar2, ...)
A third option is to call spew.Sdump to get the formatted output as a string:
str := spew.Sdump(myVar1, myVar2, ...)
# Sample Dump Output
See the Dump example for details on the setup of the types and variables being
shown here.
(main.Foo) {
unexportedField: (*main.Bar)(0xf84002e210)({
flag: (main.Flag) flagTwo,
data: (uintptr)
}),
ExportedField: (map[interface {}]interface {}) (len=1) {
(string) (len=3) "one": (bool) true
}
}
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
command as shown.
([]uint8) (len=32 cap=32) {
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
00000020 31 32 |12|
}
# Custom Formatter
Spew provides a custom formatter that implements the fmt.Formatter interface
so that it integrates cleanly with standard fmt package printing functions. The
formatter is useful for inline printing of smaller data types similar to the
standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
# Custom Formatter Usage
The simplest way to make use of the spew custom formatter is to call one of the
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
functions have syntax you are most likely already familiar with:
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Println(myVar, myVar2)
spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
See the Index for the full list convenience functions.
# Sample Formatter Output
Double pointer to a uint8:
%v: <**>5
%+v: <**>(0xf8400420d0->0xf8400420c8)5
%#v: (**uint8)5
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
Pointer to circular struct with a uint8 field and a pointer to itself:
%v: <*>{1 <*>}
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)}
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)}
%#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)}
See the Printf example for details on the setup of variables being shown
here.
# Errors
Since it is possible for custom Stringer/error interfaces to panic, spew
detects them and handles them internally by printing the panic information
inline with the output. Since spew is intended to provide deep pretty printing
capabilities on structures, it intentionally does not return any errors.
*/
package spew
================================================
FILE: internal/spew/dump.go
================================================
/*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"encoding/hex"
"fmt"
"io"
"os"
"reflect"
"regexp"
"strconv"
"strings"
)
var (
// uint8Type is a reflect.Type representing a uint8. It is used to
// convert cgo types to uint8 slices for hexdumping.
uint8Type = reflect.TypeOf(uint8(0))
// cCharRE is a regular expression that matches a cgo char.
// It is used to detect character arrays to hexdump them.
cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`)
// cUnsignedCharRE is a regular expression that matches a cgo unsigned
// char. It is used to detect unsigned character arrays to hexdump
// them.
cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
// cUint8tCharRE is a regular expression that matches a cgo uint8_t.
// It is used to detect uint8_t arrays to hexdump them.
cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`)
)
// dumpState contains information about the state of a dump operation.
type dumpState struct {
w io.Writer
depth int
pointers map[uintptr]int
ignoreNextType bool
ignoreNextIndent bool
cs *ConfigState
}
// indent performs indentation according to the depth level and cs.Indent
// option.
func (d *dumpState) indent() {
if d.ignoreNextIndent {
d.ignoreNextIndent = false
return
}
d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth))
}
// unpackValue returns values inside of non-nil interfaces when possible.
// This is useful for data types like structs, arrays, slices, and maps which
// can contain varying types packed inside an interface.
func (d *dumpState) unpackValue(v reflect.Value) reflect.Value {
if v.Kind() == reflect.Interface && !v.IsNil() {
v = v.Elem()
}
return v
}
// dumpPtr handles formatting of pointers by indirecting them as necessary.
func (d *dumpState) dumpPtr(v reflect.Value) {
// Remove pointers at or below the current depth from map used to detect
// circular refs.
for k, depth := range d.pointers {
if depth >= d.depth {
delete(d.pointers, k)
}
}
// Keep list of all dereferenced pointers to show later.
pointerChain := make([]uintptr, 0)
// Figure out how many levels of indirection there are by dereferencing
// pointers and unpacking interfaces down the chain while detecting circular
// references.
nilFound := false
cycleFound := false
indirects := 0
ve := v
for ve.Kind() == reflect.Ptr {
if ve.IsNil() {
nilFound = true
break
}
indirects++
addr := ve.Pointer()
pointerChain = append(pointerChain, addr)
if pd, ok := d.pointers[addr]; ok && pd < d.depth {
cycleFound = true
indirects--
break
}
d.pointers[addr] = d.depth
ve = ve.Elem()
if ve.Kind() == reflect.Interface {
if ve.IsNil() {
nilFound = true
break
}
ve = ve.Elem()
}
}
// Display type information.
d.w.Write(openParenBytes)
d.w.Write(bytes.Repeat(asteriskBytes, indirects))
d.w.Write([]byte(ve.Type().String()))
d.w.Write(closeParenBytes)
// Display pointer information.
if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
d.w.Write(openParenBytes)
for i, addr := range pointerChain {
if i > 0 {
d.w.Write(pointerChainBytes)
}
printHexPtr(d.w, addr)
}
d.w.Write(closeParenBytes)
}
// Display dereferenced value.
d.w.Write(openParenBytes)
switch {
case nilFound:
d.w.Write(nilAngleBytes)
case cycleFound:
d.w.Write(circularBytes)
default:
d.ignoreNextType = true
d.dump(ve)
}
d.w.Write(closeParenBytes)
}
// dumpSlice handles formatting of arrays and slices. Byte (uint8 under
// reflection) arrays and slices are dumped in hexdump -C fashion.
func (d *dumpState) dumpSlice(v reflect.Value) {
// Determine whether this type should be hex dumped or not. Also,
// for types which should be hexdumped, try to use the underlying data
// first, then fall back to trying to convert them to a uint8 slice.
var buf []uint8
doConvert := false
doHexDump := false
numEntries := v.Len()
if numEntries > 0 {
vt := v.Index(0).Type()
vts := vt.String()
switch {
// C types that need to be converted.
case cCharRE.MatchString(vts):
fallthrough
case cUnsignedCharRE.MatchString(vts):
fallthrough
case cUint8tCharRE.MatchString(vts):
doConvert = true
// Try to use existing uint8 slices and fall back to converting
// and copying if that fails.
case vt.Kind() == reflect.Uint8:
// We need an addressable interface to convert the type
// to a byte slice. However, the reflect package won't
// give us an interface on certain things like
// unexported struct fields in order to enforce
// visibility rules. We use unsafe, when available, to
// bypass these restrictions since this package does not
// mutate the values.
vs := v
if !vs.CanInterface() || !vs.CanAddr() {
vs = unsafeReflectValue(vs)
}
if !UnsafeDisabled {
vs = vs.Slice(0, numEntries)
// Use the existing uint8 slice if it can be
// type asserted.
iface := vs.Interface()
if slice, ok := iface.([]uint8); ok {
buf = slice
doHexDump = true
break
}
}
// The underlying data needs to be converted if it can't
// be type asserted to a uint8 slice.
doConvert = true
}
// Copy and convert the underlying type if needed.
if doConvert && vt.ConvertibleTo(uint8Type) {
// Convert and copy each element into a uint8 byte
// slice.
buf = make([]uint8, numEntries)
for i := 0; i < numEntries; i++ {
vv := v.Index(i)
buf[i] = uint8(vv.Convert(uint8Type).Uint())
}
doHexDump = true
}
}
// Hexdump the entire slice as needed.
if doHexDump {
indent := strings.Repeat(d.cs.Indent, d.depth)
str := indent + hex.Dump(buf)
str = strings.Replace(str, "\n", "\n"+indent, -1)
str = strings.TrimRight(str, d.cs.Indent)
d.w.Write([]byte(str))
return
}
// Recursively call dump for each item.
for i := 0; i < numEntries; i++ {
d.dump(d.unpackValue(v.Index(i)))
if i < (numEntries - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
// dump is the main workhorse for dumping a value. It uses the passed reflect
// value to figure out what kind of object we are dealing with and formats it
// appropriately. It is a recursive function, however circular data structures
// are detected and handled properly.
func (d *dumpState) dump(v reflect.Value) {
// Handle invalid reflect values immediately.
kind := v.Kind()
if kind == reflect.Invalid {
d.w.Write(invalidAngleBytes)
return
}
// Handle pointers specially.
if kind == reflect.Ptr {
d.indent()
d.dumpPtr(v)
return
}
// Print type information unless already handled elsewhere.
if !d.ignoreNextType {
d.indent()
d.w.Write(openParenBytes)
d.w.Write([]byte(v.Type().String()))
d.w.Write(closeParenBytes)
d.w.Write(spaceBytes)
}
d.ignoreNextType = false
// Display length and capacity if the built-in len and cap functions
// work with the value's kind and the len/cap itself is non-zero.
valueLen, valueCap := 0, 0
switch v.Kind() {
case reflect.Array, reflect.Slice, reflect.Chan:
valueLen, valueCap = v.Len(), v.Cap()
case reflect.Map, reflect.String:
valueLen = v.Len()
}
if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
d.w.Write(openParenBytes)
if valueLen != 0 {
d.w.Write(lenEqualsBytes)
printInt(d.w, int64(valueLen), 10)
}
if !d.cs.DisableCapacities && valueCap != 0 {
if valueLen != 0 {
d.w.Write(spaceBytes)
}
d.w.Write(capEqualsBytes)
printInt(d.w, int64(valueCap), 10)
}
d.w.Write(closeParenBytes)
d.w.Write(spaceBytes)
}
// Call Stringer/error interfaces if they exist and the handle methods flag
// is enabled
if !d.cs.DisableMethods {
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
if handled := handleMethods(d.cs, d.w, v); handled {
return
}
}
}
switch kind {
case reflect.Invalid:
// Do nothing. We should never get here since invalid has already
// been handled above.
case reflect.Bool:
printBool(d.w, v.Bool())
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
printInt(d.w, v.Int(), 10)
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
printUint(d.w, v.Uint(), 10)
case reflect.Float32:
printFloat(d.w, v.Float(), 32)
case reflect.Float64:
printFloat(d.w, v.Float(), 64)
case reflect.Complex64:
printComplex(d.w, v.Complex(), 32)
case reflect.Complex128:
printComplex(d.w, v.Complex(), 64)
case reflect.Slice:
if v.IsNil() {
d.w.Write(nilAngleBytes)
break
}
fallthrough
case reflect.Array:
d.w.Write(openBraceNewlineBytes)
d.depth++
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
d.dumpSlice(v)
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.String:
d.w.Write([]byte(strconv.Quote(v.String())))
case reflect.Interface:
// The only time we should get here is for nil interfaces due to
// unpackValue calls.
if v.IsNil() {
d.w.Write(nilAngleBytes)
}
case reflect.Ptr:
// Do nothing. We should never get here since pointers have already
// been handled above.
case reflect.Map:
// nil maps should be indicated as different than empty maps
if v.IsNil() {
d.w.Write(nilAngleBytes)
break
}
d.w.Write(openBraceNewlineBytes)
d.depth++
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
numEntries := v.Len()
keys := v.MapKeys()
if d.cs.SortKeys {
sortValues(keys, d.cs)
}
for i, key := range keys {
d.dump(d.unpackValue(key))
d.w.Write(colonSpaceBytes)
d.ignoreNextIndent = true
d.dump(d.unpackValue(v.MapIndex(key)))
if i < (numEntries - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.Struct:
d.w.Write(openBraceNewlineBytes)
d.depth++
if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
d.indent()
d.w.Write(maxNewlineBytes)
} else {
vt := v.Type()
numFields := v.NumField()
for i := 0; i < numFields; i++ {
d.indent()
vtf := vt.Field(i)
d.w.Write([]byte(vtf.Name))
d.w.Write(colonSpaceBytes)
d.ignoreNextIndent = true
d.dump(d.unpackValue(v.Field(i)))
if i < (numFields - 1) {
d.w.Write(commaNewlineBytes)
} else {
d.w.Write(newlineBytes)
}
}
}
d.depth--
d.indent()
d.w.Write(closeBraceBytes)
case reflect.Uintptr:
printHexPtr(d.w, uintptr(v.Uint()))
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
printHexPtr(d.w, v.Pointer())
// There were not any other types at the time this code was written, but
// fall back to letting the default fmt package handle it in case any new
// types are added.
default:
if v.CanInterface() {
fmt.Fprintf(d.w, "%v", v.Interface())
} else {
fmt.Fprintf(d.w, "%v", v.String())
}
}
}
// fdump is a helper function to consolidate the logic from the various public
// methods which take varying writers and config states.
func fdump(cs *ConfigState, w io.Writer, a ...interface{}) {
for _, arg := range a {
if arg == nil {
w.Write(interfaceBytes)
w.Write(spaceBytes)
w.Write(nilAngleBytes)
w.Write(newlineBytes)
continue
}
d := dumpState{w: w, cs: cs}
d.pointers = make(map[uintptr]int)
d.dump(reflect.ValueOf(arg))
d.w.Write(newlineBytes)
}
}
// Fdump formats and displays the passed arguments to io.Writer w. It formats
// exactly the same as Dump.
func Fdump(w io.Writer, a ...interface{}) {
fdump(&Config, w, a...)
}
// Sdump returns a string with the passed arguments formatted exactly the same
// as Dump.
func Sdump(a ...interface{}) string {
var buf bytes.Buffer
fdump(&Config, &buf, a...)
return buf.String()
}
/*
Dump displays the passed parameters to standard out with newlines, customizable
indentation, and additional debug information such as complete types and all
pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt
package:
- Pointers are dereferenced and followed
- Circular data structures are detected and handled properly
- Custom Stringer/error interfaces are optionally invoked, including
on unexported types
- Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
- Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by an exported package global,
spew.Config. See ConfigState for options documentation.
See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
get the formatted result as a string.
*/
func Dump(a ...interface{}) {
fdump(&Config, os.Stdout, a...)
}
================================================
FILE: internal/spew/dump_test.go
================================================
/*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
Test Summary:
NOTE: For each test, a nil pointer, a single pointer and double pointer to the
base test element are also tested to ensure proper indirection across all types.
- Max int8, int16, int32, int64, int
- Max uint8, uint16, uint32, uint64, uint
- Boolean true and false
- Standard complex64 and complex128
- Array containing standard ints
- Array containing type with custom formatter on pointer receiver only
- Array containing interfaces
- Array containing bytes
- Slice containing standard float32 values
- Slice containing type with custom formatter on pointer receiver only
- Slice containing interfaces
- Slice containing bytes
- Nil slice
- Standard string
- Nil interface
- Sub-interface
- Map with string keys and int vals
- Map with custom formatter type on pointer receiver only keys and vals
- Map with interface keys and values
- Map with nil interface value
- Struct with primitives
- Struct that contains another struct
- Struct that contains custom type with Stringer pointer interface via both
exported and unexported fields
- Struct that contains embedded struct and field to same struct
- Uintptr to 0 (null pointer)
- Uintptr address of real variable
- Unsafe.Pointer to 0 (null pointer)
- Unsafe.Pointer to address of real variable
- Nil channel
- Standard int channel
- Function with no params and no returns
- Function with param and no returns
- Function with multiple params and multiple returns
- Struct that is circular through self referencing
- Structs that are circular through cross referencing
- Structs that are indirectly circular
- Type that panics in its Stringer interface
*/
package spew_test
import (
"bytes"
"fmt"
"testing"
"unsafe"
"github.com/expr-lang/expr/internal/spew"
)
// dumpTest is used to describe a test to be performed against the Dump method.
type dumpTest struct {
in interface{}
wants []string
}
// dumpTests houses all of the tests to be performed against the Dump method.
var dumpTests = make([]dumpTest, 0)
// addDumpTest is a helper method to append the passed input and desired result
// to dumpTests
func addDumpTest(in interface{}, wants ...string) {
test := dumpTest{in, wants}
dumpTests = append(dumpTests, test)
}
func addIntDumpTests() {
// Max int8.
v := int8(127)
nv := (*int8)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "int8"
vs := "127"
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
// Max int16.
v2 := int16(32767)
nv2 := (*int16)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "int16"
v2s := "32767"
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n")
addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n")
addDumpTest(nv2, "(*"+v2t+")()\n")
// Max int32.
v3 := int32(2147483647)
nv3 := (*int32)(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "int32"
v3s := "2147483647"
addDumpTest(v3, "("+v3t+") "+v3s+"\n")
addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")\n")
addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")\n")
addDumpTest(nv3, "(*"+v3t+")()\n")
// Max int64.
v4 := int64(9223372036854775807)
nv4 := (*int64)(nil)
pv4 := &v4
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "int64"
v4s := "9223372036854775807"
addDumpTest(v4, "("+v4t+") "+v4s+"\n")
addDumpTest(pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")\n")
addDumpTest(&pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")\n")
addDumpTest(nv4, "(*"+v4t+")()\n")
// Max int.
v5 := int(2147483647)
nv5 := (*int)(nil)
pv5 := &v5
v5Addr := fmt.Sprintf("%p", pv5)
pv5Addr := fmt.Sprintf("%p", &pv5)
v5t := "int"
v5s := "2147483647"
addDumpTest(v5, "("+v5t+") "+v5s+"\n")
addDumpTest(pv5, "(*"+v5t+")("+v5Addr+")("+v5s+")\n")
addDumpTest(&pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")("+v5s+")\n")
addDumpTest(nv5, "(*"+v5t+")()\n")
}
func addUintDumpTests() {
// Max uint8.
v := uint8(255)
nv := (*uint8)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "uint8"
vs := "255"
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
// Max uint16.
v2 := uint16(65535)
nv2 := (*uint16)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "uint16"
v2s := "65535"
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n")
addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n")
addDumpTest(nv2, "(*"+v2t+")()\n")
// Max uint32.
v3 := uint32(4294967295)
nv3 := (*uint32)(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "uint32"
v3s := "4294967295"
addDumpTest(v3, "("+v3t+") "+v3s+"\n")
addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")\n")
addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")\n")
addDumpTest(nv3, "(*"+v3t+")()\n")
// Max uint64.
v4 := uint64(18446744073709551615)
nv4 := (*uint64)(nil)
pv4 := &v4
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "uint64"
v4s := "18446744073709551615"
addDumpTest(v4, "("+v4t+") "+v4s+"\n")
addDumpTest(pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")\n")
addDumpTest(&pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")\n")
addDumpTest(nv4, "(*"+v4t+")()\n")
// Max uint.
v5 := uint(4294967295)
nv5 := (*uint)(nil)
pv5 := &v5
v5Addr := fmt.Sprintf("%p", pv5)
pv5Addr := fmt.Sprintf("%p", &pv5)
v5t := "uint"
v5s := "4294967295"
addDumpTest(v5, "("+v5t+") "+v5s+"\n")
addDumpTest(pv5, "(*"+v5t+")("+v5Addr+")("+v5s+")\n")
addDumpTest(&pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")("+v5s+")\n")
addDumpTest(nv5, "(*"+v5t+")()\n")
}
func addBoolDumpTests() {
// Boolean true.
v := bool(true)
nv := (*bool)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "bool"
vs := "true"
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
// Boolean false.
v2 := bool(false)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "bool"
v2s := "false"
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n")
addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n")
}
func addFloatDumpTests() {
// Standard float32.
v := float32(3.1415)
nv := (*float32)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "float32"
vs := "3.1415"
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
// Standard float64.
v2 := float64(3.1415926)
nv2 := (*float64)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "float64"
v2s := "3.1415926"
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n")
addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n")
addDumpTest(nv2, "(*"+v2t+")()\n")
}
func addComplexDumpTests() {
// Standard complex64.
v := complex(float32(6), -2)
nv := (*complex64)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "complex64"
vs := "(6-2i)"
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
// Standard complex128.
v2 := complex(float64(-6), 2)
nv2 := (*complex128)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "complex128"
v2s := "(-6+2i)"
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n")
addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n")
addDumpTest(nv2, "(*"+v2t+")()\n")
}
func addArrayDumpTests() {
// Array containing standard ints.
v := [3]int{1, 2, 3}
vLen := fmt.Sprintf("%d", len(v))
vCap := fmt.Sprintf("%d", cap(v))
nv := (*[3]int)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "int"
vs := "(len=" + vLen + " cap=" + vCap + ") {\n (" + vt + ") 1,\n (" +
vt + ") 2,\n (" + vt + ") 3\n}"
addDumpTest(v, "([3]"+vt+") "+vs+"\n")
addDumpTest(pv, "(*[3]"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**[3]"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*[3]"+vt+")()\n")
// Array containing type with custom formatter on pointer receiver only.
v2i0 := pstringer("1")
v2i1 := pstringer("2")
v2i2 := pstringer("3")
v2 := [3]pstringer{v2i0, v2i1, v2i2}
v2i0Len := fmt.Sprintf("%d", len(v2i0))
v2i1Len := fmt.Sprintf("%d", len(v2i1))
v2i2Len := fmt.Sprintf("%d", len(v2i2))
v2Len := fmt.Sprintf("%d", len(v2))
v2Cap := fmt.Sprintf("%d", cap(v2))
nv2 := (*[3]pstringer)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "spew_test.pstringer"
v2sp := "(len=" + v2Len + " cap=" + v2Cap + ") {\n (" + v2t +
") (len=" + v2i0Len + ") stringer 1,\n (" + v2t +
") (len=" + v2i1Len + ") stringer 2,\n (" + v2t +
") (len=" + v2i2Len + ") " + "stringer 3\n}"
v2s := v2sp
if spew.UnsafeDisabled {
v2s = "(len=" + v2Len + " cap=" + v2Cap + ") {\n (" + v2t +
") (len=" + v2i0Len + ") \"1\",\n (" + v2t + ") (len=" +
v2i1Len + ") \"2\",\n (" + v2t + ") (len=" + v2i2Len +
") " + "\"3\"\n}"
}
addDumpTest(v2, "([3]"+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*[3]"+v2t+")("+v2Addr+")("+v2sp+")\n")
addDumpTest(&pv2, "(**[3]"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2sp+")\n")
addDumpTest(nv2, "(*[3]"+v2t+")()\n")
// Array containing interfaces.
v3i0 := "one"
v3 := [3]interface{}{v3i0, int(2), uint(3)}
v3i0Len := fmt.Sprintf("%d", len(v3i0))
v3Len := fmt.Sprintf("%d", len(v3))
v3Cap := fmt.Sprintf("%d", cap(v3))
nv3 := (*[3]interface{})(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "[3]interface {}"
v3t2 := "string"
v3t3 := "int"
v3t4 := "uint"
v3s := "(len=" + v3Len + " cap=" + v3Cap + ") {\n (" + v3t2 + ") " +
"(len=" + v3i0Len + ") \"one\",\n (" + v3t3 + ") 2,\n (" +
v3t4 + ") 3\n}"
addDumpTest(v3, "("+v3t+") "+v3s+"\n")
addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")\n")
addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")\n")
addDumpTest(nv3, "(*"+v3t+")()\n")
// Array containing bytes.
v4 := [34]byte{
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
0x31, 0x32,
}
v4Len := fmt.Sprintf("%d", len(v4))
v4Cap := fmt.Sprintf("%d", cap(v4))
nv4 := (*[34]byte)(nil)
pv4 := &v4
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "[34]uint8"
v4s := "(len=" + v4Len + " cap=" + v4Cap + ") " +
"{\n 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20" +
" |............... |\n" +
" 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30" +
" |!\"#$%&'()*+,-./0|\n" +
" 00000020 31 32 " +
" |12|\n}"
addDumpTest(v4, "("+v4t+") "+v4s+"\n")
addDumpTest(pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")\n")
addDumpTest(&pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")\n")
addDumpTest(nv4, "(*"+v4t+")()\n")
}
func addSliceDumpTests() {
// Slice containing standard float32 values.
v := []float32{3.14, 6.28, 12.56}
vLen := fmt.Sprintf("%d", len(v))
vCap := fmt.Sprintf("%d", cap(v))
nv := (*[]float32)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "float32"
vs := "(len=" + vLen + " cap=" + vCap + ") {\n (" + vt + ") 3.14,\n (" +
vt + ") 6.28,\n (" + vt + ") 12.56\n}"
addDumpTest(v, "([]"+vt+") "+vs+"\n")
addDumpTest(pv, "(*[]"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**[]"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*[]"+vt+")()\n")
// Slice containing type with custom formatter on pointer receiver only.
v2i0 := pstringer("1")
v2i1 := pstringer("2")
v2i2 := pstringer("3")
v2 := []pstringer{v2i0, v2i1, v2i2}
v2i0Len := fmt.Sprintf("%d", len(v2i0))
v2i1Len := fmt.Sprintf("%d", len(v2i1))
v2i2Len := fmt.Sprintf("%d", len(v2i2))
v2Len := fmt.Sprintf("%d", len(v2))
v2Cap := fmt.Sprintf("%d", cap(v2))
nv2 := (*[]pstringer)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "spew_test.pstringer"
v2s := "(len=" + v2Len + " cap=" + v2Cap + ") {\n (" + v2t + ") (len=" +
v2i0Len + ") stringer 1,\n (" + v2t + ") (len=" + v2i1Len +
") stringer 2,\n (" + v2t + ") (len=" + v2i2Len + ") " +
"stringer 3\n}"
addDumpTest(v2, "([]"+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*[]"+v2t+")("+v2Addr+")("+v2s+")\n")
addDumpTest(&pv2, "(**[]"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n")
addDumpTest(nv2, "(*[]"+v2t+")()\n")
// Slice containing interfaces.
v3i0 := "one"
v3 := []interface{}{v3i0, int(2), uint(3), nil}
v3i0Len := fmt.Sprintf("%d", len(v3i0))
v3Len := fmt.Sprintf("%d", len(v3))
v3Cap := fmt.Sprintf("%d", cap(v3))
nv3 := (*[]interface{})(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "[]interface {}"
v3t2 := "string"
v3t3 := "int"
v3t4 := "uint"
v3t5 := "interface {}"
v3s := "(len=" + v3Len + " cap=" + v3Cap + ") {\n (" + v3t2 + ") " +
"(len=" + v3i0Len + ") \"one\",\n (" + v3t3 + ") 2,\n (" +
v3t4 + ") 3,\n (" + v3t5 + ") \n}"
addDumpTest(v3, "("+v3t+") "+v3s+"\n")
addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")\n")
addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")\n")
addDumpTest(nv3, "(*"+v3t+")()\n")
// Slice containing bytes.
v4 := []byte{
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
0x31, 0x32,
}
v4Len := fmt.Sprintf("%d", len(v4))
v4Cap := fmt.Sprintf("%d", cap(v4))
nv4 := (*[]byte)(nil)
pv4 := &v4
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "[]uint8"
v4s := "(len=" + v4Len + " cap=" + v4Cap + ") " +
"{\n 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20" +
" |............... |\n" +
" 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30" +
" |!\"#$%&'()*+,-./0|\n" +
" 00000020 31 32 " +
" |12|\n}"
addDumpTest(v4, "("+v4t+") "+v4s+"\n")
addDumpTest(pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")\n")
addDumpTest(&pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")\n")
addDumpTest(nv4, "(*"+v4t+")()\n")
// Nil slice.
v5 := []int(nil)
nv5 := (*[]int)(nil)
pv5 := &v5
v5Addr := fmt.Sprintf("%p", pv5)
pv5Addr := fmt.Sprintf("%p", &pv5)
v5t := "[]int"
v5s := ""
addDumpTest(v5, "("+v5t+") "+v5s+"\n")
addDumpTest(pv5, "(*"+v5t+")("+v5Addr+")("+v5s+")\n")
addDumpTest(&pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")("+v5s+")\n")
addDumpTest(nv5, "(*"+v5t+")()\n")
}
func addStringDumpTests() {
// Standard string.
v := "test"
vLen := fmt.Sprintf("%d", len(v))
nv := (*string)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "string"
vs := "(len=" + vLen + ") \"test\""
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
}
func addInterfaceDumpTests() {
// Nil interface.
var v interface{}
nv := (*interface{})(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "interface {}"
vs := ""
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
// Sub-interface.
v2 := interface{}(uint16(65535))
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "uint16"
v2s := "65535"
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n")
addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n")
}
func addMapDumpTests() {
// Map with string keys and int vals.
k := "one"
kk := "two"
m := map[string]int{k: 1, kk: 2}
klen := fmt.Sprintf("%d", len(k)) // not kLen to shut golint up
kkLen := fmt.Sprintf("%d", len(kk))
mLen := fmt.Sprintf("%d", len(m))
nilMap := map[string]int(nil)
nm := (*map[string]int)(nil)
pm := &m
mAddr := fmt.Sprintf("%p", pm)
pmAddr := fmt.Sprintf("%p", &pm)
mt := "map[string]int"
mt1 := "string"
mt2 := "int"
ms := "(len=" + mLen + ") {\n (" + mt1 + ") (len=" + klen + ") " +
"\"one\": (" + mt2 + ") 1,\n (" + mt1 + ") (len=" + kkLen +
") \"two\": (" + mt2 + ") 2\n}"
ms2 := "(len=" + mLen + ") {\n (" + mt1 + ") (len=" + kkLen + ") " +
"\"two\": (" + mt2 + ") 2,\n (" + mt1 + ") (len=" + klen +
") \"one\": (" + mt2 + ") 1\n}"
addDumpTest(m, "("+mt+") "+ms+"\n", "("+mt+") "+ms2+"\n")
addDumpTest(pm, "(*"+mt+")("+mAddr+")("+ms+")\n",
"(*"+mt+")("+mAddr+")("+ms2+")\n")
addDumpTest(&pm, "(**"+mt+")("+pmAddr+"->"+mAddr+")("+ms+")\n",
"(**"+mt+")("+pmAddr+"->"+mAddr+")("+ms2+")\n")
addDumpTest(nm, "(*"+mt+")()\n")
addDumpTest(nilMap, "("+mt+") \n")
// Map with custom formatter type on pointer receiver only keys and vals.
k2 := pstringer("one")
v2 := pstringer("1")
m2 := map[pstringer]pstringer{k2: v2}
k2Len := fmt.Sprintf("%d", len(k2))
v2Len := fmt.Sprintf("%d", len(v2))
m2Len := fmt.Sprintf("%d", len(m2))
nilMap2 := map[pstringer]pstringer(nil)
nm2 := (*map[pstringer]pstringer)(nil)
pm2 := &m2
m2Addr := fmt.Sprintf("%p", pm2)
pm2Addr := fmt.Sprintf("%p", &pm2)
m2t := "map[spew_test.pstringer]spew_test.pstringer"
m2t1 := "spew_test.pstringer"
m2t2 := "spew_test.pstringer"
m2s := "(len=" + m2Len + ") {\n (" + m2t1 + ") (len=" + k2Len + ") " +
"stringer one: (" + m2t2 + ") (len=" + v2Len + ") stringer 1\n}"
if spew.UnsafeDisabled {
m2s = "(len=" + m2Len + ") {\n (" + m2t1 + ") (len=" + k2Len +
") " + "\"one\": (" + m2t2 + ") (len=" + v2Len +
") \"1\"\n}"
}
addDumpTest(m2, "("+m2t+") "+m2s+"\n")
addDumpTest(pm2, "(*"+m2t+")("+m2Addr+")("+m2s+")\n")
addDumpTest(&pm2, "(**"+m2t+")("+pm2Addr+"->"+m2Addr+")("+m2s+")\n")
addDumpTest(nm2, "(*"+m2t+")()\n")
addDumpTest(nilMap2, "("+m2t+") \n")
// Map with interface keys and values.
k3 := "one"
k3Len := fmt.Sprintf("%d", len(k3))
m3 := map[interface{}]interface{}{k3: 1}
m3Len := fmt.Sprintf("%d", len(m3))
nilMap3 := map[interface{}]interface{}(nil)
nm3 := (*map[interface{}]interface{})(nil)
pm3 := &m3
m3Addr := fmt.Sprintf("%p", pm3)
pm3Addr := fmt.Sprintf("%p", &pm3)
m3t := "map[interface {}]interface {}"
m3t1 := "string"
m3t2 := "int"
m3s := "(len=" + m3Len + ") {\n (" + m3t1 + ") (len=" + k3Len + ") " +
"\"one\": (" + m3t2 + ") 1\n}"
addDumpTest(m3, "("+m3t+") "+m3s+"\n")
addDumpTest(pm3, "(*"+m3t+")("+m3Addr+")("+m3s+")\n")
addDumpTest(&pm3, "(**"+m3t+")("+pm3Addr+"->"+m3Addr+")("+m3s+")\n")
addDumpTest(nm3, "(*"+m3t+")()\n")
addDumpTest(nilMap3, "("+m3t+") \n")
// Map with nil interface value.
k4 := "nil"
k4Len := fmt.Sprintf("%d", len(k4))
m4 := map[string]interface{}{k4: nil}
m4Len := fmt.Sprintf("%d", len(m4))
nilMap4 := map[string]interface{}(nil)
nm4 := (*map[string]interface{})(nil)
pm4 := &m4
m4Addr := fmt.Sprintf("%p", pm4)
pm4Addr := fmt.Sprintf("%p", &pm4)
m4t := "map[string]interface {}"
m4t1 := "string"
m4t2 := "interface {}"
m4s := "(len=" + m4Len + ") {\n (" + m4t1 + ") (len=" + k4Len + ")" +
" \"nil\": (" + m4t2 + ") \n}"
addDumpTest(m4, "("+m4t+") "+m4s+"\n")
addDumpTest(pm4, "(*"+m4t+")("+m4Addr+")("+m4s+")\n")
addDumpTest(&pm4, "(**"+m4t+")("+pm4Addr+"->"+m4Addr+")("+m4s+")\n")
addDumpTest(nm4, "(*"+m4t+")()\n")
addDumpTest(nilMap4, "("+m4t+") \n")
}
func addStructDumpTests() {
// Struct with primitives.
type s1 struct {
a int8
b uint8
}
v := s1{127, 255}
nv := (*s1)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "spew_test.s1"
vt2 := "int8"
vt3 := "uint8"
vs := "{\n a: (" + vt2 + ") 127,\n b: (" + vt3 + ") 255\n}"
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
// Struct that contains another struct.
type s2 struct {
s1 s1
b bool
}
v2 := s2{s1{127, 255}, true}
nv2 := (*s2)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "spew_test.s2"
v2t2 := "spew_test.s1"
v2t3 := "int8"
v2t4 := "uint8"
v2t5 := "bool"
v2s := "{\n s1: (" + v2t2 + ") {\n a: (" + v2t3 + ") 127,\n b: (" +
v2t4 + ") 255\n },\n b: (" + v2t5 + ") true\n}"
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n")
addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n")
addDumpTest(nv2, "(*"+v2t+")()\n")
// Struct that contains custom type with Stringer pointer interface via both
// exported and unexported fields.
type s3 struct {
s pstringer
S pstringer
}
v3 := s3{"test", "test2"}
nv3 := (*s3)(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "spew_test.s3"
v3t2 := "spew_test.pstringer"
v3s := "{\n s: (" + v3t2 + ") (len=4) stringer test,\n S: (" + v3t2 +
") (len=5) stringer test2\n}"
v3sp := v3s
if spew.UnsafeDisabled {
v3s = "{\n s: (" + v3t2 + ") (len=4) \"test\",\n S: (" +
v3t2 + ") (len=5) \"test2\"\n}"
v3sp = "{\n s: (" + v3t2 + ") (len=4) \"test\",\n S: (" +
v3t2 + ") (len=5) stringer test2\n}"
}
addDumpTest(v3, "("+v3t+") "+v3s+"\n")
addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3sp+")\n")
addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3sp+")\n")
addDumpTest(nv3, "(*"+v3t+")()\n")
// Struct that contains embedded struct and field to same struct.
e := embed{"embedstr"}
eLen := fmt.Sprintf("%d", len("embedstr"))
v4 := embedwrap{embed: &e, e: &e}
nv4 := (*embedwrap)(nil)
pv4 := &v4
eAddr := fmt.Sprintf("%p", &e)
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "spew_test.embedwrap"
v4t2 := "spew_test.embed"
v4t3 := "string"
v4s := "{\n embed: (*" + v4t2 + ")(" + eAddr + ")({\n a: (" + v4t3 +
") (len=" + eLen + ") \"embedstr\"\n }),\n e: (*" + v4t2 +
")(" + eAddr + ")({\n a: (" + v4t3 + ") (len=" + eLen + ")" +
" \"embedstr\"\n })\n}"
addDumpTest(v4, "("+v4t+") "+v4s+"\n")
addDumpTest(pv4, "(*"+v4t+")("+v4Addr+")("+v4s+")\n")
addDumpTest(&pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")("+v4s+")\n")
addDumpTest(nv4, "(*"+v4t+")()\n")
}
func addUintptrDumpTests() {
// Null pointer.
v := uintptr(0)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "uintptr"
vs := ""
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
// Address of real variable.
i := 1
v2 := uintptr(unsafe.Pointer(&i))
nv2 := (*uintptr)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "uintptr"
v2s := fmt.Sprintf("%p", &i)
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n")
addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n")
addDumpTest(nv2, "(*"+v2t+")()\n")
}
func addUnsafePointerDumpTests() {
// Null pointer.
v := unsafe.Pointer(nil)
nv := (*unsafe.Pointer)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "unsafe.Pointer"
vs := ""
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
// Address of real variable.
i := 1
v2 := unsafe.Pointer(&i)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "unsafe.Pointer"
v2s := fmt.Sprintf("%p", &i)
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n")
addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
}
func addChanDumpTests() {
// Nil channel.
var v chan int
pv := &v
nv := (*chan int)(nil)
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "chan int"
vs := ""
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
// Real channel.
v2 := make(chan int)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "chan int"
v2s := fmt.Sprintf("%p", v2)
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n")
addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n")
}
func addFuncDumpTests() {
// Function with no params and no returns.
v := addIntDumpTests
nv := (*func())(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "func()"
vs := fmt.Sprintf("%p", v)
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
// Function with param and no returns.
v2 := TestDump
nv2 := (*func(*testing.T))(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "func(*testing.T)"
v2s := fmt.Sprintf("%p", v2)
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s+")\n")
addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s+")\n")
addDumpTest(nv2, "(*"+v2t+")()\n")
// Function with multiple params and multiple returns.
var v3 = func(i int, s string) (b bool, err error) {
return true, nil
}
nv3 := (*func(int, string) (bool, error))(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "func(int, string) (bool, error)"
v3s := fmt.Sprintf("%p", v3)
addDumpTest(v3, "("+v3t+") "+v3s+"\n")
addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3s+")\n")
addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s+")\n")
addDumpTest(nv3, "(*"+v3t+")()\n")
}
func addCircularDumpTests() {
// Struct that is circular through self referencing.
type circular struct {
c *circular
}
v := circular{nil}
v.c = &v
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "spew_test.circular"
vs := "{\n c: (*" + vt + ")(" + vAddr + ")({\n c: (*" + vt + ")(" +
vAddr + ")()\n })\n}"
vs2 := "{\n c: (*" + vt + ")(" + vAddr + ")()\n}"
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs2+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs2+")\n")
// Structs that are circular through cross referencing.
v2 := xref1{nil}
ts2 := xref2{&v2}
v2.ps2 = &ts2
pv2 := &v2
ts2Addr := fmt.Sprintf("%p", &ts2)
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "spew_test.xref1"
v2t2 := "spew_test.xref2"
v2s := "{\n ps2: (*" + v2t2 + ")(" + ts2Addr + ")({\n ps1: (*" + v2t +
")(" + v2Addr + ")({\n ps2: (*" + v2t2 + ")(" + ts2Addr +
")()\n })\n })\n}"
v2s2 := "{\n ps2: (*" + v2t2 + ")(" + ts2Addr + ")({\n ps1: (*" + v2t +
")(" + v2Addr + ")()\n })\n}"
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
addDumpTest(pv2, "(*"+v2t+")("+v2Addr+")("+v2s2+")\n")
addDumpTest(&pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")("+v2s2+")\n")
// Structs that are indirectly circular.
v3 := indirCir1{nil}
tic2 := indirCir2{nil}
tic3 := indirCir3{&v3}
tic2.ps3 = &tic3
v3.ps2 = &tic2
pv3 := &v3
tic2Addr := fmt.Sprintf("%p", &tic2)
tic3Addr := fmt.Sprintf("%p", &tic3)
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "spew_test.indirCir1"
v3t2 := "spew_test.indirCir2"
v3t3 := "spew_test.indirCir3"
v3s := "{\n ps2: (*" + v3t2 + ")(" + tic2Addr + ")({\n ps3: (*" + v3t3 +
")(" + tic3Addr + ")({\n ps1: (*" + v3t + ")(" + v3Addr +
")({\n ps2: (*" + v3t2 + ")(" + tic2Addr +
")()\n })\n })\n })\n}"
v3s2 := "{\n ps2: (*" + v3t2 + ")(" + tic2Addr + ")({\n ps3: (*" + v3t3 +
")(" + tic3Addr + ")({\n ps1: (*" + v3t + ")(" + v3Addr +
")()\n })\n })\n}"
addDumpTest(v3, "("+v3t+") "+v3s+"\n")
addDumpTest(pv3, "(*"+v3t+")("+v3Addr+")("+v3s2+")\n")
addDumpTest(&pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")("+v3s2+")\n")
}
func addPanicDumpTests() {
// Type that panics in its Stringer interface.
v := panicer(127)
nv := (*panicer)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "spew_test.panicer"
vs := "(PANIC=test panic)127"
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
}
func addErrorDumpTests() {
// Type that has a custom Error interface.
v := customError(127)
nv := (*customError)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "spew_test.customError"
vs := "error: 127"
addDumpTest(v, "("+vt+") "+vs+"\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")("+vs+")\n")
addDumpTest(nv, "(*"+vt+")()\n")
}
// TestDump executes all of the tests described by dumpTests.
func TestDump(t *testing.T) {
// Setup tests.
addIntDumpTests()
addUintDumpTests()
addBoolDumpTests()
addFloatDumpTests()
addComplexDumpTests()
addArrayDumpTests()
addSliceDumpTests()
addStringDumpTests()
addInterfaceDumpTests()
addMapDumpTests()
addStructDumpTests()
addUintptrDumpTests()
addUnsafePointerDumpTests()
addChanDumpTests()
addFuncDumpTests()
addCircularDumpTests()
addPanicDumpTests()
addErrorDumpTests()
addCgoDumpTests()
t.Logf("Running %d tests", len(dumpTests))
for i, test := range dumpTests {
buf := new(bytes.Buffer)
spew.Fdump(buf, test.in)
s := buf.String()
if testFailed(s, test.wants) {
t.Errorf("Dump #%d\n got: %s %s", i, s, stringizeWants(test.wants))
continue
}
}
}
func TestDumpSortedKeys(t *testing.T) {
cfg := spew.ConfigState{SortKeys: true}
s := cfg.Sdump(map[int]string{1: "1", 3: "3", 2: "2"})
expected := "(map[int]string) (len=3) {\n(int) 1: (string) (len=1) " +
"\"1\",\n(int) 2: (string) (len=1) \"2\",\n(int) 3: (string) " +
"(len=1) \"3\"\n" +
"}\n"
if s != expected {
t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
}
s = cfg.Sdump(map[stringer]int{"1": 1, "3": 3, "2": 2})
expected = "(map[spew_test.stringer]int) (len=3) {\n" +
"(spew_test.stringer) (len=1) stringer 1: (int) 1,\n" +
"(spew_test.stringer) (len=1) stringer 2: (int) 2,\n" +
"(spew_test.stringer) (len=1) stringer 3: (int) 3\n" +
"}\n"
if s != expected {
t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
}
s = cfg.Sdump(map[pstringer]int{pstringer("1"): 1, pstringer("3"): 3, pstringer("2"): 2})
expected = "(map[spew_test.pstringer]int) (len=3) {\n" +
"(spew_test.pstringer) (len=1) stringer 1: (int) 1,\n" +
"(spew_test.pstringer) (len=1) stringer 2: (int) 2,\n" +
"(spew_test.pstringer) (len=1) stringer 3: (int) 3\n" +
"}\n"
if spew.UnsafeDisabled {
expected = "(map[spew_test.pstringer]int) (len=3) {\n" +
"(spew_test.pstringer) (len=1) \"1\": (int) 1,\n" +
"(spew_test.pstringer) (len=1) \"2\": (int) 2,\n" +
"(spew_test.pstringer) (len=1) \"3\": (int) 3\n" +
"}\n"
}
if s != expected {
t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
}
s = cfg.Sdump(map[customError]int{customError(1): 1, customError(3): 3, customError(2): 2})
expected = "(map[spew_test.customError]int) (len=3) {\n" +
"(spew_test.customError) error: 1: (int) 1,\n" +
"(spew_test.customError) error: 2: (int) 2,\n" +
"(spew_test.customError) error: 3: (int) 3\n" +
"}\n"
if s != expected {
t.Errorf("Sorted keys mismatch:\n %v %v", s, expected)
}
}
================================================
FILE: internal/spew/dumpcgo_test.go
================================================
// Copyright (c) 2013-2016 Dave Collins
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when both cgo is supported and "-tags testcgo" is added to the go test
// command line. This means the cgo tests are only added (and hence run) when
// specifically requested. This configuration is used because spew itself
// does not require cgo to run even though it does handle certain cgo types
// specially. Rather than forcing all clients to require cgo and an external
// C compiler just to run the tests, this scheme makes them optional.
//go:build cgo && testcgo
// +build cgo,testcgo
package spew_test
import (
"fmt"
"github.com/expr-lang/expr/internal/spew/testdata"
)
func addCgoDumpTests() {
// C char pointer.
v := testdata.GetCgoCharPointer()
nv := testdata.GetCgoNullCharPointer()
pv := &v
vcAddr := fmt.Sprintf("%p", v)
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "*testdata._Ctype_char"
vs := "116"
addDumpTest(v, "("+vt+")("+vcAddr+")("+vs+")\n")
addDumpTest(pv, "(*"+vt+")("+vAddr+"->"+vcAddr+")("+vs+")\n")
addDumpTest(&pv, "(**"+vt+")("+pvAddr+"->"+vAddr+"->"+vcAddr+")("+vs+")\n")
addDumpTest(nv, "("+vt+")()\n")
// C char array.
v2, v2l, v2c := testdata.GetCgoCharArray()
v2Len := fmt.Sprintf("%d", v2l)
v2Cap := fmt.Sprintf("%d", v2c)
v2t := "[6]testdata._Ctype_char"
v2s := "(len=" + v2Len + " cap=" + v2Cap + ") " +
"{\n 00000000 74 65 73 74 32 00 " +
" |test2.|\n}"
addDumpTest(v2, "("+v2t+") "+v2s+"\n")
// C unsigned char array.
v3, v3l, v3c := testdata.GetCgoUnsignedCharArray()
v3Len := fmt.Sprintf("%d", v3l)
v3Cap := fmt.Sprintf("%d", v3c)
v3t := "[6]testdata._Ctype_unsignedchar"
v3t2 := "[6]testdata._Ctype_uchar"
v3s := "(len=" + v3Len + " cap=" + v3Cap + ") " +
"{\n 00000000 74 65 73 74 33 00 " +
" |test3.|\n}"
addDumpTest(v3, "("+v3t+") "+v3s+"\n", "("+v3t2+") "+v3s+"\n")
// C signed char array.
v4, v4l, v4c := testdata.GetCgoSignedCharArray()
v4Len := fmt.Sprintf("%d", v4l)
v4Cap := fmt.Sprintf("%d", v4c)
v4t := "[6]testdata._Ctype_schar"
v4t2 := "testdata._Ctype_schar"
v4s := "(len=" + v4Len + " cap=" + v4Cap + ") " +
"{\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 101,\n (" + v4t2 +
") 115,\n (" + v4t2 + ") 116,\n (" + v4t2 + ") 52,\n (" + v4t2 +
") 0\n}"
addDumpTest(v4, "("+v4t+") "+v4s+"\n")
// C uint8_t array.
v5, v5l, v5c := testdata.GetCgoUint8tArray()
v5Len := fmt.Sprintf("%d", v5l)
v5Cap := fmt.Sprintf("%d", v5c)
v5t := "[6]testdata._Ctype_uint8_t"
v5t2 := "[6]testdata._Ctype_uchar"
v5s := "(len=" + v5Len + " cap=" + v5Cap + ") " +
"{\n 00000000 74 65 73 74 35 00 " +
" |test5.|\n}"
addDumpTest(v5, "("+v5t+") "+v5s+"\n", "("+v5t2+") "+v5s+"\n")
// C typedefed unsigned char array.
v6, v6l, v6c := testdata.GetCgoTypedefedUnsignedCharArray()
v6Len := fmt.Sprintf("%d", v6l)
v6Cap := fmt.Sprintf("%d", v6c)
v6t := "[6]testdata._Ctype_custom_uchar_t"
v6t2 := "[6]testdata._Ctype_uchar"
v6s := "(len=" + v6Len + " cap=" + v6Cap + ") " +
"{\n 00000000 74 65 73 74 36 00 " +
" |test6.|\n}"
addDumpTest(v6, "("+v6t+") "+v6s+"\n", "("+v6t2+") "+v6s+"\n")
}
================================================
FILE: internal/spew/dumpnocgo_test.go
================================================
// Copyright (c) 2013 Dave Collins
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// NOTE: Due to the following build constraints, this file will only be compiled
// when either cgo is not supported or "-tags testcgo" is not added to the go
// test command line. This file intentionally does not setup any cgo tests in
// this scenario.
//go:build !cgo || !testcgo
// +build !cgo !testcgo
package spew_test
func addCgoDumpTests() {
// Don't add any tests for cgo since this file is only compiled when
// there should not be any cgo tests.
}
================================================
FILE: internal/spew/example_test.go
================================================
/*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew_test
import (
"fmt"
"github.com/expr-lang/expr/internal/spew"
)
type Flag int
const (
flagOne Flag = iota
flagTwo
)
var flagStrings = map[Flag]string{
flagOne: "flagOne",
flagTwo: "flagTwo",
}
func (f Flag) String() string {
if s, ok := flagStrings[f]; ok {
return s
}
return fmt.Sprintf("Unknown flag (%d)", int(f))
}
type Bar struct {
data uintptr
}
type Foo struct {
unexportedField Bar
ExportedField map[interface{}]interface{}
}
// This example demonstrates how to use Dump to dump variables to stdout.
func ExampleDump() {
// The following package level declarations are assumed for this example:
/*
type Flag int
const (
flagOne Flag = iota
flagTwo
)
var flagStrings = map[Flag]string{
flagOne: "flagOne",
flagTwo: "flagTwo",
}
func (f Flag) String() string {
if s, ok := flagStrings[f]; ok {
return s
}
return fmt.Sprintf("Unknown flag (%d)", int(f))
}
type Bar struct {
data uintptr
}
type Foo struct {
unexportedField Bar
ExportedField map[interface{}]interface{}
}
*/
// Setup some sample data structures for the example.
bar := Bar{uintptr(0)}
s1 := Foo{bar, map[interface{}]interface{}{"one": true}}
f := Flag(5)
b := []byte{
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
0x31, 0x32,
}
// Dump!
spew.Dump(s1, f, b)
// Output:
// (spew_test.Foo) {
// unexportedField: (spew_test.Bar) {
// data: (uintptr)
// },
// ExportedField: (map[interface {}]interface {}) (len=1) {
// (string) (len=3) "one": (bool) true
// }
// }
// (spew_test.Flag) Unknown flag (5)
// ([]uint8) (len=34 cap=34) {
// 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
// 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
// 00000020 31 32 |12|
// }
//
}
// This example demonstrates how to use Printf to display a variable with a
// format string and inline formatting.
func ExamplePrintf() {
// Create a double pointer to a uint 8.
ui8 := uint8(5)
pui8 := &ui8
ppui8 := &pui8
// Create a circular data type.
type circular struct {
ui8 uint8
c *circular
}
c := circular{ui8: 1}
c.c = &c
// Print!
spew.Printf("ppui8: %v\n", ppui8)
spew.Printf("circular: %v\n", c)
// Output:
// ppui8: <**>5
// circular: {1 <*>{1 <*>}}
}
// This example demonstrates how to use a ConfigState.
func ExampleConfigState() {
// Modify the indent level of the ConfigState only. The global
// configuration is not modified.
scs := spew.ConfigState{Indent: "\t"}
// Output using the ConfigState instance.
v := map[string]int{"one": 1}
scs.Printf("v: %v\n", v)
scs.Dump(v)
// Output:
// v: map[one:1]
// (map[string]int) (len=1) {
// (string) (len=3) "one": (int) 1
// }
}
// This example demonstrates how to use ConfigState.Dump to dump variables to
// stdout
func ExampleConfigState_Dump() {
// See the top-level Dump example for details on the types used in this
// example.
// Create two ConfigState instances with different indentation.
scs := spew.ConfigState{Indent: "\t"}
scs2 := spew.ConfigState{Indent: " "}
// Setup some sample data structures for the example.
bar := Bar{uintptr(0)}
s1 := Foo{bar, map[interface{}]interface{}{"one": true}}
// Dump using the ConfigState instances.
scs.Dump(s1)
scs2.Dump(s1)
// Output:
// (spew_test.Foo) {
// unexportedField: (spew_test.Bar) {
// data: (uintptr)
// },
// ExportedField: (map[interface {}]interface {}) (len=1) {
// (string) (len=3) "one": (bool) true
// }
// }
// (spew_test.Foo) {
// unexportedField: (spew_test.Bar) {
// data: (uintptr)
// },
// ExportedField: (map[interface {}]interface {}) (len=1) {
// (string) (len=3) "one": (bool) true
// }
// }
//
}
// This example demonstrates how to use ConfigState.Printf to display a variable
// with a format string and inline formatting.
func ExampleConfigState_Printf() {
// See the top-level Dump example for details on the types used in this
// example.
// Create two ConfigState instances and modify the method handling of the
// first ConfigState only.
scs := spew.NewDefaultConfig()
scs2 := spew.NewDefaultConfig()
scs.DisableMethods = true
// Alternatively
// scs := spew.ConfigState{Indent: " ", DisableMethods: true}
// scs2 := spew.ConfigState{Indent: " "}
// This is of type Flag which implements a Stringer and has raw value 1.
f := flagTwo
// Dump using the ConfigState instances.
scs.Printf("f: %v\n", f)
scs2.Printf("f: %v\n", f)
// Output:
// f: 1
// f: flagTwo
}
================================================
FILE: internal/spew/format.go
================================================
/*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package spew
import (
"bytes"
"fmt"
"reflect"
"strconv"
"strings"
)
// supportedFlags is a list of all the character flags supported by fmt package.
const supportedFlags = "0-+# "
// formatState implements the fmt.Formatter interface and contains information
// about the state of a formatting operation. The NewFormatter function can
// be used to get a new Formatter which can be used directly as arguments
// in standard fmt package printing calls.
type formatState struct {
value interface{}
fs fmt.State
depth int
pointers map[uintptr]int
ignoreNextType bool
cs *ConfigState
}
// buildDefaultFormat recreates the original format string without precision
// and width information to pass in to fmt.Sprintf in the case of an
// unrecognized type. Unless new types are added to the language, this
// function won't ever be called.
func (f *formatState) buildDefaultFormat() (format string) {
buf := bytes.NewBuffer(percentBytes)
for _, flag := range supportedFlags {
if f.fs.Flag(int(flag)) {
buf.WriteRune(flag)
}
}
buf.WriteRune('v')
format = buf.String()
return format
}
// constructOrigFormat recreates the original format string including precision
// and width information to pass along to the standard fmt package. This allows
// automatic deferral of all format strings this package doesn't support.
func (f *formatState) constructOrigFormat(verb rune) (format string) {
buf := bytes.NewBuffer(percentBytes)
for _, flag := range supportedFlags {
if f.fs.Flag(int(flag)) {
buf.WriteRune(flag)
}
}
if width, ok := f.fs.Width(); ok {
buf.WriteString(strconv.Itoa(width))
}
if precision, ok := f.fs.Precision(); ok {
buf.Write(precisionBytes)
buf.WriteString(strconv.Itoa(precision))
}
buf.WriteRune(verb)
format = buf.String()
return format
}
// unpackValue returns values inside of non-nil interfaces when possible and
// ensures that types for values which have been unpacked from an interface
// are displayed when the show types flag is also set.
// This is useful for data types like structs, arrays, slices, and maps which
// can contain varying types packed inside an interface.
func (f *formatState) unpackValue(v reflect.Value) reflect.Value {
if v.Kind() == reflect.Interface {
f.ignoreNextType = false
if !v.IsNil() {
v = v.Elem()
}
}
return v
}
// formatPtr handles formatting of pointers by indirecting them as necessary.
func (f *formatState) formatPtr(v reflect.Value) {
// Display nil if top level pointer is nil.
showTypes := f.fs.Flag('#')
if v.IsNil() && (!showTypes || f.ignoreNextType) {
f.fs.Write(nilAngleBytes)
return
}
// Remove pointers at or below the current depth from map used to detect
// circular refs.
for k, depth := range f.pointers {
if depth >= f.depth {
delete(f.pointers, k)
}
}
// Keep list of all dereferenced pointers to possibly show later.
pointerChain := make([]uintptr, 0)
// Figure out how many levels of indirection there are by dereferencing
// pointers and unpacking interfaces down the chain while detecting circular
// references.
nilFound := false
cycleFound := false
indirects := 0
ve := v
for ve.Kind() == reflect.Ptr {
if ve.IsNil() {
nilFound = true
break
}
indirects++
addr := ve.Pointer()
pointerChain = append(pointerChain, addr)
if pd, ok := f.pointers[addr]; ok && pd < f.depth {
cycleFound = true
indirects--
break
}
f.pointers[addr] = f.depth
ve = ve.Elem()
if ve.Kind() == reflect.Interface {
if ve.IsNil() {
nilFound = true
break
}
ve = ve.Elem()
}
}
// Display type or indirection level depending on flags.
if showTypes && !f.ignoreNextType {
f.fs.Write(openParenBytes)
f.fs.Write(bytes.Repeat(asteriskBytes, indirects))
f.fs.Write([]byte(ve.Type().String()))
f.fs.Write(closeParenBytes)
} else {
if nilFound || cycleFound {
indirects += strings.Count(ve.Type().String(), "*")
}
f.fs.Write(openAngleBytes)
f.fs.Write([]byte(strings.Repeat("*", indirects)))
f.fs.Write(closeAngleBytes)
}
// Display pointer information depending on flags.
if f.fs.Flag('+') && (len(pointerChain) > 0) {
f.fs.Write(openParenBytes)
for i, addr := range pointerChain {
if i > 0 {
f.fs.Write(pointerChainBytes)
}
printHexPtr(f.fs, addr)
}
f.fs.Write(closeParenBytes)
}
// Display dereferenced value.
switch {
case nilFound:
f.fs.Write(nilAngleBytes)
case cycleFound:
f.fs.Write(circularShortBytes)
default:
f.ignoreNextType = true
f.format(ve)
}
}
// format is the main workhorse for providing the Formatter interface. It
// uses the passed reflect value to figure out what kind of object we are
// dealing with and formats it appropriately. It is a recursive function,
// however circular data structures are detected and handled properly.
func (f *formatState) format(v reflect.Value) {
// Handle invalid reflect values immediately.
kind := v.Kind()
if kind == reflect.Invalid {
f.fs.Write(invalidAngleBytes)
return
}
// Handle pointers specially.
if kind == reflect.Ptr {
f.formatPtr(v)
return
}
// Print type information unless already handled elsewhere.
if !f.ignoreNextType && f.fs.Flag('#') {
f.fs.Write(openParenBytes)
f.fs.Write([]byte(v.Type().String()))
f.fs.Write(closeParenBytes)
}
f.ignoreNextType = false
// Call Stringer/error interfaces if they exist and the handle methods
// flag is enabled.
if !f.cs.DisableMethods {
if (kind != reflect.Invalid) && (kind != reflect.Interface) {
if handled := handleMethods(f.cs, f.fs, v); handled {
return
}
}
}
switch kind {
case reflect.Invalid:
// Do nothing. We should never get here since invalid has already
// been handled above.
case reflect.Bool:
printBool(f.fs, v.Bool())
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
printInt(f.fs, v.Int(), 10)
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
printUint(f.fs, v.Uint(), 10)
case reflect.Float32:
printFloat(f.fs, v.Float(), 32)
case reflect.Float64:
printFloat(f.fs, v.Float(), 64)
case reflect.Complex64:
printComplex(f.fs, v.Complex(), 32)
case reflect.Complex128:
printComplex(f.fs, v.Complex(), 64)
case reflect.Slice:
if v.IsNil() {
f.fs.Write(nilAngleBytes)
break
}
fallthrough
case reflect.Array:
f.fs.Write(openBracketBytes)
f.depth++
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
f.fs.Write(maxShortBytes)
} else {
numEntries := v.Len()
for i := 0; i < numEntries; i++ {
if i > 0 {
f.fs.Write(spaceBytes)
}
f.ignoreNextType = true
f.format(f.unpackValue(v.Index(i)))
}
}
f.depth--
f.fs.Write(closeBracketBytes)
case reflect.String:
f.fs.Write([]byte(v.String()))
case reflect.Interface:
// The only time we should get here is for nil interfaces due to
// unpackValue calls.
if v.IsNil() {
f.fs.Write(nilAngleBytes)
}
case reflect.Ptr:
// Do nothing. We should never get here since pointers have already
// been handled above.
case reflect.Map:
// nil maps should be indicated as different than empty maps
if v.IsNil() {
f.fs.Write(nilAngleBytes)
break
}
f.fs.Write(openMapBytes)
f.depth++
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
f.fs.Write(maxShortBytes)
} else {
keys := v.MapKeys()
if f.cs.SortKeys {
sortValues(keys, f.cs)
}
for i, key := range keys {
if i > 0 {
f.fs.Write(spaceBytes)
}
f.ignoreNextType = true
f.format(f.unpackValue(key))
f.fs.Write(colonBytes)
f.ignoreNextType = true
f.format(f.unpackValue(v.MapIndex(key)))
}
}
f.depth--
f.fs.Write(closeMapBytes)
case reflect.Struct:
numFields := v.NumField()
f.fs.Write(openBraceBytes)
f.depth++
if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
f.fs.Write(maxShortBytes)
} else {
vt := v.Type()
for i := 0; i < numFields; i++ {
if i > 0 {
f.fs.Write(spaceBytes)
}
vtf := vt.Field(i)
if f.fs.Flag('+') || f.fs.Flag('#') {
f.fs.Write([]byte(vtf.Name))
f.fs.Write(colonBytes)
}
f.format(f.unpackValue(v.Field(i)))
}
}
f.depth--
f.fs.Write(closeBraceBytes)
case reflect.Uintptr:
printHexPtr(f.fs, uintptr(v.Uint()))
case reflect.UnsafePointer, reflect.Chan, reflect.Func:
printHexPtr(f.fs, v.Pointer())
// There were not any other types at the time this code was written, but
// fall back to letting the default fmt package handle it if any get added.
default:
format := f.buildDefaultFormat()
if v.CanInterface() {
fmt.Fprintf(f.fs, format, v.Interface())
} else {
fmt.Fprintf(f.fs, format, v.String())
}
}
}
// Format satisfies the fmt.Formatter interface. See NewFormatter for usage
// details.
func (f *formatState) Format(fs fmt.State, verb rune) {
f.fs = fs
// Use standard formatting for verbs that are not v.
if verb != 'v' {
format := f.constructOrigFormat(verb)
fmt.Fprintf(fs, format, f.value)
return
}
if f.value == nil {
if fs.Flag('#') {
fs.Write(interfaceBytes)
}
fs.Write(nilAngleBytes)
return
}
f.format(reflect.ValueOf(f.value))
}
// newFormatter is a helper function to consolidate the logic from the various
// public methods which take varying config states.
func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter {
fs := &formatState{value: v, cs: cs}
fs.pointers = make(map[uintptr]int)
return fs
}
/*
NewFormatter returns a custom formatter that satisfies the fmt.Formatter
interface. As a result, it integrates cleanly with standard fmt package
printing functions. The formatter is useful for inline printing of smaller data
types similar to the standard %v format specifier.
The custom formatter only responds to the %v (most compact), %+v (adds pointer
addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
combinations. Any other verbs such as %x and %q will be sent to the
standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
Typically this function shouldn't be called directly. It is much easier to make
use of the custom formatter by calling one of the convenience functions such as
Printf, Println, or Fprintf.
*/
func NewFormatter(v interface{}) fmt.Formatter {
return newFormatter(&Config, v)
}
================================================
FILE: internal/spew/format_test.go
================================================
/*
* Copyright (c) 2013-2016 Dave Collins
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
Test Summary:
NOTE: For each test, a nil pointer, a single pointer and double pointer to the
base test element are also tested to ensure proper indirection across all types.
- Max int8, int16, int32, int64, int
- Max uint8, uint16, uint32, uint64, uint
- Boolean true and false
- Standard complex64 and complex128
- Array containing standard ints
- Array containing type with custom formatter on pointer receiver only
- Array containing interfaces
- Slice containing standard float32 values
- Slice containing type with custom formatter on pointer receiver only
- Slice containing interfaces
- Nil slice
- Standard string
- Nil interface
- Sub-interface
- Map with string keys and int vals
- Map with custom formatter type on pointer receiver only keys and vals
- Map with interface keys and values
- Map with nil interface value
- Struct with primitives
- Struct that contains another struct
- Struct that contains custom type with Stringer pointer interface via both
exported and unexported fields
- Struct that contains embedded struct and field to same struct
- Uintptr to 0 (null pointer)
- Uintptr address of real variable
- Unsafe.Pointer to 0 (null pointer)
- Unsafe.Pointer to address of real variable
- Nil channel
- Standard int channel
- Function with no params and no returns
- Function with param and no returns
- Function with multiple params and multiple returns
- Struct that is circular through self referencing
- Structs that are circular through cross referencing
- Structs that are indirectly circular
- Type that panics in its Stringer interface
- Type that has a custom Error interface
- %x passthrough with uint
- %#x passthrough with uint
- %f passthrough with precision
- %f passthrough with width and precision
- %d passthrough with width
- %q passthrough with string
*/
package spew_test
import (
"bytes"
"fmt"
"testing"
"unsafe"
"github.com/expr-lang/expr/internal/spew"
)
// formatterTest is used to describe a test to be performed against NewFormatter.
type formatterTest struct {
format string
in interface{}
wants []string
}
// formatterTests houses all of the tests to be performed against NewFormatter.
var formatterTests = make([]formatterTest, 0)
// addFormatterTest is a helper method to append the passed input and desired
// result to formatterTests.
func addFormatterTest(format string, in interface{}, wants ...string) {
test := formatterTest{format, in, wants}
formatterTests = append(formatterTests, test)
}
func addIntFormatterTests() {
// Max int8.
v := int8(127)
nv := (*int8)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "int8"
vs := "127"
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%v", nv, "")
addFormatterTest("%+v", v, vs)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
// Max int16.
v2 := int16(32767)
nv2 := (*int16)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "int16"
v2s := "32767"
addFormatterTest("%v", v2, v2s)
addFormatterTest("%v", pv2, "<*>"+v2s)
addFormatterTest("%v", &pv2, "<**>"+v2s)
addFormatterTest("%v", nv2, "")
addFormatterTest("%+v", v2, v2s)
addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s)
addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%#v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s)
addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s)
addFormatterTest("%#v", nv2, "(*"+v2t+")"+"")
addFormatterTest("%#+v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s)
addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"")
// Max int32.
v3 := int32(2147483647)
nv3 := (*int32)(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "int32"
v3s := "2147483647"
addFormatterTest("%v", v3, v3s)
addFormatterTest("%v", pv3, "<*>"+v3s)
addFormatterTest("%v", &pv3, "<**>"+v3s)
addFormatterTest("%v", nv3, "")
addFormatterTest("%+v", v3, v3s)
addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s)
addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s)
addFormatterTest("%+v", nv3, "")
addFormatterTest("%#v", v3, "("+v3t+")"+v3s)
addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s)
addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s)
addFormatterTest("%#v", nv3, "(*"+v3t+")"+"")
addFormatterTest("%#+v", v3, "("+v3t+")"+v3s)
addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s)
addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s)
addFormatterTest("%#v", nv3, "(*"+v3t+")"+"")
// Max int64.
v4 := int64(9223372036854775807)
nv4 := (*int64)(nil)
pv4 := &v4
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "int64"
v4s := "9223372036854775807"
addFormatterTest("%v", v4, v4s)
addFormatterTest("%v", pv4, "<*>"+v4s)
addFormatterTest("%v", &pv4, "<**>"+v4s)
addFormatterTest("%v", nv4, "")
addFormatterTest("%+v", v4, v4s)
addFormatterTest("%+v", pv4, "<*>("+v4Addr+")"+v4s)
addFormatterTest("%+v", &pv4, "<**>("+pv4Addr+"->"+v4Addr+")"+v4s)
addFormatterTest("%+v", nv4, "")
addFormatterTest("%#v", v4, "("+v4t+")"+v4s)
addFormatterTest("%#v", pv4, "(*"+v4t+")"+v4s)
addFormatterTest("%#v", &pv4, "(**"+v4t+")"+v4s)
addFormatterTest("%#v", nv4, "(*"+v4t+")"+"")
addFormatterTest("%#+v", v4, "("+v4t+")"+v4s)
addFormatterTest("%#+v", pv4, "(*"+v4t+")("+v4Addr+")"+v4s)
addFormatterTest("%#+v", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")"+v4s)
addFormatterTest("%#+v", nv4, "(*"+v4t+")"+"")
// Max int.
v5 := int(2147483647)
nv5 := (*int)(nil)
pv5 := &v5
v5Addr := fmt.Sprintf("%p", pv5)
pv5Addr := fmt.Sprintf("%p", &pv5)
v5t := "int"
v5s := "2147483647"
addFormatterTest("%v", v5, v5s)
addFormatterTest("%v", pv5, "<*>"+v5s)
addFormatterTest("%v", &pv5, "<**>"+v5s)
addFormatterTest("%v", nv5, "")
addFormatterTest("%+v", v5, v5s)
addFormatterTest("%+v", pv5, "<*>("+v5Addr+")"+v5s)
addFormatterTest("%+v", &pv5, "<**>("+pv5Addr+"->"+v5Addr+")"+v5s)
addFormatterTest("%+v", nv5, "")
addFormatterTest("%#v", v5, "("+v5t+")"+v5s)
addFormatterTest("%#v", pv5, "(*"+v5t+")"+v5s)
addFormatterTest("%#v", &pv5, "(**"+v5t+")"+v5s)
addFormatterTest("%#v", nv5, "(*"+v5t+")"+"")
addFormatterTest("%#+v", v5, "("+v5t+")"+v5s)
addFormatterTest("%#+v", pv5, "(*"+v5t+")("+v5Addr+")"+v5s)
addFormatterTest("%#+v", &pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")"+v5s)
addFormatterTest("%#+v", nv5, "(*"+v5t+")"+"")
}
func addUintFormatterTests() {
// Max uint8.
v := uint8(255)
nv := (*uint8)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "uint8"
vs := "255"
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%v", nv, "")
addFormatterTest("%+v", v, vs)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
// Max uint16.
v2 := uint16(65535)
nv2 := (*uint16)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "uint16"
v2s := "65535"
addFormatterTest("%v", v2, v2s)
addFormatterTest("%v", pv2, "<*>"+v2s)
addFormatterTest("%v", &pv2, "<**>"+v2s)
addFormatterTest("%v", nv2, "")
addFormatterTest("%+v", v2, v2s)
addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s)
addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%#v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s)
addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s)
addFormatterTest("%#v", nv2, "(*"+v2t+")"+"")
addFormatterTest("%#+v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s)
addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"")
// Max uint32.
v3 := uint32(4294967295)
nv3 := (*uint32)(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "uint32"
v3s := "4294967295"
addFormatterTest("%v", v3, v3s)
addFormatterTest("%v", pv3, "<*>"+v3s)
addFormatterTest("%v", &pv3, "<**>"+v3s)
addFormatterTest("%v", nv3, "")
addFormatterTest("%+v", v3, v3s)
addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s)
addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s)
addFormatterTest("%+v", nv3, "")
addFormatterTest("%#v", v3, "("+v3t+")"+v3s)
addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s)
addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s)
addFormatterTest("%#v", nv3, "(*"+v3t+")"+"")
addFormatterTest("%#+v", v3, "("+v3t+")"+v3s)
addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s)
addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s)
addFormatterTest("%#v", nv3, "(*"+v3t+")"+"")
// Max uint64.
v4 := uint64(18446744073709551615)
nv4 := (*uint64)(nil)
pv4 := &v4
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "uint64"
v4s := "18446744073709551615"
addFormatterTest("%v", v4, v4s)
addFormatterTest("%v", pv4, "<*>"+v4s)
addFormatterTest("%v", &pv4, "<**>"+v4s)
addFormatterTest("%v", nv4, "")
addFormatterTest("%+v", v4, v4s)
addFormatterTest("%+v", pv4, "<*>("+v4Addr+")"+v4s)
addFormatterTest("%+v", &pv4, "<**>("+pv4Addr+"->"+v4Addr+")"+v4s)
addFormatterTest("%+v", nv4, "")
addFormatterTest("%#v", v4, "("+v4t+")"+v4s)
addFormatterTest("%#v", pv4, "(*"+v4t+")"+v4s)
addFormatterTest("%#v", &pv4, "(**"+v4t+")"+v4s)
addFormatterTest("%#v", nv4, "(*"+v4t+")"+"")
addFormatterTest("%#+v", v4, "("+v4t+")"+v4s)
addFormatterTest("%#+v", pv4, "(*"+v4t+")("+v4Addr+")"+v4s)
addFormatterTest("%#+v", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")"+v4s)
addFormatterTest("%#+v", nv4, "(*"+v4t+")"+"")
// Max uint.
v5 := uint(4294967295)
nv5 := (*uint)(nil)
pv5 := &v5
v5Addr := fmt.Sprintf("%p", pv5)
pv5Addr := fmt.Sprintf("%p", &pv5)
v5t := "uint"
v5s := "4294967295"
addFormatterTest("%v", v5, v5s)
addFormatterTest("%v", pv5, "<*>"+v5s)
addFormatterTest("%v", &pv5, "<**>"+v5s)
addFormatterTest("%v", nv5, "")
addFormatterTest("%+v", v5, v5s)
addFormatterTest("%+v", pv5, "<*>("+v5Addr+")"+v5s)
addFormatterTest("%+v", &pv5, "<**>("+pv5Addr+"->"+v5Addr+")"+v5s)
addFormatterTest("%+v", nv5, "")
addFormatterTest("%#v", v5, "("+v5t+")"+v5s)
addFormatterTest("%#v", pv5, "(*"+v5t+")"+v5s)
addFormatterTest("%#v", &pv5, "(**"+v5t+")"+v5s)
addFormatterTest("%#v", nv5, "(*"+v5t+")"+"")
addFormatterTest("%#+v", v5, "("+v5t+")"+v5s)
addFormatterTest("%#+v", pv5, "(*"+v5t+")("+v5Addr+")"+v5s)
addFormatterTest("%#+v", &pv5, "(**"+v5t+")("+pv5Addr+"->"+v5Addr+")"+v5s)
addFormatterTest("%#v", nv5, "(*"+v5t+")"+"")
}
func addBoolFormatterTests() {
// Boolean true.
v := bool(true)
nv := (*bool)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "bool"
vs := "true"
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%v", nv, "")
addFormatterTest("%+v", v, vs)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
// Boolean false.
v2 := bool(false)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "bool"
v2s := "false"
addFormatterTest("%v", v2, v2s)
addFormatterTest("%v", pv2, "<*>"+v2s)
addFormatterTest("%v", &pv2, "<**>"+v2s)
addFormatterTest("%+v", v2, v2s)
addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s)
addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%#v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s)
addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s)
addFormatterTest("%#+v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s)
addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s)
}
func addFloatFormatterTests() {
// Standard float32.
v := float32(3.1415)
nv := (*float32)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "float32"
vs := "3.1415"
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%v", nv, "")
addFormatterTest("%+v", v, vs)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
// Standard float64.
v2 := float64(3.1415926)
nv2 := (*float64)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "float64"
v2s := "3.1415926"
addFormatterTest("%v", v2, v2s)
addFormatterTest("%v", pv2, "<*>"+v2s)
addFormatterTest("%v", &pv2, "<**>"+v2s)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%+v", v2, v2s)
addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s)
addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%#v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s)
addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s)
addFormatterTest("%#v", nv2, "(*"+v2t+")"+"")
addFormatterTest("%#+v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s)
addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"")
}
func addComplexFormatterTests() {
// Standard complex64.
v := complex(float32(6), -2)
nv := (*complex64)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "complex64"
vs := "(6-2i)"
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%+v", v, vs)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
// Standard complex128.
v2 := complex(float64(-6), 2)
nv2 := (*complex128)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "complex128"
v2s := "(-6+2i)"
addFormatterTest("%v", v2, v2s)
addFormatterTest("%v", pv2, "<*>"+v2s)
addFormatterTest("%v", &pv2, "<**>"+v2s)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%+v", v2, v2s)
addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s)
addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%#v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s)
addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s)
addFormatterTest("%#v", nv2, "(*"+v2t+")"+"")
addFormatterTest("%#+v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s)
addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"")
}
func addArrayFormatterTests() {
// Array containing standard ints.
v := [3]int{1, 2, 3}
nv := (*[3]int)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "[3]int"
vs := "[1 2 3]"
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%+v", v, vs)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
// Array containing type with custom formatter on pointer receiver only.
v2 := [3]pstringer{"1", "2", "3"}
nv2 := (*[3]pstringer)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "[3]spew_test.pstringer"
v2sp := "[stringer 1 stringer 2 stringer 3]"
v2s := v2sp
if spew.UnsafeDisabled {
v2s = "[1 2 3]"
}
addFormatterTest("%v", v2, v2s)
addFormatterTest("%v", pv2, "<*>"+v2sp)
addFormatterTest("%v", &pv2, "<**>"+v2sp)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%+v", v2, v2s)
addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2sp)
addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2sp)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%#v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2sp)
addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2sp)
addFormatterTest("%#v", nv2, "(*"+v2t+")"+"")
addFormatterTest("%#+v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2sp)
addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2sp)
addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"")
// Array containing interfaces.
v3 := [3]interface{}{"one", int(2), uint(3)}
nv3 := (*[3]interface{})(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "[3]interface {}"
v3t2 := "string"
v3t3 := "int"
v3t4 := "uint"
v3s := "[one 2 3]"
v3s2 := "[(" + v3t2 + ")one (" + v3t3 + ")2 (" + v3t4 + ")3]"
addFormatterTest("%v", v3, v3s)
addFormatterTest("%v", pv3, "<*>"+v3s)
addFormatterTest("%v", &pv3, "<**>"+v3s)
addFormatterTest("%+v", nv3, "")
addFormatterTest("%+v", v3, v3s)
addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s)
addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s)
addFormatterTest("%+v", nv3, "")
addFormatterTest("%#v", v3, "("+v3t+")"+v3s2)
addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s2)
addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s2)
addFormatterTest("%#v", nv3, "(*"+v3t+")"+"")
addFormatterTest("%#+v", v3, "("+v3t+")"+v3s2)
addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s2)
addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s2)
addFormatterTest("%#+v", nv3, "(*"+v3t+")"+"")
}
func addSliceFormatterTests() {
// Slice containing standard float32 values.
v := []float32{3.14, 6.28, 12.56}
nv := (*[]float32)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "[]float32"
vs := "[3.14 6.28 12.56]"
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%+v", v, vs)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
// Slice containing type with custom formatter on pointer receiver only.
v2 := []pstringer{"1", "2", "3"}
nv2 := (*[]pstringer)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "[]spew_test.pstringer"
v2s := "[stringer 1 stringer 2 stringer 3]"
addFormatterTest("%v", v2, v2s)
addFormatterTest("%v", pv2, "<*>"+v2s)
addFormatterTest("%v", &pv2, "<**>"+v2s)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%+v", v2, v2s)
addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s)
addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%#v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s)
addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s)
addFormatterTest("%#v", nv2, "(*"+v2t+")"+"")
addFormatterTest("%#+v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s)
addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"")
// Slice containing interfaces.
v3 := []interface{}{"one", int(2), uint(3), nil}
nv3 := (*[]interface{})(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "[]interface {}"
v3t2 := "string"
v3t3 := "int"
v3t4 := "uint"
v3t5 := "interface {}"
v3s := "[one 2 3 ]"
v3s2 := "[(" + v3t2 + ")one (" + v3t3 + ")2 (" + v3t4 + ")3 (" + v3t5 +
")]"
addFormatterTest("%v", v3, v3s)
addFormatterTest("%v", pv3, "<*>"+v3s)
addFormatterTest("%v", &pv3, "<**>"+v3s)
addFormatterTest("%+v", nv3, "")
addFormatterTest("%+v", v3, v3s)
addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s)
addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s)
addFormatterTest("%+v", nv3, "")
addFormatterTest("%#v", v3, "("+v3t+")"+v3s2)
addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s2)
addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s2)
addFormatterTest("%#v", nv3, "(*"+v3t+")"+"")
addFormatterTest("%#+v", v3, "("+v3t+")"+v3s2)
addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s2)
addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s2)
addFormatterTest("%#+v", nv3, "(*"+v3t+")"+"")
// Nil slice.
var v4 []int
nv4 := (*[]int)(nil)
pv4 := &v4
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "[]int"
v4s := ""
addFormatterTest("%v", v4, v4s)
addFormatterTest("%v", pv4, "<*>"+v4s)
addFormatterTest("%v", &pv4, "<**>"+v4s)
addFormatterTest("%+v", nv4, "")
addFormatterTest("%+v", v4, v4s)
addFormatterTest("%+v", pv4, "<*>("+v4Addr+")"+v4s)
addFormatterTest("%+v", &pv4, "<**>("+pv4Addr+"->"+v4Addr+")"+v4s)
addFormatterTest("%+v", nv4, "")
addFormatterTest("%#v", v4, "("+v4t+")"+v4s)
addFormatterTest("%#v", pv4, "(*"+v4t+")"+v4s)
addFormatterTest("%#v", &pv4, "(**"+v4t+")"+v4s)
addFormatterTest("%#v", nv4, "(*"+v4t+")"+"")
addFormatterTest("%#+v", v4, "("+v4t+")"+v4s)
addFormatterTest("%#+v", pv4, "(*"+v4t+")("+v4Addr+")"+v4s)
addFormatterTest("%#+v", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")"+v4s)
addFormatterTest("%#+v", nv4, "(*"+v4t+")"+"")
}
func addStringFormatterTests() {
// Standard string.
v := "test"
nv := (*string)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "string"
vs := "test"
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%+v", v, vs)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
}
func addInterfaceFormatterTests() {
// Nil interface.
var v interface{}
nv := (*interface{})(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "interface {}"
vs := ""
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%+v", v, vs)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
// Sub-interface.
v2 := interface{}(uint16(65535))
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "uint16"
v2s := "65535"
addFormatterTest("%v", v2, v2s)
addFormatterTest("%v", pv2, "<*>"+v2s)
addFormatterTest("%v", &pv2, "<**>"+v2s)
addFormatterTest("%+v", v2, v2s)
addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s)
addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%#v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s)
addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s)
addFormatterTest("%#+v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s)
addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s)
}
func addMapFormatterTests() {
// Map with string keys and int vals.
v := map[string]int{"one": 1, "two": 2}
nilMap := map[string]int(nil)
nv := (*map[string]int)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "map[string]int"
vs := "map[one:1 two:2]"
vs2 := "map[two:2 one:1]"
addFormatterTest("%v", v, vs, vs2)
addFormatterTest("%v", pv, "<*>"+vs, "<*>"+vs2)
addFormatterTest("%v", &pv, "<**>"+vs, "<**>"+vs2)
addFormatterTest("%+v", nilMap, "")
addFormatterTest("%+v", nv, "")
addFormatterTest("%+v", v, vs, vs2)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs, "<*>("+vAddr+")"+vs2)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs,
"<**>("+pvAddr+"->"+vAddr+")"+vs2)
addFormatterTest("%+v", nilMap, "")
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs, "("+vt+")"+vs2)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs, "(*"+vt+")"+vs2)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs, "(**"+vt+")"+vs2)
addFormatterTest("%#v", nilMap, "("+vt+")"+"")
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs, "("+vt+")"+vs2)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs,
"(*"+vt+")("+vAddr+")"+vs2)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs,
"(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs2)
addFormatterTest("%#+v", nilMap, "("+vt+")"+"")
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
// Map with custom formatter type on pointer receiver only keys and vals.
v2 := map[pstringer]pstringer{"one": "1"}
nv2 := (*map[pstringer]pstringer)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "map[spew_test.pstringer]spew_test.pstringer"
v2s := "map[stringer one:stringer 1]"
if spew.UnsafeDisabled {
v2s = "map[one:1]"
}
addFormatterTest("%v", v2, v2s)
addFormatterTest("%v", pv2, "<*>"+v2s)
addFormatterTest("%v", &pv2, "<**>"+v2s)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%+v", v2, v2s)
addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s)
addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%#v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s)
addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s)
addFormatterTest("%#v", nv2, "(*"+v2t+")"+"")
addFormatterTest("%#+v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s)
addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"")
// Map with interface keys and values.
v3 := map[interface{}]interface{}{"one": 1}
nv3 := (*map[interface{}]interface{})(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "map[interface {}]interface {}"
v3t1 := "string"
v3t2 := "int"
v3s := "map[one:1]"
v3s2 := "map[(" + v3t1 + ")one:(" + v3t2 + ")1]"
addFormatterTest("%v", v3, v3s)
addFormatterTest("%v", pv3, "<*>"+v3s)
addFormatterTest("%v", &pv3, "<**>"+v3s)
addFormatterTest("%+v", nv3, "")
addFormatterTest("%+v", v3, v3s)
addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s)
addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s)
addFormatterTest("%+v", nv3, "")
addFormatterTest("%#v", v3, "("+v3t+")"+v3s2)
addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s2)
addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s2)
addFormatterTest("%#v", nv3, "(*"+v3t+")"+"")
addFormatterTest("%#+v", v3, "("+v3t+")"+v3s2)
addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s2)
addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s2)
addFormatterTest("%#+v", nv3, "(*"+v3t+")"+"")
// Map with nil interface value
v4 := map[string]interface{}{"nil": nil}
nv4 := (*map[string]interface{})(nil)
pv4 := &v4
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "map[string]interface {}"
v4t1 := "interface {}"
v4s := "map[nil:]"
v4s2 := "map[nil:(" + v4t1 + ")]"
addFormatterTest("%v", v4, v4s)
addFormatterTest("%v", pv4, "<*>"+v4s)
addFormatterTest("%v", &pv4, "<**>"+v4s)
addFormatterTest("%+v", nv4, "")
addFormatterTest("%+v", v4, v4s)
addFormatterTest("%+v", pv4, "<*>("+v4Addr+")"+v4s)
addFormatterTest("%+v", &pv4, "<**>("+pv4Addr+"->"+v4Addr+")"+v4s)
addFormatterTest("%+v", nv4, "")
addFormatterTest("%#v", v4, "("+v4t+")"+v4s2)
addFormatterTest("%#v", pv4, "(*"+v4t+")"+v4s2)
addFormatterTest("%#v", &pv4, "(**"+v4t+")"+v4s2)
addFormatterTest("%#v", nv4, "(*"+v4t+")"+"")
addFormatterTest("%#+v", v4, "("+v4t+")"+v4s2)
addFormatterTest("%#+v", pv4, "(*"+v4t+")("+v4Addr+")"+v4s2)
addFormatterTest("%#+v", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")"+v4s2)
addFormatterTest("%#+v", nv4, "(*"+v4t+")"+"")
}
func addStructFormatterTests() {
// Struct with primitives.
type s1 struct {
a int8
b uint8
}
v := s1{127, 255}
nv := (*s1)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "spew_test.s1"
vt2 := "int8"
vt3 := "uint8"
vs := "{127 255}"
vs2 := "{a:127 b:255}"
vs3 := "{a:(" + vt2 + ")127 b:(" + vt3 + ")255}"
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%+v", v, vs2)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs2)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs2)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs3)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs3)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs3)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs3)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs3)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs3)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
// Struct that contains another struct.
type s2 struct {
s1 s1
b bool
}
v2 := s2{s1{127, 255}, true}
nv2 := (*s2)(nil)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "spew_test.s2"
v2t2 := "spew_test.s1"
v2t3 := "int8"
v2t4 := "uint8"
v2t5 := "bool"
v2s := "{{127 255} true}"
v2s2 := "{s1:{a:127 b:255} b:true}"
v2s3 := "{s1:(" + v2t2 + "){a:(" + v2t3 + ")127 b:(" + v2t4 + ")255} b:(" +
v2t5 + ")true}"
addFormatterTest("%v", v2, v2s)
addFormatterTest("%v", pv2, "<*>"+v2s)
addFormatterTest("%v", &pv2, "<**>"+v2s)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%+v", v2, v2s2)
addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s2)
addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s2)
addFormatterTest("%+v", nv2, "")
addFormatterTest("%#v", v2, "("+v2t+")"+v2s3)
addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s3)
addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s3)
addFormatterTest("%#v", nv2, "(*"+v2t+")"+"")
addFormatterTest("%#+v", v2, "("+v2t+")"+v2s3)
addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s3)
addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s3)
addFormatterTest("%#+v", nv2, "(*"+v2t+")"+"")
// Struct that contains custom type with Stringer pointer interface via both
// exported and unexported fields.
type s3 struct {
s pstringer
S pstringer
}
v3 := s3{"test", "test2"}
nv3 := (*s3)(nil)
pv3 := &v3
v3Addr := fmt.Sprintf("%p", pv3)
pv3Addr := fmt.Sprintf("%p", &pv3)
v3t := "spew_test.s3"
v3t2 := "spew_test.pstringer"
v3s := "{stringer test stringer test2}"
v3sp := v3s
v3s2 := "{s:stringer test S:stringer test2}"
v3s2p := v3s2
v3s3 := "{s:(" + v3t2 + ")stringer test S:(" + v3t2 + ")stringer test2}"
v3s3p := v3s3
if spew.UnsafeDisabled {
v3s = "{test test2}"
v3sp = "{test stringer test2}"
v3s2 = "{s:test S:test2}"
v3s2p = "{s:test S:stringer test2}"
v3s3 = "{s:(" + v3t2 + ")test S:(" + v3t2 + ")test2}"
v3s3p = "{s:(" + v3t2 + ")test S:(" + v3t2 + ")stringer test2}"
}
addFormatterTest("%v", v3, v3s)
addFormatterTest("%v", pv3, "<*>"+v3sp)
addFormatterTest("%v", &pv3, "<**>"+v3sp)
addFormatterTest("%+v", nv3, "")
addFormatterTest("%+v", v3, v3s2)
addFormatterTest("%+v", pv3, "<*>("+v3Addr+")"+v3s2p)
addFormatterTest("%+v", &pv3, "<**>("+pv3Addr+"->"+v3Addr+")"+v3s2p)
addFormatterTest("%+v", nv3, "")
addFormatterTest("%#v", v3, "("+v3t+")"+v3s3)
addFormatterTest("%#v", pv3, "(*"+v3t+")"+v3s3p)
addFormatterTest("%#v", &pv3, "(**"+v3t+")"+v3s3p)
addFormatterTest("%#v", nv3, "(*"+v3t+")"+"")
addFormatterTest("%#+v", v3, "("+v3t+")"+v3s3)
addFormatterTest("%#+v", pv3, "(*"+v3t+")("+v3Addr+")"+v3s3p)
addFormatterTest("%#+v", &pv3, "(**"+v3t+")("+pv3Addr+"->"+v3Addr+")"+v3s3p)
addFormatterTest("%#+v", nv3, "(*"+v3t+")"+"")
// Struct that contains embedded struct and field to same struct.
e := embed{"embedstr"}
v4 := embedwrap{embed: &e, e: &e}
nv4 := (*embedwrap)(nil)
pv4 := &v4
eAddr := fmt.Sprintf("%p", &e)
v4Addr := fmt.Sprintf("%p", pv4)
pv4Addr := fmt.Sprintf("%p", &pv4)
v4t := "spew_test.embedwrap"
v4t2 := "spew_test.embed"
v4t3 := "string"
v4s := "{<*>{embedstr} <*>{embedstr}}"
v4s2 := "{embed:<*>(" + eAddr + "){a:embedstr} e:<*>(" + eAddr +
"){a:embedstr}}"
v4s3 := "{embed:(*" + v4t2 + "){a:(" + v4t3 + ")embedstr} e:(*" + v4t2 +
"){a:(" + v4t3 + ")embedstr}}"
v4s4 := "{embed:(*" + v4t2 + ")(" + eAddr + "){a:(" + v4t3 +
")embedstr} e:(*" + v4t2 + ")(" + eAddr + "){a:(" + v4t3 + ")embedstr}}"
addFormatterTest("%v", v4, v4s)
addFormatterTest("%v", pv4, "<*>"+v4s)
addFormatterTest("%v", &pv4, "<**>"+v4s)
addFormatterTest("%+v", nv4, "")
addFormatterTest("%+v", v4, v4s2)
addFormatterTest("%+v", pv4, "<*>("+v4Addr+")"+v4s2)
addFormatterTest("%+v", &pv4, "<**>("+pv4Addr+"->"+v4Addr+")"+v4s2)
addFormatterTest("%+v", nv4, "")
addFormatterTest("%#v", v4, "("+v4t+")"+v4s3)
addFormatterTest("%#v", pv4, "(*"+v4t+")"+v4s3)
addFormatterTest("%#v", &pv4, "(**"+v4t+")"+v4s3)
addFormatterTest("%#v", nv4, "(*"+v4t+")"+"")
addFormatterTest("%#+v", v4, "("+v4t+")"+v4s4)
addFormatterTest("%#+v", pv4, "(*"+v4t+")("+v4Addr+")"+v4s4)
addFormatterTest("%#+v", &pv4, "(**"+v4t+")("+pv4Addr+"->"+v4Addr+")"+v4s4)
addFormatterTest("%#+v", nv4, "(*"+v4t+")"+"")
}
func addUintptrFormatterTests() {
// Null pointer.
v := uintptr(0)
nv := (*uintptr)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "uintptr"
vs := ""
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%+v", v, vs)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
// Address of real variable.
i := 1
v2 := uintptr(unsafe.Pointer(&i))
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "uintptr"
v2s := fmt.Sprintf("%p", &i)
addFormatterTest("%v", v2, v2s)
addFormatterTest("%v", pv2, "<*>"+v2s)
addFormatterTest("%v", &pv2, "<**>"+v2s)
addFormatterTest("%+v", v2, v2s)
addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s)
addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%#v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s)
addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s)
addFormatterTest("%#+v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s)
addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s)
}
func addUnsafePointerFormatterTests() {
// Null pointer.
v := unsafe.Pointer(nil)
nv := (*unsafe.Pointer)(nil)
pv := &v
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "unsafe.Pointer"
vs := ""
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%+v", v, vs)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"")
// Address of real variable.
i := 1
v2 := unsafe.Pointer(&i)
pv2 := &v2
v2Addr := fmt.Sprintf("%p", pv2)
pv2Addr := fmt.Sprintf("%p", &pv2)
v2t := "unsafe.Pointer"
v2s := fmt.Sprintf("%p", &i)
addFormatterTest("%v", v2, v2s)
addFormatterTest("%v", pv2, "<*>"+v2s)
addFormatterTest("%v", &pv2, "<**>"+v2s)
addFormatterTest("%+v", v2, v2s)
addFormatterTest("%+v", pv2, "<*>("+v2Addr+")"+v2s)
addFormatterTest("%+v", &pv2, "<**>("+pv2Addr+"->"+v2Addr+")"+v2s)
addFormatterTest("%#v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#v", pv2, "(*"+v2t+")"+v2s)
addFormatterTest("%#v", &pv2, "(**"+v2t+")"+v2s)
addFormatterTest("%#+v", v2, "("+v2t+")"+v2s)
addFormatterTest("%#+v", pv2, "(*"+v2t+")("+v2Addr+")"+v2s)
addFormatterTest("%#+v", &pv2, "(**"+v2t+")("+pv2Addr+"->"+v2Addr+")"+v2s)
}
func addChanFormatterTests() {
// Nil channel.
var v chan int
pv := &v
nv := (*chan int)(nil)
vAddr := fmt.Sprintf("%p", pv)
pvAddr := fmt.Sprintf("%p", &pv)
vt := "chan int"
vs := ""
addFormatterTest("%v", v, vs)
addFormatterTest("%v", pv, "<*>"+vs)
addFormatterTest("%v", &pv, "<**>"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%+v", v, vs)
addFormatterTest("%+v", pv, "<*>("+vAddr+")"+vs)
addFormatterTest("%+v", &pv, "<**>("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%+v", nv, "")
addFormatterTest("%#v", v, "("+vt+")"+vs)
addFormatterTest("%#v", pv, "(*"+vt+")"+vs)
addFormatterTest("%#v", &pv, "(**"+vt+")"+vs)
addFormatterTest("%#v", nv, "(*"+vt+")"+"")
addFormatterTest("%#+v", v, "("+vt+")"+vs)
addFormatterTest("%#+v", pv, "(*"+vt+")("+vAddr+")"+vs)
addFormatterTest("%#+v", &pv, "(**"+vt+")("+pvAddr+"->"+vAddr+")"+vs)
addFormatterTest("%#+v", nv, "(*"+vt+")"+"