├── .gitignore ├── .gostyle ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── checkstyle.go ├── checkstyle_test.go ├── gocheckstyle ├── config.example └── gocheckstyle.go └── testdata ├── camel_name.go ├── caps_pkg.go ├── fileline.go ├── formated.go ├── functionline.go ├── params_num.go ├── results_num.go ├── underscore_name.go ├── underscore_pkg.go └── unformated.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /.gostyle: -------------------------------------------------------------------------------- 1 | { 2 | "file_line":500, 3 | "func_line":50, 4 | "params_num":4, 5 | "results_num":3, 6 | "formated": true, 7 | "pkg_name": true, 8 | "camel_name": true, 9 | "ignore":[ 10 | "testdata/*", 11 | "temp/*" 12 | ], 13 | "fatal":[ 14 | "formated", 15 | "file_line", 16 | "func_line", 17 | "params_num", 18 | "results_num", 19 | "formated", 20 | "pkg_name", 21 | "camel_name" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | branches: 4 | only: 5 | - master 6 | - develop 7 | 8 | script: go test 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 longbai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | bash -c "mkdir -p temp; cd temp && export GOPATH=`pwd`/temp && go get github.com/qiniu/checkstyle/gocheckstyle" 3 | temp/bin/gocheckstyle -config=.gostyle . 4 | @rm -fr temp 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-checkstyle 2 | ============= 3 | [![Build Status](https://api.travis-ci.org/qiniu/checkstyle.png?branch=master)](https://travis-ci.org/qiniu/checkstyle) 4 | 5 | checkstyle is a style check tool like java checkstyle. This tool inspired by [java checkstyle](https://github.com/checkstyle/checkstyle), [golint](https://github.com/golang/lint). The style refered to some points in [Go Code Review Comments](https://code.google.com/p/go-wiki/wiki/CodeReviewComments). 6 | 7 | # Install 8 | go get github.com/qiniu/checkstyle/gocheckstyle 9 | 10 | # Run 11 | gocheckstyle -config=.go_style dir1 dir2 12 | 13 | # Config 14 | config is json file like the following: 15 | ``` 16 | { 17 | "file_line": 500, 18 | "func_line": 50, 19 | "params_num":4, 20 | "results_num":3, 21 | "formated": true, 22 | "pkg_name": true, 23 | "camel_name":true, 24 | "ignore":[ 25 | "a/*", 26 | "b/*/c/*.go" 27 | ], 28 | "fatal":[ 29 | "formated" 30 | ] 31 | } 32 | 33 | ``` 34 | 35 | # Add to makefile 36 | ``` 37 | check_go_style: 38 | bash -c "mkdir -p checkstyle; cd checkstyle && export GOPATH=`pwd` && go get github.com/qiniu/checkstyle/gocheckstyle" 39 | checkstyle/bin/gocheckstyle -config=.go_style dir1 dir2 40 | 41 | ``` 42 | 43 | # Integrate with jenkins checkstyle plugin 44 | excute in shell 45 | ``` 46 | mkdir -p checkstyle; cd checkstyle && export GOPATH=`pwd` && go get github.com/qiniu/checkstyle/gocheckstyle" 47 | checkstyle/bin/gocheckstyle -reporter=xml -config=.go_style dir1 dir2 2>gostyle.xml 48 | ``` 49 | then add postbuild checkstyle file gostyle.xml 50 | 51 | Run checkstyle with one or more filenames or directories. The output of this tool is a list of suggestions. If you need to force obey the rule, place it in fatal. 52 | 53 | # Checkstyle's difference with other tools 54 | Checkstyle differs from gofmt. Gofmt reformats Go source code, whereas checkstyle prints out coding style suggestion. 55 | 56 | Checkstyle differs from golint. Checkstyle check file line/function line/param number, could be configed by user. 57 | -------------------------------------------------------------------------------- /checkstyle.go: -------------------------------------------------------------------------------- 1 | package checkstyle 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "go/ast" 7 | "go/format" 8 | "go/parser" 9 | "go/token" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | type ProblemType string 15 | 16 | const ( 17 | FileLine ProblemType = "file_line" 18 | FunctionLine ProblemType = "func_line" 19 | ParamsNum ProblemType = "params_num" 20 | ResultsNum ProblemType = "results_num" 21 | Formated ProblemType = "formated" 22 | PackageName ProblemType = "pkg_name" 23 | CamelName ProblemType = "camel_name" 24 | ) 25 | 26 | type Problem struct { 27 | Position *token.Position 28 | Description string 29 | // SourceLine string 30 | Type ProblemType 31 | } 32 | 33 | type Checker interface { 34 | Check(fileName string, src []byte) ([]Problem, error) 35 | IsFatal(p *Problem) bool 36 | } 37 | 38 | type checker struct { 39 | FunctionComment bool `json:"func_comment"` 40 | FileLine int `json:"file_line"` 41 | FunctionLine int `json:"func_line"` 42 | MaxIndent int `json:"max_indent"` 43 | Formated bool `json:"formated"` 44 | Fatal []string `json:"fatal"` 45 | ParamsNum int `json:"params_num"` 46 | ResultsNum int `json:"results_num"` 47 | PackageName bool `json:"pkg_name"` 48 | CamelName bool `json:"camel_name"` 49 | } 50 | 51 | func New(config []byte) (Checker, error) { 52 | var _checker checker 53 | err := json.Unmarshal(config, &_checker) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return &_checker, nil 58 | } 59 | 60 | func (c *checker) Check(fileName string, src []byte) (ps []Problem, err error) { 61 | fset := token.NewFileSet() 62 | f, err := parser.ParseFile(fset, fileName, src, parser.ParseComments) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return (&file{fileName, src, c, f, fset, []Problem{}}).check(), nil 67 | } 68 | 69 | func (c *checker) IsFatal(p *Problem) bool { 70 | for _, v := range c.Fatal { 71 | if v == string(p.Type) { 72 | return true 73 | } 74 | } 75 | return false 76 | } 77 | 78 | type file struct { 79 | fileName string 80 | src []byte 81 | 82 | config *checker 83 | 84 | ast *ast.File 85 | fset *token.FileSet 86 | 87 | problems []Problem 88 | } 89 | 90 | func (f *file) isTest() bool { 91 | return strings.HasSuffix(f.fileName, "_test.go") 92 | } 93 | 94 | func (f *file) check() (ps []Problem) { 95 | f.checkFormat() 96 | f.checkFileLine() 97 | f.checkFileContent() 98 | return f.problems 99 | } 100 | 101 | func (f *file) checkFormat() { 102 | if !f.config.Formated { 103 | return 104 | } 105 | src, err := format.Source(f.src) 106 | if err != nil { 107 | panic(f.fileName + err.Error()) 108 | } 109 | if len(src) != len(f.src) || bytes.Compare(src, f.src) != 0 { 110 | desc := "source is not formated" 111 | pos := f.fset.Position(f.ast.Pos()) 112 | problem := Problem{Description: desc, Position: &pos, Type: Formated} 113 | f.problems = append(f.problems, problem) 114 | } 115 | } 116 | 117 | func (f *file) checkFileLine() { 118 | if f.isTest() { 119 | return 120 | } 121 | 122 | lineLimit := f.config.FileLine 123 | if lineLimit == 0 { 124 | return 125 | } 126 | 127 | f.fset.Iterate(func(_file *token.File) bool { 128 | lineCount := _file.LineCount() 129 | if lineCount > lineLimit { 130 | desc := strconv.Itoa(lineCount) + " lines more than " + strconv.Itoa(lineLimit) 131 | pos := f.fset.Position(f.ast.End()) 132 | problem := Problem{Description: desc, Position: &pos, Type: FileLine} 133 | f.problems = append(f.problems, problem) 134 | } 135 | return true 136 | }) 137 | } 138 | 139 | func genFuncLineProblem(name string, lineCount, lineLimit int, start token.Position) Problem { 140 | desc := "func " + name + "() body lines num " + strconv.Itoa(lineCount) + 141 | " more than " + strconv.Itoa(lineLimit) 142 | return Problem{Description: desc, Position: &start, Type: FunctionLine} 143 | } 144 | 145 | func genParamsNumProblem(name string, paramsNum, limit int, start token.Position) Problem { 146 | desc := "func " + name + "() params num " + strconv.Itoa(paramsNum) + 147 | " more than " + strconv.Itoa(limit) 148 | return Problem{Description: desc, Position: &start, Type: ParamsNum} 149 | } 150 | 151 | func genResultsNumProblem(name string, resultsNum, limit int, start token.Position) Problem { 152 | desc := "func " + name + "() results num " + strconv.Itoa(resultsNum) + 153 | " more than " + strconv.Itoa(limit) 154 | return Problem{Description: desc, Position: &start, Type: ResultsNum} 155 | } 156 | 157 | func genFuncBodyProblem(name string, start token.Position) Problem { 158 | desc := "func " + name + " expected block '{}'" 159 | return Problem{Description: desc, Position: &start, Type: ResultsNum} 160 | } 161 | 162 | func (f *file) checkPkgName(pkg *ast.Ident) { 163 | //ref "http://golang.org/doc/effective_go.html#package-names" 164 | pkgName := pkg.Name 165 | var desc string 166 | if strings.Contains(pkgName, "_") { 167 | suggestName := strings.Replace(pkgName, "_", "/", -1) 168 | desc = "don't use an underscore in package name, " + pkgName + " should be " + suggestName 169 | } else if strings.ToLower(pkgName) != pkgName { 170 | desc = "don't use capital letters in package name: " + pkgName 171 | } 172 | if desc != "" { 173 | start := f.fset.Position(pkg.Pos()) 174 | problem := Problem{Description: desc, Position: &start, Type: PackageName} 175 | f.problems = append(f.problems, problem) 176 | } 177 | } 178 | 179 | func (f *file) checkFunctionParams(fType *ast.FuncType, funcName string) { 180 | paramsNumLimit := f.config.ParamsNum 181 | resultsNumLimit := f.config.ResultsNum 182 | params := fType.Params 183 | if params != nil { 184 | if paramsNumLimit != 0 && params.NumFields() > paramsNumLimit { 185 | start := f.fset.Position(params.Pos()) 186 | problem := genParamsNumProblem(funcName, params.NumFields(), paramsNumLimit, start) 187 | f.problems = append(f.problems, problem) 188 | } 189 | for _, v := range params.List { 190 | for _, pName := range v.Names { 191 | f.checkName(pName, "param", true) 192 | } 193 | } 194 | } 195 | 196 | results := fType.Results 197 | if results != nil { 198 | if resultsNumLimit != 0 && results != nil && results.NumFields() > resultsNumLimit { 199 | start := f.fset.Position(results.Pos()) 200 | problem := genResultsNumProblem(funcName, results.NumFields(), resultsNumLimit, start) 201 | f.problems = append(f.problems, problem) 202 | } 203 | 204 | for _, v := range results.List { 205 | for _, rName := range v.Names { 206 | f.checkName(rName, "return param", true) 207 | } 208 | } 209 | } 210 | } 211 | 212 | func (f *file) checkFunctionLine(funcDecl *ast.FuncDecl) { 213 | lineLimit := f.config.FunctionLine 214 | if lineLimit <= 0 { 215 | return 216 | } 217 | start := f.fset.Position(funcDecl.Pos()) 218 | 219 | startLine := start.Line 220 | endLine := f.fset.Position(funcDecl.End()).Line 221 | lineCount := endLine - startLine 222 | if lineCount > lineLimit { 223 | problem := genFuncLineProblem(funcDecl.Name.Name, lineCount, lineLimit, start) 224 | f.problems = append(f.problems, problem) 225 | } 226 | } 227 | 228 | func (f *file) checkFunctionBody(funcDecl *ast.FuncDecl) { 229 | if funcDecl.Body != nil { 230 | return 231 | } 232 | start := f.fset.Position(funcDecl.Pos()) 233 | problem := genFuncBodyProblem(funcDecl.Name.Name, start) 234 | f.problems = append(f.problems, problem) 235 | } 236 | 237 | func (f *file) checkFunctionDeclare(funcDecl *ast.FuncDecl) { 238 | f.checkFunctionLine(funcDecl) 239 | f.checkName(funcDecl.Name, "func", false) 240 | f.checkFunctionParams(funcDecl.Type, funcDecl.Name.Name) 241 | f.checkFunctionBody(funcDecl) 242 | receiver := funcDecl.Recv 243 | if receiver != nil && len(receiver.List) != 0 && len(receiver.List[0].Names) != 0 { 244 | f.checkName(receiver.List[0].Names[0], "receiver", true) 245 | } 246 | } 247 | 248 | func trimUnderscorePrefix(name string) string { 249 | if name[0] == '_' { 250 | return name[1:] 251 | } 252 | return name 253 | } 254 | 255 | func (f *file) checkName(id *ast.Ident, kind string, notFirstCap bool) { 256 | if !f.config.CamelName { 257 | return 258 | } 259 | name := trimUnderscorePrefix(id.Name) 260 | if name == "" { 261 | return 262 | } 263 | start := f.fset.Position(id.Pos()) 264 | 265 | if strings.Contains(name, "_") { 266 | desc := "don't use non-prefix underscores in " + kind + " name: " + id.Name + ", please use camel name" 267 | problem := Problem{Description: desc, Position: &start, Type: CamelName} 268 | f.problems = append(f.problems, problem) 269 | } else if len(name) >= 5 && strings.ToUpper(name) == name { 270 | desc := "don't use all captial letters in " + kind + " name: " + id.Name + ", please use camel name" 271 | problem := Problem{Description: desc, Position: &start, Type: CamelName} 272 | f.problems = append(f.problems, problem) 273 | } else if notFirstCap && name[0:1] == strings.ToUpper(name[0:1]) { 274 | desc := "in function ,don't use first captial letter in " + kind + " name: " + id.Name + ", please use small letter" 275 | problem := Problem{Description: desc, Position: &start, Type: CamelName} 276 | f.problems = append(f.problems, problem) 277 | } 278 | } 279 | 280 | func (f *file) checkStruct(st *ast.StructType) { 281 | if st.Fields == nil { 282 | return 283 | } 284 | for _, v := range st.Fields.List { 285 | for _, v2 := range v.Names { 286 | f.checkName(v2, "struct field", false) 287 | } 288 | } 289 | } 290 | 291 | func (f *file) checkInterface(it *ast.InterfaceType) { 292 | if it.Methods == nil { 293 | return 294 | } 295 | for _, v := range it.Methods.List { 296 | for _, v2 := range v.Names { 297 | f.checkName(v2, "interface method", false) 298 | } 299 | if v3, ok := v.Type.(*ast.FuncType); ok { 300 | f.checkFunctionParams(v3, v.Names[0].Name) 301 | } 302 | } 303 | } 304 | 305 | func (f *file) checkValueName(decl *ast.GenDecl, kind string, top bool) { 306 | for _, spec := range decl.Specs { 307 | if vSpec, ok := spec.(*ast.ValueSpec); ok { 308 | for _, name := range vSpec.Names { 309 | f.checkName(name, kind, !top) 310 | } 311 | } else if tSpec, ok := spec.(*ast.TypeSpec); ok { 312 | f.checkName(tSpec.Name, kind, false) 313 | ast.Inspect(tSpec.Type, func(node ast.Node) bool { 314 | switch decl2 := node.(type) { 315 | case *ast.GenDecl: 316 | f.checkGenDecl(decl2, false) 317 | case *ast.FuncDecl: 318 | f.checkFunctionDeclare(decl2) 319 | case *ast.StructType: 320 | f.checkStruct(decl2) 321 | case *ast.InterfaceType: 322 | f.checkInterface(decl2) 323 | } 324 | return true 325 | }) 326 | } else if iSpec, ok := spec.(*ast.ImportSpec); ok && iSpec.Name != nil { 327 | f.checkName(iSpec.Name, "import", true) 328 | } 329 | } 330 | } 331 | 332 | func (f *file) checkGenDecl(decl *ast.GenDecl, top bool) { 333 | if decl.Tok == token.CONST { 334 | f.checkValueName(decl, "const", top) 335 | } else if decl.Tok == token.VAR { 336 | f.checkValueName(decl, "var", top) 337 | } else if decl.Tok == token.TYPE { 338 | f.checkValueName(decl, "type", top) 339 | } else if decl.Tok == token.IMPORT { 340 | f.checkValueName(decl, "import", true) 341 | } 342 | } 343 | 344 | func (f *file) checkAssign(assign *ast.AssignStmt) { 345 | if assign.Tok != token.DEFINE { 346 | return 347 | } 348 | 349 | for _, v2 := range assign.Lhs { 350 | if assignName, ok := v2.(*ast.Ident); ok { 351 | f.checkName(assignName, "var", true) 352 | } 353 | } 354 | } 355 | 356 | func (f *file) checkFileContent() { 357 | if f.isTest() { 358 | return 359 | } 360 | 361 | if f.config.PackageName { 362 | f.checkPkgName(f.ast.Name) 363 | } 364 | 365 | for _, v := range f.ast.Decls { 366 | switch decl := v.(type) { 367 | case *ast.FuncDecl: 368 | f.checkFunctionDeclare(decl) 369 | if decl.Body == nil { 370 | break 371 | } 372 | ast.Inspect(decl.Body, func(node ast.Node) bool { 373 | switch decl2 := node.(type) { 374 | case *ast.GenDecl: 375 | f.checkGenDecl(decl2, false) 376 | case *ast.FuncDecl: 377 | f.checkFunctionDeclare(decl2) 378 | case *ast.AssignStmt: 379 | f.checkAssign(decl2) 380 | case *ast.StructType: 381 | f.checkStruct(decl2) 382 | } 383 | return true 384 | }) 385 | case *ast.GenDecl: 386 | f.checkGenDecl(decl, true) 387 | } 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /checkstyle_test.go: -------------------------------------------------------------------------------- 1 | package checkstyle 2 | 3 | import ( 4 | "go/parser" 5 | "go/token" 6 | "io/ioutil" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | const baseDir = "testdata/" 12 | 13 | func readFile(fileName string) []byte { 14 | file, _ := ioutil.ReadFile(baseDir + fileName) 15 | return file 16 | } 17 | 18 | func TestFileLine(t *testing.T) { 19 | fileName := "fileline.go" 20 | file := readFile(fileName) 21 | _checkerOk := checker{FileLine: 9} 22 | ps, err := _checkerOk.Check(fileName, file) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | if len(ps) != 0 { 27 | t.Fatal("expect no error") 28 | } 29 | 30 | _checkerFail := checker{FileLine: 8} 31 | ps, _ = _checkerFail.Check(fileName, file) 32 | if len(ps) != 1 || ps[0].Type != FileLine { 33 | t.Fatal("expect an error") 34 | } 35 | 36 | //pos is at file end 37 | fset := token.NewFileSet() 38 | f, _ := parser.ParseFile(fset, fileName, file, parser.ParseComments) 39 | if reflect.DeepEqual(ps[0], fset.Position(f.End())) { 40 | t.Fatal("file line problem position not match") 41 | } 42 | } 43 | 44 | func TestFunctionLine(t *testing.T) { 45 | fileName := "functionline.go" 46 | file := readFile(fileName) 47 | _checkerOk := checker{FunctionLine: 9} 48 | ps, err := _checkerOk.Check(fileName, file) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | if len(ps) != 0 { 53 | t.Fatal("expect no error") 54 | } 55 | 56 | _checkerFail := checker{FunctionLine: 8} 57 | ps, _ = _checkerFail.Check(fileName, file) 58 | if len(ps) != 1 || ps[0].Type != FunctionLine { 59 | t.Fatal("expect an error") 60 | } 61 | 62 | if ps[0].Position.Filename != fileName { 63 | t.Fatal("file name is not correct") 64 | } 65 | 66 | if ps[0].Position.Line != 7 { 67 | t.Fatal("start position is not correct") 68 | } 69 | } 70 | 71 | func TestParamsNum(t *testing.T) { 72 | fileName := "params_num.go" 73 | file := readFile(fileName) 74 | _checkerOk := checker{ParamsNum: 4} 75 | ps, err := _checkerOk.Check(fileName, file) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | if len(ps) != 0 { 80 | t.Fatal("expect no error") 81 | } 82 | 83 | _checkerFail := checker{ParamsNum: 3} 84 | ps, _ = _checkerFail.Check(fileName, file) 85 | if len(ps) != 1 || ps[0].Type != ParamsNum { 86 | t.Fatal("expect an error") 87 | } 88 | 89 | if ps[0].Position.Filename != fileName { 90 | t.Fatal("file name is not correct") 91 | } 92 | 93 | if ps[0].Position.Line != 7 { 94 | t.Fatal("start position is not correct") 95 | } 96 | 97 | _checkerFail = checker{ParamsNum: 2} 98 | ps, _ = _checkerFail.Check(fileName, file) 99 | if len(ps) != 2 { 100 | t.Fatal("expect 2 error") 101 | } 102 | } 103 | 104 | func TestResulsNum(t *testing.T) { 105 | fileName := "results_num.go" 106 | file := readFile(fileName) 107 | _checkerOk := checker{ResultsNum: 4} 108 | ps, err := _checkerOk.Check(fileName, file) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | if len(ps) != 0 { 113 | t.Fatal("expect no error") 114 | } 115 | 116 | _checkerFail := checker{ResultsNum: 3} 117 | ps, _ = _checkerFail.Check(fileName, file) 118 | if len(ps) != 1 || ps[0].Type != ResultsNum { 119 | t.Fatal("expect an error") 120 | } 121 | 122 | if ps[0].Position.Filename != fileName { 123 | t.Fatal("file name is not correct") 124 | } 125 | 126 | if ps[0].Position.Line != 7 { 127 | t.Fatal("start position is not correct") 128 | } 129 | 130 | _checkerFail = checker{ResultsNum: 2} 131 | ps, _ = _checkerFail.Check(fileName, file) 132 | if len(ps) != 2 { 133 | t.Fatal("expect 2 error") 134 | } 135 | } 136 | 137 | func TestFormated(t *testing.T) { 138 | fileName := "formated.go" 139 | file := readFile(fileName) 140 | _checker := checker{Formated: true} 141 | ps, err := _checker.Check(fileName, file) 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | if len(ps) != 0 { 146 | t.Fatal("expect no error") 147 | } 148 | 149 | fileName = "unformated.go" 150 | file = readFile(fileName) 151 | ps, _ = _checker.Check(fileName, file) 152 | if len(ps) != 1 || ps[0].Type != Formated { 153 | t.Fatal("expect an error") 154 | } 155 | 156 | //pos is at file begin 157 | fset := token.NewFileSet() 158 | f, _ := parser.ParseFile(fset, fileName, file, parser.ParseComments) 159 | if reflect.DeepEqual(ps[0], fset.Position(f.Pos())) { 160 | t.Fatal("file line problem position not match") 161 | } 162 | } 163 | 164 | func TestPackageName(t *testing.T) { 165 | fileName := "caps_pkg.go" 166 | file := readFile(fileName) 167 | _checker := checker{PackageName: false} 168 | ps, err := _checker.Check(fileName, file) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | if len(ps) != 0 { 173 | t.Fatal("expect no error") 174 | } 175 | fileName = "underscore_pkg.go" 176 | file = readFile(fileName) 177 | ps, err = _checker.Check(fileName, file) 178 | if err != nil { 179 | t.Fatal(err) 180 | } 181 | if len(ps) != 0 { 182 | t.Fatal("expect no error") 183 | } 184 | 185 | fileName = "caps_pkg.go" 186 | file = readFile(fileName) 187 | _checkerFail := checker{PackageName: true} 188 | ps, err = _checkerFail.Check(fileName, file) 189 | if err != nil { 190 | t.Fatal(err) 191 | } 192 | if len(ps) == 0 { 193 | t.Fatal("expect 1 error") 194 | } 195 | fileName = "underscore_pkg.go" 196 | file = readFile(fileName) 197 | ps, err = _checkerFail.Check(fileName, file) 198 | if err != nil { 199 | t.Fatal(err) 200 | } 201 | if len(ps) == 0 { 202 | t.Fatal("expect 1 error") 203 | } 204 | } 205 | 206 | func TestCamelName(t *testing.T) { 207 | fileName := "underscore_name.go" 208 | file := readFile(fileName) 209 | _checker := checker{CamelName: false} 210 | ps, err := _checker.Check(fileName, file) 211 | if err != nil { 212 | t.Fatal(err) 213 | } 214 | if len(ps) != 0 { 215 | t.Fatal("expect no error") 216 | } 217 | _checkerFail := checker{CamelName: true} 218 | ps, err = _checkerFail.Check(fileName, file) 219 | if err != nil { 220 | t.Fatal(err) 221 | } 222 | if len(ps) != 30 { 223 | t.Fatal("expect 30 error but ", len(ps)) 224 | } 225 | fileName = "camel_name.go" 226 | file = readFile(fileName) 227 | ps, err = _checkerFail.Check(fileName, file) 228 | if err != nil { 229 | t.Fatal(err) 230 | } 231 | if len(ps) != 0 { 232 | t.Fatal("expect no error") 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /gocheckstyle/config.example: -------------------------------------------------------------------------------- 1 | { 2 | "file_line":200, 3 | "_file_line_comment": "file line count limit", 4 | "func_line":50, 5 | "_func_line_comment": "function line count limit", 6 | "params_num":4, 7 | "_params_num_comment": "function parameter count limit", 8 | "results_num":3, 9 | "_results_num_comment": "function return variable count limit", 10 | "formated": true, 11 | "_formated_comment": "gofmt", 12 | "pkg_name": true, 13 | "_pkg_name_comment": "package name should not contain _ and camel", 14 | "camel_name": true, 15 | "_camel_name_comment": "const/var/function/import name should use camel name", 16 | "ignore":[ 17 | "tmp/*", 18 | "src/tmp.go" 19 | ], 20 | "_ignore_comment":"ignore file", 21 | "fatal":[ 22 | "formated" 23 | ], 24 | "_fatal_comment": "put the check rule of error level here" 25 | } 26 | -------------------------------------------------------------------------------- /gocheckstyle/gocheckstyle.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "flag" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/qiniu/checkstyle" 14 | ) 15 | 16 | const defaultConfig = `{ 17 | "file_line":200, 18 | "_file_line_comment": "file line count limit", 19 | "func_line":50, 20 | "_func_line_comment": "function line count limit", 21 | "params_num":4, 22 | "_params_num_comment": "function parameter count limit", 23 | "results_num":3, 24 | "_results_num_comment": "function return variable count limit", 25 | "formated": true, 26 | "_formated_comment": "gofmt", 27 | "pkg_name": true, 28 | "_pkg_name_comment": "package name should not contain _ and camel", 29 | "camel_name": true, 30 | "_camel_name_comment": "const/var/function/import name should use camel name", 31 | "ignore":[ 32 | "tmp/*", 33 | "src/tmp.go" 34 | ], 35 | "_ignore_comment":"ignore file", 36 | "fatal":[ 37 | "formated" 38 | ], 39 | "_fatal_comment": "put the check rule of error level here" 40 | }` 41 | 42 | var config = flag.String("config", "", "config json file") 43 | var reporterOption = flag.String("reporter", "plain", "report output format, plain or xml") 44 | 45 | var checker checkstyle.Checker 46 | var reporter Reporter 47 | 48 | type Ignore struct { 49 | Files []string `json:"ignore"` 50 | } 51 | 52 | var ignore Ignore 53 | 54 | type Reporter interface { 55 | ReceiveProblems(checker checkstyle.Checker, file string, problems []checkstyle.Problem) 56 | Report() 57 | } 58 | 59 | type plainReporter struct { 60 | normalProblems []*checkstyle.Problem 61 | fatalProblems []*checkstyle.Problem 62 | } 63 | 64 | func (_ *plainReporter) printProblems(ps []*checkstyle.Problem) { 65 | for _, p := range ps { 66 | log.Printf("%v: %s\n", p.Position, p.Description) 67 | } 68 | } 69 | 70 | func (p *plainReporter) Report() { 71 | if len(p.normalProblems) != 0 { 72 | log.Printf(" ========= There are %d normal problems ========= \n", len(p.normalProblems)) 73 | p.printProblems(p.normalProblems) 74 | } 75 | 76 | if len(p.fatalProblems) != 0 { 77 | log.Printf(" ========= There are %d fatal problems ========= \n", len(p.fatalProblems)) 78 | p.printProblems(p.fatalProblems) 79 | os.Exit(1) 80 | } 81 | if len(p.normalProblems) == 0 && len(p.fatalProblems) == 0 { 82 | log.Println(" ========= There are no problems ========= ") 83 | } 84 | } 85 | 86 | func (p *plainReporter) ReceiveProblems(checker checkstyle.Checker, file string, problems []checkstyle.Problem) { 87 | for i, problem := range problems { 88 | if checker.IsFatal(&problem) { 89 | p.fatalProblems = append(p.fatalProblems, &problems[i]) 90 | } else { 91 | p.normalProblems = append(p.normalProblems, &problems[i]) 92 | } 93 | } 94 | } 95 | 96 | type xmlReporter struct { 97 | problems map[string][]checkstyle.Problem 98 | hasFatal bool 99 | } 100 | 101 | func (x *xmlReporter) printProblems(ps []checkstyle.Problem) { 102 | format := "\t\t\n" 103 | for _, p := range ps { 104 | severity := "warning" 105 | if checker.IsFatal(&p) { 106 | severity = "error" 107 | x.hasFatal = true 108 | } 109 | log.Printf(format, p.Position.Line, p.Position.Column, severity, p.Description, p.Type) 110 | } 111 | } 112 | 113 | func (x *xmlReporter) Report() { 114 | log.SetFlags(0) 115 | log.Print(xml.Header) 116 | log.Println(``) 117 | for k, v := range x.problems { 118 | log.Printf("\t\n", k) 119 | x.printProblems(v) 120 | log.Println("\t") 121 | } 122 | log.Println("") 123 | if x.hasFatal { 124 | os.Exit(1) 125 | } 126 | } 127 | 128 | func (x *xmlReporter) ReceiveProblems(checker checkstyle.Checker, file string, problems []checkstyle.Problem) { 129 | if len(problems) == 0 { 130 | return 131 | } 132 | x.problems[file] = problems 133 | } 134 | 135 | func main() { 136 | flag.Parse() 137 | 138 | files := flag.Args() 139 | 140 | if reporterOption == nil || *reporterOption != "xml" { 141 | reporter = &plainReporter{} 142 | } else { 143 | reporter = &xmlReporter{problems: map[string][]checkstyle.Problem{}} 144 | } 145 | var err error 146 | var conf []byte 147 | if *config == "" { 148 | conf = []byte(defaultConfig) 149 | } else { 150 | conf, err = ioutil.ReadFile(*config) 151 | if err != nil { 152 | log.Fatalf("Open config %v fail %v\n", *config, err) 153 | } 154 | } 155 | err = json.Unmarshal(conf, &ignore) 156 | if err != nil { 157 | log.Fatalf("Parse config %v fail \n", *config, err) 158 | } 159 | checker, err = checkstyle.New(conf) 160 | if err != nil { 161 | log.Fatalf("New checker fail %v\n", err) 162 | } 163 | 164 | if len(files) == 0 { 165 | files = []string{"."} 166 | } 167 | for _, v := range files { 168 | if isDir(v) { 169 | checkDir(v) 170 | } else { 171 | checkFile(v) 172 | } 173 | } 174 | reporter.Report() 175 | } 176 | 177 | func isDir(filename string) bool { 178 | fi, err := os.Stat(filename) 179 | return err == nil && fi.IsDir() 180 | } 181 | 182 | func checkFile(fileName string) { 183 | file, err := ioutil.ReadFile(fileName) 184 | if err != nil { 185 | log.Fatalf("Read File Fail %v %v\n", fileName, err) 186 | } 187 | 188 | ps, err := checker.Check(fileName, file) 189 | if err != nil { 190 | log.Fatalf("Parse File Fail %v %v\n", fileName, err) 191 | } 192 | 193 | reporter.ReceiveProblems(checker, fileName, ps) 194 | } 195 | 196 | func isIgnoreFile(fileName string) bool { 197 | for _, v := range ignore.Files { 198 | if ok, _ := filepath.Match(v, fileName); ok { 199 | return true 200 | } 201 | } 202 | return false 203 | } 204 | 205 | func isIgnoreDir(dir string) bool { 206 | for _, v := range ignore.Files { 207 | if ok, _ := filepath.Match(v, dir); ok { 208 | return true 209 | } 210 | } 211 | return false 212 | } 213 | 214 | func checkDir(dir string) { 215 | if isIgnoreDir(dir) { 216 | return 217 | } 218 | filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 219 | if err == nil && info.IsDir() && isIgnoreDir(path) { 220 | return filepath.SkipDir 221 | } 222 | if err == nil && !info.IsDir() && strings.HasSuffix(path, ".go") && !isIgnoreFile(path) { 223 | checkFile(path) 224 | } 225 | return err 226 | }) 227 | } 228 | -------------------------------------------------------------------------------- /testdata/camel_name.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import "fmt" 4 | 5 | const ( 6 | Abc = 0 7 | _Abc = 0 8 | ) 9 | 10 | const _AB = 0 11 | 12 | var _Bc = 0 13 | 14 | var ( 15 | _CdE = 0 16 | _DE = 0 17 | ) 18 | 19 | type _Ac struct { 20 | AAAA int 21 | _BBBB int 22 | _AAAAa int 23 | } 24 | 25 | type x interface { 26 | } 27 | 28 | func (xz *_Ac) HelloWorld(ab, _ab int) (bc int) { 29 | var _xyz int 30 | fmt.Println(_xyz) 31 | return 32 | } 33 | 34 | func (_ *_Ac) helloWorld(ab, _ab int) (bc int) { 35 | var _xyz int 36 | fmt.Println(_xyz) 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /testdata/caps_pkg.go: -------------------------------------------------------------------------------- 1 | package testData 2 | -------------------------------------------------------------------------------- /testdata/fileline.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func hello() { 8 | fmt.Println("hello") 9 | } 10 | -------------------------------------------------------------------------------- /testdata/formated.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func hello() { 8 | fmt.Println("hello") 9 | } 10 | -------------------------------------------------------------------------------- /testdata/functionline.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func hello() { 8 | fmt.Println("hello1") 9 | fmt.Println("hello2") 10 | fmt.Println("hello3") 11 | fmt.Println("hello4") 12 | fmt.Println("hello5") 13 | fmt.Println("hello6") 14 | fmt.Println("hello7") 15 | fmt.Println("hello8") 16 | } 17 | -------------------------------------------------------------------------------- /testdata/params_num.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func hello(a, b, c, _ int) { 8 | fmt.Println("hello") 9 | } 10 | 11 | func hello2(a, b int, c ...int) { 12 | fmt.Println("... args") 13 | } 14 | 15 | type z int 16 | 17 | func (_ *z) hello(a, b int) { 18 | fmt.Println("one receiver") 19 | } 20 | -------------------------------------------------------------------------------- /testdata/results_num.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func hello(a, b int) (int, int, int, int) { 8 | fmt.Println("hello") 9 | } 10 | 11 | func hello2(a, b int, c ...int) (int, int, int) { 12 | fmt.Println("... args") 13 | } 14 | 15 | type z int 16 | 17 | func (_ *z) hello3(a, b, c int) (int, int) { 18 | fmt.Println("receiver") 19 | } 20 | -------------------------------------------------------------------------------- /testdata/underscore_name.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | Fmt "fmt" 5 | ) 6 | 7 | const ( 8 | A_B_C = 0 9 | ) 10 | 11 | const A_B = 0 12 | 13 | var B_C = 0 14 | 15 | var ( 16 | _C_D = 0 17 | _D_E = 0 18 | __ = 0 19 | __a = 0 20 | ) 21 | 22 | type A_C struct { 23 | AAAAA int 24 | _BBBB int 25 | } 26 | 27 | type I_A interface { 28 | F_A(P_A int) (R_A int) 29 | F_B(int) int 30 | } 31 | 32 | func (Z *A_C) Hello_World(_B, A int) (_B_C, B int) { 33 | var X_Y_Z, _C int 34 | _C = 1 35 | Fmt.Println(X_Y_Z, __C) 36 | Zx := struct { 37 | S_A int 38 | }{A_B} 39 | Fmt.Println(Zx) 40 | F := func() { 41 | TX, TY := 0 42 | Fmt.Println("TX") 43 | } 44 | for I := 0; i < 1; i++ { 45 | } 46 | if X := _C; X == 1 { 47 | } 48 | F() 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /testdata/underscore_pkg.go: -------------------------------------------------------------------------------- 1 | package test_data 2 | -------------------------------------------------------------------------------- /testdata/unformated.go: -------------------------------------------------------------------------------- 1 | package testdata 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func hello() { 8 | fmt.Println("hello")} 9 | --------------------------------------------------------------------------------