test: Rename the reflowed comments test to make it easier to find
This makes running one-of executions of this a bit easier.
This commit is contained in:
316
test/reflowed-comments.go
Normal file
316
test/reflowed-comments.go
Normal file
@@ -0,0 +1,316 @@
|
||||
// Mgmt
|
||||
// Copyright (C) James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// XXX: consider using the https://pkg.go.dev/go/doc parser instead and also
|
||||
// checking the other fields like the package doc string.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
const (
|
||||
// Debug specifies if we want to be more verbose.
|
||||
Debug = false
|
||||
|
||||
// CommentPrefix is the prefix we look for at the beginning of comments.
|
||||
CommentPrefix = "// "
|
||||
|
||||
// CommentPrefixTab is the prefix we look for at the beginning of
|
||||
// comments when we have a tab instead of a space.
|
||||
CommentPrefixTab = "//\t"
|
||||
|
||||
// CommentGolangPrefix is the magic golang prefix that tools use.
|
||||
CommentGolangPrefix = "//go:"
|
||||
|
||||
// CommentMultilinePrefix is what a multiline comment starts with.
|
||||
CommentMultilinePrefix = "/*"
|
||||
|
||||
// StandardWidth is 80 chars. If something sneaks past, this is okay,
|
||||
// but there is no excuse if docstrings don't wrap and reflow to this.
|
||||
StandardWidth = 80
|
||||
|
||||
// maxLength is the effective maximum length of each comment line.
|
||||
maxLength = StandardWidth - len(CommentPrefix)
|
||||
)
|
||||
|
||||
var (
|
||||
// commentPrefixTrimmed is a trimmed copy of the CommentPrefix constant.
|
||||
commentPrefixTrimmed = strings.TrimRightFunc(CommentPrefix, unicode.IsSpace)
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Fprintf(os.Stderr, "usage: ./%s <file>\n", os.Args[0])
|
||||
os.Exit(2)
|
||||
return
|
||||
}
|
||||
|
||||
filename := os.Args[1]
|
||||
if filename == "" {
|
||||
fmt.Fprintf(os.Stderr, "filename is empty\n")
|
||||
os.Exit(2)
|
||||
return
|
||||
}
|
||||
|
||||
if err := Check(filename); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed with: %+v\n", err)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check returns the comment checker on an individual filename.
|
||||
func Check(filename string) error {
|
||||
if Debug {
|
||||
log.Printf("filename: %s", filename)
|
||||
}
|
||||
|
||||
fset := token.NewFileSet()
|
||||
|
||||
// f is a: https://golang.org/pkg/go/ast/#File
|
||||
f, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// XXX: f.Doc // CommentGroup
|
||||
// XXX: f.Comments []*CommentGroup // list of all comments in the source file
|
||||
// XXX: f.Decls []Decl // top-level declarations; or nil
|
||||
|
||||
for _, node := range f.Decls {
|
||||
var doc *ast.CommentGroup
|
||||
|
||||
// TODO: move to type switch ?
|
||||
if x, ok := node.(*ast.FuncDecl); ok {
|
||||
doc = x.Doc
|
||||
} else if x, ok := node.(*ast.GenDecl); ok {
|
||||
switch x.Tok {
|
||||
case token.IMPORT: // i don't think this is needed, aiui
|
||||
case token.VAR:
|
||||
// TODO: recurse into https://golang.org/pkg/go/ast/#ValueSpec
|
||||
doc = x.Doc
|
||||
case token.CONST:
|
||||
// TODO: recurse into https://golang.org/pkg/go/ast/#ValueSpec
|
||||
doc = x.Doc
|
||||
case token.TYPE: // struct, usually
|
||||
// TODO: recurse into x.Tok (eg: TypeSpec.Doc and so on)
|
||||
doc = x.Doc
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
if doc == nil { // we got nothing
|
||||
continue
|
||||
}
|
||||
|
||||
pos := doc.Pos()
|
||||
ff := fset.File(pos)
|
||||
items := strings.Split(ff.Name(), "/")
|
||||
if len(items) == 0 {
|
||||
return fmt.Errorf("file name is empty")
|
||||
}
|
||||
name := items[len(items)-1]
|
||||
ident := fmt.Sprintf("%s:%d", name, ff.Line(pos))
|
||||
|
||||
block := []string{}
|
||||
for _, comment := range doc.List {
|
||||
if comment == nil {
|
||||
continue
|
||||
}
|
||||
s := comment.Text
|
||||
|
||||
// TODO: how do we deal with multiline comments?
|
||||
if strings.HasPrefix(s, CommentMultilinePrefix) {
|
||||
break // skip to the end of this block
|
||||
}
|
||||
|
||||
// skip the magic compiler comments
|
||||
if strings.HasPrefix(s, CommentGolangPrefix) {
|
||||
break // skip to the end of this block
|
||||
}
|
||||
|
||||
// Allow a comment prefix that starts with a space or
|
||||
// one that starts with a tab. (Common for code blocks!)
|
||||
if s != commentPrefixTrimmed && !strings.HasPrefix(s, CommentPrefix) && !strings.HasPrefix(s, CommentPrefixTab) {
|
||||
return fmt.Errorf("location (%s) missing comment prefix, has: %s", ident, s)
|
||||
}
|
||||
if s == commentPrefixTrimmed { // blank lines
|
||||
s = ""
|
||||
}
|
||||
|
||||
if strings.HasPrefix(s, CommentPrefix) {
|
||||
s = strings.TrimPrefix(s, CommentPrefix)
|
||||
} else if strings.HasPrefix(s, CommentPrefixTab) {
|
||||
s = strings.TrimPrefix(s, CommentPrefixTab)
|
||||
}
|
||||
|
||||
block = append(block, s)
|
||||
}
|
||||
|
||||
if err := IsWrappedProperly(block, maxLength); err != nil {
|
||||
m := strings.Join(block, "\n")
|
||||
msg := filename + " " + strings.Repeat(".", maxLength-len(filename+" "+"V")) + fmt.Sprintf("V\n%+v\n", m)
|
||||
fmt.Fprintf(os.Stderr, msg)
|
||||
return fmt.Errorf("block (%s) failed: %+v", ident, err) // TODO: errwrap ?
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsWrappedProperly returns whether a block of lines are correctly wrapped.
|
||||
// This also checks for a few related formatting situations. The list of lines
|
||||
// should not have trailing newline characters present. While you most surely
|
||||
// will want to ensure a maximum length of 80 characters, you'll want to
|
||||
// subtract the comment prefix length from this value so that the final result
|
||||
// is correctly wrapped.
|
||||
func IsWrappedProperly(lines []string, length int) error {
|
||||
blank := false
|
||||
previous := length // default to full
|
||||
for i, line := range lines {
|
||||
lineno := i + 1 // human indexing
|
||||
|
||||
// Allow a maximum of one blank line in a row.
|
||||
if line == "" {
|
||||
if blank {
|
||||
return fmt.Errorf("line %d was a sequential blank line", lineno)
|
||||
}
|
||||
blank = true
|
||||
previous = length // reset
|
||||
continue
|
||||
}
|
||||
blank = false
|
||||
|
||||
if line != strings.TrimSpace(line) {
|
||||
return fmt.Errorf("line %d wasn't trimmed properly: %s", lineno, line)
|
||||
}
|
||||
|
||||
if strings.Contains(line, " ") { // double spaces
|
||||
return fmt.Errorf("line %d contained multiple spaces", lineno)
|
||||
}
|
||||
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 {
|
||||
//continue // should not happen with above check
|
||||
return fmt.Errorf("line %d had an unexpected empty list of fields", lineno)
|
||||
}
|
||||
|
||||
lastIndex := len(fields) - 1
|
||||
lastChunk := fields[lastIndex]
|
||||
beginning := strings.Join(fields[0:lastIndex], " ")
|
||||
// !strings.Contains(lastChunk, " ") // redundant
|
||||
|
||||
// Either of these conditions is a reason we can skip this test.
|
||||
skip1 := IsSpecialLine(line)
|
||||
skip2 := (len(beginning) <= length && IsSpecialLine(lastChunk))
|
||||
|
||||
if len(line) > length && (!skip1) && (!skip2) {
|
||||
return fmt.Errorf("line %d is too long", lineno)
|
||||
}
|
||||
|
||||
// If we have a new start word, then we don't need to reflow it
|
||||
// back to the previous line, and if not, then we check the fit.
|
||||
if !IsNewStart(fields[0]) && previous+len(" ")+len(fields[0]) <= length {
|
||||
return fmt.Errorf("line %d is not reflowed properly", lineno)
|
||||
}
|
||||
|
||||
previous = len(line) // prepare for next iteration
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsNewStart returns true if the input word is one which is a valid start to a
|
||||
// new line. This means that it doesn't need to get reflowed into the previous
|
||||
// line. You should pass in the word without any surrounding whitespace.
|
||||
func IsNewStart(word string) bool {
|
||||
if word == "TODO:" {
|
||||
return true
|
||||
}
|
||||
if word == "FIXME:" {
|
||||
return true
|
||||
}
|
||||
if word == "XXX:" {
|
||||
return true
|
||||
}
|
||||
|
||||
if word == "NOTE:" {
|
||||
return true
|
||||
}
|
||||
if word == "Eg:" || word == "Example:" { // might as well
|
||||
return true
|
||||
}
|
||||
|
||||
if word == "https://" || word == "http://" { // for the occasional docs
|
||||
return true
|
||||
}
|
||||
|
||||
if word == "*" { // bullets
|
||||
return true
|
||||
}
|
||||
if IsNumberBullet(word) {
|
||||
return true
|
||||
}
|
||||
if IsCodeBlock(word) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSpecialLine allows lines that contain an entire special sentence to be
|
||||
// allowed without breaking the reflow rules.
|
||||
func IsSpecialLine(line string) bool {
|
||||
fields := strings.Fields(line)
|
||||
|
||||
// If it's a URL and it doesn't contain any words after the end of it...
|
||||
if strings.HasPrefix(line, "https://") && len(fields) == 1 {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(line, "http://") && len(fields) == 1 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsNumberBullet returns true if the word starts with a number bullet like 42).
|
||||
func IsNumberBullet(word string) bool {
|
||||
matched, err := regexp.MatchString(`[0-9]+\)*`, word)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return matched
|
||||
}
|
||||
|
||||
// IsCodeBlock returns true if the word starts with a code block backtick.
|
||||
func IsCodeBlock(word string) bool {
|
||||
if strings.HasPrefix(word, "`") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user