Skip to content

Commit f547b26

Browse files
committed
refactor(review): address xhigh code-review pass 3 findings on PR #731
Final review pass, three findings: 1. internal/linkgraph/wikilinks.go's isMarkdownName duplicated the exact ToLower-instead-of-EqualFold anti-pattern this PR already fixed, on the same feature's call path: crossfilereferenceintegrity's checkWikilinkAnchor (touched by this PR's earlier commits) resolves wikilinks through NewWikilinkIndex's fs.WalkDir callback, which called isMarkdownName once per workspace file — the same O(files) shape as the duplicatedcontent fix. Migrated to mdpath.IsMarkdownPath and removed the now-redundant local test (its cases are covered by internal/mdpath/mdpath_test.go). Declined as out of scope: a broader sweep of ~10 other unrelated hand-rolled .md/.markdown checks across internal/lint, internal/corpus, internal/lsp, internal/githooks, and internal/release — none of them sit on a call path this PR's 5 original fixes touch, and migrating all of them is a separate cleanup beyond "fix the top 5 issues the audit found." 2. fieldPatternCache (type + get/put + buildFieldPattern, added in the pass-2 commit) sat in the middle of the already-2600-line rule.go, unlike this package's existing convention of splitting cohesive concerns into their own files (runcache_wiring.go, scope_rules.go). Moved to fieldpatterncache.go / fieldpatterncache_test.go. 3. fieldPatternCache.get and .put were only exercised indirectly inside one combined test; split into TestFieldPatternCache_Get and TestFieldPatternCache_Put, each covering their nil-receiver, empty-cache, and populated-cache paths by name. All fixes verified: go build, go vet, go test ./... (including -race), golangci-lint (0 issues), gofmt, mdsmith check . (544 files, 0 failures).
1 parent 957030d commit f547b26

5 files changed

Lines changed: 100 additions & 87 deletions

File tree

internal/linkgraph/wikilinks.go

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/jeduden/mdsmith/pkg/goldmark/ast"
1111

1212
"github.com/jeduden/mdsmith/internal/lint"
13+
"github.com/jeduden/mdsmith/internal/mdpath"
1314
)
1415

1516
// WikiLink is one parsed Obsidian-style wikilink occurrence.
@@ -216,7 +217,7 @@ func NewWikilinkIndex(root fs.FS) *WikilinkIndex {
216217
base := path.Base(p)
217218
lcName := strings.ToLower(base)
218219
idx.names[lcName] = append(idx.names[lcName], p)
219-
if isMarkdownName(base) {
220+
if mdpath.IsMarkdownPath(base) {
220221
stem := strings.TrimSuffix(base, path.Ext(base))
221222
lcStem := strings.ToLower(stem)
222223
idx.stems[lcStem] = append(idx.stems[lcStem], p)
@@ -361,7 +362,7 @@ func ResolveWikiLink(root fs.FS, from, target string) (string, bool) {
361362
base := path.Base(p)
362363
if stemMode {
363364
stem := strings.TrimSuffix(base, path.Ext(base))
364-
if strings.EqualFold(stem, wantStem) && isMarkdownName(base) {
365+
if strings.EqualFold(stem, wantStem) && mdpath.IsMarkdownPath(base) {
365366
matches = append(matches, p)
366367
}
367368
return nil
@@ -407,8 +408,3 @@ func wikilinkSearchKey(target string) (wantName, wantStem string, stemMode bool)
407408
return base, "", false
408409
}
409410
}
410-
411-
func isMarkdownName(name string) bool {
412-
ext := strings.ToLower(path.Ext(name))
413-
return ext == ".md" || ext == ".markdown"
414-
}

internal/linkgraph/wikilinks_test.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -491,14 +491,6 @@ func TestWikilinkSearchKey(t *testing.T) {
491491
}
492492
}
493493

494-
func TestIsMarkdownName(t *testing.T) {
495-
assert.True(t, isMarkdownName("page.md"))
496-
assert.True(t, isMarkdownName("page.MD"))
497-
assert.True(t, isMarkdownName("page.markdown"))
498-
assert.False(t, isMarkdownName("page.txt"))
499-
assert.False(t, isMarkdownName("page"))
500-
}
501-
502494
func TestSortByDepthThenName(t *testing.T) {
503495
// Mixed depths and matching depths exercise both keys of the
504496
// sort: shorter paths come first, ties break alphabetically.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package requiredstructure
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
7+
"github.com/jeduden/mdsmith/internal/fieldinterp"
8+
)
9+
10+
// fieldPatternCache memoizes buildFieldPattern's compiled regexes by
11+
// body text, scoped to a single parseSchemaWithRootFS call: within that
12+
// one call, buildSchemaHeading and collectBodySyncPoints can both hit
13+
// the same {field} template text (e.g. a repeated table-row pattern),
14+
// so a call-scoped cache avoids rebuilding an identical NFA.
15+
//
16+
// It is deliberately not a package-level var: bodyText is schema-
17+
// authored Markdown, which churns during interactive editing in
18+
// mdsmith lsp, unlike the bounded, static config patterns
19+
// compiledPatterns caches in maxsectionlength and requiredtextpatterns
20+
// — a process-lifetime cache here would grow with edit history instead
21+
// of workspace size. parseSchemaWithCache already caches and
22+
// invalidates the *parsedSchema this produces, so a fresh per-call
23+
// cache costs nothing extra on the (already cached) common path.
24+
//
25+
// The zero value is ready to use and allocates its backing map lazily
26+
// on the first miss, so a schema with no {field} text costs nothing.
27+
// A nil *fieldPatternCache is also valid (get/put are no-ops), so
28+
// callers that don't want caching can pass nil.
29+
type fieldPatternCache struct {
30+
m map[string]*regexp.Regexp
31+
}
32+
33+
func (c *fieldPatternCache) get(bodyText string) (*regexp.Regexp, bool) {
34+
if c == nil {
35+
return nil, false
36+
}
37+
re, ok := c.m[bodyText]
38+
return re, ok
39+
}
40+
41+
func (c *fieldPatternCache) put(bodyText string, re *regexp.Regexp) {
42+
if c == nil {
43+
return
44+
}
45+
if c.m == nil {
46+
c.m = make(map[string]*regexp.Regexp)
47+
}
48+
c.m[bodyText] = re
49+
}
50+
51+
// buildFieldPattern compiles a regex that matches a body line whose
52+
// {field} placeholders have been replaced by any non-empty run. The
53+
// pattern is always valid: each part is regexp.QuoteMeta'd and joined
54+
// with ".+", so regexp.MustCompile never panics here. See
55+
// fieldPatternCache's doc comment for cache's scope and lifetime.
56+
func buildFieldPattern(bodyText string, cache *fieldPatternCache) *regexp.Regexp {
57+
if re, ok := cache.get(bodyText); ok {
58+
return re
59+
}
60+
parts := fieldinterp.SplitOnFields(bodyText)
61+
var patBuf strings.Builder
62+
patBuf.WriteString("^")
63+
for i, part := range parts {
64+
patBuf.WriteString(regexp.QuoteMeta(part))
65+
if i < len(parts)-1 {
66+
patBuf.WriteString(".+")
67+
}
68+
}
69+
patBuf.WriteString("$")
70+
compiled := regexp.MustCompile(patBuf.String())
71+
cache.put(bodyText, compiled)
72+
return compiled
73+
}

internal/rules/requiredstructure/fieldpattern_cache_test.go renamed to internal/rules/requiredstructure/fieldpatterncache_test.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,22 +55,39 @@ func TestBuildFieldPattern_CacheHitZeroAllocs(t *testing.T) {
5555
}
5656
}
5757

58-
// TestFieldPatternCache_LazyAllocation confirms the zero-value cache
59-
// never allocates a backing map until the first put — a schema with no
60-
// {field} text must not pay for a map that's never populated.
61-
func TestFieldPatternCache_LazyAllocation(t *testing.T) {
62-
var cache fieldPatternCache
63-
if cache.m != nil {
64-
t.Fatal("zero-value fieldPatternCache must not pre-allocate its map")
58+
// TestFieldPatternCache_Get covers get's three paths: a nil receiver
59+
// (the "no cache" contract buildFieldPattern relies on), a miss on a
60+
// zero-value cache, and a miss must not allocate the backing map — a
61+
// schema with no {field} text must not pay for a map that's never
62+
// populated.
63+
func TestFieldPatternCache_Get(t *testing.T) {
64+
var nilCache *fieldPatternCache
65+
if _, ok := nilCache.get("anything"); ok {
66+
t.Fatal("get on a nil *fieldPatternCache must report a miss")
6567
}
68+
69+
var cache fieldPatternCache
6670
if _, ok := cache.get("anything"); ok {
6771
t.Fatal("get on an empty cache must report a miss")
6872
}
6973
if cache.m != nil {
7074
t.Fatal("get on a miss must not allocate the backing map")
7175
}
76+
}
77+
78+
// TestFieldPatternCache_Put covers put's three paths: a nil receiver is
79+
// a no-op (never panics), the first put lazily allocates the backing
80+
// map, and a put entry is retrievable via get.
81+
func TestFieldPatternCache_Put(t *testing.T) {
82+
var nilCache *fieldPatternCache
83+
nilCache.put("key", nil) // must not panic
84+
85+
var cache fieldPatternCache
7286
cache.put("key", nil)
7387
if cache.m == nil {
7488
t.Fatal("put must lazily allocate the backing map on first use")
7589
}
90+
if _, ok := cache.get("key"); !ok {
91+
t.Fatal("a put entry must be retrievable via get")
92+
}
7693
}

internal/rules/requiredstructure/rule.go

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -787,71 +787,6 @@ func resolveBodySyncLine(
787787
return patchedLine{}, false
788788
}
789789

790-
// fieldPatternCache memoizes buildFieldPattern's compiled regexes by
791-
// body text, scoped to a single parseSchemaWithRootFS call: within that
792-
// one call, buildSchemaHeading and collectBodySyncPoints can both hit
793-
// the same {field} template text (e.g. a repeated table-row pattern),
794-
// so a call-scoped cache avoids rebuilding an identical NFA.
795-
//
796-
// It is deliberately not a package-level var: bodyText is schema-
797-
// authored Markdown, which churns during interactive editing in
798-
// mdsmith lsp, unlike the bounded, static config patterns
799-
// compiledPatterns caches in maxsectionlength and requiredtextpatterns
800-
// — a process-lifetime cache here would grow with edit history instead
801-
// of workspace size. parseSchemaWithCache already caches and
802-
// invalidates the *parsedSchema this produces, so a fresh per-call
803-
// cache costs nothing extra on the (already cached) common path.
804-
//
805-
// The zero value is ready to use and allocates its backing map lazily
806-
// on the first miss, so a schema with no {field} text costs nothing.
807-
// A nil *fieldPatternCache is also valid (get/put are no-ops), so
808-
// callers that don't want caching can pass nil.
809-
type fieldPatternCache struct {
810-
m map[string]*regexp.Regexp
811-
}
812-
813-
func (c *fieldPatternCache) get(bodyText string) (*regexp.Regexp, bool) {
814-
if c == nil {
815-
return nil, false
816-
}
817-
re, ok := c.m[bodyText]
818-
return re, ok
819-
}
820-
821-
func (c *fieldPatternCache) put(bodyText string, re *regexp.Regexp) {
822-
if c == nil {
823-
return
824-
}
825-
if c.m == nil {
826-
c.m = make(map[string]*regexp.Regexp)
827-
}
828-
c.m[bodyText] = re
829-
}
830-
831-
// buildFieldPattern compiles a regex that matches a body line whose
832-
// {field} placeholders have been replaced by any non-empty run. The
833-
// pattern is always valid: each part is regexp.QuoteMeta'd and joined
834-
// with ".+", so regexp.MustCompile never panics here. See
835-
// fieldPatternCache's doc comment for cache's scope and lifetime.
836-
func buildFieldPattern(bodyText string, cache *fieldPatternCache) *regexp.Regexp {
837-
if re, ok := cache.get(bodyText); ok {
838-
return re
839-
}
840-
parts := fieldinterp.SplitOnFields(bodyText)
841-
var patBuf strings.Builder
842-
patBuf.WriteString("^")
843-
for i, part := range parts {
844-
patBuf.WriteString(regexp.QuoteMeta(part))
845-
if i < len(parts)-1 {
846-
patBuf.WriteString(".+")
847-
}
848-
}
849-
patBuf.WriteString("$")
850-
compiled := regexp.MustCompile(patBuf.String())
851-
cache.put(bodyText, compiled)
852-
return compiled
853-
}
854-
855790
// buildSchemaHeading constructs a schemaHeading from a docHeading,
856791
// pre-compiling the field-interpolation regex when the text contains
857792
// {field} references so matchesSchema pays no per-call compile cost.

0 commit comments

Comments
 (0)