editorconfig.go 7.2 KB

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