You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

editorconfig.go 7.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. // Package editorconfig can be used to parse and generate editorconfig files.
  2. // For more information about editorconfig, see http://editorconfig.org/
  3. package editorconfig
  4. import (
  5. "bytes"
  6. "io/ioutil"
  7. "os"
  8. "path/filepath"
  9. "regexp"
  10. "strconv"
  11. "strings"
  12. "gopkg.in/ini.v1"
  13. )
  14. const (
  15. ConfigNameDefault = ".editorconfig"
  16. )
  17. // IndentStyle possible values
  18. const (
  19. IndentStyleTab = "tab"
  20. IndentStyleSpaces = "space"
  21. )
  22. // EndOfLine possible values
  23. const (
  24. EndOfLineLf = "lf"
  25. EndOfLineCr = "cr"
  26. EndOfLineCrLf = "crlf"
  27. )
  28. // Charset possible values
  29. const (
  30. CharsetLatin1 = "latin1"
  31. CharsetUTF8 = "utf-8"
  32. CharsetUTF16BE = "utf-16be"
  33. CharsetUTF16LE = "utf-16le"
  34. )
  35. // Definition represents a definition inside the .editorconfig file.
  36. // E.g. a section of the file.
  37. // The definition is composed of the selector ("*", "*.go", "*.{js.css}", etc),
  38. // plus the properties of the selected files.
  39. type Definition struct {
  40. Selector string `ini:"-" json:"-"`
  41. Charset string `ini:"charset" json:"charset,omitempty"`
  42. IndentStyle string `ini:"indent_style" json:"indent_style,omitempty"`
  43. IndentSize string `ini:"indent_size" json:"indent_size,omitempty"`
  44. TabWidth int `ini:"tab_width" json:"tab_width,omitempty"`
  45. EndOfLine string `ini:"end_of_line" json:"end_of_line,omitempty"`
  46. TrimTrailingWhitespace bool `ini:"trim_trailing_whitespace" json:"trim_trailing_whitespace,omitempty"`
  47. InsertFinalNewline bool `ini:"insert_final_newline" json:"insert_final_newline,omitempty"`
  48. Raw map[string]string `ini:"-" json:"-"`
  49. }
  50. // Editorconfig represents a .editorconfig file.
  51. // It is composed by a "root" property, plus the definitions defined in the
  52. // file.
  53. type Editorconfig struct {
  54. Root bool
  55. Definitions []*Definition
  56. }
  57. // ParseBytes parses from a slice of bytes.
  58. func ParseBytes(data []byte) (*Editorconfig, error) {
  59. iniFile, err := ini.Load(data)
  60. if err != nil {
  61. return nil, err
  62. }
  63. editorConfig := &Editorconfig{}
  64. editorConfig.Root = iniFile.Section(ini.DEFAULT_SECTION).Key("root").MustBool(false)
  65. for _, sectionStr := range iniFile.SectionStrings() {
  66. if sectionStr == ini.DEFAULT_SECTION {
  67. continue
  68. }
  69. var (
  70. iniSection = iniFile.Section(sectionStr)
  71. definition = &Definition{}
  72. raw = make(map[string]string)
  73. )
  74. err := iniSection.MapTo(&definition)
  75. if err != nil {
  76. return nil, err
  77. }
  78. // tab_width defaults to indent_size:
  79. // https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#tab_width
  80. if definition.TabWidth <= 0 {
  81. if num, err := strconv.Atoi(definition.IndentSize); err == nil {
  82. definition.TabWidth = num
  83. }
  84. }
  85. // Shallow copy all properties
  86. for k, v := range iniSection.KeysHash() {
  87. raw[k] = v
  88. }
  89. definition.Selector = sectionStr
  90. definition.Raw = raw
  91. editorConfig.Definitions = append(editorConfig.Definitions, definition)
  92. }
  93. return editorConfig, nil
  94. }
  95. // ParseFile parses from a file.
  96. func ParseFile(f string) (*Editorconfig, error) {
  97. data, err := ioutil.ReadFile(f)
  98. if err != nil {
  99. return nil, err
  100. }
  101. return ParseBytes(data)
  102. }
  103. var (
  104. regexpBraces = regexp.MustCompile("{.*}")
  105. )
  106. func filenameMatches(pattern, name string) bool {
  107. // basic match
  108. matched, _ := filepath.Match(pattern, name)
  109. if matched {
  110. return true
  111. }
  112. // foo/bar/main.go should match main.go
  113. matched, _ = filepath.Match(pattern, filepath.Base(name))
  114. if matched {
  115. return true
  116. }
  117. // foo should match foo/main.go
  118. matched, _ = filepath.Match(filepath.Join(pattern, "*"), name)
  119. if matched {
  120. return true
  121. }
  122. // *.{js,go} should match main.go
  123. if str := regexpBraces.FindString(pattern); len(str) > 0 {
  124. // remote initial "{" and final "}"
  125. str = strings.TrimPrefix(str, "{")
  126. str = strings.TrimSuffix(str, "}")
  127. // testing for empty brackets: "{}"
  128. if len(str) == 0 {
  129. patt := regexpBraces.ReplaceAllString(pattern, "*")
  130. matched, _ = filepath.Match(patt, filepath.Base(name))
  131. return matched
  132. }
  133. for _, patt := range strings.Split(str, ",") {
  134. patt = regexpBraces.ReplaceAllString(pattern, patt)
  135. matched, _ = filepath.Match(patt, filepath.Base(name))
  136. if matched {
  137. return true
  138. }
  139. }
  140. }
  141. return false
  142. }
  143. func (d *Definition) merge(md *Definition) {
  144. if len(d.Charset) == 0 {
  145. d.Charset = md.Charset
  146. }
  147. if len(d.IndentStyle) == 0 {
  148. d.IndentStyle = md.IndentStyle
  149. }
  150. if len(d.IndentSize) == 0 {
  151. d.IndentSize = md.IndentSize
  152. }
  153. if d.TabWidth <= 0 {
  154. d.TabWidth = md.TabWidth
  155. }
  156. if len(d.EndOfLine) == 0 {
  157. d.EndOfLine = md.EndOfLine
  158. }
  159. if !d.TrimTrailingWhitespace {
  160. d.TrimTrailingWhitespace = md.TrimTrailingWhitespace
  161. }
  162. if !d.InsertFinalNewline {
  163. d.InsertFinalNewline = md.InsertFinalNewline
  164. }
  165. for k, v := range md.Raw {
  166. if _, ok := d.Raw[k]; !ok {
  167. d.Raw[k] = v
  168. }
  169. }
  170. }
  171. func (d *Definition) InsertToIniFile(iniFile *ini.File) {
  172. iniSec := iniFile.Section(d.Selector)
  173. for k, v := range d.Raw {
  174. iniSec.Key(k).SetValue(v)
  175. }
  176. }
  177. // GetDefinitionForFilename returns a definition for the given filename.
  178. // The result is a merge of the selectors that matched the file.
  179. // The last section has preference over the priors.
  180. func (e *Editorconfig) GetDefinitionForFilename(name string) *Definition {
  181. def := &Definition{}
  182. def.Raw = make(map[string]string)
  183. for i := len(e.Definitions) - 1; i >= 0; i-- {
  184. actualDef := e.Definitions[i]
  185. if filenameMatches(actualDef.Selector, name) {
  186. def.merge(actualDef)
  187. }
  188. }
  189. return def
  190. }
  191. func boolToString(b bool) string {
  192. if b {
  193. return "true"
  194. }
  195. return "false"
  196. }
  197. // Serialize converts the Editorconfig to a slice of bytes, containing the
  198. // content of the file in the INI format.
  199. func (e *Editorconfig) Serialize() ([]byte, error) {
  200. var (
  201. iniFile = ini.Empty()
  202. buffer = bytes.NewBuffer(nil)
  203. )
  204. iniFile.Section(ini.DEFAULT_SECTION).Comment = "http://editorconfig.org"
  205. if e.Root {
  206. iniFile.Section(ini.DEFAULT_SECTION).Key("root").SetValue(boolToString(e.Root))
  207. }
  208. for _, d := range e.Definitions {
  209. d.InsertToIniFile(iniFile)
  210. }
  211. _, err := iniFile.WriteTo(buffer)
  212. if err != nil {
  213. return nil, err
  214. }
  215. return buffer.Bytes(), nil
  216. }
  217. // Save saves the Editorconfig to a compatible INI file.
  218. func (e *Editorconfig) Save(filename string) error {
  219. data, err := e.Serialize()
  220. if err != nil {
  221. return err
  222. }
  223. return ioutil.WriteFile(filename, data, 0666)
  224. }
  225. // GetDefinitionForFilename given a filename, searches
  226. // for .editorconfig files, starting from the file folder,
  227. // walking through the previous folders, until it reaches a
  228. // folder with `root = true`, and returns the right editorconfig
  229. // definition for the given file.
  230. func GetDefinitionForFilename(filename string) (*Definition, error) {
  231. return GetDefinitionForFilenameWithConfigname(filename, ConfigNameDefault)
  232. }
  233. func GetDefinitionForFilenameWithConfigname(filename string, configname string) (*Definition, error) {
  234. abs, err := filepath.Abs(filename)
  235. if err != nil {
  236. return nil, err
  237. }
  238. definition := &Definition{}
  239. definition.Raw = make(map[string]string)
  240. dir := abs
  241. for dir != filepath.Dir(dir) {
  242. dir = filepath.Dir(dir)
  243. ecFile := filepath.Join(dir, configname)
  244. if _, err := os.Stat(ecFile); os.IsNotExist(err) {
  245. continue
  246. }
  247. ec, err := ParseFile(ecFile)
  248. if err != nil {
  249. return nil, err
  250. }
  251. definition.merge(ec.GetDefinitionForFilename(filename))
  252. if ec.Root {
  253. break
  254. }
  255. }
  256. return definition, nil
  257. }