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