Skip to content

Commit c9e441b

Browse files
committed
introduce CompileOptions, StrictEOF option
1 parent 8d50c25 commit c9e441b

File tree

4 files changed

+204
-105
lines changed

4 files changed

+204
-105
lines changed

build.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -698,7 +698,8 @@ func (b *builder) processNode(root node, flags flag, props *builderProp) (q quer
698698
}
699699

700700
// build builds a specified XPath expressions expr.
701-
func build(expr string, namespaces map[string]string) (q query, err error) {
701+
// build returns the query and the parser used for parsing.
702+
func build(expr string, namespaces map[string]string) (q query, p *parser, err error) {
702703
defer func() {
703704
if e := recover(); e != nil {
704705
switch x := e.(type) {
@@ -711,8 +712,9 @@ func build(expr string, namespaces map[string]string) (q query, err error) {
711712
}
712713
}
713714
}()
714-
root := parse(expr, namespaces)
715+
root, p := parse(expr, namespaces)
715716
b := &builder{}
716717
props := builderProps.None
717-
return b.processNode(root, flagsEnum.None, &props)
718+
q, err = b.processNode(root, flagsEnum.None, &props)
719+
return q, p, err
718720
}

parse.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -564,12 +564,14 @@ func (p *parser) parseMethod(n node) node {
564564
}
565565

566566
// Parse parsing the XPath express string expr and returns a tree node.
567-
func parse(expr string, namespaces map[string]string) node {
567+
// parse returns the root node and the parser used for parsing.
568+
func parse(expr string, namespaces map[string]string) (node, *parser) {
568569
r := &scanner{text: expr}
569570
r.nextChar()
570571
r.nextItem()
571572
p := &parser{r: r, namespaces: namespaces}
572-
return p.parseExpression(nil)
573+
root := p.parseExpression(nil)
574+
return root, p
573575
}
574576

575577
// rootNode holds a top-level node of tree.

xpath.go

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ import (
55
"fmt"
66
)
77

8+
// CompileOptions allows customizing the behavior of the XPath parser.
9+
type CompileOptions struct {
10+
StrictEOF bool // If true, require full input consumption (no trailing tokens)
11+
// Future strictness options can be added here
12+
}
13+
14+
// StrictPreset enables all strictness options (update as new options are added)
15+
var StrictPreset = CompileOptions{
16+
StrictEOF: true,
17+
}
18+
819
// NodeType represents a type of XPath node.
920
type NodeType int
1021

@@ -138,17 +149,7 @@ func (expr *Expr) String() string {
138149

139150
// Compile compiles an XPath expression string.
140151
func Compile(expr string) (*Expr, error) {
141-
if expr == "" {
142-
return nil, errors.New("expr expression is nil")
143-
}
144-
qy, err := build(expr, nil)
145-
if err != nil {
146-
return nil, err
147-
}
148-
if qy == nil {
149-
return nil, fmt.Errorf(fmt.Sprintf("undeclared variable in XPath expression: %s", expr))
150-
}
151-
return &Expr{s: expr, q: qy}, nil
152+
return CompileWithOptionsAndNS(expr, CompileOptions{}, nil)
152153
}
153154

154155
// MustCompile compiles an XPath expression string and ignored error.
@@ -162,15 +163,27 @@ func MustCompile(expr string) *Expr {
162163

163164
// CompileWithNS compiles an XPath expression string, using given namespaces map.
164165
func CompileWithNS(expr string, namespaces map[string]string) (*Expr, error) {
166+
return CompileWithOptionsAndNS(expr, CompileOptions{}, namespaces)
167+
}
168+
169+
// CompileWithOptions compiles an XPath expression string with the given options.
170+
func CompileWithOptions(expr string, opts CompileOptions) (*Expr, error) {
171+
return CompileWithOptionsAndNS(expr, opts, nil)
172+
}
173+
174+
func CompileWithOptionsAndNS(expr string, opts CompileOptions, namespaces map[string]string) (*Expr, error) {
165175
if expr == "" {
166176
return nil, errors.New("expr expression is nil")
167177
}
168-
qy, err := build(expr, namespaces)
178+
q, p, err := build(expr, namespaces)
169179
if err != nil {
170180
return nil, err
171181
}
172-
if qy == nil {
173-
return nil, fmt.Errorf(fmt.Sprintf("undeclared variable in XPath expression: %s", expr))
182+
if opts.StrictEOF && p != nil && p.r.typ != itemEOF {
183+
return nil, fmt.Errorf("unexpected token after end of expression: %s", p.r.text[p.r.pos:])
184+
}
185+
if q == nil {
186+
return nil, fmt.Errorf("undeclared variable in XPath expression: %s", expr)
174187
}
175-
return &Expr{s: expr, q: qy}, nil
188+
return &Expr{s: expr, q: q}, nil
176189
}

xpath_test.go

Lines changed: 167 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -91,26 +91,26 @@ func test_xpath_eval(t *testing.T, root *TNode, expr string, expected ...interfa
9191
func Test_Predicates_MultiParent(t *testing.T) {
9292
// https://github.com/antchfx/xpath/issues/75
9393
/*
94-
<measCollecFile xmlns="http://www.3gpp.org/ftp/specs/archive/32_series/32.435#measCollec">
95-
<measData>
96-
<measInfo>
97-
<measType p="1">field1</measType>
98-
<measType p="2">field2</measType>
99-
<measValue>
100-
<r p="1">31854</r>
101-
<r p="2">159773</r>
102-
</measValue>
103-
</measInfo>
104-
<measInfo measInfoId="metric_name2">
105-
<measType p="1">field3</measType>
106-
<measType p="2">field4</measType>
107-
<measValue>
108-
<r p="1">1234</r>
109-
<r p="2">567</r>
110-
</measValue>
111-
</measInfo>
112-
</measData>
113-
</measCollecFile>
94+
<measCollecFile xmlns="http://www.3gpp.org/ftp/specs/archive/32_series/32.435#measCollec">
95+
<measData>
96+
<measInfo>
97+
<measType p="1">field1</measType>
98+
<measType p="2">field2</measType>
99+
<measValue>
100+
<r p="1">31854</r>
101+
<r p="2">159773</r>
102+
</measValue>
103+
</measInfo>
104+
<measInfo measInfoId="metric_name2">
105+
<measType p="1">field3</measType>
106+
<measType p="2">field4</measType>
107+
<measValue>
108+
<r p="1">1234</r>
109+
<r p="2">567</r>
110+
</measValue>
111+
</measInfo>
112+
</measData>
113+
</measCollecFile>
114114
*/
115115
doc := createNode("", RootNode)
116116
measCollecFile := doc.createChildNode("measCollecFile", ElementNode)
@@ -281,6 +281,88 @@ func TestNodeType(t *testing.T) {
281281
assertEqual(t, CommentNode, n.Type)
282282
}
283283

284+
func TestCompileWithOptions_StrictEOF(t *testing.T) {
285+
doc := createBookExample()
286+
287+
testCases := []struct {
288+
name string
289+
expr string
290+
options CompileOptions
291+
wantErr bool
292+
wantLen int // -1 if not applicable
293+
}{
294+
{
295+
name: "StrictEOF: valid expression",
296+
expr: "//book",
297+
options: CompileOptions{StrictEOF: true},
298+
wantErr: false,
299+
wantLen: 4,
300+
},
301+
{
302+
name: "StrictEOF: valid expression with predicate",
303+
expr: "//book[@category='web']",
304+
options: CompileOptions{StrictEOF: true},
305+
wantErr: false,
306+
wantLen: 2,
307+
},
308+
{
309+
name: "StrictEOF: expression with extra trailing tokens returns error",
310+
expr: "//book,foo",
311+
options: CompileOptions{StrictEOF: true},
312+
wantErr: true,
313+
wantLen: -1,
314+
},
315+
{
316+
name: "StrictEOF: undeclared variable returns error",
317+
expr: "//book[$undeclared]",
318+
options: CompileOptions{StrictEOF: true},
319+
wantErr: true,
320+
wantLen: -1,
321+
},
322+
{
323+
name: "Default: expression with extra trailing tokens is accepted",
324+
expr: "//book,foo",
325+
options: CompileOptions{},
326+
wantErr: false,
327+
wantLen: -1,
328+
},
329+
{
330+
name: "StrictPreset: valid expression",
331+
expr: "//book/title",
332+
options: StrictPreset,
333+
wantErr: false,
334+
wantLen: 4,
335+
},
336+
{
337+
name: "StrictPreset: expression with extra trailing tokens returns error",
338+
expr: "//book/title,foo",
339+
options: StrictPreset,
340+
wantErr: true,
341+
wantLen: -1,
342+
},
343+
}
344+
345+
for _, tc := range testCases {
346+
t.Run(tc.name, func(t *testing.T) {
347+
e, err := CompileWithOptions(tc.expr, tc.options)
348+
if tc.wantErr {
349+
if err == nil {
350+
t.Errorf("expected error, got nil")
351+
}
352+
return
353+
}
354+
if err != nil {
355+
t.Errorf("unexpected error: %v", err)
356+
return
357+
}
358+
if tc.wantLen >= 0 {
359+
nodes := iterateNodes(e.Select(createNavigator(doc)))
360+
assertEqual(t, tc.wantLen, len(nodes))
361+
}
362+
})
363+
}
364+
}
365+
284366
func iterateNavs(t *NodeIterator) []*TNodeNavigator {
285367
var nodes []*TNodeNavigator
286368
for t.MoveNext() {
@@ -589,37 +671,37 @@ func (n *TNode) getAttribute(key string) string {
589671

590672
func createBookExample() *TNode {
591673
/*
592-
<?xml version="1.0" encoding="UTF-8"?>
593-
<bookstore>
594-
<book category="cooking">
595-
<title lang="en">Everyday Italian</title>
596-
<author>Giada De Laurentiis</author>
597-
<year>2005</year>
598-
<price>30.00</price>
599-
</book>
600-
<book category="children">
601-
<title lang="en">Harry Potter</title>
602-
<author>J K. Rowling</author>
603-
<year>2005</year>
604-
<price>29.99</price>
605-
</book>
606-
<book category="web">
607-
<title lang="en">XQuery Kick Start</title>
608-
<author>James McGovern</author>
609-
<author>Per Bothner</author>
610-
<author>Kurt Cagle</author>
611-
<author>James Linn</author>
612-
<author>Vaidyanathan Nagarajan</author>
613-
<year>2003</year>
614-
<price>49.99</price>
615-
</book>
616-
<book category="web">
617-
<title lang="en">Learning XML</title>
618-
<author>Erik T. Ray</author>
619-
<year>2003</year>
620-
<price>39.95</price>
621-
</book>
622-
</bookstore>
674+
<?xml version="1.0" encoding="UTF-8"?>
675+
<bookstore>
676+
<book category="cooking">
677+
<title lang="en">Everyday Italian</title>
678+
<author>Giada De Laurentiis</author>
679+
<year>2005</year>
680+
<price>30.00</price>
681+
</book>
682+
<book category="children">
683+
<title lang="en">Harry Potter</title>
684+
<author>J K. Rowling</author>
685+
<year>2005</year>
686+
<price>29.99</price>
687+
</book>
688+
<book category="web">
689+
<title lang="en">XQuery Kick Start</title>
690+
<author>James McGovern</author>
691+
<author>Per Bothner</author>
692+
<author>Kurt Cagle</author>
693+
<author>James Linn</author>
694+
<author>Vaidyanathan Nagarajan</author>
695+
<year>2003</year>
696+
<price>49.99</price>
697+
</book>
698+
<book category="web">
699+
<title lang="en">Learning XML</title>
700+
<author>Erik T. Ray</author>
701+
<year>2003</year>
702+
<price>39.95</price>
703+
</book>
704+
</bookstore>
623705
*/
624706
type Element struct {
625707
Data string
@@ -708,24 +790,24 @@ func createBookExample() *TNode {
708790
// The example document from https://way2tutorial.com/xml/xpath-node-test.php
709791
func createEmployeeExample() *TNode {
710792
/*
711-
<?xml version="1.0" standalone="yes"?>
712-
<empinfo>
713-
<employee id="1">
714-
<name>Opal Kole</name>
715-
<designation discipline="web" experience="3 year">Senior Engineer</designation>
716-
<email>[email protected]</email>
717-
</employee>
718-
<employee id="2">
719-
<name from="CA">Max Miller</name>
720-
<designation discipline="DBA" experience="2 year">DBA Engineer</designation>
721-
<email>[email protected]</email>
722-
</employee>
723-
<employee id="3">
724-
<name>Beccaa Moss</name>
725-
<designation discipline="appdev">Application Developer</designation>
726-
<email>[email protected]</email>
727-
</employee>
728-
</empinfo>
793+
<?xml version="1.0" standalone="yes"?>
794+
<empinfo>
795+
<employee id="1">
796+
<name>Opal Kole</name>
797+
<designation discipline="web" experience="3 year">Senior Engineer</designation>
798+
<email>[email protected]</email>
799+
</employee>
800+
<employee id="2">
801+
<name from="CA">Max Miller</name>
802+
<designation discipline="DBA" experience="2 year">DBA Engineer</designation>
803+
<email>[email protected]</email>
804+
</employee>
805+
<employee id="3">
806+
<name>Beccaa Moss</name>
807+
<designation discipline="appdev">Application Developer</designation>
808+
<email>[email protected]</email>
809+
</employee>
810+
</empinfo>
729811
*/
730812

731813
type Element struct {
@@ -808,25 +890,25 @@ func createHtmlExample() *TNode {
808890
/*
809891
<html lang="en">
810892
<head>
811-
<title>My page</title>
812-
<meta name="language" content="en" />
893+
<title>My page</title>
894+
<meta name="language" content="en" />
813895
</head>
814896
<body>
815-
<h2>Welcome to my page</h2>
816-
<ul>
817-
<li>
818-
<a href="/">Home</a>
819-
</li>
820-
<li>
821-
<a href="/about">About</a>
822-
</li>
823-
<li>
824-
<a href="/account">Login</a>
825-
</li>
897+
<h2>Welcome to my page</h2>
898+
<ul>
899+
<li>
900+
<a href="/">Home</a>
901+
</li>
902+
<li>
903+
<a href="/about">About</a>
904+
</li>
905+
<li>
906+
<a href="/account">Login</a>
907+
</li>
826908
<li></li>
827-
</ul>
828-
<p>This is the first paragraph.</p>
829-
<!-- this is the end -->
909+
</ul>
910+
<p>This is the first paragraph.</p>
911+
<!-- this is the end -->
830912
</body>
831913
</html>
832914
*/

0 commit comments

Comments
 (0)