From 7dac17039bce216a864892c3685a6a0ef84f3188 Mon Sep 17 00:00:00 2001 From: JonahSussman Date: Thu, 16 Nov 2023 09:50:21 -0500 Subject: [PATCH] Implement JSON sorting --- output/v1/konveyor/violations.go | 242 +++++++++++++++++++++++++++---- 1 file changed, 210 insertions(+), 32 deletions(-) diff --git a/output/v1/konveyor/violations.go b/output/v1/konveyor/violations.go index f7b9b785..137cde54 100644 --- a/output/v1/konveyor/violations.go +++ b/output/v1/konveyor/violations.go @@ -6,6 +6,7 @@ import ( "strings" "go.lsp.dev/uri" + "gopkg.in/yaml.v2" ) const ( @@ -16,32 +17,57 @@ const ( type RuleSet struct { // Name is a name for the ruleset. Name string `yaml:"name,omitempty" json:"name,omitempty"` + // Description text description for the ruleset. Description string `yaml:"description,omitempty" json:"description,omitempty"` + // Tags list of generated tags from the rules in this ruleset. Tags []string `yaml:"tags,omitempty" json:"tags,omitempty"` + // Violations is a map containing violations generated for the // matched rules in this ruleset. Keys are rule IDs, values are // their respective generated violations. Violations map[string]Violation `yaml:"violations,omitempty" json:"violations,omitempty"` + // Errors is a map containing errors generated during evaluation // of rules in this ruleset. Keys are rule IDs, values are // their respective generated errors. Errors map[string]string `yaml:"errors,omitempty" json:"errors,omitempty"` + // Unmatched is a list of rule IDs of the rules that weren't matched. Unmatched []string `yaml:"unmatched,omitempty" json:"unmatched,omitempty"` + // Skipped is a list of rule IDs that were skipped Skipped []string `yaml:"skipped,omitempty" json:"skipped,omitempty"` } -func (r RuleSet) MarshalYAML() (interface{}, error) { +// Sorts all fields in a canonical way on a RuleSet +func (r *RuleSet) sortFields() { sort.Strings(r.Tags) sort.Strings(r.Unmatched) sort.Strings(r.Skipped) +} +func (r RuleSet) MarshalYAML() (interface{}, error) { + r.sortFields() return r, nil } +func (r RuleSet) MarshalJSON() ([]byte, error) { + b, err := yaml.Marshal(r) + if err != nil { + return b, err + } + + m := map[string]any{} + err = yaml.Unmarshal(b, &m) + if err != nil { + return nil, err + } + + return json.Marshal(m) +} + type Category string var ( @@ -49,10 +75,12 @@ var ( // This is used when you are trying to communicate that something may be wrong/incorrect // But individual situations may require more context. Potential Category = "potential" + // Optional - rule states that there is an issue, but that this issue can be fixed later. // Primary use case is when migrating frameworks, and something has a deprecated notice, // You should fix this but it wont break the migration/upgrade. Optional Category = "optional" + // Mandatory - rule states that there is an issue that must be fixed. // This is used, based on the ruleset, to tell the user that this is a real issue. // For migrations, this means it must be fixed or the upgrade with fail. @@ -83,62 +111,106 @@ type Violation struct { Effort *int `yaml:"effort,omitempty" json:"effort,omitempty"` } -func (v Violation) MarshalYAML() (interface{}, error) { +// Sorts all fields in a canonical way on a Violation +func (v *Violation) sortFields() { sort.Strings(v.Labels) - sort.SliceStable(v.Incidents, func(i, j int) bool { - if v.Incidents[i].URI != v.Incidents[j].URI { - return v.Incidents[i].URI < v.Incidents[j].URI - } - - if v.Incidents[i].Message != v.Incidents[j].Message { - return v.Incidents[i].Message < v.Incidents[j].Message - } - - if v.Incidents[i].CodeSnip != v.Incidents[j].CodeSnip { - return v.Incidents[i].CodeSnip < v.Incidents[j].CodeSnip - } - if *v.Incidents[i].LineNumber != *v.Incidents[j].LineNumber { - return *v.Incidents[i].LineNumber < *v.Incidents[j].LineNumber - } - - return false + sort.SliceStable(v.Incidents, func(i, j int) bool { + return v.Incidents[i].cmpLess(&v.Incidents[j]) }) - sort.SliceStable(v.Links, func(i, j int) bool { - if v.Links[i].URL != v.Links[j].URL { - return v.Links[i].URL < v.Links[j].URL - } - - if v.Links[i].Title != v.Links[j].Title { - return v.Links[i].Title < v.Links[j].Title - } - return false + sort.SliceStable(v.Links, func(i, j int) bool { + return v.Links[i].cmpLess(&v.Links[j]) }) +} +func (v Violation) MarshalYAML() (interface{}, error) { + v.sortFields() return v, nil } +func (v Violation) MarshalJSON() ([]byte, error) { + b, err := yaml.Marshal(v) + if err != nil { + return b, err + } + + m := map[string]any{} + err = yaml.Unmarshal(b, &m) + if err != nil { + return nil, err + } + + return json.Marshal(m) +} + // Incident defines instance of a violation type Incident struct { // URI defines location in the codebase where violation is found URI uri.URI `yaml:"uri" json:"uri"` + // Message text description about the incident Message string `yaml:"message" json:"message"` CodeSnip string `yaml:"codeSnip,omitempty" json:"codeSnip,omitempty"` + // Extras reserved for additional data - //Extras json.RawMessage + // Extras json.RawMessage LineNumber *int `yaml:"lineNumber,omitempty" json:"lineNumber,omitempty"` Variables map[string]interface{} `yaml:"variables,omitempty" json:"variables,omitempty"` } +// Lexicographically compares two Incidents +func (i *Incident) cmpLess(other *Incident) bool { + if i.URI != other.URI { + return i.URI < other.URI + } + + if i.Message != other.Message { + return i.Message < other.Message + } + + if i.CodeSnip != other.CodeSnip { + return i.CodeSnip < other.CodeSnip + } + + if i.LineNumber == nil { + x := 0 + i.LineNumber = &x + } + + if other.LineNumber == nil { + x := 0 + other.LineNumber = &x + } + + if *(*i).LineNumber != *(*other).LineNumber { + return *(*i).LineNumber < *(*other).LineNumber + } + + return false +} + // Link defines an external hyperlink type Link struct { URL string `yaml:"url" json:"url"` + // Title optional description Title string `yaml:"title,omitempty" json:"title,omitempty"` } +// Lexicographically compares two Links +func (l *Link) cmpLess(other *Link) bool { + if l.Title != other.Title { + return l.Title < other.Title + } + + if l.URL != other.URL { + return l.URL < other.URL + } + + return false +} + type Dep struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` Version string `json:"version,omitempty" yaml:"version,omitempty"` @@ -150,12 +222,12 @@ type Dep struct { FileURIPrefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` } -func (d Dep) MarshalYAML() (interface{}, error) { +// Sorts all fields in a canonical way on a Dep +func (d *Dep) sortFields() { sort.Strings(d.Labels) - - return d, nil } +// Lexicographically compares two Deps func (d Dep) cmpLess(other Dep) bool { if d.Name != other.Name { return d.Name < other.Name @@ -190,6 +262,26 @@ func (d Dep) cmpLess(other Dep) bool { return false } +func (d Dep) MarshalYAML() (interface{}, error) { + d.sortFields() + return d, nil +} + +func (d Dep) MarshalJSON() ([]byte, error) { + b, err := yaml.Marshal(d) + if err != nil { + return b, err + } + + m := map[string]any{} + err = yaml.Unmarshal(b, &m) + if err != nil { + return nil, err + } + + return json.Marshal(m) +} + func (d *Dep) GetLabels() []string { return d.Labels } @@ -199,14 +291,100 @@ type DepDAGItem struct { AddedDeps []DepDAGItem `yaml:"addedDep,omitempty" json:"addedDep,omitempty"` } +// Sorts all fields in a canonical way on a DepDAGItem +func (d *DepDAGItem) sortFields() { + for i := range d.AddedDeps { + d.AddedDeps[i].sortFields() + } + + sort.SliceStable(d.AddedDeps, func(i int, j int) bool { + return d.AddedDeps[i].Dep.cmpLess(d.AddedDeps[j].Dep) + }) +} + +func (d DepDAGItem) MarshalYAML() (interface{}, error) { + d.sortFields() + return d, nil +} + +func (d DepDAGItem) MarshalJSON() ([]byte, error) { + b, err := yaml.Marshal(d) + if err != nil { + return b, err + } + + m := map[string]any{} + err = yaml.Unmarshal(b, &m) + if err != nil { + return nil, err + } + + return json.Marshal(m) +} + type DepsFlatItem struct { FileURI string `yaml:"fileURI" json:"fileURI"` Provider string `yaml:"provider" json:"provider"` Dependencies []*Dep `yaml:"dependencies" json:"dependencies"` } +// Sorts all fields in a canonical way on a DepsFlatItem +func (d *DepsFlatItem) sortFields() { + sort.SliceStable(d.Dependencies, func(i, j int) bool { + return (*d.Dependencies[i]).cmpLess(*d.Dependencies[j]) + }) +} + +func (d DepsFlatItem) MarshalYAML() (interface{}, error) { + d.sortFields() + return d, nil +} + +func (d DepsFlatItem) MarshalJSON() ([]byte, error) { + b, err := yaml.Marshal(d) + if err != nil { + return b, err + } + + m := map[string]any{} + err = yaml.Unmarshal(b, &m) + if err != nil { + return nil, err + } + + return json.Marshal(m) +} + type DepsTreeItem struct { FileURI string `yaml:"fileURI" json:"fileURI"` Provider string `yaml:"provider" json:"provider"` Dependencies []DepDAGItem `yaml:"dependencies" json:"dependencies"` } + +// Sorts all fields in a canonical way on a DepsTreeItem +func (d *DepsTreeItem) sortFields() { + // Technically doesn't traverse the entire tree + sort.SliceStable(d.Dependencies, func(i, j int) bool { + return d.Dependencies[i].Dep.cmpLess(d.Dependencies[j].Dep) + }) +} + +func (d DepsTreeItem) MarshalYAML() (interface{}, error) { + d.sortFields() + return d, nil +} + +func (d DepsTreeItem) MarshalJSON() ([]byte, error) { + b, err := yaml.Marshal(d) + if err != nil { + return b, err + } + + m := map[string]any{} + err = yaml.Unmarshal(b, &m) + if err != nil { + return nil, err + } + + return json.Marshal(m) +}