diff --git a/cmd/editorconfig/main.go b/cmd/editorconfig/main.go index f3ee12c..e4bc1ba 100644 --- a/cmd/editorconfig/main.go +++ b/cmd/editorconfig/main.go @@ -9,6 +9,7 @@ import ( "gopkg.in/ini.v1" "github.com/editorconfig/editorconfig-core-go/v2" + "github.com/editorconfig/editorconfig-core-go/v2/parser" ) const ( @@ -47,7 +48,7 @@ func main() { } if len(rest) > 1 { - config.Parser = editorconfig.NewCachedParser() + config.Parser = parser.NewCached() } for _, file := range rest { diff --git a/editorconfig.go b/editorconfig.go index 7021c0b..6bce696 100644 --- a/editorconfig.go +++ b/editorconfig.go @@ -55,8 +55,8 @@ type Editorconfig struct { config *Config } -// newEditorconfig builds the configuration from an INI file. -func newEditorconfig(iniFile *ini.File) (*Editorconfig, error) { +// NewEditorconfig builds the configuration from an INI file. +func NewEditorconfig(iniFile *ini.File) (*Editorconfig, error) { editorConfig := &Editorconfig{} // Consider mixed-case values for true and false. @@ -206,7 +206,7 @@ func Parse(r io.Reader) (*Editorconfig, error) { return nil, err } - return newEditorconfig(iniFile) + return NewEditorconfig(iniFile) } // ParseBytes parses from a slice of bytes. @@ -218,7 +218,7 @@ func ParseBytes(data []byte) (*Editorconfig, error) { return nil, err } - return newEditorconfig(iniFile) + return NewEditorconfig(iniFile) } // ParseFile parses from a file. @@ -230,7 +230,7 @@ func ParseFile(path string) (*Editorconfig, error) { return nil, err } - return newEditorconfig(iniFile) + return NewEditorconfig(iniFile) } // GetDefinitionForFilename given a filename, searches diff --git a/fnmatch.go b/fnmatch.go index e5eb09e..55ac47f 100644 --- a/fnmatch.go +++ b/fnmatch.go @@ -18,7 +18,7 @@ var ( // FnmatchCase tests whether the name matches the given pattern case included. func FnmatchCase(pattern, name string) (bool, error) { - p := translate(pattern) + p := Translate(pattern) r, err := regexp.Compile(fmt.Sprintf("^%s$", p)) if err != nil { @@ -28,7 +28,10 @@ func FnmatchCase(pattern, name string) (bool, error) { return r.MatchString(name), nil } -func translate(pattern string) string { // nolint: gocyclo +// Translate converts a glob-like pattern into a regexp. +// +// Known limitation: it's pretty bad for ranges such as {1..n} +func Translate(pattern string) string { // nolint: gocyclo index := 0 pat := []rune(pattern) length := len(pat) @@ -143,7 +146,7 @@ func translate(pattern string) string { // nolint: gocyclo result.WriteString(strconv.Itoa(to)) result.WriteRune(')') } else { - r := translate(inner) + r := Translate(inner) result.WriteString(fmt.Sprintf("\\{%s\\}", r)) } diff --git a/fnmatch_test.go b/fnmatch_test.go index 1f7c1d1..99e235e 100644 --- a/fnmatch_test.go +++ b/fnmatch_test.go @@ -1,8 +1,10 @@ -package editorconfig +package editorconfig_test import ( "fmt" "testing" + + "github.com/editorconfig/editorconfig-core-go/v2" ) func TestTranslate(t *testing.T) { @@ -27,8 +29,8 @@ func TestTranslate(t *testing.T) { title := fmt.Sprintf("%d) %s => %s", i, test[0], test[1]) t.Run(title, func(t *testing.T) { t.Parallel() - result := translate(test[0]) // nolint: scopelint - if result != test[1] { // nolint: scopelint + result := editorconfig.Translate(test[0]) // nolint: scopelint + if result != test[1] { // nolint: scopelint t.Errorf("%s != %s", test[1], result) // nolint: scopelint } }) diff --git a/go.mod b/go.mod index c9c931b..abd4231 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/google/go-cmp v0.4.0 + github.com/hashicorp/golang-lru v0.5.4 github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a // indirect golang.org/x/mod v0.2.0 gopkg.in/ini.v1 v1.55.0 diff --git a/go.sum b/go.sum index 5729d0a..1926ab1 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= diff --git a/cached_parser.go b/parser/cached.go similarity index 59% rename from cached_parser.go rename to parser/cached.go index 23ff80c..500d0dc 100644 --- a/cached_parser.go +++ b/parser/cached.go @@ -1,30 +1,31 @@ -package editorconfig +package parser import ( "fmt" "os" "regexp" + "github.com/editorconfig/editorconfig-core-go/v2" "gopkg.in/ini.v1" ) -// CachedParser implements the Parser interface but caches the definition and +// Cached implements the interface but caches the definition and // the regular expressions. -type CachedParser struct { - editorconfigs map[string]*Editorconfig +type Cached struct { + editorconfigs map[string]*editorconfig.Editorconfig regexps map[string]*regexp.Regexp } -// NewCachedParser initializes the CachedParser. -func NewCachedParser() *CachedParser { - return &CachedParser{ - editorconfigs: make(map[string]*Editorconfig), +// NewCached initializes the Cached. +func NewCached() *Cached { + return &Cached{ + editorconfigs: make(map[string]*editorconfig.Editorconfig), regexps: make(map[string]*regexp.Regexp), } } // ParseIni parses the given filename to a Definition and caches the result. -func (parser *CachedParser) ParseIni(filename string) (*Editorconfig, error) { +func (parser *Cached) ParseIni(filename string) (*editorconfig.Editorconfig, error) { ec, ok := parser.editorconfigs[filename] if !ok { fp, err := os.Open(filename) @@ -39,7 +40,7 @@ func (parser *CachedParser) ParseIni(filename string) (*Editorconfig, error) { return nil, err } - ec, err = newEditorconfig(iniFile) + ec, err = editorconfig.NewEditorconfig(iniFile) if err != nil { return nil, err } @@ -51,10 +52,10 @@ func (parser *CachedParser) ParseIni(filename string) (*Editorconfig, error) { } // FnmatchCase calls the module's FnmatchCase and caches the parsed selector. -func (parser *CachedParser) FnmatchCase(selector string, filename string) (bool, error) { +func (parser *Cached) FnmatchCase(selector string, filename string) (bool, error) { r, ok := parser.regexps[selector] if !ok { - p := translate(selector) + p := editorconfig.Translate(selector) var err error diff --git a/parser/lru.go b/parser/lru.go new file mode 100644 index 0000000..8347cbf --- /dev/null +++ b/parser/lru.go @@ -0,0 +1,77 @@ +package parser + +import ( + "fmt" + "os" + "regexp" + + "github.com/editorconfig/editorconfig-core-go/v2" + "github.com/hashicorp/golang-lru" + "gopkg.in/ini.v1" +) + +// LRU implements the interface but caches the definition and +// the regular expressions using Hashicorp's LRU cache. +// +// https://github.com/hashicorp/golang-lru +type LRU struct { + editorconfigs *lru.Cache + regexps *lru.Cache +} + +// NewLRU initializes the LRU. +func NewLRU() *LRU { + c, _ := lru.New(64) + r, _ := lru.New(256) + + return &LRU{ + editorconfigs: c, + regexps: r, + } +} + +// ParseIni parses the given filename to a Definition and caches the result. +func (parser *LRU) ParseIni(filename string) (*editorconfig.Editorconfig, error) { + ec, ok := parser.editorconfigs.Get(filename) + if !ok { + fp, err := os.Open(filename) + if err != nil { + return nil, err + } + + defer fp.Close() + + iniFile, err := ini.Load(fp) + if err != nil { + return nil, err + } + + ec, err = editorconfig.NewEditorconfig(iniFile) + if err != nil { + return nil, err + } + + parser.editorconfigs.Add(filename, ec) + } + + return ec.(*editorconfig.Editorconfig), nil +} + +// FnmatchCase calls the module's FnmatchCase and caches the parsed selector. +func (parser *LRU) FnmatchCase(selector string, filename string) (bool, error) { + r, ok := parser.regexps.Get(selector) + if !ok { + p := editorconfig.Translate(selector) + + var err error + + r, err = regexp.Compile(fmt.Sprintf("^%s$", p)) + if err != nil { + return false, err + } + + parser.regexps.Add(selector, r) + } + + return r.(*regexp.Regexp).MatchString(filename), nil +} diff --git a/simple_parser.go b/simple_parser.go index af6b097..b861509 100644 --- a/simple_parser.go +++ b/simple_parser.go @@ -6,7 +6,7 @@ import ( "gopkg.in/ini.v1" ) -// SimpleParser implements the Parser interface but without doing any caching. +// SimpleParser implements the interface but without doing any caching. type SimpleParser struct{} // ParseIni calls go-ini's Load on the file. @@ -23,7 +23,7 @@ func (parser *SimpleParser) ParseIni(filename string) (*Editorconfig, error) { return nil, err } - return newEditorconfig(iniFile) + return NewEditorconfig(iniFile) } // FnmatchCase calls the module's FnmatchCase.