config.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. package config
  2. import (
  3. "bytes"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "reflect"
  8. "strings"
  9. "github.com/BurntSushi/toml"
  10. "golang.org/x/tools/go/analysis"
  11. )
  12. var Analyzer = &analysis.Analyzer{
  13. Name: "config",
  14. Doc: "loads configuration for the current package tree",
  15. Run: func(pass *analysis.Pass) (interface{}, error) {
  16. if len(pass.Files) == 0 {
  17. cfg := DefaultConfig
  18. return &cfg, nil
  19. }
  20. cache, err := os.UserCacheDir()
  21. if err != nil {
  22. cache = ""
  23. }
  24. var path string
  25. for _, f := range pass.Files {
  26. p := pass.Fset.PositionFor(f.Pos(), true).Filename
  27. // FIXME(dh): using strings.HasPrefix isn't technically
  28. // correct, but it should be good enough for now.
  29. if cache != "" && strings.HasPrefix(p, cache) {
  30. // File in the build cache of the standard Go build system
  31. continue
  32. }
  33. path = p
  34. break
  35. }
  36. if path == "" {
  37. // The package only consists of generated files.
  38. cfg := DefaultConfig
  39. return &cfg, nil
  40. }
  41. dir := filepath.Dir(path)
  42. cfg, err := Load(dir)
  43. if err != nil {
  44. return nil, fmt.Errorf("error loading staticcheck.conf: %s", err)
  45. }
  46. return &cfg, nil
  47. },
  48. RunDespiteErrors: true,
  49. ResultType: reflect.TypeOf((*Config)(nil)),
  50. }
  51. func For(pass *analysis.Pass) *Config {
  52. return pass.ResultOf[Analyzer].(*Config)
  53. }
  54. func mergeLists(a, b []string) []string {
  55. out := make([]string, 0, len(a)+len(b))
  56. for _, el := range b {
  57. if el == "inherit" {
  58. out = append(out, a...)
  59. } else {
  60. out = append(out, el)
  61. }
  62. }
  63. return out
  64. }
  65. func normalizeList(list []string) []string {
  66. if len(list) > 1 {
  67. nlist := make([]string, 0, len(list))
  68. nlist = append(nlist, list[0])
  69. for i, el := range list[1:] {
  70. if el != list[i] {
  71. nlist = append(nlist, el)
  72. }
  73. }
  74. list = nlist
  75. }
  76. for _, el := range list {
  77. if el == "inherit" {
  78. // This should never happen, because the default config
  79. // should not use "inherit"
  80. panic(`unresolved "inherit"`)
  81. }
  82. }
  83. return list
  84. }
  85. func (cfg Config) Merge(ocfg Config) Config {
  86. if ocfg.Checks != nil {
  87. cfg.Checks = mergeLists(cfg.Checks, ocfg.Checks)
  88. }
  89. if ocfg.Initialisms != nil {
  90. cfg.Initialisms = mergeLists(cfg.Initialisms, ocfg.Initialisms)
  91. }
  92. if ocfg.DotImportWhitelist != nil {
  93. cfg.DotImportWhitelist = mergeLists(cfg.DotImportWhitelist, ocfg.DotImportWhitelist)
  94. }
  95. if ocfg.HTTPStatusCodeWhitelist != nil {
  96. cfg.HTTPStatusCodeWhitelist = mergeLists(cfg.HTTPStatusCodeWhitelist, ocfg.HTTPStatusCodeWhitelist)
  97. }
  98. return cfg
  99. }
  100. type Config struct {
  101. // TODO(dh): this implementation makes it impossible for external
  102. // clients to add their own checkers with configuration. At the
  103. // moment, we don't really care about that; we don't encourage
  104. // that people use this package. In the future, we may. The
  105. // obvious solution would be using map[string]interface{}, but
  106. // that's obviously subpar.
  107. Checks []string `toml:"checks"`
  108. Initialisms []string `toml:"initialisms"`
  109. DotImportWhitelist []string `toml:"dot_import_whitelist"`
  110. HTTPStatusCodeWhitelist []string `toml:"http_status_code_whitelist"`
  111. }
  112. func (c Config) String() string {
  113. buf := &bytes.Buffer{}
  114. fmt.Fprintf(buf, "Checks: %#v\n", c.Checks)
  115. fmt.Fprintf(buf, "Initialisms: %#v\n", c.Initialisms)
  116. fmt.Fprintf(buf, "DotImportWhitelist: %#v\n", c.DotImportWhitelist)
  117. fmt.Fprintf(buf, "HTTPStatusCodeWhitelist: %#v", c.HTTPStatusCodeWhitelist)
  118. return buf.String()
  119. }
  120. var DefaultConfig = Config{
  121. Checks: []string{"all", "-ST1000", "-ST1003", "-ST1016"},
  122. Initialisms: []string{
  123. "ACL", "API", "ASCII", "CPU", "CSS", "DNS",
  124. "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID",
  125. "IP", "JSON", "QPS", "RAM", "RPC", "SLA",
  126. "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL",
  127. "UDP", "UI", "GID", "UID", "UUID", "URI",
  128. "URL", "UTF8", "VM", "XML", "XMPP", "XSRF",
  129. "XSS", "SIP", "RTP",
  130. },
  131. DotImportWhitelist: []string{},
  132. HTTPStatusCodeWhitelist: []string{"200", "400", "404", "500"},
  133. }
  134. const configName = "staticcheck.conf"
  135. func parseConfigs(dir string) ([]Config, error) {
  136. var out []Config
  137. // TODO(dh): consider stopping at the GOPATH/module boundary
  138. for dir != "" {
  139. f, err := os.Open(filepath.Join(dir, configName))
  140. if os.IsNotExist(err) {
  141. ndir := filepath.Dir(dir)
  142. if ndir == dir {
  143. break
  144. }
  145. dir = ndir
  146. continue
  147. }
  148. if err != nil {
  149. return nil, err
  150. }
  151. var cfg Config
  152. _, err = toml.DecodeReader(f, &cfg)
  153. f.Close()
  154. if err != nil {
  155. return nil, err
  156. }
  157. out = append(out, cfg)
  158. ndir := filepath.Dir(dir)
  159. if ndir == dir {
  160. break
  161. }
  162. dir = ndir
  163. }
  164. out = append(out, DefaultConfig)
  165. if len(out) < 2 {
  166. return out, nil
  167. }
  168. for i := 0; i < len(out)/2; i++ {
  169. out[i], out[len(out)-1-i] = out[len(out)-1-i], out[i]
  170. }
  171. return out, nil
  172. }
  173. func mergeConfigs(confs []Config) Config {
  174. if len(confs) == 0 {
  175. // This shouldn't happen because we always have at least a
  176. // default config.
  177. panic("trying to merge zero configs")
  178. }
  179. if len(confs) == 1 {
  180. return confs[0]
  181. }
  182. conf := confs[0]
  183. for _, oconf := range confs[1:] {
  184. conf = conf.Merge(oconf)
  185. }
  186. return conf
  187. }
  188. func Load(dir string) (Config, error) {
  189. confs, err := parseConfigs(dir)
  190. if err != nil {
  191. return Config{}, err
  192. }
  193. conf := mergeConfigs(confs)
  194. conf.Checks = normalizeList(conf.Checks)
  195. conf.Initialisms = normalizeList(conf.Initialisms)
  196. conf.DotImportWhitelist = normalizeList(conf.DotImportWhitelist)
  197. conf.HTTPStatusCodeWhitelist = normalizeList(conf.HTTPStatusCodeWhitelist)
  198. return conf, nil
  199. }