From 820eeaedadb21c53955078536f3e06480a981684 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 13 May 2026 16:20:17 +0200 Subject: [PATCH 01/11] Implement in-memory fragments resolver. --- embedding/parsing/instruction.go | 18 +-- fragmentation/fragment.go | 14 -- fragmentation/fragment_file.go | 240 ------------------------------- fragmentation/fragmentation.go | 163 +-------------------- fragmentation/resolver.go | 214 +++++++++++++++++++++++++++ 5 files changed, 219 insertions(+), 430 deletions(-) delete mode 100644 fragmentation/fragment_file.go create mode 100644 fragmentation/resolver.go diff --git a/embedding/parsing/instruction.go b/embedding/parsing/instruction.go index 1033eb3..76cc87f 100644 --- a/embedding/parsing/instruction.go +++ b/embedding/parsing/instruction.go @@ -100,25 +100,15 @@ func NewInstruction( // // Returns an error if there was an error during reading the content. func (e Instruction) Content() ([]string, error) { - fragmentName := e.Fragment - if fragmentName == "" { - fragmentName = fragmentation.DefaultFragmentName - } - file := fragmentation.FragmentFile{ - CodePath: e.CodeFile, - FragmentName: fragmentName, - Configuration: e.Configuration, + fileContent, err := fragmentation.ResolveContent(e.CodeFile, e.Fragment, e.Configuration) + if err != nil { + return nil, err } if e.StartPattern != nil || e.EndPattern != nil { - fileContent, err := file.Content() - if err != nil { - return nil, err - } - return e.matchingLines(fileContent), nil } - return file.Content() + return fileContent, nil } // Returns string representation of Instruction. diff --git a/fragmentation/fragment.go b/fragmentation/fragment.go index aedfdee..fa45fbd 100644 --- a/fragmentation/fragment.go +++ b/fragmentation/fragment.go @@ -44,20 +44,6 @@ func CreateDefaultFragment() Fragment { } } -// WriteTo takes given lines, unites them into a text and writes it into given file. -// -// file — a FragmentFile to write the lines to. -// -// lines — a list of strings to write. -// -// separator — string to insert between multiple partitions of a single fragment. -// -// Creates the file if not exists and overwrites if exists. -func (f Fragment) WriteTo(file FragmentFile, lines []string, separator string) { - text := f.text(lines, separator) - file.Write(text) -} - func (f Fragment) isDefault() bool { return f.Name == DefaultFragmentName } diff --git a/fragmentation/fragment_file.go b/fragmentation/fragment_file.go deleted file mode 100644 index c3b465e..0000000 --- a/fragmentation/fragment_file.go +++ /dev/null @@ -1,240 +0,0 @@ -// Copyright 2026, TeamDev. All rights reserved. -// -// Redistribution and use in source and/or binary forms, with or without -// modification, must retain the above copyright notice and the following -// disclaimer. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package fragmentation - -import ( - "crypto/sha256" - _type "embed-code/embed-code-go/type" - "encoding/hex" - "fmt" - "os" - "path/filepath" - "strings" - - config "embed-code/embed-code-go/configuration" - "embed-code/embed-code-go/files" -) - -// FragmentFile is a file storing a single fragment from the file. -// -// CodePath — a relative path to a code file. The path is relative to the corresponding code root, -// and starts with the code root name if it's provided. -// -// FragmentName — a name of the fragment in the code file. -// -// Configuration — a configuration for embedding. -type FragmentFile struct { - CodePath string - FragmentName string - Configuration config.Configuration -} - -// NewFragmentFileFromAbsolute composes a FragmentFile for the given fragment in given codeFile. -// -// codeFile — an absolute path to a code file. -// -// codeRoot - a _type.NamedPath to the code root. -// -// fragmentName — a name of the fragment in the code file. -// -// configuration — configuration for embedding. -// -// Returns composed fragment. -func NewFragmentFileFromAbsolute( - codeFile string, - codeRoot _type.NamedPath, - fragmentName string, - config config.Configuration, -) FragmentFile { - absolutePath, err := filepath.Abs(codeRoot.Path) - if err != nil { - panic(err) - } - - relativePath, err := filepath.Rel(absolutePath, codeFile) - if err != nil { - panic(err) - } - - if strings.TrimSpace(codeRoot.Name) != "" { - relativePath = filepath.Join(NamedPathPrefix+codeRoot.Name, relativePath) - } - - return FragmentFile{ - CodePath: relativePath, - FragmentName: fragmentName, - Configuration: config, - } -} - -// Writes the given text to the file. -// -// Creates the file if not exists and overwrites if exists. -func (f FragmentFile) Write(text string) { - byteStr := []byte(text) - filePath := f.absolutePath() - err := os.WriteFile(filePath, byteStr, os.FileMode(files.WritePermission)) - if err != nil { - panic(err) - } -} - -// Content reads content of the file. -// -// Returns contents of the file as a list of strings, or returns an error if it doesn't exist. -func (f FragmentFile) Content() ([]string, error) { - path := f.absolutePath() - isPathFileExits, err := files.IsFileExist(path) - - if err != nil { - return nil, err - } - - if !isPathFileExits { - codeFileReference, err := f.codeFileReference() - if err != nil { - return nil, err - } - - if f.FragmentName != "" { - if f.FragmentName == "_default" { - return nil, fmt.Errorf("code file `%s` not found", codeFileReference) - } - return nil, fmt.Errorf( - "fragment `%s` from code file `%s` not found", f.FragmentName, codeFileReference, - ) - } - return nil, fmt.Errorf( - "code file %s fragment not found", - codeFileReference, - ) - } - - return files.ReadFile(path) -} - -// Returns string representation of FragmentFile. -func (f FragmentFile) String() string { - return f.absolutePath() -} - -// Obtains the absolute path to this fragment file. -func (f FragmentFile) absolutePath() string { - fileExtension := filepath.Ext(f.CodePath) - fragmentsAbsDir, err := filepath.Abs(f.Configuration.FragmentsDir) - if err != nil { - panic(err) - } - - if f.FragmentName == DefaultFragmentName { - return filepath.Join(fragmentsAbsDir, f.CodePath) - } - - withoutExtension := strings.TrimSuffix(f.CodePath, fileExtension) - filename := fmt.Sprintf("%s-%s", withoutExtension, f.fragmentHash()) - - return filepath.Join(fragmentsAbsDir, filename+fileExtension) -} - -// Builds a user-facing reference to the original code file for error messages. -// -// If the source file exists, returns its absolute `file://` URL. If the file path uses a named -// code root and the resolved file does not exist, returns both the prefixed path and the expanded -// absolute path. -func (f FragmentFile) codeFileReference() (string, error) { - originalCodePath, isPrefixed, err := f.originalCodePath() - if err != nil { - return "", err - } - if originalCodePath == "" { - return f.CodePath, nil - } - - exists, err := files.IsFileExist(originalCodePath) - if err != nil { - return "", err - } - if exists { - return "file://" + originalCodePath, nil - } - if isPrefixed { - return fmt.Sprintf("%s (%s)", f.CodePath, originalCodePath), nil - } - - return originalCodePath, nil -} - -// Resolves the original source file path from the fragment's code path. -// -// Returns the absolute path to the source file and reports whether the input path used a named -// code-root prefix such as `$runtime/...`. -// -// Returns an error if the path uses a named code root that is not present in the configuration. -func (f FragmentFile) originalCodePath() (string, bool, error) { - normalizedPath := filepath.ToSlash(filepath.Clean(f.CodePath)) - - if strings.HasPrefix(normalizedPath, NamedPathPrefix) { - withoutPrefix := strings.TrimPrefix(normalizedPath, NamedPathPrefix) - codeRootName, relativePath, _ := strings.Cut(withoutPrefix, "/") - - for _, codeRoot := range f.Configuration.CodeRoots { - if strings.TrimSpace(codeRoot.Name) != codeRootName { - continue - } - - return filepath.Join(resolvedRootPath(codeRoot.Path), filepath.FromSlash(relativePath)), - true, nil - } - - return "", true, fmt.Errorf("code root with name `%s` not found for path `%s`", - codeRootName, f.CodePath) - } - - if len(f.Configuration.CodeRoots) == 1 { - return filepath.Join( - resolvedRootPath(f.Configuration.CodeRoots[0].Path), - filepath.FromSlash(normalizedPath), - ), false, nil - } - - return "", false, nil -} - -// Resolves the given path to an absolute path when possible. -// -// If absolute-path resolution fails, returns the original path. -func resolvedRootPath(path string) string { - absolutePath, err := filepath.Abs(path) - if err != nil { - return path - } - - return absolutePath -} - -// Calculates and returns a hash string for FragmentFile. -// -// Since fragments which have the same name unite into one fragment with multiple partitions, -// the name of a fragment is unique. -func (f FragmentFile) fragmentHash() string { - hash := sha256.New() - hash.Write([]byte(f.FragmentName)) - - return hex.EncodeToString(hash.Sum(nil))[:8] -} diff --git a/fragmentation/fragmentation.go b/fragmentation/fragmentation.go index b08c666..1e3ebf3 100644 --- a/fragmentation/fragmentation.go +++ b/fragmentation/fragmentation.go @@ -40,21 +40,16 @@ import ( "embed-code/embed-code-go/files" _type "embed-code/embed-code-go/type" "fmt" - "log/slog" "os" "path/filepath" - "strings" config "embed-code/embed-code-go/configuration" - - "github.com/bmatcuk/doublestar/v4" ) // NamedPathPrefix the prefix before the named code source. const NamedPathPrefix = "$" -// Fragmentation splits the given file into fragments and writes them into corresponding -// output files. +// Fragmentation splits the given file into fragments. // // Configuration — a configuration for embedding. // @@ -68,17 +63,6 @@ type Fragmentation struct { fragmentBuilders map[string]*FragmentBuilder } -// WriteFragmentFilesResult is result of the WriteFragmentFiles method. -// -// TotalSourceFiles total number of source code files. -// -// TotalFragments is the total number of fragments extracted from the source code files. -// A whole source file also counts as a fragment. -type WriteFragmentFilesResult struct { - TotalSourceFiles int - TotalFragments int -} - // NewFragmentation builds Fragmentation from given codeFileRelative and config. // // codeFileRelative — a relative path to a code file to fragment. @@ -151,129 +135,6 @@ func (f Fragmentation) DoFragmentation() ([]string, map[string]Fragment, error) return contentToRender, fragments, nil } -// WriteFragments serializes fragments to the output directory. -// -// Keeps the original directory structure relative to the sources root dir. -// That is, `SRC/src/main` becomes `OUT/src/main`. -// -// Returns fragments or an error if the fragmentation couldn't be done. -func (f Fragmentation) WriteFragments() (map[string]Fragment, error) { - allLines, fragments, err := f.DoFragmentation() - if err != nil { - return nil, err - } - - err = files.EnsureDirExists(f.targetDirectory()) - if err != nil { - return nil, err - } - - for _, fragment := range fragments { - fragmentFile := NewFragmentFileFromAbsolute( - f.CodeFile, f.SourcesRoot, fragment.Name, f.Configuration, - ) - fragment.WriteTo(fragmentFile, allLines, f.Configuration.Separator) - } - - return fragments, nil -} - -// WriteFragmentFiles writes each fragment into a corresponding file. -// -// Searches for code files with patterns defined in configuration and makes fragments of them with -// creating fragmented files as a result. -// -// All fragments are placed inside Configuration.FragmentsDir with keeping the original directory -// structure relative to the sources root dir. -// That is, `SRC/src/main` becomes `OUT/src/main`. -// If code root is named, `SRC/src/main` becomes `OUT/$CODE_ROOT_NAME/src/main` -// -// config — is a configuration for embedding. -// -// Returns an error if any of the fragments couldn't be written. -func WriteFragmentFiles(config config.Configuration) WriteFragmentFilesResult { - includes := config.CodeIncludes - codeRoots := config.CodeRoots - totalSourceFiles := 0 - totalFragments := 0 - for _, codeRoot := range codeRoots { - codeRootFiles := 0 - codeRootFragments := 0 - for _, rule := range includes { - pattern := filepath.Join(codeRoot.Path, rule) - codeFiles, err := doublestar.FilepathGlob(pattern) - codeRootFiles += len(codeFiles) - if err != nil { - panic(err) - } - for _, codeFile := range codeFiles { - fragments, err := writeFragments(config, codeRoot, codeFile) - if err != nil { - panic(err) - } - codeRootFragments += len(fragments) - } - } - totalSourceFiles += codeRootFiles - totalFragments += codeRootFragments - if codeRootFiles > 0 { - slog.Info( - fmt.Sprintf("Found `%d` source code files with `%d` fragments under `%s`%s.", - codeRootFiles, codeRootFragments, codeRoot.Path, configNameLabel(config)), - ) - } else { - slog.Warn( - fmt.Sprintf("No code fragments were found under `%s`%s.", - codeRoot.Path, configNameLabel(config)), - ) - } - } - - return WriteFragmentFilesResult{ - TotalSourceFiles: totalSourceFiles, - TotalFragments: totalFragments, - } -} - -func configNameLabel(config config.Configuration) string { - if config.Name == "" { - return "" - } - return fmt.Sprintf(" for embedding `%s`", config.Name) -} - -// CleanFragmentFiles deletes Configuration.FragmentsDir if it exists. -func CleanFragmentFiles(config config.Configuration) { - exists, err := files.IsDirExist(config.FragmentsDir) - if err != nil { - panic(err) - } - if !exists { - panic(fmt.Errorf("%s directory is not exist", config.FragmentsDir)) - } - if err = os.RemoveAll(config.FragmentsDir); err != nil { - panic(err) - } -} - -// Checks if the code is able to split into fragments and writes them to a file. -func writeFragments( - config config.Configuration, - codeRoot _type.NamedPath, - codeFile string, -) (map[string]Fragment, error) { - if shouldDoFragmentation(codeFile) { - fragmentation := NewFragmentation(codeFile, codeRoot, config) - fragments, err := fragmentation.WriteFragments() - if err != nil { - return nil, err - } - return fragments, nil - } - - return map[string]Fragment{}, nil -} - // shouldDoFragmentation reports whether the file is valid to do fragmentation: // - it exists by the given path // - it is a file (not a dir) @@ -365,25 +226,3 @@ func (f Fragmentation) parseEndDocFragments(endDocFragments []string, cursor int return nil } - -// Obtains the target directory path based on the Configuration.FragmentsDir and the parent -// dir of Fragmentation.CodeFile. -func (f Fragmentation) targetDirectory() string { - fragmentsDir := f.Configuration.FragmentsDir - codeRoot, err := filepath.Abs(f.SourcesRoot.Path) - codeRootName := strings.TrimSpace(f.SourcesRoot.Name) - if err != nil { - panic(fmt.Sprintf("error calculating absolute path: %v", err)) - } - relativeFile, err := filepath.Rel(codeRoot, f.CodeFile) - if err != nil { - panic(fmt.Sprintf("error calculating relative path: %v", err)) - } - subTree := filepath.Dir(relativeFile) - - if codeRootName != "" { - return filepath.Join(fragmentsDir, NamedPathPrefix+codeRootName, subTree) - } - - return filepath.Join(fragmentsDir, subTree) -} diff --git a/fragmentation/resolver.go b/fragmentation/resolver.go new file mode 100644 index 0000000..8635ef4 --- /dev/null +++ b/fragmentation/resolver.go @@ -0,0 +1,214 @@ +// Copyright 2026, TeamDev. All rights reserved. +// +// Redistribution and use in source and/or binary forms, with or without +// modification, must retain the above copyright notice and the following +// disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package fragmentation + +import ( + "fmt" + "path/filepath" + "strings" + "sync" + + config "embed-code/embed-code-go/configuration" + _type "embed-code/embed-code-go/type" +) + +type cachedFragmentation struct { + lines []string + fragments map[string]Fragment +} + +var resolverCache = struct { + sync.RWMutex + files map[string]cachedFragmentation +}{ + files: make(map[string]cachedFragmentation), +} + +// ResolveContent returns source lines for the requested code file fragment. +// +// Named fragments are extracted directly from the source file on demand and cached by source file. +func ResolveContent(codePath string, fragmentName string, config config.Configuration) ([]string, error) { + if fragmentName == "" { + fragmentName = DefaultFragmentName + } + + source, found, err := resolveSource(codePath, config) + if err != nil { + return nil, err + } + if !found { + return nil, unresolvedSourceError(codePath, fragmentName, config) + } + + content, err := cachedSourceFragments(source, config) + if err != nil { + return nil, err + } + + fragment, found := content.fragments[fragmentName] + if !found { + codeFileReference := "file://" + source.absolutePath + return nil, fmt.Errorf("fragment `%s` from code file `%s` not found", + fragmentName, codeFileReference) + } + + return fragmentLines(fragment, content.lines, config.Separator), nil +} + +// ClearResolverCache removes cached source fragmentations. +func ClearResolverCache() { + resolverCache.Lock() + defer resolverCache.Unlock() + + resolverCache.files = make(map[string]cachedFragmentation) +} + +type resolvedSource struct { + root _type.NamedPath + relativePath string + absolutePath string +} + +// resolveSource resolves the user-facing code path to an included textual source file. +func resolveSource(codePath string, config config.Configuration) (resolvedSource, bool, error) { + codeRootName, relativePath, named := splitNamedPath(codePath) + for _, root := range config.CodeRoots { + if named && strings.TrimSpace(root.Name) != codeRootName { + continue + } + + source, err := sourceFromRoot(root, relativePath) + if err != nil { + return resolvedSource{}, false, err + } + if !shouldDoFragmentation(source.absolutePath) { + continue + } + + return source, true, nil + } + + return resolvedSource{}, false, nil +} + +// splitNamedPath separates a named-code-root prefix from a code path. +func splitNamedPath(codePath string) (string, string, bool) { + normalizedPath := filepath.ToSlash(filepath.Clean(codePath)) + if !strings.HasPrefix(normalizedPath, NamedPathPrefix) { + return "", normalizedPath, false + } + + withoutPrefix := strings.TrimPrefix(normalizedPath, NamedPathPrefix) + rootName, relativePath, _ := strings.Cut(withoutPrefix, "/") + + return rootName, relativePath, true +} + +// sourceFromRoot builds a source path from a code root and a relative path. +func sourceFromRoot(root _type.NamedPath, relativePath string) (resolvedSource, error) { + rootAbs, err := filepath.Abs(root.Path) + if err != nil { + return resolvedSource{}, err + } + + return resolvedSource{ + root: root, + relativePath: filepath.FromSlash(relativePath), + absolutePath: filepath.Join(rootAbs, filepath.FromSlash(relativePath)), + }, nil +} + +// cachedSourceFragments returns cached source fragmentation for a resolved source file. +func cachedSourceFragments(source resolvedSource, config config.Configuration) (cachedFragmentation, error) { + cacheKey := source.absolutePath + resolverCache.RLock() + content, found := resolverCache.files[cacheKey] + resolverCache.RUnlock() + if found { + return content, nil + } + + fragmentation := NewFragmentation(source.absolutePath, source.root, config) + lines, fragments, err := fragmentation.DoFragmentation() + if err != nil { + return cachedFragmentation{}, err + } + content = cachedFragmentation{ + lines: lines, + fragments: fragments, + } + + resolverCache.Lock() + resolverCache.files[cacheKey] = content + resolverCache.Unlock() + + return content, nil +} + +// fragmentLines renders a fragment into lines. +func fragmentLines(fragment Fragment, lines []string, separator string) []string { + text := fragment.text(lines, separator) + if text == "" { + return []string{} + } + + return strings.Split(strings.TrimSuffix(text, "\n"), "\n") +} + +// unresolvedSourceError builds an error for a code path that cannot be resolved from sources. +func unresolvedSourceError(codePath string, fragmentName string, config config.Configuration) error { + codeFileReference, err := codeFileReference(codePath, config) + if err != nil { + return err + } + if fragmentName == DefaultFragmentName { + return fmt.Errorf("code file `%s` not found", codeFileReference) + } + + return fmt.Errorf("fragment `%s` from code file `%s` not found", + fragmentName, codeFileReference) +} + +// codeFileReference builds a user-facing source reference for unresolved code paths. +func codeFileReference(codePath string, config config.Configuration) (string, error) { + codeRootName, relativePath, named := splitNamedPath(codePath) + for _, root := range config.CodeRoots { + if named && strings.TrimSpace(root.Name) != codeRootName { + continue + } + + source, err := sourceFromRoot(root, relativePath) + if err != nil { + return "", err + } + if named { + return fmt.Sprintf("%s (%s)", codePath, source.absolutePath), nil + } + if len(config.CodeRoots) == 1 { + return source.absolutePath, nil + } + } + + if named { + return "", fmt.Errorf("code root with name `%s` not found for path `%s`", + codeRootName, codePath) + } + + return codePath, nil +} From 0a833d2bed1872e4297dd323a8d510c4bce84907 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 13 May 2026 16:21:59 +0200 Subject: [PATCH 02/11] Remove redundant params. --- README.md | 6 ------ cli/cli.go | 29 ----------------------------- cli/cli_test.go | 6 +++--- cli/cli_validation.go | 4 +--- main.go | 9 +-------- 5 files changed, 5 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index e7a8578..73c6183 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,6 @@ The available arguments are: * `-code-path`: (Optional) Path to the source code root directory. * `-docs-path`: (Optional) Path to the documentation root directory. * `-config-path`: (Optional) Path to a YAML configuration file containing `code-path` and `docs-path`. - * `-code-includes`: (Optional) Comma-separated glob patterns for source files to include (e.g., `"**/*.java,**/*.gradle"`). Defaults to `"**/*.*"`. - * `-code-excludes`: (Optional) Comma-separated glob patterns for source files to exclude. * `-doc-includes`: (Optional) Comma-separated glob patterns for documentation files to include. Defaults to `"**/*.md,**/*.html"`. * `-fragments-path`: (Optional) Directory for storing code fragments. Defaults to `./build/fragments`. * `-separator`: (Optional) String used to separate joined code fragments. Defaults to `...`. @@ -87,7 +85,6 @@ Optional settings can be defined in a YAML configuration file: ```yaml code-path: path/to/code/root docs-path: path/to/docs/root -code-includes: "**/*.java,**/*.gradle" doc-excludes: "**/*-old.*,**/deprecated/*.*" ``` @@ -98,7 +95,6 @@ embeddings: - name: java code-path: path/to/code/root/java docs-path: path/to/java/docs - code-includes: "**/*.java" - name: kotlin code-path: - name: samples @@ -138,8 +134,6 @@ The available fields for the configuration file are: but this may lead to fragments being overwritten if they have the same relative path and name. * `docs-path`: (Mandatory) Path to the documentation root. - * `code-includes`: (Optional) Glob patterns for source files to include. - It may be represented as a comma-separated string list or as a YAML sequence. * `doc-excludes`: (Optional) Glob patterns for documentation files to exclude. It may be represented as a comma-separated string list or as a YAML sequence. * `doc-includes`: (Optional) Glob patterns for documentation files to include. diff --git a/cli/cli.go b/cli/cli.go index a7dd473..9e93231 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -28,7 +28,6 @@ import ( "embed-code/embed-code-go/analyzing" "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/embedding" - "embed-code/embed-code-go/fragmentation" "gopkg.in/yaml.v3" ) @@ -39,12 +38,6 @@ import ( // // BaseDocsPath — a path to a root directory with docs files. // -// CodeIncludes — a StringList with patterns for filtering the code files -// to be considered. -// Directories are never matched by these patterns. -// For example, "**/*.java,**/*.gradle". -// The default value is "**/*.*". -// // DocIncludes — a StringList with patterns for filtering files // in which we should look for embedding instructions. // The patterns are resolved relatively to the `documentation_root`. @@ -73,7 +66,6 @@ import ( type Config struct { BaseCodePaths _type.NamedPathList `yaml:"code-path"` BaseDocsPath string `yaml:"docs-path"` - CodeIncludes _type.StringList `yaml:"code-includes"` DocIncludes _type.StringList `yaml:"doc-includes"` DocExcludes _type.StringList `yaml:"doc-excludes"` FragmentsPath string `yaml:"fragments-path"` @@ -90,7 +82,6 @@ type EmbeddingConfig struct { Name string `yaml:"name"` CodePaths _type.NamedPathList `yaml:"code-path"` DocsPath string `yaml:"docs-path"` - CodeIncludes _type.StringList `yaml:"code-includes"` DocIncludes _type.StringList `yaml:"doc-includes"` DocExcludes _type.StringList `yaml:"doc-excludes"` FragmentsPath string `yaml:"fragments-path"` @@ -99,11 +90,8 @@ type EmbeddingConfig struct { // EmbedCodeSamplesResult is result of the EmbedCodeSamples method. // -// WriteFragmentFilesResult the result of code fragmentation. -// // EmbedAllResult the result of embedding code fragments in the documentation. type EmbedCodeSamplesResult struct { - fragmentation.WriteFragmentFilesResult embedding.EmbedAllResult } @@ -118,7 +106,6 @@ const ( // // config — a configuration for checking code samples. func CheckCodeSamples(config configuration.Configuration) { - fragmentation.WriteFragmentFiles(config) embedding.CheckUpToDate(config) } @@ -126,11 +113,9 @@ func CheckCodeSamples(config configuration.Configuration) { // // config — a configuration for embedding. func EmbedCodeSamples(config configuration.Configuration) EmbedCodeSamplesResult { - fragmentationResult := fragmentation.WriteFragmentFiles(config) embeddingResult := embedding.EmbedAll(config) embedding.CheckUpToDate(config) return EmbedCodeSamplesResult{ - fragmentationResult, embeddingResult, } } @@ -139,9 +124,7 @@ func EmbedCodeSamples(config configuration.Configuration) EmbedCodeSamplesResult // // config — a configuration for embedding. func AnalyzeCodeSamples(config configuration.Configuration) { - fragmentation.WriteFragmentFiles(config) analyzing.AnalyzeAll(config) - fragmentation.CleanFragmentFiles(config) } // ReadArgs reads user-specified args from the command line. @@ -150,8 +133,6 @@ func AnalyzeCodeSamples(config configuration.Configuration) { func ReadArgs() Config { codePath := flag.String("code-path", "", "a path to a root directory with code files") docsPath := flag.String("docs-path", "", "a path to a root directory with docs files") - codeIncludes := flag.String("code-includes", "", - "a comma-separated string of glob patterns for code files to include") docIncludes := flag.String("doc-includes", "", "a comma-separated string of glob patterns for docs files to include") docExcludes := flag.String("doc-excludes", "", @@ -173,7 +154,6 @@ func ReadArgs() Config { return Config{ BaseCodePaths: _type.NamedPathList{_type.NamedPath{Path: *codePath}}, BaseDocsPath: *docsPath, - CodeIncludes: parseListArgument(*codeIncludes), DocIncludes: parseListArgument(*docIncludes), DocExcludes: parseListArgument(*docExcludes), FragmentsPath: *fragmentsPath, @@ -195,9 +175,6 @@ func FillArgsFromConfigFile(args Config) (Config, error) { args.BaseDocsPath = configFields.BaseDocsPath args.BaseCodePaths = configFields.BaseCodePaths - if len(configFields.CodeIncludes) > 0 { - args.CodeIncludes = configFields.CodeIncludes - } if len(configFields.Embeddings) > 0 { args.Embeddings = configFields.Embeddings } @@ -248,9 +225,6 @@ func configFromEmbedding(embedding EmbeddingConfig) configuration.Configuration embedCodeConfig.CodeRoots = embedding.CodePaths embedCodeConfig.DocumentationRoot = embedding.DocsPath - if len(embedding.CodeIncludes) > 0 { - embedCodeConfig.CodeIncludes = embedding.CodeIncludes - } if len(embedding.DocIncludes) > 0 { embedCodeConfig.DocIncludes = embedding.DocIncludes } @@ -272,9 +246,6 @@ func configFromEmbedding(embedding EmbeddingConfig) configuration.Configuration func configWithOptionalParams(userArgs Config) configuration.Configuration { embedCodeConfig := configuration.NewConfiguration() - if len(userArgs.CodeIncludes) > 0 { - embedCodeConfig.CodeIncludes = userArgs.CodeIncludes - } if len(userArgs.DocIncludes) > 0 { embedCodeConfig.DocIncludes = userArgs.DocIncludes } diff --git a/cli/cli_test.go b/cli/cli_test.go index 726acc6..882397f 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -155,9 +155,9 @@ var _ = Describe("CLI validation", func() { It("should fail validation when embeddings and root optional params are set at the same time", func() { invalidConfig := cli.Config{ - Mode: cli.ModeCheck, - CodeIncludes: []string{"**/*.java"}, - Embeddings: []cli.EmbeddingConfig{baseEmbeddingConfig()}, + Mode: cli.ModeCheck, + DocIncludes: []string{"**/*.md"}, + Embeddings: []cli.EmbeddingConfig{baseEmbeddingConfig()}, } Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) diff --git a/cli/cli_validation.go b/cli/cli_validation.go index 3880e0b..c996e91 100644 --- a/cli/cli_validation.go +++ b/cli/cli_validation.go @@ -256,14 +256,12 @@ func verifyDuplicateEmbeddingDocsPaths(embeddings []EmbeddingConfig) { // validateOptionalParamsSet reports whether at least one optional config is set. func validateOptionalParamsSet(config Config) bool { - isCodeIncludesSet := len(config.CodeIncludes) > 0 isDocIncludesSet := len(config.DocIncludes) > 0 isDocExcludesSet := len(config.DocExcludes) > 0 isSeparatorSet := isNotEmpty(config.Separator) isFragmentPathSet := isNotEmpty(config.FragmentsPath) - return isCodeIncludesSet || isDocIncludesSet || isFragmentPathSet || - isSeparatorSet || isDocExcludesSet + return isDocIncludesSet || isFragmentPathSet || isSeparatorSet || isDocExcludesSet } // validatePathSet reports whether path is set and checks if it exists. diff --git a/main.go b/main.go index d78aabf..f68115e 100644 --- a/main.go +++ b/main.go @@ -69,9 +69,6 @@ const Version = "1.1.0" // then the checking for up-to-date is performed. If it is set to 'embed', the embedding // is performed. // If it is set to 'analyze', the analyzing is performed; -// - code-includes — a comma-separated string of glob patterns for code files to include. -// For example: -// "**/*.java,**/*.gradle". Default value is "**/*.*"; // - doc-includes — a comma-separated string of glob patterns for docs files to include. // For example: // "docs/**/*.md,guides/*.html". Default value is "**/*.md,**/*.html"; @@ -146,16 +143,12 @@ func logError(message string, err error) { func embedByConfigs(configs []configuration.Configuration) { var totalEmbeddedFiles []string totalEmbeddings := 0 - totalFragments := 0 for _, config := range configs { result := cli.EmbedCodeSamples(config) totalEmbeddedFiles = append(totalEmbeddedFiles, result.UpdatedTargetFiles...) totalEmbeddings += result.TotalEmbeddings - totalFragments += result.TotalFragments } - if len(totalEmbeddedFiles) == 0 && - totalEmbeddings != 0 && - totalFragments != 0 { + if len(totalEmbeddedFiles) == 0 && totalEmbeddings != 0 { fmt.Println("All documentation files are already up to date. Nothing to update.") } if len(totalEmbeddedFiles) == 1 { From 55be0c08a857bcdd5f73f8c15d164db614420369 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 13 May 2026 16:22:32 +0200 Subject: [PATCH 03/11] Update tests. --- configuration/configuration.go | 11 - embedding/embedding_test.go | 22 +- embedding/parsing/instruction_test.go | 3 +- fragmentation/fragmentation_test.go | 245 +++++------------- .../code/java/plain-text-to-embed.txt | 7 + 5 files changed, 89 insertions(+), 199 deletions(-) create mode 100644 test/resources/code/java/plain-text-to-embed.txt diff --git a/configuration/configuration.go b/configuration/configuration.go index 133a533..0e79f50 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -28,7 +28,6 @@ const ( DefaultFragmentsDir = "./build/fragments" ) -var DefaultInclude = []string{"**/*.*"} var DefaultDocIncludes = []string{"**/*.md", "**/*.html"} // Configuration contains the settings for the plugin to work. @@ -53,15 +52,6 @@ type Configuration struct { // DocumentationRoot is a root directory of the documentation files. DocumentationRoot string - // CodeIncludes is a list of patterns for filtering the code files to be considered. - // - // Directories are never matched by these patterns. - // - // For example, ["**/*.java", "**/*.gradle"]. - // - // The default value is "**/*". - CodeIncludes []string - // DocIncludes is a list of patterns for filtering files in which we should look for embedding // instructions. // @@ -101,7 +91,6 @@ type Configuration struct { // NewConfiguration builds the default config. func NewConfiguration() Configuration { return Configuration{ - CodeIncludes: DefaultInclude, DocIncludes: DefaultDocIncludes, FragmentsDir: DefaultFragmentsDir, Separator: DefaultSeparator, diff --git a/embedding/embedding_test.go b/embedding/embedding_test.go index 0463ef7..3704fb6 100644 --- a/embedding/embedding_test.go +++ b/embedding/embedding_test.go @@ -107,6 +107,23 @@ var _ = Describe("Embedding", func() { Expect(processor.IsUpToDate()).Should(BeTrue()) }) + It("should embed directly from source without writing fragment files", func() { + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/java"}} + config.FragmentsDir = "../test/lazy-fragments" + if err := os.RemoveAll(config.FragmentsDir); err != nil { + Fail(err.Error()) + } + docPath := fmt.Sprintf("%s/doc.md", config.DocumentationRoot) + processor := embedding.NewProcessor(docPath, config) + + Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) + exists, err := files.IsDirExist(config.FragmentsDir) + + Expect(err).ShouldNot(HaveOccurred()) + Expect(exists).Should(BeFalse()) + Expect(processor.IsUpToDate()).Should(BeTrue()) + }) + It("should embed with multi lined tag attributes", func() { docPath := fmt.Sprintf("%s/multi-lined-valid-tag-attributes.md", config.DocumentationRoot) processor := embedding.NewProcessor(docPath, config) @@ -160,7 +177,7 @@ var _ = Describe("Embedding", func() { Expect(processor.IsUpToDate()).Should(BeTrue()) }) - It("should not embed to a file matched the `code-excludes` pattern", func() { + It("should not embed to a file matched the `doc-excludes` pattern", func() { config.DocExcludes = []string{"**/excluded-doc.*"} docPath := fmt.Sprintf("%s/excluded-doc.md", config.DocumentationRoot) @@ -174,8 +191,7 @@ var _ = Describe("Embedding", func() { func buildConfigWithPreparedFragments() configuration.Configuration { var config = configuration.NewConfiguration() config.DocumentationRoot = temporaryTestDir - config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code"}} - config.FragmentsDir = "../test/resources/prepared-fragments" + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/java"}} return config } diff --git a/embedding/parsing/instruction_test.go b/embedding/parsing/instruction_test.go index b70bad9..046207e 100644 --- a/embedding/parsing/instruction_test.go +++ b/embedding/parsing/instruction_test.go @@ -301,8 +301,7 @@ func getXMLExtractionContent(fileName string, params TestInstructionParams, func buildConfigWithPreparedFragments() configuration.Configuration { var config = configuration.NewConfiguration() config.DocumentationRoot = "../../test/resources/docs" - config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../../test/resources/code"}} - config.FragmentsDir = "../../test/resources/prepared-fragments" + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../../test/resources/code/java"}} return config } diff --git a/fragmentation/fragmentation_test.go b/fragmentation/fragmentation_test.go index e529773..d9e6440 100644 --- a/fragmentation/fragmentation_test.go +++ b/fragmentation/fragmentation_test.go @@ -20,13 +20,9 @@ package fragmentation_test import ( "embed-code/embed-code-go/configuration" - "embed-code/embed-code-go/files" "embed-code/embed-code-go/fragmentation" _type "embed-code/embed-code-go/type" "fmt" - "os" - "path/filepath" - "regexp" "testing" . "github.com/onsi/ginkgo/v2" @@ -53,112 +49,57 @@ var _ = Describe("Fragmentation", func() { var config configuration.Configuration BeforeEach(func() { + fragmentation.ClearResolverCache() config = configuration.NewConfiguration() config.DocumentationRoot = "../test/resources/docs" config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/java"}} }) - AfterEach(func() { - cleanupDir(config.FragmentsDir) - }) - - It("should do file fragmentation successfully", func() { - frag := buildTestFragmentation(correctFragmentsFileName, config) - Expect(frag.WriteFragments()).Error().ShouldNot(HaveOccurred()) - - fragmentChild, _ := os.ReadDir(config.FragmentsDir) - Expect(fragmentChild).Should(HaveLen(1)) - Expect(fragmentChild[0].Name()).Should(Equal("org")) - - fragmentFiles := readFragmentsDir(config) - Expect(fragmentFiles).Should(HaveLen(4)) + It("should fragment a file in memory", func() { + lines, fragments := doTestFragmentation(correctFragmentsFileName, config) - var isDefaultFragmentExist bool - for _, file := range fragmentFiles { - if file.Name() == correctFragmentsFileName { - isDefaultFragmentExist = true - } else { - Expect(file.Name()).Should(MatchRegexp(`Hello-\w+\.java`)) - } - } - - Expect(isDefaultFragmentExist).Should(BeTrue()) + Expect(lines).ShouldNot(ContainElement(ContainSubstring("#docfragment"))) + Expect(lines).ShouldNot(ContainElement(ContainSubstring("#enddocfragment"))) + Expect(fragments).Should(HaveKey(fragmentation.DefaultFragmentName)) + Expect(fragments).Should(HaveKey("Without License")) + Expect(fragments).Should(HaveKey("Hello class")) + Expect(fragments).Should(HaveKey("main()")) }) - It("should do multi-source fragmentation successfully", func() { - config := configuration.NewConfiguration() - config.DocumentationRoot = "../test/resources/docs" - javaCodePathName := "java-code" - kotlinCodePathName := "kotlin-code" - config.CodeRoots = _type.NamedPathList{ - _type.NamedPath{ - Name: javaCodePathName, - Path: "../test/resources/code/java/org/example/multitest", - }, - _type.NamedPath{ - Name: kotlinCodePathName, - Path: "../test/resources/code/kotlin/org/example/multitest", - }, - } - result := fragmentation.WriteFragmentFiles(config) - Expect(result.TotalSourceFiles).Should(Equal(2)) - javaFragments, _ := os.ReadDir( - filepath.Join(config.FragmentsDir, fragmentation.NamedPathPrefix+javaCodePathName), - ) - kotlinFragments, _ := os.ReadDir( - filepath.Join(config.FragmentsDir, fragmentation.NamedPathPrefix+kotlinCodePathName), - ) - Expect(javaFragments).Should(HaveLen(2)) - Expect(kotlinFragments).Should(HaveLen(2)) - }) - - It("should do fragmentation of a fragment without end", func() { - frag := buildTestFragmentation(unclosedFragmentFileName, config) - Expect(frag.WriteFragments()).Error().ShouldNot(HaveOccurred()) - - fragmentFiles := readFragmentsDir(config) - Expect(fragmentFiles).Should(HaveLen(2)) + It("should resolve named fragments directly from source", func() { + content := resolveTestFragment(correctFragmentsFileName, "main()", config) - fragmentFileName := findFragmentFile(fragmentFiles, unclosedFragmentFileName) - fragmentsDir := fragmentsDirPath(config.FragmentsDir) - content, err := os.ReadFile(filepath.Join(fragmentsDir, fragmentFileName)) - if err != nil { - Fail(err.Error()) - } - - re := regexp.MustCompile(`[.\n\s]+}\n}\n`) - matchedStrings := re.FindStringSubmatch(string(content)) - - Expect(matchedStrings).Should(Not(BeEmpty())) + Expect(content).Should(Equal([]string{ + "public static void main(String[] args) {", + indent + "System.out.println(\"Hello world\");", + "}", + })) }) - It("should not do fragmentation of an empty file", func() { - frag := buildTestFragmentation(emptyFileName, config) - Expect(frag.WriteFragments()).Error().ShouldNot(HaveOccurred()) - - fragmentFiles := readFragmentsDir(config) - Expect(fragmentFiles).Should(HaveLen(1)) - fragmentsFilePath := fragmentsDirPath(config.FragmentsDir) + "/" + fragmentFiles[0].Name() - - content, err := os.ReadFile(fragmentsFilePath) - if err != nil { - Fail(err.Error()) - } + It("should resolve fragments without an end marker through the end of the file", func() { + content := resolveTestFragment(unclosedFragmentFileName, "Fragment that never ends", config) - Expect(content).Should(BeEmpty()) + Expect(content).Should(Equal([]string{ + indent + indent + "System.out.println(\"Hello world\");", + indent + "}", + "}", + })) }) - It("should not do fragmentation of a binary file", func() { - config.CodeIncludes = []string{"**.jar"} + It("should fragment an empty file", func() { + lines, fragments := doTestFragmentation(emptyFileName, config) - Expect(fragmentation.WriteFragmentFiles(config).TotalFragments).Should(Equal(0)) - Expect(files.IsDirExist(config.FragmentsDir)).Should(BeFalse()) + Expect(lines).Should(BeEmpty()) + Expect(fragments).Should(HaveLen(1)) + Expect(fragments).Should(HaveKey(fragmentation.DefaultFragmentName)) }) - It("should not do fragmentation of an unopened fragment", func() { + It("should fail on an unopened fragment", func() { frag := buildTestFragmentation(unopenedFragmentFileName, config) - Expect(frag.WriteFragments()).Error().Should(HaveOccurred()) + _, _, err := frag.DoFragmentation() + + Expect(err).Should(HaveOccurred()) }) Context("fragments parsing", func() { @@ -203,20 +144,7 @@ var _ = Describe("Fragmentation", func() { }) It("should correctly parse file into many partitions", func() { - frag := buildTestFragmentation(complexFragmentsFileName, config) - _, err := frag.WriteFragments() - Expect(err).ToNot(HaveOccurred()) - - fragmentFiles := readFragmentsDir(config) - Expect(fragmentFiles).Should(HaveLen(2)) - - fragmentFileName := findFragmentFile(fragmentFiles, complexFragmentsFileName) - fragmentDir := fragmentsDirPath(config.FragmentsDir) - - content, err := files.ReadFile(fmt.Sprintf("%s/%s", fragmentDir, fragmentFileName)) - if err != nil { - Fail(err.Error()) - } + content := resolveTestFragment(complexFragmentsFileName, "Main", config) expected := []string{ "public class Main {", @@ -233,17 +161,10 @@ var _ = Describe("Fragmentation", func() { }) It("should correctly parse file with several different fragments", func() { - frag := buildTestFragmentation(twoFragmentsFileName, config) - _, err := frag.WriteFragments() - Expect(err).ToNot(HaveOccurred()) - - fragmentFiles := readFragmentsDir(config) - Expect(fragmentFiles).Should(HaveLen(3)) + mainContent := resolveTestFragment(twoFragmentsFileName, "Main", config) + helloContent := resolveTestFragment(twoFragmentsFileName, "Hello", config) - fragmentDir := fragmentsDirPath(config.FragmentsDir) - actual := readFragmentsContent(fragmentDir, fragmentFiles, twoFragmentsFileName) - - expected := [][]string{ + Expect([][]string{mainContent, helloContent}).Should(ConsistOf([][]string{ { "public class TwoFragments {", indent + config.Separator, @@ -262,23 +183,14 @@ var _ = Describe("Fragmentation", func() { indent + "System.out.println(coolText);", "}", }, - } - - Expect(actual).Should(ConsistOf(expected)) + })) }) It("should correctly parse file with several overlapping fragments", func() { - frag := buildTestFragmentation(overlappingFragmentsFileName, config) - _, err := frag.WriteFragments() - Expect(err).ToNot(HaveOccurred()) + mainContent := resolveTestFragment(overlappingFragmentsFileName, "Main", config) + helloContent := resolveTestFragment(overlappingFragmentsFileName, "Hello", config) - fragmentFiles := readFragmentsDir(config) - Expect(fragmentFiles).Should(HaveLen(3)) - - fragmentDir := fragmentsDirPath(config.FragmentsDir) - actual := readFragmentsContent(fragmentDir, fragmentFiles, overlappingFragmentsFileName) - - expected := [][]string{ + Expect([][]string{mainContent, helloContent}).Should(ConsistOf([][]string{ { "public class OverlappingFragments {", indent + config.Separator, @@ -301,9 +213,7 @@ var _ = Describe("Fragmentation", func() { config.Separator, "}", }, - } - - Expect(actual).Should(ConsistOf(expected)) + })) }) }) @@ -315,60 +225,29 @@ func buildTestFragmentation(testFileName string, return fragmentation.NewFragmentation(testFilePath, codeRoot, config) } -func readFragmentsDir(config configuration.Configuration) []os.DirEntry { - fragmentFiles, err := os.ReadDir(fragmentsDirPath(config.FragmentsDir)) - if err != nil { - Fail(err.Error()) - } - - return fragmentFiles -} - -// readFragmentsContent reads the contents of fragment files from a given directory. -// -// fragmentDir — path to the directory containing fragment files. -// fragmentFiles — list of directory entries representing the fragment files. -// skipFile — file name to skip (the default fragment file). -// -// Returns a slice of string slices, where each inner slice contains the lines -// of a fragment file. -// -// This function fails the test immediately if any file cannot be read. -func readFragmentsContent( - fragmentDir string, fragmentFiles []os.DirEntry, skipFile string, -) [][]string { - var result [][]string - for _, file := range fragmentFiles { - if file.Name() == skipFile { - continue - } - - content, err := files.ReadFile(fmt.Sprintf("%s/%s", fragmentDir, file.Name())) - Expect(err).ShouldNot(HaveOccurred()) - - result = append(result, content) - } - - return result -} - -func fragmentsDirPath(path string) string { - return fmt.Sprintf("%s/org/example", path) -} +func doTestFragmentation( + testFileName string, + config configuration.Configuration, +) ([]string, map[string]fragmentation.Fragment) { + frag := buildTestFragmentation(testFileName, config) -func findFragmentFile(files []os.DirEntry, fileName string) string { - for _, file := range files { - if file.Name() != fileName { - return file.Name() - } - } + lines, fragments, err := frag.DoFragmentation() - return "" + Expect(err).ShouldNot(HaveOccurred()) + return lines, fragments } -func cleanupDir(dirPath string) { - err := os.RemoveAll(dirPath) - if err != nil { - Fail(err.Error()) - } +func resolveTestFragment( + testFileName string, + fragmentName string, + config configuration.Configuration, +) []string { + content, err := fragmentation.ResolveContent( + fmt.Sprintf("org/example/%s", testFileName), + fragmentName, + config, + ) + + Expect(err).ShouldNot(HaveOccurred()) + return content } diff --git a/test/resources/code/java/plain-text-to-embed.txt b/test/resources/code/java/plain-text-to-embed.txt new file mode 100644 index 0000000..dc2bec0 --- /dev/null +++ b/test/resources/code/java/plain-text-to-embed.txt @@ -0,0 +1,7 @@ +This line contains foo in the middle +This line ends with foo +foo — this line starts with it + +This line contains bar in the middle +bar — this line starts with it +This line ends with bar From c024b521401ec407de31aea7546c1d93ba3c61a4 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 13 May 2026 16:33:21 +0200 Subject: [PATCH 04/11] Update version. --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index cf651c1..210065d 100644 --- a/main.go +++ b/main.go @@ -28,7 +28,7 @@ import ( ) // Version of the embed-code application. -const Version = "1.1.1" +const Version = "1.2.0" // The entry point for embed-code. // From c7d5c5e2fe9127a9a380b48567b53d1484ecdacf Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Thu, 14 May 2026 15:13:39 +0200 Subject: [PATCH 05/11] Remove `fragments-path` as redundant. --- EMBEDDING.md | 10 --------- README.md | 7 +----- cli/cli.go | 31 ++++++--------------------- cli/cli_test.go | 21 ------------------ cli/cli_validation.go | 15 ++----------- configuration/configuration.go | 22 ++++--------------- embedding/embedding_test.go | 14 +++--------- embedding/parsing/instruction_test.go | 6 +++--- main.go | 2 -- 9 files changed, 19 insertions(+), 109 deletions(-) diff --git a/EMBEDDING.md b/EMBEDDING.md index ce67931..458528a 100644 --- a/EMBEDDING.md +++ b/EMBEDDING.md @@ -209,13 +209,3 @@ The fragments can also be used in other languages: ``` - -### Providing fragments directly - -It is possible to add the desired fragment file with exactly the required content -directly to the fragments folder, for example: `FRAGMENTS/desired-path/MyFile.kt`. - -It can then be embedded just like any other fragment file: -```markdown - -``` diff --git a/README.md b/README.md index 73c6183..b5df41a 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,6 @@ The available arguments are: * `-docs-path`: (Optional) Path to the documentation root directory. * `-config-path`: (Optional) Path to a YAML configuration file containing `code-path` and `docs-path`. * `-doc-includes`: (Optional) Comma-separated glob patterns for documentation files to include. Defaults to `"**/*.md,**/*.html"`. - * `-fragments-path`: (Optional) Directory for storing code fragments. Defaults to `./build/fragments`. * `-separator`: (Optional) String used to separate joined code fragments. Defaults to `...`. Even though the `code-path`, `docs-path`, and `config-path` arguments are optional, @@ -127,18 +126,14 @@ The available fields for the configuration file are: ``` **Do not forget the dollar sign (`$`) before the path name.** - It is possible to specify a path without a name or with an empty name. - In this case, fragments will be stored in the root defined by `fragments-path`. - It is also possible to specify multiple paths with the same name, - but this may lead to fragments being overwritten if they have the same relative path and name. + but this may make source resolution ambiguous when they have the same relative path. * `docs-path`: (Mandatory) Path to the documentation root. * `doc-excludes`: (Optional) Glob patterns for documentation files to exclude. It may be represented as a comma-separated string list or as a YAML sequence. * `doc-includes`: (Optional) Glob patterns for documentation files to include. It may be represented as a comma-separated string list or as a YAML sequence. - * `fragments-path`: (Optional) Directory for code fragments. * `separator`: (Optional) Separator for fragments. * `embeddings`: (Optional) A list of complete embedding configurations for multiple documentation targets. When `embeddings` is set, do not set root-level `code-path` diff --git a/cli/cli.go b/cli/cli.go index 9e93231..833cfae 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -22,7 +22,6 @@ import ( _type "embed-code/embed-code-go/type" "flag" "os" - "path/filepath" "strings" "embed-code/embed-code-go/analyzing" @@ -48,9 +47,6 @@ import ( // DocExcludes - a StringList with patterns for filtering documentation files // which should be excluded from the embedding process. // -// FragmentsPath — a directory where fragmented code is stored. A temporary directory that should -// not be tracked in VCS. The default value is: "./build/fragments". -// // Separator — a string that's inserted between multiple partitions of a single fragment. // The default value is "...". // @@ -68,7 +64,6 @@ type Config struct { BaseDocsPath string `yaml:"docs-path"` DocIncludes _type.StringList `yaml:"doc-includes"` DocExcludes _type.StringList `yaml:"doc-excludes"` - FragmentsPath string `yaml:"fragments-path"` Separator string `yaml:"separator"` Embeddings []EmbeddingConfig `yaml:"embeddings"` Info bool `yaml:"info"` @@ -79,13 +74,12 @@ type Config struct { // EmbeddingConfig contains a complete configuration for one embedding target. type EmbeddingConfig struct { - Name string `yaml:"name"` - CodePaths _type.NamedPathList `yaml:"code-path"` - DocsPath string `yaml:"docs-path"` - DocIncludes _type.StringList `yaml:"doc-includes"` - DocExcludes _type.StringList `yaml:"doc-excludes"` - FragmentsPath string `yaml:"fragments-path"` - Separator string `yaml:"separator"` + Name string `yaml:"name"` + CodePaths _type.NamedPathList `yaml:"code-path"` + DocsPath string `yaml:"docs-path"` + DocIncludes _type.StringList `yaml:"doc-includes"` + DocExcludes _type.StringList `yaml:"doc-excludes"` + Separator string `yaml:"separator"` } // EmbedCodeSamplesResult is result of the EmbedCodeSamples method. @@ -137,8 +131,6 @@ func ReadArgs() Config { "a comma-separated string of glob patterns for docs files to include") docExcludes := flag.String("doc-excludes", "", "a comma-separated string of glob patterns for docs files to exclude") - fragmentsPath := flag.String("fragments-path", "", - "a path to a directory where fragmented code is stored") separator := flag.String("separator", "", "a string that's inserted between multiple partitions of a single fragment") configPath := flag.String("config-path", "", "a path to a yaml configuration file") @@ -156,7 +148,6 @@ func ReadArgs() Config { BaseDocsPath: *docsPath, DocIncludes: parseListArgument(*docIncludes), DocExcludes: parseListArgument(*docExcludes), - FragmentsPath: *fragmentsPath, Separator: *separator, ConfigPath: *configPath, Mode: *mode, @@ -184,9 +175,6 @@ func FillArgsFromConfigFile(args Config) (Config, error) { if len(configFields.DocExcludes) > 0 { args.DocExcludes = configFields.DocExcludes } - if isNotEmpty(configFields.FragmentsPath) { - args.FragmentsPath = configFields.FragmentsPath - } if isNotEmpty(configFields.Separator) { args.Separator = configFields.Separator } @@ -231,10 +219,6 @@ func configFromEmbedding(embedding EmbeddingConfig) configuration.Configuration if len(embedding.DocExcludes) > 0 { embedCodeConfig.DocExcludes = embedding.DocExcludes } - if isNotEmpty(embedding.FragmentsPath) { - embedCodeConfig.FragmentsDir = embedding.FragmentsPath - } - embedCodeConfig.FragmentsDir = filepath.Join(embedCodeConfig.FragmentsDir, embedding.Name) if isNotEmpty(embedding.Separator) { embedCodeConfig.Separator = embedding.Separator } @@ -249,9 +233,6 @@ func configWithOptionalParams(userArgs Config) configuration.Configuration { if len(userArgs.DocIncludes) > 0 { embedCodeConfig.DocIncludes = userArgs.DocIncludes } - if isNotEmpty(userArgs.FragmentsPath) { - embedCodeConfig.FragmentsDir = userArgs.FragmentsPath - } if isNotEmpty(userArgs.Separator) { embedCodeConfig.Separator = userArgs.Separator } diff --git a/cli/cli_test.go b/cli/cli_test.go index 882397f..3dfc51a 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -22,7 +22,6 @@ package cli_test import ( "embed-code/embed-code-go/cli" - "embed-code/embed-code-go/configuration" _type "embed-code/embed-code-go/type" "os" "path/filepath" @@ -76,20 +75,6 @@ var _ = Describe("CLI validation", func() { Expect(cli.ValidateConfig(config)).Error().ShouldNot(HaveOccurred()) }) - It("should store embedding fragments under a named subfolder", func() { - embedding := baseEmbeddingConfig() - embedding.FragmentsPath = "/tmp/fragments" - config := cli.Config{ - Mode: cli.ModeCheck, - Embeddings: []cli.EmbeddingConfig{embedding}, - } - - embedConfigs := cli.BuildEmbedCodeConfiguration(config) - - Expect(embedConfigs).To(HaveLen(1)) - Expect(embedConfigs[0].Name).To(Equal("docs")) - Expect(embedConfigs[0].FragmentsDir).To(Equal(filepath.Join("/tmp/fragments", "docs"))) - }) }) Context("with invalid config", func() { @@ -204,18 +189,12 @@ var _ = Describe("CLI validation", func() { Expect(embedConfigs[0].Name).To(Equal("java")) Expect(embedConfigs[0].CodeRoots[0].Path).To(Equal("test/resources/code/java")) Expect(embedConfigs[0].DocumentationRoot).To(Equal("test/resources/docs")) - Expect(embedConfigs[0].FragmentsDir).To(Equal( - filepath.Join(configuration.DefaultFragmentsDir, "java"))) Expect(embedConfigs[1].Name).To(Equal("kotlin")) Expect(embedConfigs[1].CodeRoots[0].Path).To(Equal("test/resources/code/kotlin")) Expect(embedConfigs[1].DocumentationRoot).To(Equal("test/resources/docs/nested-dir-1")) - Expect(embedConfigs[1].FragmentsDir).To(Equal( - filepath.Join(configuration.DefaultFragmentsDir, "kotlin"))) Expect(embedConfigs[2].Name).To(Equal("nested-java")) Expect(embedConfigs[2].DocumentationRoot).To( Equal("test/resources/docs/nested-dir-1/nested-dir-3")) - Expect(embedConfigs[2].FragmentsDir).To(Equal( - filepath.Join(configuration.DefaultFragmentsDir, "nested-java"))) Expect(embedConfigs[2].Separator).To(Equal("---")) }) diff --git a/cli/cli_validation.go b/cli/cli_validation.go index c996e91..cc20cac 100644 --- a/cli/cli_validation.go +++ b/cli/cli_validation.go @@ -118,11 +118,6 @@ func validateConfig(config Config) error { if err != nil { return err } - _, err = validatePathSet(config.FragmentsPath) - if err != nil { - return err - } - isRootsSet := isCodePathsSet && isDocsPathSet isOneOfRootsSet := isCodePathsSet || isDocsPathSet @@ -187,11 +182,6 @@ func validateEmbeddingConfig(embedding EmbeddingConfig, index int) error { if err != nil { return fmt.Errorf("embedding `%s`: %w", embedding.Name, err) } - _, err = validatePathSet(embedding.FragmentsPath) - if err != nil { - return fmt.Errorf("embedding `%s`: %w", embedding.Name, err) - } - isRootsSet := isCodePathsSet && isDocsPathSet if !isRootsSet { return fmt.Errorf("embedding `%s`: `code-path` and `docs-path` must both be set", @@ -259,9 +249,8 @@ func validateOptionalParamsSet(config Config) bool { isDocIncludesSet := len(config.DocIncludes) > 0 isDocExcludesSet := len(config.DocExcludes) > 0 isSeparatorSet := isNotEmpty(config.Separator) - isFragmentPathSet := isNotEmpty(config.FragmentsPath) - return isDocIncludesSet || isFragmentPathSet || isSeparatorSet || isDocExcludesSet + return isDocIncludesSet || isSeparatorSet || isDocExcludesSet } // validatePathSet reports whether path is set and checks if it exists. @@ -345,7 +334,7 @@ func verifyDuplicateNames(nameDuplicates map[string][]string) { if len(warnLines) > 0 { slog.Warn( "Duplicate code source names detected, it may lead to " + - "overwriting code fragments with the same relative path:\n" + + "ambiguous source resolution for the same relative path:\n" + strings.Join(warnLines, "\n"), ) } diff --git a/configuration/configuration.go b/configuration/configuration.go index 0e79f50..c62484a 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -24,24 +24,17 @@ import ( ) const ( - DefaultSeparator = "..." - DefaultFragmentsDir = "./build/fragments" + DefaultSeparator = "..." ) var DefaultDocIncludes = []string{"**/*.md", "**/*.html"} // Configuration contains the settings for the plugin to work. // -// It is used to get data for scanning for and doc files, to receive fragments' dir and separator -// for partitions. +// It is used to get data for scanning docs and resolving source files. // The example of creating the Configuration with default values: // // var config = configuration.NewConfiguration() -// -// If there's need to modify the default configuration, -// it can be done with just setting values to corresponding fields: -// -// config.FragmentsDir = "foo/bar" type Configuration struct { // Name identifies this configuration when it is built from an embeddings entry. Name string @@ -76,12 +69,6 @@ type Configuration struct { // Be the default, it is not set. DocExcludes []string - // FragmentsDir is a directory where fragmented code is stored. A temporary directory that - // should not be tracked in VCS. - // - // The default value is: "./build/fragments". - FragmentsDir string - // Separator is a string that's inserted between multiple partitions of a single fragment. // // The default value is: "..." (three dots). @@ -91,8 +78,7 @@ type Configuration struct { // NewConfiguration builds the default config. func NewConfiguration() Configuration { return Configuration{ - DocIncludes: DefaultDocIncludes, - FragmentsDir: DefaultFragmentsDir, - Separator: DefaultSeparator, + DocIncludes: DefaultDocIncludes, + Separator: DefaultSeparator, } } diff --git a/embedding/embedding_test.go b/embedding/embedding_test.go index 3704fb6..8140629 100644 --- a/embedding/embedding_test.go +++ b/embedding/embedding_test.go @@ -54,7 +54,7 @@ var _ = Describe("Embedding", func() { if err != nil { Fail("unexpected error during the test setup: " + err.Error()) } - config = buildConfigWithPreparedFragments() + config = buildConfigWithSourceFiles() // Copying files not to edit them directly during the test run. copyDirRecursive("../test/resources/docs", config.DocumentationRoot) @@ -107,20 +107,12 @@ var _ = Describe("Embedding", func() { Expect(processor.IsUpToDate()).Should(BeTrue()) }) - It("should embed directly from source without writing fragment files", func() { - config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/java"}} - config.FragmentsDir = "../test/lazy-fragments" - if err := os.RemoveAll(config.FragmentsDir); err != nil { - Fail(err.Error()) - } + It("should embed directly from source", func() { docPath := fmt.Sprintf("%s/doc.md", config.DocumentationRoot) processor := embedding.NewProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) - exists, err := files.IsDirExist(config.FragmentsDir) - Expect(err).ShouldNot(HaveOccurred()) - Expect(exists).Should(BeFalse()) Expect(processor.IsUpToDate()).Should(BeTrue()) }) @@ -188,7 +180,7 @@ var _ = Describe("Embedding", func() { }) }) -func buildConfigWithPreparedFragments() configuration.Configuration { +func buildConfigWithSourceFiles() configuration.Configuration { var config = configuration.NewConfiguration() config.DocumentationRoot = temporaryTestDir config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/java"}} diff --git a/embedding/parsing/instruction_test.go b/embedding/parsing/instruction_test.go index 046207e..c4183f6 100644 --- a/embedding/parsing/instruction_test.go +++ b/embedding/parsing/instruction_test.go @@ -56,7 +56,7 @@ var _ = Describe("Instruction", func() { if err != nil { Fail("unexpected error during the test setup: " + err.Error()) } - config = buildConfigWithPreparedFragments() + config = buildConfigWithSourceFiles() }) It("should have an error while parsing malformed XML string", func() { @@ -82,7 +82,7 @@ var _ = Describe("Instruction", func() { Expect(parsing.FromXML(xmlString, config)).Error().ShouldNot(HaveOccurred()) }) - It("should successfully read fragments directory", func() { + It("should successfully read source content", func() { instructionParams := TestInstructionParams{ closeTag: true, } @@ -298,7 +298,7 @@ func getXMLExtractionContent(fileName string, params TestInstructionParams, return readInstructionContent(instruction) } -func buildConfigWithPreparedFragments() configuration.Configuration { +func buildConfigWithSourceFiles() configuration.Configuration { var config = configuration.NewConfiguration() config.DocumentationRoot = "../../test/resources/docs" config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../../test/resources/code/java"}} diff --git a/main.go b/main.go index 210065d..1175fec 100644 --- a/main.go +++ b/main.go @@ -76,8 +76,6 @@ const Version = "1.2.0" // the embedding. // For example: // "old-docs/**/*.md,old-guides/*.html". It is not set by default; -// - fragments-path — a path to a directory with code fragments. Default value is -// "./build/fragments"; // - separator — a string which is used as a separator between code fragments. Default value // is "...". func main() { From 769131a5f04eb2023434e1b5a3a9f32a7f8078e3 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Thu, 14 May 2026 16:02:16 +0200 Subject: [PATCH 06/11] Prohibit source code paths with the same name. --- README.md | 5 ++- cli/cli_test.go | 47 +++++++++++++++++++++ cli/cli_validation.go | 97 +++++++++++++++++++++++++++---------------- 3 files changed, 111 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index b5df41a..f229040 100644 --- a/README.md +++ b/README.md @@ -126,8 +126,9 @@ The available fields for the configuration file are: ``` **Do not forget the dollar sign (`$`) before the path name.** - It is also possible to specify multiple paths with the same name, - but this may make source resolution ambiguous when they have the same relative path. + Code source names must be unique. A configuration may use either one unnamed + code source or one or more named code sources, but named and unnamed sources + cannot be mixed. * `docs-path`: (Mandatory) Path to the documentation root. * `doc-excludes`: (Optional) Glob patterns for documentation files to exclude. diff --git a/cli/cli_test.go b/cli/cli_test.go index 3dfc51a..36f6491 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -175,6 +175,42 @@ var _ = Describe("CLI validation", func() { "duplicate embedding names detected:\n- docs")) }) + It("should fail validation when code source names are duplicated", func() { + invalidConfig := baseCliConfig() + invalidConfig.BaseCodePaths = _type.NamedPathList{ + _type.NamedPath{Name: "samples", Path: codeResourcePath("java")}, + _type.NamedPath{Name: "samples", Path: codeResourcePath("kotlin")}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "duplicate source code path names detected:\n- samples")) + }) + + It("should fail validation when multiple unnamed code sources are configured", func() { + invalidConfig := baseCliConfig() + invalidConfig.BaseCodePaths = _type.NamedPathList{ + _type.NamedPath{Path: codeResourcePath("java")}, + _type.NamedPath{Path: codeResourcePath("kotlin")}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "only one unnamed source code path is allowed")) + }) + + It("should fail validation when named and unnamed code sources are mixed", func() { + invalidConfig := baseCliConfig() + invalidConfig.BaseCodePaths = _type.NamedPathList{ + _type.NamedPath{Name: "java", Path: codeResourcePath("java")}, + _type.NamedPath{Path: codeResourcePath("kotlin")}, + } + + Expect(cli.ValidateConfig(invalidConfig)).Error().Should(HaveOccurred()) + Expect(cli.ValidateConfig(invalidConfig).Error()).Should(Equal( + "named and unnamed source code paths cannot be mixed")) + }) + It("should correctly convert embeddings to a few configs", func() { config := cli.Config{ Mode: cli.ModeCheck, @@ -234,3 +270,14 @@ func configFilePath() string { return parentDir + "/test/resources/config_files/correct_config.yml" } + +// codeResourcePath builds an absolute path to a test source-code fixture directory. +func codeResourcePath(name string) string { + currentDir, err := os.Getwd() + if err != nil { + panic(err) + } + parentDir := filepath.Dir(currentDir) + + return filepath.Join(parentDir, "test/resources/code", name) +} diff --git a/cli/cli_validation.go b/cli/cli_validation.go index cc20cac..b1825bd 100644 --- a/cli/cli_validation.go +++ b/cli/cli_validation.go @@ -110,7 +110,7 @@ func validateConfig(config Config) error { if err != nil { return err } - err = findCodeSourceDuplications(config.BaseCodePaths) + err = validateCodeSources(config.BaseCodePaths) if err != nil { return err } @@ -174,7 +174,7 @@ func validateEmbeddingConfig(embedding EmbeddingConfig, index int) error { if err != nil { return fmt.Errorf("embedding `%s`: %w", embedding.Name, err) } - if err = findCodeSourceDuplications(embedding.CodePaths); err != nil { + if err = validateCodeSources(embedding.CodePaths); err != nil { return fmt.Errorf("embedding `%s`: %w", embedding.Name, err) } @@ -299,63 +299,88 @@ func validatePaths(paths _type.NamedPathList) (bool, error) { return allPathsSet, nil } -// findCodeSourceDuplications checks the provided code sources for duplicate names and paths. -// -// It logs a warning for duplicate names and returns an error for duplicate paths. -func findCodeSourceDuplications(paths _type.NamedPathList) error { - nameDuplicates := make(map[string][]string) +// validateCodeSources checks that code sources can be resolved unambiguously. +func validateCodeSources(paths _type.NamedPathList) error { + nameCount := make(map[string]int) pathCount := make(map[string]int) + pathNames := make(map[string][]string) + unnamedCount := 0 + hasNamed := false for _, p := range paths { - name := p.Name - if isEmpty(name) { - name = "(unnamed)" + if isEmpty(p.Path) { + continue + } + if isEmpty(p.Name) { + unnamedCount++ + } else { + hasNamed = true + nameCount[p.Name]++ } - nameDuplicates[name] = append(nameDuplicates[name], p.Path) pathCount[p.Path]++ + pathNames[p.Path] = append(pathNames[p.Path], codeSourceDisplayName(p.Name)) + } + + if err := verifyCodeSourceNames(nameCount); err != nil { + return err + } + if unnamedCount > 1 { + return errors.New("only one unnamed source code path is allowed") + } + if hasNamed && unnamedCount > 0 { + return errors.New("named and unnamed source code paths cannot be mixed") } - verifyDuplicateNames(nameDuplicates) - return verifyDuplicatePaths(pathCount) + warnDuplicatePaths(pathCount, pathNames) + + return nil } -// verifyDuplicateNames logs a warning if multiple code sources share the same name. -func verifyDuplicateNames(nameDuplicates map[string][]string) { - var warnLines []string - for name, ps := range nameDuplicates { - if len(ps) > 1 { - warnLines = append(warnLines, "- "+name) - for _, path := range ps { - warnLines = append(warnLines, " - "+path) - } +// verifyCodeSourceNames returns an error if multiple code sources share the same name. +func verifyCodeSourceNames(nameCount map[string]int) error { + var errLines []string + for name, count := range nameCount { + if count > 1 { + errLines = append(errLines, "- "+name) } } - if len(warnLines) > 0 { - slog.Warn( - "Duplicate code source names detected, it may lead to " + - "ambiguous source resolution for the same relative path:\n" + - strings.Join(warnLines, "\n"), + if len(errLines) > 0 { + slices.Sort(errLines) + return fmt.Errorf( + "duplicate source code path names detected:\n%s", + strings.Join(errLines, "\n"), ) } + return nil } -// verifyDuplicatePaths returns an error if multiple code sources use the same path. -func verifyDuplicatePaths(pathCount map[string]int) error { - var errLines []string +// codeSourceDisplayName returns a readable code source name for diagnostics. +func codeSourceDisplayName(name string) string { + if isEmpty(name) { + return "(unnamed)" + } + + return name +} + +// warnDuplicatePaths logs a warning if multiple code sources use the same path. +func warnDuplicatePaths(pathCount map[string]int, pathNames map[string][]string) { + var warnLines []string for path, count := range pathCount { if count > 1 { - errLines = append(errLines, "- "+path) + names := pathNames[path] + slices.Sort(names) + warnLines = append(warnLines, fmt.Sprintf("- %s: %s", path, strings.Join(names, ", "))) } } - if len(errLines) > 0 { - return fmt.Errorf( - "duplicate code source paths detected:\n%s", - strings.Join(errLines, "\n"), + if len(warnLines) > 0 { + slices.Sort(warnLines) + slog.Warn( + "Duplicate source code paths detected:\n" + strings.Join(warnLines, "\n"), ) } - return nil } // isNotEmpty reports whether the given string is not empty. From 3d5e0c71be3a0e4b76d6e6da96fefd4461775060 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Thu, 14 May 2026 16:40:46 +0200 Subject: [PATCH 07/11] Improve `check` mode outdated files output. --- cli/cli.go | 8 +++----- embedding/embedding_test.go | 7 +++++++ embedding/errors.go | 33 --------------------------------- embedding/processor.go | 9 +++------ main.go | 37 +++++++++++++++++++++++++++---------- 5 files changed, 40 insertions(+), 54 deletions(-) delete mode 100644 embedding/errors.go diff --git a/cli/cli.go b/cli/cli.go index 833cfae..ac46d41 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -95,12 +95,11 @@ const ( ModeAnalyze = "analyze" ) -// CheckCodeSamples checks documentation to be up-to-date with code files. Raises -// UnexpectedDiffError if not. +// CheckCodeSamples returns documentation files that are not up-to-date with code files. // // config — a configuration for checking code samples. -func CheckCodeSamples(config configuration.Configuration) { - embedding.CheckUpToDate(config) +func CheckCodeSamples(config configuration.Configuration) []string { + return embedding.CheckUpToDate(config) } // EmbedCodeSamples embeds code fragments in documentation files. @@ -108,7 +107,6 @@ func CheckCodeSamples(config configuration.Configuration) { // config — a configuration for embedding. func EmbedCodeSamples(config configuration.Configuration) EmbedCodeSamplesResult { embeddingResult := embedding.EmbedAll(config) - embedding.CheckUpToDate(config) return EmbedCodeSamplesResult{ embeddingResult, } diff --git a/embedding/embedding_test.go b/embedding/embedding_test.go index 8140629..b798e6a 100644 --- a/embedding/embedding_test.go +++ b/embedding/embedding_test.go @@ -116,6 +116,13 @@ var _ = Describe("Embedding", func() { Expect(processor.IsUpToDate()).Should(BeTrue()) }) + It("should report files that are not up to date", func() { + config.DocIncludes = []string{"doc.md"} + docPath := fmt.Sprintf("%s/doc.md", config.DocumentationRoot) + + Expect(embedding.CheckUpToDate(config)).Should(ContainElement(docPath)) + }) + It("should embed with multi lined tag attributes", func() { docPath := fmt.Sprintf("%s/multi-lined-valid-tag-attributes.md", config.DocumentationRoot) processor := embedding.NewProcessor(docPath, config) diff --git a/embedding/errors.go b/embedding/errors.go deleted file mode 100644 index 227adce..0000000 --- a/embedding/errors.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2024, TeamDev. All rights reserved. -// -// Redistribution and use in source and/or binary forms, with or without -// modification, must retain the above copyright notice and the following -// disclaimer. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package embedding - -import ( - "fmt" -) - -// UnexpectedDiffError describes an error which occurs if outdated files are found during -// the checking. -type UnexpectedDiffError struct { - changedFiles []string -} - -func (e *UnexpectedDiffError) Error() string { - return fmt.Sprintf("unexpected diff: %v", e.changedFiles) -} diff --git a/embedding/processor.go b/embedding/processor.go index b612947..3e7ddf9 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -189,14 +189,11 @@ func configNameLabel(config configuration.Configuration) string { return fmt.Sprintf(" for embedding `%s`", config.Name) } -// CheckUpToDate raises an error if the documentation files are not up-to-date with code files. +// CheckUpToDate returns documentation files that are not up-to-date with code files. // // config — a configuration for embedding. -func CheckUpToDate(config configuration.Configuration) { - changedFiles := findChangedFiles(config) - if len(changedFiles) > 0 { - panic(UnexpectedDiffError{changedFiles}) - } +func CheckUpToDate(config configuration.Configuration) []string { + return findChangedFiles(config) } // Iterates through the doc file line by line considering them as a states of an embedding. diff --git a/main.go b/main.go index 1175fec..98c7615 100644 --- a/main.go +++ b/main.go @@ -108,10 +108,7 @@ func main() { switch userArgs.Mode { case cli.ModeCheck: - for _, config := range configs { - cli.CheckCodeSamples(config) - } - fmt.Println("The documentation files are up-to-date with code files.") + checkByConfigs(configs) case cli.ModeEmbed: embedByConfigs(configs) fmt.Println("Embedding process finished.") @@ -137,6 +134,21 @@ func logError(message string, err error) { slog.Error(fmt.Sprintf("%s: %v", message, err)) } +// checkByConfigs runs check for all configs and logs outdated documentation files. +func checkByConfigs(configs []configuration.Configuration) { + var totalOutdatedFiles []string + for _, config := range configs { + totalOutdatedFiles = append(totalOutdatedFiles, cli.CheckCodeSamples(config)...) + } + if len(totalOutdatedFiles) == 0 { + fmt.Println("The documentation files are up-to-date with code files.") + + return + } + + printFiles("File outdated:", "Files outdated:", totalOutdatedFiles) +} + // embedByConfig runs the embedByConfig for all configs and logs the results. func embedByConfigs(configs []configuration.Configuration) { var totalEmbeddedFiles []string @@ -149,14 +161,19 @@ func embedByConfigs(configs []configuration.Configuration) { if len(totalEmbeddedFiles) == 0 && totalEmbeddings != 0 { fmt.Println("All documentation files are already up to date. Nothing to update.") } - if len(totalEmbeddedFiles) == 1 { - fmt.Println("File updated:") + printFiles("File updated:", "Files updated:", totalEmbeddedFiles) +} + +// printFiles prints file paths with the singular or plural heading. +func printFiles(singularHeading string, pluralHeading string, files []string) { + if len(files) == 1 { + fmt.Println(singularHeading) } - if len(totalEmbeddedFiles) > 1 { - fmt.Println("Files updated:") + if len(files) > 1 { + fmt.Println(pluralHeading) } - for _, updatedDocFile := range totalEmbeddedFiles { - absPath, err := filepath.Abs(updatedDocFile) + for _, file := range files { + absPath, err := filepath.Abs(file) if err != nil { panic(err) } From 53f611b52b728e76a02f52591473aa928d05d1d9 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Thu, 14 May 2026 16:52:50 +0200 Subject: [PATCH 08/11] Make `check` mode errors collectible. --- embedding/embedding_test.go | 20 ++++++++++++++++++++ embedding/processor.go | 34 +++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/embedding/embedding_test.go b/embedding/embedding_test.go index b798e6a..e498210 100644 --- a/embedding/embedding_test.go +++ b/embedding/embedding_test.go @@ -123,6 +123,26 @@ var _ = Describe("Embedding", func() { Expect(embedding.CheckUpToDate(config)).Should(ContainElement(docPath)) }) + It("should report all check errors", func() { + config.DocIncludes = []string{"missing-closing-tag.md", "unclosed-nested-tag.md"} + + var recovered any + func() { + defer func() { + recovered = recover() + }() + embedding.CheckUpToDate(config) + }() + + Expect(recovered).ShouldNot(BeNil()) + Expect(fmt.Sprint(recovered)).Should(And( + ContainSubstring("missing-closing-tag.md"), + ContainSubstring("the `` tag is not closed"), + ContainSubstring("unclosed-nested-tag.md"), + ContainSubstring("element closed by "), + )) + }) + It("should embed with multi lined tag attributes", func() { docPath := fmt.Sprintf("%s/multi-lined-valid-tag-attributes.md", config.DocumentationRoot) processor := embedding.NewProcessor(docPath, config) diff --git a/embedding/processor.go b/embedding/processor.go index 3e7ddf9..4c08300 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -124,15 +124,25 @@ func (p Processor) FindChangedEmbeddings() ([]parsing.Instruction, error) { // IsUpToDate reports whether the embedding of the target markdown is up-to-date with the code file. func (p Processor) IsUpToDate() bool { + upToDate, err := p.isUpToDate() + if err != nil { + panic(err) + } + + return upToDate +} + +// isUpToDate reports whether the target markdown is up-to-date and returns processing errors. +func (p Processor) isUpToDate() (bool, error) { if !slices.Contains(p.requiredDocPaths, p.DocFilePath) { - return true + return true, nil } context, err := p.fillEmbeddingContext() if err != nil { - panic(err) + return false, err } - return !context.IsContentChanged() + return !context.IsContentChanged(), nil } // EmbedAll processes embedding for multiple documentation files based on provided config. @@ -193,7 +203,12 @@ func configNameLabel(config configuration.Configuration) string { // // config — a configuration for embedding. func CheckUpToDate(config configuration.Configuration) []string { - return findChangedFiles(config) + changedFiles, checkErrors := findChangedFiles(config) + if len(checkErrors) > 0 { + panic(errors.Join(checkErrors...)) + } + + return changedFiles } // Iterates through the doc file line by line considering them as a states of an embedding. @@ -262,17 +277,22 @@ func (p Processor) moveToNextState(state *parsing.State, context *parsing.Contex // Returns a list of documentation files that are not up-to-date with their code files. // // config — a configuration for embedding. -func findChangedFiles(config configuration.Configuration) []string { +func findChangedFiles(config configuration.Configuration) ([]string, []error) { requiredDocPaths := requiredDocs(config) var changedFiles []string + var checkErrors []error for _, doc := range requiredDocPaths { - upToDate := NewProcessor(doc, config).IsUpToDate() + upToDate, err := NewProcessor(doc, config).isUpToDate() + if err != nil { + checkErrors = append(checkErrors, err) + continue + } if !upToDate { changedFiles = append(changedFiles, doc) } } - return changedFiles + return changedFiles, checkErrors } func requiredDocs(config configuration.Configuration) []string { From 5ad7470784576a91822986cedbb62a698ad14e6b Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Thu, 14 May 2026 17:39:54 +0200 Subject: [PATCH 09/11] Improve readability. --- cli/cli_test.go | 6 +- cli/cli_validation.go | 11 +-- fragmentation/cache.go | 105 ++++++++++++++++++++++++++++ fragmentation/fragmentation_test.go | 4 +- fragmentation/resolver.go | 27 +++---- 5 files changed, 121 insertions(+), 32 deletions(-) create mode 100644 fragmentation/cache.go diff --git a/cli/cli_test.go b/cli/cli_test.go index 36f6491..0d123a2 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -175,7 +175,7 @@ var _ = Describe("CLI validation", func() { "duplicate embedding names detected:\n- docs")) }) - It("should fail validation when code source names are duplicated", func() { + It("should fail validation when source code path names are duplicated", func() { invalidConfig := baseCliConfig() invalidConfig.BaseCodePaths = _type.NamedPathList{ _type.NamedPath{Name: "samples", Path: codeResourcePath("java")}, @@ -187,7 +187,7 @@ var _ = Describe("CLI validation", func() { "duplicate source code path names detected:\n- samples")) }) - It("should fail validation when multiple unnamed code sources are configured", func() { + It("should fail validation when multiple unnamed sources code paths are configured", func() { invalidConfig := baseCliConfig() invalidConfig.BaseCodePaths = _type.NamedPathList{ _type.NamedPath{Path: codeResourcePath("java")}, @@ -199,7 +199,7 @@ var _ = Describe("CLI validation", func() { "only one unnamed source code path is allowed")) }) - It("should fail validation when named and unnamed code sources are mixed", func() { + It("should fail validation when named and unnamed source code paths are mixed", func() { invalidConfig := baseCliConfig() invalidConfig.BaseCodePaths = _type.NamedPathList{ _type.NamedPath{Name: "java", Path: codeResourcePath("java")}, diff --git a/cli/cli_validation.go b/cli/cli_validation.go index b1825bd..5b8bd8c 100644 --- a/cli/cli_validation.go +++ b/cli/cli_validation.go @@ -318,7 +318,7 @@ func validateCodeSources(paths _type.NamedPathList) error { nameCount[p.Name]++ } pathCount[p.Path]++ - pathNames[p.Path] = append(pathNames[p.Path], codeSourceDisplayName(p.Name)) + pathNames[p.Path] = append(pathNames[p.Path], p.Name) } if err := verifyCodeSourceNames(nameCount); err != nil { @@ -355,15 +355,6 @@ func verifyCodeSourceNames(nameCount map[string]int) error { return nil } -// codeSourceDisplayName returns a readable code source name for diagnostics. -func codeSourceDisplayName(name string) string { - if isEmpty(name) { - return "(unnamed)" - } - - return name -} - // warnDuplicatePaths logs a warning if multiple code sources use the same path. func warnDuplicatePaths(pathCount map[string]int, pathNames map[string][]string) { var warnLines []string diff --git a/fragmentation/cache.go b/fragmentation/cache.go new file mode 100644 index 0000000..814a43e --- /dev/null +++ b/fragmentation/cache.go @@ -0,0 +1,105 @@ +// Copyright 2026, TeamDev. All rights reserved. +// +// Redistribution and use in source and/or binary forms, with or without +// modification, must retain the above copyright notice and the following +// disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package fragmentation + +import ( + "container/list" + "sync" +) + +// cache stores a limited number of least-recently-used values by string key. +type cache[T any] struct { + sync.Mutex + limit int + values map[string]T + entries map[string]*list.Element + order *list.List +} + +// newCache creates a cache with least-recently-used eviction. +func newCache[T any](limit int) *cache[T] { + return &cache[T]{ + limit: limit, + values: make(map[string]T), + entries: make(map[string]*list.Element), + order: list.New(), + } +} + +// get returns a cached value and marks it as recently used. +func (c *cache[T]) get(key string) (T, bool) { + c.Lock() + defer c.Unlock() + + value, found := c.values[key] + if found { + c.markUsed(key) + } + + return value, found +} + +// set stores a value and evicts the least recently used value when the cache exceeds its limit. +func (c *cache[T]) set(key string, value T) { + c.Lock() + defer c.Unlock() + + c.values[key] = value + if entry, found := c.entries[key]; found { + c.order.MoveToBack(entry) + return + } + + c.entries[key] = c.order.PushBack(key) + if len(c.values) <= c.limit { + return + } + + c.evictOldest() +} + +// clear removes all cached values. +func (c *cache[T]) clear() { + c.Lock() + defer c.Unlock() + + c.values = make(map[string]T) + c.entries = make(map[string]*list.Element) + c.order.Init() +} + +// markUsed moves a cache key to the most recently used position. +func (c *cache[T]) markUsed(key string) { + if entry, found := c.entries[key]; found { + c.order.MoveToBack(entry) + } +} + +// evictOldest removes the least recently used cache entry. +func (c *cache[T]) evictOldest() { + oldestEntry := c.order.Front() + if oldestEntry == nil { + return + } + + oldestKey := oldestEntry.Value.(string) + c.order.Remove(oldestEntry) + delete(c.entries, oldestKey) + delete(c.values, oldestKey) +} diff --git a/fragmentation/fragmentation_test.go b/fragmentation/fragmentation_test.go index d9e6440..d8bca82 100644 --- a/fragmentation/fragmentation_test.go +++ b/fragmentation/fragmentation_test.go @@ -55,7 +55,7 @@ var _ = Describe("Fragmentation", func() { config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: "../test/resources/code/java"}} }) - It("should fragment a file in memory", func() { + It("should do file fragmentation successfully", func() { lines, fragments := doTestFragmentation(correctFragmentsFileName, config) Expect(lines).ShouldNot(ContainElement(ContainSubstring("#docfragment"))) @@ -66,7 +66,7 @@ var _ = Describe("Fragmentation", func() { Expect(fragments).Should(HaveKey("main()")) }) - It("should resolve named fragments directly from source", func() { + It("should resolve named fragments", func() { content := resolveTestFragment(correctFragmentsFileName, "main()", config) Expect(content).Should(Equal([]string{ diff --git a/fragmentation/resolver.go b/fragmentation/resolver.go index 8635ef4..30e183a 100644 --- a/fragmentation/resolver.go +++ b/fragmentation/resolver.go @@ -22,23 +22,22 @@ import ( "fmt" "path/filepath" "strings" - "sync" config "embed-code/embed-code-go/configuration" _type "embed-code/embed-code-go/type" ) +// resolverCacheLimit is the maximum number of source files retained in the resolver cache. +const resolverCacheLimit = 100 + +// cachedFragmentation stores cleaned source lines and parsed fragments for one source file. type cachedFragmentation struct { lines []string fragments map[string]Fragment } -var resolverCache = struct { - sync.RWMutex - files map[string]cachedFragmentation -}{ - files: make(map[string]cachedFragmentation), -} +// resolverCache stores source fragmentations already resolved during the current run. +var resolverCache = newCache[cachedFragmentation](resolverCacheLimit) // ResolveContent returns source lines for the requested code file fragment. // @@ -73,12 +72,10 @@ func ResolveContent(codePath string, fragmentName string, config config.Configur // ClearResolverCache removes cached source fragmentations. func ClearResolverCache() { - resolverCache.Lock() - defer resolverCache.Unlock() - - resolverCache.files = make(map[string]cachedFragmentation) + resolverCache.clear() } +// resolvedSource describes a source file resolved from a user-facing embedding path. type resolvedSource struct { root _type.NamedPath relativePath string @@ -137,9 +134,7 @@ func sourceFromRoot(root _type.NamedPath, relativePath string) (resolvedSource, // cachedSourceFragments returns cached source fragmentation for a resolved source file. func cachedSourceFragments(source resolvedSource, config config.Configuration) (cachedFragmentation, error) { cacheKey := source.absolutePath - resolverCache.RLock() - content, found := resolverCache.files[cacheKey] - resolverCache.RUnlock() + content, found := resolverCache.get(cacheKey) if found { return content, nil } @@ -154,9 +149,7 @@ func cachedSourceFragments(source resolvedSource, config config.Configuration) ( fragments: fragments, } - resolverCache.Lock() - resolverCache.files[cacheKey] = content - resolverCache.Unlock() + resolverCache.set(cacheKey, content) return content, nil } From 0ac16b0297d7c28d1921097195c54b000d0f2f61 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Thu, 14 May 2026 17:51:59 +0200 Subject: [PATCH 10/11] Improve cache. --- fragmentation/cache.go | 68 ++++++++++++++++++++++----------------- fragmentation/resolver.go | 28 ++++++++-------- 2 files changed, 52 insertions(+), 44 deletions(-) diff --git a/fragmentation/cache.go b/fragmentation/cache.go index 814a43e..052bd3a 100644 --- a/fragmentation/cache.go +++ b/fragmentation/cache.go @@ -23,43 +23,63 @@ import ( "sync" ) -// cache stores a limited number of least-recently-used values by string key. -type cache[T any] struct { +// cache stores a limited number of least-recently-used values by key. +type cache[K comparable, V any] struct { sync.Mutex limit int - values map[string]T - entries map[string]*list.Element + loader func(K) (V, error) + values map[K]V + entries map[K]*list.Element order *list.List } -// newCache creates a cache with least-recently-used eviction. -func newCache[T any](limit int) *cache[T] { - return &cache[T]{ +// newCache creates a cache with a loader and least-recently-used eviction. +func newCache[K comparable, V any](limit int, loader func(K) (V, error)) *cache[K, V] { + return &cache[K, V]{ limit: limit, - values: make(map[string]T), - entries: make(map[string]*list.Element), + loader: loader, + values: make(map[K]V), + entries: make(map[K]*list.Element), order: list.New(), } } -// get returns a cached value and marks it as recently used. -func (c *cache[T]) get(key string) (T, bool) { +// get returns a cached value or loads it when missing. +func (c *cache[K, V]) get(key K) (V, error) { c.Lock() - defer c.Unlock() - value, found := c.values[key] if found { c.markUsed(key) + c.Unlock() + + return value, nil + } + c.Unlock() + + value, err := c.loader(key) + if err != nil { + return value, err } - return value, found + c.Lock() + defer c.Unlock() + c.storeLoaded(key, value) + + return value, nil } -// set stores a value and evicts the least recently used value when the cache exceeds its limit. -func (c *cache[T]) set(key string, value T) { +// clear removes all cached values. +func (c *cache[K, V]) clear() { c.Lock() defer c.Unlock() + c.values = make(map[K]V) + c.entries = make(map[K]*list.Element) + c.order.Init() +} + +// storeLoaded stores a loaded value and evicts the least recently used value when needed. +func (c *cache[K, V]) storeLoaded(key K, value V) { c.values[key] = value if entry, found := c.entries[key]; found { c.order.MoveToBack(entry) @@ -74,31 +94,21 @@ func (c *cache[T]) set(key string, value T) { c.evictOldest() } -// clear removes all cached values. -func (c *cache[T]) clear() { - c.Lock() - defer c.Unlock() - - c.values = make(map[string]T) - c.entries = make(map[string]*list.Element) - c.order.Init() -} - // markUsed moves a cache key to the most recently used position. -func (c *cache[T]) markUsed(key string) { +func (c *cache[K, V]) markUsed(key K) { if entry, found := c.entries[key]; found { c.order.MoveToBack(entry) } } // evictOldest removes the least recently used cache entry. -func (c *cache[T]) evictOldest() { +func (c *cache[K, V]) evictOldest() { oldestEntry := c.order.Front() if oldestEntry == nil { return } - oldestKey := oldestEntry.Value.(string) + oldestKey := oldestEntry.Value.(K) c.order.Remove(oldestEntry) delete(c.entries, oldestKey) delete(c.values, oldestKey) diff --git a/fragmentation/resolver.go b/fragmentation/resolver.go index 30e183a..4b499e0 100644 --- a/fragmentation/resolver.go +++ b/fragmentation/resolver.go @@ -37,7 +37,10 @@ type cachedFragmentation struct { } // resolverCache stores source fragmentations already resolved during the current run. -var resolverCache = newCache[cachedFragmentation](resolverCacheLimit) +var resolverCache = newCache[resolvedSource, cachedFragmentation]( + resolverCacheLimit, + loadSourceFragments, +) // ResolveContent returns source lines for the requested code file fragment. // @@ -55,7 +58,7 @@ func ResolveContent(codePath string, fragmentName string, config config.Configur return nil, unresolvedSourceError(codePath, fragmentName, config) } - content, err := cachedSourceFragments(source, config) + content, err := cachedSourceFragments(source) if err != nil { return nil, err } @@ -132,26 +135,21 @@ func sourceFromRoot(root _type.NamedPath, relativePath string) (resolvedSource, } // cachedSourceFragments returns cached source fragmentation for a resolved source file. -func cachedSourceFragments(source resolvedSource, config config.Configuration) (cachedFragmentation, error) { - cacheKey := source.absolutePath - content, found := resolverCache.get(cacheKey) - if found { - return content, nil - } +func cachedSourceFragments(source resolvedSource) (cachedFragmentation, error) { + return resolverCache.get(source) +} - fragmentation := NewFragmentation(source.absolutePath, source.root, config) +// loadSourceFragments reads and fragments the source file when it is not already cached. +func loadSourceFragments(source resolvedSource) (cachedFragmentation, error) { + fragmentation := NewFragmentation(source.absolutePath, source.root, config.Configuration{}) lines, fragments, err := fragmentation.DoFragmentation() if err != nil { return cachedFragmentation{}, err } - content = cachedFragmentation{ + return cachedFragmentation{ lines: lines, fragments: fragments, - } - - resolverCache.set(cacheKey, content) - - return content, nil + }, nil } // fragmentLines renders a fragment into lines. From 7331b9600154b9bbae5d3162b541d9d096ba0c51 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Fri, 15 May 2026 09:33:06 +0200 Subject: [PATCH 11/11] Improve readability. --- fragmentation/cache.go | 2 +- fragmentation/resolver.go | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/fragmentation/cache.go b/fragmentation/cache.go index 052bd3a..b986dc5 100644 --- a/fragmentation/cache.go +++ b/fragmentation/cache.go @@ -23,7 +23,7 @@ import ( "sync" ) -// cache stores a limited number of least-recently-used values by key. +// cache is a limited collection of recently used values by key. type cache[K comparable, V any] struct { sync.Mutex limit int diff --git a/fragmentation/resolver.go b/fragmentation/resolver.go index 4b499e0..84f26ec 100644 --- a/fragmentation/resolver.go +++ b/fragmentation/resolver.go @@ -30,14 +30,14 @@ import ( // resolverCacheLimit is the maximum number of source files retained in the resolver cache. const resolverCacheLimit = 100 -// cachedFragmentation stores cleaned source lines and parsed fragments for one source file. -type cachedFragmentation struct { +// fragmentedFile stores cleaned source lines and parsed fragments for one source file. +type fragmentedFile struct { lines []string fragments map[string]Fragment } // resolverCache stores source fragmentations already resolved during the current run. -var resolverCache = newCache[resolvedSource, cachedFragmentation]( +var resolverCache = newCache[resolvedPath, fragmentedFile]( resolverCacheLimit, loadSourceFragments, ) @@ -78,15 +78,15 @@ func ClearResolverCache() { resolverCache.clear() } -// resolvedSource describes a source file resolved from a user-facing embedding path. -type resolvedSource struct { +// resolvedPath is a source file path resolved from a user-facing embedding path. +type resolvedPath struct { root _type.NamedPath relativePath string absolutePath string } -// resolveSource resolves the user-facing code path to an included textual source file. -func resolveSource(codePath string, config config.Configuration) (resolvedSource, bool, error) { +// resolveSource resolves the user-facing code path to the source file. +func resolveSource(codePath string, config config.Configuration) (resolvedPath, bool, error) { codeRootName, relativePath, named := splitNamedPath(codePath) for _, root := range config.CodeRoots { if named && strings.TrimSpace(root.Name) != codeRootName { @@ -95,7 +95,7 @@ func resolveSource(codePath string, config config.Configuration) (resolvedSource source, err := sourceFromRoot(root, relativePath) if err != nil { - return resolvedSource{}, false, err + return resolvedPath{}, false, err } if !shouldDoFragmentation(source.absolutePath) { continue @@ -104,7 +104,7 @@ func resolveSource(codePath string, config config.Configuration) (resolvedSource return source, true, nil } - return resolvedSource{}, false, nil + return resolvedPath{}, false, nil } // splitNamedPath separates a named-code-root prefix from a code path. @@ -121,13 +121,13 @@ func splitNamedPath(codePath string) (string, string, bool) { } // sourceFromRoot builds a source path from a code root and a relative path. -func sourceFromRoot(root _type.NamedPath, relativePath string) (resolvedSource, error) { +func sourceFromRoot(root _type.NamedPath, relativePath string) (resolvedPath, error) { rootAbs, err := filepath.Abs(root.Path) if err != nil { - return resolvedSource{}, err + return resolvedPath{}, err } - return resolvedSource{ + return resolvedPath{ root: root, relativePath: filepath.FromSlash(relativePath), absolutePath: filepath.Join(rootAbs, filepath.FromSlash(relativePath)), @@ -135,18 +135,18 @@ func sourceFromRoot(root _type.NamedPath, relativePath string) (resolvedSource, } // cachedSourceFragments returns cached source fragmentation for a resolved source file. -func cachedSourceFragments(source resolvedSource) (cachedFragmentation, error) { +func cachedSourceFragments(source resolvedPath) (fragmentedFile, error) { return resolverCache.get(source) } // loadSourceFragments reads and fragments the source file when it is not already cached. -func loadSourceFragments(source resolvedSource) (cachedFragmentation, error) { +func loadSourceFragments(source resolvedPath) (fragmentedFile, error) { fragmentation := NewFragmentation(source.absolutePath, source.root, config.Configuration{}) lines, fragments, err := fragmentation.DoFragmentation() if err != nil { - return cachedFragmentation{}, err + return fragmentedFile{}, err } - return cachedFragmentation{ + return fragmentedFile{ lines: lines, fragments: fragments, }, nil