aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/golangci/unconvert
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2020-07-04 11:12:55 +0200
committerDmitry Vyukov <dvyukov@google.com>2020-07-04 15:05:30 +0200
commitc7d7f10bdff703e4a3c0414e8a33d4e45c91eb35 (patch)
tree0dff0ee1f98dbfa3ad8776112053a450d176592b /vendor/github.com/golangci/unconvert
parent9573094ce235bd9afe88f5da27a47dd6bcc1e13b (diff)
go.mod: vendor golangci-lint
Diffstat (limited to 'vendor/github.com/golangci/unconvert')
-rw-r--r--vendor/github.com/golangci/unconvert/LICENSE27
-rw-r--r--vendor/github.com/golangci/unconvert/README36
-rw-r--r--vendor/github.com/golangci/unconvert/unconvert.go665
3 files changed, 728 insertions, 0 deletions
diff --git a/vendor/github.com/golangci/unconvert/LICENSE b/vendor/github.com/golangci/unconvert/LICENSE
new file mode 100644
index 000000000..744875676
--- /dev/null
+++ b/vendor/github.com/golangci/unconvert/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2012 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/golangci/unconvert/README b/vendor/github.com/golangci/unconvert/README
new file mode 100644
index 000000000..dbaea4f57
--- /dev/null
+++ b/vendor/github.com/golangci/unconvert/README
@@ -0,0 +1,36 @@
+About:
+
+The unconvert program analyzes Go packages to identify unnecessary
+type conversions; i.e., expressions T(x) where x already has type T.
+
+Install:
+
+ $ go get github.com/mdempsky/unconvert
+
+Usage:
+
+ $ unconvert -v bytes fmt
+ GOROOT/src/bytes/reader.go:117:14: unnecessary conversion
+ abs = int64(r.i) + offset
+ ^
+ GOROOT/src/fmt/print.go:411:21: unnecessary conversion
+ p.fmt.integer(int64(v), 16, unsigned, udigits)
+ ^
+
+Flags:
+
+Using the -v flag, unconvert will also print the source line and a
+caret to indicate the unnecessary conversion's position therein.
+
+Using the -apply flag, unconvert will rewrite the Go source files
+without the unnecessary type conversions.
+
+Using the -all flag, unconvert will analyze the Go packages under all
+possible GOOS/GOARCH combinations, and only identify conversions that
+are unnecessary in all cases.
+
+E.g., syscall.Timespec's Sec and Nsec fields are int64 under
+linux/amd64 but int32 under linux/386. An int64(ts.Sec) conversion
+that appears in a linux/amd64-only file will be identified as
+unnecessary, but it will be preserved if it occurs in a file that's
+compiled for both linux/amd64 and linux/386.
diff --git a/vendor/github.com/golangci/unconvert/unconvert.go b/vendor/github.com/golangci/unconvert/unconvert.go
new file mode 100644
index 000000000..38737d39f
--- /dev/null
+++ b/vendor/github.com/golangci/unconvert/unconvert.go
@@ -0,0 +1,665 @@
+// Copyright 2015 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Unconvert removes redundant type conversions from Go packages.
+package unconvert
+
+import (
+ "bytes"
+ "flag"
+ "fmt"
+ "go/ast"
+ "go/build"
+ "go/format"
+ "go/parser"
+ "go/token"
+ "go/types"
+ "io/ioutil"
+ "log"
+ "os"
+ "reflect"
+ "runtime/pprof"
+ "sort"
+ "sync"
+ "unicode"
+
+ "github.com/kisielk/gotool"
+ "golang.org/x/text/width"
+ "golang.org/x/tools/go/loader"
+)
+
+// Unnecessary conversions are identified by the position
+// of their left parenthesis within a source file.
+
+type editSet map[token.Position]struct{}
+
+type fileToEditSet map[string]editSet
+
+func apply(file string, edits editSet) {
+ if len(edits) == 0 {
+ return
+ }
+
+ fset := token.NewFileSet()
+ f, err := parser.ParseFile(fset, file, nil, parser.ParseComments)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // Note: We modify edits during the walk.
+ v := editor{edits: edits, file: fset.File(f.Package)}
+ ast.Walk(&v, f)
+ if len(edits) != 0 {
+ log.Printf("%s: missing edits %s", file, edits)
+ }
+
+ // TODO(mdempsky): Write to temporary file and rename.
+ var buf bytes.Buffer
+ err = format.Node(&buf, fset, f)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ err = ioutil.WriteFile(file, buf.Bytes(), 0)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+type editor struct {
+ edits editSet
+ file *token.File
+}
+
+func (e *editor) Visit(n ast.Node) ast.Visitor {
+ if n == nil {
+ return nil
+ }
+ v := reflect.ValueOf(n).Elem()
+ for i, n := 0, v.NumField(); i < n; i++ {
+ switch f := v.Field(i).Addr().Interface().(type) {
+ case *ast.Expr:
+ e.rewrite(f)
+ case *[]ast.Expr:
+ for i := range *f {
+ e.rewrite(&(*f)[i])
+ }
+ }
+ }
+ return e
+}
+
+func (e *editor) rewrite(f *ast.Expr) {
+ call, ok := (*f).(*ast.CallExpr)
+ if !ok {
+ return
+ }
+
+ pos := e.file.Position(call.Lparen)
+ if _, ok := e.edits[pos]; !ok {
+ return
+ }
+ *f = call.Args[0]
+ delete(e.edits, pos)
+}
+
+var (
+ cr = []byte{'\r'}
+ nl = []byte{'\n'}
+)
+
+func print(conversions []token.Position) {
+ var file string
+ var lines [][]byte
+
+ for _, pos := range conversions {
+ fmt.Printf("%s:%d:%d: unnecessary conversion\n", pos.Filename, pos.Line, pos.Column)
+ if *flagV {
+ if pos.Filename != file {
+ buf, err := ioutil.ReadFile(pos.Filename)
+ if err != nil {
+ log.Fatal(err)
+ }
+ file = pos.Filename
+ lines = bytes.Split(buf, nl)
+ }
+
+ line := bytes.TrimSuffix(lines[pos.Line-1], cr)
+ fmt.Printf("%s\n", line)
+
+ // For files processed by cgo, Column is the
+ // column location after cgo processing, which
+ // may be different than the source column
+ // that we want here. In lieu of a better
+ // heuristic for detecting this case, at least
+ // avoid panicking if column is out of bounds.
+ if pos.Column <= len(line) {
+ fmt.Printf("%s^\n", rub(line[:pos.Column-1]))
+ }
+ }
+ }
+}
+
+// Rub returns a copy of buf with all non-whitespace characters replaced
+// by spaces (like rubbing them out with white out).
+func rub(buf []byte) []byte {
+ // TODO(mdempsky): Handle combining characters?
+ var res bytes.Buffer
+ for _, r := range string(buf) {
+ if unicode.IsSpace(r) {
+ res.WriteRune(r)
+ continue
+ }
+ switch width.LookupRune(r).Kind() {
+ case width.EastAsianWide, width.EastAsianFullwidth:
+ res.WriteString(" ")
+ default:
+ res.WriteByte(' ')
+ }
+ }
+ return res.Bytes()
+}
+
+var (
+ flagAll = flag.Bool("unconvert.all", false, "type check all GOOS and GOARCH combinations")
+ flagApply = flag.Bool("unconvert.apply", false, "apply edits to source files")
+ flagCPUProfile = flag.String("unconvert.cpuprofile", "", "write CPU profile to file")
+ // TODO(mdempsky): Better description and maybe flag name.
+ flagSafe = flag.Bool("unconvert.safe", false, "be more conservative (experimental)")
+ flagV = flag.Bool("unconvert.v", false, "verbose output")
+)
+
+func usage() {
+ fmt.Fprintf(os.Stderr, "usage: unconvert [flags] [package ...]\n")
+ flag.PrintDefaults()
+}
+
+func nomain() {
+ flag.Usage = usage
+ flag.Parse()
+
+ if *flagCPUProfile != "" {
+ f, err := os.Create(*flagCPUProfile)
+ if err != nil {
+ log.Fatal(err)
+ }
+ pprof.StartCPUProfile(f)
+ defer pprof.StopCPUProfile()
+ }
+
+ importPaths := gotool.ImportPaths(flag.Args())
+ if len(importPaths) == 0 {
+ return
+ }
+
+ var m fileToEditSet
+ if *flagAll {
+ m = mergeEdits(importPaths)
+ } else {
+ m = computeEdits(importPaths, build.Default.GOOS, build.Default.GOARCH, build.Default.CgoEnabled)
+ }
+
+ if *flagApply {
+ var wg sync.WaitGroup
+ for f, e := range m {
+ wg.Add(1)
+ f, e := f, e
+ go func() {
+ defer wg.Done()
+ apply(f, e)
+ }()
+ }
+ wg.Wait()
+ } else {
+ var conversions []token.Position
+ for _, positions := range m {
+ for pos := range positions {
+ conversions = append(conversions, pos)
+ }
+ }
+ sort.Sort(byPosition(conversions))
+ print(conversions)
+ if len(conversions) > 0 {
+ os.Exit(1)
+ }
+ }
+}
+
+func Run(prog *loader.Program) []token.Position {
+ m := computeEditsFromProg(prog)
+ var conversions []token.Position
+ for _, positions := range m {
+ for pos := range positions {
+ conversions = append(conversions, pos)
+ }
+ }
+ return conversions
+}
+
+var plats = [...]struct {
+ goos, goarch string
+}{
+ // TODO(mdempsky): buildall.bash also builds linux-386-387 and linux-arm-arm5.
+ {"android", "386"},
+ {"android", "amd64"},
+ {"android", "arm"},
+ {"android", "arm64"},
+ {"darwin", "386"},
+ {"darwin", "amd64"},
+ {"darwin", "arm"},
+ {"darwin", "arm64"},
+ {"dragonfly", "amd64"},
+ {"freebsd", "386"},
+ {"freebsd", "amd64"},
+ {"freebsd", "arm"},
+ {"linux", "386"},
+ {"linux", "amd64"},
+ {"linux", "arm"},
+ {"linux", "arm64"},
+ {"linux", "mips64"},
+ {"linux", "mips64le"},
+ {"linux", "ppc64"},
+ {"linux", "ppc64le"},
+ {"linux", "s390x"},
+ {"nacl", "386"},
+ {"nacl", "amd64p32"},
+ {"nacl", "arm"},
+ {"netbsd", "386"},
+ {"netbsd", "amd64"},
+ {"netbsd", "arm"},
+ {"openbsd", "386"},
+ {"openbsd", "amd64"},
+ {"openbsd", "arm"},
+ {"plan9", "386"},
+ {"plan9", "amd64"},
+ {"plan9", "arm"},
+ {"solaris", "amd64"},
+ {"windows", "386"},
+ {"windows", "amd64"},
+}
+
+func mergeEdits(importPaths []string) fileToEditSet {
+ m := make(fileToEditSet)
+ for _, plat := range plats {
+ for f, e := range computeEdits(importPaths, plat.goos, plat.goarch, false) {
+ if e0, ok := m[f]; ok {
+ for k := range e0 {
+ if _, ok := e[k]; !ok {
+ delete(e0, k)
+ }
+ }
+ } else {
+ m[f] = e
+ }
+ }
+ }
+ return m
+}
+
+type noImporter struct{}
+
+func (noImporter) Import(path string) (*types.Package, error) {
+ panic("golang.org/x/tools/go/loader said this wouldn't be called")
+}
+
+func computeEdits(importPaths []string, os, arch string, cgoEnabled bool) fileToEditSet {
+ ctxt := build.Default
+ ctxt.GOOS = os
+ ctxt.GOARCH = arch
+ ctxt.CgoEnabled = cgoEnabled
+
+ var conf loader.Config
+ conf.Build = &ctxt
+ conf.TypeChecker.Importer = noImporter{}
+ for _, importPath := range importPaths {
+ conf.Import(importPath)
+ }
+ prog, err := conf.Load()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ return computeEditsFromProg(prog)
+}
+
+func computeEditsFromProg(prog *loader.Program) fileToEditSet {
+ type res struct {
+ file string
+ edits editSet
+ }
+ ch := make(chan res)
+ var wg sync.WaitGroup
+ for _, pkg := range prog.InitialPackages() {
+ for _, file := range pkg.Files {
+ pkg, file := pkg, file
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ v := visitor{pkg: pkg, file: prog.Fset.File(file.Package), edits: make(editSet)}
+ ast.Walk(&v, file)
+ ch <- res{v.file.Name(), v.edits}
+ }()
+ }
+ }
+ go func() {
+ wg.Wait()
+ close(ch)
+ }()
+
+ m := make(fileToEditSet)
+ for r := range ch {
+ m[r.file] = r.edits
+ }
+ return m
+}
+
+type step struct {
+ n ast.Node
+ i int
+}
+
+type visitor struct {
+ pkg *loader.PackageInfo
+ file *token.File
+ edits editSet
+ path []step
+}
+
+func (v *visitor) Visit(node ast.Node) ast.Visitor {
+ if node != nil {
+ v.path = append(v.path, step{n: node})
+ } else {
+ n := len(v.path)
+ v.path = v.path[:n-1]
+ if n >= 2 {
+ v.path[n-2].i++
+ }
+ }
+
+ if call, ok := node.(*ast.CallExpr); ok {
+ v.unconvert(call)
+ }
+ return v
+}
+
+func (v *visitor) unconvert(call *ast.CallExpr) {
+ // TODO(mdempsky): Handle useless multi-conversions.
+
+ // Conversions have exactly one argument.
+ if len(call.Args) != 1 || call.Ellipsis != token.NoPos {
+ return
+ }
+ ft, ok := v.pkg.Types[call.Fun]
+ if !ok {
+ fmt.Println("Missing type for function")
+ return
+ }
+ if !ft.IsType() {
+ // Function call; not a conversion.
+ return
+ }
+ at, ok := v.pkg.Types[call.Args[0]]
+ if !ok {
+ fmt.Println("Missing type for argument")
+ return
+ }
+ if !types.Identical(ft.Type, at.Type) {
+ // A real conversion.
+ return
+ }
+ if isUntypedValue(call.Args[0], &v.pkg.Info) {
+ // Workaround golang.org/issue/13061.
+ return
+ }
+ if *flagSafe && !v.isSafeContext(at.Type) {
+ // TODO(mdempsky): Remove this message.
+ fmt.Println("Skipped a possible type conversion because of -safe at", v.file.Position(call.Pos()))
+ return
+ }
+ if v.isCgoCheckPointerContext() {
+ // cmd/cgo generates explicit type conversions that
+ // are often redundant when introducing
+ // _cgoCheckPointer calls (issue #16). Users can't do
+ // anything about these, so skip over them.
+ return
+ }
+
+ v.edits[v.file.Position(call.Lparen)] = struct{}{}
+}
+
+func (v *visitor) isCgoCheckPointerContext() bool {
+ ctxt := &v.path[len(v.path)-2]
+ if ctxt.i != 1 {
+ return false
+ }
+ call, ok := ctxt.n.(*ast.CallExpr)
+ if !ok {
+ return false
+ }
+ ident, ok := call.Fun.(*ast.Ident)
+ if !ok {
+ return false
+ }
+ return ident.Name == "_cgoCheckPointer"
+}
+
+// isSafeContext reports whether the current context requires
+// an expression of type t.
+//
+// TODO(mdempsky): That's a bad explanation.
+func (v *visitor) isSafeContext(t types.Type) bool {
+ ctxt := &v.path[len(v.path)-2]
+ switch n := ctxt.n.(type) {
+ case *ast.AssignStmt:
+ pos := ctxt.i - len(n.Lhs)
+ if pos < 0 {
+ fmt.Println("Type conversion on LHS of assignment?")
+ return false
+ }
+ if n.Tok == token.DEFINE {
+ // Skip := assignments.
+ return true
+ }
+ // We're a conversion in the pos'th element of n.Rhs.
+ // Check that the corresponding element of n.Lhs is of type t.
+ lt, ok := v.pkg.Types[n.Lhs[pos]]
+ if !ok {
+ fmt.Println("Missing type for LHS expression")
+ return false
+ }
+ return types.Identical(t, lt.Type)
+ case *ast.BinaryExpr:
+ if n.Op == token.SHL || n.Op == token.SHR {
+ if ctxt.i == 1 {
+ // RHS of a shift is always safe.
+ return true
+ }
+ // For the LHS, we should inspect up another level.
+ fmt.Println("TODO(mdempsky): Handle LHS of shift expressions")
+ return true
+ }
+ var other ast.Expr
+ if ctxt.i == 0 {
+ other = n.Y
+ } else {
+ other = n.X
+ }
+ ot, ok := v.pkg.Types[other]
+ if !ok {
+ fmt.Println("Missing type for other binop subexpr")
+ return false
+ }
+ return types.Identical(t, ot.Type)
+ case *ast.CallExpr:
+ pos := ctxt.i - 1
+ if pos < 0 {
+ // Type conversion in the function subexpr is okay.
+ return true
+ }
+ ft, ok := v.pkg.Types[n.Fun]
+ if !ok {
+ fmt.Println("Missing type for function expression")
+ return false
+ }
+ sig, ok := ft.Type.(*types.Signature)
+ if !ok {
+ // "Function" is either a type conversion (ok) or a builtin (ok?).
+ return true
+ }
+ params := sig.Params()
+ var pt types.Type
+ if sig.Variadic() && n.Ellipsis == token.NoPos && pos >= params.Len()-1 {
+ pt = params.At(params.Len() - 1).Type().(*types.Slice).Elem()
+ } else {
+ pt = params.At(pos).Type()
+ }
+ return types.Identical(t, pt)
+ case *ast.CompositeLit, *ast.KeyValueExpr:
+ fmt.Println("TODO(mdempsky): Compare against value type of composite literal type at", v.file.Position(n.Pos()))
+ return true
+ case *ast.ReturnStmt:
+ // TODO(mdempsky): Is there a better way to get the corresponding
+ // return parameter type?
+ var funcType *ast.FuncType
+ for i := len(v.path) - 1; funcType == nil && i >= 0; i-- {
+ switch f := v.path[i].n.(type) {
+ case *ast.FuncDecl:
+ funcType = f.Type
+ case *ast.FuncLit:
+ funcType = f.Type
+ }
+ }
+ var typeExpr ast.Expr
+ for i, j := ctxt.i, 0; j < len(funcType.Results.List); j++ {
+ f := funcType.Results.List[j]
+ if len(f.Names) == 0 {
+ if i >= 1 {
+ i--
+ continue
+ }
+ } else {
+ if i >= len(f.Names) {
+ i -= len(f.Names)
+ continue
+ }
+ }
+ typeExpr = f.Type
+ break
+ }
+ if typeExpr == nil {
+ fmt.Println(ctxt)
+ }
+ pt, ok := v.pkg.Types[typeExpr]
+ if !ok {
+ fmt.Println("Missing type for return parameter at", v.file.Position(n.Pos()))
+ return false
+ }
+ return types.Identical(t, pt.Type)
+ case *ast.StarExpr, *ast.UnaryExpr:
+ // TODO(mdempsky): I think these are always safe.
+ return true
+ case *ast.SwitchStmt:
+ // TODO(mdempsky): I think this is always safe?
+ return true
+ default:
+ // TODO(mdempsky): When can this happen?
+ fmt.Printf("... huh, %T at %v\n", n, v.file.Position(n.Pos()))
+ return true
+ }
+}
+
+func isUntypedValue(n ast.Expr, info *types.Info) (res bool) {
+ switch n := n.(type) {
+ case *ast.BinaryExpr:
+ switch n.Op {
+ case token.SHL, token.SHR:
+ // Shifts yield an untyped value if their LHS is untyped.
+ return isUntypedValue(n.X, info)
+ case token.EQL, token.NEQ, token.LSS, token.GTR, token.LEQ, token.GEQ:
+ // Comparisons yield an untyped boolean value.
+ return true
+ case token.ADD, token.SUB, token.MUL, token.QUO, token.REM,
+ token.AND, token.OR, token.XOR, token.AND_NOT,
+ token.LAND, token.LOR:
+ return isUntypedValue(n.X, info) && isUntypedValue(n.Y, info)
+ }
+ case *ast.UnaryExpr:
+ switch n.Op {
+ case token.ADD, token.SUB, token.NOT, token.XOR:
+ return isUntypedValue(n.X, info)
+ }
+ case *ast.BasicLit:
+ // Basic literals are always untyped.
+ return true
+ case *ast.ParenExpr:
+ return isUntypedValue(n.X, info)
+ case *ast.SelectorExpr:
+ return isUntypedValue(n.Sel, info)
+ case *ast.Ident:
+ if obj, ok := info.Uses[n]; ok {
+ if obj.Pkg() == nil && obj.Name() == "nil" {
+ // The universal untyped zero value.
+ return true
+ }
+ if b, ok := obj.Type().(*types.Basic); ok && b.Info()&types.IsUntyped != 0 {
+ // Reference to an untyped constant.
+ return true
+ }
+ }
+ case *ast.CallExpr:
+ if b, ok := asBuiltin(n.Fun, info); ok {
+ switch b.Name() {
+ case "real", "imag":
+ return isUntypedValue(n.Args[0], info)
+ case "complex":
+ return isUntypedValue(n.Args[0], info) && isUntypedValue(n.Args[1], info)
+ }
+ }
+ }
+
+ return false
+}
+
+func asBuiltin(n ast.Expr, info *types.Info) (*types.Builtin, bool) {
+ for {
+ paren, ok := n.(*ast.ParenExpr)
+ if !ok {
+ break
+ }
+ n = paren.X
+ }
+
+ ident, ok := n.(*ast.Ident)
+ if !ok {
+ return nil, false
+ }
+
+ obj, ok := info.Uses[ident]
+ if !ok {
+ return nil, false
+ }
+
+ b, ok := obj.(*types.Builtin)
+ return b, ok
+}
+
+type byPosition []token.Position
+
+func (p byPosition) Len() int {
+ return len(p)
+}
+
+func (p byPosition) Less(i, j int) bool {
+ if p[i].Filename != p[j].Filename {
+ return p[i].Filename < p[j].Filename
+ }
+ if p[i].Line != p[j].Line {
+ return p[i].Line < p[j].Line
+ }
+ return p[i].Column < p[j].Column
+}
+
+func (p byPosition) Swap(i, j int) {
+ p[i], p[j] = p[j], p[i]
+}