Skip to content

Commit dfb304f

Browse files
authored
Merge pull request #6344 from joewiz/extract/xq4-core-functions-3-1-subset
[bugfix] XQuery 3.1 mandatory fixes from v2/xq4-core-functions (audit extract #3 subset)
2 parents a6a1ba8 + ee64a15 commit dfb304f

22 files changed

Lines changed: 1344 additions & 85 deletions

exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,8 +1297,6 @@ throws XPathException
12971297
STAR
12981298
|
12991299
(
1300-
// TODO: parameter types are collected, but not used!
1301-
// Change SequenceType accordingly.
13021300
{ List<SequenceType> paramTypes = new ArrayList<SequenceType>(5); }
13031301
(
13041302
{ SequenceType paramType = new SequenceType(); }
@@ -1307,6 +1305,10 @@ throws XPathException
13071305
)*
13081306
{ SequenceType returnType = new SequenceType(); }
13091307
"as" sequenceType [returnType]
1308+
{
1309+
type.setFunctionParamTypes(paramTypes.toArray(new SequenceType[0]));
1310+
type.setFunctionReturnType(returnType);
1311+
}
13101312
)
13111313
)
13121314
)
@@ -1317,14 +1319,15 @@ throws XPathException
13171319
STAR
13181320
|
13191321
(
1320-
// TODO: parameter types are collected, but not used!
1321-
// Change SequenceType accordingly.
13221322
{ List<SequenceType> paramTypes = new ArrayList<SequenceType>(5); }
13231323
(
13241324
{ SequenceType paramType = new SequenceType(); }
13251325
sequenceType [paramType]
13261326
{ paramTypes.add(paramType); }
13271327
)*
1328+
{
1329+
type.setFunctionParamTypes(paramTypes.toArray(new SequenceType[0]));
1330+
}
13281331
)
13291332
)
13301333
)
@@ -1335,14 +1338,15 @@ throws XPathException
13351338
STAR
13361339
|
13371340
(
1338-
// TODO: parameter types are collected, but not used!
1339-
// Change SequenceType accordingly.
13401341
{ List<SequenceType> paramTypes = new ArrayList<SequenceType>(5); }
13411342
(
13421343
{ SequenceType paramType = new SequenceType(); }
13431344
sequenceType [paramType]
13441345
{ paramTypes.add(paramType); }
13451346
)*
1347+
{
1348+
type.setFunctionParamTypes(paramTypes.toArray(new SequenceType[0]));
1349+
}
13461350
)
13471351
)
13481352
)

exist-core/src/main/java/org/exist/xquery/ErrorCodes.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public class ErrorCodes {
9090
public static final ErrorCode XQST0052 = new W3CErrorCode("XQST0052", "It is a static error if the type-name in a single-type or sequence-type for a cast or castable expression does not refer to a defined atomic type.");
9191
public static final ErrorCode XQST0053 = new W3CErrorCode("XQST0053", "(Not currently used.)");
9292
public static final ErrorCode XQST0054 = new W3CErrorCode("XQST0054", "It is a static error if a variable depends on itself.");
93+
public static final ErrorCode XQDY0054 = new W3CErrorCode("XQDY0054", "It is a dynamic error if a variable depends on itself.");
9394
public static final ErrorCode XQST0055 = new W3CErrorCode("XQST0055", "It is a static error if a Prolog contains more than one copy-namespaces declaration.");
9495
public static final ErrorCode XQST0056 = new W3CErrorCode("XQST0056", "(Not currently used.)");
9596
public static final ErrorCode XQST0057 = new W3CErrorCode("XQST0057", "It is a static error if a schema import binds a namespace prefix but does not specify a target namespace other than a zero-length string.");

exist-core/src/main/java/org/exist/xquery/FunctionFactory.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.ArrayList;
2525
import java.util.Iterator;
2626
import java.util.List;
27+
import java.util.Set;
2728

2829
import org.exist.Namespaces;
2930
import org.exist.dom.QName;
@@ -48,13 +49,37 @@ public class FunctionFactory {
4849
public static final String PROPERTY_DISABLE_DEPRECATED_FUNCTIONS = "xquery.disable-deprecated-functions";
4950
public static final boolean DISABLE_DEPRECATED_FUNCTIONS_BY_DEFAULT = false;
5051

52+
/**
53+
* Reserved function names per XQuery 3.1/4.0 spec.
54+
* These names must not be used as unprefixed function calls (XPST0003).
55+
*/
56+
private static final Set<String> RESERVED_FUNCTION_NAMES = Set.of(
57+
"array", "attribute", "comment", "document-node", "element",
58+
"function", "if", "item", "map", "namespace-node", "node",
59+
"processing-instruction", "schema-attribute", "schema-element",
60+
"switch", "text", "typeswitch"
61+
);
62+
5163
public static Expression createFunction(XQueryContext context, XQueryAST ast, PathExpr parent, List<Expression> params) throws XPathException {
5264
QName qname = null;
5365
try {
5466
qname = QName.parse(context, ast.getText(), context.getDefaultFunctionNamespace());
5567
} catch(final QName.IllegalQNameException xpe) {
5668
throw new XPathException(ast, ErrorCodes.XPST0081, "Invalid qname " + ast.getText() + ". " + xpe.getMessage());
5769
}
70+
71+
// Check for reserved function names — unprefixed reserved names cannot be
72+
// used as function calls (XPST0003). Prefixed names like fn:item() are not
73+
// subject to the reserved name restriction (they just won't be found → XPST0017).
74+
final String rawName = ast.getText();
75+
if (rawName != null && !rawName.contains(":") && !rawName.contains("{")) {
76+
final String local = qname.getLocalPart();
77+
if (RESERVED_FUNCTION_NAMES.contains(local)) {
78+
throw new XPathException(ast.getLine(), ast.getColumn(), ErrorCodes.XPST0003,
79+
"'" + local + "' is a reserved function name and cannot be used as a function call");
80+
}
81+
}
82+
5883
return createFunction(context, qname, ast, parent, params);
5984
}
6085

@@ -525,9 +550,9 @@ public static FunctionCall wrap(XQueryContext context, Function call) throws XPa
525550
for (final QName varName: variables) {
526551
func.addVariable(varName);
527552
}
528-
553+
529554
call.setArguments(innerArgs);
530-
555+
531556
func.setFunctionBody(call);
532557

533558
final FunctionCall wrappedCall = new FunctionCall(context, func);

exist-core/src/main/java/org/exist/xquery/NamedFunctionReference.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.util.ArrayList;
2525
import java.util.List;
2626

27+
import java.util.Set;
28+
2729
import org.exist.dom.QName;
2830
import org.exist.xquery.parser.XQueryAST;
2931
import org.exist.xquery.util.ExpressionDumper;
@@ -52,7 +54,29 @@ public void analyze(AnalyzeContextInfo contextInfo) throws XPathException {
5254
resolvedFunction.analyze(contextInfo);
5355
}
5456

57+
/**
58+
* Reserved function names per XQuery 3.1/4.0 spec.
59+
* These names must not be used as unprefixed named function references (XPST0003).
60+
*/
61+
private static final Set<String> RESERVED_FUNCTION_NAMES = Set.of(
62+
"array", "attribute", "comment", "document-node", "element",
63+
"function", "if", "item", "map", "namespace-node", "node",
64+
"processing-instruction", "schema-attribute", "schema-element",
65+
"switch", "text", "typeswitch"
66+
);
67+
5568
public static FunctionCall lookupFunction(Expression self, XQueryContext context, QName funcName, int arity) throws XPathException {
69+
// Check for reserved function names — these cannot be used as named function references
70+
final String localPart = funcName.getLocalPart();
71+
final String nsURI = funcName.getNamespaceURI();
72+
if (RESERVED_FUNCTION_NAMES.contains(localPart) &&
73+
(nsURI == null || nsURI.isEmpty() ||
74+
Function.BUILTIN_FUNCTION_NS.equals(nsURI) ||
75+
context.getDefaultFunctionNamespace().equals(nsURI))) {
76+
throw new XPathException(self, ErrorCodes.XPST0003,
77+
"'" + localPart + "' is a reserved function name and cannot be used as a named function reference");
78+
}
79+
5680
if (Function.BUILTIN_FUNCTION_NS.equals(funcName.getNamespaceURI())
5781
&& "concat".equals(funcName.getLocalPart())
5882
&& arity < 2) {

exist-core/src/main/java/org/exist/xquery/VariableImpl.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -120,26 +120,26 @@ public void setSequenceType(SequenceType type) throws XPathException {
120120
else {actualCardinality = Cardinality.EXACTLY_ONE;}
121121
//Type.EMPTY is *not* a subtype of other types ; checking cardinality first
122122
if (!getSequenceType().getCardinality().isSuperCardinalityOrEqualOf(actualCardinality))
123-
{throw new XPathException(getValue(), "XPTY0004: Invalid cardinality for variable $" + getQName() +
123+
{throw new XPathException(getValue(), ErrorCodes.XPTY0004, "Invalid cardinality for variable $" + getQName() +
124124
". Expected " +
125125
getSequenceType().getCardinality().getHumanDescription() +
126126
", got " + actualCardinality.getHumanDescription());}
127127
//TODO : ignore nodes right now ; they are returned as xs:untypedAtomicType
128128
if (!Type.subTypeOf(getSequenceType().getPrimaryType(), Type.NODE)) {
129129
if (!getValue().isEmpty() && !Type.subTypeOf(getValue().getItemType(), getSequenceType().getPrimaryType()))
130-
{throw new XPathException(getValue(), "XPTY0004: Invalid type for variable $" + getQName() +
130+
{throw new XPathException(getValue(), ErrorCodes.XPTY0004, "Invalid type for variable $" + getQName() +
131131
". Expected " +
132132
Type.getTypeName(getSequenceType().getPrimaryType()) +
133133
", got " +Type.getTypeName(getValue().getItemType()));}
134134
//Here is an attempt to process the nodes correctly
135135
} else {
136-
//Same as above : we probably may factorize
136+
//Same as above : we probably may factorize
137137
if (!getValue().isEmpty() && !Type.subTypeOf(getValue().getItemType(), getSequenceType().getPrimaryType()))
138-
{throw new XPathException(getValue(), "XPTY0004: Invalid type for variable $" + getQName() +
138+
{throw new XPathException(getValue(), ErrorCodes.XPTY0004, "Invalid type for variable $" + getQName() +
139139
". Expected " +
140140
Type.getTypeName(getSequenceType().getPrimaryType()) +
141141
", got " +Type.getTypeName(getValue().getItemType()));}
142-
142+
143143
}
144144
}
145145

exist-core/src/main/java/org/exist/xquery/VariableReference.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException
6464
"Variable '$" + qname + "' is not declared.");
6565
}
6666
if (!var.isInitialized()) {
67-
throw new XPathException(this, ErrorCodes.XQST0054,
67+
throw new XPathException(this, ErrorCodes.XQDY0054,
6868
"variable declaration of '$" + qname + "' cannot " +
6969
"be executed because of a circularity.");
7070
}

exist-core/src/main/java/org/exist/xquery/XQueryContext.java

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,31 @@ public DocumentSet getStaticDocs() {
13821382
return textResourceSupplier.apply(getBroker(), getBroker().getCurrentTransaction(), uri, charset);
13831383
}
13841384

1385+
/**
1386+
* Gets a text resource from the "Available text resources" of the
1387+
* dynamic context, matching by URI only. This is used when no encoding
1388+
* is specified, allowing the resource to be found regardless of what
1389+
* charset it was registered with.
1390+
*
1391+
* @param uri the URI of the resource to retrieve
1392+
* @return a reader to read the resource content from, or null if not found
1393+
* @throws XPathException in case of a dynamic error
1394+
*/
1395+
public @Nullable Reader getDynamicallyAvailableTextResourceByUri(final String uri)
1396+
throws XPathException {
1397+
if (dynamicTextResources == null) {
1398+
return null;
1399+
}
1400+
1401+
for (final Map.Entry<Tuple2<String, Charset>, QuadFunctionE<DBBroker, Txn, String, Charset, Reader, XPathException>> entry : dynamicTextResources.entrySet()) {
1402+
if (entry.getKey()._1.equals(uri)) {
1403+
final Charset registeredCharset = entry.getKey()._2;
1404+
return entry.getValue().apply(getBroker(), getBroker().getCurrentTransaction(), uri, registeredCharset);
1405+
}
1406+
}
1407+
return null;
1408+
}
1409+
13851410
/**
13861411
* Gets a collection from the "Available collections" of the
13871412
* dynamic context.
@@ -3856,10 +3881,7 @@ public boolean equals(final Object o) {
38563881
}
38573882

38583883
final ModuleVertex that = (ModuleVertex) o;
3859-
if (!namespaceURI.equals(that.namespaceURI)) {
3860-
return false;
3861-
}
3862-
return location.equals(that.location);
3884+
return namespaceURI.equals(that.namespaceURI) && location.equals(that.location);
38633885
}
38643886

38653887
@Override

exist-core/src/main/java/org/exist/xquery/functions/fn/FnModule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public class FnModule extends AbstractInternalModule {
7777
new FunctionDef(FunDocumentURI.FS_DOCUMENT_URI_1, FunDocumentURI.class),
7878
new FunctionDef(FunElementWithId.FS_ELEMENT_WITH_ID_SIGNATURES[0], FunElementWithId.class),
7979
new FunctionDef(FunElementWithId.FS_ELEMENT_WITH_ID_SIGNATURES[1], FunElementWithId.class),
80+
new FunctionDef(FunElementWithId.FS_ELEMENT_WITH_ID_SIGNATURES[2], FunElementWithId.class),
8081
new FunctionDef(FunEmpty.signature, FunEmpty.class),
8182
new FunctionDef(FunEncodeForURI.signature, FunEncodeForURI.class),
8283
new FunctionDef(FunEndsWith.signatures[0], FunEndsWith.class),

exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,23 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
131131
}
132132
}
133133

134-
private void analyzeString(final MemTreeBuilder builder, final String input, String pattern, final String flags) throws XPathException {
134+
private void analyzeString(final MemTreeBuilder builder, final String input, final String pattern, final String flags) throws XPathException {
135135
final Configuration config = context.getBroker().getBrokerPool().getSaxonConfiguration();
136136

137+
// XPath 4.0 lookaround syntax is not yet implemented in eXist's XQuery 3.1 runtime.
138+
// When XQuery 4.0 lands (v2/xq4-core-functions), replace this guard with the
139+
// translateXPath4Lookaround() dispatch path.
140+
if (hasXPath4Lookaround(pattern)) {
141+
throw new XPathException(this, ErrorCodes.XPST0017,
142+
"XPath 4.0 lookaround syntax in regex patterns (e.g. (*positive_lookahead:...)) "
143+
+ "is not yet implemented in this XQuery 3.1 build. Rewrite the regex without lookaround.");
144+
}
145+
146+
// Pre-validate: reject constructs not valid in XPath 3.1 regex
147+
if (!hasLiteral(flags)) {
148+
validateXPathRegex(this, pattern, false);
149+
}
150+
137151
final List<String> warnings = new ArrayList<>(1);
138152

139153
try {

exist-core/src/main/java/org/exist/xquery/functions/fn/FunContainsToken.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class FunContainsToken extends BasicFunction {
4444

4545
private final static FunctionParameterSequenceType FS_INPUT = optManyParam("input", Type.STRING, "The input string");
4646
private final static FunctionParameterSequenceType FS_TOKEN = param("token", Type.STRING, "The token to be searched for");
47-
private final static FunctionParameterSequenceType FS_COLLATION = param("pattern", Type.STRING, "Collation to use");
47+
private final static FunctionParameterSequenceType FS_COLLATION = optParam("collation", Type.STRING, "Collation to use; an empty sequence selects the default collation");
4848

4949
public final static FunctionSignature[] FS_CONTAINS_TOKEN = functionSignatures(
5050
FS_CONTAINS_TOKEN_NAME,

0 commit comments

Comments
 (0)