Skip to content

Commit 817c528

Browse files
committed
[XPath] Fix function and namespace handling.
16229 72.1% successes 5 0.0% skipped 4010 17.8% failures 2270 10.1% errors
1 parent 8aa545c commit 817c528

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+577
-652
lines changed

.vscode/launch.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,39 @@
22
"version": "0.2.0",
33
"configurations": [
44
{
5-
"name": "Run all Tests",
5+
"name": "Run Unit Tests",
66
"type": "dart",
77
"request": "launch",
88
"program": "test"
99
},
10+
{
11+
"name": "Run QT3 Tests",
12+
"type": "dart",
13+
"request": "launch",
14+
"program": "bin/xpath_qt3tests.dart",
15+
"console": "terminal",
16+
},
1017
{
1118
"name": "Run XML Benchmarks",
1219
"type": "dart",
1320
"request": "launch",
1421
"program": "bin/xml_benchmark.dart",
1522
"console": "terminal",
23+
"enableAsserts": false,
24+
"args": [
25+
"--no-xml",
26+
],
1627
},
1728
{
1829
"name": "Run XPath Benchmarks",
1930
"type": "dart",
2031
"request": "launch",
2132
"program": "bin/xpath_benchmark.dart",
2233
"console": "terminal",
34+
"enableAsserts": false,
35+
"args": [
36+
"--no-xml"
37+
],
2338
},
2439
]
2540
}

bin/xpath_qt3tests.dart

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import 'dart:io';
88

99
import 'package:collection/collection.dart';
1010
import 'package:xml/src/xpath/evaluation/context.dart';
11-
import 'package:xml/src/xpath/evaluation/functions.dart';
1211
import 'package:xml/src/xpath/types/boolean.dart';
1312
import 'package:xml/src/xpath/types/node.dart';
1413
import 'package:xml/src/xpath/types/number.dart';
@@ -234,14 +233,9 @@ class TestEnvironment {
234233

235234
late final XmlDocument? source = _getSource();
236235

237-
XPathContext get context {
238-
final ctx = XPathContext(
239-
source ?? XPathSequence.empty,
240-
functions: standardFunctions,
241-
variables: _getParams(),
242-
);
243-
return ctx;
244-
}
236+
XPathContext get context => XPathContext.canonical(
237+
source ?? XPathSequence.empty,
238+
).copy(variables: _getParams());
245239

246240
XmlDocument? _getSource() {
247241
final file = element
@@ -256,15 +250,13 @@ class TestEnvironment {
256250

257251
Map<String, Object> _getParams() {
258252
final params = <String, Object>{};
259-
final evalContext = XPathContext(
260-
source ?? XPathSequence.empty,
261-
functions: standardFunctions,
262-
);
263253
for (final param in element.findElements('param')) {
264254
final name = param.getAttribute('name');
265255
final select = param.getAttribute('select');
266256
if (name != null && select != null) {
267-
params[name] = evalContext.evaluate(select);
257+
params[name] = XPathContext.canonical(
258+
source ?? XPathSequence.empty,
259+
).evaluate(select);
268260
}
269261
}
270262
return params;
@@ -316,11 +308,9 @@ void verifyResult(XmlElement element, Object result, XPathContext context) {
316308
// Execute the different assertion types.
317309
switch (element.localName) {
318310
case 'assert':
319-
final evaluation = XPathContext(
311+
final evaluation = XPathContext.canonical(
320312
XPathSequence.empty,
321-
variables: {'result': result},
322-
functions: standardFunctions,
323-
).evaluate(element.innerText);
313+
).copy(variables: {'result': result}).evaluate(element.innerText);
324314
if (xsBoolean.cast(evaluation) != true) {
325315
throw TestFailure(
326316
'Expected true for ${element.innerText} with result=$result, '
@@ -363,19 +353,17 @@ void verifyResult(XmlElement element, Object result, XPathContext context) {
363353
throw TestFailure('Expected $xml, but got $result');
364354
}
365355
case 'assert-type':
366-
final evaluation = XPathContext(
367-
XPathSequence.empty,
368-
variables: {'result': result},
369-
functions: standardFunctions,
370-
).evaluate('\$result instance of ${element.innerText}');
356+
final evaluation = XPathContext.canonical(XPathSequence.empty)
357+
.copy(variables: {'result': result})
358+
.evaluate('\$result instance of ${element.innerText}');
371359
if (xsBoolean.cast(evaluation) != true) {
372360
throw TestFailure(
373361
'Expected true for ${element.innerText} with result=$result, '
374362
'to be of type ${element.innerText}',
375363
);
376364
}
377365
case 'assert-permutation':
378-
final expected = XPathContext(
366+
final expected = XPathContext.canonical(
379367
XPathSequence.empty,
380368
).evaluate(element.innerText);
381369
if (const SetEquality<Object>().equals(

lib/src/xml/utils/name.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ class XmlName with XmlHasVisitor, XmlHasWriter {
4949
final index = name.indexOf(XmlToken.namespace);
5050
if (index > 0) {
5151
final prefix = name.substring(0, index);
52-
if (namespaceUris.containsKey(prefix)) {
53-
uri = namespaceUris[prefix];
54-
}
52+
uri = namespaceUris[prefix];
5553
}
5654
}
5755
// Handle the default namespace.
@@ -102,6 +100,10 @@ class XmlName with XmlHasVisitor, XmlHasWriter {
102100
? '${XmlToken.openQualifiedUrl}$namespaceUri${XmlToken.closeQualifiedUrl}$local'
103101
: qualified;
104102

103+
/// Returns a new [XmlName] with the given [namespaceUri].
104+
XmlName withNamespaceUri(String? namespaceUri) =>
105+
XmlName.qualified(qualified, namespaceUri: namespaceUri);
106+
105107
@override
106108
String toString() => qualified;
107109

lib/src/xpath/definitions/function.dart

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import '../../xml/utils/name.dart';
12
import '../evaluation/context.dart';
23
import '../exceptions/evaluation_exception.dart';
34
import '../types/any.dart';
@@ -11,18 +12,14 @@ export 'package:petitparser/petitparser.dart' show unbounded;
1112
class XPathFunctionDefinition {
1213
const XPathFunctionDefinition({
1314
required this.name,
14-
this.aliases = const [],
1515
this.requiredArguments = const [],
1616
this.optionalArguments = const [],
1717
this.variadicArgument,
1818
required this.function,
1919
});
2020

2121
/// The name of the function.
22-
final String name;
23-
24-
/// The aliases of the function.
25-
final List<String> aliases;
22+
final XmlName name;
2623

2724
/// The required argument definitions.
2825
final List<XPathArgumentDefinition> requiredArguments;
@@ -81,7 +78,7 @@ class XPathFunctionDefinition {
8178

8279
@override
8380
String toString() =>
84-
'$name(${requiredArguments.join(', ')}, '
81+
'${name.qualified}(${requiredArguments.join(', ')}, '
8582
'${optionalArguments.join(', ')}, '
8683
'${variadicArgument != null ? '...' : ''})';
8784
}
@@ -114,14 +111,14 @@ class XPathArgumentDefinition {
114111
final iterable = sequence.iterator;
115112
if (!iterable.moveNext()) {
116113
throw XPathEvaluationException(
117-
'Function "${definition.name}" expects exactly one value for '
114+
'Function "${definition.name.qualified}" expects exactly one value for '
118115
'argument "$name", but got none.',
119116
);
120117
}
121118
final value = iterable.current;
122119
if (iterable.moveNext()) {
123120
throw XPathEvaluationException(
124-
'Function "${definition.name}" expects exactly one value for '
121+
'Function "${definition.name.qualified}" expects exactly one value for '
125122
'argument "$name", but got more than one.',
126123
);
127124
}
@@ -134,7 +131,7 @@ class XPathArgumentDefinition {
134131
final value = iterable.current;
135132
if (iterable.moveNext()) {
136133
throw XPathEvaluationException(
137-
'Function "${definition.name}" expects zero or one value for '
134+
'Function "${definition.name.qualified}" expects zero or one value for '
138135
'argument "$name", but got more than one.',
139136
);
140137
}
@@ -143,7 +140,7 @@ class XPathArgumentDefinition {
143140
final iterable = sequence.iterator;
144141
if (!iterable.moveNext()) {
145142
throw XPathEvaluationException(
146-
'Function "${definition.name}" expects one or more values for '
143+
'Function "${definition.name.qualified}" expects one or more values for '
147144
'argument "$name", but got none.',
148145
);
149146
}
Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
11
import '../../xml/nodes/node.dart';
2+
import '../../xml/utils/name.dart';
23
import '../exceptions/evaluation_exception.dart';
34
import '../grammars/parser.dart';
45
import '../types/function.dart';
56
import '../types/sequence.dart';
7+
import 'functions.dart';
68
import 'namespaces.dart';
79

810
/// Runtime execution context to evaluate XPath expressions.
911
class XPathContext {
10-
XPathContext(
12+
/// Creates an empty XPath context.
13+
XPathContext.empty(
1114
this.item, {
1215
this.position = 1,
1316
this.last = 1,
1417
this.variables = const {},
1518
this.functions = const {},
19+
this.namespaceUri,
20+
this.namespaceUris = const {},
1621
this.documents = const {},
17-
this.namespaces = const {},
1822
this.onTraceCallback,
1923
});
2024

25+
/// Creates a canonical XPath context on [item].
26+
XPathContext.canonical(this.item)
27+
: position = 1,
28+
last = 1,
29+
variables = const {},
30+
functions = standardFunctions,
31+
namespaceUri = xpathFnNamespace,
32+
namespaceUris = xpathNamespaceUris,
33+
documents = const {},
34+
onTraceCallback = null;
35+
2136
/// Mutable context node.
2237
Object item;
2338

@@ -30,15 +45,18 @@ class XPathContext {
3045
/// User-defined variables.
3146
final Map<String, Object> variables;
3247

33-
/// User-defined functions.
34-
final Map<String, XPathFunction> functions;
48+
/// Available function definitions.
49+
final Map<XmlName, XPathFunction> functions;
50+
51+
/// Default namespace URI for function lookups.
52+
final String? namespaceUri;
53+
54+
/// Namespace mapping from prefix to URIs.
55+
final Map<String, String> namespaceUris;
3556

3657
/// Available documents.
3758
final Map<String, XmlNode> documents;
3859

39-
/// Available namespaces.
40-
final Map<String, String> namespaces;
41-
4260
/// Callback to trace evaluation.
4361
final XPathTraceCallback? onTraceCallback;
4462

@@ -50,27 +68,38 @@ class XPathContext {
5068
}
5169

5270
/// Looks up a XPath function with the given [name].
53-
XPathFunction getFunction(String name) {
54-
final function = functions[name] ?? functions[_resolveEqName(name)];
71+
XPathFunction getFunction(XmlName name) {
72+
final function = functions[name];
5573
if (function != null) return function;
5674
throw XPathEvaluationException('Unknown function: $name');
5775
}
5876

59-
/// Creates a copy of the current context.
77+
/// Looks up a XPath function with the given [name].
78+
XPathFunction getFunctionByString(String name) => getFunction(
79+
XmlName.parse(
80+
name,
81+
namespaceUri: namespaceUri,
82+
namespaceUris: namespaceUris,
83+
),
84+
);
85+
86+
/// Creates a copy of this context.
6087
XPathContext copy({
6188
Map<String, Object>? variables,
62-
Map<String, XPathFunction>? functions,
89+
Map<XmlName, XPathFunction>? functions,
90+
String? namespaceUri,
91+
Map<String, String>? namespaceUris,
6392
Map<String, XmlNode>? documents,
64-
Map<String, String>? namespaces,
6593
XPathTraceCallback? onTraceCallback,
66-
}) => XPathContext(
94+
}) => XPathContext.empty(
6795
item,
6896
position: position,
6997
last: last,
70-
variables: variables ?? this.variables,
71-
functions: functions ?? this.functions,
72-
documents: documents ?? this.documents,
73-
namespaces: namespaces ?? this.namespaces,
98+
variables: _extend(this.variables, variables),
99+
functions: _extend(this.functions, functions),
100+
documents: _extend(this.documents, documents),
101+
namespaceUri: namespaceUri ?? this.namespaceUri,
102+
namespaceUris: _extend(this.namespaceUris, namespaceUris),
74103
onTraceCallback: onTraceCallback ?? this.onTraceCallback,
75104
);
76105

@@ -82,13 +111,8 @@ class XPathContext {
82111
/// Function type for tracing evaluation.
83112
typedef XPathTraceCallback = void Function(XPathSequence value, String? label);
84113

85-
/// Resolves a `Q{uri}local-name` EQName to its `prefix:local-name` form.
86-
String? _resolveEqName(String name) {
87-
if (!name.startsWith('Q{')) return null;
88-
final end = name.indexOf('}');
89-
if (end < 0) return null;
90-
final uri = name.substring(2, end);
91-
final localName = name.substring(end + 1);
92-
final prefix = standardNamespaces[uri];
93-
return prefix != null ? '$prefix:$localName' : null;
114+
Map<K, V> _extend<K, V>(Map<K, V> original, Map<K, V>? other) {
115+
if (other == null || other.isEmpty) return original;
116+
if (original.isEmpty) return other;
117+
return {...original, ...other};
94118
}

lib/src/xpath/evaluation/functions.dart

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import '../../xml/utils/name.dart';
12
import '../definitions/function.dart';
23
import '../functions/accessor.dart' as accessor;
34
import '../functions/array.dart' as array;
@@ -18,14 +19,14 @@ import '../functions/sequence.dart' as sequence;
1819
import '../functions/string.dart' as string;
1920
import '../functions/uri.dart' as uri;
2021
import '../types/function.dart';
22+
import 'namespaces.dart';
2123

2224
/// The standard functions.
23-
final Map<String, XPathFunction> standardFunctions = {
24-
for (final functionDefinition in standardFunctionDefinitions) ...{
25-
functionDefinition.name: functionDefinition.call,
26-
for (var alias in functionDefinition.aliases)
27-
alias: functionDefinition.call,
28-
},
25+
final Map<XmlName, XPathFunction> standardFunctions = {
26+
for (final definition in standardFunctionDefinitions)
27+
definition.name.withNamespaceUri(
28+
xpathNamespaceUris[definition.name.prefix],
29+
): definition.call,
2930
};
3031

3132
/// Internal list of standard function definitions.

lib/src/xpath/evaluation/namespaces.dart

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,11 @@ const xpathXsNamespace = 'http://www.w3.org/2001/XMLSchema';
1111
const xpathLocalNamespace = 'http://www.w3.org/2005/xquery-local-functions';
1212

1313
/// Standard XPath namespace prefixes.
14-
const standardPrefixes = {
14+
const xpathNamespaceUris = {
1515
'fn': xpathFnNamespace,
1616
'math': xpathMathNamespace,
1717
'map': xpathMapNamespace,
1818
'array': xpathArrayNamespace,
1919
'xs': xpathXsNamespace,
2020
'local': xpathLocalNamespace,
2121
};
22-
23-
/// Standard XPath namespaces.
24-
const standardNamespaces = {
25-
xpathFnNamespace: 'fn',
26-
xpathMathNamespace: 'math',
27-
xpathMapNamespace: 'map',
28-
xpathArrayNamespace: 'array',
29-
xpathXsNamespace: 'xs',
30-
xpathLocalNamespace: 'local',
31-
};

0 commit comments

Comments
 (0)