├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── ctxscope ├── ctxscope.go ├── ctxscope_test.go └── testdata │ └── src │ ├── a │ ├── a.go │ └── a_test.go │ └── b │ └── context │ └── context.go ├── go.mod ├── go.sum ├── main.go └── rangeptr ├── rangeptr.go ├── rangeptr_test.go └── testdata └── src └── a └── a.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.11.11" 5 | - "1.12.x" 6 | 7 | env: 8 | - GO111MODULE=on 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Daisuke Suzuki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Source Checker 2 | 3 | [![Build Status](https://travis-ci.org/daisuzu/gsc.svg?branch=master)](https://travis-ci.org/daisuzu/gsc) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/daisuzu/gsc)](https://goreportcard.com/report/github.com/daisuzu/gsc) 5 | 6 | _gsc_ checkes bits and pieces of problems in Go source code. 7 | 8 | ## Checks 9 | 10 | | Check | Description | 11 | | ----- | ----------- | 12 | | [ctxscope](ctxscope/testdata/src/a/a.go) | Not to use [context.Context](https://golang.org/pkg/context/#Context) outside the scope. | 13 | | [rangeptr](rangeptr/testdata/src/a/a.go) | Not to use pointer to the loop iteration variable. | 14 | 15 | ## Installation 16 | 17 | ```sh 18 | go get -u github.com/daisuzu/gsc 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```sh 24 | Usage: gsc [-flag] [package] 25 | 26 | Run 'gsc help' for more detail, 27 | or 'gsc help name' for details and flags of a specific analyzer. 28 | ``` 29 | -------------------------------------------------------------------------------- /ctxscope/ctxscope.go: -------------------------------------------------------------------------------- 1 | package ctxscope 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/types" 7 | "os" 8 | "strings" 9 | 10 | "golang.org/x/tools/go/analysis" 11 | "golang.org/x/tools/go/analysis/passes/inspect" 12 | "golang.org/x/tools/go/ast/inspector" 13 | ) 14 | 15 | var Analyzer = &analysis.Analyzer{ 16 | Name: "ctxscope", 17 | Doc: "report passing outer scope context", 18 | Requires: []*analysis.Analyzer{inspect.Analyzer}, 19 | RunDespiteErrors: true, 20 | Run: run, 21 | } 22 | 23 | type contexts []string 24 | 25 | func (v *contexts) Set(s string) error { 26 | *v = append(*v, s) 27 | return nil 28 | } 29 | 30 | func (v *contexts) Get() interface{} { return *v } 31 | func (v *contexts) String() string { return "" } 32 | 33 | var ( 34 | exitNonZero bool 35 | tests bool 36 | ctxs contexts 37 | ) 38 | 39 | func init() { 40 | Analyzer.Flags.BoolVar(&exitNonZero, "exit-non-zero", true, "exit non-zero if any problems were found") 41 | Analyzer.Flags.BoolVar(&tests, "tests", false, "include tests") 42 | Analyzer.Flags.Var(&ctxs, "target-context", "additional target context types other than the standard library's context") 43 | } 44 | 45 | func isContext(s string) bool { 46 | for _, v := range append([]string{"context.Context"}, ctxs...) { 47 | if strings.HasSuffix(s, v) { 48 | return true 49 | } 50 | } 51 | return false 52 | } 53 | 54 | func run(pass *analysis.Pass) (interface{}, error) { 55 | if !exitNonZero { 56 | pass.Report = func(diag analysis.Diagnostic) { 57 | posn := pass.Fset.Position(diag.Pos) 58 | fmt.Fprintf(os.Stderr, "%s: %s\n", posn, diag.Message) 59 | } 60 | } 61 | 62 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 63 | nodeFilter := []ast.Node{ 64 | (*ast.CallExpr)(nil), 65 | } 66 | inspect.Preorder(nodeFilter, func(n ast.Node) { 67 | if !tests && strings.HasSuffix(pass.Fset.File(n.Pos()).Name(), "_test.go") { 68 | return 69 | } 70 | 71 | call := n.(*ast.CallExpr) 72 | if len(call.Args) == 0 { 73 | return 74 | } 75 | 76 | sig, ok := pass.TypesInfo.TypeOf(call.Fun).(*types.Signature) 77 | if !ok { 78 | return 79 | } 80 | if sig.Params().Len() == 0 { 81 | return 82 | } 83 | if !isContext(types.TypeString(sig.Params().At(0).Type(), nil)) { 84 | return 85 | } 86 | 87 | // Check ctx declaration exists in the function that contains current scope. 88 | scope := pass.Pkg.Scope().Innermost(call.Pos()) 89 | for scope != nil && !strings.HasPrefix(scope.String(), "function scope") { 90 | scope = scope.Parent() 91 | } 92 | if scope == nil { 93 | return 94 | } 95 | if obj := scope.Lookup(types.ExprString(call.Args[0])); obj != nil { 96 | return 97 | } 98 | 99 | // Allow deferred closure without arguments. 100 | var dc bool 101 | ast.Inspect(getFile(pass, scope), func(n ast.Node) bool { 102 | d, ok := n.(*ast.DeferStmt) 103 | if !ok { 104 | return true 105 | } 106 | f, ok := d.Call.Fun.(*ast.FuncLit) 107 | if !ok { 108 | return true 109 | } 110 | if f.Body == nil { 111 | return true 112 | } 113 | if f.Body.Pos() != scope.Pos() { 114 | return true 115 | } 116 | dc = len(f.Type.Params.List) == 0 117 | return false 118 | }) 119 | if dc { 120 | return 121 | } 122 | 123 | if allowedCtx(call.Args[0]) { 124 | return 125 | } 126 | 127 | pass.Reportf(call.Args[0].Pos(), "passing outer scope context %q to %s()", types.ExprString(call.Args[0]), types.ExprString(call.Fun)) 128 | }) 129 | 130 | return nil, nil 131 | } 132 | 133 | // allowedCtx checks whether arg which returns context is whitelisted. 134 | // - "context" or "google.golang.org/appengine" package 135 | // - "net/http".Request.Context 136 | // - func that returns above context 137 | func allowedCtx(arg ast.Expr) bool { 138 | if c, ok := arg.(*ast.CallExpr); ok { 139 | switch t := c.Fun.(type) { 140 | case *ast.SelectorExpr: 141 | if i, ok := t.X.(*ast.Ident); ok { 142 | if i.Obj == nil { 143 | if i.Name == "context" || i.Name == "appengine" { 144 | return true 145 | } 146 | } else { 147 | if f, ok := i.Obj.Decl.(*ast.Field); ok { 148 | if types.ExprString(f.Type) == "*http.Request" { 149 | return true 150 | } 151 | } 152 | } 153 | } 154 | case *ast.Ident: 155 | if len(c.Args) == 0 { 156 | return true 157 | } 158 | return allowedCtx(c.Args[0]) 159 | } 160 | } 161 | return false 162 | } 163 | 164 | func getFile(pass *analysis.Pass, scope *types.Scope) *ast.File { 165 | name := pass.Fset.Position(scope.Pos()).Filename 166 | for _, v := range pass.Files { 167 | if name == pass.Fset.Position(v.Pos()).Filename { 168 | return v 169 | } 170 | } 171 | return nil 172 | } 173 | -------------------------------------------------------------------------------- /ctxscope/ctxscope_test.go: -------------------------------------------------------------------------------- 1 | package ctxscope_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "golang.org/x/tools/go/analysis/analysistest" 7 | 8 | "github.com/daisuzu/gsc/ctxscope" 9 | ) 10 | 11 | func init() { 12 | ctxscope.Analyzer.Flags.Set("target-context", "MyCtx") 13 | } 14 | 15 | func Test(t *testing.T) { 16 | testdata := analysistest.TestData() 17 | analysistest.Run(t, testdata, ctxscope.Analyzer, "a") 18 | analysistest.Run(t, testdata, ctxscope.Analyzer, "b/context") 19 | } 20 | -------------------------------------------------------------------------------- /ctxscope/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | type datastoreInterface struct{} 9 | 10 | func (datastoreInterface) RunInTransaction(c context.Context, f func(tc context.Context) error, opts interface{}) error { 11 | return nil 12 | } 13 | 14 | func (datastoreInterface) Get(c context.Context, key, dst interface{}) error { 15 | return nil 16 | } 17 | 18 | func (datastoreInterface) Put(c context.Context, key, src interface{}) (interface{}, error) { 19 | return nil, nil 20 | } 21 | 22 | func (datastoreInterface) Delete(c context.Context, key interface{}) error { 23 | return nil 24 | } 25 | 26 | var datastore = datastoreInterface{} 27 | 28 | var _, _ = context.WithCancel(context.TODO()) 29 | 30 | func updateWithTxCtx(c context.Context) { 31 | datastore.RunInTransaction(c, func(tc context.Context) error { 32 | if err := datastore.Get(tc, nil, nil); err != nil { 33 | return err 34 | } 35 | _, err := datastore.Put(tc, nil, nil) 36 | return err 37 | }, nil) 38 | } 39 | 40 | func updateWithCtx(c context.Context) { 41 | datastore.RunInTransaction(c, func(tc context.Context) error { 42 | if err := datastore.Get(c, nil, nil); err != nil { // want `passing outer scope context` 43 | return err 44 | } 45 | _, err := datastore.Put(c, nil, nil) // want `passing outer scope context "c" to datastore.Put()` 46 | return err 47 | }, nil) 48 | } 49 | 50 | func updateWithMyCtx(c context.Context) { 51 | type MyCtx struct{ context.Context } 52 | 53 | get := func(c *MyCtx) error { 54 | return datastore.Get(c, nil, nil) 55 | } 56 | put := func(c *MyCtx) error { 57 | _, err := datastore.Put(c, nil, nil) 58 | return err 59 | } 60 | 61 | ctx := &MyCtx{c} 62 | datastore.RunInTransaction(ctx, func(tc context.Context) error { 63 | if err := get(ctx); err != nil { // want `passing outer scope context "ctx" to get()` 64 | return err 65 | } 66 | return put(ctx) // want `passing outer scope context "ctx" to put()` 67 | }, nil) 68 | } 69 | 70 | func updateWithUnregisteredCtx(c context.Context) { 71 | type Ctx struct{ context.Context } 72 | 73 | get := func(c *Ctx) error { 74 | return datastore.Get(c, nil, nil) 75 | } 76 | put := func(c *Ctx) error { 77 | _, err := datastore.Put(c, nil, nil) 78 | return err 79 | } 80 | 81 | ctx := &Ctx{c} 82 | datastore.RunInTransaction(ctx, func(tc context.Context) error { 83 | if err := get(ctx); err != nil { 84 | return err 85 | } 86 | return put(ctx) 87 | }, nil) 88 | } 89 | 90 | func useCtxInClosure(c context.Context) { 91 | func() { 92 | datastore.Delete(c, nil) // want `passing outer scope context "c" to datastore.Delete()` 93 | }() 94 | } 95 | 96 | func useCtxInDeferredClosure(c context.Context) { 97 | defer func() { 98 | datastore.Delete(c, nil) 99 | }() 100 | } 101 | 102 | func handler(w http.ResponseWriter, r *http.Request) { 103 | f := func(ctx context.Context) {} 104 | 105 | f(context.Background()) 106 | f(r.Context()) 107 | } 108 | 109 | func middleware(next http.Handler) http.Handler { 110 | f := func(ctx context.Context) context.Context { return ctx } 111 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 112 | next.ServeHTTP(w, r.WithContext(r.Context())) 113 | next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "key", "val"))) 114 | next.ServeHTTP(w, r.WithContext(f(r.Context()))) 115 | }) 116 | } 117 | -------------------------------------------------------------------------------- /ctxscope/testdata/src/a/a_test.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | ) 7 | 8 | func testUpdateWithTxCtx(c context.Context) { 9 | datastore.RunInTransaction(c, func(tc context.Context) error { 10 | if err := datastore.Get(tc, nil, nil); err != nil { 11 | return err 12 | } 13 | _, err := datastore.Put(tc, nil, nil) 14 | return err 15 | }, nil) 16 | } 17 | 18 | func testUpdateWithCtx(c context.Context) { 19 | datastore.RunInTransaction(c, func(tc context.Context) error { 20 | if err := datastore.Get(c, nil, nil); err != nil { 21 | return err 22 | } 23 | _, err := datastore.Put(c, nil, nil) 24 | return err 25 | }, nil) 26 | } 27 | 28 | func testUpdateWithMyCtx(c context.Context) { 29 | type MyCtx struct{ context.Context } 30 | 31 | get := func(c *MyCtx) error { 32 | return datastore.Get(c, nil, nil) 33 | } 34 | put := func(c *MyCtx) error { 35 | _, err := datastore.Put(c, nil, nil) 36 | return err 37 | } 38 | 39 | ctx := &MyCtx{c} 40 | datastore.RunInTransaction(ctx, func(tc context.Context) error { 41 | if err := get(ctx); err != nil { 42 | return err 43 | } 44 | return put(ctx) 45 | }, nil) 46 | } 47 | 48 | func testUpdateWithUnregisteredCtx(c context.Context) { 49 | type Ctx struct{ context.Context } 50 | 51 | get := func(c *Ctx) error { 52 | return datastore.Get(c, nil, nil) 53 | } 54 | put := func(c *Ctx) error { 55 | _, err := datastore.Put(c, nil, nil) 56 | return err 57 | } 58 | 59 | ctx := &Ctx{c} 60 | datastore.RunInTransaction(ctx, func(tc context.Context) error { 61 | if err := get(ctx); err != nil { 62 | return err 63 | } 64 | return put(ctx) 65 | }, nil) 66 | } 67 | 68 | func testUseCtxInClosure(c context.Context) { 69 | func() { 70 | datastore.Delete(c, nil) 71 | }() 72 | } 73 | 74 | func testUseCtxInDeferredClosure(c context.Context) { 75 | defer func() { 76 | datastore.Delete(c, nil) 77 | }() 78 | } 79 | 80 | func testMiddleware(next http.Handler) http.Handler { 81 | f := func(ctx context.Context) context.Context { return ctx } 82 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 83 | next.ServeHTTP(w, r.WithContext(r.Context())) 84 | next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "key", "val"))) 85 | next.ServeHTTP(w, r.WithContext(f(r.Context()))) 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /ctxscope/testdata/src/b/context/context.go: -------------------------------------------------------------------------------- 1 | package context 2 | 3 | type Context interface { 4 | } 5 | 6 | type ContextBuilder interface { 7 | Context() Context 8 | } 9 | 10 | type contextImpl struct { 11 | } 12 | 13 | func New() Context { 14 | return &contextImpl{} 15 | } 16 | 17 | func FromMessage(message map[string]interface{}) Context { 18 | builder := Builder(New()) 19 | return builder.Context() 20 | } 21 | 22 | func Builder(ctx Context) ContextBuilder { 23 | builder := &contextBuilder{ctx: &contextImpl{}} 24 | return builder 25 | } 26 | 27 | type contextBuilder struct { 28 | ctx *contextImpl 29 | } 30 | 31 | func (b *contextBuilder) Context() Context { 32 | return b.ctx 33 | } 34 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/daisuzu/gsc 2 | 3 | require golang.org/x/tools v0.0.0-20190628222527-fb37f6ba8261 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 2 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 3 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 4 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 5 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 6 | golang.org/x/tools v0.0.0-20190628222527-fb37f6ba8261 h1:KP5slYyJf3GFQbPLTWjQ0TCqBQ73hYpqtCElF+iSruQ= 7 | golang.org/x/tools v0.0.0-20190628222527-fb37f6ba8261/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= 8 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "golang.org/x/tools/go/analysis/multichecker" 5 | 6 | "github.com/daisuzu/gsc/ctxscope" 7 | "github.com/daisuzu/gsc/rangeptr" 8 | ) 9 | 10 | func main() { 11 | multichecker.Main( 12 | ctxscope.Analyzer, 13 | rangeptr.Analyzer, 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /rangeptr/rangeptr.go: -------------------------------------------------------------------------------- 1 | package rangeptr 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "strings" 7 | 8 | "golang.org/x/tools/go/analysis" 9 | "golang.org/x/tools/go/analysis/passes/inspect" 10 | "golang.org/x/tools/go/ast/inspector" 11 | ) 12 | 13 | var Analyzer = &analysis.Analyzer{ 14 | Name: "rangeptr", 15 | Doc: "report using pointer to the loop iteration variable", 16 | Requires: []*analysis.Analyzer{inspect.Analyzer}, 17 | RunDespiteErrors: true, 18 | Run: run, 19 | } 20 | 21 | func run(pass *analysis.Pass) (interface{}, error) { 22 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 23 | nodeFilter := []ast.Node{ 24 | (*ast.AssignStmt)(nil), 25 | } 26 | inspect.Preorder(nodeFilter, func(n ast.Node) { 27 | assign := n.(*ast.AssignStmt) 28 | 29 | scope := pass.Pkg.Scope().Innermost(assign.Pos()).Parent() 30 | if !strings.HasPrefix(scope.String(), "for scope") { 31 | return 32 | } 33 | 34 | var exprs []*ast.UnaryExpr 35 | for _, v := range assign.Rhs { 36 | switch t := v.(type) { 37 | case *ast.UnaryExpr: 38 | exprs = append(exprs, t) 39 | case *ast.CallExpr: 40 | for _, vv := range t.Args { 41 | if ue, ok := vv.(*ast.UnaryExpr); ok { 42 | exprs = append(exprs, ue) 43 | } 44 | } 45 | } 46 | } 47 | 48 | for _, v := range exprs { 49 | if v.Op != token.AND { 50 | continue 51 | } 52 | 53 | ident, ok := v.X.(*ast.Ident) 54 | if !ok { 55 | continue 56 | } 57 | 58 | if obj := scope.Lookup(ident.Name); obj != nil { 59 | pass.Reportf(ident.Pos(), "using pointer to the loop iteration variable %q", obj.Name()) 60 | } 61 | } 62 | }) 63 | 64 | return nil, nil 65 | } 66 | -------------------------------------------------------------------------------- /rangeptr/rangeptr_test.go: -------------------------------------------------------------------------------- 1 | package rangeptr_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "golang.org/x/tools/go/analysis/analysistest" 7 | 8 | "github.com/daisuzu/gsc/rangeptr" 9 | ) 10 | 11 | func Test(t *testing.T) { 12 | testdata := analysistest.TestData() 13 | analysistest.Run(t, testdata, rangeptr.Analyzer, "a") 14 | } 15 | -------------------------------------------------------------------------------- /rangeptr/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | func assignPtr() { 4 | src := []int{1, 2, 3} 5 | dst := make([]*int, len(src)) 6 | for i, v := range src { 7 | dst[i] = &v // want `using pointer to the loop iteration variable "v"` 8 | } 9 | } 10 | 11 | func assignSrcPtr() { 12 | src := []int{1, 2, 3} 13 | dst := make([]*int, len(src)) 14 | for i := range src { 15 | dst[i] = &src[i] 16 | } 17 | } 18 | 19 | func assignTmp() { 20 | src := []int{1, 2, 3} 21 | dst := make([]*int, len(src)) 22 | for i, v := range src { 23 | tmp := v 24 | dst[i] = &tmp 25 | } 26 | } 27 | 28 | func appendPtr() { 29 | src := []int{1, 2, 3} 30 | dst := make([]*int, 0) 31 | for _, v := range src { 32 | dst = append(dst, &v) // want `using pointer to the loop iteration variable "v"` 33 | } 34 | } 35 | 36 | func appendSrcPtr() { 37 | src := []int{1, 2, 3} 38 | dst := make([]*int, 0) 39 | for i := range src { 40 | dst = append(dst, &src[i]) 41 | } 42 | } 43 | 44 | func appendTmp() { 45 | src := []int{1, 2, 3} 46 | dst := make([]*int, 0) 47 | for _, v := range src { 48 | tmp := v 49 | dst = append(dst, &tmp) 50 | } 51 | } 52 | --------------------------------------------------------------------------------