diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c40aa71..5f15d06 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -21,6 +21,12 @@ jobs: with: go-version: '1.22.1' + - name: Run linter + uses: golangci/golangci-lint-action@v8 + with: + version: v2.12.2 + args: ./... + - name: Run Tests # Tests must be run sequentially because they create temporary files that can cause issues. # Therefore, the "-p 1" argument is required. diff --git a/.golangci.yml b/.golangci.yml index 6cd8aef..ea49e15 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,40 +1,41 @@ # This file holds the configs for the linters, and it should be placed in the current working directory. +version: "2" run: - timeout: 5m - tests: true + go: 1.22.1 modules-download-mode: readonly + tests: true allow-parallel-runners: true - go: '1.22.1' linters: enable: # Reference doc with available linters and description for the linters below: https://golangci-lint.run/usage/linters/ # Default - - gosimple - govet - ineffassign - staticcheck - unused + # Bug linters - errorlint - gosec - gosmopolitan - nilerr - testifylint + # Comment linters - godot + # Complexity linters - cyclop - funlen - gocognit - gocyclo - nestif - # Format linters - - gofmt - - goimports + # Meta linters - gocritic - revive - staticcheck + # Style linters - copyloopvar - dogsled @@ -53,19 +54,39 @@ linters: - nilnil - nlreturn - nonamedreturns - - stylecheck - unconvert - unparam - usestdlibvars - varnamelen - wastedassign - whitespace + # Test linters - ginkgolinter - - tenv - testableexamples - testpackage - fast: false + settings: + lll: + line-length: 100 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - revive + path: _test\.go + text: 'dot-imports: should not use dot imports' + - linters: + - goconst + path: _test\.go + paths: + - third_party$ + - builtin$ + - examples$ issues: # Default values allow for the showing of only 50 issues. # Setting it to 0 disables the default config and shows all issues at once. @@ -73,16 +94,14 @@ issues: # Default values allow showing only 3 repeating issues. # Setting it to 0 disables the default config and shows all repeating issues at once. max-same-issues: 0 - exclude-rules: - # As we use external test libs, tests will be hard to read if we have package reference everywhere. - - linters: - - revive - text: "dot-imports: should not use dot imports" - path: _test\.go - # We may have repeating lines in tests and it should be valid. - - linters: - - goconst - path: _test\.go -linters-settings: - lll: - line-length: 100 +formatters: + enable: + # Format linters. + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/cli/cli.go b/cli/cli.go index cd75493..7147ddc 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -202,6 +202,7 @@ func BuildEmbedCodeConfiguration(userArgs Config) []configuration.Configuration )) embedCodeConfigs = append(embedCodeConfigs, configFromEmbedding(embedding)) } + return embedCodeConfigs } diff --git a/cli/cli_test.go b/cli/cli_test.go index 2ec99bc..4eb4ef4 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -137,7 +137,7 @@ var _ = Describe("CLI validation", func() { "`code-path` and `docs-path` cannot be set when `embeddings` are set")) }) - It("should fail validation when embeddings and root optional params are set at the same time", func() { + It("should reject embeddings with root optional params", func() { invalidConfig := cli.Config{ Mode: cli.ModeCheck, DocIncludes: []string{"**/*.md"}, @@ -263,6 +263,7 @@ func baseCliConfig() cli.Config { func baseEmbeddingConfig() cli.EmbeddingConfig { baseConfig := baseCliConfig() + return cli.EmbeddingConfig{ Name: "docs", CodePaths: baseConfig.BaseCodePaths, diff --git a/cli/cli_validation.go b/cli/cli_validation.go index 6220bb0..a9fb828 100644 --- a/cli/cli_validation.go +++ b/cli/cli_validation.go @@ -207,11 +207,13 @@ func findEmbeddingNameDuplications(embeddings []EmbeddingConfig) error { if len(errLines) > 0 { slices.Sort(errLines) + return fmt.Errorf( "duplicate embedding names detected:\n%s", strings.Join(errLines, "\n"), ) } + return nil } @@ -296,6 +298,7 @@ func validatePaths(paths _type.NamedPathList) (bool, error) { allPathsSet = false } } + return allPathsSet, nil } @@ -307,18 +310,18 @@ func validateCodeSources(paths _type.NamedPathList) error { unnamedCount := 0 hasNamed := false - for _, p := range paths { - if isEmpty(p.Path) { + for _, pathEntry := range paths { + if isEmpty(pathEntry.Path) { continue } - if isEmpty(p.Name) { + if isEmpty(pathEntry.Name) { unnamedCount++ } else { hasNamed = true - nameCount[p.Name]++ + nameCount[pathEntry.Name]++ } - pathCount[p.Path]++ - pathNames[p.Path] = append(pathNames[p.Path], p.Name) + pathCount[pathEntry.Path]++ + pathNames[pathEntry.Path] = append(pathNames[pathEntry.Path], pathEntry.Name) } if err := verifyCodeSourceNames(nameCount); err != nil { @@ -347,11 +350,13 @@ func verifyCodeSourceNames(nameCount map[string]int) error { if len(errLines) > 0 { slices.Sort(errLines) + return fmt.Errorf( "duplicate source code path names detected:\n%s", strings.Join(errLines, "\n"), ) } + return nil } diff --git a/embedding/commentfilter/config.go b/embedding/commentfilter/config.go index 913a63d..0bf8151 100644 --- a/embedding/commentfilter/config.go +++ b/embedding/commentfilter/config.go @@ -18,6 +18,13 @@ package commentfilter +const ( + cStyleBlockCommentStart = "/*" + cStyleBlockCommentEnd = "*/" + cStyleDocCommentStart = "/**" + jsQuoteChars = "\"'`" +) + // filtersByExtension is a mapping of the file extension to its comment filter. var filtersByExtension = map[string]filterEntry{ // Java/Kotlin @@ -83,10 +90,10 @@ type filterEntry struct { var javaSyntax = CommentMarker{ Inline: []string{"//"}, Block: []BlockMarker{ - {Start: "/*", End: "*/"}, + {Start: cStyleBlockCommentStart, End: cStyleBlockCommentEnd}, }, Documentation: DocumentationMarker{ - Block: []BlockMarker{{Start: "/**", End: "*/"}}, + Block: []BlockMarker{{Start: cStyleDocCommentStart, End: cStyleBlockCommentEnd}}, }, QuoteChars: "\"'", } @@ -94,30 +101,30 @@ var javaSyntax = CommentMarker{ var jsSyntax = CommentMarker{ Inline: []string{"//"}, Block: []BlockMarker{ - {Start: "/*", End: "*/"}, + {Start: cStyleBlockCommentStart, End: cStyleBlockCommentEnd}, }, Documentation: DocumentationMarker{ - Block: []BlockMarker{{Start: "/**", End: "*/"}}, + Block: []BlockMarker{{Start: cStyleDocCommentStart, End: cStyleBlockCommentEnd}}, }, - QuoteChars: "\"'`", + QuoteChars: jsQuoteChars, } var csharpSyntax = CommentMarker{ Inline: []string{"//"}, Block: []BlockMarker{ - {Start: "/*", End: "*/"}, + {Start: cStyleBlockCommentStart, End: cStyleBlockCommentEnd}, }, Documentation: DocumentationMarker{ Inline: []string{"///"}, - Block: []BlockMarker{{Start: "/**", End: "*/"}}, + Block: []BlockMarker{{Start: cStyleDocCommentStart, End: cStyleBlockCommentEnd}}, }, - QuoteChars: "\"'`", + QuoteChars: jsQuoteChars, } var cStyleSyntax = CommentMarker{ Inline: []string{"//"}, Block: []BlockMarker{ - {Start: "/*", End: "*/"}, + {Start: cStyleBlockCommentStart, End: cStyleBlockCommentEnd}, }, QuoteChars: "\"'", } @@ -125,9 +132,9 @@ var cStyleSyntax = CommentMarker{ var goSyntax = CommentMarker{ Inline: []string{"//"}, Block: []BlockMarker{ - {Start: "/*", End: "*/"}, + {Start: cStyleBlockCommentStart, End: cStyleBlockCommentEnd}, }, - QuoteChars: "\"'`", + QuoteChars: jsQuoteChars, } var hashLineSyntax = CommentMarker{ diff --git a/embedding/commentfilter/filter.go b/embedding/commentfilter/filter.go index 0387be3..63ab5ec 100644 --- a/embedding/commentfilter/filter.go +++ b/embedding/commentfilter/filter.go @@ -60,12 +60,12 @@ func (f EmbeddingCommentFilter) Filter(lines []string, mode Mode) []string { if mode == RetainAll { return lines } - filter, found := filterFor(f.filePath, mode, f.embeddingDocPath, f.embeddingLine) + entry, found := filterFor(f.filePath, mode, f.embeddingDocPath, f.embeddingLine) if !found { return lines } - return filter.Filter(lines, mode) + return entry.filter.Filter(lines, mode) } // filterFor returns the comment filter registered for the given file path and warns on odd modes. @@ -74,18 +74,25 @@ func filterFor( mode Mode, embeddingDocPath string, embeddingLine int, -) (CommentFilter, bool) { +) (filterEntry, bool) { extension := normalizeExtension(filepath.Ext(filePath)) entry, found := filtersByExtension[extension] if !found { warnUnsupportedFileType(filePath, mode, embeddingDocPath, embeddingLine) - return nil, false + + return filterEntry{}, false } - if warnUnsupportedCommentsMode(filePath, mode, embeddingDocPath, embeddingLine, entry.supportedModes) { - return nil, false + if warnUnsupportedCommentsMode( + filePath, + mode, + embeddingDocPath, + embeddingLine, + entry.supportedModes, + ) { + return filterEntry{}, false } - return entry.filter, true + return entry, true } // normalizeExtension returns a lowercase file extension with a leading dot. diff --git a/embedding/commentfilter/filter_test.go b/embedding/commentfilter/filter_test.go index f33c630..c7fd149 100644 --- a/embedding/commentfilter/filter_test.go +++ b/embedding/commentfilter/filter_test.go @@ -16,13 +16,15 @@ // (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 commentfilter +package commentfilter_test import ( "bytes" "log/slog" "testing" + . "embed-code/embed-code-go/embedding/commentfilter" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) diff --git a/embedding/commentfilter/marker_comment_filter.go b/embedding/commentfilter/marker_comment_filter.go index 75a4b3c..a306923 100644 --- a/embedding/commentfilter/marker_comment_filter.go +++ b/embedding/commentfilter/marker_comment_filter.go @@ -105,6 +105,7 @@ func (f *markerLineFilter) filterLine() (string, bool) { if stop { break } + continue } f.consumeCodeByte() @@ -125,6 +126,7 @@ func (f *markerLineFilter) consumeActiveBlock() bool { f.result.WriteString(f.line[f.position:]) } f.position = len(f.line) + return true } endPosition := f.position + end + len(f.state.block.End) @@ -159,6 +161,7 @@ func quotedSegmentEnd(line string, position int, quoteChars string) int { for cursor < len(line) { if line[cursor] == '\\' { cursor += 2 + continue } if line[cursor] == quote { @@ -172,20 +175,24 @@ func quotedSegmentEnd(line string, position int, quoteChars string) int { // consumeComment consumes a comment and reports whether it consumed input and ended the line. func (f *markerLineFilter) consumeComment() (bool, bool) { - if _, found := prefixAt(f.line, f.position, f.filter.Syntax.Documentation.Inline); found { + if prefixAt(f.line, f.position, f.filter.Syntax.Documentation.Inline) { f.consumeInlineComment(f.mode == RetainDocumentation) + return true, true } if block, found := blockAt(f.line, f.position, f.filter.Syntax.Documentation.Block); found { f.startBlockComment(block, f.mode == RetainDocumentation) + return true, false } - if _, found := prefixAt(f.line, f.position, f.filter.Syntax.Inline); found { + if prefixAt(f.line, f.position, f.filter.Syntax.Inline) { f.consumeInlineComment(f.mode == RetainInline || f.mode == RetainRegular) + return true, true } if block, found := blockAt(f.line, f.position, f.filter.Syntax.Block); found { f.startBlockComment(block, f.mode == RetainBlock || f.mode == RetainRegular) + return true, false } @@ -216,14 +223,14 @@ func (f *markerLineFilter) consumeCodeByte() { } // prefixAt reports whether one of the given prefixes starts at the position. -func prefixAt(line string, position int, prefixes []string) (string, bool) { +func prefixAt(line string, position int, prefixes []string) bool { for _, prefix := range prefixes { if strings.HasPrefix(line[position:], prefix) { - return prefix, true + return true } } - return "", false + return false } // blockAt reports whether one of the given block markers starts at the position. diff --git a/embedding/commentfilter/visual_basic.go b/embedding/commentfilter/visual_basic.go index 7e31cc5..e6e7dcf 100644 --- a/embedding/commentfilter/visual_basic.go +++ b/embedding/commentfilter/visual_basic.go @@ -30,7 +30,7 @@ const ( ) // VisualBasicCommentFilter filters the Visual Basic comment forms: -// - documentation comments starting with `'''`; +// - documentation comments starting with three apostrophes; // - apostrophe comments starting with `'`; // - REM comments starting with `REM`. type VisualBasicCommentFilter struct{} @@ -57,18 +57,21 @@ func filterVisualBasicLine(line string, mode Mode) (string, bool) { if quoteEnd := quotedSegmentEnd(line, position, "\""); quoteEnd > position { result.WriteString(line[position:quoteEnd]) position = quoteEnd + continue } if strings.HasPrefix(line[position:], docPrefix) { if mode == RetainDocumentation { result.WriteString(line[position:]) } + return result.String(), true } if line[position] == commentPrefix || remCommentAt(line, position) { if mode == RetainInline || mode == RetainRegular { result.WriteString(line[position:]) } + return result.String(), true } result.WriteByte(line[position]) @@ -87,6 +90,7 @@ func remCommentAt(line string, position int) bool { ) { return false } + return remPrefixBoundary(line, position) && remSuffixBoundary(line, position+len(rem)) } @@ -97,6 +101,7 @@ func remPrefixBoundary(line string, position int) bool { if unicode.IsSpace(rune(line[cursor])) { continue } + return line[cursor] == ':' } diff --git a/embedding/embedding_test.go b/embedding/embedding_test.go index 25d8aaa..601b6d1 100644 --- a/embedding/embedding_test.go +++ b/embedding/embedding_test.go @@ -345,6 +345,7 @@ func newProcessor( processor, err := embedding.NewProcessor(docPath, config) Expect(err).ShouldNot(HaveOccurred()) + return processor } @@ -356,6 +357,7 @@ func newProcessorWithTransitions( processor, err := embedding.NewProcessorWithTransitions(docPath, config, transitions) Expect(err).ShouldNot(HaveOccurred()) + return processor } diff --git a/embedding/parsing/context.go b/embedding/parsing/context.go index c1ac395..722739a 100644 --- a/embedding/parsing/context.go +++ b/embedding/parsing/context.go @@ -58,7 +58,7 @@ type Context struct { // fileContainsEmbedding - a flag indicating whether the file contains an embedding instruction. fileContainsEmbedding bool // embeddings - a list of embedding instructions found in the markdown file. - embeddings []ParsingContext + embeddings []EmbeddingContext } // EmbeddingsCount returns number of found embeddings. @@ -66,7 +66,7 @@ func (c *Context) EmbeddingsCount() int { return len(c.embeddings) } -// ParsingContext contains the information about the position in the source and the +// EmbeddingContext contains the information about the position in the source and the // resulting Markdown files. // // embeddingInstruction - an Instruction, containing all the needed embedding information. @@ -78,7 +78,7 @@ func (c *Context) EmbeddingsCount() int { // resultStartIndex - an index of the StartState line in the result markdown file. // // resultEndIndex - an index of the end line in the result markdown file. -type ParsingContext struct { +type EmbeddingContext struct { embeddingInstruction Instruction SourceStartIndex int SourceEndIndex int @@ -111,16 +111,17 @@ func NewEmptyContext(markdownFile string) Context { } } -// CurrentLine returns the line of source code at the current ParsingContext.lineIndex. +// CurrentLine returns the line of source code at the current Context line index. func (c *Context) CurrentLine() string { return c.source[c.lineIndex] } +// CurrentIndex returns the current one-based source line number. func (c *Context) CurrentIndex() int { return c.lineIndex + 1 } -// ToNextLine increments ParsingContext.lineIndex field by 1. +// ToNextLine advances the parser to the next source line. func (c *Context) ToNextLine() { c.lineIndex++ } @@ -190,7 +191,7 @@ func (c *Context) SetEmbedding(embedding *Instruction) { c.CurrentEmbedding().resultEndIndex = resultIndex } else { c.fileContainsEmbedding = true - context := ParsingContext{ + context := EmbeddingContext{ embeddingInstruction: *embedding, } @@ -216,23 +217,27 @@ func (c *Context) GetResult() []string { // Returns a string representation of Context. func (c *Context) String() string { - return fmt.Sprintf("ParsingContext[embedding=`%s`, file=`%s`, line=`%d`]", + return fmt.Sprintf("Context[embedding=`%s`, file=`%s`, line=`%d`]", c.EmbeddingInstruction, c.MarkdownFilePath, c.lineIndex) } -func (c *Context) CurrentEmbedding() *ParsingContext { +// CurrentEmbedding returns the embedding currently being parsed. +func (c *Context) CurrentEmbedding() *EmbeddingContext { return &c.embeddings[c.currentEmbeddingIndex()] } +// currentEmbeddingIndex returns the index of the latest embedding. func (c *Context) currentEmbeddingIndex() int { return len(c.embeddings) - 1 } -func (c *Context) readEmbeddingSource(context ParsingContext) []string { +// readEmbeddingSource returns original Markdown lines for one embedding. +func (c *Context) readEmbeddingSource(context EmbeddingContext) []string { return c.source[context.SourceStartIndex:context.SourceEndIndex] } -func (c *Context) readEmbeddingResult(context ParsingContext) []string { +// readEmbeddingResult returns generated Markdown lines for one embedding. +func (c *Context) readEmbeddingResult(context EmbeddingContext) []string { return c.Result[context.resultStartIndex:context.resultEndIndex] } diff --git a/embedding/parsing/instruction.go b/embedding/parsing/instruction.go index ba5b7bd..e65df22 100644 --- a/embedding/parsing/instruction.go +++ b/embedding/parsing/instruction.go @@ -116,42 +116,47 @@ func NewInstruction( return Instruction{}, err } - if fragment != "" && (startValue != "" || endValue != "" || lineValue != "") { - return Instruction{}, - fmt.Errorf(" must NOT specify both a fragment name and start/end/line patterns") - } - if lineValue != "" && (startValue != "" || endValue != "") { - return Instruction{}, - fmt.Errorf(" must NOT specify both a line pattern and start/end patterns") - } - var end *Pattern - var line *Pattern - var start *Pattern - - if startValue != "" { - startPattern := NewPattern(startValue) - start = &startPattern - } - if endValue != "" { - endPattern := NewPattern(endValue) - end = &endPattern - } - if lineValue != "" { - linePattern := NewPattern(lineValue) - line = &linePattern + if err = validateExclusiveAttributes(fragment, startValue, endValue, lineValue); err != nil { + return Instruction{}, err } return Instruction{ CodeFile: codeFile, Fragment: fragment, - StartPattern: start, - EndPattern: end, - LinePattern: line, + StartPattern: patternFromValue(startValue), + EndPattern: patternFromValue(endValue), + LinePattern: patternFromValue(lineValue), CommentMode: commentMode, Configuration: config, }, nil } +// validateExclusiveAttributes reports mutually exclusive instruction attributes. +func validateExclusiveAttributes(fragment string, start string, end string, line string) error { + if fragment != "" && (start != "" || end != "" || line != "") { + return fmt.Errorf( + " must NOT specify both a fragment name and start/end/line patterns", + ) + } + if line != "" && (start != "" || end != "") { + return fmt.Errorf( + " must NOT specify both a line pattern and start/end patterns", + ) + } + + return nil +} + +// patternFromValue creates a Pattern pointer for a non-empty attribute value. +func patternFromValue(value string) *Pattern { + if value == "" { + return nil + } + pattern := NewPattern(value) + + return &pattern +} + // Content reads and returns the lines for specified fragment from the code. // // Returns an error if there was an error during reading the content. @@ -303,11 +308,13 @@ func (e Instruction) matchGlob(pattern *Pattern, lines []string, startFrom int, if kind == "end" { return end, nil } + return start, nil } if line, found := matchSingleLine(pattern, lines, startFrom); found { return line, nil } + return 0, PatternNotFoundError{ Line: e.DocumentationLine, CodeFileReference: codeFileReference, diff --git a/embedding/parsing/instruction_test.go b/embedding/parsing/instruction_test.go index fcba144..531dcb6 100644 --- a/embedding/parsing/instruction_test.go +++ b/embedding/parsing/instruction_test.go @@ -162,7 +162,9 @@ var _ = Describe("Instruction", func() { "org/example/Comments.java", instructionParams, config) Expect(actualLines).Should(ContainElement(" // Full-line inline comment.")) - Expect(actualLines).Should(ContainElement(" String create(String name); // end-of-line inline comment.")) + Expect(actualLines).Should(ContainElement( + " String create(String name); // end-of-line inline comment.", + )) Expect(actualLines).ShouldNot(ContainElement("/**")) Expect(actualLines).ShouldNot(ContainElement(" * The block comment.")) }) @@ -193,7 +195,9 @@ var _ = Describe("Instruction", func() { Expect(actualLines).ShouldNot(ContainElement(" * Documents the public API.")) Expect(actualLines).Should(ContainElement(" * The block comment.")) Expect(actualLines).Should(ContainElement(" // Full-line inline comment.")) - Expect(actualLines).Should(ContainElement(" String create(String name); // end-of-line inline comment.")) + Expect(actualLines).Should(ContainElement( + " String create(String name); // end-of-line inline comment.", + )) }) It("should have an error when parsing fragment with start glob", func() { diff --git a/embedding/parsing/instruction_token.go b/embedding/parsing/instruction_token.go index de0f2c3..19c15f6 100644 --- a/embedding/parsing/instruction_token.go +++ b/embedding/parsing/instruction_token.go @@ -135,6 +135,7 @@ func parseFailureReason(instructionBody []string, parseErr error) string { if errors.As(parseErr, &syntaxErr) { return syntaxErr.Msg } + return parseErr.Error() } diff --git a/embedding/parsing/pattern.go b/embedding/parsing/pattern.go index fba743c..c891473 100644 --- a/embedding/parsing/pattern.go +++ b/embedding/parsing/pattern.go @@ -160,31 +160,32 @@ func (p Pattern) linePatterns() ([]string, bool) { var line strings.Builder hasSeparator := false trimLeft := false - for i := 0; i < len(p.sourceGlob); { - remaining := p.sourceGlob[i:] + for cursor := 0; cursor < len(p.sourceGlob); { + remaining := p.sourceGlob[cursor:] switch { case strings.HasPrefix(remaining, escapedLineSeparator): line.WriteString(escapedLineSeparator) - i += len(escapedLineSeparator) + cursor += len(escapedLineSeparator) case strings.HasPrefix(remaining, lineSeparator): patternLines = append(patternLines, strings.TrimRightFunc(line.String(), unicode.IsSpace)) line.Reset() hasSeparator = true trimLeft = true - i += len(lineSeparator) + cursor += len(lineSeparator) case trimLeft: r, size := utf8.DecodeRuneInString(remaining) if !unicode.IsSpace(r) { trimLeft = false - line.WriteByte(p.sourceGlob[i]) - i++ + line.WriteByte(p.sourceGlob[cursor]) + cursor++ + continue } - i += size + cursor += size default: trimLeft = false - line.WriteByte(p.sourceGlob[i]) - i++ + line.WriteByte(p.sourceGlob[cursor]) + cursor++ } } patternLines = append(patternLines, line.String()) diff --git a/embedding/parsing/regular_line.go b/embedding/parsing/regular_line.go index 2a3ec20..193e598 100644 --- a/embedding/parsing/regular_line.go +++ b/embedding/parsing/regular_line.go @@ -24,6 +24,8 @@ import ( "embed-code/embed-code-go/configuration" ) +const minCodeFenceMarkerLength = 3 + // RegularLineState represents a regular line of a markdown. type RegularLineState struct{} @@ -54,13 +56,14 @@ func updateMarkdownFenceContext(context *Context, line string) { return } marker := codeFenceMarker(trimmedLine) - if len(marker) < 3 { + if len(marker) < minCodeFenceMarkerLength { return } if !context.MarkdownFenceStarted { context.MarkdownFenceStarted = true context.MarkdownFenceMarker = marker context.MarkdownFenceIndentation = leadingSpaces + return } if context.MarkdownFenceIndentation != leadingSpaces { diff --git a/embedding/processor.go b/embedding/processor.go index fd3d40c..0a9b90d 100644 --- a/embedding/processor.go +++ b/embedding/processor.go @@ -125,7 +125,10 @@ func (p Processor) Embed() (*parsing.Context, error) { slog.Info(fmt.Sprintf("Updated `%s` after processing %d embedding(s).", logging.FileReference(p.DocFilePath), context.EmbeddingsCount())) } else { - slog.Info(fmt.Sprintf("Documentation is up-to-date in `%s`.", logging.FileReference(p.DocFilePath))) + slog.Info(fmt.Sprintf( + "Documentation is up-to-date in `%s`.", + logging.FileReference(p.DocFilePath), + )) } return &context, nil @@ -163,6 +166,7 @@ func (p Processor) isUpToDate() (bool, error) { if !slices.Contains(p.requiredDocPaths, p.DocFilePath) { slog.Info(fmt.Sprintf("Skipping `%s`; it is excluded by the configuration.", logging.FileReference(p.DocFilePath))) + return true, nil } slog.Info(fmt.Sprintf("Checking `%s`.", logging.FileReference(p.DocFilePath))) @@ -201,6 +205,7 @@ func EmbedAll(config configuration.Configuration) (EmbedAllResult, error) { context, err := processor.Embed() if err != nil { embeddingErrors = append(embeddingErrors, err) + continue } totalEmbeddings += context.EmbeddingsCount() @@ -226,6 +231,7 @@ func EmbedAll(config configuration.Configuration) (EmbedAllResult, error) { logging.FileReference(config.DocumentationRoot), configNameLabel(config)), ) } + return EmbedAllResult{ TargetFiles: requiredDocPaths, TotalEmbeddings: totalEmbeddings, @@ -238,6 +244,7 @@ func configNameLabel(config configuration.Configuration) string { if config.Name == "" { return "" } + return fmt.Sprintf(" for `%s` embedding setup", config.Name) } @@ -366,6 +373,7 @@ func findChangedFiles(config configuration.Configuration) ([]string, []error) { ).isUpToDate() if err != nil { checkErrors = append(checkErrors, err) + continue } if !upToDate { @@ -397,12 +405,14 @@ func requiredDocs(config configuration.Configuration) ([]string, error) { len(includedDocs), logging.FileReference(documentationRoot), patternsLabel(includedPatterns), )) + return includedDocs, nil } result := removeElements(includedDocs, excludedDocs) slog.Info(fmt.Sprintf( - "Found %d documentation file(s) from `%s` matching include pattern(s) %s and exclude pattern(s) %s.", + "Found %d documentation file(s) from `%s` matching include pattern(s) %s "+ + "and exclude pattern(s) %s.", len(result), logging.FileReference(documentationRoot), patternsLabel(includedPatterns), patternsLabel(excludedPatterns), )) diff --git a/files/files.go b/files/files.go index 3a6f005..2410a24 100644 --- a/files/files.go +++ b/files/files.go @@ -20,12 +20,8 @@ package files import ( - "bufio" "fmt" "os" - "path/filepath" - - "embed-code/embed-code-go/configuration" "github.com/bmatcuk/doublestar/v4" ) @@ -35,77 +31,6 @@ const ( WritePermission uint32 = 0600 ) -// WriteLinesToFile writes lines to the file at given file path (relative or absolute). -func WriteLinesToFile(filepath string, lines []string) error { - file, err := os.Create(filepath) - if err != nil { - return err - } - - for _, s := range lines { - _, err := file.WriteString(s + "\n") - if err != nil { - _ = file.Close() - - return err - } - } - - return file.Close() -} - -// ReadFile reads and returns all lines from the file at given file path (relative or absolute). -func ReadFile(filepath string) ([]string, error) { - file, err := os.Open(filepath) - if err != nil { - return nil, err - } - - var lines []string - defer func(file *os.File) { - err = file.Close() - }(file) - - reader := bufio.NewReader(file) - - for { - line, _, err := reader.ReadLine() - if err != nil { - break - } - lines = append(lines, string(line)) - } - - return lines, nil -} - -// BuildDocRelativePath builds a relative path for documentation file with a given config. -func BuildDocRelativePath(absolutePath string, config configuration.Configuration) (string, error) { - relativePath, err := filepath.Rel(config.DocumentationRoot, absolutePath) - if err != nil { - return "", err - } - - return filepath.ToSlash(relativePath), nil -} - -// EnsureDirExists creates dir at given path (relative or absolute) if it doesn't exist. -// Does nothing if exists. -func EnsureDirExists(path string) error { - exist, err := IsDirExist(path) - if err != nil { - return err - } - if !exist { - err = os.MkdirAll(path, os.FileMode(ReadWriteExecPermission)) - if err != nil { - return err - } - } - - return nil -} - // IsFileExist reports whether the given path (relative or absolute) to a file exists in the // file system. func IsFileExist(filePath string) (bool, error) { @@ -147,15 +72,14 @@ func IsDirExist(path string) (bool, error) { func validatePathExists(path string) (bool, os.FileInfo, error) { // Getting matches for the given path if it is a glob format. Otherwise, does nothing. matches, err := doublestar.FilepathGlob(path) + if err != nil { + return false, nil, err + } if len(matches) == 0 { return false, nil, nil } - if err != nil { - return false, nil, err - } - firstMatch := matches[0] info, err := os.Stat(firstMatch) diff --git a/fragmentation/cache.go b/fragmentation/cache.go index b986dc5..bc5a3fe 100644 --- a/fragmentation/cache.go +++ b/fragmentation/cache.go @@ -45,6 +45,8 @@ func newCache[K comparable, V any](limit int, loader func(K) (V, error)) *cache[ } // get returns a cached value or loads it when missing. +// +//nolint:ireturn // The cache is generic, so returning V preserves the stored value type. func (c *cache[K, V]) get(key K) (V, error) { c.Lock() value, found := c.values[key] @@ -83,6 +85,7 @@ 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) + return } @@ -108,7 +111,12 @@ func (c *cache[K, V]) evictOldest() { return } - oldestKey := oldestEntry.Value.(K) + oldestKey, isKey := oldestEntry.Value.(K) + if !isKey { + c.order.Remove(oldestEntry) + + return + } c.order.Remove(oldestEntry) delete(c.entries, oldestKey) delete(c.values, oldestKey) diff --git a/fragmentation/fragmentation.go b/fragmentation/fragmentation.go index 0e8c8a5..b9627b2 100644 --- a/fragmentation/fragmentation.go +++ b/fragmentation/fragmentation.go @@ -117,7 +117,7 @@ func (f Fragmentation) DoFragmentation() ([]string, map[string]Fragment, error) contentToRender, err = f.parseLine(line, contentToRender) if err != nil { return nil, nil, fmt.Errorf( - "failed to do fragmentation on file `file://%s:%d`: %s", + "failed to do fragmentation on file `file://%s:%d`: %w", f.CodeFile, lineNumber, err, ) } diff --git a/fragmentation/fragmentation_test.go b/fragmentation/fragmentation_test.go index 1b28900..742aaa4 100644 --- a/fragmentation/fragmentation_test.go +++ b/fragmentation/fragmentation_test.go @@ -224,6 +224,7 @@ func buildTestFragmentation(testFileName string, frag, err := fragmentation.NewFragmentation(testFilePath, codeRoot, config) Expect(err).ShouldNot(HaveOccurred()) + return frag } @@ -236,6 +237,7 @@ func doTestFragmentation( lines, fragments, err := frag.DoFragmentation() Expect(err).ShouldNot(HaveOccurred()) + return lines, fragments } @@ -251,5 +253,6 @@ func resolveTestFragment( ) Expect(err).ShouldNot(HaveOccurred()) + return content } diff --git a/fragmentation/lookup.go b/fragmentation/lookup.go index bc508d7..8a9cba6 100644 --- a/fragmentation/lookup.go +++ b/fragmentation/lookup.go @@ -25,6 +25,8 @@ import ( "strings" ) +var quotedNamePattern = regexp.MustCompile("\"(.*)\"") + const ( FragmentStart = "#docfragment" FragmentEnd = "#enddocfragment" @@ -89,14 +91,10 @@ func lookup(line string, prefix string) ([]string, error) { // Returns the unquoted name from given quotedName. func unquoteName(quotedName string) (string, error) { - r, compilationErr := regexp.Compile("\"(.*)\"") - if compilationErr != nil { - return "", fmt.Errorf("failed to unquote name `%s`: %s", quotedName, compilationErr) - } - nameQuoted := r.FindString(quotedName) + nameQuoted := quotedNamePattern.FindString(quotedName) nameCleaned, err := strconv.Unquote(nameQuoted) if err != nil { - return "", fmt.Errorf("failed to unquote name `%s`: %s", quotedName, err) + return "", fmt.Errorf("failed to unquote name `%s`: %w", quotedName, err) } return nameCleaned, nil diff --git a/fragmentation/resolver.go b/fragmentation/resolver.go index eb39841..a6dff15 100644 --- a/fragmentation/resolver.go +++ b/fragmentation/resolver.go @@ -47,7 +47,11 @@ var resolverCache = newCache[resolvedPath, fragmentedFile]( // 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) { +func ResolveContent( + codePath string, + fragmentName string, + config config.Configuration, +) ([]string, error) { if fragmentName == "" { fragmentName = DefaultFragmentName } @@ -61,6 +65,7 @@ func ResolveContent(codePath string, fragmentName string, config config.Configur "Could not find source file `%s` in the configured source code folders.", codePath, )) + return nil, unresolvedSourceError(codePath, fragmentName, config) } @@ -73,6 +78,7 @@ func ResolveContent(codePath string, fragmentName string, config config.Configur if !found { codeFileReference := logging.FileReference(source.absolutePath) slog.Info(missingFragmentLogMessage(fragmentName, source.absolutePath)) + return nil, fmt.Errorf("fragment `%s` from code file `%s` not found", fragmentName, codeFileReference) } @@ -183,6 +189,7 @@ func loadSourceFragments(source resolvedPath) (fragmentedFile, error) { if err != nil { return fragmentedFile{}, err } + return fragmentedFile{ lines: lines, fragments: fragments, @@ -203,7 +210,11 @@ func fragmentLines(fragment Fragment, lines []string, separator string) ([]strin } // unresolvedSourceError builds an error for a code path that cannot be resolved from sources. -func unresolvedSourceError(codePath string, fragmentName string, config config.Configuration) error { +func unresolvedSourceError( + codePath string, + fragmentName string, + config config.Configuration, +) error { codeFileReference, err := codeFileReference(codePath, config) if err != nil { return err diff --git a/logging/logger.go b/logging/logger.go index 59f4226..d9e7338 100644 --- a/logging/logger.go +++ b/logging/logger.go @@ -20,7 +20,6 @@ package logging import ( "fmt" - "golang.org/x/net/context" "log/slog" "net/url" "os" @@ -28,8 +27,12 @@ import ( "runtime/debug" "strconv" "strings" + + "golang.org/x/net/context" ) +const fileScheme = "file" + // Handler is a custom slog.Handler that formats log records for simple console output. // // It displays each log message in the format: @@ -70,8 +73,10 @@ func (h *Handler) Handle(_ context.Context, record slog.Record) error { record.Attrs(func(attr slog.Attr) bool { fmt.Printf(" %s%s=%v\n", prefix, attr.Key, attr.Value) + return true }) + return nil } @@ -79,6 +84,7 @@ func (h *Handler) Handle(_ context.Context, record slog.Record) error { func (h *Handler) WithAttrs(attributes []slog.Attr) slog.Handler { newHandler := *h newHandler.attributes = append(append([]slog.Attr{}, h.attributes...), attributes...) + return &newHandler } @@ -86,6 +92,7 @@ func (h *Handler) WithAttrs(attributes []slog.Attr) slog.Handler { func (h *Handler) WithGroup(name string) slog.Handler { newHandler := *h newHandler.groups = append(append([]string{}, h.groups...), name) + return &newHandler } @@ -114,7 +121,7 @@ func fileURLFromAbsolutePath(path string) string { normalizedPath := filepath.ToSlash(strings.ReplaceAll(path, "\\", "/")) if isWindowsDrivePath(normalizedPath) { return (&url.URL{ - Scheme: "file", + Scheme: fileScheme, Path: "/" + normalizedPath, }).String() } @@ -123,14 +130,14 @@ func fileURLFromAbsolutePath(path string) string { host, pathAfterHost, _ := strings.Cut(withoutSlashes, "/") return (&url.URL{ - Scheme: "file", + Scheme: fileScheme, Host: host, Path: "/" + pathAfterHost, }).String() } return (&url.URL{ - Scheme: "file", + Scheme: fileScheme, Path: normalizedPath, }).String() } @@ -142,6 +149,7 @@ func isWindowsDrivePath(path string) bool { } driveLetter := path[0] + return (driveLetter >= 'A' && driveLetter <= 'Z') || (driveLetter >= 'a' && driveLetter <= 'z') } @@ -164,15 +172,15 @@ func HandlePanic(withStacktrace bool) { // formatPanicMessage formats panic values for console output. func formatPanicMessage(recovered any) string { - err, ok := recovered.(error) - if !ok { + err, isError := recovered.(error) + if !isError { return fmt.Sprintf("panic: %v", recovered) } - joined, ok := err.(interface { + joined, isJoined := err.(interface { Unwrap() []error }) - if !ok || len(joined.Unwrap()) <= 1 { + if !isJoined || len(joined.Unwrap()) <= 1 { return fmt.Sprintf("panic: %v", err) } diff --git a/logging/logger_test.go b/logging/logger_test.go index 3cd0b97..e275fce 100644 --- a/logging/logger_test.go +++ b/logging/logger_test.go @@ -16,7 +16,7 @@ // (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 logging +package logging //nolint:testpackage // Tests OS-specific normalization in an unexported helper. import "testing" diff --git a/main.go b/main.go index 9a5d082..94547d7 100644 --- a/main.go +++ b/main.go @@ -80,7 +80,7 @@ const Version = "1.2.2" // - separator — a string which is used as a separator between code fragments. Default value // is "...". func main() { - fmt.Println(fmt.Sprintf("Running embed-code v%s.", Version)) + fmt.Printf("Running embed-code v%s.\n", Version) userArgs := cli.ReadArgs() configureLogging(userArgs) defer logging.HandlePanic(userArgs.Stacktrace) @@ -183,6 +183,7 @@ func checkByConfigs(configs []configuration.Configuration) error { outdatedFiles, err := cli.CheckCodeSamples(config) if err != nil { checkErrors = append(checkErrors, err) + continue } totalOutdatedFiles = append(totalOutdatedFiles, outdatedFiles...) @@ -210,6 +211,7 @@ func embedByConfigs(configs []configuration.Configuration) error { result, err := cli.EmbedCodeSamples(config) if err != nil { embeddingErrors = append(embeddingErrors, err) + continue } totalEmbeddedFiles = append(totalEmbeddedFiles, result.UpdatedTargetFiles...) diff --git a/type/named_path_list.go b/type/named_path_list.go index b3380e7..474d10d 100644 --- a/type/named_path_list.go +++ b/type/named_path_list.go @@ -20,8 +20,9 @@ package _type import ( "fmt" - "gopkg.in/yaml.v3" "strings" + + "gopkg.in/yaml.v3" ) // NamedPath represents a path that may optionally have a name. @@ -56,11 +57,11 @@ type NamedPathList []NamedPath // path: "../runtime" func (pathList *NamedPathList) UnmarshalYAML(value *yaml.Node) error { switch value.Kind { - case yaml.ScalarNode: *pathList = []NamedPath{ {Path: strings.TrimSpace(value.Value)}, } + return nil case yaml.SequenceNode: @@ -68,7 +69,6 @@ func (pathList *NamedPathList) UnmarshalYAML(value *yaml.Node) error { for _, node := range value.Content { switch node.Kind { - case yaml.ScalarNode: result = append(result, NamedPath{ Path: strings.TrimSpace(node.Value), @@ -87,6 +87,7 @@ func (pathList *NamedPathList) UnmarshalYAML(value *yaml.Node) error { } *pathList = result + return nil default: return fmt.Errorf("invalid format for named paths") diff --git a/type/string_list.go b/type/string_list.go index d626d05..d8e1bba 100644 --- a/type/string_list.go +++ b/type/string_list.go @@ -20,8 +20,9 @@ package _type import ( "fmt" - "gopkg.in/yaml.v3" "strings" + + "gopkg.in/yaml.v3" ) // StringList is a list of strings. @@ -43,7 +44,6 @@ type StringList []string // - c func (s *StringList) UnmarshalYAML(value *yaml.Node) error { switch value.Kind { - case yaml.ScalarNode: parts := strings.Split(value.Value, ",") res := make([]string, 0, len(parts)) @@ -53,6 +53,7 @@ func (s *StringList) UnmarshalYAML(value *yaml.Node) error { } } *s = res + return nil case yaml.SequenceNode: @@ -61,6 +62,7 @@ func (s *StringList) UnmarshalYAML(value *yaml.Node) error { res = append(res, strings.TrimSpace(n.Value)) } *s = res + return nil default: return fmt.Errorf("invalid format for string list")