+
{t("order.title")}
+
+
+
+
+
+
+
+
+
+
+
+ | {t("order.customer")} |
+ {t("order.date")} |
+ {t("order.amount")} |
+
+
+
+ {orders.map((o) => (
+
+ | {o.customer} |
+
+ {/* ❌ BAD: Hardcoded date formatting */}
+ {o.date.toLocaleDateString("en-US")} |
+
+ {/* ✅ GOOD: i18n aware date */}
+ {/* {t("order.date", { date: o.date, format: "date" })} | */}
+
+ {/* ❌ BAD: Hardcoded number */}
+ ${o.amount.toFixed(2)} |
+
+ {/* ✅ GOOD: Locale-sensitive */}
+ {/* {t("order.amount", { amount: o.amount, format: "number" })} | */}
+
+ ))}
+
+
+
+
{t("order.total", { count: orders.length })}
+
+ {/* ❌ BAD: Revenue displayed without locale formatting */}
+
Total Revenue: {orders.reduce((sum, o) => sum + o.amount, 0)}
+
+ {/* ✅ GOOD: Revenue with i18n interpolation */}
+ {/*
{t("order.revenue", { revenue: orders.reduce((s, o) => s + o.amount, 0), format: "number" })}
*/}
+
+ );
+}
diff --git a/docs/multiple-tests/i18n/src/PaymentService.java b/docs/multiple-tests/i18n/src/PaymentService.java
new file mode 100644
index 0000000..b5db9f0
--- /dev/null
+++ b/docs/multiple-tests/i18n/src/PaymentService.java
@@ -0,0 +1,23 @@
+import java.text.MessageFormat;
+import java.util.ResourceBundle;
+
+public class PaymentService {
+ private final ResourceBundle messages;
+
+ public PaymentService(ResourceBundle messages) {
+ this.messages = messages;
+ }
+
+ public void processPayment(String customer, double amount, boolean success) {
+ if (success) {
+ String msg = messages.getString("payment.success");
+ System.out.println(MessageFormat.format(msg, customer, amount));
+ } else {
+ // BAD: Inline error
+ System.out.println("Payment failed for " + customer + "! Amount: " + amount); // ❌
+
+ // GOOD: Localized
+ System.out.println(messages.getString("error.payment"));
+ }
+ }
+}
diff --git a/docs/multiple-tests/i18n/src/UILayer.java b/docs/multiple-tests/i18n/src/UILayer.java
new file mode 100644
index 0000000..725d3af
--- /dev/null
+++ b/docs/multiple-tests/i18n/src/UILayer.java
@@ -0,0 +1,25 @@
+import java.util.ResourceBundle;
+
+public class UILayer {
+ private final ResourceBundle messages;
+
+ public UILayer(ResourceBundle messages) {
+ this.messages = messages;
+ }
+
+ public void showWelcome() {
+ // BAD: Hardcoded welcome
+ System.out.println("Welcome to Order Processing System"); // ❌
+
+ // GOOD: Localized welcome
+ System.out.println(messages.getString("app.start"));
+ }
+
+ public void showLabel(String key) {
+ try {
+ System.out.println(messages.getString(key));
+ } catch (Exception e) {
+ System.out.println("Missing i18n key: " + key); // ❌ fallback
+ }
+ }
+}
diff --git a/docs/multiple-tests/i18n/src/i18n.js b/docs/multiple-tests/i18n/src/i18n.js
new file mode 100644
index 0000000..f29f5aa
--- /dev/null
+++ b/docs/multiple-tests/i18n/src/i18n.js
@@ -0,0 +1,51 @@
+import i18n from "i18next";
+import { initReactI18next } from "react-i18next";
+
+const resources = {
+ en: {
+ translation: {
+ "order.title": "Order Management",
+ "order.create": "Create Order",
+ "order.customer": "Customer",
+ "order.date": "Order Date",
+ "order.amount": "Amount",
+ "order.success": "Order created successfully",
+ "order.error": "Failed to create order",
+ "order.total": "Total Orders: {{count}}",
+ "order.revenue": "Total Revenue: {{revenue, number}}",
+ },
+ },
+ fr: {
+ translation: {
+ "order.title": "Gestion des commandes",
+ // missing "order.success" -> should fallback
+ "order.create": "Créer une commande",
+ },
+ },
+ pseudo: {
+ translation: new Proxy({}, {
+ get: (_, key) => `[[${key}]]`, // Pseudo-localization
+ }),
+ },
+};
+
+i18n.use(initReactI18next).init({
+ resources,
+ lng: "en", // default
+ fallbackLng: "en", // fallback
+ debug: true,
+ interpolation: {
+ escapeValue: false,
+ format: function (value, format) {
+ if (format === "number") {
+ return new Intl.NumberFormat(i18n.language).format(value);
+ }
+ if (format === "date") {
+ return new Intl.DateTimeFormat(i18n.language).format(value);
+ }
+ return value;
+ },
+ },
+});
+
+export default i18n;
diff --git a/internal/docgen/parsing.go b/internal/docgen/parsing.go
index 013641c..ef7cf09 100644
--- a/internal/docgen/parsing.go
+++ b/internal/docgen/parsing.go
@@ -10,10 +10,13 @@ import (
"sort"
"strings"
+ codacy "github.com/codacy/codacy-engine-golang-seed/v6"
"github.com/samber/lo"
"gopkg.in/yaml.v3"
)
+var htmlCommentRegex = regexp.MustCompile(``)
+
// Downloads Semgrep rules from the official repository.
// Downloads the default rules from the Registry.
// Parses Semgrep rules from YAML files.
@@ -24,11 +27,12 @@ type SemgrepConfig struct {
}
type SemgrepRule struct {
- ID string `yaml:"id"`
- Message string `yaml:"message"`
- Severity string `yaml:"severity"`
- Languages []string `yaml:"languages"`
- Metadata SemgrepRuleMetadata `yaml:"metadata"`
+ ID string `yaml:"id"`
+ Message string `yaml:"message"`
+ Severity string `yaml:"severity"`
+ Languages []string `yaml:"languages"`
+ Metadata SemgrepRuleMetadata `yaml:"metadata"`
+ Parameters []codacy.PatternParameter
}
type SemgrepRuleMetadata struct {
@@ -80,6 +84,7 @@ func semgrepRules(destinationDir string) ([]PatternWithExplanation, *ParsedSemgr
allRulesFiles := append(parsedSemgrepRegistryRules.Files, parsedGitLabRules.Files...)
allRulesFiles = append(allRulesFiles, parsedCodacyRules.Files...)
+
parsedRules := ParsedSemgrepRules{
Rules: allRules,
Files: allRulesFiles,
@@ -106,12 +111,33 @@ func getGitLabRules() (*ParsedSemgrepRules, error) {
}
func getCodacyRules(docsDir string) (*ParsedSemgrepRules, error) {
- filePath, _ := filepath.Abs(path.Join(docsDir, "codacy-rules.yaml"))
- return getRules(
- filePath,
- "",
- func(_ string) bool { return true },
- func(_ string, unprefixedID string) string { return unprefixedID })
+
+ parsedRules := &ParsedSemgrepRules{
+ Rules: SemgrepRules{},
+ Files: []SemgrepRuleFile{},
+ IDMapper: map[IDMapperKey]string{},
+ }
+ customRulesFiles := []string{
+ "codacy-rules.yaml",
+ "codacy-rules-i18n.yaml",
+ "codacy-rules-ai.yaml",
+ }
+ for _, file := range customRulesFiles {
+ filePath, _ := filepath.Abs(path.Join(docsDir, file))
+ rules, err := getRules(
+ filePath,
+ "",
+ func(_ string) bool { return true },
+ func(_ string, unprefixedID string) string { return unprefixedID })
+ if err != nil {
+ return nil, err
+ }
+ parsedRules.Rules = append(parsedRules.Rules, rules.Rules...)
+ parsedRules.Files = append(parsedRules.Files, rules.Files...)
+ maps.Copy(parsedRules.IDMapper, rules.IDMapper)
+ }
+ return parsedRules, nil
+
}
type FilenameValidator func(string) bool
@@ -220,10 +246,7 @@ func prefixRuleIDWithPath(relativePath string, unprefixedID string) string {
filename := filepath.Base(relativePath)
filenameWithoutExt := strings.TrimSuffix(filename, filepath.Ext(filename))
prefixedID := strings.ReplaceAll(filepath.Dir(relativePath), "/", ".") + "." + filenameWithoutExt + "." + unprefixedID
-
- // Remove leading "tmp." if present (unconditionally)
- lowerPrefixedID := strings.ToLower(prefixedID)
- return lowerPrefixedID
+ return strings.ToLower(prefixedID)
}
func getSemgrepRegistryDefaultRules() (SemgrepRules, error) {
@@ -242,16 +265,93 @@ func readRulesFromYaml(yamlFile string) ([]SemgrepRule, error) {
return nil, &DocGenError{msg: fmt.Sprintf("Failed to read file: %s", yamlFile), w: err}
}
+ // First unmarshal to raw map to extract regex patterns
+ var rawConfig map[string]interface{}
+ err = yaml.Unmarshal(buf, &rawConfig)
+ if err != nil {
+ return nil, &DocGenError{msg: fmt.Sprintf("Failed to unmarshal file: %s", yamlFile), w: err}
+ }
+
+ // Then unmarshal to structured config
c := &SemgrepConfig{}
err = yaml.Unmarshal(buf, c)
if err != nil {
return nil, &DocGenError{msg: fmt.Sprintf("Failed to unmarshal file: %s", yamlFile), w: err}
+ }
+ // Extract parameters from regex placeholders
+ if rawRules, ok := rawConfig["rules"].([]interface{}); ok {
+ for i, rawRule := range rawRules {
+ if i >= len(c.Rules) {
+ break
+ }
+ if ruleMap, ok := rawRule.(map[string]interface{}); ok {
+ c.Rules[i].Parameters = extractParametersFromRule(ruleMap)
+ }
+ }
}
return c.Rules, nil
}
+// extractParametersFromRule recursively searches for regex fields with HTML comment placeholders
+// and creates PatternParameters for each one found
+func extractParametersFromRule(ruleMap map[string]interface{}) []codacy.PatternParameter {
+ var parameters []codacy.PatternParameter
+ seenParams := make(map[string]bool)
+
+ var searchForRegex func(obj interface{})
+ searchForRegex = func(obj interface{}) {
+ switch v := obj.(type) {
+ case map[string]interface{}:
+ for key, value := range v {
+ if key == "regex" {
+ if regexStr, ok := value.(string); ok {
+ if matches := htmlCommentRegex.FindStringSubmatch(regexStr); len(matches) > 1 {
+ paramName := matches[1]
+ if !seenParams[paramName] {
+ seenParams[paramName] = true
+ // Convert to proper case (e.g., MODEL_REGEX -> modelRegex)
+ formattedName := formatParameterName(paramName)
+ parameters = append(parameters, codacy.PatternParameter{
+ Name: formattedName,
+ Description: fmt.Sprintf("Regular expression pattern for %s", strings.ToLower(strings.ReplaceAll(paramName, "_", " "))),
+ Default: ".*",
+ })
+ }
+ }
+ }
+ } else {
+ searchForRegex(value)
+ }
+ }
+ case []interface{}:
+ for _, item := range v {
+ searchForRegex(item)
+ }
+ }
+ }
+
+ searchForRegex(ruleMap)
+ return parameters
+}
+
+// formatParameterName converts UPPER_CASE to camelCase
+func formatParameterName(name string) string {
+ parts := strings.Split(strings.ToLower(name), "_")
+ if len(parts) == 0 {
+ return name
+ }
+
+ result := parts[0]
+ for i := 1; i < len(parts); i++ {
+ if len(parts[i]) > 0 {
+ result += strings.ToUpper(string(parts[i][0])) + parts[i][1:]
+ }
+ }
+ return result
+}
+
func (r SemgrepRule) toPatternWithExplanation() PatternWithExplanation {
return PatternWithExplanation{
ID: r.ID,
@@ -264,6 +364,7 @@ func (r SemgrepRule) toPatternWithExplanation() PatternWithExplanation {
Languages: toCodacyLanguages(r),
Enabled: isEnabledByDefault(r),
Explanation: r.Message,
+ Parameters: r.Parameters,
}
}
diff --git a/internal/docgen/pattern-with-explanation.go b/internal/docgen/pattern-with-explanation.go
index bb6fa30..4bb9bf6 100644
--- a/internal/docgen/pattern-with-explanation.go
+++ b/internal/docgen/pattern-with-explanation.go
@@ -17,6 +17,7 @@ type PatternWithExplanation struct {
Languages []string
Enabled bool
Explanation string
+ Parameters []codacy.PatternParameter
}
func (r PatternWithExplanation) toCodacyPattern() codacy.Pattern {
@@ -28,6 +29,7 @@ func (r PatternWithExplanation) toCodacyPattern() codacy.Pattern {
Level: string(r.Level),
Languages: r.Languages,
Enabled: r.Enabled,
+ Parameters: r.Parameters,
}
}
@@ -36,6 +38,7 @@ func (r PatternWithExplanation) toCodacyPatternDescription() codacy.PatternDescr
PatternID: r.ID,
Description: r.Description,
Title: r.Title,
+ Parameters: r.Parameters,
}
}
diff --git a/internal/tool/configuration.go b/internal/tool/configuration.go
index d4a1ae4..8bb2346 100644
--- a/internal/tool/configuration.go
+++ b/internal/tool/configuration.go
@@ -2,16 +2,20 @@ package tool
import (
"bufio"
+ "fmt"
"io/fs"
"os"
"path"
"path/filepath"
+ "regexp"
"strings"
codacy "github.com/codacy/codacy-engine-golang-seed/v6"
"github.com/samber/lo"
)
+var htmlCommentRegex = regexp.MustCompile(``)
+
const sourceConfigurationFileName = ".semgrep.yaml"
// TODO: should respect cli flag for docs location
@@ -25,12 +29,14 @@ func newConfigurationFile(toolExecution codacy.ToolExecution) (*os.File, error)
if sourceConfigurationFileExists(toolExecution.SourceDir) {
return getSourceConfigurationFile(toolExecution.SourceDir)
}
+
return createConfigurationFileFromDefaultPatterns(*toolExecution.ToolDefinition.Patterns)
}
if len(*toolExecution.Patterns) == 0 {
return nil, nil
}
+
// if there are configured patterns, create a configuration file from them
return createConfigurationFileFromPatterns(toolExecution.Patterns)
}
@@ -96,12 +102,15 @@ func createAndWriteConfigurationFile(scanner *bufio.Scanner, patterns *[]codacy.
}
idIsPresent := false
+ var currentPattern *codacy.Pattern
for scanner.Scan() {
line := scanner.Text()
- idIsPresent = defaultRuleIsConfigured(line, patterns, idIsPresent)
+ idIsPresent, currentPattern = defaultRuleIsConfiguredWithPattern(line, patterns, idIsPresent, currentPattern)
if idIsPresent {
- _, err := configurationFile.WriteString(line + "\n")
+ // Replace HTML comment placeholders with parameter values
+ processedLine := replaceParameterPlaceholders(line, currentPattern)
+ _, err := configurationFile.WriteString(processedLine + "\n")
if err != nil {
return nil, err
}
@@ -118,6 +127,20 @@ func defaultRuleIsConfigured(line string, patterns *[]codacy.Pattern, idIsPresen
return idIsPresent // We want to keep the same value
}
+func defaultRuleIsConfiguredWithPattern(line string, patterns *[]codacy.Pattern, idIsPresent bool, currentPattern *codacy.Pattern) (bool, *codacy.Pattern) {
+ if strings.Contains(line, "- id:") {
+ id := strings.TrimSpace(strings.Split(line, ":")[1])
+ pattern, found := lo.Find(*patterns, func(item codacy.Pattern) bool {
+ return item.ID == id
+ })
+ if found {
+ return true, &pattern
+ }
+ return false, nil
+ }
+ return idIsPresent, currentPattern
+}
+
func isIDPresent(id string, patterns *[]codacy.Pattern) bool {
_, res := lo.Find(*patterns, func(item codacy.Pattern) bool {
return item.ID == id
@@ -125,6 +148,90 @@ func isIDPresent(id string, patterns *[]codacy.Pattern) bool {
return res
}
+// replaceParameterPlaceholders replaces HTML comment placeholders (e.g., )
+// with the corresponding parameter values from the pattern
+func replaceParameterPlaceholders(line string, pattern *codacy.Pattern) string {
+ if pattern == nil || len(pattern.Parameters) == 0 {
+ return line
+ }
+
+ // Check if line contains an HTML comment placeholder
+ if !htmlCommentRegex.MatchString(line) {
+ return line
+ }
+
+ // Replace each HTML comment with the corresponding parameter value
+ result := htmlCommentRegex.ReplaceAllStringFunc(line, func(match string) string {
+ matches := htmlCommentRegex.FindStringSubmatch(match)
+ if len(matches) > 1 {
+ paramName := matches[1]
+ // Convert UPPER_CASE to camelCase to match parameter name format
+ formattedParamName := formatParameterName(paramName)
+ // Find the parameter in the pattern
+ for _, param := range pattern.Parameters {
+
+ if param.Name == formattedParamName {
+ // Use Value if set, otherwise use Default
+ value := param.Value
+ if value == nil {
+ value = param.Default
+ }
+ if value != nil {
+ valueStr := fmt.Sprintf("%v", value)
+
+ // If parameter name ends with _ALLOW_LIST, convert comma-separated list to regex pattern
+ if strings.HasSuffix(paramName, "_ALLOW_LIST") {
+ return convertListToRegex(valueStr, false)
+ }
+ return valueStr
+ }
+ }
+ }
+ }
+ // If no parameter found, keep the original placeholder
+ return match
+ })
+
+ return result
+}
+
+// convertListToRegex converts a comma-separated list into a regex alternation pattern
+// Example: "gemini-2.5-flash,gpt-3.5-turbo,old-llama-model" -> "^(gemini-2\\.5-flash|gpt-3\\.5-turbo|old-llama-model)$"
+func convertListToRegex(list string, include bool) string {
+ // Split by comma and trim spaces
+ items := strings.Split(list, ",")
+ for i, item := range items {
+ // Trim whitespace
+ item = strings.TrimSpace(item)
+ // Escape dots for regex
+ item = strings.ReplaceAll(item, ".", "\\.")
+ items[i] = item
+ }
+
+ // Join with pipe separator and wrap in regex anchors
+ if include {
+ return fmt.Sprintf("^(%s)$", strings.Join(items, "|"))
+ }
+
+ return fmt.Sprintf("^(?!(%s)$).*", strings.Join(items, "|"))
+}
+
+// formatParameterName converts UPPER_CASE to camelCase
+func formatParameterName(name string) string {
+ parts := strings.Split(strings.ToLower(name), "_")
+ if len(parts) == 0 {
+ return name
+ }
+
+ result := parts[0]
+ for i := 1; i < len(parts); i++ {
+ if len(parts[i]) > 0 {
+ result += strings.ToUpper(string(parts[i][0])) + parts[i][1:]
+ }
+ }
+ return result
+}
+
var filesByLanguage = make(map[string][]string)
// Semgrep: supported language tags are: apex, bash, c, c#, c++, cairo, clojure, cpp, csharp, dart, docker, dockerfile, elixir, ex, generic, go, golang, hack, hcl, html, java, javascript, js, json, jsonnet, julia, kotlin, kt, lisp, lua, none, ocaml, php, promql, proto, proto3, protobuf, py, python, python2, python3, r, regex, ruby, rust, scala, scheme, sh, sol, solidity, swift, terraform, tf, ts, typescript, vue, xml, yaml