Skip to content

Commit 0305c51

Browse files
committed
Tool
1 parent c3b0264 commit 0305c51

11 files changed

Lines changed: 765 additions & 375 deletions

File tree

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,3 @@ zig-out/
2020
# Although this was renamed to .zig-cache, let's leave it here for a few
2121
# releases to make it less annoying to work with multiple branches.
2222
zig-cache/
23-
24-
*/cover.txt

secretary/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
SECRETARY
22
node_modules
3+
4+
cover.txt
5+
cover.html
6+
callgraph.js

secretary/utils/ast/example.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
)
7+
8+
// ABC Comment
9+
const ABC = `ABC`
10+
11+
// Hello Comment
12+
type Hello struct {
13+
Greet int // HelloGreet
14+
}
15+
16+
//Hi
17+
/*
18+
* How are you
19+
* Hi you
20+
**/
21+
func (hello *Hello) Hi() error {
22+
// --->> Hi error
23+
return errors.New("Hello error") // Error ....
24+
}
25+
26+
// PrintHello comment
27+
func PrintHello() {
28+
fmt.Println("Hello")
29+
}
30+
31+
/*
32+
* Greet calls PrintHello
33+
**/
34+
func Greet() {
35+
PrintHello()
36+
// Greetings comment
37+
fmt.Println("Greetings!")
38+
}

secretary/utils/ast/main.go

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"go/ast"
7+
"go/parser"
8+
"go/token"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"regexp"
13+
"strings"
14+
15+
"github.com/codeharik/secretary/utils"
16+
)
17+
18+
type InfoType string
19+
20+
const (
21+
FUNCTION InfoType = "FUNCTION"
22+
COMMENT = "COMMENT"
23+
GLOBAL = "GLOBAL"
24+
STRUCT = "STRUCT"
25+
)
26+
27+
// FunctionInfo holds information about a function and its calls.
28+
type Info struct {
29+
Name string
30+
Type InfoType
31+
Calls []string
32+
Comments []string
33+
Content string
34+
Fields map[string]string
35+
Range [2]int
36+
Processed bool
37+
}
38+
39+
// ParseGoFile parses a Go file and extracts functions, global variables, and structs.
40+
func ParseGoFile(filename string) ([]Info, error) {
41+
src, err := os.ReadFile(filename)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
fset := token.NewFileSet()
47+
node, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
48+
if err != nil {
49+
return nil, err
50+
}
51+
52+
commentsMap := make(map[int]Info)
53+
infoMap := make(map[int]Info)
54+
55+
for _, commentGroup := range node.Comments {
56+
for _, comment := range commentGroup.List {
57+
commentsMap[fset.Position(comment.Pos()).Line] = Info{Type: COMMENT, Comments: []string{NormalizeComment(comment.Text)}}
58+
}
59+
}
60+
61+
ast.Inspect(node, func(n ast.Node) bool {
62+
switch n := n.(type) {
63+
64+
case *ast.FuncDecl: // Function declaration
65+
funcName := n.Name.Name
66+
67+
start, end := n.Pos(), n.End()
68+
pos := fset.Position(n.Pos())
69+
endPos := fset.Position(n.Body.Rbrace)
70+
71+
funcInfo := Info{
72+
Type: FUNCTION,
73+
Name: fmt.Sprintf("%s %s:%d", funcName, filename, pos.Line),
74+
Range: [2]int{pos.Line, endPos.Line},
75+
Content: string(src[start-1 : end]),
76+
}
77+
78+
// Find function calls inside body
79+
ast.Inspect(n.Body, func(bodyNode ast.Node) bool {
80+
switch bodyNode := bodyNode.(type) {
81+
case *ast.CallExpr: // Function call
82+
if ident, ok := bodyNode.Fun.(*ast.Ident); ok {
83+
callPos := fset.Position(bodyNode.Pos())
84+
funcInfo.Calls = append(funcInfo.Calls,
85+
fmt.Sprintf("%s %s:%d", ident.Name, filename, callPos.Line))
86+
}
87+
}
88+
return true
89+
})
90+
91+
infoMap[fset.Position(n.Pos()).Line] = funcInfo
92+
93+
case *ast.GenDecl: // General declarations (variables, structs, etc.)
94+
if n.Tok == token.IMPORT {
95+
return true // Skip imports
96+
}
97+
98+
for _, spec := range n.Specs {
99+
switch spec := spec.(type) {
100+
case *ast.ValueSpec: // Global variable
101+
for _, ident := range spec.Names {
102+
103+
start, end := n.Pos(), n.End()
104+
pos := fset.Position(ident.Pos())
105+
endPos := fset.Position(spec.End())
106+
107+
typeName := "unknown"
108+
if spec.Type != nil {
109+
typeName = fmt.Sprintf("%s", spec.Type)
110+
}
111+
112+
infoMap[fset.Position(n.Pos()).Line] = Info{
113+
Name: fmt.Sprintf("%s %s:%d %s", ident.Name, filename, pos.Line, typeName),
114+
Type: GLOBAL,
115+
Range: [2]int{pos.Line, endPos.Line},
116+
Content: string(src[start-1 : end]),
117+
}
118+
119+
}
120+
121+
case *ast.TypeSpec: // Structs
122+
if structType, ok := spec.Type.(*ast.StructType); ok {
123+
124+
start, end := n.Pos(), n.End()
125+
pos := fset.Position(spec.Pos())
126+
endPos := fset.Position(structType.Fields.Closing)
127+
128+
fields := make(map[string]string)
129+
130+
for _, field := range structType.Fields.List {
131+
fieldType := "unknown"
132+
if field.Type != nil {
133+
fieldType = fmt.Sprintf("%s", field.Type)
134+
}
135+
for _, fieldName := range field.Names {
136+
fields[fieldName.Name] = fieldType
137+
}
138+
}
139+
140+
infoMap[pos.Line] = Info{
141+
Name: fmt.Sprintf("%s %s:%d", spec.Name.Name, filename, pos.Line),
142+
Type: STRUCT,
143+
Fields: fields,
144+
Range: [2]int{pos.Line, endPos.Line},
145+
Content: string(src[start-1 : end]),
146+
}
147+
}
148+
}
149+
}
150+
}
151+
return true
152+
})
153+
154+
sortedInfos := utils.SortMapByKeyDesc(infoMap)
155+
sortedComments := utils.SortMapByKeyDesc(commentsMap)
156+
157+
infos := []Info{}
158+
159+
for _, info := range sortedInfos {
160+
for _, comment := range sortedComments {
161+
if comment.Key < info.Value.Range[1] && !comment.Value.Processed {
162+
info.Value.Comments = append(info.Value.Comments, comment.Value.Comments...)
163+
comment.Value.Processed = true
164+
}
165+
}
166+
infos = append(infos, *info.Value)
167+
}
168+
169+
return infos, nil
170+
}
171+
172+
func NormalizeComment(comment string) string {
173+
// Remove line comment markers (//)
174+
comment = strings.TrimPrefix(comment, "//")
175+
176+
// Remove block comment markers (/* */)
177+
comment = strings.TrimPrefix(comment, "/*")
178+
comment = strings.TrimSuffix(comment, "*/")
179+
180+
// Remove leading '*' in block comments (common in formatted comments)
181+
comment = regexp.MustCompile(`(?m)^\s*\*\s?`).ReplaceAllString(comment, "")
182+
183+
// Trim leading and trailing spaces
184+
return strings.TrimSpace(comment)
185+
}
186+
187+
// Main function to run the parser on a file
188+
func main() {
189+
var infos []Info
190+
err := filepath.WalkDir("../", func(path string, d os.DirEntry, err error) error {
191+
if err != nil {
192+
fmt.Println("Error accessing path:", path, err)
193+
return nil
194+
}
195+
196+
// Skip directories
197+
if d.IsDir() {
198+
return nil
199+
}
200+
201+
// Process only Go files
202+
if strings.HasSuffix(path, ".go") {
203+
204+
// Call your function
205+
fileInfo, err := ParseGoFile(path)
206+
if err != nil {
207+
fmt.Println("Error parsing file:", err)
208+
return nil
209+
}
210+
211+
infos = append(infos, fileInfo...)
212+
}
213+
214+
return nil
215+
})
216+
if err != nil {
217+
fmt.Println("Error walking directory:", err)
218+
}
219+
220+
file, err := os.OpenFile("callgraph.js", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
221+
if err != nil {
222+
fmt.Println("Error opening coverage file:", err)
223+
return
224+
}
225+
defer file.Close()
226+
227+
if err = json.NewEncoder(file).Encode(infos); err != nil {
228+
fmt.Println(err)
229+
}
230+
}
231+
232+
func cover() {
233+
// Run tests and generate cover.out
234+
cmdTest := exec.Command("go", "test", "-coverprofile=cover.txt", "./...")
235+
cmdTest.Stdout = os.Stdout
236+
cmdTest.Stderr = os.Stderr
237+
238+
if err := cmdTest.Run(); err != nil {
239+
fmt.Println("Error running tests:", err)
240+
return
241+
}
242+
243+
// Append coverage results to cover.txt
244+
file, err := os.OpenFile("cover.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
245+
if err != nil {
246+
fmt.Println("Error opening coverage file:", err)
247+
return
248+
}
249+
defer file.Close()
250+
251+
cmdCover := exec.Command("go", "tool", "cover", "-html=./cover.txt", "-o", "./cover.html")
252+
cmdCover.Stdout = file
253+
cmdCover.Stderr = os.Stderr
254+
255+
if err := cmdCover.Run(); err != nil {
256+
fmt.Println("Error generating coverage report:", err)
257+
return
258+
}
259+
260+
fmt.Println("Coverage report appended to cover.txt")
261+
}

0 commit comments

Comments
 (0)