Skip to content
Closed
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
63 changes: 59 additions & 4 deletions internal/gitlab/codequality.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package gitlab
import (
"crypto/sha256"
"fmt"
"strings"

"github.com/google/yamlfmt"
)
Expand All @@ -35,7 +36,14 @@ type CodeQuality struct {

// Location is the location of a Code Quality finding.
type Location struct {
Path string `json:"path,omitempty"`
Path string `json:"path,omitempty"`
Lines *Lines `json:"lines,omitempty"`
}

// Lines follows the GitLab Code Quality schema.
type Lines struct {
Begin *int `json:"begin,omitempty"`
Copy link
Collaborator

Choose a reason for hiding this comment

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

My previous comment was for the End field. #294 (comment)

I didn't want it to be removed; I wanted End to change to *int and get omitempty. Begin was supposed to stay the way it was.

End *int `json:"end,omitempty"`
}

// NewCodeQuality creates a new CodeQuality object from a yamlfmt.FileDiff.
Expand All @@ -46,24 +54,71 @@ func NewCodeQuality(diff yamlfmt.FileDiff) (CodeQuality, bool) {
return CodeQuality{}, false
}

begin, end := detectChangedLines(&diff)

return CodeQuality{
Description: "Not formatted correctly, run yamlfmt to resolve.",
Name: "yamlfmt",
Fingerprint: fingerprint(diff),
Severity: Major,
Location: Location{
Path: diff.Path,
Lines: &Lines{
Begin: &begin,
End: &end,
},
},
}, true
}

// detectChangedLines finds the first and last lines that differ between original and formatted content.
func detectChangedLines(diff *yamlfmt.FileDiff) (begin int, end int) {
original := strings.Split(diff.Diff.Original, "\n")
formatted := strings.Split(diff.Diff.Formatted, "\n")

max := len(original)
if len(formatted) > max {
max = len(formatted)
}

begin = -1
end = -1

for i := 0; i < max; i++ {
origLine := ""
fmtLine := ""

if i < len(original) {
origLine = original[i]
}
if i < len(formatted) {
fmtLine = formatted[i]
}

if origLine != fmtLine {
if begin == -1 {
begin = i + 1
}
end = i + 1
}
}

// fallback (should not happen because diff.Changed() was true)
if begin == -1 {
begin = 1
}
if end == -1 {
end = 1
}

return begin, end
}

// fingerprint returns a 256-bit SHA256 hash of the original unformatted file.
// This is used to uniquely identify a code quality finding.
func fingerprint(diff yamlfmt.FileDiff) string {
hash := sha256.New()

fmt.Fprint(hash, diff.Diff.GetOriginal())

fmt.Fprint(hash, diff.Diff.Original)
return fmt.Sprintf("%x", hash.Sum(nil)) //nolint:perfsprint
}

Expand Down
193 changes: 193 additions & 0 deletions internal/gitlab/codequality_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package gitlab_test

import (
"encoding/json"
"os"
"path/filepath"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -93,3 +95,194 @@ func TestCodeQuality(t *testing.T) {
})
}
}

func TestCodeQuality_DetectChangedLines_MultipleCases(t *testing.T) {
t.Parallel()

cases := []struct {
name string
original string
formatted string
wantBegin int
wantEnd int
}{
{
name: "single line change",
original: "a: b",
formatted: "a: b",
wantBegin: 1,
wantEnd: 1,
},
{
name: "multiple consecutive lines",
original: `line1
line2: value
line3: value
line4`,
formatted: `line1
line2: value
line3: value
line4`,
wantBegin: 2,
wantEnd: 3,
},
{
name: "non-consecutive changes",
original: `line1
line2: value
line3
line4: value
line5`,
formatted: `line1
line2: value
line3
line4: value
line5`,
wantBegin: 2,
wantEnd: 4,
},
{
name: "change at beginning",
original: `key: value
line2
line3`,
formatted: `key: value
line2
line3`,
wantBegin: 1,
wantEnd: 1,
},
{
name: "change at end",
original: `line1
line2
key: value`,
formatted: `line1
line2
key: value`,
wantBegin: 3,
wantEnd: 3,
},
}

for _, tc := range cases {
tc := tc

t.Run(tc.name, func(t *testing.T) {
t.Parallel()

diff := yamlfmt.FileDiff{
Path: "test.yaml",
Diff: &yamlfmt.FormatDiff{
Original: tc.original,
Formatted: tc.formatted,
},
}

cq, ok := gitlab.NewCodeQuality(diff)
if !ok {
t.Fatal("NewCodeQuality() returned false, expected true")
}

if cq.Location.Lines == nil {
t.Fatal("Location.Lines is nil")
}

if cq.Location.Lines.Begin == nil {
t.Fatal("Location.Lines.Begin is nil")
}

if cq.Location.Lines.End == nil {
t.Fatal("Location.Lines.End is nil")
}

gotBegin := *cq.Location.Lines.Begin
if gotBegin != tc.wantBegin {
t.Errorf("Location.Lines.Begin = %d, want %d", gotBegin, tc.wantBegin)
}

gotEnd := *cq.Location.Lines.End
if gotEnd != tc.wantEnd {
t.Errorf("Location.Lines.End = %d, want %d", gotEnd, tc.wantEnd)
}
})
}
}

func TestCodeQuality_DetectChangedLines(t *testing.T) {
t.Parallel()

testdataDir := "testdata/gitlab/changed_line"
print(testdataDir)
originalPath := filepath.Join(testdataDir, "original.yaml")
formattedPath := filepath.Join(testdataDir, "formatted.yaml")

original, err := os.ReadFile(originalPath)
if err != nil {
t.Fatalf("failed to read original file: %v", err)
}

formatted, err := os.ReadFile(formattedPath)
if err != nil {
t.Fatalf("failed to read formatted file: %v", err)
}

diff := yamlfmt.FileDiff{
Path: "testdata/original.yaml",
Diff: &yamlfmt.FormatDiff{
Original: string(original),
Formatted: string(formatted),
},
}

cq, ok := gitlab.NewCodeQuality(diff)
if !ok {
t.Fatal("NewCodeQuality() returned false, expected true")
}

if cq.Location.Lines == nil {
t.Fatal("Location.Lines is nil")
}

if cq.Location.Lines.Begin == nil {
t.Fatal("Location.Lines.Begin is nil")
}

if cq.Location.Lines.End == nil {
t.Fatal("Location.Lines.End is nil")
}

wantBeginLine := 6
gotBeginLine := *cq.Location.Lines.Begin

if gotBeginLine != wantBeginLine {
t.Errorf("Location.Lines.Begin = %d, want %d", gotBeginLine, wantBeginLine)
}

wantEndLine := 7
gotEndLine := *cq.Location.Lines.End

if gotEndLine != wantEndLine {
t.Errorf("Location.Lines.End = %d, want %d", gotEndLine, wantEndLine)
}

if cq.Location.Path != diff.Path {
t.Errorf("Location.Path = %q, want %q", cq.Location.Path, diff.Path)
}

if cq.Description == "" {
t.Error("Description is empty")
}

if cq.Name == "" {
t.Error("Name is empty")
}

if cq.Fingerprint == "" {
t.Error("Fingerprint is empty")
}

if cq.Severity == "" {
t.Error("Severity is empty")
}
}
18 changes: 18 additions & 0 deletions testdata/gitlab/changed_line/formatted.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Example configuration file
version: 1.0

services:
web:
image: nginx:latest
ports:
- 80:80
- 443:443
environment:
- ENV=production
- DEBUG=false

database:
image: postgres:14
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=admin
18 changes: 18 additions & 0 deletions testdata/gitlab/changed_line/original.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Example configuration file
version: 1.0

services:
web:
image: nginx:latest
ports:
- 80:80
- 443:443
environment:
- ENV=production
- DEBUG=false

database:
image: postgres:14
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=admin