Skip to content

Commit a1fb571

Browse files
committed
feat(analyzer): upgrade language-aware regex
and dependency detection
1 parent e905c08 commit a1fb571

2 files changed

Lines changed: 210 additions & 187 deletions

File tree

internal/analyzer/analyzer.go

Lines changed: 82 additions & 187 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,16 @@ func (a *Analyzer) AnalyzeChanges(totalAdded, totalRemoved int, branchName strin
153153
}
154154
}
155155

156+
// NEW: Monitoring Dependency Changes (Dependency Watcher)
157+
newDeps := a.detectNewDependencies()
158+
if len(newDeps) > 0 {
159+
commitMessage.Action = "chore"
160+
commitMessage.Scope = "deps"
161+
commitMessage.Item = strings.Join(newDeps, ", ")
162+
commitMessage.Purpose = "update dependencies"
163+
return commitMessage // Priority return for dependency updates
164+
}
165+
156166
// NEW: Learning from recent commit history (Commit History Consistency)
157167
if historyScope := a.analyzeHistoryScopes(); historyScope != "" {
158168
// Only override if scope is empty or "core"
@@ -762,113 +772,32 @@ func (a *Analyzer) detectFunctions(diff string) []string {
762772
var functions []string
763773
scanner := bufio.NewScanner(strings.NewReader(diff))
764774

775+
// Regex registry for functions
776+
patterns := map[string]*regexp.Regexp{
777+
"go": regexp.MustCompile(`func\s+(?:\([^)]*\)\s+)?([A-Z][A-Za-z0-9]*)`),
778+
"ts": regexp.MustCompile(`(?:function\s+([a-zA-Z0-9]*)|const\s+([a-zA-Z0-9]*)\s*=\s*(?:\([^)]*\)|[a-zA-Z0-9]*)\s*=>)`),
779+
"js": regexp.MustCompile(`(?:function\s+([a-zA-Z0-9]*)|const\s+([a-zA-Z0-9]*)\s*=\s*(?:\([^)]*\)|[a-zA-Z0-9]*)\s*=>)`),
780+
"python": regexp.MustCompile(`def\s+([a-zA-Z0-9_]+)\s*\(`),
781+
"java": regexp.MustCompile(`(?:public|private|protected|static)\s+(?:[\w<>[\]]+\s+)+([a-zA-Z0-9_]+)\s*\(`),
782+
}
783+
765784
for scanner.Scan() {
766785
line := scanner.Text()
767-
768-
// Only look at added lines
769-
if !strings.HasPrefix(line, "+") {
786+
if !strings.HasPrefix(line, "+") || strings.HasPrefix(line, "+++") {
770787
continue
771788
}
772789

773-
cleanLine := strings.TrimSpace(strings.TrimPrefix(line, "+"))
774-
775-
// Go functions
776-
if strings.HasPrefix(cleanLine, "func ") {
777-
// Extract function name: func FunctionName( or func (receiver) MethodName(
778-
if strings.Contains(cleanLine, "(") {
779-
// Check for method receiver
780-
if cleanLine[5] == '(' {
781-
// Method: func (r Receiver) MethodName
782-
parts := strings.SplitN(cleanLine[5:], ")", 2)
783-
if len(parts) == 2 {
784-
methodPart := strings.TrimSpace(parts[1])
785-
if idx := strings.Index(methodPart, "("); idx > 0 {
786-
methodName := strings.TrimSpace(methodPart[:idx])
787-
if methodName != "" {
788-
functions = append(functions, methodName)
789-
}
790-
}
791-
}
792-
} else {
793-
// Regular function: func FunctionName(
794-
parts := strings.Fields(cleanLine)
795-
if len(parts) >= 2 {
796-
funcName := strings.Split(parts[1], "(")[0]
797-
if funcName != "" {
798-
functions = append(functions, funcName)
799-
}
800-
}
801-
}
802-
}
803-
}
804-
805-
// JavaScript/TypeScript functions
806-
if strings.Contains(cleanLine, "function ") {
807-
// function functionName( or function(
808-
idx := strings.Index(cleanLine, "function ")
809-
if idx >= 0 {
810-
remaining := cleanLine[idx+9:]
811-
if parenIdx := strings.Index(remaining, "("); parenIdx > 0 {
812-
funcName := strings.TrimSpace(remaining[:parenIdx])
813-
if funcName != "" && funcName != "function" {
814-
functions = append(functions, funcName)
815-
}
816-
}
817-
}
818-
}
819-
820-
// Arrow functions: const funcName = () =>
821-
if strings.Contains(cleanLine, "=>") && (strings.Contains(cleanLine, "const ") || strings.Contains(cleanLine, "let ") || strings.Contains(cleanLine, "var ")) {
822-
// Extract: const funcName = ...
823-
for _, prefix := range []string{"const ", "let ", "var "} {
824-
if strings.Contains(cleanLine, prefix) {
825-
idx := strings.Index(cleanLine, prefix)
826-
remaining := cleanLine[idx+len(prefix):]
827-
if eqIdx := strings.Index(remaining, "="); eqIdx > 0 {
828-
funcName := strings.TrimSpace(remaining[:eqIdx])
829-
if funcName != "" {
830-
functions = append(functions, funcName)
831-
}
832-
}
833-
break
834-
}
835-
}
836-
}
790+
cleanLine := strings.TrimPrefix(line, "+")
837791

838-
// Python functions
839-
if strings.HasPrefix(cleanLine, "def ") || strings.HasPrefix(cleanLine, "async def ") {
840-
// Extract: def function_name( or async def function_name(
841-
var remaining string
842-
if strings.HasPrefix(cleanLine, "async def ") {
843-
remaining = cleanLine[10:]
844-
} else {
845-
remaining = cleanLine[4:]
846-
}
847-
if parenIdx := strings.Index(remaining, "("); parenIdx > 0 {
848-
funcName := strings.TrimSpace(remaining[:parenIdx])
849-
if funcName != "" {
850-
functions = append(functions, funcName)
851-
}
852-
}
853-
}
854-
855-
// Java/C/C++ methods
856-
// Pattern: public/private/protected Type methodName(
857-
if strings.Contains(cleanLine, "(") {
858-
for _, modifier := range []string{"public ", "private ", "protected ", "static "} {
859-
if strings.Contains(cleanLine, modifier) {
860-
parts := strings.Fields(cleanLine)
861-
// Find the part before (
862-
for _, part := range parts {
863-
if strings.Contains(part, "(") {
864-
funcName := strings.Split(part, "(")[0]
865-
if funcName != "" && funcName != "if" && funcName != "for" && funcName != "while" && funcName != "switch" {
866-
functions = append(functions, funcName)
867-
break
868-
}
869-
}
792+
for _, re := range patterns {
793+
matches := re.FindStringSubmatch(cleanLine)
794+
if len(matches) > 0 {
795+
// The first captured group (that is not empty) is the function name
796+
for i := 1; i < len(matches); i++ {
797+
if matches[i] != "" {
798+
functions = append(functions, matches[i])
799+
break
870800
}
871-
break
872801
}
873802
}
874803
}
@@ -881,87 +810,27 @@ func (a *Analyzer) detectStructs(diff string) []string {
881810
var structs []string
882811
scanner := bufio.NewScanner(strings.NewReader(diff))
883812

813+
// Regex registry for structs/classes
814+
patterns := map[string]*regexp.Regexp{
815+
"go": regexp.MustCompile(`type\s+([A-Z][A-Za-z0-9]*)\s+(?:struct|interface)`),
816+
"ts": regexp.MustCompile(`class\s+([a-zA-Z0-9]*)`),
817+
"js": regexp.MustCompile(`class\s+([a-zA-Z0-9]*)`),
818+
"python": regexp.MustCompile(`class\s+([a-zA-Z0-9_]+)\s*(?:\(|:)`),
819+
"java": regexp.MustCompile(`(?:public|private|protected|abstract)?\s*class\s+([a-zA-Z0-9_]+)`),
820+
}
821+
884822
for scanner.Scan() {
885823
line := scanner.Text()
886-
887-
// Only look at added lines
888-
if !strings.HasPrefix(line, "+") {
824+
if !strings.HasPrefix(line, "+") || strings.HasPrefix(line, "+++") {
889825
continue
890826
}
891827

892-
cleanLine := strings.TrimSpace(strings.TrimPrefix(line, "+"))
828+
cleanLine := strings.TrimPrefix(line, "+")
893829

894-
// Go structs and interfaces
895-
if strings.HasPrefix(cleanLine, "type ") && (strings.Contains(cleanLine, "struct") || strings.Contains(cleanLine, "interface")) {
896-
parts := strings.Fields(cleanLine)
897-
if len(parts) >= 2 {
898-
structName := parts[1]
899-
if structName != "" {
900-
structs = append(structs, structName)
901-
}
902-
}
903-
}
904-
905-
// JavaScript/TypeScript classes
906-
if strings.HasPrefix(cleanLine, "class ") || strings.HasPrefix(cleanLine, "export class ") {
907-
var remaining string
908-
if strings.HasPrefix(cleanLine, "export class ") {
909-
remaining = cleanLine[13:]
910-
} else {
911-
remaining = cleanLine[6:]
912-
}
913-
914-
// Extract class name (before space, { or extends)
915-
className := remaining
916-
for _, delimiter := range []string{" ", "{", "extends"} {
917-
if idx := strings.Index(className, delimiter); idx > 0 {
918-
className = className[:idx]
919-
break
920-
}
921-
}
922-
className = strings.TrimSpace(className)
923-
if className != "" {
924-
structs = append(structs, className)
925-
}
926-
}
927-
928-
// Python classes
929-
if strings.HasPrefix(cleanLine, "class ") {
930-
remaining := cleanLine[6:]
931-
// Extract class name (before ( or :)
932-
className := remaining
933-
for _, delimiter := range []string{"(", ":"} {
934-
if idx := strings.Index(className, delimiter); idx > 0 {
935-
className = className[:idx]
936-
break
937-
}
938-
}
939-
className = strings.TrimSpace(className)
940-
if className != "" {
941-
structs = append(structs, className)
942-
}
943-
}
944-
945-
// Java classes
946-
if strings.Contains(cleanLine, "class ") {
947-
for _, modifier := range []string{"public class ", "private class ", "protected class ", "abstract class "} {
948-
if strings.Contains(cleanLine, modifier) {
949-
idx := strings.Index(cleanLine, modifier)
950-
remaining := cleanLine[idx+len(modifier):]
951-
// Extract class name (before space, { or extends/implements)
952-
className := remaining
953-
for _, delimiter := range []string{" ", "{", "extends", "implements"} {
954-
if idx := strings.Index(className, delimiter); idx > 0 {
955-
className = className[:idx]
956-
break
957-
}
958-
}
959-
className = strings.TrimSpace(className)
960-
if className != "" {
961-
structs = append(structs, className)
962-
}
963-
break
964-
}
830+
for _, re := range patterns {
831+
matches := re.FindStringSubmatch(cleanLine)
832+
if len(matches) > 1 && matches[1] != "" {
833+
structs = append(structs, matches[1])
965834
}
966835
}
967836
}
@@ -1141,37 +1010,63 @@ func (a *Analyzer) analyzeDiffStat(totalAdded, totalRemoved int) string {
11411010
return ""
11421011
}
11431012

1144-
deletedRatio := float64(totalRemoved) / float64(total)
1145-
addedRatio := float64(totalAdded) / float64(total)
1013+
// Structural Ratio Calculation
1014+
ratio := float64(totalAdded) / float64(total)
11461015

11471016
threshold := a.config.DiffStatThreshold
11481017
if threshold == 0 {
11491018
threshold = 0.5
11501019
}
11511020

1152-
// If deleted lines dominate, suggest cleanup or refactor
1153-
if deletedRatio > threshold+0.2 { // More than 70% deletions
1021+
// If deletions heavily dominate (Ratio < 0.2)
1022+
if ratio < 0.2 {
11541023
return "refactor"
11551024
}
11561025

1157-
// If a large number of lines are added with minimal deletions, suggest feat
1158-
if addedRatio > threshold+0.2 && totalAdded > 50 {
1159-
// Check if it's a new file addition
1160-
for _, change := range a.changes {
1161-
if change.Action == "A" && change.Added > 30 {
1162-
return "feat"
1163-
}
1026+
// If additions heavily dominate (Ratio > 0.8)
1027+
if ratio > 0.8 {
1028+
// If many lines added, likely a feature
1029+
if totalAdded > 30 {
1030+
return "feat"
11641031
}
11651032
}
11661033

11671034
// Balanced changes often indicate modifications or fixes
1168-
if deletedRatio > 0.3 && addedRatio > 0.3 {
1035+
if ratio >= 0.3 && ratio <= 0.7 {
11691036
return "refactor"
11701037
}
11711038

11721039
return ""
11731040
}
11741041

1042+
// detectNewDependencies identifies newly added libraries in package management files
1043+
func (a *Analyzer) detectNewDependencies() []string {
1044+
var newDeps []string
1045+
depFiles := map[string]*regexp.Regexp{
1046+
"go.mod": regexp.MustCompile(`^\+\s+([^\s]+)\s+v`),
1047+
"package.json": regexp.MustCompile(`^\+\s+"([^"]+)":`),
1048+
"requirements.txt": regexp.MustCompile(`^\+([a-zA-Z0-9\-_]+)==`),
1049+
"Cargo.toml": regexp.MustCompile(`^\+([a-zA-Z0-9\-_]+)\s+=`),
1050+
}
1051+
1052+
for _, change := range a.changes {
1053+
fileName := filepath.Base(change.File)
1054+
if re, ok := depFiles[fileName]; ok {
1055+
scanner := bufio.NewScanner(strings.NewReader(change.Diff))
1056+
for scanner.Scan() {
1057+
line := scanner.Text()
1058+
if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
1059+
matches := re.FindStringSubmatch(line)
1060+
if len(matches) > 1 {
1061+
newDeps = append(newDeps, matches[1])
1062+
}
1063+
}
1064+
}
1065+
}
1066+
}
1067+
return uniqueStrings(newDeps)
1068+
}
1069+
11751070
// parseBranchName extracts type and scope from branch name
11761071
func (a *Analyzer) parseBranchName(branch string) (string, string) {
11771072
// Patterns like feature/auth-login or bugfix/fix-memleak

0 commit comments

Comments
 (0)