package metrieke import ( "fmt" "io/ioutil" "strings" sitter "github.com/smacker/go-tree-sitter" "github.com/smacker/go-tree-sitter/java" ) type JavaFile struct { Source []byte Path string Root *sitter.Node Metrics *JavaFileMetrics } type JavaFileMetrics struct { Loc int Lloc int Dloc int Cloc int DocumentationLines int AvgComplexity float64 SumComplexity int AvgNesting float64 SumNesting int } func NewJavaFileFromContents(path string, contents []byte) *JavaFile { parser := sitter.NewParser() parser.SetLanguage(java.GetLanguage()) tree := parser.Parse(nil, contents) node := tree.RootNode() return &JavaFile{ Path: path, Source: contents, Root: node, Metrics: &JavaFileMetrics{}, } } func NewJavaFile(path string) *JavaFile { contents, err := ioutil.ReadFile(path) if err != nil { fmt.Println("got error") } return NewJavaFileFromContents(path, contents) } func (jf *JavaFile) Parse() { jf.javaLines() //fmt.Printf(string(pf.Source)) numMethods := 0 sumComplexity := 0 var sumNesting int var avgNesting float64 mccc := make([]int, 0) pattern := []byte(`(method_declaration name: (identifier) @function-name)`) queryChildNodes(pattern, java.GetLanguage(), jf.Root, func(node *sitter.Node) { //fmt.Printf("node: %v\n", node.Parent()) complexity := javaMcCabeComplexity(node.Parent()) sumNesting += javaMaxNesting(node.Parent()) numMethods += 1 sumComplexity += complexity mccc = append(mccc, complexity) }) avgComplexity := float64(0) if numMethods > 0 { avgComplexity = float64(sumComplexity) / float64(numMethods) avgNesting = float64(sumNesting) / float64(numMethods) } jf.Metrics.AvgComplexity = avgComplexity jf.Metrics.SumComplexity = sumComplexity jf.Metrics.SumNesting = sumNesting jf.Metrics.AvgNesting = avgNesting } // handle the simple line stuff parsing func (jf *JavaFile) javaLines() { normalized := strings.ReplaceAll(string(jf.Source), "\r\n", "\n") lines := strings.Split(normalized, "\n") loc := len(lines) lloc := 0 dloc := 0 cloc := 0 inDocumentation := false inComment := false for _, line := range lines { trimmedLine := strings.TrimSpace(line) if inDocumentation == false && strings.HasPrefix(trimmedLine, `/**`) { inDocumentation = true trimmedLine = strings.Replace(trimmedLine, `/**`, "", 1) } if inComment == false && strings.HasPrefix(trimmedLine, `/*`) { inComment = true trimmedLine = strings.Replace(trimmedLine, `/*`, "", 1) } if inDocumentation { dloc += 1 } if inComment { cloc += 1 } if inDocumentation && strings.HasSuffix(trimmedLine, `**/`) { inDocumentation = false continue } if inComment && strings.HasSuffix(trimmedLine, `*/`) { inComment = false continue } if strings.HasPrefix(trimmedLine, "//") { cloc += 1 continue } if !inComment && !inDocumentation && trimmedLine != "" { lloc += 1 } } jf.Metrics.Loc = loc jf.Metrics.Lloc = lloc jf.Metrics.Dloc = dloc jf.Metrics.Cloc = cloc } func javaNumMethods(node *sitter.Node) int { numMethods := 0 pattern := []byte(`(method_definition name: (identifier) @function-name)`) queryChildNodes(pattern, java.GetLanguage(), node, func(node *sitter.Node) { numMethods += 1 }) return numMethods } func javaMaxNesting(node *sitter.Node) int { maxChild := -1 for i := 0; i < int(node.NamedChildCount()); i++ { childDepth := javaMaxNesting(node.NamedChild(i)) if childDepth >= maxChild { maxChild = childDepth } } if node.Type() == "block" { return maxChild + 1 } else { return maxChild } } // traverse down the method node to get the cyclomatic complexity func javaMcCabeComplexity(node *sitter.Node) int { iter := sitter.NewNamedIterator(node, sitter.BFSMode) complexity := 1 // 1 per method for { n, err := iter.Next() if err != nil { return complexity } // +1 for every branch statement if n.Type() == "if_statement" { complexity += 1 } if n.Type() == "for_statement" { complexity += 1 } if n.Type() == "while_statement" { complexity += 1 } if n.Type() == "switch_statement" { // could also go for switch_label complexity += 1 } if n.Type() == "catch_clause" { complexity += 1 } if n.Type() == "do_statement" { complexity += 1 } } }