Skip to content

Commit

Permalink
add similar annotation or annotation parameter if it is not found
Browse files Browse the repository at this point in the history
  • Loading branch information
mkideal committed Sep 30, 2024
1 parent d3cdde2 commit faf84b7
Show file tree
Hide file tree
Showing 20 changed files with 199 additions and 74 deletions.
30 changes: 28 additions & 2 deletions src/compile/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/compile/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
2 changes: 1 addition & 1 deletion src/compile/next.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
23 changes: 3 additions & 20 deletions src/compile/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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" {
Expand Down
27 changes: 2 additions & 25 deletions src/grammar/grammar.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 := &parameters[i].value
Expand Down Expand Up @@ -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)
Expand Down
File renamed without changes.
138 changes: 138 additions & 0 deletions src/internal/stringutil/stringutil.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion website/example/gen/c/demo/demo.next.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion website/example/gen/cpp/demo.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ class User {
std::unordered_map<std::string, int> scores;
std::array<double, 3> coordinates = {0.0};
std::array<std::array<int, 2>, 3> matrix;
Color favoriteColor = {Color(0)};
std::string email = {""};
Color favoriteColor = {Color(0)};
std::any extra;
public:
User() = default;
Expand Down
2 changes: 1 addition & 1 deletion website/example/gen/csharp/demo/demo.next.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ public class User
public Dictionary<string, int> 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; }
}

Expand Down
2 changes: 1 addition & 1 deletion website/example/gen/go/demo/demo.next.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 10 additions & 10 deletions website/example/gen/java/com/example/demo/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down
4 changes: 2 additions & 2 deletions website/example/gen/js/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ export class User {
this.coordinates = [];
/** @type { 'Array<'Array<Number>'>' } */
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;
}
Expand Down
2 changes: 1 addition & 1 deletion website/example/gen/lua/demo.lua
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ function User:new()
scores = {},
coordinates = {},
matrix = {},
favoriteColor = nil,
email = "",
favoriteColor = nil,
extra = nil
}
setmetatable(obj, self)
Expand Down
Loading

0 comments on commit faf84b7

Please sign in to comment.