From 22186b78de32e7e0f202fd2af47d92eb27035b64 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 3 Jun 2026 10:49:15 +0200 Subject: [PATCH 1/3] Reduce `panic` number in `processor.go`. --- cli/cli.go | 25 ++++++--- embedding/embedding_test.go | 34 ++++-------- embedding/processor.go | 48 ++++++++++------ main.go | 106 ++++++++++++++++++++++++++++-------- 4 files changed, 144 insertions(+), 69 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index c3dcc7f..cd75493 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -99,18 +99,22 @@ const ( // 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) []string { +func CheckCodeSamples(config configuration.Configuration) ([]string, error) { return embedding.CheckUpToDate(config) } // EmbedCodeSamples embeds code fragments in documentation files. // // config — a configuration for embedding. -func EmbedCodeSamples(config configuration.Configuration) EmbedCodeSamplesResult { - embeddingResult := embedding.EmbedAll(config) +func EmbedCodeSamples(config configuration.Configuration) (EmbedCodeSamplesResult, error) { + embeddingResult, err := embedding.EmbedAll(config) + if err != nil { + return EmbedCodeSamplesResult{}, err + } + return EmbedCodeSamplesResult{ embeddingResult, - } + }, nil } // ReadArgs reads user-specified args from the command line. @@ -154,7 +158,10 @@ func ReadArgs() Config { // // Returns filled Config. func FillArgsFromConfigFile(args Config) (Config, error) { - configFields := readConfigFields(args.ConfigPath) + configFields, err := readConfigFields(args.ConfigPath) + if err != nil { + return args, err + } slog.Info(fmt.Sprintf( "Loaded config file `%s`. Found %d embedding setup(s).", logging.FileReference(args.ConfigPath), len(configFields.Embeddings), @@ -288,17 +295,17 @@ func parseListArgument(listArgument string) []string { // configFilePath — a path to a yaml configuration file. // // Returns a filled ConfigFields struct. -func readConfigFields(configFilePath string) Config { +func readConfigFields(configFilePath string) (Config, error) { content, err := os.ReadFile(configFilePath) if err != nil { - panic(err) + return Config{}, err } configFields := Config{} err = yaml.Unmarshal(content, &configFields) if err != nil { - panic(err) + return Config{}, err } - return configFields + return configFields, nil } diff --git a/embedding/embedding_test.go b/embedding/embedding_test.go index f2403b6..4957124 100644 --- a/embedding/embedding_test.go +++ b/embedding/embedding_test.go @@ -116,7 +116,10 @@ var _ = Describe("Embedding", func() { config.DocIncludes = []string{"doc.md"} docPath := fmt.Sprintf("%s/doc.md", config.DocumentationRoot) - Expect(embedding.CheckUpToDate(config)).Should(ContainElement(docPath)) + outdatedFiles, err := embedding.CheckUpToDate(config) + + Expect(err).ShouldNot(HaveOccurred()) + Expect(outdatedFiles).Should(ContainElement(docPath)) }) It("should ignore embed-code samples inside markdown code fences", func() { @@ -143,16 +146,10 @@ var _ = Describe("Embedding", func() { 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) - }() + _, err := embedding.CheckUpToDate(config) - Expect(recovered).ShouldNot(BeNil()) - Expect(fmt.Sprint(recovered)).Should(And( + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(And( ContainSubstring("missing-closing-tag.md"), ContainSubstring("the `` tag is not closed"), ContainSubstring("unclosed-nested-tag.md"), @@ -163,16 +160,10 @@ var _ = Describe("Embedding", func() { It("should report all pattern matching errors", func() { config.DocIncludes = []string{"missing-start-pattern.md", "missing-end-pattern.md"} - var recovered any - func() { - defer func() { - recovered = recover() - }() - embedding.CheckUpToDate(config) - }() + _, err := embedding.CheckUpToDate(config) - Expect(recovered).ShouldNot(BeNil()) - Expect(fmt.Sprint(recovered)).Should(And( + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(And( ContainSubstring("missing-start-pattern.md:3"), ContainSubstring( "no line in code file `file://", @@ -317,10 +308,9 @@ var _ = Describe("Embedding", func() { config.DocumentationRoot) processor := embedding.NewProcessor(docPath, config) - Expect(func() { - embedding.EmbedAll(config) - }).NotTo(Panic()) + _, err := embedding.EmbedAll(config) + Expect(err).ShouldNot(HaveOccurred()) Expect(processor.IsUpToDate()).Should(BeTrue()) }) diff --git a/embedding/processor.go b/embedding/processor.go index a5db680..4e6ba7d 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -63,14 +63,24 @@ type EmbedAllResult struct { // NewProcessor creates and returns new Processor with given docFile and config. func NewProcessor(docFile string, config configuration.Configuration) Processor { - return newProcessor(docFile, config, parsing.Transitions, requiredDocs(config)) + requiredDocPaths, err := requiredDocs(config) + if err != nil { + panic(err) + } + + return newProcessor(docFile, config, parsing.Transitions, requiredDocPaths) } // NewProcessorWithTransitions Creates and returns new Processor with given docFile, config // and transitions. func NewProcessorWithTransitions(docFile string, config configuration.Configuration, transitions parsing.TransitionMap) Processor { - return newProcessor(docFile, config, transitions, requiredDocs(config)) + requiredDocPaths, err := requiredDocs(config) + if err != nil { + panic(err) + } + + return newProcessor(docFile, config, transitions, requiredDocPaths) } // newProcessor creates a Processor with a precomputed documentation file list. @@ -178,8 +188,11 @@ func (p Processor) isUpToDate() (bool, error) { // creates an EmbeddingProcessor for each file, and embeds code fragments in them. // // config — a configuration for embedding. -func EmbedAll(config configuration.Configuration) EmbedAllResult { - requiredDocPaths := requiredDocs(config) +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 @@ -196,7 +209,7 @@ func EmbedAll(config configuration.Configuration) EmbedAllResult { } } if len(embeddingErrors) > 0 { - panic(errors.Join(embeddingErrors...)) + return EmbedAllResult{}, errors.Join(embeddingErrors...) } if totalEmbeddings > 0 { slog.Info( @@ -217,7 +230,7 @@ func EmbedAll(config configuration.Configuration) EmbedAllResult { TargetFiles: requiredDocPaths, TotalEmbeddings: totalEmbeddings, UpdatedTargetFiles: updatedTargetFiles, - } + }, nil } // configNameLabel formats a configuration name for summary log messages. @@ -231,13 +244,13 @@ func configNameLabel(config configuration.Configuration) string { // CheckUpToDate returns documentation files that are not up-to-date with code files. // // config — a configuration for embedding. -func CheckUpToDate(config configuration.Configuration) []string { +func CheckUpToDate(config configuration.Configuration) ([]string, error) { changedFiles, checkErrors := findChangedFiles(config) if len(checkErrors) > 0 { - panic(errors.Join(checkErrors...)) + return nil, errors.Join(checkErrors...) } - return changedFiles + return changedFiles, nil } // Iterates through the doc file line by line considering them as a states of an embedding. @@ -334,11 +347,14 @@ func (p Processor) moveToNextState(state *parsing.State, context *parsing.Contex return false, state, nil } -// Returns a list of documentation files that are not up-to-date with their code files. +// findChangedFiles returns documentation files that are not up-to-date with their code files. // // config — a configuration for embedding. func findChangedFiles(config configuration.Configuration) ([]string, []error) { - requiredDocPaths := requiredDocs(config) + requiredDocPaths, err := requiredDocs(config) + if err != nil { + return nil, []error{err} + } var changedFiles []string var checkErrors []error for _, doc := range requiredDocPaths { @@ -356,19 +372,19 @@ func findChangedFiles(config configuration.Configuration) ([]string, []error) { } // requiredDocs returns documentation files matched by includes minus excludes. -func requiredDocs(config configuration.Configuration) []string { +func requiredDocs(config configuration.Configuration) ([]string, error) { documentationRoot := config.DocumentationRoot includedPatterns := config.DocIncludes excludedPatterns := config.DocExcludes includedDocs, err := getFilesByPatterns(documentationRoot, includedPatterns) if err != nil { - panic(err) + return nil, err } excludedDocs, err := getFilesByPatterns(documentationRoot, excludedPatterns) if err != nil { - panic(err) + return nil, err } if len(excludedDocs) == 0 { slog.Info(fmt.Sprintf( @@ -376,7 +392,7 @@ func requiredDocs(config configuration.Configuration) []string { len(includedDocs), logging.FileReference(documentationRoot), patternsLabel(includedPatterns), )) - return includedDocs + return includedDocs, nil } result := removeElements(includedDocs, excludedDocs) @@ -386,7 +402,7 @@ func requiredDocs(config configuration.Configuration) []string { patternsLabel(excludedPatterns), )) - return result + return result, nil } // patternsLabel formats glob patterns for human-readable log messages. diff --git a/main.go b/main.go index 95d4ae1..47cb4f2 100644 --- a/main.go +++ b/main.go @@ -22,8 +22,11 @@ import ( "embed-code/embed-code-go/cli" "embed-code/embed-code-go/configuration" "embed-code/embed-code-go/logging" + "errors" "fmt" "log/slog" + "os" + "strings" ) // Version of the embed-code application. @@ -90,30 +93,28 @@ func main() { if cli.IsUsingConfigFile(userArgs) { err := cli.ValidateConfigFile(userArgs) if err != nil { - logError("The provided config file is not valid", err) - - return + exitWithError("The provided config file is not valid", err) } userArgs, err = cli.FillArgsFromConfigFile(userArgs) if err != nil { - logError("Received an issue while reading config file", err) - - return + exitWithError("Received an issue while reading config file", err) } } err := cli.ValidateConfig(userArgs) if err != nil { - logError("User arguments are not valid", err) - - return + exitWithError("User arguments are not valid", err) } configs := cli.BuildEmbedCodeConfiguration(userArgs) switch userArgs.Mode { case cli.ModeCheck: - checkByConfigs(configs) + if err := checkByConfigs(configs); err != nil { + exitWithError("Check failed", err) + } case cli.ModeEmbed: - embedByConfigs(configs) + if err := embedByConfigs(configs); err != nil { + exitWithError("Embedding failed", err) + } fmt.Println("Embedding process finished.") } } @@ -130,38 +131,99 @@ func configureLogging(config cli.Config) { // logError writes a user-facing error through the configured logger. func logError(message string, err error) { - slog.Error(fmt.Sprintf("%s: %v", message, err)) + slog.Error(formatError(message, err)) +} + +// formatError formats single errors inline and joined errors as a bullet list. +func formatError(message string, err error) string { + errs := flattenedErrors(err) + if len(errs) <= 1 { + return fmt.Sprintf("%s: %v", message, err) + } + + var builder strings.Builder + builder.WriteString(message) + builder.WriteString(":") + for _, nestedErr := range errs { + builder.WriteString("\n- ") + builder.WriteString(nestedErr.Error()) + } + + return builder.String() +} + +// flattenedErrors returns the leaf errors from an error joined with errors.Join. +func flattenedErrors(err error) []error { + joined, ok := err.(interface { + Unwrap() []error + }) + if !ok { + return []error{err} + } + + var result []error + for _, nestedErr := range joined.Unwrap() { + result = append(result, flattenedErrors(nestedErr)...) + } + + return result } -// checkByConfigs runs check for all configs and panics if documentation files are outdated. -func checkByConfigs(configs []configuration.Configuration) { +// exitWithError writes a user-facing error and exits with a failing status. +func exitWithError(message string, err error) { + logError(message, err) + os.Exit(1) +} + +// checkByConfigs runs check for all configs and reports documentation files that are outdated. +func checkByConfigs(configs []configuration.Configuration) error { var totalOutdatedFiles []string + var checkErrors []error for _, config := range configs { - totalOutdatedFiles = append(totalOutdatedFiles, cli.CheckCodeSamples(config)...) + outdatedFiles, err := cli.CheckCodeSamples(config) + if err != nil { + checkErrors = append(checkErrors, err) + continue + } + totalOutdatedFiles = append(totalOutdatedFiles, outdatedFiles...) + } + if len(totalOutdatedFiles) > 0 { + printFiles("File to update:", "Files to update:", totalOutdatedFiles) + checkErrors = append(checkErrors, + fmt.Errorf("the documentation files are not up-to-date with code files")) } - if len(totalOutdatedFiles) == 0 { + if len(checkErrors) == 0 { fmt.Println("The documentation files are up-to-date with code files.") - return + return nil } - printFiles("File to update:", "Files to update:", totalOutdatedFiles) - panic("the documentation files are not up-to-date with code files") + return errors.Join(checkErrors...) } -// embedByConfig runs the embedByConfig for all configs and logs the results. -func embedByConfigs(configs []configuration.Configuration) { +// embedByConfigs runs embedding for all configs and logs the results. +func embedByConfigs(configs []configuration.Configuration) error { var totalEmbeddedFiles []string totalEmbeddings := 0 + var embeddingErrors []error for _, config := range configs { - result := cli.EmbedCodeSamples(config) + result, err := cli.EmbedCodeSamples(config) + if err != nil { + embeddingErrors = append(embeddingErrors, err) + continue + } totalEmbeddedFiles = append(totalEmbeddedFiles, result.UpdatedTargetFiles...) totalEmbeddings += result.TotalEmbeddings } + if len(embeddingErrors) > 0 { + return errors.Join(embeddingErrors...) + } if len(totalEmbeddedFiles) == 0 && totalEmbeddings != 0 { fmt.Println("All documentation files are already up to date. Nothing to update.") } printFiles("File updated:", "Files updated:", totalEmbeddedFiles) + + return nil } // printFiles prints file paths with the singular or plural heading. From 4b1a3b94364e7a899735ab49bbf8df14b7689492 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 3 Jun 2026 11:02:26 +0200 Subject: [PATCH 2/3] Reduce `panic` number. --- embedding/embedding_test.go | 57 ++++++++++++++++++++--------- embedding/parsing/context.go | 19 ++++++---- embedding/processor.go | 23 +++++++----- files/files.go | 24 +++++------- fragmentation/encoding.go | 6 +-- fragmentation/fragment.go | 20 ++++++---- fragmentation/fragmentation.go | 24 ++++++------ fragmentation/fragmentation_test.go | 4 +- fragmentation/partition.go | 18 ++++++--- fragmentation/resolver.go | 24 ++++++++---- 10 files changed, 136 insertions(+), 83 deletions(-) diff --git a/embedding/embedding_test.go b/embedding/embedding_test.go index 4957124..25d8aaa 100644 --- a/embedding/embedding_test.go +++ b/embedding/embedding_test.go @@ -69,7 +69,7 @@ var _ = Describe("Embedding", func() { It("should be up to date", func() { docPath := fmt.Sprintf("%s/whole-file-fragment.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) Expect(processor.IsUpToDate()).Should(BeTrue()) @@ -77,7 +77,7 @@ var _ = Describe("Embedding", func() { It("should be up to date as there is nothing to update", func() { docPath := fmt.Sprintf("%s/no-embedding-doc.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) Expect(processor.IsUpToDate()).Should(BeTrue()) @@ -91,13 +91,13 @@ var _ = Describe("Embedding", func() { parsing.RegularLine: {parsing.CodeFenceEnd}, } - falseProcessor := embedding.NewProcessorWithTransitions(docPath, config, falseTransitions) + falseProcessor := newProcessorWithTransitions(docPath, config, falseTransitions) Expect(falseProcessor.Embed()).Error().Should(HaveOccurred()) }) It("should successfully embed with multi lined tag", func() { docPath := fmt.Sprintf("%s/multi-lined-tag.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) Expect(processor.IsUpToDate()).Should(BeTrue()) @@ -105,7 +105,7 @@ var _ = Describe("Embedding", func() { It("should embed directly from source", func() { docPath := fmt.Sprintf("%s/doc.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -124,7 +124,7 @@ var _ = Describe("Embedding", func() { It("should ignore embed-code samples inside markdown code fences", func() { docPath := fmt.Sprintf("%s/embed-code-sample-in-fence.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) Expect(processor.IsUpToDate()).Should(BeTrue()) @@ -132,7 +132,7 @@ var _ = Describe("Embedding", func() { It("should detect markdown fences by triple-or-more backticks only", func() { docPath := fmt.Sprintf("%s/triple-backticks-only-fence.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -182,7 +182,7 @@ var _ = Describe("Embedding", func() { 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) + processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) Expect(processor.IsUpToDate()).Should(BeTrue()) @@ -191,7 +191,7 @@ var _ = Describe("Embedding", func() { It("should embed a method with escaped newline patterns", func() { config.DocIncludes = []string{"escaped-newline-pattern.md"} docPath := fmt.Sprintf("%s/escaped-newline-pattern.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -206,7 +206,7 @@ var _ = Describe("Embedding", func() { It("should embed a method with exact escaped newline patterns", func() { config.DocIncludes = []string{"escaped-newline-exact-pattern.md"} docPath := fmt.Sprintf("%s/escaped-newline-exact-pattern.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -221,7 +221,7 @@ var _ = Describe("Embedding", func() { It("should embed matching lines with an escaped newline line pattern", func() { config.DocIncludes = []string{"escaped-newline-line-pattern.md"} docPath := fmt.Sprintf("%s/escaped-newline-line-pattern.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -236,7 +236,7 @@ var _ = Describe("Embedding", func() { It("should embed a line with an escaped newline literal pattern", func() { config.DocIncludes = []string{"escaped-newline-literal-pattern.md"} docPath := fmt.Sprintf("%s/escaped-newline-literal-pattern.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) Expect(processor.Embed()).Error().ShouldNot(HaveOccurred()) @@ -249,7 +249,7 @@ var _ = Describe("Embedding", func() { It("should report a missing closing tag", func() { docPath := fmt.Sprintf("%s/missing-closing-tag.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) _, err := processor.Embed() @@ -263,7 +263,7 @@ var _ = Describe("Embedding", func() { It("should report the XML parser error", func() { docPath := fmt.Sprintf("%s/unclosed-nested-tag.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) _, err := processor.Embed() @@ -277,7 +277,7 @@ var _ = Describe("Embedding", func() { It("should report a missing code fence after the instruction", func() { docPath := fmt.Sprintf("%s/missing-code-fence.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) _, err := processor.Embed() @@ -290,7 +290,7 @@ var _ = Describe("Embedding", func() { It("should report an unclosed code fence after the instruction", func() { docPath := fmt.Sprintf("%s/unclosed-code-fence.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) _, err := processor.Embed() @@ -306,7 +306,7 @@ var _ = Describe("Embedding", func() { config.DocIncludes = []string{"nested-dir-1/nested-dir-2/nested-dir-doc.md"} docPath := fmt.Sprintf("%s/nested-dir-1/nested-dir-2/nested-dir-doc.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) _, err := embedding.EmbedAll(config) @@ -318,7 +318,7 @@ var _ = Describe("Embedding", func() { config.DocExcludes = []string{"**/excluded-doc.*"} docPath := fmt.Sprintf("%s/excluded-doc.md", config.DocumentationRoot) - processor := embedding.NewProcessor(docPath, config) + processor := newProcessor(docPath, config) context, err := processor.Embed() @@ -338,6 +338,27 @@ func buildConfigWithSourceFiles() configuration.Configuration { return config } +func newProcessor( + docPath string, + config configuration.Configuration, +) embedding.Processor { + processor, err := embedding.NewProcessor(docPath, config) + + Expect(err).ShouldNot(HaveOccurred()) + return processor +} + +func newProcessorWithTransitions( + docPath string, + config configuration.Configuration, + transitions parsing.TransitionMap, +) embedding.Processor { + processor, err := embedding.NewProcessorWithTransitions(docPath, config, transitions) + + Expect(err).ShouldNot(HaveOccurred()) + return processor +} + func copyDirRecursive(sourceDirPath string, targetDirPath string) { info, err := os.Stat(sourceDirPath) if err != nil { diff --git a/embedding/parsing/context.go b/embedding/parsing/context.go index 01fa227..c1ac395 100644 --- a/embedding/parsing/context.go +++ b/embedding/parsing/context.go @@ -88,13 +88,18 @@ type ParsingContext struct { // NewContext Creates and returns a new Context struct with initial values for markdownFile, source, // lineIndex, and result. -func NewContext(markdownFile string) Context { +func NewContext(markdownFile string) (Context, error) { + source, err := readLines(markdownFile) + if err != nil { + return Context{}, err + } + return Context{ MarkdownFilePath: markdownFile, Result: make([]string, 0), - source: readLines(markdownFile), + source: source, lineIndex: 0, - } + }, nil } // NewEmptyContext creates a Context for a documentation file that was not parsed. @@ -231,16 +236,16 @@ func (c *Context) readEmbeddingResult(context ParsingContext) []string { return c.Result[context.resultStartIndex:context.resultEndIndex] } -// Returns the content of a file placed at filepath as a list of strings. -func readLines(filepath string) []string { +// readLines returns the content of a file placed at filepath as a list of strings. +func readLines(filepath string) ([]string, error) { bytes, err := os.ReadFile(filepath) if err != nil { - panic(err) + return nil, err } str := string(bytes) lines := regexp.MustCompile("\r?\n").Split(str, -1) - return lines + return lines, nil } func isStringSlicesEqual(first, second []string) bool { diff --git a/embedding/processor.go b/embedding/processor.go index 4e6ba7d..fd3d40c 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -62,25 +62,25 @@ type EmbedAllResult struct { } // NewProcessor creates and returns new Processor with given docFile and config. -func NewProcessor(docFile string, config configuration.Configuration) Processor { +func NewProcessor(docFile string, config configuration.Configuration) (Processor, error) { requiredDocPaths, err := requiredDocs(config) if err != nil { - panic(err) + return Processor{}, err } - return newProcessor(docFile, config, parsing.Transitions, requiredDocPaths) + return newProcessor(docFile, config, parsing.Transitions, requiredDocPaths), nil } // NewProcessorWithTransitions Creates and returns new Processor with given docFile, config // and transitions. func NewProcessorWithTransitions(docFile string, config configuration.Configuration, - transitions parsing.TransitionMap) Processor { + transitions parsing.TransitionMap) (Processor, error) { requiredDocPaths, err := requiredDocs(config) if err != nil { - panic(err) + return Processor{}, err } - return newProcessor(docFile, config, transitions, requiredDocPaths) + return newProcessor(docFile, config, transitions, requiredDocPaths), nil } // newProcessor creates a Processor with a precomputed documentation file list. @@ -152,7 +152,7 @@ func (p Processor) FindChangedEmbeddings() ([]parsing.Instruction, error) { func (p Processor) IsUpToDate() bool { upToDate, err := p.isUpToDate() if err != nil { - panic(err) + return false } return upToDate @@ -260,7 +260,10 @@ func CheckUpToDate(config configuration.Configuration) ([]string, error) { // // Returns a parsing.Context and an error if any occurs. func (p Processor) fillEmbeddingContext() (parsing.Context, error) { - context := parsing.NewContext(p.DocFilePath) + context, err := parsing.NewContext(p.DocFilePath) + if err != nil { + return context, err + } absDocPath, _ := filepath.Abs(p.DocFilePath) errorStr := "failed to embed code fragment into doc file `file://%s:%d`: %s" @@ -358,7 +361,9 @@ func findChangedFiles(config configuration.Configuration) ([]string, []error) { var changedFiles []string var checkErrors []error for _, doc := range requiredDocPaths { - upToDate, err := newProcessor(doc, config, parsing.Transitions, requiredDocPaths).isUpToDate() + upToDate, err := newProcessor( + doc, config, parsing.Transitions, requiredDocPaths, + ).isUpToDate() if err != nil { checkErrors = append(checkErrors, err) continue diff --git a/files/files.go b/files/files.go index 0464acd..3a6f005 100644 --- a/files/files.go +++ b/files/files.go @@ -36,23 +36,22 @@ const ( ) // WriteLinesToFile writes lines to the file at given file path (relative or absolute). -func WriteLinesToFile(filepath string, lines []string) { +func WriteLinesToFile(filepath string, lines []string) error { file, err := os.Create(filepath) if err != nil { - panic(err) + return err } - defer func(file *os.File) { - if err = file.Close(); err != nil { - panic(err) - } - }(file) for _, s := range lines { _, err := file.WriteString(s + "\n") if err != nil { - panic(err) + _ = file.Close() + + return err } } + + return file.Close() } // ReadFile reads and returns all lines from the file at given file path (relative or absolute). @@ -65,9 +64,6 @@ func ReadFile(filepath string) ([]string, error) { var lines []string defer func(file *os.File) { err = file.Close() - if err != nil { - panic(err) - } }(file) reader := bufio.NewReader(file) @@ -84,13 +80,13 @@ func ReadFile(filepath string) ([]string, error) { } // BuildDocRelativePath builds a relative path for documentation file with a given config. -func BuildDocRelativePath(absolutePath string, config configuration.Configuration) string { +func BuildDocRelativePath(absolutePath string, config configuration.Configuration) (string, error) { relativePath, err := filepath.Rel(config.DocumentationRoot, absolutePath) if err != nil { - panic(err) + return "", err } - return filepath.ToSlash(relativePath) + return filepath.ToSlash(relativePath), nil } // EnsureDirExists creates dir at given path (relative or absolute) if it doesn't exist. diff --git a/fragmentation/encoding.go b/fragmentation/encoding.go index 3b1fc59..9f9d081 100644 --- a/fragmentation/encoding.go +++ b/fragmentation/encoding.go @@ -28,17 +28,17 @@ const lastASCIIchar = 127 // IsEncodedAsText reports whether the file stored at filePath is encoded as a text. // // If file encoded in ASCII or UTF-8, it is meant to be a text file. -func IsEncodedAsText(filePath string) bool { +func IsEncodedAsText(filePath string) (bool, error) { // Read the entire file into memory. content, err := os.ReadFile(filePath) if err != nil { - panic(err) + return false, err } isUTF8Encoded := utf8.Valid(content) isASCIIEncoded := areASCIIEncoded(content) - return isUTF8Encoded || isASCIIEncoded + return isUTF8Encoded || isASCIIEncoded, nil } // Reports whether given bytes are ASCII-encoded. diff --git a/fragmentation/fragment.go b/fragmentation/fragment.go index fa45fbd..3eb6dc6 100644 --- a/fragmentation/fragment.go +++ b/fragmentation/fragment.go @@ -53,11 +53,14 @@ func (f Fragment) isDefault() bool { // lines — a list with every line of the file. // // separator — string to insert between multiple partitions of a single fragment. -func (f Fragment) text(lines []string, separator string) string { +func (f Fragment) text(lines []string, separator string) (string, error) { if f.isDefault() { - return strings.Join(lines, "\n") + return strings.Join(lines, "\n"), nil + } + partitionsTexts, err := f.obtainPartitionTexts(lines) + if err != nil { + return "", err } - partitionsTexts := f.obtainPartitionTexts(lines) var fragmentText []string for _, partition := range partitionsTexts { fragmentText = append(fragmentText, partition...) @@ -76,7 +79,7 @@ func (f Fragment) text(lines []string, separator string) string { text += strings.Join(cutIndentLines, "\n") + "\n" } - return text + return text, nil } // Calculates and returns a list which contains corresponding lines for every partition. @@ -84,14 +87,17 @@ func (f Fragment) text(lines []string, separator string) string { // lines — a list with every line of the file. // // partitions — a list with partitions to select lines from. -func (f Fragment) obtainPartitionTexts(lines []string) [][]string { +func (f Fragment) obtainPartitionTexts(lines []string) ([][]string, error) { var partitionLines [][]string for _, part := range f.Partitions { - partitionText := part.Select(lines) + partitionText, err := part.Select(lines) + if err != nil { + return nil, err + } partitionLines = append(partitionLines, partitionText) } - return partitionLines + return partitionLines, nil } // Returns string indent for separator. diff --git a/fragmentation/fragmentation.go b/fragmentation/fragmentation.go index 1e3ebf3..0e8c8a5 100644 --- a/fragmentation/fragmentation.go +++ b/fragmentation/fragmentation.go @@ -72,25 +72,25 @@ func NewFragmentation( codeFileRelative string, codeRoot _type.NamedPath, config config.Configuration, -) Fragmentation { +) (Fragmentation, error) { fragmentation := Fragmentation{} fragmentation.SourcesRoot = codeRoot _, err := filepath.Abs(codeRoot.Path) if err != nil { - panic(err) + return Fragmentation{}, err } absoluteCodeFile, err := filepath.Abs(codeFileRelative) - fragmentation.CodeFile = absoluteCodeFile if err != nil { - panic(err) + return Fragmentation{}, err } + fragmentation.CodeFile = absoluteCodeFile fragmentation.Configuration = config fragmentation.fragmentBuilders = make(map[string]*FragmentBuilder) - return fragmentation + return fragmentation, nil } // DoFragmentation splits the file into fragments. @@ -102,14 +102,11 @@ func (f Fragmentation) DoFragmentation() ([]string, map[string]Fragment, error) file, err := os.Open(f.CodeFile) if err != nil { - panic(err) + return nil, nil, err } defer func(file *os.File) { err = file.Close() - if err != nil { - panic(err) - } }(file) scanner := bufio.NewScanner(file) @@ -125,6 +122,9 @@ func (f Fragmentation) DoFragmentation() ([]string, map[string]Fragment, error) ) } } + if err = scanner.Err(); err != nil { + return nil, nil, err + } fragments := make(map[string]Fragment) for k, v := range f.fragmentBuilders { @@ -139,16 +139,16 @@ func (f Fragmentation) DoFragmentation() ([]string, map[string]Fragment, error) // - it exists by the given path // - it is a file (not a dir) // - it is textual-encoded. -func shouldDoFragmentation(filePath string) bool { +func shouldDoFragmentation(filePath string) (bool, error) { exists, err := files.IsFileExist(filePath) if err != nil { - return false + return false, err } if exists { return IsEncodedAsText(filePath) } - return false + return false, nil } // Parses a single line of input and performs the following actions: diff --git a/fragmentation/fragmentation_test.go b/fragmentation/fragmentation_test.go index d8bca82..1b28900 100644 --- a/fragmentation/fragmentation_test.go +++ b/fragmentation/fragmentation_test.go @@ -221,8 +221,10 @@ func buildTestFragmentation(testFileName string, config configuration.Configuration) fragmentation.Fragmentation { codeRoot := config.CodeRoots[0] testFilePath := fmt.Sprintf("%s/org/example/%s", codeRoot.Path, testFileName) + frag, err := fragmentation.NewFragmentation(testFilePath, codeRoot, config) - return fragmentation.NewFragmentation(testFilePath, codeRoot, config) + Expect(err).ShouldNot(HaveOccurred()) + return frag } func doTestFragmentation( diff --git a/fragmentation/partition.go b/fragmentation/partition.go index 0b20923..f2285ae 100644 --- a/fragmentation/partition.go +++ b/fragmentation/partition.go @@ -18,6 +18,8 @@ package fragmentation +import "fmt" + // Partition a code fragment partition. // // A fragment may consist of a few partitions, collected from different points in the code file. @@ -44,26 +46,32 @@ func NewPartition() Partition { // Select returns the partition-related lines from given lines. // If EndPosition is not set, returns all the lines started from StartPosition. -func (p Partition) Select(lines []string) []string { +func (p Partition) Select(lines []string) ([]string, error) { startPosition := p.StartPosition endPosition := p.EndPosition // Verifying lines actually have those indexes. hasStartPosition := safeAccess(lines, startPosition) if !hasStartPosition { - panic("an unexpected error occurred. the given lines don't have start position") + return nil, fmt.Errorf( + "fragment partition start position %d is outside source lines", + startPosition, + ) } if endPosition < 0 { - return lines[startPosition:] + return lines[startPosition:], nil } hasEndPosition := safeAccess(lines, endPosition) if !hasEndPosition { - panic("an unexpected error occurred. the given lines don't have end position") + return nil, fmt.Errorf( + "fragment partition end position %d is outside source lines", + endPosition, + ) } - return lines[startPosition : endPosition+1] + return lines[startPosition : endPosition+1], nil } func safeAccess(slice []string, index int) bool { diff --git a/fragmentation/resolver.go b/fragmentation/resolver.go index 4203137..eb39841 100644 --- a/fragmentation/resolver.go +++ b/fragmentation/resolver.go @@ -77,7 +77,7 @@ func ResolveContent(codePath string, fragmentName string, config config.Configur fragmentName, codeFileReference) } - return fragmentLines(fragment, content.lines, config.Separator), nil + return fragmentLines(fragment, content.lines, config.Separator) } // missingFragmentLogMessage describes a missing fragment without exposing internal names. @@ -127,7 +127,11 @@ func resolveSource(codePath string, config config.Configuration) (resolvedPath, if err != nil { return resolvedPath{}, false, err } - if !shouldDoFragmentation(source.absolutePath) { + shouldFragment, err := shouldDoFragmentation(source.absolutePath) + if err != nil { + return resolvedPath{}, false, err + } + if !shouldFragment { continue } @@ -171,7 +175,10 @@ func cachedSourceFragments(source resolvedPath) (fragmentedFile, error) { // loadSourceFragments reads and fragments the source file when it is not already cached. func loadSourceFragments(source resolvedPath) (fragmentedFile, error) { - fragmentation := NewFragmentation(source.absolutePath, source.root, config.Configuration{}) + fragmentation, err := NewFragmentation(source.absolutePath, source.root, config.Configuration{}) + if err != nil { + return fragmentedFile{}, err + } lines, fragments, err := fragmentation.DoFragmentation() if err != nil { return fragmentedFile{}, err @@ -183,13 +190,16 @@ func loadSourceFragments(source resolvedPath) (fragmentedFile, error) { } // fragmentLines renders a fragment into lines. -func fragmentLines(fragment Fragment, lines []string, separator string) []string { - text := fragment.text(lines, separator) +func fragmentLines(fragment Fragment, lines []string, separator string) ([]string, error) { + text, err := fragment.text(lines, separator) + if err != nil { + return nil, err + } if text == "" { - return []string{} + return []string{}, nil } - return strings.Split(strings.TrimSuffix(text, "\n"), "\n") + return strings.Split(strings.TrimSuffix(text, "\n"), "\n"), nil } // unresolvedSourceError builds an error for a code path that cannot be resolved from sources. From 48767966f2783d8062baec841bd6301e8e4b2b72 Mon Sep 17 00:00:00 2001 From: Vladyslav Kuksiuk Date: Wed, 3 Jun 2026 12:20:31 +0200 Subject: [PATCH 3/3] Improve readability. --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 47cb4f2..9a5d082 100644 --- a/main.go +++ b/main.go @@ -152,7 +152,7 @@ func formatError(message string, err error) string { return builder.String() } -// flattenedErrors returns the leaf errors from an error joined with errors.Join. +// flattenedErrors returns the leaf errors from a joined error joined. func flattenedErrors(err error) []error { joined, ok := err.(interface { Unwrap() []error