/tools/vendor/github.com/tetafro/godot/getters.go

https://github.com/coreos/coreos-assembler · Go · 283 lines · 216 code · 26 blank · 41 comment · 62 complexity · 1749182f761efadcfcaee6ca7ddc03d9 MD5 · raw file

  1. package godot
  2. import (
  3. "errors"
  4. "fmt"
  5. "go/ast"
  6. "go/token"
  7. "io/ioutil"
  8. "regexp"
  9. "strings"
  10. )
  11. var (
  12. errEmptyInput = errors.New("empty input")
  13. errUnsuitableInput = errors.New("unsuitable input")
  14. )
  15. // specialReplacer is a replacer for some types of special lines in comments,
  16. // which shouldn't be checked. For example, if comment ends with a block of
  17. // code it should not necessarily have a period at the end.
  18. const specialReplacer = "<godotSpecialReplacer>"
  19. type parsedFile struct {
  20. fset *token.FileSet
  21. file *ast.File
  22. lines []string
  23. }
  24. func newParsedFile(file *ast.File, fset *token.FileSet) (*parsedFile, error) {
  25. if file == nil || fset == nil || len(file.Comments) == 0 {
  26. return nil, errEmptyInput
  27. }
  28. pf := parsedFile{
  29. fset: fset,
  30. file: file,
  31. }
  32. var err error
  33. // Read original file. This is necessary for making a replacements for
  34. // inline comments. I couldn't find a better way to get original line
  35. // with code and comment without reading the file. Function `Format`
  36. // from "go/format" won't help here if the original file is not gofmt-ed.
  37. pf.lines, err = readFile(file, fset)
  38. if err != nil {
  39. return nil, fmt.Errorf("read file: %v", err)
  40. }
  41. // Dirty hack. For some cases Go generates temporary files during
  42. // compilation process if there is a cgo block in the source file. Some of
  43. // these temporary files are just copies of original source files but with
  44. // new generated comments at the top. Because of them the content differs
  45. // from AST. For some reason it differs only in golangci-lint. I failed to
  46. // find out the exact description of the process, so let's just skip files
  47. // generated by cgo.
  48. if isCgoGenerated(pf.lines) {
  49. return nil, errUnsuitableInput
  50. }
  51. // Check consistency to avoid checking slice indexes in each function
  52. lastComment := pf.file.Comments[len(pf.file.Comments)-1]
  53. if p := pf.fset.Position(lastComment.End()); len(pf.lines) < p.Line {
  54. return nil, fmt.Errorf("inconsistency between file and AST: %s", p.Filename)
  55. }
  56. return &pf, nil
  57. }
  58. // getComments extracts comments from a file.
  59. func (pf *parsedFile) getComments(scope Scope, exclude []*regexp.Regexp) []comment {
  60. var comments []comment
  61. decl := pf.getDeclarationComments(exclude)
  62. switch scope {
  63. case AllScope:
  64. // All comments
  65. comments = pf.getAllComments(exclude)
  66. case TopLevelScope:
  67. // All top level comments and comments from the inside
  68. // of top level blocks
  69. comments = append(
  70. pf.getBlockComments(exclude),
  71. pf.getTopLevelComments(exclude)...,
  72. )
  73. default:
  74. // Top level declaration comments and comments from the inside
  75. // of top level blocks
  76. comments = append(pf.getBlockComments(exclude), decl...)
  77. }
  78. // Set `decl` flag
  79. setDecl(comments, decl)
  80. return comments
  81. }
  82. // getBlockComments gets comments from the inside of top level blocks:
  83. // var (...), const (...).
  84. func (pf *parsedFile) getBlockComments(exclude []*regexp.Regexp) []comment {
  85. var comments []comment
  86. for _, decl := range pf.file.Decls {
  87. d, ok := decl.(*ast.GenDecl)
  88. if !ok {
  89. continue
  90. }
  91. // No parenthesis == no block
  92. if d.Lparen == 0 {
  93. continue
  94. }
  95. for _, c := range pf.file.Comments {
  96. if c == nil || len(c.List) == 0 {
  97. continue
  98. }
  99. // Skip comments outside this block
  100. if d.Lparen > c.Pos() || c.Pos() > d.Rparen {
  101. continue
  102. }
  103. // Skip comments that are not top-level for this block
  104. // (the block itself is top level, so comments inside this block
  105. // would be on column 2)
  106. // nolint: gomnd
  107. if pf.fset.Position(c.Pos()).Column != 2 {
  108. continue
  109. }
  110. firstLine := pf.fset.Position(c.Pos()).Line
  111. lastLine := pf.fset.Position(c.End()).Line
  112. comments = append(comments, comment{
  113. lines: pf.lines[firstLine-1 : lastLine],
  114. text: getText(c, exclude),
  115. start: pf.fset.Position(c.List[0].Slash),
  116. })
  117. }
  118. }
  119. return comments
  120. }
  121. // getTopLevelComments gets all top level comments.
  122. func (pf *parsedFile) getTopLevelComments(exclude []*regexp.Regexp) []comment {
  123. var comments []comment // nolint: prealloc
  124. for _, c := range pf.file.Comments {
  125. if c == nil || len(c.List) == 0 {
  126. continue
  127. }
  128. if pf.fset.Position(c.Pos()).Column != 1 {
  129. continue
  130. }
  131. firstLine := pf.fset.Position(c.Pos()).Line
  132. lastLine := pf.fset.Position(c.End()).Line
  133. comments = append(comments, comment{
  134. lines: pf.lines[firstLine-1 : lastLine],
  135. text: getText(c, exclude),
  136. start: pf.fset.Position(c.List[0].Slash),
  137. })
  138. }
  139. return comments
  140. }
  141. // getDeclarationComments gets top level declaration comments.
  142. func (pf *parsedFile) getDeclarationComments(exclude []*regexp.Regexp) []comment {
  143. var comments []comment // nolint: prealloc
  144. for _, decl := range pf.file.Decls {
  145. var cg *ast.CommentGroup
  146. switch d := decl.(type) {
  147. case *ast.GenDecl:
  148. cg = d.Doc
  149. case *ast.FuncDecl:
  150. cg = d.Doc
  151. }
  152. if cg == nil || len(cg.List) == 0 {
  153. continue
  154. }
  155. firstLine := pf.fset.Position(cg.Pos()).Line
  156. lastLine := pf.fset.Position(cg.End()).Line
  157. comments = append(comments, comment{
  158. lines: pf.lines[firstLine-1 : lastLine],
  159. text: getText(cg, exclude),
  160. start: pf.fset.Position(cg.List[0].Slash),
  161. })
  162. }
  163. return comments
  164. }
  165. // getAllComments gets every single comment from the file.
  166. func (pf *parsedFile) getAllComments(exclude []*regexp.Regexp) []comment {
  167. var comments []comment //nolint: prealloc
  168. for _, c := range pf.file.Comments {
  169. if c == nil || len(c.List) == 0 {
  170. continue
  171. }
  172. firstLine := pf.fset.Position(c.Pos()).Line
  173. lastLine := pf.fset.Position(c.End()).Line
  174. comments = append(comments, comment{
  175. lines: pf.lines[firstLine-1 : lastLine],
  176. start: pf.fset.Position(c.List[0].Slash),
  177. text: getText(c, exclude),
  178. })
  179. }
  180. return comments
  181. }
  182. // getText extracts text from comment. If comment is a special block
  183. // (e.g., CGO code), a block of empty lines is returned. If comment contains
  184. // special lines (e.g., tags or indented code examples), they are replaced
  185. // with `specialReplacer` to skip checks for it.
  186. // The result can be multiline.
  187. func getText(comment *ast.CommentGroup, exclude []*regexp.Regexp) (s string) {
  188. if len(comment.List) == 1 &&
  189. strings.HasPrefix(comment.List[0].Text, "/*") &&
  190. isSpecialBlock(comment.List[0].Text) {
  191. return ""
  192. }
  193. for _, c := range comment.List {
  194. text := c.Text
  195. isBlock := false
  196. if strings.HasPrefix(c.Text, "/*") {
  197. isBlock = true
  198. text = strings.TrimPrefix(text, "/*")
  199. text = strings.TrimSuffix(text, "*/")
  200. }
  201. for _, line := range strings.Split(text, "\n") {
  202. if isSpecialLine(line) {
  203. s += specialReplacer + "\n"
  204. continue
  205. }
  206. if !isBlock {
  207. line = strings.TrimPrefix(line, "//")
  208. }
  209. if matchAny(line, exclude) {
  210. s += specialReplacer + "\n"
  211. continue
  212. }
  213. s += line + "\n"
  214. }
  215. }
  216. if len(s) == 0 {
  217. return ""
  218. }
  219. return s[:len(s)-1] // trim last "\n"
  220. }
  221. // readFile reads file and returns it's lines as strings.
  222. func readFile(file *ast.File, fset *token.FileSet) ([]string, error) {
  223. fname := fset.File(file.Package)
  224. f, err := ioutil.ReadFile(fname.Name())
  225. if err != nil {
  226. return nil, err
  227. }
  228. return strings.Split(string(f), "\n"), nil
  229. }
  230. // setDecl sets `decl` flag to comments which are declaration comments.
  231. func setDecl(comments, decl []comment) {
  232. for _, d := range decl {
  233. for i, c := range comments {
  234. if d.start == c.start {
  235. comments[i].decl = true
  236. break
  237. }
  238. }
  239. }
  240. }
  241. // matchAny checks if string matches any of given regexps.
  242. func matchAny(s string, rr []*regexp.Regexp) bool {
  243. for _, re := range rr {
  244. if re.MatchString(s) {
  245. return true
  246. }
  247. }
  248. return false
  249. }
  250. func isCgoGenerated(lines []string) bool {
  251. for i := range lines {
  252. if strings.Contains(lines[i], "Code generated by cmd/cgo") {
  253. return true
  254. }
  255. }
  256. return false
  257. }