pkgsrc/pkgtools/pkglint/files/line.go
rillig 3998949c17 Updated pkglint to 5.3.4
Changes since 5.3.3:

* Added some unit tests
* Fixed the Makefile parser to recognize seldomly-used variable modifiers
  like :S///S/// without intermediate colon or :ts\n
* Cleaned up some unit tests
* Combined diagnostics that span multiple lines into single-line ones
2016-01-27 21:55:50 +00:00

294 lines
8.8 KiB
Go

package main
// When files are read in by pkglint, they are interpreted in terms of
// lines. For Makefiles, line continuations are handled properly, allowing
// multiple raw lines to end in a single logical line. For other files
// there is a 1:1 translation.
//
// A difference between the raw and the logical lines is that the
// raw lines include the line end sequence, whereas the logical lines
// do not.
//
// Some methods allow modification of the raw lines contained in the
// logical line, but leave the “text” field untouched. These methods are
// used in the --autofix mode.
import (
"fmt"
"io"
"path"
"strconv"
"strings"
)
type RawLine struct {
Lineno int
orignl string
textnl string
}
func (rline *RawLine) String() string {
return strconv.Itoa(rline.Lineno) + ":" + rline.textnl
}
type Line struct {
Fname string
firstLine int32 // Zero means not applicable, -1 means EOF
lastLine int32 // Usually the same as firstLine, may differ in Makefiles
Text string
raw []*RawLine
changed bool
before []*RawLine
after []*RawLine
autofixMessage *string
}
func NewLine(fname string, lineno int, text string, rawLines []*RawLine) *Line {
return NewLineMulti(fname, lineno, lineno, text, rawLines)
}
// NewLineMulti is for logical Makefile lines that end with backslash.
func NewLineMulti(fname string, firstLine, lastLine int, text string, rawLines []*RawLine) *Line {
return &Line{fname, int32(firstLine), int32(lastLine), text, rawLines, false, nil, nil, nil}
}
// NewLineEOF creates a dummy line for logging, with the “line number” EOF.
func NewLineEOF(fname string) *Line {
return NewLineMulti(fname, -1, 0, "", nil)
}
func (line *Line) rawLines() []*RawLine {
switch { // prevent inlining
}
return append(append(append([]*RawLine(nil), line.before...), line.raw...), line.after...)
}
func (line *Line) linenos() string {
switch {
case line.firstLine == -1:
return "EOF"
case line.firstLine == 0:
return ""
case line.firstLine == line.lastLine:
return strconv.Itoa(int(line.firstLine))
default:
return strconv.Itoa(int(line.firstLine)) + "--" + strconv.Itoa(int(line.lastLine))
}
}
func (line *Line) ReferenceFrom(other *Line) string {
if line.Fname != other.Fname {
return cleanpath(relpath(path.Dir(other.Fname), line.Fname)) + ":" + line.linenos()
}
return "line " + line.linenos()
}
func (line *Line) IsMultiline() bool {
return line.firstLine > 0 && line.firstLine != line.lastLine
}
func (line *Line) printSource(out io.Writer) {
if G.opts.PrintSource {
io.WriteString(out, "\n")
for _, rawLine := range line.rawLines() {
if rawLine.textnl != rawLine.orignl {
if rawLine.orignl != "" {
io.WriteString(out, "- "+rawLine.orignl)
}
if rawLine.textnl != "" {
io.WriteString(out, "+ "+rawLine.textnl)
}
} else {
io.WriteString(out, "> "+rawLine.orignl)
}
}
}
}
func (line *Line) Fatalf(format string, args ...interface{}) {
line.printSource(G.logErr)
Fatalf(line.Fname, line.linenos(), format, args...)
}
func (line *Line) Errorf(format string, args ...interface{}) {
line.printSource(G.logOut)
Errorf(line.Fname, line.linenos(), format, args...)
line.logAutofix()
}
func (line *Line) Error0(format string) { line.Errorf(format) }
func (line *Line) Error1(format, arg1 string) { line.Errorf(format, arg1) }
func (line *Line) Error2(format, arg1, arg2 string) { line.Errorf(format, arg1, arg2) }
func (line *Line) Warnf(format string, args ...interface{}) {
line.printSource(G.logOut)
Warnf(line.Fname, line.linenos(), format, args...)
line.logAutofix()
}
func (line *Line) Warn0(format string) { line.Warnf(format) }
func (line *Line) Warn1(format, arg1 string) { line.Warnf(format, arg1) }
func (line *Line) Warn2(format, arg1, arg2 string) { line.Warnf(format, arg1, arg2) }
func (line *Line) Notef(format string, args ...interface{}) {
line.printSource(G.logOut)
Notef(line.Fname, line.linenos(), format, args...)
line.logAutofix()
}
func (line *Line) Note0(format string) { line.Notef(format) }
func (line *Line) Note1(format, arg1 string) { line.Notef(format, arg1) }
func (line *Line) Note2(format, arg1, arg2 string) { line.Notef(format, arg1, arg2) }
func (line *Line) Debugf(format string, args ...interface{}) {
line.printSource(G.logOut)
Debugf(line.Fname, line.linenos(), format, args...)
line.logAutofix()
}
func (line *Line) Debug1(format, arg1 string) { line.Debugf(format, arg1) }
func (line *Line) Debug2(format, arg1, arg2 string) { line.Debugf(format, arg1, arg2) }
func (line *Line) String() string {
return line.Fname + ":" + line.linenos() + ": " + line.Text
}
func (line *Line) logAutofix() {
if line.autofixMessage != nil {
autofixf(line.Fname, line.linenos(), "%s", *line.autofixMessage)
line.autofixMessage = nil
}
}
func (line *Line) AutofixInsertBefore(text string) bool {
if G.opts.PrintAutofix || G.opts.Autofix {
line.before = append(line.before, &RawLine{0, "", text + "\n"})
}
return line.RememberAutofix("Inserting a line %q before this line.", text)
}
func (line *Line) AutofixInsertAfter(text string) bool {
if G.opts.PrintAutofix || G.opts.Autofix {
line.after = append(line.after, &RawLine{0, "", text + "\n"})
}
return line.RememberAutofix("Inserting a line %q after this line.", text)
}
func (line *Line) AutofixDelete() bool {
if G.opts.PrintAutofix || G.opts.Autofix {
for _, rawLine := range line.raw {
rawLine.textnl = ""
}
}
return line.RememberAutofix("Deleting this line.")
}
func (line *Line) AutofixReplace(from, to string) bool {
for _, rawLine := range line.raw {
if rawLine.Lineno != 0 {
if replaced := strings.Replace(rawLine.textnl, from, to, 1); replaced != rawLine.textnl {
if G.opts.PrintAutofix || G.opts.Autofix {
rawLine.textnl = replaced
}
return line.RememberAutofix("Replacing %q with %q.", from, to)
}
}
}
return false
}
func (line *Line) AutofixReplaceRegexp(from, to string) bool {
for _, rawLine := range line.raw {
if rawLine.Lineno != 0 {
if replaced := regcomp(from).ReplaceAllString(rawLine.textnl, to); replaced != rawLine.textnl {
if G.opts.PrintAutofix || G.opts.Autofix {
rawLine.textnl = replaced
}
return line.RememberAutofix("Replacing regular expression %q with %q.", from, to)
}
}
}
return false
}
func (line *Line) RememberAutofix(format string, args ...interface{}) (hasBeenFixed bool) {
if line.firstLine < 1 {
return false
}
line.changed = true
if G.opts.Autofix {
autofixf(line.Fname, line.linenos(), format, args...)
return true
}
if G.opts.PrintAutofix {
msg := fmt.Sprintf(format, args...)
line.autofixMessage = &msg
}
return false
}
func (line *Line) CheckAbsolutePathname(text string) {
if G.opts.DebugTrace {
defer tracecall1(text)()
}
// In the GNU coding standards, DESTDIR is defined as a (usually
// empty) prefix that can be used to install files to a different
// location from what they have been built for. Therefore
// everything following it is considered an absolute pathname.
//
// Another context where absolute pathnames usually appear is in
// assignments like "bindir=/bin".
if m, path := match1(text, `(?:^|\$[{(]DESTDIR[)}]|[\w_]+\s*=\s*)(/(?:[^"'\s]|"[^"*]"|'[^']*')*)`); m {
if matches(path, `^/\w`) {
checkwordAbsolutePathname(line, path)
}
}
}
func (line *Line) CheckLength(maxlength int) {
if len(line.Text) > maxlength {
line.Warnf("Line too long (should be no more than %d characters).", maxlength)
Explain3(
"Back in the old time, terminals with 80x25 characters were common.",
"And this is still the default size of many terminal emulators.",
"Moderately short lines also make reading easier.")
}
}
func (line *Line) CheckValidCharacters(reChar string) {
rest := regcomp(reChar).ReplaceAllString(line.Text, "")
if rest != "" {
uni := ""
for _, c := range rest {
uni += fmt.Sprintf(" %U", c)
}
line.Warn1("Line contains invalid characters (%s).", uni[1:])
}
}
func (line *Line) CheckTrailingWhitespace() {
if hasSuffix(line.Text, " ") || hasSuffix(line.Text, "\t") {
if !line.AutofixReplaceRegexp(`\s+\n$`, "\n") {
line.Note0("Trailing white-space.")
Explain2(
"When a line ends with some white-space, that space is in most cases",
"irrelevant and can be removed.")
}
}
}
func (line *Line) CheckRcsid(prefixRe, suggestedPrefix string) bool {
if G.opts.DebugTrace {
defer tracecall2(prefixRe, suggestedPrefix)()
}
if matches(line.Text, `^`+prefixRe+`\$`+`NetBSD(?::[^\$]+)?\$$`) {
return true
}
if !line.AutofixInsertBefore(suggestedPrefix + "$" + "NetBSD$") {
line.Error1("Expected %q.", suggestedPrefix+"$"+"NetBSD$")
Explain3(
"Several files in pkgsrc must contain the CVS Id, so that their",
"current version can be traced back later from a binary package.",
"This is to ensure reproducible builds, for example for finding bugs.")
}
return false
}