Skip to content

Commit

Permalink
PythonMutator: add diagnostics
Browse files Browse the repository at this point in the history
  • Loading branch information
kanterov committed Jun 26, 2024
1 parent 2ec6abf commit ca3bdfd
Show file tree
Hide file tree
Showing 4 changed files with 331 additions and 34 deletions.
129 changes: 129 additions & 0 deletions bundle/config/mutator/python/python_diagnostics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package python

import (
"bufio"
"encoding/json"
"fmt"
"io"
"regexp"
"strconv"

"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/dyn"
)

type pythonDiagnostic struct {
Severity pythonSeverity `json:"severity"`
Summary string `json:"summary"`
Detail string `json:"detail,omitempty"`
Location string `json:"location,omitempty"`
Path string `json:"path,omitempty"`
}

type pythonSeverity = string

var locationRegex = regexp.MustCompile(`^(.*):(\d+):(\d+)$`)

const (
pythonError pythonSeverity = "error"
pythonWarning pythonSeverity = "warning"
)

// parsePythonDiagnostics parses diagnostics from the Python mutator.
//
// diagnostics file is newline-separated JSON objects with pythonDiagnostic structure.
func parsePythonDiagnostics(input io.Reader) (diag.Diagnostics, error) {
// the default limit is 64 Kb which should be enough for diagnostics
scanner := bufio.NewScanner(input)
diagnostics := diag.Diagnostics{}

for scanner.Scan() {
line := scanner.Text()

if line == "" {
continue
}

parsedLine := pythonDiagnostic{}

err := json.Unmarshal([]byte(line), &parsedLine)

if err != nil {
return nil, fmt.Errorf("failed to parse diagnostics: %s", err)
}

severity, err := convertPythonSeverity(parsedLine.Severity)
if err != nil {
return nil, fmt.Errorf("failed to parse severity: %s", err)
}

location, err := convertPythonLocation(parsedLine.Location)
if err != nil {
return nil, fmt.Errorf("failed to parse location: %s", err)
}

path, err := convertPythonPath(parsedLine.Path)
if err != nil {
return nil, fmt.Errorf("failed to parse path: %s", err)
}

diagnostic := diag.Diagnostic{
Severity: severity,
Summary: parsedLine.Summary,
Detail: parsedLine.Detail,
Location: location,
Path: path,
}

diagnostics = diagnostics.Append(diagnostic)
}

return diagnostics, nil
}

func convertPythonPath(path string) (dyn.Path, error) {
if path == "" {
return nil, nil
}

return dyn.NewPathFromString(path)
}

func convertPythonSeverity(severity pythonSeverity) (diag.Severity, error) {
switch severity {
case pythonError:
return diag.Error, nil
case pythonWarning:
return diag.Warning, nil
default:
return 0, fmt.Errorf("unexpected value: %s", severity)
}
}

func convertPythonLocation(location string) (dyn.Location, error) {
if location == "" {
return dyn.Location{}, nil
}

matches := locationRegex.FindStringSubmatch(location)

if len(matches) == 4 {
line, err := strconv.Atoi(matches[2])
if err != nil {
return dyn.Location{}, fmt.Errorf("failed to parse line number: %s", location)
}

column, err := strconv.Atoi(matches[3])
if err != nil {
return dyn.Location{}, fmt.Errorf("failed to parse column number: %s", location)
}

return dyn.Location{
File: matches[1],
Line: line,
Column: column,
}, nil
}

return dyn.Location{}, fmt.Errorf("failed to parse location: %s", location)
}
104 changes: 104 additions & 0 deletions bundle/config/mutator/python/python_diagnostics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package python

import (
"bytes"
"testing"

"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/dyn"
assert "github.com/databricks/cli/libs/dyn/dynassert"
)

func TestConvertPythonLocation(t *testing.T) {
location, err := convertPythonLocation("my:file.py:1:2")

assert.NoError(t, err)
assert.Equal(t, dyn.Location{
File: "my:file.py",
Line: 1,
Column: 2,
}, location)
}

type parsePythonDiagnosticsTest struct {
name string
input string
expected diag.Diagnostics
}

func TestParsePythonDiagnostics(t *testing.T) {

testCases := []parsePythonDiagnosticsTest{
{
name: "short error with location",
input: `{"severity": "error", "summary": "error summary", "location": "src/examples/file.py:1:2"}`,
expected: diag.Diagnostics{
{
Severity: diag.Error,
Summary: "error summary",
Location: dyn.Location{
File: "src/examples/file.py",
Line: 1,
Column: 2,
},
},
},
},
{
name: "short error with path",
input: `{"severity": "error", "summary": "error summary", "path": "resources.jobs.job0.name"}`,
expected: diag.Diagnostics{
{
Severity: diag.Error,
Summary: "error summary",
Path: dyn.MustPathFromString("resources.jobs.job0.name"),
},
},
},
{
name: "empty file",
input: "",
expected: diag.Diagnostics{},
},
{
name: "newline file",
input: "\n",
expected: diag.Diagnostics{},
},
{
name: "warning with detail",
input: `{"severity": "warning", "summary": "warning summary", "detail": "warning detail"}`,
expected: diag.Diagnostics{
{
Severity: diag.Warning,
Summary: "warning summary",
Detail: "warning detail",
},
},
},
{
name: "multiple errors",
input: `{"severity": "error", "summary": "error summary (1)"}` + "\n" +
`{"severity": "error", "summary": "error summary (2)"}`,
expected: diag.Diagnostics{
{
Severity: diag.Error,
Summary: "error summary (1)",
},
{
Severity: diag.Error,
Summary: "error summary (2)",
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
diagnostics, err := parsePythonDiagnostics(bytes.NewReader([]byte(tc.input)))

assert.NoError(t, err)
assert.Equal(t, tc.expected, diagnostics)
})
}
}
Loading

0 comments on commit ca3bdfd

Please sign in to comment.