diff --git a/editorconfig/path_matcher.go b/editorconfig/path_matcher.go new file mode 100644 index 0000000..e373fb1 --- /dev/null +++ b/editorconfig/path_matcher.go @@ -0,0 +1,75 @@ +package editorconfig + +/* + * Converts a string from a .editorconfig file to a Go-compatible regex, according to the rules + * documented under "Wildcard Patterns" here: + * http://docs.editorconfig.org/en/master/editorconfig-format.html#patterns + * + * * Matches any string of characters, except path separators (/) + * ** Matches any string of characters + * ? Matches any single character + * [seq] Matches any single character in seq + * [!seq] Matches any single character not in seq + * {s1,s2,s3} Matches any of the strings given (separated by commas, can be nested) + * {num1..num2} Matches any integer numbers between num1 and num2, where num1 and num2 can be either positive or negative + */ + +import ( + "regexp" + "strconv" + "strings" +) + +var metaCharsRegexp = []*regexp.Regexp{ + // Characters .+$^!() + regexp.MustCompile(`([\.\+\$\^\!\(\)])`), + // [ not followed by a ] + regexp.MustCompile(`(\[[^\]]*)$`), + // ] not preceded by a [ + regexp.MustCompile(`^([^\[]*\])`), +} + +func ConvertWildcardPatternToGoRegexp(pattern string) *regexp.Regexp { + originalPattern := pattern + + for _, r := range metaCharsRegexp { + pattern = r.ReplaceAllString(pattern, "\\$1") + } + + // Handle ** + pattern = strings.Replace(pattern, `**`, `.+`, -1) + + // Handle * + pattern = strings.Replace(pattern, `*`, `[^/\\]+`, -1) + + // Handle ? + pattern = strings.Replace(pattern, `?`, `.`, -1) + + // [seq] should work already. + + // Handle [!seq] + pattern = strings.Replace(pattern, `[\!`, `[^`, -1) + + // Handle {s1,s2,s3} + for i := 1; i < 7; i++ { + find := `\{([^,}]+)` + strings.Repeat(`,([^,}]+)`, i) + `\}` + replace := "($1" + for ii := 1; ii <= i; ii++ { + replace += "|$" + strconv.Itoa(ii+1) + } + replace += ")" + pattern = regexp.MustCompile(find).ReplaceAllString(pattern, replace) + } + + // Handle {num1..num2} + // @todo - This is currently not fully supported. If there is a numeric range, we only check + // that numbers are present; we don't check if the numbers present are within the correct range. + pattern = regexp.MustCompile(`\{-?\d+\\.\\.-?\d+\}`).ReplaceAllString(pattern, `[-0-9]+`) + + r, err := regexp.Compile(pattern) + if err != nil { + ExitBecauseOfInternalError("A file pattern could not be parsed: " + originalPattern) + } + + return r +} diff --git a/editorconfig/path_matcher_test.go b/editorconfig/path_matcher_test.go new file mode 100644 index 0000000..87ddbcc --- /dev/null +++ b/editorconfig/path_matcher_test.go @@ -0,0 +1,46 @@ +package editorconfig + +import ( + "testing" +) + +func TestConvertWildcardPatternToGoRegexp(t *testing.T) { + RunConvertWildcardPatternToGoRegexp := func(input string, expected string) { + result := ConvertWildcardPatternToGoRegexp(input).String() + if result != expected { + t.Error("For " + input + " got " + result + " but expected " + expected) + } + } + + // Test that special regex characters are escaped. + RunConvertWildcardPatternToGoRegexp(`[hello :).sql`, `\[hello :\)\.sql`) + RunConvertWildcardPatternToGoRegexp(`]hello(`, `\]hello\(`) + + // Test * + RunConvertWildcardPatternToGoRegexp(`*.go`, `[^/\\]+\.go`) + RunConvertWildcardPatternToGoRegexp(`folder/*.go`, `folder/[^/\\]+\.go`) + + // Test ** + RunConvertWildcardPatternToGoRegexp(`**.go`, `.+\.go`) + RunConvertWildcardPatternToGoRegexp(`folder (copy)/**.go`, `folder \(copy\)/.+\.go`) + + // Test ? + RunConvertWildcardPatternToGoRegexp(`words-beginning-with-?.txt`, `words-beginning-with-.\.txt`) + RunConvertWildcardPatternToGoRegexp(`words beginning with ??.txt`, `words beginning with ..\.txt`) + + // Test [seq] + RunConvertWildcardPatternToGoRegexp(`hexadecimal-ids/[a-f0-9]/document`, `hexadecimal-ids/[a-f0-9]/document`) + + // Test [!seq] + RunConvertWildcardPatternToGoRegexp(`names-not-beginning-with-[!A-G]`, `names-not-beginning-with-[^A-G]`) + + // Test {s1,s2,s3} + RunConvertWildcardPatternToGoRegexp(`animals/{aardvark,bunny}/pictures`, `animals/(aardvark|bunny)/pictures`) + RunConvertWildcardPatternToGoRegexp(`animals/{aardvark,bunny,cheetah,donkey}/pictures`, `animals/(aardvark|bunny|cheetah|donkey)/pictures`) + + // Test {num1..num2} (this is not yet fully supported) + RunConvertWildcardPatternToGoRegexp(`photos/{-500..999}.jpg`, `photos/[-0-9]+\.jpg`) + + // Test everything together. + RunConvertWildcardPatternToGoRegexp(`*/**/[a-z]/{photos,videos}/{0..5}.*`, `[^/\\]+/.+/[a-z]/(photos|videos)/[-0-9]+\.[^/\\]+`) +}