259 lines
6.1 KiB
Go
Vendored
259 lines
6.1 KiB
Go
Vendored
package editorconfig
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"gopkg.in/ini.v1"
|
|
)
|
|
|
|
const (
|
|
// ConfigNameDefault represents the name of the configuration file.
|
|
ConfigNameDefault = ".editorconfig"
|
|
// UnsetValue is the value that unsets a preexisting variable.
|
|
UnsetValue = "unset"
|
|
)
|
|
|
|
// IndentStyle possible values.
|
|
const (
|
|
IndentStyleTab = "tab"
|
|
IndentStyleSpaces = "space"
|
|
)
|
|
|
|
// EndOfLine possible values.
|
|
const (
|
|
EndOfLineLf = "lf"
|
|
EndOfLineCr = "cr"
|
|
EndOfLineCrLf = "crlf"
|
|
)
|
|
|
|
// Charset possible values.
|
|
const (
|
|
CharsetLatin1 = "latin1"
|
|
CharsetUTF8 = "utf-8"
|
|
CharsetUTF16BE = "utf-16be"
|
|
CharsetUTF16LE = "utf-16le"
|
|
CharsetUTF8BOM = "utf-8 bom"
|
|
)
|
|
|
|
// Limit for section name.
|
|
const (
|
|
MaxSectionLength = 4096
|
|
)
|
|
|
|
// Editorconfig represents a .editorconfig file.
|
|
//
|
|
// It is composed by a "root" property, plus the definitions defined in the
|
|
// file.
|
|
type Editorconfig struct {
|
|
Root bool
|
|
Definitions []*Definition
|
|
config *Config
|
|
}
|
|
|
|
// 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.
|
|
rootKey := iniFile.Section(ini.DefaultSection).Key("root")
|
|
rootKey.SetValue(strings.ToLower(rootKey.Value()))
|
|
editorConfig.Root = rootKey.MustBool(false)
|
|
|
|
for _, sectionStr := range iniFile.SectionStrings() {
|
|
if sectionStr == ini.DefaultSection || len(sectionStr) > MaxSectionLength {
|
|
continue
|
|
}
|
|
|
|
iniSection := iniFile.Section(sectionStr)
|
|
definition := &Definition{}
|
|
raw := make(map[string]string)
|
|
|
|
if err := iniSection.MapTo(&definition); err != nil {
|
|
return nil, fmt.Errorf("error mapping current section: %w", err)
|
|
}
|
|
|
|
// Shallow copy all the properties
|
|
for k, v := range iniSection.KeysHash() {
|
|
raw[strings.ToLower(k)] = v
|
|
}
|
|
|
|
definition.Raw = raw
|
|
definition.Selector = sectionStr
|
|
|
|
if err := definition.normalize(); err != nil {
|
|
return nil, fmt.Errorf("normalization error: %w", err)
|
|
}
|
|
|
|
editorConfig.Definitions = append(editorConfig.Definitions, definition)
|
|
}
|
|
|
|
return editorConfig, nil
|
|
}
|
|
|
|
// GetDefinitionForFilename returns a definition for the given filename.
|
|
//
|
|
// The result is a merge of the selectors that matched the file.
|
|
// The last section has preference over the priors.
|
|
func (e *Editorconfig) GetDefinitionForFilename(name string) (*Definition, error) {
|
|
def := &Definition{
|
|
Raw: make(map[string]string),
|
|
}
|
|
|
|
// The last section has preference over the priors.
|
|
for i := len(e.Definitions) - 1; i >= 0; i-- {
|
|
actualDef := e.Definitions[i]
|
|
selector := actualDef.Selector
|
|
|
|
if !strings.HasPrefix(selector, "/") {
|
|
if strings.ContainsRune(selector, '/') {
|
|
selector = "/" + selector
|
|
} else {
|
|
selector = "/**/" + selector
|
|
}
|
|
}
|
|
|
|
if !strings.HasPrefix(name, "/") {
|
|
if runtime.GOOS != "windows" {
|
|
name = "/" + name
|
|
} else {
|
|
name = "\\" + name
|
|
}
|
|
}
|
|
|
|
ok, err := e.FnmatchCase(selector, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if ok {
|
|
def.merge(actualDef)
|
|
}
|
|
}
|
|
|
|
return def, nil
|
|
}
|
|
|
|
// FnmatchCase calls the matcher from the config's parser or the vanilla's.
|
|
func (e *Editorconfig) FnmatchCase(selector string, filename string) (bool, error) {
|
|
if e.config != nil && e.config.Parser != nil {
|
|
return e.config.Parser.FnmatchCase(selector, filename)
|
|
}
|
|
|
|
return FnmatchCase(selector, filename)
|
|
}
|
|
|
|
// Serialize converts the Editorconfig to a slice of bytes, containing the
|
|
// content of the file in the INI format.
|
|
func (e *Editorconfig) Serialize() ([]byte, error) {
|
|
buffer := bytes.NewBuffer(nil)
|
|
|
|
err := e.Write(buffer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot write into buffer: %w", err)
|
|
}
|
|
|
|
return buffer.Bytes(), nil
|
|
}
|
|
|
|
// Write writes the Editorconfig to the Writer in a compatible INI file.
|
|
func (e *Editorconfig) Write(w io.Writer) error {
|
|
iniFile := ini.Empty()
|
|
|
|
iniFile.Section(ini.DefaultSection).Comment = "https://editorconfig.org"
|
|
|
|
if e.Root {
|
|
iniFile.Section(ini.DefaultSection).Key("root").SetValue(boolToString(e.Root))
|
|
}
|
|
|
|
for _, d := range e.Definitions {
|
|
d.InsertToIniFile(iniFile)
|
|
}
|
|
|
|
_, err := iniFile.WriteTo(w)
|
|
if err != nil {
|
|
return fmt.Errorf("error writing ini file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Save saves the Editorconfig to a compatible INI file.
|
|
func (e *Editorconfig) Save(filename string) error {
|
|
f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot open file %q: %w", filename, err)
|
|
}
|
|
|
|
return e.Write(f)
|
|
}
|
|
|
|
func boolToString(b bool) string {
|
|
if b {
|
|
return "true"
|
|
}
|
|
|
|
return "false"
|
|
}
|
|
|
|
// Parse parses from a reader.
|
|
func Parse(r io.Reader) (*Editorconfig, error) {
|
|
iniFile, err := ini.Load(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot load ini file: %w", err)
|
|
}
|
|
|
|
return newEditorconfig(iniFile)
|
|
}
|
|
|
|
// ParseBytes parses from a slice of bytes.
|
|
//
|
|
// Deprecated: use Parse instead.
|
|
func ParseBytes(data []byte) (*Editorconfig, error) {
|
|
iniFile, err := ini.Load(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot load ini file: %w", err)
|
|
}
|
|
|
|
return newEditorconfig(iniFile)
|
|
}
|
|
|
|
// ParseFile parses from a file.
|
|
//
|
|
// Deprecated: use Parse instead.
|
|
func ParseFile(path string) (*Editorconfig, error) {
|
|
iniFile, err := ini.Load(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot load ini file: %w", err)
|
|
}
|
|
|
|
return newEditorconfig(iniFile)
|
|
}
|
|
|
|
// GetDefinitionForFilename given a filename, searches
|
|
// for .editorconfig files, starting from the file folder,
|
|
// walking through the previous folders, until it reaches a
|
|
// folder with `root = true`, and returns the right editorconfig
|
|
// definition for the given file.
|
|
func GetDefinitionForFilename(filename string) (*Definition, error) {
|
|
config := new(Config)
|
|
|
|
return config.Load(filename)
|
|
}
|
|
|
|
// GetDefinitionForFilenameWithConfigname given a filename and a configname,
|
|
// searches for configname files, starting from the file folder,
|
|
// walking through the previous folders, until it reaches a
|
|
// folder with `root = true`, and returns the right editorconfig
|
|
// definition for the given file.
|
|
func GetDefinitionForFilenameWithConfigname(filename string, configname string) (*Definition, error) {
|
|
config := &Config{
|
|
Name: configname,
|
|
}
|
|
|
|
return config.Load(filename)
|
|
}
|