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 8.0KB

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