Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e28b682
[bugfix] Improve XPath regex compliance: validate patterns and suppor…
joewiz Apr 26, 2026
a421dce
[feature] Improve fn:unparsed-text conformance and add function type …
joewiz Apr 26, 2026
3c925ad
[bugfix] Fix function call/ref XQTS failures: reserved names, context…
joewiz Apr 26, 2026
dffc2ae
[bugfix] Fix variable declaration error codes for XQuery 3.1 compliance
joewiz Apr 26, 2026
aa1c753
[feature] Version-aware XPath regex validation with XQ4 extensions
joewiz Apr 26, 2026
cd6b475
[bugfix] Fix fn:contains-token collation parameter to accept empty se…
joewiz Apr 26, 2026
4b18660
[bugfix] fn:load-xquery-module: check loaded module's own version
joewiz Apr 28, 2026
502cc16
[bugfix] Tighten XPath regex validation to reject more invalid constr…
joewiz Apr 28, 2026
42bbec5
[bugfix] XPath regex: validate back-references and tighten char class…
joewiz Apr 28, 2026
e6c547b
[test] LoadXQueryModuleContentTest: align with backward-compat semantics
joewiz May 1, 2026
68ff4a9
[bugfix] Adapt v2/xq4-core-functions extraction for develop's XQ 3.1-…
joewiz May 11, 2026
25d48de
[refactor] Address Codacy findings from PR #6344 review
joewiz May 11, 2026
69ef1f7
[bugfix] fn:matches/analyze-string: raise XPST0017 for XPath 4.0 look…
joewiz May 12, 2026
8c4fbfa
[bugfix] SequenceType: restrict function-arity check to function() ty…
joewiz May 12, 2026
ad70f18
[refactor] Address 4 low-risk Codacy findings on this PR
joewiz May 27, 2026
12340dd
[refactor] Address Codacy findings on the XPath regex validator
joewiz May 29, 2026
ee64a15
[refactor] Drop XQuery 4.0 binary type promotion blocks
joewiz May 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g
Original file line number Diff line number Diff line change
Expand Up @@ -1297,8 +1297,6 @@ throws XPathException
STAR
|
(
// TODO: parameter types are collected, but not used!
// Change SequenceType accordingly.
{ List<SequenceType> paramTypes = new ArrayList<SequenceType>(5); }
(
{ SequenceType paramType = new SequenceType(); }
Expand All @@ -1307,6 +1305,10 @@ throws XPathException
)*
{ SequenceType returnType = new SequenceType(); }
"as" sequenceType [returnType]
{
type.setFunctionParamTypes(paramTypes.toArray(new SequenceType[0]));
type.setFunctionReturnType(returnType);
}
)
)
)
Expand All @@ -1317,14 +1319,15 @@ throws XPathException
STAR
|
(
// TODO: parameter types are collected, but not used!
// Change SequenceType accordingly.
{ List<SequenceType> paramTypes = new ArrayList<SequenceType>(5); }
(
{ SequenceType paramType = new SequenceType(); }
sequenceType [paramType]
{ paramTypes.add(paramType); }
)*
{
type.setFunctionParamTypes(paramTypes.toArray(new SequenceType[0]));
}
)
)
)
Expand All @@ -1335,14 +1338,15 @@ throws XPathException
STAR
|
(
// TODO: parameter types are collected, but not used!
// Change SequenceType accordingly.
{ List<SequenceType> paramTypes = new ArrayList<SequenceType>(5); }
(
{ SequenceType paramType = new SequenceType(); }
sequenceType [paramType]
{ paramTypes.add(paramType); }
)*
{
type.setFunctionParamTypes(paramTypes.toArray(new SequenceType[0]));
}
)
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public class ErrorCodes {
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.");
public static final ErrorCode XQST0053 = new W3CErrorCode("XQST0053", "(Not currently used.)");
public static final ErrorCode XQST0054 = new W3CErrorCode("XQST0054", "It is a static error if a variable depends on itself.");
public static final ErrorCode XQDY0054 = new W3CErrorCode("XQDY0054", "It is a dynamic error if a variable depends on itself.");
public static final ErrorCode XQST0055 = new W3CErrorCode("XQST0055", "It is a static error if a Prolog contains more than one copy-namespaces declaration.");
public static final ErrorCode XQST0056 = new W3CErrorCode("XQST0056", "(Not currently used.)");
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.");
Expand Down
29 changes: 27 additions & 2 deletions exist-core/src/main/java/org/exist/xquery/FunctionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

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

/**
* Reserved function names per XQuery 3.1/4.0 spec.
* These names must not be used as unprefixed function calls (XPST0003).
*/
private static final Set<String> RESERVED_FUNCTION_NAMES = Set.of(
"array", "attribute", "comment", "document-node", "element",
"function", "if", "item", "map", "namespace-node", "node",
"processing-instruction", "schema-attribute", "schema-element",
"switch", "text", "typeswitch"
);

public static Expression createFunction(XQueryContext context, XQueryAST ast, PathExpr parent, List<Expression> params) throws XPathException {
QName qname = null;
try {
qname = QName.parse(context, ast.getText(), context.getDefaultFunctionNamespace());
} catch(final QName.IllegalQNameException xpe) {
throw new XPathException(ast, ErrorCodes.XPST0081, "Invalid qname " + ast.getText() + ". " + xpe.getMessage());
}

// Check for reserved function names — unprefixed reserved names cannot be
// used as function calls (XPST0003). Prefixed names like fn:item() are not
// subject to the reserved name restriction (they just won't be found → XPST0017).
final String rawName = ast.getText();
if (rawName != null && !rawName.contains(":") && !rawName.contains("{")) {
final String local = qname.getLocalPart();
if (RESERVED_FUNCTION_NAMES.contains(local)) {
throw new XPathException(ast.getLine(), ast.getColumn(), ErrorCodes.XPST0003,
"'" + local + "' is a reserved function name and cannot be used as a function call");
}
}

return createFunction(context, qname, ast, parent, params);
}

Expand Down Expand Up @@ -525,9 +550,9 @@ public static FunctionCall wrap(XQueryContext context, Function call) throws XPa
for (final QName varName: variables) {
func.addVariable(varName);
}

call.setArguments(innerArgs);

func.setFunctionBody(call);

final FunctionCall wrappedCall = new FunctionCall(context, func);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import java.util.ArrayList;
import java.util.List;

import java.util.Set;

import org.exist.dom.QName;
import org.exist.xquery.parser.XQueryAST;
import org.exist.xquery.util.ExpressionDumper;
Expand Down Expand Up @@ -52,7 +54,29 @@ public void analyze(AnalyzeContextInfo contextInfo) throws XPathException {
resolvedFunction.analyze(contextInfo);
}

/**
* Reserved function names per XQuery 3.1/4.0 spec.
* These names must not be used as unprefixed named function references (XPST0003).
*/
private static final Set<String> RESERVED_FUNCTION_NAMES = Set.of(
"array", "attribute", "comment", "document-node", "element",
"function", "if", "item", "map", "namespace-node", "node",
"processing-instruction", "schema-attribute", "schema-element",
"switch", "text", "typeswitch"
);

public static FunctionCall lookupFunction(Expression self, XQueryContext context, QName funcName, int arity) throws XPathException {
// Check for reserved function names — these cannot be used as named function references
final String localPart = funcName.getLocalPart();
final String nsURI = funcName.getNamespaceURI();
if (RESERVED_FUNCTION_NAMES.contains(localPart) &&
(nsURI == null || nsURI.isEmpty() ||
Function.BUILTIN_FUNCTION_NS.equals(nsURI) ||
context.getDefaultFunctionNamespace().equals(nsURI))) {
throw new XPathException(self, ErrorCodes.XPST0003,
"'" + localPart + "' is a reserved function name and cannot be used as a named function reference");
}

if (Function.BUILTIN_FUNCTION_NS.equals(funcName.getNamespaceURI())
&& "concat".equals(funcName.getLocalPart())
&& arity < 2) {
Expand Down
10 changes: 5 additions & 5 deletions exist-core/src/main/java/org/exist/xquery/VariableImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,26 +120,26 @@ public void setSequenceType(SequenceType type) throws XPathException {
else {actualCardinality = Cardinality.EXACTLY_ONE;}
//Type.EMPTY is *not* a subtype of other types ; checking cardinality first
if (!getSequenceType().getCardinality().isSuperCardinalityOrEqualOf(actualCardinality))
{throw new XPathException(getValue(), "XPTY0004: Invalid cardinality for variable $" + getQName() +
{throw new XPathException(getValue(), ErrorCodes.XPTY0004, "Invalid cardinality for variable $" + getQName() +
". Expected " +
getSequenceType().getCardinality().getHumanDescription() +
", got " + actualCardinality.getHumanDescription());}
//TODO : ignore nodes right now ; they are returned as xs:untypedAtomicType
if (!Type.subTypeOf(getSequenceType().getPrimaryType(), Type.NODE)) {
if (!getValue().isEmpty() && !Type.subTypeOf(getValue().getItemType(), getSequenceType().getPrimaryType()))
{throw new XPathException(getValue(), "XPTY0004: Invalid type for variable $" + getQName() +
{throw new XPathException(getValue(), ErrorCodes.XPTY0004, "Invalid type for variable $" + getQName() +
". Expected " +
Type.getTypeName(getSequenceType().getPrimaryType()) +
", got " +Type.getTypeName(getValue().getItemType()));}
//Here is an attempt to process the nodes correctly
} else {
//Same as above : we probably may factorize
//Same as above : we probably may factorize
if (!getValue().isEmpty() && !Type.subTypeOf(getValue().getItemType(), getSequenceType().getPrimaryType()))
{throw new XPathException(getValue(), "XPTY0004: Invalid type for variable $" + getQName() +
{throw new XPathException(getValue(), ErrorCodes.XPTY0004, "Invalid type for variable $" + getQName() +
". Expected " +
Type.getTypeName(getSequenceType().getPrimaryType()) +
", got " +Type.getTypeName(getValue().getItemType()));}

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException
"Variable '$" + qname + "' is not declared.");
}
if (!var.isInitialized()) {
throw new XPathException(this, ErrorCodes.XQST0054,
throw new XPathException(this, ErrorCodes.XQDY0054,
"variable declaration of '$" + qname + "' cannot " +
"be executed because of a circularity.");
}
Expand Down
30 changes: 26 additions & 4 deletions exist-core/src/main/java/org/exist/xquery/XQueryContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -1375,6 +1375,31 @@ public DocumentSet getStaticDocs() {
return textResourceSupplier.apply(getBroker(), getBroker().getCurrentTransaction(), uri, charset);
}

/**
* Gets a text resource from the "Available text resources" of the
* dynamic context, matching by URI only. This is used when no encoding
* is specified, allowing the resource to be found regardless of what
* charset it was registered with.
*
* @param uri the URI of the resource to retrieve
* @return a reader to read the resource content from, or null if not found
* @throws XPathException in case of a dynamic error
*/
public @Nullable Reader getDynamicallyAvailableTextResourceByUri(final String uri)
throws XPathException {
if (dynamicTextResources == null) {
return null;
}

for (final Map.Entry<Tuple2<String, Charset>, QuadFunctionE<DBBroker, Txn, String, Charset, Reader, XPathException>> entry : dynamicTextResources.entrySet()) {
if (entry.getKey()._1.equals(uri)) {
final Charset registeredCharset = entry.getKey()._2;
return entry.getValue().apply(getBroker(), getBroker().getCurrentTransaction(), uri, registeredCharset);
}
}
return null;
}

/**
* Gets a collection from the "Available collections" of the
* dynamic context.
Expand Down Expand Up @@ -3849,10 +3874,7 @@ public boolean equals(final Object o) {
}

final ModuleVertex that = (ModuleVertex) o;
if (!namespaceURI.equals(that.namespaceURI)) {
return false;
}
return location.equals(that.location);
return namespaceURI.equals(that.namespaceURI) && location.equals(that.location);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public class FnModule extends AbstractInternalModule {
new FunctionDef(FunDocumentURI.FS_DOCUMENT_URI_1, FunDocumentURI.class),
new FunctionDef(FunElementWithId.FS_ELEMENT_WITH_ID_SIGNATURES[0], FunElementWithId.class),
new FunctionDef(FunElementWithId.FS_ELEMENT_WITH_ID_SIGNATURES[1], FunElementWithId.class),
new FunctionDef(FunElementWithId.FS_ELEMENT_WITH_ID_SIGNATURES[2], FunElementWithId.class),
new FunctionDef(FunEmpty.signature, FunEmpty.class),
new FunctionDef(FunEncodeForURI.signature, FunEncodeForURI.class),
new FunctionDef(FunEndsWith.signatures[0], FunEndsWith.class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,23 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
}
}

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

// XPath 4.0 lookaround syntax is not yet implemented in eXist's XQuery 3.1 runtime.
// When XQuery 4.0 lands (v2/xq4-core-functions), replace this guard with the
// translateXPath4Lookaround() dispatch path.
if (hasXPath4Lookaround(pattern)) {
throw new XPathException(this, ErrorCodes.XPST0017,
"XPath 4.0 lookaround syntax in regex patterns (e.g. (*positive_lookahead:...)) "
+ "is not yet implemented in this XQuery 3.1 build. Rewrite the regex without lookaround.");
}

// Pre-validate: reject constructs not valid in XPath 3.1 regex
Comment thread
line-o marked this conversation as resolved.
if (!hasLiteral(flags)) {
validateXPathRegex(this, pattern, false);
}

final List<String> warnings = new ArrayList<>(1);

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class FunContainsToken extends BasicFunction {

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

public final static FunctionSignature[] FS_CONTAINS_TOKEN = functionSignatures(
FS_CONTAINS_TOKEN_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ public class FunElementWithId extends BasicFunction {
"If none is matching or $idrefs is the empty sequence, returns the empty sequence.";
private static final FunctionReturnSequenceType FN_RETURN = returnsOptMany(Type.STRING, "the elements with IDs matching IDREFs from $idref-sequence");
private static final FunctionParameterSequenceType PARAM_ID_REFS_STRING = optManyParam("idrefs", Type.STRING, "The IDREF sequence");
private static final FunctionParameterSequenceType PARAM_NODE = param("node", Type.NODE, "A node in the document to search");
public static final FunctionSignature[] FS_ELEMENT_WITH_ID_SIGNATURES = functionSignatures(
FN_NAME,
FN_DESCRIPTION,
FN_RETURN,
arities(
arity(),
arity(PARAM_ID_REFS_STRING)
arity(PARAM_ID_REFS_STRING),
arity(PARAM_ID_REFS_STRING, PARAM_NODE)
)
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,13 +441,10 @@ private Sequence evalWithIndex(final Sequence contextSequence, final Item contex
// restricted to that QName
contextQName = null;
}
if (!indexFound && contextQName == null) {
// if there are some indexes defined on a qname,
// we need to check them all
if (iflags.hasIndexOnQNames()) {
indexScan = true;
}
// else use range index defined on path by default
// if there are some indexes defined on a qname, we need to check them all;
// otherwise use range index defined on path by default
if (!indexFound && contextQName == null && iflags.hasIndexOnQNames()) {
indexScan = true;
}
} else {
result = evalFallback(nodes, pattern, flags, indexType);
Expand Down Expand Up @@ -526,7 +523,22 @@ private Sequence evalGeneric(final Sequence contextSequence, final Item contextI
}


private boolean matchXmlRegex(final String string, final String pattern, final String flags) throws XPathException {
private boolean matchXmlRegex(String string, final String pattern, final String flags) throws XPathException {
// XPath 4.0 lookaround syntax is not yet implemented in eXist's XQuery 3.1 runtime.
// When XQuery 4.0 lands (v2/xq4-core-functions), replace this guard with the
// translateXPath4Lookaround / Java-regex dispatch path.
if (hasXPath4Lookaround(pattern)) {
throw new XPathException(this, ErrorCodes.XPST0017,
"XPath 4.0 lookaround syntax in regex patterns (e.g. (*positive_lookahead:...)) "
+ "is not yet implemented in this XQuery 3.1 build. Rewrite the regex without lookaround.");
}

// Pre-validate: reject constructs that are not valid in XPath 3.1 regex
// but that Saxon's XP30 mode accepts (Java/Perl extensions)
if (!hasLiteral(flags)) {
validateXPathRegex(this, pattern, false);
}

try {
List<String> warnings = new ArrayList<>(1);
RegularExpression regex = context.getBroker().getBrokerPool()
Expand Down
Comment thread
line-o marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,21 @@ public Sequence eval(final Sequence[] args, final Sequence contextSequence) thro
flags = "";
}
final String string = stringArg.getStringValue();
final String pattern = args[1].itemAt(0).getStringValue();
String pattern = args[1].itemAt(0).getStringValue();
final String replace = args[2].itemAt(0).getStringValue();

final boolean isXQuery40 = context.getXQueryVersion() >= 40;

// XQ4: translate (*positive_lookahead:...) etc. to Java regex (?=...) syntax
if (isXQuery40 && hasXPath4Lookaround(pattern)) {
pattern = translateXPath4Lookaround(pattern);
}

// Pre-validate: reject constructs not valid in XPath regex
if (!hasLiteral(flags)) {
validateXPathRegex(this, pattern, isXQuery40);
}

final Configuration config = context.getBroker().getBrokerPool().getSaxonConfiguration();

final List<String> warnings = new ArrayList<>(1);
Expand Down
Loading
Loading