Skip to content
Merged
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
49 changes: 31 additions & 18 deletions cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,36 +251,49 @@ 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{}
}
return tool, patterns
}

var versionedToolNames = map[string]map[int]string{
"eslint": {
7: "ESLint (deprecated)",
8: "ESLint",
9: "ESLint9",
},
"pmd": {
6: "PMD",
7: "PMD7",
},
}

var simpleToolAliases = map[string]string{
"lizard": "Lizard",
"semgrep": "Semgrep",
"pylint": "pylintpython3",
"trivy": "Trivy",
}

func getToolName(toolName string, version string) string {
majorVersion := getMajorVersion(version)
if toolName == "eslint" {
switch majorVersion {
case 7:
return "ESLint (deprecated)"
case 8:
return "ESLint"
case 9:
return "ESLint9"
}
} else {
if toolName == "pmd" {
switch majorVersion {
case 6:
return "PMD"
case 7:
return "PMD7"
}

// Check for version-specific tool name: for eslint and pmd
if versions, ok := versionedToolNames[toolName]; ok {
if name, ok := versions[majorVersion]; ok {
return name
}
}

// Check for non-versioned tool name alias
if codacyToolName, ok := simpleToolAliases[toolName]; ok {
return codacyToolName
}

// Default: Return the original tool name if no map or version matches
return toolName
}

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
67 changes: 63 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

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,7 +129,15 @@ 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 {
//getToolName will take care of mapping sarif tool names to codacy tool names
//especially for eslint and pmd that have multiple versions
var toolName = getToolName(strings.ToLower(run.Tool.Driver.Name), run.Tool.Driver.Version)
tool, patterns := loadsToolAndPatterns(toolName, false)

Expand All @@ -98,8 +149,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 +174,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 +228,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)

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

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
Loading