Skip to content

Commit 9c6d5c3

Browse files
committed
feat: add doc comment structure compliance test
TestDocCommentStructure verifies exported functions with parameters have Parameters: sections and functions with return values have Returns: sections, per CONVENTIONS.md. 68 pre-existing violations grandfathered; new code must not increase the count. Supersedes the ctx-docstrings skill task — deterministic test enforcement is stronger than a skill. Spec: specs/cli-examples.md Signed-off-by: Jose Alekhinne <jose@ctx.ist>
1 parent be417a6 commit 9c6d5c3

2 files changed

Lines changed: 144 additions & 8 deletions

File tree

.context/TASKS.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,10 @@ TASK STATUS LABELS:
8484
accuracy (does the description match behavior?) needs periodic
8585
LLM audit — not automatable. #priority:medium #added:2026-04-05
8686

87-
- [ ] Create ctx-docstrings skill: audit and fix docstrings
88-
against CONVENTIONS.md Documentation section. Skill loads
89-
CONVENTIONS.md, scans functions in scope for
90-
missing/incomplete docstring sections (Parameters, Returns),
91-
reports violations, and optionally fixes them.
92-
Language-agnostic design with Go as first implementation.
93-
Deterministic enforcement via linter is tracked separately
94-
in ideas/spec-convention-enforcement.md
87+
- [-] Create ctx-docstrings skill: audit and fix docstrings
88+
against CONVENTIONS.md Documentation section. Superseded by
89+
TestDocCommentStructure compliance test (68 grandfathered).
90+
#added:2026-03-20-163413
9591
#added:2026-03-16-114445
9692

9793
### Phase -2: Task completion nudge:
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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/ast"
11+
"strings"
12+
"testing"
13+
)
14+
15+
// grandfatheredDocStructure is the number of pre-existing
16+
// doc structure violations. New code must not add to this
17+
// count. Reduce it as violations are fixed.
18+
const grandfatheredDocStructure = 68
19+
20+
// TestDocCommentStructure verifies that exported functions
21+
// with parameters include a "Parameters:" section and
22+
// functions with return values include a "Returns:" section
23+
// in their doc comments, per CONVENTIONS.md.
24+
//
25+
// Test files and methods with receivers are included.
26+
// Functions without doc comments are skipped (caught by
27+
// TestDocComments).
28+
func TestDocCommentStructure(t *testing.T) {
29+
pkgs := loadPackages(t)
30+
var violations []string
31+
32+
for _, pkg := range pkgs {
33+
for _, file := range pkg.Syntax {
34+
fpath := pkg.Fset.Position(
35+
file.Pos(),
36+
).Filename
37+
if isTestFile(fpath) {
38+
continue
39+
}
40+
41+
for _, decl := range file.Decls {
42+
fn, ok := decl.(*ast.FuncDecl)
43+
if !ok {
44+
continue
45+
}
46+
if !fn.Name.IsExported() {
47+
continue
48+
}
49+
if fn.Doc == nil {
50+
continue
51+
}
52+
53+
doc := fn.Doc.Text()
54+
hasParams := fnHasParams(fn)
55+
hasReturns := fnHasReturns(fn)
56+
57+
if hasParams &&
58+
!strings.Contains(
59+
doc, "Parameters:",
60+
) {
61+
violations = append(
62+
violations,
63+
posString(
64+
pkg.Fset, fn.Pos(),
65+
)+": "+fn.Name.Name+
66+
" has parameters but"+
67+
" missing Parameters:"+
68+
" section",
69+
)
70+
}
71+
72+
if hasReturns &&
73+
!strings.Contains(
74+
doc, "Returns:",
75+
) {
76+
violations = append(
77+
violations,
78+
posString(
79+
pkg.Fset, fn.Pos(),
80+
)+": "+fn.Name.Name+
81+
" has return values but"+
82+
" missing Returns:"+
83+
" section",
84+
)
85+
}
86+
}
87+
}
88+
}
89+
90+
if len(violations) > grandfatheredDocStructure {
91+
t.Errorf(
92+
"%d doc structure violations "+
93+
"(grandfathered: %d, new: %d):",
94+
len(violations),
95+
grandfatheredDocStructure,
96+
len(violations)-grandfatheredDocStructure,
97+
)
98+
// Show only the newest violations.
99+
start := grandfatheredDocStructure
100+
limit := 20
101+
if len(violations)-start < limit {
102+
limit = len(violations) - start
103+
}
104+
for _, v := range violations[start : start+limit] {
105+
t.Error(v)
106+
}
107+
} else if len(violations) < grandfatheredDocStructure {
108+
t.Errorf(
109+
"violations dropped to %d — "+
110+
"update grandfatheredDocStructure "+
111+
"from %d to %d",
112+
len(violations),
113+
grandfatheredDocStructure,
114+
len(violations),
115+
)
116+
}
117+
}
118+
119+
// fnHasParams reports whether fn has at least one
120+
// named parameter (excluding the receiver).
121+
func fnHasParams(fn *ast.FuncDecl) bool {
122+
if fn.Type.Params == nil {
123+
return false
124+
}
125+
for _, field := range fn.Type.Params.List {
126+
for _, name := range field.Names {
127+
if name.Name != "_" {
128+
return true
129+
}
130+
}
131+
}
132+
return false
133+
}
134+
135+
// fnHasReturns reports whether fn declares at least
136+
// one return value.
137+
func fnHasReturns(fn *ast.FuncDecl) bool {
138+
return fn.Type.Results != nil &&
139+
len(fn.Type.Results.List) > 0
140+
}

0 commit comments

Comments
 (0)