From 39ea30074e8dcc3d19c49fc5d6fb4a9766a0bf6c Mon Sep 17 00:00:00 2001 From: Amy Boyd Date: Wed, 22 Jun 2016 23:11:59 +0100 Subject: [PATCH] Add config file finder. --- editorconfig/config_file.go | 103 ++++++++++++++++++++++++ editorconfig/config_file_finder.go | 88 ++++++++++++++++++++ editorconfig/config_file_finder_test.go | 34 ++++++++ 3 files changed, 225 insertions(+) create mode 100644 editorconfig/config_file.go create mode 100644 editorconfig/config_file_finder.go create mode 100644 editorconfig/config_file_finder_test.go diff --git a/editorconfig/config_file.go b/editorconfig/config_file.go new file mode 100644 index 0000000..ee56c14 --- /dev/null +++ b/editorconfig/config_file.go @@ -0,0 +1,103 @@ +package editorconfig + +import ( + "github.com/go-ini/ini" + "regexp" + "strings" +) + +type ConfigFile struct { + Path string + DefaultRuleSet RuleSet + FileConstrainedRuleSets []FileConstrainedRuleSet +} + +func (cf *ConfigFile) Dir() string { + return GetParentDir(cf.Path) +} + +func (cf *ConfigFile) IsRoot() bool { + return cf.DefaultRuleSet.Get("root") == "true" +} + +func (cf *ConfigFile) Precedence() int { + return strings.Count(cf.Path, "/") +} + +type Rule struct { + Name string + Value string +} + +type RuleSet []Rule + +func (rs *RuleSet) Add(r Rule) { + *rs = append(*rs, r) +} + +func (rs RuleSet) Get(name string) string { + for i := 0; i < len(rs); i++ { + if rs[i].Name == name { + return rs[i].Value + } + } + return "" +} + +type FileConstrainedRuleSet struct { + Constraint string + ConstraintRegexp *regexp.Regexp + Rules RuleSet +} + +func CreateConfigFileStruct(path string) ConfigFile { + ini, err := ini.Load(path) + if err != nil { + ExitBecauseOfInternalError("Could not parse " + path) + } + + cf := ConfigFile{} + cf.Path = path + cf.DefaultRuleSet = CreateRuleSetFromIniSectonName(ini, "") + + for _, sectionName := range ini.SectionStrings() { + if sectionName == "DEFAULT" { + continue + } + + fcrs := FileConstrainedRuleSet{ + Constraint: sectionName, + ConstraintRegexp: ConvertWildcardPatternToGoRegexp(sectionName), + Rules: CreateRuleSetFromIniSectonName(ini, sectionName), + } + cf.FileConstrainedRuleSets = append(cf.FileConstrainedRuleSets, fcrs) + } + + return cf +} + +func CreateRuleSetFromIniSectonName(ini *ini.File, name string) RuleSet { + section, err := ini.GetSection(name) + if err != nil { + ExitBecauseOfInternalError(err.Error()) + // return RuleSet{} + } + + return CreateRuleSetFromIniSecton(section) +} + +func CreateRuleSetFromIniSecton(section *ini.Section) RuleSet { + rs := RuleSet{} + + for name, val := range section.KeysHash() { + // The spec says rule names should be case-insensitive. + name = strings.ToLower(name) + + rs.Add(Rule{ + Name: name, + Value: val, + }) + } + + return rs +} diff --git a/editorconfig/config_file_finder.go b/editorconfig/config_file_finder.go new file mode 100644 index 0000000..58f4f78 --- /dev/null +++ b/editorconfig/config_file_finder.go @@ -0,0 +1,88 @@ +package editorconfig + +import ( + "os" + "path/filepath" + "strings" +) + +var configFiles = []ConfigFile{} + +var checkedDirectories = []string{} + +func FindConfigFiles(sourceFilePaths []string) []ConfigFile { + configFiles = []ConfigFile{} + + for _, path := range sourceFilePaths { + // Convert to absolute path so we can go all the way up the file system path looking for configs. + path, err := filepath.Abs(path) + if err != nil { + ExitBecauseOfInternalError("Could not get absolute path for " + path) + } + + path = GetParentDir(path) + + var searchParents bool + for strings.Contains(path, "/") { + searchParents = CheckDirectoryForConfigFile(path) + if !searchParents { + break + } + path = GetParentDir(path) + } + + if searchParents { + // `path` is now the volume root without a trailing slash, e.g. '' for '/' or 'C:' for 'C:/'. + CheckDirectoryForConfigFile(path + "/") + } + } + + return configFiles +} + +/** + * @return Whether or not to search parent directories. + */ +func CheckDirectoryForConfigFile(dir string) bool { + for _, cd := range checkedDirectories { + if dir == cd { + return false + } + } + + stat, err := os.Stat(dir) + if err != nil { + ExitBecauseOfInternalError("Could not stat directory: " + dir) + } + if !stat.IsDir() { + ExitBecauseOfInternalError("Expected this path to be a directory, but it is not: " + dir) + } + + checkedDirectories = append(checkedDirectories, dir) + + possibleFile := GetConfigFilePathInDirectory(dir) + stat, err = os.Stat(possibleFile) + if err != nil { + return true + } + if stat.IsDir() { + return true + } + if stat.Size() == 0 { + return true + } + + cf := CreateConfigFileStruct(possibleFile) + configFiles = append(configFiles, cf) + + return !cf.IsRoot() +} + +func GetConfigFilePathInDirectory(dir string) string { + if strings.HasSuffix(dir, "/") { + // This will only be possible in Unix for the '/' directory. + return dir + ".editorconfig" + } else { + return dir + "/.editorconfig" + } +} diff --git a/editorconfig/config_file_finder_test.go b/editorconfig/config_file_finder_test.go new file mode 100644 index 0000000..109d678 --- /dev/null +++ b/editorconfig/config_file_finder_test.go @@ -0,0 +1,34 @@ +package editorconfig + +import ( + "strconv" + "testing" +) + +func TestGetConfigFilePathInDirectoryForNonRootPath(t *testing.T) { + if GetConfigFilePathInDirectory("/home/x") != "/home/x/.editorconfig" { + t.Error("GetConfigFilePathInDirectory failed for path /home/x") + } +} + +func TestGetConfigFilePathInDirectoryForRootPath(t *testing.T) { + if GetConfigFilePathInDirectory("/") != "/.editorconfig" { + t.Error("GetConfigFilePathInDirectory failed for path /") + } +} + +func TestFindConfigFilesWhenDirectoryHasRootConfig(t *testing.T) { + files := FindConfigFiles([]string{"tests/a/b/has-root-config/file.java"}) + + if len(files) != 1 { + t.Error("Should have found 1 .editorconfig file but found " + strconv.Itoa(len(files))) + } +} + +func TestFindConfigFilesFindsRootConfigInParentDirectory(t *testing.T) { + files := FindConfigFiles([]string{"tests/a/b/c/d/file.java"}) + + if len(files) != 2 { + t.Error("Should have found 2 .editorconfig files but found " + strconv.Itoa(len(files))) + } +}