Skip to content
Open
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
57 changes: 38 additions & 19 deletions command/issues/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type IssuesListOptions struct {
FileArg []string
RepoArg string
AnalyzerArg []string
SeverityArg []string
LimitArg int
OutputFilenameArg string
JSONArg bool
Expand All @@ -39,34 +40,36 @@ func NewCmdIssuesList() *cobra.Command {
RepoArg: "",
LimitArg: 30,
}

doc := heredoc.Docf(`
List issues reported by DeepSource.
List issues reported by DeepSource.

To list issues for the current repository:
%[1]s

To list issues for the current repository:
%[1]s
To list issues for a specific repository, use the %[2]s flag:
%[3]s

To list issues for a specific repository, use the %[2]s flag:
%[3]s
To list issues for a specific analyzer, use the %[4]s flag:
%[5]s

To list issues for a specific analyzer, use the %[4]s flag:
%[5]s
To limit the number of issues reported, use the %[6]s flag:
%[7]s

To limit the number of issues reported, use the %[6]s flag:
%[7]s
To export listed issues to a file, use the %[8]s flag:
%[9]s

To export listed issues to a file, use the %[8]s flag:
%[9]s
To export listed issues to a JSON file, use the %[10]s flag:
%[11]s

To export listed issues to a JSON file, use the %[10]s flag:
%[11]s
To export listed issues to a CSV file, use the %[12]s flag:
%[13]s

To export listed issues to a CSV file, use the %[12]s flag:
%[13]s
To export listed issues to a SARIF file, use the %[14]s flag:
%[15]s

To export listed issues to a SARIF file, use the %[14]s flag:
%[15]s
`, utils.Cyan("deepsource issues list"), utils.Yellow("--repo"), utils.Cyan("deepsource issues list --repo repo_name"), utils.Yellow("--analyzer"), utils.Cyan("deepsource issues list --analyzer python"), utils.Yellow("--limit"), utils.Cyan("deepsource issues list --limit 100"), utils.Yellow("--output-file"), utils.Cyan("deepsource issues list --output-file file_name"), utils.Yellow("--json"), utils.Cyan("deepsource issues list --json --output-file example.json"), utils.Yellow("--csv"), utils.Cyan("deepsource issues list --csv --output-file example.csv"), utils.Yellow("--sarif"), utils.Cyan("deepsource issues list --sarif --output-file example.sarif"))
To list issues for specific severities, use the %[16]s flag:
%[17]s
`, utils.Cyan("deepsource issues list"), utils.Yellow("--repo"), utils.Cyan("deepsource issues list --repo repo_name"), utils.Yellow("--analyzer"), utils.Cyan("deepsource issues list --analyzer python"), utils.Yellow("--limit"), utils.Cyan("deepsource issues list --limit 100"), utils.Yellow("--output-file"), utils.Cyan("deepsource issues list --output-file file_name"), utils.Yellow("--json"), utils.Cyan("deepsource issues list --json --output-file example.json"), utils.Yellow("--csv"), utils.Cyan("deepsource issues list --csv --output-file example.csv"), utils.Yellow("--sarif"), utils.Cyan("deepsource issues list --sarif --output-file example.sarif"), utils.Yellow("--severity"), utils.Cyan("deepsource issues list --severity critical --severity major"))

cmd := &cobra.Command{
Use: "list",
Expand All @@ -81,6 +84,9 @@ func NewCmdIssuesList() *cobra.Command {
// --repo, -r flag
cmd.Flags().StringVarP(&opts.RepoArg, "repo", "r", "", "List the issues of the specified repository")

// --severity -s flag
cmd.Flags().StringArrayVarP(&opts.SeverityArg, "severity", "s", nil, "List issues for specified severity (CRITICAL, MAJOR, MINOR)")

// --analyzer, -a flag
cmd.Flags().StringArrayVarP(&opts.AnalyzerArg, "analyzer", "a", nil, "List the issues for the specified analyzer")

Expand Down Expand Up @@ -198,6 +204,19 @@ func (opts *IssuesListOptions) getIssuesData(ctx context.Context) (err error) {
opts.issuesData = getUniqueIssues(fetchedIssues)
}

if len(opts.SeverityArg) != 0 {
var fetchedIssues []issues.Issue
//Filter issues based on the severity option specified
filteredIssues, err = filterIssuesBySeverity(opts.SeverityArg, opts.issuesData)
if err != nil {
return err
}
fetchedIssues = append(fetchedIssues, filteredIssues...)
// set fetched issues as issue data
opts.issuesData = getUniqueIssues(fetchedIssues)

}

return nil
}

Expand Down
72 changes: 72 additions & 0 deletions command/issues/list/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,75 @@ func TestFilterIssuesByAnalyzer(t *testing.T) {
}
})
}

func TestFilterIssuesBySeverity(t *testing.T) {
// Path to the dedicated severity test data
testDataPath := "./testdata/dummy/issues_severity.json"

// Case 1: Filter by a single severity
t.Run("must work with a single severity", func(t *testing.T) {
issues_data := ReadIssues(testDataPath)
// Testing lowercase "critical" to verify the ToUpper normalization logic
got, err := filterIssuesBySeverity([]string{"critical"}, issues_data)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

// Expecting only the one CRITICAL issue defined in our JSON
if len(got) != 1 || strings.ToUpper(got[0].IssueSeverity) != "CRITICAL" {
t.Errorf("got: %v; expected 1 CRITICAL issue", got)
}
})

// Case 2: Filter by multiple severities simultaneously (Logical OR)
t.Run("must work with multiple severities", func(t *testing.T) {
issues_data := ReadIssues(testDataPath)

// Should return both MAJOR and MINOR issues
got, err := filterIssuesBySeverity([]string{"MAJOR", "MINOR"}, issues_data)

if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if len(got) != 2 {
t.Errorf("got: %d issues; want 2", len(got))
}
})

// Case 3: Handle invalid severity strings
t.Run("must return error for invalid severity input", func(t *testing.T) {
issues_data := ReadIssues(testDataPath)

// Verifying that the validator catches illegal entries
_, err := filterIssuesBySeverity([]string{"invalid_level"}, issues_data)
if err == nil {
t.Error("expected error for invalid severity 'invalid_level', got nil")
}
})

// Case 4: Handle valid severity that has no matches in the data
t.Run("must return empty list when no matches exist", func(t *testing.T) {
// Create a subset with only MINOR issues
subset := []issues.Issue{{IssueSeverity: "MINOR"}}

// Filtering for CRITICAL should yield 0 results but NO error
got, err := filterIssuesBySeverity([]string{"CRITICAL"}, subset)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if len(got) != 0 {
t.Errorf("expected 0 issues, got %d", len(got))
}
})

t.Run("should handle duplicate severity flags gracefully", func(t *testing.T) {
issues_data := ReadIssues(testDataPath)

got, _ := filterIssuesBySeverity([]string{"critical", "critical"}, issues_data)
if len(got) != 1 {
t.Errorf("expected 1 issue despite duplicate flag, got %d", len(got))
}
})
}
23 changes: 23 additions & 0 deletions command/issues/list/testdata/dummy/issues_severity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"issue_title": "Critical Security Bug",
"issue_code": "SEC-001",
"issue_severity": "CRITICAL",
"location": { "path": "main.go", "position": { "begin": 10, "end": 10 } },
"Analyzer": { "analyzer": "go" }
},
{
"issue_title": "Major Performance Issue",
"issue_code": "PERF-002",
"issue_severity": "MAJOR",
"location": { "path": "utils.go", "position": { "begin": 20, "end": 20 } },
"Analyzer": { "analyzer": "go" }
},
{
"issue_title": "Minor Style Nitpick",
"issue_code": "STYLE-003",
"issue_severity": "MINOR",
"location": { "path": "list.go", "position": { "begin": 30, "end": 30 } },
"Analyzer": { "analyzer": "go" }
}
]
34 changes: 34 additions & 0 deletions command/issues/list/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func filterIssuesByPath(path string, issuesData []issues.Issue) ([]issues.Issue,
// get relative path
rel, err := filepath.Rel(path, issue.Location.Path)
if err != nil {

return nil, err
}

Expand All @@ -50,6 +51,39 @@ func filterIssuesByPath(path string, issuesData []issues.Issue) ([]issues.Issue,
return getUniqueIssues(filteredIssues), nil
}

//Filters issues based on the severity of issue specified

func filterIssuesBySeverity(severity []string, issuesData []issues.Issue) ([]issues.Issue, error) {
var filteredIssues []issues.Issue

// valid options for severity
validSeverities := map[string]bool{
"CRITICAL": true,
"MAJOR": true,
"MINOR": true,
}

// Validate user input and normalize to uppercase
severityMap := make(map[string]bool)
for _, s := range severity {
upperS := strings.ToUpper(s)
if !validSeverities[upperS] {
return nil, fmt.Errorf("invalid severity level: %s (valid options: CRITICAL, MAJOR, MINOR)", s)
}
severityMap[upperS] = true
}

// Filter the issues list
for _, issue := range issuesData {
//match against the IssueSeverity field from the SDK Issue Struct
if severityMap[strings.ToUpper(issue.IssueSeverity)] {
filteredIssues = append(filteredIssues, issue)
}
}

return getUniqueIssues(filteredIssues), nil
}

// Filters issues based on the analyzer shortcode.
func filterIssuesByAnalyzer(analyzer []string, issuesData []issues.Issue) ([]issues.Issue, error) {
var filteredIssues []issues.Issue
Expand Down
Loading