diff --git a/build.go b/build.go index a93c8eb..ba62ef7 100644 --- a/build.go +++ b/build.go @@ -698,7 +698,8 @@ func (b *builder) processNode(root node, flags flag, props *builderProp) (q quer } // build builds a specified XPath expressions expr. -func build(expr string, namespaces map[string]string) (q query, err error) { +// build returns the query and the parser used for parsing. +func build(expr string, namespaces map[string]string) (q query, p *parser, err error) { defer func() { if e := recover(); e != nil { switch x := e.(type) { @@ -711,8 +712,9 @@ func build(expr string, namespaces map[string]string) (q query, err error) { } } }() - root := parse(expr, namespaces) + root, p := parse(expr, namespaces) b := &builder{} props := builderProps.None - return b.processNode(root, flagsEnum.None, &props) + q, err = b.processNode(root, flagsEnum.None, &props) + return q, p, err } diff --git a/parse.go b/parse.go index 5393125..2ca8123 100644 --- a/parse.go +++ b/parse.go @@ -564,12 +564,14 @@ func (p *parser) parseMethod(n node) node { } // Parse parsing the XPath express string expr and returns a tree node. -func parse(expr string, namespaces map[string]string) node { +// parse returns the root node and the parser used for parsing. +func parse(expr string, namespaces map[string]string) (node, *parser) { r := &scanner{text: expr} r.nextChar() r.nextItem() p := &parser{r: r, namespaces: namespaces} - return p.parseExpression(nil) + root := p.parseExpression(nil) + return root, p } // rootNode holds a top-level node of tree. diff --git a/xpath.go b/xpath.go index 04bbe8d..2544a12 100644 --- a/xpath.go +++ b/xpath.go @@ -5,6 +5,17 @@ import ( "fmt" ) +// CompileOptions allows customizing the behavior of the XPath parser. +type CompileOptions struct { + StrictEOF bool // If true, require full input consumption (no trailing tokens) + // Future strictness options can be added here +} + +// StrictPreset enables all strictness options (update as new options are added) +var StrictPreset = CompileOptions{ + StrictEOF: true, +} + // NodeType represents a type of XPath node. type NodeType int @@ -138,17 +149,7 @@ func (expr *Expr) String() string { // Compile compiles an XPath expression string. func Compile(expr string) (*Expr, error) { - if expr == "" { - return nil, errors.New("expr expression is nil") - } - qy, err := build(expr, nil) - if err != nil { - return nil, err - } - if qy == nil { - return nil, fmt.Errorf(fmt.Sprintf("undeclared variable in XPath expression: %s", expr)) - } - return &Expr{s: expr, q: qy}, nil + return CompileWithOptionsAndNS(expr, CompileOptions{}, nil) } // MustCompile compiles an XPath expression string and ignored error. @@ -162,15 +163,27 @@ func MustCompile(expr string) *Expr { // CompileWithNS compiles an XPath expression string, using given namespaces map. func CompileWithNS(expr string, namespaces map[string]string) (*Expr, error) { + return CompileWithOptionsAndNS(expr, CompileOptions{}, namespaces) +} + +// CompileWithOptions compiles an XPath expression string with the given options. +func CompileWithOptions(expr string, opts CompileOptions) (*Expr, error) { + return CompileWithOptionsAndNS(expr, opts, nil) +} + +func CompileWithOptionsAndNS(expr string, opts CompileOptions, namespaces map[string]string) (*Expr, error) { if expr == "" { return nil, errors.New("expr expression is nil") } - qy, err := build(expr, namespaces) + q, p, err := build(expr, namespaces) if err != nil { return nil, err } - if qy == nil { - return nil, fmt.Errorf(fmt.Sprintf("undeclared variable in XPath expression: %s", expr)) + if opts.StrictEOF && p != nil && p.r.typ != itemEOF { + return nil, fmt.Errorf("unexpected token after end of expression: %s", p.r.text[p.r.pos-p.r.currSize-1:]) + } + if q == nil { + return nil, fmt.Errorf("undeclared variable in XPath expression: %s", expr) } - return &Expr{s: expr, q: qy}, nil + return &Expr{s: expr, q: q}, nil } diff --git a/xpath_test.go b/xpath_test.go index 7379f70..1af91dd 100644 --- a/xpath_test.go +++ b/xpath_test.go @@ -91,26 +91,26 @@ func test_xpath_eval(t *testing.T, root *TNode, expr string, expected ...interfa func Test_Predicates_MultiParent(t *testing.T) { // https://github.com/antchfx/xpath/issues/75 /* - - - - field1 - field2 - - 31854 - 159773 - - - - field3 - field4 - - 1234 - 567 - - - - + + + + field1 + field2 + + 31854 + 159773 + + + + field3 + field4 + + 1234 + 567 + + + + */ doc := createNode("", RootNode) measCollecFile := doc.createChildNode("measCollecFile", ElementNode) @@ -281,6 +281,88 @@ func TestNodeType(t *testing.T) { assertEqual(t, CommentNode, n.Type) } +func TestCompileWithOptions_StrictEOF(t *testing.T) { + doc := createBookExample() + + testCases := []struct { + name string + expr string + options CompileOptions + wantErr bool + wantLen int // -1 if not applicable + errMsg string // expected error message (substring match) + }{ + { + name: "StrictEOF: valid expression", + expr: "//book", + options: CompileOptions{StrictEOF: true}, + wantErr: false, + wantLen: 4, + }, + { + name: "StrictEOF: valid expression with predicate", + expr: "//book[@category='web']", + options: CompileOptions{StrictEOF: true}, + wantErr: false, + wantLen: 2, + }, + { + name: "StrictEOF: expression with extra trailing tokens returns error", + expr: "//book,foo", + options: CompileOptions{StrictEOF: true}, + wantErr: true, + wantLen: -1, + errMsg: "unexpected token after end of expression: ,foo", + }, + { + name: "Default: expression with extra trailing tokens is accepted", + expr: "//book,foo", + options: CompileOptions{}, + wantErr: false, + wantLen: -1, + }, + { + name: "StrictPreset: valid expression", + expr: "//book/title", + options: StrictPreset, + wantErr: false, + wantLen: 4, + }, + { + name: "StrictPreset: expression with extra trailing tokens returns error", + expr: "//book/title,foo", + options: StrictPreset, + wantErr: true, + wantLen: -1, + errMsg: "unexpected token after end of expression: ,foo", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + e, err := CompileWithOptions(tc.expr, tc.options) + if tc.wantErr { + if err == nil { + t.Errorf("expected error, got nil") + return + } + if tc.errMsg != "" && !strings.Contains(err.Error(), tc.errMsg) { + t.Errorf("expected error message to contain %q, got: %v", tc.errMsg, err) + } + return + } + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + if tc.wantLen >= 0 { + nodes := iterateNodes(e.Select(createNavigator(doc))) + assertEqual(t, tc.wantLen, len(nodes)) + } + }) + } +} + func iterateNavs(t *NodeIterator) []*TNodeNavigator { var nodes []*TNodeNavigator for t.MoveNext() { @@ -589,37 +671,37 @@ func (n *TNode) getAttribute(key string) string { func createBookExample() *TNode { /* - - - - Everyday Italian - Giada De Laurentiis - 2005 - 30.00 - - - Harry Potter - J K. Rowling - 2005 - 29.99 - - - XQuery Kick Start - James McGovern - Per Bothner - Kurt Cagle - James Linn - Vaidyanathan Nagarajan - 2003 - 49.99 - - - Learning XML - Erik T. Ray - 2003 - 39.95 - - + + + + Everyday Italian + Giada De Laurentiis + 2005 + 30.00 + + + Harry Potter + J K. Rowling + 2005 + 29.99 + + + XQuery Kick Start + James McGovern + Per Bothner + Kurt Cagle + James Linn + Vaidyanathan Nagarajan + 2003 + 49.99 + + + Learning XML + Erik T. Ray + 2003 + 39.95 + + */ type Element struct { Data string @@ -708,24 +790,24 @@ func createBookExample() *TNode { // The example document from https://way2tutorial.com/xml/xpath-node-test.php func createEmployeeExample() *TNode { /* - - - - Opal Kole - Senior Engineer - OpalKole@myemail.com - - - Max Miller - DBA Engineer - maxmiller@email.com - - - Beccaa Moss - Application Developer - beccaamoss@email.com - - + + + + Opal Kole + Senior Engineer + OpalKole@myemail.com + + + Max Miller + DBA Engineer + maxmiller@email.com + + + Beccaa Moss + Application Developer + beccaamoss@email.com + + */ type Element struct { @@ -808,25 +890,25 @@ func createHtmlExample() *TNode { /* - My page - + My page + -

Welcome to my page

- +

This is the first paragraph.

+ */