From faf84b722078151b68518649a308e28cc73a5b53 Mon Sep 17 00:00:00 2001 From: wangjin Date: Mon, 30 Sep 2024 13:14:43 +0800 Subject: [PATCH] add similar annotation or annotation parameter if it is not found --- src/compile/compiler.go | 30 +++- src/compile/generate.go | 2 +- src/compile/next.go | 2 +- src/compile/template.go | 23 +-- src/grammar/grammar.go | 27 +--- src/{ => internal}/fsutil/fsutil.go | 0 src/internal/stringutil/stringutil.go | 138 ++++++++++++++++++ website/example/gen/c/demo/demo.next.h | 2 +- website/example/gen/cpp/demo.h | 2 +- website/example/gen/csharp/demo/demo.next.cs | 2 +- website/example/gen/go/demo/demo.next.go | 2 +- .../gen/java/com/example/demo/User.java | 20 +-- website/example/gen/js/demo.js | 4 +- website/example/gen/lua/demo.lua | 2 +- website/example/gen/php/demo.php | 4 +- website/example/gen/protobuf/demo.proto | 4 +- website/example/gen/python/demo.py | 2 +- website/example/gen/rust/src/demo/demo.rs | 2 +- website/example/gen/ts/demo.ts | 2 +- website/example/next/demo.next | 3 +- 20 files changed, 199 insertions(+), 74 deletions(-) rename src/{ => internal}/fsutil/fsutil.go (100%) create mode 100644 src/internal/stringutil/stringutil.go diff --git a/src/compile/compiler.go b/src/compile/compiler.go index c9347f6..88cc752 100644 --- a/src/compile/compiler.go +++ b/src/compile/compiler.go @@ -16,6 +16,7 @@ import ( "github.com/next/next/src/ast" "github.com/next/next/src/constant" "github.com/next/next/src/grammar" + "github.com/next/next/src/internal/stringutil" "github.com/next/next/src/scanner" "github.com/next/next/src/token" ) @@ -950,7 +951,15 @@ func (c *Compiler) validateAnnotations(node Node, annotations grammar.Annotation for name, annotation := range node.Annotations() { ga := grammar.LookupAnnotation(annotations, name) if ga == nil { - c.addErrorf(annotation.Pos().pos, "annotation %s not supported", name) + s, score := stringutil.FindBestMatchFunc(slices.All(annotations), name, stringutil.DefaultSimilarityThreshold, func(i int, a grammar.Options[grammar.Annotation]) string { + return a.Value().Name + }) + if score > 0 { + fmt.Printf("FIXME: score=%v\n", score) + c.addErrorf(annotation.Pos().pos, "annotation %s not supported, did you mean %s?", name, s) + } else { + c.addErrorf(annotation.Pos().pos, "annotation %s not supported", name) + } continue } for p, v := range annotation { @@ -960,7 +969,24 @@ func (c *Compiler) validateAnnotations(node Node, annotations grammar.Annotation } gp := grammar.LookupAnnotationParameter(ga.Parameters, p) if gp == nil { - c.addErrorf(annotation.NamePos(p).pos, "annotation %s: parameter %s not supported", name, p) + s, score := stringutil.FindBestMatchFunc(slices.All(ga.Parameters), p, stringutil.DefaultSimilarityThreshold, func(i int, a grammar.Options[grammar.AnnotationParameter]) string { + name := a.Value().Name + if strings.HasPrefix(name, ".+_") { + if index := strings.Index(p, "_"); index > 0 { + return p[:index] + strings.TrimPrefix(name, ".+") + } + } else if strings.HasSuffix(name, "_.+") { + if index := strings.LastIndex(p, "_"); index > 0 { + return strings.TrimSuffix(name, "_.+") + p[index:] + } + } + return name + }) + if score > 0 { + c.addErrorf(annotation.NamePos(p).pos, "annotation %s: parameter %s not supported, did you mean %s?", name, p, s) + } else { + c.addErrorf(annotation.NamePos(p).pos, "annotation %s: parameter %s not supported", name, p) + } continue } rv := reflect.ValueOf(v) diff --git a/src/compile/generate.go b/src/compile/generate.go index dbb9933..5870e1c 100644 --- a/src/compile/generate.go +++ b/src/compile/generate.go @@ -15,7 +15,7 @@ import ( "github.com/gopherd/core/flags" "github.com/gopherd/core/op" - "github.com/next/next/src/fsutil" + "github.com/next/next/src/internal/fsutil" ) const ( diff --git a/src/compile/next.go b/src/compile/next.go index 87768d9..a4bbc55 100644 --- a/src/compile/next.go +++ b/src/compile/next.go @@ -19,8 +19,8 @@ import ( "github.com/gopherd/core/flags" "github.com/gopherd/core/op" "github.com/gopherd/core/term" - "github.com/next/next/src/fsutil" "github.com/next/next/src/grammar" + "github.com/next/next/src/internal/fsutil" "github.com/next/next/src/parser" "github.com/next/next/src/scanner" ) diff --git a/src/compile/template.go b/src/compile/template.go index 6ef3110..4fa6a33 100644 --- a/src/compile/template.go +++ b/src/compile/template.go @@ -22,7 +22,8 @@ import ( "github.com/gopherd/core/text/document" "github.com/gopherd/core/text/templates" - "github.com/next/next/src/fsutil" + "github.com/next/next/src/internal/fsutil" + "github.com/next/next/src/internal/stringutil" ) // StubPrefix is the prefix for stub templates. @@ -39,26 +40,8 @@ func (m Meta) lookup(key string) pair.Pair[string, bool] { return pair.New(v, ok) } -func isIdentifer(s string) bool { - if len(s) == 0 { - return false - } - for i, r := range s { - if i == 0 { - if !('a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || r == '_') { - return false - } - } else { - if !('a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || '0' <= r && r <= '9' || r == '_') { - return false - } - } - } - return true -} - func validMetaKey(key string) error { - if !isIdentifer(key) { + if !stringutil.IsIdentifer(key) { return fmt.Errorf("must be a valid identifier") } if strings.HasPrefix(key, "_") || key == "this" || key == "path" || key == "skip" { diff --git a/src/grammar/grammar.go b/src/grammar/grammar.go index 0b01186..8b5554d 100644 --- a/src/grammar/grammar.go +++ b/src/grammar/grammar.go @@ -10,6 +10,7 @@ import ( "github.com/gopherd/core/container/iters" "github.com/gopherd/core/text/templates" + "github.com/next/next/src/internal/stringutil" ) const ( @@ -280,30 +281,6 @@ type AnnotationParameter struct { } `json:"-"` } -func isIdentifer(s string) bool { - r := []rune(s) - if len(r) == 0 { - return false - } - if !isLetter(r[0]) || r[0] == '_' { - return false - } - for i := 1; i < len(r); i++ { - if !isLetter(r[i]) && !isDigit(r[i]) && r[i] != '_' { - return false - } - } - return true -} - -func isDigit(r rune) bool { - return '0' <= r && r <= '9' -} - -func isLetter(r rune) bool { - return 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' -} - func LookupAnnotationParameter(parameters []Options[AnnotationParameter], name string) *AnnotationParameter { for i := range parameters { p := ¶meters[i].value @@ -1032,7 +1009,7 @@ func (p *AnnotationParameter) validate() error { if p.Name == "" { return fmt.Errorf("parameter name is required") } - if !isIdentifer(p.Name) { + if !stringutil.IsIdentifer(p.Name) { pattern, err := regexp.Compile("^" + p.Name + "$") if err != nil { return fmt.Errorf("invalid parameter name pattern %q: %w", p.Name, err) diff --git a/src/fsutil/fsutil.go b/src/internal/fsutil/fsutil.go similarity index 100% rename from src/fsutil/fsutil.go rename to src/internal/fsutil/fsutil.go diff --git a/src/internal/stringutil/stringutil.go b/src/internal/stringutil/stringutil.go new file mode 100644 index 0000000..f916983 --- /dev/null +++ b/src/internal/stringutil/stringutil.go @@ -0,0 +1,138 @@ +// Package cmdsim provides functionality for finding the best matching command +// based on the Jaro-Winkler distance algorithm. +package stringutil + +import ( + "iter" + "slices" + "unicode/utf8" +) + +// DefaultSimilarityThreshold is the default similarity threshold for the Jaro-Winkler distance. +const DefaultSimilarityThreshold = 0.7 + +// JaroWinklerDistance calculates the Jaro-Winkler distance between two strings. +// The result is a value between 0 and 1, where 1 indicates a perfect match. +// This function is case-sensitive. +func JaroWinklerDistance(s1, s2 string) float64 { + // If strings are identical, return 1 + if s1 == s2 { + return 1 + } + + // Get string lengths + len1 := utf8.RuneCountInString(s1) + len2 := utf8.RuneCountInString(s2) + + // Calculate match window + maxDist := max(len1, len2)/2 - 1 + + // Initialize match and transposition counts + matches := 0 + transpositions := 0 + matched1 := make([]bool, len1) + matched2 := make([]bool, len2) + + // Count matching characters + for i, r1 := range s1 { + start := max(0, i-maxDist) + end := min(i+maxDist+1, len2) + for j := start; j < end; j++ { + if !matched2[j] && r1 == rune(s2[j]) { + matched1[i] = true + matched2[j] = true + matches++ + break + } + } + } + + // If no matches, return 0 + if matches == 0 { + return 0 + } + + // Count transpositions + j := 0 + for i, r1 := range s1 { + if matched1[i] { + for !matched2[j] { + j++ + } + if r1 != rune(s2[j]) { + transpositions++ + } + j++ + } + } + + // Calculate Jaro distance + jaro := (float64(matches)/float64(len1) + + float64(matches)/float64(len2) + + float64(matches-transpositions/2)/float64(matches)) / 3.0 + + // Calculate common prefix length (up to 4 characters) + prefixLen := 0 + for i := 0; i < min(min(len1, len2), 4); i++ { + if s1[i] == s2[i] { + prefixLen++ + } else { + break + } + } + + // Calculate and return Jaro-Winkler distance + return jaro + float64(prefixLen)*0.1*(1-jaro) +} + +// FindBestMatchFunc finds the best matching value and its similarity score +// from the given list of values. It returns the best match and its similarity score. +// If no command meets the threshold, it returns an zero key and zero similarity score. +func FindBestMatchFunc[F ~func(K, V) string, K, V any](seq iter.Seq2[K, V], input string, threshold float64, fn F) (string, float64) { + var bestMatch string + var highestSimilarity float64 + + for k, v := range seq { + s := fn(k, v) + sim := JaroWinklerDistance(input, s) + if sim > highestSimilarity { + highestSimilarity = sim + bestMatch = s + } + } + + if highestSimilarity >= threshold { + return bestMatch, highestSimilarity + } + return bestMatch, 0 +} + +// FindBestMatch finds the best matching string to the input +func FindBestMatch(source []string, input string, threshold float64) string { + s, sim := FindBestMatchFunc(slices.All(source), input, threshold, func(i int, s string) string { + return s + }) + if sim > 0 { + return s + } + return "" +} + +// IsIdentifer checks if a string is a valid identifier. +func IsIdentifer(s string) bool { + if len(s) == 0 { + return false + } + for i, r := range s { + if i == 0 { + if !('a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || r == '_') { + return false + } + } else { + if !('a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || '0' <= r && r <= '9' || r == '_') { + return false + } + } + } + return true +} diff --git a/website/example/gen/c/demo/demo.next.h b/website/example/gen/c/demo/demo.next.h index 880c091..6a3fe5b 100644 --- a/website/example/gen/c/demo/demo.next.h +++ b/website/example/gen/c/demo/demo.next.h @@ -75,8 +75,8 @@ typedef struct DEMO_User { void* scores; double coordinates[3]; int32_t matrix[3][2]; - DEMO_Color favoriteColor; char* email; + DEMO_Color favoriteColor; void* extra; } DEMO_User; diff --git a/website/example/gen/cpp/demo.h b/website/example/gen/cpp/demo.h index 4c7420e..59e379a 100644 --- a/website/example/gen/cpp/demo.h +++ b/website/example/gen/cpp/demo.h @@ -86,8 +86,8 @@ class User { std::unordered_map scores; std::array coordinates = {0.0}; std::array, 3> matrix; - Color favoriteColor = {Color(0)}; std::string email = {""}; + Color favoriteColor = {Color(0)}; std::any extra; public: User() = default; diff --git a/website/example/gen/csharp/demo/demo.next.cs b/website/example/gen/csharp/demo/demo.next.cs index b45909e..75764c1 100644 --- a/website/example/gen/csharp/demo/demo.next.cs +++ b/website/example/gen/csharp/demo/demo.next.cs @@ -45,8 +45,8 @@ public class User public Dictionary scores { get; set; } public double[] coordinates { get; set; } public int[][] matrix { get; set; } - public Color favoriteColor { get; set; } public string email { get; set; } + public Color favoriteColor { get; set; } public object extra { get; set; } } diff --git a/website/example/gen/go/demo/demo.next.go b/website/example/gen/go/demo/demo.next.go index b5ac064..552a9b3 100644 --- a/website/example/gen/go/demo/demo.next.go +++ b/website/example/gen/go/demo/demo.next.go @@ -50,8 +50,8 @@ type User struct { Scores map[string]int Coordinates [3]float64 Matrix [3][2]int - FavoriteColor Color Email string + FavoriteColor Color Extra any } diff --git a/website/example/gen/java/com/example/demo/User.java b/website/example/gen/java/com/example/demo/User.java index 55abec4..450a521 100644 --- a/website/example/gen/java/com/example/demo/User.java +++ b/website/example/gen/java/com/example/demo/User.java @@ -70,16 +70,6 @@ public void setMatrix(int[][] matrix) { this.matrix = matrix; } - private Color favoriteColor; - - public Color getFavoriteColor() { - return favoriteColor; - } - - public void setFavoriteColor(Color favoriteColor) { - this.favoriteColor = favoriteColor; - } - private String email; public String getEmail() { @@ -90,6 +80,16 @@ public void setEmail(String email) { this.email = email; } + private Color favoriteColor; + + public Color getFavoriteColor() { + return favoriteColor; + } + + public void setFavoriteColor(Color favoriteColor) { + this.favoriteColor = favoriteColor; + } + private Object extra; public Object getExtra() { diff --git a/website/example/gen/js/demo.js b/website/example/gen/js/demo.js index 0bf984d..659fba4 100644 --- a/website/example/gen/js/demo.js +++ b/website/example/gen/js/demo.js @@ -52,10 +52,10 @@ export class User { this.coordinates = []; /** @type { 'Array<'Array'>' } */ this.matrix = []; - /** @type { 'number' } */ - this.favoriteColor = Color[Object.keys(Color)[0]]; /** @type { String } */ this.email = ""; + /** @type { 'number' } */ + this.favoriteColor = Color[Object.keys(Color)[0]]; /** @type { 'Object' } */ this.extra = null; } diff --git a/website/example/gen/lua/demo.lua b/website/example/gen/lua/demo.lua index 5f6b5d4..313c1b1 100644 --- a/website/example/gen/lua/demo.lua +++ b/website/example/gen/lua/demo.lua @@ -48,8 +48,8 @@ function User:new() scores = {}, coordinates = {}, matrix = {}, - favoriteColor = nil, email = "", + favoriteColor = nil, extra = nil } setmetatable(obj, self) diff --git a/website/example/gen/php/demo.php b/website/example/gen/php/demo.php index 1c27740..f3cbff5 100644 --- a/website/example/gen/php/demo.php +++ b/website/example/gen/php/demo.php @@ -40,8 +40,8 @@ class User public array $scores; public array $coordinates; public array $matrix; - public Color $favoriteColor; public string $email; + public Color $favoriteColor; public mixed $extra; public function __construct() @@ -52,8 +52,8 @@ public function __construct() $this->scores = []; $this->coordinates = []; $this->matrix = []; - $this->favoriteColor = Color::RED; $this->email = ""; + $this->favoriteColor = Color::RED; $this->extra = null; } } diff --git a/website/example/gen/protobuf/demo.proto b/website/example/gen/protobuf/demo.proto index c479cde..1f23a0c 100644 --- a/website/example/gen/protobuf/demo.proto +++ b/website/example/gen/protobuf/demo.proto @@ -31,8 +31,8 @@ message User { map scores = 4; repeated double coordinates = 5; repeated int32 matrix = 6; - Color favoriteColor = 7; - string email = 8 [deprecated = true]; + string email = 7 [deprecated = true]; + Color favoriteColor = 8 [deprecated = true]; google.protobuf.Any extra = 9; } diff --git a/website/example/gen/python/demo.py b/website/example/gen/python/demo.py index 726568a..970282a 100644 --- a/website/example/gen/python/demo.py +++ b/website/example/gen/python/demo.py @@ -44,8 +44,8 @@ def __init__(self): self.scores = {} self.coordinates = [0 for _ in range(3)] self.matrix = [[0 for _ in range(2)] for _ in range(3)] - self.favorite_color = Color(0) self.email = "" + self.favorite_color = Color(0) self.extra = None """ diff --git a/website/example/gen/rust/src/demo/demo.rs b/website/example/gen/rust/src/demo/demo.rs index f722288..e5ba1c0 100644 --- a/website/example/gen/rust/src/demo/demo.rs +++ b/website/example/gen/rust/src/demo/demo.rs @@ -76,8 +76,8 @@ pub struct User { pub scores: HashMap, pub coordinates: [f64; 3], pub matrix: [[i32; 2]; 3], - pub favorite_color: Color, pub email: String, + pub favorite_color: Color, pub extra: Box, } diff --git a/website/example/gen/ts/demo.ts b/website/example/gen/ts/demo.ts index 4db9a14..0f4ebd2 100644 --- a/website/example/gen/ts/demo.ts +++ b/website/example/gen/ts/demo.ts @@ -44,8 +44,8 @@ export class User { scores: Map = new Map(); coordinates: Array = []; matrix: Array> = []; - favoriteColor: Color = 0 as Color; email: string = ""; + favoriteColor: Color = 0 as Color; extra: any = null; } diff --git a/website/example/next/demo.next b/website/example/next/demo.next index e69a9fd..dc2d6ae 100644 --- a/website/example/next/demo.next +++ b/website/example/next/demo.next @@ -46,8 +46,9 @@ struct User { map scores; array coordinates; array, 3> matrix; - Color favoriteColor; @deprecated string email; + @deprecated(message="favoriteColor is deprecated, use tags instead") + Color favoriteColor; any extra; }