├── .travis.yml ├── LICENSE ├── README.md ├── main.go ├── main_test.go └── vim ├── README.md └── plugin └── abcgo.vim /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.8 5 | - 1.9 6 | 7 | script: 8 | - go test ./... 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sergey Novikov 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 | # ABCGo 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/droptheplot/abcgo)](https://goreportcard.com/report/github.com/droptheplot/abcgo) 4 | [![Build Status](https://travis-ci.org/droptheplot/abcgo.svg?branch=master)](https://travis-ci.org/droptheplot/abcgo) 5 | [![GoDoc](https://godoc.org/github.com/droptheplot/abcgo?status.svg)](https://godoc.org/github.com/droptheplot/abcgo) 6 | 7 | ABC metrics for Go source code. 8 | 9 | ## Definition 10 | 11 | ABCGo uses these rules to calculate ABC: 12 | 13 | * Add one to the **assignment** count when: 14 | * Occurrence of an assignment operator: `=`, `*=`, `/=`, `%=`, `+=`, `<<=`, `>>=`, `&=`, `^=`. 15 | * Occurrence of an increment or a decrement operator: `++`, `--`. 16 | * Add one to **branch** count when: 17 | * Occurrence of a function call. 18 | * Add one to **condition** count when: 19 | * Occurrence of a conditional operator: `<`, `>`, `<=`, `>=`, `==`, `!=`. 20 | * Occurrence of the following keywords: `else`, `case`. 21 | 22 | Final score is calculated as follows: 23 | 24 | 25 | 26 | [Read more about ABC metrics.](https://en.wikipedia.org/wiki/ABC_Software_Metric) 27 | 28 | ## Getting Started 29 | 30 | ### Installation 31 | 32 | ```shell 33 | $ go get -u github.com/droptheplot/abcgo 34 | $ (cd $GOPATH/src/github.com/droptheplot/abcgo && go install) 35 | ``` 36 | 37 | ### Usage 38 | 39 | #### Single file 40 | 41 | ```shell 42 | $ abcgo -path main.go 43 | Source Func Score A B C 44 | main.go:28 init 9 1 8 5 45 | main.go:54 main 13 5 13 1 46 | ``` 47 | 48 | #### Directory 49 | 50 | ```shell 51 | $ abcgo -path ./ 52 | Source Func Score A B C 53 | main.go:28 init 9 1 8 5 54 | main.go:54 main 13 5 13 1 55 | main_test.go:54 TestSomething 9 0 9 2 56 | ``` 57 | 58 | #### JSON 59 | 60 | ```shell 61 | $ abcgo -path main.go -format json 62 | [ 63 | { 64 | "path": "main.go", 65 | "line": 54, 66 | "name": "main", 67 | "assignment": 5, 68 | "branch": 13, 69 | "condition": 1, 70 | "score": 13 71 | }, 72 | { 73 | "path": "main.go", 74 | "line": 54, 75 | "name": "init", 76 | "assignment": 1, 77 | "branch": 8, 78 | "condition": 5, 79 | "score": 9 80 | } 81 | ] 82 | ``` 83 | 84 | #### Raw 85 | 86 | *(source, line, function name, score)* 87 | 88 | ```shell 89 | $ abcgo -path main.go -format raw 90 | main.go 28 init 9 91 | main.go 54 main 13 92 | main_test.go 54 TestSomething 9 93 | ``` 94 | 95 | #### Summary 96 | ```shell 97 | $ abcgo -path ./ -format summary 98 | A B C 99 | Project summary: 22 43 15 100 | ``` 101 | 102 | ### Options 103 | 104 | * `-path [path]` - Path to file or directory. 105 | * `-format [format]` - Output format (`table` (default), `raw` or `json`). 106 | * `-sort` - Sort functions by score. 107 | * `-no-test` - Skip `*_test.go` files. 108 | 109 | ### Plugins 110 | 111 | * [Vim](https://github.com/droptheplot/abcgo/vim) 112 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "go/ast" 8 | "go/parser" 9 | "go/token" 10 | "math" 11 | "os" 12 | "path/filepath" 13 | "sort" 14 | "text/tabwriter" 15 | ) 16 | 17 | // Reports is a collection of Report. 18 | type Reports []Report 19 | 20 | // Report contains statistics for single function. 21 | type Report struct { 22 | Path string `json:"path"` 23 | Line int `json:"line"` 24 | Name string `json:"name"` 25 | Assignment int `json:"assignment"` 26 | Branch int `json:"branch"` 27 | Condition int `json:"condition"` 28 | Score int `json:"score"` 29 | } 30 | 31 | func main() { 32 | var ( 33 | path string 34 | format string 35 | sort bool 36 | noTest bool 37 | reports Reports 38 | ) 39 | 40 | flag.StringVar(&path, "path", "", "Path to file") 41 | flag.StringVar(&format, "format", "table", "Output format") 42 | flag.BoolVar(&sort, "sort", false, "Sort by score") 43 | flag.BoolVar(&noTest, "no-test", false, "Skip *_test.go files") 44 | 45 | flag.Parse() 46 | 47 | if path == "" { 48 | flag.PrintDefaults() 49 | os.Exit(1) 50 | } 51 | 52 | fileList := listFiles(path) 53 | fileSet := token.NewFileSet() 54 | 55 | for _, path := range fileList { 56 | node, err := parser.ParseFile(fileSet, path, nil, 0) 57 | 58 | if err != nil { 59 | continue 60 | } 61 | 62 | if noTest && isTest(path) { 63 | continue 64 | } 65 | 66 | reports = append(reports, reportFile(fileSet, node)...) 67 | } 68 | 69 | if sort { 70 | reports.Sort() 71 | } 72 | 73 | switch format { 74 | case "summary": 75 | reports.renderSummary() 76 | case "table": 77 | reports.renderTable() 78 | case "json": 79 | reports.renderJSON() 80 | case "raw": 81 | reports.renderRaw() 82 | default: 83 | panic("unknown format.") 84 | } 85 | } 86 | 87 | func listFiles(path string) []string { 88 | var fileList []string 89 | 90 | fileInfo, _ := os.Stat(path) 91 | 92 | appendAbsPath := func(p string) { 93 | p, _ = filepath.Abs(p) 94 | fileList = append(fileList, p) 95 | } 96 | 97 | if fileInfo.IsDir() { 98 | filepath.Walk(path, func(p string, f os.FileInfo, err error) error { 99 | if filepath.Ext(f.Name()) == ".go" { 100 | appendAbsPath(p) 101 | } 102 | 103 | return nil 104 | }) 105 | } else { 106 | appendAbsPath(fileInfo.Name()) 107 | } 108 | 109 | return fileList 110 | } 111 | 112 | func reportFile(fset *token.FileSet, n ast.Node) Reports { 113 | var reports Reports 114 | 115 | ast.Inspect(n, func(n ast.Node) bool { 116 | if fn, ok := n.(*ast.FuncDecl); ok { 117 | report := Report{ 118 | Path: fset.File(fn.Pos()).Name(), 119 | Line: fset.Position(fn.Pos()).Line, 120 | Name: fn.Name.Name, 121 | } 122 | 123 | ast.Inspect(n, func(n ast.Node) bool { 124 | reportNode(&report, n) 125 | return true 126 | }) 127 | 128 | report.Calc() 129 | reports = append(reports, report) 130 | return false 131 | } 132 | return true 133 | }) 134 | 135 | return reports 136 | } 137 | 138 | func reportNode(report *Report, n ast.Node) { 139 | switch n := n.(type) { 140 | case *ast.AssignStmt, *ast.IncDecStmt: 141 | report.Assignment++ 142 | case *ast.CallExpr: 143 | report.Branch++ 144 | case *ast.IfStmt: 145 | if n.Else != nil { 146 | report.Condition++ 147 | } 148 | case *ast.BinaryExpr, *ast.CaseClause: 149 | report.Condition++ 150 | } 151 | } 152 | 153 | func (report Report) String() string { 154 | return fmt.Sprintf( 155 | "%s:%d\t%s\t%d\t%d\t%d\t%d", 156 | report.Path, 157 | report.Line, 158 | report.Name, 159 | report.Score, 160 | report.Assignment, 161 | report.Branch, 162 | report.Condition, 163 | ) 164 | } 165 | 166 | // Calc updates Score value. 167 | func (report *Report) Calc() { 168 | a := math.Pow(float64(report.Assignment), 2) 169 | b := math.Pow(float64(report.Branch), 2) 170 | c := math.Pow(float64(report.Condition), 2) 171 | 172 | report.Score = int(math.Sqrt(a + b + c)) 173 | } 174 | 175 | func (reports Reports) renderSummary() { 176 | w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) 177 | defer w.Flush() 178 | a, b, c := 0, 0, 0 179 | for _, report := range reports { 180 | a = a + report.Assignment 181 | b = b + report.Branch 182 | c = c + report.Condition 183 | } 184 | 185 | fmt.Fprintln(w, "\tA\tB\tC") 186 | fmt.Fprintf(w, "%s\t%d\t%d\t%d\n", "Project summary:", a, b, c) 187 | } 188 | 189 | func (reports Reports) renderTable() { 190 | w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) 191 | defer w.Flush() 192 | 193 | fmt.Fprintln(w, "Source\tFunc\tScore\tA\tB\tC") 194 | 195 | for _, report := range reports { 196 | fmt.Fprintln(w, report.String()) 197 | } 198 | } 199 | 200 | func (reports Reports) renderJSON() { 201 | bytes, err := json.Marshal(reports) 202 | 203 | if err != nil { 204 | fmt.Println(err) 205 | } 206 | 207 | os.Stdout.Write(bytes) 208 | } 209 | 210 | func (reports Reports) renderRaw() { 211 | for _, report := range reports { 212 | fmt.Printf( 213 | "%s\t%d\t%s\t%d\n", 214 | report.Path, 215 | report.Line, 216 | report.Name, 217 | report.Score, 218 | ) 219 | } 220 | } 221 | 222 | func (reports Reports) Sort() { 223 | sort.Slice(reports, func(i, j int) bool { 224 | return reports[i].Score > reports[j].Score 225 | }) 226 | } 227 | 228 | func isTest(p string) bool { 229 | match, err := filepath.Match("*_test.go", filepath.Base(p)) 230 | 231 | return match && err == nil 232 | } 233 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/parser" 5 | "go/token" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestReportFile(t *testing.T) { 12 | fileSet := token.NewFileSet() 13 | 14 | testCases := []struct { 15 | rule string 16 | report Report 17 | src string 18 | }{ 19 | { 20 | "=", 21 | Report{Assignment: 1}, 22 | ` 23 | package main 24 | 25 | func main() { 26 | bar := 1 27 | } 28 | `, 29 | }, 30 | { 31 | "*=", 32 | Report{Assignment: 1}, 33 | ` 34 | package main 35 | 36 | func main() { 37 | bar *= 1 38 | } 39 | `, 40 | }, 41 | { 42 | "/=", 43 | Report{Assignment: 1}, 44 | ` 45 | package main 46 | 47 | func main() { 48 | bar /= 1 49 | } 50 | `, 51 | }, 52 | { 53 | "%=", 54 | Report{Assignment: 1}, 55 | ` 56 | package main 57 | 58 | func main() { 59 | bar %= 1 60 | } 61 | `, 62 | }, 63 | { 64 | "+=", 65 | Report{Assignment: 1}, 66 | ` 67 | package main 68 | 69 | func main() { 70 | bar += 1 71 | } 72 | `, 73 | }, 74 | { 75 | "<<=", 76 | Report{Assignment: 1}, 77 | ` 78 | package main 79 | 80 | func main() { 81 | bar <<= 1 82 | } 83 | `, 84 | }, 85 | { 86 | ">>=", 87 | Report{Assignment: 1}, 88 | ` 89 | package main 90 | 91 | func main() { 92 | bar >>= 1 93 | } 94 | `, 95 | }, 96 | { 97 | "&=", 98 | Report{Assignment: 1}, 99 | ` 100 | package main 101 | 102 | func main() { 103 | bar &= 1 104 | } 105 | `, 106 | }, 107 | { 108 | "^=", 109 | Report{Assignment: 1}, 110 | ` 111 | package main 112 | 113 | func main() { 114 | bar ^= 1 115 | } 116 | `, 117 | }, 118 | { 119 | "++", 120 | Report{Assignment: 1}, 121 | ` 122 | package main 123 | 124 | func main() { 125 | bar++ 126 | } 127 | `, 128 | }, 129 | { 130 | "--", 131 | Report{Assignment: 1}, 132 | ` 133 | package main 134 | 135 | func main() { 136 | bar-- 137 | } 138 | `, 139 | }, 140 | { 141 | "Function call", 142 | Report{Branch: 1}, 143 | ` 144 | package main 145 | 146 | func main() { 147 | bar() 148 | } 149 | `, 150 | }, 151 | { 152 | "Anonymous function call", 153 | Report{Branch: 1}, 154 | ` 155 | package main 156 | 157 | func main() { 158 | func() {}() 159 | } 160 | `, 161 | }, 162 | { 163 | "Function call inside anonymous function", 164 | Report{Branch: 1}, 165 | ` 166 | package main 167 | 168 | func main() { 169 | func() { 170 | bar() 171 | } 172 | } 173 | `, 174 | }, 175 | { 176 | "Function call inside anonymous function call", 177 | Report{Branch: 2}, 178 | ` 179 | package main 180 | 181 | func main() { 182 | func() { 183 | bar() 184 | }() 185 | } 186 | `, 187 | }, 188 | { 189 | ">", 190 | Report{Condition: 1}, 191 | ` 192 | package main 193 | 194 | func main() { 195 | if 1 > 2 {} 196 | } 197 | `, 198 | }, 199 | { 200 | "<", 201 | Report{Condition: 1}, 202 | ` 203 | package main 204 | 205 | func main() { 206 | if 1 < 2 {} 207 | } 208 | `, 209 | }, 210 | { 211 | "<=", 212 | Report{Condition: 1}, 213 | ` 214 | package main 215 | 216 | func main() { 217 | if 1 <= 2 {} 218 | } 219 | `, 220 | }, 221 | { 222 | ">=", 223 | Report{Condition: 1}, 224 | ` 225 | package main 226 | 227 | func main() { 228 | if 1 >= 2 {} 229 | } 230 | `, 231 | }, 232 | { 233 | "==", 234 | Report{Condition: 1}, 235 | ` 236 | package main 237 | 238 | func main() { 239 | if 1 == 2 {} 240 | } 241 | `, 242 | }, 243 | { 244 | "!=", 245 | Report{Condition: 1}, 246 | ` 247 | package main 248 | 249 | func main() { 250 | if 1 != 2 {} 251 | } 252 | `, 253 | }, 254 | { 255 | "`if` without `else`", 256 | Report{}, 257 | ` 258 | package main 259 | 260 | func main() { 261 | if true {} 262 | } 263 | `, 264 | }, 265 | { 266 | "`if` with `else`", 267 | Report{Condition: 1}, 268 | ` 269 | package main 270 | 271 | func main() { 272 | if true {} else {} 273 | } 274 | `, 275 | }, 276 | { 277 | "`case`", 278 | Report{Condition: 2}, 279 | ` 280 | package main 281 | 282 | func main() { 283 | switch nil { 284 | case true: 285 | case false: 286 | } 287 | } 288 | `, 289 | }, 290 | } 291 | 292 | for _, tc := range testCases { 293 | t.Run(tc.rule, func(t *testing.T) { 294 | node, _ := parser.ParseFile(fileSet, "", tc.src, 0) 295 | report := reportFile(fileSet, node)[0] 296 | 297 | assert.Equal(t, tc.report.Assignment, report.Assignment, "Assignment") 298 | assert.Equal(t, tc.report.Branch, report.Branch, "Branch") 299 | assert.Equal(t, tc.report.Condition, report.Condition, "Condition") 300 | }) 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /vim/README.md: -------------------------------------------------------------------------------- 1 | # ABCGo 2 | 3 | Vim plugin for ABCGo. 4 | 5 | ## Getting Started 6 | 7 | ### Installation 8 | 9 | #### Vundle 10 | 11 | Place this in your `.vimrc`: 12 | 13 | ```vim 14 | Plugin 'droptheplot/abcgo', {'rtp': 'vim/'} 15 | ``` 16 | 17 | Run the following in Vim: 18 | 19 | ```vim 20 | :source % 21 | :PluginInstall 22 | ``` 23 | 24 | #### VimPlug 25 | 26 | Place this in your `.vimrc`: 27 | 28 | ```vim 29 | Plug 'droptheplot/abcgo', { 'rtp': 'vim/' } 30 | ``` 31 | 32 | Run the following in Vim: 33 | 34 | ```vim 35 | :source % 36 | :PlugInstall 37 | ``` 38 | 39 | ### Configuration 40 | 41 | Change maximum allowed ABC score: 42 | 43 | ```vim 44 | let g:abcgo_max = 20 45 | ``` 46 | -------------------------------------------------------------------------------- /vim/plugin/abcgo.vim: -------------------------------------------------------------------------------- 1 | function! ABCGoBackground(channel, reports) 2 | if !exists("g:abcgo_max") 3 | let g:abcgo_max = 20 4 | endif 5 | 6 | if !exists("b:abcgo_count") 7 | let b:abcgo_count = 0 8 | endif 9 | 10 | let current_file = expand("%:p") 11 | sign define abcgo text=c texthl=WarningMsg 12 | 13 | if strlen(a:reports) > 0 14 | let i = 0 15 | 16 | while i <= b:abcgo_count 17 | let i += 1 18 | 19 | execute "sign unplace 9314 file=" . current_file 20 | endwhile 21 | endif 22 | 23 | let b:abcgo_count = 0 24 | 25 | for report in split(a:reports, "\n") 26 | let report = split(report, "\t") 27 | 28 | if report[3] >= g:abcgo_max 29 | let b:abcgo_count += 1 30 | execute ":sign place 9314 line=" . report[1] . " name=abcgo file=" . current_file 31 | endif 32 | endfor 33 | endfunction 34 | 35 | function! ABCGo() 36 | call job_start($GOPATH . "/bin/abcgo -format raw -path " . expand("%:p"), {'callback': 'ABCGoBackground', 'mode': 'raw'}) 37 | endfunction 38 | 39 | augroup abcgo_autocmd 40 | autocmd! 41 | 42 | autocmd BufWritePost *.go call ABCGo() 43 | autocmd BufReadPost *.go call ABCGo() 44 | augroup END 45 | --------------------------------------------------------------------------------