Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ func loadsToolAndPatterns(toolName string, onlyEnabledPatterns bool) (domain.Too
}
}
var patterns []domain.PatternConfiguration
patterns, err = codacyclient.GetDefaultToolPatternsConfig(domain.InitFlags{}, tool.Uuid, onlyEnabledPatterns)
patterns, err = codacyclient.GetToolPatternsConfig(domain.InitFlags{}, tool.Uuid, onlyEnabledPatterns)
if err != nil {
fmt.Println("Error:", err)
return domain.Tool{}, []domain.PatternConfiguration{}
Expand Down
2 changes: 1 addition & 1 deletion cmd/configsetup/default_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func createDefaultConfigurationsForSpecificTools(discoveredToolNames map[string]
// createToolConfigurationsForUUIDs creates tool configurations for specific UUIDs
func createToolConfigurationsForUUIDs(uuids []string, toolsConfigDir string, initFlags domain.InitFlags) error {
for _, uuid := range uuids {
patternsConfig, err := codacyclient.GetDefaultToolPatternsConfig(initFlags, uuid, true)
patternsConfig, err := codacyclient.GetToolPatternsConfig(initFlags, uuid, true)
if err != nil {
logToolConfigWarning(uuid, "Failed to get default patterns", err)
continue
Expand Down
2 changes: 1 addition & 1 deletion cmd/configsetup/repository_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func CreateToolConfigurationFile(toolName string, flags domain.InitFlags) error
return fmt.Errorf("tool '%s' not found in supported tools", toolName)
}

patternsConfig, err := codacyclient.GetDefaultToolPatternsConfig(flags, toolUUID, true)
patternsConfig, err := codacyclient.GetToolPatternsConfig(flags, toolUUID, true)
if err != nil {
return fmt.Errorf("failed to get default patterns: %w", err)
}
Expand Down
65 changes: 61 additions & 4 deletions cmd/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"

Expand Down Expand Up @@ -43,6 +45,47 @@ var uploadResultsCmd = &cobra.Command{
},
}

var sarifShortNameMap = map[string]string{
// The keys here MUST match the exact string found in run.Tool.Driver.Name
"ESLint (deprecated)": "eslint",
"ESLint": "eslint-8",
"ESLint9": "eslint-9",
"PMD": "pmd",
"PMD7": "pmd-7",
"Trivy": "trivy",
"Pylint": "pylintpython3",
"dartanalyzer": "dartanalyzer",
"Semgrep": "semgrep",
"Lizard": "lizard",
"revive": "revive",
}

func getToolShortName(fullName string) string {
if shortName, ok := sarifShortNameMap[fullName]; ok {
return shortName
}
// Fallback: Use the original name if no mapping is found
return fullName
}

func getRelativePath(baseDir string, fullURI string) string {

localPath := fullURI
u, err := url.Parse(fullURI)
if err == nil && u.Scheme == "file" {
// url.Path extracts the local path component correctly
localPath = u.Path
}
relativePath, err := filepath.Rel(baseDir, localPath)
if err != nil {
// Fallback to the normalized absolute path if calculation fails
fmt.Printf("Warning: Could not get relative path for '%s' relative to '%s': %v. Using absolute path.\n", localPath, baseDir, err)
return localPath
}

return relativePath
}
Comment on lines +71 to +87
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

❗ Issue

The new getRelativePath parses file:// URIs and returns a relative path using filepath.Rel. Issues to address:

  1. URL-encoded paths (e.g., spaces) should be unescaped after parsing. url.Path may contain percent-encoding.
  2. On Windows, file URIs like file:///C:/path produce /C:/...; test expectation currently expects that leading slash — that is not a portable normalized Windows path. Use filepath.ToSlash/FromSlash or remove leading slash on Windows when present.
  3. Use filepath.Clean on returned path to normalize ".." segments and separators.
  4. Consider treating non-file schemes (http/https) as non-local and return the original fullURI unchanged — current code falls back to filepath.Rel which will produce odd relatives for URLs.

This might be a simple fix: add unescaping and cleaning, and strip a leading slash for Windows drive letters when runtime.GOOS == "windows".

This might be a simple fix:

Suggested change
func getRelativePath(baseDir string, fullURI string) string {
localPath := fullURI
u, err := url.Parse(fullURI)
if err == nil && u.Scheme == "file" {
// url.Path extracts the local path component correctly
localPath = u.Path
}
relativePath, err := filepath.Rel(baseDir, localPath)
if err != nil {
// Fallback to the normalized absolute path if calculation fails
fmt.Printf("Warning: Could not get relative path for '%s' relative to '%s': %v. Using absolute path.\n", localPath, baseDir, err)
return localPath
}
return relativePath
}
func getRelativePath(baseDir string, fullURI string) string {
localPath := fullURI
u, err := url.Parse(fullURI)
if err == nil && u.Scheme == "file" {
// url.Path may be percent-encoded
if p, err2 := url.PathUnescape(u.Path); err2 == nil {
localPath = p
} else {
localPath = u.Path
}
// On Windows, strip leading slash in file:///C:/... -> C:/... for filepath handling
if runtime.GOOS == "windows" && strings.HasPrefix(localPath, "/") && len(localPath) > 2 && localPath[2] == ':' {
localPath = localPath[1:]
}
} else if err == nil && u.Scheme != "" {
// Non-file URI (http/https): return as-is
return fullURI
}
// Normalize paths
localPath = filepath.Clean(localPath)
relativePath, err := filepath.Rel(baseDir, localPath)
if err != nil {
fmt.Printf("Warning: Could not get relative path for '%s' relative to '%s': %v. Using absolute path.\n", localPath, baseDir, err)
return localPath
}
return relativePath
}

🟡 Medium risk


func processSarifAndSendResults(sarifPath string, commitUUID string, projectToken string, apiToken string, tools map[string]*plugins.ToolInfo) {
if projectToken == "" && apiToken == "" && provider == "" && repository == "" {
fmt.Println("Error: api-token, provider and repository are required when project-token is not provided")
Expand Down Expand Up @@ -86,6 +129,12 @@ func processSarif(sarif Sarif, tools map[string]*plugins.ToolInfo) [][]map[strin
var codacyIssues []map[string]interface{}
var payloads [][]map[string]interface{}

baseDir, err := os.Getwd()
if err != nil {
fmt.Printf("Error getting current working directory: %v\n", err)
os.Exit(1)
}

for _, run := range sarif.Runs {
var toolName = getToolName(strings.ToLower(run.Tool.Driver.Name), run.Tool.Driver.Version)
tool, patterns := loadsToolAndPatterns(toolName, false)
Expand All @@ -98,8 +147,12 @@ func processSarif(sarif Sarif, tools map[string]*plugins.ToolInfo) [][]map[strin
continue
}
for _, location := range result.Locations {

fullURI := location.PhysicalLocation.ArtifactLocation.URI
relativePath := getRelativePath(baseDir, fullURI)

issue := map[string]interface{}{
"source": location.PhysicalLocation.ArtifactLocation.URI,
"source": relativePath,
"line": location.PhysicalLocation.Region.StartLine,
"type": pattern.ID,
"message": result.Message.Text,
Expand All @@ -119,8 +172,12 @@ func processSarif(sarif Sarif, tools map[string]*plugins.ToolInfo) [][]map[strin
// Iterate through run.Artifacts and create entries in the results object
for _, artifact := range run.Artifacts {
if artifact.Location.URI != "" {

fullURI := artifact.Location.URI
relativePath := getRelativePath(baseDir, fullURI)

results = append(results, map[string]interface{}{
"filename": artifact.Location.URI,
"filename": relativePath,
"results": []map[string]interface{}{},
})
}
Expand Down Expand Up @@ -169,10 +226,10 @@ func processSarif(sarif Sarif, tools map[string]*plugins.ToolInfo) [][]map[strin
}

}

var toolShortName = getToolShortName(toolName)
payload := []map[string]interface{}{
{
"tool": toolName,
"tool": toolShortName,
"issues": map[string]interface{}{
"Success": map[string]interface{}{
"results": results,
Expand Down
129 changes: 129 additions & 0 deletions cmd/upload_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package cmd

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func TestGetRelativePath(t *testing.T) {

Check warning on line 10 in cmd/upload_test.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

cmd/upload_test.go#L10

Method TestGetRelativePath has 61 lines of code (limit is 50)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

const baseDir = "/home/user/project/src"

tests := []struct {
name string
baseDir string
fullURI string
expected string
}{
{
name: "1. File URI with standard path",
baseDir: baseDir,
fullURI: "file:///home/user/project/src/lib/file.go",
expected: "lib/file.go",
},
{
name: "2. File URI with baseDir as the file path",
baseDir: baseDir,
fullURI: "file:///home/user/project/src",
expected: ".",
},
{
name: "3. Simple path (no scheme)",
baseDir: baseDir,
fullURI: "/home/user/project/src/main.go",
expected: "main.go",
},
{
name: "4. URI outside baseDir (should return absolute path if relative fails)",
baseDir: baseDir,
fullURI: "file:///etc/config/app.json",
// This is outside of baseDir, so we expect the absolute path starting from the baseDir root
expected: "../../../../etc/config/app.json",
},
{
name: "5. Plain URI with different scheme (should be treated as plain path)",
baseDir: baseDir,
fullURI: "http://example.com/api/v1/file.go",
expected: "http://example.com/api/v1/file.go",
},
{
name: "6. Empty URI",
baseDir: baseDir,
fullURI: "",
expected: "",
},
{
name: "7. Windows path on a file URI (should correctly strip the leading slash from the path component)",
baseDir: "C:\\Users\\dev\\repo",
fullURI: "file:///C:/Users/dev/repo/app/main.go",
expected: "/C:/Users/dev/repo/app/main.go",
},
{
name: "8. URI with spaces (URL encoded)",
baseDir: baseDir,
fullURI: "file:///home/user/project/src/file%20with%20spaces.go",
expected: "file with spaces.go",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := getRelativePath(tt.baseDir, tt.fullURI)
expectedNormalized := filepath.FromSlash(tt.expected)
assert.Equal(t, expectedNormalized, actual, "Relative path should match expected")
})
}
Comment on lines +10 to +76
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Suggestion

Codacy flagged TestGetRelativePath as exceeding 50 lines (Lizard_nloc-medium). The test is large and contains many cases; split it into smaller table-driven sub-tests or move helper expectations into separate test functions to reduce function NLOC and improve clarity.

⚪ Low risk


See Issue in Codacy

}
func TestGetToolShortName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "MappedTool_ESLint8",
input: "ESLint",
expected: "eslint-8",
},
{
name: "MappedTool_PMD7",
input: "PMD7",
expected: "pmd-7",
},
{
name: "MappedTool_Pylint",
input: "Pylint",
expected: "pylintpython3",
},
{
name: "UnmappedTool_Fallback",
input: "NewToolName",
expected: "NewToolName",
},
{
name: "UnmappedTool_AnotherFallback",
input: "SomeAnalyzer",
expected: "SomeAnalyzer",
},
{
name: "EmptyInput_Fallback",
input: "",
expected: "",
},
{
name: "MappedTool_Deprecated",
input: "ESLint (deprecated)",
expected: "eslint",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := getToolShortName(tt.input)
if actual != tt.expected {
t.Errorf("getToolShortName(%q) = %q; want %q", tt.input, actual, tt.expected)
}
})
}
}
19 changes: 6 additions & 13 deletions codacy-client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,13 +170,13 @@ func parsePatternConfigurations(response []byte) ([]domain.PatternConfiguration,
return patternConfigurations, pagination.Cursor, nil
}

// GetDefaultToolPatternsConfig fetches the default patterns for a tool
func GetDefaultToolPatternsConfig(initFlags domain.InitFlags, toolUUID string, onlyEnabledPatterns bool) ([]domain.PatternConfiguration, error) {
return GetDefaultToolPatternsConfigWithCodacyAPIBase(CodacyApiBase, initFlags, toolUUID, onlyEnabledPatterns)
// GetToolPatternsConfig fetches the default patterns for a tool
func GetToolPatternsConfig(initFlags domain.InitFlags, toolUUID string, onlyEnabledPatterns bool) ([]domain.PatternConfiguration, error) {
return GetToolPatternsConfigWithCodacyAPIBase(CodacyApiBase, initFlags, toolUUID, onlyEnabledPatterns)
}

// GetDefaultToolPatternsConfigWithCodacyAPIBase fetches the default patterns for a tool, and a base api url
func GetDefaultToolPatternsConfigWithCodacyAPIBase(codacyAPIBaseURL string, initFlags domain.InitFlags, toolUUID string, onlyEnabledPatterns bool) ([]domain.PatternConfiguration, error) {
// GetToolPatternsConfigWithCodacyAPIBase fetches the default patterns for a tool, and a base api url
func GetToolPatternsConfigWithCodacyAPIBase(codacyAPIBaseURL string, initFlags domain.InitFlags, toolUUID string, onlyEnabledPatterns bool) ([]domain.PatternConfiguration, error) {
baseURL := fmt.Sprintf("%s/api/v3/tools/%s/patterns", codacyAPIBaseURL, toolUUID)
if onlyEnabledPatterns {
baseURL += "?enabled=true"
Expand All @@ -187,14 +187,7 @@ func GetDefaultToolPatternsConfigWithCodacyAPIBase(codacyAPIBaseURL string, init
return nil, err
}

onlyRecommendedPatterns := make([]domain.PatternConfiguration, 0)
for _, pattern := range allPaterns {
if pattern.PatternDefinition.Enabled {
onlyRecommendedPatterns = append(onlyRecommendedPatterns, pattern)
}
}

return onlyRecommendedPatterns, nil
return allPaterns, nil
}

// GetRepositoryToolPatterns fetches the patterns for a tool in a repository
Expand Down
50 changes: 2 additions & 48 deletions codacy-client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package codacyclient

import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
Expand Down Expand Up @@ -91,7 +90,7 @@ func TestGetPageAndGetAllPages(t *testing.T) {
assert.Len(t, allItems, 3)
}

func TestGetDefaultToolPatternsConfig_Empty(t *testing.T) {
func TestGetToolPatternsConfig_Empty(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := map[string]interface{}{
"data": []interface{}{},
Expand All @@ -104,52 +103,7 @@ func TestGetDefaultToolPatternsConfig_Empty(t *testing.T) {
CodacyApiBase = ts.URL

initFlags := domain.InitFlags{ApiToken: "dummy"}
patterns, err := GetDefaultToolPatternsConfigWithCodacyAPIBase(CodacyApiBase, initFlags, "tool-uuid", true)
patterns, err := GetToolPatternsConfigWithCodacyAPIBase(CodacyApiBase, initFlags, "tool-uuid", true)
assert.NoError(t, err)
assert.Empty(t, patterns)
}

func TestGetDefaultToolPatternsConfig_WithNonRecommended(t *testing.T) {

config := []domain.PatternDefinition{
{
Id: "internal_id_1",
Enabled: true,
},
{
Id: "internal_id_2",
Enabled: false,
},
}

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

resp := map[string]interface{}{
"data": config,
"pagination": map[string]interface{}{"cursor": ""},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()

expected := []domain.PatternConfiguration{
{
Enabled: true,
PatternDefinition: domain.PatternDefinition{
Id: "internal_id_1",
Enabled: true,
},
},
}

CodacyApiBase = ts.URL

initFlags := domain.InitFlags{ApiToken: "dummy"}
patterns, err := GetDefaultToolPatternsConfigWithCodacyAPIBase(CodacyApiBase, initFlags, "tool-uuid", true)

fmt.Println(len(patterns))

assert.NoError(t, err)
assert.Equal(t, expected, patterns)
}