From 61a5c020ccf28ce1877aa783b264a6bb6f482fd2 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sun, 8 Dec 2024 12:33:10 +0100 Subject: [PATCH 01/20] feat: add `p/moul/{md,debug,web25}` + update `r/moul/home` (#2819) Related with https://hackmd.io/a8k09_TeQUu6WawvkpDDpw?both - [x] `p/moul/web25` - displays a link suggesting to view the realm from an external webui, but avoid recursive printing when seen from the external webui - [x] `p/moul/md` - minimal `markdown` helpers library - [x] `p/moul/debug` - displays useful informations when adding `?debug=1` - [x] update `r/moul/home` - [x] markdown table with links example - [x] svg example - [x] use of the new `p/moul/...` packages - [x] Release a static web25 interface somewhere (https://github.com/moul/gno-moul-home-web25) --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- contribs/gnodev/cmd/gnodev/main.go | 12 +- contribs/gnodev/cmd/gnodev/setup_web.go | 1 + examples/gno.land/p/moul/debug/debug.gno | 92 +++++++ examples/gno.land/p/moul/debug/gno.mod | 1 + .../gno.land/p/moul/debug/z1_filetest.gno | 31 +++ .../gno.land/p/moul/debug/z2_filetest.gno | 37 +++ examples/gno.land/p/moul/md/gno.mod | 1 + examples/gno.land/p/moul/md/md.gno | 242 ++++++++++++++++++ examples/gno.land/p/moul/md/md_test.gno | 88 +++++++ examples/gno.land/p/moul/md/z1_filetest.gno | 87 +++++++ examples/gno.land/p/moul/web25/gno.mod | 1 + examples/gno.land/p/moul/web25/web25.gno | 51 ++++ examples/gno.land/p/moul/web25/web25_test.gno | 1 + .../gno.land/r/moul/config/config_test.gno | 1 + examples/gno.land/r/moul/home/home.gno | 83 ++++-- examples/gno.land/r/moul/home/z1_filetest.gno | 24 +- examples/gno.land/r/moul/home/z2_filetest.gno | 63 ++++- 17 files changed, 778 insertions(+), 38 deletions(-) create mode 100644 examples/gno.land/p/moul/debug/debug.gno create mode 100644 examples/gno.land/p/moul/debug/gno.mod create mode 100644 examples/gno.land/p/moul/debug/z1_filetest.gno create mode 100644 examples/gno.land/p/moul/debug/z2_filetest.gno create mode 100644 examples/gno.land/p/moul/md/gno.mod create mode 100644 examples/gno.land/p/moul/md/md.gno create mode 100644 examples/gno.land/p/moul/md/md_test.gno create mode 100644 examples/gno.land/p/moul/md/z1_filetest.gno create mode 100644 examples/gno.land/p/moul/web25/gno.mod create mode 100644 examples/gno.land/p/moul/web25/web25.gno create mode 100644 examples/gno.land/p/moul/web25/web25_test.gno create mode 100644 examples/gno.land/r/moul/config/config_test.gno diff --git a/contribs/gnodev/cmd/gnodev/main.go b/contribs/gnodev/cmd/gnodev/main.go index c9d6487d753..082d0cb8270 100644 --- a/contribs/gnodev/cmd/gnodev/main.go +++ b/contribs/gnodev/cmd/gnodev/main.go @@ -59,6 +59,7 @@ type devCfg struct { // Web Configuration webListenerAddr string webRemoteHelperAddr string + webWithHTML bool // Node Configuration minimal bool @@ -126,14 +127,21 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { &c.webListenerAddr, "web-listener", defaultDevOptions.webListenerAddr, - "web server listener address", + "gnoweb: web server listener address", ) fs.StringVar( &c.webRemoteHelperAddr, "web-help-remote", defaultDevOptions.webRemoteHelperAddr, - "web server help page's remote addr (default to )", + "gnoweb: web server help page's remote addr (default to )", + ) + + fs.BoolVar( + &c.webWithHTML, + "web-with-html", + defaultDevOptions.webWithHTML, + "gnoweb: enable HTML parsing in markdown rendering", ) fs.StringVar( diff --git a/contribs/gnodev/cmd/gnodev/setup_web.go b/contribs/gnodev/cmd/gnodev/setup_web.go index 635c27af19d..d55814142a6 100644 --- a/contribs/gnodev/cmd/gnodev/setup_web.go +++ b/contribs/gnodev/cmd/gnodev/setup_web.go @@ -15,6 +15,7 @@ func setupGnoWebServer(logger *slog.Logger, cfg *devCfg, dnode *gnodev.Node) htt webConfig.HelpChainID = cfg.chainId webConfig.RemoteAddr = dnode.GetRemoteAddress() webConfig.HelpRemote = cfg.webRemoteHelperAddr + webConfig.WithHTML = cfg.webWithHTML // If `HelpRemote` is empty default it to `RemoteAddr` if webConfig.HelpRemote == "" { diff --git a/examples/gno.land/p/moul/debug/debug.gno b/examples/gno.land/p/moul/debug/debug.gno new file mode 100644 index 00000000000..9ba3dd36a98 --- /dev/null +++ b/examples/gno.land/p/moul/debug/debug.gno @@ -0,0 +1,92 @@ +// Package debug provides utilities for logging and displaying debug information +// within Gno realms. It supports conditional rendering of logs and metadata, +// toggleable via query parameters. +// +// Key Features: +// - Log collection and display using Markdown formatting. +// - Metadata display for realm path, address, and height. +// - Collapsible debug section for cleaner presentation. +// - Query-based debug toggle using `?debug=1`. +package debug + +import ( + "std" + "time" + + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + "gno.land/p/moul/mdtable" + "gno.land/p/moul/realmpath" +) + +// Debug encapsulates debug information, including logs and metadata. +type Debug struct { + Logs []string + HideMetadata bool +} + +// Log appends a new line of debug information to the Logs slice. +func (d *Debug) Log(line string) { + d.Logs = append(d.Logs, line) +} + +// Render generates the debug content as a collapsible Markdown section. +// It conditionally renders logs and metadata if enabled via the `?debug=1` query parameter. +func (d Debug) Render(path string) string { + if realmpath.Parse(path).Query.Get("debug") != "1" { + return "" + } + + var content string + + if d.Logs != nil { + content += md.H3("Logs") + content += md.BulletList(d.Logs) + } + + if !d.HideMetadata { + content += md.H3("Metadata") + table := mdtable.Table{ + Headers: []string{"Key", "Value"}, + } + table.Append([]string{"`std.CurrentRealm().PkgPath()`", string(std.CurrentRealm().PkgPath())}) + table.Append([]string{"`std.CurrentRealm().Addr()`", string(std.CurrentRealm().Addr())}) + table.Append([]string{"`std.PrevRealm().PkgPath()`", string(std.PrevRealm().PkgPath())}) + table.Append([]string{"`std.PrevRealm().Addr()`", string(std.PrevRealm().Addr())}) + table.Append([]string{"`std.GetHeight()`", ufmt.Sprintf("%d", std.GetHeight())}) + table.Append([]string{"`time.Now().Format(time.RFC3339)`", time.Now().Format(time.RFC3339)}) + content += table.String() + } + + if content == "" { + return "" + } + + return md.CollapsibleSection("debug", content) +} + +// Render displays metadata about the current realm but does not display logs. +// This function uses a default Debug struct with metadata enabled and no logs. +func Render(path string) string { + return Debug{}.Render(path) +} + +// IsEnabled checks if the `?debug=1` query parameter is set in the given path. +// Returns true if debugging is enabled, otherwise false. +func IsEnabled(path string) bool { + req := realmpath.Parse(path) + return req.Query.Get("debug") == "1" +} + +// ToggleURL modifies the given path's query string to toggle the `?debug=1` parameter. +// If debugging is currently enabled, it removes the parameter. +// If debugging is disabled, it adds the parameter. +func ToggleURL(path string) string { + req := realmpath.Parse(path) + if IsEnabled(path) { + req.Query.Del("debug") + } else { + req.Query.Add("debug", "1") + } + return req.String() +} diff --git a/examples/gno.land/p/moul/debug/gno.mod b/examples/gno.land/p/moul/debug/gno.mod new file mode 100644 index 00000000000..eb48ed292ca --- /dev/null +++ b/examples/gno.land/p/moul/debug/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/debug diff --git a/examples/gno.land/p/moul/debug/z1_filetest.gno b/examples/gno.land/p/moul/debug/z1_filetest.gno new file mode 100644 index 00000000000..8203749d3c7 --- /dev/null +++ b/examples/gno.land/p/moul/debug/z1_filetest.gno @@ -0,0 +1,31 @@ +package main + +import "gno.land/p/moul/debug" + +func main() { + println("---") + println(debug.Render("")) + println("---") + println(debug.Render("?debug=1")) + println("---") +} + +// Output: +// --- +// +// --- +//
debug +// +// ### Metadata +// | Key | Value | +// | --- | --- | +// | `std.CurrentRealm().PkgPath()` | | +// | `std.CurrentRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.PrevRealm().PkgPath()` | | +// | `std.PrevRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.GetHeight()` | 123 | +// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z | +// +//
+// +// --- diff --git a/examples/gno.land/p/moul/debug/z2_filetest.gno b/examples/gno.land/p/moul/debug/z2_filetest.gno new file mode 100644 index 00000000000..32c2fe49951 --- /dev/null +++ b/examples/gno.land/p/moul/debug/z2_filetest.gno @@ -0,0 +1,37 @@ +package main + +import "gno.land/p/moul/debug" + +func main() { + var d debug.Debug + d.Log("hello world!") + d.Log("foobar") + println("---") + println(d.Render("")) + println("---") + println(d.Render("?debug=1")) + println("---") +} + +// Output: +// --- +// +// --- +//
debug +// +// ### Logs +// - hello world! +// - foobar +// ### Metadata +// | Key | Value | +// | --- | --- | +// | `std.CurrentRealm().PkgPath()` | | +// | `std.CurrentRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.PrevRealm().PkgPath()` | | +// | `std.PrevRealm().Addr()` | g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm | +// | `std.GetHeight()` | 123 | +// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z | +// +//
+// +// --- diff --git a/examples/gno.land/p/moul/md/gno.mod b/examples/gno.land/p/moul/md/gno.mod new file mode 100644 index 00000000000..55d124d9e6b --- /dev/null +++ b/examples/gno.land/p/moul/md/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/md diff --git a/examples/gno.land/p/moul/md/md.gno b/examples/gno.land/p/moul/md/md.gno new file mode 100644 index 00000000000..61d6948b997 --- /dev/null +++ b/examples/gno.land/p/moul/md/md.gno @@ -0,0 +1,242 @@ +// Package md provides helper functions for generating Markdown content programmatically. +// +// It includes utilities for text formatting, creating lists, blockquotes, code blocks, +// links, images, and more. +// +// Highlights: +// - Supports basic Markdown syntax such as bold, italic, strikethrough, headers, and lists. +// - Manages multiline support in lists (e.g., bullet, ordered, and todo lists). +// - Includes advanced helpers like inline images with links and nested list prefixes. +package md + +import ( + "strconv" + "strings" +) + +// Bold returns bold text for markdown. +// Example: Bold("foo") => "**foo**" +func Bold(text string) string { + return "**" + text + "**" +} + +// Italic returns italicized text for markdown. +// Example: Italic("foo") => "*foo*" +func Italic(text string) string { + return "*" + text + "*" +} + +// Strikethrough returns strikethrough text for markdown. +// Example: Strikethrough("foo") => "~~foo~~" +func Strikethrough(text string) string { + return "~~" + text + "~~" +} + +// H1 returns a level 1 header for markdown. +// Example: H1("foo") => "# foo\n" +func H1(text string) string { + return "# " + text + "\n" +} + +// H2 returns a level 2 header for markdown. +// Example: H2("foo") => "## foo\n" +func H2(text string) string { + return "## " + text + "\n" +} + +// H3 returns a level 3 header for markdown. +// Example: H3("foo") => "### foo\n" +func H3(text string) string { + return "### " + text + "\n" +} + +// H4 returns a level 4 header for markdown. +// Example: H4("foo") => "#### foo\n" +func H4(text string) string { + return "#### " + text + "\n" +} + +// H5 returns a level 5 header for markdown. +// Example: H5("foo") => "##### foo\n" +func H5(text string) string { + return "##### " + text + "\n" +} + +// H6 returns a level 6 header for markdown. +// Example: H6("foo") => "###### foo\n" +func H6(text string) string { + return "###### " + text + "\n" +} + +// BulletList returns a bullet list for markdown. +// Example: BulletList([]string{"foo", "bar"}) => "- foo\n- bar\n" +func BulletList(items []string) string { + var sb strings.Builder + for _, item := range items { + sb.WriteString(BulletItem(item)) + } + return sb.String() +} + +// BulletItem returns a bullet item for markdown. +// Example: BulletItem("foo") => "- foo\n" +func BulletItem(item string) string { + var sb strings.Builder + lines := strings.Split(item, "\n") + sb.WriteString("- " + lines[0] + "\n") + for _, line := range lines[1:] { + sb.WriteString(" " + line + "\n") + } + return sb.String() +} + +// OrderedList returns an ordered list for markdown. +// Example: OrderedList([]string{"foo", "bar"}) => "1. foo\n2. bar\n" +func OrderedList(items []string) string { + var sb strings.Builder + for i, item := range items { + lines := strings.Split(item, "\n") + sb.WriteString(strconv.Itoa(i+1) + ". " + lines[0] + "\n") + for _, line := range lines[1:] { + sb.WriteString(" " + line + "\n") + } + } + return sb.String() +} + +// TodoList returns a list of todo items with checkboxes for markdown. +// Example: TodoList([]string{"foo", "bar\nmore bar"}, []bool{true, false}) => "- [x] foo\n- [ ] bar\n more bar\n" +func TodoList(items []string, done []bool) string { + var sb strings.Builder + for i, item := range items { + sb.WriteString(TodoItem(item, done[i])) + } + return sb.String() +} + +// TodoItem returns a todo item with checkbox for markdown. +// Example: TodoItem("foo", true) => "- [x] foo\n" +func TodoItem(item string, done bool) string { + var sb strings.Builder + checkbox := " " + if done { + checkbox = "x" + } + lines := strings.Split(item, "\n") + sb.WriteString("- [" + checkbox + "] " + lines[0] + "\n") + for _, line := range lines[1:] { + sb.WriteString(" " + line + "\n") + } + return sb.String() +} + +// Nested prefixes each line with a given prefix, enabling nested lists. +// Example: Nested("- foo\n- bar", " ") => " - foo\n - bar\n" +func Nested(content, prefix string) string { + lines := strings.Split(content, "\n") + for i := range lines { + if strings.TrimSpace(lines[i]) != "" { + lines[i] = prefix + lines[i] + } + } + return strings.Join(lines, "\n") +} + +// Blockquote returns a blockquote for markdown. +// Example: Blockquote("foo\nbar") => "> foo\n> bar\n" +func Blockquote(text string) string { + lines := strings.Split(text, "\n") + var sb strings.Builder + for _, line := range lines { + sb.WriteString("> " + line + "\n") + } + return sb.String() +} + +// InlineCode returns inline code for markdown. +// Example: InlineCode("foo") => "`foo`" +func InlineCode(code string) string { + return "`" + strings.ReplaceAll(code, "`", "\\`") + "`" +} + +// CodeBlock creates a markdown code block. +// Example: CodeBlock("foo") => "```\nfoo\n```" +func CodeBlock(content string) string { + return "```\n" + strings.ReplaceAll(content, "```", "\\```") + "\n```" +} + +// LanguageCodeBlock creates a markdown code block with language-specific syntax highlighting. +// Example: LanguageCodeBlock("go", "foo") => "```go\nfoo\n```" +func LanguageCodeBlock(language, content string) string { + return "```" + language + "\n" + strings.ReplaceAll(content, "```", "\\```") + "\n```" +} + +// HorizontalRule returns a horizontal rule for markdown. +// Example: HorizontalRule() => "---\n" +func HorizontalRule() string { + return "---\n" +} + +// Link returns a hyperlink for markdown. +// Example: Link("foo", "http://example.com") => "[foo](http://example.com)" +func Link(text, url string) string { + return "[" + EscapeText(text) + "](" + url + ")" +} + +// InlineImageWithLink creates an inline image wrapped in a hyperlink for markdown. +// Example: InlineImageWithLink("alt text", "image-url", "link-url") => "[![alt text](image-url)](link-url)" +func InlineImageWithLink(altText, imageUrl, linkUrl string) string { + return "[" + Image(altText, imageUrl) + "](" + linkUrl + ")" +} + +// Image returns an image for markdown. +// Example: Image("foo", "http://example.com") => "![foo](http://example.com)" +func Image(altText, url string) string { + return "![" + EscapeText(altText) + "](" + url + ")" +} + +// Footnote returns a footnote for markdown. +// Example: Footnote("foo", "bar") => "[foo]: bar" +func Footnote(reference, text string) string { + return "[" + EscapeText(reference) + "]: " + text +} + +// Paragraph wraps the given text in a Markdown paragraph. +// Example: Paragraph("foo") => "foo\n" +func Paragraph(content string) string { + return content + "\n\n" +} + +// CollapsibleSection creates a collapsible section for markdown using +// HTML
and tags. +// Example: +// CollapsibleSection("Click to expand", "Hidden content") +// => +//
Click to expand +// +// Hidden content +//
+func CollapsibleSection(title, content string) string { + return "
" + EscapeText(title) + "\n\n" + content + "\n
\n" +} + +// EscapeText escapes special Markdown characters in regular text where needed. +func EscapeText(text string) string { + replacer := strings.NewReplacer( + `*`, `\*`, + `_`, `\_`, + `[`, `\[`, + `]`, `\]`, + `(`, `\(`, + `)`, `\)`, + `~`, `\~`, + `>`, `\>`, + `|`, `\|`, + `-`, `\-`, + `+`, `\+`, + ".", `\.`, + "!", `\!`, + "`", "\\`", + ) + return replacer.Replace(text) +} diff --git a/examples/gno.land/p/moul/md/md_test.gno b/examples/gno.land/p/moul/md/md_test.gno new file mode 100644 index 00000000000..144ae58d918 --- /dev/null +++ b/examples/gno.land/p/moul/md/md_test.gno @@ -0,0 +1,88 @@ +package md + +import ( + "testing" + + "gno.land/p/moul/md" +) + +func TestHelpers(t *testing.T) { + tests := []struct { + name string + function func() string + expected string + }{ + {"Bold", func() string { return md.Bold("foo") }, "**foo**"}, + {"Italic", func() string { return md.Italic("foo") }, "*foo*"}, + {"Strikethrough", func() string { return md.Strikethrough("foo") }, "~~foo~~"}, + {"H1", func() string { return md.H1("foo") }, "# foo\n"}, + {"HorizontalRule", md.HorizontalRule, "---\n"}, + {"InlineCode", func() string { return md.InlineCode("foo") }, "`foo`"}, + {"CodeBlock", func() string { return md.CodeBlock("foo") }, "```\nfoo\n```"}, + {"LanguageCodeBlock", func() string { return md.LanguageCodeBlock("go", "foo") }, "```go\nfoo\n```"}, + {"Link", func() string { return md.Link("foo", "http://example.com") }, "[foo](http://example.com)"}, + {"Image", func() string { return md.Image("foo", "http://example.com") }, "![foo](http://example.com)"}, + {"InlineImageWithLink", func() string { return md.InlineImageWithLink("alt", "image-url", "link-url") }, "[![alt](image-url)](link-url)"}, + {"Footnote", func() string { return md.Footnote("foo", "bar") }, "[foo]: bar"}, + {"Paragraph", func() string { return md.Paragraph("foo") }, "foo\n\n"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.function() + if result != tt.expected { + t.Errorf("%s() = %q, want %q", tt.name, result, tt.expected) + } + }) + } +} + +func TestLists(t *testing.T) { + t.Run("BulletList", func(t *testing.T) { + items := []string{"foo", "bar"} + expected := "- foo\n- bar\n" + result := md.BulletList(items) + if result != expected { + t.Errorf("BulletList(%q) = %q, want %q", items, result, expected) + } + }) + + t.Run("OrderedList", func(t *testing.T) { + items := []string{"foo", "bar"} + expected := "1. foo\n2. bar\n" + result := md.OrderedList(items) + if result != expected { + t.Errorf("OrderedList(%q) = %q, want %q", items, result, expected) + } + }) + + t.Run("TodoList", func(t *testing.T) { + items := []string{"foo", "bar\nmore bar"} + done := []bool{true, false} + expected := "- [x] foo\n- [ ] bar\n more bar\n" + result := md.TodoList(items, done) + if result != expected { + t.Errorf("TodoList(%q, %q) = %q, want %q", items, done, result, expected) + } + }) +} + +func TestNested(t *testing.T) { + t.Run("Nested Single Level", func(t *testing.T) { + content := "- foo\n- bar" + expected := " - foo\n - bar" + result := md.Nested(content, " ") + if result != expected { + t.Errorf("Nested(%q) = %q, want %q", content, result, expected) + } + }) + + t.Run("Nested Double Level", func(t *testing.T) { + content := " - foo\n - bar" + expected := " - foo\n - bar" + result := md.Nested(content, " ") + if result != expected { + t.Errorf("Nested(%q) = %q, want %q", content, result, expected) + } + }) +} diff --git a/examples/gno.land/p/moul/md/z1_filetest.gno b/examples/gno.land/p/moul/md/z1_filetest.gno new file mode 100644 index 00000000000..077e1732bcb --- /dev/null +++ b/examples/gno.land/p/moul/md/z1_filetest.gno @@ -0,0 +1,87 @@ +package main + +import "gno.land/p/moul/md" + +func main() { + println(md.H1("Header 1")) + println(md.H2("Header 2")) + println(md.H3("Header 3")) + println(md.H4("Header 4")) + println(md.H5("Header 5")) + println(md.H6("Header 6")) + println(md.Bold("bold")) + println(md.Italic("italic")) + println(md.Strikethrough("strikethrough")) + println(md.BulletList([]string{ + "Item 1", + "Item 2\nMore details for item 2", + })) + println(md.OrderedList([]string{"Step 1", "Step 2"})) + println(md.TodoList([]string{"Task 1", "Task 2\nSubtask 2"}, []bool{true, false})) + println(md.Nested(md.BulletList([]string{"Parent Item", md.OrderedList([]string{"Child 1", "Child 2"})}), " ")) + println(md.Blockquote("This is a blockquote\nSpanning multiple lines")) + println(md.InlineCode("inline `code`")) + println(md.CodeBlock("line1\nline2")) + println(md.LanguageCodeBlock("go", "func main() {\nprintln(\"Hello, world!\")\n}")) + println(md.HorizontalRule()) + println(md.Link("Gno", "http://gno.land")) + println(md.Image("Alt Text", "http://example.com/image.png")) + println(md.InlineImageWithLink("Alt Text", "http://example.com/image.png", "http://example.com")) + println(md.Footnote("ref", "This is a footnote")) + println(md.Paragraph("This is a paragraph.")) +} + +// Output: +// # Header 1 +// +// ## Header 2 +// +// ### Header 3 +// +// #### Header 4 +// +// ##### Header 5 +// +// ###### Header 6 +// +// **bold** +// *italic* +// ~~strikethrough~~ +// - Item 1 +// - Item 2 +// More details for item 2 +// +// 1. Step 1 +// 2. Step 2 +// +// - [x] Task 1 +// - [ ] Task 2 +// Subtask 2 +// +// - Parent Item +// - 1. Child 1 +// 2. Child 2 +// +// +// > This is a blockquote +// > Spanning multiple lines +// +// `inline \`code\`` +// ``` +// line1 +// line2 +// ``` +// ```go +// func main() { +// println("Hello, world!") +// } +// ``` +// --- +// +// [Gno](http://gno.land) +// ![Alt Text](http://example.com/image.png) +// [![Alt Text](http://example.com/image.png)](http://example.com) +// [ref]: This is a footnote +// This is a paragraph. +// +// diff --git a/examples/gno.land/p/moul/web25/gno.mod b/examples/gno.land/p/moul/web25/gno.mod new file mode 100644 index 00000000000..f27bc793bf7 --- /dev/null +++ b/examples/gno.land/p/moul/web25/gno.mod @@ -0,0 +1 @@ +module gno.land/p/moul/web25 diff --git a/examples/gno.land/p/moul/web25/web25.gno b/examples/gno.land/p/moul/web25/web25.gno new file mode 100644 index 00000000000..46d564b70ad --- /dev/null +++ b/examples/gno.land/p/moul/web25/web25.gno @@ -0,0 +1,51 @@ +// Pacakge web25 provides an opinionated way to register an external web2 +// frontend to provide a "better" web2.5 experience. +package web25 + +import ( + "strings" + + "gno.land/p/moul/realmpath" +) + +type Config struct { + CID string + URL string + Text string +} + +func (c *Config) SetRemoteFrontendByURL(url string) { + c.CID = "" + c.URL = url +} + +func (c *Config) SetRemoteFrontendByCID(cid string) { + c.CID = cid + c.URL = "" +} + +func (c Config) GetLink() string { + if c.CID != "" { + return "https://ipfs.io/ipfs/" + c.CID + } + return c.URL +} + +const DefaultText = "Click [here]({link}) to visit the full rendering experience.\n" + +// Render displays a frontend link at the top of your realm's Render function in +// a concistent way to help gno visitors to have a consistent experience. +// +// if query is not nil, then it will check if it's not disable by ?no-web25, so +// that you can call the render function from an external point of view. +func (c Config) Render(path string) string { + if realmpath.Parse(path).Query.Get("no-web25") == "1" { + return "" + } + text := c.Text + if text == "" { + text = DefaultText + } + text = strings.ReplaceAll(text, "{link}", c.GetLink()) + return text +} diff --git a/examples/gno.land/p/moul/web25/web25_test.gno b/examples/gno.land/p/moul/web25/web25_test.gno new file mode 100644 index 00000000000..6d58a586595 --- /dev/null +++ b/examples/gno.land/p/moul/web25/web25_test.gno @@ -0,0 +1 @@ +package web25 diff --git a/examples/gno.land/r/moul/config/config_test.gno b/examples/gno.land/r/moul/config/config_test.gno new file mode 100644 index 00000000000..d912156bec0 --- /dev/null +++ b/examples/gno.land/r/moul/config/config_test.gno @@ -0,0 +1 @@ +package config diff --git a/examples/gno.land/r/moul/home/home.gno b/examples/gno.land/r/moul/home/home.gno index 140e7b5e0c8..1094ce29cc5 100644 --- a/examples/gno.land/r/moul/home/home.gno +++ b/examples/gno.land/r/moul/home/home.gno @@ -1,14 +1,23 @@ package home import ( + "strconv" + + "gno.land/p/demo/svg" + "gno.land/p/moul/debug" + "gno.land/p/moul/md" + "gno.land/p/moul/mdtable" + "gno.land/p/moul/txlink" + "gno.land/p/moul/web25" "gno.land/r/leon/hof" "gno.land/r/moul/config" ) var ( - todos []string - status string - memeImgURL string + todos []string + status string + memeImgURL string + web25config = web25.Config{URL: "https://moul.github.io/gno-moul-home-web25/"} ) func init() { @@ -19,36 +28,74 @@ func init() { } func Render(path string) string { - content := "# Manfred's (gn)home Dashboard\n\n" + content := web25config.Render(path) + var d debug.Debug + + content += md.H1("Manfred's (gn)home Dashboard") - content += "## Meme\n" - content += "![](" + memeImgURL + ")\n\n" + content += md.H2("Meme") + content += md.Paragraph( + md.Image("meme", memeImgURL), + ) - content += "## Status\n" - content += status + "\n\n" + content += md.H2("Status") + content += md.Paragraph(status) + content += md.Paragraph(md.Link("update", txlink.Call("UpdateStatus"))) - content += "## Personal ToDo List\n" - for _, todo := range todos { - content += "- [ ] " + todo + "\n" + d.Log("hello world!") + + content += md.H2("Personal TODO List (bullet list)") + for i, todo := range todos { + idstr := strconv.Itoa(i) + deleteLink := md.Link("x", txlink.Call("DeleteTodo", "idx", idstr)) + content += md.BulletItem(todo + " " + deleteLink) } - content += "\n" + content += md.BulletItem(md.Link("[new]", txlink.Call("AddTodo"))) + + content += md.H2("Personal TODO List (table)") + table := mdtable.Table{ + Headers: []string{"ID", "Item", "Links"}, + } + for i, todo := range todos { + idstr := strconv.Itoa(i) + deleteLink := md.Link("[del]", txlink.Call("DeleteTodo", "idx", idstr)) + table.Append([]string{"#" + idstr, todo, deleteLink}) + } + content += table.String() + + content += md.H2("SVG Example") + content += md.Paragraph("this feature may not work with the current gnoweb version and/or configuration.") + content += md.Paragraph(svg.Canvas{ + Width: 500, Height: 500, + Elems: []svg.Elem{ + svg.Rectangle{50, 50, 100, 100, "red"}, + svg.Circle{50, 50, 100, "red"}, + svg.Text{100, 100, "hello world!", "magenta"}, + }, + }.String()) - // TODO: Implement a feature to list replies on r/boards on my posts - // TODO: Maybe integrate a calendar feature for upcoming events? + content += md.H2("Debug") + content += md.Paragraph("this feature may not work with the current gnoweb version and/or configuration.") + content += md.Paragraph( + md.Link("toggle debug", debug.ToggleURL(path)), + ) + // TODO: my r/boards posts + // TODO: my r/events events + content += d.Render(path) return content } -func AddNewTodo(todo string) { +func AddTodo(todo string) { config.AssertIsAdmin() todos = append(todos, todo) } -func DeleteTodo(todoIndex int) { +func DeleteTodo(idx int) { config.AssertIsAdmin() - if todoIndex >= 0 && todoIndex < len(todos) { + if idx >= 0 && idx < len(todos) { // Remove the todo from the list by merging slices from before and after the todo - todos = append(todos[:todoIndex], todos[todoIndex+1:]...) + todos = append(todos[:idx], todos[idx+1:]...) } else { panic("Invalid todo index") } diff --git a/examples/gno.land/r/moul/home/z1_filetest.gno b/examples/gno.land/r/moul/home/z1_filetest.gno index b26c919dd3a..b9d7d91a702 100644 --- a/examples/gno.land/r/moul/home/z1_filetest.gno +++ b/examples/gno.land/r/moul/home/z1_filetest.gno @@ -7,15 +7,31 @@ func main() { } // Output: +// Click [here](https://moul.github.io/gno-moul-home-web25/) to visit the full rendering experience. // # Manfred's (gn)home Dashboard -// // ## Meme -// ![](https://i.imgflip.com/7ze8dc.jpg) +// ![meme](https://i.imgflip.com/7ze8dc.jpg) // // ## Status // Online // -// ## Personal ToDo List -// - [ ] fill this todo list... +// [update](/r/moul/home$help&func=UpdateStatus) +// +// ## Personal TODO List (bullet list) +// - fill this todo list... [x](/r/moul/home$help&func=DeleteTodo&idx=0) +// - [\[new\]](/r/moul/home$help&func=AddTodo) +// ## Personal TODO List (table) +// | ID | Item | Links | +// | --- | --- | --- | +// | #0 | fill this todo list... | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=0) | +// ## SVG Example +// this feature may not work with the current gnoweb version and/or configuration. +// +// hello world! +// +// ## Debug +// this feature may not work with the current gnoweb version and/or configuration. +// +// [toggle debug](/r/moul/home:?debug=1) // // diff --git a/examples/gno.land/r/moul/home/z2_filetest.gno b/examples/gno.land/r/moul/home/z2_filetest.gno index 489dc2aeecd..f471280d8ef 100644 --- a/examples/gno.land/r/moul/home/z2_filetest.gno +++ b/examples/gno.land/r/moul/home/z2_filetest.gno @@ -8,30 +8,65 @@ import ( func main() { std.TestSetOrigCaller("g1manfred47kzduec920z88wfr64ylksmdcedlf5") - home.AddNewTodo("aaa") - home.AddNewTodo("bbb") - home.AddNewTodo("ccc") - home.AddNewTodo("ddd") - home.AddNewTodo("eee") + home.AddTodo("aaa") + home.AddTodo("bbb") + home.AddTodo("ccc") + home.AddTodo("ddd") + home.AddTodo("eee") home.UpdateStatus("Lorem Ipsum") home.DeleteTodo(3) - println(home.Render("")) + println(home.Render("?debug=1")) } // Output: +// Click [here](https://moul.github.io/gno-moul-home-web25/) to visit the full rendering experience. // # Manfred's (gn)home Dashboard -// // ## Meme -// ![](https://i.imgflip.com/7ze8dc.jpg) +// ![meme](https://i.imgflip.com/7ze8dc.jpg) // // ## Status // Lorem Ipsum // -// ## Personal ToDo List -// - [ ] fill this todo list... -// - [ ] aaa -// - [ ] bbb -// - [ ] ddd -// - [ ] eee +// [update](/r/moul/home$help&func=UpdateStatus) +// +// ## Personal TODO List (bullet list) +// - fill this todo list... [x](/r/moul/home$help&func=DeleteTodo&idx=0) +// - aaa [x](/r/moul/home$help&func=DeleteTodo&idx=1) +// - bbb [x](/r/moul/home$help&func=DeleteTodo&idx=2) +// - ddd [x](/r/moul/home$help&func=DeleteTodo&idx=3) +// - eee [x](/r/moul/home$help&func=DeleteTodo&idx=4) +// - [\[new\]](/r/moul/home$help&func=AddTodo) +// ## Personal TODO List (table) +// | ID | Item | Links | +// | --- | --- | --- | +// | #0 | fill this todo list... | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=0) | +// | #1 | aaa | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=1) | +// | #2 | bbb | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=2) | +// | #3 | ddd | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=3) | +// | #4 | eee | [\[del\]](/r/moul/home$help&func=DeleteTodo&idx=4) | +// ## SVG Example +// this feature may not work with the current gnoweb version and/or configuration. +// +// hello world! +// +// ## Debug +// this feature may not work with the current gnoweb version and/or configuration. +// +// [toggle debug](/r/moul/home:) +// +//
debug +// +// ### Logs +// - hello world! +// ### Metadata +// | Key | Value | +// | --- | --- | +// | `std.CurrentRealm().PkgPath()` | gno.land/r/moul/home | +// | `std.CurrentRealm().Addr()` | g1h8h57ntxadcze3f703skymfzdwa6t3ugf0nq3z | +// | `std.PrevRealm().PkgPath()` | | +// | `std.PrevRealm().Addr()` | g1manfred47kzduec920z88wfr64ylksmdcedlf5 | +// | `std.GetHeight()` | 123 | +// | `time.Now().Format(time.RFC3339)` | 2009-02-13T23:31:30Z | // +//
// From 052a2a1cfdeddbb8a3c6ac291917226aaf46491e Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:06:50 +0100 Subject: [PATCH 02/20] feat(examples): add avl_pager reverse option, update userbook (#3297) ## Description This PR adds the reverse option in the avl_pager package, and updates the rendering in the `r/demo/userbook` realm.
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--- examples/gno.land/p/demo/avl/pager/pager.gno | 24 +++-- .../gno.land/p/demo/avl/pager/pager_test.gno | 93 +++++++++++++------ .../gno.land/p/demo/avl/pager/z_filetest.gno | 4 +- examples/gno.land/r/demo/userbook/render.gno | 35 +++---- .../gno.land/r/demo/userbook/userbook.gno | 20 ++-- examples/gno.land/r/demo/users/users.gno | 4 +- .../gno.land/r/docs/avl_pager/avl_pager.gno | 6 +- examples/gno.land/r/leon/hof/render.gno | 4 +- 8 files changed, 115 insertions(+), 75 deletions(-) diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno index 60bb44d97b6..cccdc0df645 100644 --- a/examples/gno.land/p/demo/avl/pager/pager.gno +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -15,6 +15,7 @@ type Pager struct { PageQueryParam string SizeQueryParam string DefaultPageSize int + Reversed bool } // Page represents a single page of results. @@ -36,12 +37,13 @@ type Item struct { } // NewPager creates a new Pager with default values. -func NewPager(tree *avl.Tree, defaultPageSize int) *Pager { +func NewPager(tree *avl.Tree, defaultPageSize int, reversed bool) *Pager { return &Pager{ Tree: tree, PageQueryParam: "page", SizeQueryParam: "size", DefaultPageSize: defaultPageSize, + Reversed: reversed, } } @@ -86,10 +88,18 @@ func (p *Pager) GetPageWithSize(pageNumber, pageSize int) *Page { } items := []Item{} - p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { - items = append(items, Item{Key: key, Value: value}) - return false - }) + + if p.Reversed { + p.Tree.IterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { + items = append(items, Item{Key: key, Value: value}) + return false + }) + } else { + p.Tree.ReverseIterateByOffset(startIndex, endIndex-startIndex, func(key string, value interface{}) bool { + items = append(items, Item{Key: key, Value: value}) + return false + }) + } page.Items = items page.PageNumber = pageNumber @@ -115,8 +125,8 @@ func (p *Pager) GetPageByPath(rawURL string) (*Page, error) { return p.GetPageWithSize(pageNumber, pageSize), nil } -// UI generates the Markdown UI for the page selector. -func (p *Page) Selector() string { +// Picker generates the Markdown UI for the page Picker +func (p *Page) Picker() string { pageNumber := p.PageNumber pageNumber = max(pageNumber, 1) diff --git a/examples/gno.land/p/demo/avl/pager/pager_test.gno b/examples/gno.land/p/demo/avl/pager/pager_test.gno index da4680db8c7..9869924e5b5 100644 --- a/examples/gno.land/p/demo/avl/pager/pager_test.gno +++ b/examples/gno.land/p/demo/avl/pager/pager_test.gno @@ -18,34 +18,67 @@ func TestPager_GetPage(t *testing.T) { tree.Set("d", 4) tree.Set("e", 5) - // Create a new pager. - pager := NewPager(tree, 10) + t.Run("normal ordering", func(t *testing.T) { + // Create a new pager. + pager := NewPager(tree, 10, false) + + // Define test cases. + tests := []struct { + pageNumber int + pageSize int + expected []Item + }{ + {1, 2, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}}}, + {2, 2, []Item{{Key: "c", Value: 3}, {Key: "d", Value: 4}}}, + {3, 2, []Item{{Key: "e", Value: 5}}}, + {1, 3, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}}}, + {2, 3, []Item{{Key: "d", Value: 4}, {Key: "e", Value: 5}}}, + {1, 5, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}, {Key: "d", Value: 4}, {Key: "e", Value: 5}}}, + {2, 5, []Item{}}, + } - // Define test cases. - tests := []struct { - pageNumber int - pageSize int - expected []Item - }{ - {1, 2, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}}}, - {2, 2, []Item{{Key: "c", Value: 3}, {Key: "d", Value: 4}}}, - {3, 2, []Item{{Key: "e", Value: 5}}}, - {1, 3, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}}}, - {2, 3, []Item{{Key: "d", Value: 4}, {Key: "e", Value: 5}}}, - {1, 5, []Item{{Key: "a", Value: 1}, {Key: "b", Value: 2}, {Key: "c", Value: 3}, {Key: "d", Value: 4}, {Key: "e", Value: 5}}}, - {2, 5, []Item{}}, - } + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) - for _, tt := range tests { - page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + uassert.Equal(t, len(tt.expected), len(page.Items)) - uassert.Equal(t, len(tt.expected), len(page.Items)) + for i, item := range page.Items { + uassert.Equal(t, tt.expected[i].Key, item.Key) + uassert.Equal(t, tt.expected[i].Value, item.Value) + } + } + }) + + t.Run("reversed ordering", func(t *testing.T) { + // Create a new pager. + pager := NewPager(tree, 10, true) + + // Define test cases. + tests := []struct { + pageNumber int + pageSize int + expected []Item + }{ + {1, 2, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}}}, + {2, 2, []Item{{Key: "c", Value: 3}, {Key: "b", Value: 2}}}, + {3, 2, []Item{{Key: "a", Value: 1}}}, + {1, 3, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}, {Key: "c", Value: 3}}}, + {2, 3, []Item{{Key: "b", Value: 2}, {Key: "a", Value: 1}}}, + {1, 5, []Item{{Key: "e", Value: 5}, {Key: "d", Value: 4}, {Key: "c", Value: 3}, {Key: "b", Value: 2}, {Key: "a", Value: 1}}}, + {2, 5, []Item{}}, + } - for i, item := range page.Items { - uassert.Equal(t, tt.expected[i].Key, item.Key) - uassert.Equal(t, tt.expected[i].Value, item.Value) + for _, tt := range tests { + page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) + + uassert.Equal(t, len(tt.expected), len(page.Items)) + + for i, item := range page.Items { + uassert.Equal(t, tt.expected[i].Key, item.Key) + uassert.Equal(t, tt.expected[i].Value, item.Value) + } } - } + }) } func TestPager_GetPageByPath(t *testing.T) { @@ -56,7 +89,7 @@ func TestPager_GetPageByPath(t *testing.T) { } // Create a new pager. - pager := NewPager(tree, 10) + pager := NewPager(tree, 10, false) // Define test cases. tests := []struct { @@ -80,7 +113,7 @@ func TestPager_GetPageByPath(t *testing.T) { } } -func TestPage_Selector(t *testing.T) { +func TestPage_Picker(t *testing.T) { // Create a new AVL tree and populate it with some key-value pairs. tree := avl.NewTree() tree.Set("a", 1) @@ -90,7 +123,7 @@ func TestPage_Selector(t *testing.T) { tree.Set("e", 5) // Create a new pager. - pager := NewPager(tree, 10) + pager := NewPager(tree, 10, false) // Define test cases. tests := []struct { @@ -106,7 +139,7 @@ func TestPage_Selector(t *testing.T) { for _, tt := range tests { page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) - ui := page.Selector() + ui := page.Picker() uassert.Equal(t, tt.expected, ui) } } @@ -119,7 +152,7 @@ func TestPager_UI_WithManyPages(t *testing.T) { } // Create a new pager. - pager := NewPager(tree, 10) + pager := NewPager(tree, 10, false) // Define test cases for a large number of pages. tests := []struct { @@ -145,7 +178,7 @@ func TestPager_UI_WithManyPages(t *testing.T) { for _, tt := range tests { page := pager.GetPageWithSize(tt.pageNumber, tt.pageSize) - ui := page.Selector() + ui := page.Picker() uassert.Equal(t, tt.expected, ui) } } @@ -160,7 +193,7 @@ func TestPager_ParseQuery(t *testing.T) { tree.Set("e", 5) // Create a new pager. - pager := NewPager(tree, 10) + pager := NewPager(tree, 10, false) // Define test cases. tests := []struct { diff --git a/examples/gno.land/p/demo/avl/pager/z_filetest.gno b/examples/gno.land/p/demo/avl/pager/z_filetest.gno index 17029f57861..6342888d6b4 100644 --- a/examples/gno.land/p/demo/avl/pager/z_filetest.gno +++ b/examples/gno.land/p/demo/avl/pager/z_filetest.gno @@ -16,7 +16,7 @@ func main() { } // Create a new pager. - pager := pager.NewPager(tree, 7) + pager := pager.NewPager(tree, 7, false) for pn := -1; pn < 8; pn++ { page := pager.GetPage(pn) @@ -25,7 +25,7 @@ func main() { for idx, item := range page.Items { println(ufmt.Sprintf("- idx=%d key=%s value=%d", idx, item.Key, item.Value)) } - println(page.Selector()) + println(page.Picker()) println() } } diff --git a/examples/gno.land/r/demo/userbook/render.gno b/examples/gno.land/r/demo/userbook/render.gno index 22d7f97eabd..94f7567cbf4 100644 --- a/examples/gno.land/r/demo/userbook/render.gno +++ b/examples/gno.land/r/demo/userbook/render.gno @@ -2,16 +2,19 @@ package userbook import ( - "sort" "strconv" + "gno.land/r/demo/users" + "gno.land/p/demo/avl/pager" "gno.land/p/demo/ufmt" "gno.land/p/moul/txlink" ) +const usersLink = "/r/demo/users" + func Render(path string) string { - p := pager.NewPager(signupsTree, 2) + p := pager.NewPager(signupsTree, 20, true) page := p.MustGetPageByPath(path) out := "# Welcome to UserBook!\n\n" @@ -19,33 +22,19 @@ func Render(path string) string { out += ufmt.Sprintf("## [Click here to sign up!](%s)\n\n", txlink.Call("SignUp")) out += "---\n\n" - var sorted sortedSignups for _, item := range page.Items { - sorted = append(sorted, item.Value.(*Signup)) - } + signup := item.Value.(*Signup) + user := signup.address.String() - sort.Sort(sorted) + if data := users.GetUserByAddress(signup.address); data != nil { + user = ufmt.Sprintf("[%s](%s:%s)", data.Name, usersLink, data.Name) + } - for _, item := range sorted { - out += ufmt.Sprintf("- **User #%d - %s - signed up on %s**\n\n", item.ordinal, item.address.String(), item.timestamp.Format("02-01-2006 15:04:05")) + out += ufmt.Sprintf("- **User #%d - %s - signed up on %s**\n\n", signup.ordinal, user, signup.timestamp.Format("January 2 2006, 03:04:04 PM")) } out += "---\n\n" out += "**Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "**\n\n" - out += page.Selector() // Repeat selector for ease of navigation + out += page.Picker() return out } - -type sortedSignups []*Signup - -func (s sortedSignups) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -func (s sortedSignups) Len() int { - return len(s) -} - -func (s sortedSignups) Less(i, j int) bool { - return s[i].timestamp.Before(s[j].timestamp) -} diff --git a/examples/gno.land/r/demo/userbook/userbook.gno b/examples/gno.land/r/demo/userbook/userbook.gno index c958dc9e5b0..03027f064b0 100644 --- a/examples/gno.land/r/demo/userbook/userbook.gno +++ b/examples/gno.land/r/demo/userbook/userbook.gno @@ -6,6 +6,7 @@ import ( "time" "gno.land/p/demo/avl" + "gno.land/p/demo/seqid" "gno.land/p/demo/ufmt" ) @@ -15,7 +16,11 @@ type Signup struct { timestamp time.Time } -var signupsTree = avl.NewTree() +var ( + signupsTree = avl.NewTree() + tracker = avl.NewTree() + idCounter seqid.ID +) const signUpEvent = "SignUp" @@ -28,19 +33,22 @@ func SignUp() string { caller := std.PrevRealm().Addr() // Check if the user is already signed up - if _, exists := signupsTree.Get(caller.String()); exists { - panic(caller + " is already signed up!") + if _, exists := tracker.Get(caller.String()); exists { + panic(caller.String() + " is already signed up!") } now := time.Now() + // Sign up the user - signupsTree.Set(caller.String(), &Signup{ - std.PrevRealm().Addr(), + signupsTree.Set(idCounter.Next().String(), &Signup{ + caller, signupsTree.Size(), now, }) - std.Emit(signUpEvent, "SignedUpAccount", caller.String()) + tracker.Set(caller.String(), struct{}{}) + + std.Emit(signUpEvent, "account", caller.String()) return ufmt.Sprintf("%s added to userbook! Timestamp: %s", caller.String(), now.Format(time.RFC822Z)) } diff --git a/examples/gno.land/r/demo/users/users.gno b/examples/gno.land/r/demo/users/users.gno index 1f08c9ae08c..8547a6e60e0 100644 --- a/examples/gno.land/r/demo/users/users.gno +++ b/examples/gno.land/r/demo/users/users.gno @@ -328,14 +328,14 @@ func Render(fullPath string) string { func renderHome(path string) string { doc := "" - page := pager.NewPager(&name2User, 50).MustGetPageByPath(path) + page := pager.NewPager(&name2User, 50, false).MustGetPageByPath(path) for _, item := range page.Items { user := item.Value.(*users.User) doc += " * [" + user.Name + "](/r/demo/users:" + user.Name + ")\n" } doc += "\n" - doc += page.Selector() + doc += page.Picker() return doc } diff --git a/examples/gno.land/r/docs/avl_pager/avl_pager.gno b/examples/gno.land/r/docs/avl_pager/avl_pager.gno index 75807b71981..af8a6a10b48 100644 --- a/examples/gno.land/r/docs/avl_pager/avl_pager.gno +++ b/examples/gno.land/r/docs/avl_pager/avl_pager.gno @@ -22,19 +22,19 @@ func init() { // Render paginated content based on the given URL path. // URL format: `...?page=&size=` (default is page 1 and size 10). func Render(path string) string { - p := pager.NewPager(tree, 10) // Default page size is 10 + p := pager.NewPager(tree, 10, false) // Default page size is 10 page := p.MustGetPageByPath(path) // Header and pagination info result := "# Paginated Items\n" result += "Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "\n\n" - result += page.Selector() + "\n\n" + result += page.Picker() + "\n\n" // Display items on the current page for _, item := range page.Items { result += "- " + item.Key + ": " + item.Value.(string) + "\n" } - result += "\n" + page.Selector() // Repeat selector for ease of navigation + result += "\n" + page.Picker() // Repeat page picker for ease of navigation return result } diff --git a/examples/gno.land/r/leon/hof/render.gno b/examples/gno.land/r/leon/hof/render.gno index b4d51d03362..0721c7d6e72 100644 --- a/examples/gno.land/r/leon/hof/render.gno +++ b/examples/gno.land/r/leon/hof/render.gno @@ -38,7 +38,7 @@ func (e Exhibition) Render(path string, dashboard bool) string { out += "
\n\n" - page := pager.NewPager(e.itemsSorted, pageSize).MustGetPageByPath(path) + page := pager.NewPager(e.itemsSorted, pageSize, false).MustGetPageByPath(path) for i := len(page.Items) - 1; i >= 0; i-- { item := page.Items[i] @@ -52,7 +52,7 @@ func (e Exhibition) Render(path string, dashboard bool) string { out += "
\n\n" - out += page.Selector() + out += page.Picker() return out } From e35fc9a1c6f5e561660a29988bfc3eefa9280c4f Mon Sep 17 00:00:00 2001 From: Leon Hudak <33522493+leohhhn@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:32:23 +0100 Subject: [PATCH 03/20] docs: add `std.ChainDomain()` reference (#3301) ## Description Adds the reference docs on `std.ChainDomain()`. Co-authored-by: Antoine Eddi <5222525+aeddi@users.noreply.github.com> --- docs/reference/stdlibs/std/chain.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index 0e5ead338c5..b1791e65608 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -28,6 +28,18 @@ std.AssertOriginCall() ``` --- +## ChainDomain +```go +func ChainDomain() string +``` +Returns the chain domain. Currently only `gno.land` is supported. + +#### Usage +```go +domain := std.ChainDomain() // gno.land +``` +--- + ## Emit ```go func Emit(typ string, attrs ...string) From e46f457e70ac2881a8d2c0e3fe625ca9ecdda183 Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Sun, 8 Dec 2024 17:51:01 +0100 Subject: [PATCH 04/20] chore: remove PR template (#3300) See https://github.com/gnolang/gno/issues/3238#issuecomment-2526138055 Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .github/pull_request_template.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 12e07a9cde6..00000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,11 +0,0 @@ - - -
Contributors' checklist... - -- [ ] Added new tests, or not needed, or not feasible -- [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory -- [ ] Updated the official documentation or not needed -- [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description -- [ ] Added references to related issues and PRs -- [ ] Provided any useful hints for running manual tests -
From 666c54af3d4a7cf354546028de89ec99fe1ce984 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Sun, 8 Dec 2024 19:20:00 +0100 Subject: [PATCH 05/20] chore: import gnolang/overflow into gno/tm2/pkg/overflow (#3302) We remove the dependency on gnolang/overflow, a clone of https://github.com/JohnCGriffin/overflow which is now unmaintained, and import it into gno/tm2/pkg/overflow. We can now have full control on it, fix it and improve it. This PR just changes the import path, no content change is done yet. --------- Co-authored-by: Morgan Bazalgette --- contribs/gnodev/go.mod | 1 - contribs/gnodev/go.sum | 2 - contribs/gnofaucet/go.mod | 1 - contribs/gnofaucet/go.sum | 2 - contribs/gnogenesis/go.mod | 1 - contribs/gnogenesis/go.sum | 2 - contribs/gnohealth/go.mod | 1 - contribs/gnohealth/go.sum | 2 - contribs/gnokeykc/go.mod | 1 - contribs/gnokeykc/go.sum | 2 - contribs/gnomigrate/go.mod | 1 - contribs/gnomigrate/go.sum | 2 - gnovm/pkg/gnolang/machine.go | 3 +- go.mod | 1 - go.sum | 2 - misc/autocounterd/go.mod | 1 - misc/autocounterd/go.sum | 2 - misc/loop/go.mod | 1 - misc/loop/go.sum | 4 - tm2/pkg/overflow/README.md | 66 +++++ tm2/pkg/overflow/overflow.go | 131 ++++++++++ tm2/pkg/overflow/overflow_impl.go | 360 ++++++++++++++++++++++++++ tm2/pkg/overflow/overflow_template.sh | 112 ++++++++ tm2/pkg/overflow/overflow_test.go | 115 ++++++++ tm2/pkg/std/coin.go | 2 +- tm2/pkg/store/gas/store.go | 2 +- tm2/pkg/store/types/gas.go | 2 +- tm2/pkg/store/types/gas_test.go | 2 +- 28 files changed, 789 insertions(+), 35 deletions(-) create mode 100644 tm2/pkg/overflow/README.md create mode 100644 tm2/pkg/overflow/overflow.go create mode 100644 tm2/pkg/overflow/overflow_impl.go create mode 100755 tm2/pkg/overflow/overflow_template.sh create mode 100644 tm2/pkg/overflow/overflow_test.go diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index a315d88591c..2053a61db6c 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -50,7 +50,6 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index e38c3621483..f9250d34462 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -101,8 +101,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/contribs/gnofaucet/go.mod b/contribs/gnofaucet/go.mod index c5bb1ad0d81..eab9fc90c50 100644 --- a/contribs/gnofaucet/go.mod +++ b/contribs/gnofaucet/go.mod @@ -20,7 +20,6 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-chi/chi/v5 v5.1.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/contribs/gnofaucet/go.sum b/contribs/gnofaucet/go.sum index f4bdc65d7ec..aabe858e893 100644 --- a/contribs/gnofaucet/go.sum +++ b/contribs/gnofaucet/go.sum @@ -49,8 +49,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gnolang/faucet v0.3.2 h1:3QBrdmnQszRaAZbxgO5xDDm3czNa0L/RFmhnCkbxy5I= github.com/gnolang/faucet v0.3.2/go.mod h1:/wbw9h4ooMzzyNBuM0X+ol7CiPH2OFjAFF3bYAXqA7U= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= diff --git a/contribs/gnogenesis/go.mod b/contribs/gnogenesis/go.mod index b777cc6e5eb..f1b316c2bee 100644 --- a/contribs/gnogenesis/go.mod +++ b/contribs/gnogenesis/go.mod @@ -18,7 +18,6 @@ require ( github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect diff --git a/contribs/gnogenesis/go.sum b/contribs/gnogenesis/go.sum index 3c6127ac216..7ba3aede534 100644 --- a/contribs/gnogenesis/go.sum +++ b/contribs/gnogenesis/go.sum @@ -50,8 +50,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/contribs/gnohealth/go.mod b/contribs/gnohealth/go.mod index e6d9f119c7b..4f5862a0d2e 100644 --- a/contribs/gnohealth/go.mod +++ b/contribs/gnohealth/go.mod @@ -12,7 +12,6 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/contribs/gnohealth/go.sum b/contribs/gnohealth/go.sum index 116cfbff021..dd287d9ca84 100644 --- a/contribs/gnohealth/go.sum +++ b/contribs/gnohealth/go.sum @@ -45,8 +45,6 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/contribs/gnokeykc/go.mod b/contribs/gnokeykc/go.mod index 0c794afd54c..479daed22f6 100644 --- a/contribs/gnokeykc/go.mod +++ b/contribs/gnokeykc/go.mod @@ -21,7 +21,6 @@ require ( github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect diff --git a/contribs/gnokeykc/go.sum b/contribs/gnokeykc/go.sum index 50eb5add218..cacf6788d45 100644 --- a/contribs/gnokeykc/go.sum +++ b/contribs/gnokeykc/go.sum @@ -54,8 +54,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/contribs/gnomigrate/go.mod b/contribs/gnomigrate/go.mod index a81c2de4ba0..cd31adc4f6f 100644 --- a/contribs/gnomigrate/go.mod +++ b/contribs/gnomigrate/go.mod @@ -17,7 +17,6 @@ require ( github.com/cockroachdb/apd/v3 v3.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/snappy v0.0.4 // indirect diff --git a/contribs/gnomigrate/go.sum b/contribs/gnomigrate/go.sum index 3c6127ac216..7ba3aede534 100644 --- a/contribs/gnomigrate/go.sum +++ b/contribs/gnomigrate/go.sum @@ -50,8 +50,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index b48c0742e6f..a497648dbc8 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -11,10 +11,9 @@ import ( "strings" "sync" - "github.com/gnolang/overflow" - "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/tm2/pkg/errors" + "github.com/gnolang/gno/tm2/pkg/overflow" "github.com/gnolang/gno/tm2/pkg/store" ) diff --git a/go.mod b/go.mod index f73ba1926e6..f389e60b988 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/fortytw2/leaktest v1.3.0 - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 github.com/google/gofuzz v1.2.0 github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.3 diff --git a/go.sum b/go.sum index 78d60eeea90..b987535607e 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,6 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/misc/autocounterd/go.mod b/misc/autocounterd/go.mod index 5de1d3c2974..30a6f23b458 100644 --- a/misc/autocounterd/go.mod +++ b/misc/autocounterd/go.mod @@ -14,7 +14,6 @@ require ( github.com/cosmos/ledger-cosmos-go v0.13.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang/protobuf v1.5.4 // indirect diff --git a/misc/autocounterd/go.sum b/misc/autocounterd/go.sum index b34cbde0c00..5d624ca18cb 100644 --- a/misc/autocounterd/go.sum +++ b/misc/autocounterd/go.sum @@ -50,8 +50,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= diff --git a/misc/loop/go.mod b/misc/loop/go.mod index a6bbdad3c82..70e9d21734b 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -29,7 +29,6 @@ require ( github.com/distribution/reference v0.5.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect diff --git a/misc/loop/go.sum b/misc/loop/go.sum index 740cc629a21..8e0feb11e4a 100644 --- a/misc/loop/go.sum +++ b/misc/loop/go.sum @@ -68,8 +68,6 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= -github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/gnolang/tx-archive v0.4.0 h1:+1Rgo0U0HjLQLq/xqeGdJwtAzo9xWj09t1oZLvrL3bU= github.com/gnolang/tx-archive v0.4.0/go.mod h1:seKHGnvxUnDgH/mSsCEdwG0dHY/FrpbUm6Hd0+KMd9w= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -258,8 +256,6 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= -golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/tm2/pkg/overflow/README.md b/tm2/pkg/overflow/README.md new file mode 100644 index 00000000000..55a9ba4c327 --- /dev/null +++ b/tm2/pkg/overflow/README.md @@ -0,0 +1,66 @@ +# overflow + +Check for int/int8/int16/int64/int32 integer overflow in Golang arithmetic. + +Forked from https://github.com/JohnCGriffin/overflow + +### Install +``` +go get github.com/johncgriffin/overflow +``` +Note that because Go has no template types, the majority of repetitive code is +generated by overflow_template.sh. If you have to change an +algorithm, change it there and regenerate the Go code via: +``` +go generate +``` +### Synopsis + +``` +package main + +import "fmt" +import "math" +import "github.com/JohnCGriffin/overflow" + +func main() { + + addend := math.MaxInt64 - 5 + + for i := 0; i < 10; i++ { + sum, ok := overflow.Add(addend, i) + fmt.Printf("%v+%v -> (%v,%v)\n", + addend, i, sum, ok) + } + +} +``` +yields the output +``` +9223372036854775802+0 -> (9223372036854775802,true) +9223372036854775802+1 -> (9223372036854775803,true) +9223372036854775802+2 -> (9223372036854775804,true) +9223372036854775802+3 -> (9223372036854775805,true) +9223372036854775802+4 -> (9223372036854775806,true) +9223372036854775802+5 -> (9223372036854775807,true) +9223372036854775802+6 -> (0,false) +9223372036854775802+7 -> (0,false) +9223372036854775802+8 -> (0,false) +9223372036854775802+9 -> (0,false) +``` + +For int, int64, and int32 types, provide Add, Add32, Add64, Sub, Sub32, Sub64, etc. +Unsigned types not covered at the moment, but such additions are welcome. + +### Stay calm and panic + +There's a good case to be made that a panic is an unidiomatic but proper response. Iff you +believe that there's no valid way to continue your program after math goes wayward, you can +use the easier Addp, Mulp, Subp, and Divp versions which return the normal result or panic. + + + + + + + diff --git a/tm2/pkg/overflow/overflow.go b/tm2/pkg/overflow/overflow.go new file mode 100644 index 00000000000..b476ea5776e --- /dev/null +++ b/tm2/pkg/overflow/overflow.go @@ -0,0 +1,131 @@ +/* +Package overflow offers overflow-checked integer arithmetic operations +for int, int32, and int64. Each of the operations returns a +result,bool combination. This was prompted by the need to know when +to flow into higher precision types from the math.big library. + +For instance, assuing a 64 bit machine: + +10 + 20 -> 30 +int(math.MaxInt64) + 1 -> -9223372036854775808 + +whereas + +overflow.Add(10,20) -> (30, true) +overflow.Add(math.MaxInt64,1) -> (0, false) + +Add, Sub, Mul, Div are for int. Add64, Add32, etc. are specifically sized. + +If anybody wishes an unsigned version, submit a pull request for code +and new tests. +*/ +package overflow + +//go:generate ./overflow_template.sh + +import "math" + +func _is64Bit() bool { + maxU32 := uint(math.MaxUint32) + return ((maxU32 << 1) >> 1) == maxU32 +} + +/********** PARTIAL TEST COVERAGE FROM HERE DOWN ************* + +The only way that I could see to do this is a combination of +my normal 64 bit system and a GopherJS running on Node. My +understanding is that its ints are 32 bit. + +So, FEEL FREE to carefully review the code visually. + +*************************************************************/ + +// Unspecified size, i.e. normal signed int + +// Add sums two ints, returning the result and a boolean status. +func Add(a, b int) (int, bool) { + if _is64Bit() { + r64, ok := Add64(int64(a), int64(b)) + return int(r64), ok + } + r32, ok := Add32(int32(a), int32(b)) + return int(r32), ok +} + +// Sub returns the difference of two ints and a boolean status. +func Sub(a, b int) (int, bool) { + if _is64Bit() { + r64, ok := Sub64(int64(a), int64(b)) + return int(r64), ok + } + r32, ok := Sub32(int32(a), int32(b)) + return int(r32), ok +} + +// Mul returns the product of two ints and a boolean status. +func Mul(a, b int) (int, bool) { + if _is64Bit() { + r64, ok := Mul64(int64(a), int64(b)) + return int(r64), ok + } + r32, ok := Mul32(int32(a), int32(b)) + return int(r32), ok +} + +// Div returns the quotient of two ints and a boolean status +func Div(a, b int) (int, bool) { + if _is64Bit() { + r64, ok := Div64(int64(a), int64(b)) + return int(r64), ok + } + r32, ok := Div32(int32(a), int32(b)) + return int(r32), ok +} + +// Quotient returns the quotient, remainder and status of two ints +func Quotient(a, b int) (int, int, bool) { + if _is64Bit() { + q64, r64, ok := Quotient64(int64(a), int64(b)) + return int(q64), int(r64), ok + } + q32, r32, ok := Quotient32(int32(a), int32(b)) + return int(q32), int(r32), ok +} + +/************* Panic versions for int ****************/ + +// Addp returns the sum of two ints, panicking on overflow +func Addp(a, b int) int { + r, ok := Add(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Subp returns the difference of two ints, panicking on overflow. +func Subp(a, b int) int { + r, ok := Sub(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mulp returns the product of two ints, panicking on overflow. +func Mulp(a, b int) int { + r, ok := Mul(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Divp returns the quotient of two ints, panicking on overflow. +func Divp(a, b int) int { + r, ok := Div(a, b) + if !ok { + panic("division failure") + } + return r +} diff --git a/tm2/pkg/overflow/overflow_impl.go b/tm2/pkg/overflow/overflow_impl.go new file mode 100644 index 00000000000..a9a90c43835 --- /dev/null +++ b/tm2/pkg/overflow/overflow_impl.go @@ -0,0 +1,360 @@ +package overflow + +// This is generated code, created by overflow_template.sh executed +// by "go generate" + +// Add8 performs + operation on two int8 operands +// returning a result and status +func Add8(a, b int8) (int8, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add8p is the unchecked panicing version of Add8 +func Add8p(a, b int8) int8 { + r, ok := Add8(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Sub8 performs - operation on two int8 operands +// returning a result and status +func Sub8(a, b int8) (int8, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub8p is the unchecked panicing version of Sub8 +func Sub8p(a, b int8) int8 { + r, ok := Sub8(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mul8 performs * operation on two int8 operands +// returning a result and status +func Mul8(a, b int8) (int8, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul8p is the unchecked panicing version of Mul8 +func Mul8p(a, b int8) int8 { + r, ok := Mul8(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Div8 performs / operation on two int8 operands +// returning a result and status +func Div8(a, b int8) (int8, bool) { + q, _, ok := Quotient8(a, b) + return q, ok +} + +// Div8p is the unchecked panicing version of Div8 +func Div8p(a, b int8) int8 { + r, ok := Div8(a, b) + if !ok { + panic("division failure") + } + return r +} + +// Quotient8 performs + operation on two int8 operands +// returning a quotient, a remainder and status +func Quotient8(a, b int8) (int8, int8, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} + +// Add16 performs + operation on two int16 operands +// returning a result and status +func Add16(a, b int16) (int16, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add16p is the unchecked panicing version of Add16 +func Add16p(a, b int16) int16 { + r, ok := Add16(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Sub16 performs - operation on two int16 operands +// returning a result and status +func Sub16(a, b int16) (int16, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub16p is the unchecked panicing version of Sub16 +func Sub16p(a, b int16) int16 { + r, ok := Sub16(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mul16 performs * operation on two int16 operands +// returning a result and status +func Mul16(a, b int16) (int16, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul16p is the unchecked panicing version of Mul16 +func Mul16p(a, b int16) int16 { + r, ok := Mul16(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Div16 performs / operation on two int16 operands +// returning a result and status +func Div16(a, b int16) (int16, bool) { + q, _, ok := Quotient16(a, b) + return q, ok +} + +// Div16p is the unchecked panicing version of Div16 +func Div16p(a, b int16) int16 { + r, ok := Div16(a, b) + if !ok { + panic("division failure") + } + return r +} + +// Quotient16 performs + operation on two int16 operands +// returning a quotient, a remainder and status +func Quotient16(a, b int16) (int16, int16, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} + +// Add32 performs + operation on two int32 operands +// returning a result and status +func Add32(a, b int32) (int32, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add32p is the unchecked panicing version of Add32 +func Add32p(a, b int32) int32 { + r, ok := Add32(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Sub32 performs - operation on two int32 operands +// returning a result and status +func Sub32(a, b int32) (int32, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub32p is the unchecked panicing version of Sub32 +func Sub32p(a, b int32) int32 { + r, ok := Sub32(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mul32 performs * operation on two int32 operands +// returning a result and status +func Mul32(a, b int32) (int32, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul32p is the unchecked panicing version of Mul32 +func Mul32p(a, b int32) int32 { + r, ok := Mul32(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Div32 performs / operation on two int32 operands +// returning a result and status +func Div32(a, b int32) (int32, bool) { + q, _, ok := Quotient32(a, b) + return q, ok +} + +// Div32p is the unchecked panicing version of Div32 +func Div32p(a, b int32) int32 { + r, ok := Div32(a, b) + if !ok { + panic("division failure") + } + return r +} + +// Quotient32 performs + operation on two int32 operands +// returning a quotient, a remainder and status +func Quotient32(a, b int32) (int32, int32, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} + +// Add64 performs + operation on two int64 operands +// returning a result and status +func Add64(a, b int64) (int64, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add64p is the unchecked panicing version of Add64 +func Add64p(a, b int64) int64 { + r, ok := Add64(a, b) + if !ok { + panic("addition overflow") + } + return r +} + +// Sub64 performs - operation on two int64 operands +// returning a result and status +func Sub64(a, b int64) (int64, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub64p is the unchecked panicing version of Sub64 +func Sub64p(a, b int64) int64 { + r, ok := Sub64(a, b) + if !ok { + panic("subtraction overflow") + } + return r +} + +// Mul64 performs * operation on two int64 operands +// returning a result and status +func Mul64(a, b int64) (int64, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul64p is the unchecked panicing version of Mul64 +func Mul64p(a, b int64) int64 { + r, ok := Mul64(a, b) + if !ok { + panic("multiplication overflow") + } + return r +} + +// Div64 performs / operation on two int64 operands +// returning a result and status +func Div64(a, b int64) (int64, bool) { + q, _, ok := Quotient64(a, b) + return q, ok +} + +// Div64p is the unchecked panicing version of Div64 +func Div64p(a, b int64) int64 { + r, ok := Div64(a, b) + if !ok { + panic("division failure") + } + return r +} + +// Quotient64 performs + operation on two int64 operands +// returning a quotient, a remainder and status +func Quotient64(a, b int64) (int64, int64, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} diff --git a/tm2/pkg/overflow/overflow_template.sh b/tm2/pkg/overflow/overflow_template.sh new file mode 100755 index 00000000000..a2a85f2c581 --- /dev/null +++ b/tm2/pkg/overflow/overflow_template.sh @@ -0,0 +1,112 @@ +#!/bin/sh + +exec > overflow_impl.go + +echo "package overflow + +// This is generated code, created by overflow_template.sh executed +// by \"go generate\" + +" + + +for SIZE in 8 16 32 64 +do +echo " + +// Add${SIZE} performs + operation on two int${SIZE} operands +// returning a result and status +func Add${SIZE}(a, b int${SIZE}) (int${SIZE}, bool) { + c := a + b + if (c > a) == (b > 0) { + return c, true + } + return c, false +} + +// Add${SIZE}p is the unchecked panicing version of Add${SIZE} +func Add${SIZE}p(a, b int${SIZE}) int${SIZE} { + r, ok := Add${SIZE}(a, b) + if !ok { + panic(\"addition overflow\") + } + return r +} + + +// Sub${SIZE} performs - operation on two int${SIZE} operands +// returning a result and status +func Sub${SIZE}(a, b int${SIZE}) (int${SIZE}, bool) { + c := a - b + if (c < a) == (b > 0) { + return c, true + } + return c, false +} + +// Sub${SIZE}p is the unchecked panicing version of Sub${SIZE} +func Sub${SIZE}p(a, b int${SIZE}) int${SIZE} { + r, ok := Sub${SIZE}(a, b) + if !ok { + panic(\"subtraction overflow\") + } + return r +} + + +// Mul${SIZE} performs * operation on two int${SIZE} operands +// returning a result and status +func Mul${SIZE}(a, b int${SIZE}) (int${SIZE}, bool) { + if a == 0 || b == 0 { + return 0, true + } + c := a * b + if (c < 0) == ((a < 0) != (b < 0)) { + if c/b == a { + return c, true + } + } + return c, false +} + +// Mul${SIZE}p is the unchecked panicing version of Mul${SIZE} +func Mul${SIZE}p(a, b int${SIZE}) int${SIZE} { + r, ok := Mul${SIZE}(a, b) + if !ok { + panic(\"multiplication overflow\") + } + return r +} + + + +// Div${SIZE} performs / operation on two int${SIZE} operands +// returning a result and status +func Div${SIZE}(a, b int${SIZE}) (int${SIZE}, bool) { + q, _, ok := Quotient${SIZE}(a, b) + return q, ok +} + +// Div${SIZE}p is the unchecked panicing version of Div${SIZE} +func Div${SIZE}p(a, b int${SIZE}) int${SIZE} { + r, ok := Div${SIZE}(a, b) + if !ok { + panic(\"division failure\") + } + return r +} + +// Quotient${SIZE} performs + operation on two int${SIZE} operands +// returning a quotient, a remainder and status +func Quotient${SIZE}(a, b int${SIZE}) (int${SIZE}, int${SIZE}, bool) { + if b == 0 { + return 0, 0, false + } + c := a / b + status := (c < 0) == ((a < 0) != (b < 0)) + return c, a % b, status +} +" +done + +go run -modfile ../../../misc/devdeps/go.mod mvdan.cc/gofumpt -w overflow_impl.go diff --git a/tm2/pkg/overflow/overflow_test.go b/tm2/pkg/overflow/overflow_test.go new file mode 100644 index 00000000000..2b2d345b55d --- /dev/null +++ b/tm2/pkg/overflow/overflow_test.go @@ -0,0 +1,115 @@ +package overflow + +import ( + "fmt" + "math" + "testing" +) + +// sample all possibilities of 8 bit numbers +// by checking against 64 bit numbers + +func TestAlgorithms(t *testing.T) { + errors := 0 + + for a64 := int64(math.MinInt8); a64 <= int64(math.MaxInt8); a64++ { + for b64 := int64(math.MinInt8); b64 <= int64(math.MaxInt8) && errors < 10; b64++ { + a8 := int8(a64) + b8 := int8(b64) + + if int64(a8) != a64 || int64(b8) != b64 { + t.Fatal("LOGIC FAILURE IN TEST") + } + + // ADDITION + { + r64 := a64 + b64 + + // now the verification + result, ok := Add8(a8, b8) + if ok && int64(result) != r64 { + t.Errorf("failed to fail on %v + %v = %v instead of %v\n", + a8, b8, result, r64) + errors++ + } + if !ok && int64(result) == r64 { + t.Fail() + errors++ + } + } + + // SUBTRACTION + { + r64 := a64 - b64 + + // now the verification + result, ok := Sub8(a8, b8) + if ok && int64(result) != r64 { + t.Errorf("failed to fail on %v - %v = %v instead of %v\n", + a8, b8, result, r64) + } + if !ok && int64(result) == r64 { + t.Fail() + errors++ + } + } + + // MULTIPLICATION + { + r64 := a64 * b64 + + // now the verification + result, ok := Mul8(a8, b8) + if ok && int64(result) != r64 { + t.Errorf("failed to fail on %v * %v = %v instead of %v\n", + a8, b8, result, r64) + errors++ + } + if !ok && int64(result) == r64 { + t.Fail() + errors++ + } + } + + // DIVISION + if b8 != 0 { + r64 := a64 / b64 + + // now the verification + result, _, ok := Quotient8(a8, b8) + if ok && int64(result) != r64 { + t.Errorf("failed to fail on %v / %v = %v instead of %v\n", + a8, b8, result, r64) + errors++ + } + if !ok && result != 0 && int64(result) == r64 { + t.Fail() + errors++ + } + } + } + } +} + +func TestQuotient(t *testing.T) { + q, r, ok := Quotient(100, 3) + if r != 1 || q != 33 || !ok { + t.Errorf("expected 100/3 => 33, r=1") + } + if _, _, ok = Quotient(1, 0); ok { + t.Error("unexpected lack of failure") + } +} + +//func TestAdditionInt(t *testing.T) { +// fmt.Printf("\nminint8 = %v\n", math.MinInt8) +// fmt.Printf("maxint8 = %v\n\n", math.MaxInt8) +// fmt.Printf("maxint32 = %v\n", math.MaxInt32) +// fmt.Printf("minint32 = %v\n\n", math.MinInt32) +// fmt.Printf("maxint64 = %v\n", math.MaxInt64) +// fmt.Printf("minint64 = %v\n\n", math.MinInt64) +//} + +func Test64(t *testing.T) { + fmt.Println("64bit:", _is64Bit()) +} diff --git a/tm2/pkg/std/coin.go b/tm2/pkg/std/coin.go index 6457b193a6b..fba20a5ba78 100644 --- a/tm2/pkg/std/coin.go +++ b/tm2/pkg/std/coin.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/gnolang/gno/tm2/pkg/errors" - "github.com/gnolang/overflow" + "github.com/gnolang/gno/tm2/pkg/overflow" ) // ----------------------------------------------------------------------------- diff --git a/tm2/pkg/store/gas/store.go b/tm2/pkg/store/gas/store.go index db5ea7a79b0..81e898a90d8 100644 --- a/tm2/pkg/store/gas/store.go +++ b/tm2/pkg/store/gas/store.go @@ -1,9 +1,9 @@ package gas import ( + "github.com/gnolang/gno/tm2/pkg/overflow" "github.com/gnolang/gno/tm2/pkg/store/types" "github.com/gnolang/gno/tm2/pkg/store/utils" - "github.com/gnolang/overflow" ) var _ types.Store = &Store{} diff --git a/tm2/pkg/store/types/gas.go b/tm2/pkg/store/types/gas.go index fd631dd3259..9d1f3d70c28 100644 --- a/tm2/pkg/store/types/gas.go +++ b/tm2/pkg/store/types/gas.go @@ -3,7 +3,7 @@ package types import ( "math" - "github.com/gnolang/overflow" + "github.com/gnolang/gno/tm2/pkg/overflow" ) // Gas consumption descriptors. diff --git a/tm2/pkg/store/types/gas_test.go b/tm2/pkg/store/types/gas_test.go index 410ba0b7e92..115d347bd5e 100644 --- a/tm2/pkg/store/types/gas_test.go +++ b/tm2/pkg/store/types/gas_test.go @@ -4,7 +4,7 @@ import ( "math" "testing" - "github.com/gnolang/overflow" + "github.com/gnolang/gno/tm2/pkg/overflow" "github.com/stretchr/testify/require" ) From 5f16b8c703867c3311c9023b60105b010a9e97be Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 9 Dec 2024 10:14:40 +0100 Subject: [PATCH 06/20] fix(gnovm): use strconv.UnquoteChar to parse rune literals (#3296) This fixes a bug, as shown in rune3.gno, whereby rune literals which would not be parsed correctly by `strconv.Unquote` are now parsed correctly. Previously, the test would print out 65533, for the unicode invalid code point. --- gnovm/pkg/gnolang/nodes.go | 1 + gnovm/pkg/gnolang/op_eval.go | 10 ++++------ gnovm/tests/files/rune3.gno | 10 ++++++++++ 3 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 gnovm/tests/files/rune3.gno diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index 8d3d6d8a2cc..0496d37ed72 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -2153,6 +2153,7 @@ type ValuePather interface { // Utility func (x *BasicLitExpr) GetString() string { + // Matches string literal parsing in go/constant.MakeFromLiteral. str, err := strconv.Unquote(x.Value) if err != nil { panic("error in parsing string literal: " + err.Error()) diff --git a/gnovm/pkg/gnolang/op_eval.go b/gnovm/pkg/gnolang/op_eval.go index 1beba1d6e3f..2aa13b21753 100644 --- a/gnovm/pkg/gnolang/op_eval.go +++ b/gnovm/pkg/gnolang/op_eval.go @@ -204,16 +204,14 @@ func (m *Machine) doOpEval() { // and github.com/golang/go/issues/19921 panic("imaginaries are not supported") case CHAR: - cstr, err := strconv.Unquote(x.Value) + // Matching character literal parsing in go/constant.MakeFromLiteral. + val := x.Value + rne, _, _, err := strconv.UnquoteChar(val[1:len(val)-1], '\'') if err != nil { panic("error in parsing character literal: " + err.Error()) } - runes := []rune(cstr) - if len(runes) != 1 { - panic(fmt.Sprintf("error in parsing character literal: 1 rune expected, but got %v (%s)", len(runes), cstr)) - } tv := TypedValue{T: UntypedRuneType} - tv.SetInt32(runes[0]) + tv.SetInt32(rne) m.PushValue(tv) case STRING: m.PushValue(TypedValue{ diff --git a/gnovm/tests/files/rune3.gno b/gnovm/tests/files/rune3.gno new file mode 100644 index 00000000000..e848565e3a4 --- /dev/null +++ b/gnovm/tests/files/rune3.gno @@ -0,0 +1,10 @@ +package main + +const overflow = '\xff' + +func main() { + println(overflow) +} + +// Output: +// 255 From 1fba5cfa840d3499d9ce22507a7d2ada19abbdd4 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 9 Dec 2024 11:13:38 +0100 Subject: [PATCH 07/20] feat(cmd/gno): perform type checking when calling linter (#1730) Depends on (in order): 1. #1700 2. #1702 This PR uses the type checker added in #1702 to perform Gno type checking when calling `gno lint`. Additionally, it adds validation of gno.mod indirectly (the parsed gno mod is used to determine if a package is a draft, and if so skip type checking). Because `gno lint` uses the TestStore, the resulting `MemPackage`s may contain redefinitions, for overwriting standard libraries like `AssertOriginCall`. I changed the type checker to filter out the redefinitions before they reach the Go type checker. Further improvements, which can be done after this: - Add shims for gonative special libraries (`fmt`, `os`...) - This will allow us to fully type check also tests and filetests - Make the type checking on-chain (#1702) also typecheck tests - as a consequence of the above. --- gnovm/cmd/gno/lint.go | 205 ++++++++++++------ gnovm/cmd/gno/lint_test.go | 39 ++-- gnovm/cmd/gno/run_test.go | 19 +- .../gno/testdata/{gno_fmt => fmt}/empty.txtar | 0 .../{gno_fmt => fmt}/import_cleaning.txtar | 0 .../testdata/{gno_fmt => fmt}/include.txtar | 0 .../{gno_fmt => fmt}/multi_import.txtar | 0 .../{gno_fmt => fmt}/noimport_format.txtar | 0 .../{gno_fmt => fmt}/parse_error.txtar | 0 .../{gno_fmt => fmt}/shadow_import.txtar | 0 .../gno/testdata/gno_lint/file_error_txtar | 20 -- .../{gno_lint => lint}/bad_import.txtar | 8 +- .../{gno_lint => lint}/file_error.txtar | 5 +- .../{gno_lint => lint}/no_error.txtar | 9 +- .../{gno_lint => lint}/no_gnomod.txtar | 6 +- .../{gno_lint => lint}/not_declared.txtar | 12 +- .../{gno_test => test}/dir_not_exist.txtar | 0 .../{gno_test => test}/empty_dir.txtar | 0 .../{gno_test => test}/empty_gno1.txtar | 0 .../{gno_test => test}/empty_gno2.txtar | 0 .../{gno_test => test}/empty_gno3.txtar | 0 .../{gno_test => test}/error_correct.txtar | 0 .../{gno_test => test}/error_incorrect.txtar | 0 .../{gno_test => test}/error_sync.txtar | 0 .../{gno_test => test}/failing_filetest.txtar | 0 .../{gno_test => test}/failing_test.txtar | 0 .../{gno_test => test}/filetest_events.txtar | 0 .../flag_print-runtime-metrics.txtar | 0 .../{gno_test => test}/flag_run.txtar | 0 .../{gno_test => test}/flag_timeout.txtar | 0 .../{gno_test => test}/fmt_write_import.txtar | 0 .../testdata/{gno_test => test}/minim1.txtar | 0 .../testdata/{gno_test => test}/minim2.txtar | 0 .../testdata/{gno_test => test}/minim3.txtar | 0 .../{gno_test => test}/multitest_events.txtar | 0 .../testdata/{gno_test => test}/no_args.txtar | 0 .../{gno_test => test}/output_correct.txtar | 0 .../{gno_test => test}/output_incorrect.txtar | 0 .../{gno_test => test}/output_sync.txtar | 0 .../testdata/{gno_test => test}/panic.txtar | 0 .../pkg_underscore_test.txtar | 0 .../realm_boundmethod.txtar | 0 .../{gno_test => test}/realm_correct.txtar | 0 .../{gno_test => test}/realm_incorrect.txtar | 0 .../{gno_test => test}/realm_sync.txtar | 0 .../testdata/{gno_test => test}/recover.txtar | 0 .../testdata/{gno_test => test}/skip.txtar | 0 .../{gno_test => test}/unknown_package.txtar | 0 .../{gno_test => test}/valid_filetest.txtar | 0 .../{gno_test => test}/valid_test.txtar | 0 .../gobuild_flag_build_error.txtar | 0 .../gobuild_flag_parse_error.txtar | 0 .../invalid_import.txtar | 0 .../no_args.txtar | 0 .../parse_error.txtar | 0 .../valid_empty_dir.txtar | 0 .../valid_gobuild_file.txtar | 0 .../valid_gobuild_flag.txtar | 0 .../valid_output_flag.txtar | 0 .../valid_output_gobuild.txtar | 0 .../valid_transpile_file.txtar | 0 .../valid_transpile_package.txtar | 0 .../valid_transpile_tree.txtar | 0 gnovm/cmd/gno/testdata_test.go | 1 - gnovm/cmd/gno/transpile_test.go | 20 -- gnovm/pkg/gnolang/go2gno.go | 59 ++++- .../integ/typecheck_missing_return/gno.mod | 1 + .../integ/typecheck_missing_return/main.gno | 5 + 68 files changed, 263 insertions(+), 146 deletions(-) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/empty.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/import_cleaning.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/include.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/multi_import.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/noimport_format.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/parse_error.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_fmt => fmt}/shadow_import.txtar (100%) delete mode 100644 gnovm/cmd/gno/testdata/gno_lint/file_error_txtar rename gnovm/cmd/gno/testdata/{gno_lint => lint}/bad_import.txtar (54%) rename gnovm/cmd/gno/testdata/{gno_lint => lint}/file_error.txtar (88%) rename gnovm/cmd/gno/testdata/{gno_lint => lint}/no_error.txtar (68%) rename gnovm/cmd/gno/testdata/{gno_lint => lint}/no_gnomod.txtar (60%) rename gnovm/cmd/gno/testdata/{gno_lint => lint}/not_declared.txtar (55%) rename gnovm/cmd/gno/testdata/{gno_test => test}/dir_not_exist.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/empty_dir.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/empty_gno1.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/empty_gno2.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/empty_gno3.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/error_correct.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/error_incorrect.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/error_sync.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/failing_filetest.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/failing_test.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/filetest_events.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/flag_print-runtime-metrics.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/flag_run.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/flag_timeout.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/fmt_write_import.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/minim1.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/minim2.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/minim3.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/multitest_events.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/no_args.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/output_correct.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/output_incorrect.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/output_sync.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/panic.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/pkg_underscore_test.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/realm_boundmethod.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/realm_correct.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/realm_incorrect.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/realm_sync.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/recover.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/skip.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/unknown_package.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/valid_filetest.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_test => test}/valid_test.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/gobuild_flag_build_error.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/gobuild_flag_parse_error.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/invalid_import.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/no_args.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/parse_error.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_empty_dir.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_gobuild_file.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_gobuild_flag.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_output_flag.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_output_gobuild.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_transpile_file.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_transpile_package.txtar (100%) rename gnovm/cmd/gno/testdata/{gno_transpile => transpile}/valid_transpile_tree.txtar (100%) create mode 100644 gnovm/tests/integ/typecheck_missing_return/gno.mod create mode 100644 gnovm/tests/integ/typecheck_missing_return/main.gno diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index 6d5399ca932..a3e7f5310e1 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -6,17 +6,19 @@ import ( "flag" "fmt" "go/scanner" + "go/types" "io" "os" "path/filepath" "regexp" "strings" + "github.com/gnolang/gno/gnovm" "github.com/gnolang/gno/gnovm/pkg/gnoenv" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/gnovm/pkg/test" "github.com/gnolang/gno/tm2/pkg/commands" - osm "github.com/gnolang/gno/tm2/pkg/os" "go.uber.org/multierr" ) @@ -50,6 +52,31 @@ func (c *lintCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar(&c.rootDir, "root-dir", rootdir, "clone location of github.com/gnolang/gno (gno tries to guess it)") } +type lintCode int + +const ( + lintUnknown lintCode = iota + lintGnoMod + lintGnoError + lintParserError + lintTypeCheckError + + // TODO: add new linter codes here. +) + +type lintIssue struct { + Code lintCode + Msg string + Confidence float64 // 1 is 100% + Location string // file:line, or equivalent + // TODO: consider writing fix suggestions +} + +func (i lintIssue) String() string { + // TODO: consider crafting a doc URL based on Code. + return fmt.Sprintf("%s: %s (code=%d)", i.Location, i.Msg, i.Code) +} + func execLint(cfg *lintCfg, args []string, io commands.IO) error { if len(args) < 1 { return flag.ErrHelp @@ -72,37 +99,55 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { for _, pkgPath := range pkgPaths { if verbose { - fmt.Fprintf(io.Err(), "Linting %q...\n", pkgPath) + io.ErrPrintln(pkgPath) + } + + info, err := os.Stat(pkgPath) + if err == nil && !info.IsDir() { + pkgPath = filepath.Dir(pkgPath) } // Check if 'gno.mod' exists - gnoModPath := filepath.Join(pkgPath, "gno.mod") - if !osm.FileExists(gnoModPath) { - hasError = true + gmFile, err := gnomod.ParseAt(pkgPath) + if err != nil { issue := lintIssue{ - Code: lintNoGnoMod, + Code: lintGnoMod, Confidence: 1, Location: pkgPath, - Msg: "missing 'gno.mod' file", + Msg: err.Error(), } - fmt.Fprint(io.Err(), issue.String()+"\n") + io.ErrPrintln(issue) + hasError = true } - // Handle runtime errors - hasError = catchRuntimeError(pkgPath, io.Err(), func() { - stdout, stdin, stderr := io.Out(), io.In(), io.Err() - _, testStore := test.Store( - rootDir, false, - stdin, stdout, stderr, - ) - - targetPath := pkgPath - info, err := os.Stat(pkgPath) - if err == nil && !info.IsDir() { - targetPath = filepath.Dir(pkgPath) + stdout, stdin, stderr := io.Out(), io.In(), io.Err() + _, testStore := test.Store( + rootDir, false, + stdin, stdout, stderr, + ) + + memPkg, err := gno.ReadMemPackage(pkgPath, pkgPath) + if err != nil { + io.ErrPrintln(issueFromError(pkgPath, err).String()) + hasError = true + continue + } + + // Run type checking + if gmFile == nil || !gmFile.Draft { + foundErr, err := lintTypeCheck(io, memPkg, testStore) + if err != nil { + io.ErrPrintln(err) + hasError = true + } else if foundErr { + hasError = true } + } else if verbose { + io.ErrPrintfln("%s: module is draft, skipping type check", pkgPath) + } - memPkg := gno.MustReadMemPackage(targetPath, targetPath) + // Handle runtime errors + hasRuntimeErr := catchRuntimeError(pkgPath, io.Err(), func() { tm := test.Machine(testStore, stdout, memPkg.Path) defer tm.Release() @@ -110,28 +155,13 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { tm.RunMemPackage(memPkg, true) // Check test files - testfiles := &gno.FileSet{} - for _, mfile := range memPkg.Files { - if !strings.HasSuffix(mfile.Name, ".gno") { - continue // Skip non-GNO files - } + testFiles := lintTestFiles(memPkg) - n, _ := gno.ParseFile(mfile.Name, mfile.Body) - if n == nil { - continue // Skip empty files - } - - // XXX: package ending with `_test` is not supported yet - if strings.HasSuffix(mfile.Name, "_test.gno") && !strings.HasSuffix(string(n.PkgName), "_test") { - // Keep only test files - testfiles.AddFiles(n) - } - } - - tm.RunFiles(testfiles.Files...) - }) || hasError - - // TODO: Add more checkers + tm.RunFiles(testFiles.Files...) + }) + if hasRuntimeErr { + hasError = true + } } if hasError { @@ -141,6 +171,66 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { return nil } +func lintTypeCheck(io commands.IO, memPkg *gnovm.MemPackage, testStore gno.Store) (errorsFound bool, err error) { + tcErr := gno.TypeCheckMemPackageTest(memPkg, testStore) + if tcErr == nil { + return false, nil + } + + errs := multierr.Errors(tcErr) + for _, err := range errs { + switch err := err.(type) { + case types.Error: + io.ErrPrintln(lintIssue{ + Code: lintTypeCheckError, + Msg: err.Msg, + Confidence: 1, + Location: err.Fset.Position(err.Pos).String(), + }) + case scanner.ErrorList: + for _, scErr := range err { + io.ErrPrintln(lintIssue{ + Code: lintParserError, + Msg: scErr.Msg, + Confidence: 1, + Location: scErr.Pos.String(), + }) + } + case scanner.Error: + io.ErrPrintln(lintIssue{ + Code: lintParserError, + Msg: err.Msg, + Confidence: 1, + Location: err.Pos.String(), + }) + default: + return false, fmt.Errorf("unexpected error type: %T", err) + } + } + return true, nil +} + +func lintTestFiles(memPkg *gnovm.MemPackage) *gno.FileSet { + testfiles := &gno.FileSet{} + for _, mfile := range memPkg.Files { + if !strings.HasSuffix(mfile.Name, ".gno") { + continue // Skip non-GNO files + } + + n, _ := gno.ParseFile(mfile.Name, mfile.Body) + if n == nil { + continue // Skip empty files + } + + // XXX: package ending with `_test` is not supported yet + if strings.HasSuffix(mfile.Name, "_test.gno") && !strings.HasSuffix(string(n.PkgName), "_test") { + // Keep only test files + testfiles.AddFiles(n) + } + } + return testfiles +} + func guessSourcePath(pkg, source string) string { if info, err := os.Stat(pkg); !os.IsNotExist(err) && !info.IsDir() { pkg = filepath.Dir(pkg) @@ -174,21 +264,21 @@ func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (ha switch verr := r.(type) { case *gno.PreprocessError: err := verr.Unwrap() - fmt.Fprint(stderr, issueFromError(pkgPath, err).String()+"\n") + fmt.Fprintln(stderr, issueFromError(pkgPath, err).String()) case error: errors := multierr.Errors(verr) for _, err := range errors { errList, ok := err.(scanner.ErrorList) if ok { for _, errorInList := range errList { - fmt.Fprint(stderr, issueFromError(pkgPath, errorInList).String()+"\n") + fmt.Fprintln(stderr, issueFromError(pkgPath, errorInList).String()) } } else { - fmt.Fprint(stderr, issueFromError(pkgPath, err).String()+"\n") + fmt.Fprintln(stderr, issueFromError(pkgPath, err).String()) } } case string: - fmt.Fprint(stderr, issueFromError(pkgPath, errors.New(verr)).String()+"\n") + fmt.Fprintln(stderr, issueFromError(pkgPath, errors.New(verr)).String()) default: panic(r) } @@ -198,29 +288,6 @@ func catchRuntimeError(pkgPath string, stderr io.WriteCloser, action func()) (ha return } -type lintCode int - -const ( - lintUnknown lintCode = 0 - lintNoGnoMod lintCode = iota - lintGnoError - - // TODO: add new linter codes here. -) - -type lintIssue struct { - Code lintCode - Msg string - Confidence float64 // 1 is 100% - Location string // file:line, or equivalent - // TODO: consider writing fix suggestions -} - -func (i lintIssue) String() string { - // TODO: consider crafting a doc URL based on Code. - return fmt.Sprintf("%s: %s (code=%d).", i.Location, i.Msg, i.Code) -} - func issueFromError(pkgPath string, err error) lintIssue { var issue lintIssue issue.Confidence = 1 diff --git a/gnovm/cmd/gno/lint_test.go b/gnovm/cmd/gno/lint_test.go index 031c252bc79..4589fc55f92 100644 --- a/gnovm/cmd/gno/lint_test.go +++ b/gnovm/cmd/gno/lint_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "strings" + "testing" +) func TestLintApp(t *testing.T) { tc := []testMainCase{ @@ -9,7 +12,7 @@ func TestLintApp(t *testing.T) { errShouldBe: "flag: help requested", }, { args: []string{"lint", "../../tests/integ/run_main/"}, - stderrShouldContain: "./../../tests/integ/run_main: missing 'gno.mod' file (code=1).", + stderrShouldContain: "./../../tests/integ/run_main: gno.mod file not found in current or any parent directory (code=1)", errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/undefined_variable_test/undefined_variables_test.gno"}, @@ -17,33 +20,43 @@ func TestLintApp(t *testing.T) { errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/package_not_declared/main.gno"}, - stderrShouldContain: "main.gno:4:2: name fmt not declared (code=2).", + stderrShouldContain: "main.gno:4:2: name fmt not declared (code=2)", errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/several-lint-errors/main.gno"}, - stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several-lint-errors/main.gno:6", + stderrShouldContain: "../../tests/integ/several-lint-errors/main.gno:5:5: expected ';', found example (code=2)\n../../tests/integ/several-lint-errors/main.gno:6", errShouldBe: "exit code: 1", }, { - args: []string{"lint", "../../tests/integ/several-files-multiple-errors/main.gno"}, - stderrShouldContain: "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2).\n", - errShouldBe: "exit code: 1", - }, { - args: []string{"lint", "../../tests/integ/run_main/"}, - stderrShouldContain: "./../../tests/integ/run_main: missing 'gno.mod' file (code=1).", - errShouldBe: "exit code: 1", + args: []string{"lint", "../../tests/integ/several-files-multiple-errors/main.gno"}, + stderrShouldContain: func() string { + lines := []string{ + "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2)", + "../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2)", + "../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2)", + "../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2)", + } + return strings.Join(lines, "\n") + "\n" + }(), + errShouldBe: "exit code: 1", }, { args: []string{"lint", "../../tests/integ/minimalist_gnomod/"}, // TODO: raise an error because there is a gno.mod, but no .gno files }, { args: []string{"lint", "../../tests/integ/invalid_module_name/"}, // TODO: raise an error because gno.mod is invalid + }, { + args: []string{"lint", "../../tests/integ/invalid_gno_file/"}, + stderrShouldContain: "../../tests/integ/invalid_gno_file/invalid.gno:1:1: expected 'package', found packag (code=2)", + errShouldBe: "exit code: 1", + }, { + args: []string{"lint", "../../tests/integ/typecheck_missing_return/"}, + stderrShouldContain: "../../tests/integ/typecheck_missing_return/main.gno:5:1: missing return (code=4)", + errShouldBe: "exit code: 1", }, // TODO: 'gno mod' is valid? - // TODO: is gno source valid? // TODO: are dependencies valid? // TODO: is gno source using unsafe/discouraged features? - // TODO: consider making `gno transpile; go lint *gen.go` // TODO: check for imports of native libs from non _test.gno files } testMainCaseRun(t, tc) diff --git a/gnovm/cmd/gno/run_test.go b/gnovm/cmd/gno/run_test.go index 74f99f7490c..aa7780c149e 100644 --- a/gnovm/cmd/gno/run_test.go +++ b/gnovm/cmd/gno/run_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "strings" + "testing" +) func TestRunApp(t *testing.T) { tc := []testMainCase{ @@ -84,9 +87,17 @@ func TestRunApp(t *testing.T) { stdoutShouldContain: "Context worked", }, { - args: []string{"run", "../../tests/integ/several-files-multiple-errors/"}, - stderrShouldContain: "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2).\n../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2).\n../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2).\n", - errShouldBe: "exit code: 1", + args: []string{"run", "../../tests/integ/several-files-multiple-errors/"}, + stderrShouldContain: func() string { + lines := []string{ + "../../tests/integ/several-files-multiple-errors/file2.gno:3:5: expected 'IDENT', found '{' (code=2)", + "../../tests/integ/several-files-multiple-errors/file2.gno:5:1: expected type, found '}' (code=2)", + "../../tests/integ/several-files-multiple-errors/main.gno:5:5: expected ';', found example (code=2)", + "../../tests/integ/several-files-multiple-errors/main.gno:6:2: expected '}', found 'EOF' (code=2)", + } + return strings.Join(lines, "\n") + "\n" + }(), + errShouldBe: "exit code: 1", }, // TODO: a test file // TODO: args diff --git a/gnovm/cmd/gno/testdata/gno_fmt/empty.txtar b/gnovm/cmd/gno/testdata/fmt/empty.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/empty.txtar rename to gnovm/cmd/gno/testdata/fmt/empty.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/import_cleaning.txtar b/gnovm/cmd/gno/testdata/fmt/import_cleaning.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/import_cleaning.txtar rename to gnovm/cmd/gno/testdata/fmt/import_cleaning.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/include.txtar b/gnovm/cmd/gno/testdata/fmt/include.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/include.txtar rename to gnovm/cmd/gno/testdata/fmt/include.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/multi_import.txtar b/gnovm/cmd/gno/testdata/fmt/multi_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/multi_import.txtar rename to gnovm/cmd/gno/testdata/fmt/multi_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/noimport_format.txtar b/gnovm/cmd/gno/testdata/fmt/noimport_format.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/noimport_format.txtar rename to gnovm/cmd/gno/testdata/fmt/noimport_format.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/parse_error.txtar b/gnovm/cmd/gno/testdata/fmt/parse_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/parse_error.txtar rename to gnovm/cmd/gno/testdata/fmt/parse_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_fmt/shadow_import.txtar b/gnovm/cmd/gno/testdata/fmt/shadow_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_fmt/shadow_import.txtar rename to gnovm/cmd/gno/testdata/fmt/shadow_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar b/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar deleted file mode 100644 index 9482eeb1f4f..00000000000 --- a/gnovm/cmd/gno/testdata/gno_lint/file_error_txtar +++ /dev/null @@ -1,20 +0,0 @@ -# gno lint: test file error - -! gno lint ./i_have_error_test.gno - -cmp stdout stdout.golden -cmp stderr stderr.golden - --- i_have_error_test.gno -- -package main - -import "fmt" - -func TestIHaveSomeError() { - i := undefined_variable - fmt.Println("Hello", 42) -} - --- stdout.golden -- --- stderr.golden -- -i_have_error_test.gno:6: name undefined_variable not declared (code=2). diff --git a/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar b/gnovm/cmd/gno/testdata/lint/bad_import.txtar similarity index 54% rename from gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar rename to gnovm/cmd/gno/testdata/lint/bad_import.txtar index 52141dff09b..b5edbdd0223 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/bad_import.txtar +++ b/gnovm/cmd/gno/testdata/lint/bad_import.txtar @@ -11,9 +11,13 @@ package main import "python" func main() { - fmt.Println("Hello", 42) + println("Hello", 42) } +-- gno.mod -- +module gno.land/p/test + -- stdout.golden -- -- stderr.golden -- -bad_file.gno:3:8: unknown import path python (code=2). +bad_file.gno:3:8: could not import python (import not found: python) (code=4) +bad_file.gno:3:8: unknown import path python (code=2) diff --git a/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar b/gnovm/cmd/gno/testdata/lint/file_error.txtar similarity index 88% rename from gnovm/cmd/gno/testdata/gno_lint/file_error.txtar rename to gnovm/cmd/gno/testdata/lint/file_error.txtar index 5aa3a3282d5..4fa50c6da81 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/file_error.txtar +++ b/gnovm/cmd/gno/testdata/lint/file_error.txtar @@ -15,6 +15,9 @@ func TestIHaveSomeError() { fmt.Println("Hello", 42) } +-- gno.mod -- +module gno.land/p/test + -- stdout.golden -- -- stderr.golden -- -i_have_error_test.gno:6:7: name undefined_variable not declared (code=2). +i_have_error_test.gno:6:7: name undefined_variable not declared (code=2) diff --git a/gnovm/cmd/gno/testdata/gno_lint/no_error.txtar b/gnovm/cmd/gno/testdata/lint/no_error.txtar similarity index 68% rename from gnovm/cmd/gno/testdata/gno_lint/no_error.txtar rename to gnovm/cmd/gno/testdata/lint/no_error.txtar index 95356b1ba2b..5dd3b164952 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/no_error.txtar +++ b/gnovm/cmd/gno/testdata/lint/no_error.txtar @@ -1,6 +1,6 @@ # testing simple gno lint command with any error -gno lint ./good_file.gno +gno lint ./good_file.gno cmp stdout stdout.golden cmp stdout stderr.golden @@ -8,11 +8,12 @@ cmp stdout stderr.golden -- good_file.gno -- package main -import "fmt" - func main() { - fmt.Println("Hello", 42) + println("Hello", 42) } +-- gno.mod -- +module gno.land/p/demo/test + -- stdout.golden -- -- stderr.golden -- diff --git a/gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar b/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar similarity index 60% rename from gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar rename to gnovm/cmd/gno/testdata/lint/no_gnomod.txtar index 52daa6f0e9b..b5a046a7095 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/no_gnomod.txtar +++ b/gnovm/cmd/gno/testdata/lint/no_gnomod.txtar @@ -8,12 +8,10 @@ cmp stderr stderr.golden -- good_file.gno -- package main -import "fmt" - func main() { - fmt.Println("Hello", 42) + println("Hello", 42) } -- stdout.golden -- -- stderr.golden -- -./.: missing 'gno.mod' file (code=1). +./.: parsing gno.mod at ./.: gno.mod file not found in current or any parent directory (code=1) diff --git a/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar b/gnovm/cmd/gno/testdata/lint/not_declared.txtar similarity index 55% rename from gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar rename to gnovm/cmd/gno/testdata/lint/not_declared.txtar index b63c5c447e1..ac56b27e0df 100644 --- a/gnovm/cmd/gno/testdata/gno_lint/not_declared.txtar +++ b/gnovm/cmd/gno/testdata/lint/not_declared.txtar @@ -8,13 +8,15 @@ cmp stderr stderr.golden -- bad_file.gno -- package main -import "fmt" - func main() { - hello.Foo() - fmt.Println("Hello", 42) + hello.Foo() + println("Hello", 42) } +-- gno.mod -- +module gno.land/p/demo/hello + -- stdout.golden -- -- stderr.golden -- -bad_file.gno:6:3: name hello not declared (code=2). +bad_file.gno:4:2: undefined: hello (code=4) +bad_file.gno:4:2: name hello not declared (code=2) diff --git a/gnovm/cmd/gno/testdata/gno_test/dir_not_exist.txtar b/gnovm/cmd/gno/testdata/test/dir_not_exist.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/dir_not_exist.txtar rename to gnovm/cmd/gno/testdata/test/dir_not_exist.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar b/gnovm/cmd/gno/testdata/test/empty_dir.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/empty_dir.txtar rename to gnovm/cmd/gno/testdata/test/empty_dir.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar b/gnovm/cmd/gno/testdata/test/empty_gno1.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/empty_gno1.txtar rename to gnovm/cmd/gno/testdata/test/empty_gno1.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno2.txtar b/gnovm/cmd/gno/testdata/test/empty_gno2.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/empty_gno2.txtar rename to gnovm/cmd/gno/testdata/test/empty_gno2.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/empty_gno3.txtar b/gnovm/cmd/gno/testdata/test/empty_gno3.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/empty_gno3.txtar rename to gnovm/cmd/gno/testdata/test/empty_gno3.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/error_correct.txtar b/gnovm/cmd/gno/testdata/test/error_correct.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/error_correct.txtar rename to gnovm/cmd/gno/testdata/test/error_correct.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar b/gnovm/cmd/gno/testdata/test/error_incorrect.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/error_incorrect.txtar rename to gnovm/cmd/gno/testdata/test/error_incorrect.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/error_sync.txtar b/gnovm/cmd/gno/testdata/test/error_sync.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/error_sync.txtar rename to gnovm/cmd/gno/testdata/test/error_sync.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar b/gnovm/cmd/gno/testdata/test/failing_filetest.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/failing_filetest.txtar rename to gnovm/cmd/gno/testdata/test/failing_filetest.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/failing_test.txtar b/gnovm/cmd/gno/testdata/test/failing_test.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/failing_test.txtar rename to gnovm/cmd/gno/testdata/test/failing_test.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar b/gnovm/cmd/gno/testdata/test/filetest_events.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar rename to gnovm/cmd/gno/testdata/test/filetest_events.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar b/gnovm/cmd/gno/testdata/test/flag_print-runtime-metrics.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/flag_print-runtime-metrics.txtar rename to gnovm/cmd/gno/testdata/test/flag_print-runtime-metrics.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_run.txtar b/gnovm/cmd/gno/testdata/test/flag_run.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/flag_run.txtar rename to gnovm/cmd/gno/testdata/test/flag_run.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/flag_timeout.txtar b/gnovm/cmd/gno/testdata/test/flag_timeout.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/flag_timeout.txtar rename to gnovm/cmd/gno/testdata/test/flag_timeout.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/fmt_write_import.txtar b/gnovm/cmd/gno/testdata/test/fmt_write_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/fmt_write_import.txtar rename to gnovm/cmd/gno/testdata/test/fmt_write_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/minim1.txtar b/gnovm/cmd/gno/testdata/test/minim1.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/minim1.txtar rename to gnovm/cmd/gno/testdata/test/minim1.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/minim2.txtar b/gnovm/cmd/gno/testdata/test/minim2.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/minim2.txtar rename to gnovm/cmd/gno/testdata/test/minim2.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/minim3.txtar b/gnovm/cmd/gno/testdata/test/minim3.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/minim3.txtar rename to gnovm/cmd/gno/testdata/test/minim3.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/multitest_events.txtar b/gnovm/cmd/gno/testdata/test/multitest_events.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/multitest_events.txtar rename to gnovm/cmd/gno/testdata/test/multitest_events.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/no_args.txtar b/gnovm/cmd/gno/testdata/test/no_args.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/no_args.txtar rename to gnovm/cmd/gno/testdata/test/no_args.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/output_correct.txtar b/gnovm/cmd/gno/testdata/test/output_correct.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/output_correct.txtar rename to gnovm/cmd/gno/testdata/test/output_correct.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar b/gnovm/cmd/gno/testdata/test/output_incorrect.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/output_incorrect.txtar rename to gnovm/cmd/gno/testdata/test/output_incorrect.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/output_sync.txtar b/gnovm/cmd/gno/testdata/test/output_sync.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/output_sync.txtar rename to gnovm/cmd/gno/testdata/test/output_sync.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/panic.txtar b/gnovm/cmd/gno/testdata/test/panic.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/panic.txtar rename to gnovm/cmd/gno/testdata/test/panic.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar b/gnovm/cmd/gno/testdata/test/pkg_underscore_test.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/pkg_underscore_test.txtar rename to gnovm/cmd/gno/testdata/test/pkg_underscore_test.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_boundmethod.txtar b/gnovm/cmd/gno/testdata/test/realm_boundmethod.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/realm_boundmethod.txtar rename to gnovm/cmd/gno/testdata/test/realm_boundmethod.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar b/gnovm/cmd/gno/testdata/test/realm_correct.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/realm_correct.txtar rename to gnovm/cmd/gno/testdata/test/realm_correct.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar b/gnovm/cmd/gno/testdata/test/realm_incorrect.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/realm_incorrect.txtar rename to gnovm/cmd/gno/testdata/test/realm_incorrect.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar b/gnovm/cmd/gno/testdata/test/realm_sync.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/realm_sync.txtar rename to gnovm/cmd/gno/testdata/test/realm_sync.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/recover.txtar b/gnovm/cmd/gno/testdata/test/recover.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/recover.txtar rename to gnovm/cmd/gno/testdata/test/recover.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/skip.txtar b/gnovm/cmd/gno/testdata/test/skip.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/skip.txtar rename to gnovm/cmd/gno/testdata/test/skip.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar b/gnovm/cmd/gno/testdata/test/unknown_package.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/unknown_package.txtar rename to gnovm/cmd/gno/testdata/test/unknown_package.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar b/gnovm/cmd/gno/testdata/test/valid_filetest.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/valid_filetest.txtar rename to gnovm/cmd/gno/testdata/test/valid_filetest.txtar diff --git a/gnovm/cmd/gno/testdata/gno_test/valid_test.txtar b/gnovm/cmd/gno/testdata/test/valid_test.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_test/valid_test.txtar rename to gnovm/cmd/gno/testdata/test/valid_test.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_build_error.txtar rename to gnovm/cmd/gno/testdata/transpile/gobuild_flag_build_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_parse_error.txtar b/gnovm/cmd/gno/testdata/transpile/gobuild_flag_parse_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/gobuild_flag_parse_error.txtar rename to gnovm/cmd/gno/testdata/transpile/gobuild_flag_parse_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/invalid_import.txtar b/gnovm/cmd/gno/testdata/transpile/invalid_import.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/invalid_import.txtar rename to gnovm/cmd/gno/testdata/transpile/invalid_import.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/no_args.txtar b/gnovm/cmd/gno/testdata/transpile/no_args.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/no_args.txtar rename to gnovm/cmd/gno/testdata/transpile/no_args.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/parse_error.txtar b/gnovm/cmd/gno/testdata/transpile/parse_error.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/parse_error.txtar rename to gnovm/cmd/gno/testdata/transpile/parse_error.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_empty_dir.txtar b/gnovm/cmd/gno/testdata/transpile/valid_empty_dir.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_empty_dir.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_empty_dir.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_file.txtar b/gnovm/cmd/gno/testdata/transpile/valid_gobuild_file.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_file.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_gobuild_file.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_flag.txtar b/gnovm/cmd/gno/testdata/transpile/valid_gobuild_flag.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_gobuild_flag.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_gobuild_flag.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_output_flag.txtar b/gnovm/cmd/gno/testdata/transpile/valid_output_flag.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_output_flag.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_output_flag.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_output_gobuild.txtar b/gnovm/cmd/gno/testdata/transpile/valid_output_gobuild.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_output_gobuild.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_output_gobuild.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_file.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_file.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_transpile_file.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_package.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_package.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_package.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_transpile_package.txtar diff --git a/gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_tree.txtar b/gnovm/cmd/gno/testdata/transpile/valid_transpile_tree.txtar similarity index 100% rename from gnovm/cmd/gno/testdata/gno_transpile/valid_transpile_tree.txtar rename to gnovm/cmd/gno/testdata/transpile/valid_transpile_tree.txtar diff --git a/gnovm/cmd/gno/testdata_test.go b/gnovm/cmd/gno/testdata_test.go index 15bc8d96e26..6b1bbd1d459 100644 --- a/gnovm/cmd/gno/testdata_test.go +++ b/gnovm/cmd/gno/testdata_test.go @@ -24,7 +24,6 @@ func Test_Scripts(t *testing.T) { } name := dir.Name() - t.Logf("testing: %s", name) t.Run(name, func(t *testing.T) { updateScripts, _ := strconv.ParseBool(os.Getenv("UPDATE_SCRIPTS")) p := testscript.Params{ diff --git a/gnovm/cmd/gno/transpile_test.go b/gnovm/cmd/gno/transpile_test.go index 827c09e23f1..5a03ddc7657 100644 --- a/gnovm/cmd/gno/transpile_test.go +++ b/gnovm/cmd/gno/transpile_test.go @@ -6,29 +6,9 @@ import ( "strconv" "testing" - "github.com/rogpeppe/go-internal/testscript" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/gnolang/gno/gnovm/pkg/integration" ) -func Test_ScriptsTranspile(t *testing.T) { - p := testscript.Params{ - Dir: "testdata/gno_transpile", - } - - if coverdir, ok := integration.ResolveCoverageDir(); ok { - err := integration.SetupTestscriptsCoverage(&p, coverdir) - require.NoError(t, err) - } - - err := integration.SetupGno(&p, t.TempDir()) - require.NoError(t, err) - - testscript.Run(t, p) -} - func Test_parseGoBuildErrors(t *testing.T) { t.Parallel() diff --git a/gnovm/pkg/gnolang/go2gno.go b/gnovm/pkg/gnolang/go2gno.go index 338efa20fcc..82d5c69b08b 100644 --- a/gnovm/pkg/gnolang/go2gno.go +++ b/gnovm/pkg/gnolang/go2gno.go @@ -39,7 +39,9 @@ import ( "go/token" "go/types" "os" + "path" "reflect" + "slices" "strconv" "strings" @@ -499,6 +501,18 @@ type MemPackageGetter interface { // If format is true, the code will be automatically updated with the // formatted source code. func TypeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, format bool) error { + return typeCheckMemPackage(mempkg, getter, false, format) +} + +// TypeCheckMemPackageTest performs the same type checks as [TypeCheckMemPackage], +// but allows re-declarations. +// +// Note: like TypeCheckMemPackage, this function ignores tests and filetests. +func TypeCheckMemPackageTest(mempkg *gnovm.MemPackage, getter MemPackageGetter) error { + return typeCheckMemPackage(mempkg, getter, true, false) +} + +func typeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, testing, format bool) error { var errs error imp := &gnoImporter{ getter: getter, @@ -508,6 +522,7 @@ func TypeCheckMemPackage(mempkg *gnovm.MemPackage, getter MemPackageGetter, form errs = multierr.Append(errs, err) }, }, + allowRedefinitions: testing, } imp.cfg.Importer = imp @@ -529,6 +544,9 @@ type gnoImporter struct { getter MemPackageGetter cache map[string]gnoImporterResult cfg *types.Config + + // allow symbol redefinitions? (test standard libraries) + allowRedefinitions bool } // Unused, but satisfies the Importer interface. @@ -559,22 +577,39 @@ func (g *gnoImporter) ImportFrom(path, _ string, _ types.ImportMode) (*types.Pac } func (g *gnoImporter) parseCheckMemPackage(mpkg *gnovm.MemPackage, fmt bool) (*types.Package, error) { + // This map is used to allow for function re-definitions, which are allowed + // in Gno (testing context) but not in Go. + // This map links each function identifier with a closure to remove its + // associated declaration. + var delFunc map[string]func() + if g.allowRedefinitions { + delFunc = make(map[string]func()) + } + fset := token.NewFileSet() files := make([]*ast.File, 0, len(mpkg.Files)) var errs error for _, file := range mpkg.Files { + // Ignore non-gno files. + // TODO: support filetest type checking. (should probably handle as each its + // own separate pkg, which should also be typechecked) if !strings.HasSuffix(file.Name, ".gno") || - endsWith(file.Name, []string{"_test.gno", "_filetest.gno"}) { - continue // skip spurious file. + strings.HasSuffix(file.Name, "_test.gno") || + strings.HasSuffix(file.Name, "_filetest.gno") { + continue } const parseOpts = parser.ParseComments | parser.DeclarationErrors | parser.SkipObjectResolution - f, err := parser.ParseFile(fset, file.Name, file.Body, parseOpts) + f, err := parser.ParseFile(fset, path.Join(mpkg.Path, file.Name), file.Body, parseOpts) if err != nil { errs = multierr.Append(errs, err) continue } + if delFunc != nil { + deleteOldIdents(delFunc, f) + } + // enforce formatting if fmt { var buf bytes.Buffer @@ -595,6 +630,24 @@ func (g *gnoImporter) parseCheckMemPackage(mpkg *gnovm.MemPackage, fmt bool) (*t return g.cfg.Check(mpkg.Path, fset, files, nil) } +func deleteOldIdents(idents map[string]func(), f *ast.File) { + for _, decl := range f.Decls { + fd, ok := decl.(*ast.FuncDecl) + if !ok || fd.Recv != nil { // ignore methods + continue + } + if del := idents[fd.Name.Name]; del != nil { + del() + } + decl := decl + idents[fd.Name.Name] = func() { + // NOTE: cannot use the index as a file may contain multiple decls to be removed, + // so removing one would make all "later" indexes wrong. + f.Decls = slices.DeleteFunc(f.Decls, func(d ast.Decl) bool { return decl == d }) + } + } +} + //---------------------------------------- // utility methods diff --git a/gnovm/tests/integ/typecheck_missing_return/gno.mod b/gnovm/tests/integ/typecheck_missing_return/gno.mod new file mode 100644 index 00000000000..3eaaa374994 --- /dev/null +++ b/gnovm/tests/integ/typecheck_missing_return/gno.mod @@ -0,0 +1 @@ +module gno.land/p/integ/valid diff --git a/gnovm/tests/integ/typecheck_missing_return/main.gno b/gnovm/tests/integ/typecheck_missing_return/main.gno new file mode 100644 index 00000000000..5d6e547097c --- /dev/null +++ b/gnovm/tests/integ/typecheck_missing_return/main.gno @@ -0,0 +1,5 @@ +package valid + +func Hello() int { + // no return +} From 1bd64192a170fdf7ca904fb6bf27f10e2acc8ed5 Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:16:05 +0100 Subject: [PATCH 08/20] feat(github-bot): add a fork condition and handle PR reviews (#3303) This PR improves the bot on two points: - it now handle `pull_request_review` events to address [this concern](https://github.com/gnolang/gno/issues/3238#issuecomment-2526206058) https://github.com/gnolang/gno/commit/4f7b0b80c6e726806a473556b02444fded707254 - a new condition allows to check if a PR was created from a fork to address [this concern](https://github.com/gnolang/gno/issues/3238#issuecomment-2524018469) https://github.com/gnolang/gno/commit/f491d95d68c755d7154cbbee48a9cebc7693fa89 --- .github/workflows/bot.yml | 4 +++ .../github-bot/internal/conditions/fork.go | 27 ++++++++++++++++ .../internal/conditions/fork_test.go | 31 +++++++++++++++++++ contribs/github-bot/internal/config/config.go | 2 +- contribs/github-bot/internal/matrix/matrix.go | 2 +- .../github-bot/internal/matrix/matrix_test.go | 9 ++++++ contribs/github-bot/internal/utils/actions.go | 2 +- .../github-bot/internal/utils/github_const.go | 1 + 8 files changed, 75 insertions(+), 3 deletions(-) create mode 100644 contribs/github-bot/internal/conditions/fork.go create mode 100644 contribs/github-bot/internal/conditions/fork_test.go diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index 644540c1aaf..300a5928e25 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -14,6 +14,10 @@ on: - converted_to_draft - ready_for_review + # Watch for changes on PR reviews + pull_request_review: + types: [submitted, edited, dismissed] + # Watch for changes on PR comment issue_comment: types: [created, edited, deleted] diff --git a/contribs/github-bot/internal/conditions/fork.go b/contribs/github-bot/internal/conditions/fork.go new file mode 100644 index 00000000000..72cbae12004 --- /dev/null +++ b/contribs/github-bot/internal/conditions/fork.go @@ -0,0 +1,27 @@ +package conditions + +import ( + "fmt" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +// CreatedFromFork Condition. +type createdFromFork struct{} + +var _ Condition = &createdFromFork{} + +func (b *createdFromFork) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + pr.GetHead().GetRepo().GetFullName() != pr.GetBase().GetRepo().GetFullName(), + fmt.Sprintf("The pull request was created from a fork (head branch repo: %s)", pr.GetHead().GetRepo().GetFullName()), + details, + ) +} + +func CreatedFromFork() Condition { + return &createdFromFork{} +} diff --git a/contribs/github-bot/internal/conditions/fork_test.go b/contribs/github-bot/internal/conditions/fork_test.go new file mode 100644 index 00000000000..fe7e9a95bf1 --- /dev/null +++ b/contribs/github-bot/internal/conditions/fork_test.go @@ -0,0 +1,31 @@ +package conditions + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/stretchr/testify/assert" + + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func TestCreatedFromFork(t *testing.T) { + t.Parallel() + + var ( + repo = &github.PullRequestBranch{Repo: &github.Repository{Owner: &github.User{Login: github.String("main")}, Name: github.String("repo"), FullName: github.String("main/repo")}} + fork = &github.PullRequestBranch{Repo: &github.Repository{Owner: &github.User{Login: github.String("fork")}, Name: github.String("repo"), FullName: github.String("fork/repo")}} + ) + + prFromMain := &github.PullRequest{Base: repo, Head: repo} + prFromFork := &github.PullRequest{Base: repo, Head: fork} + + details := treeprint.New() + assert.False(t, CreatedFromFork().IsMet(prFromMain, details)) + assert.True(t, utils.TestLastNodeStatus(t, false, details), "condition details should have a status: false") + + details = treeprint.New() + assert.True(t, CreatedFromFork().IsMet(prFromFork, details)) + assert.True(t, utils.TestLastNodeStatus(t, true, details), "condition details should have a status: true") +} diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go index c1d89e4cde5..2d595c7ce51 100644 --- a/contribs/github-bot/internal/config/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -28,7 +28,7 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { auto := []AutomaticCheck{ { Description: "Maintainers must be able to edit this pull request ([more info](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork))", - If: c.Always(), + If: c.CreatedFromFork(), Then: r.MaintainerCanModify(), }, { diff --git a/contribs/github-bot/internal/matrix/matrix.go b/contribs/github-bot/internal/matrix/matrix.go index 9c8f12e4214..02840721c80 100644 --- a/contribs/github-bot/internal/matrix/matrix.go +++ b/contribs/github-bot/internal/matrix/matrix.go @@ -113,7 +113,7 @@ func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContex // Event triggered by an issue / PR comment being created / edited / deleted // or any update on a PR. - case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestTarget: + case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestReview, utils.EventPullRequestTarget: // For these events, retrieve the number of the associated PR from the context. prNum, err := utils.GetPRNumFromActionsCtx(actionCtx) if err != nil { diff --git a/contribs/github-bot/internal/matrix/matrix_test.go b/contribs/github-bot/internal/matrix/matrix_test.go index fe5b7452a49..f6b34f16c24 100644 --- a/contribs/github-bot/internal/matrix/matrix_test.go +++ b/contribs/github-bot/internal/matrix/matrix_test.go @@ -54,6 +54,15 @@ func TestProcessEvent(t *testing.T) { prs, utils.PRList{1}, false, + }, { + "valid pull_request_review event", + &githubactions.GitHubContext{ + EventName: utils.EventPullRequestReview, + Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, + }, + prs, + utils.PRList{1}, + false, }, { "valid pull_request_target event", &githubactions.GitHubContext{ diff --git a/contribs/github-bot/internal/utils/actions.go b/contribs/github-bot/internal/utils/actions.go index 3e08a8e1548..0686e8c29c5 100644 --- a/contribs/github-bot/internal/utils/actions.go +++ b/contribs/github-bot/internal/utils/actions.go @@ -30,7 +30,7 @@ func GetPRNumFromActionsCtx(actionCtx *githubactions.GitHubContext) (int, error) switch actionCtx.EventName { case EventIssueComment: firstKey = "issue" - case EventPullRequest, EventPullRequestTarget: + case EventPullRequest, EventPullRequestReview, EventPullRequestTarget: firstKey = "pull_request" default: return 0, fmt.Errorf("unsupported event: %s", actionCtx.EventName) diff --git a/contribs/github-bot/internal/utils/github_const.go b/contribs/github-bot/internal/utils/github_const.go index 26d7d54d477..f030d9365f7 100644 --- a/contribs/github-bot/internal/utils/github_const.go +++ b/contribs/github-bot/internal/utils/github_const.go @@ -5,6 +5,7 @@ const ( // GitHub Actions Event Names. EventIssueComment = "issue_comment" EventPullRequest = "pull_request" + EventPullRequestReview = "pull_request_review" EventPullRequestTarget = "pull_request_target" EventWorkflowDispatch = "workflow_dispatch" From 9bd9e47e7354d98cbc2980abbd6ee61096efd92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20=C5=BDivkovi=C4=87?= Date: Mon, 9 Dec 2024 16:39:22 +0100 Subject: [PATCH 09/20] chore: update portal loop archiver version (#3308) ## Description This PR updates the portal loop tx-archiver version to `v0.4.2`, which eliminates the millisecond timestamp issue --- misc/loop/go.mod | 2 +- misc/loop/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/misc/loop/go.mod b/misc/loop/go.mod index 70e9d21734b..af7783e57bb 100644 --- a/misc/loop/go.mod +++ b/misc/loop/go.mod @@ -8,7 +8,7 @@ require ( github.com/docker/docker v24.0.7+incompatible github.com/docker/go-connections v0.4.0 github.com/gnolang/gno v0.1.0-nightly.20240627 - github.com/gnolang/tx-archive v0.4.0 + github.com/gnolang/tx-archive v0.4.2 github.com/prometheus/client_golang v1.17.0 github.com/sirupsen/logrus v1.9.3 ) diff --git a/misc/loop/go.sum b/misc/loop/go.sum index 8e0feb11e4a..0d235f2cfb1 100644 --- a/misc/loop/go.sum +++ b/misc/loop/go.sum @@ -68,8 +68,8 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gnolang/tx-archive v0.4.0 h1:+1Rgo0U0HjLQLq/xqeGdJwtAzo9xWj09t1oZLvrL3bU= -github.com/gnolang/tx-archive v0.4.0/go.mod h1:seKHGnvxUnDgH/mSsCEdwG0dHY/FrpbUm6Hd0+KMd9w= +github.com/gnolang/tx-archive v0.4.2 h1:xBBqLLKY9riv9yxpQgVhItCWxIji2rX6xNFmCY1cEOQ= +github.com/gnolang/tx-archive v0.4.2/go.mod h1:AGUBGO+DCLuKL80a1GJRnpcJ5gxVd9L4jEJXQB9uXp4= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= From bb38fb1942aac60999bed90c904711c46fe7b783 Mon Sep 17 00:00:00 2001 From: 6h057 <15034695+omarsy@users.noreply.github.com> Date: Tue, 10 Dec 2024 02:15:27 +0100 Subject: [PATCH 10/20] fix(gnovm): improve error message for nil assignment in variable declaration (#3068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …aration
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Co-authored-by: Morgan Co-authored-by: ltzmaxwell --- gnovm/pkg/gnolang/preprocess.go | 199 ++++++++++++++------------- gnovm/pkg/gnolang/type_check.go | 53 ++++--- gnovm/pkg/gnolang/type_check_test.go | 2 +- gnovm/pkg/gnolang/types.go | 38 ++--- gnovm/tests/files/add3.gno | 9 ++ gnovm/tests/files/assign38.gno | 10 ++ gnovm/tests/files/fun28.gno | 10 ++ gnovm/tests/files/slice3.gno | 9 ++ gnovm/tests/files/var35.gno | 8 ++ 9 files changed, 200 insertions(+), 138 deletions(-) create mode 100644 gnovm/tests/files/add3.gno create mode 100644 gnovm/tests/files/assign38.gno create mode 100644 gnovm/tests/files/fun28.gno create mode 100644 gnovm/tests/files/slice3.gno create mode 100644 gnovm/tests/files/var35.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 78b11a4ebc5..6e749053d72 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -743,7 +743,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { for i, cx := range n.Cases { cx = Preprocess( store, last, cx).(Expr) - checkOrConvertType(store, last, &cx, tt, false) // #nosec G601 + checkOrConvertType(store, last, n, &cx, tt, false) // #nosec G601 n.Cases[i] = cx } } @@ -882,7 +882,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // Preprocess and convert tag if const. if n.X != nil { n.X = Preprocess(store, last, n.X).(Expr) - convertIfConst(store, last, n.X) + convertIfConst(store, last, n, n.X) } } return n, TRANS_CONTINUE @@ -1102,10 +1102,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // First, convert untyped as necessary. if !shouldSwapOnSpecificity(lcx.T, rcx.T) { // convert n.Left to right type. - checkOrConvertType(store, last, &n.Left, rcx.T, false) + checkOrConvertType(store, last, n, &n.Left, rcx.T, false) } else { // convert n.Right to left type. - checkOrConvertType(store, last, &n.Right, lcx.T, false) + checkOrConvertType(store, last, n, &n.Right, lcx.T, false) } // Then, evaluate the expression. cx := evalConst(store, last, n) @@ -1125,7 +1125,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { rnt.String())) } // convert n.Left to pt type, - checkOrConvertType(store, last, &n.Left, pt, false) + checkOrConvertType(store, last, n, &n.Left, pt, false) // if check pass, convert n.Right to (gno) pt type, rn := Expr(Call(pt.String(), n.Right)) // and convert result back. @@ -1154,7 +1154,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } if !isUntyped(rt) { // right is typed - checkOrConvertType(store, last, &n.Left, rt, false) + checkOrConvertType(store, last, n, &n.Left, rt, false) } else { if shouldSwapOnSpecificity(lt, rt) { checkUntypedShiftExpr(n.Right) @@ -1165,10 +1165,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } } else if lcx.T == nil { // LHS is nil. // convert n.Left to typed-nil type. - checkOrConvertType(store, last, &n.Left, rt, false) + checkOrConvertType(store, last, n, &n.Left, rt, false) } else { if isUntyped(rt) { - checkOrConvertType(store, last, &n.Right, lt, false) + checkOrConvertType(store, last, n, &n.Right, lt, false) } } } else if ric { // right is const, left is not @@ -1186,7 +1186,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // convert n.Left to (gno) pt type, ln := Expr(Call(pt.String(), n.Left)) // convert n.Right to pt type, - checkOrConvertType(store, last, &n.Right, pt, false) + checkOrConvertType(store, last, n, &n.Right, pt, false) // and convert result back. tx := constType(n, lnt) // reset/create n2 to preprocess left child. @@ -1212,7 +1212,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } // both untyped, e.g. 1< float64. // (const) untyped bigint -> int. if !constConverted { - convertConst(store, last, arg0, nil) + convertConst(store, last, n, arg0, nil) } // evaluate the new expression. cx := evalConst(store, last, n) @@ -1397,15 +1397,15 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { if isUntyped(at) { switch arg0.Op { case EQL, NEQ, LSS, GTR, LEQ, GEQ: - assertAssignableTo(at, ct, false) + assertAssignableTo(n, at, ct, false) break default: - checkOrConvertType(store, last, &n.Args[0], ct, false) + checkOrConvertType(store, last, n, &n.Args[0], ct, false) } } case *UnaryExpr: if isUntyped(at) { - checkOrConvertType(store, last, &n.Args[0], ct, false) + checkOrConvertType(store, last, n, &n.Args[0], ct, false) } default: // do nothing @@ -1549,7 +1549,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { panic("should not happen") } // Specify function param/result generics. - sft := ft.Specify(store, argTVs, isVarg) + sft := ft.Specify(store, n, argTVs, isVarg) spts := sft.Params srts := FieldTypeList(sft.Results).Types() // If generics were specified, override attr @@ -1575,12 +1575,12 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { for i, tv := range argTVs { if hasVarg { if (len(spts) - 1) <= i { - assertAssignableTo(tv.T, spts[len(spts)-1].Type.Elem(), true) + assertAssignableTo(n, tv.T, spts[len(spts)-1].Type.Elem(), true) } else { - assertAssignableTo(tv.T, spts[i].Type, true) + assertAssignableTo(n, tv.T, spts[i].Type, true) } } else { - assertAssignableTo(tv.T, spts[i].Type, true) + assertAssignableTo(n, tv.T, spts[i].Type, true) } } } else { @@ -1591,16 +1591,16 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { if len(spts) <= i { panic("expected final vargs slice but got many") } - checkOrConvertType(store, last, &n.Args[i], spts[i].Type, true) + checkOrConvertType(store, last, n, &n.Args[i], spts[i].Type, true) } else { - checkOrConvertType(store, last, &n.Args[i], + checkOrConvertType(store, last, n, &n.Args[i], spts[len(spts)-1].Type.Elem(), true) } } else { - checkOrConvertType(store, last, &n.Args[i], spts[i].Type, true) + checkOrConvertType(store, last, n, &n.Args[i], spts[i].Type, true) } } else { - checkOrConvertType(store, last, &n.Args[i], spts[i].Type, true) + checkOrConvertType(store, last, n, &n.Args[i], spts[i].Type, true) } } } @@ -1621,10 +1621,10 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { case StringKind, ArrayKind, SliceKind: // Replace const index with int *ConstExpr, // or if not const, assert integer type.. - checkOrConvertIntegerKind(store, last, n.Index) + checkOrConvertIntegerKind(store, last, n, n.Index) case MapKind: mt := baseOf(gnoTypeOf(store, dt)).(*MapType) - checkOrConvertType(store, last, &n.Index, mt.Key, false) + checkOrConvertType(store, last, n, &n.Index, mt.Key, false) default: panic(fmt.Sprintf( "unexpected index base kind for type %s", @@ -1635,15 +1635,15 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { case *SliceExpr: // Replace const L/H/M with int *ConstExpr, // or if not const, assert integer type.. - checkOrConvertIntegerKind(store, last, n.Low) - checkOrConvertIntegerKind(store, last, n.High) - checkOrConvertIntegerKind(store, last, n.Max) + checkOrConvertIntegerKind(store, last, n, n.Low) + checkOrConvertIntegerKind(store, last, n, n.High) + checkOrConvertIntegerKind(store, last, n, n.Max) // if n.X is untyped, convert to corresponding type t := evalStaticTypeOf(store, last, n.X) if isUntyped(t) { dt := defaultTypeOf(t) - checkOrConvertType(store, last, &n.X, dt, false) + checkOrConvertType(store, last, n, &n.X, dt, false) } // TRANS_LEAVE ----------------------- @@ -1722,28 +1722,28 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { key := n.Elts[i].Key.(*NameExpr).Name path := cclt.GetPathForName(key) ft := cclt.GetStaticTypeOfAt(path) - checkOrConvertType(store, last, &n.Elts[i].Value, ft, false) + checkOrConvertType(store, last, n, &n.Elts[i].Value, ft, false) } } else { for i := 0; i < len(n.Elts); i++ { ft := cclt.Fields[i].Type - checkOrConvertType(store, last, &n.Elts[i].Value, ft, false) + checkOrConvertType(store, last, n, &n.Elts[i].Value, ft, false) } } case *ArrayType: for i := 0; i < len(n.Elts); i++ { - convertType(store, last, &n.Elts[i].Key, IntType) - checkOrConvertType(store, last, &n.Elts[i].Value, cclt.Elt, false) + convertType(store, last, n, &n.Elts[i].Key, IntType) + checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Elt, false) } case *SliceType: for i := 0; i < len(n.Elts); i++ { - convertType(store, last, &n.Elts[i].Key, IntType) - checkOrConvertType(store, last, &n.Elts[i].Value, cclt.Elt, false) + convertType(store, last, n, &n.Elts[i].Key, IntType) + checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Elt, false) } case *MapType: for i := 0; i < len(n.Elts); i++ { - checkOrConvertType(store, last, &n.Elts[i].Key, cclt.Key, false) - checkOrConvertType(store, last, &n.Elts[i].Value, cclt.Value, false) + checkOrConvertType(store, last, n, &n.Elts[i].Key, cclt.Key, false) + checkOrConvertType(store, last, n, &n.Elts[i].Value, cclt.Value, false) } case *NativeType: clt = cclt.GnoType(store) @@ -1943,7 +1943,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *FieldTypeExpr: // Replace const Tag with default *ConstExpr. - convertIfConst(store, last, n.Tag) + convertIfConst(store, last, n, n.Tag) // TRANS_LEAVE ----------------------- case *ArrayTypeExpr: @@ -1952,7 +1952,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } else { // Replace const Len with int *ConstExpr. cx := evalConst(store, last, n.Len) - convertConst(store, last, cx, IntType) + convertConst(store, last, n, cx, IntType) n.Len = cx } // NOTE: For all TypeExprs, the node is not replaced @@ -1993,7 +1993,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // Rhs consts become default *ConstExprs. for _, rx := range n.Rhs { // NOTE: does nothing if rx is "nil". - convertIfConst(store, last, rx) + convertIfConst(store, last, n, rx) } nameExprs := make(NameExprs, len(n.Lhs)) @@ -2001,7 +2001,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { nameExprs[i] = *n.Lhs[i].(*NameExpr) } - defineOrDecl(store, last, false, nameExprs, nil, n.Rhs) + defineOrDecl(store, last, n, false, nameExprs, nil, n.Rhs) } else { // ASSIGN, or assignment operation (+=, -=, <<=, etc.) // NOTE: Keep in sync with DEFINE above. if len(n.Lhs) > len(n.Rhs) { @@ -2090,11 +2090,11 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } else { // len(Lhs) == len(Rhs) if n.Op == SHL_ASSIGN || n.Op == SHR_ASSIGN { // Special case if shift assign <<= or >>=. - convertType(store, last, &n.Rhs[0], UintType) + convertType(store, last, n, &n.Rhs[0], UintType) } else if n.Op == ADD_ASSIGN || n.Op == SUB_ASSIGN || n.Op == MUL_ASSIGN || n.Op == QUO_ASSIGN || n.Op == REM_ASSIGN { // e.g. a += b, single value for lhs and rhs, lt := evalStaticTypeOf(store, last, n.Lhs[0]) - checkOrConvertType(store, last, &n.Rhs[0], lt, true) + checkOrConvertType(store, last, n, &n.Rhs[0], lt, true) } else { // all else, like BAND_ASSIGN, etc // General case: a, b = x, y. for i, lx := range n.Lhs { @@ -2104,7 +2104,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { } // if lt is interface, nothing will happen - checkOrConvertType(store, last, &n.Rhs[i], lt, true) + checkOrConvertType(store, last, n, &n.Rhs[i], lt, true) } } } @@ -2181,12 +2181,12 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *ForStmt: // Cond consts become bool *ConstExprs. - checkOrConvertBoolKind(store, last, n.Cond) + checkOrConvertBoolKind(store, last, n, n.Cond) // TRANS_LEAVE ----------------------- case *IfStmt: // Cond consts become bool *ConstExprs. - checkOrConvertBoolKind(store, last, n.Cond) + checkOrConvertBoolKind(store, last, n, n.Cond) // TRANS_LEAVE ----------------------- case *RangeStmt: @@ -2242,7 +2242,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // XXX how to deal? panic("not yet implemented") } else { - checkOrConvertType(store, last, &n.Results[i], rt, false) + checkOrConvertType(store, last, n, &n.Results[i], rt, false) } } } @@ -2250,7 +2250,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // TRANS_LEAVE ----------------------- case *SendStmt: // Value consts become default *ConstExprs. - checkOrConvertType(store, last, &n.Value, nil, false) + checkOrConvertType(store, last, n, &n.Value, nil, false) // TRANS_LEAVE ----------------------- case *SelectCaseStmt: @@ -2303,7 +2303,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { // runDeclaration(), as this uses OpStaticTypeOf. } - defineOrDecl(store, last, n.Const, n.NameExprs, n.Type, n.Values) + defineOrDecl(store, last, n, n.Const, n.NameExprs, n.Type, n.Values) // TODO make note of constance in static block for // future use, or consider "const paths". set as @@ -2383,6 +2383,7 @@ func preprocess1(store Store, ctx BlockNode, n Node) Node { func defineOrDecl( store Store, bn BlockNode, + n Node, isConst bool, nameExprs []NameExpr, typeExpr Expr, @@ -2399,9 +2400,9 @@ func defineOrDecl( tvs := make([]TypedValue, numNames) if numVals == 1 && numNames > 1 { - parseMultipleAssignFromOneExpr(sts, tvs, store, bn, nameExprs, typeExpr, valueExprs[0]) + parseMultipleAssignFromOneExpr(store, bn, n, sts, tvs, nameExprs, typeExpr, valueExprs[0]) } else { - parseAssignFromExprList(sts, tvs, store, bn, isConst, nameExprs, typeExpr, valueExprs) + parseAssignFromExprList(store, bn, n, sts, tvs, isConst, nameExprs, typeExpr, valueExprs) } node := skipFile(bn) @@ -2420,10 +2421,11 @@ func defineOrDecl( // parseAssignFromExprList parses assignment to multiple variables from a list of expressions. // This function will alter the value of sts, tvs. func parseAssignFromExprList( - sts []Type, - tvs []TypedValue, store Store, bn BlockNode, + n Node, + sts []Type, + tvs []TypedValue, isConst bool, nameExprs []NameExpr, typeExpr Expr, @@ -2450,7 +2452,7 @@ func parseAssignFromExprList( } // Convert if const to nt. for i := range valueExprs { - checkOrConvertType(store, bn, &valueExprs[i], nt, false) + checkOrConvertType(store, bn, n, &valueExprs[i], nt, false) } } else if isConst { // Derive static type from values. @@ -2462,10 +2464,10 @@ func parseAssignFromExprList( // Convert n.Value to default type. for i, vx := range valueExprs { if cx, ok := vx.(*ConstExpr); ok { - convertConst(store, bn, cx, nil) + convertConst(store, bn, n, cx, nil) // convertIfConst(store, last, vx) } else { - checkOrConvertType(store, bn, &vx, nil, false) + checkOrConvertType(store, bn, n, &vx, nil, false) } vt := evalStaticTypeOf(store, bn, vx) sts[i] = vt @@ -2506,10 +2508,11 @@ func parseAssignFromExprList( // - a, b := n.(T) // - a, b := n[i], where n is a map func parseMultipleAssignFromOneExpr( - sts []Type, - tvs []TypedValue, store Store, bn BlockNode, + n Node, + sts []Type, + tvs []TypedValue, nameExprs []NameExpr, typeExpr Expr, valueExpr Expr, @@ -2567,7 +2570,7 @@ func parseMultipleAssignFromOneExpr( if st != nil { tt := tuple.Elts[i] - if checkAssignableTo(tt, st, false) != nil { + if checkAssignableTo(n, tt, st, false) != nil { panic( fmt.Sprintf( "cannot use %v (value of type %s) as %s value in assignment", @@ -3491,14 +3494,14 @@ func isConstType(x Expr) bool { } // check before convert type -func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative bool) { +func checkOrConvertType(store Store, last BlockNode, n Node, x *Expr, t Type, autoNative bool) { if debug { debug.Printf("checkOrConvertType, *x: %v:, t:%v \n", *x, t) } if cx, ok := (*x).(*ConstExpr); ok { if _, ok := t.(*NativeType); !ok { // not native type, refer to time4_native.gno. // e.g. int(1) == int8(1) - assertAssignableTo(cx.T, t, autoNative) + assertAssignableTo(n, cx.T, t, autoNative) } } else if bx, ok := (*x).(*BinaryExpr); ok && (bx.Op == SHL || bx.Op == SHR) { xt := evalStaticTypeOf(store, last, *x) @@ -3507,22 +3510,22 @@ func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative } if isUntyped(xt) { // check assignable first, see: types/shift_b6.gno - assertAssignableTo(xt, t, autoNative) + assertAssignableTo(n, xt, t, autoNative) if t == nil || t.Kind() == InterfaceKind { t = defaultTypeOf(xt) } bx.assertShiftExprCompatible2(t) - checkOrConvertType(store, last, &bx.Left, t, autoNative) + checkOrConvertType(store, last, n, &bx.Left, t, autoNative) } else { - assertAssignableTo(xt, t, autoNative) + assertAssignableTo(n, xt, t, autoNative) } return } else if *x != nil { xt := evalStaticTypeOf(store, last, *x) if t != nil { - assertAssignableTo(xt, t, autoNative) + assertAssignableTo(n, xt, t, autoNative) } if isUntyped(xt) { // Push type into expr if qualifying binary expr. @@ -3534,8 +3537,8 @@ func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative rt := evalStaticTypeOf(store, last, bx.Right) if t != nil { // push t into bx.Left and bx.Right - checkOrConvertType(store, last, &bx.Left, t, autoNative) - checkOrConvertType(store, last, &bx.Right, t, autoNative) + checkOrConvertType(store, last, n, &bx.Left, t, autoNative) + checkOrConvertType(store, last, n, &bx.Right, t, autoNative) return } else { if shouldSwapOnSpecificity(lt, rt) { @@ -3546,11 +3549,11 @@ func checkOrConvertType(store Store, last BlockNode, x *Expr, t Type, autoNative // without a specific context type, '1.0< + (const (undefined)) (mismatched types int and untyped nil) diff --git a/gnovm/tests/files/assign38.gno b/gnovm/tests/files/assign38.gno new file mode 100644 index 00000000000..5ef3549ccf6 --- /dev/null +++ b/gnovm/tests/files/assign38.gno @@ -0,0 +1,10 @@ +package main + +func main() { + a := 1 + a = nil + println(a) +} + +// Error: +// main/files/assign38.gno:5:2: cannot use nil as int value in assignment diff --git a/gnovm/tests/files/fun28.gno b/gnovm/tests/files/fun28.gno new file mode 100644 index 00000000000..cf969f9f34b --- /dev/null +++ b/gnovm/tests/files/fun28.gno @@ -0,0 +1,10 @@ +package main + +func f(i int) {} + +func main() { + f(nil) +} + +// Error: +// main/files/fun28.gno:6:2: cannot use nil as int value in argument to f diff --git a/gnovm/tests/files/slice3.gno b/gnovm/tests/files/slice3.gno new file mode 100644 index 00000000000..1132da01420 --- /dev/null +++ b/gnovm/tests/files/slice3.gno @@ -0,0 +1,9 @@ +package main + +func main() { + i := []string{nil} + println(i) +} + +// Error: +// main/files/slice3.gno:4:7: cannot use nil as string value in array, slice literal or map literal diff --git a/gnovm/tests/files/var35.gno b/gnovm/tests/files/var35.gno new file mode 100644 index 00000000000..87b1cc68590 --- /dev/null +++ b/gnovm/tests/files/var35.gno @@ -0,0 +1,8 @@ +package main + +func main() { + var i int = nil +} + +// Error: +// main/files/var35.gno:4:6: cannot use nil as int value in variable declaration From ed4ebe826b24189d0fcaa50c57eab03ff178c9fa Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Tue, 10 Dec 2024 02:36:38 -0800 Subject: [PATCH 11/20] fix(gnovm): do not allow nil as type declaration (#3309) Fix https://github.com/gnolang/gno/issues/3307 --------- Co-authored-by: hieu.ha --- gnovm/pkg/gnolang/preprocess.go | 6 ++++++ gnovm/tests/files/type40.gno | 2 +- gnovm/tests/files/type41.gno | 9 +++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 gnovm/tests/files/type41.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 6e749053d72..a3e498710bb 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -4283,6 +4283,12 @@ func tryPredefine(store Store, last BlockNode, d Decl) (un Name) { if isBlankIdentifier(tx) { panic("cannot use _ as value or type") } + + // do not allow nil as type. + if tx.Name == "nil" { + panic("nil is not a type") + } + if tv := last.GetValueRef(store, tx.Name, true); tv != nil { t = tv.GetType() if dt, ok := t.(*DeclaredType); ok { diff --git a/gnovm/tests/files/type40.gno b/gnovm/tests/files/type40.gno index 65210798007..fe312e220e0 100644 --- a/gnovm/tests/files/type40.gno +++ b/gnovm/tests/files/type40.gno @@ -43,4 +43,4 @@ func main() { // 5 // 6 // 7 -// yo \ No newline at end of file +// yo diff --git a/gnovm/tests/files/type41.gno b/gnovm/tests/files/type41.gno new file mode 100644 index 00000000000..ea1a3b1df24 --- /dev/null +++ b/gnovm/tests/files/type41.gno @@ -0,0 +1,9 @@ +package main + +type A nil + +func main() { +} + +// Error: +// main/files/type41.gno:3:6: nil is not a type From 8e7fb503adc7a728ea30fedd0891f27d80a2fe1b Mon Sep 17 00:00:00 2001 From: Antoine Eddi <5222525+aeddi@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:02:55 +0100 Subject: [PATCH 12/20] feat(github-bot): refactor comment + add force skip (#3311) This PR significantly modifies the github-bot's comment and adds a button to force the success of its CI check, even it the requirements provided in the config are not met. Related to https://github.com/gnolang/gno/issues/3238#issuecomment-2526174402 **Edit**: I updated [the comment below](https://github.com/gnolang/gno/pull/3311#issuecomment-2528477336) by running the bot on my laptop if you want to see the result (so the skip button is not working yet). --- contribs/github-bot/internal/check/check.go | 32 +++++++++++++++---- contribs/github-bot/internal/check/comment.go | 30 +++++++++-------- .../github-bot/internal/check/comment.tmpl | 32 ++++++++++++++----- .../github-bot/internal/check/comment_test.go | 19 +++++++++-- contribs/github-bot/internal/config/config.go | 9 ++++++ 5 files changed, 91 insertions(+), 31 deletions(-) diff --git a/contribs/github-bot/internal/check/check.go b/contribs/github-bot/internal/check/check.go index 5ca2235e823..cb1848b757c 100644 --- a/contribs/github-bot/internal/check/check.go +++ b/contribs/github-bot/internal/check/check.go @@ -101,7 +101,8 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { go func(pr *github.PullRequest) { defer wg.Done() commentContent := CommentContent{} - commentContent.allSatisfied = true + commentContent.AutoAllSatisfied = true + commentContent.ManualAllSatisfied = true // Iterate over all automatic rules in config. for _, autoRule := range autoRules { @@ -120,7 +121,7 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.Success)) c.Satisfied = true } else { - commentContent.allSatisfied = false + commentContent.AutoAllSatisfied = false } c.ConditionDetails = ifDetails.String() @@ -160,8 +161,14 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { }, ) - if checkedBy == "" { - commentContent.allSatisfied = false + // If this check is the special one, store its state in the dedicated var. + if manualRule.Description == config.ForceSkipDescription { + if checkedBy != "" { + commentContent.ForceSkip = true + } + } else if checkedBy == "" { + // Or if its a normal check, just verify if it was checked by someone. + commentContent.ManualAllSatisfied = false } } @@ -224,9 +231,20 @@ func logResults(logger logger.Logger, prNum int, commentContent CommentContent) } logger.Infof("Conclusion:") - if commentContent.allSatisfied { - logger.Infof("%s All requirements are satisfied\n", utils.Success) + + if commentContent.AutoAllSatisfied { + logger.Infof("%s All automated checks are satisfied", utils.Success) + } else { + logger.Infof("%s Some automated checks are not satisfied", utils.Fail) + } + + if commentContent.ManualAllSatisfied { + logger.Infof("%s All manual checks are satisfied\n", utils.Success) } else { - logger.Infof("%s Not all requirements are satisfied\n", utils.Fail) + logger.Infof("%s Some manual checks are not satisfied\n", utils.Fail) + } + + if commentContent.ForceSkip { + logger.Infof("%s Bot checks are force skipped\n", utils.Success) } } diff --git a/contribs/github-bot/internal/check/comment.go b/contribs/github-bot/internal/check/comment.go index 297395ffe4b..d2b386cfa2e 100644 --- a/contribs/github-bot/internal/check/comment.go +++ b/contribs/github-bot/internal/check/comment.go @@ -24,9 +24,9 @@ var errTriggeredByBot = errors.New("event triggered by bot") // Compile regex only once. var ( // Regex for capturing the entire line of a manual check. - manualCheckLine = regexp.MustCompile(`(?m:^-\s\[([ xX])\]\s+(.+?)\s*(\(checked by @(\w+)\))?$)`) + manualCheckLine = regexp.MustCompile(`(?m:^- \[([ xX])\] (.+?)(?: \(checked by @([A-Za-z0-9-]+)\))?$)`) // Regex for capturing only the checkboxes. - checkboxes = regexp.MustCompile(`(?m:^- \[[ x]\])`) + checkboxes = regexp.MustCompile(`(?m:^- \[[ xX]\])`) // Regex used to capture markdown links. markdownLink = regexp.MustCompile(`\[(.*)\]\([^)]*\)`) ) @@ -46,9 +46,11 @@ type ManualContent struct { Teams []string } type CommentContent struct { - AutoRules []AutoContent - ManualRules []ManualContent - allSatisfied bool + AutoRules []AutoContent + ManualRules []ManualContent + AutoAllSatisfied bool + ManualAllSatisfied bool + ForceSkip bool } type manualCheckDetails struct { @@ -64,10 +66,10 @@ func getCommentManualChecks(commentBody string) map[string]manualCheckDetails { // For each line that matches the "Manual check" regex. for _, match := range manualCheckLine.FindAllStringSubmatch(commentBody, -1) { description := match[2] - status := match[1] + status := strings.ToLower(match[1]) // if X captured, convert it to x. checkedBy := "" - if len(match) > 4 { - checkedBy = strings.ToLower(match[4]) // if X captured, convert it to x. + if len(match) > 3 { + checkedBy = match[3] } checks[description] = manualCheckDetails{status: status, checkedBy: checkedBy} @@ -261,13 +263,15 @@ func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content Commen var ( context = "Merge Requirements" targetURL = comment.GetHTMLURL() - state = "failure" - description = "Some requirements are not satisfied yet. See bot comment." + state = "success" + description = "All requirements are satisfied." ) - if content.allSatisfied { - state = "success" - description = "All requirements are satisfied." + if content.ForceSkip { + description = "Bot checks are skipped for this PR." + } else if !content.AutoAllSatisfied || !content.ManualAllSatisfied { + state = "failure" + description = "Some requirements are not satisfied yet. See bot comment." } // Update or create commit status. diff --git a/contribs/github-bot/internal/check/comment.tmpl b/contribs/github-bot/internal/check/comment.tmpl index 4312019dd2e..d9b633a69d5 100644 --- a/contribs/github-bot/internal/check/comment.tmpl +++ b/contribs/github-bot/internal/check/comment.tmpl @@ -1,19 +1,34 @@ -I'm a bot that assists the Gno Core team in maintaining this repository. My role is to ensure that contributors understand and follow our guidelines, helping to streamline the development process. +#### 🛠 PR Checks Summary +{{ if and .AutoRules (not .AutoAllSatisfied) }}{{ range .AutoRules }}{{ if not .Satisfied }} 🔴 {{ .Description }} +{{end}}{{end}}{{ else }}All **Automated Checks** passed. ✅{{end}} -The following requirements must be fulfilled before a pull request can be merged. -Some requirement checks are automated and can be verified by the CI, while others need manual verification by a staff member. +##### Manual Checks (for Reviewers): +{{ if .ManualRules }}{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }} +{{ end }}{{ else }}*No manual checks match this pull request.*{{ end }} -These requirements are defined in this [configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go). +
Read More -## Automated Checks +🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers. +##### ✅ Automated Checks (for Contributors): {{ if .AutoRules }}{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }} {{ end }}{{ else }}*No automated checks match this pull request.*{{ end }} -## Manual Checks +##### ☑️ Contributor Actions: +1. Fix any issues flagged by automated checks. +2. Follow the Contributor Checklist to ensure your PR is ready for review. + - Add new tests, or document why they are unnecessary. + - Provide clear examples/screenshots, if necessary. + - Update documentation, if required. + - Ensure no breaking changes, or include `BREAKING CHANGE` notes. + - Link related issues/PRs, where applicable. -{{ if .ManualRules }}{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }} -{{ end }}{{ else }}*No manual checks match this pull request.*{{ end }} +##### ☑️ Reviewer Actions: +1. Complete manual checks for the PR, including the guidelines and additional checks if applicable. + +##### 📚 Resources: +- [Report a bug with the bot](https://github.com/gnolang/gno/issues/3238). +- [View the bot’s configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go). {{ if or .AutoRules .ManualRules }}
Debug
{{ if .AutoRules }}
Automated Checks
@@ -52,3 +67,4 @@ These requirements are defined in this [configuration file](https://github.com/g {{ end }}
{{ end }} +
diff --git a/contribs/github-bot/internal/check/comment_test.go b/contribs/github-bot/internal/check/comment_test.go index 0334b76f95c..29886f80f43 100644 --- a/contribs/github-bot/internal/check/comment_test.go +++ b/contribs/github-bot/internal/check/comment_test.go @@ -31,31 +31,44 @@ func TestGeneratedComment(t *testing.T) { {Description: "Test automatic 5", Satisfied: false}, } manualRules := []ManualContent{ - {Description: "Test manual 1", CheckedBy: "user_1"}, + {Description: "Test manual 1", CheckedBy: "user-1"}, {Description: "Test manual 2", CheckedBy: ""}, {Description: "Test manual 3", CheckedBy: ""}, - {Description: "Test manual 4", CheckedBy: "user_4"}, - {Description: "Test manual 5", CheckedBy: "user_5"}, + {Description: "Test manual 4", CheckedBy: "user-4"}, + {Description: "Test manual 5", CheckedBy: "user-5"}, } commentText, err := generateComment(content) assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) assert.True(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should contains automated check placeholder") assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + assert.True(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") content.AutoRules = autoRules + content.AutoAllSatisfied = true commentText, err = generateComment(content) assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + assert.True(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") assert.Equal(t, 2, len(autoCheckSuccessLine.FindAllStringSubmatch(commentText, -1)), "wrong number of succeeded automatic check") assert.Equal(t, 3, len(autoCheckFailLine.FindAllStringSubmatch(commentText, -1)), "wrong number of failed automatic check") + content.AutoAllSatisfied = false + commentText, err = generateComment(content) + assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) + assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") + assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + assert.False(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") + assert.Equal(t, 2, len(autoCheckSuccessLine.FindAllStringSubmatch(commentText, -1)), "wrong number of succeeded automatic check") + assert.Equal(t, 3+3, len(autoCheckFailLine.FindAllStringSubmatch(commentText, -1)), "wrong number of failed automatic check") + content.ManualRules = manualRules commentText, err = generateComment(content) assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") assert.False(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should not contains manual check placeholder") + assert.False(t, strings.Contains(commentText, "All **Automated Checks** passed. ✅"), "should contains automated checks passed placeholder") manualChecks := getCommentManualChecks(commentText) assert.Equal(t, len(manualChecks), len(manualRules), "wrong number of manual checks found") diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go index 2d595c7ce51..fd29f5e5f57 100644 --- a/contribs/github-bot/internal/config/config.go +++ b/contribs/github-bot/internal/config/config.go @@ -22,6 +22,10 @@ type ManualCheck struct { Teams Teams // Members of these teams can check the checkbox to make the check pass. } +// This is the description for a persistent rule with a non-standard behavior +// that allow maintainer to force the "success" state of the CI check +const ForceSkipDescription = "**SKIP**: Do not block the CI for this PR" + // This function returns the configuration of the bot consisting of automatic and manual checks // in which the GitHub client is injected. func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { @@ -53,6 +57,11 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) { } manual := []ManualCheck{ + { + // WARN: Do not edit this special rule which must remain persistent. + Description: ForceSkipDescription, + If: c.Always(), + }, { Description: "The pull request description provides enough details", If: c.Not(c.AuthorInTeam(gh, "core-contributors")), From 4e7305b67ba23f079e33e73e31e0abd90c33d4b9 Mon Sep 17 00:00:00 2001 From: matijamarjanovic <93043005+matijamarjanovic@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:15:51 +0100 Subject: [PATCH 13/20] feat: add Matija's Homepage realm to examples (#2916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Summary This pull request adds a new realm example to the Gno `examples` repository—Matija's Homepage. It showcases a personal homepage built on the Gno chain where users can interact by voting with GNOT tokens to change the page's color. The more tokens users send, the greater influence they have on the color scheme, providing an interactive and dynamic experience. ### Key Features - **Profile Section**: Displays a personal profile with an image and description. - **Color Voting**: Users can vote for the page's color (red, green, blue) by sending GNOT tokens. RGB values are adjusted based on the amount sent. - **Dynamic Updates**: The homepage dynamically updates the color based on votes, showcasing real-time interaction on the Gno blockchain. - **Links to GitHub and LinkedIn**: Includes buttons for GitHub and LinkedIn, making it easy for users to connect. ### Tools & Technologies - Utilizes Gno's native functions to handle voting and token transfers. - Provides a simple, yet effective example of how personal realms can be interactive and engaging on the Gno platform. ### Why this is valuable This example highlights the possibilities of personal realms on Gno, showing how users can create unique and interactive profiles. It’s a fun and approachable entry point for anyone new to Gno development, while also demonstrating the platform's flexibility and potential for creative expression. --------- Co-authored-by: Morgan Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- .../r/matijamarjanovic/home/config.gno | 64 +++++ .../gno.land/r/matijamarjanovic/home/gno.mod | 1 + .../gno.land/r/matijamarjanovic/home/home.gno | 238 ++++++++++++++++++ .../r/matijamarjanovic/home/home_test.gno | 134 ++++++++++ 4 files changed, 437 insertions(+) create mode 100644 examples/gno.land/r/matijamarjanovic/home/config.gno create mode 100644 examples/gno.land/r/matijamarjanovic/home/gno.mod create mode 100644 examples/gno.land/r/matijamarjanovic/home/home.gno create mode 100644 examples/gno.land/r/matijamarjanovic/home/home_test.gno diff --git a/examples/gno.land/r/matijamarjanovic/home/config.gno b/examples/gno.land/r/matijamarjanovic/home/config.gno new file mode 100644 index 00000000000..2a9669c0b58 --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/config.gno @@ -0,0 +1,64 @@ +package home + +import ( + "errors" + "std" +) + +var ( + mainAddr = std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y") // matija's main address + backupAddr std.Address // backup address + + errorInvalidAddr = errors.New("config: invalid address") + errorUnauthorized = errors.New("config: unauthorized") +) + +func Address() std.Address { + return mainAddr +} + +func Backup() std.Address { + return backupAddr +} + +func SetAddress(newAddress std.Address) error { + if !newAddress.IsValid() { + return errorInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + mainAddr = newAddress + return nil +} + +func SetBackup(newAddress std.Address) error { + if !newAddress.IsValid() { + return errorInvalidAddr + } + + if err := checkAuthorized(); err != nil { + return err + } + + backupAddr = newAddress + return nil +} + +func checkAuthorized() error { + caller := std.GetOrigCaller() + if caller != mainAddr && caller != backupAddr { + return errorUnauthorized + } + + return nil +} + +func AssertAuthorized() { + caller := std.GetOrigCaller() + if caller != mainAddr && caller != backupAddr { + panic(errorUnauthorized) + } +} diff --git a/examples/gno.land/r/matijamarjanovic/home/gno.mod b/examples/gno.land/r/matijamarjanovic/home/gno.mod new file mode 100644 index 00000000000..0457c947c01 --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/gno.mod @@ -0,0 +1 @@ +module gno.land/r/matijamarjanovic/home diff --git a/examples/gno.land/r/matijamarjanovic/home/home.gno b/examples/gno.land/r/matijamarjanovic/home/home.gno new file mode 100644 index 00000000000..3757324108a --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/home.gno @@ -0,0 +1,238 @@ +package home + +import ( + "std" + "strings" + + "gno.land/p/demo/ufmt" + "gno.land/p/moul/md" + "gno.land/r/leon/hof" +) + +var ( + pfp string // link to profile picture + pfpCaption string // profile picture caption + abtMe string + + modernVotes int64 + classicVotes int64 + minimalVotes int64 + currentTheme string + + modernLink string + classicLink string + minimalLink string +) + +func init() { + pfp = "https://static.artzone.ai/media/38734/conversions/IPF9dR7ro7n05CmMLLrXIojycr1qdLFxgutaaanG-w768.webp" + pfpCaption = "My profile picture - Tarantula Nebula" + abtMe = `Motivated Computer Science student with strong + analytical and problem-solving skills. Proficient in + programming and version control, with a high level of + focus and attention to detail. Eager to apply academic + knowledge to real-world projects and contribute to + innovative technology solutions. + In addition to my academic pursuits, + I enjoy traveling and staying active through weightlifting. + I have a keen interest in electronic music and often explore various genres. + I believe in maintaining a balanced lifestyle that complements my professional development.` + + modernVotes = 0 + classicVotes = 0 + minimalVotes = 0 + currentTheme = "classic" + modernLink = "https://www.google.com" + classicLink = "https://www.google.com" + minimalLink = "https://www.google.com" + hof.Register() +} + +func UpdatePFP(url, caption string) { + AssertAuthorized() + pfp = url + pfpCaption = caption +} + +func UpdateAboutMe(col1 string) { + AssertAuthorized() + abtMe = col1 +} + +func maxOfThree(a, b, c int64) int64 { + max := a + if b > max { + max = b + } + if c > max { + max = c + } + return max +} + +func VoteModern() { + ugnotAmount := std.GetOrigSend().AmountOf("ugnot") + votes := ugnotAmount + modernVotes += votes + updateCurrentTheme() +} + +func VoteClassic() { + ugnotAmount := std.GetOrigSend().AmountOf("ugnot") + votes := ugnotAmount + classicVotes += votes + updateCurrentTheme() +} + +func VoteMinimal() { + ugnotAmount := std.GetOrigSend().AmountOf("ugnot") + votes := ugnotAmount + minimalVotes += votes + updateCurrentTheme() +} + +func updateCurrentTheme() { + maxVotes := maxOfThree(modernVotes, classicVotes, minimalVotes) + + if maxVotes == modernVotes { + currentTheme = "modern" + } else if maxVotes == classicVotes { + currentTheme = "classic" + } else { + currentTheme = "minimal" + } +} + +func CollectBalance() { + AssertAuthorized() + + banker := std.GetBanker(std.BankerTypeRealmSend) + ownerAddr := Address() + + banker.SendCoins(std.CurrentRealm().Addr(), ownerAddr, banker.GetCoins(std.CurrentRealm().Addr())) +} + +func Render(path string) string { + var sb strings.Builder + + // Theme-specific header styling + switch currentTheme { + case "modern": + // Modern theme - Clean and minimalist with emojis + sb.WriteString(md.H1("🚀 Matija's Space")) + sb.WriteString(md.Image(pfpCaption, pfp)) + sb.WriteString("\n") + sb.WriteString(md.Italic(pfpCaption)) + sb.WriteString("\n") + sb.WriteString(md.HorizontalRule()) + sb.WriteString(abtMe) + sb.WriteString("\n") + + case "minimal": + // Minimal theme - No emojis, minimal formatting + sb.WriteString(md.H1("Matija Marjanovic")) + sb.WriteString("\n") + sb.WriteString(abtMe) + sb.WriteString("\n") + sb.WriteString(md.Image(pfpCaption, pfp)) + sb.WriteString("\n") + sb.WriteString(pfpCaption) + sb.WriteString("\n") + + default: // classic + // Classic theme - Traditional blog style with decorative elements + sb.WriteString(md.H1("✨ Welcome to Matija's Homepage ✨")) + sb.WriteString("\n") + sb.WriteString(md.Image(pfpCaption, pfp)) + sb.WriteString("\n") + sb.WriteString(pfpCaption) + sb.WriteString("\n") + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H2("About me")) + sb.WriteString("\n") + sb.WriteString(abtMe) + sb.WriteString("\n") + } + + // Theme-specific voting section + switch currentTheme { + case "modern": + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H2("🎨 Theme Selector")) + sb.WriteString("Choose your preferred viewing experience:\n") + items := []string{ + md.Link(ufmt.Sprintf("Modern Design (%d votes)", modernVotes), modernLink), + md.Link(ufmt.Sprintf("Classic Style (%d votes)", classicVotes), classicLink), + md.Link(ufmt.Sprintf("Minimal Look (%d votes)", minimalVotes), minimalLink), + } + sb.WriteString(md.BulletList(items)) + + case "minimal": + sb.WriteString("\n") + sb.WriteString(md.H3("Theme Selection")) + sb.WriteString(ufmt.Sprintf("Current theme: %s\n", currentTheme)) + sb.WriteString(ufmt.Sprintf("Votes - Modern: %d | Classic: %d | Minimal: %d\n", + modernVotes, classicVotes, minimalVotes)) + sb.WriteString(md.Link("Modern", modernLink)) + sb.WriteString(" | ") + sb.WriteString(md.Link("Classic", classicLink)) + sb.WriteString(" | ") + sb.WriteString(md.Link("Minimal", minimalLink)) + sb.WriteString("\n") + + default: // classic + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H2("✨ Theme Customization ✨")) + sb.WriteString(md.Bold("Choose Your Preferred Theme:")) + sb.WriteString("\n\n") + items := []string{ + ufmt.Sprintf("Modern 🚀 (%d votes) - %s", modernVotes, md.Link("Vote", modernLink)), + ufmt.Sprintf("Classic ✨ (%d votes) - %s", classicVotes, md.Link("Vote", classicLink)), + ufmt.Sprintf("Minimal ⚡ (%d votes) - %s", minimalVotes, md.Link("Vote", minimalLink)), + } + sb.WriteString(md.BulletList(items)) + } + + // Theme-specific footer/links section + switch currentTheme { + case "modern": + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.Link("GitHub", "https://github.com/matijamarjanovic")) + sb.WriteString(" | ") + sb.WriteString(md.Link("LinkedIn", "https://www.linkedin.com/in/matijamarjanovic")) + sb.WriteString("\n") + + case "minimal": + sb.WriteString("\n") + sb.WriteString(md.Link("GitHub", "https://github.com/matijamarjanovic")) + sb.WriteString(" | ") + sb.WriteString(md.Link("LinkedIn", "https://www.linkedin.com/in/matijamarjanovic")) + sb.WriteString("\n") + + default: // classic + sb.WriteString(md.HorizontalRule()) + sb.WriteString(md.H3("✨ Connect With Me")) + items := []string{ + md.Link("🌟 GitHub", "https://github.com/matijamarjanovic"), + md.Link("💼 LinkedIn", "https://www.linkedin.com/in/matijamarjanovic"), + } + sb.WriteString(md.BulletList(items)) + } + + return sb.String() +} + +func UpdateModernLink(link string) { + AssertAuthorized() + modernLink = link +} + +func UpdateClassicLink(link string) { + AssertAuthorized() + classicLink = link +} + +func UpdateMinimalLink(link string) { + AssertAuthorized() + minimalLink = link +} diff --git a/examples/gno.land/r/matijamarjanovic/home/home_test.gno b/examples/gno.land/r/matijamarjanovic/home/home_test.gno new file mode 100644 index 00000000000..8cc6e6e5608 --- /dev/null +++ b/examples/gno.land/r/matijamarjanovic/home/home_test.gno @@ -0,0 +1,134 @@ +package home + +import ( + "std" + "strings" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +// Helper function to set up test environment +func setupTest() { + std.TestSetOrigCaller(std.Address("g1ej0qca5ptsw9kfr64ey8jvfy9eacga6mpj2z0y")) +} + +func TestUpdatePFP(t *testing.T) { + setupTest() + pfp = "" + pfpCaption = "" + + UpdatePFP("https://example.com/pic.png", "New Caption") + + urequire.Equal(t, pfp, "https://example.com/pic.png", "Profile picture URL should be updated") + urequire.Equal(t, pfpCaption, "New Caption", "Profile picture caption should be updated") +} + +func TestUpdateAboutMe(t *testing.T) { + setupTest() + abtMe = "" + + UpdateAboutMe("This is my new bio.") + + urequire.Equal(t, abtMe, "This is my new bio.", "About Me should be updated") +} + +func TestVoteModern(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000)) + coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1)) + + std.TestSetOrigSend(coinsSent, coinsSpent) + VoteModern() + + uassert.Equal(t, int64(75000000), modernVotes, "Modern votes should be calculated correctly") + uassert.Equal(t, "modern", currentTheme, "Theme should be updated to modern") +} + +func TestVoteClassic(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000)) + coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1)) + + std.TestSetOrigSend(coinsSent, coinsSpent) + VoteClassic() + + uassert.Equal(t, int64(75000000), classicVotes, "Classic votes should be calculated correctly") + uassert.Equal(t, "classic", currentTheme, "Theme should be updated to classic") +} + +func TestVoteMinimal(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + + coinsSent := std.NewCoins(std.NewCoin("ugnot", 75000000)) + coinsSpent := std.NewCoins(std.NewCoin("ugnot", 1)) + + std.TestSetOrigSend(coinsSent, coinsSpent) + VoteMinimal() + + uassert.Equal(t, int64(75000000), minimalVotes, "Minimal votes should be calculated correctly") + uassert.Equal(t, "minimal", currentTheme, "Theme should be updated to minimal") +} + +func TestRender(t *testing.T) { + setupTest() + // Reset the state to known values + modernVotes, classicVotes, minimalVotes = 0, 0, 0 + currentTheme = "classic" + pfp = "https://example.com/pic.png" + pfpCaption = "Test Caption" + abtMe = "Test About Me" + + out := Render("") + urequire.NotEqual(t, out, "", "Render output should not be empty") + + // Test classic theme specific content + uassert.True(t, strings.Contains(out, "✨ Welcome to Matija's Homepage ✨"), "Classic theme should have correct header") + uassert.True(t, strings.Contains(out, pfp), "Should contain profile picture URL") + uassert.True(t, strings.Contains(out, pfpCaption), "Should contain profile picture caption") + uassert.True(t, strings.Contains(out, "About me"), "Should contain About me section") + uassert.True(t, strings.Contains(out, abtMe), "Should contain about me content") + uassert.True(t, strings.Contains(out, "Theme Customization"), "Should contain theme customization section") + uassert.True(t, strings.Contains(out, "Connect With Me"), "Should contain connect section") +} + +func TestRenderModernTheme(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 100, 0, 0 + currentTheme = "modern" + updateCurrentTheme() + + out := Render("") + uassert.True(t, strings.Contains(out, "🚀 Matija's Space"), "Modern theme should have correct header") +} + +func TestRenderMinimalTheme(t *testing.T) { + setupTest() + modernVotes, classicVotes, minimalVotes = 0, 0, 100 + currentTheme = "minimal" + updateCurrentTheme() + + out := Render("") + uassert.True(t, strings.Contains(out, "Matija Marjanovic"), "Minimal theme should have correct header") +} + +func TestUpdateLinks(t *testing.T) { + setupTest() + + newLink := "https://example.com/vote" + + UpdateModernLink(newLink) + urequire.Equal(t, modernLink, newLink, "Modern link should be updated") + + UpdateClassicLink(newLink) + urequire.Equal(t, classicLink, newLink, "Classic link should be updated") + + UpdateMinimalLink(newLink) + urequire.Equal(t, minimalLink, newLink, "Minimal link should be updated") +} From 5c31552b05c78575f45876ab07abd73c1d96bf27 Mon Sep 17 00:00:00 2001 From: ltzmaxwell Date: Tue, 10 Dec 2024 21:50:53 +0800 Subject: [PATCH 14/20] fix(gnovm): make static-analysis handle block stmt (#3313) as the title says. it give incorrect error before fix: ``` unexpected panic: main/files/block0.gno:3:1: [function "foo" does not terminate] ``` --- gnovm/pkg/gnolang/static_analysis.go | 2 ++ gnovm/tests/files/block0.gno | 14 ++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 gnovm/tests/files/block0.gno diff --git a/gnovm/pkg/gnolang/static_analysis.go b/gnovm/pkg/gnolang/static_analysis.go index 311a0d42feb..7094ccbb4c8 100644 --- a/gnovm/pkg/gnolang/static_analysis.go +++ b/gnovm/pkg/gnolang/static_analysis.go @@ -108,6 +108,8 @@ func (s *staticAnalysis) staticAnalysisExpr(expr Expr) bool { // indicating whether a statement is terminating or not func (s *staticAnalysis) staticAnalysisStmt(stmt Stmt) bool { switch n := stmt.(type) { + case *BlockStmt: + return s.staticAnalysisBlockStmt(n.Body) case *BranchStmt: switch n.Op { case BREAK: diff --git a/gnovm/tests/files/block0.gno b/gnovm/tests/files/block0.gno new file mode 100644 index 00000000000..b6d554ce500 --- /dev/null +++ b/gnovm/tests/files/block0.gno @@ -0,0 +1,14 @@ +package main + +func foo() int { + { + return 1 + } +} + +func main() { + println(foo()) +} + +// Output: +// 1 From c33cf676daed03a29ca85f7386daf98e35d2b38f Mon Sep 17 00:00:00 2001 From: piux2 <90544084+piux2@users.noreply.github.com> Date: Tue, 10 Dec 2024 08:20:52 -0800 Subject: [PATCH 15/20] fix: catch the out of gas exception in preprocess (#2638)
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Morgan --- .../gnoland/testdata/addpkg_outofgas.txtar | 57 +++++++++++++++++++ gnovm/pkg/gnolang/preprocess.go | 7 +++ 2 files changed, 64 insertions(+) create mode 100644 gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar diff --git a/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar b/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar new file mode 100644 index 00000000000..56050f4733b --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/addpkg_outofgas.txtar @@ -0,0 +1,57 @@ +# ensure users get proper out of gas errors when they add packages + +# start a new node +gnoland start + +# add foo package +gnokey maketx addpkg -pkgdir $WORK/foo -pkgpath gno.land/r/foo -gas-fee 1000000ugnot -gas-wanted 220000 -broadcast -chainid=tendermint_test test1 + + +# add bar package +# out of gas at store.GetPackage() with gas 60000 + +! gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/bar -gas-fee 1000000ugnot -gas-wanted 60000 -broadcast -chainid=tendermint_test test1 + +# Out of gas error + +stderr '--= Error =--' +stderr 'Data: out of gas error' +stderr 'Msg Traces:' +stderr 'out of gas.*?in preprocess' +stderr '--= /Error =--' + + + +# out of gas at store.store.GetTypeSafe() with gas 63000 + +! gnokey maketx addpkg -pkgdir $WORK/bar -pkgpath gno.land/r/bar -gas-fee 1000000ugnot -gas-wanted 63000 -broadcast -chainid=tendermint_test test1 + +stderr '--= Error =--' +stderr 'Data: out of gas error' +stderr 'Msg Traces:' +stderr 'out of gas.*?in preprocess' +stderr '--= /Error =--' + + +-- foo/foo.gno -- +package foo + +type Counter int + +func Inc(i Counter) Counter{ + i = i+1 + return i +} + +-- bar/bar.gno -- +package bar + +import "gno.land/r/foo" + +type NewCounter foo.Counter + +func Add2(i NewCounter) NewCounter{ + i=i+2 + + return i +} diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index a3e498710bb..15f268f6321 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -10,6 +10,7 @@ import ( "sync/atomic" "github.com/gnolang/gno/tm2/pkg/errors" + tmstore "github.com/gnolang/gno/tm2/pkg/store" ) const ( @@ -365,6 +366,12 @@ func initStaticBlocks(store Store, ctx BlockNode, bn BlockNode) { func doRecover(stack []BlockNode, n Node) { if r := recover(); r != nil { + // Catch the out-of-gas exception and throw it + if exp, ok := r.(tmstore.OutOfGasException); ok { + exp.Descriptor = fmt.Sprintf("in preprocess: %v", r) + panic(exp) + } + if _, ok := r.(*PreprocessError); ok { // re-panic directly if this is a PreprocessError already. panic(r) From 7185cefe2e091cb1795aef1d3d4c4d938a81778c Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 11 Dec 2024 09:05:18 +0100 Subject: [PATCH 16/20] fix(gnovm): in op_binary, return typed booleans where appropriate (#3298) bool8.gno was failing, because the result of the `==` expression is an untyped boolean, while the first value is a typed boolean. This PR ensures that if either of the values in a binary expression is typed, we return a typed bool instead of an untyped bool. --------- Co-authored-by: ltzmaxwell --- gnovm/pkg/gnolang/preprocess.go | 4 ++++ gnovm/tests/files/bool8.gno | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 gnovm/tests/files/bool8.gno diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index 15f268f6321..4ff182670cd 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -3574,6 +3574,10 @@ func checkOrConvertType(store Store, last BlockNode, n Node, x *Expr, t Type, au checkOrConvertType(store, last, n, &bx.Left, rt, autoNative) checkOrConvertType(store, last, n, &bx.Right, rt, autoNative) } + // this is not a constant expression; the result here should + // always be a BoolType. (in this scenario, we may have some + // UntypedBoolTypes) + t = BoolType default: // do nothing } diff --git a/gnovm/tests/files/bool8.gno b/gnovm/tests/files/bool8.gno new file mode 100644 index 00000000000..9efbbbe6da2 --- /dev/null +++ b/gnovm/tests/files/bool8.gno @@ -0,0 +1,17 @@ +package main + +// results from comparisons should not be untyped bools + +var a interface{} = true + +func main() { + buf := "hello=" + isEqual(a, (buf[len(buf)-1] == '=')) +} + +func isEqual(v1, v2 interface{}) { + println("v1 == v2", v1 == v2) +} + +// Output: +// v1 == v2 true From 6f48a5b6eb26e6dcb7cd2797c57f4545cb02f744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Albi?= Date: Wed, 11 Dec 2024 11:42:06 +0100 Subject: [PATCH 17/20] feat: generic datasource package (#3318) The new package is a generic implementation for datasources. It aims to be one possible solution to integrate/aggregate data from different realms. --- .../p/jeronimoalbi/datasource/datasource.gno | 103 +++++++++++ .../datasource/datasource_test.gno | 171 ++++++++++++++++++ .../p/jeronimoalbi/datasource/gno.mod | 1 + .../p/jeronimoalbi/datasource/query.gno | 70 +++++++ .../p/jeronimoalbi/datasource/query_test.gno | 104 +++++++++++ 5 files changed, 449 insertions(+) create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/datasource.gno create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/gno.mod create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/query.gno create mode 100644 examples/gno.land/p/jeronimoalbi/datasource/query_test.gno diff --git a/examples/gno.land/p/jeronimoalbi/datasource/datasource.gno b/examples/gno.land/p/jeronimoalbi/datasource/datasource.gno new file mode 100644 index 00000000000..bf80964a9a0 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/datasource.gno @@ -0,0 +1,103 @@ +// Package datasource defines generic interfaces for datasources. +// +// Datasources contain a set of records which can optionally be +// taggable. Tags can optionally be used to filter records by taxonomy. +// +// Datasources can help in cases where the data sent during +// communication between different realms needs to be generic +// to avoid direct dependencies. +package datasource + +import "errors" + +// ErrInvalidRecord indicates that a datasource contains invalid records. +var ErrInvalidRecord = errors.New("datasource records is not valid") + +type ( + // Fields defines an interface for read-only fields. + Fields interface { + // Has checks whether a field exists. + Has(name string) bool + + // Get retrieves the value associated with the given field. + Get(name string) (value interface{}, found bool) + } + + // Record defines a datasource record. + Record interface { + // ID returns the unique record's identifier. + ID() string + + // String returns a string representation of the record. + String() string + + // Fields returns record fields and values. + Fields() (Fields, error) + } + + // TaggableRecord defines a datasource record that supports tags. + // Tags can be used to build a taxonomy to filter records by category. + TaggableRecord interface { + // Tags returns a list of tags for the record. + Tags() []string + } + + // ContentRecord defines a datasource record that can return content. + ContentRecord interface { + // Content returns the record content. + Content() (string, error) + } + + // Iterator defines an iterator of datasource records. + Iterator interface { + // Next returns true when a new record is available. + Next() bool + + // Err returns any error raised when reading records. + Err() error + + // Record returns the current record. + Record() Record + } + + // Datasource defines a generic datasource. + Datasource interface { + // Records returns a new datasource records iterator. + Records(Query) Iterator + + // Size returns the total number of records in the datasource. + // When -1 is returned it means datasource doesn't support size. + Size() int + + // Record returns a single datasource record. + Record(id string) (Record, error) + } +) + +// NewIterator returns a new record iterator for a datasource query. +func NewIterator(ds Datasource, options ...QueryOption) Iterator { + return ds.Records(NewQuery(options...)) +} + +// QueryRecords return a slice of records for a datasource query. +func QueryRecords(ds Datasource, options ...QueryOption) ([]Record, error) { + var ( + records []Record + query = NewQuery(options...) + iter = ds.Records(query) + ) + + for i := 0; i < query.Count && iter.Next(); i++ { + r := iter.Record() + if r == nil { + return nil, ErrInvalidRecord + } + + records = append(records, r) + } + + if err := iter.Err(); err != nil { + return nil, err + } + return records, nil +} diff --git a/examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno b/examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno new file mode 100644 index 00000000000..304a311ced7 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/datasource_test.gno @@ -0,0 +1,171 @@ +package datasource + +import ( + "errors" + "testing" + + "gno.land/p/demo/uassert" + "gno.land/p/demo/urequire" +) + +func TestNewIterator(t *testing.T) { + cases := []struct { + name string + records []Record + err error + }{ + { + name: "ok", + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "2"}, + testRecord{id: "3"}, + }, + }, + { + name: "error", + err: errors.New("test"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + ds := testDatasource{ + records: tc.records, + err: tc.err, + } + + // Act + iter := NewIterator(ds) + + // Assert + if tc.err != nil { + uassert.ErrorIs(t, tc.err, iter.Err()) + return + } + + uassert.NoError(t, iter.Err()) + + for i := 0; iter.Next(); i++ { + r := iter.Record() + urequire.NotEqual(t, nil, r, "valid record") + urequire.True(t, i < len(tc.records), "iteration count") + uassert.Equal(t, tc.records[i].ID(), r.ID()) + } + }) + } +} + +func TestQueryRecords(t *testing.T) { + cases := []struct { + name string + records []Record + recordCount int + options []QueryOption + err error + }{ + { + name: "ok", + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "2"}, + testRecord{id: "3"}, + }, + recordCount: 3, + }, + { + name: "with count", + options: []QueryOption{WithCount(2)}, + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "2"}, + testRecord{id: "3"}, + }, + recordCount: 2, + }, + { + name: "invalid record", + records: []Record{ + testRecord{id: "1"}, + nil, + testRecord{id: "3"}, + }, + err: ErrInvalidRecord, + }, + { + name: "iterator error", + records: []Record{ + testRecord{id: "1"}, + testRecord{id: "3"}, + }, + err: errors.New("test"), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + ds := testDatasource{ + records: tc.records, + err: tc.err, + } + + // Act + records, err := QueryRecords(ds, tc.options...) + + // Assert + if tc.err != nil { + uassert.ErrorIs(t, tc.err, err) + return + } + + uassert.NoError(t, err) + + urequire.Equal(t, tc.recordCount, len(records), "record count") + for i, r := range records { + urequire.NotEqual(t, nil, r, "valid record") + uassert.Equal(t, tc.records[i].ID(), r.ID()) + } + }) + } +} + +type testDatasource struct { + records []Record + err error +} + +func (testDatasource) Size() int { return -1 } +func (testDatasource) Record(string) (Record, error) { return nil, nil } +func (ds testDatasource) Records(Query) Iterator { return &testIter{records: ds.records, err: ds.err} } + +type testRecord struct { + id string + fields Fields + err error +} + +func (r testRecord) ID() string { return r.id } +func (r testRecord) String() string { return "str" + r.id } +func (r testRecord) Fields() (Fields, error) { return r.fields, r.err } + +type testIter struct { + index int + records []Record + current Record + err error +} + +func (it testIter) Err() error { return it.err } +func (it testIter) Record() Record { return it.current } + +func (it *testIter) Next() bool { + count := len(it.records) + if it.err != nil || count == 0 || it.index >= count { + return false + } + it.current = it.records[it.index] + it.index++ + return true +} diff --git a/examples/gno.land/p/jeronimoalbi/datasource/gno.mod b/examples/gno.land/p/jeronimoalbi/datasource/gno.mod new file mode 100644 index 00000000000..3b398971b41 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/gno.mod @@ -0,0 +1 @@ +module gno.land/p/jeronimoalbi/datasource diff --git a/examples/gno.land/p/jeronimoalbi/datasource/query.gno b/examples/gno.land/p/jeronimoalbi/datasource/query.gno new file mode 100644 index 00000000000..f971f9c64db --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/query.gno @@ -0,0 +1,70 @@ +package datasource + +import "gno.land/p/demo/avl" + +// DefaultQueryRecords defines the default number of records returned by queries. +const DefaultQueryRecords = 50 + +var defaultQuery = Query{Count: DefaultQueryRecords} + +type ( + // QueryOption configures datasource queries. + QueryOption func(*Query) + + // Query contains datasource query options. + Query struct { + // Offset of the first record to return during iteration. + Offset int + + // Count contains the number to records that query should return. + Count int + + // Tag contains a tag to use as filter for the records. + Tag string + + // Filters contains optional query filters by field value. + Filters avl.Tree + } +) + +// WithOffset configures query to return records starting from an offset. +func WithOffset(offset int) QueryOption { + return func(q *Query) { + q.Offset = offset + } +} + +// WithCount configures the number of records that query returns. +func WithCount(count int) QueryOption { + return func(q *Query) { + if count < 1 { + count = DefaultQueryRecords + } + q.Count = count + } +} + +// ByTag configures query to filter by tag. +func ByTag(tag string) QueryOption { + return func(q *Query) { + q.Tag = tag + } +} + +// WithFilter assigns a new filter argument to a query. +// This option can be used multiple times if more than one +// filter has to be given to the query. +func WithFilter(field string, value interface{}) QueryOption { + return func(q *Query) { + q.Filters.Set(field, value) + } +} + +// NewQuery creates a new datasource query. +func NewQuery(options ...QueryOption) Query { + q := defaultQuery + for _, apply := range options { + apply(&q) + } + return q +} diff --git a/examples/gno.land/p/jeronimoalbi/datasource/query_test.gno b/examples/gno.land/p/jeronimoalbi/datasource/query_test.gno new file mode 100644 index 00000000000..6f78d41bb35 --- /dev/null +++ b/examples/gno.land/p/jeronimoalbi/datasource/query_test.gno @@ -0,0 +1,104 @@ +package datasource + +import ( + "fmt" + "testing" + + "gno.land/p/demo/uassert" +) + +func TestNewQuery(t *testing.T) { + cases := []struct { + name string + options []QueryOption + setup func() Query + }{ + { + name: "default", + setup: func() Query { + return Query{Count: DefaultQueryRecords} + }, + }, + { + name: "with offset", + options: []QueryOption{WithOffset(100)}, + setup: func() Query { + return Query{ + Offset: 100, + Count: DefaultQueryRecords, + } + }, + }, + { + name: "with count", + options: []QueryOption{WithCount(10)}, + setup: func() Query { + return Query{Count: 10} + }, + }, + { + name: "with invalid count", + options: []QueryOption{WithCount(0)}, + setup: func() Query { + return Query{Count: DefaultQueryRecords} + }, + }, + { + name: "by tag", + options: []QueryOption{ByTag("foo")}, + setup: func() Query { + return Query{ + Tag: "foo", + Count: DefaultQueryRecords, + } + }, + }, + { + name: "with filter", + options: []QueryOption{WithFilter("foo", 42)}, + setup: func() Query { + q := Query{Count: DefaultQueryRecords} + q.Filters.Set("foo", 42) + return q + }, + }, + { + name: "with multiple filters", + options: []QueryOption{ + WithFilter("foo", 42), + WithFilter("bar", "baz"), + }, + setup: func() Query { + q := Query{Count: DefaultQueryRecords} + q.Filters.Set("foo", 42) + q.Filters.Set("bar", "baz") + return q + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + want := tc.setup() + + // Act + q := NewQuery(tc.options...) + + // Assert + uassert.Equal(t, want.Offset, q.Offset) + uassert.Equal(t, want.Count, q.Count) + uassert.Equal(t, want.Tag, q.Tag) + uassert.Equal(t, want.Filters.Size(), q.Filters.Size()) + + want.Filters.Iterate("", "", func(k string, v interface{}) bool { + got, exists := q.Filters.Get(k) + uassert.True(t, exists) + if exists { + uassert.Equal(t, fmt.Sprint(v), fmt.Sprint(got)) + } + return false + }) + }) + } +} From a85a53d5b38f0a21d66262a823a8b07f4f836b68 Mon Sep 17 00:00:00 2001 From: piux2 <90544084+piux2@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:19:04 -0800 Subject: [PATCH 18/20] fix: prevent false positive return for guarding dao member store (#3121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If we want to guard the MemStore by checking the active DAO realm, m.daoPkgPath must first be assigned a realm package path; otherwise, the isCallerDAORealm() method may return a false positive, failing to protect the MemStore.
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests
--------- Co-authored-by: Miloš Živković --- examples/gno.land/p/demo/membstore/membstore.gno | 2 +- examples/gno.land/r/gov/dao/v2/dao.gno | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/p/demo/membstore/membstore.gno b/examples/gno.land/p/demo/membstore/membstore.gno index 6e1932978d9..ca721d078e6 100644 --- a/examples/gno.land/p/demo/membstore/membstore.gno +++ b/examples/gno.land/p/demo/membstore/membstore.gno @@ -205,5 +205,5 @@ func (m *MembStore) TotalPower() uint64 { // the API of the member store is public and callable // by anyone who has a reference to the member store instance. func (m *MembStore) isCallerDAORealm() bool { - return m.daoPkgPath == "" || std.CurrentRealm().PkgPath() == m.daoPkgPath + return m.daoPkgPath != "" && std.CurrentRealm().PkgPath() == m.daoPkgPath } diff --git a/examples/gno.land/r/gov/dao/v2/dao.gno b/examples/gno.land/r/gov/dao/v2/dao.gno index 9263d8d440b..5ee8e63236a 100644 --- a/examples/gno.land/r/gov/dao/v2/dao.gno +++ b/examples/gno.land/r/gov/dao/v2/dao.gno @@ -13,6 +13,8 @@ var ( members membstore.MemberStore // the member store ) +const daoPkgPath = "gno.land/r/gov/dao/v2" + func init() { // Example initial member set (just test addresses) set := []membstore.Member{ @@ -23,7 +25,7 @@ func init() { } // Set the member store - members = membstore.NewMembStore(membstore.WithInitialMembers(set)) + members = membstore.NewMembStore(membstore.WithInitialMembers(set), membstore.WithDAOPkgPath(daoPkgPath)) // Set the DAO implementation d = simpledao.New(members) From 79ca9a958dea7c94aaf6c3dc74f522c5f05e791e Mon Sep 17 00:00:00 2001 From: Mikael VALLENET Date: Thu, 12 Dec 2024 16:47:27 +0100 Subject: [PATCH 19/20] fix(std): add full denom in banker issue & remove coin (#3239) fix #2107 -------------------------- ## Problem > From [#875 (review)](https://github.com/gnolang/gno/pull/875#pullrequestreview-2043984930): > > > That said, soon after this is merged, I think we'll need to change this API again. This current implementation creates an inconsistency within the Banker API. All other banker methods now require you to pass in the full realm path to the token you're referring to, but IssueCoin and RemoveCoin do not. > > Thus, I think a few more changes are in order: > > > > 1. There should be a `RealmDenom(pkgpath, denom string)` function in `std`, which creates a realm denomination (ie. `/gno.land/r/morgan:bitcoin`). There can be a helper method `Realm.Denom(denom string)` (so you can do `std.CurrentRealm().Denom("bitcoin")` > > 2. Instead of modifying `denom`'s value in the native function, we should check it matches what we expect. ie. `strings.HasPrefix(denom, RealmDenom(std.CurrentRealm().PkgPath())`, then check the last part of the denom to see that it matches the Gno regex. (This can all be done in gno, without needing to put it in native code) > > Related with #1475 #1576 ------------------------- ## Solution BREAKING CHANGE: All previous realm calling IssueCoin or RemoveCoin are now expected to append the prefix "/" + realmPkgPath + ":" before the denom, it should be done by using ``std.CurrentRealm().CoinDenom(denom string)`` or by using ``std.CoinDenom(pkgPath, denom string)`` For now to avoid to mix coins and fix security issues like being able to issue coins from other realm, when a realm issue a coin, the pkg path of the realm is added as a prefix to the coin. the thing is some function expect only the base denom ``bitcoin`` (issue & remove) but the others like get require the qualified denom ``gno.land/r/demo/banktest:bitcoin``. it can be confusing I also answer the requirements of the comment @thehowl made: - Two functions are now available ``std.CoinDenom(pkgpath, demon string)`` && the method ``std.Realm.CoinDenom(denom string)`` - the denom's value is changed in the `.gno` file and not the native. Here is an example of how it looks like: ```go func IssueNewCoin(denom string, amount int64) string { std.AssertOriginCall() banker := std.GetBanker(std.BankerTypeRealmIssue) addr := std.PrevRealm().Addr() banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) return std.CurrentRealm().Denom(denom) } func RemoveCoin(denom string, amount int64) { std.AssertOriginCall() banker := std.GetBanker(std.BankerTypeRealmIssue) addr := std.PrevRealm().Addr() banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) } func GetCoins(denom string) uint64 { banker := std.GetBanker(std.BankerTypeReadonly) addr := std.PrevRealm().Addr() coins := banker.GetCoins(addr) for _, coin := range coins { if coin.Denom == std.CurrentRealm().CoinDenom(denom) { return uint64(coin.Amount) } } return 0 } ```
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [x] Added references to related issues and PRs - [x] Provided any useful hints for running manual tests
--------- Co-authored-by: Morgan Bazalgette Co-authored-by: Leon Hudak <33522493+leohhhn@users.noreply.github.com> --- docs/reference/stdlibs/std/banker.md | 4 ++ docs/reference/stdlibs/std/chain.md | 16 ++++++++ docs/reference/stdlibs/std/realm.md | 13 ++++++ .../gnoland/testdata/assertorigincall.txtar | 40 +++++++++---------- .../cmd/gnoland/testdata/grc20_registry.txtar | 8 ++-- gno.land/cmd/gnoland/testdata/prevrealm.txtar | 22 +++++----- .../realm_banker_issued_coin_denom.txtar | 40 ++++++++++++++++++- gno.land/pkg/gnoclient/integration_test.go | 14 +++---- gnovm/stdlibs/std/banker.gno | 32 +++++++++++++++ gnovm/stdlibs/std/banker.go | 30 +------------- gnovm/stdlibs/std/frame.gno | 12 ++++++ gnovm/tests/files/std5.gno | 2 +- gnovm/tests/files/std8.gno | 2 +- gnovm/tests/files/zrealm_natbind0.gno | 2 +- 14 files changed, 162 insertions(+), 75 deletions(-) diff --git a/docs/reference/stdlibs/std/banker.md b/docs/reference/stdlibs/std/banker.md index 71eb3709ea2..b60b55ee93b 100644 --- a/docs/reference/stdlibs/std/banker.md +++ b/docs/reference/stdlibs/std/banker.md @@ -38,6 +38,10 @@ Returns `Banker` of the specified type. ```go banker := std.GetBanker(std.) ``` + +:::info `Banker` methods expect qualified denomination of the coins. Read more [here](./realm.md#coindenom). +::: + --- ## GetCoins diff --git a/docs/reference/stdlibs/std/chain.md b/docs/reference/stdlibs/std/chain.md index b1791e65608..6a1da6483fd 100644 --- a/docs/reference/stdlibs/std/chain.md +++ b/docs/reference/stdlibs/std/chain.md @@ -162,3 +162,19 @@ Derives the Realm address from its `pkgpath` parameter. ```go realmAddr := std.DerivePkgAddr("gno.land/r/demo/tamagotchi") // g1a3tu874agjlkrpzt9x90xv3uzncapcn959yte4 ``` +--- + +## CoinDenom +```go +func CoinDenom(pkgPath, coinName string) string +``` +Composes a qualified denomination string from the realm's `pkgPath` and the provided coin name, e.g. `/gno.land/r/demo/blog:blgcoin`. This method should be used to get fully qualified denominations of coins when interacting with the `Banker` module. It can also be used as a method of the `Realm` object, Read more[here](./realm.md#coindenom). + +#### Parameters +- `pkgPath` **string** - package path of the realm +- `coinName` **string** - The coin name used to build the qualified denomination. Must start with a lowercase letter, followed by 2–15 lowercase letters or digits. + +#### Usage +```go +denom := std.CoinDenom("gno.land/r/demo/blog", "blgcoin") // /gno.land/r/demo/blog:blgcoin +``` diff --git a/docs/reference/stdlibs/std/realm.md b/docs/reference/stdlibs/std/realm.md index 0c99b7134ea..f69cd874c75 100644 --- a/docs/reference/stdlibs/std/realm.md +++ b/docs/reference/stdlibs/std/realm.md @@ -14,6 +14,7 @@ type Realm struct { func (r Realm) Addr() Address {...} func (r Realm) PkgPath() string {...} func (r Realm) IsUser() bool {...} +func (r Realm) CoinDenom(coinName string) string {...} ``` ## Addr @@ -39,3 +40,15 @@ Checks if the realm it was called upon is a user realm. ```go if r.IsUser() {...} ``` +--- +## CoinDenom +Composes a qualified denomination string from the realm's `pkgPath` and the provided coin name, e.g. `/gno.land/r/demo/blog:blgcoin`. This method should be used to get fully qualified denominations of coins when interacting with the `Banker` module. + +#### Parameters +- `coinName` **string** - The coin name used to build the qualified denomination. Must start with a lowercase letter, followed by 2–15 lowercase letters or digits. + +#### Usage +```go +// in "gno.land/r/gnoland/blog" +denom := r.CoinDenom("blgcoin") // /gno.land/r/gnoland/blog:blgcoin +``` diff --git a/gno.land/cmd/gnoland/testdata/assertorigincall.txtar b/gno.land/cmd/gnoland/testdata/assertorigincall.txtar index 62d660a9215..1a5664d6bef 100644 --- a/gno.land/cmd/gnoland/testdata/assertorigincall.txtar +++ b/gno.land/cmd/gnoland/testdata/assertorigincall.txtar @@ -33,85 +33,85 @@ gnoland start # Test cases ## 1. MsgCall -> myrlm.A: PANIC -! gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +! gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stderr 'invalid non-origin call' ## 2. MsgCall -> myrlm.B: PASS -gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout 'OK!' ## 3. MsgCall -> myrlm.C: PASS -gnokey maketx call -pkgpath gno.land/r/myrlm -func C -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/myrlm -func C -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout 'OK!' ## 4. MsgCall -> r/foo.A -> myrlm.A: PANIC -! gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +! gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stderr 'invalid non-origin call' ## 5. MsgCall -> r/foo.B -> myrlm.B: PASS -gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout 'OK!' ## 6. MsgCall -> r/foo.C -> myrlm.C: PANIC -! gnokey maketx call -pkgpath gno.land/r/foo -func C -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +! gnokey maketx call -pkgpath gno.land/r/foo -func C -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stderr 'invalid non-origin call' ## remove due to update to maketx call can only call realm (case 7,8,9) ## 7. MsgCall -> p/demo/bar.A: PANIC -## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 ## stderr 'invalid non-origin call' ## 8. MsgCall -> p/demo/bar.B: PASS -## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +## gnokey maketx call -pkgpath gno.land/p/demo/bar -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 ## stdout 'OK!' ## 9. MsgCall -> p/demo/bar.C: PANIC -## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func C -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +## ! gnokey maketx call -pkgpath gno.land/p/demo/bar -func C -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 ## stderr 'invalid non-origin call' ## 10. MsgRun -> run.main -> myrlm.A: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno stderr 'invalid non-origin call' ## 11. MsgRun -> run.main -> myrlm.B: PASS -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno stdout 'OK!' ## 12. MsgRun -> run.main -> myrlm.C: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmC.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmC.gno stderr 'invalid non-origin call' ## 13. MsgRun -> run.main -> foo.A: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno stderr 'invalid non-origin call' ## 14. MsgRun -> run.main -> foo.B: PASS -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno stdout 'OK!' ## 15. MsgRun -> run.main -> foo.C: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooC.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooC.gno stderr 'invalid non-origin call' ## 16. MsgRun -> run.main -> bar.A: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno stderr 'invalid non-origin call' ## 17. MsgRun -> run.main -> bar.B: PASS -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno stdout 'OK!' ## 18. MsgRun -> run.main -> bar.C: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barC.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barC.gno stderr 'invalid non-origin call' ## remove testcase 19 due to maketx call forced to call a realm ## 19. MsgCall -> std.AssertOriginCall: pass -## gnokey maketx call -pkgpath std -func AssertOriginCall -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +## gnokey maketx call -pkgpath std -func AssertOriginCall -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 ## stdout 'OK!' ## 20. MsgRun -> std.AssertOriginCall: PANIC -! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno +! gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno stderr 'invalid non-origin call' diff --git a/gno.land/cmd/gnoland/testdata/grc20_registry.txtar b/gno.land/cmd/gnoland/testdata/grc20_registry.txtar index a5f7ad5eee3..417ab04539d 100644 --- a/gno.land/cmd/gnoland/testdata/grc20_registry.txtar +++ b/gno.land/cmd/gnoland/testdata/grc20_registry.txtar @@ -6,15 +6,15 @@ loadpkg gno.land/r/registry $WORK/registry gnoland start # we call Transfer with foo20, before it's registered -gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test test1 stdout 'not found' # add foo20, and foo20wrapper -gnokey maketx addpkg -pkgdir $WORK/foo20 -pkgpath gno.land/r/foo20 -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 -gnokey maketx addpkg -pkgdir $WORK/foo20wrapper -pkgpath gno.land/r/foo20wrapper -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK/foo20 -pkgpath gno.land/r/foo20 -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx addpkg -pkgdir $WORK/foo20wrapper -pkgpath gno.land/r/foo20wrapper -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test test1 # we call Transfer with foo20, after it's registered -gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 4000000 -broadcast -chainid=tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/registry -func TransferByName -args 'foo20' -args 'g123456789' -args '42' -gas-fee 1000000ugnot -gas-wanted 5000000 -broadcast -chainid=tendermint_test test1 stdout 'same address, success!' -- registry/registry.gno -- diff --git a/gno.land/cmd/gnoland/testdata/prevrealm.txtar b/gno.land/cmd/gnoland/testdata/prevrealm.txtar index 4a7cece6d62..58b0cdce1d6 100644 --- a/gno.land/cmd/gnoland/testdata/prevrealm.txtar +++ b/gno.land/cmd/gnoland/testdata/prevrealm.txtar @@ -34,19 +34,19 @@ env RFOO_ADDR=g1evezrh92xaucffmtgsaa3rvmz5s8kedffsg469 # Test cases ## 1. MsgCall -> myrlm.A: user address -gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/myrlm -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout ${USER_ADDR_test1} ## 2. MsgCall -> myrealm.B -> myrlm.A: user address -gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/myrlm -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout ${USER_ADDR_test1} ## 3. MsgCall -> r/foo.A -> myrlm.A: r/foo -gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/foo -func A -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout ${RFOO_ADDR} ## 4. MsgCall -> r/foo.B -> myrlm.B -> r/foo.A: r/foo -gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 +gnokey maketx call -pkgpath gno.land/r/foo -func B -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 stdout ${RFOO_ADDR} ## remove due to update to maketx call can only call realm (case 5, 6, 13) @@ -59,27 +59,27 @@ stdout ${RFOO_ADDR} ## stdout ${USER_ADDR_test1} ## 7. MsgRun -> myrlm.A: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmA.gno stdout ${USER_ADDR_test1} ## 8. MsgRun -> myrealm.B -> myrlm.A: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/myrlmB.gno stdout ${USER_ADDR_test1} ## 9. MsgRun -> r/foo.A -> myrlm.A: r/foo -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooA.gno stdout ${RFOO_ADDR} ## 10. MsgRun -> r/foo.B -> myrlm.B -> r/foo.A: r/foo -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/fooB.gno stdout ${RFOO_ADDR} ## 11. MsgRun -> p/demo/bar.A: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barA.gno stdout ${USER_ADDR_test1} ## 12. MsgRun -> p/demo/bar.B: user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/barB.gno stdout ${USER_ADDR_test1} ## 13. MsgCall -> std.PrevRealm(): user address @@ -87,7 +87,7 @@ stdout ${USER_ADDR_test1} ## stdout ${USER_ADDR_test1} ## 14. MsgRun -> std.PrevRealm(): user address -gnokey maketx run -gas-fee 100000ugnot -gas-wanted 4000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno +gnokey maketx run -gas-fee 100000ugnot -gas-wanted 5000000 -broadcast -chainid tendermint_test test1 $WORK/run/baz.gno stdout ${USER_ADDR_test1} -- r/myrlm/myrlm.gno -- diff --git a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar b/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar index 71ef6400471..be9a686bac6 100644 --- a/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar +++ b/gno.land/cmd/gnoland/testdata/realm_banker_issued_coin_denom.txtar @@ -12,6 +12,9 @@ gnokey maketx addpkg -pkgdir $WORK/short -pkgpath gno.land/r/test/realm_banker - ## add realm_banker with long package_name gnokey maketx addpkg -pkgdir $WORK/long -pkgpath gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890 -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 +## add invalid realm_denom +gnokey maketx addpkg -pkgdir $WORK/invalid_realm_denom -pkgpath gno.land/r/test/invalid_realm_denom -gas-fee 1000000ugnot -gas-wanted 100000000 -broadcast -chainid=tendermint_test test1 + ## test2 spend all balance gnokey maketx send -send "9999999ugnot" -to g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5 -gas-fee 1ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test2 @@ -52,6 +55,22 @@ gnokey maketx call -pkgpath gno.land/r/test/package89_123456789_123456789_123456 gnokey query bank/balances/g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7 stdout '"100/gno.land/r/test/package89_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_123456789_1234567890:ugnot"' +## mint invalid base denom +! gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "2gnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits' + +## burn invalid base denom +! gnokey maketx call -pkgpath gno.land/r/test/realm_banker -func Burn -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "2gnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits' + +## mint invalid realm denom +! gnokey maketx call -pkgpath gno.land/r/test/invalid_realm_denom -func Mint -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'invalid denom, can only issue/remove coins with the realm.s prefix' + +## burn invalid realm denom +! gnokey maketx call -pkgpath gno.land/r/test/invalid_realm_denom -func Burn -args "g1cq2ecdq3eyn5qa0fzznpurg87zq3k77g63q6u7" -args "ugnot" -args "100" -gas-fee 1000000ugnot -gas-wanted 10000000 -broadcast -chainid=tendermint_test test1 +stderr 'invalid denom, can only issue/remove coins with the realm.s prefix' + -- short/realm_banker.gno -- package realm_banker @@ -61,12 +80,12 @@ import ( func Mint(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) - banker.IssueCoin(addr, denom, amount) + banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) } func Burn(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) - banker.RemoveCoin(addr, denom, amount) + banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) } -- long/realm_banker.gno -- @@ -77,6 +96,23 @@ import ( "std" ) +func Mint(addr std.Address, denom string, amount int64) { + banker := std.GetBanker(std.BankerTypeRealmIssue) + banker.IssueCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) +} + +func Burn(addr std.Address, denom string, amount int64) { + banker := std.GetBanker(std.BankerTypeRealmIssue) + banker.RemoveCoin(addr, std.CurrentRealm().CoinDenom(denom), amount) +} + +-- invalid_realm_denom/realm_banker.gno -- +package invalid_realm_denom + +import ( + "std" +) + func Mint(addr std.Address, denom string, amount int64) { banker := std.GetBanker(std.BankerTypeRealmIssue) banker.IssueCoin(addr, denom, amount) diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go index 0a06eb4756a..945121fbacf 100644 --- a/gno.land/pkg/gnoclient/integration_test.go +++ b/gno.land/pkg/gnoclient/integration_test.go @@ -40,7 +40,7 @@ func TestCallSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -93,7 +93,7 @@ func TestCallMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -155,7 +155,7 @@ func TestSendSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -219,7 +219,7 @@ func TestSendMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -291,7 +291,7 @@ func TestRunSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -452,7 +452,7 @@ func TestAddPackageSingle_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", @@ -537,7 +537,7 @@ func TestAddPackageMultiple_Integration(t *testing.T) { // Make Tx config baseCfg := BaseTxCfg{ GasFee: ugnot.ValueString(10000), - GasWanted: 8000000, + GasWanted: 9000000, AccountNumber: 0, SequenceNumber: 0, Memo: "", diff --git a/gnovm/stdlibs/std/banker.gno b/gnovm/stdlibs/std/banker.gno index 5412b73281c..4c20e8d4b61 100644 --- a/gnovm/stdlibs/std/banker.gno +++ b/gnovm/stdlibs/std/banker.gno @@ -2,6 +2,7 @@ package std import ( "strconv" + "strings" ) // Realm functions can call std.GetBanker(options) to get @@ -126,6 +127,7 @@ func (b banker) IssueCoin(addr Address, denom string, amount int64) { if b.bt != BankerTypeRealmIssue { panic(b.bt.String() + " cannot issue coins") } + assertCoinDenom(denom) bankerIssueCoin(uint8(b.bt), string(addr), denom, amount) } @@ -133,5 +135,35 @@ func (b banker) RemoveCoin(addr Address, denom string, amount int64) { if b.bt != BankerTypeRealmIssue { panic(b.bt.String() + " cannot remove coins") } + assertCoinDenom(denom) bankerRemoveCoin(uint8(b.bt), string(addr), denom, amount) } + +func assertCoinDenom(denom string) { + prefix := "/" + CurrentRealm().PkgPath() + ":" + if !strings.HasPrefix(denom, prefix) { + panic("invalid denom, can only issue/remove coins with the realm's prefix: " + prefix) + } + + baseDenom := denom[len(prefix):] + if !isValidBaseDenom(baseDenom) { + panic("cannot issue coins with invalid denom base name, it should start by a lowercase letter and be followed by 2-15 lowercase letters or digits") + } +} + +// check start by a lowercase letter and be followed by 2-15 lowercase letters or digits +func isValidBaseDenom(denom string) bool { + length := len(denom) + if length < 3 || length > 16 { + return false + } + for i, c := range denom { + switch { + case c >= 'a' && c <= 'z', + i > 0 && (c >= '0' && c <= '9'): // continue + default: + return false + } + } + return true +} diff --git a/gnovm/stdlibs/std/banker.go b/gnovm/stdlibs/std/banker.go index 892af94777f..c57ba8529ed 100644 --- a/gnovm/stdlibs/std/banker.go +++ b/gnovm/stdlibs/std/banker.go @@ -2,7 +2,6 @@ package std import ( "fmt" - "regexp" gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/tm2/pkg/crypto" @@ -33,9 +32,6 @@ const ( btRealmIssue ) -// regexp for denom format -var reDenom = regexp.MustCompile("[a-z][a-z0-9]{2,15}") - func X_bankerGetCoins(m *gno.Machine, bt uint8, addr string) (denoms []string, amounts []int64) { coins := GetContext(m).Banker.GetCoins(crypto.Bech32Address(addr)) return ExpandCoins(coins) @@ -74,31 +70,9 @@ func X_bankerTotalCoin(m *gno.Machine, bt uint8, denom string) int64 { } func X_bankerIssueCoin(m *gno.Machine, bt uint8, addr string, denom string, amount int64) { - // gno checks for bt == RealmIssue - - // check origin denom format - matched := reDenom.MatchString(denom) - if !matched { - m.Panic(typedString("invalid denom format to issue coin, must be " + reDenom.String())) - return - } - - // Similar to ibc spec - // ibc_denom := 'ibc/' + hash('path' + 'base_denom') - // gno_realm_denom := '/' + 'pkg_path' + ':' + 'base_denom' - newDenom := "/" + m.Realm.Path + ":" + denom - GetContext(m).Banker.IssueCoin(crypto.Bech32Address(addr), newDenom, amount) + GetContext(m).Banker.IssueCoin(crypto.Bech32Address(addr), denom, amount) } func X_bankerRemoveCoin(m *gno.Machine, bt uint8, addr string, denom string, amount int64) { - // gno checks for bt == RealmIssue - - matched := reDenom.MatchString(denom) - if !matched { - m.Panic(typedString("invalid denom format to remove coin, must be " + reDenom.String())) - return - } - - newDenom := "/" + m.Realm.Path + ":" + denom - GetContext(m).Banker.RemoveCoin(crypto.Bech32Address(addr), newDenom, amount) + GetContext(m).Banker.RemoveCoin(crypto.Bech32Address(addr), denom, amount) } diff --git a/gnovm/stdlibs/std/frame.gno b/gnovm/stdlibs/std/frame.gno index bc3a000f5a0..1709f8cb8b5 100644 --- a/gnovm/stdlibs/std/frame.gno +++ b/gnovm/stdlibs/std/frame.gno @@ -16,3 +16,15 @@ func (r Realm) PkgPath() string { func (r Realm) IsUser() bool { return r.pkgPath == "" } + +func (r Realm) CoinDenom(coinName string) string { + return CoinDenom(r.pkgPath, coinName) +} + +func CoinDenom(pkgPath, coinName string) string { + // TODO: Possibly remove after https://github.com/gnolang/gno/issues/3164 + // Similar to ibc spec + // ibc_denom := 'ibc/' + hash('path' + 'base_denom') + // gno_qualified_denom := '/' + 'pkg_path' + ':' + 'base_denom' + return "/" + pkgPath + ":" + coinName +} diff --git a/gnovm/tests/files/std5.gno b/gnovm/tests/files/std5.gno index 2baba6b5005..e339d7a6364 100644 --- a/gnovm/tests/files/std5.gno +++ b/gnovm/tests/files/std5.gno @@ -13,7 +13,7 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(2) // std/native.gno:45 diff --git a/gnovm/tests/files/std8.gno b/gnovm/tests/files/std8.gno index 4f749c3a6e1..ee717bf16be 100644 --- a/gnovm/tests/files/std8.gno +++ b/gnovm/tests/files/std8.gno @@ -23,7 +23,7 @@ func main() { // Stacktrace: // panic: frame not found -// callerAt(n) +// callerAt(n) // gonative:std.callerAt // std.GetCallerAt(4) // std/native.gno:45 diff --git a/gnovm/tests/files/zrealm_natbind0.gno b/gnovm/tests/files/zrealm_natbind0.gno index 16a374164d5..8e5f641e734 100644 --- a/gnovm/tests/files/zrealm_natbind0.gno +++ b/gnovm/tests/files/zrealm_natbind0.gno @@ -69,7 +69,7 @@ func main() { // "Closure": { // "@type": "/gno.RefValue", // "Escaped": true, -// "ObjectID": "a7f5397443359ea76c50be82c77f1f893a060925:8" +// "ObjectID": "a7f5397443359ea76c50be82c77f1f893a060925:9" // }, // "FileName": "native.gno", // "IsMethod": false, From c48219a1f3782d318e7753e1df7f1da0c5fd10c6 Mon Sep 17 00:00:00 2001 From: Nemanja Aleksic Date: Fri, 13 Dec 2024 07:12:06 +0100 Subject: [PATCH 20/20] docs: add bad contribution section (#3329) Co-authored-by: Nathan Toups <612924+n2p5@users.noreply.github.com> Co-authored-by: Morgan --- CONTRIBUTING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bc125a6da73..b58d63c6c75 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -469,6 +469,18 @@ Resources for idiomatic Go docs: - [godoc](https://go.dev/blog/godoc) - [Go Doc Comments](https://tip.golang.org/doc/comment) +## Avoding Unhelpful Contributions + +While we welcome all contributions to the Gno project, it's important to ensure that your changes provide meaningful value or improve the quality of the codebase. Contributions that fail to meet these criteria may not be accepted. Examples of unhelpful contributions include (but not limited to): + +- Airdrop farming & karma farming: Making minimal, superficial changes, with the goal of becoming eligible for airdrops and GovDAO participation. +- Incomplete submissions: Changes that lack adequate context, link to a related issue, documentation, or test coverage. + +Before submitting a pull request, ask yourself: +- Does this change solve a specific problem or add clear value? +- Is the implementation aligned with the gno.land's goals and style guide? +- Have I tested my changes and included relevant documentation? + ## Additional Notes ### Issue and Pull Request Labels @@ -502,3 +514,4 @@ automatic label management. | info needed | Issue is lacking information needed for resolving | | investigating | Issue is still being investigated by the team | | question | Issue starts a discussion or raises a question | +