Skip to content

Commit 717ebe6

Browse files
committed
feat(issues): add severity filtering to list command
This adds the --severity (-s) flag to allow users to filter issues by CRITICAL, MAJOR, or MINOR. Includes input validation and dedicated test data.
1 parent e96b14d commit 717ebe6

4 files changed

Lines changed: 115 additions & 7 deletions

File tree

command/issues/list/list.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ func NewCmdIssuesList() *cobra.Command {
4040
RepoArg: "",
4141
LimitArg: 30,
4242
}
43-
// TODO:: add severity feature specifics here
4443
doc := heredoc.Docf(`
4544
List issues reported by DeepSource.
4645
@@ -204,19 +203,20 @@ func (opts *IssuesListOptions) getIssuesData(ctx context.Context) (err error) {
204203
// set fetched issues as issue data
205204
opts.issuesData = getUniqueIssues(fetchedIssues)
206205
}
206+
207207
if len(opts.SeverityArg) != 0 {
208-
//now we have some flag in the severity arg section use this and filter only those issues
209208
var fetchedIssues []issues.Issue
210-
209+
//Filter issues based on the severity option specified
211210
filteredIssues, err = filterIssuesBySeverity(opts.SeverityArg, opts.issuesData)
212211
if err != nil {
213212
return err
214213
}
215214
fetchedIssues = append(fetchedIssues, filteredIssues...)
216-
215+
// set fetched issues as issue data
217216
opts.issuesData = getUniqueIssues(fetchedIssues)
218217

219218
}
219+
220220
return nil
221221
}
222222

command/issues/list/list_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,75 @@ func TestFilterIssuesByAnalyzer(t *testing.T) {
157157
}
158158
})
159159
}
160+
161+
func TestFilterIssuesBySeverity(t *testing.T) {
162+
// Path to the dedicated severity test data
163+
testDataPath := "./testdata/dummy/issues_severity.json"
164+
165+
// Case 1: Filter by a single severity
166+
t.Run("must work with a single severity", func(t *testing.T) {
167+
issues_data := ReadIssues(testDataPath)
168+
// Testing lowercase "critical" to verify the ToUpper normalization logic
169+
got, err := filterIssuesBySeverity([]string{"critical"}, issues_data)
170+
if err != nil {
171+
t.Fatalf("unexpected error: %v", err)
172+
}
173+
174+
// Expecting only the one CRITICAL issue defined in our JSON
175+
if len(got) != 1 || strings.ToUpper(got[0].IssueSeverity) != "CRITICAL" {
176+
t.Errorf("got: %v; expected 1 CRITICAL issue", got)
177+
}
178+
})
179+
180+
// Case 2: Filter by multiple severities simultaneously (Logical OR)
181+
t.Run("must work with multiple severities", func(t *testing.T) {
182+
issues_data := ReadIssues(testDataPath)
183+
184+
// Should return both MAJOR and MINOR issues
185+
got, err := filterIssuesBySeverity([]string{"MAJOR", "MINOR"}, issues_data)
186+
187+
if err != nil {
188+
t.Fatalf("unexpected error: %v", err)
189+
}
190+
191+
if len(got) != 2 {
192+
t.Errorf("got: %d issues; want 2", len(got))
193+
}
194+
})
195+
196+
// Case 3: Handle invalid severity strings
197+
t.Run("must return error for invalid severity input", func(t *testing.T) {
198+
issues_data := ReadIssues(testDataPath)
199+
200+
// Verifying that the validator catches illegal entries
201+
_, err := filterIssuesBySeverity([]string{"invalid_level"}, issues_data)
202+
if err == nil {
203+
t.Error("expected error for invalid severity 'invalid_level', got nil")
204+
}
205+
})
206+
207+
// Case 4: Handle valid severity that has no matches in the data
208+
t.Run("must return empty list when no matches exist", func(t *testing.T) {
209+
// Create a subset with only MINOR issues
210+
subset := []issues.Issue{{IssueSeverity: "MINOR"}}
211+
212+
// Filtering for CRITICAL should yield 0 results but NO error
213+
got, err := filterIssuesBySeverity([]string{"CRITICAL"}, subset)
214+
if err != nil {
215+
t.Fatalf("unexpected error: %v", err)
216+
}
217+
218+
if len(got) != 0 {
219+
t.Errorf("expected 0 issues, got %d", len(got))
220+
}
221+
})
222+
223+
t.Run("should handle duplicate severity flags gracefully", func(t *testing.T) {
224+
issues_data := ReadIssues(testDataPath)
225+
226+
got, _ := filterIssuesBySeverity([]string{"critical", "critical"}, issues_data)
227+
if len(got) != 1 {
228+
t.Errorf("expected 1 issue despite duplicate flag, got %d", len(got))
229+
}
230+
})
231+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[
2+
{
3+
"issue_title": "Critical Security Bug",
4+
"issue_code": "SEC-001",
5+
"issue_severity": "CRITICAL",
6+
"location": { "path": "main.go", "position": { "begin": 10, "end": 10 } },
7+
"Analyzer": { "analyzer": "go" }
8+
},
9+
{
10+
"issue_title": "Major Performance Issue",
11+
"issue_code": "PERF-002",
12+
"issue_severity": "MAJOR",
13+
"location": { "path": "utils.go", "position": { "begin": 20, "end": 20 } },
14+
"Analyzer": { "analyzer": "go" }
15+
},
16+
{
17+
"issue_title": "Minor Style Nitpick",
18+
"issue_code": "STYLE-003",
19+
"issue_severity": "MINOR",
20+
"location": { "path": "list.go", "position": { "begin": 30, "end": 30 } },
21+
"Analyzer": { "analyzer": "go" }
22+
}
23+
]

command/issues/list/utils.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func filterIssuesByPath(path string, issuesData []issues.Issue) ([]issues.Issue,
3333
// get relative path
3434
rel, err := filepath.Rel(path, issue.Location.Path)
3535
if err != nil {
36+
3637
return nil, err
3738
}
3839

@@ -55,14 +56,26 @@ func filterIssuesByPath(path string, issuesData []issues.Issue) ([]issues.Issue,
5556
func filterIssuesBySeverity(severity []string, issuesData []issues.Issue) ([]issues.Issue, error) {
5657
var filteredIssues []issues.Issue
5758

58-
// Create a map for quick lookup of severities
59+
// valid options for severity
60+
validSeverities := map[string]bool{
61+
"CRITICAL": true,
62+
"MAJOR": true,
63+
"MINOR": true,
64+
}
65+
66+
// Validate user input and normalize to uppercase
5967
severityMap := make(map[string]bool)
6068
for _, s := range severity {
61-
severityMap[strings.ToUpper(s)] = true // Ensure case-insensitivity
69+
upperS := strings.ToUpper(s)
70+
if !validSeverities[upperS] {
71+
return nil, fmt.Errorf("invalid severity level: %s (valid options: CRITICAL, MAJOR, MINOR)", s)
72+
}
73+
severityMap[upperS] = true
6274
}
6375

76+
// Filter the issues list
6477
for _, issue := range issuesData {
65-
// Match against the IssueSeverity field from the SDK struct
78+
//match against the IssueSeverity field from the SDK Issue Struct
6679
if severityMap[strings.ToUpper(issue.IssueSeverity)] {
6780
filteredIssues = append(filteredIssues, issue)
6881
}

0 commit comments

Comments
 (0)