From 02240f62d73966cbe96d9a04879b7fa2cc8ef747 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 8 Jun 2026 21:41:57 +0200 Subject: [PATCH 1/3] Extract `processRequiredDocs` method. --- embedding/processor.go | 61 +++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/embedding/processor.go b/embedding/processor.go index c1ac45a..7a9b0c3 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -61,6 +61,9 @@ type EmbedAllResult struct { UpdatedTargetFiles []string } +// processorHandler applies one processing mode to a configured documentation processor. +type processorHandler func(processor Processor) error + // NewProcessor creates and returns new Processor with given docFile and config. func NewProcessor(docFile string, config configuration.Configuration) (Processor, error) { requiredDocPaths, err := requiredDocs(config) @@ -192,26 +195,20 @@ func (p Processor) isUpToDate() (bool, error) { // // config — a configuration for embedding. func EmbedAll(config configuration.Configuration) (EmbedAllResult, error) { - requiredDocPaths, err := requiredDocs(config) - if err != nil { - return EmbedAllResult{}, err - } totalEmbeddings := 0 var updatedTargetFiles []string - var embeddingErrors []error - for _, doc := range requiredDocPaths { - processor := newProcessor(doc, config, parsing.Transitions, requiredDocPaths) + requiredDocPaths, embeddingErrors := processRequiredDocs(config, func(processor Processor) error { context, err := processor.Embed() if err != nil { - embeddingErrors = append(embeddingErrors, err) - - continue + return err } totalEmbeddings += context.EmbeddingsCount() if context.IsContentChanged() { - updatedTargetFiles = append(updatedTargetFiles, doc) + updatedTargetFiles = append(updatedTargetFiles, processor.DocFilePath) } - } + + return nil + }) if len(embeddingErrors) > 0 { return EmbedAllResult{}, errors.Join(embeddingErrors...) } @@ -360,27 +357,41 @@ func (p Processor) moveToNextState(state *parsing.State, context *parsing.Contex // // config — a configuration for embedding. func findChangedFiles(config configuration.Configuration) ([]string, []error) { + var changedFiles []string + _, checkErrors := processRequiredDocs(config, func(processor Processor) error { + upToDate, err := processor.isUpToDate() + if err != nil { + return err + } + if !upToDate { + changedFiles = append(changedFiles, processor.DocFilePath) + } + + return nil + }) + + return changedFiles, checkErrors +} + +// processRequiredDocs applies a processing handler to every documentation file in config. +func processRequiredDocs( + config configuration.Configuration, + handle processorHandler, +) ([]string, []error) { requiredDocPaths, err := requiredDocs(config) if err != nil { return nil, []error{err} } - var changedFiles []string - var checkErrors []error - for _, doc := range requiredDocPaths { - upToDate, err := newProcessor( - doc, config, parsing.Transitions, requiredDocPaths, - ).isUpToDate() - if err != nil { - checkErrors = append(checkErrors, err) - continue - } - if !upToDate { - changedFiles = append(changedFiles, doc) + var processingErrors []error + for _, doc := range requiredDocPaths { + processor := newProcessor(doc, config, parsing.Transitions, requiredDocPaths) + if err := handle(processor); err != nil { + processingErrors = append(processingErrors, err) } } - return changedFiles, checkErrors + return requiredDocPaths, processingErrors } // requiredDocs returns documentation files matched by includes minus excludes. From aeea0706fefe821f0df7d35215a4073730f97470 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 8 Jun 2026 22:00:52 +0200 Subject: [PATCH 2/3] Extract `ProcessingError`. --- embedding/embedding_test.go | 31 +++++++++++++++++++++++-- embedding/error.go | 46 +++++++++++++++++++++++++++++++++++++ embedding/processor.go | 15 ++++++++---- 3 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 embedding/error.go diff --git a/embedding/embedding_test.go b/embedding/embedding_test.go index 601b6d1..1a45a91 100644 --- a/embedding/embedding_test.go +++ b/embedding/embedding_test.go @@ -19,8 +19,7 @@ package embedding_test import ( - "embed-code/embed-code-go/files" - _type "embed-code/embed-code-go/type" + "errors" "fmt" "io" "os" @@ -31,6 +30,8 @@ import ( "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/embedding" "embed-code/embed-code-go/embedding/parsing" + "embed-code/embed-code-go/files" + _type "embed-code/embed-code-go/type" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -155,6 +156,11 @@ var _ = Describe("Embedding", func() { ContainSubstring("unclosed-nested-tag.md"), ContainSubstring("element closed by "), )) + var processingErr embedding.ProcessingError + Expect(errors.As(err, &processingErr)).Should(BeTrue()) + + var parseErr parsing.InstructionParseError + Expect(errors.As(err, &parseErr)).Should(BeTrue()) }) It("should report all pattern matching errors", func() { @@ -178,6 +184,9 @@ var _ = Describe("Embedding", func() { "`*doesNotExistEnd*`", ), )) + var patternErr parsing.PatternNotFoundError + Expect(errors.As(err, &patternErr)).Should(BeTrue()) + Expect(patternErr.Line).Should(Equal(3)) }) It("should embed with multi lined tag attributes", func() { @@ -261,6 +270,24 @@ var _ = Describe("Embedding", func() { )) }) + It("should preserve typed parser errors after adding document context", func() { + docPath := fmt.Sprintf("%s/missing-closing-tag.md", config.DocumentationRoot) + processor := newProcessor(docPath, config) + + _, err := processor.Embed() + + Expect(err).Should(HaveOccurred()) + var processingErr embedding.ProcessingError + Expect(errors.As(err, &processingErr)).Should(BeTrue()) + Expect(processingErr.DocFilePath).Should(Equal(docPath)) + Expect(processingErr.Line).Should(Equal(3)) + + var parseErr parsing.InstructionParseError + Expect(errors.As(err, &parseErr)).Should(BeTrue()) + Expect(parseErr.Line).Should(Equal(3)) + Expect(parseErr.Reason).Should(Equal("the `` tag is not closed")) + }) + It("should report the XML parser error", func() { docPath := fmt.Sprintf("%s/unclosed-nested-tag.md", config.DocumentationRoot) processor := newProcessor(docPath, config) diff --git a/embedding/error.go b/embedding/error.go new file mode 100644 index 0000000..ddad64c --- /dev/null +++ b/embedding/error.go @@ -0,0 +1,46 @@ +// 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 embedding + +import ( + "fmt" + + "embed-code/embed-code-go/logging" +) + +// ProcessingError wraps a parser or source-resolution error with documentation location. +type ProcessingError struct { + DocFilePath string + Line int + Err error +} + +// Error returns a user-facing description of the failed documentation processing operation. +func (e ProcessingError) Error() string { + return fmt.Sprintf( + "failed to embed code fragment into doc file `%s`: %s", + logging.FileReferenceWithLine(e.DocFilePath, e.Line), + e.Err, + ) +} + +// Unwrap returns the parser or source-resolution error that caused processing to fail. +func (e ProcessingError) Unwrap() error { + return e.Err +} diff --git a/embedding/processor.go b/embedding/processor.go index 7a9b0c3..6155558 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -267,8 +267,6 @@ func (p Processor) fillEmbeddingContext() (parsing.Context, error) { if err != nil { return context, err } - absDocPath, _ := filepath.Abs(p.DocFilePath) - errorStr := "failed to embed code fragment into doc file `file://%s:%d`: %s" var currentState parsing.State currentState = parsing.Start @@ -277,7 +275,7 @@ func (p Processor) fillEmbeddingContext() (parsing.Context, error) { for currentState != finishState { accepted, newState, err := p.moveToNextState(¤tState, &context) if err != nil { - return context, fmt.Errorf(errorStr, absDocPath, errorLine(context, err), err) + return context, p.processingError(context, err) } if !accepted { err = unacceptedTransitionError(context) @@ -286,7 +284,7 @@ func (p Processor) fillEmbeddingContext() (parsing.Context, error) { context.ResolveUnacceptedEmbedding() } - return context, fmt.Errorf(errorStr, absDocPath, errorLine(context, err), err) + return context, p.processingError(context, err) } currentState = *newState } @@ -294,6 +292,15 @@ func (p Processor) fillEmbeddingContext() (parsing.Context, error) { return context, nil } +// processingError wraps a parsing error with the current documentation location. +func (p Processor) processingError(context parsing.Context, err error) ProcessingError { + return ProcessingError{ + DocFilePath: p.DocFilePath, + Line: errorLine(context, err), + Err: err, + } +} + // errorLine returns the source line that should be used in the embedding error location. func errorLine(context parsing.Context, err error) int { var parseErr parsing.InstructionParseError From 81edc51e08d07de9fee757121fd42358b543c518 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Mon, 8 Jun 2026 22:18:36 +0200 Subject: [PATCH 3/3] Divide `SetEmbedding` on two methods. --- embedding/parsing/code_fence_end.go | 2 +- embedding/parsing/context.go | 32 +++++++++++++------------- embedding/parsing/instruction_token.go | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/embedding/parsing/code_fence_end.go b/embedding/parsing/code_fence_end.go index 0a9ad56..f225fdb 100644 --- a/embedding/parsing/code_fence_end.go +++ b/embedding/parsing/code_fence_end.go @@ -61,7 +61,7 @@ func (c CodeFenceEndState) Recognize(context Context) bool { func (c CodeFenceEndState) Accept(context *Context, _ configuration.Configuration) error { line := context.CurrentLine() err := renderSample(context) - context.SetEmbedding(nil) + context.FinishEmbedding() if err == nil { context.Result = append(context.Result, line) } else { diff --git a/embedding/parsing/context.go b/embedding/parsing/context.go index 722739a..c09c5de 100644 --- a/embedding/parsing/context.go +++ b/embedding/parsing/context.go @@ -181,23 +181,23 @@ func (c *Context) ResolveUnacceptedEmbedding() { c.EmbeddingInstruction = nil } -// SetEmbedding sets an embedding to Context. Also sets fileContainsEmbedding flag. -func (c *Context) SetEmbedding(embedding *Instruction) { - sourceIndex := c.lineIndex - resultIndex := len(c.Result) - - if embedding == nil { - c.CurrentEmbedding().SourceEndIndex = sourceIndex - c.CurrentEmbedding().resultEndIndex = resultIndex - } else { - c.fileContainsEmbedding = true - context := EmbeddingContext{ - embeddingInstruction: *embedding, - } - - c.embeddings = append(c.embeddings, context) +// StartEmbedding records an instruction as the current embedding. +func (c *Context) StartEmbedding(instruction Instruction) { + c.fileContainsEmbedding = true + embeddingContext := EmbeddingContext{ + embeddingInstruction: instruction, } - c.EmbeddingInstruction = embedding + + c.embeddings = append(c.embeddings, embeddingContext) + c.EmbeddingInstruction = &c.CurrentEmbedding().embeddingInstruction +} + +// FinishEmbedding closes the current embedding at the current parser position. +func (c *Context) FinishEmbedding() { + currentEmbedding := c.CurrentEmbedding() + currentEmbedding.SourceEndIndex = c.lineIndex + currentEmbedding.resultEndIndex = len(c.Result) + c.EmbeddingInstruction = nil } // SetCodeStart sets the current line as a start of a code lines in the result. It's needed to not diff --git a/embedding/parsing/instruction_token.go b/embedding/parsing/instruction_token.go index 19c15f6..3b44a22 100644 --- a/embedding/parsing/instruction_token.go +++ b/embedding/parsing/instruction_token.go @@ -103,7 +103,7 @@ func (e EmbedInstructionTokenState) Accept(context *Context, if err == nil { instruction.DocumentationFile = context.MarkdownFilePath instruction.DocumentationLine = startLine - context.SetEmbedding(&instruction) + context.StartEmbedding(instruction) } else { parseErr = err }