From 4b6967aa7d2a4d169e04492adcc763d990af43e3 Mon Sep 17 00:00:00 2001 From: Youichi Fujimoto Date: Sat, 27 Jun 2026 10:21:39 +0900 Subject: [PATCH 1/2] chore: fix golangci-lint issues Make `make lint` pass with a focused set of changes (no repo-wide reformatting): - staticcheck QF1003: use a tagged switch on validationResult.Status - gosec G306: tighten golden-file test write perms to 0600 - lll: wrap an over-length tool description in register.go - unparam: mark the unused context param of runValidationCheck as `_` - goconst: extract check-name and status constants in app/validation.go and a declTypeFunction constant in app/ast.go Config (.golangci.yaml exclusions): - exclude goconst/dupl from _test.go (repeated literals are expected in tests) - exclude dupl/goconst from the intentionally-parallel godoc/rustdoc/pydoc doc-integration files - exclude staticcheck QF1012 (Fprintf vs WriteString(Sprintf)); the codebase uses strings.Builder.WriteString consistently across ~30 sites Co-Authored-By: Claude Opus 4.8 --- .golangci.yaml | 18 ++++++++++ internal/app/ast.go | 9 +++-- internal/app/godoc_golden_test.go | 2 +- internal/app/validation.go | 57 +++++++++++++++++++------------ internal/mcptool/register.go | 4 ++- internal/mcptool/validation.go | 5 +-- 6 files changed, 67 insertions(+), 28 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index a89c542..525b075 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -24,6 +24,24 @@ linters: - common-false-positives - legacy - std-error-handling + rules: + # QF1012 (Fprintf over WriteString(Sprintf(...))) is an opinionated style + # quickfix; this codebase uses strings.Builder.WriteString consistently. + - linters: + - staticcheck + text: QF1012 + # Test files routinely repeat small literals; constants add noise there. + - path: _test\.go + linters: + - goconst + - dupl + # The per-language doc integrations (godoc/rustdoc/pydoc) are + # intentionally parallel implementations of the same pattern, so the + # structural duplication and repeated command literals are by design. + - path: internal/(subcmd|mcptool|app)/(godoc|rustdoc|pydoc)\.go + linters: + - dupl + - goconst paths: - third_party$ - builtin$ diff --git a/internal/app/ast.go b/internal/app/ast.go index db7e368..d72322c 100644 --- a/internal/app/ast.go +++ b/internal/app/ast.go @@ -13,6 +13,9 @@ import ( "github.com/fpt/go-dev-mcp/internal/repository" ) +// declTypeFunction is the Declaration.Type value for functions and methods. +const declTypeFunction = "function" + type Declaration struct { Name string Type string // "function", "type", "interface", "struct", "const", "var" @@ -101,7 +104,7 @@ func ExtractFunctionNames( for _, result := range results { var functionDeclarations []Declaration for _, decl := range result.Declarations { - if decl.Type == "function" { + if decl.Type == declTypeFunction { functionDeclarations = append(functionDeclarations, decl) } } @@ -178,11 +181,11 @@ func extractDeclarationsFromFile(filePath string) []Declaration { name = receiverType + "." + d.Name.Name info = "method on " + receiverType } else { - info = "function" + info = declTypeFunction } decl := Declaration{ Name: name, - Type: "function", + Type: declTypeFunction, Info: info, Line: fset.Position(d.Pos()).Line, } diff --git a/internal/app/godoc_golden_test.go b/internal/app/godoc_golden_test.go index b5a21ec..917cd77 100644 --- a/internal/app/godoc_golden_test.go +++ b/internal/app/godoc_golden_test.go @@ -106,7 +106,7 @@ func readGolden(t *testing.T, path string) string { func writeGolden(t *testing.T, path, content string) { t.Helper() - err := os.WriteFile(path, []byte(content), 0o644) + err := os.WriteFile(path, []byte(content), 0o600) require.NoError(t, err, "failed to write golden file %s", path) t.Logf("updated golden file: %s", path) diff --git a/internal/app/validation.go b/internal/app/validation.go index 292995c..3e004e6 100644 --- a/internal/app/validation.go +++ b/internal/app/validation.go @@ -8,10 +8,25 @@ import ( "github.com/fpt/go-dev-mcp/internal/infra" ) +// Validation check names. +const ( + checkGoVet = "go vet" + checkGoBuild = "go build (dry-run)" + checkGoModTidy = "go mod tidy (check)" + checkGofmt = "gofmt check" +) + +// Validation result statuses. +const ( + statusPass = "pass" + statusFail = "fail" + statusError = "error" +) + // ValidationResult represents the result of a single validation check type ValidationResult struct { Check string `json:"check"` - Status string `json:"status"` // "pass", "fail", "error" + Status string `json:"status"` // pass, fail, or error Output string `json:"output,omitempty"` Summary string `json:"summary"` } @@ -38,25 +53,25 @@ func ValidateGoCode(ctx context.Context, directory string) (*ValidationReport, e description string }{ { - name: "go vet", + name: checkGoVet, cmd: "go", args: []string{"vet", "./..."}, description: "Static analysis to find suspicious constructs", }, { - name: "go build (dry-run)", + name: checkGoBuild, cmd: "go", args: []string{"build", "-n", "./..."}, description: "Check if code compiles without building", }, { - name: "go mod tidy (check)", + name: checkGoModTidy, cmd: "go", args: []string{"mod", "tidy", "-diff"}, description: "Check if go.mod is tidy", }, { - name: "gofmt check", + name: checkGofmt, cmd: "gofmt", args: []string{"-l", "."}, description: "Check if code is properly formatted", @@ -82,11 +97,11 @@ func ValidateGoCode(ctx context.Context, directory string) (*ValidationReport, e errors := 0 for _, result := range report.Results { switch result.Status { - case "pass": + case statusPass: passed++ - case "fail": + case statusFail: failed++ - case "error": + case statusError: errors++ } } @@ -112,7 +127,7 @@ func ValidateGoCode(ctx context.Context, directory string) (*ValidationReport, e } func runValidationCheck( - ctx context.Context, + _ context.Context, workDir, name, cmdName string, args []string, description string, @@ -126,12 +141,12 @@ func runValidationCheck( if err != nil { // Command couldn't run at all - result.Status = "error" + result.Status = statusError result.Output = err.Error() result.Summary = fmt.Sprintf("Could not run %s: %v", name, err) } else if exitCode != 0 { // Command ran but returned non-zero exit code - result.Status = "fail" + result.Status = statusFail // Use stderr for error information, stdout for normal output if stderr != "" { @@ -141,7 +156,7 @@ func runValidationCheck( } switch name { - case "gofmt check": + case checkGofmt: if stdout != "" { result.Summary = fmt.Sprintf( "Files need formatting: %s", @@ -150,9 +165,9 @@ func runValidationCheck( } else { result.Summary = "Files need formatting" } - case "go mod tidy (check)": + case checkGoModTidy: result.Summary = "go.mod needs tidying" - case "go vet": + case checkGoVet: // go vet typically outputs to stderr issues := stderr if issues == "" { @@ -164,20 +179,20 @@ func runValidationCheck( } else { result.Summary = "Vet found issues" } - case "go build (dry-run)": + case checkGoBuild: result.Summary = "Build would fail - compilation errors found" default: result.Summary = fmt.Sprintf("Check failed: %s", name) } } else { // Command succeeded (exit code 0) - result.Status = "pass" + result.Status = statusPass switch name { - case "gofmt check": + case checkGofmt: // gofmt -l returns 0 even when files need formatting, but lists files to stdout if stdout != "" { - result.Status = "fail" + result.Status = statusFail result.Output = stdout result.Summary = fmt.Sprintf( "Files need formatting: %s", @@ -186,12 +201,12 @@ func runValidationCheck( } else { result.Summary = "All files are properly formatted" } - case "go vet": + case checkGoVet: result.Summary = "No vet issues found" - case "go build (dry-run)": + case checkGoBuild: // Don't include verbose build output when successful result.Summary = "Code compiles successfully" - case "go mod tidy (check)": + case checkGoModTidy: result.Summary = "go.mod is tidy" default: if stdout != "" { diff --git a/internal/mcptool/register.go b/internal/mcptool/register.go index f66cbb9..aff53e8 100644 --- a/internal/mcptool/register.go +++ b/internal/mcptool/register.go @@ -168,7 +168,9 @@ func Register(s *server.MCPServer, workdir string) error { "ref", mcp.DefaultString(""), mcp.Description( - "Git ref to fetch at: branch name, tag, or commit SHA (e.g., 'main', 'v1.2.0', 'abc1234'). Defaults to the repository's default branch.", + "Git ref to fetch at: branch name, tag, or commit SHA"+ + " (e.g., 'main', 'v1.2.0', 'abc1234')."+ + " Defaults to the repository's default branch.", ), ), mcp.WithNumber("offset", diff --git a/internal/mcptool/validation.go b/internal/mcptool/validation.go index 2d6f4a9..ee01f66 100644 --- a/internal/mcptool/validation.go +++ b/internal/mcptool/validation.go @@ -33,9 +33,10 @@ func validateGoCode( for _, validationResult := range report.Results { // Add check header statusEmoji := "✓" - if validationResult.Status == "fail" { + switch validationResult.Status { + case "fail": statusEmoji = "✗" - } else if validationResult.Status == "error" { + case "error": statusEmoji = "⚠" } From fe2da36ccaa461087abc927d9ae39e734188b23b Mon Sep 17 00:00:00 2001 From: Youichi Fujimoto Date: Sat, 27 Jun 2026 11:34:49 +0900 Subject: [PATCH 2/2] fmt --- internal/app/markdown.go | 3 +- internal/app/tree.go | 3 +- internal/mcptool/github.go | 9 ++-- internal/mcptool/register.go | 87 ++++++++++++++++++++++------------ internal/subcmd/localsearch.go | 3 +- internal/subcmd/validation.go | 1 + pkg/dq/dq_test.go | 3 +- 7 files changed, 73 insertions(+), 36 deletions(-) diff --git a/internal/app/markdown.go b/internal/app/markdown.go index e1edfd0..df14fc7 100644 --- a/internal/app/markdown.go +++ b/internal/app/markdown.go @@ -181,7 +181,8 @@ func FormatMarkdownScanResult(files []MarkdownFile) string { for _, heading := range file.Headings { indent := strings.Repeat(" ", heading.Level-1) - result.WriteString(fmt.Sprintf("%s%s %s (line %d)\n", + result.WriteString(fmt.Sprintf( + "%s%s %s (line %d)\n", indent, strings.Repeat("#", heading.Level), heading.Title, diff --git a/internal/app/tree.go b/internal/app/tree.go index 1363441..4324380 100644 --- a/internal/app/tree.go +++ b/internal/app/tree.go @@ -42,7 +42,8 @@ func PrintTree( "", path, ignoreDot, - maxDepth) + maxDepth, + ) if err != nil { return errors.Wrap(err, "failed to walk directory") } diff --git a/internal/mcptool/github.go b/internal/mcptool/github.go index 85431d3..2577247 100644 --- a/internal/mcptool/github.go +++ b/internal/mcptool/github.go @@ -320,7 +320,8 @@ func getGitHubIssue( return mcp.NewToolResultError(fmt.Sprintf("Error getting issue: %v", err)), nil } - comments, _, err := gh.Issues.ListComments(ctx, owner, repo, args.Number, + comments, _, err := gh.Issues.ListComments( + ctx, owner, repo, args.Number, &github.IssueListCommentsOptions{ListOptions: github.ListOptions{PerPage: 100}}, ) if err != nil { @@ -460,7 +461,8 @@ func getGitHubPull( return mcp.NewToolResultError(fmt.Sprintf("Error getting pull request: %v", err)), nil } - files, _, err := gh.PullRequests.ListFiles(ctx, owner, repo, args.Number, + files, _, err := gh.PullRequests.ListFiles( + ctx, owner, repo, args.Number, &github.ListOptions{PerPage: 100}, ) if err != nil { @@ -623,7 +625,8 @@ func getGitHubWorkflowRun( return mcp.NewToolResultError(fmt.Sprintf("Error getting workflow run: %v", err)), nil } - jobs, _, err := gh.Actions.ListWorkflowJobs(ctx, owner, repo, args.RunID, + jobs, _, err := gh.Actions.ListWorkflowJobs( + ctx, owner, repo, args.RunID, &github.ListWorkflowJobsOptions{ Filter: "latest", ListOptions: github.ListOptions{PerPage: 100}, diff --git a/internal/mcptool/register.go b/internal/mcptool/register.go index aff53e8..6376dd0 100644 --- a/internal/mcptool/register.go +++ b/internal/mcptool/register.go @@ -17,7 +17,8 @@ func Register(s *server.MCPServer, workdir string) error { " (default: 4 levels) to reduce token usage by 60-80%."+ " Perfect for project exploration and understanding codebase structure.", ), - mcp.WithString("root_dir", + mcp.WithString( + "root_dir", mcp.Required(), mcp.Description("Root directory to scan (absolute path)"), ), @@ -28,7 +29,8 @@ func Register(s *server.MCPServer, workdir string) error { "Ignore dot files and directories (except .git which is always ignored)", ), ), - mcp.WithNumber("max_depth", + mcp.WithNumber( + "max_depth", mcp.DefaultNumber(4), mcp.Description("Maximum directory depth to traverse (default: 4 levels)"), ), @@ -41,7 +43,8 @@ func Register(s *server.MCPServer, workdir string) error { s.AddTool(tool, mcp.NewTypedToolHandler(treeDir)) // Add GoDoc search tool - tool = mcp.NewTool("search_godoc", + tool = mcp.NewTool( + "search_godoc", mcp.WithDescription("Search for Go package in pkg.go.dev"), mcp.WithString( "query", @@ -73,7 +76,8 @@ func Register(s *server.MCPServer, workdir string) error { "Go package URL (e.g., 'golang.org/x/net/html', 'github.com/user/repo')", ), ), - mcp.WithNumber("offset", + mcp.WithNumber( + "offset", mcp.DefaultNumber(0), mcp.Description("Line number to start reading from (0-based)"), ), @@ -111,7 +115,8 @@ func Register(s *server.MCPServer, workdir string) error { mcp.Required(), mcp.Description("Keyword to search for within the documentation"), ), - mcp.WithNumber("max_matches", + mcp.WithNumber( + "max_matches", mcp.DefaultNumber(10), mcp.Description("Maximum number of matches to return (default: 10)"), ), @@ -131,18 +136,21 @@ func Register(s *server.MCPServer, workdir string) error { " (30-40% token reduction)."+ " Returns repository/path format instead of verbose labels for efficient scanning.", ), - mcp.WithString("query", + mcp.WithString( + "query", mcp.Required(), mcp.Description("Code search query (e.g., 'function main', 'import context')"), ), - mcp.WithString("language", + mcp.WithString( + "language", mcp.Required(), mcp.Description( "Programming language to filter results"+ " (e.g., 'go', 'python', 'rust', 'typescript', 'c++', 'arduino', 'yaml', 'makefile')", ), ), - mcp.WithString("repo", + mcp.WithString( + "repo", mcp.Description("GitHub repository in 'owner/repo' format to limit search scope"), ), mcp.WithReadOnlyHintAnnotation(true), @@ -154,13 +162,16 @@ func Register(s *server.MCPServer, workdir string) error { s.AddTool(tool, mcp.NewTypedToolHandler(searchCodeGitHub)) // Add GitHub get content tool - tool = mcp.NewTool("get_github_content", + tool = mcp.NewTool( + "get_github_content", mcp.WithDescription("Get content from GitHub with line-based paging for large files"), - mcp.WithString("repo", + mcp.WithString( + "repo", mcp.Required(), mcp.Description("GitHub repository in 'owner/repo' format"), ), - mcp.WithString("path", + mcp.WithString( + "path", mcp.Required(), mcp.Description("Path to the file in the repository"), ), @@ -173,11 +184,13 @@ func Register(s *server.MCPServer, workdir string) error { " Defaults to the repository's default branch.", ), ), - mcp.WithNumber("offset", + mcp.WithNumber( + "offset", mcp.DefaultNumber(0), mcp.Description("Line number to start reading from (0-based)"), ), - mcp.WithNumber("limit", + mcp.WithNumber( + "limit", mcp.DefaultNumber(100), mcp.Description("Number of lines to read (default: 100, 0 for all lines)"), ), @@ -374,11 +387,13 @@ func Register(s *server.MCPServer, workdir string) error { " (default: 3 levels for network efficiency)."+ " Conservative token usage for remote repository exploration.", ), - mcp.WithString("repo", + mcp.WithString( + "repo", mcp.Required(), mcp.Description("GitHub repository in 'owner/repo' format"), ), - mcp.WithString("path", + mcp.WithString( + "path", mcp.DefaultString(""), mcp.Description("Path in the repository (defaults to root)"), ), @@ -412,22 +427,26 @@ func Register(s *server.MCPServer, workdir string) error { " (default: 10 matches per file) to reduce token usage by 50-70%."+ " Shows line numbers and truncation indicators.", ), - mcp.WithString("path", + mcp.WithString( + "path", mcp.Required(), mcp.Description("Directory path to search in (absolute path)"), ), - mcp.WithString("query", + mcp.WithString( + "query", mcp.Required(), mcp.Description("Text to search for in file contents (case-sensitive exact match)"), ), - mcp.WithString("extension", + mcp.WithString( + "extension", mcp.Required(), mcp.Description( "File extension to search without dot"+ " (e.g., 'go', 'py', 'rs', 'ts', 'js', 'ino', 'c', 'h', 'txt', 'yaml')", ), ), - mcp.WithNumber("max_matches", + mcp.WithNumber( + "max_matches", mcp.DefaultNumber(10), mcp.Description("Maximum number of matches to show per file (default: 10)"), ), @@ -448,19 +467,23 @@ func Register(s *server.MCPServer, workdir string) error { " Analyzes all Go source files in the specified directory."+ " Use skip_* flags to omit sections and reduce output size.", ), - mcp.WithString("directory", + mcp.WithString( + "directory", mcp.Required(), mcp.Description("Directory containing Go source files to analyze"), ), - mcp.WithBoolean("skip_dependencies", + mcp.WithBoolean( + "skip_dependencies", mcp.DefaultBool(false), mcp.Description("Skip the dependencies section"), ), - mcp.WithBoolean("skip_declarations", + mcp.WithBoolean( + "skip_declarations", mcp.DefaultBool(false), mcp.Description("Skip the declarations section"), ), - mcp.WithBoolean("skip_call_graph", + mcp.WithBoolean( + "skip_call_graph", mcp.DefaultBool(false), mcp.Description("Skip the call graph section (largest section)"), ), @@ -515,7 +538,8 @@ func Register(s *server.MCPServer, workdir string) error { s.AddTool(tool, mcp.NewTypedToolHandler(validateGoCode)) // Add Rust documentation search tool - tool = mcp.NewTool("search_rustdoc", + tool = mcp.NewTool( + "search_rustdoc", mcp.WithDescription("Search for Rust crates on docs.rs"), mcp.WithString( "query", @@ -545,7 +569,8 @@ func Register(s *server.MCPServer, workdir string) error { "Crate name or path (e.g., 'serde', 'serde/de', 'tokio/runtime')", ), ), - mcp.WithNumber("offset", + mcp.WithNumber( + "offset", mcp.DefaultNumber(0), mcp.Description("Line number to start reading from (0-based)"), ), @@ -583,7 +608,8 @@ func Register(s *server.MCPServer, workdir string) error { mcp.Required(), mcp.Description("Keyword to search for within the documentation"), ), - mcp.WithNumber("max_matches", + mcp.WithNumber( + "max_matches", mcp.DefaultNumber(10), mcp.Description("Maximum number of matches to return (default: 10)"), ), @@ -596,7 +622,8 @@ func Register(s *server.MCPServer, workdir string) error { s.AddTool(tool, mcp.NewTypedToolHandler(searchWithinRustDoc)) // Add Python documentation search tool - tool = mcp.NewTool("search_pydoc", + tool = mcp.NewTool( + "search_pydoc", mcp.WithDescription( "Search Python standard library modules on docs.python.org."+ " Searches the module index by name and description.", @@ -630,7 +657,8 @@ func Register(s *server.MCPServer, workdir string) error { "Python module name as used in imports (e.g., 'json', 'os.path', 'collections.abc')", ), ), - mcp.WithNumber("offset", + mcp.WithNumber( + "offset", mcp.DefaultNumber(0), mcp.Description("Line number to start reading from (0-based)"), ), @@ -668,7 +696,8 @@ func Register(s *server.MCPServer, workdir string) error { mcp.Required(), mcp.Description("Keyword to search for within the documentation"), ), - mcp.WithNumber("max_matches", + mcp.WithNumber( + "max_matches", mcp.DefaultNumber(10), mcp.Description("Maximum number of matches to return (default: 10)"), ), diff --git a/internal/subcmd/localsearch.go b/internal/subcmd/localsearch.go index 29a15a9..82425c1 100644 --- a/internal/subcmd/localsearch.go +++ b/internal/subcmd/localsearch.go @@ -77,7 +77,8 @@ func (p *LocalSearchCmd) Execute( } for _, result := range results { for _, match := range result.Matches { - fmt.Printf("Found file: %s\nMatch: %s (Line: %d)\n", + fmt.Printf( + "Found file: %s\nMatch: %s (Line: %d)\n", result.Filename, match.Text, match.LineNo, ) } diff --git a/internal/subcmd/validation.go b/internal/subcmd/validation.go index 407e12e..c97ff64 100644 --- a/internal/subcmd/validation.go +++ b/internal/subcmd/validation.go @@ -18,6 +18,7 @@ type ValidateCmd struct { func (*ValidateCmd) Name() string { return "validate" } func (*ValidateCmd) Synopsis() string { return "Validate Go code using multiple static analysis tools" } + func (*ValidateCmd) Usage() string { return `validate [-directory ]: Validate Go code using go vet, build checks, formatting validation, and module tidiness. diff --git a/pkg/dq/dq_test.go b/pkg/dq/dq_test.go index cc00e32..726e18a 100644 --- a/pkg/dq/dq_test.go +++ b/pkg/dq/dq_test.go @@ -314,7 +314,8 @@ func TestInnerTextWithFilter(t *testing.T) { t.Run("InnerText uses default filter", func(t *testing.T) { // InnerText and InnerTextWithFilter(DefaultNodeFilter) produce same result - assert.Equal(t, + assert.Equal( + t, dq.InnerText(div, true), dq.InnerTextWithFilter(div, true, dq.DefaultNodeFilter), )