{"record":{"id":"dffe919e12344fee","repo":"JuliusBrussee/caveman","slug":"w-rule-q-matches-the-empty-string","errorCode":null,"errorMessage":"%w: rule %q matches the empty string","messagePattern":"%w: rule %q matches the empty string","errorType":"validation","errorClass":"ErrInvalidRule","httpStatus":null,"severity":"error","filePath":"shared/platform/redact/payload.go","lineNumber":592,"sourceCode":"\t\tdefault:\n\t\t\treturn nil, \"\", fmt.Errorf(\"%w: %q (rule %q)\", ErrRuleUnsupported, r.Type, r.Name)\n\t\t}\n\n\t\tif strings.TrimSpace(r.Name) == \"\" {\n\t\t\treturn nil, \"\", fmt.Errorf(\"%w: empty name\", ErrInvalidRule)\n\t\t}\n\t\tif r.Pattern == \"\" {\n\t\t\treturn nil, \"\", fmt.Errorf(\"%w: rule %q has an empty pattern\", ErrInvalidRule, r.Name)\n\t\t}\n\t\tre, err := regexp.Compile(r.Pattern)\n\t\tif err != nil {\n\t\t\treturn nil, \"\", fmt.Errorf(\"%w: rule %q: %s\", ErrInvalidRule, r.Name, err)\n\t\t}\n\t\tif re.MatchString(\"\") {\n\t\t\t// Such a pattern matches at every position and would replace the\n\t\t\t// whole body with placeholders. Refuse it rather than destroy the\n\t\t\t// capture.\n\t\t\treturn nil, \"\", fmt.Errorf(\"%w: rule %q matches the empty string\", ErrInvalidRule, r.Name)\n\t\t}\n\t\trepl := r.Replacement\n\t\tif repl == \"\" {\n\t\t\trepl = \"[REDACTED:\" + r.Name + \"]\"\n\t\t}\n\t\tout = append(out, compiledRule{\n\t\t\tname:                 r.Name,\n\t\t\torigin:               OriginOrg,\n\t\t\tre:                   re,\n\t\t\treplIntroducesNeedle: introducesNeedle([]byte(repl)),\n\t\t\t// The replacement is operator-supplied data, not a regexp\n\t\t\t// template: a literal replace keeps \"$1\" from expanding a captured\n\t\t\t// group back into the output.\n\t\t\trepl: []byte(repl),\n\t\t})\n\t}\n\treturn out, sb.String(), nil\n}","sourceCodeStart":574,"sourceCodeEnd":610,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/shared/platform/redact/payload.go#L574-L610","documentation":"The compiled pattern matches the empty string (re.MatchString(\"\") is true — e.g. `.*`, `a*`, `\\s*`, `(a|)`). Applied over the whole body with literal replacement, such a pattern matches at every offset and would collapse the entire capture into a wall of placeholders — a self-inflicted total data loss. Compilation refuses it with ErrInvalidRule, naming the rule. Only this and other 'unusable' shapes are rejected; merely greedy-but-bounded patterns are allowed.","triggerScenarios":"Any rule whose regex can match zero characters: `.*`, `[A-Z]*`, `\\w*`, optional groups that allow empty. The check runs at compile time, so Payload fails on first use with any body.","commonSituations":"Using '*' where '+' was intended (e.g. `\\d*` for digit runs); patterns designed for search contexts where empty matches are harmless; testing the rule with a non-empty sample where the empty match is invisible.","solutions":["Change the quantifier so at least one character is required: `.+` instead of `.*`, `\\d+` instead of `\\d*`.","Test every rule with re.MatchString(\"\") in a rules unit test — this is exactly the package's own guard, replicated cheaply at authoring time.","Add a lint rule in the rule-editor UI flagging unanchored empty-matching patterns."],"exampleFix":"// before\n{Name: \"free-text\", Type: redact.RuleTypeRegex, Pattern: `.*`} // matches empty -> rejected\n\n// after\n{Name: \"free-text\", Type: redact.RuleTypeRegex, Pattern: `.+`} // requires at least one character","handlingStrategy":"validation","validationCode":"func noEmptyMatches(rules []redact.Rule) error {\n    for _, r := range rules {\n        if r.Type != redact.RuleTypeRegex { continue }\n        re, err := regexp.Compile(r.Pattern)\n        if err != nil { return fmt.Errorf(\"rule %q: %w\", r.Name, err) }\n        if re.MatchString(\"\") {\n            return fmt.Errorf(\"rule %q: pattern matches empty string\", r.Name)\n        }\n    }\n    return nil\n}","typeGuard":"func matchesNonEmptyOnly(p string) bool {\n    re, err := regexp.Compile(p)\n    return err == nil && !re.MatchString(\"\")\n}","tryCatchPattern":"if _, _, err := redact.Payload(body, rules); err != nil {\n    if errors.Is(err, redact.ErrInvalidRule) && strings.Contains(err.Error(), \"matches the empty string\") {\n        // change '*' quantifiers to '+' for that rule and revalidate the whole set\n    }\n}","preventionTips":["Prefer '+' over '*' for token-like patterns; require at least one character.","Run re.MatchString(\"\") as a rule-lint in the editor and in CI.","Test rules against representative samples so greedy behavior is visible before deploy."],"tags":["redaction","regex","footgun","validation"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}