Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improving DX, adding a --ci option and listing generated reports in output #68

Merged
merged 2 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package main

import (
"bufio"
"fmt"
"os"

"github.com/charmbracelet/lipgloss"
"github.com/halleck45/ast-metrics/src/Cli"
"github.com/halleck45/ast-metrics/src/Command"
"github.com/halleck45/ast-metrics/src/Configuration"
Expand Down Expand Up @@ -82,8 +84,8 @@ func main() {
},
// JSON report
&cli.StringFlag{
Name: "report-json",
Usage: "Generate a report in JSON format",
Name: "report-json",
Usage: "Generate a report in JSON format",
Category: "Report",
},
// Watch mode
Expand All @@ -92,6 +94,12 @@ func main() {
Usage: "Re-run the analysis when files change",
Category: "Global options",
},
// CI mode (alias of --non-interactive, --report-html and --report-markdown)
&cli.BoolFlag{
Name: "ci",
Usage: "Enable CI mode",
Category: "Global options",
},
// Configuration
&cli.StringFlag{
Name: "config",
Expand All @@ -114,14 +122,16 @@ func main() {

// get option --non-interactive
isInteractive := true
if cCtx.Bool("non-interactive") {
if cCtx.Bool("non-interactive") || cCtx.Bool("ci") {
pterm.DisableColor()
isInteractive = false
}

// Stdout
outWriter := bufio.NewWriter(os.Stdout)
pterm.DefaultBasicText.Println(pterm.LightMagenta(" AST Metrics ") + "is a language-agnostic static code analyzer.")
var style = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")).Bold(true)
fmt.Println(style.Render("\n🦫 AST Metrics is a language-agnostic static code analyzer."))
fmt.Println("")

// Prepare configuration object
configuration := Configuration.NewConfiguration()
Expand Down Expand Up @@ -183,6 +193,19 @@ func main() {
configuration.Reports.Json = cCtx.String("report-json")
}

// CI mode
if cCtx.Bool("ci") {
if configuration.Reports.Html == "" {
configuration.Reports.Html = "ast-metrics-html-report"
}
if configuration.Reports.Markdown == "" {
configuration.Reports.Markdown = "ast-metrics-markdown-report.md"
}
if configuration.Reports.Json == "" {
configuration.Reports.Json = "ast-metrics-report.json"
}
}

// Compare with
if cCtx.String("compare-with") != "" {
configuration.CompareWith = cCtx.String("compare-with")
Expand Down
92 changes: 92 additions & 0 deletions src/Cli/ScreenEnd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package Cli

import (
"fmt"

tea "github.com/charmbracelet/bubbletea"
"github.com/halleck45/ast-metrics/src/Analyzer"
"github.com/halleck45/ast-metrics/src/Configuration"
pb "github.com/halleck45/ast-metrics/src/NodeType"
"github.com/halleck45/ast-metrics/src/Report"
)

type ScreenEnd struct {
isInteractive bool
files []*pb.File
projectAggregated Analyzer.ProjectAggregated
// program
tea *tea.Program
// reports
configuration Configuration.Configuration
reports []Report.GeneratedReport
}

func NewScreenEnd(
isInteractive bool,
files []*pb.File,
projectAggregated Analyzer.ProjectAggregated,
configuration Configuration.Configuration,
reports []Report.GeneratedReport,
) *ScreenEnd {
return &ScreenEnd{
isInteractive: isInteractive,
files: files,
projectAggregated: projectAggregated,
configuration: configuration,
reports: reports,
}
}

type modelEnd struct {
}

func (m modelEnd) Init() tea.Cmd {
return nil
}

func (m *modelEnd) Reset(files []*pb.File, projectAggregated Analyzer.ProjectAggregated) {
}

func (m modelEnd) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}

func (m modelEnd) View() string {
return ""
}
Comment on lines +40 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Now I have enough context to provide the final response:

The modelEnd implementation is necessary but can be improved

The modelEnd struct is a required implementation as it's used by ScreenEnd to satisfy the Screen interface contract, which requires a GetModel() method returning a tea.Model. However, its current empty implementation could be enhanced:

  • The empty View() method should return a meaningful message or confirmation of completion
  • The Reset() method should handle cleanup if needed
  • Consider adding documentation explaining its purpose as the final screen
🔗 Analysis chain

Verify the necessity of the modelEnd implementation.

The modelEnd struct appears to be a placeholder implementation of the tea.Model interface. If it's not actively used, consider removing it to reduce unnecessary complexity. If it's intended for future use, please add TODO comments explaining the planned functionality.


Let me search for more context about where this model is being used.


Let me check if there's any interface or contract that requires this tea.Model implementation.


Let me check one of the other screen implementations to understand the pattern better.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check if modelEnd is used anywhere in the codebase
rg "modelEnd" --type go

Length of output: 412


Script:

#!/bin/bash
# Check the entire ScreenEnd.go file to understand the context
cat src/Cli/ScreenEnd.go

# Search for any references to ScreenEnd
rg "ScreenEnd" --type go -A 5

Length of output: 4312


Script:

#!/bin/bash
# Look for any interface definitions that ScreenEnd might be implementing
rg "type.*interface" --type go -A 5

# Check for any other screens to understand the pattern
fd -e go -x grep -l "GetModel()" {}

Length of output: 2133


Script:

#!/bin/bash
# Check implementation of another screen for comparison
cat src/Cli/ScreenHome.go

# Also check the Screen interface definition
cat src/Cli/Screen.go

Length of output: 7224


func (r *ScreenEnd) Render() {
// List reports
if r.configuration.Reports.HasReports() {

fmt.Println("\n📁 These reports have been generated:")

for _, report := range r.reports {
fmt.Println("\n ✔ " + report.Path + " (" + report.Type + ")")
fmt.Println("\n " + report.Description)
}

fmt.Println("")
}

// Tips if configuration file does not exist
if !r.configuration.IsComingFromConfigFile {
fmt.Println("\n💡 We noticed that you haven't yet created a configuration file. You can create a .ast-metrics.yaml configuration file by running: ast-metrics init")
fmt.Println("")
}

fmt.Println("\n🌟 If you like AST Metrics, please consider starring the project on GitHub: https://github.com/Halleck45/ast-metrics/. Thanks!")
fmt.Println("")

}

func (r *ScreenEnd) Reset(files []*pb.File, projectAggregated Analyzer.ProjectAggregated) {
}

func (r ScreenEnd) GetModel() tea.Model {
return modelEnd{}
}

func (r ScreenEnd) GetScreenName() string {
return "End"
}
11 changes: 9 additions & 2 deletions src/Cli/ScreenHome.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/fsnotify/fsnotify"
"github.com/halleck45/ast-metrics/src/Analyzer"
pb "github.com/halleck45/ast-metrics/src/NodeType"
Expand Down Expand Up @@ -74,7 +75,8 @@ func (r *ScreenHome) Render() {

if !r.isInteractive {
// If not interactive
fmt.Println("No interactive mode detected.")
var style = lipgloss.NewStyle().Foreground(lipgloss.Color("#666666")).Italic(true)
fmt.Println(style.Render("No interactive mode detected."))
return
}

Expand Down Expand Up @@ -107,7 +109,7 @@ func (r *ScreenHome) Reset(files []*pb.File, projectAggregated Analyzer.ProjectA
}

// Get Tea model
func (r ScreenHome) GetModel() modelChoices {
func (r ScreenHome) GetModel() tea.Model {

// Prepare list of accepted screens
m := modelChoices{files: r.files, projectAggregated: r.projectAggregated}
Expand Down Expand Up @@ -237,6 +239,11 @@ func (m modelChoices) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}

// GetScreenName returns the name of the screen
func (r ScreenHome) GetScreenName() string {
return "Home"
}

// Color a string's foreground with the given value.
func colorFg(val, color string) string {
return termenv.String(val).Foreground(term.Color(color)).String()
Expand Down
57 changes: 30 additions & 27 deletions src/Cli/ScreenHome_test.go
Original file line number Diff line number Diff line change
@@ -1,46 +1,49 @@
package Cli

import (
"testing"
"testing"

"github.com/halleck45/ast-metrics/src/Analyzer"
pb "github.com/halleck45/ast-metrics/src/NodeType"
tea "github.com/charmbracelet/bubbletea"
"github.com/halleck45/ast-metrics/src/Analyzer"
pb "github.com/halleck45/ast-metrics/src/NodeType"
"github.com/stretchr/testify/assert"
)

func TestNewScreenHome(t *testing.T) {
isInteractive := true
files := []*pb.File{}
projectAggregated := Analyzer.ProjectAggregated{}
isInteractive := true
files := []*pb.File{}
projectAggregated := Analyzer.ProjectAggregated{}

screenHome := NewScreenHome(isInteractive, files, projectAggregated)
screenHome := NewScreenHome(isInteractive, files, projectAggregated)

if screenHome.isInteractive != isInteractive {
t.Errorf("Expected isInteractive to be %v, but got %v", isInteractive, screenHome.isInteractive)
}
if screenHome.isInteractive != isInteractive {
t.Errorf("Expected isInteractive to be %v, but got %v", isInteractive, screenHome.isInteractive)
}

if len(screenHome.files) != len(files) {
t.Errorf("Expected files to be %v, but got %v", files, screenHome.files)
}
if len(screenHome.files) != len(files) {
t.Errorf("Expected files to be %v, but got %v", files, screenHome.files)
}
}

func TestGetModel(t *testing.T) {
isInteractive := true
files := []*pb.File{}
projectAggregated := Analyzer.ProjectAggregated{}
isInteractive := true
files := []*pb.File{}
projectAggregated := Analyzer.ProjectAggregated{}

screenHome := NewScreenHome(isInteractive, files, projectAggregated)
model := screenHome.GetModel()
screenHome := NewScreenHome(isInteractive, files, projectAggregated)
model := screenHome.GetModel()

if len(model.files) != len(files) {
t.Errorf("Expected files to be %v, but got %v", files, model.files)
}
// sending the "enter" key
model, _ = model.Update(tea.KeyMsg{Type: tea.KeyEnter})
_, ok := model.(modelScreenSummary)
assert.True(t, ok)
}

func TestInit(t *testing.T) {
model := modelChoices{}
cmd := model.Init()
model := modelChoices{}
cmd := model.Init()

if cmd != nil {
t.Errorf("Expected cmd to be nil, but got %v", cmd)
}
}
if cmd != nil {
t.Errorf("Expected cmd to be nil, but got %v", cmd)
}
}
2 changes: 1 addition & 1 deletion src/Cli/ScreenHtmlReport.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (m modelScreenHtmlReport) View() string {
// Generate report
// report: html
htmlReportGenerator := Report.NewHtmlReportGenerator(directory)
err := htmlReportGenerator.Generate(m.files, m.projectAggregated)
_, err := htmlReportGenerator.Generate(m.files, m.projectAggregated)
if err != nil {
return fmt.Sprintf("Error generating report: %s", err)
}
Expand Down
47 changes: 28 additions & 19 deletions src/Command/AnalyzeCommand.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import (
"github.com/halleck45/ast-metrics/src/Configuration"
"github.com/halleck45/ast-metrics/src/Engine"
pb "github.com/halleck45/ast-metrics/src/NodeType"
Report "github.com/halleck45/ast-metrics/src/Report/Html"
Report "github.com/halleck45/ast-metrics/src/Report"
Html "github.com/halleck45/ast-metrics/src/Report/Html"
Json "github.com/halleck45/ast-metrics/src/Report/Json"
Markdown "github.com/halleck45/ast-metrics/src/Report/Markdown"
"github.com/halleck45/ast-metrics/src/Storage"
Expand Down Expand Up @@ -174,24 +175,28 @@ func (v *AnalyzeCommand) Execute() error {
v.spinner.Increment()
}

// report: html
htmlReportGenerator := Report.NewHtmlReportGenerator(v.configuration.Reports.Html)
err = htmlReportGenerator.Generate(allResults, projectAggregated)
if err != nil {
pterm.Error.Println("Cannot generate html report: " + err.Error())
return err
}
// report: markdown
markdownReportGenerator := Markdown.NewMarkdownReportGenerator(v.configuration.Reports.Markdown)
err = markdownReportGenerator.Generate(allResults, projectAggregated)
if err != nil {
pterm.Error.Println("Cannot generate markdown report: " + err.Error())
}
// report: json
jsonReportGenerator := Json.NewJsonReportGenerator(v.configuration.Reports.Json)
err = jsonReportGenerator.Generate(allResults, projectAggregated)
if err != nil {
pterm.Error.Println("Cannot generate json report: " + err.Error())
reporters := []Report.Reporter{}
generatedReports := []Report.GeneratedReport{}
if v.configuration.Reports.HasReports() {
if v.configuration.Reports.Html != "" {
reporters = append(reporters, Html.NewHtmlReportGenerator(v.configuration.Reports.Html))
}
if v.configuration.Reports.Markdown != "" {
reporters = append(reporters, Markdown.NewMarkdownReportGenerator(v.configuration.Reports.Markdown))
}
if v.configuration.Reports.Json != "" {
reporters = append(reporters, Json.NewJsonReportGenerator(v.configuration.Reports.Json))
}

// Generate reports
for _, reporter := range reporters {
reports, err := reporter.Generate(allResults, projectAggregated)
if err != nil {
pterm.Error.Println("Cannot generate report: " + err.Error())
return err
}
generatedReports = append(generatedReports, reports...)
}
}

// Evaluate requirements
Expand Down Expand Up @@ -253,6 +258,10 @@ func (v *AnalyzeCommand) Execute() error {
// Store state of the command
v.alreadyExecuted = true

// End screen
screen := Cli.NewScreenEnd(v.isInteractive, allResults, projectAggregated, *v.configuration, generatedReports)
screen.Render()

if shouldFail {
os.Exit(1)
}
Expand Down
8 changes: 8 additions & 0 deletions src/Configuration/Configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type Configuration struct {

// Location of cache files
Storage *Storage.Workdir

IsComingFromConfigFile bool
}

type ConfigurationReport struct {
Expand All @@ -35,6 +37,11 @@ type ConfigurationReport struct {
Json string `yaml:"json"`
}

// function HasReports() bool {
func (c *ConfigurationReport) HasReports() bool {
return c.Html != "" || c.Markdown != "" || c.Json != ""
}

type ConfigurationRequirements struct {
Rules *struct {
CyclomaticComplexity *ConfigurationDefaultRule `yaml:"cyclomatic_complexity"`
Expand Down Expand Up @@ -64,6 +71,7 @@ func NewConfiguration() *Configuration {
Watching: false,
CompareWith: "",
Storage: Storage.Default(),
IsComingFromConfigFile: false,
}
}

Expand Down
Loading
Loading