diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-03 17:15:04 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-03 17:15:04 +0300 |
| commit | 23ea1749d303c1263e8a3d2393dee95d7914ddf7 (patch) | |
| tree | 39ed6756070345420ca6efa7d414575b0c9efd94 | |
| parent | d2ee730256c6ecfad7dd2a164d2bb822236b7b44 (diff) | |
chore: add scripts/scan_uncovered.go and generated UNITTESTSTOADD.md with recommendations for missing tests
| -rw-r--r-- | UNITTESTSTOADD.md | 511 | ||||
| -rw-r--r-- | scripts/scan_uncovered.go | 187 |
2 files changed, 698 insertions, 0 deletions
diff --git a/UNITTESTSTOADD.md b/UNITTESTSTOADD.md new file mode 100644 index 0000000..2ebcd02 --- /dev/null +++ b/UNITTESTSTOADD.md @@ -0,0 +1,511 @@ +# Unit tests to consider adding + +This report lists functions that do not appear to be referenced in test files in the same package directory. Recommendations are heuristic. + +## main (cmd/hexai-lsp) + +- cmd/hexai-lsp/main.go — func main + - exported: false complexity: 5 has-control: true + - recommendation: add test (non-trivial logic) + +## hexaicli (internal/hexaicli) + +- internal/hexaicli/run.go — func Run + - exported: true complexity: 5 has-control: true + - recommendation: add test (exported) +- internal/hexaicli/run.go — func newClientFromConfig + - exported: false complexity: 6 has-control: true + - recommendation: add test (non-trivial logic) + +## llm (internal/llm) + +- internal/llm/copilot.go — method (copilotClient).Chat + - exported: true complexity: 24 has-control: true + - recommendation: add test (exported) +- internal/llm/copilot.go — method (copilotClient).CodeCompletion + - exported: true complexity: 18 has-control: true + - recommendation: add test (exported) +- internal/llm/copilot.go — method (copilotClient).DefaultModel + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- internal/llm/copilot.go — method (copilotClient).Name + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- internal/llm/copilot.go — func decodeCopilotChat + - exported: false complexity: 3 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/copilot.go — method (copilotClient).ensureSession + - exported: false complexity: 19 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/copilot.go — func handleCopilotNon2xx + - exported: false complexity: 6 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/copilot.go — method (copilotClient).headersChat + - exported: false complexity: 3 has-control: false + - recommendation: add test (non-trivial logic) +- internal/llm/copilot.go — method (copilotClient).headersGhost + - exported: false complexity: 3 has-control: false + - recommendation: add test (non-trivial logic) +- internal/llm/copilot.go — func newCopilot + - exported: false complexity: 3 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/copilot.go — func parseInt64 + - exported: false complexity: 3 has-control: false + - recommendation: add test (non-trivial logic) +- internal/llm/copilot.go — func parseJWTExp + - exported: false complexity: 8 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/copilot.go — method (copilotClient).postJSON + - exported: false complexity: 4 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/copilot.go — func randHex + - exported: false complexity: 4 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/ollama.go — method (ollamaClient).Chat + - exported: true complexity: 20 has-control: true + - recommendation: add test (exported) +- internal/llm/ollama.go — method (ollamaClient).ChatStream + - exported: true complexity: 18 has-control: true + - recommendation: add test (exported) +- internal/llm/ollama.go — method (ollamaClient).DefaultModel + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- internal/llm/ollama.go — method (ollamaClient).Name + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- internal/llm/ollama.go — method (ollamaClient).doJSON + - exported: false complexity: 4 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/ollama.go — func handleOllamaNon2xx + - exported: false complexity: 6 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/ollama.go — method (ollamaClient).logStart + - exported: false complexity: 3 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/ollama.go — func newOllama + - exported: false complexity: 3 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/openai.go — method (openAIClient).Chat + - exported: true complexity: 21 has-control: true + - recommendation: add test (exported) +- internal/llm/openai.go — method (openAIClient).ChatStream + - exported: true complexity: 18 has-control: true + - recommendation: add test (exported) +- internal/llm/openai.go — method (openAIClient).DefaultModel + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- internal/llm/openai.go — method (openAIClient).Name + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- internal/llm/openai.go — func decodeOpenAIChat + - exported: false complexity: 3 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/openai.go — method (openAIClient).doJSON + - exported: false complexity: 5 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/openai.go — method (openAIClient).doJSONWithAccept + - exported: false complexity: 6 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/openai.go — method (openAIClient).logStart + - exported: false complexity: 3 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/openai.go — method (openAIClient).logf + - exported: false complexity: 1 has-control: false + - recommendation: optional (helper/trivial) +- internal/llm/openai.go — func newOpenAI + - exported: false complexity: 3 has-control: true + - recommendation: add test (non-trivial logic) +- internal/llm/provider.go — func NewFromConfig + - exported: true complexity: 3 has-control: true + - recommendation: add test (exported) +- internal/llm/provider.go — func WithMaxTokens + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- internal/llm/provider.go — func WithModel + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- internal/llm/provider.go — func WithStop + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- internal/llm/provider.go — func WithTemperature + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- internal/llm/util.go — func nilStringErr + - exported: false complexity: 1 has-control: false + - recommendation: optional (helper/trivial) + +## lsp (internal/lsp) + +- internal/lsp/context.go — method (Server).fullFileContext + - exported: false complexity: 3 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/document.go — method (Server).deleteDocument + - exported: false complexity: 3 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/document.go — method (Server).getDocument + - exported: false complexity: 3 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/document.go — func hasAny + - exported: false complexity: 2 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/document.go — method (Server).isDefiningNewFunction + - exported: false complexity: 10 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers.go — method (Server).compCacheTouchLocked + - exported: false complexity: 4 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers.go — method (Server).completionCacheGet + - exported: false complexity: 6 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers.go — method (Server).completionCacheKey + - exported: false complexity: 13 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers.go — method (Server).completionCachePut + - exported: false complexity: 6 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers.go — method (Server).fallbackCompletionItems + - exported: false complexity: 1 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers.go — method (Server).handle + - exported: false complexity: 2 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers.go — func instructionFromSelection + - exported: false complexity: 3 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers.go — method (Server).isTriggerEvent + - exported: false complexity: 7 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers.go — method (Server).makeCompletionItems + - exported: false complexity: 6 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers.go — method (Server).reply + - exported: false complexity: 2 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers_codeaction.go — method (Server).buildDocumentCodeAction + - exported: false complexity: 6 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_codeaction.go — method (Server).buildGoUnitTestCodeAction + - exported: false complexity: 9 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_codeaction.go — func deriveGoFuncName + - exported: false complexity: 7 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_codeaction.go — method (Server).diagnosticsInRange + - exported: false complexity: 7 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_codeaction.go — func exportName + - exported: false complexity: 4 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_codeaction.go — func fileExists + - exported: false complexity: 2 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_codeaction.go — func findGoFunctionAtLine + - exported: false complexity: 10 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_codeaction.go — method (Server).generateGoTestFunction + - exported: false complexity: 4 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_codeaction.go — func greaterPos + - exported: false complexity: 2 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_codeaction.go — method (Server).handleCodeAction + - exported: false complexity: 11 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_codeaction.go — method (Server).handleCodeActionResolve + - exported: false complexity: 4 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_codeaction.go — func lessPos + - exported: false complexity: 2 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_codeaction.go — method (Server).loadFileText + - exported: false complexity: 6 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_codeaction.go — func parseGoPackageName + - exported: false complexity: 2 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_codeaction.go — func rangesOverlap + - exported: false complexity: 5 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_codeaction.go — method (Server).resolveGoTest + - exported: false complexity: 30 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_completion.go — method (Server).buildCompletionMessages + - exported: false complexity: 5 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_completion.go — method (Server).buildDocString + - exported: false complexity: 1 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers_completion.go — func extractTriggerInfo + - exported: false complexity: 4 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_completion.go — method (Server).handleCompletion + - exported: false complexity: 5 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_completion.go — method (Server).logCompletionContext + - exported: false complexity: 1 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers_completion.go — func parseManualInvoke + - exported: false complexity: 4 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_completion.go — method (Server).postProcessCompletion + - exported: false complexity: 6 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_completion.go — method (Server).prefixHeuristicAllows + - exported: false complexity: 11 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_completion.go — method (Server).shouldSuppressForChatTriggerEOL + - exported: false complexity: 2 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_completion.go — method (Server).tryProviderNativeCompletion + - exported: false complexity: 18 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_completion.go — method (Server).waitForDebounce + - exported: false complexity: 3 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_completion.go — method (Server).waitForThrottle + - exported: false complexity: 4 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_document.go — method (Server).applyChatEdits + - exported: false complexity: 10 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_document.go — method (Server).buildChatHistory + - exported: false complexity: 10 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_document.go — method (Server).clientApplyEdit + - exported: false complexity: 6 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers_document.go — method (Server).clientShowDocument + - exported: false complexity: 9 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers_document.go — method (Server).deferShowDocument + - exported: false complexity: 1 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers_document.go — method (Server).detectAndHandleChat + - exported: false complexity: 4 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_document.go — method (Server).docBeforeAfter + - exported: false complexity: 16 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_document.go — method (Server).handleDidChange + - exported: false complexity: 2 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_document.go — method (Server).handleDidClose + - exported: false complexity: 2 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_document.go — method (Server).handleDidOpen + - exported: false complexity: 2 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_document.go — method (Server).nextReqID + - exported: false complexity: 6 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers_document.go — func stripTrailingTrigger + - exported: false complexity: 6 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_execute.go — method (Server).handleExecuteCommand + - exported: false complexity: 3 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_init.go — method (Server).handleExit + - exported: false complexity: 2 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers_init.go — method (Server).handleInitialize + - exported: false complexity: 4 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_init.go — method (Server).handleInitialized + - exported: false complexity: 1 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers_init.go — method (Server).handleShutdown + - exported: false complexity: 1 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers_utils.go — func applyIndent + - exported: false complexity: 4 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_utils.go — func buildPrompts + - exported: false complexity: 4 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_utils.go — method (Server).collectPromptRemovalEdits + - exported: false complexity: 5 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_utils.go — func computeTextEditAndFilter + - exported: false complexity: 5 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_utils.go — func computeWordStart + - exported: false complexity: 3 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_utils.go — func extractRangeText + - exported: false complexity: 13 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_utils.go — func inParamList + - exported: false complexity: 4 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_utils.go — method (Server).incRecvCounters + - exported: false complexity: 4 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers_utils.go — method (Server).incSentCounters + - exported: false complexity: 4 has-control: false + - recommendation: add test (exported receiver) +- internal/lsp/handlers_utils.go — func isBareDoubleSemicolon + - exported: false complexity: 5 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_utils.go — func isIdentBoundary + - exported: false complexity: 1 has-control: false + - recommendation: optional (helper/trivial) +- internal/lsp/handlers_utils.go — func isIdentChar + - exported: false complexity: 1 has-control: false + - recommendation: optional (helper/trivial) +- internal/lsp/handlers_utils.go — func labelForCompletion + - exported: false complexity: 3 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_utils.go — func leadingIndent + - exported: false complexity: 4 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_utils.go — func lineHasInlinePrompt + - exported: false complexity: 2 has-control: true + - recommendation: add test (non-trivial logic) +- internal/lsp/handlers_utils.go — method (Server).llmRequestOpts + - exported: false complexity: 3 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/handlers_utils.go — method (Server).logLLMStats + - exported: false complexity: 13 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/server.go — method (Server).Run + - exported: true complexity: 1 has-control: true + - recommendation: add test (exported) +- internal/lsp/transport.go — method (Server).readMessage + - exported: false complexity: 7 has-control: true + - recommendation: add test (exported receiver) +- internal/lsp/transport.go — method (Server).writeMessage + - exported: false complexity: 5 has-control: true + - recommendation: add test (exported receiver) + +## main (scripts) + +- scripts/scan_uncovered.go — func isExportedIdent + - exported: false complexity: 3 has-control: true + - recommendation: add test (non-trivial logic) +- scripts/scan_uncovered.go — func main + - exported: false complexity: 12 has-control: true + - recommendation: add test (non-trivial logic) +- scripts/scan_uncovered.go — func recommend + - exported: false complexity: 4 has-control: true + - recommendation: add test (non-trivial logic) +- scripts/scan_uncovered.go — func relPath + - exported: false complexity: 2 has-control: true + - recommendation: add test (non-trivial logic) +- scripts/scan_uncovered.go — func typeString + - exported: false complexity: 1 has-control: false + - recommendation: optional (helper/trivial) + +## appconfig (internal/appconfig) + +- internal/appconfig/config.go — func Load + - exported: true complexity: 6 has-control: true + - recommendation: add test (exported) +- internal/appconfig/config.go — func getConfigPath + - exported: false complexity: 3 has-control: true + - recommendation: add test (non-trivial logic) +- internal/appconfig/config.go — func loadFromEnv + - exported: false complexity: 27 has-control: true + - recommendation: add test (non-trivial logic) +- internal/appconfig/config.go — func loadFromFile + - exported: false complexity: 7 has-control: true + - recommendation: add test (non-trivial logic) +- internal/appconfig/config.go — method (App).mergeBasics + - exported: false complexity: 11 has-control: true + - recommendation: add test (exported receiver) +- internal/appconfig/config.go — method (App).mergeProviderFields + - exported: false complexity: 9 has-control: true + - recommendation: add test (exported receiver) +- internal/appconfig/config.go — method (App).mergeWith + - exported: false complexity: 2 has-control: false + - recommendation: add test (exported receiver) +- internal/appconfig/config.go — func newDefaultConfig + - exported: false complexity: 2 has-control: false + - recommendation: optional (helper/trivial) + +## hexailsp (internal/hexailsp) + +- internal/hexailsp/run.go — func buildClientIfNil + - exported: false complexity: 7 has-control: true + - recommendation: add test (non-trivial logic) +- internal/hexailsp/run.go — func ensureFactory + - exported: false complexity: 2 has-control: true + - recommendation: add test (non-trivial logic) +- internal/hexailsp/run.go — func makeServerOptions + - exported: false complexity: 1 has-control: false + - recommendation: optional (helper/trivial) +- internal/hexailsp/run.go — func normalizeLoggingConfig + - exported: false complexity: 2 has-control: true + - recommendation: add test (non-trivial logic) + +## logging (internal/logging) + +- internal/logging/chatlogger.go — method (ChatLogger).LogStart + - exported: true complexity: 4 has-control: true + - recommendation: add test (exported) +- internal/logging/chatlogger.go — func NewChatLogger + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- internal/logging/logging.go — func Bind + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- internal/logging/logging.go — func Logf + - exported: true complexity: 3 has-control: true + - recommendation: add test (exported) +- internal/logging/logging.go — func PreviewForLog + - exported: true complexity: 2 has-control: true + - recommendation: add test (exported) +- internal/logging/logging.go — func SetLogPreviewLimit + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) + +## main (.) + +- Magefile.go — func Build + - exported: true complexity: 3 has-control: false + - recommendation: add test (exported) +- Magefile.go — func BuildHexaiCLI + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- Magefile.go — func BuildHexaiLSP + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- Magefile.go — func Cover + - exported: true complexity: 10 has-control: true + - recommendation: add test (exported) +- Magefile.go — func CoverAll + - exported: true complexity: 10 has-control: true + - recommendation: add test (exported) +- Magefile.go — func Dev + - exported: true complexity: 3 has-control: true + - recommendation: add test (exported) +- Magefile.go — func DevInstall + - exported: true complexity: 2 has-control: true + - recommendation: add test (exported) +- Magefile.go — func Install + - exported: true complexity: 7 has-control: true + - recommendation: add test (exported) +- Magefile.go — func Lint + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- Magefile.go — func RunCLI + - exported: true complexity: 3 has-control: false + - recommendation: add test (exported) +- Magefile.go — func Test + - exported: true complexity: 2 has-control: true + - recommendation: add test (exported) +- Magefile.go — func Vet + - exported: true complexity: 1 has-control: false + - recommendation: add test (exported) +- Magefile.go — func totalCoveragePercent + - exported: false complexity: 8 has-control: true + - recommendation: add test (non-trivial logic) +- Magefile.go — func warnIfLowCoverage + - exported: false complexity: 6 has-control: true + - recommendation: add test (non-trivial logic) + +## main (cmd/hexai) + +- cmd/hexai/main.go — func main + - exported: false complexity: 4 has-control: true + - recommendation: add test (non-trivial logic) + diff --git a/scripts/scan_uncovered.go b/scripts/scan_uncovered.go new file mode 100644 index 0000000..9eb22fd --- /dev/null +++ b/scripts/scan_uncovered.go @@ -0,0 +1,187 @@ +// Command scan_uncovered analyzes Go source to find functions without tests +// and recommends whether to add tests based on export status and basic complexity. +package main + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "sort" + "strings" +) + +type FuncInfo struct { + File string + Package string + Name string + Recv string // receiver type for methods + Exported bool + Complexity int // naive: number of statements in body + HasControl bool // has if/for/switch/select +} + +func main() { + root := "." + dirs := map[string][]string{} + _ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { return nil } + // Skip vendor, tmp, .git, node_modules, build caches + base := filepath.Base(path) + if d.IsDir() { + if (path != "." && strings.HasPrefix(base, ".")) || base == "vendor" || base == "tmp" || base == "node_modules" || base == ".gocache" || base == ".gomodcache" { + return filepath.SkipDir + } + return nil + } + if strings.HasSuffix(path, ".go") && !strings.HasSuffix(path, "_test.go") { + dir := filepath.Dir(path) + dirs[dir] = append(dirs[dir], path) + } + return nil + }) + + fset := token.NewFileSet() + type missing struct{ + pkg string + dir string + items []FuncInfo + } + var all []missing + + for dir, files := range dirs { + // parse package name from any file + pkg := "" + var funcs []FuncInfo + for _, file := range files { + f, err := parser.ParseFile(fset, file, nil, 0) + if err != nil { continue } + if pkg == "" { pkg = f.Name.Name } + for _, d := range f.Decls { + fd, ok := d.(*ast.FuncDecl) + if !ok || fd.Name == nil { continue } + // Skip init/test helpers in non-test files? keep all funcs + info := FuncInfo{File: file, Package: pkg, Name: fd.Name.Name, Exported: ast.IsExported(fd.Name.Name)} + if fd.Recv != nil && len(fd.Recv.List) > 0 { + info.Recv = typeString(fd.Recv.List[0].Type) + } + if fd.Body != nil { + info.Complexity = len(fd.Body.List) + ast.Inspect(fd.Body, func(n ast.Node) bool { + switch n.(type) { + case *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt, *ast.SwitchStmt, *ast.SelectStmt: + info.HasControl = true + } + return true + }) + } + funcs = append(funcs, info) + } + } + if len(funcs) == 0 { continue } + // Gather test identifiers and test names in this dir + testIdents := make(map[string]struct{}) + testNames := make(map[string]struct{}) + _ = filepath.WalkDir(dir, func(p string, d os.DirEntry, err error) error { + if err != nil { return nil } + if d.IsDir() { return nil } + if !strings.HasSuffix(p, "_test.go") { return nil } + tf, err := parser.ParseFile(fset, p, nil, 0) + if err != nil { return nil } + for _, d := range tf.Decls { + if fd, ok := d.(*ast.FuncDecl); ok && fd.Name != nil { + if strings.HasPrefix(fd.Name.Name, "Test") { testNames[fd.Name.Name] = struct{}{} } + } + } + ast.Inspect(tf, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.Ident: + testIdents[x.Name] = struct{}{} + case *ast.SelectorExpr: + testIdents[x.Sel.Name] = struct{}{} + } + return true + }) + return nil + }) + + // Filter funcs missing coverage signal + var miss []FuncInfo + for _, fn := range funcs { + // Heuristic: if function name appears in tests, assume covered + if _, ok := testIdents[fn.Name]; ok { continue } + // Methods: also consider receiver type name for TestType_Method style + if fn.Recv != "" { + // e.g., TestType_Method often contains Method name as Ident; already covered by above + } + // Skip generated or trivial files? we'll decide in recommendation + miss = append(miss, fn) + } + if len(miss) == 0 { continue } + sort.Slice(miss, func(i, j int) bool { + if miss[i].File == miss[j].File { + return miss[i].Name < miss[j].Name + } + return miss[i].File < miss[j].File + }) + all = append(all, missing{pkg: pkg, dir: dir, items: miss}) + } + + // Write markdown report to stdout + fmt.Println("# Unit tests to consider adding") + fmt.Println() + fmt.Println("This report lists functions that do not appear to be referenced in test files in the same package directory. Recommendations are heuristic.") + fmt.Println() + for _, m := range all { + fmt.Printf("## %s (%s)\n\n", m.pkg, m.dir) + for _, fn := range m.items { + rec := recommend(fn) + var sig string + if fn.Recv != "" { sig = fmt.Sprintf("method (%s).%s", fn.Recv, fn.Name) } else { sig = "func " + fn.Name } + fmt.Printf("- %s — %s\n", relPath(fn.File), sig) + fmt.Printf(" - exported: %t complexity: %d has-control: %t\n", fn.Exported, fn.Complexity, fn.HasControl) + fmt.Printf(" - recommendation: %s\n", rec) + } + fmt.Println() + } +} + +func typeString(t ast.Expr) string { + switch x := t.(type) { + case *ast.Ident: + return x.Name + case *ast.StarExpr: + return typeString(x.X) + case *ast.IndexExpr: + return typeString(x.X) + case *ast.IndexListExpr: + return typeString(x.X) + case *ast.SelectorExpr: + return x.Sel.Name + default: + return "" + } +} + +func recommend(fn FuncInfo) string { + // Strongly recommend for exported functions and methods on exported types + if fn.Exported { return "add test (exported)" } + if fn.Recv != "" && isExportedIdent(fn.Recv) { return "add test (exported receiver)" } + // Recommend for functions with control flow or non-trivial body + if fn.HasControl || fn.Complexity >= 3 { return "add test (non-trivial logic)" } + // Otherwise, optional + return "optional (helper/trivial)" +} + +func isExportedIdent(name string) bool { + if name == "" { return false } + r := []rune(name) + return r[0] >= 'A' && r[0] <= 'Z' +} + +func relPath(p string) string { + if rp, err := filepath.Rel(".", p); err == nil { return rp } + return p +} |
