Skip to content

Commit 010fb67

Browse files
committed
'Dump' command
* Added a 'dump' command to allow dumping internal state/representation for manual inspection & debugging purposes Dump command currently accepts only a `graph` subcommand and outputs only in `dot` format * Removed some un-necessary code * Removed log clutter during tests * Updated `SymbolGraph.FindByKind` to accept 'rest' kinds * Fixed a case of panic when encountering certain embedded fields in controllers
1 parent f8e6d5f commit 010fb67

File tree

14 files changed

+483
-99
lines changed

14 files changed

+483
-99
lines changed

cmd/dump.go

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
8+
"github.com/gopher-fleece/gleece/v2/core/pipeline"
9+
"github.com/gopher-fleece/gleece/v2/definitions"
10+
"github.com/gopher-fleece/gleece/v2/infrastructure/logger"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
type DumpFormat string
15+
16+
const (
17+
DumpFormatDot DumpFormat = "dot"
18+
DumpFormatPlain DumpFormat = "plain"
19+
)
20+
21+
var (
22+
gleeceConfigPath string
23+
dumpOutput string
24+
dumpFormat string
25+
)
26+
27+
// dumpCmd is a command used to dump information such as controllers, models, graph representations etc.
28+
var dumpCmd = &cobra.Command{
29+
Use: "dump",
30+
Short: "Dumps information about a Gleece project",
31+
Long: `The dump command returns information on the given Gleece project.
32+
This information is obtained by running the project through the analysis facilities`,
33+
}
34+
35+
var graphCmd = &cobra.Command{
36+
Use: "graph",
37+
Short: "Dumps the primary symbolic graph to a textual representation",
38+
Long: `The 'graph' command can be used to dump a full representation of Gleece primary symbolic graph.
39+
This graph depict all entities and their relations and is most useful as means to visualize entity relations and dependencies`,
40+
PreRunE: func(cmd *cobra.Command, args []string) error {
41+
switch dumpFormat {
42+
case string(DumpFormatDot), string(DumpFormatPlain), "":
43+
default:
44+
return fmt.Errorf("invalid --format: '%s' (want dot|plain)", dumpFormat)
45+
}
46+
return nil
47+
},
48+
RunE: func(cmd *cobra.Command, args []string) error {
49+
err := dumpGraph(cmd)
50+
if err != nil {
51+
logger.Fatal("Failed to dump graph - %v", err)
52+
}
53+
return err
54+
},
55+
}
56+
57+
func initDumpCommandHierarchy() {
58+
graphCmd.Flags().StringVarP(
59+
&dumpFormat,
60+
"format",
61+
"f",
62+
"dot",
63+
"-f=dot",
64+
)
65+
66+
dumpCmd.PersistentFlags().StringVarP(
67+
&dumpOutput,
68+
"output",
69+
"o",
70+
"",
71+
"-o \"/target-directory/dump.dot\"",
72+
)
73+
74+
dumpCmd.PersistentFlags().StringVarP(
75+
&gleeceConfigPath,
76+
"config",
77+
"c",
78+
"./gleece.config.json",
79+
"-c \"/project-directory/gleece.config.json\"",
80+
)
81+
82+
dumpCmd.AddCommand(graphCmd)
83+
}
84+
85+
func resetDumpCommand() {
86+
gleeceConfigPath = ""
87+
dumpOutput = ""
88+
dumpFormat = ""
89+
}
90+
91+
func loadGleeceConfig(cmd *cobra.Command) (*definitions.GleeceConfig, error) {
92+
gleeceConfigPath, err := cmd.Flags().GetString("config")
93+
if err != nil {
94+
logger.Error("Failed to read Gleece config location from CLI - %v", err)
95+
return nil, fmt.Errorf("failed to read Gleece config location from CLI - %v", err)
96+
}
97+
98+
trimmedPath := strings.Trim(gleeceConfigPath, "\"")
99+
if trimmedPath == "" {
100+
trimmedPath = "./gleece.config.json"
101+
logger.Info("Gleece config path was no specified. Defaulting to '%s'", trimmedPath)
102+
}
103+
104+
gleeceConfig, err := LoadGleeceConfig(trimmedPath)
105+
if err != nil {
106+
logger.Error("Failed to load Gleece config from '%s' - %v", trimmedPath, err)
107+
return nil, fmt.Errorf("failed to load Gleece config from '%s' - %v", trimmedPath, err)
108+
}
109+
110+
return gleeceConfig, nil
111+
112+
}
113+
114+
func getPipeline(cmd *cobra.Command) (*pipeline.GleecePipeline, error) {
115+
gleeceConfig, err := loadGleeceConfig(cmd)
116+
if err != nil {
117+
return nil, err
118+
}
119+
120+
pipe, err := pipeline.NewGleecePipeline(gleeceConfig)
121+
if err != nil {
122+
logger.Error("Failed to construct the analysis pipeline - %v", err)
123+
return nil, fmt.Errorf("failed to construct the analysis pipeline - %v", err)
124+
}
125+
126+
return &pipe, nil
127+
}
128+
129+
func dumpGraph(cmd *cobra.Command) error {
130+
logger.Info("Dumping graph to '%s' format", dumpFormat)
131+
pipe, err := getPipeline(cmd)
132+
if err != nil {
133+
return err
134+
}
135+
136+
err = pipe.GenerateGraph()
137+
if err != nil {
138+
logger.Error("Failed to construct a symbol graph for the project - %v", err)
139+
return fmt.Errorf("failed to construct a symbol graph for the project - %v", err)
140+
}
141+
142+
var text string
143+
switch dumpFormat {
144+
case string(DumpFormatDot):
145+
text = pipe.Graph().ToDot(nil)
146+
case string(DumpFormatPlain):
147+
text = pipe.Graph().String()
148+
default:
149+
logger.Debug("Dump format not specified - defaulting to DOT")
150+
text = pipe.Graph().ToDot(nil)
151+
}
152+
153+
if dumpOutput == "" {
154+
logger.Debug("Output not specified - using stdout")
155+
cmd.Println(string(text))
156+
return nil
157+
}
158+
159+
logger.Debug("Writing %d bytes to '%s'", len(text), dumpOutput)
160+
if err = os.WriteFile(dumpOutput, []byte(text), 0o644); err != nil {
161+
logger.Error("Failed to write to '%s' - %v", dumpOutput, err)
162+
return fmt.Errorf("failed to write to '%s' - %v", dumpOutput, err)
163+
}
164+
165+
logger.Info("Graph successfully written to '%s'", dumpOutput)
166+
return nil
167+
}

cmd/generate.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import (
1010
var generateCmd = &cobra.Command{
1111
Use: "generate spec-and-routes --config \"/path/to/gleece.config.json\"",
1212
Short: "Generate OpenAPI schema and routing middlewares from a Go project",
13-
Run: func(cmd *cobra.Command, args []string) {
14-
},
1513
}
1614

1715
var specCommand = &cobra.Command{

cmd/root.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ var rootCmd = &cobra.Command{
2828
),
2929
PersistentPreRun: func(cmd *cobra.Command, args []string) {
3030
if !cliArgs.NoBanner {
31-
logger.Raw(arguments.GopherAscii)
31+
fmt.Fprintln(cmd.OutOrStdout(), arguments.GopherAscii)
3232
}
3333

3434
// This is basically safe as Cobra have already validated or provided default
@@ -47,6 +47,12 @@ var rootCmd = &cobra.Command{
4747
},
4848
}
4949

50+
func Reset() {
51+
// There must be a better way to orchestrate this...
52+
cliArgs = arguments.CliArguments{}
53+
resetDumpCommand()
54+
}
55+
5056
func Execute() {
5157
err := rootCmd.Execute()
5258
if err != nil {
@@ -92,6 +98,8 @@ func ExecuteWithArgs(args []string, redirectLogs bool) arguments.ExecuteWithArgs
9298

9399
func init() {
94100
initGenerateCommandHierarchy()
101+
initDumpCommandHierarchy()
102+
95103
rootCmd.PersistentFlags().BoolVar(
96104
&cliArgs.NoBanner,
97105
"no-banner",
@@ -109,4 +117,5 @@ func init() {
109117

110118
rootCmd.AddCommand(versionCmd)
111119
rootCmd.AddCommand(generateCmd)
120+
rootCmd.AddCommand(dumpCmd)
112121
}

common/enumerations.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package common
22

3+
import "fmt"
4+
35
type SymKind string
46

57
const (
@@ -150,3 +152,44 @@ func ToSpecialType(s string) (SpecialType, bool) {
150152
return "", false
151153
}
152154
}
155+
156+
func ToSymbolKind(value string) (SymKind, error) {
157+
switch value {
158+
case string(SymKindUnknown):
159+
return SymKindUnknown, nil
160+
case string(SymKindPackage):
161+
return SymKindPackage, nil
162+
case string(SymKindStruct):
163+
return SymKindStruct, nil
164+
case string(SymKindController):
165+
return SymKindController, nil
166+
case string(SymKindInterface):
167+
return SymKindInterface, nil
168+
case string(SymKindAlias):
169+
return SymKindAlias, nil
170+
case string(SymKindEnum):
171+
return SymKindEnum, nil
172+
case string(SymKindEnumValue):
173+
return SymKindEnumValue, nil
174+
case string(SymKindFunction):
175+
return SymKindFunction, nil
176+
case string(SymKindReceiver):
177+
return SymKindReceiver, nil
178+
case string(SymKindField):
179+
return SymKindField, nil
180+
case string(SymKindParameter):
181+
return SymKindParameter, nil
182+
case string(SymKindVariable):
183+
return SymKindVariable, nil
184+
case string(SymKindConstant):
185+
return SymKindConstant, nil
186+
case string(SymKindReturnType):
187+
return SymKindReturnType, nil
188+
case string(SymKindBuiltin):
189+
return SymKindBuiltin, nil
190+
case string(SymKindSpecialBuiltin):
191+
return SymKindSpecialBuiltin, nil
192+
default:
193+
return "", fmt.Errorf("invalid SymKind '%q'", value)
194+
}
195+
}

gast/ast.utils.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ func IsFuncDeclReceiverForStruct(structName string, funcDecl *ast.FuncDecl) bool
2626
case *ast.Ident:
2727
return expr.Name == structName
2828
case *ast.StarExpr:
29-
return expr.X.(*ast.Ident).Name == structName
30-
default:
31-
return false
29+
ident, isIdent := expr.X.(*ast.Ident)
30+
if isIdent && ident.Name == structName {
31+
return true
32+
}
3233
}
34+
35+
return false
3336
}
3437

3538
// DoesStructEmbedStruct tests the given structToCheck in the *ast.File `sourceFile`

graphs/symboldg/graph.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -386,12 +386,29 @@ func (g *SymbolGraph) Exists(key graphs.SymbolKey) bool {
386386
return g.Get(key) != nil
387387
}
388388

389-
func (g *SymbolGraph) FindByKind(kind common.SymKind) []*SymbolNode {
390-
var results []*SymbolNode
391-
392-
for _, node := range g.nodes {
393-
if node.Kind == kind {
394-
results = append(results, node)
389+
// FindByKinds searches the graph for nodes of the given kinds.
390+
//
391+
// Returns an empty slice if no matches were found.
392+
//
393+
// Nodes are returned by address and are *not* safe to mutate.
394+
func (g *SymbolGraph) FindByKind(kinds ...common.SymKind) []*SymbolNode {
395+
results := []*SymbolNode{}
396+
397+
switch len(kinds) {
398+
case 0:
399+
return results
400+
case 1:
401+
for _, node := range g.nodes {
402+
if node.Kind == kinds[0] {
403+
results = append(results, node)
404+
}
405+
}
406+
default:
407+
kindsSet := mapset.NewThreadUnsafeSet(kinds...)
408+
for _, node := range g.nodes {
409+
if kindsSet.ContainsOne(node.Kind) {
410+
results = append(results, node)
411+
}
395412
}
396413
}
397414

graphs/symboldg/interfaces.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ type SymbolGraphBuilder interface {
3131
Exists(key graphs.SymbolKey) bool
3232
Get(key graphs.SymbolKey) *SymbolNode
3333
GetEdges(key graphs.SymbolKey, kinds []SymbolEdgeKind) map[string]SymbolEdgeDescriptor
34-
FindByKind(kind common.SymKind) []*SymbolNode
34+
FindByKind(kind... common.SymKind) []*SymbolNode
3535

3636
IsPrimitivePresent(primitive common.PrimitiveType) bool
3737
IsSpecialPresent(special common.SpecialType) bool

infrastructure/logger/logger.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package logger
22

33
import (
4+
"flag"
45
"fmt"
56
"log"
67
)
@@ -54,7 +55,10 @@ func getPrintPrefix(level LogLevel) string {
5455
// SetLogLevel Sets the logger's verbosity level
5556
func SetLogLevel(level LogLevel) {
5657
verbosityLevel = level
57-
System("Verbosity level set to %s\n", getPrintPrefix(level))
58+
// This message is annoying during testing
59+
if flag.Lookup("test.v") == nil {
60+
System("Verbosity level set to %s\n", getPrintPrefix(level))
61+
}
5862
}
5963

6064
// GetLogLevel returns the logger's current verbosity level
@@ -63,7 +67,7 @@ func GetLogLevel() LogLevel {
6367
}
6468

6569
// Prints a message, if level is greater to or equal to the currently set verbosity level
66-
func logger(level LogLevel, format string, v ...interface{}) {
70+
func logger(level LogLevel, format string, v ...any) {
6771
if level >= verbosityLevel {
6872
prefix := fmt.Sprintf("[%s]", getPrintPrefix(level))
6973
message := fmt.Sprintf("%-8s %s", prefix, format)
@@ -72,34 +76,34 @@ func logger(level LogLevel, format string, v ...interface{}) {
7276
}
7377

7478
// Prints a debug message
75-
func Debug(format string, v ...interface{}) {
79+
func Debug(format string, v ...any) {
7680
logger(LogLevelDebug, format, v...)
7781
}
7882

7983
// Prints an info message
80-
func Info(format string, v ...interface{}) {
84+
func Info(format string, v ...any) {
8185
logger(LogLevelInfo, format, v...)
8286
}
8387

8488
// Prints a warning message
85-
func Warn(format string, v ...interface{}) {
89+
func Warn(format string, v ...any) {
8690
logger(LogLevelWarn, format, v...)
8791
}
8892

8993
// Prints an error message
90-
func Error(format string, v ...interface{}) {
94+
func Error(format string, v ...any) {
9195
logger(LogLevelError, format, v...)
9296
}
9397

9498
// Prints a fatal message
95-
func Fatal(format string, v ...interface{}) {
99+
func Fatal(format string, v ...any) {
96100
logger(LogLevelFatal, format, v...)
97101
}
98102

99-
func System(format string, v ...interface{}) {
103+
func System(format string, v ...any) {
100104
message := fmt.Sprintf("[SYSTEM] %s", format)
101105
log.Printf(message, v...)
102106
}
103-
func Raw(format string, v ...interface{}) {
107+
func Raw(format string, v ...any) {
104108
log.Printf(format, v...)
105109
}

0 commit comments

Comments
 (0)