diff --git a/examples/gno.land/r/demo/lintest/gnomod.toml b/examples/gno.land/r/demo/lintest/gnomod.toml new file mode 100644 index 00000000000..fad3b830383 --- /dev/null +++ b/examples/gno.land/r/demo/lintest/gnomod.toml @@ -0,0 +1,2 @@ +module = "gno.land/r/demo/lintest" +gno = "0.9" \ No newline at end of file diff --git a/examples/gno.land/r/demo/lintest/lintest.gno b/examples/gno.land/r/demo/lintest/lintest.gno new file mode 100644 index 00000000000..199d641ee54 --- /dev/null +++ b/examples/gno.land/r/demo/lintest/lintest.gno @@ -0,0 +1,41 @@ +package lintest + +import "gno.land/p/nt/avl" + +var lintest *avl.Tree + +type whatever struct{} + +func (w whatever) Iterate(start, end string) + +func init() { + lintest = avl.NewTree() + lintest.Set("key1", "value1") + lintest.Set("key2", "value2") + lintest.Set("key3", "value3") +} + +func JoinValues() string { + result := "" + lintest.Iterate("", "", func(key string, value any) bool { + result += value.(string) + "," + return false + }) + w := whatever{} + w.Iterate("", "") + return result +} + +func JoinValuesReverse() string { + result := "" + //nolint + lintest.ReverseIterate("", "", func(key string, value any) bool { + result += value.(string) + "," + return false + }) + return result +} + +func Render(path string) string { + return "Rendering AVL Tree at path: " + path +} diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index f3fd0fda853..ff5ca5b47ea 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -10,7 +10,9 @@ import ( "path/filepath" "github.com/gnolang/gno/gnovm/cmd/gno/internal/cmdutil" + "github.com/gnolang/gno/gnovm/cmd/gno/lintrules" "github.com/gnolang/gno/gnovm/pkg/gnoenv" + "github.com/gnolang/gno/gnovm/pkg/gnolang" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/gnovm/pkg/packages" @@ -286,6 +288,33 @@ func execLint(cmd *lintCmd, args []string, io commands.IO) error { pn, _ := tm.PreprocessFiles( mpkg.Name, mpkg.Path, fset, false, false, "") ppkg.AddNormal(pn, fset) + + rules := []lintrules.LintRule{ + lintrules.AvlLimitRule{}, + } + + // TODO: Should i recreate a new store ? + tm.Store = newProdGnoStore() + pn = gno.NewPackageNode(gno.Name(mpkg.Name), pkgPath, fset) + tm.Store.SetBlockNode(pn) + gno.PredefineFileSet(tm.Store, pn, fset) + for _, fn := range fset.Files { + mf := mpkg.GetFile(fn.FileName) + src := string(mf.Body) + runLintExtensions( + tm.Store, + pn, + fn, + src, + rules, + func(err error, pos gnolang.Pos) { + fmt.Printf("%s:%d:%d: %s\n", + fn.FileName, pos.Line, pos.Column, + err.Error(), + ) + }, + ) + } } { // LINT STEP 5: PreprocessFiles() @@ -390,3 +419,46 @@ func lintTargetName(pkg *packages.Package) string { return tryRelativizePath(pkg.Dir) } + +func runLintExtensions( + store gnolang.Store, + pn *gnolang.PackageNode, + fn *gnolang.FileNode, + source string, + rules []lintrules.LintRule, + report func(error, gnolang.Pos), +) { + ctx := &lintrules.RuleContext{ + Store: store, + File: pn.GetFileByName(pn.FileNames()[0]), + Source: source, + } + + var currentFile *gnolang.FileNode + gnolang.TranscribeB(pn, fn, func( + ns []gnolang.Node, + stack []gnolang.BlockNode, + last gnolang.BlockNode, + ftype gnolang.TransField, + index int, + n gnolang.Node, + stage gnolang.TransStage, + ) (gnolang.Node, gnolang.TransCtrl) { + + if stage == gnolang.TRANS_ENTER { + + if fn, ok := n.(*gnolang.FileNode); ok { + currentFile = fn + } + ctx.File = currentFile + + for _, rule := range rules { + if err := rule.Run(ctx, n); err != nil { + report(err, n.GetPos()) + } + } + } + + return n, gnolang.TRANS_CONTINUE + }) +} diff --git a/gnovm/cmd/gno/lintrules/avl-limit.go b/gnovm/cmd/gno/lintrules/avl-limit.go new file mode 100644 index 00000000000..38e2b992fe9 --- /dev/null +++ b/gnovm/cmd/gno/lintrules/avl-limit.go @@ -0,0 +1,86 @@ +package lintrules + +import ( + "errors" + "fmt" + "strings" + + "github.com/gnolang/gno/gnovm/pkg/gnolang" +) + +type AvlLimitRule struct{} + +// XXX: this is a PoC not a production ready code +func (AvlLimitRule) Run(ctx *RuleContext, node gnolang.Node) error { + call, ok := node.(*gnolang.CallExpr) + if !ok { + return nil + } + + sel, ok := call.Func.(*gnolang.SelectorExpr) + if !ok { + return nil + } + + m := string(sel.Sel) + if m != "Iterate" && m != "ReverseIterate" { + return nil + } + + recvT := gnolang.EvalStaticTypeOf(ctx.Store, ctx.File, sel.X) + if !isAVLTree(recvT) { + // DEBUG: + // fmt.Printf("receiver is not AVL tree for %s\n", method) + return nil + } + + if len(call.Args) < 2 { + return nil + } + + if !isEmptyConstString(call.Args[0]) || + !isEmptyConstString(call.Args[1]) { + return nil + } + + if hasNoLintDirective(ctx, node.GetPos()) { + return nil + } + + return errors.New("avl tree Iterate/Reverse iterate") +} + +func isEmptyConstString(expr gnolang.Expr) bool { + cs, ok := expr.(*gnolang.ConstExpr) + if !ok { + return false + } + if cs.T.Kind() != gnolang.StringKind { + return false + } + return string(cs.V.(gnolang.StringValue)) == "" +} + +func hasNoLintDirective(ctx *RuleContext, pos gnolang.Pos) bool { + if ctx.Source == "" { + return false + } + + lines := strings.Split(ctx.Source, "\n") + line := pos.Line - 2 + + if line <= 0 || line > len(lines) { + return false + } + prev := strings.TrimSpace(lines[line]) + return strings.HasPrefix(prev, "//nolint") +} + +func isAVLTree(t gnolang.Type) bool { + dt, ok := gnolang.UnwrapPointerType(t).(*gnolang.DeclaredType) + if !ok { + fmt.Printf("DEBUG: not declared type %T \n", t) + return false + } + return dt.PkgPath == "gno.land/p/nt/avl" && dt.Name == "Tree" +} diff --git a/gnovm/cmd/gno/lintrules/lintrule.go b/gnovm/cmd/gno/lintrules/lintrule.go new file mode 100644 index 00000000000..12099ff1031 --- /dev/null +++ b/gnovm/cmd/gno/lintrules/lintrule.go @@ -0,0 +1,19 @@ +package lintrules + +import ( + "github.com/gnolang/gno/gnovm/pkg/gnolang" +) + +// The PoC aim to implement a way where gnolang pkg does not about linter +// We use TranscribeB to go through the AST built with gnolang.Nodes +// And we apply all the LintRules activated on all nodes. + +type RuleContext struct { + Store gnolang.Store + File *gnolang.FileNode + Source string +} + +type LintRule interface { + Run(ctx *RuleContext, node gnolang.Node) error +} diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index ee2c17934a3..306686b335b 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -3586,6 +3586,17 @@ func evalStaticTypeOf(store Store, last BlockNode, x Expr) Type { return t } +// EXPERTIMENT +func EvalStaticTypeOf(store Store, last BlockNode, x Expr) Type { + t := evalStaticTypeOfRaw(store, last, x) + + if tt, ok := t.(*tupleType); ok && len(tt.Elts) == 1 { + return tt.Elts[0] + } + + return t +} + // like evalStaticTypeOf() but returns the raw *tupleType for *CallExpr. func evalStaticTypeOfRaw(store Store, last BlockNode, x Expr) (t Type) { if t, ok := x.GetAttribute(ATTR_TYPEOF_VALUE).(Type); ok { diff --git a/gnovm/pkg/gnolang/types.go b/gnovm/pkg/gnolang/types.go index f2f57f09235..00cb292e0e3 100644 --- a/gnovm/pkg/gnolang/types.go +++ b/gnovm/pkg/gnolang/types.go @@ -1474,6 +1474,14 @@ func unwrapPointerType(t Type) Type { return t } +// XXX: TEMPORARY FOR EXPERIMENTATION +func UnwrapPointerType(t Type) Type { + if pt, ok := t.(*PointerType); ok { + return pt.Elem() + } + return t +} + // NOTE: it may be faster to switch on baseOf(). func (dt *DeclaredType) Kind() Kind { return dt.Base.Kind()