Skip to content

Commit d56de63

Browse files
committed
Address Copilot round 3: format-flag UX, terminator check, dead error paths
- runHelpPatterns now treats `-f` without a value as exit-2 usage error, and rejects unexpected positional arguments. Previous behavior fell back to text output silently for both cases. - parseFrontMatter explicitly detects a missing closing `---` and any scanner error, returning a clear "unterminated front matter" error instead of silently scanning past the front-matter region. - Replace the unreachable rules.ListRules error branches in cachedRuleInfo, handleRulePatterns, and runHelpPatterns with comments noting why the embedded-FS read cannot fail at runtime. showRule now delegates to a new ruledocs.LookupRuleInfo helper that consolidates the list-and-find step into a single error path. - Drop the unreachable json.Encode error branch in runHelpPatterns (encoding plain string/bool fields cannot fail). - Add tests for the new error UX, the unterminated-front-matter detection, and LookupRuleInfo's success and failure paths.
1 parent 93476d5 commit d56de63

6 files changed

Lines changed: 119 additions & 38 deletions

File tree

cmd/mdsmith/main.go

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"os"
1010
"path/filepath"
1111
"runtime/debug"
12-
"strings"
1312

1413
flag "github.com/spf13/pflag"
1514

@@ -1070,7 +1069,15 @@ func runHelp(args []string) int {
10701069

10711070
func runHelpPatterns(args []string) int {
10721071
format := "text"
1073-
if len(args) >= 2 && (args[0] == "-f" || args[0] == "--format") {
1072+
if len(args) > 0 {
1073+
if args[0] != "-f" && args[0] != "--format" {
1074+
fmt.Fprintf(os.Stderr, "mdsmith: help patterns: unexpected argument %q\n", args[0])
1075+
return 2
1076+
}
1077+
if len(args) < 2 {
1078+
fmt.Fprintf(os.Stderr, "mdsmith: help patterns: %s requires a value (text or json)\n", args[0])
1079+
return 2
1080+
}
10741081
format = args[1]
10751082
}
10761083
switch format {
@@ -1079,11 +1086,9 @@ func runHelpPatterns(args []string) int {
10791086
fmt.Fprintf(os.Stderr, "mdsmith: help patterns: unknown format %q (valid: text, json)\n", format)
10801087
return 2
10811088
}
1082-
rules, err := ruledocs.ListRules()
1083-
if err != nil {
1084-
fmt.Fprintf(os.Stderr, "mdsmith: %v\n", err)
1085-
return 2
1086-
}
1089+
// ruledocs.ListRules reads an embedded FS, so its error is unreachable
1090+
// in a correctly built binary. Treat any failure as an empty rule list.
1091+
rules, _ := ruledocs.ListRules()
10871092
type rec struct {
10881093
ID string `json:"id"`
10891094
Name string `json:"name"`
@@ -1107,10 +1112,8 @@ func runHelpPatterns(args []string) int {
11071112
if format == "json" {
11081113
enc := json.NewEncoder(os.Stdout)
11091114
enc.SetIndent("", " ")
1110-
if err := enc.Encode(items); err != nil {
1111-
fmt.Fprintf(os.Stderr, "mdsmith: %v\n", err)
1112-
return 2
1113-
}
1115+
// items is a slice of plain string/bool fields, so encoding cannot fail.
1116+
_ = enc.Encode(items)
11141117
return 0
11151118
}
11161119
for _, it := range items {
@@ -1249,25 +1252,13 @@ func listAllRules() int {
12491252
}
12501253

12511254
func showRule(query string) int {
1252-
rules, err := ruledocs.ListRules()
1255+
info, err := ruledocs.LookupRuleInfo(query)
12531256
if err != nil {
12541257
fmt.Fprintf(os.Stderr, "mdsmith: %v\n", err)
12551258
return 2
12561259
}
1257-
var chosen *ruledocs.RuleInfo
1258-
q := strings.ToUpper(query)
1259-
for i := range rules {
1260-
if strings.ToUpper(rules[i].ID) == q || rules[i].Name == query {
1261-
chosen = &rules[i]
1262-
break
1263-
}
1264-
}
1265-
if chosen == nil {
1266-
fmt.Fprintf(os.Stderr, "mdsmith: unknown rule %q\n", query)
1267-
return 2
1268-
}
1269-
content := ruledocs.StripFrontMatter(chosen.Content)
1270-
if m := chosen.Maintainability; m != nil {
1260+
content := ruledocs.StripFrontMatter(info.Content)
1261+
if m := info.Maintainability; m != nil {
12711262
content += "\n\n## Maintainability pattern\n\n"
12721263
content += fmt.Sprintf("- Signal: %s\n- Fix: %s\n- For diagnostic: %t\n",
12731264
m.Signal, m.Fix, m.ForDiagnostic)

cmd/mdsmith/main_unit_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,28 @@ func TestRunHelpPatterns_UnknownFormat_ExitsTwo(t *testing.T) {
734734
assert.Contains(t, stderr, "jsno")
735735
}
736736

737+
func TestRunHelpPatterns_FormatFlagWithoutValue_ExitsTwo(t *testing.T) {
738+
var stderr string
739+
captureStdout(func() {
740+
stderr = captureStderr(func() {
741+
code := runHelpPatterns([]string{"-f"})
742+
assert.Equal(t, 2, code)
743+
})
744+
})
745+
assert.Contains(t, stderr, "requires a value")
746+
}
747+
748+
func TestRunHelpPatterns_UnexpectedArg_ExitsTwo(t *testing.T) {
749+
var stderr string
750+
captureStdout(func() {
751+
stderr = captureStderr(func() {
752+
code := runHelpPatterns([]string{"garbage"})
753+
assert.Equal(t, 2, code)
754+
})
755+
})
756+
assert.Contains(t, stderr, "unexpected argument")
757+
}
758+
737759
func TestRunHelp_PatternsTopicDispatches(t *testing.T) {
738760
out := captureStdout(func() {
739761
code := runHelp([]string{"patterns", "-f", "json"})

internal/lsp/hover.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,12 @@ func ruleHoverContent(d Diagnostic) string {
142142
// cachedRuleInfo returns the rule metadata for a given diagnostic code, or
143143
// (zero, false) when not found. Content is already stripped of front matter.
144144
// The first call loads all embedded rule READMEs; later calls are O(1) lookups.
145+
// rules.ListRules can only fail if the embedded FS itself is corrupt, in
146+
// which case the cache stays empty and every lookup returns false — the same
147+
// behavior as an unknown code.
145148
func cachedRuleInfo(code string) (rules.RuleInfo, bool) {
146149
ruleInfoCache.Do(func() {
147-
all, err := rules.ListRules()
148-
if err != nil {
149-
ruleInfoCache.infos = map[string]rules.RuleInfo{}
150-
return
151-
}
150+
all, _ := rules.ListRules()
152151
m := make(map[string]rules.RuleInfo, len(all))
153152
for _, r := range all {
154153
r.Content = rules.StripFrontMatter(r.Content)

internal/lsp/patterns.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package lsp
22

33
import (
4-
"fmt"
5-
64
"github.com/jeduden/mdsmith/internal/rules"
75
)
86

@@ -14,12 +12,13 @@ type rulePattern struct {
1412
ForDiagnostic bool `json:"for-diagnostic"`
1513
}
1614

15+
// handleRulePatterns serves the `mdsmith/rulePatterns` LSP request. It returns
16+
// every rule with a non-null maintainability block in the same shape as
17+
// `mdsmith help patterns -f json`. rules.ListRules reads an embedded FS that
18+
// cannot fail at runtime, so any error degrades silently to an empty list —
19+
// the same response shape as a workspace where every rule is `maintainability: null`.
1720
func (s *Server) handleRulePatterns(msg *requestMessage) {
18-
all, err := rules.ListRules()
19-
if err != nil {
20-
_ = s.t.writeError(msg.ID, codeInternalError, fmt.Sprintf("listing rules: %v", err))
21-
return
22-
}
21+
all, _ := rules.ListRules()
2322
out := make([]rulePattern, 0)
2423
for _, r := range all {
2524
if r.Maintainability == nil {

internal/rules/ruledocs.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,27 @@ func LookupRule(query string) (string, error) {
4646
return lookupRuleFromFS(rulesFS, query)
4747
}
4848

49+
// LookupRuleInfo finds a rule by ID (e.g. "MDS001") or name (e.g. "line-length")
50+
// and returns its full metadata, including the parsed maintainability block
51+
// and the raw README content (front matter not stripped).
52+
func LookupRuleInfo(query string) (RuleInfo, error) {
53+
return lookupRuleInfoFromFS(rulesFS, query)
54+
}
55+
56+
func lookupRuleInfoFromFS(fsys fs.FS, query string) (RuleInfo, error) {
57+
rules, err := listRulesFromFS(fsys)
58+
if err != nil {
59+
return RuleInfo{}, err
60+
}
61+
q := strings.ToUpper(query)
62+
for _, r := range rules {
63+
if strings.ToUpper(r.ID) == q || r.Name == query {
64+
return r, nil
65+
}
66+
}
67+
return RuleInfo{}, fmt.Errorf("unknown rule %q", query)
68+
}
69+
4970
func listRulesFromFS(fsys fs.FS) ([]RuleInfo, error) {
5071
entries, err := fs.ReadDir(fsys, ".")
5172
if err != nil {
@@ -103,13 +124,21 @@ func parseFrontMatter(content string) (RuleInfo, error) {
103124
}
104125

105126
var front []string
127+
terminated := false
106128
for scanner.Scan() {
107129
line := scanner.Text()
108130
if strings.TrimSpace(line) == "---" {
131+
terminated = true
109132
break
110133
}
111134
front = append(front, line)
112135
}
136+
if err := scanner.Err(); err != nil {
137+
return RuleInfo{}, fmt.Errorf("scanning front matter: %w", err)
138+
}
139+
if !terminated {
140+
return RuleInfo{}, fmt.Errorf("unterminated front matter")
141+
}
113142
var meta struct {
114143
ID string `yaml:"id"`
115144
Name string `yaml:"name"`

internal/rules/ruledocs_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,33 @@ func TestLookupRule_Unknown(t *testing.T) {
7777
assert.Contains(t, err.Error(), "unknown rule", "error = %q, want it to contain 'unknown rule'", err.Error())
7878
}
7979

80+
func TestLookupRuleInfo_ByID(t *testing.T) {
81+
info, err := LookupRuleInfo("MDS019")
82+
require.NoError(t, err)
83+
assert.Equal(t, "MDS019", info.ID)
84+
assert.Equal(t, "catalog", info.Name)
85+
require.NotNil(t, info.Maintainability)
86+
assert.NotEmpty(t, info.Maintainability.Signal)
87+
}
88+
89+
func TestLookupRuleInfo_ByName(t *testing.T) {
90+
info, err := LookupRuleInfo("line-length")
91+
require.NoError(t, err)
92+
assert.Equal(t, "MDS001", info.ID)
93+
assert.Nil(t, info.Maintainability)
94+
}
95+
96+
func TestLookupRuleInfo_Unknown(t *testing.T) {
97+
_, err := LookupRuleInfo("MDSXXX")
98+
require.Error(t, err)
99+
assert.Contains(t, err.Error(), "unknown rule")
100+
}
101+
102+
func TestLookupRuleInfoFromFS_PropagatesReadDirError(t *testing.T) {
103+
_, err := lookupRuleInfoFromFS(errFS{}, "anything")
104+
require.Error(t, err)
105+
}
106+
80107
func TestListRulesFromFS_SkipsBadFrontMatter(t *testing.T) {
81108
fsys := fstest.MapFS{
82109
"good/README.md": &fstest.MapFile{
@@ -284,6 +311,20 @@ func TestParseFrontMatter_FoldsBlockScalarDescription(t *testing.T) {
284311
assert.NotContains(t, info.Description, ">-")
285312
}
286313

314+
// TestParseFrontMatter_UnterminatedFrontMatter verifies that a front matter
315+
// block without a closing `---` line fails with a clear error instead of
316+
// silently treating the rest of the file as YAML.
317+
func TestParseFrontMatter_UnterminatedFrontMatter(t *testing.T) {
318+
content := "---\n" +
319+
"id: MDS999\n" +
320+
"name: example\n" +
321+
"status: ready\n" +
322+
"# body without closing delimiter\n"
323+
_, err := parseFrontMatter(content)
324+
require.Error(t, err)
325+
assert.Contains(t, err.Error(), "unterminated front matter")
326+
}
327+
287328
// TestParseFrontMatter_RejectsYAMLAliases verifies that the safe-YAML wrapper
288329
// rejects anchor/alias usage in rule README front matter rather than silently
289330
// expanding aliases.

0 commit comments

Comments
 (0)