Halleck45 / ast-metrics

AST Metrics is a language-agnostic static code analyzer.
https://halleck45.github.io/ast-metrics/
MIT License
71 stars 7 forks source link

Feature request: Add a `--report-json` option #45

Closed Halleck45 closed 4 months ago

Halleck45 commented 7 months ago

It would be great to have a --report-json option. This is not a huge development, and it would be a great first contribution to Golang.

So, if you want to learn Golang, this is probably the right opportunity! :heart:

Here is a short tutorial to try to guide you through this task if you are interested.

:pray: Thank you for your help!

Halleck45 commented 7 months ago

This tutorial will guide you through the process of adding a new command-line option, --report-json, to the project. This option will allow users to generate a report in JSON format. Even if you're not familiar with Go, you should be able to follow along.

If there are any typos in this tutorial, I apologize. Please feel free to point them out to me.

This feature would be useful for users who want to process the analysis results programmatically or integrate them with other tools, and is asked in this discussion

Prerequisites

Install Golang on your machine. You can download it from the official website.

Step 1: Understand the existing code

Open the main.go file

This file is responsible for loading a configuration file and processing command-line arguments.

You can see that the program uses the cli package to parse command-line arguments. The cli package is a popular package for building command-line applications in Go.

For example, the --report-html option is managed with:

&cli.StringFlag{
    Name:     "report-html",
    Usage:    "Generate an HTML report",
    Category: "Report",
},

Step 2: Add the new option

You'll need to add the --report-json option to the list of command-line options that the program accepts. This is typically done in the function that sets up the command-line parser. Look for a function call that looks something like this:

cli.BoolFlag{
    Name:  "report-json",
    Usage: "Generate a report in JSON format",
}

Step 3: Use the new option

Back in the code main.go file. You'll need to check if the --report-json option was provided by the user. You can do this with cCtx.Bool("report-json"). If it's true, you'll want to generate a report in JSON format.

// Reports
// ...
if cCtx.String("report-json") != "" {
    configuration.Reports.Json = cCtx.String("report-json")
}

Step 4 : Add the option to the configuration

Open the src/Configuration/Configuration.go file. This file contains the definition of the Configuration struct, which holds the program's configuration settings.

Add a new field to the Configuration struct to store the path to the JSON report file:

type ConfigurationReport struct {
    Html     string `yaml:"html"`
    Markdown string `yaml:"markdown"`
    Json     bool   `yaml:"json"`
}

Step 5: Generate the JSON report

Open the src/Command/AnalyzeCommand.go file. This file contains the AnalyzeCommand struct, which is responsible for running the analysis on the codebase.

Add a call to our future function JsonReportGenerator.Generate:

// report: json
jsonReportGenerator := JsonReport.NewJsonReportGenerator(v.configuration.Reports.Json)
err = jsonReportGenerator.Generate(allResults, projectAggregated)
if err != nil {
    pterm.Error.Println("Cannot generate json report: " + err.Error())
}

Now, you'll need to implement the JsonReportGenerator.Generate function.

Create a new file called src/Report/JsonReport/JsonReportGenerator.go and add the following code:

package Report

import (
    "encoding/json"
    "io/ioutil"
    "os"
)

type JsonReportGenerator struct {
    ReportPath string
}

// This factory creates a new JsonReportGenerator
func NewJsonReportGenerator(ReportPath string) *JsonReportGenerator {
    return &JsonReportGenerator{ReportPath: ReportPath}
}

// Generate generates a JSON report
func (j *JsonReportGenerator) Generate(allResults []Result, projectAggregated AggregatedResult) error {

    // This code serializes the results to JSON
    jsonReport, err := json.MarshalIndent(allResults, "", "  ")
    if err != nil {
        return err
    }

    // This code writes the JSON report to a file
    err = ioutil.WriteFile(j.ReportPath, jsonReport, os.ModePerm)
    if err != nil {
        return err
    }

    return nil
}

Step 6: Test your changes

You can run the application with the --report-json option to generate a JSON report.

go run . analyze --report-json=report.json

If all is well, you should see a new file called report.json in the root directory of the project.

Step 7: Added automatic tests

You should add tests to ensure that the JSON report is generated correctly.

Create a new file called src/Report/JsonReport/JsonReportGenerator_test.go and add the following code:

package Report

import (
    "testing"
)

func TestJsonReportGenerator_Generate(t *testing.T) {
    func TestGenerate(t *testing.T) {
    tests := []struct {
        name        string
        reportPath  string
        expectError bool
    }{
        {
            name:        "Test with valid report path",
            reportPath:  "/tmp/report.json",
            expectError: false,
        },
        {
            name:        "Test with empty report path",
            reportPath:  "",
            expectError: false,
        },
        {
            name:        "Test with non-writable report path",
            reportPath:  "/nonexistent/report.json",
            expectError: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            generator := &NewJsonReportGenerator{ReportPath: tt.reportPath}
            files := []*pb.File{}
            projectAggregated := Analyzer.ProjectAggregated{}

            err := generator.Generate(files, projectAggregated)

            if tt.expectError {
                if err == nil {
                    t.Errorf("Expected an error but got none")
                }
            } else {
                if err != nil {
                    t.Errorf("Did not expect an error but got: %v", err)
                } else {
                    if tt.reportPath != "" {
                        if _, err := os.Stat(tt.reportPath); os.IsNotExist(err) {
                            t.Errorf("Report file was not created")
                        } else {
                            // cleanup
                            os.Remove(tt.reportPath)
                        }
                    }
                }
            }
        })
    }
}

This test is very basic and only checks if the report file is created. You should add more tests to cover edge cases and ensure that the JSON report is generated correctly.

Run your test with the command:

go test ./...
julian776 commented 7 months ago

Reuse src/Analyzer/Aggregator.go types? (Struct tags)

Halleck45 commented 7 months ago

@julian776 I forgot that part indeed!

So I think using the ProjectAggregated.Combined structure might be enough.

However, to avoid having really large content, for each ConcernedFiles, I think we should only keep the Stmts.Analyze, which summarizes the analysis, without having the details for all statements.

What do you think?

Enz000 commented 7 months ago

Hello 👋🏻

It's my first lines of code in Go and i'm stuck on this function : func (j *JsonReportGenerator) Generate(allResults []Result, projectAggregated AggregatedResult) allResults are type in Result, and projectAggregated in an AggregatedResult

When I checked the Generate function in other Generator (like html and markown), projectAggregated are typed in Analyzer.ProjectAggregated which from "github.com/halleck45/ast-metrics/src/Analyzer" if i understood.

But i didn’t find the AggregatedResult or Result in the Analyzer.

I got the followed error : src/Report/Json/JsonReportGenerator.go:21:53: undefined: Result same for AggregatedResult

Thanks !

Halleck45 commented 7 months ago

Hello @Enz000 ,

that's a mistake in my tutorial :/

You should use something like:

func (j *JsonReportGenerator) Generate(files []*pb.File, projectAggregated Analyzer.ProjectAggregated) error {

and import the Analyzer first:

import (
    ...
    "github.com/halleck45/ast-metrics/src/Analyzer"
    ...
)

Looks at the src/Report/Html/HtmlReportGenerator.go file for example