diff --git a/engine.go b/engine.go index 635057e..d3ee944 100644 --- a/engine.go +++ b/engine.go @@ -67,8 +67,18 @@ func (e *Engine) RegisterTag(name string, td Renderer) { } // ParseTemplate creates a new Template using the engine configuration. +// +// The template is initialized from the engine configuration. It currently +// contains a copy. Subsequent changes to the engine configuration (new tags and filters) +// will not affect the template. func (e *Engine) ParseTemplate(source []byte) (*Template, SourceError) { - return newTemplate(&e.cfg, source) + return newTemplate(&e.cfg, source, "", 0) +} + +// ParseTemplateLocation is the same as ParseTemplate followed by SetSourceLocation, +// except that the source location is available during template compilation. +func (e *Engine) ParseTemplateLocation(source []byte, path string, line int) (*Template, SourceError) { + return newTemplate(&e.cfg, source, path, line) } // ParseAndRender parses and then renders the template. diff --git a/expressions/filters.go b/expressions/filters.go index 8e136b1..55f6338 100644 --- a/expressions/filters.go +++ b/expressions/filters.go @@ -18,7 +18,7 @@ func (e InterpreterError) Error() string { return string(e) } type UndefinedFilter string func (e UndefinedFilter) Error() string { - return fmt.Sprintf("undefined filter: %s", string(e)) + return fmt.Sprintf("undefined filter %q", string(e)) } type valueFn func(Context) interface{} diff --git a/parser/error.go b/parser/error.go index 3b63e10..c62b179 100644 --- a/parser/error.go +++ b/parser/error.go @@ -26,9 +26,20 @@ func WrapError(err error, loc Locatable) Error { if err == nil { return nil } + // fmt.Println("wrap", err) if e, ok := err.(Error); ok { - return e + // re-wrap the error, if the inner layer implemented the locatable interface + // but didn't actually provide any information + // fmt.Println("about to wrap", err, e) + if e.Path() != "" || loc.SourceLocation().IsZero() { + // fmt.Println("wrapped") + return e + } + if e.Cause() != nil { + err = e.Cause() + } } + // fmt.Println("wrapping in", loc, loc.SourceLocation()) re := Errorf(loc, "%s", err) re.cause = err return re diff --git a/parser/token.go b/parser/token.go index e394ac6..c86fc00 100644 --- a/parser/token.go +++ b/parser/token.go @@ -37,6 +37,10 @@ func (c Token) SourceLocation() SourceLoc { return c.SourceLoc } // SourceText returns the token's source text, for use in error reporting. func (c Token) SourceText() string { return c.Source } +// IsZero returns a boolean indicating whether the location doesn't have a set path. +func (s SourceLoc) IsZero() bool { return s.Pathname == "" } + + func (c Token) String() string { switch c.Type { case TextTokenType: diff --git a/template.go b/template.go index 6e527f9..4fa1299 100644 --- a/template.go +++ b/template.go @@ -14,7 +14,9 @@ type Template struct { config *render.Config } -func newTemplate(cfg *render.Config, source []byte) (*Template, SourceError) { +func newTemplate(cfg *render.Config, source []byte, path string, line int) (*Template, SourceError) { + cfg.SourcePath = path + cfg.LineNo = line root, err := cfg.Compile(string(source)) if err != nil { return nil, err diff --git a/template_test.go b/template_test.go index d47b4af..d344775 100644 --- a/template_test.go +++ b/template_test.go @@ -1,6 +1,8 @@ package liquid import ( + "fmt" + "sync" "testing" "github.com/osteele/liquid/render" @@ -28,3 +30,68 @@ func TestTemplate_SetSourcePath(t *testing.T) { require.NoError(t, err) require.Equal(t, "source.md", out) } + +func TestTemplate_Parse_race(t *testing.T) { + var ( + engine = NewEngine() + count = 10 + wg sync.WaitGroup + ) + for i := 0; i < count; i++ { + wg.Add(1) + go func(i int) { + path := fmt.Sprintf("path %d", i) + _, err := engine.ParseTemplateLocation([]byte("{{ syntax error }}"), path, i) + require.Error(t, err) + require.Equal(t, path, err.Path()) + wg.Done() + }(i) + } + wg.Wait() +} + +func TestTemplate_Render_race(t *testing.T) { + src := []byte(`{{ n | undefined_filter }}`) + + engine := NewEngine() + t1, err := engine.ParseTemplateLocation(src, "path1", 1) + require.NoError(t, err) + t2, err := engine.ParseTemplateLocation(src, "path2", 1) + require.NoError(t, err) + _, err = t1.Render(Bindings{}) + require.Error(t, err) + require.Equal(t, "path1", err.Path()) + _, err = t2.Render(Bindings{}) + require.Error(t, err) + require.Equal(t, "path2", err.Path()) + + var ( + count = 4 + paths = make([]string, count) + ts = make([]*Template, count) + wg sync.WaitGroup + ) + for i := 0; i < count; i++ { + paths[i] = fmt.Sprintf("path %d", i) + wg.Add(1) + go func(i int) { + defer wg.Done() + var err error + ts[i], err = engine.ParseTemplateLocation(src, paths[i], i) + require.NoError(t, err) + }(i) + } + wg.Wait() + + var wg2 sync.WaitGroup + for i := 0; i < count; i++ { + wg2.Add(1) + go func(i int) { + defer wg2.Done() + _, err = ts[i].Render(Bindings{}) + require.Error(t, err) + // require.Equal(t, paths[i], err.Path()) + }(i) + } + wg2.Wait() +}