-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
162 lines (142 loc) · 4 KB
/
main.go
1
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
package main
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/alecthomas/kong"
"github.com/crockeo/schoner/pkg/astutil"
"github.com/crockeo/schoner/pkg/graph"
"github.com/crockeo/schoner/pkg/phases/fileinfo"
"github.com/crockeo/schoner/pkg/phases/references"
"github.com/crockeo/schoner/pkg/set"
"github.com/crockeo/schoner/pkg/visualize"
"github.com/crockeo/schoner/pkg/walk"
)
func main() {
if err := mainImpl(); err != nil {
errMsg := strings.TrimSpace(err.Error())
fmt.Fprintln(os.Stderr, errMsg)
os.Exit(1)
}
}
type args struct {
Visualize visualizeArgs `cmd:"" help:"Visualize references in a project."`
Unreachable unreachableArgs `cmd:"" help:"List all unreachable declarations in a project."`
}
type visualizeArgs struct {
OutputDir string `name:"output-dir" help:"The directory in which .svg files will be generated."`
Paths []string `arg:"" name:"path" help:"List of projects to visualize." type:"path"`
}
type unreachableArgs struct {
Paths []string `arg:"" name:"path" help:"List of projects to analyze." type:"path"`
}
func mainImpl() error {
args := args{}
ctx := kong.Parse(&args)
switch ctx.Command() {
case "visualize <path>":
return visualizeMain(args.Visualize)
case "unreachable <path>":
return unreachableMain(args.Unreachable)
default:
panic("unreachable")
}
}
func visualizeMain(args visualizeArgs) error {
outputDir, err := filepath.Abs(args.OutputDir)
if err != nil {
return err
}
// TODO: check that outputDir is actually a directory
for _, path := range args.Paths {
path, err := filepath.Abs(path)
if err != nil {
return err
}
// TODO: check that path is a directory
analysis, err := analyzeProject(path)
if err != nil {
return err
}
visualize.Visualize(
fmt.Sprintf("%s.svg", filepath.Join(outputDir, filepath.Base(path))),
analysis.ReferenceGraph,
analysis.Entrypoints,
analysis.Unreachable,
)
}
return nil
}
func unreachableMain(args unreachableArgs) error {
for _, path := range args.Paths {
path, err := filepath.Abs(path)
if err != nil {
return err
}
// TODO: check that path is a directory
analysis, err := analyzeProject(path)
if err != nil {
return err
}
unreachableByName := map[string]fileinfo.Declaration{}
unreachableNames := make([]string, 0, len(analysis.Unreachable))
for decl := range analysis.Unreachable {
filename := decl.Parent.Filename
if !strings.HasPrefix(filename, path) {
return fmt.Errorf("file %s does not begin with expected path %s", filename, path)
}
filename = filename[len(path):]
filename = strings.TrimPrefix(filename, "/")
unreachableName := astutil.Qualify(filename, decl.Name)
unreachableByName[unreachableName] = decl
unreachableNames = append(unreachableNames, unreachableName)
}
sort.Strings(unreachableNames)
for _, unreachableName := range unreachableNames {
fmt.Println(unreachableName)
}
return nil
}
return nil
}
type analysis struct {
FileInfos map[string]*fileinfo.FileInfo
ReferenceGraph graph.Graph[fileinfo.Declaration]
Unreachable set.Set[fileinfo.Declaration]
Entrypoints set.Set[fileinfo.Declaration]
}
func analyzeProject(path string) (analysis, error) {
// TODO: make these configurable?
walkOptions := walk.WithOptions(
walk.WithIgnoreDirs(".git"),
// walk.WithIgnoreTests(true),
)
fileInfos, err := fileinfo.FindFileInfos(path, walkOptions)
if err != nil {
return analysis{}, err
}
referenceGraph, err := references.BuildReferenceGraph(path, fileInfos, walkOptions)
if err != nil {
return analysis{}, err
}
unreachable := set.NewSet[fileinfo.Declaration]()
entrypoints := set.NewSet[fileinfo.Declaration]()
for decl := range referenceGraph {
unreachable.Add(decl)
if decl.Parent.Entrypoints.Contains(decl.Name) {
entrypoints.Add(decl)
}
}
_ = referenceGraph.DFS(entrypoints.ToSlice(), func(node fileinfo.Declaration) error {
unreachable.Remove(node)
return nil
})
return analysis{
fileInfos,
referenceGraph,
unreachable,
entrypoints,
}, nil
}