From cc1026de6c10307c2069d892ec3b29bb88a05a9e Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Fri, 26 Jun 2026 09:56:23 +0200 Subject: [PATCH 1/5] Make resolver cache configurable. --- embedding/orchestration.go | 5 ++++- embedding/parsing/context.go | 17 ++++++++++++++++- embedding/parsing/instruction.go | 12 ++++++++++-- embedding/processor.go | 6 +++++- fragmentation/resolver.go | 25 ++++++++++++++++++++----- 5 files changed, 55 insertions(+), 10 deletions(-) diff --git a/embedding/orchestration.go b/embedding/orchestration.go index 8348b39..2db313d 100644 --- a/embedding/orchestration.go +++ b/embedding/orchestration.go @@ -159,7 +159,10 @@ func processRequiredDocs( } var processingErrors []error - resolver := fragmentation.NewResolver() + resolver, err := fragmentation.NewResolver(fragmentation.DefaultResolverCacheLimit) + if err != nil { + return requiredDocPaths, []error{err} + } for _, doc := range requiredDocPaths { processor := newProcessor(doc, config, parsing.Transitions, requiredDocPaths, resolver) if err := handle(doc, processor); err != nil { diff --git a/embedding/parsing/context.go b/embedding/parsing/context.go index 7ed365e..8c347a3 100644 --- a/embedding/parsing/context.go +++ b/embedding/parsing/context.go @@ -106,14 +106,29 @@ type EmbeddingContext struct { // Context - initialized parsing context. // error - when the documentation file cannot be read. func NewContext(markdownFile string) (Context, error) { - return NewContextWithResolver(markdownFile, fragmentation.NewResolver()) + resolver, err := fragmentation.NewResolver(fragmentation.DefaultResolverCacheLimit) + if err != nil { + return Context{}, err + } + + return NewContextWithResolver(markdownFile, resolver) } // NewContextWithResolver creates a parsing context using the provided source resolver. +// +// If resolver is nil, it creates a resolver with the default cache limit. func NewContextWithResolver( markdownFile string, resolver *fragmentation.Resolver, ) (Context, error) { + if resolver == nil { + var err error + resolver, err = fragmentation.NewResolver(fragmentation.DefaultResolverCacheLimit) + if err != nil { + return Context{}, err + } + } + source, err := readLines(markdownFile) if err != nil { return Context{}, err diff --git a/embedding/parsing/instruction.go b/embedding/parsing/instruction.go index e2a0a75..e63fd13 100644 --- a/embedding/parsing/instruction.go +++ b/embedding/parsing/instruction.go @@ -133,6 +133,10 @@ func NewInstruction( if err != nil { return Instruction{}, err } + resolver, err := fragmentation.NewResolver(fragmentation.DefaultResolverCacheLimit) + if err != nil { + return Instruction{}, err + } return Instruction{ CodeFile: codeFile, @@ -142,7 +146,7 @@ func NewInstruction( LinePattern: patterns.line, CommentMode: commentMode, Configuration: config, - resolver: fragmentation.NewResolver(), + resolver: resolver, }, nil } @@ -223,7 +227,11 @@ func parseInstructionPattern(attribute string, value string) (Pattern, error) { func (e Instruction) Content() ([]string, error) { resolver := e.resolver if resolver == nil { - resolver = fragmentation.NewResolver() + var err error + resolver, err = fragmentation.NewResolver(fragmentation.DefaultResolverCacheLimit) + if err != nil { + return nil, err + } } fileContent, err := resolver.ResolveContent(e.CodeFile, e.Fragment, e.Configuration) diff --git a/embedding/processor.go b/embedding/processor.go index 73e7e7d..5b6a368 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -65,13 +65,17 @@ func NewProcessor(docFile string, config configuration.Configuration) (Processor if err != nil { return Processor{}, err } + resolver, err := fragmentation.NewResolver(fragmentation.DefaultResolverCacheLimit) + if err != nil { + return Processor{}, err + } return newProcessor( docFile, config, parsing.Transitions, requiredDocPaths, - fragmentation.NewResolver(), + resolver, ), nil } diff --git a/fragmentation/resolver.go b/fragmentation/resolver.go index 9f9729c..39ae538 100644 --- a/fragmentation/resolver.go +++ b/fragmentation/resolver.go @@ -31,8 +31,12 @@ import ( _type "embed-code/embed-code-go/type" ) -// resolverCacheLimit is the maximum number of source files retained in the resolver cache. -const resolverCacheLimit = 100 +// DefaultResolverCacheLimit is the default maximum number of source files +// retained in a resolver cache. +const DefaultResolverCacheLimit = 100 + +// ErrInvalidResolverCacheLimit reports a resolver cache size below one. +var ErrInvalidResolverCacheLimit = errors.New("resolver cache limit must be at least 1") // fragmentedFile stores cleaned source lines and parsed fragments for one source file. type fragmentedFile struct { @@ -53,13 +57,24 @@ type Resolver struct { } // NewResolver creates a resolver with an independent source-fragment cache. -func NewResolver() *Resolver { +// +// Parameters: +// cacheLimit - maximum number of source files retained in the resolver cache. +// +// Returns: +// *Resolver - resolver with an independent cache. +// error - when cacheLimit is below one. +func NewResolver(cacheLimit int) (*Resolver, error) { + if cacheLimit < 1 { + return nil, ErrInvalidResolverCacheLimit + } + return &Resolver{ cache: newCache[absolutePath, fragmentedFile]( - resolverCacheLimit, + cacheLimit, loadSourceFragments, ), - } + }, nil } // ResolveContent returns source lines for the requested code file fragment. From c27cd9b636c427656abecd711fdbb5ec594ee0b6 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Fri, 26 Jun 2026 09:56:29 +0200 Subject: [PATCH 2/5] Add tests. --- embedding/parsing/instruction_test.go | 34 ++++++++++++++ fragmentation/fragmentation_test.go | 65 ++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/embedding/parsing/instruction_test.go b/embedding/parsing/instruction_test.go index 9722722..7fe8c9d 100644 --- a/embedding/parsing/instruction_test.go +++ b/embedding/parsing/instruction_test.go @@ -542,6 +542,40 @@ var _ = Describe("Instruction", func() { ), )) }) + + It("should default a nil context resolver to a shared resolver", func() { + sourceRoot := GinkgoT().TempDir() + markdownPath := filepath.Join(GinkgoT().TempDir(), "doc.md") + codePath := filepath.Join(sourceRoot, "Example.java") + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: sourceRoot}} + Expect(os.WriteFile(markdownPath, []byte(""), 0600)).To(Succeed()) + Expect(os.WriteFile( + codePath, + []byte("class Example { String version = \"first\"; }"), + 0600, + )).To(Succeed()) + context, err := parsing.NewContextWithResolver(markdownPath, nil) + Expect(err).ShouldNot(HaveOccurred()) + context.StartEmbedding(parsing.Instruction{ + CodeFile: "Example.java", + Configuration: config, + }) + + firstContent, err := context.EmbeddingInstruction.Content() + Expect(err).ShouldNot(HaveOccurred()) + Expect(os.WriteFile( + codePath, + []byte("class Example { String version = \"second\"; }"), + 0600, + )).To(Succeed()) + secondContent, err := context.EmbeddingInstruction.Content() + Expect(err).ShouldNot(HaveOccurred()) + + Expect(secondContent).Should(Equal(firstContent)) + Expect(secondContent).Should(Equal([]string{ + "class Example { String version = \"first\"; }", + })) + }) }) // getXMLExtractionContent returns source lines selected by an XML instruction fixture. diff --git a/fragmentation/fragmentation_test.go b/fragmentation/fragmentation_test.go index 690a97f..500ba37 100644 --- a/fragmentation/fragmentation_test.go +++ b/fragmentation/fragmentation_test.go @@ -22,6 +22,7 @@ import ( "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/fragmentation" _type "embed-code/embed-code-go/type" + "errors" "fmt" "os" "path/filepath" @@ -53,7 +54,7 @@ var _ = Describe("Fragmentation", func() { var resolver *fragmentation.Resolver BeforeEach(func() { - resolver = fragmentation.NewResolver() + resolver = newTestResolver(fragmentation.DefaultResolverCacheLimit) config = configuration.NewConfiguration() config.DocumentationRoot = "../test/resources/docs" config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/java"}} @@ -149,7 +150,8 @@ var _ = Describe("Fragmentation", func() { config, ) Expect(err).ShouldNot(HaveOccurred()) - freshContent, err := fragmentation.NewResolver().ResolveContent( + freshResolver := newTestResolver(fragmentation.DefaultResolverCacheLimit) + freshContent, err := freshResolver.ResolveContent( fileName, fragmentation.DefaultFragmentName, config, @@ -161,6 +163,36 @@ var _ = Describe("Fragmentation", func() { Expect(freshContent).Should(Equal([]string{"class Second {}"})) }) + It("should evict the least recently used source from a limited resolver cache", func() { + sourceRoot := GinkgoT().TempDir() + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: sourceRoot}} + resolver = newTestResolver(2) + writeSourceFile(sourceRoot, "A.java", "class A { String version = \"first\"; }") + writeSourceFile(sourceRoot, "B.java", "class B { String version = \"first\"; }") + writeSourceFile(sourceRoot, "C.java", "class C { String version = \"first\"; }") + + firstA := resolveSourceFile(resolver, "A.java", config) + _ = resolveSourceFile(resolver, "B.java", config) + cachedA := resolveSourceFile(resolver, "A.java", config) + writeSourceFile(sourceRoot, "A.java", "class A { String version = \"second\"; }") + writeSourceFile(sourceRoot, "B.java", "class B { String version = \"second\"; }") + _ = resolveSourceFile(resolver, "C.java", config) + + stillCachedA := resolveSourceFile(resolver, "A.java", config) + reloadedB := resolveSourceFile(resolver, "B.java", config) + + Expect(cachedA).Should(Equal(firstA)) + Expect(stillCachedA).Should(Equal(firstA)) + Expect(reloadedB).Should(Equal([]string{"class B { String version = \"second\"; }"})) + }) + + It("should reject cache limits below one", func() { + resolver, err := fragmentation.NewResolver(0) + + Expect(resolver).Should(BeNil()) + Expect(errors.Is(err, fragmentation.ErrInvalidResolverCacheLimit)).Should(BeTrue()) + }) + It("should fail on an unopened fragment", func() { frag := buildTestFragmentation(unopenedFragmentFileName, config) @@ -327,3 +359,32 @@ func resolveTestFragment( return content } + +// newTestResolver creates a resolver with a test-provided cache limit. +func newTestResolver(cacheLimit int) *fragmentation.Resolver { + resolver, err := fragmentation.NewResolver(cacheLimit) + Expect(err).ShouldNot(HaveOccurred()) + + return resolver +} + +// writeSourceFile writes one source fixture into a temporary source root. +func writeSourceFile(sourceRoot string, fileName string, content string) { + Expect(os.WriteFile(filepath.Join(sourceRoot, fileName), []byte(content), 0600)).To(Succeed()) +} + +// resolveSourceFile returns the whole-file content resolved from a temporary source root. +func resolveSourceFile( + resolver *fragmentation.Resolver, + fileName string, + config configuration.Configuration, +) []string { + content, err := resolver.ResolveContent( + fileName, + fragmentation.DefaultFragmentName, + config, + ) + Expect(err).ShouldNot(HaveOccurred()) + + return content +} From 0dbf946ba3d6df41bf0bc2c56edbf85ba06a98b9 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Fri, 26 Jun 2026 13:26:13 +0200 Subject: [PATCH 3/5] Remove unused resolver provisioning. --- embedding/parsing/instruction.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/embedding/parsing/instruction.go b/embedding/parsing/instruction.go index e63fd13..d653203 100644 --- a/embedding/parsing/instruction.go +++ b/embedding/parsing/instruction.go @@ -133,10 +133,6 @@ func NewInstruction( if err != nil { return Instruction{}, err } - resolver, err := fragmentation.NewResolver(fragmentation.DefaultResolverCacheLimit) - if err != nil { - return Instruction{}, err - } return Instruction{ CodeFile: codeFile, @@ -146,7 +142,6 @@ func NewInstruction( LinePattern: patterns.line, CommentMode: commentMode, Configuration: config, - resolver: resolver, }, nil } From 567501bf6fb6e5684d929d0b26cb7db713482509 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 30 Jun 2026 17:28:16 +0200 Subject: [PATCH 4/5] Improve doc. --- embedding/parsing/instruction_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embedding/parsing/instruction_test.go b/embedding/parsing/instruction_test.go index 7fe8c9d..fc3a8ac 100644 --- a/embedding/parsing/instruction_test.go +++ b/embedding/parsing/instruction_test.go @@ -543,7 +543,7 @@ var _ = Describe("Instruction", func() { )) }) - It("should default a nil context resolver to a shared resolver", func() { + It("should use shared resolver as default", func() { sourceRoot := GinkgoT().TempDir() markdownPath := filepath.Join(GinkgoT().TempDir(), "doc.md") codePath := filepath.Join(sourceRoot, "Example.java") From c2ce65d1d740bdf6f9294e768e50adfa4d632263 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Tue, 30 Jun 2026 17:31:52 +0200 Subject: [PATCH 5/5] Improve error behavior. --- fragmentation/fragmentation_test.go | 3 +-- fragmentation/resolver.go | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/fragmentation/fragmentation_test.go b/fragmentation/fragmentation_test.go index 500ba37..6159143 100644 --- a/fragmentation/fragmentation_test.go +++ b/fragmentation/fragmentation_test.go @@ -22,7 +22,6 @@ import ( "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/fragmentation" _type "embed-code/embed-code-go/type" - "errors" "fmt" "os" "path/filepath" @@ -190,7 +189,7 @@ var _ = Describe("Fragmentation", func() { resolver, err := fragmentation.NewResolver(0) Expect(resolver).Should(BeNil()) - Expect(errors.Is(err, fragmentation.ErrInvalidResolverCacheLimit)).Should(BeTrue()) + Expect(err).Should(HaveOccurred()) }) It("should fail on an unopened fragment", func() { diff --git a/fragmentation/resolver.go b/fragmentation/resolver.go index 39ae538..2588f01 100644 --- a/fragmentation/resolver.go +++ b/fragmentation/resolver.go @@ -35,9 +35,6 @@ import ( // retained in a resolver cache. const DefaultResolverCacheLimit = 100 -// ErrInvalidResolverCacheLimit reports a resolver cache size below one. -var ErrInvalidResolverCacheLimit = errors.New("resolver cache limit must be at least 1") - // fragmentedFile stores cleaned source lines and parsed fragments for one source file. type fragmentedFile struct { // lines contains the cleaned source lines. @@ -66,7 +63,7 @@ type Resolver struct { // error - when cacheLimit is below one. func NewResolver(cacheLimit int) (*Resolver, error) { if cacheLimit < 1 { - return nil, ErrInvalidResolverCacheLimit + return nil, errors.New("resolver cache limit must be at least 1") } return &Resolver{