Skip to content

Commit

Permalink
Merge pull request #157 from doramatadora/dora-json-support
Browse files Browse the repository at this point in the history
Adds JSON support for generic, terraform and lint usage
  • Loading branch information
ysugimoto authored Aug 21, 2023
2 parents e40770b + 689a467 commit f922864
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 57 deletions.
22 changes: 16 additions & 6 deletions cmd/falco/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ Flags:
-h, --help : Show this help
-r, --remote : Communicate with Fastly API
-V, --version : Display build version
-v : Verbose warning lint result
-vv : Varbose all lint result
-json : Output statistics as JSON
-v : Output lint warnings (verbose)
-vv : Output all lint results (very verbose)
-json : Output results as JSON (very verbose)
Simple Linting example:
falco -I . -vv /path/to/vcl/main.vcl
Expand Down Expand Up @@ -134,7 +134,7 @@ func main() {
var exitErr error
switch c.Commands.At(0) {
case subcommandStat:
exitErr = runStats(runner, v, c.Json)
exitErr = runStats(runner, v)
default:
exitErr = runLint(runner, v)
}
Expand All @@ -158,6 +158,16 @@ func runLint(runner *Runner, rslv resolver.Resolver) error {
return ErrExit
}

if runner.jsonMode {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(result); err != nil {
writeln(red, err.Error())
os.Exit(1)
}
return ErrExit
}

write(red, ":fire:%d errors, ", result.Errors)
write(yellow, ":exclamation:%d warnings, ", result.Warnings)
writeln(cyan, ":speaker:%d infos.", result.Infos)
Expand Down Expand Up @@ -195,7 +205,7 @@ func runLint(runner *Runner, rslv resolver.Resolver) error {
return nil
}

func runStats(runner *Runner, rslv resolver.Resolver, printJson bool) error {
func runStats(runner *Runner, rslv resolver.Resolver) error {
stats, err := runner.Stats(rslv)
if err != nil {
if err != ErrParser {
Expand All @@ -204,7 +214,7 @@ func runStats(runner *Runner, rslv resolver.Resolver, printJson bool) error {
return ErrExit
}

if printJson {
if runner.jsonMode {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(stats); err != nil {
Expand Down
63 changes: 50 additions & 13 deletions cmd/falco/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ type RunnerResult struct {
Infos int
Warnings int
Errors int
Vcl *plugin.VCL

LintErrors map[string][]*linter.LintError
ParseErrors map[string]*parser.ParseError

Vcl *plugin.VCL
}

type StatsResult struct {
Expand Down Expand Up @@ -69,12 +73,16 @@ type Runner struct {
lexers map[string]*lexer.Lexer
snippets *context.FastlySnippet

level Level
level Level
lintErrors map[string][]*linter.LintError
parseErrors map[string]*parser.ParseError

// runner result fields
infos int
warnings int
errors int

jsonMode bool
}

func NewRunner(c *config.Config, f Fetcher) (*Runner, error) {
Expand All @@ -84,8 +92,16 @@ func NewRunner(c *config.Config, f Fetcher) (*Runner, error) {
lexers: make(map[string]*lexer.Lexer),
}

if c.Json {
r.jsonMode = true
r.lintErrors = make(map[string][]*linter.LintError)
r.parseErrors = make(map[string]*parser.ParseError)
}

if c.Remote {
writeln(cyan, "Remote option supplied. Fetch snippets from Fastly.")
if !r.jsonMode {
writeln(cyan, "Remote option supplied. Fetch snippets from Fastly.")
}
// If remote flag is provided, fetch predefined data from Fastly.
// Currently, we only support Edge Dictionary.
//
Expand All @@ -100,7 +116,7 @@ func NewRunner(c *config.Config, f Fetcher) (*Runner, error) {
// We allow each API call to take up to to 5 seconds
f := remote.NewFastlyApiFetcher(c.FastlyServiceID, c.FastlyApiKey, 5*time.Second)
snippets, err := NewSnippet(f).Fetch()
if err != nil {
if err != nil && !r.jsonMode {
writeln(red, err.Error())
}
// Stack to runner field, combime before run()
Expand All @@ -110,7 +126,7 @@ func NewRunner(c *config.Config, f Fetcher) (*Runner, error) {

if f != nil {
snippets, err := NewSnippet(f).Fetch()
if err != nil {
if err != nil && !r.jsonMode {
writeln(red, err.Error())
}
r.snippets = snippets
Expand Down Expand Up @@ -146,7 +162,9 @@ func NewRunner(c *config.Config, f Fetcher) (*Runner, error) {
case "IGNORE":
r.overrides[key] = linter.IGNORE
default:
writeln(yellow, "level for rule %s has invalid value %s, skip it.", key, value)
if !r.jsonMode {
writeln(yellow, "level for rule %s has invalid value %s, skip it.", key, value)
}
}
}

Expand Down Expand Up @@ -183,15 +201,17 @@ func (r *Runner) Run(rslv resolver.Resolver) (*RunnerResult, error) {
// Note: this context is not Go context, our parsing context :)
ctx := context.New(options...)
vcl, err := r.run(ctx, main, RunModeLint)
if err != nil {
if err != nil && !r.jsonMode {
return nil, err
}

return &RunnerResult{
Infos: r.infos,
Warnings: r.warnings,
Errors: r.errors,
Vcl: vcl,
Infos: r.infos,
Warnings: r.warnings,
Errors: r.errors,
LintErrors: r.lintErrors,
ParseErrors: r.parseErrors,
Vcl: vcl,
}, nil
}

Expand Down Expand Up @@ -269,7 +289,16 @@ func (r *Runner) printParseError(lx *lexer.Lexer, err *parser.ParseError) {
var file string
if err.Token.File != "" {
file = "in " + err.Token.File + " "
if r.jsonMode {
r.parseErrors[err.Token.File] = err
}
}

// Nothing to print to stdout if JSON mode is enabled, exit early.
if r.jsonMode {
return
}

writeln(red, ":boom: %s\n%sat line %d, position %d", err.Message, file, err.Token.Line, err.Token.Position)

problemLine := err.Token.Line
Expand Down Expand Up @@ -312,19 +341,27 @@ func (r *Runner) printLinterError(lx *lexer.Lexer, err *linter.LintError) {
severity = v
}

// Store all but ignored linter errors
if r.jsonMode && severity != linter.IGNORE {
r.lintErrors[err.Token.File] = append(r.lintErrors[err.Token.File], err)
}

switch severity {
case linter.ERROR:
r.errors++
if r.jsonMode {
return
}
writeln(red, ":fire:[ERROR] %s%s", err.Message, rule)
case linter.WARNING:
r.warnings++
if r.level < LevelWarning {
if r.jsonMode || r.level < LevelWarning {
return
}
writeln(yellow, ":exclamation:[WARNING] %s%s", err.Message, rule)
case linter.INFO:
r.infos++
if r.level < LevelInfo {
if r.jsonMode || r.level < LevelInfo {
return
}
writeln(cyan, ":speaker:[INFO] %s%s", err.Message, rule)
Expand Down
145 changes: 107 additions & 38 deletions cmd/falco/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,52 @@ import (
"testing"

"github.com/ysugimoto/falco/config"
"github.com/ysugimoto/falco/linter"
"github.com/ysugimoto/falco/resolver"
"github.com/ysugimoto/falco/terraform"
)

type RepoExampleTestMetadata struct {
name string
fileName string
errors int
warnings int
infos int
}

func loadRepoExampleTestMetadata() []RepoExampleTestMetadata {
return []RepoExampleTestMetadata{
{
name: "example 1",
fileName: "../../examples/default01.vcl",
errors: 0,
warnings: 0,
infos: 0,
},
{
name: "example 2",
fileName: "../../examples/default02.vcl",
errors: 1,
warnings: 0,
infos: 0,
},
{
name: "example 3",
fileName: "../../examples/default03.vcl",
errors: 0,
warnings: 0,
infos: 1,
},
{
name: "example 4",
fileName: "../../examples/default04.vcl",
errors: 0,
warnings: 0,
infos: 1,
},
}
}

func loadFromTfJson(fileName string, t *testing.T) ([]resolver.Resolver, Fetcher) {
buf, err := os.ReadFile(fileName)
if err != nil {
Expand Down Expand Up @@ -127,46 +169,12 @@ func TestResolveModulesWithoutVCLExtension(t *testing.T) {
}
}

// Adds a test for all the example code in the repo to make sure we don't accidentally
// Tests for all the example code in the repo to make sure we don't accidentally
// break those as they are the first thing someone might try on the repo.
func TestRepositoryExamples(t *testing.T) {
tests := []struct {
name string
fileName string
errors int
warnings int
infos int
}{
{
name: "example 1",
fileName: "../../examples/default01.vcl",
errors: 0,
warnings: 0,
infos: 0,
},
{
name: "example 2",
fileName: "../../examples/default02.vcl",
errors: 1,
warnings: 0,
infos: 0,
},
{
name: "example 3",
fileName: "../../examples/default03.vcl",
errors: 0,
warnings: 0,
infos: 1,
},
{
name: "example 4",
fileName: "../../examples/default04.vcl",
errors: 0,
warnings: 0,
infos: 1,
},
}

// Test cases for when the command runs in stdout mode (no -json flag set)
func TestRepositoryExamples(t *testing.T) {
tests := loadRepoExampleTestMetadata()
c := &config.Config{VerboseWarning: true}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -200,3 +208,64 @@ func TestRepositoryExamples(t *testing.T) {
})
}
}

// Test cases for JSON mode (-json flag set)
func TestRepositoryExamplesJSONMode(t *testing.T) {
tests := loadRepoExampleTestMetadata()
c := &config.Config{VerboseWarning: true, Json: true}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resolvers, err := resolver.NewFileResolvers(tt.fileName, c.IncludePaths)
if err != nil {
t.Errorf("Unexpected runner creation error: %s", err)
return
}
r, err := NewRunner(c, nil)
if err != nil {
t.Errorf("Unexpected runner creation error: %s", err)
return
}
ret, err := r.Run(resolvers[0])
if tt.errors != 0 {
if err != nil {
t.Errorf("Unexpected error running Run(): %s", err)
}
return
}
if ret.Infos != tt.infos {
t.Errorf("Infos expects %d, got %d", tt.infos, ret.Infos)
}
if ret.Warnings != tt.warnings {
t.Errorf("Warning expects %d, got %d", tt.warnings, ret.Warnings)
}
if ret.Errors != tt.errors {
t.Errorf("Errors expects %d, got %d", tt.errors, ret.Errors)
}
if len(ret.LintErrors) != tt.infos+tt.warnings+tt.errors {
t.Errorf("Expected %d linting errors, got %d", tt.infos+tt.warnings+tt.errors, len(ret.LintErrors))
}

countLintErrorsWithSeverity := func(sev linter.Severity) int {
var counter int = 0
for result := range ret.LintErrors {
for _, issue := range ret.LintErrors[result] {
if issue.Severity == sev {
counter++
}
}
}
return counter
}
testIssuesWithSeverity := func(sev linter.Severity, expected int) {
var issueCount = countLintErrorsWithSeverity(sev)
if issueCount != expected {
t.Errorf("Expected %d linting errors with severity %s, got %d", expected, sev, issueCount)
}
}
testIssuesWithSeverity("Info", tt.infos)
testIssuesWithSeverity("Warning", tt.warnings)
testIssuesWithSeverity("Error", tt.errors)
})
}
}

0 comments on commit f922864

Please sign in to comment.