feat(config): add TOML configuration system with wildcard pattern matching
- Add .shush.toml config file discovery (project > git root > global) - Support exact patterns (TODO:, @ts-ignore) and wildcards (*IMPORTANT*) - Include smart defaults for common comment preservation patterns - Add config validation and example file generation
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
|
||||
type Config struct {
|
||||
Preserve []string `toml:"preserve"`
|
||||
}
|
||||
|
||||
|
||||
func Default() *Config {
|
||||
return &Config{
|
||||
Preserve: []string{
|
||||
"TODO:",
|
||||
"FIXME:",
|
||||
"HACK:",
|
||||
"XXX:",
|
||||
"@ts-ignore",
|
||||
"@ts-expect-error",
|
||||
"eslint-",
|
||||
"prettier-ignore",
|
||||
"pylint:",
|
||||
"mypy:",
|
||||
"type: ignore",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func Load() (*Config, string, error) {
|
||||
|
||||
if configPath, found := findProjectConfig(); found {
|
||||
config, err := loadFromFile(configPath)
|
||||
if err != nil {
|
||||
return Default(), configPath, err
|
||||
}
|
||||
return config, configPath, nil
|
||||
}
|
||||
|
||||
|
||||
if configPath, found := findGlobalConfig(); found {
|
||||
config, err := loadFromFile(configPath)
|
||||
if err != nil {
|
||||
return Default(), configPath, err
|
||||
}
|
||||
return config, configPath, nil
|
||||
}
|
||||
|
||||
|
||||
return Default(), "", nil
|
||||
}
|
||||
|
||||
|
||||
func findProjectConfig() (string, bool) {
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
localConfig := filepath.Join(cwd, ".shush.toml")
|
||||
if fileExists(localConfig) {
|
||||
return localConfig, true
|
||||
}
|
||||
|
||||
|
||||
if gitRoot := findGitRoot(cwd); gitRoot != "" {
|
||||
gitConfig := filepath.Join(gitRoot, ".shush.toml")
|
||||
if fileExists(gitConfig) && gitConfig != localConfig {
|
||||
return gitConfig, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
|
||||
func findGlobalConfig() (string, bool) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
configPath := filepath.Join(home, ".config", ".shush.toml")
|
||||
if fileExists(configPath) {
|
||||
return configPath, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
|
||||
func findGitRoot(startDir string) string {
|
||||
dir := startDir
|
||||
for {
|
||||
gitDir := filepath.Join(dir, ".git")
|
||||
if fileExists(gitDir) {
|
||||
return dir
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
func loadFromFile(path string) (*Config, error) {
|
||||
config := &Config{}
|
||||
if _, err := toml.DecodeFile(path, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
if len(config.Preserve) == 0 {
|
||||
config.Preserve = Default().Preserve
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
|
||||
func (c *Config) ShouldPreserveComment(comment string) bool {
|
||||
comment = strings.TrimSpace(comment)
|
||||
|
||||
for _, pattern := range c.Preserve {
|
||||
if matchesPattern(comment, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func matchesPattern(text, pattern string) bool {
|
||||
|
||||
if strings.Contains(pattern, "*") {
|
||||
return matchesWildcard(text, pattern)
|
||||
}
|
||||
|
||||
|
||||
return strings.Contains(text, pattern)
|
||||
}
|
||||
|
||||
|
||||
func matchesWildcard(text, pattern string) bool {
|
||||
|
||||
parts := strings.Split(pattern, "*")
|
||||
|
||||
|
||||
if !strings.HasPrefix(pattern, "*") {
|
||||
if !strings.HasPrefix(text, parts[0]) {
|
||||
return false
|
||||
}
|
||||
text = text[len(parts[0]):]
|
||||
parts = parts[1:]
|
||||
} else {
|
||||
parts = parts[1:]
|
||||
}
|
||||
|
||||
|
||||
if !strings.HasSuffix(pattern, "*") && len(parts) > 0 {
|
||||
lastPart := parts[len(parts)-1]
|
||||
if !strings.HasSuffix(text, lastPart) {
|
||||
return false
|
||||
}
|
||||
text = text[:len(text)-len(lastPart)]
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
|
||||
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
index := strings.Index(text, part)
|
||||
if index == -1 {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
text = text[index+len(part):]
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
func CreateExampleConfig() error {
|
||||
content := `# Shush Configuration
|
||||
# Patterns to preserve in comments (supports wildcards with *)
|
||||
preserve = [
|
||||
"TODO:",
|
||||
"FIXME:",
|
||||
"HACK:",
|
||||
"XXX:",
|
||||
"@ts-ignore",
|
||||
"@ts-expect-error",
|
||||
"eslint-",
|
||||
"prettier-ignore",
|
||||
"pylint:",
|
||||
"mypy:",
|
||||
"type: ignore",
|
||||
"*IMPORTANT*", # Example wildcard: preserves any comment containing IMPORTANT
|
||||
"*DEBUG*", # Example wildcard: preserves any comment containing DEBUG
|
||||
]
|
||||
`
|
||||
|
||||
return os.WriteFile(".shush.toml", []byte(content), 0644)
|
||||
}
|
||||
Reference in New Issue
Block a user