1d1d611496
Changes since 21.2.0: Files whose names ends in '~' are ignored by pkglint since they are ignored by CVS as well. Variables with name BUILDLINK_TRANSFORM.* may contain '-Wl,-rpath,' directly in commands of the form 'rm:*', just like their counterpart BUILDLINK_TRANSFORM without a package name in the variable name. Several new tests.
1260 lines
29 KiB
Go
1260 lines
29 KiB
Go
package pkglint
|
|
|
|
import (
|
|
"fmt"
|
|
"hash/crc64"
|
|
"netbsd.org/pkglint/regex"
|
|
"netbsd.org/pkglint/textproc"
|
|
"path"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type YesNoUnknown uint8
|
|
|
|
const (
|
|
no YesNoUnknown = iota
|
|
yes
|
|
unknown
|
|
)
|
|
|
|
func (ynu YesNoUnknown) String() string {
|
|
return [...]string{"no", "yes", "unknown"}[ynu]
|
|
}
|
|
|
|
// Short names for commonly used functions.
|
|
|
|
func contains(s, substr string) bool {
|
|
return strings.Contains(s, substr)
|
|
}
|
|
func hasPrefix(s, prefix string) bool {
|
|
return strings.HasPrefix(s, prefix)
|
|
}
|
|
func hasSuffix(s, suffix string) bool {
|
|
return strings.HasSuffix(s, suffix)
|
|
}
|
|
func sprintf(format string, args ...interface{}) string {
|
|
return fmt.Sprintf(format, args...)
|
|
}
|
|
func regcomp(re regex.Pattern) *regexp.Regexp {
|
|
return G.res.Compile(re)
|
|
}
|
|
func match(s string, re regex.Pattern) []string {
|
|
return G.res.Match(s, re)
|
|
}
|
|
func matches(s string, re regex.Pattern) bool {
|
|
return G.res.Matches(s, re)
|
|
}
|
|
func match1(s string, re regex.Pattern) (matched bool, m1 string) {
|
|
return G.res.Match1(s, re)
|
|
}
|
|
func match2(s string, re regex.Pattern) (matched bool, m1, m2 string) {
|
|
return G.res.Match2(s, re)
|
|
}
|
|
func match3(s string, re regex.Pattern) (matched bool, m1, m2, m3 string) {
|
|
return G.res.Match3(s, re)
|
|
}
|
|
func replaceAll(s string, re regex.Pattern, repl string) string {
|
|
return G.res.Compile(re).ReplaceAllString(s, repl)
|
|
}
|
|
func replaceAllFunc(s string, re regex.Pattern, repl func(string) string) string {
|
|
return G.res.Compile(re).ReplaceAllStringFunc(s, repl)
|
|
}
|
|
|
|
func containsStr(slice []string, s string) bool {
|
|
for _, str := range slice {
|
|
if s == str {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func mapStr(slice []string, fn func(s string) string) []string {
|
|
result := make([]string, len(slice))
|
|
for i, str := range slice {
|
|
result[i] = fn(str)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func anyStr(slice []string, fn func(s string) bool) bool {
|
|
for _, str := range slice {
|
|
if fn(str) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func filterStr(slice []string, fn func(s string) bool) []string {
|
|
result := make([]string, 0, len(slice))
|
|
for _, str := range slice {
|
|
if fn(str) {
|
|
result = append(result, str)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func invalidCharacters(s string, valid *textproc.ByteSet) string {
|
|
var unis strings.Builder
|
|
|
|
for _, r := range s {
|
|
switch {
|
|
case r == rune(byte(r)) && valid.Contains(byte(r)):
|
|
continue
|
|
case '!' <= r && r <= '~':
|
|
unis.WriteByte(' ')
|
|
unis.WriteByte(byte(r))
|
|
case r == ' ':
|
|
unis.WriteString(" space")
|
|
case r == '\t':
|
|
unis.WriteString(" tab")
|
|
default:
|
|
_, _ = fmt.Fprintf(&unis, " %U", r)
|
|
}
|
|
}
|
|
|
|
if unis.Len() == 0 {
|
|
return ""
|
|
}
|
|
return unis.String()[1:]
|
|
}
|
|
|
|
// intern returns an independent copy of the given string.
|
|
//
|
|
// It should be called when only a small substring of a large string
|
|
// is needed for the rest of the program's lifetime.
|
|
//
|
|
// All strings allocated here will stay in memory forever,
|
|
// therefore it should only be used for long-lived strings.
|
|
func intern(str string) string { return G.interner.Intern(str) }
|
|
|
|
// trimHspace returns str, with leading and trailing space (U+0020)
|
|
// and tab (U+0009) removed.
|
|
//
|
|
// It is simpler and faster than strings.TrimSpace.
|
|
func trimHspace(str string) string {
|
|
start := 0
|
|
end := len(str)
|
|
for start < end && isHspace(str[start]) {
|
|
start++
|
|
}
|
|
for start < end && isHspace(str[end-1]) {
|
|
end--
|
|
}
|
|
return str[start:end]
|
|
}
|
|
|
|
func rtrimHspace(str string) string {
|
|
end := len(str)
|
|
for end > 0 && isHspace(str[end-1]) {
|
|
end--
|
|
}
|
|
return str[:end]
|
|
}
|
|
|
|
// trimCommon returns the middle portion of the given strings that differs.
|
|
func trimCommon(a, b string) (string, string) {
|
|
// trim common prefix
|
|
for len(a) > 0 && len(b) > 0 && a[0] == b[0] {
|
|
a = a[1:]
|
|
b = b[1:]
|
|
}
|
|
|
|
// trim common suffix
|
|
for len(a) > 0 && len(b) > 0 && a[len(a)-1] == b[len(b)-1] {
|
|
a = a[:len(a)-1]
|
|
b = b[:len(b)-1]
|
|
}
|
|
|
|
return a, b
|
|
}
|
|
|
|
func replaceOnce(s, from, to string) (ok bool, replaced string) {
|
|
|
|
index := strings.Index(s, from)
|
|
if index != -1 && index == strings.LastIndex(s, from) {
|
|
return true, s[:index] + to + s[index+len(from):]
|
|
}
|
|
return false, s
|
|
}
|
|
|
|
func isHspace(ch byte) bool {
|
|
return ch == ' ' || ch == '\t'
|
|
}
|
|
|
|
func condStr(cond bool, a, b string) string {
|
|
if cond {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func condInt(cond bool, trueValue, falseValue int) int {
|
|
if cond {
|
|
return trueValue
|
|
}
|
|
return falseValue
|
|
}
|
|
|
|
func keysJoined(m map[string]bool) string {
|
|
return strings.Join(keysSorted(m), " ")
|
|
}
|
|
|
|
func keysSorted(m map[string]bool) []string {
|
|
var keys []string
|
|
for key := range m {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func copyStringMkLine(m map[string]*MkLine) map[string]*MkLine {
|
|
c := make(map[string]*MkLine, len(m))
|
|
for k, v := range m {
|
|
c[k] = v
|
|
}
|
|
return c
|
|
}
|
|
|
|
func forEachStringMkLine(m map[string]*MkLine, action func(s string, mkline *MkLine)) {
|
|
var keys []string
|
|
for key := range m {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
for _, key := range keys {
|
|
action(key, m[key])
|
|
}
|
|
}
|
|
|
|
func imax(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func imin(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// assertNil ensures that the given error is nil.
|
|
//
|
|
// Contrary to other diagnostics, the format should not end in a period
|
|
// since it is followed by the error.
|
|
//
|
|
// Other than Assertf, this method does not require any comparison operator in the calling code.
|
|
// This makes it possible to get 100% branch coverage for cases that "really can never fail".
|
|
func assertNil(err error, format string, args ...interface{}) {
|
|
if err != nil {
|
|
panic("Pkglint internal error: " + sprintf(format, args...) + ": " + err.Error())
|
|
}
|
|
}
|
|
|
|
func assertNotNil(obj interface{}) {
|
|
|
|
// https://stackoverflow.com/questions/13476349/check-for-nil-and-nil-interface-in-go
|
|
isNil := func() bool {
|
|
defer func() { _ = recover() }()
|
|
return reflect.ValueOf(obj).IsNil()
|
|
}
|
|
|
|
if obj == nil || isNil() {
|
|
panic("Pkglint internal error: unexpected nil pointer")
|
|
}
|
|
}
|
|
|
|
// assert checks that the condition is true. Otherwise it terminates the
|
|
// process with a fatal error message, prefixed with "Pkglint internal error".
|
|
//
|
|
// This method must only be used for programming errors.
|
|
// For runtime errors, use dummyLine.Fatalf.
|
|
func assert(cond bool) {
|
|
if !cond {
|
|
panic("Pkglint internal error")
|
|
}
|
|
}
|
|
|
|
// assertf checks that the condition is true. Otherwise it terminates the
|
|
// process with a fatal error message, prefixed with "Pkglint internal error".
|
|
//
|
|
// This method must only be used for programming errors.
|
|
// For runtime errors, use dummyLine.Fatalf.
|
|
func assertf(cond bool, format string, args ...interface{}) {
|
|
if !cond {
|
|
panic("Pkglint internal error: " + sprintf(format, args...))
|
|
}
|
|
}
|
|
|
|
func isEmptyDir(filename CurrPath) bool {
|
|
if filename.HasSuffixPath("CVS") {
|
|
return true
|
|
}
|
|
|
|
dirents, err := filename.ReadDir()
|
|
if err != nil {
|
|
return true // XXX: Why not false?
|
|
}
|
|
|
|
for _, dirent := range dirents {
|
|
name := dirent.Name()
|
|
if isIgnoredFilename(name) {
|
|
continue
|
|
}
|
|
if dirent.IsDir() && isEmptyDir(filename.JoinNoClean(NewRelPathString(name))) {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func getSubdirs(filename CurrPath) []RelPath {
|
|
dirents, err := filename.ReadDir()
|
|
if err != nil {
|
|
G.Logger.TechFatalf(filename, "Cannot be read: %s", err)
|
|
}
|
|
|
|
var subdirs []RelPath
|
|
for _, dirent := range dirents {
|
|
name := dirent.Name()
|
|
if dirent.IsDir() && !isIgnoredFilename(name) && !isEmptyDir(filename.JoinNoClean(NewRelPathString(name))) {
|
|
subdirs = append(subdirs, NewRelPathString(name))
|
|
}
|
|
}
|
|
return subdirs
|
|
}
|
|
|
|
func isIgnoredFilename(filename string) bool {
|
|
switch filename {
|
|
case "CVS", ".svn", ".git", ".hg", ".idea":
|
|
return true
|
|
}
|
|
|
|
// https://www.gnu.org/software/trans-coord/manual/cvs/cvs.html#cvsignore
|
|
return hasPrefix(filename, ".#") || hasSuffix(filename, "~")
|
|
}
|
|
|
|
// Checks whether a file is already committed to the CVS repository.
|
|
func isCommitted(filename CurrPath) bool {
|
|
entries := G.loadCvsEntries(filename)
|
|
_, found := entries[filename.Base()]
|
|
return found
|
|
}
|
|
|
|
// isLocallyModified tests whether a file (not a directory) is modified,
|
|
// as seen by CVS.
|
|
//
|
|
// There is no corresponding test for Git (as used by pkgsrc-wip) since that
|
|
// is more difficult to implement than simply reading a CVS/Entries file.
|
|
func isLocallyModified(filename CurrPath) bool {
|
|
entries := G.loadCvsEntries(filename)
|
|
entry, found := entries[filename.Base()]
|
|
if !found {
|
|
return false
|
|
}
|
|
|
|
st, err := filename.Stat()
|
|
if err != nil {
|
|
return true
|
|
}
|
|
|
|
// Following http://cvsman.com/cvs-1.12.12/cvs_19.php, format both timestamps.
|
|
cvsModTime := entry.Timestamp
|
|
fsModTime := st.ModTime().UTC().Format(time.ANSIC)
|
|
if trace.Tracing {
|
|
trace.Stepf("cvs.time=%q fs.time=%q", cvsModTime, fsModTime)
|
|
}
|
|
|
|
return cvsModTime != fsModTime
|
|
}
|
|
|
|
// CvsEntry is one of the entries in a CVS/Entries file.
|
|
//
|
|
// See http://cvsman.com/cvs-1.12.12/cvs_19.php.
|
|
type CvsEntry struct {
|
|
Name RelPath
|
|
Revision string
|
|
Timestamp string
|
|
Options string
|
|
TagDate string
|
|
}
|
|
|
|
// Returns the number of columns that a string occupies when printed with
|
|
// a tabulator size of 8.
|
|
func tabWidth(s string) int { return tabWidthAppend(0, s) }
|
|
|
|
func tabWidthSlice(strs ...string) int {
|
|
w := 0
|
|
for _, str := range strs {
|
|
w = tabWidthAppend(w, str)
|
|
}
|
|
return w
|
|
}
|
|
|
|
func tabWidthAppend(width int, s string) int {
|
|
for _, r := range s {
|
|
assert(r != '\n')
|
|
if r == '\t' {
|
|
width = width&-8 + 8
|
|
} else {
|
|
width++
|
|
}
|
|
}
|
|
return width
|
|
}
|
|
|
|
func detab(s string) string {
|
|
var detabbed strings.Builder
|
|
for _, r := range s {
|
|
if r == '\t' {
|
|
detabbed.WriteString(" "[:8-detabbed.Len()&7])
|
|
} else {
|
|
detabbed.WriteRune(r)
|
|
}
|
|
}
|
|
return detabbed.String()
|
|
}
|
|
|
|
// alignWith extends str with as many tabs and spaces as needed to reach
|
|
// the same screen width as the other string.
|
|
func alignWith(str, other string) string {
|
|
return str + alignmentTo(str, other)
|
|
}
|
|
|
|
// alignmentTo returns the whitespace that is necessary to
|
|
// bring str to the same width as other.
|
|
func alignmentTo(str, other string) string {
|
|
strWidth := tabWidth(str)
|
|
otherWidth := tabWidth(other)
|
|
return alignmentToWidths(strWidth, otherWidth)
|
|
}
|
|
|
|
func alignmentToWidths(strWidth, otherWidth int) string {
|
|
if otherWidth <= strWidth {
|
|
return ""
|
|
}
|
|
if strWidth&-8 != otherWidth&-8 {
|
|
strWidth &= -8
|
|
}
|
|
return indent(otherWidth - strWidth)
|
|
}
|
|
|
|
func indent(width int) string {
|
|
const tabsAndSpaces = "\t\t\t\t\t\t\t\t\t "
|
|
middle := len(tabsAndSpaces) - 7
|
|
if width <= 8*middle+7 {
|
|
start := middle - width>>3
|
|
end := middle + width&7
|
|
return tabsAndSpaces[start:end]
|
|
}
|
|
return strings.Repeat("\t", width>>3) + " "[:width&7]
|
|
}
|
|
|
|
// alignmentAfter returns the indentation that is necessary to get
|
|
// from the given prefix to the desired width.
|
|
func alignmentAfter(prefix string, width int) string {
|
|
pw := tabWidth(prefix)
|
|
assert(width >= pw)
|
|
return indent(width - condInt(pw&-8 != width&-8, pw&-8, pw))
|
|
}
|
|
|
|
func shorten(s string, maxChars int) string {
|
|
codePoints := 0
|
|
for i := range s {
|
|
if codePoints >= maxChars {
|
|
return s[:i] + "..."
|
|
}
|
|
codePoints++
|
|
}
|
|
return s
|
|
}
|
|
|
|
func varnameBase(varname string) string {
|
|
dot := strings.IndexByte(varname, '.')
|
|
if dot > 0 {
|
|
return varname[:dot]
|
|
}
|
|
return varname
|
|
}
|
|
|
|
func varnameCanon(varname string) string {
|
|
dot := strings.IndexByte(varname, '.')
|
|
if dot > 0 {
|
|
return varname[:dot] + ".*"
|
|
}
|
|
return varname
|
|
}
|
|
|
|
func varnameParam(varname string) string {
|
|
dot := strings.IndexByte(varname, '.')
|
|
if dot > 0 {
|
|
return varname[dot+1:]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func toInt(s string, def int) int {
|
|
if n, err := strconv.Atoi(s); err == nil {
|
|
return n
|
|
}
|
|
return def
|
|
}
|
|
|
|
func containsVarUse(s string) bool {
|
|
if !contains(s, "$") {
|
|
return false
|
|
}
|
|
lex := NewMkLexer(s, nil)
|
|
tokens, _ := lex.MkTokens()
|
|
for _, token := range tokens {
|
|
if token.Varuse != nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func containsVarRefLong(s string) bool {
|
|
if !contains(s, "$") {
|
|
return false
|
|
}
|
|
lex := NewMkLexer(s, nil)
|
|
tokens, _ := lex.MkTokens()
|
|
for _, token := range tokens {
|
|
if token.Varuse != nil && len(token.Text) > 2 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Once remembers with which arguments its FirstTime method has been called
|
|
// and only returns true on each first call.
|
|
type Once struct {
|
|
seen map[uint64]struct{}
|
|
|
|
// Only used during testing, to trace the actual arguments,
|
|
// since hashing is a one-way function.
|
|
Trace bool
|
|
}
|
|
|
|
func (o *Once) FirstTime(what string) bool {
|
|
key := o.keyString(what)
|
|
firstTime := o.check(key)
|
|
if firstTime && o.Trace {
|
|
G.Logger.out.WriteLine("FirstTime: " + what)
|
|
}
|
|
return firstTime
|
|
}
|
|
|
|
func (o *Once) FirstTimeSlice(whats ...string) bool {
|
|
key := o.keyStrings(whats)
|
|
firstTime := o.check(key)
|
|
if firstTime && o.Trace {
|
|
G.Logger.out.WriteLine("FirstTime: " + strings.Join(whats, ", "))
|
|
}
|
|
return firstTime
|
|
}
|
|
|
|
func (o *Once) Seen(what string) bool {
|
|
_, seen := o.seen[o.keyString(what)]
|
|
return seen
|
|
}
|
|
|
|
func (o *Once) SeenSlice(whats ...string) bool {
|
|
_, seen := o.seen[o.keyStrings(whats)]
|
|
return seen
|
|
}
|
|
|
|
func (*Once) keyString(what string) uint64 {
|
|
return crc64.Checksum([]byte(what), crc64.MakeTable(crc64.ECMA))
|
|
}
|
|
|
|
func (*Once) keyStrings(whats []string) uint64 {
|
|
crc := crc64.New(crc64.MakeTable(crc64.ECMA))
|
|
for i, what := range whats {
|
|
if i != 0 {
|
|
_, _ = crc.Write([]byte{0})
|
|
}
|
|
_, _ = crc.Write([]byte(what))
|
|
}
|
|
return crc.Sum64()
|
|
}
|
|
|
|
func (o *Once) check(key uint64) bool {
|
|
if _, ok := o.seen[key]; ok {
|
|
return false
|
|
}
|
|
if o.seen == nil {
|
|
o.seen = make(map[uint64]struct{})
|
|
}
|
|
o.seen[key] = struct{}{}
|
|
return true
|
|
}
|
|
|
|
// The MIT License (MIT)
|
|
//
|
|
// Copyright (c) 2015 Frits van Bommel
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
// The above copyright notice and this permission notice shall be included in all
|
|
// copies or substantial portions of the Software.
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
//
|
|
// Taken from https://github.com/fvbommel/util/blob/11997822f8/sortorder/natsort.go
|
|
func naturalLess(str1, str2 string) bool {
|
|
|
|
isDigit := func(b byte) bool { return '0' <= b && b <= '9' }
|
|
|
|
idx := 0
|
|
len1, len2 := len(str1), len(str2)
|
|
minLen := len1 + len2 - imax(len1, len2)
|
|
for idx < minLen {
|
|
c1, c2 := str1[idx], str2[idx]
|
|
dig1, dig2 := isDigit(c1), isDigit(c2)
|
|
switch {
|
|
case dig1 != dig2: // Digits before other characters.
|
|
return dig1 // True if LHS is a digit, false if the RHS is one.
|
|
case !dig1: // && !dig2, because dig1 == dig2
|
|
// UTF-8 compares bytewise-lexicographically, no need to decode
|
|
// codepoints.
|
|
if c1 != c2 {
|
|
return c1 < c2
|
|
}
|
|
idx++
|
|
default: // Digits
|
|
// Eat zeros.
|
|
idx1, idx2 := idx, idx
|
|
for ; idx1 < len1 && str1[idx1] == '0'; idx1++ {
|
|
}
|
|
for ; idx2 < len2 && str2[idx2] == '0'; idx2++ {
|
|
}
|
|
// Eat all digits.
|
|
nonZero1, nonZero2 := idx1, idx2
|
|
for ; idx1 < len1 && isDigit(str1[idx1]); idx1++ {
|
|
}
|
|
for ; idx2 < len2 && isDigit(str2[idx2]); idx2++ {
|
|
}
|
|
// If lengths of numbers with non-zero prefix differ, the shorter
|
|
// one is less.
|
|
if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 {
|
|
return len1 < len2
|
|
}
|
|
// If they're not equal, string comparison is correct.
|
|
if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 {
|
|
return nr1 < nr2
|
|
}
|
|
// Otherwise, the one with fewer zeros is less.
|
|
// Because everything up to the number is equal, comparing the index
|
|
// after the zeros is sufficient.
|
|
if nonZero1 != nonZero2 {
|
|
return nonZero1 < nonZero2
|
|
}
|
|
idx = idx1
|
|
}
|
|
// They're identical so far, so continue comparing.
|
|
}
|
|
// So far they are identical. At least one is ended. If the other continues,
|
|
// it sorts last.
|
|
return len1 < len2
|
|
}
|
|
|
|
// LoadsPrefs returns whether the given file, when included, loads the user
|
|
// preferences.
|
|
func LoadsPrefs(filename RelPath) bool {
|
|
switch filename.Base() {
|
|
case // See https://github.com/golang/go/issues/28057
|
|
"bsd.prefs.mk", // in mk/
|
|
"bsd.fast.prefs.mk", // in mk/
|
|
"bsd.builtin.mk", // in mk/buildlink3/
|
|
"pkgconfig-builtin.mk", // in mk/buildlink3/
|
|
"pkg-build-options.mk", // in mk/
|
|
"compiler.mk", // in mk/
|
|
"options.mk", // in package directories
|
|
"bsd.options.mk": // in mk/
|
|
return true
|
|
}
|
|
|
|
// Just assume that every pkgsrc infrastructure file includes
|
|
// bsd.prefs.mk, at least indirectly.
|
|
return filename.ContainsPath("mk")
|
|
}
|
|
|
|
func IsPrefs(filename RelPath) bool {
|
|
base := filename.Base()
|
|
return base == "bsd.prefs.mk" || base == "bsd.fast.prefs.mk"
|
|
}
|
|
|
|
// FileCache reduces the IO load for commonly loaded files by about 50%,
|
|
// especially for buildlink3.mk and *.buildlink3.mk files.
|
|
type FileCache struct {
|
|
table []*fileCacheEntry
|
|
mapping map[string]*fileCacheEntry // Pointers into FileCache.table
|
|
hits int
|
|
misses int
|
|
}
|
|
|
|
type fileCacheEntry struct {
|
|
count int
|
|
key string
|
|
options LoadOptions
|
|
lines *Lines
|
|
}
|
|
|
|
func NewFileCache(size int) *FileCache {
|
|
return &FileCache{
|
|
make([]*fileCacheEntry, 0, size),
|
|
make(map[string]*fileCacheEntry),
|
|
0,
|
|
0}
|
|
}
|
|
|
|
func (c *FileCache) Put(filename CurrPath, options LoadOptions, lines *Lines) {
|
|
key := c.key(filename)
|
|
|
|
entry := c.mapping[key]
|
|
if entry == nil {
|
|
if len(c.table) == cap(c.table) {
|
|
c.removeOldEntries()
|
|
}
|
|
|
|
entry = new(fileCacheEntry)
|
|
c.table = append(c.table, entry)
|
|
c.mapping[key] = entry
|
|
}
|
|
|
|
entry.count = 1
|
|
entry.key = key
|
|
entry.options = options
|
|
entry.lines = lines
|
|
}
|
|
|
|
func (c *FileCache) removeOldEntries() {
|
|
sort.Slice(c.table, func(i, j int) bool {
|
|
return c.table[j].count < c.table[i].count
|
|
})
|
|
|
|
if G.Testing {
|
|
for _, e := range c.table {
|
|
if trace.Tracing {
|
|
trace.Stepf("FileCache %q with count %d.", e.key, e.count)
|
|
}
|
|
}
|
|
}
|
|
|
|
minCount := c.table[len(c.table)-1].count
|
|
newLen := len(c.table)
|
|
for newLen > 0 && c.table[newLen-1].count == minCount {
|
|
e := c.table[newLen-1]
|
|
if trace.Tracing {
|
|
trace.Stepf("FileCache.Evict %q with count %d.", e.key, e.count)
|
|
}
|
|
delete(c.mapping, e.key)
|
|
newLen--
|
|
}
|
|
c.table = c.table[0:newLen]
|
|
|
|
// To avoid files getting stuck in the cache.
|
|
for _, e := range c.table {
|
|
if trace.Tracing {
|
|
trace.Stepf("FileCache.Halve %q with count %d.", e.key, e.count)
|
|
}
|
|
e.count /= 2
|
|
}
|
|
}
|
|
|
|
func (c *FileCache) Get(filename CurrPath, options LoadOptions) *Lines {
|
|
key := c.key(filename)
|
|
entry, found := c.mapping[key]
|
|
if found && entry.options == options {
|
|
c.hits++
|
|
entry.count++
|
|
|
|
lines := make([]*Line, entry.lines.Len())
|
|
for i, line := range entry.lines.Lines {
|
|
lines[i] = NewLineMulti(filename, line.Location.lineno, line.Text, line.raw)
|
|
}
|
|
return NewLines(filename, lines)
|
|
}
|
|
c.misses++
|
|
return nil
|
|
}
|
|
|
|
func (c *FileCache) Evict(filename CurrPath) {
|
|
key := c.key(filename)
|
|
entry, found := c.mapping[key]
|
|
if !found {
|
|
return
|
|
}
|
|
|
|
delete(c.mapping, key)
|
|
|
|
for i, e := range c.table {
|
|
if e == entry {
|
|
c.table[i] = c.table[len(c.table)-1]
|
|
c.table = c.table[:len(c.table)-1]
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *FileCache) key(filename CurrPath) string { return filename.Clean().String() }
|
|
|
|
func bmakeHelp(topic string) string { return bmake("help topic=" + topic) }
|
|
|
|
func bmake(target string) string { return sprintf("%s %s", confMake, target) }
|
|
|
|
func seeGuide(sectionName, sectionID string) string {
|
|
return sprintf("See the pkgsrc guide, section %q: https://www.NetBSD.org/docs/pkgsrc/pkgsrc.html#%s",
|
|
sectionName, sectionID)
|
|
}
|
|
|
|
// wrap performs automatic word wrapping on the given lines.
|
|
//
|
|
// Empty lines, indented lines and lines starting with "*" are kept as-is.
|
|
func wrap(max int, lines ...string) []string {
|
|
var wrapped []string
|
|
var sb strings.Builder
|
|
|
|
for _, line := range lines {
|
|
|
|
if line == "" || isHspace(line[0]) || line[0] == '*' {
|
|
|
|
// Finish current paragraph.
|
|
if sb.Len() > 0 {
|
|
wrapped = append(wrapped, sb.String())
|
|
sb.Reset()
|
|
}
|
|
|
|
wrapped = append(wrapped, line)
|
|
continue
|
|
}
|
|
|
|
lexer := textproc.NewLexer(line)
|
|
for !lexer.EOF() {
|
|
bol := len(lexer.Rest()) == len(line)
|
|
space := lexer.NextBytesSet(textproc.Space)
|
|
word := lexer.NextBytesSet(notSpace)
|
|
|
|
if bol && sb.Len() > 0 {
|
|
space = " "
|
|
}
|
|
|
|
if sb.Len() > 0 && sb.Len()+len(space)+len(word) > max {
|
|
wrapped = append(wrapped, sb.String())
|
|
sb.Reset()
|
|
space = ""
|
|
}
|
|
|
|
sb.WriteString(space)
|
|
sb.WriteString(word)
|
|
}
|
|
}
|
|
|
|
if sb.Len() > 0 {
|
|
wrapped = append(wrapped, sb.String())
|
|
}
|
|
|
|
return wrapped
|
|
}
|
|
|
|
// escapePrintable returns an ASCII-only string that represents the given string
|
|
// very closely, but without putting any physical terminal or terminal emulator
|
|
// at the risk of interpreting malicious data from the files checked by pkglint.
|
|
// This escaping is not reversible, and it doesn't need to.
|
|
func escapePrintable(s string) string {
|
|
escaped := NewLazyStringBuilder(s)
|
|
for i, r := range s {
|
|
switch {
|
|
case rune(byte(r)) == r && textproc.XPrint.Contains(s[i]):
|
|
escaped.WriteByte(byte(r))
|
|
case r == 0xFFFD && !hasPrefix(s[i:], "\uFFFD"):
|
|
_, _ = fmt.Fprintf(&escaped, "<0x%02X>", s[i])
|
|
default:
|
|
_, _ = fmt.Fprintf(&escaped, "<%U>", r)
|
|
}
|
|
}
|
|
return escaped.String()
|
|
}
|
|
|
|
func stringSliceLess(a, b []string) bool {
|
|
limit := len(a)
|
|
if len(b) < limit {
|
|
limit = len(b)
|
|
}
|
|
|
|
for i := 0; i < limit; i++ {
|
|
if a[i] != b[i] {
|
|
return a[i] < b[i]
|
|
}
|
|
}
|
|
|
|
return len(a) < len(b)
|
|
}
|
|
|
|
func joinSkipEmpty(sep string, elements ...string) string {
|
|
var nonempty []string
|
|
for _, element := range elements {
|
|
if element != "" {
|
|
nonempty = append(nonempty, element)
|
|
}
|
|
}
|
|
return strings.Join(nonempty, sep)
|
|
}
|
|
|
|
// joinCambridge returns "first, second conn third".
|
|
// It is used when each element is a single word.
|
|
// Empty elements are ignored completely.
|
|
func joinCambridge(conn string, elements ...string) string {
|
|
parts := make([]string, 0, 2+2*len(elements))
|
|
for _, element := range elements {
|
|
if element != "" {
|
|
parts = append(parts, ", ", element)
|
|
}
|
|
}
|
|
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
if len(parts) < 4 {
|
|
return parts[1]
|
|
}
|
|
|
|
parts = append(parts[1:len(parts)-2], " ", conn, " ", parts[len(parts)-1])
|
|
return strings.Join(parts, "")
|
|
}
|
|
|
|
// joinOxford returns "first, second, conn third".
|
|
// It is used when each element may consist of multiple words.
|
|
// Empty elements are ignored completely.
|
|
func joinOxford(conn string, elements ...string) string {
|
|
var nonempty []string
|
|
for _, element := range elements {
|
|
if element != "" {
|
|
nonempty = append(nonempty, element)
|
|
}
|
|
}
|
|
|
|
if lastIndex := len(nonempty) - 1; lastIndex >= 1 {
|
|
nonempty[lastIndex] = conn + " " + nonempty[lastIndex]
|
|
}
|
|
|
|
return strings.Join(nonempty, ", ")
|
|
}
|
|
|
|
var pathMatchers = make(map[string]*pathMatcher)
|
|
|
|
type pathMatcher struct {
|
|
matchType pathMatchType
|
|
pattern string
|
|
originalPattern string
|
|
}
|
|
|
|
func newPathMatcher(pattern string) *pathMatcher {
|
|
matcher := pathMatchers[pattern]
|
|
if matcher == nil {
|
|
matcher = newPathMatcherUncached(pattern)
|
|
pathMatchers[pattern] = matcher
|
|
}
|
|
return matcher
|
|
}
|
|
|
|
func newPathMatcherUncached(pattern string) *pathMatcher {
|
|
assert(strings.IndexByte(pattern, '[') == -1)
|
|
assert(strings.IndexByte(pattern, '?') == -1)
|
|
|
|
stars := strings.Count(pattern, "*")
|
|
assert(stars == 0 || stars == 1)
|
|
switch {
|
|
case stars == 0:
|
|
return &pathMatcher{pmExact, pattern, pattern}
|
|
case pattern[0] == '*':
|
|
return &pathMatcher{pmSuffix, pattern[1:], pattern}
|
|
default:
|
|
assert(pattern[len(pattern)-1] == '*')
|
|
return &pathMatcher{pmPrefix, pattern[:len(pattern)-1], pattern}
|
|
}
|
|
}
|
|
|
|
func (m pathMatcher) matches(subject string) bool {
|
|
switch m.matchType {
|
|
case pmPrefix:
|
|
return hasPrefix(subject, m.pattern)
|
|
case pmSuffix:
|
|
return hasSuffix(subject, m.pattern)
|
|
default:
|
|
return subject == m.pattern
|
|
}
|
|
}
|
|
|
|
type pathMatchType uint8
|
|
|
|
const (
|
|
pmExact pathMatchType = iota
|
|
pmPrefix
|
|
pmSuffix
|
|
)
|
|
|
|
// StringInterner collects commonly used strings to avoid wasting heap memory
|
|
// by duplicated strings.
|
|
type StringInterner struct {
|
|
strs map[string]string
|
|
}
|
|
|
|
func NewStringInterner() StringInterner {
|
|
return StringInterner{make(map[string]string)}
|
|
}
|
|
|
|
func (si *StringInterner) Intern(str string) string {
|
|
interned, found := si.strs[str]
|
|
if found {
|
|
return interned
|
|
}
|
|
|
|
// Ensure that the original string is never stored directly in the map
|
|
// since it might be a substring of a very large string. The interned
|
|
// strings must be completely independent of anything from the outside,
|
|
// so that the large source string can be freed afterwards.
|
|
var sb strings.Builder
|
|
sb.WriteString(str)
|
|
key := sb.String()
|
|
|
|
si.strs[key] = key
|
|
return key
|
|
}
|
|
|
|
// StringSet stores unique strings in insertion order.
|
|
type StringSet struct {
|
|
Elements []string
|
|
seen map[string]struct{}
|
|
}
|
|
|
|
func NewStringSet() StringSet {
|
|
return StringSet{nil, make(map[string]struct{})}
|
|
}
|
|
|
|
func (s *StringSet) Add(element string) {
|
|
if _, found := s.seen[element]; !found {
|
|
s.seen[element] = struct{}{}
|
|
s.Elements = append(s.Elements, element)
|
|
}
|
|
}
|
|
|
|
func (s *StringSet) AddAll(elements []string) {
|
|
for _, element := range elements {
|
|
s.Add(element)
|
|
}
|
|
}
|
|
|
|
// See mk/tools/shquote.sh.
|
|
func shquote(s string) string {
|
|
if matches(s, `^[!%+,\-./0-9:=@A-Z_a-z]+$`) {
|
|
return s
|
|
}
|
|
return "'" + strings.Replace(s, "'", "'\\''", -1) + "'"
|
|
}
|
|
|
|
func pathMatches(pattern, s string) bool {
|
|
matched, err := path.Match(pattern, s)
|
|
return err == nil && matched
|
|
}
|
|
|
|
type CurrPathQueue struct {
|
|
entries []CurrPath
|
|
}
|
|
|
|
func (q *CurrPathQueue) PushFront(entries ...CurrPath) {
|
|
q.entries = append(append([]CurrPath(nil), entries...), q.entries...)
|
|
}
|
|
|
|
func (q *CurrPathQueue) Push(entries ...CurrPath) {
|
|
q.entries = append(q.entries, entries...)
|
|
}
|
|
|
|
func (q *CurrPathQueue) IsEmpty() bool {
|
|
return len(q.entries) == 0
|
|
}
|
|
|
|
func (q *CurrPathQueue) Front() CurrPath {
|
|
return q.entries[0]
|
|
}
|
|
|
|
func (q *CurrPathQueue) Pop() CurrPath {
|
|
front := q.entries[0]
|
|
q.entries = q.entries[1:]
|
|
return front
|
|
}
|
|
|
|
// LazyStringBuilder builds a string that is most probably equal to an
|
|
// already existing string. In that case, it avoids any memory allocations.
|
|
type LazyStringBuilder struct {
|
|
expected string
|
|
len int
|
|
usingBuf bool
|
|
buf []byte
|
|
}
|
|
|
|
func NewLazyStringBuilder(expected string) LazyStringBuilder {
|
|
return LazyStringBuilder{expected: expected}
|
|
}
|
|
|
|
func (b *LazyStringBuilder) Write(p []byte) (n int, err error) {
|
|
for _, c := range p {
|
|
b.WriteByte(c)
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (b *LazyStringBuilder) Len() int {
|
|
return b.len
|
|
}
|
|
|
|
func (b *LazyStringBuilder) WriteString(s string) {
|
|
if !b.usingBuf && b.len+len(s) <= len(b.expected) && hasPrefix(b.expected[b.len:], s) {
|
|
b.len += len(s)
|
|
return
|
|
}
|
|
for _, c := range []byte(s) {
|
|
b.WriteByte(c)
|
|
}
|
|
}
|
|
|
|
func (b *LazyStringBuilder) WriteByte(c byte) {
|
|
if !b.usingBuf && b.len < len(b.expected) && b.expected[b.len] == c {
|
|
b.len++
|
|
return
|
|
}
|
|
b.writeToBuf(c)
|
|
}
|
|
|
|
func (b *LazyStringBuilder) writeToBuf(c byte) {
|
|
if !b.usingBuf {
|
|
if cap(b.buf) >= b.len {
|
|
b.buf = b.buf[:b.len]
|
|
assert(copy(b.buf, b.expected) == b.len)
|
|
} else {
|
|
b.buf = []byte(b.expected)[:b.len]
|
|
}
|
|
b.usingBuf = true
|
|
}
|
|
|
|
b.buf = append(b.buf, c)
|
|
b.len++
|
|
}
|
|
|
|
func (b *LazyStringBuilder) Reset(expected string) {
|
|
b.expected = expected
|
|
b.usingBuf = false
|
|
b.len = 0
|
|
}
|
|
|
|
func (b *LazyStringBuilder) String() string {
|
|
if b.usingBuf {
|
|
return string(b.buf[:b.len])
|
|
}
|
|
return b.expected[:b.len]
|
|
}
|
|
|
|
type interval struct {
|
|
min int
|
|
max int
|
|
}
|
|
|
|
func newInterval() *interval {
|
|
return &interval{int(^uint(0) >> 1), ^int(^uint(0) >> 1)}
|
|
}
|
|
|
|
func (i *interval) add(x int) {
|
|
if x < i.min {
|
|
i.min = x
|
|
}
|
|
if x > i.max {
|
|
i.max = x
|
|
}
|
|
}
|
|
|
|
type optInt struct {
|
|
isSet bool
|
|
value int
|
|
}
|
|
|
|
func (i *optInt) get() int {
|
|
assert(i.isSet)
|
|
return i.value
|
|
}
|
|
|
|
func (i *optInt) set(value int) {
|
|
i.value = value
|
|
i.isSet = true
|
|
}
|
|
|
|
type bag struct {
|
|
// Wrapping the slice in an extra struct avoids 'receiver might be nil'
|
|
// warnings.
|
|
|
|
entries []bagEntry
|
|
}
|
|
|
|
func (b *bag) sortDesc() {
|
|
es := b.entries
|
|
less := func(i, j int) bool { return es[j].count < es[i].count }
|
|
sort.SliceStable(es, less)
|
|
}
|
|
|
|
func (b *bag) opt(index int) int {
|
|
if uint(index) < uint(len(b.entries)) {
|
|
return b.entries[index].count
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (b *bag) key(index int) interface{} { return b.entries[index].key }
|
|
|
|
func (b *bag) add(key interface{}, count int) {
|
|
b.entries = append(b.entries, bagEntry{key, count})
|
|
}
|
|
|
|
func (b *bag) len() int { return len(b.entries) }
|
|
|
|
type bagEntry struct {
|
|
key interface{}
|
|
count int
|
|
}
|
|
|
|
type lazyBool struct {
|
|
fn func() bool
|
|
value bool
|
|
}
|
|
|
|
func newLazyBool(fn func() bool) *lazyBool { return &lazyBool{fn, false} }
|
|
|
|
func (b *lazyBool) get() bool {
|
|
if b.fn != nil {
|
|
b.value = b.fn()
|
|
b.fn = nil
|
|
}
|
|
return b.value
|
|
}
|