Skip to content

feat(gnodoc): generate docs from contract codebase#1160

Open
notJoon wants to merge 26 commits intomainfrom
doc-renderer
Open

feat(gnodoc): generate docs from contract codebase#1160
notJoon wants to merge 26 commits intomainfrom
doc-renderer

Conversation

@notJoon
Copy link
Copy Markdown
Member

@notJoon notJoon commented Jan 14, 2026

Description

This PR introduces the full gnodoc pipeline to generate Markdown documentation directly from the contract codebase. It provides a local, source-driven alternative to gno doc, allowing documentation to be produced before deployment while preserving pkg.go.dev-style structure. The implementation covers parsing, modeling, rendering, and CLI behaviors so contracts can be documented consistently in development workflows.

Implementation details

  • Added .gno-aware parsing: when a package contains .gno files, the parser falls back to per-file parsing instead of ParseDir, which only handles .go files. This enables documentation generation for contract packages that are Gno-only.
  • Implemented gnomod.toml module resolution: the parser detects module root via gnomod.toml and reads its module field to build ImportPath values used in Markdown. Source position filenames are normalized relative to the module root when available.
  • Extracted examples from test files: examples are collected from *_test.go files and rendered in Markdown. Example output is trimmed for stable presentation.
  • Added return-aware documentation: function/method docs include a Returns block based on named returns and return statement expressions. The renderer deduplicates repeated return expressions and renders nil once at the end.

Markdown Output Example (Simplified)

# pkgname

`import "module/path/pkgname"`

Package overview text.

## Index
- [TypeA](#typea)
- [FuncX](#funcx)

## Constants
```go
const (
	MAX = 1
)
```

## Functions
### FuncX
```go
func FuncX(arg int) (result int, err error)
```
Description...

#### Returns
- named: result, err
- return: result, nil

## Types
### TypeA
```go
type TypeA struct
```
Description...

#### Methods
##### MethodY
```go
func (t *TypeA) MethodY() error
```

#### Returns
- return: nil

Summary by CodeRabbit

  • New Features

    • GnoDoc CLI to generate/export package documentation (export, list, default commands) with flags for output, tests inclusion, filtering, and source links.
    • Markdown renderer producing structured README output with stable anchors.
  • Parser & Model

    • Rich package parser and documentation model capturing funcs, types, values, examples, notes, and indexes.
  • Documentation

    • Comprehensive README describing usage, flags, output examples, and limitations.
  • Tests

    • Extensive unit and integration tests covering CLI, parser, renderer, and model.
  • Chore

    • .gitignore updated, module file and CLI entrypoint added.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 14, 2026

Walkthrough

Adds GnoDoc: a CLI tool that parses Go/Gno sources into a DocPackage model (module-root detection, file discovery with include/exclude/test flags), builds anchors and indexes, and renders structured Markdown; includes a Runner CLI, Parser, Renderer, model types, and tests.

Changes

Cohort / File(s) Summary
Repository & Entry
/.gitignore, gnodoc/go.mod, gnodoc/main.go
Add gnodoc to .gitignore, initialize module, and add main that wires Runner to process CLI args and exit with runner ExitCode.
Top-level Docs
gnodoc/README.md
New project README describing tool behavior, CLI flags, rendering format, examples, and limitations.
CLI: core types & flags
gnodoc/cmd/exitcode.go, gnodoc/cmd/exitcode_test.go, gnodoc/cmd/options.go, gnodoc/cmd/options_test.go
Add ExitCode enum with String(), option structs (Global/Export/List), Default* constructors, and unit tests.
CLI: runner & tests
gnodoc/cmd/runner.go, gnodoc/cmd/runner_test.go
New Runner type with Run and subcommands (default/export/list), flag parsing, generateDoc/listPackages flows, file I/O, exit codes, and comprehensive tests — review for CLI logic, error handling, and I/O side-effects.
Parser: options, implementation & tests
gnodoc/parser/options.go, gnodoc/parser/parser.go, gnodoc/parser/parser_test.go
Add parser Options and a full Parser: module-root detection (gnomod.toml), import/path resolution, file collection (include/exclude/test), tolerant parsing, conversion to model.DocPackage (symbols, examples, deprecations, returns), plus extensive tests — high-complexity logic to review.
Render: anchors, options, markdown & tests
gnodoc/render/anchor.go, gnodoc/render/anchor_test.go, gnodoc/render/options.go, gnodoc/render/options_test.go, gnodoc/render/markdown.go, gnodoc/render/markdown_test.go
Anchor generation/registry, RenderOptions and MarkdownRenderer producing structured sections and anchors; extensive rendering logic and tests — review for anchor uniqueness, link generation, and markdown structure.
Model: core & index
gnodoc/model/doc_node.go, gnodoc/model/doc_node_test.go, gnodoc/model/doc_index.go, gnodoc/model/doc_index_test.go
Add SymbolKind, DocNode base, DocIndexItem and helper functions with tests.
Model: package container
gnodoc/model/doc_package.go, gnodoc/model/doc_package_test.go
DocPackage type, collection fields, filtering helpers, BuildIndex implementation and tests.
Model: functions, params, types
gnodoc/model/doc_func.go, gnodoc/model/doc_func_test.go, gnodoc/model/doc_param.go, gnodoc/model/doc_param_test.go, gnodoc/model/doc_type.go, gnodoc/model/doc_type_test.go
DocFunc (params/results/receiver/signature), DocParam/DocReceiver, DocType and TypeKind with helpers and tests.
Model: values & fields
gnodoc/model/doc_value.go, gnodoc/model/doc_value_test.go, gnodoc/model/doc_field.go, gnodoc/model/doc_field_test.go
DocValueSpec/DocValueGroup with Names/HasExported, DocField with tag parsing (reflect + fallback) and HasTag/TagValue, plus tests.
Model: examples, notes, deprecated
gnodoc/model/doc_extra.go, gnodoc/model/doc_extra_test.go
DocExample, DocNote, DocDeprecated types and helpers (Suffix, HasOutput, IsBug) with tests.
Model: source file & position
gnodoc/model/source_file.go, gnodoc/model/source_file_test.go, gnodoc/model/source_pos.go, gnodoc/model/source_pos_test.go
SourceFile and SourcePos types with BaseName, IsTestFile, IsValid, and String methods, plus tests.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI
    participant Parser
    participant Model as DocPackage
    participant Renderer
    participant FS as FileSystem

    User->>CLI: invoke gnodoc [cmd] [path] [flags]
    CLI->>Parser: ParsePackage(path, options)
    Parser->>Parser: detect module root (gnomod.toml) / resolve import path
    Parser->>Parser: collect files (include/exclude, tests)
    Parser->>Parser: parse files -> build symbols, examples, deprecations
    Parser->>Model: return DocPackage
    CLI->>Renderer: Render(DocPackage)
    Renderer->>Renderer: register anchors, build sections
    Renderer->>CLI: return Markdown
    CLI->>FS: create/write output file(s)
    CLI->>User: exit with ExitCode (Success/PartialError/Error)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.72% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(gnodoc): generate docs from contract codebase' accurately describes the primary feature addition in this PR: a complete documentation generation tool (gnodoc) for creating Markdown docs from Go/Gno packages.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (16)
gnodoc/.gitignore (1)

1-1: Clarify the ignore pattern to explicitly target the binary.

This pattern will ignore anything named gnodoc within the gnodoc/ directory. If the intent is to ignore the compiled binary, consider making it explicit:

Suggested clarification
-gnodoc
+# Compiled binary
+/gnodoc
gnodoc/model/source_file_test.go (1)

44-75: Consider adding .gno test file cases.

Once IsTestFile() is updated to support .gno files (as mentioned in parser options), add corresponding test cases:

Suggested additional test cases
{
	name:     "gno regular file",
	file:     SourceFile{Name: "foo.gno"},
	expected: false,
},
{
	name:     "gno test file",
	file:     SourceFile{Name: "foo_test.gno"},
	expected: true,
},
gnodoc/cmd/options_test.go (1)

22-34: Consider testing embedded GlobalOptions defaults.

The test validates ExportOptions-specific fields but doesn't verify that embedded GlobalOptions fields are also set correctly (e.g., opts.ExportedOnly, opts.IncludeTests). This could catch regressions if the embedded defaults change.

♻️ Suggested addition
 func TestExportOptions_Default(t *testing.T) {
 	opts := DefaultExportOptions()
 
+	// Verify embedded GlobalOptions defaults
+	if !opts.ExportedOnly {
+		t.Error("ExportedOnly should be true by default")
+	}
+	if opts.IncludeTests {
+		t.Error("IncludeTests should be false by default")
+	}
+
 	if opts.Format != "md" {
 		t.Errorf("Format should be 'md', got %q", opts.Format)
 	}
gnodoc/model/doc_func_test.go (1)

90-159: Good coverage; consider additional edge cases for completeness.

The FullSignature tests cover the common cases well. For comprehensive coverage, consider adding tests for:

  • Named return values (e.g., func Foo() (n int, err error))
  • Variadic parameters (e.g., func Printf(format string, args ...interface{}))
  • Pointer receiver formatting (e.g., func (f *Foo) Method())

These can be addressed in a follow-up if not critical for the initial implementation.

gnodoc/render/options.go (1)

40-45: Consider nil receiver safety.

If ShouldRender is called on a nil *RenderOptions, it will panic. Consider adding a nil check or documenting that callers must ensure a non-nil receiver.

♻️ Optional: Add nil receiver guard
 func (o *RenderOptions) ShouldRender(exported bool) bool {
+	if o == nil {
+		return exported // default to exported-only behavior
+	}
 	if !o.ExportedOnly {
 		return true
 	}
 	return exported
 }
gnodoc/README.md (1)

13-19: Minor markdown formatting inconsistencies flagged by linter.

The tables at lines 14, 22, and 31 should be surrounded by blank lines per MD058. Additionally, line 64 uses a tilde code fence while the rest of the document uses backticks (MD048).

📝 Suggested fix for table spacing
 ### Output
+
 | Flag | Default | Description |
 | --- | --- | --- |
 | `--out=DIR` | current directory | Output directory for generated Markdown. |
 | `--output-file=NAME` | `README.md` | Output filename for the default command. |
 | `--filename=NAME` | `README.md` | Output filename for `export`. |
 | `--format=md` | `md` | Output format for `export` (Markdown only). |
+
 ### Parsing and Filtering

Apply similar spacing around the other tables and use backticks instead of tildes at line 64.

gnodoc/model/doc_package_test.go (1)

137-174: Good test coverage for BuildIndex, consider adding Vars.

The test validates that Consts, Funcs, and Types are properly indexed. You might want to add a Vars group to ensure parity with Consts handling, though this may already be covered if they share the same code path.

gnodoc/model/doc_type.go (1)

47-67: Consider adding ExportedConstructors() for API consistency.

ExportedFields() and ExportedMethods() are provided, but there's no corresponding ExportedConstructors() method. If the renderer needs to filter constructors by export status (consistent with other filtering patterns), this method would be useful.

Optional addition
// ExportedConstructors returns only the exported constructors.
func (t DocType) ExportedConstructors() []DocFunc {
	var result []DocFunc
	for _, c := range t.Constructors {
		if c.IsExported() {
			result = append(result, c)
		}
	}
	return result
}
gnodoc/model/doc_package.go (1)

61-97: Consider pre-allocating the Index slice for better performance.

The BuildIndex method correctly uses a pointer receiver since it mutates the struct. However, when appending to p.Index in a loop, you could estimate capacity upfront to reduce allocations.

♻️ Optional: Pre-allocate Index slice
 func (p *DocPackage) BuildIndex() {
-	p.Index = nil
+	// Estimate capacity to reduce allocations
+	capacity := 0
+	for _, group := range p.Consts {
+		capacity += len(group.Specs)
+	}
+	for _, group := range p.Vars {
+		capacity += len(group.Specs)
+	}
+	capacity += len(p.Funcs) + len(p.Types)
+	p.Index = make([]DocIndexItem, 0, capacity)
 
 	// Add constants
 	for _, group := range p.Consts {
gnodoc/parser/parser_test.go (1)

134-137: Consider renaming TestMain to avoid shadowing the test framework's TestMain.

The function name TestMain in the test file content could be confusing since TestMain has special meaning in Go's testing framework. While this is just string content for the parser test, consider using a different name like TestSomething for clarity.

gnodoc/parser/parser.go (2)

188-203: Consider adding a timeout to the go list command.

The exec.Command("go", "list", ...) call has no timeout, which could cause the CLI to hang indefinitely if Go tooling is misconfigured or the network is slow (for module downloads).

♻️ Add context with timeout
+import (
+	"context"
+	"time"
+)
+
 // resolveImportPath resolves a Go import path to a directory path.
 func (p *Parser) resolveImportPath(importPath string) (string, error) {
 	// Use go list to resolve the import path
-	cmd := exec.Command("go", "list", "-f", "{{.Dir}}", importPath)
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+	cmd := exec.CommandContext(ctx, "go", "list", "-f", "{{.Dir}}", importPath)
 	output, err := cmd.Output()
 	if err != nil {
+		if ctx.Err() == context.DeadlineExceeded {
+			return "", fmt.Errorf("timeout resolving import path %q", importPath)
+		}
 		return "", fmt.Errorf("cannot resolve import path %q: %w", importPath, err)
 	}

814-859: Consider extracting duplicated return info extraction logic.

The AST inspection logic in collectReturnInfo (lines 831-852) and fallbackReturnInfo (lines 892-913) is nearly identical. This could be extracted into a shared helper function.

♻️ Extract shared helper
func (p *Parser) extractReturnInfoFromBody(body *ast.BlockStmt) returnInfo {
	var info returnInfo
	ast.Inspect(body, func(n ast.Node) bool {
		ret, ok := n.(*ast.ReturnStmt)
		if !ok {
			return true
		}
		if len(ret.Results) == 0 {
			info.naked = true
			return true
		}
		var exprs []string
		for _, expr := range ret.Results {
			if ident, ok := expr.(*ast.Ident); ok {
				exprs = append(exprs, ident.Name)
				continue
			}
			exprs = append(exprs, p.exprString(expr))
		}
		if len(exprs) > 0 {
			info.exprs = append(info.exprs, strings.Join(exprs, ", "))
		}
		return true
	})
	return info
}

Also applies to: 861-918

gnodoc/cmd/runner.go (2)

108-113: Simplify the --all / --exported-only flag handling.

The current logic is slightly redundant. Since exportedOnly defaults to true via flag definition, the else if branch will always set it to true. The logic can be simplified.

♻️ Simplified flag handling
-	// --all overrides --exported-only
-	if all {
-		opts.ExportedOnly = false
-	} else if exportedOnly {
-		opts.ExportedOnly = true
-	}
+	// --all overrides --exported-only
+	opts.ExportedOnly = !all

279-323: Silent error handling in filepath.Walk may hide issues.

The walk callback returns nil for all errors (lines 281 and 291), silently skipping directories that can't be accessed. Consider logging these errors to stderr when they occur, so users are aware of skipped paths.

♻️ Log skipped directories
 	err = filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
 		if err != nil {
+			fmt.Fprintf(r.stderr, "warning: skipping %s: %v\n", path, err)
 			return nil // Skip errors
 		}
 
 		if !info.IsDir() {
 			return nil
 		}
 
 		// Check for Go/Gno files
 		entries, err := os.ReadDir(path)
 		if err != nil {
+			fmt.Fprintf(r.stderr, "warning: cannot read %s: %v\n", path, err)
 			return nil
 		}
gnodoc/render/markdown.go (2)

250-326: Consider extracting shared logic between RenderConstants and RenderVariables.

Both methods share nearly identical structure: checking for exported specs, rendering group docs, anchors, code blocks, and source links. A helper accepting the declaration keyword (const/var) and a value formatter could reduce duplication.

This is optional since both implementations are correct.


429-432: Consider extracting the magic number or documenting the baseline count.

The check len(fieldLines) > 2 relies on the header and empty line being exactly 2 items. Same pattern appears at lines 463 and 482. A named constant or comment would clarify intent and reduce fragility if the header format changes.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ca7f4c and 4f3bd34.

📒 Files selected for processing (41)
  • gnodoc/.gitignore
  • gnodoc/README.md
  • gnodoc/cmd/exitcode.go
  • gnodoc/cmd/exitcode_test.go
  • gnodoc/cmd/options.go
  • gnodoc/cmd/options_test.go
  • gnodoc/cmd/runner.go
  • gnodoc/cmd/runner_test.go
  • gnodoc/go.mod
  • gnodoc/main.go
  • gnodoc/model/doc_extra.go
  • gnodoc/model/doc_extra_test.go
  • gnodoc/model/doc_field.go
  • gnodoc/model/doc_field_test.go
  • gnodoc/model/doc_func.go
  • gnodoc/model/doc_func_test.go
  • gnodoc/model/doc_index.go
  • gnodoc/model/doc_index_test.go
  • gnodoc/model/doc_node.go
  • gnodoc/model/doc_node_test.go
  • gnodoc/model/doc_package.go
  • gnodoc/model/doc_package_test.go
  • gnodoc/model/doc_param.go
  • gnodoc/model/doc_param_test.go
  • gnodoc/model/doc_type.go
  • gnodoc/model/doc_type_test.go
  • gnodoc/model/doc_value.go
  • gnodoc/model/doc_value_test.go
  • gnodoc/model/source_file.go
  • gnodoc/model/source_file_test.go
  • gnodoc/model/source_pos.go
  • gnodoc/model/source_pos_test.go
  • gnodoc/parser/options.go
  • gnodoc/parser/parser.go
  • gnodoc/parser/parser_test.go
  • gnodoc/render/anchor.go
  • gnodoc/render/anchor_test.go
  • gnodoc/render/markdown.go
  • gnodoc/render/markdown_test.go
  • gnodoc/render/options.go
  • gnodoc/render/options_test.go
🧰 Additional context used
🧬 Code graph analysis (20)
gnodoc/cmd/exitcode_test.go (1)
gnodoc/cmd/exitcode.go (4)
  • ExitSuccess (8-8)
  • ExitError (11-11)
  • ExitPartialError (15-15)
  • ExitCode (4-4)
gnodoc/cmd/runner_test.go (2)
gnodoc/cmd/runner.go (1)
  • NewRunner (48-53)
gnodoc/cmd/exitcode.go (3)
  • ExitSuccess (8-8)
  • ExitError (11-11)
  • ExitPartialError (15-15)
gnodoc/model/doc_type.go (3)
gnodoc/model/doc_node.go (1)
  • DocNode (24-34)
gnodoc/model/doc_field.go (1)
  • DocField (9-13)
gnodoc/model/doc_func.go (1)
  • DocFunc (6-17)
gnodoc/model/doc_value.go (1)
gnodoc/model/doc_node.go (1)
  • DocNode (24-34)
gnodoc/model/doc_extra.go (1)
gnodoc/model/source_pos.go (1)
  • SourcePos (7-11)
gnodoc/model/doc_index_test.go (2)
gnodoc/model/doc_index.go (2)
  • DocIndexItem (4-9)
  • NewIndexFromNode (17-24)
gnodoc/model/doc_node.go (2)
  • KindFunc (11-11)
  • DocNode (24-34)
gnodoc/cmd/options_test.go (1)
gnodoc/cmd/options.go (3)
  • DefaultGlobalOptions (28-35)
  • DefaultExportOptions (52-59)
  • DefaultListOptions (71-76)
gnodoc/model/doc_param_test.go (1)
gnodoc/model/doc_param.go (2)
  • DocParam (6-9)
  • DocReceiver (21-24)
gnodoc/parser/parser_test.go (2)
gnodoc/parser/parser.go (1)
  • New (32-47)
gnodoc/parser/options.go (1)
  • DefaultOptions (22-30)
gnodoc/main.go (1)
gnodoc/cmd/runner.go (1)
  • NewRunner (48-53)
gnodoc/model/doc_index.go (1)
gnodoc/model/doc_node.go (2)
  • SymbolKind (6-6)
  • DocNode (24-34)
gnodoc/model/source_pos_test.go (1)
gnodoc/model/source_pos.go (1)
  • SourcePos (7-11)
gnodoc/cmd/runner.go (5)
gnodoc/cmd/exitcode.go (4)
  • ExitCode (4-4)
  • ExitError (11-11)
  • ExitSuccess (8-8)
  • ExitPartialError (15-15)
gnodoc/cmd/options.go (5)
  • DefaultGlobalOptions (28-35)
  • DefaultExportOptions (52-59)
  • GlobalOptions (4-25)
  • DefaultListOptions (71-76)
  • ListOptions (62-68)
gnodoc/parser/options.go (2)
  • Options (4-19)
  • DefaultOptions (22-30)
gnodoc/parser/parser.go (1)
  • New (32-47)
gnodoc/render/markdown.go (1)
  • NewMarkdownRenderer (19-27)
gnodoc/model/doc_node_test.go (1)
gnodoc/model/doc_node.go (8)
  • SymbolKind (6-6)
  • KindConst (9-9)
  • KindVar (10-10)
  • KindFunc (11-11)
  • KindType (12-12)
  • KindMethod (13-13)
  • KindField (14-14)
  • DocNode (24-34)
gnodoc/render/markdown.go (8)
gnodoc/render/options.go (2)
  • RenderOptions (4-25)
  • DefaultOptions (28-36)
gnodoc/render/anchor.go (4)
  • AnchorRegistry (36-40)
  • NewAnchorRegistry (43-49)
  • ToAnchor (12-25)
  • MethodAnchor (29-33)
gnodoc/model/doc_package.go (1)
  • DocPackage (7-22)
gnodoc/model/doc_node.go (2)
  • SymbolKind (6-6)
  • KindMethod (13-13)
gnodoc/model/source_pos.go (1)
  • SourcePos (7-11)
gnodoc/model/doc_type.go (1)
  • TypeKind (4-4)
gnodoc/model/doc_extra.go (1)
  • DocNote (35-39)
gnodoc/model/doc_func.go (1)
  • DocFunc (6-17)
gnodoc/model/source_file_test.go (1)
gnodoc/model/source_file.go (1)
  • SourceFile (7-10)
gnodoc/model/doc_type_test.go (4)
gnodoc/model/doc_type.go (6)
  • TypeKind (4-4)
  • TypeKindStruct (7-7)
  • TypeKindInterface (8-8)
  • TypeKindAlias (9-9)
  • TypeKindOther (10-10)
  • DocType (19-25)
gnodoc/model/doc_func.go (1)
  • DocFunc (6-17)
gnodoc/model/doc_node.go (1)
  • DocNode (24-34)
gnodoc/model/doc_field.go (1)
  • DocField (9-13)
gnodoc/model/doc_node.go (2)
gnodoc/model/source_pos.go (1)
  • SourcePos (7-11)
gnodoc/model/doc_extra.go (1)
  • DocDeprecated (47-50)
gnodoc/model/doc_extra_test.go (2)
gnodoc/model/doc_extra.go (3)
  • DocExample (6-12)
  • DocNote (35-39)
  • DocDeprecated (47-50)
gnodoc/model/source_pos.go (1)
  • SourcePos (7-11)
gnodoc/render/options_test.go (1)
gnodoc/render/options.go (2)
  • DefaultOptions (28-36)
  • RenderOptions (4-25)
🪛 markdownlint-cli2 (0.18.1)
gnodoc/README.md

14-14: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


22-22: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


31-31: Tables should be surrounded by blank lines

(MD058, blanks-around-tables)


64-64: Code fence style
Expected: backtick; Actual: tilde

(MD048, code-fence-style)


78-78: Hard tabs
Column: 1

(MD010, no-hard-tabs)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: test-gnoswap (gnoswap/scenario/gov/staker, contract/r/scenario/gov/staker)
  • GitHub Check: test-gnoswap (gnoswap/scenario/router, contract/r/scenario/router)
  • GitHub Check: test-gnoswap (gnoswap/staker, contract/r/gnoswap/staker)
  • GitHub Check: test-gnoswap (gnoswap/scenario/pool, contract/r/scenario/pool)
  • GitHub Check: test-gnoswap (gnoswap/scenario/staker, contract/r/scenario/staker)
  • GitHub Check: test-gnoswap (gnoswap/gov/governance/v1, contract/r/gnoswap/gov/governance/v1)
  • GitHub Check: test-gnoswap (gnoswap/test/fuzz, contract/r/gnoswap/test/fuzz)
  • GitHub Check: test-gnoswap (gnoswap/pool/v1, contract/r/gnoswap/pool/v1)
  • GitHub Check: test-gnoswap (gnoswap/staker/v1, contract/r/gnoswap/staker/v1)
  • GitHub Check: run-integration-test
🔇 Additional comments (100)
gnodoc/model/source_file_test.go (1)

16-42: Test summary mentions fallback behavior that doesn't exist.

The AI summary states BaseName() "uses the basename of Path" as fallback, but the implementation simply returns Name. Consider adding a test case for when Name is empty to document expected behavior, or update BaseName() to fall back to extracting basename from Path.

gnodoc/parser/options.go (1)

1-30: LGTM!

The Options struct is well-designed with clear documentation for each field. Default values are appropriate for typical documentation generation (exported symbols only, no tests by default).

Note: The IncludeTests comment correctly mentions *_test.gno files, which aligns with my earlier comment on model/source_file.go needing to handle .gno test files.

gnodoc/go.mod (1)

3-3: No action needed. Go version 1.25.5 is valid and is the latest stable release as of January 2026.

Likely an incorrect or invalid review comment.

gnodoc/cmd/exitcode.go (1)

1-30: LGTM!

Clean implementation of exit code constants with appropriate documentation. The String() method correctly handles all defined cases plus an unknown fallback for defensive handling of unexpected values.

gnodoc/cmd/exitcode_test.go (1)

1-37: LGTM!

Comprehensive test coverage for exit code values and their string representations. Good use of table-driven tests and inclusion of the unknown code edge case.

gnodoc/render/anchor.go (4)

64-71: RegisterName overwrites previous mapping on duplicate names.

When the same name is registered twice, RegisterName overwrites the entry in r.names with the new unique anchor. This means Get(name) will return the last registered anchor, not the first. If the intent is to always retrieve the first anchor for a given name, this is a bug.

For example:

reg.RegisterName("Foo")  // returns "foo", stores names["Foo"] = "foo"
reg.RegisterName("Foo")  // returns "foo-1", overwrites names["Foo"] = "foo-1"
reg.Get("Foo")           // returns "foo-1", not "foo"

If this is intentional behavior, consider documenting it. If the first anchor should be preserved, add a check:

Suggested fix to preserve first registration
 func (r *AnchorRegistry) RegisterName(name string) string {
+	if existing, ok := r.names[name]; ok {
+		return existing
+	}
 	anchor := ToAnchor(name)
 	unique := r.Register(anchor)
 	r.names[name] = unique
 	return unique
 }

9-33: LGTM!

ToAnchor and MethodAnchor are well-implemented. The anchor conversion correctly handles the documented character classes, and the method anchor properly strips the pointer prefix before generating the combined anchor.


35-62: LGTM!

The AnchorRegistry struct and Register method correctly implement collision avoidance with numeric suffixes. The count tracking ensures unique anchors across repeated registrations.


73-94: LGTM!

RegisterKey provides correct idempotent behavior by returning existing anchors for already-registered keys. GetKey and Get are straightforward lookups.

gnodoc/render/anchor_test.go (4)

1-56: LGTM!

Comprehensive table-driven tests for ToAnchor covering lowercase conversion, underscore preservation, space-to-dash conversion, special character removal, and digit preservation.


58-84: LGTM!

Good test coverage for Register collision handling, verifying the suffix increments correctly (foo, foo-1, foo-2) and that different anchors remain unsuffixed.


86-118: Tests align with current RegisterName overwrite behavior.

Note that TestAnchorRegistry_RegisterName (lines 95-99) verifies that registering the same name twice returns "newfoo-1" for the second call. This test would need updating if RegisterName is changed to return the existing anchor for duplicate names (as mentioned in the anchor.go review).


120-150: LGTM!

Good coverage of MethodAnchor including the pointer receiver case where the * prefix is stripped.

gnodoc/model/doc_node_test.go (5)

1-26: LGTM!

Comprehensive test coverage for SymbolKind.String() verifying all six defined kinds.


28-40: LGTM!

Good defensive test verifying the zero-value behavior of DocNode struct fields.


42-73: LGTM!

Tests correctly verify that IsExported() returns the value of the Exported field, including the case where the name starts with uppercase but Exported is explicitly false.


75-106: LGTM!

Good coverage including the important edge case of whitespace-only documentation being treated as no doc.


108-139: LGTM!

Tests verify that AnchorID() returns the node name for different symbol kinds.

gnodoc/main.go (1)

9-12: LGTM!

Clean and minimal entry point. The exit code propagation from Runner.Run to os.Exit is correctly implemented.

gnodoc/model/doc_type_test.go (4)

5-24: LGTM!

Good table-driven test structure for TypeKind.String().


26-44: LGTM!

Zero-value test ensures clean struct initialization defaults.


46-164: LGTM!

Table-driven tests for IsStruct, IsInterface, HasMethods, and HasConstructors are well-structured and cover the relevant cases.


166-206: LGTM!

ExportedFields and ExportedMethods tests correctly verify both filtering by the Exported flag and preservation of order.

gnodoc/render/options_test.go (1)

31-67: LGTM!

Good table-driven test covering the key ShouldRender visibility combinations.

gnodoc/model/doc_param_test.go (2)

5-47: LGTM!

Comprehensive tests for DocParam zero value and String() method covering named, unnamed, and pointer type cases.


49-119: LGTM!

Good coverage of DocReceiver behavior including String() formatting for value/pointer/unnamed receivers and IsPointer() detection.

gnodoc/model/source_pos.go (1)

5-31: LGTM!

Clean implementation of SourcePos following Go conventions (similar to token.Position). The IsValid() and String() methods are well-documented and handle edge cases appropriately.

gnodoc/model/doc_field_test.go (4)

5-17: LGTM!

Zero-value test correctly validates default field values for DocField.


19-40: LGTM!

Properly tests field initialization with DocNode embedding and validates Name, Type, and Tag assignments.


42-68: LGTM!

Table-driven test for HasTag() is idiomatic and covers the essential cases.


70-111: LGTM!

Comprehensive test coverage for TagValue() including edge cases for multiple tags and missing keys.

gnodoc/model/source_pos_test.go (3)

5-17: LGTM!

Zero-value test correctly validates default field values for SourcePos.


19-60: LGTM!

Comprehensive table-driven tests for IsValid() covering all edge cases including the optional column behavior.


62-93: LGTM!

Good coverage for String() formatting including the special cases of omitted column and invalid position returning "-".

gnodoc/cmd/options_test.go (2)

5-20: LGTM!

Test correctly validates all default values for GlobalOptions.


36-45: LGTM!

Test correctly validates ListOptions defaults.

gnodoc/model/doc_index.go (3)

4-9: LGTM!

Clean struct definition with appropriate fields for documentation index items.


11-14: LGTM!

Simple and correct anchor link generation.


16-24: LGTM!

Clean factory function that correctly maps DocNode fields to DocIndexItem, using AnchorID() for consistent anchor generation.

gnodoc/model/doc_index_test.go (4)

5-20: LGTM!

Zero-value test correctly validates all default field values for DocIndexItem.


22-42: LGTM!

Test correctly validates field assignments using the KindFunc constant.


44-70: LGTM!

Table-driven test covers AnchorLink() including the edge case of an empty anchor.


72-93: LGTM!

Test validates that NewIndexFromNode correctly maps DocNode fields to DocIndexItem, including the implicit AnchorID() call for the Anchor field.

gnodoc/model/doc_value_test.go (1)

1-148: LGTM!

The test file provides solid coverage for DocValueSpec and DocValueGroup types:

  • Zero-value behavior is verified for both types
  • Field assignments and accessors are tested
  • Table-driven tests for HasExported() cover empty, all-unexported, and mixed-exported scenarios
  • Names() method is verified for correct ordering
gnodoc/model/doc_extra_test.go (1)

1-180: LGTM!

Comprehensive test coverage for the documentation extras:

  • All three types (DocExample, DocNote, DocDeprecated) have zero-value and populated-value tests
  • Helper methods (HasOutput, Suffix, IsBug) are tested with table-driven tests covering relevant cases
  • SourcePos.IsValid() is appropriately tested for populated structs
gnodoc/model/doc_param.go (1)

1-38: LGTM!

Clean implementation of DocParam and DocReceiver with well-documented methods:

  • String() methods correctly handle both named and unnamed cases
  • IsPointer() uses strings.HasPrefix appropriately for pointer detection
  • Documentation comments follow Go conventions and clearly explain formatting
gnodoc/render/options.go (1)

1-36: LGTM!

Well-structured configuration type with:

  • Clear field documentation explaining each option's purpose
  • Sensible defaults in DefaultOptions() (exported-only, all sections included)
  • Good example for SourceLinkBase usage
gnodoc/README.md (1)

1-114: Documentation is comprehensive and well-structured.

The README provides clear explanations of module resolution, file discovery, CLI flags, usage examples, and the Markdown output structure. This serves as an excellent reference for users of the gnodoc tool.

gnodoc/model/doc_extra.go (2)

19-31: Suffix() logic is correct and handles edge cases well.

The method properly handles:

  • Non-Example names (returns original)
  • "Example" → ""
  • "Example_Foo" → "Foo"
  • "ExampleFoo" → "Foo"

33-50: DocNote and DocDeprecated types are well-designed.

Clean, minimal structures with appropriate helper methods. The IsBug() helper aligns with Go's convention for documentation notes.

gnodoc/model/doc_field.go (1)

20-49: TagValue implementation is reasonable with best-effort fallback.

The primary path using reflect.StructTag.Get() is correct. The fallback manual parsing handles cases where the tag format might not match reflect.StructTag expectations (e.g., tags stored without surrounding backticks).

Note that the fallback's space-split approach at line 35 may not handle quoted values containing spaces correctly, but this is acceptable for a documentation tool where perfect tag parsing isn't critical.

gnodoc/model/doc_value.go (1)

17-34: Clean and efficient implementations.

Names() correctly pre-allocates the slice, and HasExported() uses early return for efficiency. Both methods leverage the embedded DocNode appropriately.

gnodoc/model/doc_package_test.go (1)

1-234: Comprehensive test suite with good coverage.

Tests cover zero-value behavior, populated state, doc presence checks, export filtering, index building, and examples/notes presence. Table-driven tests are used appropriately for boolean method checks. The test structure follows Go conventions well.

gnodoc/model/doc_func.go (3)

1-17: Well-structured data model for function documentation.

The DocFunc struct cleanly separates signature metadata (Params, Results, Receiver) from flow analysis fields (ReturnNames, ReturnExprs, HasNakedReturn). The embedding of DocNode provides consistent base metadata across all symbol types.


19-31: LGTM!

The IsMethod() and ReceiverType() methods are simple, correct, and nil-safe.


33-74: Solid signature construction logic.

The FullSignature() method correctly handles:

  • Optional receiver prefix
  • Parameter list formatting
  • Single unnamed result without parentheses (Go convention)
  • Multiple/named results with parentheses

The use of strings.Builder is appropriate for this string assembly pattern.

gnodoc/render/markdown_test.go (7)

1-9: Good test file structure.

Clean imports and package declaration. The test suite exercises the public API of MarkdownRenderer without relying on implementation details.


10-84: Comprehensive coverage for Overview and Index rendering.

Tests validate presence of package name, import path, documentation content, and index links. The assertions correctly check for expected Markdown structure.


86-153: Good coverage for Constants and Variables sections.

Tests verify section headers and content rendering with appropriate model fixtures.


155-254: Thorough function and type rendering tests.

The TestMarkdownRenderer_Functions and TestMarkdownRenderer_Types tests validate signature rendering, documentation inclusion, and method handling.


256-322: Examples and Notes tests are well-structured.

Tests validate code block formatting, output sections, and note/deprecation rendering.


324-348: ExportedOnly filtering test is correct.

Validates that ExportedOnly option properly filters unexported symbols from the output.


350-450: FullRender and EmptyPackage tests provide good integration coverage.

The section ordering validation (Overview → Index → Constants) and empty package handling ensure the renderer behaves correctly in edge cases.

gnodoc/cmd/runner_test.go (7)

11-40: Well-structured test helper.

The createTestPackage helper properly uses t.Helper() and creates isolated test fixtures.


73-98: Good pattern for output directory isolation.

This test correctly uses a separate temporary directory for output, ensuring clean isolation.


100-121: Error path tests are appropriate.

Tests for invalid paths and missing arguments correctly expect ExitError.


123-185: Subcommand tests (export, list, help) are well-structured.

Tests validate the export subcommand with custom filename, list output, and help behavior.


187-270: ExportedOnly flag test is thorough.

Validates that exported symbols appear and unexported symbols are excluded when using --exported-only.


272-336: Partial error handling test is valuable.

Tests the --ignore-parse-errors behavior with a package containing a syntax error, validating that valid files are still documented with ExitPartialError.


338-453: --all and --exclude flag tests provide good coverage.

These tests validate that --all includes unexported symbols and --exclude properly filters files by pattern.

gnodoc/model/doc_node.go (3)

1-20: Clean enum definition with appropriate constants.

The SymbolKind type and its constants provide clear categorization for documented symbols. The String() method enables easy debugging and logging.


22-45: Well-designed base struct for documentation metadata.

DocNode provides a consistent foundation for all symbol types. The HasDoc() method correctly trims whitespace before checking for empty documentation.


47-51: Collision handling is already implemented at the renderer level.

The renderer doesn't use DocNode.AnchorID() for generating anchor links. Instead, it calls symbolAnchor(kind, name) (lines 78–86 in gnodoc/render/markdown.go), which creates logical keys that include both the SymbolKind and name: fmt.Sprintf("%s:%s", kind, name). The AnchorRegistry uses these keys to prevent collisions across different symbol kinds (e.g., type:Foo and func:Foo are distinct entries). DocNode.AnchorID() is either unused or only referenced in NewIndexFromNode(), which itself doesn't appear to be the source of truth for anchor generation in the actual renderer.

Likely an incorrect or invalid review comment.

gnodoc/model/doc_type.go (2)

1-16: Clean type kind enumeration.

The TypeKind constants cover the essential Go type categories. The "other" category provides a sensible fallback for type aliases to non-struct/interface types.


18-45: Well-structured type documentation model.

The DocType struct cleanly organizes fields, methods, and constructors. The predicate methods are simple and correct.

gnodoc/model/doc_package.go (2)

1-27: LGTM! Well-structured package documentation model.

The DocPackage struct provides a comprehensive container for package documentation with appropriate fields for all documentation elements. The use of a value receiver for HasDoc() is correct for a read-only method.


29-59: LGTM! Clean filtering and predicate methods.

The ExportedFuncs, ExportedTypes, HasExamples, and HasNotes methods are implemented correctly with value receivers since they don't mutate state.

gnodoc/cmd/options.go (2)

1-35: LGTM! Clean option structures with sensible defaults.

The GlobalOptions struct and its constructor provide clear, well-documented configuration options with appropriate defaults.


61-76: LGTM! Simple list options.

The ListOptions struct is appropriately minimal for its purpose.

gnodoc/parser/parser_test.go (3)

10-113: LGTM! Comprehensive basic parsing test.

The test thoroughly validates package parsing, including constants, variables, types with methods and constructors. Good use of temp directories for isolation.


737-830: LGTM! Good module root and gnomod.toml detection tests.

These tests properly validate relative path handling and automatic module root detection from gnomod.toml. The test setup is clean and assertions are appropriate.


678-734: Syntax is intentionally supported by the parser for multi-name value assignments.

The test content const A, B = 1 and var X, Y = 2 is valid for this Gno parser. The parser has explicit logic (if len(vs.Values) == 1) that assigns a single value to multiple names. The test name "TestParser_ValueSpec_MultipleNamesSingleValue" confirms this is a deliberate feature, and the parser correctly assigns the value "1" to both A and B, and "2" to both X and Y. No fix is required.

Likely an incorrect or invalid review comment.

gnodoc/parser/parser.go (3)

1-47: LGTM! Clean parser initialization.

The Parser struct and New constructor are well-structured with appropriate nil handling for options.


920-951: LGTM! Clean example conversion with proper normalization.

The convertExample function handles both Code and Play nodes appropriately, with formatting and normalization.


224-258: The readGnoModModule function adequately handles the actual gnomod.toml format used in practice. All files in the codebase use the simple module = "value" format at the root level, with no multi-line strings, escaped quotes, or other complex TOML features in the module field itself. The function's early return upon finding the "module" key means it never encounters the bracketed sections ([replace], [addpkg]) that do exist in the TOML spec. No refactoring is required.

gnodoc/cmd/runner.go (3)

1-53: LGTM! Clean runner structure with dependency injection.

The Runner struct with configurable stdout/stderr writers enables testability and flexibility.


55-80: LGTM! Clear command dispatch logic.

The Run method handles help flags and subcommand routing cleanly.


212-259: LGTM! Well-structured documentation generation flow.

The generateDoc method correctly:

  1. Creates parser with appropriate options
  2. Handles parse errors
  3. Ensures output directory exists
  4. Writes the output file
  5. Reports partial errors via ExitPartialError
gnodoc/render/markdown.go (12)

1-9: LGTM!

Clean imports with minimal dependencies. Standard library usage is appropriate for string manipulation and formatting.


11-27: LGTM!

Good defensive handling of nil options with fallback to defaults. The struct design cleanly separates configuration, state (anchors), and output accumulation (sections).


29-72: LGTM!

The render orchestration is clean with proper section ordering and conditional inclusion. Resetting anchors on each call ensures the renderer is reusable across multiple packages.


74-93: LGTM!

Helper methods are well-factored. The sourceLink correctly validates both the base URL and position validity before generating links.


95-114: LGTM!

Clean overview rendering with proper handling of optional import path and documentation.


116-166: LGTM!

Index generation properly filters by export status and generates consistent anchor links. The structure cleanly collects all symbol types.


168-248: LGTM!

The two-pass approach efficiently checks for exported content before rendering. Group documentation and source links are properly handled.


328-377: LGTM!

Clean function rendering with proper signature display and returns documentation. The horizontal rule separators improve readability between function entries.


502-547: LGTM!

Examples are rendered cleanly with proper handling of optional documentation and output sections. Using ex.Suffix() for display names is a nice touch.


549-595: LGTM!

Notes are properly grouped and sorted for deterministic output. The blockquote formatting with bold labels provides clear visual distinction.


597-638: LGTM!

Return rendering properly handles all cases: named returns, naked returns, and expressions. The deduplication with nil-at-end ordering matches the PR objectives for stable presentation.


398-402: The code is functioning correctly. The parser always populates Signature alongside TypeKind in all type processing paths (struct, interface, and alias cases at lines 586, 594, and 597 in gnodoc/parser/parser.go). The fallback in markdown.go would produce identical output: "type Name struct" for structs, "type Name interface" for interfaces. These documentation-style signatures intentionally omit details like braces and are consistent with Go's documentation conventions.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread gnodoc/cmd/options.go
Comment on lines +37 to +59
// ExportOptions contains options for the export command.
type ExportOptions struct {
GlobalOptions

// Format is the output format (md).
Format string

// OutputDir is the output directory.
OutputDir string

// Filename is the output filename.
Filename string
}

// DefaultExportOptions returns the default export options.
func DefaultExportOptions() *ExportOptions {
return &ExportOptions{
GlobalOptions: *DefaultGlobalOptions(),
Format: "md",
OutputDir: ".",
Filename: "README.md",
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential confusion: ExportOptions has both OutputFile (embedded) and Filename.

ExportOptions embeds GlobalOptions which has OutputFile, but also declares its own Filename field. Both are set to "README.md" in DefaultExportOptions(). This creates ambiguity about which field controls the output filename.

Looking at runner.go line 177, there's explicit synchronization: opts.OutputFile = opts.Filename. Consider either removing one field or documenting the relationship clearly.

💡 Suggested fix: Remove duplicate field
 // ExportOptions contains options for the export command.
 type ExportOptions struct {
 	GlobalOptions
 
 	// Format is the output format (md).
 	Format string
 
 	// OutputDir is the output directory.
 	OutputDir string
-
-	// Filename is the output filename.
-	Filename string
 }
 
 // DefaultExportOptions returns the default export options.
 func DefaultExportOptions() *ExportOptions {
 	return &ExportOptions{
 		GlobalOptions: *DefaultGlobalOptions(),
 		Format:        "md",
 		OutputDir:     ".",
-		Filename:      "README.md",
 	}
 }

Comment thread gnodoc/cmd/runner_test.go
Comment on lines +42 to +71
func TestRunner_Run_BasicCommand(t *testing.T) {
tmpDir := createTestPackage(t)
defer os.RemoveAll(tmpDir)

var stdout, stderr bytes.Buffer
r := NewRunner(&stdout, &stderr)

// Run with path argument
code := r.Run([]string{tmpDir})

if code != ExitSuccess {
t.Errorf("expected ExitSuccess, got %v: %s", code, stderr.String())
}

// Check output file was created
outputPath := filepath.Join(".", "README.md")
content, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("failed to read output file: %v", err)
}
defer os.Remove(outputPath)

// Verify content contains expected sections
if !strings.Contains(string(content), "# testpkg") {
t.Error("expected package name in output")
}
if !strings.Contains(string(content), "Foo") {
t.Error("expected Foo type in output")
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test cleanup issue: README.md written to working directory may persist on test failure.

The test writes README.md to the current working directory (line 57-58), and the cleanup (os.Remove) is deferred after the read operations. If the test fails between lines 58-61, the file remains and could pollute subsequent test runs or the repository.

Consider using a dedicated output directory (like other tests do) or ensuring cleanup happens regardless of assertion failures.

Proposed fix
 func TestRunner_Run_BasicCommand(t *testing.T) {
 	tmpDir := createTestPackage(t)
 	defer os.RemoveAll(tmpDir)

+	outDir, err := os.MkdirTemp("", "gnodoc-basic-out-*")
+	if err != nil {
+		t.Fatalf("failed to create output dir: %v", err)
+	}
+	defer os.RemoveAll(outDir)
+
 	var stdout, stderr bytes.Buffer
 	r := NewRunner(&stdout, &stderr)

 	// Run with path argument
-	code := r.Run([]string{tmpDir})
+	code := r.Run([]string{"--out=" + outDir, tmpDir})

 	if code != ExitSuccess {
 		t.Errorf("expected ExitSuccess, got %v: %s", code, stderr.String())
 	}

 	// Check output file was created
-	outputPath := filepath.Join(".", "README.md")
+	outputPath := filepath.Join(outDir, "README.md")
 	content, err := os.ReadFile(outputPath)
 	if err != nil {
 		t.Fatalf("failed to read output file: %v", err)
 	}
-	defer os.Remove(outputPath)

 	// Verify content contains expected sections
 	if !strings.Contains(string(content), "# testpkg") {
 		t.Error("expected package name in output")
 	}
 	if !strings.Contains(string(content), "Foo") {
 		t.Error("expected Foo type in output")
 	}
 }

Comment on lines +17 to +19
// IsTestFile reports whether this is a test file.
func (f SourceFile) IsTestFile() bool {
return strings.HasSuffix(f.Name, "_test.go")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing .gno test file detection.

The PR adds support for .gno files, and the parser Options.IncludeTests comment mentions *_test.gno files. However, IsTestFile() only checks for _test.go suffix, missing .gno test files.

Proposed fix to handle .gno test files
 // IsTestFile reports whether this is a test file.
 func (f SourceFile) IsTestFile() bool {
-	return strings.HasSuffix(f.Name, "_test.go")
+	return strings.HasSuffix(f.Name, "_test.go") || strings.HasSuffix(f.Name, "_test.gno")
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// IsTestFile reports whether this is a test file.
func (f SourceFile) IsTestFile() bool {
return strings.HasSuffix(f.Name, "_test.go")
// IsTestFile reports whether this is a test file.
func (f SourceFile) IsTestFile() bool {
return strings.HasSuffix(f.Name, "_test.go") || strings.HasSuffix(f.Name, "_test.gno")
}

Comment thread gnodoc/parser/parser.go
Comment on lines +725 to +729
case *ast.ArrayType:
if t.Len == nil {
return "[]" + p.typeString(t.Elt)
}
return "[...]" + p.typeString(t.Elt)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Array type with explicit length renders incorrectly as [...].

When an array has an explicit length (e.g., [5]int), the code renders it as [...]int instead of [5]int. The length expression is lost.

🐛 Fix array length rendering
 	case *ast.ArrayType:
 		if t.Len == nil {
 			return "[]" + p.typeString(t.Elt)
 		}
-		return "[...]" + p.typeString(t.Elt)
+		return "[" + p.exprString(t.Len) + "]" + p.typeString(t.Elt)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case *ast.ArrayType:
if t.Len == nil {
return "[]" + p.typeString(t.Elt)
}
return "[...]" + p.typeString(t.Elt)
case *ast.ArrayType:
if t.Len == nil {
return "[]" + p.typeString(t.Elt)
}
return "[" + p.exprString(t.Len) + "]" + p.typeString(t.Elt)

Comment thread gnodoc/parser/parser.go
Comment on lines +861 to +918
func (p *Parser) fallbackReturnInfo(f *doc.Func, receiverType string) (returnInfo, bool) {
if f.Decl == nil {
return returnInfo{}, false
}
pos := p.fset.Position(f.Decl.Pos())
if pos.Filename == "" {
return returnInfo{}, false
}

file, err := parser.ParseFile(token.NewFileSet(), pos.Filename, nil, parser.ParseComments)
if err != nil {
return returnInfo{}, false
}

for _, decl := range file.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || fn.Name == nil || fn.Body == nil {
continue
}
if fn.Name.Name != f.Name {
continue
}
recvType := ""
if fn.Recv != nil && len(fn.Recv.List) > 0 {
recvType = p.typeString(fn.Recv.List[0].Type)
}
if recvType != receiverType {
continue
}

info := returnInfo{}
ast.Inspect(fn.Body, func(n ast.Node) bool {
ret, ok := n.(*ast.ReturnStmt)
if !ok {
return true
}
if len(ret.Results) == 0 {
info.naked = true
return true
}
var exprs []string
for _, expr := range ret.Results {
if ident, ok := expr.(*ast.Ident); ok {
exprs = append(exprs, ident.Name)
continue
}
exprs = append(exprs, p.exprString(expr))
}
if len(exprs) > 0 {
info.exprs = append(info.exprs, strings.Join(exprs, ", "))
}
return true
})
return info, true
}

return returnInfo{}, false
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

fallbackReturnInfo creates a new FileSet, causing position mismatches.

Line 870 creates a new token.NewFileSet() instead of reusing p.fset. This means positions from the fallback parsing won't match the main parser's positions, and p.exprString calls within this function use the wrong fset context.

🐛 Reuse the parser's FileSet
 func (p *Parser) fallbackReturnInfo(f *doc.Func, receiverType string) (returnInfo, bool) {
 	if f.Decl == nil {
 		return returnInfo{}, false
 	}
 	pos := p.fset.Position(f.Decl.Pos())
 	if pos.Filename == "" {
 		return returnInfo{}, false
 	}
 
-	file, err := parser.ParseFile(token.NewFileSet(), pos.Filename, nil, parser.ParseComments)
+	file, err := parser.ParseFile(p.fset, pos.Filename, nil, parser.ParseComments)
 	if err != nil {
 		return returnInfo{}, false
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (p *Parser) fallbackReturnInfo(f *doc.Func, receiverType string) (returnInfo, bool) {
if f.Decl == nil {
return returnInfo{}, false
}
pos := p.fset.Position(f.Decl.Pos())
if pos.Filename == "" {
return returnInfo{}, false
}
file, err := parser.ParseFile(token.NewFileSet(), pos.Filename, nil, parser.ParseComments)
if err != nil {
return returnInfo{}, false
}
for _, decl := range file.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || fn.Name == nil || fn.Body == nil {
continue
}
if fn.Name.Name != f.Name {
continue
}
recvType := ""
if fn.Recv != nil && len(fn.Recv.List) > 0 {
recvType = p.typeString(fn.Recv.List[0].Type)
}
if recvType != receiverType {
continue
}
info := returnInfo{}
ast.Inspect(fn.Body, func(n ast.Node) bool {
ret, ok := n.(*ast.ReturnStmt)
if !ok {
return true
}
if len(ret.Results) == 0 {
info.naked = true
return true
}
var exprs []string
for _, expr := range ret.Results {
if ident, ok := expr.(*ast.Ident); ok {
exprs = append(exprs, ident.Name)
continue
}
exprs = append(exprs, p.exprString(expr))
}
if len(exprs) > 0 {
info.exprs = append(info.exprs, strings.Join(exprs, ", "))
}
return true
})
return info, true
}
return returnInfo{}, false
}
func (p *Parser) fallbackReturnInfo(f *doc.Func, receiverType string) (returnInfo, bool) {
if f.Decl == nil {
return returnInfo{}, false
}
pos := p.fset.Position(f.Decl.Pos())
if pos.Filename == "" {
return returnInfo{}, false
}
file, err := parser.ParseFile(p.fset, pos.Filename, nil, parser.ParseComments)
if err != nil {
return returnInfo{}, false
}
for _, decl := range file.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || fn.Name == nil || fn.Body == nil {
continue
}
if fn.Name.Name != f.Name {
continue
}
recvType := ""
if fn.Recv != nil && len(fn.Recv.List) > 0 {
recvType = p.typeString(fn.Recv.List[0].Type)
}
if recvType != receiverType {
continue
}
info := returnInfo{}
ast.Inspect(fn.Body, func(n ast.Node) bool {
ret, ok := n.(*ast.ReturnStmt)
if !ok {
return true
}
if len(ret.Results) == 0 {
info.naked = true
return true
}
var exprs []string
for _, expr := range ret.Results {
if ident, ok := expr.(*ast.Ident); ok {
exprs = append(exprs, ident.Name)
continue
}
exprs = append(exprs, p.exprString(expr))
}
if len(exprs) > 0 {
info.exprs = append(info.exprs, strings.Join(exprs, ", "))
}
return true
})
return info, true
}
return returnInfo{}, false
}

Comment on lines +5 to +20
func TestRenderOptions_Default(t *testing.T) {
opts := DefaultOptions()

if !opts.ExportedOnly {
t.Error("expected ExportedOnly to be true by default")
}
if !opts.IncludeIndex {
t.Error("expected IncludeIndex to be true by default")
}
if !opts.IncludeExamples {
t.Error("expected IncludeExamples to be true by default")
}
if opts.SourceLinkBase != "" {
t.Error("expected empty SourceLinkBase by default")
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing test for IncludeNotes default.

Based on the DefaultOptions() implementation in gnodoc/render/options.go, IncludeNotes is set to true by default, but this isn't verified here.

📝 Suggested addition
 	if opts.SourceLinkBase != "" {
 		t.Error("expected empty SourceLinkBase by default")
 	}
+	if !opts.IncludeNotes {
+		t.Error("expected IncludeNotes to be true by default")
+	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestRenderOptions_Default(t *testing.T) {
opts := DefaultOptions()
if !opts.ExportedOnly {
t.Error("expected ExportedOnly to be true by default")
}
if !opts.IncludeIndex {
t.Error("expected IncludeIndex to be true by default")
}
if !opts.IncludeExamples {
t.Error("expected IncludeExamples to be true by default")
}
if opts.SourceLinkBase != "" {
t.Error("expected empty SourceLinkBase by default")
}
}
func TestRenderOptions_Default(t *testing.T) {
opts := DefaultOptions()
if !opts.ExportedOnly {
t.Error("expected ExportedOnly to be true by default")
}
if !opts.IncludeIndex {
t.Error("expected IncludeIndex to be true by default")
}
if !opts.IncludeExamples {
t.Error("expected IncludeExamples to be true by default")
}
if opts.SourceLinkBase != "" {
t.Error("expected empty SourceLinkBase by default")
}
if !opts.IncludeNotes {
t.Error("expected IncludeNotes to be true by default")
}
}

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (2)
gnodoc/parser/parser.go (2)

721-725: Array type with explicit length renders incorrectly as [...].

When an array has an explicit length (e.g., [5]int), the code renders it as [...]int instead of [5]int. The length expression is lost.

🐛 Fix array length rendering
 	case *ast.ArrayType:
 		if t.Len == nil {
 			return "[]" + p.typeString(t.Elt)
 		}
-		return "[...]" + p.typeString(t.Elt)
+		return "[" + p.exprString(t.Len) + "]" + p.typeString(t.Elt)

857-870: fallbackReturnInfo creates a new FileSet, causing position mismatches.

Line 866 creates a new token.NewFileSet() instead of reusing p.fset. This means positions from the fallback parsing won't match the main parser's positions, and p.exprString calls within this function use the wrong fset context.

🐛 Reuse the parser's FileSet
 func (p *Parser) fallbackReturnInfo(f *doc.Func, receiverType string) (returnInfo, bool) {
 	if f.Decl == nil {
 		return returnInfo{}, false
 	}
 	pos := p.fset.Position(f.Decl.Pos())
 	if pos.Filename == "" {
 		return returnInfo{}, false
 	}

-	file, err := parser.ParseFile(token.NewFileSet(), pos.Filename, nil, parser.ParseComments)
+	file, err := parser.ParseFile(p.fset, pos.Filename, nil, parser.ParseComments)
 	if err != nil {
 		return returnInfo{}, false
 	}
🧹 Nitpick comments (5)
gnodoc/model/doc_field.go (1)

33-46: Minor edge case: tag values containing spaces.

The manual parsing fallback splits on space, which could incorrectly break up tag values that contain spaces (e.g., description:"hello world"). While rare in practice, consider using a more robust parsing approach if such tags are expected.

For standard struct tags (e.g., json:"id,omitempty" xml:"id"), the primary reflect.StructTag.Get() path handles them correctly, so this is only a concern if non-standard tag formats are anticipated.

gnodoc/parser/parser.go (4)

111-125: Non-deterministic package selection when multiple packages exist.

Map iteration order in Go is undefined. If a directory contains multiple non-test packages, the selected mainPkg will be arbitrary and may vary between runs. Consider sorting package names or using a deterministic selection strategy.

♻️ Suggested fix for deterministic selection
 	// Get the main package (not test package)
 	var mainPkg *ast.Package
+	var mainPkgName string
 	for name, pkg := range pkgs {
 		if !strings.HasSuffix(name, "_test") {
-			mainPkg = pkg
-			break
+			if mainPkg == nil || name < mainPkgName {
+				mainPkg = pkg
+				mainPkgName = name
+			}
 		}
 	}
 	if mainPkg == nil {
 		// If only test packages, use first one
+		var testPkgName string
 		for _, pkg := range pkgs {
-			mainPkg = pkg
-			break
+			for name := range pkgs {
+				if mainPkg == nil || name < testPkgName {
+					mainPkg = pkg
+					testPkgName = name
+				}
+			}
 		}
 	}

189-204: External command execution without timeout.

exec.Command without a context or timeout could hang indefinitely if go list stalls (e.g., network issues resolving remote modules). For a CLI tool this is lower risk, but consider adding a timeout for robustness.

♻️ Suggested fix using context with timeout
+import (
+	"context"
+	"time"
+)

 func (p *Parser) resolveImportPath(importPath string) (string, error) {
-	cmd := exec.Command("go", "list", "-f", "{{.Dir}}", importPath)
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+	cmd := exec.CommandContext(ctx, "go", "list", "-f", "{{.Dir}}", importPath)
 	output, err := cmd.Output()
 	if err != nil {
 		return "", fmt.Errorf("cannot resolve import path %q: %w", importPath, err)
 	}

225-259: Manual TOML parsing is fragile.

This hand-rolled parser doesn't handle TOML edge cases like escaped quotes (module = "foo\"bar"), multiline strings, or inline comments. Consider using a proper TOML library like github.com/BurntSushi/toml or github.com/pelletier/go-toml for robustness.


1013-1081: Complex indentation normalization logic is hard to follow.

The two-pass approach with minIndent and minIndentNonZero handles edge cases but is difficult to reason about. Consider adding a comment explaining the intent, or simplifying if the edge cases aren't critical.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f3bd34 and c295cc3.

📒 Files selected for processing (2)
  • gnodoc/model/doc_field.go
  • gnodoc/parser/parser.go
🧰 Additional context used
🧬 Code graph analysis (1)
gnodoc/model/doc_field.go (1)
gnodoc/model/doc_node.go (1)
  • DocNode (24-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: test-gnoswap (gnoswap/gov/staker/v1, contract/r/gnoswap/gov/staker/v1)
  • GitHub Check: test-gnoswap (gnoswap/scenario/router, contract/r/scenario/router)
  • GitHub Check: test-gnoswap (gnoswap/scenario/staker, contract/r/scenario/staker)
  • GitHub Check: test-gnoswap (gnoswap/test/fuzz, contract/r/gnoswap/test/fuzz)
  • GitHub Check: test-gnoswap (gnoswap/scenario/position, contract/r/scenario/position)
  • GitHub Check: test-gnoswap (gnoswap/staker/v1, contract/r/gnoswap/staker/v1)
  • GitHub Check: test-gnoswap (gnoswap/pool/v1, contract/r/gnoswap/pool/v1)
  • GitHub Check: test-gnoswap (gnoswap/position, contract/r/gnoswap/position)
  • GitHub Check: run-integration-test
🔇 Additional comments (2)
gnodoc/model/doc_field.go (2)

8-13: LGTM!

Clean struct definition with appropriate embedding of DocNode. The additional Type and Tag fields are well-suited for representing struct/interface field metadata.


15-18: LGTM!

Simple and correct implementation.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread gnodoc/parser/parser.go
Comment on lines +179 to +187
// isExcluded checks if a filename matches any exclude pattern.
func (p *Parser) isExcluded(name string) bool {
for _, pattern := range p.opts.Exclude {
if matched, _ := filepath.Match(pattern, name); matched {
return true
}
}
return false
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent error suppression in pattern matching.

filepath.Match returns an error for malformed patterns (e.g., unclosed brackets). Ignoring this error means users won't know if their exclude pattern is invalid.

🐛 Suggested fix to log or collect pattern errors
 func (p *Parser) isExcluded(name string) bool {
 	for _, pattern := range p.opts.Exclude {
-		if matched, _ := filepath.Match(pattern, name); matched {
+		matched, err := filepath.Match(pattern, name)
+		if err != nil {
+			// Invalid pattern - could log warning or collect errors
+			continue
+		}
+		if matched {
 			return true
 		}
 	}
 	return false
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// isExcluded checks if a filename matches any exclude pattern.
func (p *Parser) isExcluded(name string) bool {
for _, pattern := range p.opts.Exclude {
if matched, _ := filepath.Match(pattern, name); matched {
return true
}
}
return false
}
// isExcluded checks if a filename matches any exclude pattern.
func (p *Parser) isExcluded(name string) bool {
for _, pattern := range p.opts.Exclude {
matched, err := filepath.Match(pattern, name)
if err != nil {
// Invalid pattern - could log warning or collect errors
continue
}
if matched {
return true
}
}
return false
}

Comment thread gnodoc/parser/parser.go
Comment on lines +627 to +636
// Field name (may be anonymous)
if len(field.Names) > 0 {
f.Name = field.Names[0].Name
f.Exported = isExported(f.Name)
f.Pos = p.convertPos(field.Names[0].Pos())
} else {
// Anonymous field - use type name
f.Name = f.Type
f.Exported = isExported(f.Name)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Multi-name struct fields lose names beyond the first.

For struct fields like a, b, c int, only the first name (a) is captured. The other names (b, c) are silently dropped. This may be intentional for documentation purposes, but could lead to incomplete docs.

🐛 Suggested fix to capture all field names
 	// Field name (may be anonymous)
-	if len(field.Names) > 0 {
-		f.Name = field.Names[0].Name
-		f.Exported = isExported(f.Name)
-		f.Pos = p.convertPos(field.Names[0].Pos())
-	} else {
+	if len(field.Names) == 0 {
 		// Anonymous field - use type name
 		f.Name = f.Type
 		f.Exported = isExported(f.Name)
+	} else if len(field.Names) == 1 {
+		f.Name = field.Names[0].Name
+		f.Exported = isExported(f.Name)
+		f.Pos = p.convertPos(field.Names[0].Pos())
+	} else {
+		// Multiple names - join them
+		names := make([]string, len(field.Names))
+		for i, n := range field.Names {
+			names[i] = n.Name
+		}
+		f.Name = strings.Join(names, ", ")
+		f.Exported = isExported(field.Names[0].Name)
+		f.Pos = p.convertPos(field.Names[0].Pos())
 	}

Alternatively, return multiple DocField entries for multi-name fields if the model supports it.

Comment thread gnodoc/parser/parser.go
Comment on lines +728 to +729
case *ast.ChanType:
return "chan " + p.typeString(t.Value)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Channel direction is not represented in type string.

chan int, chan<- int, and <-chan int all render as "chan int". The send-only and receive-only directions are lost.

🐛 Suggested fix to include channel direction
 	case *ast.ChanType:
-		return "chan " + p.typeString(t.Value)
+		switch t.Dir {
+		case ast.SEND:
+			return "chan<- " + p.typeString(t.Value)
+		case ast.RECV:
+			return "<-chan " + p.typeString(t.Value)
+		default:
+			return "chan " + p.typeString(t.Value)
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case *ast.ChanType:
return "chan " + p.typeString(t.Value)
case *ast.ChanType:
switch t.Dir {
case ast.SEND:
return "chan<- " + p.typeString(t.Value)
case ast.RECV:
return "<-chan " + p.typeString(t.Value)
default:
return "chan " + p.typeString(t.Value)
}

Comment thread gnodoc/parser/parser.go
Comment on lines +777 to +796
// collectExamplesFromFiles extracts examples from test files listed for the package.
func (p *Parser) collectExamplesFromFiles(dir string, files []string) []*doc.Example {
var testFiles []*ast.File
for _, name := range files {
if !strings.HasSuffix(name, "_test.go") {
continue
}
path := filepath.Join(dir, name)
file, err := parser.ParseFile(p.fset, path, nil, parser.ParseComments)
if err != nil {
continue
}
testFiles = append(testFiles, file)
}

if len(testFiles) == 0 {
return nil
}
return doc.Examples(testFiles...)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Example collection ignores *_test.gno files.

Line 781 only checks for _test.go suffix, but isTestFile() (line 952) also recognizes _test.gno. This inconsistency means examples in .gno test files won't be collected.

🐛 Suggested fix to also collect from .gno test files
 func (p *Parser) collectExamplesFromFiles(dir string, files []string) []*doc.Example {
 	var testFiles []*ast.File
 	for _, name := range files {
-		if !strings.HasSuffix(name, "_test.go") {
+		if !isTestFile(name) {
 			continue
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// collectExamplesFromFiles extracts examples from test files listed for the package.
func (p *Parser) collectExamplesFromFiles(dir string, files []string) []*doc.Example {
var testFiles []*ast.File
for _, name := range files {
if !strings.HasSuffix(name, "_test.go") {
continue
}
path := filepath.Join(dir, name)
file, err := parser.ParseFile(p.fset, path, nil, parser.ParseComments)
if err != nil {
continue
}
testFiles = append(testFiles, file)
}
if len(testFiles) == 0 {
return nil
}
return doc.Examples(testFiles...)
}
// collectExamplesFromFiles extracts examples from test files listed for the package.
func (p *Parser) collectExamplesFromFiles(dir string, files []string) []*doc.Example {
var testFiles []*ast.File
for _, name := range files {
if !isTestFile(name) {
continue
}
path := filepath.Join(dir, name)
file, err := parser.ParseFile(p.fset, path, nil, parser.ParseComments)
if err != nil {
continue
}
testFiles = append(testFiles, file)
}
if len(testFiles) == 0 {
return nil
}
return doc.Examples(testFiles...)
}

Comment thread gnodoc/parser/parser.go
Comment on lines +1096 to +1116
for i := 0; i < len(lines); {
line := lines[i]
trimmed := strings.TrimSpace(line)
if after, ok := strings.CutPrefix(trimmed, "Deprecated:"); ok {
body := strings.TrimSpace(after)
j := i + 1
for j < len(lines) {
next := strings.TrimSpace(lines[j])
if next == "" {
break
}
if body != "" {
body += " "
}
body += next
j++
}
deprecated = append(deprecated, model.DocDeprecated{Body: body, Pos: pos})
i = j
continue
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Deprecation message parsing may consume unrelated documentation.

The loop at lines 1102-1111 continues appending lines until it hits an empty line. If there's no blank line after the deprecation notice, subsequent documentation gets incorrectly included in the deprecation body.

For example:

// Deprecated: Use NewFoo instead.
// Foo creates a new instance.
func Foo() {}

Would result in deprecation body: "Use NewFoo instead. Foo creates a new instance."

🐛 Suggested fix to be more conservative

Consider stopping at lines that look like new documentation sentences (start with uppercase after period) or limiting to a single line unless the next line starts with whitespace (continuation indent).

@notJoon notJoon marked this pull request as draft January 14, 2026 07:24
@notJoon notJoon marked this pull request as ready for review January 16, 2026 02:23
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (4)
gnodoc/parser/parser.go (4)

627-631: Preserve all names in multi-name struct fields.

Fields like a, b int currently keep only a, dropping b from docs. Consider joining names or emitting multiple DocField entries. (Duplicate of prior review.)

🛠️ Proposed fix (join names)
-	if len(field.Names) > 0 {
-		f.Name = field.Names[0].Name
-		f.Exported = isExported(f.Name)
-		f.Pos = p.convertPos(field.Names[0].Pos())
-	} else {
+	if len(field.Names) == 0 {
 		// Anonymous field - use type name
 		f.Name = f.Type
 		f.Exported = isExported(f.Name)
+	} else if len(field.Names) == 1 {
+		f.Name = field.Names[0].Name
+		f.Exported = isExported(f.Name)
+		f.Pos = p.convertPos(field.Names[0].Pos())
+	} else {
+		names := make([]string, len(field.Names))
+		for i, n := range field.Names {
+			names[i] = n.Name
+		}
+		f.Name = strings.Join(names, ", ")
+		f.Exported = isExported(field.Names[0].Name)
+		f.Pos = p.convertPos(field.Names[0].Pos())
 	}

721-725: Render explicit array lengths instead of [...].

[5]int currently renders as [...]int, losing the length expression. (Duplicate of prior review.)

🛠️ Proposed fix
 	case *ast.ArrayType:
 		if t.Len == nil {
 			return "[]" + p.typeString(t.Elt)
 		}
-		return "[...]" + p.typeString(t.Elt)
+		return "[" + p.exprString(t.Len) + "]" + p.typeString(t.Elt)

728-729: Include channel direction in type strings.

Send-only and receive-only channels are currently rendered the same as bidirectional. (Duplicate of prior review.)

🛠️ Proposed fix
 	case *ast.ChanType:
-		return "chan " + p.typeString(t.Value)
+		switch t.Dir {
+		case ast.SEND:
+			return "chan<- " + p.typeString(t.Value)
+		case ast.RECV:
+			return "<-chan " + p.typeString(t.Value)
+		default:
+			return "chan " + p.typeString(t.Value)
+		}

825-827: Collect examples from _test.gno files too.

isTestFile already recognizes _test.gno, but this loop only includes _test.go, so Gno tests are skipped. (Duplicate of prior review.)

🛠️ Proposed fix
 	for _, name := range files {
-		if !strings.HasSuffix(name, "_test.go") {
+		if !isTestFile(name) {
 			continue
 		}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bf4029 and 0f2efc3.

📒 Files selected for processing (2)
  • gnodoc/parser/parser.go
  • gnodoc/render/markdown.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: test-gnoswap (gnoswap/pool/v1, contract/r/gnoswap/pool/v1)
  • GitHub Check: test-gnoswap (gnoswap/scenario/router, contract/r/scenario/router)
  • GitHub Check: test-gnoswap (gnoswap/scenario/upgrade, contract/r/scenario/upgrade)
  • GitHub Check: test-gnoswap (gnoswap/scenario/staker, contract/r/scenario/staker)
  • GitHub Check: test-gnoswap (gnoswap/test/fuzz, contract/r/gnoswap/test/fuzz)
  • GitHub Check: test-gnoswap (gnoswap/router/v1, contract/r/gnoswap/router/v1)
  • GitHub Check: test-gnoswap (gnoswap/staker/v1, contract/r/gnoswap/staker/v1)
  • GitHub Check: test-gnoswap (gnoswap/gov/staker/v1, contract/r/gnoswap/gov/staker/v1)
  • GitHub Check: run-integration-test
🔇 Additional comments (1)
gnodoc/parser/parser.go (1)

289-296: Respect IgnoreParseErrors when parsing individual files.

Line 292 skips parse errors unconditionally even when IgnoreParseErrors is false, which can silently drop files and yield partial docs. The error handling should check the flag before deciding to continue.

Proposed fix
 	for _, filename := range files {
 		path := filepath.Join(dir, filename)
 		f, err := parser.ParseFile(p.fset, path, nil, parser.ParseComments)
 		if err != nil {
+			if !p.opts.IgnoreParseErrors {
+				return nil, fmt.Errorf("parse error in %s: %w", path, err)
+			}
 			// Mark that we had parse errors, then skip the failed file
 			p.hadParseErrors = true
 			continue
 		}

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread gnodoc/render/markdown.go
Comment on lines +206 to +214
for _, spec := range group.Specs {
if !r.opts.ShouldRender(spec.Exported) {
continue
}
if spec.Value != "" {
groupParts = append(groupParts, fmt.Sprintf("\t%s = %s", spec.Name, spec.Value))
} else {
groupParts = append(groupParts, fmt.Sprintf("\t%s %s", spec.Name, spec.Type))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Render explicit const types when a value is present.

Typed constants like const Foo int = 1 will render as Foo = 1, dropping the explicit type.

Proposed fix
-			if spec.Value != "" {
-				groupParts = append(groupParts, fmt.Sprintf("\t%s = %s", spec.Name, spec.Value))
-			} else {
-				groupParts = append(groupParts, fmt.Sprintf("\t%s %s", spec.Name, spec.Type))
-			}
+			switch {
+			case spec.Value != "" && spec.Type != "":
+				groupParts = append(groupParts, fmt.Sprintf("\t%s %s = %s", spec.Name, spec.Type, spec.Value))
+			case spec.Value != "":
+				groupParts = append(groupParts, fmt.Sprintf("\t%s = %s", spec.Name, spec.Value))
+			case spec.Type != "":
+				groupParts = append(groupParts, fmt.Sprintf("\t%s %s", spec.Name, spec.Type))
+			default:
+				groupParts = append(groupParts, fmt.Sprintf("\t%s", spec.Name))
+			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, spec := range group.Specs {
if !r.opts.ShouldRender(spec.Exported) {
continue
}
if spec.Value != "" {
groupParts = append(groupParts, fmt.Sprintf("\t%s = %s", spec.Name, spec.Value))
} else {
groupParts = append(groupParts, fmt.Sprintf("\t%s %s", spec.Name, spec.Type))
}
for _, spec := range group.Specs {
if !r.opts.ShouldRender(spec.Exported) {
continue
}
switch {
case spec.Value != "" && spec.Type != "":
groupParts = append(groupParts, fmt.Sprintf("\t%s %s = %s", spec.Name, spec.Type, spec.Value))
case spec.Value != "":
groupParts = append(groupParts, fmt.Sprintf("\t%s = %s", spec.Name, spec.Value))
case spec.Type != "":
groupParts = append(groupParts, fmt.Sprintf("\t%s %s", spec.Name, spec.Type))
default:
groupParts = append(groupParts, fmt.Sprintf("\t%s", spec.Name))
}

Comment thread gnodoc/render/markdown.go
Comment on lines +418 to +423
for _, f := range typ.Fields {
if r.opts.ShouldRender(f.Exported) {
line := fmt.Sprintf("- `%s %s`", f.Name, f.Type)
if link := r.sourceLink(f.Pos); link != "" {
line = fmt.Sprintf("%s ([source](%s))", line, link)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle embedded fields without a name.

For embedded fields, f.Name is empty, so the output becomes ` io.Reader` with a leading space.

Proposed fix
-					line := fmt.Sprintf("- `%s %s`", f.Name, f.Type)
+					var line string
+					if f.Name != "" {
+						line = fmt.Sprintf("- `%s %s`", f.Name, f.Type)
+					} else {
+						line = fmt.Sprintf("- `%s`", f.Type)
+					}

Comment thread gnodoc/render/markdown.go
Comment on lines +712 to +725
for _, param := range params {
desc := ""
if param.Name != "" {
desc = descByName[param.Name]
}
name := ""
if param.Name != "" {
name = "`" + escapeTableCell(param.Name) + "`"
}
lines = append(lines, fmt.Sprintf("| %s | %s | %s |",
name,
escapeTableCell(param.Type),
escapeTableCell(desc),
))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t drop descriptions for unnamed parameters.

When parameters are unnamed, descriptions keyed by type are ignored, so docs can disappear.

Proposed fix
-		desc := ""
-		if param.Name != "" {
-			desc = descByName[param.Name]
-		}
+		desc := ""
+		if param.Name != "" {
+			desc = descByName[param.Name]
+		} else if param.Type != "" {
+			desc = descByName[param.Type]
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for _, param := range params {
desc := ""
if param.Name != "" {
desc = descByName[param.Name]
}
name := ""
if param.Name != "" {
name = "`" + escapeTableCell(param.Name) + "`"
}
lines = append(lines, fmt.Sprintf("| %s | %s | %s |",
name,
escapeTableCell(param.Type),
escapeTableCell(desc),
))
for _, param := range params {
desc := ""
if param.Name != "" {
desc = descByName[param.Name]
} else if param.Type != "" {
desc = descByName[param.Type]
}
name := ""
if param.Name != "" {
name = "`" + escapeTableCell(param.Name) + "`"
}
lines = append(lines, fmt.Sprintf("| %s | %s | %s |",
name,
escapeTableCell(param.Type),
escapeTableCell(desc),
))

@jinoosss jinoosss added the DO NOT MERGE do not merge this PR label Jan 19, 2026
@junghoon-vans
Copy link
Copy Markdown
Member

I think it would be best to move this to a separate repository.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DO NOT MERGE do not merge this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants