├── .gitignore ├── cmd └── cyclomatix │ └── main.go ├── examples ├── basic.go ├── if.go ├── switch.go └── for.go ├── go.mod ├── internal ├── commands │ ├── version.go │ ├── root.go │ ├── complexity.go │ └── cfg.go ├── fsexplorer │ ├── explorer.go │ └── gofhandler.go ├── fctinfo │ └── info.go └── utils │ └── graph.go ├── go.sum ├── Makefile ├── README.md ├── .circleci └── config.yml ├── test └── gofhander_test.go └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /bin -------------------------------------------------------------------------------- /cmd/cyclomatix/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/Assifar-Karim/cyclomatix/internal/commands" 4 | 5 | func main() { 6 | commands.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /examples/basic.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | func basic_fc1() { 4 | var x int 5 | x = 1 6 | v := 0 7 | x++ 8 | v-- 9 | x += 1 10 | } 11 | 12 | func basic_fc2(x int) int { 13 | return x + 1 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Assifar-Karim/cyclomatix 2 | 3 | go 1.21.4 4 | 5 | require ( 6 | github.com/dominikbraun/graph v0.23.0 7 | github.com/spf13/cobra v1.8.0 8 | ) 9 | 10 | require ( 11 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 12 | github.com/spf13/pflag v1.0.5 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /internal/commands/version.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | rootCmd.AddCommand(versionCmd) 11 | } 12 | 13 | var versionCmd *cobra.Command = &cobra.Command{ 14 | Use: "version", 15 | Short: "Print the version of cyclomatix", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | fmt.Println("Cyclomatix version 0.1") 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /examples/if.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "fmt" 4 | 5 | func if_fc1() { 6 | v := 0 7 | if v >= 1 { 8 | y := 1 9 | fmt.Println(y) 10 | } 11 | } 12 | 13 | func if_fc2() { 14 | v := 0 15 | if v >= 1 { 16 | y := 1 17 | fmt.Println(y) 18 | } else { 19 | a := 77 20 | fmt.Println(a) 21 | } 22 | } 23 | 24 | func if_fc3() { 25 | v := 0 26 | if v >= 1 { 27 | y := 1 28 | fmt.Println(y) 29 | } else if v <= -2 { 30 | fmt.Println("Hello 1") 31 | z := 7 32 | fmt.Println(z) 33 | } else { 34 | a := 77 35 | fmt.Println(a) 36 | } 37 | fmt.Println("Hello 2") 38 | } 39 | 40 | func if_fc4(v int) int { 41 | if v < 0 { 42 | return -v 43 | } 44 | return v 45 | } 46 | 47 | func if_fc5(v int) { 48 | if i := if_fc4(v); i == 5 { 49 | fmt.Println("Bingo") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/switch.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "fmt" 4 | 5 | func switch_fc1(idx int) string { 6 | switch idx { 7 | case 0: 8 | return "Monday" 9 | case 1: 10 | return "Tuesday" 11 | case 2: 12 | return "Wednesday" 13 | case 3: 14 | return "Thursday" 15 | case 4: 16 | return "Friday" 17 | case 5: 18 | return "Saturday" 19 | case 6: 20 | return "Sunday" 21 | default: 22 | return "Weirday" 23 | } 24 | } 25 | 26 | func switch_fc2(idx int) { 27 | switch day := switch_fc1(idx); day { 28 | case "Monday", "Tuesday", "Wednesday": 29 | fmt.Println("First half of the week") 30 | case "Thursday", "Friday": 31 | fmt.Println("Second half of the week") 32 | case "Saturday", "Sunday": 33 | fmt.Println("WEEK END") 34 | default: 35 | fmt.Println("ERROR! ERROR! MACHINE EXPLODING!!!!") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/for.go: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import "fmt" 4 | 5 | func for_fc1() { 6 | for i := 0; i < 2; i++ { 7 | fmt.Println(i) 8 | } 9 | } 10 | 11 | func for_fc2() { 12 | i := 0 13 | for i < 2 { 14 | fmt.Println(i) 15 | i++ 16 | } 17 | } 18 | 19 | func for_fc3() { 20 | for i := 0; i < 2; i++ { 21 | if i == 1 { 22 | break 23 | } 24 | fmt.Println(i) 25 | } 26 | } 27 | 28 | func for_fc4() { 29 | for i := 0; i < 2; i++ { 30 | if i%2 == 0 { 31 | continue 32 | } 33 | fmt.Println(i) 34 | } 35 | } 36 | 37 | func for_fc5() string { 38 | for i := 0; i < 2; i++ { 39 | if i%2 == 1 { 40 | return "Bingo" 41 | } 42 | fmt.Println(i) 43 | } 44 | return "7:3" 45 | } 46 | 47 | func for_fc6() { 48 | arr := []int{1, 3, 5, 7, 11, 13} 49 | 50 | for _, v := range arr { 51 | fmt.Println(v) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= 3 | github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= 4 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 5 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 6 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 7 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 8 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 9 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 10 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /internal/commands/root.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | fsinfo "github.com/Assifar-Karim/cyclomatix/internal/fctinfo" 8 | "github.com/Assifar-Karim/cyclomatix/internal/fsexplorer" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var files []string 13 | var fileExplorer FileExplorer 14 | var functionTable []fsinfo.FctInfo 15 | var fileHandler fsexplorer.FileHandler 16 | 17 | var rootCmd *cobra.Command = &cobra.Command{ 18 | Use: "cyclo", 19 | Short: "A static code analysis tool that computes the cyclomatic complexity of functions", 20 | Run: func(cmd *cobra.Command, args []string) {}, 21 | } 22 | 23 | func printBanner() { 24 | fmt.Println(" ________ __________ ____ __ ______ ___________ __") 25 | fmt.Println(" / ____/\\ \\/ / ____/ / / __ \\/ |/ / |/_ __/ _/ |/ /") 26 | fmt.Println(" / / \\ / / / / / / / / /|_/ / /| | / / / / | / ") 27 | fmt.Println("/ /___ / / /___/ /___/ /_/ / / / / ___ |/ / _/ / / | ") 28 | fmt.Println("\\____/ /_/\\____/_____/\\____/_/ /_/_/ |_/_/ /___//_/|_| ") 29 | fmt.Println() 30 | } 31 | 32 | func Execute() { 33 | if err := rootCmd.Execute(); err != nil { 34 | fmt.Println(err) 35 | os.Exit(1) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/commands/complexity.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | fsinfo "github.com/Assifar-Karim/cyclomatix/internal/fctinfo" 5 | "github.com/Assifar-Karim/cyclomatix/internal/fsexplorer" 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type FileExplorer interface { 10 | Handle() 11 | } 12 | 13 | func init() { 14 | rootCmd.AddCommand(complexityCmd) 15 | } 16 | 17 | var indirectionLvl int32 18 | 19 | func init() { 20 | complexityCmd.Flags().Int32VarP(&indirectionLvl, "indirection-lvl", "i", 4, "Sets the maximum allowed level of indirection") 21 | complexityCmd.Flags().StringArrayVarP(&files, "files", "f", []string{}, "Defines the files/directory to analyze") 22 | } 23 | 24 | var complexityCmd *cobra.Command = &cobra.Command{ 25 | Use: "complexity", 26 | Short: "List the cyclomatic complexity of all functions in the input files", 27 | PreRun: func(cmd *cobra.Command, args []string) { 28 | functionTable = []fsinfo.FctInfo{} 29 | fileHandler = fsexplorer.NewGoFileHandler(indirectionLvl) 30 | fileExplorer = fsexplorer.NewFileList( 31 | files, 32 | &functionTable, 33 | fileHandler, 34 | ) 35 | }, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | printBanner() 38 | fileExplorer.Handle() 39 | fileHandler.ComputeComplexities(&functionTable) 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /internal/fsexplorer/explorer.go: -------------------------------------------------------------------------------- 1 | package fsexplorer 2 | 3 | import ( 4 | "io/fs" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | fsinfo "github.com/Assifar-Karim/cyclomatix/internal/fctinfo" 11 | ) 12 | 13 | type FileHandler interface { 14 | HandleFile(path string, fctTable *[]fsinfo.FctInfo) 15 | ComputeComplexities(fctTable *[]fsinfo.FctInfo) 16 | } 17 | 18 | type FileList struct { 19 | paths []string 20 | fctTable *[]fsinfo.FctInfo 21 | fileHandler FileHandler 22 | } 23 | 24 | func (fl FileList) Handle() { 25 | for _, path := range fl.paths { 26 | info, err := os.Stat(path) 27 | if err != nil { 28 | log.Printf("[INFO] Couldn't get %q's path information: %s\n", path, err) 29 | continue 30 | } 31 | if info.IsDir() { 32 | fl.handleDir(path) 33 | } else { 34 | fl.handleFile(path) 35 | } 36 | } 37 | } 38 | 39 | func (fl FileList) handleDir(path string) { 40 | filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error { 41 | if d.IsDir() && (d.Name() == "vendor" || d.Name() == "test" || d.Name() == ".git") { 42 | return filepath.SkipDir 43 | } 44 | if err == nil && !d.IsDir() && strings.HasSuffix(d.Name(), ".go") { 45 | fl.handleFile(p) 46 | } 47 | return err 48 | }) 49 | } 50 | 51 | func (fl FileList) handleFile(path string) { 52 | fl.fileHandler.HandleFile(path, fl.fctTable) 53 | } 54 | 55 | func NewFileList(paths []string, fctTable *[]fsinfo.FctInfo, fileHandler FileHandler) FileList { 56 | return FileList{ 57 | paths: paths, 58 | fctTable: fctTable, 59 | fileHandler: fileHandler, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/fctinfo/info.go: -------------------------------------------------------------------------------- 1 | package fsinfo 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/Assifar-Karim/cyclomatix/internal/utils" 7 | ) 8 | 9 | type FctInfo struct { 10 | pkgName string 11 | fctName string 12 | filename string 13 | cyclomaticCmplx int32 14 | cfg utils.Graph 15 | CallList map[string]int 16 | IsVisited bool 17 | } 18 | 19 | func (f FctInfo) Print() { 20 | fmt.Printf("%-10s %-10s %-20s %v\n", f.pkgName, f.fctName, f.filename, f.cyclomaticCmplx) 21 | } 22 | 23 | func (f FctInfo) GetCfg() utils.Graph { 24 | return f.cfg 25 | } 26 | 27 | func (f FctInfo) GetName() string { 28 | return f.fctName 29 | } 30 | 31 | func (f FctInfo) GetPkg() string { 32 | return f.pkgName 33 | } 34 | 35 | func GetFctByNameAndPkg(fctTable *[]FctInfo, name string, pkg string) (*FctInfo, error) { 36 | for _, fct := range *fctTable { 37 | if name == fct.fctName && pkg == fct.pkgName { 38 | return &fct, nil 39 | } 40 | } 41 | return nil, fmt.Errorf("function not found") 42 | } 43 | 44 | func (f *FctInfo) SetAsVisited() { 45 | f.IsVisited = true 46 | } 47 | 48 | func (f FctInfo) GetCycloCmplx() int32 { 49 | return f.cyclomaticCmplx 50 | } 51 | 52 | func (f *FctInfo) SetCycloCmplx(value int32) { 53 | f.cyclomaticCmplx = value 54 | } 55 | 56 | func NewFctInfo(pkgName string, fctName string, filename string, cfg utils.Graph, callList map[string]int) FctInfo { 57 | return FctInfo{ 58 | pkgName: pkgName, 59 | fctName: fctName, 60 | filename: filename, 61 | cfg: cfg, 62 | CallList: callList, 63 | IsVisited: false, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINARY_NAME=cyclo 2 | 3 | build: 4 | @echo "[INFO] Building linux amd64 binary" 5 | GOARCH=amd64 GOOS=linux go build -o bin/${BINARY_NAME}-linux-amd64 cmd/cyclomatix/main.go 6 | 7 | @echo "[INFO] Building linux arm64 binary" 8 | GOARCH=arm64 GOOS=linux go build -o bin/${BINARY_NAME}-linux-arm64 cmd/cyclomatix/main.go 9 | 10 | @echo "[INFO] Building darwin amd64 binary" 11 | GOARCH=amd64 GOOS=darwin go build -o bin/${BINARY_NAME}-darwin-amd64 cmd/cyclomatix/main.go 12 | 13 | @echo "[INFO] Building darwin arm64 binary" 14 | GOARCH=arm64 GOOS=darwin go build -o bin/${BINARY_NAME}-darwin-arm64 cmd/cyclomatix/main.go 15 | 16 | @echo "[INFO] Building windows amd64 binary" 17 | GOARCH=amd64 GOOS=windows go build -o bin/${BINARY_NAME}-windows-amd64 cmd/cyclomatix/main.go 18 | package: build 19 | @echo ["INFO"] Compressing linux amd64 binaries 20 | @mv bin/${BINARY_NAME}-linux-amd64 bin/${BINARY_NAME} 21 | tar -czvf bin/cyclomatix-linux-amd64.tar.gz bin/${BINARY_NAME} 22 | @rm bin/${BINARY_NAME} 23 | 24 | @echo ["INFO"] Compressing linux arm64 binaries 25 | @mv bin/${BINARY_NAME}-linux-arm64 bin/${BINARY_NAME} 26 | tar -czvf bin/cyclomatix-linux-arm64.tar.gz bin/${BINARY_NAME} 27 | @rm bin/${BINARY_NAME} 28 | 29 | @echo ["INFO"] Compressing darwin amd64 binaries 30 | @mv bin/${BINARY_NAME}-darwin-amd64 bin/${BINARY_NAME} 31 | tar -czvf bin/cyclomatix-darwin-amd64.tar.gz bin/${BINARY_NAME} 32 | @rm bin/${BINARY_NAME} 33 | 34 | @echo ["INFO"] Compressing darwin arm64 binaries 35 | @mv bin/${BINARY_NAME}-darwin-arm64 bin/${BINARY_NAME} 36 | tar -czvf bin/cyclomatix-darwin-arm64.tar.gz bin/${BINARY_NAME} 37 | @rm bin/${BINARY_NAME} 38 | 39 | @echo ["INFO"] Compressing windows amd64 binaries 40 | @mv bin/${BINARY_NAME}-windows-amd64 bin/${BINARY_NAME}.exe 41 | zip bin/cyclomatix-windows-amd64.zip bin/${BINARY_NAME}.exe 42 | @rm bin/${BINARY_NAME}.exe -------------------------------------------------------------------------------- /internal/commands/cfg.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | 10 | fsinfo "github.com/Assifar-Karim/cyclomatix/internal/fctinfo" 11 | "github.com/Assifar-Karim/cyclomatix/internal/fsexplorer" 12 | "github.com/dominikbraun/graph/draw" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func init() { 17 | rootCmd.AddCommand(cfgVizCommand) 18 | } 19 | 20 | var outputDir string 21 | 22 | func init() { 23 | cfgVizCommand.Flags().StringVarP(&outputDir, "output", "o", ".", "Defines the output directory"+ 24 | " where the cfg dot files will be generated") 25 | cfgVizCommand.Flags().StringArrayVarP(&files, "files", "f", []string{}, "Defines the files/directory to analyze") 26 | } 27 | 28 | var cfgVizCommand *cobra.Command = &cobra.Command{ 29 | Use: "cfg", 30 | Short: "Generate the control flow graphs of all functions in the input files as dot files", 31 | PreRun: func(cmd *cobra.Command, args []string) { 32 | functionTable = []fsinfo.FctInfo{} 33 | fileHandler = fsexplorer.NewGoFileHandler(indirectionLvl) 34 | fileExplorer = fsexplorer.NewFileList( 35 | files, 36 | &functionTable, 37 | fileHandler, 38 | ) 39 | }, 40 | Run: func(cmd *cobra.Command, args []string) { 41 | printBanner() 42 | fileExplorer.Handle() 43 | if outputDir != "." { 44 | os.Mkdir(outputDir, 0755) 45 | } 46 | for _, fct := range functionTable { 47 | fctPath := filepath.Join(outputDir, fct.GetPkg()) 48 | os.Mkdir(fctPath, 0755) 49 | dotGraph := fct.GetCfg().GenerateDot() 50 | fpath := filepath.Join(fctPath, fct.GetName()+".gv") 51 | file, _ := os.Create(fpath) 52 | draw.DOT[string, string](dotGraph, file) 53 | fmt.Printf("[INFO]: The DOT Graph for %v was generated.\n", fct.GetPkg()+"/"+fct.GetName()) 54 | cmd := exec.Command("dot", "-Tpng", "-O", fpath) 55 | err := cmd.Run() 56 | if err != nil { 57 | log.Println(err) 58 | } 59 | } 60 | }, 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cyclomatix 2 |
 3 |  ▄▄▄▄▄▄▄▄▄▄▄  ▄         ▄  ▄▄▄▄▄▄▄▄▄▄▄  ▄            ▄▄▄▄▄▄▄▄▄▄▄ 
 4 | ▐░░░░░░░░░░░▌▐░▌       ▐░▌▐░░░░░░░░░░░▌▐░▌          ▐░░░░░░░░░░░▌
 5 | ▐░█▀▀▀▀▀▀▀▀▀ ▐░▌       ▐░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░▌          ▐░█▀▀▀▀▀▀▀█░▌
 6 | ▐░▌          ▐░▌       ▐░▌▐░▌          ▐░▌          ▐░▌       ▐░▌
 7 | ▐░▌          ▐░█▄▄▄▄▄▄▄█░▌▐░▌          ▐░▌          ▐░▌       ▐░▌
 8 | ▐░▌          ▐░░░░░░░░░░░▌▐░▌          ▐░▌          ▐░▌       ▐░▌
 9 | ▐░▌           ▀▀▀▀█░█▀▀▀▀ ▐░▌          ▐░▌          ▐░▌       ▐░▌
10 | ▐░▌               ▐░▌     ▐░▌          ▐░▌          ▐░▌       ▐░▌
11 | ▐░█▄▄▄▄▄▄▄▄▄      ▐░▌     ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌
12 | ▐░░░░░░░░░░░▌     ▐░▌     ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌
13 |  ▀▀▀▀▀▀▀▀▀▀▀       ▀       ▀▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀▀ 
14 | 
15 |  ▄▄       ▄▄  ▄▄▄▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄▄▄  ▄       ▄   
16 | ▐░░▌     ▐░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌     ▐░▌  
17 | ▐░▌░▌   ▐░▐░▌▐░█▀▀▀▀▀▀▀█░▌ ▀▀▀▀█░█▀▀▀▀  ▀▀▀▀█░█▀▀▀▀  ▐░▌   ▐░▌   
18 | ▐░▌▐░▌ ▐░▌▐░▌▐░▌       ▐░▌     ▐░▌          ▐░▌       ▐░▌ ▐░▌    
19 | ▐░▌ ▐░▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄█░▌     ▐░▌          ▐░▌        ▐░▐░▌     
20 | ▐░▌  ▐░▌  ▐░▌▐░░░░░░░░░░░▌     ▐░▌          ▐░▌         ▐░▌      
21 | ▐░▌   ▀   ▐░▌▐░█▀▀▀▀▀▀▀█░▌     ▐░▌          ▐░▌        ▐░▌░▌     
22 | ▐░▌       ▐░▌▐░▌       ▐░▌     ▐░▌          ▐░▌       ▐░▌ ▐░▌    
23 | ▐░▌       ▐░▌▐░▌       ▐░▌     ▐░▌      ▄▄▄▄█░█▄▄▄▄  ▐░▌   ▐░▌   
24 | ▐░▌       ▐░▌▐░▌       ▐░▌     ▐░▌     ▐░░░░░░░░░░░▌▐░▌     ▐░▌  
25 |  ▀         ▀  ▀         ▀       ▀       ▀▀▀▀▀▀▀▀▀▀▀  ▀       ▀   
26 | 
27 |
28 | A Go static analysis tool to generate control flow graphs and compute cyclomatic complexity
29 | 
30 | 31 | ## Features 32 | 33 | ### Cyclomatic Complexity Computation 34 | 35 | Cyclomatix computes the cyclomatic complexity of each and every function found in the input files given by the user to the tool 36 | 37 | ### Control Flow Graph Generation 38 | 39 | Cyclomatix traverses all of the functions found in the files inputted by the users to generate their control flow graphs then outputs them in DOT files used by Graphviz. 40 | 41 | > [!WARNING] 42 | > To fully use the control flow graph generation feature, the user must install Graphviz in their machine. 43 | 44 | ## Installation guide 45 | 46 | 1. Download the latest release that corresponds with your system from the [releases page](https://github.com/Assifar-Karim/cyclomatix/releases). 47 | 2. Decompress the archive containing the binaries. 48 | 3. Install Graphviz on your system, if it's not already installed, by following the instructions found [here](https://graphviz.org/download/). 49 | 4. Add the binary to your PATH environment variable 50 | 5. Enjoy 51 | 52 | ## Getting started 53 | 54 | After having installed cyclomatix on your system, you can follow the steps to get started on using the tool. 55 | 56 | 1. Pull the `.go` files from the `examples` directory in this repo. 57 | 2. Run the command `cyclo complexity -f examples` to get the cyclomatic complexity table of the functions in the files. 58 | 3. Run the command `cyclo cfg -f example -o target` to generate the control flow graph of each function that can be found on the example files. -------------------------------------------------------------------------------- /internal/utils/graph.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/dominikbraun/graph" 8 | ) 9 | 10 | type Graph struct { 11 | AdjList map[int32][]int32 12 | NodesNames []string 13 | LatestNode int32 14 | } 15 | 16 | func NewCFGraph() *Graph { 17 | nodesNames := []string{"start"} 18 | adjList := make(map[int32][]int32) 19 | adjList[0] = []int32{} 20 | 21 | return &Graph{ 22 | AdjList: adjList, 23 | NodesNames: nodesNames, 24 | LatestNode: int32(len(adjList)) - 1, 25 | } 26 | } 27 | 28 | func (g *Graph) Append(node string) { 29 | g.NodesNames = append(g.NodesNames, node) 30 | latestNode := int32(len(g.AdjList)) - 1 31 | g.AdjList[latestNode], latestNode = append(g.AdjList[latestNode], latestNode+1), latestNode+1 32 | g.AdjList[latestNode] = []int32{} 33 | g.LatestNode = latestNode 34 | } 35 | 36 | func (g *Graph) AddStatement(stmt string) { 37 | g.NodesNames[g.LatestNode] += stmt + "\n" 38 | } 39 | 40 | // This method creates a new node without performing the appending on the adj list 41 | func (g *Graph) AddNode(node string) { 42 | g.NodesNames = append(g.NodesNames, node) 43 | g.LatestNode++ 44 | g.AdjList[g.LatestNode] = []int32{} 45 | } 46 | 47 | func (g *Graph) DeleteNode(node int32) { 48 | if node == g.LatestNode { 49 | g.LatestNode-- 50 | } 51 | nodesNames := []string{} 52 | nodesNames = append(nodesNames, g.NodesNames[:node]...) 53 | g.NodesNames = append(nodesNames, g.NodesNames[node+1:]...) 54 | 55 | delete(g.AdjList, node) 56 | for k, v := range g.AdjList { 57 | for idx, n := range v { 58 | if n == node { 59 | replacement := []int32{} 60 | replacement = append(replacement, v[:idx]...) 61 | g.AdjList[k] = append(replacement, v[idx+1:]...) 62 | break 63 | } 64 | } 65 | } 66 | 67 | } 68 | 69 | func (g *Graph) LinkNodes(node1, node2 int32) { 70 | g.AdjList[node1] = append(g.AdjList[node1], node2) 71 | } 72 | 73 | func (g Graph) CountEdges() int { 74 | result := 0 75 | for _, list := range g.AdjList { 76 | result += len(list) 77 | } 78 | return result 79 | } 80 | 81 | func (g Graph) GenerateDot() graph.Graph[string, string] { 82 | cteBlocksCounter := []int{0, 0, 0, 0, 0} 83 | dg := graph.New[string, string](graph.StringHash, graph.Directed()) 84 | for node := range g.AdjList { 85 | n := sanitizeNodeName(strings.Replace(g.NodesNames[node], "\"", "\\\"", -1), cteBlocksCounter) 86 | g.NodesNames[node] = n 87 | dg.AddVertex(n) 88 | } 89 | 90 | for node, adjList := range g.AdjList { 91 | n := g.NodesNames[node] 92 | for _, adjNode := range adjList { 93 | aN := g.NodesNames[adjNode] 94 | dg.AddEdge(n, aN) 95 | } 96 | } 97 | return dg 98 | } 99 | 100 | func sanitizeNodeName(nodeName string, cteBlocksCounter []int) string { 101 | if strings.Contains(nodeName, "If End") { 102 | cteBlocksCounter[0]++ 103 | return fmt.Sprintf("%v %v", nodeName, cteBlocksCounter[0]) 104 | } else if strings.Contains(nodeName, "For End") { 105 | cteBlocksCounter[1]++ 106 | return fmt.Sprintf("%v %v", nodeName, cteBlocksCounter[1]) 107 | } else if strings.Contains(nodeName, "Switch End") { 108 | cteBlocksCounter[2]++ 109 | return fmt.Sprintf("%v %v", nodeName, cteBlocksCounter[2]) 110 | } else if strings.Contains(nodeName, "continue") { 111 | cteBlocksCounter[3]++ 112 | return fmt.Sprintf("%v %v", nodeName, cteBlocksCounter[3]) 113 | } else if strings.Contains(nodeName, "break") { 114 | cteBlocksCounter[4]++ 115 | return fmt.Sprintf("%v %v", nodeName, cteBlocksCounter[4]) 116 | } else { 117 | return nodeName 118 | } 119 | } 120 | 121 | // NOTE (KARIM) : This method is used for debugging purposes 122 | func (g *Graph) Print() { 123 | fmt.Println(g.AdjList) 124 | for key, values := range g.AdjList { 125 | fmt.Printf("[%v] -> [%v]\n", key, values) 126 | for _, value := range values { 127 | fmt.Printf("[[%v]] ->[[%v]]\n", g.NodesNames[key], g.NodesNames[value]) 128 | } 129 | 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | Test: 4 | docker: 5 | - image: cimg/go:1.21.4 6 | steps: 7 | - checkout 8 | - run: 9 | name: Run tests 10 | command: | 11 | mkdir -p /tmp/test-results 12 | gotestsum --format testname --junitfile /tmp/test-results/unit-tests.xml github.com/Assifar-Karim/cyclomatix/test 13 | - store_test_results: 14 | path: /tmp/test-results 15 | Build: 16 | docker: 17 | - image: cimg/go:1.21.4 18 | steps: 19 | - checkout 20 | - run: 21 | name: Build and Package 22 | command: make package 23 | - persist_to_workspace: 24 | root: bin 25 | paths: 26 | - cyclomatix-darwin-amd64.tar.gz 27 | - cyclomatix-darwin-arm64.tar.gz 28 | - cyclomatix-linux-amd64.tar.gz 29 | - cyclomatix-linux-arm64.tar.gz 30 | - cyclomatix-windows.zip 31 | Release: 32 | docker: 33 | - image: cimg/base:2024.01 34 | steps: 35 | - checkout 36 | - attach_workspace: 37 | at: bin 38 | - run: 39 | name: Install jq 40 | command: sudo apt update && sudo apt install -y jq 41 | - run: 42 | name: Create release 43 | command: | 44 | export RELEASE_ID=$(curl -L -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $TOKEN" \ 45 | -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/Assifar-Karim/cyclomatix/releases \ 46 | -d "{\"tag_name\":\"$CIRCLE_TAG\",\"name\":\"$CIRCLE_TAG\",\"draft\":true,\"prerelease\":false,\"generate_release_notes\":false}" | jq .id) 47 | cd bin 48 | curl -L -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $TOKEN" \ 49 | -H "X-GitHub-Api-Version: 2022-11-28" -H "Content-Type: application/octet-stream" \ 50 | "https://uploads.github.com/repos/Assifar-Karim/cyclomatix/releases/$RELEASE_ID/assets?name=cyclomatix-darwin-amd64.tar.gz" \ 51 | --data-binary "@cyclomatix-darwin-amd64.tar.gz" 52 | curl -L -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $TOKEN" \ 53 | -H "X-GitHub-Api-Version: 2022-11-28" -H "Content-Type: application/octet-stream" \ 54 | "https://uploads.github.com/repos/Assifar-Karim/cyclomatix/releases/$RELEASE_ID/assets?name=cyclomatix-darwin-arm64.tar.gz" \ 55 | --data-binary "@cyclomatix-darwin-arm64.tar.gz" 56 | curl -L -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $TOKEN" \ 57 | -H "X-GitHub-Api-Version: 2022-11-28" -H "Content-Type: application/octet-stream" \ 58 | "https://uploads.github.com/repos/Assifar-Karim/cyclomatix/releases/$RELEASE_ID/assets?name=cyclomatix-linux-amd64.tar.gz" \ 59 | --data-binary "@cyclomatix-linux-amd64.tar.gz" 60 | curl -L -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $TOKEN" \ 61 | -H "X-GitHub-Api-Version: 2022-11-28" -H "Content-Type: application/octet-stream" \ 62 | "https://uploads.github.com/repos/Assifar-Karim/cyclomatix/releases/$RELEASE_ID/assets?name=cyclomatix-linux-arm64.tar.gz" \ 63 | --data-binary "@cyclomatix-linux-arm64.tar.gz" 64 | curl -L -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $TOKEN" \ 65 | -H "X-GitHub-Api-Version: 2022-11-28" -H "Content-Type: application/octet-stream" \ 66 | "https://uploads.github.com/repos/Assifar-Karim/cyclomatix/releases/$RELEASE_ID/assets?name=cyclomatix-windows-amd64.zip" \ 67 | --data-binary "@cyclomatix-windows-amd64.zip" 68 | 69 | workflows: 70 | QA: 71 | jobs: 72 | - Test 73 | Build and Release: 74 | jobs: 75 | - Build: 76 | filters: 77 | branches: 78 | ignore: /.*/ 79 | tags: 80 | only: /^v.*/ 81 | - Release: 82 | requires: 83 | - Build 84 | filters: 85 | branches: 86 | ignore: /.*/ 87 | tags: 88 | only: /^v.*/ -------------------------------------------------------------------------------- /test/gofhander_test.go: -------------------------------------------------------------------------------- 1 | package fs_explorer 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | fsinfo "github.com/Assifar-Karim/cyclomatix/internal/fctinfo" 8 | "github.com/Assifar-Karim/cyclomatix/internal/fsexplorer" 9 | ) 10 | 11 | type TestInput struct { 12 | path string 13 | fctTable *[]fsinfo.FctInfo 14 | } 15 | 16 | type HandleFileTest struct { 17 | name string 18 | input TestInput 19 | want []map[int32][]int32 20 | } 21 | 22 | type ComputeComplexitiesTest struct { 23 | name string 24 | input TestInput 25 | want []int32 26 | } 27 | 28 | func TestHandleFile(t *testing.T) { 29 | tests := []HandleFileTest{ 30 | { 31 | "Basic Functions", 32 | TestInput{ 33 | path: "../examples/basic.go", 34 | fctTable: &[]fsinfo.FctInfo{}, 35 | }, 36 | []map[int32][]int32{ 37 | {0: []int32{1}, 1: []int32{2}, 2: []int32{}}, 38 | {0: []int32{1}, 1: []int32{2}, 2: []int32{}}, 39 | }, 40 | }, 41 | { 42 | "Conditional Functions 1", 43 | TestInput{ 44 | path: "../examples/if.go", 45 | fctTable: &[]fsinfo.FctInfo{}, 46 | }, 47 | []map[int32][]int32{ 48 | {0: []int32{1}, 1: []int32{2, 4}, 2: []int32{3}, 3: []int32{4}, 49 | 4: []int32{5}, 5: []int32{}}, 50 | {0: []int32{1}, 1: []int32{2, 4}, 2: []int32{3}, 3: []int32{6}, 51 | 4: []int32{5}, 5: []int32{6}, 6: []int32{7}, 7: []int32{}}, 52 | {0: []int32{1}, 1: []int32{2, 4}, 2: []int32{3}, 3: []int32{11}, 53 | 4: []int32{5, 8}, 5: []int32{6}, 6: []int32{7}, 7: []int32{10}, 54 | 8: []int32{9}, 9: []int32{10}, 10: []int32{11}, 11: []int32{12}, 55 | 12: []int32{13}, 13: []int32{}}, 56 | {0: []int32{1}, 1: []int32{2, 3}, 2: []int32{5}, 3: []int32{4}, 57 | 4: []int32{5}, 5: []int32{}}, 58 | {0: []int32{1}, 1: []int32{2}, 2: []int32{3}, 3: []int32{4, 5}, 59 | 4: []int32{5}, 5: []int32{6}, 6: []int32{}}, 60 | }, 61 | }, 62 | { 63 | "Conditional Functions 2", 64 | TestInput{ 65 | path: "../examples/switch.go", 66 | fctTable: &[]fsinfo.FctInfo{}, 67 | }, 68 | []map[int32][]int32{ 69 | {0: []int32{1}, 1: []int32{2, 4, 6, 8, 10, 12, 14, 16}, 70 | 2: []int32{3}, 3: []int32{19}, 4: []int32{5}, 71 | 5: []int32{19}, 6: []int32{7}, 7: []int32{19}, 72 | 8: []int32{9}, 9: []int32{19}, 10: []int32{11}, 73 | 11: []int32{19}, 12: []int32{13}, 13: []int32{19}, 74 | 14: []int32{15}, 15: []int32{19}, 16: []int32{17}, 75 | 17: []int32{19}, 18: []int32{19}, 19: []int32{}}, 76 | {0: []int32{1}, 1: []int32{2}, 2: []int32{3}, 3: []int32{4, 6, 8, 10}, 77 | 4: []int32{5}, 5: []int32{12}, 6: []int32{7}, 7: []int32{12}, 78 | 8: []int32{9}, 9: []int32{12}, 10: []int32{11}, 79 | 11: []int32{12}, 12: []int32{13}, 13: []int32{}}, 80 | }, 81 | }, 82 | { 83 | "Iterative Functions", 84 | TestInput{ 85 | path: "../examples/for.go", 86 | fctTable: &[]fsinfo.FctInfo{}, 87 | }, 88 | []map[int32][]int32{ 89 | {0: []int32{1}, 1: []int32{2, 3}, 2: []int32{1}, 90 | 3: []int32{4}, 4: []int32{}}, 91 | {0: []int32{1}, 1: []int32{2}, 2: []int32{3, 5}, 92 | 3: []int32{4}, 4: []int32{2}, 5: []int32{6}, 6: []int32{}}, 93 | {0: []int32{1}, 1: []int32{2, 6}, 2: []int32{3, 4}, 94 | 3: []int32{6}, 4: []int32{5}, 5: []int32{1}, 95 | 6: []int32{7}, 7: []int32{}}, 96 | {0: []int32{1}, 1: []int32{2, 6}, 2: []int32{3, 4}, 97 | 3: []int32{1}, 4: []int32{5}, 5: []int32{1}, 98 | 6: []int32{7}, 7: []int32{}}, 99 | {0: []int32{1}, 1: []int32{2, 6}, 2: []int32{3, 4}, 100 | 3: []int32{8}, 4: []int32{5}, 5: []int32{1}, 101 | 6: []int32{7}, 7: []int32{8}, 8: []int32{}}, 102 | {0: []int32{1}, 1: []int32{2}, 2: []int32{3, 4}, 103 | 3: []int32{2}, 4: []int32{5}, 5: []int32{}}, 104 | }, 105 | }, 106 | } 107 | g := fsexplorer.NewGoFileHandler(4) 108 | for _, tt := range tests { 109 | t.Run(tt.name, func(t *testing.T) { 110 | g.HandleFile(tt.input.path, tt.input.fctTable) 111 | }) 112 | ft := *tt.input.fctTable 113 | for i, fs := range ft { 114 | ans := fs.GetCfg().AdjList 115 | if !reflect.DeepEqual(ans, tt.want[i]) { 116 | t.Errorf("got %v, want %v", ans, tt.want[i]) 117 | } 118 | } 119 | } 120 | } 121 | 122 | func TestComputeComplexities(t *testing.T) { 123 | tests := []ComputeComplexitiesTest{ 124 | { 125 | "Basic Functions", 126 | TestInput{ 127 | path: "../examples/basic.go", 128 | fctTable: &[]fsinfo.FctInfo{}, 129 | }, 130 | []int32{1, 1}, 131 | }, 132 | { 133 | "Conditional Functions 1", 134 | TestInput{ 135 | path: "../examples/if.go", 136 | fctTable: &[]fsinfo.FctInfo{}, 137 | }, 138 | []int32{2, 2, 3, 2, 3}, 139 | }, 140 | { 141 | "Conditional Functions 2", 142 | TestInput{ 143 | path: "../examples/switch.go", 144 | fctTable: &[]fsinfo.FctInfo{}, 145 | }, 146 | []int32{8, 11}, 147 | }, 148 | { 149 | "Iterative Functions", 150 | TestInput{ 151 | path: "../examples/for.go", 152 | fctTable: &[]fsinfo.FctInfo{}, 153 | }, 154 | []int32{2, 2, 3, 3, 3, 2}, 155 | }, 156 | } 157 | g := fsexplorer.NewGoFileHandler(4) 158 | for _, tt := range tests { 159 | t.Run(tt.name, func(t *testing.T) { 160 | g.HandleFile(tt.input.path, tt.input.fctTable) 161 | g.ComputeComplexities(tt.input.fctTable) 162 | }) 163 | ft := *tt.input.fctTable 164 | for i, fs := range ft { 165 | ans := fs.GetCycloCmplx() 166 | if ans != tt.want[i] { 167 | t.Errorf("got %v, want %v", ans, tt.want[i]) 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Karim Assifar 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /internal/fsexplorer/gofhandler.go: -------------------------------------------------------------------------------- 1 | package fsexplorer 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/token" 9 | "log" 10 | "os" 11 | "strconv" 12 | "strings" 13 | 14 | fsinfo "github.com/Assifar-Karim/cyclomatix/internal/fctinfo" 15 | "github.com/Assifar-Karim/cyclomatix/internal/utils" 16 | ) 17 | 18 | type GoFileHandler struct { 19 | indirectionLvl int32 20 | } 21 | 22 | type GoFctVisitor struct { 23 | cfg *utils.Graph 24 | fset *token.FileSet 25 | lines []string 26 | visitedNodes []ast.Node 27 | deferredCalls []ast.Node 28 | returnBlocks []int32 29 | loopStack []int32 30 | afterLoopStack []int32 31 | breakQueue []int32 32 | callList map[string]int 33 | pkg string 34 | } 35 | 36 | func (fh GoFileHandler) HandleFile(path string, fctTable *[]fsinfo.FctInfo) { 37 | fset := token.NewFileSet() 38 | file, err := parser.ParseFile(fset, path, nil, 0) 39 | if err != nil { 40 | log.Fatalf("[ERROR] Couldn't parse %q file: %s\n", path, err) 41 | } 42 | lines := readSourceFile(path) 43 | 44 | for _, decl := range file.Decls { 45 | switch d := decl.(type) { 46 | case *ast.FuncDecl: 47 | visitor := GoFctVisitor{ 48 | cfg: utils.NewCFGraph(), 49 | fset: fset, 50 | lines: lines, 51 | callList: map[string]int{}, 52 | pkg: file.Name.Name, 53 | } 54 | visitor.cfg.Append("") 55 | ast.Walk(&visitor, d) 56 | // Reinitialize visited nodes for deferred calls visit 57 | visitor.visitedNodes = []ast.Node{} 58 | for i := len(visitor.deferredCalls) - 1; i >= 0; i-- { 59 | deferredCall := visitor.deferredCalls[i] 60 | ast.Walk(&visitor, deferredCall) 61 | } 62 | if visitor.cfg.NodesNames[visitor.cfg.LatestNode] == "" { 63 | visitor.cfg.AddStatement("End") 64 | } else { 65 | visitor.cfg.Append("End") 66 | } 67 | // Link return blocks to the end block 68 | for _, value := range visitor.returnBlocks { 69 | visitor.cfg.AdjList[value] = []int32{visitor.cfg.LatestNode} 70 | } 71 | //visitor.cfg.Print() 72 | *fctTable = append(*fctTable, fsinfo.NewFctInfo( 73 | file.Name.Name, 74 | d.Name.Name, 75 | path, 76 | *visitor.cfg, 77 | visitor.callList, 78 | )) 79 | } 80 | } 81 | } 82 | 83 | func (fh GoFileHandler) ComputeComplexities(fctTable *[]fsinfo.FctInfo) { 84 | for i := 0; i < len(*fctTable); i++ { 85 | if (*fctTable)[i].IsVisited { 86 | continue 87 | } 88 | computeComplexity(&(*fctTable)[i], fctTable, fh.indirectionLvl) 89 | } 90 | fmt.Printf("%-10s %-10s %-20s %s\n", "Package", "Function", "Filename", "Complexity") 91 | for _, fct := range *fctTable { 92 | fct.Print() 93 | } 94 | } 95 | 96 | func computeComplexity(f *fsinfo.FctInfo, fctTable *[]fsinfo.FctInfo, indirectLvl int32) { 97 | if indirectLvl == 0 { 98 | f.SetCycloCmplx(1) 99 | return 100 | } 101 | cfg := f.GetCfg() 102 | callCounter := len(f.CallList) 103 | for calledF, totalCalls := range f.CallList { 104 | callCounter-- 105 | callInfo := strings.Split(calledF, ".") 106 | call, err := fsinfo.GetFctByNameAndPkg(fctTable, callInfo[1], callInfo[0]) 107 | if err != nil { 108 | continue 109 | } 110 | if !call.IsVisited { 111 | computeComplexity(call, fctTable, indirectLvl-1) 112 | } 113 | cmplx := f.GetCycloCmplx() + call.GetCycloCmplx()*int32(totalCalls) - int32(totalCalls) 114 | f.SetCycloCmplx(cmplx) 115 | } 116 | if callCounter == 0 { 117 | totalNodes := len(cfg.AdjList) 118 | cmplx := f.GetCycloCmplx() + int32(cfg.CountEdges()-totalNodes+2) 119 | f.SetCycloCmplx(cmplx) 120 | f.SetAsVisited() 121 | } 122 | 123 | } 124 | 125 | func (v *GoFctVisitor) Visit(node ast.Node) ast.Visitor { 126 | // Check if the current node hasn't been visited before 127 | for _, visitedNode := range v.visitedNodes { 128 | if node == visitedNode { 129 | return v 130 | } 131 | } 132 | 133 | switch n := node.(type) { 134 | case *ast.DeclStmt, *ast.AssignStmt, *ast.IncDecStmt: 135 | stmt := v.getLine(n.Pos(), n.End()) 136 | v.cfg.AddStatement(stmt) 137 | case *ast.DeferStmt: 138 | for _, args := range n.Call.Args { 139 | switch a := args.(type) { 140 | case *ast.CallExpr: 141 | v.visitedNodes = append(v.visitedNodes, a) 142 | } 143 | } 144 | v.visitedNodes = append(v.visitedNodes, n.Call) 145 | v.deferredCalls = append(v.deferredCalls, n.Call) 146 | case *ast.CallExpr: 147 | stmt := v.getLine(n.Pos(), n.End()) 148 | switch s := n.Fun.(type) { 149 | case *ast.SelectorExpr: 150 | key := fmt.Sprintf("%v.%v", s.X, s.Sel) 151 | v.callList[key]++ 152 | case *ast.Ident: 153 | key := fmt.Sprintf("%v.%v", v.pkg, s.Name) 154 | v.callList[key]++ 155 | } 156 | for _, arg := range n.Args { 157 | switch a := arg.(type) { 158 | case *ast.CallExpr: 159 | ast.Walk(v, a) 160 | } 161 | } 162 | 163 | if v.cfg.NodesNames[v.cfg.LatestNode] == "" { 164 | v.cfg.AddStatement(stmt) 165 | } else { 166 | v.cfg.Append(stmt) 167 | } 168 | v.cfg.Append("") 169 | case *ast.IfStmt: 170 | if n.Init != nil { 171 | ast.Walk(v, n.Init) 172 | } 173 | cond := v.getLine(n.Cond.Pos(), n.Cond.End()) 174 | stmt := "if " + cond[:len(cond)-1] 175 | v.cfg.AddStatement(stmt) 176 | condBlockIdx := v.cfg.LatestNode 177 | 178 | // If body block 179 | v.cfg.Append("") 180 | ast.Walk(v, n.Body) 181 | ifBlockEndIdx := v.cfg.LatestNode 182 | if v.cfg.NodesNames[ifBlockEndIdx] == "" { 183 | v.cfg.DeleteNode(ifBlockEndIdx) 184 | ifBlockEndIdx = v.cfg.LatestNode 185 | } 186 | 187 | var elseBlockEndIdx int32 = -1 188 | if n.Else != nil { 189 | // Else body block 190 | v.cfg.AddNode("") 191 | v.cfg.LinkNodes(condBlockIdx, v.cfg.LatestNode) 192 | ast.Walk(v, n.Else) 193 | elseBlockEndIdx = v.cfg.LatestNode 194 | } 195 | // After if block 196 | afterIfBlockIdx := elseBlockEndIdx 197 | if elseBlockEndIdx == -1 || v.cfg.NodesNames[elseBlockEndIdx] != "" { 198 | v.cfg.AddNode("If End") 199 | afterIfBlockIdx = v.cfg.LatestNode 200 | } else { 201 | v.cfg.AddStatement("If End") 202 | } 203 | v.cfg.Append("") 204 | ifBlockEndStmt := strings.TrimSuffix(v.cfg.NodesNames[ifBlockEndIdx], "\n") 205 | 206 | if ifBlockEndStmt != "break" && ifBlockEndStmt != "continue" { 207 | v.cfg.LinkNodes(ifBlockEndIdx, afterIfBlockIdx) 208 | } 209 | if elseBlockEndIdx != -1 && afterIfBlockIdx != elseBlockEndIdx { 210 | elseBlockEndStmt := strings.TrimSuffix(v.cfg.NodesNames[elseBlockEndIdx], "\n") 211 | if elseBlockEndStmt != "break" && elseBlockEndStmt != "continue" { 212 | v.cfg.LinkNodes(elseBlockEndIdx, afterIfBlockIdx) 213 | } 214 | } else if elseBlockEndIdx == -1 { 215 | v.cfg.LinkNodes(condBlockIdx, afterIfBlockIdx) 216 | } 217 | case *ast.SwitchStmt: 218 | stmt := "switch " 219 | if n.Init != nil { 220 | ast.Walk(v, n.Init) 221 | init := v.getLine(n.Init.Pos(), n.Init.End()) 222 | stmt += init[:len(init)-1] 223 | v.visitedNodes = append(v.visitedNodes, n.Init) 224 | } else if n.Tag != nil { 225 | tag := v.getLine(n.Tag.Pos(), n.Tag.End()) 226 | stmt += tag[:len(tag)-1] 227 | v.visitedNodes = append(v.visitedNodes, n.Tag) 228 | } 229 | v.cfg.AddStatement(stmt) 230 | condBlockIdx := v.cfg.LatestNode 231 | // Case statements handling 232 | caseBlockEndIdxs := []int32{} 233 | for _, caseStmt := range n.Body.List { 234 | switch c := caseStmt.(type) { 235 | case *ast.CaseClause: 236 | if c.List == nil { 237 | stmt = "default: " 238 | } else { 239 | stmt = "case " 240 | stmt += v.getLine(c.List[0].Pos(), c.List[0].End()) 241 | } 242 | v.cfg.AddNode(stmt) 243 | v.cfg.LinkNodes(condBlockIdx, v.cfg.LatestNode) 244 | for _, b := range c.Body { 245 | ast.Walk(v, b) 246 | } 247 | if v.cfg.NodesNames[v.cfg.LatestNode] == "" { 248 | v.cfg.DeleteNode(v.cfg.LatestNode) 249 | } 250 | caseBlockEndIdxs = append(caseBlockEndIdxs, v.cfg.LatestNode) 251 | } 252 | } 253 | // After switch block 254 | v.cfg.AddNode("Switch End") 255 | for _, caseBlockEndIdx := range caseBlockEndIdxs { 256 | v.cfg.LinkNodes(caseBlockEndIdx, v.cfg.LatestNode) 257 | } 258 | v.cfg.Append("") 259 | 260 | case *ast.ForStmt, *ast.RangeStmt: 261 | stmt := "for " 262 | var body *ast.BlockStmt 263 | switch f := n.(type) { 264 | case *ast.ForStmt: 265 | body = f.Body 266 | if f.Init != nil { 267 | stmt += v.getLine(f.Init.Pos(), f.Init.End()) 268 | v.visitedNodes = append(v.visitedNodes, f.Init) 269 | } else if f.Cond != nil { 270 | stmt += v.getLine(f.Cond.Pos(), f.Cond.End()) 271 | } 272 | if f.Post != nil { 273 | v.visitedNodes = append(v.visitedNodes, f.Post) 274 | } 275 | case *ast.RangeStmt: 276 | body = f.Body 277 | key := v.getLine(f.Key.Pos(), f.Key.End()) 278 | stmt += key 279 | switch fc := f.X.(type) { 280 | case *ast.CallExpr: 281 | ast.Walk(v, fc) 282 | } 283 | } 284 | 285 | stmt = stmt[:len(stmt)-1] 286 | if v.cfg.NodesNames[v.cfg.LatestNode] == "" { 287 | v.cfg.AddStatement(stmt) 288 | } else { 289 | v.cfg.Append(stmt + "\n") 290 | } 291 | v.loopStack = append(v.loopStack, v.cfg.LatestNode) 292 | 293 | // For body block 294 | hasBreak := false 295 | v.cfg.Append("") 296 | ast.Walk(v, body) 297 | for _, inst := range body.List { 298 | switch i := inst.(type) { 299 | case *ast.BranchStmt: 300 | if i.Tok == token.BREAK { 301 | hasBreak = true 302 | break 303 | } 304 | } 305 | } 306 | 307 | if v.cfg.NodesNames[v.cfg.LatestNode] == "" { 308 | v.cfg.DeleteNode(v.cfg.LatestNode) 309 | } 310 | 311 | if !hasBreak { 312 | v.cfg.LinkNodes(v.cfg.LatestNode, v.loopStack[len(v.loopStack)-1]) 313 | } 314 | 315 | // After for block 316 | v.cfg.AddNode("Loop End") 317 | v.cfg.LinkNodes(v.loopStack[len(v.loopStack)-1], v.cfg.LatestNode) 318 | v.afterLoopStack = append(v.afterLoopStack, v.cfg.LatestNode) 319 | v.cfg.Append("") 320 | for _, breakBlockIdx := range v.breakQueue { 321 | v.cfg.LinkNodes(breakBlockIdx, v.afterLoopStack[len(v.afterLoopStack)-1]) 322 | } 323 | v.breakQueue = []int32{} 324 | 325 | v.loopStack = v.loopStack[:len(v.loopStack)-1] 326 | v.afterLoopStack = v.afterLoopStack[:len(v.afterLoopStack)-1] 327 | 328 | case *ast.BranchStmt: 329 | stmt := v.getLine(n.Pos(), n.End()) 330 | if v.cfg.NodesNames[v.cfg.LatestNode] == "" { 331 | v.cfg.AddStatement(stmt) 332 | } else { 333 | v.cfg.Append(stmt + "\n") 334 | } 335 | 336 | if n.Tok == token.BREAK { 337 | v.breakQueue = append(v.breakQueue, v.cfg.LatestNode) 338 | } else if n.Tok == token.CONTINUE { 339 | v.cfg.LinkNodes(v.cfg.LatestNode, v.loopStack[len(v.loopStack)-1]) 340 | } 341 | 342 | case *ast.ReturnStmt: 343 | stmt := v.getLine(n.Pos(), n.End()) 344 | if v.cfg.NodesNames[v.cfg.LatestNode] == "" { 345 | v.cfg.AddStatement(stmt) 346 | } else { 347 | v.cfg.Append(stmt + "\n") 348 | } 349 | v.returnBlocks = append(v.returnBlocks, v.cfg.LatestNode) 350 | } 351 | v.visitedNodes = append(v.visitedNodes, node) 352 | return v 353 | } 354 | 355 | func (v *GoFctVisitor) getLine(start token.Pos, end token.Pos) string { 356 | startStr := fmt.Sprintf("%v", v.fset.PositionFor(start, false)) 357 | endStr := fmt.Sprintf("%v", v.fset.PositionFor(end, false)) 358 | startInfo := strings.Split(startStr, ":") 359 | endInfo := strings.Split(endStr, ":") 360 | 361 | startLn := atoi(startInfo[1]) 362 | startIdx := atoi(startInfo[2]) 363 | endLn := atoi(endInfo[1]) 364 | endIdx := atoi(endInfo[2]) 365 | 366 | output := "" 367 | 368 | for i := startLn - 1; i < endLn; i++ { 369 | if i == startLn-1 { 370 | output += v.lines[i][startIdx-1:] 371 | } else if i == endLn-1 { 372 | output += v.lines[i][:endIdx] 373 | } else { 374 | output += v.lines[i] 375 | } 376 | } 377 | return output 378 | } 379 | 380 | func readSourceFile(path string) []string { 381 | lines := []string{} 382 | f, err := os.Open(path) 383 | if err != nil { 384 | log.Fatalf("[ERROR] Couldn't open %q file: %s\n", path, err) 385 | } 386 | defer f.Close() 387 | 388 | scanner := bufio.NewScanner(f) 389 | for scanner.Scan() { 390 | lines = append(lines, scanner.Text()) 391 | } 392 | return lines 393 | } 394 | 395 | func atoi(input string) int { 396 | output, err := strconv.Atoi(input) 397 | if err != nil { 398 | log.Fatal(err) 399 | } 400 | return output 401 | } 402 | 403 | func NewGoFileHandler(indirectionLvl int32) GoFileHandler { 404 | return GoFileHandler{ 405 | indirectionLvl: indirectionLvl, 406 | } 407 | } 408 | --------------------------------------------------------------------------------