Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions embedding/embedding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,35 @@ 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)
}()

Expect(recovered).ShouldNot(BeNil())
Expect(fmt.Sprint(recovered)).Should(And(
ContainSubstring("missing-start-pattern.md:3"),
ContainSubstring(
"no line in code file `file://",
),
ContainSubstring(
"` matches the start pattern "+
"`*doesNotExistStart*`",
),
ContainSubstring("missing-end-pattern.md:3"),
ContainSubstring(
"` matches the end pattern "+
"`*doesNotExistEnd*`",
),
))
})

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)
Expand Down Expand Up @@ -201,6 +230,32 @@ 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)

_, err := processor.Embed()

Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring(
"missing-code-fence.md:3`: " +
"expected a markdown code fence after the embedding instruction",
))
})

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)

_, err := processor.Embed()

Expect(err).Should(HaveOccurred())
Expect(err.Error()).Should(ContainSubstring(
"unclosed-code-fence.md:3`: " +
"the markdown code fence after the embedding instruction is not closed",
))
})

// TODO:olena-zmiiova:https://github.com/SpineEventEngine/embed-code/issues/65
It("should successfully embed to a file in a nested dir", func() {
Skip(
Expand Down
3 changes: 1 addition & 2 deletions embedding/parsing/blank_line.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ type BlankLineState struct{}
// Checks if the current line is empty and not part of a code fence, and if there is an embedding.
// If these conditions are met, it returns true. Otherwise, it returns false.
func (b BlankLineState) Recognize(context Context) bool {
isEmptyString := strings.TrimSpace(context.CurrentLine()) == ""
if !context.ReachedEOF() && isEmptyString {
if !context.ReachedEOF() && strings.TrimSpace(context.CurrentLine()) == "" {
return !context.CodeFenceStarted && context.EmbeddingInstruction != nil
}

Expand Down
2 changes: 1 addition & 1 deletion embedding/parsing/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func (c *Context) ResolveUnacceptedEmbedding() {
currentEmbeddingInstruction := c.CurrentEmbedding().embeddingInstruction
c.UnacceptedEmbeddings = append(c.UnacceptedEmbeddings, currentEmbeddingInstruction)
c.embeddings = c.embeddings[:c.currentEmbeddingIndex()]
c.SetEmbedding(nil)
c.EmbeddingInstruction = nil
}

// SetEmbedding sets an embedding to Context. Also sets fileContainsEmbedding flag.
Expand Down
64 changes: 56 additions & 8 deletions embedding/parsing/instruction.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,29 @@ type Instruction struct {
Configuration configuration.Configuration
}

// PatternNotFoundError reports that a start or end pattern did not match the code file.
type PatternNotFoundError struct {
Line int
CodeFileReference string
Kind string
Pattern *Pattern
}

// Error returns a user-facing description of an unmatched start or end pattern.
func (e PatternNotFoundError) Error() string {
pattern := ""
if e.Pattern != nil {
pattern = e.Pattern.sourceGlob
}

return fmt.Sprintf(
"no line in code file `%s` matches the %s pattern `%s`",
e.CodeFileReference,
e.Kind,
pattern,
)
}

// NewInstruction creates an Instruction based on provided attributes and configuration.
//
// attributes — a map with string-typed both keys and values. Possible keys are:
Expand Down Expand Up @@ -121,7 +144,14 @@ func (e Instruction) Content() ([]string, error) {
return nil, err
}
if e.StartPattern != nil || e.EndPattern != nil {
fileContent = e.matchingLines(fileContent)
codeFileReference, err := fragmentation.ResolveCodeFileReference(e.CodeFile, e.Configuration)
if err != nil {
return nil, err
}
fileContent, err = e.matchingLines(fileContent, codeFileReference)
if err != nil {
return nil, err
}
}

return commentfilter.Filter(
Expand All @@ -144,19 +174,31 @@ func (e Instruction) String() string {
// Filters and returns a subset of input lines based on start and end patterns.
//
// lines — a list of strings representing the input lines.
func (e Instruction) matchingLines(lines []string) []string {
func (e Instruction) matchingLines(lines []string, codeFileReference string) ([]string, error) {
startPosition := 0
if e.StartPattern != nil {
startPosition = e.matchGlob(e.StartPattern, lines, 0)
var err error
startPosition, err = e.matchGlob(
e.StartPattern, lines, 0, "start", codeFileReference,
)
if err != nil {
return nil, err
}
}
endPosition := len(lines) - 1
if e.EndPattern != nil {
endPosition = e.matchGlob(e.EndPattern, lines, startPosition)
var err error
endPosition, err = e.matchGlob(
e.EndPattern, lines, startPosition, "end", codeFileReference,
)
if err != nil {
return nil, err
}
}
requiredLines := lines[startPosition : endPosition+1]
indentation := indent.MaxCommonIndentation(requiredLines)

return indent.CutIndent(requiredLines, indentation)
return indent.CutIndent(requiredLines, indentation), nil
}

// Returns the index of a first line that matches given pattern.
Expand All @@ -166,15 +208,21 @@ func (e Instruction) matchingLines(lines []string) []string {
// lines — a list of lines to search in.
//
// startFrom — an index from which to start searching.
func (e Instruction) matchGlob(pattern *Pattern, lines []string, startFrom int) int {
func (e Instruction) matchGlob(pattern *Pattern, lines []string, startFrom int,
kind string, codeFileReference string) (int, error) {
lineCount := len(lines)
resultLine := startFrom
for resultLine < lineCount {
line := lines[resultLine]
if pattern.Match(line) {
return resultLine
return resultLine, nil
}
resultLine++
}
panic(fmt.Sprintf("there is no line matching `%s`", pattern))
return 0, PatternNotFoundError{
Line: e.DocumentationLine,
CodeFileReference: codeFileReference,
Kind: kind,
Pattern: pattern,
}
}
42 changes: 28 additions & 14 deletions embedding/parsing/instruction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
_type "embed-code/embed-code-go/type"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -343,36 +344,40 @@ var _ = Describe("Instruction", func() {
Expect(actualLines[5]).Should(Equal(expectedLastLine))
})

It("should panic when start glob does not match", func() {
It("should report an error when start glob does not match", func() {
instructionParams := TestInstructionParams{
startGlob: "foo bar",
endGlob: "*main*",
}
xmlString := buildInstruction("org/example/Hello.java", instructionParams)
instruction := createInstructionFromXML(xmlString, config)

Expect(func() {
_, err := instruction.Content()
if err != nil {
return
}
}).To(Panic())
_, err := instruction.Content()

Expect(err).Should(MatchError(
fmt.Sprintf(
"no line in code file `file://%s` matches the start pattern `foo bar`",
absTestCodeFile("org/example/Hello.java"),
),
))
})

It("should panic when end glob does not match", func() {
It("should report an error when end glob does not match", func() {
instructionParams := TestInstructionParams{
startGlob: "*main*",
endGlob: "foo bar",
}
xmlString := buildInstruction("org/example/Hello.java", instructionParams)
instruction := createInstructionFromXML(xmlString, config)

Expect(func() {
_, err := instruction.Content()
if err != nil {
return
}
}).To(Panic())
_, err := instruction.Content()

Expect(err).Should(MatchError(
fmt.Sprintf(
"no line in code file `file://%s` matches the end pattern `foo bar`",
absTestCodeFile("org/example/Hello.java"),
),
))
})
})

Expand Down Expand Up @@ -436,6 +441,15 @@ func readInstructionContent(instruction parsing.Instruction) []string {
return lines
}

func absTestCodeFile(path string) string {
absolutePath, err := filepath.Abs(filepath.Join("../../test/resources/code/java", path))
if err != nil {
Fail("unexpected error while resolving test code file: " + err.Error())
}

return absolutePath
}

func xmlAttribute(name string, value string) string {
return fmt.Sprintf("%s=\"%v\"", name, value)
}
20 changes: 20 additions & 0 deletions embedding/parsing/instruction_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ type InstructionParseError struct {
Reason string
}

// MissingCodeFenceError reports that an embedding instruction is not followed by a code fence.
type MissingCodeFenceError struct {
Line int
}

// UnclosedCodeFenceError reports that an embedding code fence is not closed.
type UnclosedCodeFenceError struct {
Line int
}

// Error returns a user-facing description of an embedding instruction parse failure.
func (e InstructionParseError) Error() string {
return fmt.Sprintf(
Expand All @@ -44,6 +54,16 @@ func (e InstructionParseError) Error() string {
)
}

// Error returns a user-facing description of a missing code fence after an instruction.
func (e MissingCodeFenceError) Error() string {
return "expected a markdown code fence after the embedding instruction"
}

// Error returns a user-facing description of an unclosed embedding code fence.
func (e UnclosedCodeFenceError) Error() string {
return "the markdown code fence after the embedding instruction is not closed"
}

// Recognize reports whether the current line in the parsing context starts with "<embed-code",
// and if there is no ongoing embedding and the end of the file is not reached, it returns true.
// Otherwise, it returns false.
Expand Down
33 changes: 32 additions & 1 deletion embedding/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,11 @@ func (p Processor) fillEmbeddingContext() (parsing.Context, error) {
return context, fmt.Errorf(errorStr, absDocPath, errorLine(context, err), err)
}
if !accepted {
err = unacceptedTransitionError(context)
currentState = &parsing.RegularLineState{}
context.ResolveUnacceptedEmbedding()
if context.EmbeddingInstruction != nil {
context.ResolveUnacceptedEmbedding()
}

return context, fmt.Errorf(errorStr, absDocPath, errorLine(context, err), err)
}
Expand All @@ -249,13 +252,41 @@ func errorLine(context parsing.Context, err error) int {
if errors.As(err, &parseErr) {
return parseErr.Line
}
var missingFenceErr parsing.MissingCodeFenceError
if errors.As(err, &missingFenceErr) {
return missingFenceErr.Line
}
var unclosedFenceErr parsing.UnclosedCodeFenceError
if errors.As(err, &unclosedFenceErr) {
return unclosedFenceErr.Line
}
var patternErr parsing.PatternNotFoundError
if errors.As(err, &patternErr) {
return patternErr.Line
}
if context.EmbeddingsCount() > 0 {
return context.CurrentEmbedding().SourceStartIndex - 1
}

return context.CurrentIndex()
}

// unacceptedTransitionError explains why the parser could not accept the current state.
func unacceptedTransitionError(context parsing.Context) error {
if context.EmbeddingInstruction != nil && context.CodeFenceStarted {
return parsing.UnclosedCodeFenceError{
Line: context.EmbeddingInstruction.DocumentationLine,
}
}
if context.EmbeddingInstruction != nil && !context.CodeFenceStarted {
return parsing.MissingCodeFenceError{
Line: context.EmbeddingInstruction.DocumentationLine,
}
}

return fmt.Errorf("unexpected parser state at line %d", context.CurrentIndex())
}

// Moves to the next state accordingly to a transition map from the current state. Reports whether
// it successfully moved to the next state and returns the new state.
func (p Processor) moveToNextState(state *parsing.State, context *parsing.Context) (
Expand Down
13 changes: 13 additions & 0 deletions fragmentation/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,19 @@ func ResolveContent(codePath string, fragmentName string, config config.Configur
return fragmentLines(fragment, content.lines, config.Separator), nil
}

// ResolveCodeFileReference returns a user-facing reference to the source file.
func ResolveCodeFileReference(codePath string, config config.Configuration) (string, error) {
source, found, err := resolveSource(codePath, config)
if err != nil {
return "", err
}
if found {
return "file://" + source.absolutePath, nil
}

return codeFileReference(codePath, config)
}

// ClearResolverCache removes cached source fragmentations.
func ClearResolverCache() {
resolverCache.clear()
Expand Down
4 changes: 4 additions & 0 deletions test/resources/docs/missing-code-fence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Example without a code fence

<embed-code file="org/example/Hello.java" fragment="main()"/>
public static void main(String[] args) {
5 changes: 5 additions & 0 deletions test/resources/docs/missing-end-pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Example with a missing end pattern

<embed-code file="org/example/Hello.java" start="*main*" end="*doesNotExistEnd*"/>
```java
```
5 changes: 5 additions & 0 deletions test/resources/docs/missing-start-pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Example with a missing start pattern

<embed-code file="org/example/Hello.java" start="*doesNotExistStart*" end="*main*"/>
```java
```
Loading
Loading