Update Vendors (#250)
update go min version Update Vendors: * code.gitea.io/gitea-vet v0.2.0 -> v0.2.1 * code.gitea.io/sdk/gitea v0.13.0 -> v0.13.1 * github.com/AlecAivazis/survey v2.1.1 -> v2.2.2 * github.com/adrg/xdg v0.2.1 -> v0.2.2 * github.com/araddon/dateparse d820a6159ab1 -> 8aadafed4dc4 * github.com/go-git/go-git v5.1.0 -> v5.2.0 * github.com/muesli/termenv v0.7.2 -> v0.7.4 * github.com/stretchr/testify v1.5.1 -> v1.6.1 * github.com/urfave/cli v2.2.0 -> v2.3.0 Co-authored-by: 6543 <6543@obermui.de> Reviewed-on: https://gitea.com/gitea/tea/pulls/250 Reviewed-by: Andrew Thornton <art27@cantab.net> Reviewed-by: mrsdizzie <info@mrsdizzie.com> Co-Authored-By: 6543 <6543@noreply.gitea.io> Co-Committed-By: 6543 <6543@noreply.gitea.io>
This commit is contained in:
13
vendor/golang.org/x/tools/go/analysis/analysis.go
generated
vendored
13
vendor/golang.org/x/tools/go/analysis/analysis.go
generated
vendored
@@ -95,12 +95,13 @@ type Pass struct {
|
||||
Analyzer *Analyzer // the identity of the current analyzer
|
||||
|
||||
// syntax and type information
|
||||
Fset *token.FileSet // file position information
|
||||
Files []*ast.File // the abstract syntax tree of each file
|
||||
OtherFiles []string // names of non-Go files of this package
|
||||
Pkg *types.Package // type information about the package
|
||||
TypesInfo *types.Info // type information about the syntax trees
|
||||
TypesSizes types.Sizes // function for computing sizes of types
|
||||
Fset *token.FileSet // file position information
|
||||
Files []*ast.File // the abstract syntax tree of each file
|
||||
OtherFiles []string // names of non-Go files of this package
|
||||
IgnoredFiles []string // names of ignored source files in this package
|
||||
Pkg *types.Package // type information about the package
|
||||
TypesInfo *types.Info // type information about the syntax trees
|
||||
TypesSizes types.Sizes // function for computing sizes of types
|
||||
|
||||
// Report reports a Diagnostic, a finding about a specific location
|
||||
// in the analyzed source code such as a potential mistake.
|
||||
|
||||
21
vendor/golang.org/x/tools/go/analysis/doc.go
generated
vendored
21
vendor/golang.org/x/tools/go/analysis/doc.go
generated
vendored
@@ -121,13 +121,14 @@ package being analyzed, and provides operations to the Run function for
|
||||
reporting diagnostics and other information back to the driver.
|
||||
|
||||
type Pass struct {
|
||||
Fset *token.FileSet
|
||||
Files []*ast.File
|
||||
OtherFiles []string
|
||||
Pkg *types.Package
|
||||
TypesInfo *types.Info
|
||||
ResultOf map[*Analyzer]interface{}
|
||||
Report func(Diagnostic)
|
||||
Fset *token.FileSet
|
||||
Files []*ast.File
|
||||
OtherFiles []string
|
||||
IgnoredFiles []string
|
||||
Pkg *types.Package
|
||||
TypesInfo *types.Info
|
||||
ResultOf map[*Analyzer]interface{}
|
||||
Report func(Diagnostic)
|
||||
...
|
||||
}
|
||||
|
||||
@@ -139,6 +140,12 @@ files such as assembly that are part of this package. See the "asmdecl"
|
||||
or "buildtags" analyzers for examples of loading non-Go files and reporting
|
||||
diagnostics against them.
|
||||
|
||||
The IgnoredFiles field provides the names, but not the contents,
|
||||
of ignored Go and non-Go source files that are not part of this package
|
||||
with the current build configuration but may be part of other build
|
||||
configurations. See the "buildtags" analyzer for an example of loading
|
||||
and checking IgnoredFiles.
|
||||
|
||||
The ResultOf field provides the results computed by the analyzers
|
||||
required by this one, as expressed in its Analyzer.Requires field. The
|
||||
driver runs the required analyzers first and makes their results
|
||||
|
||||
2
vendor/golang.org/x/tools/go/analysis/unitchecker/unitchecker.go
generated
vendored
2
vendor/golang.org/x/tools/go/analysis/unitchecker/unitchecker.go
generated
vendored
@@ -63,6 +63,7 @@ type Config struct {
|
||||
ImportPath string
|
||||
GoFiles []string
|
||||
NonGoFiles []string
|
||||
IgnoredFiles []string
|
||||
ImportMap map[string]string
|
||||
PackageFile map[string]string
|
||||
Standard map[string]bool
|
||||
@@ -333,6 +334,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re
|
||||
Fset: fset,
|
||||
Files: files,
|
||||
OtherFiles: cfg.NonGoFiles,
|
||||
IgnoredFiles: cfg.IgnoredFiles,
|
||||
Pkg: pkg,
|
||||
TypesInfo: info,
|
||||
TypesSizes: tc.Sizes,
|
||||
|
||||
33
vendor/golang.org/x/tools/go/analysis/validate.go
generated
vendored
33
vendor/golang.org/x/tools/go/analysis/validate.go
generated
vendored
@@ -3,6 +3,7 @@ package analysis
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
@@ -58,14 +59,28 @@ func Validate(analyzers []*Analyzer) error {
|
||||
}
|
||||
|
||||
// recursion
|
||||
for i, req := range a.Requires {
|
||||
for _, req := range a.Requires {
|
||||
if err := visit(req); err != nil {
|
||||
return fmt.Errorf("%s.Requires[%d]: %v", a.Name, i, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
color[a] = black
|
||||
}
|
||||
|
||||
if color[a] == grey {
|
||||
stack := []*Analyzer{a}
|
||||
inCycle := map[string]bool{}
|
||||
for len(stack) > 0 {
|
||||
current := stack[len(stack)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
if color[current] == grey && !inCycle[current.Name] {
|
||||
inCycle[current.Name] = true
|
||||
stack = append(stack, current.Requires...)
|
||||
}
|
||||
}
|
||||
return &CycleInRequiresGraphError{AnalyzerNames: inCycle}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
for _, a := range analyzers {
|
||||
@@ -95,3 +110,17 @@ func validIdent(name string) bool {
|
||||
}
|
||||
return name != ""
|
||||
}
|
||||
|
||||
type CycleInRequiresGraphError struct {
|
||||
AnalyzerNames map[string]bool
|
||||
}
|
||||
|
||||
func (e *CycleInRequiresGraphError) Error() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("cycle detected involving the following analyzers:")
|
||||
for n := range e.AnalyzerNames {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(n)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
168
vendor/golang.org/x/tools/internal/analysisinternal/analysis.go
generated
vendored
168
vendor/golang.org/x/tools/internal/analysisinternal/analysis.go
generated
vendored
@@ -14,6 +14,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/ast/astutil"
|
||||
"golang.org/x/tools/internal/lsp/fuzzy"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -50,7 +51,7 @@ func ZeroValue(fset *token.FileSet, f *ast.File, pkg *types.Package, typ types.T
|
||||
default:
|
||||
panic("unknown basic type")
|
||||
}
|
||||
case *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Signature, *types.Slice:
|
||||
case *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Signature, *types.Slice, *types.Array:
|
||||
return ast.NewIdent("nil")
|
||||
case *types.Struct:
|
||||
texpr := TypeExpr(fset, f, pkg, typ) // typ because we want the name here.
|
||||
@@ -60,21 +61,23 @@ func ZeroValue(fset *token.FileSet, f *ast.File, pkg *types.Package, typ types.T
|
||||
return &ast.CompositeLit{
|
||||
Type: texpr,
|
||||
}
|
||||
case *types.Array:
|
||||
texpr := TypeExpr(fset, f, pkg, u.Elem())
|
||||
if texpr == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.CompositeLit{
|
||||
Type: &ast.ArrayType{
|
||||
Elt: texpr,
|
||||
Len: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%v", u.Len())},
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsZeroValue checks whether the given expression is a 'zero value' (as determined by output of
|
||||
// analysisinternal.ZeroValue)
|
||||
func IsZeroValue(expr ast.Expr) bool {
|
||||
switch e := expr.(type) {
|
||||
case *ast.BasicLit:
|
||||
return e.Value == "0" || e.Value == `""`
|
||||
case *ast.Ident:
|
||||
return e.Name == "nil" || e.Name == "false"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func TypeExpr(fset *token.FileSet, f *ast.File, pkg *types.Package, typ types.Type) ast.Expr {
|
||||
switch t := typ.(type) {
|
||||
case *types.Basic:
|
||||
@@ -195,8 +198,12 @@ func TypeExpr(fset *token.FileSet, f *ast.File, pkg *types.Package, typ types.Ty
|
||||
X: ast.NewIdent(pkgName),
|
||||
Sel: ast.NewIdent(t.Obj().Name()),
|
||||
}
|
||||
case *types.Struct:
|
||||
return ast.NewIdent(t.String())
|
||||
case *types.Interface:
|
||||
return ast.NewIdent(t.String())
|
||||
default:
|
||||
return nil // TODO: anonymous structs, but who does that
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,3 +288,138 @@ func baseIfStmt(path []ast.Node, index int) ast.Stmt {
|
||||
}
|
||||
return stmt.(ast.Stmt)
|
||||
}
|
||||
|
||||
// WalkASTWithParent walks the AST rooted at n. The semantics are
|
||||
// similar to ast.Inspect except it does not call f(nil).
|
||||
func WalkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) {
|
||||
var ancestors []ast.Node
|
||||
ast.Inspect(n, func(n ast.Node) (recurse bool) {
|
||||
if n == nil {
|
||||
ancestors = ancestors[:len(ancestors)-1]
|
||||
return false
|
||||
}
|
||||
|
||||
var parent ast.Node
|
||||
if len(ancestors) > 0 {
|
||||
parent = ancestors[len(ancestors)-1]
|
||||
}
|
||||
ancestors = append(ancestors, n)
|
||||
return f(n, parent)
|
||||
})
|
||||
}
|
||||
|
||||
// FindMatchingIdents finds all identifiers in 'node' that match any of the given types.
|
||||
// 'pos' represents the position at which the identifiers may be inserted. 'pos' must be within
|
||||
// the scope of each of identifier we select. Otherwise, we will insert a variable at 'pos' that
|
||||
// is unrecognized.
|
||||
func FindMatchingIdents(typs []types.Type, node ast.Node, pos token.Pos, info *types.Info, pkg *types.Package) map[types.Type][]*ast.Ident {
|
||||
matches := map[types.Type][]*ast.Ident{}
|
||||
// Initialize matches to contain the variable types we are searching for.
|
||||
for _, typ := range typs {
|
||||
if typ == nil {
|
||||
continue
|
||||
}
|
||||
matches[typ] = []*ast.Ident{}
|
||||
}
|
||||
seen := map[types.Object]struct{}{}
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
if n == nil {
|
||||
return false
|
||||
}
|
||||
// Prevent circular definitions. If 'pos' is within an assignment statement, do not
|
||||
// allow any identifiers in that assignment statement to be selected. Otherwise,
|
||||
// we could do the following, where 'x' satisfies the type of 'f0':
|
||||
//
|
||||
// x := fakeStruct{f0: x}
|
||||
//
|
||||
assignment, ok := n.(*ast.AssignStmt)
|
||||
if ok && pos > assignment.Pos() && pos <= assignment.End() {
|
||||
return false
|
||||
}
|
||||
if n.End() > pos {
|
||||
return n.Pos() <= pos
|
||||
}
|
||||
ident, ok := n.(*ast.Ident)
|
||||
if !ok || ident.Name == "_" {
|
||||
return true
|
||||
}
|
||||
obj := info.Defs[ident]
|
||||
if obj == nil || obj.Type() == nil {
|
||||
return true
|
||||
}
|
||||
if _, ok := obj.(*types.TypeName); ok {
|
||||
return true
|
||||
}
|
||||
// Prevent duplicates in matches' values.
|
||||
if _, ok = seen[obj]; ok {
|
||||
return true
|
||||
}
|
||||
seen[obj] = struct{}{}
|
||||
// Find the scope for the given position. Then, check whether the object
|
||||
// exists within the scope.
|
||||
innerScope := pkg.Scope().Innermost(pos)
|
||||
if innerScope == nil {
|
||||
return true
|
||||
}
|
||||
_, foundObj := innerScope.LookupParent(ident.Name, pos)
|
||||
if foundObj != obj {
|
||||
return true
|
||||
}
|
||||
// The object must match one of the types that we are searching for.
|
||||
if idents, ok := matches[obj.Type()]; ok {
|
||||
matches[obj.Type()] = append(idents, ast.NewIdent(ident.Name))
|
||||
}
|
||||
// If the object type does not exactly match any of the target types, greedily
|
||||
// find the first target type that the object type can satisfy.
|
||||
for typ := range matches {
|
||||
if obj.Type() == typ {
|
||||
continue
|
||||
}
|
||||
if equivalentTypes(obj.Type(), typ) {
|
||||
matches[typ] = append(matches[typ], ast.NewIdent(ident.Name))
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
return matches
|
||||
}
|
||||
|
||||
func equivalentTypes(want, got types.Type) bool {
|
||||
if want == got || types.Identical(want, got) {
|
||||
return true
|
||||
}
|
||||
// Code segment to help check for untyped equality from (golang/go#32146).
|
||||
if rhs, ok := want.(*types.Basic); ok && rhs.Info()&types.IsUntyped > 0 {
|
||||
if lhs, ok := got.Underlying().(*types.Basic); ok {
|
||||
return rhs.Info()&types.IsConstType == lhs.Info()&types.IsConstType
|
||||
}
|
||||
}
|
||||
return types.AssignableTo(want, got)
|
||||
}
|
||||
|
||||
// FindBestMatch employs fuzzy matching to evaluate the similarity of each given identifier to the
|
||||
// given pattern. We return the identifier whose name is most similar to the pattern.
|
||||
func FindBestMatch(pattern string, idents []*ast.Ident) ast.Expr {
|
||||
fuzz := fuzzy.NewMatcher(pattern)
|
||||
var bestFuzz ast.Expr
|
||||
highScore := float32(0) // minimum score is 0 (no match)
|
||||
for _, ident := range idents {
|
||||
// TODO: Improve scoring algorithm.
|
||||
score := fuzz.Score(ident.Name)
|
||||
if score > highScore {
|
||||
highScore = score
|
||||
bestFuzz = ident
|
||||
} else if score == 0 {
|
||||
// Order matters in the fuzzy matching algorithm. If we find no match
|
||||
// when matching the target to the identifier, try matching the identifier
|
||||
// to the target.
|
||||
revFuzz := fuzzy.NewMatcher(ident.Name)
|
||||
revScore := revFuzz.Score(pattern)
|
||||
if revScore > highScore {
|
||||
highScore = revScore
|
||||
bestFuzz = ident
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestFuzz
|
||||
}
|
||||
|
||||
168
vendor/golang.org/x/tools/internal/lsp/fuzzy/input.go
generated
vendored
Normal file
168
vendor/golang.org/x/tools/internal/lsp/fuzzy/input.go
generated
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package fuzzy
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// RuneRole specifies the role of a rune in the context of an input.
|
||||
type RuneRole byte
|
||||
|
||||
const (
|
||||
// RNone specifies a rune without any role in the input (i.e., whitespace/non-ASCII).
|
||||
RNone RuneRole = iota
|
||||
// RSep specifies a rune with the role of segment separator.
|
||||
RSep
|
||||
// RTail specifies a rune which is a lower-case tail in a word in the input.
|
||||
RTail
|
||||
// RUCTail specifies a rune which is an upper-case tail in a word in the input.
|
||||
RUCTail
|
||||
// RHead specifies a rune which is the first character in a word in the input.
|
||||
RHead
|
||||
)
|
||||
|
||||
// RuneRoles detects the roles of each byte rune in an input string and stores it in the output
|
||||
// slice. The rune role depends on the input type. Stops when it parsed all the runes in the string
|
||||
// or when it filled the output. If output is nil, then it gets created.
|
||||
func RuneRoles(str string, reuse []RuneRole) []RuneRole {
|
||||
var output []RuneRole
|
||||
if cap(reuse) < len(str) {
|
||||
output = make([]RuneRole, 0, len(str))
|
||||
} else {
|
||||
output = reuse[:0]
|
||||
}
|
||||
|
||||
prev, prev2 := rtNone, rtNone
|
||||
for i := 0; i < len(str); i++ {
|
||||
r := rune(str[i])
|
||||
|
||||
role := RNone
|
||||
|
||||
curr := rtLower
|
||||
if str[i] <= unicode.MaxASCII {
|
||||
curr = runeType(rt[str[i]] - '0')
|
||||
}
|
||||
|
||||
if curr == rtLower {
|
||||
if prev == rtNone || prev == rtPunct {
|
||||
role = RHead
|
||||
} else {
|
||||
role = RTail
|
||||
}
|
||||
} else if curr == rtUpper {
|
||||
role = RHead
|
||||
|
||||
if prev == rtUpper {
|
||||
// This and previous characters are both upper case.
|
||||
|
||||
if i+1 == len(str) {
|
||||
// This is last character, previous was also uppercase -> this is UCTail
|
||||
// i.e., (current char is C): aBC / BC / ABC
|
||||
role = RUCTail
|
||||
}
|
||||
}
|
||||
} else if curr == rtPunct {
|
||||
switch r {
|
||||
case '.', ':':
|
||||
role = RSep
|
||||
}
|
||||
}
|
||||
if curr != rtLower {
|
||||
if i > 1 && output[i-1] == RHead && prev2 == rtUpper && (output[i-2] == RHead || output[i-2] == RUCTail) {
|
||||
// The previous two characters were uppercase. The current one is not a lower case, so the
|
||||
// previous one can't be a HEAD. Make it a UCTail.
|
||||
// i.e., (last char is current char - B must be a UCTail): ABC / ZABC / AB.
|
||||
output[i-1] = RUCTail
|
||||
}
|
||||
}
|
||||
|
||||
output = append(output, role)
|
||||
prev2 = prev
|
||||
prev = curr
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
type runeType byte
|
||||
|
||||
const (
|
||||
rtNone runeType = iota
|
||||
rtPunct
|
||||
rtLower
|
||||
rtUpper
|
||||
)
|
||||
|
||||
const rt = "00000000000000000000000000000000000000000000001122222222221000000333333333333333333333333330000002222222222222222222222222200000"
|
||||
|
||||
// LastSegment returns the substring representing the last segment from the input, where each
|
||||
// byte has an associated RuneRole in the roles slice. This makes sense only for inputs of Symbol
|
||||
// or Filename type.
|
||||
func LastSegment(input string, roles []RuneRole) string {
|
||||
// Exclude ending separators.
|
||||
end := len(input) - 1
|
||||
for end >= 0 && roles[end] == RSep {
|
||||
end--
|
||||
}
|
||||
if end < 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
start := end - 1
|
||||
for start >= 0 && roles[start] != RSep {
|
||||
start--
|
||||
}
|
||||
|
||||
return input[start+1 : end+1]
|
||||
}
|
||||
|
||||
// ToLower transforms the input string to lower case, which is stored in the output byte slice.
|
||||
// The lower casing considers only ASCII values - non ASCII values are left unmodified.
|
||||
// Stops when parsed all input or when it filled the output slice. If output is nil, then it gets
|
||||
// created.
|
||||
func ToLower(input string, reuse []byte) []byte {
|
||||
output := reuse
|
||||
if cap(reuse) < len(input) {
|
||||
output = make([]byte, len(input))
|
||||
}
|
||||
|
||||
for i := 0; i < len(input); i++ {
|
||||
r := rune(input[i])
|
||||
if r <= unicode.MaxASCII {
|
||||
if 'A' <= r && r <= 'Z' {
|
||||
r += 'a' - 'A'
|
||||
}
|
||||
}
|
||||
output[i] = byte(r)
|
||||
}
|
||||
return output[:len(input)]
|
||||
}
|
||||
|
||||
// WordConsumer defines a consumer for a word delimited by the [start,end) byte offsets in an input
|
||||
// (start is inclusive, end is exclusive).
|
||||
type WordConsumer func(start, end int)
|
||||
|
||||
// Words find word delimiters in an input based on its bytes' mappings to rune roles. The offset
|
||||
// delimiters for each word are fed to the provided consumer function.
|
||||
func Words(roles []RuneRole, consume WordConsumer) {
|
||||
var wordStart int
|
||||
for i, r := range roles {
|
||||
switch r {
|
||||
case RUCTail, RTail:
|
||||
case RHead, RNone, RSep:
|
||||
if i != wordStart {
|
||||
consume(wordStart, i)
|
||||
}
|
||||
wordStart = i
|
||||
if r != RHead {
|
||||
// Skip this character.
|
||||
wordStart = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if wordStart != len(roles) {
|
||||
consume(wordStart, len(roles))
|
||||
}
|
||||
}
|
||||
398
vendor/golang.org/x/tools/internal/lsp/fuzzy/matcher.go
generated
vendored
Normal file
398
vendor/golang.org/x/tools/internal/lsp/fuzzy/matcher.go
generated
vendored
Normal file
@@ -0,0 +1,398 @@
|
||||
// Copyright 2019 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package fuzzy implements a fuzzy matching algorithm.
|
||||
package fuzzy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxInputSize is the maximum size of the input scored against the fuzzy matcher. Longer inputs
|
||||
// will be truncated to this size.
|
||||
MaxInputSize = 127
|
||||
// MaxPatternSize is the maximum size of the pattern used to construct the fuzzy matcher. Longer
|
||||
// inputs are truncated to this size.
|
||||
MaxPatternSize = 63
|
||||
)
|
||||
|
||||
type scoreVal int
|
||||
|
||||
func (s scoreVal) val() int {
|
||||
return int(s) >> 1
|
||||
}
|
||||
|
||||
func (s scoreVal) prevK() int {
|
||||
return int(s) & 1
|
||||
}
|
||||
|
||||
func score(val int, prevK int /*0 or 1*/) scoreVal {
|
||||
return scoreVal(val<<1 + prevK)
|
||||
}
|
||||
|
||||
// Matcher implements a fuzzy matching algorithm for scoring candidates against a pattern.
|
||||
// The matcher does not support parallel usage.
|
||||
type Matcher struct {
|
||||
pattern string
|
||||
patternLower []byte // lower-case version of the pattern
|
||||
patternShort []byte // first characters of the pattern
|
||||
caseSensitive bool // set if the pattern is mix-cased
|
||||
|
||||
patternRoles []RuneRole // the role of each character in the pattern
|
||||
roles []RuneRole // the role of each character in the tested string
|
||||
|
||||
scores [MaxInputSize + 1][MaxPatternSize + 1][2]scoreVal
|
||||
|
||||
scoreScale float32
|
||||
|
||||
lastCandidateLen int // in bytes
|
||||
lastCandidateMatched bool
|
||||
|
||||
// Here we save the last candidate in lower-case. This is basically a byte slice we reuse for
|
||||
// performance reasons, so the slice is not reallocated for every candidate.
|
||||
lowerBuf [MaxInputSize]byte
|
||||
rolesBuf [MaxInputSize]RuneRole
|
||||
}
|
||||
|
||||
func (m *Matcher) bestK(i, j int) int {
|
||||
if m.scores[i][j][0].val() < m.scores[i][j][1].val() {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// NewMatcher returns a new fuzzy matcher for scoring candidates against the provided pattern.
|
||||
func NewMatcher(pattern string) *Matcher {
|
||||
if len(pattern) > MaxPatternSize {
|
||||
pattern = pattern[:MaxPatternSize]
|
||||
}
|
||||
|
||||
m := &Matcher{
|
||||
pattern: pattern,
|
||||
patternLower: ToLower(pattern, nil),
|
||||
}
|
||||
|
||||
for i, c := range m.patternLower {
|
||||
if pattern[i] != c {
|
||||
m.caseSensitive = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(pattern) > 3 {
|
||||
m.patternShort = m.patternLower[:3]
|
||||
} else {
|
||||
m.patternShort = m.patternLower
|
||||
}
|
||||
|
||||
m.patternRoles = RuneRoles(pattern, nil)
|
||||
|
||||
if len(pattern) > 0 {
|
||||
maxCharScore := 4
|
||||
m.scoreScale = 1 / float32(maxCharScore*len(pattern))
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Score returns the score returned by matching the candidate to the pattern.
|
||||
// This is not designed for parallel use. Multiple candidates must be scored sequentially.
|
||||
// Returns a score between 0 and 1 (0 - no match, 1 - perfect match).
|
||||
func (m *Matcher) Score(candidate string) float32 {
|
||||
if len(candidate) > MaxInputSize {
|
||||
candidate = candidate[:MaxInputSize]
|
||||
}
|
||||
lower := ToLower(candidate, m.lowerBuf[:])
|
||||
m.lastCandidateLen = len(candidate)
|
||||
|
||||
if len(m.pattern) == 0 {
|
||||
// Empty patterns perfectly match candidates.
|
||||
return 1
|
||||
}
|
||||
|
||||
if m.match(candidate, lower) {
|
||||
sc := m.computeScore(candidate, lower)
|
||||
if sc > minScore/2 && !m.poorMatch() {
|
||||
m.lastCandidateMatched = true
|
||||
if len(m.pattern) == len(candidate) {
|
||||
// Perfect match.
|
||||
return 1
|
||||
}
|
||||
|
||||
if sc < 0 {
|
||||
sc = 0
|
||||
}
|
||||
normalizedScore := float32(sc) * m.scoreScale
|
||||
if normalizedScore > 1 {
|
||||
normalizedScore = 1
|
||||
}
|
||||
|
||||
return normalizedScore
|
||||
}
|
||||
}
|
||||
|
||||
m.lastCandidateMatched = false
|
||||
return 0
|
||||
}
|
||||
|
||||
const minScore = -10000
|
||||
|
||||
// MatchedRanges returns matches ranges for the last scored string as a flattened array of
|
||||
// [begin, end) byte offset pairs.
|
||||
func (m *Matcher) MatchedRanges() []int {
|
||||
if len(m.pattern) == 0 || !m.lastCandidateMatched {
|
||||
return nil
|
||||
}
|
||||
i, j := m.lastCandidateLen, len(m.pattern)
|
||||
if m.scores[i][j][0].val() < minScore/2 && m.scores[i][j][1].val() < minScore/2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ret []int
|
||||
k := m.bestK(i, j)
|
||||
for i > 0 {
|
||||
take := (k == 1)
|
||||
k = m.scores[i][j][k].prevK()
|
||||
if take {
|
||||
if len(ret) == 0 || ret[len(ret)-1] != i {
|
||||
ret = append(ret, i)
|
||||
ret = append(ret, i-1)
|
||||
} else {
|
||||
ret[len(ret)-1] = i - 1
|
||||
}
|
||||
j--
|
||||
}
|
||||
i--
|
||||
}
|
||||
// Reverse slice.
|
||||
for i := 0; i < len(ret)/2; i++ {
|
||||
ret[i], ret[len(ret)-1-i] = ret[len(ret)-1-i], ret[i]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (m *Matcher) match(candidate string, candidateLower []byte) bool {
|
||||
i, j := 0, 0
|
||||
for ; i < len(candidateLower) && j < len(m.patternLower); i++ {
|
||||
if candidateLower[i] == m.patternLower[j] {
|
||||
j++
|
||||
}
|
||||
}
|
||||
if j != len(m.patternLower) {
|
||||
return false
|
||||
}
|
||||
|
||||
// The input passes the simple test against pattern, so it is time to classify its characters.
|
||||
// Character roles are used below to find the last segment.
|
||||
m.roles = RuneRoles(candidate, m.rolesBuf[:])
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Matcher) computeScore(candidate string, candidateLower []byte) int {
|
||||
pattLen, candLen := len(m.pattern), len(candidate)
|
||||
|
||||
for j := 0; j <= len(m.pattern); j++ {
|
||||
m.scores[0][j][0] = minScore << 1
|
||||
m.scores[0][j][1] = minScore << 1
|
||||
}
|
||||
m.scores[0][0][0] = score(0, 0) // Start with 0.
|
||||
|
||||
segmentsLeft, lastSegStart := 1, 0
|
||||
for i := 0; i < candLen; i++ {
|
||||
if m.roles[i] == RSep {
|
||||
segmentsLeft++
|
||||
lastSegStart = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
// A per-character bonus for a consecutive match.
|
||||
consecutiveBonus := 2
|
||||
wordIdx := 0 // Word count within segment.
|
||||
for i := 1; i <= candLen; i++ {
|
||||
|
||||
role := m.roles[i-1]
|
||||
isHead := role == RHead
|
||||
|
||||
if isHead {
|
||||
wordIdx++
|
||||
} else if role == RSep && segmentsLeft > 1 {
|
||||
wordIdx = 0
|
||||
segmentsLeft--
|
||||
}
|
||||
|
||||
var skipPenalty int
|
||||
if i == 1 || (i-1) == lastSegStart {
|
||||
// Skipping the start of first or last segment.
|
||||
skipPenalty++
|
||||
}
|
||||
|
||||
for j := 0; j <= pattLen; j++ {
|
||||
// By default, we don't have a match. Fill in the skip data.
|
||||
m.scores[i][j][1] = minScore << 1
|
||||
|
||||
// Compute the skip score.
|
||||
k := 0
|
||||
if m.scores[i-1][j][0].val() < m.scores[i-1][j][1].val() {
|
||||
k = 1
|
||||
}
|
||||
|
||||
skipScore := m.scores[i-1][j][k].val()
|
||||
// Do not penalize missing characters after the last matched segment.
|
||||
if j != pattLen {
|
||||
skipScore -= skipPenalty
|
||||
}
|
||||
m.scores[i][j][0] = score(skipScore, k)
|
||||
|
||||
if j == 0 || candidateLower[i-1] != m.patternLower[j-1] {
|
||||
// Not a match.
|
||||
continue
|
||||
}
|
||||
pRole := m.patternRoles[j-1]
|
||||
|
||||
if role == RTail && pRole == RHead {
|
||||
if j > 1 {
|
||||
// Not a match: a head in the pattern matches a tail character in the candidate.
|
||||
continue
|
||||
}
|
||||
// Special treatment for the first character of the pattern. We allow
|
||||
// matches in the middle of a word if they are long enough, at least
|
||||
// min(3, pattern.length) characters.
|
||||
if !bytes.HasPrefix(candidateLower[i-1:], m.patternShort) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Compute the char score.
|
||||
var charScore int
|
||||
// Bonus 1: the char is in the candidate's last segment.
|
||||
if segmentsLeft <= 1 {
|
||||
charScore++
|
||||
}
|
||||
// Bonus 2: Case match or a Head in the pattern aligns with one in the word.
|
||||
// Single-case patterns lack segmentation signals and we assume any character
|
||||
// can be a head of a segment.
|
||||
if candidate[i-1] == m.pattern[j-1] || role == RHead && (!m.caseSensitive || pRole == RHead) {
|
||||
charScore++
|
||||
}
|
||||
|
||||
// Penalty 1: pattern char is Head, candidate char is Tail.
|
||||
if role == RTail && pRole == RHead {
|
||||
charScore--
|
||||
}
|
||||
// Penalty 2: first pattern character matched in the middle of a word.
|
||||
if j == 1 && role == RTail {
|
||||
charScore -= 4
|
||||
}
|
||||
|
||||
// Third dimension encodes whether there is a gap between the previous match and the current
|
||||
// one.
|
||||
for k := 0; k < 2; k++ {
|
||||
sc := m.scores[i-1][j-1][k].val() + charScore
|
||||
|
||||
isConsecutive := k == 1 || i-1 == 0 || i-1 == lastSegStart
|
||||
if isConsecutive {
|
||||
// Bonus 3: a consecutive match. First character match also gets a bonus to
|
||||
// ensure prefix final match score normalizes to 1.0.
|
||||
// Logically, this is a part of charScore, but we have to compute it here because it
|
||||
// only applies for consecutive matches (k == 1).
|
||||
sc += consecutiveBonus
|
||||
}
|
||||
if k == 0 {
|
||||
// Penalty 3: Matching inside a segment (and previous char wasn't matched). Penalize for the lack
|
||||
// of alignment.
|
||||
if role == RTail || role == RUCTail {
|
||||
sc -= 3
|
||||
}
|
||||
}
|
||||
|
||||
if sc > m.scores[i][j][1].val() {
|
||||
m.scores[i][j][1] = score(sc, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := m.scores[len(candidate)][len(m.pattern)][m.bestK(len(candidate), len(m.pattern))].val()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ScoreTable returns the score table computed for the provided candidate. Used only for debugging.
|
||||
func (m *Matcher) ScoreTable(candidate string) string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
var line1, line2, separator bytes.Buffer
|
||||
line1.WriteString("\t")
|
||||
line2.WriteString("\t")
|
||||
for j := 0; j < len(m.pattern); j++ {
|
||||
line1.WriteString(fmt.Sprintf("%c\t\t", m.pattern[j]))
|
||||
separator.WriteString("----------------")
|
||||
}
|
||||
|
||||
buf.WriteString(line1.String())
|
||||
buf.WriteString("\n")
|
||||
buf.WriteString(separator.String())
|
||||
buf.WriteString("\n")
|
||||
|
||||
for i := 1; i <= len(candidate); i++ {
|
||||
line1.Reset()
|
||||
line2.Reset()
|
||||
|
||||
line1.WriteString(fmt.Sprintf("%c\t", candidate[i-1]))
|
||||
line2.WriteString("\t")
|
||||
|
||||
for j := 1; j <= len(m.pattern); j++ {
|
||||
line1.WriteString(fmt.Sprintf("M%6d(%c)\t", m.scores[i][j][0].val(), dir(m.scores[i][j][0].prevK())))
|
||||
line2.WriteString(fmt.Sprintf("H%6d(%c)\t", m.scores[i][j][1].val(), dir(m.scores[i][j][1].prevK())))
|
||||
}
|
||||
buf.WriteString(line1.String())
|
||||
buf.WriteString("\n")
|
||||
buf.WriteString(line2.String())
|
||||
buf.WriteString("\n")
|
||||
buf.WriteString(separator.String())
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func dir(prevK int) rune {
|
||||
if prevK == 0 {
|
||||
return 'M'
|
||||
}
|
||||
return 'H'
|
||||
}
|
||||
|
||||
func (m *Matcher) poorMatch() bool {
|
||||
if len(m.pattern) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
i, j := m.lastCandidateLen, len(m.pattern)
|
||||
k := m.bestK(i, j)
|
||||
|
||||
var counter, len int
|
||||
for i > 0 {
|
||||
take := (k == 1)
|
||||
k = m.scores[i][j][k].prevK()
|
||||
if take {
|
||||
len++
|
||||
if k == 0 && len < 3 && m.roles[i-1] == RTail {
|
||||
// Short match in the middle of a word
|
||||
counter++
|
||||
if counter > 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
j--
|
||||
} else {
|
||||
len = 0
|
||||
}
|
||||
i--
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user