Skip to content

Commit 6a39186

Browse files
committed
feat: add TestNoDeadExports audit check, quarantine 67 dead symbols
Uses go/types to detect exported constants, functions, types, and vars with zero references anywhere in the codebase (including cmd/). Methods (interface implementations) are excluded. Quarantined 67 dead exports to quarantine/deadcode/ with .go.dead extension, mirroring the source tree. 17 test-only exports added to an allowlist (used exclusively from _test.go files). Also removed 5 cascaded orphan DescKey constants whose only callers were among the deleted functions. Signed-off-by: Jose Alekhinne <jose@ctx.ist>
1 parent e597beb commit 6a39186

47 files changed

Lines changed: 267 additions & 324 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,6 @@ ideas
8181
# Generated skills
8282
.claude/skills/generated
8383
.claude/skills/gitnexus
84+
85+
# Dead code quarantine
86+
quarantine/

internal/assets/hooks/messages/doc.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
//
99
// The embedded registry.yaml maps each hook+variant pair to a
1010
// category and description. [Registry] returns all entries,
11-
// [Lookup] finds a specific one, and [Hooks]/[Variants] enumerate
12-
// the available names. Categories are [CategoryCustomizable]
13-
// (user can override the template) or [CategoryCtxSpecific]
14-
// (internal to ctx).
11+
// [Lookup] finds a specific one, and [Variants] enumerates
12+
// the available names. [CategoryCtxSpecific] marks entries
13+
// internal to ctx.
1514
package messages

internal/assets/read/hook/doc.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,5 @@
99
//
1010
// [Message] reads a specific template file by hook name and
1111
// filename. [MessageRegistry] returns the raw registry.yaml.
12-
// [MessageList] and [VariantList] enumerate available hooks
13-
// and their template variants for discovery and testing.
12+
// [TraceScript] reads an embedded trace git hook script.
1413
package hook

internal/assets/read/hook/hook.go

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -51,49 +51,3 @@ func Message(hook, filename string) ([]byte, error) {
5151
func MessageRegistry() ([]byte, error) {
5252
return assets.FS.ReadFile(asset.PathMessageRegistry)
5353
}
54-
55-
// MessageList returns available hook message directory names.
56-
//
57-
// Each hook is a directory under hooks/messages/ containing one or
58-
// more variant .txt template files.
59-
//
60-
// Returns:
61-
// - []string: List of hook directory names
62-
// - error: Non-nil if directory read fails
63-
func MessageList() ([]string, error) {
64-
entries, readErr := assets.FS.ReadDir(asset.DirHooksMessages)
65-
if readErr != nil {
66-
return nil, readErr
67-
}
68-
69-
names := make([]string, 0, len(entries))
70-
for _, entry := range entries {
71-
if entry.IsDir() {
72-
names = append(names, entry.Name())
73-
}
74-
}
75-
return names, nil
76-
}
77-
78-
// VariantList returns available variant filenames for a hook.
79-
//
80-
// Parameters:
81-
// - hook: Hook directory name (e.g., "qa-reminder")
82-
//
83-
// Returns:
84-
// - []string: List of variant filenames (e.g., "gate.txt")
85-
// - error: Non-nil if the hook directory is not found or read fails
86-
func VariantList(hook string) ([]string, error) {
87-
entries, readErr := assets.FS.ReadDir(path.Join(asset.DirHooksMessages, hook))
88-
if readErr != nil {
89-
return nil, readErr
90-
}
91-
92-
names := make([]string, 0, len(entries))
93-
for _, entry := range entries {
94-
if !entry.IsDir() {
95-
names = append(names, entry.Name())
96-
}
97-
}
98-
return names, nil
99-
}

internal/assets/read/project/doc.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
// Package project provides access to project-root files and
88
// directory README templates from embedded assets.
99
//
10-
// [File] reads a project-root file by name (e.g. .gitignore
11-
// additions). [Readme] reads the README template for a specific
10+
// [Readme] reads the README template for a specific
1211
// subdirectory (e.g. specs/README.md).
1312
package project

internal/assets/read/project/project.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,6 @@ import (
1313
"github.com/ActiveMemory/ctx/internal/config/asset"
1414
)
1515

16-
// File reads a project-root file by name from the embedded filesystem.
17-
//
18-
// These files are deployed to the project root (not .context/) by dedicated
19-
// handlers during initialization.
20-
//
21-
// Parameters:
22-
// - name: Filename (e.g., "Makefile.ctx")
23-
//
24-
// Returns:
25-
// - []byte: File content
26-
// - error: Non-nil if the file is not found or read fails
27-
func File(name string) ([]byte, error) {
28-
return assets.FS.ReadFile(path.Join(asset.DirProject, name))
29-
}
30-
3116
// Readme reads a project directory README template by directory name.
3217
//
3318
// Templates are stored as project/<dir>-README.md in the embedded filesystem.

internal/assets/tpl/tpl_recall.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@ const (
1515
// Args: date, slug, shortID.
1616
RecallFilename = "%s-%s-%s.md"
1717

18-
// RecallTokens formats the token stats line.
19-
// Args: total, in, out.
20-
//nolint:gosec // G101: display template, not a credential
21-
RecallTokens = "**Tokens**: %s (in: %s, out: %s)"
22-
2318
// RecallPartOf formats the part indicator.
2419
// Args: part, totalParts.
2520
RecallPartOf = "**Part %d of %d**"
@@ -40,9 +35,6 @@ const (
4035
// Args: name, count.
4136
RecallToolCount = "- %s: %d"
4237

43-
// RecallSummaryPlaceholder is the placeholder text in the summary section.
44-
RecallSummaryPlaceholder = "[Add your summary of this session]"
45-
4638
// RecallErrorMarker is the error indicator for tool results.
4739
RecallErrorMarker = "❌ Error"
4840

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// / ctx: https://ctx.ist
2+
// ,'`./ do you remember?
3+
// `.,'\
4+
// \ Copyright 2026-present Context contributors.
5+
// SPDX-License-Identifier: Apache-2.0
6+
7+
package audit
8+
9+
import (
10+
"go/types"
11+
"strings"
12+
"testing"
13+
"unicode"
14+
15+
"golang.org/x/tools/go/packages"
16+
)
17+
18+
// TestNoDeadExports flags exported constants, variables,
19+
// functions, and types in internal/ that have zero
20+
// references outside their definition file.
21+
//
22+
// Test files are exempt (both as definition and usage
23+
// sites).
24+
//
25+
// Unexported symbols are skipped: they are package-
26+
// internal and may be used via reflection or are
27+
// genuinely file-scoped helpers.
28+
29+
// testOnlyExports lists exported symbols that exist
30+
// solely for test usage. The dead-export scanner skips
31+
// test files, so these would otherwise be false
32+
// positives. Keep this list small: prefer eliminating
33+
// the export over adding it here.
34+
var testOnlyExports = map[string]bool{
35+
"github.com/ActiveMemory/ctx/internal/assets/hooks/messages.CategoryCustomizable": true,
36+
"github.com/ActiveMemory/ctx/internal/assets/hooks/messages.Hooks": true,
37+
"github.com/ActiveMemory/ctx/internal/assets/hooks/messages.RegistryError": true,
38+
"github.com/ActiveMemory/ctx/internal/cli/initialize/core/vscode.CreateVSCodeArtifacts": true,
39+
"github.com/ActiveMemory/ctx/internal/cli/journal/core/lock.LockedFrontmatterLine": true,
40+
"github.com/ActiveMemory/ctx/internal/cli/pad/core/store.EnsureGitignore": true,
41+
"github.com/ActiveMemory/ctx/internal/cli/system/core/state.SetDirForTest": true,
42+
"github.com/ActiveMemory/ctx/internal/config/asset.DirReferences": true,
43+
"github.com/ActiveMemory/ctx/internal/config/regex.Phase": true,
44+
"github.com/ActiveMemory/ctx/internal/inspect.StartsWithCtxMarker": true,
45+
"github.com/ActiveMemory/ctx/internal/journal/parser.Parser": true,
46+
"github.com/ActiveMemory/ctx/internal/journal/parser.RegisteredTools": true,
47+
"github.com/ActiveMemory/ctx/internal/mcp/proto.ErrCodeInvalidReq": true,
48+
"github.com/ActiveMemory/ctx/internal/mcp/proto.InitializeParams": true,
49+
"github.com/ActiveMemory/ctx/internal/mcp/proto.UnsubscribeParams": true,
50+
"github.com/ActiveMemory/ctx/internal/rc.Reset": true,
51+
"github.com/ActiveMemory/ctx/internal/task.MatchFull": true,
52+
}
53+
54+
func TestNoDeadExports(t *testing.T) {
55+
pkgs := loadPackages(t)
56+
57+
// Also load cmd/ packages to catch cross-boundary
58+
// usage (cmd/ctx/main.go calls internal/ exports).
59+
cmdPkgs := loadCmdPackages(t)
60+
allPkgs := make([]*packages.Package, 0, len(pkgs)+len(cmdPkgs))
61+
allPkgs = append(allPkgs, pkgs...)
62+
allPkgs = append(allPkgs, cmdPkgs...)
63+
64+
// Phase 1: collect all exported definitions.
65+
// Key: "pkgPath.Name" (stable across type-checker
66+
// instances). Value: definition metadata.
67+
type defInfo struct {
68+
label string // e.g. "const config/dep.BuilderGo"
69+
pos string // file:line
70+
file string // definition filename
71+
}
72+
defs := make(map[string]defInfo)
73+
74+
for _, pkg := range pkgs {
75+
for ident, obj := range pkg.TypesInfo.Defs {
76+
if obj == nil {
77+
continue
78+
}
79+
if !isExported(ident.Name) {
80+
continue
81+
}
82+
83+
pos := pkg.Fset.Position(ident.Pos())
84+
if isTestFile(pos.Filename) {
85+
continue
86+
}
87+
88+
kind := objectKind(obj)
89+
if kind == "" {
90+
continue
91+
}
92+
93+
key := obj.Pkg().Path() + "." + obj.Name()
94+
defs[key] = defInfo{
95+
label: kind + " " +
96+
shortPkg(pkg.PkgPath) +
97+
"." + ident.Name,
98+
pos: pos.String(),
99+
file: pos.Filename,
100+
}
101+
}
102+
}
103+
104+
// Phase 2: collect all usage sites. Remove any
105+
// def that has at least one use outside its own
106+
// definition file. Scan both internal/ and cmd/.
107+
for _, pkg := range allPkgs {
108+
for ident, obj := range pkg.TypesInfo.Uses {
109+
if obj == nil || obj.Pkg() == nil {
110+
continue
111+
}
112+
113+
pos := pkg.Fset.Position(ident.Pos())
114+
if isTestFile(pos.Filename) {
115+
continue
116+
}
117+
118+
key := obj.Pkg().Path() + "." + obj.Name()
119+
_, defined := defs[key]
120+
if !defined {
121+
continue
122+
}
123+
124+
// Any use (same or different package)
125+
// means the symbol is alive.
126+
delete(defs, key)
127+
}
128+
}
129+
130+
// Phase 3: remove test-only allowlist entries.
131+
for key := range testOnlyExports {
132+
delete(defs, key)
133+
}
134+
135+
// Phase 4: report survivors as dead exports.
136+
var violations []string
137+
for _, info := range defs {
138+
violations = append(violations,
139+
info.pos+
140+
": dead export: "+info.label,
141+
)
142+
}
143+
144+
if len(violations) == 0 {
145+
return
146+
}
147+
148+
t.Errorf(
149+
"%d dead exports found:", len(violations),
150+
)
151+
limit := 30
152+
if len(violations) < limit {
153+
limit = len(violations)
154+
}
155+
for _, v := range violations[:limit] {
156+
t.Error(v)
157+
}
158+
if len(violations) > 30 {
159+
t.Errorf(
160+
"... and %d more",
161+
len(violations)-30,
162+
)
163+
}
164+
}
165+
166+
// loadCmdPackages loads cmd/ packages for cross-
167+
// boundary usage detection.
168+
func loadCmdPackages(t *testing.T) []*packages.Package {
169+
t.Helper()
170+
cfg := &packages.Config{
171+
Mode: packages.NeedName |
172+
packages.NeedFiles |
173+
packages.NeedSyntax |
174+
packages.NeedTypes |
175+
packages.NeedTypesInfo,
176+
Tests: false,
177+
}
178+
pkgs, err := packages.Load(
179+
cfg,
180+
"github.com/ActiveMemory/ctx/cmd/...",
181+
)
182+
if err != nil {
183+
t.Fatalf("packages.Load cmd: %v", err)
184+
}
185+
return pkgs
186+
}
187+
188+
// isExported reports whether name starts with an
189+
// uppercase letter.
190+
func isExported(name string) bool {
191+
if name == "" {
192+
return false
193+
}
194+
return unicode.IsUpper(rune(name[0]))
195+
}
196+
197+
// objectKind returns a human-readable kind string for
198+
// a types.Object, or "" to skip.
199+
func objectKind(obj types.Object) string {
200+
switch o := obj.(type) {
201+
case *types.Const:
202+
return "const"
203+
case *types.Var:
204+
// Skip struct fields and function parameters.
205+
// Only flag package-level vars.
206+
if obj.Parent() == nil {
207+
return ""
208+
}
209+
return "var"
210+
case *types.Func:
211+
// Skip methods (have receivers) — they may
212+
// implement interfaces via dynamic dispatch.
213+
if o.Type().(*types.Signature).Recv() != nil {
214+
return ""
215+
}
216+
return "func"
217+
case *types.TypeName:
218+
return "type"
219+
default:
220+
return ""
221+
}
222+
}
223+
224+
// shortPkg returns the last two path elements of a
225+
// package path for readable labels.
226+
func shortPkg(path string) string {
227+
parts := strings.Split(path, "/")
228+
if len(parts) <= 2 {
229+
return path
230+
}
231+
return strings.Join(parts[len(parts)-2:], "/")
232+
}

internal/cli/pad/core/store/doc.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
// Package store manages scratchpad file persistence including.
88
//
99
// Key exports: [ScratchpadPath], [KeyPath],
10-
// [EnsureKey], [EnsureGitignore], [ReadEntries].
10+
// [EnsureKey], [ReadEntries], [WriteEntries].
1111
// Shared helpers used by sibling cmd/ packages.
1212
// Used by core cmd/ packages.
1313
package store

internal/cli/system/core/journal/types.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,3 @@ package journal
1010
type CheckResult struct {
1111
Value string
1212
}
13-
14-
// MarkResult holds the outcome of marking a stage.
15-
type MarkResult struct {
16-
Marked bool
17-
}

0 commit comments

Comments
 (0)