From a284952f308df9647d0caf76cf89ccecc9ceeaf7 Mon Sep 17 00:00:00 2001 From: kolkov Date: Thu, 12 Mar 2026 21:56:29 +0300 Subject: [PATCH 1/2] fix(test): use Go overlay for instrumented files instead of file copying The `racedetector test` command copied .go files to a temp directory and ran `go test` from there. This broke projects that import internal/ packages from their own module (Go enforces internal/ imports only from within the module tree). Same issue affected //go:embed directives. Replace file-copying with Go's -overlay flag: - Instrumented files written to temp dir with overlay JSON mapping - Go compiles from original module tree (preserves internal/ and embed) - -modfile + -mod=mod for dependency injection without modifying go.mod - GOWORK=off for Go workspace mode compatibility - GONOSUMCHECK for racedetector module checksum skip Fixes test failures on projects like gogpu/ui with internal/ dependencies. --- CHANGELOG.md | 29 +++++ README.md | 2 + ROADMAP.md | 102 ++++++++++----- cmd/racedetector/build.go | 8 ++ cmd/racedetector/test.go | 253 ++++++++++++++++++-------------------- 5 files changed, 230 insertions(+), 164 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d202d2b..d4517f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.8.5] - 2026-03-12 + +### Fixed + +**`racedetector test` broken for projects with `internal/` packages** + +The `test` command copied `.go` files to a temporary directory and ran `go test` from there. This broke projects that import `internal/` packages from their own module, because Go enforces that `internal/` packages can only be imported from within the module tree — and a temp directory is not part of that tree. The same issue affected `//go:embed` directives (embedded assets not found in temp dir). + +**Root cause:** File-copying approach fundamentally incompatible with Go's `internal/` import restrictions and `//go:embed` path resolution. + +**Fix:** Replaced file-copying with Go's `-overlay` flag. Instrumented files are written to a temp directory, but the overlay JSON maps original file paths to their instrumented replacements. Go compiles from the original module tree (preserving `internal/` imports and `//go:embed`), reading only the overlaid files from the temp directory. + +**Changes:** +- `instrumentTestSources()` now creates overlay JSON + `race.mod` instead of copying files +- `runTests()` uses `-overlay`, `-modfile`, `-mod=mod` flags +- Added `GOWORK=off` env var (makes `-modfile` compatible with Go workspace mode) +- Added `GONOSUMCHECK` for racedetector module (skip checksum verification) +- Removed unused `copyFile` function + +**Technical details:** +- Overlay JSON format: `{"Replace": {"original/abs/path.go": "instrumented/abs/path.go"}}` +- `race.mod` = user's `go.mod` + `require github.com/kolkov/racedetector` +- `race.sum` = copy of user's `go.sum` (Go derives sum filename from modfile name) +- Original `go.mod`/`go.sum` are never modified + +Found while testing on [gogpu/ui](https://github.com/gogpu/ui) — a multi-package project with `internal/` dependencies. + +--- + ## [0.8.4] - 2025-12-19 ### Fixed diff --git a/README.md b/README.md index 10cc5b2..f377f37 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ go build -toolexec="racedetector toolexec" ./... All standard `go build`, `go run`, and `go test` flags are supported. +**v0.8.5+:** Full support for projects with `internal/` packages and `//go:embed` via Go overlay architecture. **v0.8.0+:** Escape analysis integration reduces false positives by 30-50%. --- @@ -154,6 +155,7 @@ Ported **359 test scenarios** from Go's official race detector test suite: - Performance overhead higher than ThreadSanitizer for some workloads - Struct field access via dot notation has limited coverage - Assembly optimization only on amd64/arm64 (fallback available) +- `build` command does not yet use overlay architecture (copies files to temp dir) ### Atomic Operations diff --git a/ROADMAP.md b/ROADMAP.md index 0867ef7..43679d7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,7 +3,7 @@ > **Strategic Advantage**: Proven FastTrack algorithm implementation without CGO dependency! > **Approach**: Scientific algorithm + Go best practices - eliminates C++ ThreadSanitizer dependency -**Last Updated**: 2025-12-19 | **Current Version**: v0.8.4 (STABLE) | **Strategy**: MVP → Optimization + Hardening → Advanced Optimizations → Assembly GID → Test Suite → Escape Analysis → Community Bug Fixes → Runtime Integration → Go Proposal | **Milestone**: v0.8.4 (Bug Fixes) → v0.9.0 (Polish) → v1.0.0 (Q1 2026) +**Last Updated**: 2026-03-12 | **Current Version**: v0.8.5 (STABLE) | **Strategy**: MVP → Optimization + Hardening → Advanced Optimizations → Assembly GID → Test Suite → Escape Analysis → Community Bug Fixes → **Deep Runtime Integration** → Go Proposal | **Milestone**: v0.8.5 (Overlay Fix) → v0.9.0 (Polish) → v0.6.0 (Runtime Integration) → v1.0.0 (Q2 2026) --- @@ -66,6 +66,8 @@ v0.8.3 (Issue #30: s.x++ fix) ✅ RELEASED 2025-12-19 ↓ (Struct field access instrumentation, by @thepudds) v0.8.4 (Issues #33, #34) ✅ RELEASED 2025-12-19 ↓ (Version prefix fix, type expression fix) +v0.8.5 (Overlay Architecture) ✅ RELEASED 2026-03-12 + ↓ (Fix internal/ imports and //go:embed for test command) v0.9.0 (Polish & Stabilization) → Final polish before v1.0 ↓ (1-2 weeks) v1.0.0 LTS → Production-ready with Go community adoption (Q1 2026) @@ -160,11 +162,11 @@ v1.0.0 LTS → Production-ready with Go community adoption (Q1 2026) --- -## 📊 Current Status (v0.8.3 Stable) +## 📊 Current Status (v0.8.5 Stable) -**Phase**: ✅ Community Bug Fixes Complete! -**Detector**: Production-grade with critical bug fixes from @thepudds! ⚡ -**AST Instrumentation**: Complete with escape analysis and struct field support! ✅ +**Phase**: ✅ Overlay Architecture for `test` command! +**Detector**: Production-grade, tested on real-world multi-package projects! ⚡ +**AST Instrumentation**: Complete with escape analysis, overlay-based test workflow! ✅ **What Works**: - ✅ `racedetector build` command (drop-in for `go build`) @@ -313,7 +315,27 @@ func update(s *S) { --- -### **v0.9.0 - Polish & Stabilization** (January 2026) [PLANNED] +### **v0.8.5 - Overlay Architecture** (March 2026) [RELEASED! ✅] + +**Goal**: Fix `racedetector test` for projects with `internal/` packages and `//go:embed` + +**Duration**: 1 day (March 12, 2026) + +**Status**: ✅ RELEASED + +**Root Cause**: The `test` command copied `.go` files to a temp directory. Go enforces that `internal/` packages can only be imported from within the module tree — temp dir breaks this. Same issue for `//go:embed` (assets not found). + +**Fix**: Replaced file-copying with Go's `-overlay` flag: +- Instrumented files written to temp dir with overlay JSON mapping +- Go compiles from original module tree (preserves `internal/` and `//go:embed`) +- `-modfile` + `-mod=mod` for dependency injection without modifying user's `go.mod` +- `GOWORK=off` for compatibility with Go workspace mode + +**Validated on**: [gogpu/ui](https://github.com/gogpu/ui) — multi-package project with `internal/` dependencies. + +--- + +### **v0.9.0 - Polish & Stabilization** (Q2 2026) [PLANNED] **Goal**: Final polish before v1.0.0 LTS release @@ -326,6 +348,7 @@ func update(s *S) { 2. **Edge case fixes** - Any remaining false positive patterns - Improved error messages + - Port overlay architecture to `build` command 3. **Performance profiling** - Benchmark against ThreadSanitizer @@ -335,7 +358,7 @@ func update(s *S) { - Address reported issues - Feature requests evaluation -**Target**: January 2026 +**Target**: Q2 2026 --- @@ -405,31 +428,54 @@ func update(s *S) { --- -### **v0.6.0 - Go Runtime Integration** (January 2026) [PLANNED] +### **v0.6.0 - Go Runtime Integration** (Q2 2026) [PLANNED] + +**Goal**: Deep compiler/runtime integration per @dvyukov's guidance -**Goal**: Replace ThreadSanitizer in Go toolchain +**Context**: Per [golang/go#6508](https://github.com/golang/go/issues/6508#issuecomment-3681427164): +> "If you want this to be a replacement for the current race detector, then this need to be +> integrated into the compiler/runtime -- use the same compiler instrumentation, implement +> race annotations, etc, rather than being on the side." -**Duration**: 1-2 months (including testing) +**Why Deep Integration is Required**: +- AST-based instrumentation doesn't see stdlib sync primitives (#36) +- `iter.Pull`, channels, etc. use `internal/race.Acquire/Release` we can't intercept +- Results in false positives on properly synchronized code (#37) +- Tracked in: #38 (Deep Integration Roadmap) -**Note**: Renumbered from v0.4.0 to v0.6.0 after adding v0.5.0 (Assembly GID) +**Duration**: 2-3 months (research + implementation + testing) **Planned Work**: -1. **Runtime Integration** - - Replace `runtime/race/*.syso` with pure Go implementation - - Hook into `go build -race` flag - - Maintain API compatibility with existing instrumentation - -2. **Compiler Coordination** - - Work with existing `cmd/compile/internal/walk/race.go` - - Ensure instrumentation calls match new runtime - - Test with official Go test suite - -3. **Validation** - - Run official Go race detector tests - - Benchmark against ThreadSanitizer - - Cross-platform testing (Linux, macOS, Windows) -**Target**: January 31, 2026 +1. **Research Phase** (P0) + - [ ] Study `cmd/compile/internal/walk/race.go` instrumentation points + - [ ] Study `runtime/race.go` and `runtime/race/*.syso` API + - [ ] Understand stdlib race annotations (`internal/race`) + - [ ] Benchmark comparison: our detector vs TSAN + +2. **Runtime Integration** (P1) + - [ ] Implement pure-Go runtime matching TSAN API + - [ ] Replace `runtime/race/*.syso` with our implementation + - [ ] Hook into `go build -race` flag + - [ ] Handle `race.Acquire/Release/Read/Write` from stdlib + +3. **Compiler Coordination** (P1) + - [ ] Work with existing compiler instrumentation + - [ ] Ensure all race annotations flow to our runtime + - [ ] Test with official Go test suite + +4. **Validation** (P2) + - [ ] Run official Go race detector tests (359+ scenarios) + - [ ] Benchmark against ThreadSanitizer + - [ ] Cross-platform testing (Linux, macOS, Windows) + - [ ] No false positives on iter.Pull patterns + +**Target**: Q2 2026 + +**Related Issues**: +- #36 - stdlib sync limitation (root cause) +- #37 - iter.Pull heuristic (temporary workaround) +- #38 - Deep integration roadmap (strategic) --- @@ -518,5 +564,5 @@ func update(s *S) { --- -*Version 1.6 (Updated 2025-12-19)* -*Current: v0.8.3 (STABLE - Community Bug Fixes) | Next: v0.9.0 (Polish) | Target: v1.0.0 LTS (Q1 2026)* +*Version 1.7 (Updated 2026-03-12)* +*Current: v0.8.5 (STABLE - Overlay Architecture) | Next: v0.9.0 (Polish) | Target: v1.0.0 LTS (Q2 2026)* diff --git a/cmd/racedetector/build.go b/cmd/racedetector/build.go index 1d932a7..be69674 100644 --- a/cmd/racedetector/build.go +++ b/cmd/racedetector/build.go @@ -215,6 +215,14 @@ type workspace struct { // Original source directory (where original .go files come from) // Used to find original go.mod for replace directives originalSourceDir string + + // overlayPath is the path to the overlay JSON file (used by test command). + // When set, go test uses -overlay flag instead of compiling from srcDir. + overlayPath string + + // modfilePath is the path to a modified go.mod with racedetector dependency. + // Used with -modfile flag so the original go.mod is not modified. + modfilePath string } // createWorkspace creates a temporary workspace for building instrumented code. diff --git a/cmd/racedetector/test.go b/cmd/racedetector/test.go index 5d8b5b9..09fad2e 100644 --- a/cmd/racedetector/test.go +++ b/cmd/racedetector/test.go @@ -2,6 +2,7 @@ package main import ( + "encoding/json" "errors" "fmt" "os" @@ -86,11 +87,7 @@ func testCommand(args []string) { os.Exit(1) } - // Setup runtime linking - if err := workspace.setupRuntimeLinking(); err != nil { - fmt.Fprintf(os.Stderr, "Error setting up runtime: %v\n", err) - os.Exit(1) - } + // Runtime linking is handled via -modfile flag (no setupRuntimeLinking needed) // Run tests exitCode := runTests(workspace, config) @@ -209,7 +206,13 @@ func testFlagNeedsValue(flag string) bool { return false } -// instrumentTestSources instruments all source files including test files. +// instrumentTestSources instruments source files and creates a Go overlay. +// +// Instead of copying files to a temp workspace (which breaks internal/ imports +// and //go:embed), this creates a Go overlay JSON that maps original files to +// their instrumented replacements. The Go toolchain reads all other files from +// the original module tree, so internal packages, embedded assets, and all +// imports resolve correctly. func instrumentTestSources(config *testConfig, workspace *workspace) error { // Resolve package patterns to actual directories dirs, err := resolvePackagePatterns(config.packages, config.workDir) @@ -221,106 +224,103 @@ func instrumentTestSources(config *testConfig, workspace *workspace) error { return fmt.Errorf("no packages found matching patterns: %v", config.packages) } - // Store original source directory for go.mod replace directive handling workspace.originalSourceDir = config.workDir - // Collect all .go files (including test files) - var allGoFiles []string - for _, dir := range dirs { - goFiles, err := collectTestGoFiles(dir) - if err != nil { - return fmt.Errorf("failed to collect files from %s: %w", dir, err) - } - allGoFiles = append(allGoFiles, goFiles...) + // Create directory for instrumented files + instrDir := filepath.Join(workspace.dir, "instr") + if err := os.MkdirAll(instrDir, 0755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) } - if len(allGoFiles) == 0 { - return fmt.Errorf("no Go source files found") - } + // Build overlay: original absolute path → instrumented absolute path + replace := make(map[string]string) - // Instrument each file - for _, srcPath := range allGoFiles { - // Instrument the file - result, err := instrument.InstrumentFile(srcPath, nil) + for _, dir := range dirs { + goFiles, err := collectTestGoFiles(dir) if err != nil { - return fmt.Errorf("failed to instrument %s: %w", srcPath, err) + return fmt.Errorf("failed to collect files from %s: %w", dir, err) } - // Determine output path in workspace - // Preserve relative path structure for package resolution - relPath, err := filepath.Rel(config.workDir, srcPath) - if err != nil { - // Fallback to just filename - relPath = filepath.Base(srcPath) - } + for _, srcPath := range goFiles { + result, err := instrument.InstrumentFile(srcPath, nil) + if err != nil { + return fmt.Errorf("failed to instrument %s: %w", srcPath, err) + } - outPath := filepath.Join(workspace.srcDir, relPath) + relPath, relErr := filepath.Rel(config.workDir, srcPath) + if relErr != nil { + relPath = filepath.Base(srcPath) + } - // Create parent directories if needed - if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { - return fmt.Errorf("failed to create directory for %s: %w", outPath, err) - } + // CGO files: skip instrumentation, no overlay entry needed + if result.Stats.CGOSkipped { + fmt.Printf("Skipped (CGO): %s\n", relPath) + continue + } - // Handle CGO files: copy unchanged instead of instrumenting - if result.Stats.CGOSkipped { - // Read original file and copy to workspace unchanged - originalCode, readErr := os.ReadFile(srcPath) - if readErr != nil { - return fmt.Errorf("failed to read CGO file %s: %w", srcPath, readErr) + // Write instrumented file to temp dir, preserving relative path + outPath := filepath.Join(instrDir, relPath) + if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { + return fmt.Errorf("failed to create directory for %s: %w", outPath, err) } - if err := os.WriteFile(outPath, originalCode, 0644); err != nil { - return fmt.Errorf("failed to copy CGO file %s: %w", outPath, err) + if err := os.WriteFile(outPath, []byte(result.Code), 0644); err != nil { + return fmt.Errorf("failed to write %s: %w", outPath, err) } - fmt.Printf("Skipped (CGO): %s\n", relPath) - continue - } - - // Write instrumented code to workspace - if err := os.WriteFile(outPath, []byte(result.Code), 0644); err != nil { - return fmt.Errorf("failed to write instrumented file %s: %w", outPath, err) - } - // Print instrumentation info - fmt.Printf("Instrumented: %s\n", relPath) - if config.verbose { - stats := result.Stats - if stats.Total() > 0 { - fmt.Printf(" - %d writes, %d reads instrumented\n", - stats.WritesInstrumented, stats.ReadsInstrumented) + // Overlay requires absolute paths + absSrc, _ := filepath.Abs(srcPath) + absOut, _ := filepath.Abs(outPath) + replace[absSrc] = absOut + + fmt.Printf("Instrumented: %s\n", relPath) + if config.verbose { + stats := result.Stats + if stats.Total() > 0 { + fmt.Printf(" - %d writes, %d reads instrumented\n", + stats.WritesInstrumented, stats.ReadsInstrumented) + } } } } - // Copy go.mod to srcDir and add racedetector dependency - // The replace directive points to ./src, which must be a valid module - // AND instrumented code imports github.com/kolkov/racedetector/race + // Create modified go.mod with racedetector dependency (for -modfile flag). + // We use -modfile instead of overlaying go.mod because the overlaid go.mod + // would require "go mod tidy" (writes to original files through overlay). + // With -modfile, Go reads/writes race.mod and race.sum in the temp dir, + // leaving the user's go.mod/go.sum untouched. goModSrc := filepath.Join(config.workDir, "go.mod") - if _, err := os.Stat(goModSrc); err == nil { - goModDst := filepath.Join(workspace.srcDir, "go.mod") - data, err := os.ReadFile(goModSrc) - if err == nil { - // Append racedetector require to the go.mod - modContent := string(data) - modContent += fmt.Sprintf("\nrequire github.com/kolkov/racedetector %s\n", runtime.GetVersion()) - _ = os.WriteFile(goModDst, []byte(modContent), 0644) + if data, err := os.ReadFile(goModSrc); err == nil { + version := runtime.GetVersion() + modContent := string(data) + fmt.Sprintf("\nrequire github.com/kolkov/racedetector %s\n", version) + + raceModPath := filepath.Join(workspace.dir, "race.mod") + if err := os.WriteFile(raceModPath, []byte(modContent), 0644); err != nil { + return fmt.Errorf("failed to write race.mod: %w", err) } - } + workspace.modfilePath = raceModPath - // Also copy go.sum if exists - goSumSrc := filepath.Join(config.workDir, "go.sum") - if _, err := os.Stat(goSumSrc); err == nil { - goSumDst := filepath.Join(workspace.srcDir, "go.sum") - data, err := os.ReadFile(goSumSrc) - if err == nil { - _ = os.WriteFile(goSumDst, data, 0644) + // Copy go.sum → race.sum (Go derives sum path from modfile name) + goSumSrc := filepath.Join(config.workDir, "go.sum") + raceSumPath := filepath.Join(workspace.dir, "race.sum") + if sumData, sumErr := os.ReadFile(goSumSrc); sumErr == nil { + _ = os.WriteFile(raceSumPath, sumData, 0644) } } - // Run go mod tidy in srcDir to update go.sum with racedetector dependency - tidyCmd := exec.Command("go", "mod", "tidy") - tidyCmd.Dir = workspace.srcDir - // Ignore errors - tidy may complain about missing imports that we'll resolve later - _ = tidyCmd.Run() + // Write overlay JSON (must be after all entries are added to replace map) + type overlayJSON struct { + Replace map[string]string `json:"Replace"` + } + overlayData, err := json.Marshal(overlayJSON{Replace: replace}) + if err != nil { + return fmt.Errorf("failed to marshal overlay: %w", err) + } + workspace.overlayPath = filepath.Join(workspace.dir, "overlay.json") + if err := os.WriteFile(workspace.overlayPath, overlayData, 0644); err != nil { + return fmt.Errorf("failed to write overlay: %w", err) + } + + return nil } @@ -426,11 +426,28 @@ func collectTestGoFiles(dir string) ([]string, error) { return goFiles, nil } -// runTests executes 'go test' in the workspace with instrumented code. +// runTests executes 'go test' with the overlay from the original module directory. +// +// Instead of running from a temp workspace, this runs from the original project +// directory with -overlay and -modfile flags. This ensures all imports (including +// internal/ packages) and //go:embed directives resolve correctly. func runTests(workspace *workspace, config *testConfig) int { - // Prepare go test command args := []string{"test"} + // Use overlay for instrumented files + if workspace.overlayPath != "" { + args = append(args, "-overlay="+workspace.overlayPath) + } + + // Use modified go.mod with racedetector dependency. + // -mod=mod allows Go to update race.sum for new dependencies. + // GOWORK=off disables workspace mode (incompatible with -modfile). + // All writes go to race.mod/race.sum in the temp dir, not user files. + if workspace.modfilePath != "" { + args = append(args, "-modfile="+workspace.modfilePath) + args = append(args, "-mod=mod") + } + // Add test flags args = append(args, config.testFlags...) @@ -438,76 +455,40 @@ func runTests(workspace *workspace, config *testConfig) int { runtimeFlags := runtime.BuildFlags() args = append(args, runtimeFlags...) - // Handle -o flag: compile to temp location, then copy to user's desired path - var tempOutputPath string + // Handle -o flag (works directly since we run from original workDir) if config.outputFile != "" { - // When using -c -o, we compile to workspace then copy to user path - tempOutputPath = filepath.Join(workspace.srcDir, "test.exe") - args = append(args, "-o", tempOutputPath) + outputPath := config.outputFile + if !filepath.IsAbs(outputPath) { + outputPath = filepath.Join(config.workDir, outputPath) + } + args = append(args, "-o", outputPath) } - // Test the current package (instrumented sources are in workspace) - args = append(args, "./...") + // Use original package patterns (resolved from the real module tree) + args = append(args, config.packages...) - // Run go test + // Run go test from the ORIGINAL project directory cmd := exec.Command("go", args...) - cmd.Dir = workspace.srcDir + cmd.Dir = config.workDir cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + // GOWORK=off: disable workspace mode which is incompatible with -modfile. + // GONOSUMCHECK: skip checksum verification for racedetector module. + cmd.Env = append(os.Environ(), + "GOWORK=off", + "GONOSUMCHECK=github.com/kolkov/racedetector", + ) if err := cmd.Run(); err != nil { - // Check if it's an exit error var exitErr *exec.ExitError if errors.As(err, &exitErr) { return exitErr.ExitCode() } - // Other error (failed to start, etc.) fmt.Fprintf(os.Stderr, "Error executing tests: %v\n", err) return 1 } - // If -o was specified, copy the compiled binary to user's desired location - if config.outputFile != "" && tempOutputPath != "" { - // Make output path absolute if relative - outputPath := config.outputFile - if !filepath.IsAbs(outputPath) { - outputPath = filepath.Join(config.workDir, outputPath) - } - - // Copy the binary - if err := copyFile(tempOutputPath, outputPath); err != nil { - fmt.Fprintf(os.Stderr, "Error copying test binary: %v\n", err) - return 1 - } - fmt.Printf("Test binary written to: %s\n", config.outputFile) - } - return 0 } -// copyFile copies a file from src to dst. -func copyFile(src, dst string) error { - data, err := os.ReadFile(src) - if err != nil { - return fmt.Errorf("failed to read %s: %w", src, err) - } - - // Get source file permissions - srcInfo, err := os.Stat(src) - if err != nil { - return fmt.Errorf("failed to stat %s: %w", src, err) - } - - // Create destination directory if needed - if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { - return fmt.Errorf("failed to create directory for %s: %w", dst, err) - } - - // Write with same permissions as source (executable) - if err := os.WriteFile(dst, data, srcInfo.Mode()); err != nil { - return fmt.Errorf("failed to write %s: %w", dst, err) - } - - return nil -} From 1080989e18ee1117dd27f9865d68661dc355269c Mon Sep 17 00:00:00 2001 From: kolkov Date: Thu, 12 Mar 2026 22:14:54 +0300 Subject: [PATCH 2/2] fix(lint): resolve golangci-lint v2.11 issues Updated golangci-lint catches new issues: - Remove unused //nolint:gosec directives (G115, G602 no longer flagged) - Remove unused //nolint:prealloc directives - Use fmt.Fprintf instead of WriteString(fmt.Sprintf(...)) (staticcheck QF1012) - Fix getcallerpc() to capture PC from correct return position - Preallocate stack slice in test - Fix test.go formatting (trailing blank lines) --- cmd/racedetector/runtime/link.go | 22 +++++++++++----------- cmd/racedetector/test.go | 3 --- examples/channel_sync/main.go | 2 -- internal/race/api/go_race_patterns_test.go | 2 +- internal/race/api/goid_generic.go | 1 - internal/race/api/race.go | 7 +++---- internal/race/epoch/epoch.go | 1 - 7 files changed, 15 insertions(+), 23 deletions(-) diff --git a/cmd/racedetector/runtime/link.go b/cmd/racedetector/runtime/link.go index f8ef058..a8ffa75 100644 --- a/cmd/racedetector/runtime/link.go +++ b/cmd/racedetector/runtime/link.go @@ -235,10 +235,10 @@ func ModFileOverlay(tempDir, sourceDir string) (string, error) { if err == nil { // Development mode - use local replace directive content.WriteString("require github.com/kolkov/racedetector v0.0.0\n\n") - content.WriteString(fmt.Sprintf("replace github.com/kolkov/racedetector => %s\n", projectRoot)) + fmt.Fprintf(&content, "replace github.com/kolkov/racedetector => %s\n", projectRoot) } else { // Published mode (CI, installed via go install) - require published package - content.WriteString(fmt.Sprintf("require github.com/kolkov/racedetector %s\n", GetVersion())) + fmt.Fprintf(&content, "require github.com/kolkov/racedetector %s\n", GetVersion()) } // Find and parse original project's go.mod to: @@ -252,7 +252,7 @@ func ModFileOverlay(tempDir, sourceDir string) (string, error) { // Don't add replace for our own module (racedetector) - it's already handled above if originalModuleName != "" && originalModuleName != "github.com/kolkov/racedetector" { content.WriteString("\n// Replace original module with instrumented sources:\n") - content.WriteString(fmt.Sprintf("replace %s => ./src\n", originalModuleName)) + fmt.Fprintf(&content, "replace %s => ./src\n", originalModuleName) } // Copy existing replace directives from original go.mod @@ -343,20 +343,20 @@ func extractReplaceDirectives(goModPath string) string { if rep.Old.Version != "" { // Replace specific version: replace foo v1.0.0 => bar if rep.New.Version != "" { - result.WriteString(fmt.Sprintf("replace %s %s => %s %s\n", - rep.Old.Path, rep.Old.Version, newPath, rep.New.Version)) + fmt.Fprintf(&result, "replace %s %s => %s %s\n", + rep.Old.Path, rep.Old.Version, newPath, rep.New.Version) } else { - result.WriteString(fmt.Sprintf("replace %s %s => %s\n", - rep.Old.Path, rep.Old.Version, newPath)) + fmt.Fprintf(&result, "replace %s %s => %s\n", + rep.Old.Path, rep.Old.Version, newPath) } } else { // Replace all versions: replace foo => bar if rep.New.Version != "" { - result.WriteString(fmt.Sprintf("replace %s => %s %s\n", - rep.Old.Path, newPath, rep.New.Version)) + fmt.Fprintf(&result, "replace %s => %s %s\n", + rep.Old.Path, newPath, rep.New.Version) } else { - result.WriteString(fmt.Sprintf("replace %s => %s\n", - rep.Old.Path, newPath)) + fmt.Fprintf(&result, "replace %s => %s\n", + rep.Old.Path, newPath) } } } diff --git a/cmd/racedetector/test.go b/cmd/racedetector/test.go index 09fad2e..6c8c157 100644 --- a/cmd/racedetector/test.go +++ b/cmd/racedetector/test.go @@ -320,8 +320,6 @@ func instrumentTestSources(config *testConfig, workspace *workspace) error { return fmt.Errorf("failed to write overlay: %w", err) } - - return nil } @@ -491,4 +489,3 @@ func runTests(workspace *workspace, config *testConfig) int { return 0 } - diff --git a/examples/channel_sync/main.go b/examples/channel_sync/main.go index 60cfb7d..d6fea5a 100644 --- a/examples/channel_sync/main.go +++ b/examples/channel_sync/main.go @@ -119,7 +119,6 @@ func demo2WorkerPool() { // Receive results (main goroutine) fmt.Println("Collector: Waiting for results...") - //nolint:prealloc // Size unknown at compile time, dynamic allocation acceptable var allResults []int for result := range results { // Safe: synchronized by channel allResults = append(allResults, result) @@ -188,7 +187,6 @@ func demo3FanOutFanIn() { // Collect merged results fmt.Println("Output: Collecting results...") - //nolint:prealloc // Size unknown at compile time, dynamic allocation acceptable var results []int for val := range merged { // Safe: channel synchronization results = append(results, val) diff --git a/internal/race/api/go_race_patterns_test.go b/internal/race/api/go_race_patterns_test.go index 6fa24b1..08f521c 100644 --- a/internal/race/api/go_race_patterns_test.go +++ b/internal/race/api/go_race_patterns_test.go @@ -1365,7 +1365,7 @@ func TestGoNoRace_StackPushPop(t *testing.T) { type stack []int - var s stack + s := make(stack, 0, 1) addr := uintptr(unsafe.Pointer(&s)) ch := make(chan bool, 1) var mu sync.Mutex diff --git a/internal/race/api/goid_generic.go b/internal/race/api/goid_generic.go index cf4bfd6..749d5ed 100644 --- a/internal/race/api/goid_generic.go +++ b/internal/race/api/goid_generic.go @@ -99,7 +99,6 @@ func parseGID(buf []byte) int64 { // Format after prefix: "123 [running]:..." var gid int64 for i := prefixLen; i < len(buf); i++ { - //nolint:gosec // G602: i is always < len(buf) due to loop condition c := buf[i] if c >= '0' && c <= '9' { gid = gid*10 + int64(c-'0') diff --git a/internal/race/api/race.go b/internal/race/api/race.go index 6317159..5118cea 100644 --- a/internal/race/api/race.go +++ b/internal/race/api/race.go @@ -925,8 +925,7 @@ func initTIDPool() { // We want ascending allocation, so we reverse the order. freeTIDs = make([]uint16, 65536) for i := 0; i < 65536; i++ { - //nolint:gosec // G115: Safe conversion, i is always < 256 - freeTIDs[i] = uint16(i) // Stack order: [0, 1, 2, ..., 255] + freeTIDs[i] = uint16(i) } } @@ -1196,11 +1195,11 @@ func getcallerpc() uintptr { // - getcallerpc (this function) - skip 0 // - raceread/racewrite - skip 1 // - returns: instrumented code - skip 2 - _, _, pc, ok := runtime.Caller(2) + pc, _, _, ok := runtime.Caller(2) if !ok { return 0 } - return uintptr(pc) + return pc } // Enable turns on race detection. diff --git a/internal/race/epoch/epoch.go b/internal/race/epoch/epoch.go index 543d715..38f2310 100644 --- a/internal/race/epoch/epoch.go +++ b/internal/race/epoch/epoch.go @@ -125,7 +125,6 @@ func NewEpoch(tid uint16, clock uint64) Epoch { // //go:nosplit func (e Epoch) Decode() (tid uint16, clock uint64) { - //nolint:gosec // G115: Intentional truncation to extract top 16 bits as TID. tid = uint16(e >> ClockBits) clock = uint64(e) & ClockMask return