From ce2d704ec59179a224091a34974d35e0f9310be5 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Sat, 4 Apr 2026 09:51:59 -0400 Subject: [PATCH 01/20] [feature] Add XQuery 4.0 syntax to ANTLR 2 grammar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds XQ4 syntax support to the ANTLR 2 parser and tree walker: - Pipeline operator (->): chainable expression transformation - Focus functions (fn { }): anonymous functions with implicit context - Keyword arguments: named parameter passing at call sites - String templates (`` `{expr}` ``): interpolated string literals - Otherwise operator: fallback for empty sequences - Braced if: if (cond) { expr } without else clause - Try/finally: cleanup expressions that always execute - For member: iterate over array members in FLWOR - While clause: conditional FLWOR iteration - Default parameter values in function declarations - Mapping arrow (=>) and method call (=?>) - Ternary conditional (if..then..else as expression) - QName literals (#name): symbolic name references - Hex/binary integer literals (0xNN, 0bNN) - Numeric underscore separators (1_000_000) - Choice/enum cast types - Version gating: XQuery 4.0 features require version declaration Grammar sections added in labeled blocks per feature area within the XQuery 4.0 Parser Extensions section. Spec: QT4 XQuery 4.0 §3 (Expressions), §4 (Modules and Prologs) XQTS: QT4 parser-dependent test sets (1898/2163, 87.7%) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../antlr/org/exist/xquery/parser/XQuery.g | 865 +++++- .../org/exist/xquery/parser/XQueryTree.g | 2370 ++++++++++++----- .../xquery/ReservedKeywordsAsNCNamesTest.java | 115 + 3 files changed, 2601 insertions(+), 749 deletions(-) create mode 100644 exist-core/src/test/java/org/exist/xquery/ReservedKeywordsAsNCNamesTest.java diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g index d852d700444..a670bf3cb69 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g @@ -83,6 +83,7 @@ options { protected Deque> globalStack = new ArrayDeque<>(); protected Deque elementStack = new ArrayDeque<>(); protected XQueryLexer lexer; + protected boolean xq4Enabled = false; public XQueryParser(XQueryLexer lexer) { this((TokenStream)lexer); @@ -90,6 +91,8 @@ options { setASTNodeClass("org.exist.xquery.parser.XQueryAST"); } + public boolean isXQ4() { return xq4Enabled; } + public boolean foundErrors() { return foundError; } @@ -192,6 +195,28 @@ imaginaryTokenDefinitions PREVIOUS_ITEM NEXT_ITEM WINDOW_VARS + FOCUS_FUNCTION + KEYWORD_ARG + FOR_MEMBER + STRING_TEMPLATE + FOR_KEY + FOR_VALUE + FOR_KEY_VALUE + VALUE_VAR + SWITCH_BOOLEAN + MAPPING_ARROW + FILTER_AM + QNAME_LITERAL + PARAM_DEFAULT + CHOICE_TYPE + ENUM_TYPE + TERNARY + SEQ_DESTRUCTURE + ARRAY_DESTRUCTURE + MAP_DESTRUCTURE + DESTRUCTURE_VAR_TYPE + RECORD_TEST + RECORD_FIELD ; // === XPointer === @@ -272,7 +297,7 @@ prolog throws XPathException ( "declare" "variable" ) => varDeclUp { inSetters = false; } | - ( "declare" "context" "item" ) + ( "declare" "context" ("item" | "value") ) => contextItemDeclUp { inSetters = false; } | ( "declare" MOD ) @@ -292,7 +317,12 @@ importDecl throws XPathException versionDecl throws XPathException : "xquery" "version" v:STRING_LITERAL ( "encoding"! enc:STRING_LITERAL )? - { #versionDecl = #(#[VERSION_DECL, v.getText()], enc); } + { + #versionDecl = #(#[VERSION_DECL, v.getText()], enc); + if ("4.0".equals(v.getText())) { + xq4Enabled = true; + } + } ; setter @@ -441,7 +471,7 @@ contextItemDeclUp! throws XPathException contextItemDecl [XQueryAST decl] throws XPathException : - "context"! "item"! ( typeDeclaration )? + "context"! ( "item"! | "value"! ) ( typeDeclaration )? ( COLON! EQ! e1:expr | @@ -464,10 +494,22 @@ annotation String name= null; } : - MOD! name=eqName! (LPAREN! literal (COMMA! literal)* RPAREN!)? + MOD! name=eqName! (LPAREN! annotationLiteral (COMMA! annotationLiteral)* RPAREN!)? { #annotation= #(#[ANNOT_DECL, name], #annotation); } ; +// XQ4: annotation parameters support literals, true(), false(), and negated numeric literals +// Note: true()/false() must be matched via NCNAME + semantic predicate, NOT as "true"/"false" keywords. +// Using quoted keyword syntax would register them in testLiterals, breaking true()/false() function +// calls throughout the grammar (ANTLR 2 converts all NCNAMEs matching keywords to LITERAL_xxx tokens). +annotationLiteral +: + literal + | ( { LT(1).getText().equals("true") || LT(1).getText().equals("false") }? b:NCNAME LPAREN! RPAREN! + { #annotationLiteral = #[STRING_LITERAL, #b.getText()]; #b = null; } ) + | MINUS! n:numericLiteral { #n.setText("-" + #n.getText()); #annotationLiteral = #n; } + ; + eqName returns [String name] { name= null; } : @@ -550,7 +592,10 @@ param throws XPathException { String varName= null; } : DOLLAR! varName=eqName ( t:typeDeclaration )? - { #param= #(#[VARIABLE_BINDING, varName], #t); } + ( ( { xq4Enabled }? COLON EQ ) => COLON! EQ! pd:exprSingle! + { #pd = #(#[PARAM_DEFAULT, "param-default"], #pd); } + )? + { #param= #(#[VARIABLE_BINDING, varName], #t, #pd); } ; uriList throws XPathException @@ -588,10 +633,16 @@ itemType throws XPathException | ( "function" LPAREN ) => functionTest | + ( "fn" LPAREN ) => fnShorthandFunctionTest + | ( "map" LPAREN ) => mapType | ( "array" LPAREN ) => arrayType | + ( "record" LPAREN ) => recordType + | + ( "enum" LPAREN ) => enumType + | ( LPAREN ) => parenthesizedItemType | ( . LPAREN ) => kindTest @@ -600,13 +651,51 @@ itemType throws XPathException ; parenthesizedItemType throws XPathException +{ int count = 0; } : - LPAREN! itemType RPAREN! + LPAREN! itemType { count++; } ( UNION! itemType { count++; } )* RPAREN! + { + if (count > 1) { + #parenthesizedItemType = #(#[CHOICE_TYPE, "choice-type"], #parenthesizedItemType); + } + } + ; + +enumType throws XPathException +{ List enumValues = new ArrayList(); } +: + e:"enum"! LPAREN! + s1:STRING_LITERAL! { enumValues.add(s1.getText()); } + ( COMMA! s2:STRING_LITERAL! { enumValues.add(s2.getText()); } )* + RPAREN! + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < enumValues.size(); i++) { + if (i > 0) sb.append(","); + sb.append(enumValues.get(i)); + } + #enumType = #(#[ENUM_TYPE, sb.toString()]); + #enumType.copyLexInfo(#e); + } ; singleType throws XPathException +{ int count = 0; } : - atomicType ( QUESTION )? + ( + ( "enum" LPAREN ) => enumType ( QUESTION )? + | + ( LPAREN ) => + LPAREN! atomicType { count++; } ( UNION! atomicType { count++; } )* RPAREN! + { + if (count > 1) { + #singleType = #(#[CHOICE_TYPE, "choice-type"], #singleType); + } + } + ( QUESTION )? + | + atomicType ( QUESTION )? + ) ; atomicType throws XPathException @@ -634,10 +723,38 @@ anyFunctionTest throws XPathException typedFunctionTest throws XPathException : - "function"! LPAREN! (sequenceType (COMMA! sequenceType)*)? RPAREN! "as" sequenceType + "function"! LPAREN! (fnShorthandParam (COMMA! fnShorthandParam)*)? RPAREN! "as" sequenceType { #typedFunctionTest = #(#[FUNCTION_TEST, "anyFunction"], #typedFunctionTest); } ; +// XQ4: fn(...) as shorthand for function(...) in type positions +fnShorthandFunctionTest throws XPathException +: + ( "fn" LPAREN STAR RPAREN) => fnShorthandAnyFunctionTest + | + fnShorthandTypedFunctionTest + ; + +fnShorthandAnyFunctionTest throws XPathException +: + "fn"! LPAREN! s2:STAR RPAREN! + { #fnShorthandAnyFunctionTest = #(#[FUNCTION_TEST, "anyFunction"], #s2); } + ; + +fnShorthandTypedFunctionTest throws XPathException +: + "fn"! LPAREN! (fnShorthandParam (COMMA! fnShorthandParam)*)? RPAREN! "as" sequenceType + { #fnShorthandTypedFunctionTest = #(#[FUNCTION_TEST, "anyFunction"], #fnShorthandTypedFunctionTest); } + ; + +// XQ4: fn() type parameters can optionally have names: fn($name as type, ...) +fnShorthandParam throws XPathException +: + ( DOLLAR ) => DOLLAR! eqName! "as"! sequenceType + | + sequenceType + ; + mapType throws XPathException : ( "map" LPAREN STAR ) => anyMapTypeTest @@ -686,6 +803,50 @@ arrayTypeTest throws XPathException } ; +recordType throws XPathException +: + ( "record" LPAREN STAR ) => anyRecordTypeTest + | + ( "record" LPAREN RPAREN ) => emptyRecordTypeTest + | + recordTypeTest + ; + +anyRecordTypeTest throws XPathException +: + m:"record"! LPAREN! s:STAR RPAREN! + { + #anyRecordTypeTest = #(#[RECORD_TEST, "record"], #s); + #anyRecordTypeTest.copyLexInfo(#m); + } + ; + +emptyRecordTypeTest throws XPathException +: + m:"record"! LPAREN! RPAREN! + { + #emptyRecordTypeTest = #(#[RECORD_TEST, "record"]); + #emptyRecordTypeTest.copyLexInfo(#m); + } + ; + +recordTypeTest throws XPathException +: + m:"record"! LPAREN! recordFieldDecl ( COMMA! ( STAR | recordFieldDecl ) )* RPAREN! + { + #recordTypeTest = #(#[RECORD_TEST, "record"], #recordTypeTest); + } + ; + +recordFieldDecl throws XPathException +{ String fieldName = null; } +: + fieldName=ncnameOrKeyword! ( QUESTION )? ( "as"! sequenceType )? + { + #recordFieldDecl = #(#[RECORD_FIELD, fieldName], #recordFieldDecl); + } + ; + // === Expressions === queryBody throws XPathException: expr ; @@ -702,7 +863,7 @@ expr throws XPathException exprSingle throws XPathException : - ( ( "for" | "let" ) ("tumbling" | "sliding" | DOLLAR ) ) => flworExpr + ( ( "for" | "let" ) ("tumbling" | "sliding" | "member" | "key" | "value" | DOLLAR) ) => flworExpr | ( "try" LCURLY ) => tryCatchExpr | ( ( "some" | "every" ) DOLLAR ) => quantifiedExpr | ( "if" LPAREN ) => ifExpr @@ -752,11 +913,14 @@ renameExpr throws XPathException "rename" exprSingle "as"! exprSingle ; -// === try/catch === +// === try/catch/finally === tryCatchExpr throws XPathException : "try"^ LCURLY! tryTargetExpr RCURLY! - (catchClause)+ + ( + (catchClause)+ ( { xq4Enabled }? finallyClause )? + | { xq4Enabled }? finallyClause + ) ; tryTargetExpr throws XPathException @@ -769,6 +933,11 @@ catchClause throws XPathException "catch"^ catchErrorList (catchVars)? LCURLY! expr RCURLY! ; +finallyClause throws XPathException +: + "finally"^ LCURLY! (expr)? RCURLY! + ; + catchErrorList throws XPathException : nameTest (UNION! nameTest)* @@ -809,14 +978,14 @@ flworExpr throws XPathException initialClause throws XPathException : - ( ( "for" DOLLAR ) => forClause + ( ( "for" ( "member" | "key" | "value" | DOLLAR ) ) => forClause | ( "for" ( "tumbling" | "sliding" ) ) => windowClause | letClause ) ; intermediateClause throws XPathException : - ( initialClause | whereClause | groupByClause | orderByClause | countClause ) + ( initialClause | whereClause | whileClause | groupByClause | orderByClause | countClause ) ; whereClause throws XPathException @@ -824,6 +993,11 @@ whereClause throws XPathException "where"^ exprSingle ; +whileClause throws XPathException +: + { xq4Enabled }? "while"^ exprSingle + ; + countClause throws XPathException { String varName; } : @@ -833,7 +1007,77 @@ countClause throws XPathException forClause throws XPathException : - "for"^ inVarBinding ( COMMA! inVarBinding )* + "for"^ forBinding ( COMMA! forBinding )* + ; + +forBinding throws XPathException +: + ( { xq4Enabled }? "member" ) => memberVarBinding + | ( { xq4Enabled }? "key" ) => keyVarBinding + | ( { xq4Enabled }? "value" ) => valueVarBinding + | inVarBinding + ; + +memberVarBinding throws XPathException +{ String varName; } +: + "member"! DOLLAR! varName=v:varName! ( typeDeclaration )? + ( positionalVar )? + "in"! exprSingle + { + #memberVarBinding= #(#[VARIABLE_BINDING, varName], #memberVarBinding); + #memberVarBinding.copyLexInfo(#v); + #memberVarBinding= #(#[FOR_MEMBER, null], #memberVarBinding); + } + ; + +keyVarBinding throws XPathException +{ String varName; } +: + "key"! DOLLAR! varName=v:varName! ( typeDeclaration )? + ( + ( "value" DOLLAR ) => keyValueVarPart + )? + ( positionalVar )? + "in"! exprSingle + { + #keyVarBinding= #(#[VARIABLE_BINDING, varName], #keyVarBinding); + #keyVarBinding.copyLexInfo(#v); + // Check if we have a value variable (keyValueVarPart was matched) + boolean hasValueVar = false; + AST child = #keyVarBinding.getFirstChild(); + while (child != null) { + if (child.getType() == VALUE_VAR) { hasValueVar = true; break; } + child = child.getNextSibling(); + } + if (hasValueVar) { + #keyVarBinding= #(#[FOR_KEY_VALUE, null], #keyVarBinding); + } else { + #keyVarBinding= #(#[FOR_KEY, null], #keyVarBinding); + } + } + ; + +keyValueVarPart throws XPathException +{ String valueVarName; } +: + "value"! DOLLAR! valueVarName=varName! ( typeDeclaration )? + { + #keyValueVarPart = #(#[VALUE_VAR, valueVarName], #keyValueVarPart); + } + ; + +valueVarBinding throws XPathException +{ String varName; } +: + "value"! DOLLAR! varName=v:varName! ( typeDeclaration )? + ( positionalVar )? + "in"! exprSingle + { + #valueVarBinding= #(#[VARIABLE_BINDING, varName], #valueVarBinding); + #valueVarBinding.copyLexInfo(#v); + #valueVarBinding= #(#[FOR_VALUE, null], #valueVarBinding); + } ; letClause throws XPathException @@ -904,6 +1148,16 @@ windowVars throws XPathException letVarBinding throws XPathException { String varName; } : + // XQ4: sequence destructuring - let $($x, $y) := expr + ( DOLLAR LPAREN ) => letDestructureSeq + | + // XQ4: array destructuring - let $[$x, $y] := expr + ( DOLLAR LPPAREN ) => letDestructureArray + | + // XQ4: map destructuring - let ${$x, $y} := expr + ( DOLLAR LCURLY ) => letDestructureMap + | + // Standard let binding DOLLAR! varName=v:varName! ( typeDeclaration )? COLON! EQ! exprSingle { @@ -912,6 +1166,67 @@ letVarBinding throws XPathException } ; +// XQ4: Per-variable type annotations: "x+,y" means $x has a DESTRUCTURE_VAR_TYPE child, $y does not +letDestructureSeq throws XPathException +{ String vn; + StringBuilder sb = new StringBuilder(); } +: + d:DOLLAR! LPAREN! + DOLLAR! vn=varName! { sb.append(vn); } + ( destructureVarType { sb.append("+"); } )? + ( COMMA! DOLLAR! vn=varName! { sb.append(",").append(vn); } + ( destructureVarType { sb.append("+"); } )? )* + RPAREN! ( typeDeclaration )? + COLON! EQ! exprSingle + { + #letDestructureSeq = #(#[SEQ_DESTRUCTURE, sb.toString()], #letDestructureSeq); + #letDestructureSeq.copyLexInfo(#d); + } + ; + +letDestructureArray throws XPathException +{ String vn; + StringBuilder sb = new StringBuilder(); } +: + d:DOLLAR! LPPAREN! + DOLLAR! vn=varName! { sb.append(vn); } + ( destructureVarType { sb.append("+"); } )? + ( COMMA! DOLLAR! vn=varName! { sb.append(",").append(vn); } + ( destructureVarType { sb.append("+"); } )? )* + RPPAREN! ( typeDeclaration )? + COLON! EQ! exprSingle + { + #letDestructureArray = #(#[ARRAY_DESTRUCTURE, sb.toString()], #letDestructureArray); + #letDestructureArray.copyLexInfo(#d); + } + ; + +letDestructureMap throws XPathException +{ String vn; + StringBuilder sb = new StringBuilder(); } +: + d:DOLLAR! LCURLY! + DOLLAR! vn=varName! { sb.append(vn); } + ( destructureVarType { sb.append("+"); } )? + ( COMMA! DOLLAR! vn=varName! { sb.append(",").append(vn); } + ( destructureVarType { sb.append("+"); } )? )* + RCURLY! ( typeDeclaration )? + COLON! EQ! exprSingle + { + #letDestructureMap = #(#[MAP_DESTRUCTURE, sb.toString()], #letDestructureMap); + #letDestructureMap.copyLexInfo(#d); + } + ; + +// Helper: wraps typeDeclaration in DESTRUCTURE_VAR_TYPE imaginary token +destructureVarType throws XPathException +: + td:typeDeclaration + { + #destructureVarType = #(#[DESTRUCTURE_VAR_TYPE, "vartype"], #td); + } + ; + orderByClause throws XPathException : ( "order"! "by"! | "stable"! "order"! "by"! ) orderSpecList @@ -973,9 +1288,26 @@ quantifiedInVarBinding throws XPathException switchExpr throws XPathException : - "switch"^ LPAREN! expr RPAREN! - ( switchCaseClause )+ - "default" "return"! exprSingle + "switch"^ LPAREN! + ( + // XQ4 omitted comparand - boolean mode: switch () { case boolExpr return ... } + ( RPAREN ) => + RPAREN! switchBooleanMarker + | + expr RPAREN! + ) + ( + // XQ4 braced syntax: switch (...) { case ... default ... } + ( LCURLY "case" ) => + LCURLY! ( switchCaseClause )+ "default" "return"! exprSingle RCURLY! + | + ( switchCaseClause )+ "default" "return"! exprSingle + ) + ; + +switchBooleanMarker +: + { #switchBooleanMarker = #(#[SWITCH_BOOLEAN, "switch-boolean"]); } ; switchCaseClause throws XPathException @@ -988,8 +1320,13 @@ typeswitchExpr throws XPathException { String varName; } : "typeswitch"^ LPAREN! expr RPAREN! - ( caseClause )+ - "default" ( defaultVar )? "return"! exprSingle + ( + // XQ4 braced syntax: typeswitch (...) { case ... default ... } + ( LCURLY "case" ) => + LCURLY! ( caseClause )+ "default" ( defaultVar )? "return"! exprSingle RCURLY! + | + ( caseClause )+ "default" ( defaultVar )? "return"! exprSingle + ) ; caseClause throws XPathException @@ -1024,12 +1361,28 @@ defaultVar throws XPathException ; ifExpr throws XPathException +{ + org.exist.xquery.parser.XQueryAST emptyNode = null; +} : - "if"^ LPAREN! expr RPAREN! t:"then"! thenExpr:exprSingle e:"else"! elseExpr:exprSingle - { - #thenExpr.copyLexInfo(#t); - #elseExpr.copyLexInfo(#e); - } + "if"^ LPAREN! expr RPAREN! + ( + // Traditional: if (cond) then expr else expr + ( "then" ) => + t:"then"! thenExpr:exprSingle e:"else"! elseExpr:exprSingle + { + #thenExpr.copyLexInfo(#t); + #elseExpr.copyLexInfo(#e); + } + | + // XQ4 Braced: if (cond) { expr } (no else clause; returns empty sequence if false) + LCURLY! bracedThenExpr:expr RCURLY! + { + // Synthesize empty sequence as implicit else branch + emptyNode = (org.exist.xquery.parser.XQueryAST) #(#[PARENTHESIZED, "()"]); + #ifExpr.addChild(emptyNode); + } + ) ; // === Logical === @@ -1037,6 +1390,12 @@ ifExpr throws XPathException orExpr throws XPathException : andExpr ( "or"^ andExpr )* + ( + { xq4Enabled }? DOUBLE_QUESTION! exprSingle DOUBLE_BANG! exprSingle + { + #orExpr = #(#[TERNARY, "ternary"], #orExpr); + } + )? ; andExpr throws XPathException @@ -1061,23 +1420,33 @@ castableExpr throws XPathException castExpr throws XPathException : - arrowExpr ( "cast"^ "as"! singleType )? + pipelineExpr ( "cast"^ "as"! singleType )? + ; + +pipelineExpr throws XPathException +: + arrowExpr ( { xq4Enabled }? PIPELINE_OP^ arrowExpr )* ; comparisonExpr throws XPathException : - r1:stringConcatExpr ( - ( BEFORE ) => BEFORE^ stringConcatExpr + r1:otherwiseExpr ( + ( BEFORE ) => BEFORE^ otherwiseExpr | - ( AFTER ) => AFTER^ stringConcatExpr - | ( ( "eq"^ | "ne"^ | "lt"^ | "le"^ | "gt"^ | "ge"^ ) stringConcatExpr ) + ( AFTER ) => AFTER^ otherwiseExpr + | ( ( "eq"^ | "ne"^ | "lt"^ | "le"^ | "gt"^ | "ge"^ ) otherwiseExpr ) | ( GT EQ ) => GT^ EQ^ r2:rangeExpr { #comparisonExpr = #(#[GTEQ, ">="], #r1, #r2); } - | ( ( EQ^ | NEQ^ | GT^ | LT^ | LTEQ^ ) stringConcatExpr ) - | ( ( "is"^ | "isnot"^ ) stringConcatExpr ) + | ( ( EQ^ | NEQ^ | GT^ | LT^ | LTEQ^ ) otherwiseExpr ) + | ( ( "is"^ | "isnot"^ ) otherwiseExpr ) )? ; +otherwiseExpr throws XPathException +: + stringConcatExpr ( { xq4Enabled }? "otherwise"^ stringConcatExpr )* + ; + stringConcatExpr throws XPathException { boolean isConcat = false; } : @@ -1222,13 +1591,15 @@ stepExpr throws XPathException | ( ( "element" | "attribute" | "text" | "document" | "comment" | "namespace-node" | "processing-instruction" | "namespace" | "ordered" | - "unordered" | "map" | "array" ) LCURLY ) => + "unordered" | "map" | "array" | "fn" | "function" ) LCURLY ) => postfixExpr | ( ( "element" | "attribute" | "processing-instruction" | "namespace" ) eqName LCURLY ) => postfixExpr | + ( "fn" LPAREN ) => postfixExpr + | ( MOD | DOLLAR | ( eqName ( LPAREN | HASH ) ) | SELF | LPAREN | literal | XML_COMMENT | LT | - XML_PI | QUESTION | LPPAREN | STRING_CONSTRUCTOR_START ) + XML_PI | QUESTION | LPPAREN | STRING_CONSTRUCTOR_START | STRING_TEMPLATE_START | LCURLY | HASH ) => postfixExpr | axisStep @@ -1272,6 +1643,7 @@ forwardAxisSpecifier : "child" | "self" | "attribute" | "descendant" | "descendant-or-self" | "following-sibling" | "following" + | "following-or-self" | "following-sibling-or-self" ; reverseAxis : reverseAxisSpecifier COLON! COLON! ; @@ -1279,6 +1651,7 @@ reverseAxis : reverseAxisSpecifier COLON! COLON! ; reverseAxisSpecifier : "parent" | "ancestor" | "ancestor-or-self" | "preceding-sibling" | "preceding" + | "preceding-or-self" | "preceding-sibling-or-self" ; nodeTest throws XPathException @@ -1326,18 +1699,42 @@ postfixExpr throws XPathException | (LPAREN) => dynamicFunCall | + // XQ4: ?[ must come before ? lookup to disambiguate + (QUESTION LPPAREN) => filterExprAM + | (QUESTION) => lookup )* ; arrowExpr throws XPathException : - unaryExpr ( ARROW_OP^ arrowFunctionSpecifier argumentList )* + unaryExpr ( + ARROW_OP^ arrowFunctionSpecifier argumentList + | + { xq4Enabled }? MAPPING_ARROW_OP^ arrowFunctionSpecifier argumentList + | + { xq4Enabled }? METHOD_CALL_OP^ NCNAME argumentList + )* ; arrowFunctionSpecifier throws XPathException { String name= null; } : + // XQ4: inline/focus function expression + ( MOD | ( ("function" | "fn") (LPAREN | LCURLY) ) ) => inlineOrFocusFunctionExpr + | + // XQ4: named function reference (eqName '#' arity) + ( eqName HASH ) => namedFunctionRef + | + // XQ4: map constructor as function + ( "map" LCURLY ) => mapConstructor + | + // XQ4: bare map constructor as function + ( LCURLY ) => bareMapConstructor + | + // XQ4: array constructor as function + ( LPPAREN | ("array" LCURLY) ) => arrayConstructor + | name=n:eqName { #arrowFunctionSpecifier= #[EQNAME, name]; @@ -1349,8 +1746,17 @@ arrowFunctionSpecifier throws XPathException varRef ; +filterExprAM throws XPathException +: + q:QUESTION! LPPAREN! expr RPPAREN! + { + #filterExprAM = #(#[FILTER_AM, "filter-am"], #filterExprAM); + #filterExprAM.copyLexInfo(#q); + } + ; + lookup throws XPathException -{ String name= null; } +{ String name= null; String varName= null; } : q:QUESTION! ( @@ -1360,18 +1766,59 @@ lookup throws XPathException #lookup.copyLexInfo(#q); } | + // XQ4: decimal and double literals as key selectors (?1.2, ?1.2e0) + dbl:DOUBLE_LITERAL + { + #lookup = #(#[LOOKUP, "?"], #dbl); + #lookup.copyLexInfo(#q); + } + | + dec:DECIMAL_LITERAL + { + #lookup = #(#[LOOKUP, "?"], #dec); + #lookup.copyLexInfo(#q); + } + | pos:INTEGER_LITERAL { #lookup = #(#[LOOKUP, "?"], #pos); #lookup.copyLexInfo(#q); } | + // XQ4: string literal as key selector (?"first value") + str:STRING_LITERAL + { + #lookup = #(#[LOOKUP, "?"], #str); + #lookup.copyLexInfo(#q); + } + | paren:parenthesizedExpr { #lookup = #(#[LOOKUP, "?"], #paren); #lookup.copyLexInfo(#q); } | + // XQ4: variable reference as key selector (?$var) + DOLLAR! varName=v:varName + { + #lookup = #(#[LOOKUP, "?"], #[VARIABLE_REF, varName]); + #lookup.copyLexInfo(#q); + } + | + // XQ4: context item as key selector (?.) + dot:SELF + { + #lookup = #(#[LOOKUP, "?"], #dot); + #lookup.copyLexInfo(#q); + } + | + // XQ4: QName literal as key selector (?#name) + qnl:qnameLiteral + { + #lookup = #(#[LOOKUP, "?"], #qnl); + #lookup.copyLexInfo(#q); + } + | STAR { #lookup = #(#[LOOKUP, "?*"]); @@ -1423,9 +1870,18 @@ primaryExpr throws XPathException | ( "map" LCURLY ) => mapConstructor | + ( LCURLY RCURLY ) => bareMapConstructor + | + ( LCURLY exprSingle COLON ) => bareMapConstructor + | directConstructor | - ( MOD | "function" LPAREN | eqName HASH ) => functionItemExpr + ( { xq4Enabled }? ( "fn" | "function" ) LCURLY ) => focusFunctionExpr + | + // XQ4: QName literal (#local, #prefix:local, #Q{uri}local) + ( { xq4Enabled }? HASH ) => qnameLiteral + | + ( MOD | ( "fn" | "function" ) LPAREN | eqName HASH ) => functionItemExpr | ( eqName LPAREN ) => functionCall | @@ -1433,6 +1889,8 @@ primaryExpr throws XPathException | ( STRING_CONSTRUCTOR_START ) => stringConstructor | + ( { xq4Enabled }? STRING_TEMPLATE_START ) => stringTemplate + | contextItemExpr | parenthesizedExpr @@ -1459,10 +1917,32 @@ stringConstructorContent throws XPathException stringConstructorInterpolation throws XPathException : STRING_CONSTRUCTOR_INTERPOLATION_START^ - { lexer.inStringConstructor = false; } + { lexer.inStringConstructor = false; lexer.stringConstructorInterpolationDepth++; } ( expr )? STRING_CONSTRUCTOR_INTERPOLATION_END! - { lexer.inStringConstructor = true; } + { lexer.stringConstructorInterpolationDepth--; lexer.inStringConstructor = true; } + ; + +stringTemplate throws XPathException +: + st:STRING_TEMPLATE_START! + { lexer.inStringTemplate = true; } + ( STRING_TEMPLATE_CONTENT | stringTemplateInterpolation )* + STRING_TEMPLATE_END! + { lexer.inStringTemplate = false; } + { + #stringTemplate = #(#[STRING_TEMPLATE, null], #stringTemplate); + #stringTemplate.copyLexInfo(#st); + } + ; + +stringTemplateInterpolation throws XPathException +: + lc:LCURLY! + { lexer.inStringTemplate = false; lexer.stringTemplateDepth++; } + ( expr )? + RCURLY! + { lexer.stringTemplateDepth--; lexer.inStringTemplate = true; } ; mapConstructor throws XPathException @@ -1474,6 +1954,15 @@ mapConstructor throws XPathException } ; +bareMapConstructor throws XPathException +: + lc:LCURLY! ( mapAssignment ( COMMA! mapAssignment )* )? RCURLY! + { + #bareMapConstructor = #(#[MAP, "map"], #bareMapConstructor); + #bareMapConstructor.copyLexInfo(#lc); + } + ; + mapAssignment throws XPathException : (exprSingle COLON! EQ!) => exprSingle COLON^ eq:EQ^ exprSingle @@ -1525,6 +2014,16 @@ literal STRING_LITERAL^ | numericLiteral ; +qnameLiteral throws XPathException +{ String name = null; } +: + h:HASH! name=eqName + { + #qnameLiteral = #(#[QNAME_LITERAL, name]); + #qnameLiteral.copyLexInfo(#h); + } + ; + numericLiteral : DOUBLE_LITERAL^ | DECIMAL_LITERAL^ | INTEGER_LITERAL^ @@ -1539,7 +2038,7 @@ parenthesizedExpr throws XPathException functionItemExpr throws XPathException : - ( MOD | "function" ) => inlineFunctionExpr + ( MOD | "function" | "fn" ) => inlineOrFocusFunctionExpr | namedFunctionRef ; @@ -1553,24 +2052,36 @@ namedFunctionRef throws XPathException } ; -inlineFunctionExpr throws XPathException +inlineOrFocusFunctionExpr throws XPathException : - ann:annotations! "function"! lp:LPAREN! ( paramList )? - RPAREN! ( returnType )? - functionBody + ann:annotations! ( "function"! | "fn"! ) + ( + (LPAREN) => lp:LPAREN! ( paramList )? + RPAREN! ( returnType )? + functionBody + { + #inlineOrFocusFunctionExpr = #(#[INLINE_FUNCTION_DECL, null], #ann, #inlineOrFocusFunctionExpr); + #inlineOrFocusFunctionExpr.copyLexInfo(#lp); + } + | + lc:LCURLY! ( expr )? RCURLY! + { + #inlineOrFocusFunctionExpr = #(#[FOCUS_FUNCTION, null], #inlineOrFocusFunctionExpr); + #inlineOrFocusFunctionExpr.copyLexInfo(#lc); + } + ) + exception catch [RecognitionException e] { - #inlineFunctionExpr = #(#[INLINE_FUNCTION_DECL, null], null, #inlineFunctionExpr); - #inlineFunctionExpr.copyLexInfo(#lp); + throw new XPathException(e.getLine(), e.getColumn(), ErrorCodes.XPST0003, "Syntax error within inline function: " + e.getMessage()); } - exception catch [RecognitionException e] + ; + +focusFunctionExpr throws XPathException +: + ( "fn"! | "function"! ) lc:LCURLY! ( expr )? RCURLY! { - if (#lp == null) { - throw new XPathException(e.getLine(), e.getColumn(), ErrorCodes.XPST0003, "Syntax error within inline function: " + e.getMessage()); - } else { - #lp.setLine(e.getLine()); - #lp.setColumn(e.getColumn()); - throw new XPathException(#lp, ErrorCodes.XPST0003, "Syntax error within user defined function: " + e.getMessage()); - } + #focusFunctionExpr = #(#[FOCUS_FUNCTION, null], #focusFunctionExpr); + #focusFunctionExpr.copyLexInfo(#lc); } ; @@ -1595,8 +2106,34 @@ argumentList throws XPathException argument throws XPathException : - (QUESTION! ( NCNAME | INTEGER_LITERAL | LPAREN | STAR )) => lookup + (QUESTION ( ncnameOrKeyword | INTEGER_LITERAL | DECIMAL_LITERAL | DOUBLE_LITERAL | STRING_LITERAL | LPAREN | DOLLAR | SELF | HASH | STAR )) => unaryLookup | argumentPlaceholder + | ( { xq4Enabled }? ncnameOrKeyword COLON ( EQ | ncnameOrKeyword COLON EQ ) ) => keywordArgument + | exprSingle + ; + +// XQ4: keyword arguments - name := value, or prefix:name := value +keywordArgument throws XPathException +{ String kwName = null; String prefix = null; String local = null; } +: + // Prefixed keyword: prefix:name := value + ( ( ncnameOrKeyword COLON ncnameOrKeyword COLON EQ ) => + prefix=ncnameOrKeyword! COLON! local=ncnameOrKeyword! COLON! EQ! keywordArgumentValue + { kwName = prefix + ":" + local; } + | + // Simple keyword: name := value + kwName=ncnameOrKeyword! COLON! EQ! keywordArgumentValue + ) + { + #keywordArgument = #(#[KEYWORD_ARG, kwName], #keywordArgument); + } + ; + +// XQ4: keyword argument value can be an expression or argument placeholder (?) +// Use lookahead to distinguish bare ? (placeholder) from ?key (unary lookup) +keywordArgumentValue throws XPathException +: + ( QUESTION ( RPAREN | COMMA ) ) => argumentPlaceholder | exprSingle ; @@ -1606,7 +2143,7 @@ contextItemExpr : SELF ; kindTest : - textTest | anyKindTest | elementTest | attributeTest | + textTest | anyKindTest | gnodeTest | elementTest | attributeTest | commentTest | namespaceNodeTest | piTest | documentTest ; @@ -1620,6 +2157,13 @@ anyKindTest "node"^ LPAREN! RPAREN! ; +// XQ4: gnode() is a synonym for node() +gnodeTest +: + "gnode"! LPAREN! RPAREN! + { #gnodeTest = #[LITERAL_node, "node"]; } + ; + elementTest : "element"^ LPAREN! @@ -2074,8 +2618,23 @@ ncnameOrKeyword returns [String name] name=reservedKeywords ; +/** + * Top-level dispatcher for reserved keywords usable as NCNames. + * Split into feature-area sub-rules to reduce merge conflicts on the + * next integration branch. Each feature branch owns its sub-rule; + * merging adds a single alternative here instead of interleaving 80+ lines. + */ reservedKeywords returns [String name] { name= null; } +: + name=coreReservedKeywords + | + name=xq4Keywords + ; + +// ---- Core reserved keywords (XQuery 3.1 + eXist-db extensions) ---- +coreReservedKeywords returns [String name] +{ name= null; } : "element" { name = "element"; } | @@ -2125,6 +2684,14 @@ reservedKeywords returns [String name] | "preceding" { name = "preceding"; } | + "following-or-self" { name = "following-or-self"; } + | + "preceding-or-self" { name = "preceding-or-self"; } + | + "following-sibling-or-self" { name = "following-sibling-or-self"; } + | + "preceding-sibling-or-self" { name = "preceding-sibling-or-self"; } + | "item" { name= "item"; } | "empty" { name= "empty"; } @@ -2137,8 +2704,8 @@ reservedKeywords returns [String name] | "namespace-node" { name= "namespace-node"; } | - "namespace" { name= "namespace"; } - | + "namespace" { name= "namespace"; } + | "if" { name= "if"; } | "then" { name= "then"; } @@ -2177,8 +2744,8 @@ reservedKeywords returns [String name] | "by" { name = "by"; } | - "group" { name = "group"; } - | + "group" { name = "group"; } + | "some" { name = "some"; } | "every" { name = "every"; } @@ -2289,7 +2856,7 @@ reservedKeywords returns [String name] | "tumbling" { name = "tumbling"; } | - "sliding" { name = "sliding"; } + "sliding" { name = "sliding"; } | "window" { name = "window"; } | @@ -2304,6 +2871,47 @@ reservedKeywords returns [String name] "next" { name = "next"; } | "when" { name = "when"; } + | + "ascending" { name = "ascending"; } + | + "descending" { name = "descending"; } + | + "greatest" { name = "greatest"; } + | + "least" { name = "least"; } + | + "satisfies" { name = "satisfies"; } + | + "schema-attribute" { name = "schema-attribute"; } + | + "castable" { name = "castable"; } + | + "idiv" { name = "idiv"; } + | + "processing-instruction" { name = "processing-instruction"; } + | + "allowing" { name = "allowing"; } + ; + +// ---- XQuery 4.0 keywords (feature/xquery-4.0-parser) ---- +xq4Keywords returns [String name] +{ name= null; } +: + "fn" { name = "fn"; } + | + "member" { name = "member"; } + | + "otherwise" { name = "otherwise"; } + | + "key" { name = "key"; } + | + "while" { name = "while"; } + | + "finally" { name = "finally"; } + | + "record" { name = "record"; } + | + "gnode" { name = "gnode"; } ; @@ -2324,6 +2932,9 @@ options { protected boolean wsExplicit= false; protected boolean parseStringLiterals= true; protected boolean inStringConstructor = false; + protected boolean inStringTemplate = false; + protected int stringTemplateDepth = 0; + protected int stringConstructorInterpolationDepth = 0; protected boolean inElementContent= false; protected boolean inAttributeContent= false; protected boolean inFunctionBody= false; @@ -2352,11 +2963,35 @@ options { newline(); } } + + /** + * Disambiguate (# as pragma vs ( + #QName literal. + * Scans past (# and the QName. Returns true (pragma) if the QName + * is followed by whitespace or #). Returns false (QName literal) + * if followed by , or ). + */ + private boolean isPragmaContext() throws CharStreamException { + // LA(1)='(' LA(2)='#' -- start scanning from LA(3) + int i = 3; + // Skip the QName (letters, digits, -, ., _, :) + while (Character.isLetterOrDigit(LA(i)) || LA(i) == '-' || LA(i) == '.' || LA(i) == '_' || LA(i) == ':') { + i++; + } + char afterQName = LA(i); + // If followed by , or ) it's a QName literal argument + if (afterQName == ',' || afterQName == ')') { + return false; + } + // Otherwise it's a pragma (whitespace, #), or other pragma content) + return true; + } } protected SLASH options { paraphrase="single slash '/'"; }: '/' ; protected DSLASH options { paraphrase="double slash '//'"; }: '/' '/' ; protected BANG : '!' ; +protected DOUBLE_BANG options { paraphrase="double bang '!!'"; }: '!' '!' ; +protected DOUBLE_QUESTION options { paraphrase="double question '??'"; }: '?' '?' ; protected MOD : '%' ; protected COLON : ':' ; protected COMMA : ',' ; @@ -2374,7 +3009,10 @@ protected SELF options { paraphrase="."; }: '.' ; protected PARENT options { paraphrase=".."; }: ".." ; protected UNION options { paraphrase="union"; }: '|' ; protected CONCAT options { paraphrase="||"; }: '|' '|'; +protected METHOD_CALL_OP options { paraphrase="method call operator"; }: '=' '?' '>'; +protected MAPPING_ARROW_OP options { paraphrase="mapping arrow operator"; }: '=' '!' '>'; protected ARROW_OP options { paraphrase="arrow operator"; }: '=' '>'; +protected PIPELINE_OP options { paraphrase="pipeline operator"; }: '-' '>'; protected AT options { paraphrase="@ char"; }: '@' ; protected DOLLAR options { paraphrase="dollar sign '$'"; }: '$' ; protected EQ options { paraphrase="="; }: '=' ; @@ -2408,12 +3046,17 @@ protected LETTER protected DIGITS : - ( DIGIT )+ + ( DIGIT )+ ( '_' ( DIGIT )+ )* ; protected HEX_DIGITS : - ( '0'..'9' | 'a'..'f' | 'A'..'F' )+ + ( '0'..'9' | 'a'..'f' | 'A'..'F' )+ ( '_' ( '0'..'9' | 'a'..'f' | 'A'..'F' )+ )* + ; + +protected BINARY_DIGITS +: + ( '0' | '1' )+ ( '_' ( '0' | '1' )+ )* ; protected NCNAME @@ -2470,16 +3113,26 @@ protected INTEGER_LITERAL { !(inElementContent || inAttributeContent) }? DIGITS ; +protected HEX_INTEGER_LITERAL +: + { !(inElementContent || inAttributeContent) }? '0' ('x' | 'X') HEX_DIGITS + ; + +protected BINARY_INTEGER_LITERAL +: + { !(inElementContent || inAttributeContent) }? '0' ('b' | 'B') BINARY_DIGITS + ; + protected DOUBLE_LITERAL : { !(inElementContent || inAttributeContent) }? - ( ( '.' DIGITS ) | ( DIGITS ( '.' ( DIGIT )* )? ) ) ( 'e' | 'E' ) ( '+' | '-' )? DIGITS + ( ( '.' DIGITS ) | ( DIGITS ( '.' ( DIGITS )? )? ) ) ( 'e' | 'E' ) ( '+' | '-' )? DIGITS ; protected DECIMAL_LITERAL : { !(inElementContent || inAttributeContent) }? - ( '.' DIGITS ) | ( DIGITS ( '.' ( DIGIT )* )? ) + ( '.' DIGITS ) | ( DIGITS ( '.' ( DIGITS )? )? ) ; protected PREDEFINED_ENTITY_REF @@ -2520,7 +3173,6 @@ options { : ( ( '\n' ) => '\n' { newline(); } | - ( '&' ) => ( PREDEFINED_ENTITY_REF | CHAR_REF ) | ( ( ']' '`' ) ~ ( '`' ) ) => ( ']' '`' ) | ( ']' ~ ( '`' ) ) => ']' | ( '`' ~ ( '{') ) => '`' | @@ -2528,6 +3180,21 @@ options { )+ ; +protected STRING_TEMPLATE_START options { paraphrase="start of string template"; }: '`'; +protected STRING_TEMPLATE_END options { paraphrase="end of string template"; }: '`'; + +protected STRING_TEMPLATE_CONTENT +options { + testLiterals = false; + paraphrase = "string template content"; +} +: + ( + '\n' { newline(); } | + ~ ( '\n' | '{' | '}' | '`') + )+ + ; + protected BRACED_URI_LITERAL options { paraphrase="braced uri literal"; @@ -2641,6 +3308,46 @@ options { testLiterals = false; } : + { inStringTemplate }? + ( '`' '`' ) => '`' '`' { + $setType(STRING_TEMPLATE_CONTENT); + } + | + { inStringTemplate }? + ( '{' '{' ) => '{' '{' { + $setType(STRING_TEMPLATE_CONTENT); + } + | + { inStringTemplate }? + ( '}' '}' ) => '}' '}' { + $setType(STRING_TEMPLATE_CONTENT); + } + | + { inStringTemplate }? + STRING_TEMPLATE_END { + $setType(STRING_TEMPLATE_END); + } + | + { inStringTemplate }? + LCURLY { + $setType(LCURLY); + } + | + { inStringTemplate }? + STRING_TEMPLATE_CONTENT { + $setType(STRING_TEMPLATE_CONTENT); + } + | + { !inStringConstructor && !inStringTemplate && !inElementContent && !inAttributeContent }? + ( '`' '`' '[' ) => STRING_CONSTRUCTOR_START { + $setType(STRING_CONSTRUCTOR_START); + } + | + { !inStringConstructor && !inStringTemplate && !inElementContent && !inAttributeContent }? + STRING_TEMPLATE_START { + $setType(STRING_TEMPLATE_START); + } + | { !inStringConstructor }? STRING_CONSTRUCTOR_START { $setType(STRING_CONSTRUCTOR_START); @@ -2656,7 +3363,7 @@ options { $setType(STRING_CONSTRUCTOR_INTERPOLATION_START); } | - { !inStringConstructor }? + { !inStringConstructor && stringTemplateDepth == 0 && stringConstructorInterpolationDepth > 0 }? STRING_CONSTRUCTOR_INTERPOLATION_END { $setType(STRING_CONSTRUCTOR_INTERPOLATION_END); } @@ -2777,7 +3484,7 @@ options { ( NAME_START_CHAR ) => ncname:NCNAME { $setType(ncname.getType()); } | - { parseStringLiterals && !inElementContent && !inStringConstructor }? + { parseStringLiterals && !inElementContent && !inStringConstructor && !inStringTemplate }? STRING_LITERAL { $setType(STRING_LITERAL); } | BRACED_URI_LITERAL { $setType(BRACED_URI_LITERAL); } @@ -2801,7 +3508,15 @@ options { ( '.' ) => SELF { $setType(SELF); } | - ( INTEGER_LITERAL ( '.' ( INTEGER_LITERAL )? )? ( 'e' | 'E' ) ) + // XQ4: hex integer literals (0xFF, 0xCAFE_BABE) + ( '0' ('x' | 'X') ) + => HEX_INTEGER_LITERAL { $setType(INTEGER_LITERAL); } + | + // XQ4: binary integer literals (0b1010, 0b1111_0000) + ( '0' ('b' | 'B') ) + => BINARY_INTEGER_LITERAL { $setType(INTEGER_LITERAL); } + | + ( INTEGER_LITERAL ( '.' ( DIGITS )? )? ( 'e' | 'E' ) ) => DOUBLE_LITERAL { $setType(DOUBLE_LITERAL); } | @@ -2816,6 +3531,8 @@ options { { !(inAttributeContent || inElementContent) }? DSLASH { $setType(DSLASH); } | + ( DOUBLE_BANG ) => DOUBLE_BANG { $setType(DOUBLE_BANG); } + | BANG { $setType(BANG); } | COLON { $setType(COLON); } @@ -2828,10 +3545,17 @@ options { | STAR { $setType(STAR); } | + // XQ4: Unicode multiplication sign (U+00D7) as alternative to * + '\u00D7' { $setType(STAR); } + | + ( DOUBLE_QUESTION ) => DOUBLE_QUESTION { $setType(DOUBLE_QUESTION); } + | QUESTION { $setType(QUESTION); } | PLUS { $setType(PLUS); } | + ( PIPELINE_OP ) => PIPELINE_OP { $setType(PIPELINE_OP); } + | MINUS { $setType(MINUS); } | LPPAREN { $setType(LPPAREN); } @@ -2846,6 +3570,10 @@ options { | DOLLAR { $setType(DOLLAR); } | + ( METHOD_CALL_OP ) => METHOD_CALL_OP { $setType(METHOD_CALL_OP); } + | + ( MAPPING_ARROW_OP ) => MAPPING_ARROW_OP { $setType(MAPPING_ARROW_OP); } + | ARROW_OP { $setType(ARROW_OP); } | EQ { $setType(EQ); } @@ -2863,6 +3591,7 @@ options { | XML_CDATA_END { $setType(XML_CDATA_END); } | + { LA(1) == '(' && LA(2) == '#' && isPragmaContext() }? PRAGMA_START { $setType(PRAGMA_START); diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g index 20308296806..45ca178fd5e 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g @@ -139,6 +139,11 @@ options { List windowConditions = null; WindowExpr.WindowType windowType = null; boolean allowEmpty = false; + QName valueVarName = null; + SequenceType valueSequenceType = null; + // XQ4 destructuring + List destructureVarNames = null; + List destructureVarTypes = null; } /** @@ -267,14 +272,20 @@ throws PermissionDeniedException, EXistException, XPathException v:VERSION_DECL { final String version = v.getText(); - if (version.equals("3.1")) { + if (version.equals("4.0")) { + context.setXQueryVersion(40); + staticContext.setXQueryVersion(40); + } else if (version.equals("3.1")) { context.setXQueryVersion(31); + staticContext.setXQueryVersion(31); } else if (version.equals("3.0")) { context.setXQueryVersion(30); + staticContext.setXQueryVersion(30); } else if (version.equals("1.0")) { context.setXQueryVersion(10); + staticContext.setXQueryVersion(10); } else { - throw new XPathException(v, ErrorCodes.XQST0031, "Wrong XQuery version: require 1.0, 3.0 or 3.1"); + throw new XPathException(v, ErrorCodes.XQST0031, "Wrong XQuery version: require 1.0, 3.0, 3.1 or 4.0"); } } ( enc:STRING_LITERAL )? @@ -828,7 +839,13 @@ throws PermissionDeniedException, EXistException, XPathException { QName qn= null; try { - qn = QName.parse(staticContext, name.getText(), staticContext.getDefaultFunctionNamespace()); + // XQ4 (PR2200): unprefixed function declarations go into "no namespace" + // instead of the default function namespace (fn:) + if (name.getText() != null && !name.getText().contains(":") && staticContext.getXQueryVersion() >= 40) { + qn = new QName(name.getText(), ""); + } else { + qn = QName.parse(staticContext, name.getText(), staticContext.getDefaultFunctionNamespace()); + } } catch (final IllegalQNameException iqe) { throw new XPathException(name.getLine(), name.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + name.getText()); } @@ -930,11 +947,42 @@ throws PermissionDeniedException, EXistException, XPathException ) ; +focusFunctionDecl [PathExpr path] +returns [Expression step] +throws PermissionDeniedException, EXistException, XPathException +{ step = null; }: + #( + ff:FOCUS_FUNCTION + { + PathExpr body = new PathExpr(context); + body.setASTNode(focusFunctionDecl_AST_in); + + // Create a function with a single implicit parameter + FunctionSignature signature = new FunctionSignature(InlineFunction.INLINE_FUNCTION_QNAME); + UserDefinedFunction func = new UserDefinedFunction(context, signature); + func.setASTNode(ff); + + // Add the implicit focus parameter: $(.focus) as item()* + FunctionParameterSequenceType focusParam = new FunctionParameterSequenceType( + FocusFunction.FOCUS_PARAM_NAME, Type.ITEM, Cardinality.ZERO_OR_MORE, + "implicit focus parameter"); + signature.setArgumentTypes(new SequenceType[] { focusParam }); + signature.setReturnType(new SequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE)); + func.addVariable(FocusFunction.FOCUS_PARAM_NAME); + } + ( expr [body] )? + { + func.setFunctionBody(body); + step = new FocusFunction(context, func); + } + ) + ; + /** * Parse params in function declaration. */ paramList [List vars] -throws XPathException +throws PermissionDeniedException, EXistException, XPathException : param [vars] ( param [vars] )* ; @@ -943,7 +991,7 @@ throws XPathException * Single function param. */ param [List vars] -throws XPathException +throws PermissionDeniedException, EXistException, XPathException : #( varname:VARIABLE_BINDING @@ -959,6 +1007,18 @@ throws XPathException sequenceType [var] ) )? + ( + #( + PARAM_DEFAULT + { + PathExpr defaultExpr = new PathExpr(context); + } + expr [defaultExpr] + { + var.setDefaultValue(defaultExpr.simplify()); + } + ) + )? ) ; @@ -1132,6 +1192,38 @@ throws XPathException ) ) | + #( + RECORD_TEST { type.setPrimaryType(Type.RECORD); } + ( + STAR + { type.setRecordExtensible(true); } + | + ( + ( + #( + rf:RECORD_FIELD + { + final String fieldName = rf.getText(); + boolean optional = false; + SequenceType fieldType = null; + } + ( QUESTION { optional = true; } )? + ( + { fieldType = new SequenceType(); } + sequenceType [fieldType] + )? + { + type.addRecordField(new SequenceType.RecordField( + fieldName, optional, fieldType)); + } + ) + | + STAR { type.setRecordExtensible(true); } + )* + ) + )? + ) + | #( "item" { type.setPrimaryType(Type.ITEM); } ) @@ -1262,6 +1354,37 @@ throws XPathException #( "schema-element" EQNAME ) )? ) + | + #( + CHOICE_TYPE + { + List alternatives = new ArrayList(); + } + ( + { + SequenceType altType = new SequenceType(); + } + sequenceType [altType] + { + alternatives.add(altType); + } + )+ + { + for (final SequenceType alt : alternatives) { + type.addChoiceAlternative(alt); + } + type.setPrimaryType(Type.ITEM); + } + ) + | + #( + en:ENUM_TYPE + { + String enumText = en.getText(); + String[] enumVals = enumText.split(",", -1); + type.setEnumValues(enumVals); + } + ) ) ( STAR { type.setCardinality(Cardinality.ZERO_OR_MORE); } @@ -1293,6 +1416,14 @@ throws PermissionDeniedException, EXistException, XPathException | step=arrowOp [path] | + step=mappingArrowOp [path] + | + step=pipelineOp [path] + | + step=methodCallOp [path] // XQ4 method call operator =?> + | + step=otherwiseExpr [path] + | step=typeCastExpr [path] | // sequence constructor: @@ -1363,301 +1494,1047 @@ throws PermissionDeniedException, EXistException, XPathException } ) | - // conditional: + step=exprFlowControl [path] + | + // treat as: #( - astIf:"if" + "treat" { - PathExpr testExpr= new PathExpr(context); - PathExpr thenExpr= new PathExpr(context); - PathExpr elseExpr= new PathExpr(context); + PathExpr expr = new PathExpr(context); + expr.setASTNode(expr_AST_in); + SequenceType type= new SequenceType(); } - step=expr [testExpr] - step=astThen:expr [thenExpr] - step=astElse:expr [elseExpr] + step=expr [expr] + sequenceType [type] { - thenExpr.setASTNode(astThen); - elseExpr.setASTNode(astElse); - ConditionalExpression cond = - new ConditionalExpression(context, testExpr, thenExpr, - new DebuggableExpression(elseExpr)); - cond.setASTNode(astIf); - path.add(cond); - step = cond; + step = new TreatAsExpression(context, expr, type); + step.setASTNode(expr_AST_in); + path.add(step); } ) | - // quantified expression: some + // switch #( - "some" + switchAST:"switch" { - List clauses= new ArrayList(); - PathExpr satisfiesExpr = new PathExpr(context); - satisfiesExpr.setASTNode(expr_AST_in); + PathExpr operand = new PathExpr(context); + operand.setASTNode(expr_AST_in); + boolean booleanMode = false; } ( - #( - someVarName:VARIABLE_BINDING - { - ForLetClause clause= new ForLetClause(); - PathExpr inputSequence = new PathExpr(context); - inputSequence.setASTNode(expr_AST_in); - } - ( - #( - "as" - { SequenceType type= new SequenceType(); } - sequenceType[type] - ) - { clause.sequenceType = type; } - )? - step=expr[inputSequence] - { - try { - clause.varName = QName.parse(staticContext, someVarName.getText(), null); - } catch (final IllegalQNameException iqe) { - throw new XPathException(someVarName.getLine(), someVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + someVarName.getText()); - } - clause.inputSequence= inputSequence; - clauses.add(clause); - } - ) - )* - step=expr[satisfiesExpr] - { - Expression action = satisfiesExpr; - for (int i= clauses.size() - 1; i >= 0; i--) { - ForLetClause clause= (ForLetClause) clauses.get(i); - BindingExpression expr = new QuantifiedExpression(context, QuantifiedExpression.SOME); - expr.setASTNode(expr_AST_in); - expr.setVariable(clause.varName); - expr.setSequenceType(clause.sequenceType); - expr.setInputSequence(clause.inputSequence); - expr.setReturnExpression(action); - satisfiesExpr= null; - action= expr; - } - path.add(action); - step = action; - } - ) - | - // quantified expression: every - #( - "every" + SWITCH_BOOLEAN + { booleanMode = true; } + | + step=expr [operand] + ) { - List clauses= new ArrayList(); - PathExpr satisfiesExpr = new PathExpr(context); - satisfiesExpr.setASTNode(expr_AST_in); + SwitchExpression switchExpr = new SwitchExpression(context, operand); + switchExpr.setBooleanMode(booleanMode); + switchExpr.setASTNode(switchAST); + path.add(switchExpr); } ( - #( - everyVarName:VARIABLE_BINDING - { - ForLetClause clause= new ForLetClause(); - PathExpr inputSequence = new PathExpr(context); - inputSequence.setASTNode(expr_AST_in); - } - ( - #( - "as" - { SequenceType type= new SequenceType(); } - sequenceType[type] - ) - { clause.sequenceType = type; } - )? - step=expr[inputSequence] - { - try { - clause.varName = QName.parse(staticContext, everyVarName.getText(), null); - } catch (final IllegalQNameException iqe) { - throw new XPathException(everyVarName.getLine(), everyVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + everyVarName.getText()); - } - clause.inputSequence= inputSequence; - clauses.add(clause); - } - ) - )* - step=expr[satisfiesExpr] - { - Expression action = satisfiesExpr; - for (int i= clauses.size() - 1; i >= 0; i--) { - ForLetClause clause= (ForLetClause) clauses.get(i); - BindingExpression expr = new QuantifiedExpression(context, QuantifiedExpression.EVERY); - expr.setASTNode(expr_AST_in); - expr.setVariable(clause.varName); - expr.setSequenceType(clause.sequenceType); - expr.setInputSequence(clause.inputSequence); - expr.setReturnExpression(action); - satisfiesExpr= null; - action= expr; + { + List caseOperands = new ArrayList(2); + PathExpr returnExpr = new PathExpr(context); + returnExpr.setASTNode(expr_AST_in); } - path.add(action); - step = action; - } + (( + { + PathExpr caseOperand = new PathExpr(context); + caseOperand.setASTNode(expr_AST_in); + } + "case" + expr [caseOperand] + { caseOperands.add(caseOperand); } + )+ + #( + "return" + step= expr [returnExpr] + { switchExpr.addCase(caseOperands, returnExpr); } + )) + )+ + ( + "default" + { + PathExpr returnExpr = new PathExpr(context); + returnExpr.setASTNode(expr_AST_in); + } + step=expr [returnExpr] + { + switchExpr.setDefault(returnExpr); + } + ) + { step = switchExpr; } ) | - //try/catch expression + // typeswitch #( - astTry:"try" + "typeswitch" { - PathExpr tryTargetExpr = new PathExpr(context); - tryTargetExpr.setASTNode(expr_AST_in); + PathExpr operand = new PathExpr(context); + operand.setASTNode(expr_AST_in); } - step=expr [tryTargetExpr] + step=expr [operand] { - TryCatchExpression cond = new TryCatchExpression(context, tryTargetExpr); - cond.setASTNode(astTry); - path.add(cond); + TypeswitchExpression tswitch = new TypeswitchExpression(context, operand); + tswitch.setASTNode(expr_AST_in); + path.add(tswitch); } ( { - final List catchErrorList = new ArrayList<>(2); - final List catchVars = new ArrayList<>(3); - final PathExpr catchExpr = new PathExpr(context); - catchExpr.setASTNode(expr_AST_in); + PathExpr returnExpr = new PathExpr(context); + returnExpr.setASTNode(expr_AST_in); + QName qn = null; + List types = new ArrayList(2); + SequenceType type = new SequenceType(); } #( - astCatch:"catch" - (catchErrorList [catchErrorList]) + "case" ( - { - QName qncode = null; - QName qndesc = null; - QName qnval = null; - } - code:CATCH_ERROR_CODE + var:VARIABLE_BINDING { try { - qncode = QName.parse(staticContext, code.getText()); - catchVars.add(qncode); + qn = QName.parse(staticContext, var.getText()); } catch (final IllegalQNameException iqe) { - throw new XPathException(code.getLine(), code.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + code.getText()); + throw new XPathException(var.getLine(), var.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + var.getText()); } } - ( - desc:CATCH_ERROR_DESC - { - try { - qndesc = QName.parse(staticContext, desc.getText()); - catchVars.add(qndesc); - } catch (final IllegalQNameException iqe) { - throw new XPathException(desc.getLine(), desc.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + desc.getText()); - } - } - - ( - val:CATCH_ERROR_VAL - { - try { - qnval = QName.parse(staticContext, val.getText()); - catchVars.add(qnval); - } catch (final IllegalQNameException iqe) { - throw new XPathException(val.getLine(), val.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + val.getText()); - } - } - - )? - )? )? - step= expr [catchExpr] - { - catchExpr.setASTNode(astCatch); - cond.addCatchClause(catchErrorList, catchVars, catchExpr); - } + ( + sequenceType[type] + { + types.add(type); + type = new SequenceType(); + } + )+ + // Need return as root in following to disambiguate + // e.g. ( case a xs:integer ( * 3 3 ) ) + // which gives xs:integer* and no operator left for 3 3 ... + // Now ( case a xs:integer ( return ( + 3 3 ) ) ) /ljo + #( + "return" + step= expr [returnExpr] + { + SequenceType[] atype = new SequenceType[types.size()]; + atype = types.toArray(atype); + tswitch.addCase(atype, qn, returnExpr); + } + ) ) + )+ + ( + "default" + { + PathExpr returnExpr = new PathExpr(context); + returnExpr.setASTNode(expr_AST_in); + QName qn = null; + } + ( + dvar:VARIABLE_BINDING + { + try { + qn = QName.parse(staticContext, dvar.getText()); + } catch (final IllegalQNameException iqe) { + throw new XPathException(dvar.getLine(), dvar.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + dvar.getText()); + } + } + )? + step=expr [returnExpr] + { + tswitch.setDefault(qn, returnExpr); + } + ) + { step = tswitch; } + ) + | + // logical operator: or + #( + "or" + { + PathExpr left= new PathExpr(context); + left.setASTNode(expr_AST_in); + } + step=expr [left] + { + PathExpr right= new PathExpr(context); + right.setASTNode(expr_AST_in); + } + step=expr [right] + ) + { + OpOr or= new OpOr(context); + or.addPath(left); + or.addPath(right); + path.addPath(or); + step = or; + } + | + // logical operator: and + #( + "and" + { + PathExpr left= new PathExpr(context); + left.setASTNode(expr_AST_in); + + PathExpr right= new PathExpr(context); + right.setASTNode(expr_AST_in); + } + step=expr [left] + step=expr [right] + ) + { + OpAnd and= new OpAnd(context); + and.addPath(left); + and.addPath(right); + path.addPath(and); + step = and; + } + | + // union expressions: | and union + #( + UNION + { + PathExpr left= new PathExpr(context); + left.setASTNode(expr_AST_in); + PathExpr right= new PathExpr(context); + right.setASTNode(expr_AST_in); + } + step=expr [left] + step=expr [right] + ) + { + Union union= new Union(context, left, right); + path.add(union); + step = union; + } + | + // intersections: + #( "intersect" { - step = cond; + PathExpr left = new PathExpr(context); + left.setASTNode(expr_AST_in); + + PathExpr right = new PathExpr(context); + right.setASTNode(expr_AST_in); + } + step=expr [left] + step=expr [right] + ) + { + Intersect intersect = new Intersect(context, left, right); + path.add(intersect); + step = intersect; + } + | + #( "except" + { + PathExpr left = new PathExpr(context); + left.setASTNode(expr_AST_in); + + PathExpr right = new PathExpr(context); + right.setASTNode(expr_AST_in); } + step=expr [left] + step=expr [right] ) + { + Except intersect = new Except(context, left, right); + path.add(intersect); + step = intersect; + } | - // FLWOR expressions: let and for + // absolute path expression starting with a / #( - r:"return" + ABSOLUTE_SLASH { - List clauses= new ArrayList(); - Expression action= new PathExpr(context); - action.setASTNode(r); - PathExpr whereExpr= null; - List orderBy= null; + RootNode root= new RootNode(context); + path.add(root); + } + ( step=expr [path] )? + ) + | + // absolute path expression starting with // + #( + ABSOLUTE_DSLASH + { + RootNode root= new RootNode(context); + path.add(root); } ( - #( - f:"for" - ( - #( - varName:VARIABLE_BINDING - { - ForLetClause clause= new ForLetClause(); - clause.ast = varName; - PathExpr inputSequence= new PathExpr(context); - inputSequence.setASTNode(expr_AST_in);inputSequence.setASTNode(expr_AST_in); - final DistinctVariableNames distinctVariableNames = new DistinctVariableNames(); - } - ( - #( - "as" - { clause.sequenceType= new SequenceType(); } - sequenceType [clause.sequenceType] - ) - )? - ( - "empty" - { clause.allowEmpty = true; } - )? - ( - posVar:POSITIONAL_VAR - { - try { - clause.posVar = distinctVariableNames.check(ErrorCodes.XQST0089, posVar, QName.parse(staticContext, posVar.getText(), null)); - } catch (final IllegalQNameException iqe) { - throw new XPathException(posVar.getLine(), posVar.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + posVar.getText()); - } - } - )? - step=expr [inputSequence] - { - try { - clause.varName = distinctVariableNames.check(ErrorCodes.XQST0089, varName, QName.parse(staticContext, varName.getText(), null)); - } catch (final IllegalQNameException iqe) { - throw new XPathException(varName.getLine(), varName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + varName.getText()); - } - clause.inputSequence= inputSequence; - clauses.add(clause); - } - ) - )+ - ) - | - #( - l:"let" - ( - #( - letVarName:VARIABLE_BINDING - { - ForLetClause clause= new ForLetClause(); - clause.ast = letVarName; - clause.type = FLWORClause.ClauseType.LET; - PathExpr inputSequence= new PathExpr(context); - inputSequence.setASTNode(expr_AST_in); - } + step=expr [path] + { + if (step instanceof LocationStep) { + LocationStep s= (LocationStep) step; + if (s.getAxis() == Constants.ATTRIBUTE_AXIS || + (s.getTest().getType() == Type.ATTRIBUTE && s.getAxis() == Constants.CHILD_AXIS)) + // combines descendant-or-self::node()/attribute:* + s.setAxis(Constants.DESCENDANT_ATTRIBUTE_AXIS); + else { + s.setAxis(Constants.DESCENDANT_SELF_AXIS); + s.setAbbreviated(true); + } + } else + step.setPrimaryAxis(Constants.DESCENDANT_SELF_AXIS); + } + )? + ) + | + // range expression: to + #( + "to" + { + PathExpr start= new PathExpr(context); + start.setASTNode(expr_AST_in); + + PathExpr end= new PathExpr(context); + end.setASTNode(expr_AST_in); + + List args= new ArrayList(2); + args.add(start); + args.add(end); + } + step=expr [start] + step=expr [end] + { + RangeExpression range= new RangeExpression(context); + range.setASTNode(expr_AST_in); + range.setArguments(args); + path.addPath(range); + step = range; + } + ) + | + step=generalComp [path] + | + step=valueComp [path] + | + step=nodeComp [path] + | + step=primaryExpr [path] + | + step=pathExpr [path] + | + step=extensionExpr [path] + | + step=numericExpr [path] + | + step=updateExpr [path] + ; + +/** + * Flow control expressions extracted from expr to avoid + * Java method size limit (64KB bytecode). + * Handles: conditional, ternary, quantified (some/every), + * try/catch/finally, FLWOR, instance of. + */ +exprFlowControl [PathExpr path] +returns [Expression step] +throws PermissionDeniedException, EXistException, XPathException +{ step = null; } +: + // conditional: + #( + astIf:"if" + { + PathExpr testExpr= new PathExpr(context); + PathExpr thenExpr= new PathExpr(context); + PathExpr elseExpr= new PathExpr(context); + } + step=expr [testExpr] + step=astThen:expr [thenExpr] + step=astElse:expr [elseExpr] + { + thenExpr.setASTNode(astThen); + elseExpr.setASTNode(astElse); + ConditionalExpression cond = + new ConditionalExpression(context, testExpr, thenExpr, + new DebuggableExpression(elseExpr)); + cond.setASTNode(astIf); + path.add(cond); + step = cond; + } + ) + | + // ternary conditional: condition ?? then !! else + #( + astTernary:TERNARY + { + PathExpr ternTestExpr = new PathExpr(context); + PathExpr ternThenExpr = new PathExpr(context); + PathExpr ternElseExpr = new PathExpr(context); + } + step=expr [ternTestExpr] + step=expr [ternThenExpr] + step=expr [ternElseExpr] + { + ConditionalExpression ternCond = + new ConditionalExpression(context, ternTestExpr, ternThenExpr, + new DebuggableExpression(ternElseExpr)); + ternCond.setASTNode(astTernary); + path.add(ternCond); + step = ternCond; + } + ) + | + // quantified expression: some + #( + "some" + { + List clauses= new ArrayList(); + PathExpr satisfiesExpr = new PathExpr(context); + satisfiesExpr.setASTNode(exprFlowControl_AST_in); + } + ( + #( + someVarName:VARIABLE_BINDING + { + ForLetClause clause= new ForLetClause(); + PathExpr inputSequence = new PathExpr(context); + inputSequence.setASTNode(exprFlowControl_AST_in); + } + ( + #( + "as" + { SequenceType type= new SequenceType(); } + sequenceType[type] + ) + { clause.sequenceType = type; } + )? + step=expr[inputSequence] + { + try { + clause.varName = QName.parse(staticContext, someVarName.getText(), null); + } catch (final IllegalQNameException iqe) { + throw new XPathException(someVarName.getLine(), someVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + someVarName.getText()); + } + clause.inputSequence= inputSequence; + clauses.add(clause); + } + ) + )* + step=expr[satisfiesExpr] + { + Expression action = satisfiesExpr; + for (int i= clauses.size() - 1; i >= 0; i--) { + ForLetClause clause= (ForLetClause) clauses.get(i); + BindingExpression expr = new QuantifiedExpression(context, QuantifiedExpression.SOME); + expr.setASTNode(exprFlowControl_AST_in); + expr.setVariable(clause.varName); + expr.setSequenceType(clause.sequenceType); + expr.setInputSequence(clause.inputSequence); + expr.setReturnExpression(action); + satisfiesExpr= null; + action= expr; + } + path.add(action); + step = action; + } + ) + | + // quantified expression: every + #( + "every" + { + List clauses= new ArrayList(); + PathExpr satisfiesExpr = new PathExpr(context); + satisfiesExpr.setASTNode(exprFlowControl_AST_in); + } + ( + #( + everyVarName:VARIABLE_BINDING + { + ForLetClause clause= new ForLetClause(); + PathExpr inputSequence = new PathExpr(context); + inputSequence.setASTNode(exprFlowControl_AST_in); + } + ( + #( + "as" + { SequenceType type= new SequenceType(); } + sequenceType[type] + ) + { clause.sequenceType = type; } + )? + step=expr[inputSequence] + { + try { + clause.varName = QName.parse(staticContext, everyVarName.getText(), null); + } catch (final IllegalQNameException iqe) { + throw new XPathException(everyVarName.getLine(), everyVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + everyVarName.getText()); + } + clause.inputSequence= inputSequence; + clauses.add(clause); + } + ) + )* + step=expr[satisfiesExpr] + { + Expression action = satisfiesExpr; + for (int i= clauses.size() - 1; i >= 0; i--) { + ForLetClause clause= (ForLetClause) clauses.get(i); + BindingExpression expr = new QuantifiedExpression(context, QuantifiedExpression.EVERY); + expr.setASTNode(exprFlowControl_AST_in); + expr.setVariable(clause.varName); + expr.setSequenceType(clause.sequenceType); + expr.setInputSequence(clause.inputSequence); + expr.setReturnExpression(action); + satisfiesExpr= null; + action= expr; + } + path.add(action); + step = action; + } + ) + | + //try/catch expression + #( + astTry:"try" + { + PathExpr tryTargetExpr = new PathExpr(context); + tryTargetExpr.setASTNode(exprFlowControl_AST_in); + } + step=expr [tryTargetExpr] + { + TryCatchExpression cond = new TryCatchExpression(context, tryTargetExpr); + cond.setASTNode(astTry); + path.add(cond); + } + ( + { + final List catchErrorList = new ArrayList<>(2); + final List catchVars = new ArrayList<>(3); + final PathExpr catchExpr = new PathExpr(context); + catchExpr.setASTNode(exprFlowControl_AST_in); + } + #( + astCatch:"catch" + (catchErrorList [catchErrorList]) + ( + { + QName qncode = null; + QName qndesc = null; + QName qnval = null; + } + code:CATCH_ERROR_CODE + { + try { + qncode = QName.parse(staticContext, code.getText()); + catchVars.add(qncode); + } catch (final IllegalQNameException iqe) { + throw new XPathException(code.getLine(), code.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + code.getText()); + } + } + ( + desc:CATCH_ERROR_DESC + { + try { + qndesc = QName.parse(staticContext, desc.getText()); + catchVars.add(qndesc); + } catch (final IllegalQNameException iqe) { + throw new XPathException(desc.getLine(), desc.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + desc.getText()); + } + } + + ( + val:CATCH_ERROR_VAL + { + try { + qnval = QName.parse(staticContext, val.getText()); + catchVars.add(qnval); + } catch (final IllegalQNameException iqe) { + throw new XPathException(val.getLine(), val.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + val.getText()); + } + } + + )? + )? + )? + step= expr [catchExpr] + { + catchExpr.setASTNode(astCatch); + cond.addCatchClause(catchErrorList, catchVars, catchExpr); + } + ) + )* + ( + #( + astFinally:"finally" + { + final PathExpr finallyExpr = new PathExpr(context); + finallyExpr.setASTNode(astFinally); + } + (step=expr [finallyExpr])? + { + finallyExpr.setASTNode(astFinally); + cond.setFinallyExpr(finallyExpr); + } + ) + )? + + { + step = cond; + } + ) + | + // FLWOR expressions: let and for + #( + r:"return" + { + List clauses= new ArrayList(); + Expression action= new PathExpr(context); + action.setASTNode(r); + PathExpr whereExpr= null; + List orderBy= null; + } + ( + #( + f:"for" + ( + #( + varName:VARIABLE_BINDING + { + ForLetClause clause= new ForLetClause(); + clause.ast = varName; + PathExpr inputSequence= new PathExpr(context); + inputSequence.setASTNode(exprFlowControl_AST_in);inputSequence.setASTNode(exprFlowControl_AST_in); + final DistinctVariableNames distinctVariableNames = new DistinctVariableNames(); + } + ( + #( + "as" + { clause.sequenceType= new SequenceType(); } + sequenceType [clause.sequenceType] + ) + )? + ( + "empty" + { clause.allowEmpty = true; } + )? + ( + posVar:POSITIONAL_VAR + { + try { + clause.posVar = distinctVariableNames.check(ErrorCodes.XQST0089, posVar, QName.parse(staticContext, posVar.getText(), null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(posVar.getLine(), posVar.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + posVar.getText()); + } + } + )? + step=expr [inputSequence] + { + try { + clause.varName = distinctVariableNames.check(ErrorCodes.XQST0089, varName, QName.parse(staticContext, varName.getText(), null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(varName.getLine(), varName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + varName.getText()); + } + clause.inputSequence= inputSequence; + clauses.add(clause); + } + ) + | + #( + FOR_MEMBER + #( + memberVarName:VARIABLE_BINDING + { + ForLetClause clause= new ForLetClause(); + clause.ast = memberVarName; + clause.type = FLWORClause.ClauseType.FOR_MEMBER; + PathExpr inputSequence= new PathExpr(context); + inputSequence.setASTNode(exprFlowControl_AST_in); + final DistinctVariableNames memberDistinctVars = new DistinctVariableNames(); + } + ( + #( + "as" + { clause.sequenceType= new SequenceType(); } + sequenceType [clause.sequenceType] + ) + )? + ( + memberPosVar:POSITIONAL_VAR + { + try { + clause.posVar = memberDistinctVars.check(ErrorCodes.XQST0089, memberPosVar, QName.parse(staticContext, memberPosVar.getText(), null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(memberPosVar.getLine(), memberPosVar.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + memberPosVar.getText()); + } + } + )? + step=expr [inputSequence] + { + try { + clause.varName = memberDistinctVars.check(ErrorCodes.XQST0089, memberVarName, QName.parse(staticContext, memberVarName.getText(), null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(memberVarName.getLine(), memberVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + memberVarName.getText()); + } + clause.inputSequence= inputSequence; + clauses.add(clause); + } + ) + ) + | + #( + FOR_KEY + #( + keyVarName:VARIABLE_BINDING + { + ForLetClause clause= new ForLetClause(); + clause.ast = keyVarName; + clause.type = FLWORClause.ClauseType.FOR_KEY; + PathExpr inputSequence= new PathExpr(context); + inputSequence.setASTNode(exprFlowControl_AST_in); + final DistinctVariableNames keyDistinctVars = new DistinctVariableNames(); + } + ( + #( + "as" + { clause.sequenceType= new SequenceType(); } + sequenceType [clause.sequenceType] + ) + )? + ( + keyPosVar:POSITIONAL_VAR + { + try { + clause.posVar = keyDistinctVars.check(ErrorCodes.XQST0089, keyPosVar, QName.parse(staticContext, keyPosVar.getText(), null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(keyPosVar.getLine(), keyPosVar.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + keyPosVar.getText()); + } + } + )? + step=expr [inputSequence] + { + try { + clause.varName = keyDistinctVars.check(ErrorCodes.XQST0089, keyVarName, QName.parse(staticContext, keyVarName.getText(), null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(keyVarName.getLine(), keyVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + keyVarName.getText()); + } + clause.inputSequence= inputSequence; + clauses.add(clause); + } + ) + ) + | + #( + FOR_VALUE + #( + valueVarName:VARIABLE_BINDING + { + ForLetClause clause= new ForLetClause(); + clause.ast = valueVarName; + clause.type = FLWORClause.ClauseType.FOR_VALUE; + PathExpr inputSequence= new PathExpr(context); + inputSequence.setASTNode(exprFlowControl_AST_in); + final DistinctVariableNames valueDistinctVars = new DistinctVariableNames(); + } + ( + #( + "as" + { clause.sequenceType= new SequenceType(); } + sequenceType [clause.sequenceType] + ) + )? + ( + valuePosVar:POSITIONAL_VAR + { + try { + clause.posVar = valueDistinctVars.check(ErrorCodes.XQST0089, valuePosVar, QName.parse(staticContext, valuePosVar.getText(), null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(valuePosVar.getLine(), valuePosVar.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + valuePosVar.getText()); + } + } + )? + step=expr [inputSequence] + { + try { + clause.varName = valueDistinctVars.check(ErrorCodes.XQST0089, valueVarName, QName.parse(staticContext, valueVarName.getText(), null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(valueVarName.getLine(), valueVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + valueVarName.getText()); + } + clause.inputSequence= inputSequence; + clauses.add(clause); + } + ) + ) + | + #( + FOR_KEY_VALUE + #( + kvKeyVarName:VARIABLE_BINDING + { + ForLetClause clause= new ForLetClause(); + clause.ast = kvKeyVarName; + clause.type = FLWORClause.ClauseType.FOR_KEY_VALUE; + PathExpr inputSequence= new PathExpr(context); + inputSequence.setASTNode(exprFlowControl_AST_in); + final DistinctVariableNames kvDistinctVars = new DistinctVariableNames(); + } + ( + #( + "as" + { clause.sequenceType= new SequenceType(); } + sequenceType [clause.sequenceType] + ) + )? + ( + #( + kvValueVar:VALUE_VAR + { + try { + clause.valueVarName = kvDistinctVars.check(ErrorCodes.XQST0089, kvValueVar, QName.parse(staticContext, kvValueVar.getText(), null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(kvValueVar.getLine(), kvValueVar.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + kvValueVar.getText()); + } + } + ( + #( + "as" + { clause.valueSequenceType = new SequenceType(); } + sequenceType [clause.valueSequenceType] + ) + )? + ) + )? + ( + kvPosVar:POSITIONAL_VAR + { + try { + clause.posVar = kvDistinctVars.check(ErrorCodes.XQST0089, kvPosVar, QName.parse(staticContext, kvPosVar.getText(), null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(kvPosVar.getLine(), kvPosVar.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + kvPosVar.getText()); + } + } + )? + step=expr [inputSequence] + { + try { + clause.varName = kvDistinctVars.check(ErrorCodes.XQST0089, kvKeyVarName, QName.parse(staticContext, kvKeyVarName.getText(), null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(kvKeyVarName.getLine(), kvKeyVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + kvKeyVarName.getText()); + } + clause.inputSequence= inputSequence; + clauses.add(clause); + } + ) + ) + )+ + ) + | + #( + l:"let" + ( + #( + letVarName:VARIABLE_BINDING + { + ForLetClause clause= new ForLetClause(); + clause.ast = letVarName; + clause.type = FLWORClause.ClauseType.LET; + PathExpr inputSequence= new PathExpr(context); + inputSequence.setASTNode(exprFlowControl_AST_in); + } + ( + #( + "as" + { clause.sequenceType= new SequenceType(); } + sequenceType [clause.sequenceType] + ) + )? + step=expr [inputSequence] + { + try { + clause.varName = QName.parse(staticContext, letVarName.getText(), null); + } catch (final IllegalQNameException iqe) { + throw new XPathException(letVarName.getLine(), letVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + letVarName.getText()); + } + clause.inputSequence= inputSequence; + clauses.add(clause); + } + ) + | + // XQ4: sequence destructuring + #( + seqDestAST:SEQ_DESTRUCTURE + { + ForLetClause seqClause = new ForLetClause(); + seqClause.ast = seqDestAST; + seqClause.type = FLWORClause.ClauseType.LET_SEQ_DESTRUCTURE; + seqClause.destructureVarNames = new ArrayList(); + seqClause.destructureVarTypes = new ArrayList(); + String[] seqVarNames = seqDestAST.getText().split(",", -1); + int seqTypedIdx = 0; + boolean[] seqHasType = new boolean[seqVarNames.length]; + for (int dv = 0; dv < seqVarNames.length; dv++) { + String svn = seqVarNames[dv]; + seqHasType[dv] = svn.endsWith("+"); + if (seqHasType[dv]) svn = svn.substring(0, svn.length() - 1); + try { + seqClause.destructureVarNames.add( + QName.parse(staticContext, svn, null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(seqDestAST.getLine(), seqDestAST.getColumn(), + ErrorCodes.XPST0081, "No namespace defined for prefix " + svn); + } + seqClause.destructureVarTypes.add(null); + } + PathExpr seqInput = new PathExpr(context); + seqInput.setASTNode(exprFlowControl_AST_in); + } + ( + #( + DESTRUCTURE_VAR_TYPE + #( + "as" + { + SequenceType seqVarType = new SequenceType(); + while (seqTypedIdx < seqHasType.length && !seqHasType[seqTypedIdx]) seqTypedIdx++; + } + sequenceType [seqVarType] + { + if (seqTypedIdx < seqClause.destructureVarTypes.size()) { + seqClause.destructureVarTypes.set(seqTypedIdx, seqVarType); + } + seqTypedIdx++; + } + ) + ) + )* + ( + #( + "as" + { seqClause.sequenceType = new SequenceType(); } + sequenceType [seqClause.sequenceType] + ) + )? + step=expr [seqInput] + { + seqClause.inputSequence = seqInput; + clauses.add(seqClause); + } + ) + | + // XQ4: array destructuring + #( + arrDestAST:ARRAY_DESTRUCTURE + { + ForLetClause arrClause = new ForLetClause(); + arrClause.ast = arrDestAST; + arrClause.type = FLWORClause.ClauseType.LET_ARRAY_DESTRUCTURE; + arrClause.destructureVarNames = new ArrayList(); + arrClause.destructureVarTypes = new ArrayList(); + String[] arrVarNames = arrDestAST.getText().split(",", -1); + int arrTypedIdx = 0; + boolean[] arrHasType = new boolean[arrVarNames.length]; + for (int dv = 0; dv < arrVarNames.length; dv++) { + String avn = arrVarNames[dv]; + arrHasType[dv] = avn.endsWith("+"); + if (arrHasType[dv]) avn = avn.substring(0, avn.length() - 1); + try { + arrClause.destructureVarNames.add( + QName.parse(staticContext, avn, null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(arrDestAST.getLine(), arrDestAST.getColumn(), + ErrorCodes.XPST0081, "No namespace defined for prefix " + avn); + } + arrClause.destructureVarTypes.add(null); + } + PathExpr arrInput = new PathExpr(context); + arrInput.setASTNode(exprFlowControl_AST_in); + } + ( + #( + DESTRUCTURE_VAR_TYPE + #( + "as" + { + SequenceType arrVarType = new SequenceType(); + while (arrTypedIdx < arrHasType.length && !arrHasType[arrTypedIdx]) arrTypedIdx++; + } + sequenceType [arrVarType] + { + if (arrTypedIdx < arrClause.destructureVarTypes.size()) { + arrClause.destructureVarTypes.set(arrTypedIdx, arrVarType); + } + arrTypedIdx++; + } + ) + ) + )* ( #( "as" - { clause.sequenceType= new SequenceType(); } - sequenceType [clause.sequenceType] + { arrClause.sequenceType = new SequenceType(); } + sequenceType [arrClause.sequenceType] ) )? - step=expr [inputSequence] + step=expr [arrInput] { - try { - clause.varName = QName.parse(staticContext, letVarName.getText(), null); - } catch (final IllegalQNameException iqe) { - throw new XPathException(letVarName.getLine(), letVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + letVarName.getText()); + arrClause.inputSequence = arrInput; + clauses.add(arrClause); + } + ) + | + // XQ4: map destructuring + #( + mapDestAST:MAP_DESTRUCTURE + { + ForLetClause mapClause = new ForLetClause(); + mapClause.ast = mapDestAST; + mapClause.type = FLWORClause.ClauseType.LET_MAP_DESTRUCTURE; + mapClause.destructureVarNames = new ArrayList(); + mapClause.destructureVarTypes = new ArrayList(); + String[] mapVarNames = mapDestAST.getText().split(",", -1); + int mapTypedIdx = 0; + boolean[] mapHasType = new boolean[mapVarNames.length]; + for (int dv = 0; dv < mapVarNames.length; dv++) { + String mvn = mapVarNames[dv]; + mapHasType[dv] = mvn.endsWith("+"); + if (mapHasType[dv]) mvn = mvn.substring(0, mvn.length() - 1); + try { + mapClause.destructureVarNames.add( + QName.parse(staticContext, mvn, null)); + } catch (final IllegalQNameException iqe) { + throw new XPathException(mapDestAST.getLine(), mapDestAST.getColumn(), + ErrorCodes.XPST0081, "No namespace defined for prefix " + mvn); + } + mapClause.destructureVarTypes.add(null); } - clause.inputSequence= inputSequence; - clauses.add(clause); + PathExpr mapInput = new PathExpr(context); + mapInput.setASTNode(exprFlowControl_AST_in); + } + ( + #( + DESTRUCTURE_VAR_TYPE + #( + "as" + { + SequenceType mapVarType = new SequenceType(); + while (mapTypedIdx < mapHasType.length && !mapHasType[mapTypedIdx]) mapTypedIdx++; + } + sequenceType [mapVarType] + { + if (mapTypedIdx < mapClause.destructureVarTypes.size()) { + mapClause.destructureVarTypes.set(mapTypedIdx, mapVarType); + } + mapTypedIdx++; + } + ) + ) + )* + ( + #( + "as" + { mapClause.sequenceType = new SequenceType(); } + sequenceType [mapClause.sequenceType] + ) + )? + step=expr [mapInput] + { + mapClause.inputSequence = mapInput; + clauses.add(mapClause); } ) )+ @@ -1884,7 +2761,7 @@ throws PermissionDeniedException, EXistException, XPathException ( { groupSpecExpr = new PathExpr(context); - groupSpecExpr.setASTNode(expr_AST_in); + groupSpecExpr.setASTNode(exprFlowControl_AST_in); } step=expr [groupSpecExpr] ) @@ -1915,7 +2792,7 @@ throws PermissionDeniedException, EXistException, XPathException ( { PathExpr orderSpecExpr= new PathExpr(context); - orderSpecExpr.setASTNode(expr_AST_in); + orderSpecExpr.setASTNode(exprFlowControl_AST_in); } step=expr [orderSpecExpr] { @@ -1981,7 +2858,7 @@ throws PermissionDeniedException, EXistException, XPathException w:"where" { whereExpr= new PathExpr(context); - whereExpr.setASTNode(expr_AST_in); + whereExpr.setASTNode(exprFlowControl_AST_in); } step=expr [whereExpr] { @@ -1994,422 +2871,176 @@ throws PermissionDeniedException, EXistException, XPathException ) | #( - co:"count" - countVarName:VARIABLE_BINDING + wh:"while" + { + PathExpr whileExpr = new PathExpr(context); + whileExpr.setASTNode(exprFlowControl_AST_in); + } + step=expr [whileExpr] { ForLetClause clause = new ForLetClause(); - clause.ast = co; - try { - clause.varName = QName.parse(staticContext, countVarName.getText(), null); - } catch (final IllegalQNameException iqe) { - throw new XPathException(countVarName.getLine(), countVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + countVarName.getText()); - } - clause.type = FLWORClause.ClauseType.COUNT; - clause.inputSequence = null; + clause.ast = wh; + clause.type = FLWORClause.ClauseType.WHILE; + clause.inputSequence = whileExpr; clauses.add(clause); } ) - )+ - step=expr [(PathExpr) action] - { - for (int i= clauses.size() - 1; i >= 0; i--) { - ForLetClause clause= (ForLetClause) clauses.get(i); - FLWORClause expr; - switch (clause.type) { - case LET: - expr = new LetExpr(context); - expr.setASTNode(expr_AST_in); - break; - case GROUPBY: - expr = new GroupByClause(context); - break; - case ORDERBY: - expr = new OrderByClause(context, clause.orderSpecs); - break; - case WHERE: - expr = new WhereClause(context, new DebuggableExpression(clause.inputSequence)); - break; - case COUNT: - expr = new CountClause(context, clause.varName); - break; - case WINDOW: - expr = new WindowExpr(context, clause.windowType, clause.windowConditions.get(0), clause.windowConditions.size() > 1 ? clause.windowConditions.get(1) : null); - break; - default: - expr = new ForExpr(context, clause.allowEmpty); - break; - } - expr.setASTNode(clause.ast); - if (clause.type == FLWORClause.ClauseType.FOR || clause.type == FLWORClause.ClauseType.LET - || clause.type == FLWORClause.ClauseType.WINDOW) { - final BindingExpression bind = (BindingExpression)expr; - bind.setVariable(clause.varName); - bind.setSequenceType(clause.sequenceType); - bind.setInputSequence(clause.inputSequence); - if (clause.type == FLWORClause.ClauseType.FOR) { - ((ForExpr) bind).setPositionalVariable(clause.posVar); - } - } else if (clause.type == FLWORClause.ClauseType.GROUPBY) { - if (clause.groupSpecs != null) { - GroupSpec specs[] = new GroupSpec[clause.groupSpecs.size()]; - int k = 0; - for (GroupSpec groupSpec : clause.groupSpecs) { - specs[k++]= groupSpec; - } - ((GroupByClause)expr).setGroupSpecs(specs); - } - } - if (!(action instanceof FLWORClause)) - expr.setReturnExpression(new DebuggableExpression(action)); - else { - expr.setReturnExpression(action); - ((FLWORClause)action).setPreviousClause(expr); - } - - action= expr; - } - - path.add(action); - step = action; - } - ) - | - // instance of: - #( - "instance" - { - PathExpr expr = new PathExpr(context); - expr.setASTNode(expr_AST_in); - SequenceType type= new SequenceType(); - } - step=expr [expr] - sequenceType [type] - { - step = new InstanceOfExpression(context, expr, type); - step.setASTNode(expr_AST_in); - path.add(step); - } - ) - | - // treat as: - #( - "treat" - { - PathExpr expr = new PathExpr(context); - expr.setASTNode(expr_AST_in); - SequenceType type= new SequenceType(); - } - step=expr [expr] - sequenceType [type] - { - step = new TreatAsExpression(context, expr, type); - step.setASTNode(expr_AST_in); - path.add(step); - } - ) - | - // switch - #( - switchAST:"switch" - { - PathExpr operand = new PathExpr(context); - operand.setASTNode(expr_AST_in); - } - step=expr [operand] - { - SwitchExpression switchExpr = new SwitchExpression(context, operand); - switchExpr.setASTNode(switchAST); - path.add(switchExpr); - } - ( - { - List caseOperands = new ArrayList(2); - PathExpr returnExpr = new PathExpr(context); - returnExpr.setASTNode(expr_AST_in); - } - (( - { - PathExpr caseOperand = new PathExpr(context); - caseOperand.setASTNode(expr_AST_in); - } - "case" - expr [caseOperand] - { caseOperands.add(caseOperand); } - )+ - #( - "return" - step= expr [returnExpr] - { switchExpr.addCase(caseOperands, returnExpr); } - )) - )+ - ( - "default" - { - PathExpr returnExpr = new PathExpr(context); - returnExpr.setASTNode(expr_AST_in); - } - step=expr [returnExpr] - { - switchExpr.setDefault(returnExpr); - } - ) - { step = switchExpr; } - ) - | - // typeswitch - #( - "typeswitch" - { - PathExpr operand = new PathExpr(context); - operand.setASTNode(expr_AST_in); - } - step=expr [operand] - { - TypeswitchExpression tswitch = new TypeswitchExpression(context, operand); - tswitch.setASTNode(expr_AST_in); - path.add(tswitch); - } - ( - { - PathExpr returnExpr = new PathExpr(context); - returnExpr.setASTNode(expr_AST_in); - QName qn = null; - List types = new ArrayList(2); - SequenceType type = new SequenceType(); - } - #( - "case" - ( - var:VARIABLE_BINDING - { - try { - qn = QName.parse(staticContext, var.getText()); - } catch (final IllegalQNameException iqe) { - throw new XPathException(var.getLine(), var.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + var.getText()); - } - } - )? - ( - sequenceType[type] - { - types.add(type); - type = new SequenceType(); - } - )+ - // Need return as root in following to disambiguate - // e.g. ( case a xs:integer ( * 3 3 ) ) - // which gives xs:integer* and no operator left for 3 3 ... - // Now ( case a xs:integer ( return ( + 3 3 ) ) ) /ljo - #( - "return" - step= expr [returnExpr] - { - SequenceType[] atype = new SequenceType[types.size()]; - atype = types.toArray(atype); - tswitch.addCase(atype, qn, returnExpr); - } - ) - ) - - )+ - ( - "default" - { - PathExpr returnExpr = new PathExpr(context); - returnExpr.setASTNode(expr_AST_in); - QName qn = null; - } - ( - dvar:VARIABLE_BINDING + | + #( + co:"count" + countVarName:VARIABLE_BINDING { + ForLetClause clause = new ForLetClause(); + clause.ast = co; try { - qn = QName.parse(staticContext, dvar.getText()); + clause.varName = QName.parse(staticContext, countVarName.getText(), null); } catch (final IllegalQNameException iqe) { - throw new XPathException(dvar.getLine(), dvar.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + dvar.getText()); + throw new XPathException(countVarName.getLine(), countVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + countVarName.getText()); } + clause.type = FLWORClause.ClauseType.COUNT; + clause.inputSequence = null; + clauses.add(clause); } - )? - step=expr [returnExpr] - { - tswitch.setDefault(qn, returnExpr); + ) + )+ + step=expr [(PathExpr) action] + { + for (int i= clauses.size() - 1; i >= 0; i--) { + ForLetClause clause= (ForLetClause) clauses.get(i); + FLWORClause expr; + switch (clause.type) { + case LET: + expr = new LetExpr(context); + expr.setASTNode(exprFlowControl_AST_in); + break; + case GROUPBY: + expr = new GroupByClause(context); + break; + case ORDERBY: + expr = new OrderByClause(context, clause.orderSpecs); + break; + case WHERE: + expr = new WhereClause(context, new DebuggableExpression(clause.inputSequence)); + break; + case WHILE: + expr = new WhileClause(context, new DebuggableExpression(clause.inputSequence)); + break; + case COUNT: + expr = new CountClause(context, clause.varName); + break; + case WINDOW: + expr = new WindowExpr(context, clause.windowType, clause.windowConditions.get(0), clause.windowConditions.size() > 1 ? clause.windowConditions.get(1) : null); + break; + case FOR_MEMBER: + expr = new ForMemberExpr(context); + break; + case FOR_KEY: + expr = new ForKeyValueExpr(context, FLWORClause.ClauseType.FOR_KEY); + break; + case FOR_VALUE: + expr = new ForKeyValueExpr(context, FLWORClause.ClauseType.FOR_VALUE); + break; + case FOR_KEY_VALUE: + expr = new ForKeyValueExpr(context, FLWORClause.ClauseType.FOR_KEY_VALUE); + break; + case LET_SEQ_DESTRUCTURE: + case LET_ARRAY_DESTRUCTURE: + case LET_MAP_DESTRUCTURE: + { + LetDestructureExpr.DestructureMode dmode; + if (clause.type == FLWORClause.ClauseType.LET_SEQ_DESTRUCTURE) { + dmode = LetDestructureExpr.DestructureMode.SEQUENCE; + } else if (clause.type == FLWORClause.ClauseType.LET_ARRAY_DESTRUCTURE) { + dmode = LetDestructureExpr.DestructureMode.ARRAY; + } else { + dmode = LetDestructureExpr.DestructureMode.MAP; + } + LetDestructureExpr dexpr = new LetDestructureExpr(context, dmode); + dexpr.setASTNode(clause.ast); + for (int j = 0; j < clause.destructureVarNames.size(); j++) { + dexpr.addVariable( + (QName) clause.destructureVarNames.get(j), + clause.destructureVarTypes.size() > j ? + (SequenceType) clause.destructureVarTypes.get(j) : null); + } + dexpr.setInputSequence(clause.inputSequence); + if (clause.sequenceType != null) { + dexpr.setOverallType(clause.sequenceType); + } + expr = dexpr; + break; + } + default: + expr = new ForExpr(context, clause.allowEmpty); + break; + } + expr.setASTNode(clause.ast); + if (clause.type == FLWORClause.ClauseType.FOR || clause.type == FLWORClause.ClauseType.LET + || clause.type == FLWORClause.ClauseType.WINDOW + || clause.type == FLWORClause.ClauseType.FOR_MEMBER + || clause.type == FLWORClause.ClauseType.FOR_KEY + || clause.type == FLWORClause.ClauseType.FOR_VALUE + || clause.type == FLWORClause.ClauseType.FOR_KEY_VALUE) { + final BindingExpression bind = (BindingExpression)expr; + bind.setVariable(clause.varName); + bind.setSequenceType(clause.sequenceType); + bind.setInputSequence(clause.inputSequence); + if (clause.type == FLWORClause.ClauseType.FOR) { + ((ForExpr) bind).setPositionalVariable(clause.posVar); + } else if (clause.type == FLWORClause.ClauseType.FOR_MEMBER) { + ((ForMemberExpr) bind).setPositionalVariable(clause.posVar); + } else if (clause.type == FLWORClause.ClauseType.FOR_KEY + || clause.type == FLWORClause.ClauseType.FOR_VALUE + || clause.type == FLWORClause.ClauseType.FOR_KEY_VALUE) { + ((ForKeyValueExpr) bind).setPositionalVariable(clause.posVar); + if (clause.valueVarName != null) { + ((ForKeyValueExpr) bind).setValueVariable(clause.valueVarName); + if (clause.valueSequenceType != null) { + ((ForKeyValueExpr) bind).setValueSequenceType(clause.valueSequenceType); + } + } + } + } else if (clause.type == FLWORClause.ClauseType.GROUPBY) { + if (clause.groupSpecs != null) { + GroupSpec specs[] = new GroupSpec[clause.groupSpecs.size()]; + int k = 0; + for (GroupSpec groupSpec : clause.groupSpecs) { + specs[k++]= groupSpec; + } + ((GroupByClause)expr).setGroupSpecs(specs); } - ) - { step = tswitch; } - ) - | - // logical operator: or - #( - "or" - { - PathExpr left= new PathExpr(context); - left.setASTNode(expr_AST_in); - } - step=expr [left] - { - PathExpr right= new PathExpr(context); - right.setASTNode(expr_AST_in); - } - step=expr [right] - ) - { - OpOr or= new OpOr(context); - or.addPath(left); - or.addPath(right); - path.addPath(or); - step = or; - } - | - // logical operator: and - #( - "and" - { - PathExpr left= new PathExpr(context); - left.setASTNode(expr_AST_in); - - PathExpr right= new PathExpr(context); - right.setASTNode(expr_AST_in); - } - step=expr [left] - step=expr [right] - ) - { - OpAnd and= new OpAnd(context); - and.addPath(left); - and.addPath(right); - path.addPath(and); - step = and; - } - | - // union expressions: | and union - #( - UNION - { - PathExpr left= new PathExpr(context); - left.setASTNode(expr_AST_in); - - PathExpr right= new PathExpr(context); - right.setASTNode(expr_AST_in); + } + if (!(action instanceof FLWORClause)) + expr.setReturnExpression(new DebuggableExpression(action)); + else { + expr.setReturnExpression(action); + ((FLWORClause)action).setPreviousClause(expr); } - step=expr [left] - step=expr [right] - ) - { - Union union= new Union(context, left, right); - path.add(union); - step = union; - } - | - // intersections: - #( "intersect" - { - PathExpr left = new PathExpr(context); - left.setASTNode(expr_AST_in); - PathExpr right = new PathExpr(context); - right.setASTNode(expr_AST_in); - } - step=expr [left] - step=expr [right] - ) - { - Intersect intersect = new Intersect(context, left, right); - path.add(intersect); - step = intersect; - } - | - #( "except" - { - PathExpr left = new PathExpr(context); - left.setASTNode(expr_AST_in); + action= expr; + } - PathExpr right = new PathExpr(context); - right.setASTNode(expr_AST_in); - } - step=expr [left] - step=expr [right] - ) - { - Except intersect = new Except(context, left, right); - path.add(intersect); - step = intersect; - } - | - // absolute path expression starting with a / - #( - ABSOLUTE_SLASH - { - RootNode root= new RootNode(context); - path.add(root); - } - ( step=expr [path] )? - ) - | - // absolute path expression starting with // - #( - ABSOLUTE_DSLASH - { - RootNode root= new RootNode(context); - path.add(root); + path.add(action); + step = action; } - ( - step=expr [path] - { - if (step instanceof LocationStep) { - LocationStep s= (LocationStep) step; - if (s.getAxis() == Constants.ATTRIBUTE_AXIS || - (s.getTest().getType() == Type.ATTRIBUTE && s.getAxis() == Constants.CHILD_AXIS)) - // combines descendant-or-self::node()/attribute:* - s.setAxis(Constants.DESCENDANT_ATTRIBUTE_AXIS); - else { - s.setAxis(Constants.DESCENDANT_SELF_AXIS); - s.setAbbreviated(true); - } - } else - step.setPrimaryAxis(Constants.DESCENDANT_SELF_AXIS); - } - )? ) | - // range expression: to + // instance of: #( - "to" - { - PathExpr start= new PathExpr(context); - start.setASTNode(expr_AST_in); - - PathExpr end= new PathExpr(context); - end.setASTNode(expr_AST_in); - - List args= new ArrayList(2); - args.add(start); - args.add(end); - } - step=expr [start] - step=expr [end] + "instance" { - RangeExpression range= new RangeExpression(context); - range.setASTNode(expr_AST_in); - range.setArguments(args); - path.addPath(range); - step = range; + PathExpr expr = new PathExpr(context); + expr.setASTNode(exprFlowControl_AST_in); + SequenceType type= new SequenceType(); } - ) - | - step=generalComp [path] - | - step=valueComp [path] - | - step=nodeComp [path] - | - step=primaryExpr [path] - | - step=pathExpr [path] - | - step=extensionExpr [path] - | - step=numericExpr [path] - | - step=updateExpr [path] + step=expr [expr] + sequenceType [type] + { + step = new InstanceOfExpression(context, expr, type); + step.setASTNode(exprFlowControl_AST_in); + path.add(step); + } + ) ; /** @@ -2495,14 +3126,63 @@ throws PermissionDeniedException, EXistException, XPathException step=postfixExpr [step] { path.add(step); } | + ql:QNAME_LITERAL + { + final String qlText = ql.getText(); + final QName qlQName; + try { + qlQName = QName.parse(staticContext, qlText); + } catch (final IllegalQNameException iqe) { + throw new XPathException(ql.getLine(), ql.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + qlText); + } + step = new LiteralValue(context, new QNameValue(context, qlQName)); + step.setASTNode(ql); + } + step=postfixExpr [step] + { path.add(step); } + | step=inlineFunctionDecl [path] step=postfixExpr [step] { path.add(step); } | + step=focusFunctionDecl [path] + step=postfixExpr [step] + { path.add(step); } + | step = lookup [null] step=postfixExpr [step] { path.add(step); } | + #( + stAST:STRING_TEMPLATE + { + StringConstructor st = new StringConstructor(context); + st.setASTNode(stAST); + } + ( + stContent:STRING_TEMPLATE_CONTENT + { + // Unescape {{ -> {, }} -> }, `` -> ` + String raw = stContent.getText(); + raw = raw.replace("{{", "{").replace("}}", "}").replace("``", "`"); + st.addContent(raw); + } + | + { + PathExpr stInterpolation = new PathExpr(context); + stInterpolation.setASTNode(primaryExpr_AST_in); + } + expr[stInterpolation] + { + st.addInterpolation(stInterpolation.simplify()); + } + )* + { + path.add(st); + step = st; + } + ) + | #( scAST:STRING_CONSTRUCTOR_START { @@ -3024,21 +3704,30 @@ throws XPathException | i:INTEGER_LITERAL { - step= new LiteralValue(context, new IntegerValue(i.getText())); + String itext = i.getText().replace("_", ""); + java.math.BigInteger intVal; + if (itext.startsWith("0x") || itext.startsWith("0X")) { + intVal = new java.math.BigInteger(itext.substring(2), 16); + } else if (itext.startsWith("0b") || itext.startsWith("0B")) { + intVal = new java.math.BigInteger(itext.substring(2), 2); + } else { + intVal = new java.math.BigInteger(itext); + } + step= new LiteralValue(context, new IntegerValue(intVal)); step.setASTNode(i); } | ( dec:DECIMAL_LITERAL { - step= new LiteralValue(context, new DecimalValue(dec.getText())); + step= new LiteralValue(context, new DecimalValue(dec.getText().replace("_", ""))); step.setASTNode(dec); } | dbl:DOUBLE_LITERAL { step= new LiteralValue(context, - new DoubleValue(Double.parseDouble(dbl.getText()))); + new DoubleValue(Double.parseDouble(dbl.getText().replace("_", "")))); step.setASTNode(dbl); } ) @@ -3137,6 +3826,19 @@ throws PermissionDeniedException, EXistException, XPathException ( step = lookup [step] | + #( + fam:FILTER_AM + { + PathExpr filterPred = new PathExpr(context); + filterPred.setASTNode(postfixExpr_AST_in); + } + expr [filterPred] + { + step = new FilterExprAM(context, step, filterPred.simplify()); + step.setASTNode(fam); + } + ) + | #( PREDICATE { @@ -3212,6 +3914,55 @@ throws PermissionDeniedException, EXistException, XPathException ( pos:INTEGER_VALUE { position = Integer.parseInt(pos.getText()); } | + // XQ4: string literal as key selector (?"first value") + strKey:STRING_LITERAL + { + lookupExpr.add(new LiteralValue(context, new StringValue(strKey.getText()))); + } + | + // XQ4: decimal literal as key selector (?1.2) + decKey:DECIMAL_LITERAL + { + lookupExpr.add(new LiteralValue(context, new DecimalValue(decKey.getText().replace("_", "")))); + } + | + // XQ4: double literal as key selector (?1.2e0) + dblKey:DOUBLE_LITERAL + { + lookupExpr.add(new LiteralValue(context, new DoubleValue(Double.parseDouble(dblKey.getText().replace("_", ""))))); + } + | + // XQ4: variable reference as key selector (?$var) + varKey:VARIABLE_REF + { + final QName varQn; + try { + varQn = QName.parse(staticContext, varKey.getText(), null); + } catch (final IllegalQNameException iqe) { + throw new XPathException(varKey.getLine(), varKey.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + varKey.getText()); + } + lookupExpr.add(new VariableReference(context, varQn)); + } + | + // XQ4: context item as key selector (?.) + ctxKey:SELF + { + lookupExpr.add(new ContextItemExpression(context)); + } + | + // XQ4: QName literal as key selector (?#name) + qnKey:QNAME_LITERAL + { + final String qnText = qnKey.getText(); + final QName qnQName; + try { + qnQName = QName.parse(staticContext, qnText); + } catch (final IllegalQNameException iqe) { + throw new XPathException(qnKey.getLine(), qnKey.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + qnText); + } + lookupExpr.add(new LiteralValue(context, new QNameValue(context, qnQName))); + } + | ( expr [lookupExpr] )+ )? { @@ -3254,6 +4005,27 @@ throws PermissionDeniedException, EXistException, XPathException isPartial = true; } | + #( + kw:KEYWORD_ARG + ( + QUESTION { + // Keyword argument with placeholder value: name := ? + params.add(new KeywordArgumentExpression(context, kw.getText(), + new Function.Placeholder(context))); + isPartial = true; + } + | + { + PathExpr kwExpr = new PathExpr(context); + kwExpr.setASTNode(functionCall_AST_in); + } + expr [kwExpr] + { + params.add(new KeywordArgumentExpression(context, kw.getText(), kwExpr)); + } + ) + ) + | expr [pathExpr] { params.add(pathExpr); } ) )* @@ -3288,7 +4060,7 @@ throws PermissionDeniedException, EXistException, XPathException } catch (final IllegalQNameException iqe) { throw new XPathException(name.getLine(), name.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + name.getText()); } - NamedFunctionReference ref = new NamedFunctionReference(context, qname, Integer.parseInt(arity.getText())); + NamedFunctionReference ref = new NamedFunctionReference(context, qname, Integer.parseInt(arity.getText().replace("_", ""))); step = ref; } ) @@ -3321,6 +4093,14 @@ throws PermissionDeniedException, EXistException "ancestor" { axis= Constants.ANCESTOR_AXIS; } | "ancestor-or-self" { axis= Constants.ANCESTOR_SELF_AXIS; } + | + "following-or-self" { axis= Constants.FOLLOWING_OR_SELF_AXIS; } + | + "preceding-or-self" { axis= Constants.PRECEDING_OR_SELF_AXIS; } + | + "following-sibling-or-self" { axis= Constants.FOLLOWING_SIBLING_OR_SELF_AXIS; } + | + "preceding-sibling-or-self" { axis= Constants.PRECEDING_SIBLING_OR_SELF_AXIS; } ; valueComp [PathExpr path] @@ -3818,6 +4598,140 @@ throws PermissionDeniedException, EXistException, XPathException ) ; +mappingArrowOp [PathExpr path] +returns [Expression step] +throws PermissionDeniedException, EXistException, XPathException +{ + step= null; +}: + #( + mapArrowAST:MAPPING_ARROW_OP + { + PathExpr leftExpr = new PathExpr(context); + leftExpr.setASTNode(mappingArrowOp_AST_in); + } + expr [leftExpr] + { + MappingArrowOperator op = new MappingArrowOperator(context, leftExpr.simplify()); + op.setASTNode(mapArrowAST); + path.add(op); + step = op; + + PathExpr nameExpr = new PathExpr(context); + nameExpr.setASTNode(mappingArrowOp_AST_in); + String name = null; + } + ( + eq:EQNAME + { name = eq.toString(); } + | + expr [nameExpr] + ) + { List params = new ArrayList(5); } + ( + { + PathExpr pathExpr = new PathExpr(context); + pathExpr.setASTNode(mappingArrowOp_AST_in); + } + expr [pathExpr] { params.add(pathExpr.simplify()); } + )* + { + if (name == null) { + op.setArrowFunction(nameExpr, params); + } else { + op.setArrowFunction(name, params); + } + } + ) + ; + +pipelineOp [PathExpr path] +returns [Expression step] +throws PermissionDeniedException, EXistException, XPathException +{ + step = null; +}: + #( + pipeAST:PIPELINE_OP + { + PathExpr leftExpr = new PathExpr(context); + leftExpr.setASTNode(pipelineOp_AST_in); + } + expr [leftExpr] + { + PathExpr rightExpr = new PathExpr(context); + rightExpr.setASTNode(pipelineOp_AST_in); + } + expr [rightExpr] + { + step = new PipelineExpression(context, leftExpr.simplify(), rightExpr.simplify()); + step.setASTNode(pipeAST); + path.add(step); + } + ) + ; + +methodCallOp [PathExpr path] +returns [Expression step] +throws PermissionDeniedException, EXistException, XPathException +{ + step = null; +}: + #( + mcAST:METHOD_CALL_OP + { + PathExpr leftExpr = new PathExpr(context); + leftExpr.setASTNode(methodCallOp_AST_in); + } + expr [leftExpr] + mn:NCNAME + { + MethodCallOperator op = new MethodCallOperator(context, leftExpr.simplify()); + op.setASTNode(mcAST); + path.add(op); + step = op; + + List params = new ArrayList(5); + } + ( + { + PathExpr pathExpr = new PathExpr(context); + pathExpr.setASTNode(methodCallOp_AST_in); + } + expr [pathExpr] { params.add(pathExpr.simplify()); } + )* + { + op.setMethod(mn.getText(), params); + } + ) + ; + +otherwiseExpr [PathExpr path] +returns [Expression step] +throws PermissionDeniedException, EXistException, XPathException +{ + step = null; +}: + #( + owAST:LITERAL_otherwise + { + PathExpr leftExpr = new PathExpr(context); + leftExpr.setASTNode(otherwiseExpr_AST_in); + } + expr [leftExpr] + { + PathExpr rightExpr = new PathExpr(context); + rightExpr.setASTNode(otherwiseExpr_AST_in); + } + expr [rightExpr] + { + step = new OtherwiseExpression(context, leftExpr.simplify(), rightExpr.simplify()); + step.setASTNode(owAST); + path.add(step); + } + ) + ; + typeCastExpr [PathExpr path] returns [Expression step] throws PermissionDeniedException, EXistException, XPathException @@ -3832,25 +4746,72 @@ throws PermissionDeniedException, EXistException, XPathException Cardinality cardinality= Cardinality.EXACTLY_ONE; } step=expr [expr] - t:ATOMIC_TYPE ( - QUESTION - { cardinality= Cardinality.ZERO_OR_ONE; } - )? - { - try { - QName qn= QName.parse(staticContext, t.getText()); - int code= Type.getType(qn); - CastExpression castExpr= new CastExpression(context, expr, code, cardinality); + #( + CHOICE_TYPE + { + List choiceTypes = new ArrayList(); + } + ( + ct:ATOMIC_TYPE + { + try { + QName qn = QName.parse(staticContext, ct.getText()); + choiceTypes.add(Type.getType(qn)); + } catch (final XPathException e) { + throw new XPathException(ct.getLine(), ct.getColumn(), ErrorCodes.XPST0051, "Unknown simple type " + ct.getText()); + } catch (final IllegalQNameException e) { + throw new XPathException(ct.getLine(), ct.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + ct.getText()); + } + } + )+ + ) + ( + QUESTION + { cardinality= Cardinality.ZERO_OR_ONE; } + )? + { + int[] types = new int[choiceTypes.size()]; + for (int ci = 0; ci < choiceTypes.size(); ci++) { types[ci] = choiceTypes.get(ci); } + ChoiceCastExpression castExpr = new ChoiceCastExpression(context, expr, types, cardinality); castExpr.setASTNode(castAST); path.add(castExpr); step = castExpr; - } catch (final XPathException e) { - throw new XPathException(t.getLine(), t.getColumn(), ErrorCodes.XPST0051, "Unknown simple type " + t.getText()); - } catch (final IllegalQNameException e) { - throw new XPathException(t.getLine(), t.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + t.getText()); } - } + | + t:ATOMIC_TYPE + ( + QUESTION + { cardinality= Cardinality.ZERO_OR_ONE; } + )? + { + try { + QName qn= QName.parse(staticContext, t.getText()); + int code= Type.getType(qn); + CastExpression castExpr= new CastExpression(context, expr, code, cardinality); + castExpr.setASTNode(castAST); + path.add(castExpr); + step = castExpr; + } catch (final XPathException e) { + throw new XPathException(t.getLine(), t.getColumn(), ErrorCodes.XPST0051, "Unknown simple type " + t.getText()); + } catch (final IllegalQNameException e) { + throw new XPathException(t.getLine(), t.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + t.getText()); + } + } + | + enumCast:ENUM_TYPE + ( + QUESTION + { cardinality= Cardinality.ZERO_OR_ONE; } + )? + { + String[] enumVals = enumCast.getText().split(",", -1); + EnumCastExpression enumCastExpr = new EnumCastExpression(context, expr, enumVals, cardinality, false); + enumCastExpr.setASTNode(castAST); + path.add(enumCastExpr); + step = enumCastExpr; + } + ) ) | #( @@ -3861,25 +4822,72 @@ throws PermissionDeniedException, EXistException, XPathException Cardinality cardinality= Cardinality.EXACTLY_ONE; } step=expr [expr] - t2:ATOMIC_TYPE ( - QUESTION - { cardinality= Cardinality.ZERO_OR_ONE; } - )? - { - try { - QName qn= QName.parse(staticContext, t2.getText()); - int code= Type.getType(qn); - CastableExpression castExpr= new CastableExpression(context, expr, code, cardinality); - castExpr.setASTNode(castAST); + #( + CHOICE_TYPE + { + List choiceTypes2 = new ArrayList(); + } + ( + ct2:ATOMIC_TYPE + { + try { + QName qn = QName.parse(staticContext, ct2.getText()); + choiceTypes2.add(Type.getType(qn)); + } catch (final XPathException e) { + throw new XPathException(ct2.getLine(), ct2.getColumn(), ErrorCodes.XPST0051, "Unknown simple type " + ct2.getText()); + } catch (final IllegalQNameException e) { + throw new XPathException(ct2.getLine(), ct2.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + ct2.getText()); + } + } + )+ + ) + ( + QUESTION + { cardinality= Cardinality.ZERO_OR_ONE; } + )? + { + int[] types2 = new int[choiceTypes2.size()]; + for (int ci = 0; ci < choiceTypes2.size(); ci++) { types2[ci] = choiceTypes2.get(ci); } + ChoiceCastableExpression castExpr = new ChoiceCastableExpression(context, expr, types2, cardinality); + castExpr.setASTNode(castableAST); path.add(castExpr); step = castExpr; - } catch (final XPathException e) { - throw new XPathException(t2.getLine(), t2.getColumn(), ErrorCodes.XPST0051, "Unknown simple type " + t2.getText()); - } catch (final IllegalQNameException e) { - throw new XPathException(t2.getLine(), t2.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + t2.getText()); } - } + | + t2:ATOMIC_TYPE + ( + QUESTION + { cardinality= Cardinality.ZERO_OR_ONE; } + )? + { + try { + QName qn= QName.parse(staticContext, t2.getText()); + int code= Type.getType(qn); + CastableExpression castExpr= new CastableExpression(context, expr, code, cardinality); + castExpr.setASTNode(castableAST); + path.add(castExpr); + step = castExpr; + } catch (final XPathException e) { + throw new XPathException(t2.getLine(), t2.getColumn(), ErrorCodes.XPST0051, "Unknown simple type " + t2.getText()); + } catch (final IllegalQNameException e) { + throw new XPathException(t2.getLine(), t2.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + t2.getText()); + } + } + | + enumCastable:ENUM_TYPE + ( + QUESTION + { cardinality= Cardinality.ZERO_OR_ONE; } + )? + { + String[] enumVals2 = enumCastable.getText().split(",", -1); + EnumCastExpression enumCastExpr2 = new EnumCastExpression(context, expr, enumVals2, cardinality, true); + enumCastExpr2.setASTNode(castableAST); + path.add(enumCastExpr2); + step = enumCastExpr2; + } + ) ) ; diff --git a/exist-core/src/test/java/org/exist/xquery/ReservedKeywordsAsNCNamesTest.java b/exist-core/src/test/java/org/exist/xquery/ReservedKeywordsAsNCNamesTest.java new file mode 100644 index 00000000000..1d693030c1c --- /dev/null +++ b/exist-core/src/test/java/org/exist/xquery/ReservedKeywordsAsNCNamesTest.java @@ -0,0 +1,115 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.test.ExistXmldbEmbeddedServer; +import org.junit.ClassRule; +import org.junit.Test; +import org.xmldb.api.base.ResourceSet; +import org.xmldb.api.base.XMLDBException; +import org.xmldb.api.modules.XQueryService; + +import static org.junit.Assert.assertEquals; + +/** + * Verifies that XQuery keywords can be used as variable names (NCNames). + * + * The reservedKeywords grammar rule lists keywords that are also valid NCNames. + * Keywords used in grammar rules but missing from this list cause XPST0003 + * parse errors when used as variable names. + */ +public class ReservedKeywordsAsNCNamesTest { + + @ClassRule + public static final ExistXmldbEmbeddedServer existEmbeddedServer = + new ExistXmldbEmbeddedServer(false, true, true); + + private ResourceSet execute(final String xquery) throws XMLDBException { + final XQueryService xqs = existEmbeddedServer.getRoot().getService(XQueryService.class); + return xqs.query(xquery); + } + + @Test + public void ascendingDescendingAsVariableNames() throws XMLDBException { + final ResourceSet result = execute( + "let $ascending := 1, $descending := 2 return $ascending + $descending"); + assertEquals("3", result.getResource(0).getContent().toString()); + } + + @Test + public void greatestLeastAsVariableNames() throws XMLDBException { + final ResourceSet result = execute( + "let $greatest := 10, $least := 1 return $greatest - $least"); + assertEquals("9", result.getResource(0).getContent().toString()); + } + + @Test + public void satisfiesAsVariableName() throws XMLDBException { + final ResourceSet result = execute( + "let $satisfies := 'ok' return $satisfies"); + assertEquals("ok", result.getResource(0).getContent().toString()); + } + + @Test + public void castableAsVariableName() throws XMLDBException { + final ResourceSet result = execute( + "let $castable := true() return $castable"); + assertEquals("true", result.getResource(0).getContent().toString()); + } + + @Test + public void idivAsVariableName() throws XMLDBException { + final ResourceSet result = execute( + "let $idiv := 42 return $idiv"); + assertEquals("42", result.getResource(0).getContent().toString()); + } + + @Test + public void processingInstructionAsVariableName() throws XMLDBException { + final ResourceSet result = execute( + "let $processing-instruction := 'test' return $processing-instruction"); + assertEquals("test", result.getResource(0).getContent().toString()); + } + + @Test + public void schemaAttributeAsVariableName() throws XMLDBException { + final ResourceSet result = execute( + "let $schema-attribute := 'sa' return $schema-attribute"); + assertEquals("sa", result.getResource(0).getContent().toString()); + } + + @Test + public void allowingAsVariableName() throws XMLDBException { + final ResourceSet result = execute( + "let $allowing := 'yes' return $allowing"); + assertEquals("yes", result.getResource(0).getContent().toString()); + } + + @Test + public void allKeywordsTogether() throws XMLDBException { + final ResourceSet result = execute( + "let $ascending := 1, $descending := 2, $greatest := 3, $least := 4,\n" + + " $satisfies := 5, $castable := 6, $idiv := 7\n" + + "return $ascending + $descending + $greatest + $least + $satisfies + $castable + $idiv"); + assertEquals("28", result.getResource(0).getContent().toString()); + } +} From 2916f59de33b0de1e460eef6fddd3a7df3757b29 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Sat, 4 Apr 2026 09:52:12 -0400 Subject: [PATCH 02/20] [feature] Add XQuery 4.0 expression classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New expression classes that implement the evaluation logic for XQ4 syntax features parsed by the grammar: - PipelineExpression: evaluate left expr, bind to context for right - FocusFunction: anonymous function with implicit context item (.) - KeywordArgumentExpression: wraps named args for function dispatch - MappingArrowOperator: => operator (function application) - MethodCallOperator: =?> operator (method-style dispatch) - OtherwiseExpression: return left if non-empty, else right - FilterExprAM: ?[predicate] — array/map member filter - ForMemberExpr: for member $x in $array — iterate array members - ForKeyValueExpr: for key/value pair iteration - WhileClause: while (cond) in FLWOR — conditional iteration - LetDestructureExpr: let destructuring bindings - StringConstructor: XQ4 string template interpolation - ChoiceCastExpression/ChoiceCastableExpression: union/choice type casts - EnumCastExpression: enumeration type casts Each class extends Expression/AbstractExpression and implements eval() with proper context handling and dependency tracking. Spec: QT4 XQuery 4.0 §3 (Expressions) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../exist/xquery/ChoiceCastExpression.java | 137 +++++++ .../xquery/ChoiceCastableExpression.java | 128 +++++++ .../org/exist/xquery/EnumCastExpression.java | 141 ++++++++ .../java/org/exist/xquery/FilterExprAM.java | 242 +++++++++++++ .../java/org/exist/xquery/FocusFunction.java | 140 ++++++++ .../org/exist/xquery/ForKeyValueExpr.java | 308 ++++++++++++++++ .../java/org/exist/xquery/ForMemberExpr.java | 239 +++++++++++++ .../xquery/KeywordArgumentExpression.java | 85 +++++ .../org/exist/xquery/LetDestructureExpr.java | 335 ++++++++++++++++++ .../exist/xquery/MappingArrowOperator.java | 207 +++++++++++ .../org/exist/xquery/MethodCallOperator.java | 211 +++++++++++ .../org/exist/xquery/OtherwiseExpression.java | 90 +++++ .../org/exist/xquery/PipelineExpression.java | 106 ++++++ .../java/org/exist/xquery/WhileClause.java | 136 +++++++ 14 files changed, 2505 insertions(+) create mode 100644 exist-core/src/main/java/org/exist/xquery/ChoiceCastExpression.java create mode 100644 exist-core/src/main/java/org/exist/xquery/ChoiceCastableExpression.java create mode 100644 exist-core/src/main/java/org/exist/xquery/EnumCastExpression.java create mode 100644 exist-core/src/main/java/org/exist/xquery/FilterExprAM.java create mode 100644 exist-core/src/main/java/org/exist/xquery/FocusFunction.java create mode 100644 exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java create mode 100644 exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java create mode 100644 exist-core/src/main/java/org/exist/xquery/KeywordArgumentExpression.java create mode 100644 exist-core/src/main/java/org/exist/xquery/LetDestructureExpr.java create mode 100644 exist-core/src/main/java/org/exist/xquery/MappingArrowOperator.java create mode 100644 exist-core/src/main/java/org/exist/xquery/MethodCallOperator.java create mode 100644 exist-core/src/main/java/org/exist/xquery/OtherwiseExpression.java create mode 100644 exist-core/src/main/java/org/exist/xquery/PipelineExpression.java create mode 100644 exist-core/src/main/java/org/exist/xquery/WhileClause.java diff --git a/exist-core/src/main/java/org/exist/xquery/ChoiceCastExpression.java b/exist-core/src/main/java/org/exist/xquery/ChoiceCastExpression.java new file mode 100644 index 00000000000..1f58834103f --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/ChoiceCastExpression.java @@ -0,0 +1,137 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.dom.persistent.DocumentSet; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.*; + +/** + * Implements cast as (T1 | T2 | ...) from XQuery 4.0. + * Tries each target type in order and returns the first successful cast. + */ +public class ChoiceCastExpression extends AbstractExpression { + + private final int[] targetTypes; + private final Cardinality cardinality; + private Expression expression; + + public ChoiceCastExpression(final XQueryContext context, final Expression expr, + final int[] targetTypes, final Cardinality cardinality) { + super(context); + this.targetTypes = targetTypes; + this.cardinality = cardinality; + this.expression = expr; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + contextInfo.setParent(this); + expression.analyze(contextInfo); + } + + @Override + public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException { + final Sequence seq = Atomize.atomize(expression.eval(contextSequence, contextItem)); + if (seq.isEmpty()) { + if (cardinality.atLeastOne()) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Type error: empty sequence is not allowed here"); + } + return Sequence.EMPTY_SEQUENCE; + } + if (seq.hasMany()) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "cardinality error: sequence with more than one item is not allowed here"); + } + + final Item item = seq.itemAt(0); + XPathException lastError = null; + + for (final int targetType : targetTypes) { + try { + return item.convertTo(targetType); + } catch (final XPathException e) { + lastError = e; + } + } + + throw new XPathException(this, ErrorCodes.FORG0001, + "Cannot cast " + Type.getTypeName(item.getType()) + + " to any of the choice types", lastError); + } + + @Override + public int returnsType() { + return Type.ANY_ATOMIC_TYPE; + } + + @Override + public Cardinality getCardinality() { + return Cardinality.ZERO_OR_ONE; + } + + @Override + public void dump(final ExpressionDumper dumper) { + expression.dump(dumper); + dumper.display(" cast as ("); + for (int i = 0; i < targetTypes.length; i++) { + if (i > 0) { + dumper.display(" | "); + } + dumper.display(Type.getTypeName(targetTypes[i])); + } + dumper.display(")"); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append(expression.toString()).append(" cast as ("); + for (int i = 0; i < targetTypes.length; i++) { + if (i > 0) { + sb.append(" | "); + } + sb.append(Type.getTypeName(targetTypes[i])); + } + sb.append(")"); + return sb.toString(); + } + + @Override + public int getDependencies() { + return expression.getDependencies() | Dependency.CONTEXT_ITEM; + } + + @Override + public void setContextDocSet(final DocumentSet contextSet) { + super.setContextDocSet(contextSet); + expression.setContextDocSet(contextSet); + } + + @Override + public void resetState(final boolean postOptimization) { + super.resetState(postOptimization); + expression.resetState(postOptimization); + } + +} diff --git a/exist-core/src/main/java/org/exist/xquery/ChoiceCastableExpression.java b/exist-core/src/main/java/org/exist/xquery/ChoiceCastableExpression.java new file mode 100644 index 00000000000..4d867b21e44 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/ChoiceCastableExpression.java @@ -0,0 +1,128 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.dom.persistent.DocumentSet; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.*; + +/** + * Implements castable as (T1 | T2 | ...) from XQuery 4.0. + * Returns true if the value can be cast to any of the target types. + */ +public class ChoiceCastableExpression extends AbstractExpression { + + private final int[] targetTypes; + private final Cardinality requiredCardinality; + private final Expression expression; + + public ChoiceCastableExpression(final XQueryContext context, final Expression expr, + final int[] targetTypes, final Cardinality requiredCardinality) { + super(context); + this.expression = expr; + this.targetTypes = targetTypes; + this.requiredCardinality = requiredCardinality; + } + + @Override + public int returnsType() { + return Type.BOOLEAN; + } + + @Override + public Cardinality getCardinality() { + return Cardinality.EXACTLY_ONE; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + contextInfo.setParent(this); + expression.analyze(contextInfo); + } + + @Override + public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException { + final Sequence seq = Atomize.atomize(expression.eval(contextSequence, contextItem)); + if (seq.isEmpty()) { + return BooleanValue.valueOf( + requiredCardinality.isSuperCardinalityOrEqualOf(Cardinality.EMPTY_SEQUENCE)); + } + if (!requiredCardinality.isSuperCardinalityOrEqualOf(seq.getCardinality())) { + return BooleanValue.FALSE; + } + + final Item item = seq.itemAt(0); + for (final int targetType : targetTypes) { + try { + item.convertTo(targetType); + return BooleanValue.TRUE; + } catch (final XPathException e) { + // try next type + } + } + return BooleanValue.FALSE; + } + + @Override + public void dump(final ExpressionDumper dumper) { + expression.dump(dumper); + dumper.display(" castable as ("); + for (int i = 0; i < targetTypes.length; i++) { + if (i > 0) { + dumper.display(" | "); + } + dumper.display(Type.getTypeName(targetTypes[i])); + } + dumper.display(")"); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append(expression.toString()).append(" castable as ("); + for (int i = 0; i < targetTypes.length; i++) { + if (i > 0) { + sb.append(" | "); + } + sb.append(Type.getTypeName(targetTypes[i])); + } + sb.append(")"); + return sb.toString(); + } + + @Override + public int getDependencies() { + return Dependency.CONTEXT_SET + Dependency.CONTEXT_ITEM; + } + + @Override + public void setContextDocSet(final DocumentSet contextSet) { + super.setContextDocSet(contextSet); + expression.setContextDocSet(contextSet); + } + + @Override + public void resetState(final boolean postOptimization) { + super.resetState(postOptimization); + expression.resetState(postOptimization); + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/EnumCastExpression.java b/exist-core/src/main/java/org/exist/xquery/EnumCastExpression.java new file mode 100644 index 00000000000..bf0fc6ce7b2 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/EnumCastExpression.java @@ -0,0 +1,141 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.dom.persistent.DocumentSet; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.*; + +/** + * Implements cast as enum("a","b","c") and castable as enum("a","b","c") from XQuery 4.0. + */ +public class EnumCastExpression extends AbstractExpression { + + private final String[] enumValues; + private final Cardinality cardinality; + private final Expression expression; + private final boolean isCastable; + + public EnumCastExpression(final XQueryContext context, final Expression expr, + final String[] enumValues, final Cardinality cardinality, + final boolean isCastable) { + super(context); + this.expression = expr; + this.enumValues = enumValues; + this.cardinality = cardinality; + this.isCastable = isCastable; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + contextInfo.setParent(this); + expression.analyze(contextInfo); + } + + @Override + public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException { + final Sequence seq = Atomize.atomize(expression.eval(contextSequence, contextItem)); + + if (seq.isEmpty()) { + if (isCastable) { + return BooleanValue.valueOf( + cardinality.isSuperCardinalityOrEqualOf(Cardinality.EMPTY_SEQUENCE)); + } + if (cardinality.atLeastOne()) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Type error: empty sequence is not allowed here"); + } + return Sequence.EMPTY_SEQUENCE; + } + + final String value = seq.itemAt(0).getStringValue(); + + for (final String enumVal : enumValues) { + if (enumVal.equals(value)) { + if (isCastable) { + return BooleanValue.TRUE; + } + return new StringValue(this, value); + } + } + + if (isCastable) { + return BooleanValue.FALSE; + } + throw new XPathException(this, ErrorCodes.FORG0001, + "Cannot cast '" + value + "' to enum type"); + } + + @Override + public int returnsType() { + return isCastable ? Type.BOOLEAN : Type.STRING; + } + + @Override + public Cardinality getCardinality() { + return isCastable ? Cardinality.EXACTLY_ONE : Cardinality.ZERO_OR_ONE; + } + + @Override + public void dump(final ExpressionDumper dumper) { + expression.dump(dumper); + dumper.display(isCastable ? " castable as enum(" : " cast as enum("); + for (int i = 0; i < enumValues.length; i++) { + if (i > 0) { + dumper.display(", "); + } + dumper.display("\"" + enumValues[i] + "\""); + } + dumper.display(")"); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append(expression.toString()).append(isCastable ? " castable as enum(" : " cast as enum("); + for (int i = 0; i < enumValues.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append("\"").append(enumValues[i]).append("\""); + } + sb.append(")"); + return sb.toString(); + } + + @Override + public int getDependencies() { + return expression.getDependencies() | Dependency.CONTEXT_ITEM; + } + + @Override + public void setContextDocSet(final DocumentSet contextSet) { + super.setContextDocSet(contextSet); + expression.setContextDocSet(contextSet); + } + + @Override + public void resetState(final boolean postOptimization) { + super.resetState(postOptimization); + expression.resetState(postOptimization); + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/FilterExprAM.java b/exist-core/src/main/java/org/exist/xquery/FilterExprAM.java new file mode 100644 index 00000000000..f07af305e12 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/FilterExprAM.java @@ -0,0 +1,242 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.xquery.functions.array.ArrayType; +import org.exist.xquery.functions.map.AbstractMapType; +import org.exist.xquery.functions.map.MapType; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.AtomicValue; +import org.exist.xquery.value.Item; +import org.exist.xquery.value.NumericValue; +import org.exist.xquery.value.Sequence; +import org.exist.xquery.value.SequenceIterator; +import org.exist.xquery.value.StringValue; +import org.exist.xquery.value.Type; +import org.exist.xquery.value.ValueSequence; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Implements the XQuery 4.0 array/map filter expression ({@code ?[predicate]}). + * + *

For arrays, iterates over members and keeps those where the predicate + * evaluates to true with the context item set to each member. + * Numeric predicates select by position (1-based).

+ * + *

For maps, iterates over entries and keeps those where the predicate + * evaluates to true with the context item set to + * {@code map { "key": key, "value": value }} for each entry. + * Numeric predicates select by position in insertion order.

+ */ +public class FilterExprAM extends AbstractExpression { + + private Expression contextExpr; + private Expression predicate; + + public FilterExprAM(final XQueryContext context, final Expression contextExpr, final Expression predicate) { + super(context); + this.contextExpr = contextExpr; + this.predicate = predicate; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + contextExpr.analyze(contextInfo); + final AnalyzeContextInfo predicateInfo = new AnalyzeContextInfo(contextInfo); + predicate.analyze(predicateInfo); + } + + @Override + public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { + if (contextItem != null) { + contextSequence = contextItem.toSequence(); + } + final Sequence input = contextExpr.eval(contextSequence, null); + + if (input.isEmpty()) { + return input; + } + + final Item item = input.itemAt(0); + if (Type.subTypeOf(item.getType(), Type.ARRAY_ITEM)) { + return filterArray((ArrayType) item); + } else if (Type.subTypeOf(item.getType(), Type.MAP_ITEM)) { + return filterMap((AbstractMapType) item); + } else { + throw new XPathException(this, ErrorCodes.XPTY0004, + "?[] filter requires an array or map, got " + Type.getTypeName(item.getType())); + } + } + + private ArrayType filterArray(final ArrayType array) throws XPathException { + final int size = array.getSize(); + + // Build a context sequence of all member items for position()/last() + final ValueSequence contextSeq = new ValueSequence(size); + final List members = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + final Sequence member = array.get(i); + members.add(member); + // For context sequence, we need each member as an item. + // If a member is a sequence, wrap it — but for position/last to work + // we need exactly `size` items in the context sequence. + if (member.isEmpty()) { + // Empty sequence member: use empty sequence as placeholder + contextSeq.add(AtomicValue.EMPTY_VALUE); + } else if (member.getItemCount() == 1) { + contextSeq.add(member.itemAt(0)); + } else { + // Multi-item member: use first item as representative for context + contextSeq.add(member.itemAt(0)); + } + } + + final int savedPos = context.getContextPosition(); + final Sequence savedSeq = context.getContextSequence(); + try { + final ArrayType result = new ArrayType(context, new ArrayList<>()); + for (int i = 0; i < size; i++) { + final Sequence member = members.get(i); + context.setContextSequencePosition(i, contextSeq); + + final Sequence predResult = predicate.eval(member, null); + if (isSelected(predResult, i + 1)) { + result.add(member); + } + } + return result; + } finally { + context.setContextSequencePosition(savedPos, savedSeq); + } + } + + private AbstractMapType filterMap(final AbstractMapType map) throws XPathException { + final Sequence keys = map.keys(); + final int size = keys.getItemCount(); + + // Build entry maps and context sequence for position/last + final ValueSequence contextSeq = new ValueSequence(size); + final List keyList = new ArrayList<>(size); + final List entryMaps = new ArrayList<>(size); + + for (final SequenceIterator i = keys.iterate(); i.hasNext(); ) { + final AtomicValue key = (AtomicValue) i.nextItem(); + keyList.add(key); + final Sequence value = map.get(key); + + final MapType entryMap = new MapType(context, null); + entryMap.add(new StringValue(this, "key"), key.toSequence()); + entryMap.add(new StringValue(this, "value"), value); + entryMaps.add(entryMap); + contextSeq.add(entryMap); + } + + final int savedPos = context.getContextPosition(); + final Sequence savedSeq = context.getContextSequence(); + try { + final MapType result = new MapType(context, null); + for (int i = 0; i < size; i++) { + context.setContextSequencePosition(i, contextSeq); + final AbstractMapType entryMap = entryMaps.get(i); + + final Sequence predResult = predicate.eval(entryMap.toSequence(), null); + if (isSelected(predResult, i + 1)) { + result.add(keyList.get(i), map.get(keyList.get(i))); + } + } + return result; + } finally { + context.setContextSequencePosition(savedPos, savedSeq); + } + } + + /** + * Determines whether a member/entry at the given 1-based position is selected + * by the predicate result, following XQ4 array/map filter semantics: + * - If the result is a single numeric value, select if it equals the position. + * - If the result is a multi-item all-numeric sequence, select if any value + * equals the position (XQ4 extension for ?[] filters). + * - If the result is a multi-item sequence mixing numeric and non-numeric, + * raise FORG0006. + * - Otherwise, evaluate effective boolean value. + */ + private boolean isSelected(final Sequence predResult, final int position) throws XPathException { + if (predResult.isEmpty()) { + return false; + } + + // Single numeric value: positional predicate + if (predResult.hasOne() && Type.subTypeOfUnion(predResult.itemAt(0).getType(), Type.NUMERIC)) { + final double pos = ((NumericValue) predResult.itemAt(0)).getDouble(); + return pos == position; + } + + // Multi-item sequence starting with numeric: check all items are numeric + if (predResult.getItemCount() > 1 && + Type.subTypeOfUnion(predResult.itemAt(0).getType(), Type.NUMERIC)) { + for (final SequenceIterator i = predResult.iterate(); i.hasNext(); ) { + final Item item = i.nextItem(); + if (!Type.subTypeOfUnion(item.getType(), Type.NUMERIC)) { + throw new XPathException((Expression) null, ErrorCodes.FORG0006, + "Mixed numeric and non-numeric values in filter predicate"); + } + final double pos = ((NumericValue) item).getDouble(); + if (pos == position) { + return true; + } + } + return false; + } + + // Boolean predicate + return predResult.effectiveBooleanValue(); + } + + @Override + public int returnsType() { + return Type.ITEM; + } + + @Override + public Cardinality getCardinality() { + return Cardinality.EXACTLY_ONE; + } + + @Override + public void dump(final ExpressionDumper dumper) { + contextExpr.dump(dumper); + dumper.display("?["); + predicate.dump(dumper); + dumper.display("]"); + } + + @Override + public void resetState(final boolean postOptimization) { + super.resetState(postOptimization); + contextExpr.resetState(postOptimization); + predicate.resetState(postOptimization); + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/FocusFunction.java b/exist-core/src/main/java/org/exist/xquery/FocusFunction.java new file mode 100644 index 00000000000..28d930a3102 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/FocusFunction.java @@ -0,0 +1,140 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.dom.persistent.DocumentSet; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.*; + +import java.util.ArrayDeque; +import java.util.List; + +/** + * Implements XQuery 4.0 focus functions: {@code fn { expr }} and {@code function { expr }}. + * + *

A focus function is an inline function with an implicit single parameter + * of type {@code item()*}. When called, the argument is bound as the context + * item for the body expression.

+ * + *

Formally: {@code fn { EXPR }} is equivalent to + * {@code function($dot as item()*) as item()* { EXPR }} where EXPR is + * evaluated with the context value set to {@code $dot}.

+ */ +public class FocusFunction extends AbstractExpression { + + public static final String FOCUS_PARAM_NAME = ".focus"; + + private final UserDefinedFunction function; + private final ArrayDeque calls = new ArrayDeque<>(); + private AnalyzeContextInfo cachedContextInfo; + + public FocusFunction(final XQueryContext context, final UserDefinedFunction function) { + super(context); + this.function = function; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + cachedContextInfo = new AnalyzeContextInfo(contextInfo); + cachedContextInfo.addFlag(SINGLE_STEP_EXECUTION); + cachedContextInfo.setParent(this); + } + + @Override + public void dump(final ExpressionDumper dumper) { + dumper.display("fn "); + function.dump(dumper); + } + + @Override + public Sequence eval(final Sequence contextSequence, final Item contextItem) + throws XPathException { + final List closureVars = context.getLocalStack(); + + final FunctionCall call = new FocusFunctionCall(context, function); + call.getFunction().setClosureVariables(closureVars); + call.setLocation(function.getLine(), function.getColumn()); + call.analyze(new AnalyzeContextInfo(cachedContextInfo)); + + calls.push(call); + + return new FunctionReference(this, call); + } + + @Override + public int returnsType() { + return Type.FUNCTION; + } + + @Override + public void resetState(final boolean postOptimization) { + super.resetState(postOptimization); + calls.clear(); + function.resetState(postOptimization); + } + + /** + * A specialized FunctionCall that sets the argument as context item + * before evaluating the function body. + */ + public static class FocusFunctionCall extends FunctionCall { + + public FocusFunctionCall(final XQueryContext context, final UserDefinedFunction function) { + super(context, function); + } + + @Override + public Sequence evalFunction(final Sequence contextSequence, final Item contextItem, + final Sequence[] seq, final DocumentSet[] contextDocs) throws XPathException { + // The focus function's single argument becomes the context item + // for the body evaluation. + final Sequence focusArg = (seq != null && seq.length > 0) ? seq[0] : Sequence.EMPTY_SEQUENCE; + + context.stackEnter(this); + final LocalVariable mark = context.markLocalVariables(true); + if (getFunction().getClosureVariables() != null) { + context.restoreStack(getFunction().getClosureVariables()); + } + try { + // Bind the implicit parameter + final UserDefinedFunction func = getFunction(); + if (!func.getParameters().isEmpty()) { + final LocalVariable var = new LocalVariable( + func.getParameters().get(0)); + var.setValue(focusArg); + context.declareVariableBinding(var); + } + + // Evaluate the body with the argument as context + final Expression body = func.getFunctionBody(); + if (focusArg.getItemCount() == 1) { + return body.eval(focusArg, focusArg.itemAt(0)); + } else { + return body.eval(focusArg, null); + } + } finally { + context.popLocalVariables(mark); + context.stackLeave(this); + } + } + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java b/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java new file mode 100644 index 00000000000..e2956b36d3b --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java @@ -0,0 +1,308 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.dom.QName; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.functions.map.AbstractMapType; +import org.exist.xquery.value.*; + +import java.util.HashSet; +import java.util.Set; + +/** + * Implements the XQuery 4.0 "for key", "for value", and "for key/value" clauses. + * + *

{@code for key $k in map-expr} iterates over the keys of a map.

+ *

{@code for value $v in map-expr} iterates over the values of a map.

+ *

{@code for key $k value $v in map-expr} iterates over key-value pairs.

+ */ +public class ForKeyValueExpr extends BindingExpression { + + private final ClauseType clauseType; + private QName positionalVariable = null; + private QName valueVariable = null; + private SequenceType valueSequenceType = null; + + public ForKeyValueExpr(final XQueryContext context, final ClauseType clauseType) { + super(context); + this.clauseType = clauseType; + } + + public void setPositionalVariable(final QName variable) { + positionalVariable = variable; + } + + public void setValueVariable(final QName variable) { + valueVariable = variable; + } + + public void setValueSequenceType(final SequenceType type) { + valueSequenceType = type; + } + + @Override + public ClauseType getType() { + return clauseType; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + super.analyze(contextInfo); + final LocalVariable mark = context.markLocalVariables(false); + try { + contextInfo.setParent(this); + final AnalyzeContextInfo varContextInfo = new AnalyzeContextInfo(contextInfo); + inputSequence.analyze(varContextInfo); + final LocalVariable inVar = new LocalVariable(varName); + inVar.setSequenceType(sequenceType); + inVar.setStaticType(Type.ITEM); + context.declareVariableBinding(inVar); + if (valueVariable != null) { + final LocalVariable valVar = new LocalVariable(valueVariable); + valVar.setSequenceType(valueSequenceType); + valVar.setStaticType(Type.ITEM); + context.declareVariableBinding(valVar); + } + if (positionalVariable != null) { + final LocalVariable posVar = new LocalVariable(positionalVariable); + posVar.setSequenceType(POSITIONAL_VAR_TYPE); + posVar.setStaticType(Type.INTEGER); + context.declareVariableBinding(posVar); + } + + final AnalyzeContextInfo newContextInfo = new AnalyzeContextInfo(contextInfo); + newContextInfo.addFlag(SINGLE_STEP_EXECUTION); + returnExpr.analyze(newContextInfo); + } finally { + context.popLocalVariables(mark); + } + } + + @Override + public Sequence eval(Sequence contextSequence, final Item contextItem) + throws XPathException { + if (context.getProfiler().isEnabled()) { + context.getProfiler().start(this); + context.getProfiler().message(this, Profiler.DEPENDENCIES, + "DEPENDENCIES", Dependency.getDependenciesName(this.getDependencies())); + if (contextSequence != null) { + context.getProfiler().message(this, Profiler.START_SEQUENCES, + "CONTEXT SEQUENCE", contextSequence); + } + } + context.expressionStart(this); + + final LocalVariable mark = context.markLocalVariables(false); + final Sequence resultSequence = new ValueSequence(unordered); + try { + final Sequence in = inputSequence.eval(contextSequence, null); + + if (in.isEmpty()) { + // Empty map produces no iterations + } else if (in.getItemCount() != 1 || !(in.itemAt(0) instanceof AbstractMapType)) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "for " + clauseLabel() + + " expression requires a single map, got " + + Type.getTypeName(in.getItemType())); + } else { + final AbstractMapType map = (AbstractMapType) in.itemAt(0); + final LocalVariable var = createVariable(varName); + var.setSequenceType(sequenceType); + context.declareVariableBinding(var); + + LocalVariable valVar = null; + if (valueVariable != null) { + valVar = new LocalVariable(valueVariable); + valVar.setSequenceType(valueSequenceType); + context.declareVariableBinding(valVar); + } + + LocalVariable at = null; + if (positionalVariable != null) { + at = new LocalVariable(positionalVariable); + at.setSequenceType(POSITIONAL_VAR_TYPE); + context.declareVariableBinding(at); + } + + final Sequence keys = map.keys(); + int pos = 0; + try { + for (final SequenceIterator i = keys.iterate(); i.hasNext() && !WhileClause.isTerminated(); ) { + context.proceed(this); + final AtomicValue key = (AtomicValue) i.nextItem(); + pos++; + + final Sequence bindValue; + if (clauseType == ClauseType.FOR_VALUE) { + bindValue = map.get(key); + } else { + // FOR_KEY or FOR_KEY_VALUE: primary var is key + bindValue = key; + } + var.setValue(bindValue); + + if (valVar != null) { + valVar.setValue(map.get(key)); + } + + if (positionalVariable != null) { + at.setValue(new IntegerValue(this, pos)); + } + if (sequenceType != null) { + var.checkType(); + } + if (valVar != null && valueSequenceType != null) { + valVar.checkType(); + } + + final Sequence returnResult; + if (returnExpr instanceof OrderByClause) { + returnResult = returnExpr.eval(bindValue, null); + } else { + returnResult = returnExpr.eval(null, null); + } + resultSequence.addAll(returnResult); + var.destroy(context, resultSequence); + } + } catch (final WhileClause.WhileTerminationException e) { + // while clause signaled end of iteration + } + if (getPreviousClause() == null && WhileClause.isTerminated()) { + WhileClause.clearTerminated(); + } + } + } finally { + context.popLocalVariables(mark, resultSequence); + } + + if (callPostEval()) { + final Sequence postResult = postEval(resultSequence); + context.expressionEnd(this); + if (context.getProfiler().isEnabled()) { + context.getProfiler().end(this, "", postResult); + } + return postResult; + } + + context.expressionEnd(this); + if (context.getProfiler().isEnabled()) { + context.getProfiler().end(this, "", resultSequence); + } + return resultSequence; + } + + private String clauseLabel() { + switch (clauseType) { + case FOR_KEY: return "key"; + case FOR_VALUE: return "value"; + case FOR_KEY_VALUE: return "key/value"; + default: return "key"; + } + } + + private boolean callPostEval() { + FLWORClause prev = getPreviousClause(); + while (prev != null) { + switch (prev.getType()) { + case LET: + case FOR: + case FOR_MEMBER: + case FOR_KEY: + case FOR_VALUE: + case FOR_KEY_VALUE: + return false; + case ORDERBY: + case GROUPBY: + return true; + default: + break; + } + prev = prev.getPreviousClause(); + } + return true; + } + + @Override + public void dump(final ExpressionDumper dumper) { + dumper.display("for " + clauseLabel() + " ", line); + dumper.startIndent(); + dumper.display("$").display(varName); + if (valueVariable != null) { + dumper.display(" value $").display(valueVariable); + } + if (sequenceType != null) { + dumper.display(" as ").display(sequenceType); + } + dumper.display(" in "); + inputSequence.dump(dumper); + dumper.endIndent().nl(); + if (returnExpr instanceof LetExpr) { + dumper.display(" ", returnExpr.getLine()); + } else { + dumper.display("return", returnExpr.getLine()); + } + dumper.startIndent(); + returnExpr.dump(dumper); + dumper.endIndent().nl(); + } + + @Override + public String toString() { + final StringBuilder result = new StringBuilder(); + result.append("for ").append(clauseLabel()).append(" "); + result.append("$").append(varName); + if (valueVariable != null) { + result.append(" value $").append(valueVariable); + } + if (sequenceType != null) { + result.append(" as ").append(sequenceType); + } + result.append(" in "); + result.append(inputSequence.toString()); + result.append(" "); + if (returnExpr instanceof LetExpr) { + result.append(" "); + } else { + result.append("return "); + } + result.append(returnExpr.toString()); + return result.toString(); + } + + @Override + public Set getTupleStreamVariables() { + final Set variables = new HashSet<>(); + final QName variable = getVariable(); + if (variable != null) { + variables.add(variable); + } + if (valueVariable != null) { + variables.add(valueVariable); + } + final LocalVariable startVar = getStartVariable(); + if (startVar != null) { + variables.add(startVar.getQName()); + } + return variables; + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java b/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java new file mode 100644 index 00000000000..522cb213331 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java @@ -0,0 +1,239 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.dom.QName; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.functions.array.ArrayType; +import org.exist.xquery.value.*; + +import java.util.HashSet; +import java.util.Set; + +/** + * Implements the XQuery 4.0 "for member" clause in FLWOR expressions. + * + *

{@code for member $m in $array-expr} iterates over the members of an array, + * binding each member (which is a sequence) to the variable.

+ */ +public class ForMemberExpr extends BindingExpression { + + private QName positionalVariable = null; + + public ForMemberExpr(final XQueryContext context) { + super(context); + } + + public void setPositionalVariable(final QName variable) { + positionalVariable = variable; + } + + @Override + public ClauseType getType() { + return ClauseType.FOR_MEMBER; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + super.analyze(contextInfo); + final LocalVariable mark = context.markLocalVariables(false); + try { + contextInfo.setParent(this); + final AnalyzeContextInfo varContextInfo = new AnalyzeContextInfo(contextInfo); + inputSequence.analyze(varContextInfo); + final LocalVariable inVar = new LocalVariable(varName); + inVar.setSequenceType(sequenceType); + inVar.setStaticType(Type.ITEM); + context.declareVariableBinding(inVar); + if (positionalVariable != null) { + final LocalVariable posVar = new LocalVariable(positionalVariable); + posVar.setSequenceType(POSITIONAL_VAR_TYPE); + posVar.setStaticType(Type.INTEGER); + context.declareVariableBinding(posVar); + } + + final AnalyzeContextInfo newContextInfo = new AnalyzeContextInfo(contextInfo); + newContextInfo.addFlag(SINGLE_STEP_EXECUTION); + returnExpr.analyze(newContextInfo); + } finally { + context.popLocalVariables(mark); + } + } + + @Override + public Sequence eval(Sequence contextSequence, final Item contextItem) + throws XPathException { + if (context.getProfiler().isEnabled()) { + context.getProfiler().start(this); + context.getProfiler().message(this, Profiler.DEPENDENCIES, + "DEPENDENCIES", Dependency.getDependenciesName(this.getDependencies())); + if (contextSequence != null) { + context.getProfiler().message(this, Profiler.START_SEQUENCES, + "CONTEXT SEQUENCE", contextSequence); + } + } + context.expressionStart(this); + + final LocalVariable mark = context.markLocalVariables(false); + final Sequence resultSequence = new ValueSequence(unordered); + try { + final Sequence in = inputSequence.eval(contextSequence, null); + + if (!(in instanceof ArrayType)) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "for member expression requires an array, got " + + Type.getTypeName(in.getItemType())); + } + + final ArrayType array = (ArrayType) in; + final LocalVariable var = createVariable(varName); + var.setSequenceType(sequenceType); + context.declareVariableBinding(var); + + LocalVariable at = null; + if (positionalVariable != null) { + at = new LocalVariable(positionalVariable); + at.setSequenceType(POSITIONAL_VAR_TYPE); + context.declareVariableBinding(at); + } + + try { + for (int i = 0; i < array.getSize() && !WhileClause.isTerminated(); i++) { + context.proceed(this); + final Sequence member = array.get(i); + var.setValue(member); + if (positionalVariable != null) { + at.setValue(new IntegerValue(this, i + 1)); + } + if (sequenceType == null) { + var.checkType(); + } + + final Sequence returnResult; + if (returnExpr instanceof OrderByClause) { + returnResult = returnExpr.eval(member, null); + } else { + returnResult = returnExpr.eval(null, null); + } + resultSequence.addAll(returnResult); + var.destroy(context, resultSequence); + } + } catch (final WhileClause.WhileTerminationException e) { + // while clause signaled end of iteration + } + if (getPreviousClause() == null && WhileClause.isTerminated()) { + WhileClause.clearTerminated(); + } + } finally { + context.popLocalVariables(mark, resultSequence); + } + + if (callPostEval()) { + final Sequence postResult = postEval(resultSequence); + context.expressionEnd(this); + if (context.getProfiler().isEnabled()) { + context.getProfiler().end(this, "", postResult); + } + return postResult; + } + + context.expressionEnd(this); + if (context.getProfiler().isEnabled()) { + context.getProfiler().end(this, "", resultSequence); + } + return resultSequence; + } + + private boolean callPostEval() { + FLWORClause prev = getPreviousClause(); + while (prev != null) { + switch (prev.getType()) { + case LET: + case FOR: + case FOR_MEMBER: + return false; + case ORDERBY: + case GROUPBY: + return true; + default: + break; + } + prev = prev.getPreviousClause(); + } + return true; + } + + @Override + public void dump(final ExpressionDumper dumper) { + dumper.display("for member ", line); + dumper.startIndent(); + dumper.display("$").display(varName); + if (sequenceType != null) { + dumper.display(" as ").display(sequenceType); + } + dumper.display(" in "); + inputSequence.dump(dumper); + dumper.endIndent().nl(); + if (returnExpr instanceof LetExpr) { + dumper.display(" ", returnExpr.getLine()); + } else { + dumper.display("return", returnExpr.getLine()); + } + dumper.startIndent(); + returnExpr.dump(dumper); + dumper.endIndent().nl(); + } + + @Override + public String toString() { + final StringBuilder result = new StringBuilder(); + result.append("for member "); + result.append("$").append(varName); + if (sequenceType != null) { + result.append(" as ").append(sequenceType); + } + result.append(" in "); + result.append(inputSequence.toString()); + result.append(" "); + if (returnExpr instanceof LetExpr) { + result.append(" "); + } else { + result.append("return "); + } + result.append(returnExpr.toString()); + return result.toString(); + } + + @Override + public Set getTupleStreamVariables() { + final Set variables = new HashSet<>(); + final QName variable = getVariable(); + if (variable != null) { + variables.add(variable); + } + final LocalVariable startVar = getStartVariable(); + if (startVar != null) { + variables.add(startVar.getQName()); + } + return variables; + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/KeywordArgumentExpression.java b/exist-core/src/main/java/org/exist/xquery/KeywordArgumentExpression.java new file mode 100644 index 00000000000..6bd237072a9 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/KeywordArgumentExpression.java @@ -0,0 +1,85 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.Item; +import org.exist.xquery.value.Sequence; +import org.exist.xquery.value.Type; + +/** + * Wraps a function argument expression with a keyword name for XQuery 4.0 + * keyword argument syntax: {@code fn:slice($input, start := 3)}. + * + *

This is a transient wrapper used during function call construction. + * The keyword name is used to match the argument to the correct parameter + * position in the function signature.

+ */ +public class KeywordArgumentExpression extends AbstractExpression { + + private final String keywordName; + private final Expression argument; + + public KeywordArgumentExpression(final XQueryContext context, final String keywordName, + final Expression argument) { + super(context); + this.keywordName = keywordName; + this.argument = argument; + } + + public String getKeywordName() { + return keywordName; + } + + public Expression getArgument() { + return argument; + } + + @Override + public Sequence eval(final Sequence contextSequence, final Item contextItem) + throws XPathException { + return argument.eval(contextSequence, contextItem); + } + + @Override + public int returnsType() { + return argument.returnsType(); + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + argument.analyze(contextInfo); + } + + @Override + public void dump(final ExpressionDumper dumper) { + dumper.display(keywordName); + dumper.display(" := "); + argument.dump(dumper); + } + + @Override + public void resetState(final boolean postOptimization) { + super.resetState(postOptimization); + argument.resetState(postOptimization); + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/LetDestructureExpr.java b/exist-core/src/main/java/org/exist/xquery/LetDestructureExpr.java new file mode 100644 index 00000000000..9aedcb6f144 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/LetDestructureExpr.java @@ -0,0 +1,335 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.dom.QName; +import org.exist.xquery.functions.array.ArrayType; +import org.exist.xquery.functions.map.AbstractMapType; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.*; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Implements XQuery 4.0 let destructuring: + *
    + *
  • {@code let $($x, $y) := (1, 2)} — sequence destructuring
  • + *
  • {@code let $[$x, $y] := [1, 2]} — array destructuring
  • + *
  • {@code let ${$x, $y} := map{'x':1,'y':2}} — map destructuring
  • + *
+ */ +public class LetDestructureExpr extends AbstractFLWORClause { + + public enum DestructureMode { + SEQUENCE, ARRAY, MAP + } + + private final DestructureMode mode; + private final List varNames; + private final List varTypes; + private Expression inputSequence; + + public LetDestructureExpr(final XQueryContext context, final DestructureMode mode) { + super(context); + this.mode = mode; + this.varNames = new ArrayList<>(); + this.varTypes = new ArrayList<>(); + } + + public void addVariable(final QName name, final SequenceType type) { + varNames.add(name); + varTypes.add(type); + } + + public void setInputSequence(final Expression seq) { + this.inputSequence = seq.simplify(); + } + + public void setOverallType(final SequenceType type) { + // Reserved for future type checking of overall destructure type + } + + @Override + public ClauseType getType() { + switch (mode) { + case SEQUENCE: return ClauseType.LET_SEQ_DESTRUCTURE; + case ARRAY: return ClauseType.LET_ARRAY_DESTRUCTURE; + case MAP: return ClauseType.LET_MAP_DESTRUCTURE; + default: return ClauseType.LET; + } + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + final LocalVariable mark = context.markLocalVariables(false); + try { + contextInfo.setParent(this); + final AnalyzeContextInfo varContextInfo = new AnalyzeContextInfo(contextInfo); + inputSequence.analyze(varContextInfo); + + for (int i = 0; i < varNames.size(); i++) { + final LocalVariable var = new LocalVariable(varNames.get(i)); + if (varTypes.get(i) != null) { + var.setSequenceType(varTypes.get(i)); + } + context.declareVariableBinding(var); + } + + context.setContextSequencePosition(0, null); + returnExpr.analyze(contextInfo); + } finally { + context.popLocalVariables(mark); + } + } + + @Override + public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { + context.expressionStart(this); + context.pushDocumentContext(); + try { + final LocalVariable mark = context.markLocalVariables(false); + Sequence resultSequence = null; + try { + final Sequence input = inputSequence.eval(contextSequence, null); + + switch (mode) { + case SEQUENCE: + bindSequenceVars(input); + break; + case ARRAY: + bindArrayVars(input); + break; + case MAP: + bindMapVars(input); + break; + default: + throw new XPathException(this, ErrorCodes.ERROR, "Unknown destructure mode: " + mode); + } + + resultSequence = returnExpr.eval(contextSequence, null); + } finally { + context.popLocalVariables(mark, resultSequence); + } + if (resultSequence == null) { + return Sequence.EMPTY_SEQUENCE; + } + if (getPreviousClause() == null) { + resultSequence = postEval(resultSequence); + } + return resultSequence; + } finally { + context.popDocumentContext(); + context.expressionEnd(this); + } + } + + private void bindSequenceVars(final Sequence input) throws XPathException { + for (int i = 0; i < varNames.size(); i++) { + final LocalVariable var = createVariable(varNames.get(i)); + final SequenceType type = varTypes.get(i); + if (type != null) { + var.setSequenceType(type); + } + context.declareVariableBinding(var); + + if (i < input.getItemCount()) { + var.setValue(input.itemAt(i).toSequence()); + } else { + var.setValue(Sequence.EMPTY_SEQUENCE); + } + if (type != null) { + checkVarType(var, type); + } + } + } + + private void bindArrayVars(final Sequence input) throws XPathException { + if (input.isEmpty()) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Array destructuring requires an array, got empty sequence"); + } + final Item item = input.itemAt(0); + if (!Type.subTypeOf(item.getType(), Type.ARRAY_ITEM)) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Array destructuring requires an array, got " + + Type.getTypeName(item.getType())); + } + final ArrayType array = (ArrayType) item; + for (int i = 0; i < varNames.size(); i++) { + final LocalVariable var = createVariable(varNames.get(i)); + final SequenceType type = varTypes.get(i); + if (type != null) { + var.setSequenceType(type); + } + context.declareVariableBinding(var); + + if (i < array.getSize()) { + var.setValue(array.get(i)); + } else { + var.setValue(Sequence.EMPTY_SEQUENCE); + } + if (type != null) { + checkVarType(var, type); + } + } + } + + private void bindMapVars(final Sequence input) throws XPathException { + if (input.isEmpty()) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Map destructuring requires a map, got empty sequence"); + } + final Item item = input.itemAt(0); + if (!Type.subTypeOf(item.getType(), Type.MAP_ITEM)) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Map destructuring requires a map, got " + + Type.getTypeName(item.getType())); + } + final AbstractMapType map = (AbstractMapType) item; + for (int i = 0; i < varNames.size(); i++) { + final QName qn = varNames.get(i); + final LocalVariable var = createVariable(qn); + final SequenceType type = varTypes.get(i); + if (type != null) { + var.setSequenceType(type); + } + context.declareVariableBinding(var); + + final Sequence value = map.get(new StringValue(this, qn.getLocalPart())); + if (value != null && !value.isEmpty()) { + var.setValue(value); + } else { + var.setValue(Sequence.EMPTY_SEQUENCE); + } + if (type != null) { + checkVarType(var, type); + } + } + } + + private void checkVarType(final LocalVariable var, final SequenceType type) throws XPathException { + final Sequence val = var.getValue(); + if (val == null) { + return; + } + final Cardinality actualCard; + if (val.isEmpty()) { + actualCard = Cardinality.EMPTY_SEQUENCE; + } else if (val.hasMany()) { + actualCard = Cardinality._MANY; + } else { + actualCard = Cardinality.EXACTLY_ONE; + } + if (!type.getCardinality().isSuperCardinalityOrEqualOf(actualCard)) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Invalid cardinality for variable $" + var.getQName() + + ". Expected " + type.getCardinality().getHumanDescription() + + ", got " + actualCard.getHumanDescription(), val); + } + if (!Type.subTypeOf(type.getPrimaryType(), Type.NODE) && + !val.isEmpty() && + !Type.subTypeOf(val.getItemType(), type.getPrimaryType())) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Invalid type for variable $" + var.getQName() + + ". Expected " + Type.getTypeName(type.getPrimaryType()) + + ", got " + Type.getTypeName(val.getItemType()), val); + } + } + + @Override + public void dump(final ExpressionDumper dumper) { + dumper.display("let "); + switch (mode) { + case SEQUENCE: dumper.display("$("); break; + case ARRAY: dumper.display("$["); break; + case MAP: dumper.display("${"); break; + default: break; + } + for (int i = 0; i < varNames.size(); i++) { + if (i > 0) dumper.display(", "); + dumper.display("$").display(varNames.get(i).getLocalPart()); + } + switch (mode) { + case SEQUENCE: dumper.display(")"); break; + case ARRAY: dumper.display("]"); break; + case MAP: dumper.display("}"); break; + default: break; + } + dumper.display(" := "); + inputSequence.dump(dumper); + dumper.nl().display("return "); + returnExpr.dump(dumper); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("let "); + switch (mode) { + case SEQUENCE: sb.append("$("); break; + case ARRAY: sb.append("$["); break; + case MAP: sb.append("${"); break; + default: break; + } + for (int i = 0; i < varNames.size(); i++) { + if (i > 0) sb.append(", "); + sb.append("$").append(varNames.get(i).getLocalPart()); + } + switch (mode) { + case SEQUENCE: sb.append(")"); break; + case ARRAY: sb.append("]"); break; + case MAP: sb.append("}"); break; + default: break; + } + sb.append(" := ").append(inputSequence.toString()); + sb.append(" return ").append(returnExpr.toString()); + return sb.toString(); + } + + @Override + public void accept(final ExpressionVisitor visitor) { + // No specific visitor method for destructure - use default + } + + @Override + public boolean allowMixedNodesInReturn() { + return true; + } + + @Override + public Set getTupleStreamVariables() { + return new HashSet<>(varNames); + } + + @Override + public void resetState(final boolean postOptimization) { + super.resetState(postOptimization); + inputSequence.resetState(postOptimization); + } + + @Override + public int getDependencies() { + return Dependency.CONTEXT_SET; + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/MappingArrowOperator.java b/exist-core/src/main/java/org/exist/xquery/MappingArrowOperator.java new file mode 100644 index 00000000000..1e738abc9c4 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/MappingArrowOperator.java @@ -0,0 +1,207 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.dom.QName; +import org.exist.dom.QName.IllegalQNameException; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.FunctionReference; +import org.exist.xquery.value.Item; +import org.exist.xquery.value.Sequence; +import org.exist.xquery.value.Type; +import org.exist.xquery.value.ValueSequence; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implements the XQuery 4.0 mapping arrow operator (=!>). + * + * Unlike the fat arrow (=>), which passes the entire left-hand sequence + * as the first argument, the mapping arrow iterates over each item in + * the sequence and passes each one individually, concatenating the results. + * + * {@code (1, 2, 3) =!> string()} is equivalent to {@code (1, 2, 3) ! string(.)}. + */ +public class MappingArrowOperator extends AbstractExpression { + + private QName qname = null; + private Expression leftExpr; + private FunctionCall fcall = null; + private Expression funcSpec = null; + private List parameters; + private AnalyzeContextInfo cachedContextInfo; + + public MappingArrowOperator(final XQueryContext context, final Expression leftExpr) throws XPathException { + super(context); + this.leftExpr = leftExpr; + } + + public void setArrowFunction(final String fname, final List params) throws XPathException { + try { + this.qname = QName.parse(context, fname, context.getDefaultFunctionNamespace()); + this.parameters = params; + } catch (final IllegalQNameException e) { + throw new XPathException(this, ErrorCodes.XPST0081, "No namespace defined for prefix " + fname); + } + } + + public void setArrowFunction(final PathExpr funcSpec, final List params) { + this.funcSpec = funcSpec.simplify(); + this.parameters = params; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + if (qname != null) { + fcall = NamedFunctionReference.lookupFunction(this, context, qname, parameters.size() + 1); + } + this.cachedContextInfo = contextInfo; + leftExpr.analyze(contextInfo); + if (fcall != null) { + fcall.analyze(contextInfo); + } + if (funcSpec != null) { + funcSpec.analyze(contextInfo); + } + } + + @Override + public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { + if (contextItem != null) { + contextSequence = contextItem.toSequence(); + } + final Sequence inputSeq = leftExpr.eval(contextSequence, null); + + if (inputSeq.isEmpty()) { + return Sequence.EMPTY_SEQUENCE; + } + + final ValueSequence result = new ValueSequence(); + for (int i = 0; i < inputSeq.getItemCount(); i++) { + final Item item = inputSeq.itemAt(i); + final Sequence itemSeq = item.toSequence(); + + final FunctionReference fref; + if (fcall != null) { + fref = new FunctionReference(this, fcall); + } else { + final Sequence funcSeq = funcSpec.eval(itemSeq, null); + if (funcSeq.getCardinality() != Cardinality.EXACTLY_ONE) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Expected exactly one item for the function to be called, got " + funcSeq.getItemCount() + + ". Expression: " + ExpressionDumper.dump(funcSpec)); + } + final Item item0 = funcSeq.itemAt(0); + if (!Type.subTypeOf(item0.getType(), Type.FUNCTION)) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Type error: expected function, got " + Type.getTypeName(item0.getType())); + } + fref = (FunctionReference) item0; + } + try { + final List fparams = new ArrayList<>(parameters.size() + 1); + fparams.add(new ContextParam(context, itemSeq)); + fparams.addAll(parameters); + + fref.setArguments(fparams); + fref.analyze(new AnalyzeContextInfo(cachedContextInfo)); + result.addAll(fref.eval(null)); + } finally { + fref.close(); + } + } + return result; + } + + @Override + public int returnsType() { + return fcall == null ? Type.ITEM : fcall.returnsType(); + } + + @Override + public Cardinality getCardinality() { + return Cardinality.ZERO_OR_MORE; + } + + @Override + public void dump(final ExpressionDumper dumper) { + leftExpr.dump(dumper); + dumper.display(" =!> "); + if (fcall != null) { + dumper.display(fcall.getFunction().getName()).display('('); + } else { + funcSpec.dump(dumper); + } + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) { + dumper.display(", "); + parameters.get(i).dump(dumper); + } + } + dumper.display(')'); + } + + @Override + public void resetState(boolean postOptimization) { + super.resetState(postOptimization); + leftExpr.resetState(postOptimization); + if (fcall != null) { + fcall.resetState(postOptimization); + } + if (funcSpec != null) { + funcSpec.resetState(postOptimization); + } + for (Expression param : parameters) { + param.resetState(postOptimization); + } + } + + private class ContextParam extends Function.Placeholder { + private final Sequence sequence; + + ContextParam(XQueryContext context, Sequence sequence) { + super(context); + this.sequence = sequence; + } + + @Override + public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { + // no-op: context param is pre-evaluated + } + + @Override + public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException { + return sequence; + } + + @Override + public int returnsType() { + return sequence.getItemType(); + } + + @Override + public void dump(ExpressionDumper dumper) { + // no-op: context param has no source representation + } + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/MethodCallOperator.java b/exist-core/src/main/java/org/exist/xquery/MethodCallOperator.java new file mode 100644 index 00000000000..b302c8bdac3 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/MethodCallOperator.java @@ -0,0 +1,211 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.xquery.functions.map.AbstractMapType; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.*; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implements the XQuery 4.0 method call operator (=?>). + * + * {@code $map =?> method(args)} looks up the key "method" in the map, + * retrieves the function stored there, and calls it with the map as + * the first argument followed by any additional arguments. + * + * For each item in the left-hand sequence: + *
    + *
  1. The item must be a map (XPTY0004 otherwise)
  2. + *
  3. The method name is looked up as a key in the map
  4. + *
  5. The value must be exactly one function (XPTY0004 otherwise)
  6. + *
  7. The function is called with the map as first argument + additional args
  8. + *
+ * + * Like the mapping arrow (=!>), it processes each item individually + * and concatenates results. + */ +public class MethodCallOperator extends AbstractExpression { + + private Expression leftExpr; + private String methodName; + private List parameters; + private AnalyzeContextInfo cachedContextInfo; + + public MethodCallOperator(final XQueryContext context, final Expression leftExpr) throws XPathException { + super(context); + this.leftExpr = leftExpr; + } + + public void setMethod(final String methodName, final List params) { + this.methodName = methodName; + this.parameters = params; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + this.cachedContextInfo = contextInfo; + leftExpr.analyze(contextInfo); + if (parameters != null) { + for (final Expression param : parameters) { + param.analyze(contextInfo); + } + } + } + + @Override + public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { + if (contextItem != null) { + contextSequence = contextItem.toSequence(); + } + final Sequence inputSeq = leftExpr.eval(contextSequence, null); + + if (inputSeq.isEmpty()) { + return Sequence.EMPTY_SEQUENCE; + } + + final ValueSequence result = new ValueSequence(); + for (int i = 0; i < inputSeq.getItemCount(); i++) { + final Item item = inputSeq.itemAt(i); + + // The item must be a map + if (!Type.subTypeOf(item.getType(), Type.MAP_ITEM)) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Method call operator (=?>) requires a map, got " + + Type.getTypeName(item.getType())); + } + + final AbstractMapType map = (AbstractMapType) item; + + // Look up the method name as a key in the map + final Sequence methodValue = map.get(new StringValue(this, methodName)); + if (methodValue == null || methodValue.isEmpty()) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Method '" + methodName + "' not found in map"); + } + + if (methodValue.getItemCount() != 1) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Method '" + methodName + "' must be a single function, got " + + methodValue.getItemCount() + " items"); + } + + final Item methodItem = methodValue.itemAt(0); + if (!Type.subTypeOf(methodItem.getType(), Type.FUNCTION)) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Method '" + methodName + "' is not a function, got " + + Type.getTypeName(methodItem.getType())); + } + + final FunctionReference fref = (FunctionReference) methodItem; + + // Check arity: function must accept at least 1 argument (the map itself) + final int expectedArity = (parameters != null ? parameters.size() : 0) + 1; + if (fref.getSignature().getArgumentCount() == 0) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Method '" + methodName + "' has arity 0 and cannot accept the map as first argument"); + } + + try { + final List fparams = new ArrayList<>(expectedArity); + fparams.add(new ContextParam(context, item.toSequence())); + if (parameters != null) { + fparams.addAll(parameters); + } + + fref.setArguments(fparams); + fref.analyze(new AnalyzeContextInfo(cachedContextInfo)); + result.addAll(fref.eval(null)); + } finally { + fref.close(); + } + } + return result; + } + + @Override + public int returnsType() { + return Type.ITEM; + } + + @Override + public Cardinality getCardinality() { + return Cardinality.ZERO_OR_MORE; + } + + @Override + public void dump(final ExpressionDumper dumper) { + leftExpr.dump(dumper); + dumper.display(" =?> ").display(methodName).display('('); + if (parameters != null) { + for (int i = 0; i < parameters.size(); i++) { + if (i > 0) { + dumper.display(", "); + } + parameters.get(i).dump(dumper); + } + } + dumper.display(')'); + } + + @Override + public void resetState(boolean postOptimization) { + super.resetState(postOptimization); + leftExpr.resetState(postOptimization); + if (parameters != null) { + for (Expression param : parameters) { + param.resetState(postOptimization); + } + } + } + + private class ContextParam extends Function.Placeholder { + private final Sequence sequence; + + ContextParam(XQueryContext context, Sequence sequence) { + super(context); + this.sequence = sequence; + } + + @Override + public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { + // no-op: context param is pre-evaluated + } + + @Override + public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException { + return sequence; + } + + @Override + public int returnsType() { + return sequence.getItemType(); + } + + @Override + public void dump(ExpressionDumper dumper) { + // no-op: context param has no source representation + } + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/OtherwiseExpression.java b/exist-core/src/main/java/org/exist/xquery/OtherwiseExpression.java new file mode 100644 index 00000000000..760ab147c54 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/OtherwiseExpression.java @@ -0,0 +1,90 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.Sequence; +import org.exist.xquery.value.Item; + +/** + * Implements the XQuery 4.0 "otherwise" operator. + * + * {@code E1 otherwise E2} returns E1 if it is non-empty, otherwise E2. + */ +public class OtherwiseExpression extends AbstractExpression { + + private Expression left; + private Expression right; + + public OtherwiseExpression(final XQueryContext context, final Expression left, final Expression right) { + super(context); + this.left = left; + this.right = right; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + left.analyze(new AnalyzeContextInfo(contextInfo)); + right.analyze(new AnalyzeContextInfo(contextInfo)); + } + + @Override + public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { + if (contextItem != null) { + contextSequence = contextItem.toSequence(); + } + final Sequence leftResult = left.eval(contextSequence, null); + if (leftResult != null && !leftResult.isEmpty()) { + return leftResult; + } + return right.eval(contextSequence, null); + } + + @Override + public int returnsType() { + return left.returnsType(); + } + + @Override + public Cardinality getCardinality() { + return Cardinality.ZERO_OR_MORE; + } + + @Override + public void dump(final ExpressionDumper dumper) { + left.dump(dumper); + dumper.display(" otherwise "); + right.dump(dumper); + } + + @Override + public String toString() { + return left.toString() + " otherwise " + right.toString(); + } + + @Override + public void resetState(final boolean postOptimization) { + super.resetState(postOptimization); + left.resetState(postOptimization); + right.resetState(postOptimization); + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/PipelineExpression.java b/exist-core/src/main/java/org/exist/xquery/PipelineExpression.java new file mode 100644 index 00000000000..5c746c1127f --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/PipelineExpression.java @@ -0,0 +1,106 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.Item; +import org.exist.xquery.value.Sequence; +import org.exist.xquery.value.ValueSequence; + +/** + * Implements the XQuery 4.0 pipeline operator "->". + * + * The expression {@code E1 -> E2} evaluates E1, then evaluates E2 with the + * result of E1 as the context value, position 1, and last 1. + */ +public class PipelineExpression extends AbstractExpression { + + private Expression left; + private Expression right; + + public PipelineExpression(final XQueryContext context, final Expression left, final Expression right) { + super(context); + this.left = left; + this.right = right; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + left.analyze(new AnalyzeContextInfo(contextInfo)); + right.analyze(new AnalyzeContextInfo(contextInfo)); + } + + @Override + public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { + if (contextItem != null) { + contextSequence = contextItem.toSequence(); + } + final Sequence leftResult = left.eval(contextSequence, null); + + // Pipeline: set context position=0 (position()=1) and a single-item + // context sequence so last()=1, per XQ4 spec. + final Sequence singletonContext; + if (leftResult.isEmpty()) { + singletonContext = Sequence.EMPTY_SEQUENCE; + } else { + singletonContext = new ValueSequence(1); + singletonContext.add(leftResult.itemAt(0)); + } + final int savedPos = context.getContextPosition(); + final Sequence savedSeq = context.getContextSequence(); + context.setContextSequencePosition(0, singletonContext); + try { + return right.eval(leftResult, null); + } finally { + context.setContextSequencePosition(savedPos, savedSeq); + } + } + + @Override + public int returnsType() { + return right.returnsType(); + } + + @Override + public Cardinality getCardinality() { + return Cardinality.ZERO_OR_MORE; + } + + @Override + public void dump(final ExpressionDumper dumper) { + left.dump(dumper); + dumper.display(" -> "); + right.dump(dumper); + } + + @Override + public String toString() { + return left.toString() + " -> " + right.toString(); + } + + @Override + public void resetState(final boolean postOptimization) { + super.resetState(postOptimization); + left.resetState(postOptimization); + right.resetState(postOptimization); + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/WhileClause.java b/exist-core/src/main/java/org/exist/xquery/WhileClause.java new file mode 100644 index 00000000000..e2c9d7a41df --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/WhileClause.java @@ -0,0 +1,136 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.dom.QName; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.Item; +import org.exist.xquery.value.Sequence; + +import java.util.HashSet; +import java.util.Set; + +/** + * Implements the XQuery 4.0 while clause in FLWOR expressions. + * + *

The while clause evaluates a condition for each tuple in the stream. + * If the condition is true, the tuple is retained; if false, the tuple + * and all subsequent tuples are discarded (iteration stops).

+ */ +public class WhileClause extends AbstractFLWORClause { + + /** + * Thread-local flag that signals all enclosing binding expressions + * in the same FLWOR to stop iteration after the current item. + */ + private static final ThreadLocal terminated = ThreadLocal.withInitial(() -> false); + + private final Expression whileExpr; + + /** + * Lightweight control-flow exception used to signal the immediately + * enclosing for/let binding expression to stop iteration. + */ + public static class WhileTerminationException extends XPathException { + public WhileTerminationException() { + super((Expression) null, "while clause terminated"); + } + } + + public static boolean isTerminated() { + return terminated.get(); + } + + public static void clearTerminated() { + terminated.set(false); + } + + public WhileClause(final XQueryContext context, final Expression whileExpr) { + super(context); + this.whileExpr = whileExpr; + } + + @Override + public ClauseType getType() { + return ClauseType.WHILE; + } + + public Expression getWhileExpr() { + return whileExpr; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + contextInfo.setParent(this); + final AnalyzeContextInfo newContextInfo = new AnalyzeContextInfo(contextInfo); + newContextInfo.setFlags(contextInfo.getFlags() | IN_PREDICATE | IN_WHERE_CLAUSE); + newContextInfo.setContextId(getExpressionId()); + whileExpr.analyze(newContextInfo); + + final AnalyzeContextInfo returnContextInfo = new AnalyzeContextInfo(contextInfo); + returnContextInfo.addFlag(SINGLE_STEP_EXECUTION); + returnExpr.analyze(returnContextInfo); + } + + @Override + public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException { + final Sequence condResult = whileExpr.eval(null, null); + if (condResult.effectiveBooleanValue()) { + return returnExpr.eval(null, null); + } + terminated.set(true); + throw new WhileTerminationException(); + } + + @Override + public Sequence postEval(final Sequence seq) throws XPathException { + if (returnExpr instanceof FLWORClause flworClause) { + return flworClause.postEval(seq); + } + return super.postEval(seq); + } + + @Override + public void dump(final ExpressionDumper dumper) { + dumper.display("while", whileExpr.getLine()); + dumper.startIndent(); + whileExpr.dump(dumper); + dumper.endIndent().nl(); + } + + @Override + public void resetState(final boolean postOptimization) { + super.resetState(postOptimization); + whileExpr.resetState(postOptimization); + returnExpr.resetState(postOptimization); + } + + @Override + public Set getTupleStreamVariables() { + final Set vars = new HashSet<>(); + final LocalVariable startVar = getStartVariable(); + if (startVar != null) { + vars.add(startVar.getQName()); + } + return vars; + } +} From 4202cef2c4da047fc0d9a320c9cb4d5920778cf8 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Sat, 4 Apr 2026 09:52:27 -0400 Subject: [PATCH 03/20] [feature] Add XQuery 4.0 type infrastructure and expression modifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the existing type system and expression classes for XQ4: Type system: - Type.CHOICE: union/choice type constant - Type.ENUM: enumeration type constant - SequenceType: XQ4 record type support, named function parameters - FunctionParameterSequenceType: default value support for params - Constants: XQ4 axis constants (ancestor-or-self::*, etc.) Expression modifications: - Function: support default parameter values (XQ4 §4.15) - FunctionFactory: keyword argument dispatch - UserDefinedFunction: default parameter evaluation - ForExpr/LetExpr: for-member and while clause integration - FLWORClause: while clause chaining - TryCatchExpression: finally clause support - SwitchExpression: XQ4 fall-through semantics - StringConstructor: XQ4 string template evaluation - XQueryContext: XQ4 version detection, xquery version "4.0" - LocationStep: combined axis support Spec: QT4 XQuery 4.0 §2.5 (Types), §4 (Modules) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../main/java/org/exist/xquery/Constants.java | 12 +- .../java/org/exist/xquery/FLWORClause.java | 3 +- .../main/java/org/exist/xquery/ForExpr.java | 20 +- .../main/java/org/exist/xquery/Function.java | 27 +- .../org/exist/xquery/FunctionFactory.java | 210 ++++++++++- .../main/java/org/exist/xquery/LetExpr.java | 9 +- .../java/org/exist/xquery/LocationStep.java | 97 +++++ .../exist/xquery/StaticXQueryException.java | 25 +- .../org/exist/xquery/StringConstructor.java | 6 +- .../org/exist/xquery/SwitchExpression.java | 79 +++-- .../org/exist/xquery/TryCatchExpression.java | 214 ++++++----- .../org/exist/xquery/UserDefinedFunction.java | 48 ++- .../java/org/exist/xquery/XQueryContext.java | 35 +- .../value/FunctionParameterSequenceType.java | 14 + .../org/exist/xquery/value/SequenceType.java | 335 +++++++++++++++--- .../java/org/exist/xquery/value/Type.java | 13 +- 16 files changed, 945 insertions(+), 202 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/Constants.java b/exist-core/src/main/java/org/exist/xquery/Constants.java index 7a5069d7416..62f16a2d304 100644 --- a/exist-core/src/main/java/org/exist/xquery/Constants.java +++ b/exist-core/src/main/java/org/exist/xquery/Constants.java @@ -46,7 +46,11 @@ public interface Constants { "following-sibling", "namespace", "self", - "attribute-descendant" + "attribute-descendant", + "following-or-self", + "preceding-or-self", + "following-sibling-or-self", + "preceding-sibling-or-self" }; /** @@ -73,6 +77,12 @@ public interface Constants { //combines /descendant-or-self::node()/attribute:* int DESCENDANT_ATTRIBUTE_AXIS = 13; + /** XQuery 4.0 axes */ + int FOLLOWING_OR_SELF_AXIS = 14; + int PRECEDING_OR_SELF_AXIS = 15; + int FOLLOWING_SIBLING_OR_SELF_AXIS = 16; + int PRECEDING_SIBLING_OR_SELF_AXIS = 17; + /** * Node types */ diff --git a/exist-core/src/main/java/org/exist/xquery/FLWORClause.java b/exist-core/src/main/java/org/exist/xquery/FLWORClause.java index d56ed4777d2..ea632d51e17 100644 --- a/exist-core/src/main/java/org/exist/xquery/FLWORClause.java +++ b/exist-core/src/main/java/org/exist/xquery/FLWORClause.java @@ -34,7 +34,8 @@ public interface FLWORClause extends Expression { enum ClauseType { - FOR, LET, GROUPBY, ORDERBY, WHERE, SOME, EVERY, COUNT, WINDOW + FOR, LET, GROUPBY, ORDERBY, WHERE, WHILE, SOME, EVERY, COUNT, WINDOW, FOR_MEMBER, FOR_KEY, FOR_VALUE, FOR_KEY_VALUE, + LET_SEQ_DESTRUCTURE, LET_ARRAY_DESTRUCTURE, LET_MAP_DESTRUCTURE } /** diff --git a/exist-core/src/main/java/org/exist/xquery/ForExpr.java b/exist-core/src/main/java/org/exist/xquery/ForExpr.java index 1a5eab2f4dd..577784de185 100644 --- a/exist-core/src/main/java/org/exist/xquery/ForExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/ForExpr.java @@ -176,15 +176,23 @@ public Sequence eval(Sequence contextSequence, Item contextItem) // Loop through each variable binding int p = 0; - if (in.isEmpty() && allowEmpty) { - processItem(var, AtomicValue.EMPTY_VALUE, Sequence.EMPTY_SEQUENCE, resultSequence, at, p); - } else { - for (final SequenceIterator i = in.iterate(); i.hasNext(); p++) { - processItem(var, i.nextItem(), in, resultSequence, at, p); + try { + if (in.isEmpty() && allowEmpty) { + processItem(var, AtomicValue.EMPTY_VALUE, Sequence.EMPTY_SEQUENCE, resultSequence, at, p); + } else { + for (final SequenceIterator i = in.iterate(); i.hasNext() && !WhileClause.isTerminated(); p++) { + processItem(var, i.nextItem(), in, resultSequence, at, p); + } } + } catch (final WhileClause.WhileTerminationException e) { + // while clause signaled end of iteration for this for loop + } + // clear terminated flag if this is the outermost for + if (isOuterFor && WhileClause.isTerminated()) { + WhileClause.clearTerminated(); } } finally { - // restore the local variable stack + // restore the local variable stack context.popLocalVariables(mark, resultSequence); } diff --git a/exist-core/src/main/java/org/exist/xquery/Function.java b/exist-core/src/main/java/org/exist/xquery/Function.java index 161cba2957b..a22837100ab 100644 --- a/exist-core/src/main/java/org/exist/xquery/Function.java +++ b/exist-core/src/main/java/org/exist/xquery/Function.java @@ -212,10 +212,29 @@ public void setParent(final Expression parent) { * @throws XPathException if an error occurs setting the arguments */ public void setArguments(final List arguments) throws XPathException { - if ((!mySignature.isVariadic()) && arguments.size() != mySignature.getArgumentCount()) { - throw new XPathException(this, ErrorCodes.XPST0017, - "Number of arguments of function " + getName() + " doesn't match function signature (expected " - + mySignature.getArgumentCount() + ", got " + arguments.size() + ')'); + final int argCount = mySignature.getArgumentCount(); + if ((!mySignature.isVariadic()) && arguments.size() != argCount) { + // XQ4: Allow fewer arguments if trailing params have default values + if (arguments.size() < argCount) { + boolean hasDefaults = true; + final SequenceType[] argTypes = mySignature.getArgumentTypes(); + for (int i = arguments.size(); i < argCount; i++) { + if (!(argTypes[i] instanceof FunctionParameterSequenceType) || + !((FunctionParameterSequenceType) argTypes[i]).hasDefaultValue()) { + hasDefaults = false; + break; + } + } + if (!hasDefaults) { + throw new XPathException(this, ErrorCodes.XPST0017, + "Number of arguments of function " + getName() + " doesn't match function signature (expected " + + argCount + ", got " + arguments.size() + ')'); + } + } else { + throw new XPathException(this, ErrorCodes.XPST0017, + "Number of arguments of function " + getName() + " doesn't match function signature (expected " + + argCount + ", got " + arguments.size() + ')'); + } } steps.clear(); diff --git a/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java b/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java index adcf7d3d5cb..07d6a924516 100644 --- a/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java +++ b/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java @@ -54,6 +54,17 @@ public static Expression createFunction(XQueryContext context, XQueryAST ast, Pa } catch(final QName.IllegalQNameException xpe) { throw new XPathException(ast, ErrorCodes.XPST0081, "Invalid qname " + ast.getText() + ". " + xpe.getMessage()); } + // XQ4 (PR2200): for unprefixed function calls, check if there's a + // no-namespace user-defined function that should override fn: + if (context.getXQueryVersion() >= 40 + && !ast.getText().contains(":") + && Namespaces.XPATH_FUNCTIONS_NS.equals(qname.getNamespaceURI())) { + final QName noNsName = new QName(ast.getText(), ""); + final UserDefinedFunction noNsFunc = context.resolveFunction(noNsName, params.size()); + if (noNsFunc != null) { + qname = noNsName; + } + } return createFunction(context, qname, ast, parent, params); } @@ -240,12 +251,25 @@ private static GeneralComparison equals(XQueryContext context, XQueryAST ast, private static CastExpression castExpression(XQueryContext context, XQueryAST ast, List params, QName qname) throws XPathException { - if (params.size() != 1) { + final Expression arg; + if (params.size() == 1) { + arg = params.getFirst(); + } else if (params.isEmpty() && context.getXQueryVersion() >= 31) { + // XQ4 focus constructor: xs:type() uses context item as argument + arg = new ContextItemExpression(context); + ((ContextItemExpression) arg).setLocation(ast.getLine(), ast.getColumn()); + } else { throw new XPathException(ast.getLine(), ast.getColumn(), ErrorCodes.XPST0017, "Wrong number of arguments for constructor function"); } - final Expression arg = params.getFirst(); - final int code = Type.getType(qname); + final int code; + try { + code = Type.getType(qname); + } catch (final XPathException e) { + // Unknown type name in xs: namespace → XPST0017 (no such function) + throw new XPathException(ast.getLine(), ast.getColumn(), + ErrorCodes.XPST0017, "Unknown constructor function: " + qname.getStringValue()); + } final CastExpression castExpr = new CastExpression(context, arg, code, Cardinality.ZERO_OR_ONE); castExpr.setLocation(ast.getLine(), ast.getColumn()); return castExpr; @@ -305,10 +329,34 @@ private static Function functionCall(final XQueryContext context, * @param throwOnNotFound true to throw an XPST0017 if the functions is not found, false to just return null */ private static @Nullable Function getInternalModuleFunction(final XQueryContext context, - final XQueryAST ast, final List params, QName qname, Module module, + final XQueryAST ast, List params, QName qname, Module module, final boolean throwOnNotFound) throws XPathException { //For internal modules: create a new function instance from the class - FunctionDef def = ((InternalModule) module).getFunctionDef(qname, params.size()); + final boolean hasKeywordArgs = hasKeywordArguments(params); + FunctionDef def = null; + + // When keyword args are present, skip the initial arity-based lookup because + // params.size() may not match the correct overload. Instead, resolve keyword + // args against all signatures (largest arity first) to find the right one. + if (hasKeywordArgs) { + final List funcs = ((InternalModule) module).getFunctionsByName(qname); + // Sort by arity descending — keyword args typically target the largest overload + funcs.sort((a, b) -> b.getArgumentCount() - a.getArgumentCount()); + for (final FunctionSignature sig : funcs) { + final List resolved = resolveKeywordArguments(context, params, sig, ast); + if (resolved != null) { + def = ((InternalModule) module).getFunctionDef(qname, sig.getArgumentCount()); + if (def != null) { + params = resolved; + break; + } + } + } + } + + if (def == null && !hasKeywordArgs) { + def = ((InternalModule) module).getFunctionDef(qname, params.size()); + } //TODO: rethink: xsl namespace function should search xpath one too if (def == null && Namespaces.XSL_NS.equals(qname.getNamespaceURI())) { //Search xpath namespace @@ -360,7 +408,12 @@ private static Function functionCall(final XQueryContext context, "Access to deprecated functions is not allowed. Call to '" + qname.getStringValue() + "()' denied. " + def.getSignature().getDeprecated()); } final Function fn = Function.createFunction(context, ast, module, def); - fn.setArguments(params); + if (hasKeywordArgs) { + final List resolved = resolveKeywordArguments(context, params, def.getSignature(), ast); + fn.setArguments(resolved != null ? resolved : params); + } else { + fn.setArguments(params); + } fn.setASTNode(ast); return new InternalFunctionCall(fn); } @@ -370,11 +423,36 @@ private static Function functionCall(final XQueryContext context, */ private static FunctionCall getUserDefinedFunction(XQueryContext context, XQueryAST ast, List params, QName qname) throws XPathException { final FunctionCall fc; - final UserDefinedFunction func = context.resolveFunction(qname, params.size()); + final boolean hasKeywordArgs = hasKeywordArguments(params); + + // Count positional arguments to determine resolution arity + int positionalCount = params.size(); + if (hasKeywordArgs) { + positionalCount = 0; + for (final Expression param : params) { + if (param instanceof KeywordArgumentExpression) { + break; + } + positionalCount++; + } + } + + UserDefinedFunction func = context.resolveFunction(qname, params.size()); + + // If keyword args and no exact match, try resolving with positional count + if (func == null && hasKeywordArgs && positionalCount != params.size()) { + func = context.resolveFunction(qname, positionalCount); + } + if (func != null) { fc = new FunctionCall(context, func); fc.setLocation(ast.getLine(), ast.getColumn()); - fc.setArguments(params); + if (hasKeywordArgs) { + final List resolved = resolveKeywordArguments(context, params, func.getSignature(), ast); + fc.setArguments(resolved != null ? resolved : params); + } else { + fc.setArguments(params); + } } else { //Create a forward reference which will be resolved later fc = new FunctionCall(context, qname, params); @@ -482,4 +560,120 @@ public static FunctionCall wrap(XQueryContext context, Function call) throws XPa wrappedCall.setArguments(wrapperArgs); return wrappedCall; } + + /** + * Check if any parameter is a keyword argument. + */ + private static boolean hasKeywordArguments(final List params) { + for (final Expression param : params) { + if (param instanceof KeywordArgumentExpression) { + return true; + } + } + return false; + } + + /** + * Resolve keyword arguments to positional arguments using the function signature. + * + * Keyword arguments (name := value) are matched to the corresponding parameter + * position in the function signature. Positional arguments must come before + * keyword arguments. Gaps between positional and keyword arguments are filled + * with empty sequence expressions for optional parameters. Returns null if + * resolution fails. + */ + private static @Nullable List resolveKeywordArguments( + final XQueryContext context, + final List params, final FunctionSignature signature, + final XQueryAST ast) throws XPathException { + final SequenceType[] argTypes = signature.getArgumentTypes(); + if (argTypes == null) { + return null; + } + + // Find where keyword arguments start + int firstKeyword = -1; + for (int i = 0; i < params.size(); i++) { + if (params.get(i) instanceof KeywordArgumentExpression) { + firstKeyword = i; + break; + } + } + if (firstKeyword < 0) { + return params; // no keyword args + } + + // Build the resolved argument list + final List resolved = new ArrayList<>(argTypes.length); + + // Copy positional arguments + for (int i = 0; i < firstKeyword; i++) { + resolved.add(params.get(i)); + } + + // Fill remaining positions with nulls (to be filled by keyword args) + for (int i = firstKeyword; i < argTypes.length; i++) { + resolved.add(null); + } + + // Match keyword arguments to parameter positions + for (int i = firstKeyword; i < params.size(); i++) { + final Expression param = params.get(i); + if (!(param instanceof KeywordArgumentExpression)) { + throw new XPathException(ast.getLine(), ast.getColumn(), + ErrorCodes.XPST0003, + "Positional arguments must not follow keyword arguments"); + } + final KeywordArgumentExpression kwArg = (KeywordArgumentExpression) param; + final String kwName = kwArg.getKeywordName(); + + // Find matching parameter by name + int matchPos = -1; + for (int j = firstKeyword; j < argTypes.length; j++) { + if (argTypes[j] instanceof org.exist.xquery.value.FunctionParameterSequenceType) { + final String paramName = ((org.exist.xquery.value.FunctionParameterSequenceType) argTypes[j]) + .getAttributeName(); + if (kwName.equals(paramName)) { + matchPos = j; + break; + } + } + } + + if (matchPos < 0) { + return null; // no matching parameter found — signature mismatch + } + if (resolved.get(matchPos) != null) { + throw new XPathException(ast.getLine(), ast.getColumn(), + ErrorCodes.XPST0003, + "Duplicate keyword argument: " + kwName); + } + resolved.set(matchPos, kwArg.getArgument()); + } + + // Fill gaps: for parameters that allow empty sequences or have defaults, + // supply an empty sequence expression. This enables keyword arguments to + // skip optional positional parameters in overloaded built-in functions. + for (int i = 0; i < resolved.size(); i++) { + if (resolved.get(i) == null) { + if (argTypes[i] instanceof org.exist.xquery.value.FunctionParameterSequenceType) { + final org.exist.xquery.value.FunctionParameterSequenceType pst = + (org.exist.xquery.value.FunctionParameterSequenceType) argTypes[i]; + if (pst.hasDefaultValue()) { + resolved.set(i, pst.getDefaultValue()); + } else if (pst.getCardinality().isSuperCardinalityOrEqualOf( + org.exist.xquery.Cardinality.EMPTY_SEQUENCE)) { + // Parameter allows empty — fill with empty sequence + resolved.set(i, new PathExpr(context)); + } else { + return null; // required parameter missing + } + } else { + return null; // can't determine if parameter is optional + } + } + } + + return resolved; + } } diff --git a/exist-core/src/main/java/org/exist/xquery/LetExpr.java b/exist-core/src/main/java/org/exist/xquery/LetExpr.java index 278e7d18295..b18f6d5f257 100644 --- a/exist-core/src/main/java/org/exist/xquery/LetExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/LetExpr.java @@ -108,7 +108,14 @@ public Sequence eval(Sequence contextSequence, Item contextItem) var.setContextDocs(inputSequence.getContextDocSet()); registerUpdateListener(in); - resultSequence = returnExpr.eval(contextSequence, null); + try { + resultSequence = returnExpr.eval(contextSequence, null); + } catch (final WhileClause.WhileTerminationException e) { + resultSequence = Sequence.EMPTY_SEQUENCE; + } + if (getPreviousClause() == null && WhileClause.isTerminated()) { + WhileClause.clearTerminated(); + } if (sequenceType != null) { Cardinality actualCardinality; diff --git a/exist-core/src/main/java/org/exist/xquery/LocationStep.java b/exist-core/src/main/java/org/exist/xquery/LocationStep.java index 624795add20..db87581b741 100644 --- a/exist-core/src/main/java/org/exist/xquery/LocationStep.java +++ b/exist-core/src/main/java/org/exist/xquery/LocationStep.java @@ -443,6 +443,16 @@ public Sequence eval(Sequence contextSequence, final Item contextItem) result = getSiblings(context, contextSequence); break; + case Constants.FOLLOWING_OR_SELF_AXIS: + case Constants.PRECEDING_OR_SELF_AXIS: + result = getOrSelfAxis(context, contextSequence); + break; + + case Constants.FOLLOWING_SIBLING_OR_SELF_AXIS: + case Constants.PRECEDING_SIBLING_OR_SELF_AXIS: + result = getSiblingOrSelfAxis(context, contextSequence); + break; + default: throw new IllegalArgumentException("Unsupported axis specified"); } @@ -1003,6 +1013,93 @@ private Sequence getPrecedingOrFollowing(final XQueryContext context, final Sequ } } + /** + * XQ4: Evaluate following-or-self or preceding-or-self axis. + * Combines self:: with following:: or preceding:: and returns + * results in document order. + */ + private Sequence getOrSelfAxis(final XQueryContext context, final Sequence contextSequence) + throws XPathException { + // Evaluate self:: axis + final int savedAxis = axis; + axis = Constants.SELF_AXIS; + final Sequence selfResult = getSelf(context, contextSequence); + + // Evaluate the base axis (following or preceding) + axis = (savedAxis == Constants.FOLLOWING_OR_SELF_AXIS) + ? Constants.FOLLOWING_AXIS : Constants.PRECEDING_AXIS; + final Sequence baseResult = getPrecedingOrFollowing(context, contextSequence); + + axis = savedAxis; + + // Merge results + if (selfResult.isEmpty()) { + return baseResult; + } + if (baseResult.isEmpty()) { + return selfResult; + } + final ValueSequence combined = new ValueSequence(); + if (savedAxis == Constants.PRECEDING_OR_SELF_AXIS) { + // preceding comes first in document order, then self + combined.addAll(baseResult); + combined.addAll(selfResult); + } else { + // self comes first, then following + combined.addAll(selfResult); + combined.addAll(baseResult); + } + combined.sortInDocumentOrder(); + combined.removeDuplicates(); + return combined; + } + + /** + * XQ4: Evaluate following-sibling-or-self or preceding-sibling-or-self axis. + * Combines self:: with following-sibling:: or preceding-sibling:: and returns + * results in document order. + */ + private Sequence getSiblingOrSelfAxis(final XQueryContext context, final Sequence contextSequence) + throws XPathException { + // Evaluate self:: axis + final int savedAxis = axis; + axis = Constants.SELF_AXIS; + final Sequence selfResult = getSelf(context, contextSequence); + + // Evaluate the base sibling axis — guard against document nodes + // which don't have siblings and cause ArrayIndexOutOfBounds + axis = (savedAxis == Constants.FOLLOWING_SIBLING_OR_SELF_AXIS) + ? Constants.FOLLOWING_SIBLING_AXIS : Constants.PRECEDING_SIBLING_AXIS; + Sequence baseResult; + try { + baseResult = getSiblings(context, contextSequence); + } catch (final ArrayIndexOutOfBoundsException e) { + // Document nodes don't have siblings + baseResult = Sequence.EMPTY_SEQUENCE; + } + + axis = savedAxis; + + // Merge results + if (selfResult.isEmpty()) { + return baseResult; + } + if (baseResult.isEmpty()) { + return selfResult; + } + final ValueSequence combined = new ValueSequence(); + if (savedAxis == Constants.PRECEDING_SIBLING_OR_SELF_AXIS) { + combined.addAll(baseResult); + combined.addAll(selfResult); + } else { + combined.addAll(selfResult); + combined.addAll(baseResult); + } + combined.sortInDocumentOrder(); + combined.removeDuplicates(); + return combined; + } + /** * If the optimizer has determined that the first filter after this step is a simple positional * predicate and can be optimized, try to precompute the position and return it to limit the diff --git a/exist-core/src/main/java/org/exist/xquery/StaticXQueryException.java b/exist-core/src/main/java/org/exist/xquery/StaticXQueryException.java index 682be4dfff1..36494f688cc 100644 --- a/exist-core/src/main/java/org/exist/xquery/StaticXQueryException.java +++ b/exist-core/src/main/java/org/exist/xquery/StaticXQueryException.java @@ -30,19 +30,19 @@ public StaticXQueryException(String message) { } public StaticXQueryException(final Expression expression, String message) { - super(expression, message); + super(expression, ErrorCodes.XPST0003, message); } public StaticXQueryException(int line, int column, String message) { - super(line, column, message); + super(line, column, ErrorCodes.XPST0003, message); } - + public StaticXQueryException(Throwable cause) { this((Expression) null, cause); } - + public StaticXQueryException(final Expression expression, Throwable cause) { - super(expression, cause); + super(expression, ErrorCodes.XPST0003, cause.getMessage(), cause); } public StaticXQueryException(String message, Throwable cause) { @@ -50,11 +50,20 @@ public StaticXQueryException(String message, Throwable cause) { } public StaticXQueryException(final Expression expression, String message, Throwable cause) { - super(expression, message, cause); + super(expression, ErrorCodes.XPST0003, message, cause); } - //TODO add in ErrorCode and ErrorVal public StaticXQueryException(int line, int column, String message, Throwable cause) { - super(line, column, message, cause); + super(line, column, ErrorCodes.XPST0003, message); + initCause(cause); + } + + public StaticXQueryException(int line, int column, ErrorCodes.ErrorCode errorCode, String message) { + super(line, column, errorCode, message); + } + + public StaticXQueryException(int line, int column, ErrorCodes.ErrorCode errorCode, String message, Throwable cause) { + super(line, column, errorCode, message); + initCause(cause); } } \ No newline at end of file diff --git a/exist-core/src/main/java/org/exist/xquery/StringConstructor.java b/exist-core/src/main/java/org/exist/xquery/StringConstructor.java index 3d725e63c66..ba3b0fce492 100644 --- a/exist-core/src/main/java/org/exist/xquery/StringConstructor.java +++ b/exist-core/src/main/java/org/exist/xquery/StringConstructor.java @@ -159,9 +159,13 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException public String eval(final Sequence contextSequence) throws XPathException { final Sequence result = expression.eval(contextSequence, null); + // Atomize the result per spec: string constructor interpolation + // atomizes its content, joining with spaces + final Sequence atomized = Atomize.atomize(result); + final StringBuilder out = new StringBuilder(); boolean gotOne = false; - for(final SequenceIterator i = result.iterate(); i.hasNext(); ) { + for(final SequenceIterator i = atomized.iterate(); i.hasNext(); ) { final Item next = i.nextItem(); if (gotOne) { out.append(' '); diff --git a/exist-core/src/main/java/org/exist/xquery/SwitchExpression.java b/exist-core/src/main/java/org/exist/xquery/SwitchExpression.java index d75361bf784..70e263539cf 100644 --- a/exist-core/src/main/java/org/exist/xquery/SwitchExpression.java +++ b/exist-core/src/main/java/org/exist/xquery/SwitchExpression.java @@ -56,11 +56,20 @@ public Case(List caseOperands, Expression caseClause) { private Expression operand; private Case defaultClause = null; private List cases = new ArrayList<>(5); - + private boolean booleanMode = false; + public SwitchExpression(XQueryContext context, Expression operand) { super(context); this.operand = operand; } + + /** + * Set boolean mode for XQ4 omitted comparand: switch () { case boolExpr return ... } + * In boolean mode, each case operand is evaluated and its effective boolean value determines the match. + */ + public void setBooleanMode(boolean booleanMode) { + this.booleanMode = booleanMode; + } /** * Add case clause(s) with a return. @@ -88,34 +97,58 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc if (contextItem != null) {contextSequence = contextItem.toSequence();} + + if (booleanMode) { + // XQ4 omitted comparand: evaluate each case operand as boolean + return evalBooleanMode(contextSequence, contextItem); + } + final Sequence opSeq = operand.eval(contextSequence, null); - Sequence result = null; + if (opSeq.hasMany()) { + throw new XPathException(this, ErrorCodes.XPTY0004, "Cardinality error in switch operand ", opSeq); + } + final Collator defaultCollator = context.getDefaultCollator(); if (opSeq.isEmpty()) { - result = defaultClause.returnClause.eval(contextSequence, null); + // XQ4: empty comparand can match case () (empty case operand) + for (final Case next : cases) { + for (final Expression caseOperand : next.operands) { + final Sequence caseSeq = caseOperand.eval(contextSequence, contextItem); + if (caseSeq.isEmpty()) { + return next.returnClause.eval(contextSequence, null); + } + } + } } else { - if (opSeq.hasMany()) { - throw new XPathException(this, ErrorCodes.XPTY0004, "Cardinality error in switch operand ", opSeq); + final AtomicValue opVal = opSeq.itemAt(0).atomize(); + for (final Case next : cases) { + for (final Expression caseOperand : next.operands) { + final Sequence caseSeq = caseOperand.eval(contextSequence, contextItem); + if (context.getXQueryVersion() <= 30 && caseSeq.hasMany()) { + throw new XPathException(this, ErrorCodes.XPTY0004, "Cardinality error in switch case operand ", caseSeq); + } + // XQ4: case operand may be a sequence; match if any item equals the comparand + for (int i = 0; i < caseSeq.getItemCount(); i++) { + final AtomicValue caseVal = caseSeq.itemAt(i).atomize(); + if (FunDeepEqual.deepEquals(caseVal, opVal, defaultCollator)) { + return next.returnClause.eval(contextSequence, null); + } + } + } } - final AtomicValue opVal = opSeq.itemAt(0).atomize(); - final Collator defaultCollator = context.getDefaultCollator(); - for (final Case next : cases) { - for (final Expression caseOperand : next.operands) { - final Sequence caseSeq = caseOperand.eval(contextSequence, contextItem); - if (caseSeq.hasMany()) { - throw new XPathException(this, ErrorCodes.XPTY0004, "Cardinality error in switch case operand ", caseSeq); - } - final AtomicValue caseVal = caseSeq.isEmpty() ? AtomicValue.EMPTY_VALUE : caseSeq.itemAt(0).atomize(); - if (FunDeepEqual.deepEquals(caseVal, opVal, defaultCollator)) { - return next.returnClause.eval(contextSequence, null); - } - } - } } - if (result == null) { - result = defaultClause.returnClause.eval(contextSequence, null); + return defaultClause.returnClause.eval(contextSequence, null); + } + + private Sequence evalBooleanMode(Sequence contextSequence, Item contextItem) throws XPathException { + for (final Case next : cases) { + for (final Expression caseOperand : next.operands) { + final Sequence caseSeq = caseOperand.eval(contextSequence, contextItem); + if (caseSeq.effectiveBooleanValue()) { + return next.returnClause.eval(contextSequence, null); + } + } } - - return result; + return defaultClause.returnClause.eval(contextSequence, null); } public int returnsType() { diff --git a/exist-core/src/main/java/org/exist/xquery/TryCatchExpression.java b/exist-core/src/main/java/org/exist/xquery/TryCatchExpression.java index c11a2acf065..0712770b636 100644 --- a/exist-core/src/main/java/org/exist/xquery/TryCatchExpression.java +++ b/exist-core/src/main/java/org/exist/xquery/TryCatchExpression.java @@ -63,6 +63,7 @@ public class TryCatchExpression extends AbstractExpression { private final Expression tryTargetExpr; private final List catchClauses = new ArrayList<>(); + private Expression finallyExpr; /** * Constructor. @@ -88,6 +89,10 @@ public void addCatchClause(final List catchErrorList, final List c catchClauses.add( new CatchClause(catchErrorList, catchVars, catchExpr) ); } + public void setFinallyExpr(final Expression finallyExpr) { + this.finallyExpr = finallyExpr; + } + @Override public int getDependencies() { return Dependency.CONTEXT_SET | Dependency.CONTEXT_ITEM; @@ -126,6 +131,9 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException for (final CatchClause catchClause : catchClauses) { catchClause.getCatchExpr().analyze(contextInfo); } + if (finallyExpr != null) { + finallyExpr.analyze(contextInfo); + } } finally { // restore the local variable stack context.popLocalVariables(mark); @@ -141,107 +149,136 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr throw new XPathException(this, ErrorCodes.EXXQDY0003, "The try-catch expression is only available in xquery version \"3.0\" and later."); } + Sequence result = null; + Throwable pendingError = null; + try { // Evaluate 'try' expression - final Sequence tryTargetSeq = tryTargetExpr.eval(contextSequence, contextItem); - return tryTargetSeq; + result = tryTargetExpr.eval(contextSequence, contextItem); - } catch (final Throwable throwable) { + } catch (final Throwable throwable) { - final ErrorCode errorCode; + // If no catch clauses (try/finally only), re-throw after finally + if (catchClauses.isEmpty()) { + pendingError = throwable; + } else { - // fn:error throws an XPathException - if(throwable instanceof XPathException xpe){ - // Get errorcode from nicely thrown xpathexception + final ErrorCode errorCode; - if(xpe.getErrorCode() != null) { - if(xpe.getErrorCode() == ErrorCodes.ERROR) { - errorCode = extractErrorCode(xpe); + // fn:error throws an XPathException + if (throwable instanceof XPathException xpe) { + // Get errorcode from nicely thrown xpathexception + + if (xpe.getErrorCode() != null) { + if (xpe.getErrorCode() == ErrorCodes.ERROR) { + errorCode = extractErrorCode(xpe); + } else { + errorCode = xpe.getErrorCode(); + } } else { - errorCode = xpe.getErrorCode(); + // if no errorcode is found, reconstruct by parsing the error text. + errorCode = extractErrorCode(xpe); } } else { - // if no errorcode is found, reconstruct by parsing the error text. - errorCode = extractErrorCode(xpe); + // Get errorcode from all other errors and exceptions + errorCode = new JavaErrorCode(throwable); } - } else { - // Get errorcode from all other errors and exceptions - errorCode = new JavaErrorCode(throwable); - } - // We need the qname in the end - final QName errorCodeQname = errorCode.getErrorQName(); - - // Exception in thrown, catch expression will be evaluated. - // catchvars (CatchErrorCode (, CatchErrorDesc (, CatchErrorVal)?)? ) - // need to be retrieved as variables - Sequence catchResultSeq = null; - final LocalVariable mark0 = context.markLocalVariables(false); // DWES: what does this do? - - // DWES: should I use popLocalVariables - context.declareInScopeNamespace(Namespaces.W3C_XQUERY_XPATH_ERROR_PREFIX, Namespaces.W3C_XQUERY_XPATH_ERROR_NS); - context.declareInScopeNamespace(Namespaces.EXIST_XQUERY_XPATH_ERROR_PREFIX, Namespaces.EXIST_XQUERY_XPATH_ERROR_NS); - - //context.declareInScopeNamespace(null, null); - - try { - // flag used to escape loop when errorcode has matched - boolean errorMatched = false; - - // Iterate on all catch clauses - for (final CatchClause catchClause : catchClauses) { - - if (isErrorInList(errorCodeQname, catchClause.getCatchErrorList()) && !errorMatched) { - - errorMatched = true; - - // Get catch variables - final LocalVariable mark1 = context.markLocalVariables(false); // DWES: what does this do? - - try { - // Add std errors - addErrCode(errorCodeQname); - addErrDescription(throwable, errorCode); - addErrValue(throwable); - addErrModule(throwable); - addErrLineNumber(throwable); - addErrColumnNumber(throwable); - addErrAdditional(throwable); - addFunctionTrace(throwable); - addJavaTrace(throwable); - - // Evaluate catch expression - catchResultSeq = ((Expression) catchClause.getCatchExpr()).eval(contextSequence, contextItem); - - - } finally { - context.popLocalVariables(mark1, catchResultSeq); + // We need the qname in the end + final QName errorCodeQname = errorCode.getErrorQName(); + + // Exception in thrown, catch expression will be evaluated. + // catchvars (CatchErrorCode (, CatchErrorDesc (, CatchErrorVal)?)? ) + // need to be retrieved as variables + Sequence catchResultSeq = null; + final LocalVariable mark0 = context.markLocalVariables(false); + + context.declareInScopeNamespace(Namespaces.W3C_XQUERY_XPATH_ERROR_PREFIX, Namespaces.W3C_XQUERY_XPATH_ERROR_NS); + context.declareInScopeNamespace(Namespaces.EXIST_XQUERY_XPATH_ERROR_PREFIX, Namespaces.EXIST_XQUERY_XPATH_ERROR_NS); + + try { + // flag used to escape loop when errorcode has matched + boolean errorMatched = false; + + // Iterate on all catch clauses + for (final CatchClause catchClause : catchClauses) { + + if (isErrorInList(errorCodeQname, catchClause.getCatchErrorList()) && !errorMatched) { + + errorMatched = true; + + // Get catch variables + final LocalVariable mark1 = context.markLocalVariables(false); + + try { + // Add std errors + addErrCode(errorCodeQname); + addErrDescription(throwable, errorCode); + addErrValue(throwable); + addErrModule(throwable); + addErrLineNumber(throwable); + addErrColumnNumber(throwable); + addErrAdditional(throwable); + addFunctionTrace(throwable); + addJavaTrace(throwable); + + // Evaluate catch expression + catchResultSeq = ((Expression) catchClause.getCatchExpr()).eval(contextSequence, contextItem); + + + } finally { + context.popLocalVariables(mark1, catchResultSeq); + } + + } else { + // if in the end nothing is set, rethrow after loop } + } // for catch clauses + // If an error hasn't been caught, store for re-throw after finally + if (!errorMatched) { + pendingError = throwable; } else { - // if in the end nothing is set, rethrow after loop + result = catchResultSeq; } - } // for catch clauses - // If an error hasn't been caught, throw new one - if (!errorMatched) { - if (throwable instanceof XPathException) { - throw throwable; - } else { - LOG.error(throwable); - throw new XPathException(this, throwable); + } finally { + context.popLocalVariables(mark0, catchResultSeq); + } + } + } finally { + // XQ4: Evaluate finally clause (always, even if try/catch succeeded or failed) + if (finallyExpr != null) { + try { + final Sequence finallyResult = finallyExpr.eval(contextSequence, contextItem); + // If finally produces a non-empty sequence, raise XQTY0153 + if (finallyResult != null && !finallyResult.isEmpty()) { + throw new XPathException(this, ErrorCodes.XQTY0153, + "The finally clause must evaluate to an empty sequence, got " + + finallyResult.getItemCount() + " item(s)"); } + } catch (final XPathException finallyError) { + // Finally error replaces any pending error or result + context.expressionEnd(this); + throw finallyError; } - - } finally { - context.popLocalVariables(mark0, catchResultSeq); } - return catchResultSeq; + // Re-throw pending error from try body (if not caught) + if (pendingError != null) { + context.expressionEnd(this); + if (pendingError instanceof XPathException) { + throw (XPathException) pendingError; + } else { + LOG.error(pendingError); + throw new XPathException(this, pendingError); + } + } - } finally { context.expressionEnd(this); } + + return result; } @@ -384,6 +421,13 @@ public void dump(final ExpressionDumper dumper) { dumper.nl().display("}"); dumper.endIndent(); } + if (finallyExpr != null) { + dumper.nl().display("} finally {"); + dumper.startIndent(); + finallyExpr.dump(dumper); + dumper.nl().display("}"); + dumper.endIndent(); + } } /** @@ -428,6 +472,11 @@ public String toString() { result.append(catchExpr.toString()); result.append("}"); } + if (finallyExpr != null) { + result.append(" finally { "); + result.append(finallyExpr.toString()); + result.append("}"); + } return result.toString(); } @@ -436,8 +485,10 @@ public String toString() { */ @Override public int returnsType() { - // fixme! /ljo - return ((Expression) catchClauses.getFirst().getCatchExpr()).returnsType(); + if (!catchClauses.isEmpty()) { + return ((Expression) catchClauses.getFirst().getCatchExpr()).returnsType(); + } + return tryTargetExpr.returnsType(); } /* (non-Javadoc) @@ -451,6 +502,9 @@ public void resetState(final boolean postOptimization) { final Expression catchExpr = (Expression) catchClause.getCatchExpr(); catchExpr.resetState(postOptimization); } + if (finallyExpr != null) { + finallyExpr.resetState(postOptimization); + } } @Override diff --git a/exist-core/src/main/java/org/exist/xquery/UserDefinedFunction.java b/exist-core/src/main/java/org/exist/xquery/UserDefinedFunction.java index a56db1a200b..33b781868ed 100644 --- a/exist-core/src/main/java/org/exist/xquery/UserDefinedFunction.java +++ b/exist-core/src/main/java/org/exist/xquery/UserDefinedFunction.java @@ -24,8 +24,10 @@ import org.exist.dom.persistent.DocumentSet; import org.exist.dom.QName; import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.FunctionParameterSequenceType; import org.exist.xquery.value.Item; import org.exist.xquery.value.Sequence; +import org.exist.xquery.value.SequenceType; import java.util.ArrayList; import java.util.List; @@ -125,31 +127,51 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc } Sequence result = null; try { - QName varName; - LocalVariable var; - int j = 0; - for (int i = 0; i < parameters.size(); i++, j++) { - varName = parameters.get(i); - var = new LocalVariable(varName); - var.setValue(currentArguments[j]); - if (contextDocs != null) { + final SequenceType[] argTypes = getSignature().getArgumentTypes(); + + // Evaluate all argument values first, BEFORE declaring any parameters. + // Default value expressions must be evaluated in the prolog's variable scope, + // not the function body scope (XQ4 spec: default sees variables in scope at + // the function declaration, not other parameters). Context is passed so that + // default values like "." can access the context item at the call site. + final Sequence[] argValues = new Sequence[parameters.size()]; + for (int i = 0; i < parameters.size(); i++) { + if (i < currentArguments.length) { + argValues[i] = currentArguments[i]; + } else if (argTypes[i] instanceof FunctionParameterSequenceType && + ((FunctionParameterSequenceType) argTypes[i]).hasDefaultValue()) { + argValues[i] = ((FunctionParameterSequenceType) argTypes[i]) + .getDefaultValue().eval(contextSequence, contextItem); + } else { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Missing required argument $" + parameters.get(i)); + } + } + + // Now declare all parameters with their resolved values + for (int i = 0; i < parameters.size(); i++) { + final QName varName = parameters.get(i); + final LocalVariable var = new LocalVariable(varName); + + var.setValue(argValues[i]); + if (contextDocs != null && i < contextDocs.length) { var.setContextDocs(contextDocs[i]); } context.declareVariableBinding(var); Cardinality actualCardinality; - if (currentArguments[j].isEmpty()) { + if (argValues[i].isEmpty()) { actualCardinality = Cardinality.EMPTY_SEQUENCE; - } else if (currentArguments[j].hasMany()) { + } else if (argValues[i].hasMany()) { actualCardinality = Cardinality._MANY; } else { actualCardinality = Cardinality.EXACTLY_ONE; } - if (!getSignature().getArgumentTypes()[j].getCardinality().isSuperCardinalityOrEqualOf(actualCardinality)) { + if (!argTypes[i].getCardinality().isSuperCardinalityOrEqualOf(actualCardinality)) { throw new XPathException(this, ErrorCodes.XPTY0004, "Invalid cardinality for parameter $" + varName + - ". Expected " + getSignature().getArgumentTypes()[j].getCardinality().getHumanDescription() + - ", got " + currentArguments[j].getItemCount()); + ". Expected " + argTypes[i].getCardinality().getHumanDescription() + + ", got " + argValues[i].getItemCount()); } } result = body.eval(null, null); diff --git a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java index b3721c34179..600ef6b336b 100644 --- a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java +++ b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java @@ -1840,7 +1840,7 @@ public void declareFunction(final UserDefinedFunction function) throws XPathExce final QName name = function.getSignature().getName(); final String uri = name.getNamespaceURI(); - if (uri.isEmpty()) { + if (uri.isEmpty() && getXQueryVersion() < 40) { throw new XPathException(function, ErrorCodes.XQST0060, "Every declared function name must have a non-null namespace URI, " + "but function '" + name + "' does not meet this requirement."); @@ -1865,7 +1865,31 @@ public void declareFunction(final UserDefinedFunction function) throws XPathExce @Override public @Nullable UserDefinedFunction resolveFunction(final QName name, final int argCount) { final FunctionId id = new FunctionId(name, argCount); - return declaredFunctions.get(id); + final UserDefinedFunction exact = declaredFunctions.get(id); + if (exact != null) { + return exact; + } + // XQ4: Try to find a function with more params where trailing params have defaults + for (final UserDefinedFunction func : declaredFunctions.values()) { + if (func.getName().equals(name)) { + final SequenceType[] argTypes = func.getSignature().getArgumentTypes(); + if (argTypes.length > argCount) { + // Check that all params from argCount onwards have defaults + boolean allDefaulted = true; + for (int i = argCount; i < argTypes.length; i++) { + if (!(argTypes[i] instanceof FunctionParameterSequenceType) || + !((FunctionParameterSequenceType) argTypes[i]).hasDefaultValue()) { + allDefaulted = false; + break; + } + } + if (allDefaulted) { + return func; + } + } + } + } + return null; } @Override @@ -2730,6 +2754,13 @@ private ExternalModule compileOrBorrowModule(final String namespaceURI, final St * @return The compiled module, or null if the source is not a module * @throws XPathException if the module could not be loaded (XQST0059) or compiled (XPST0003) */ + /** + * Compile a module from a Source. Public wrapper for fn:load-xquery-module content option. + */ + public @Nullable ExternalModule compileModuleFromSource(final String namespaceURI, final Source source) throws XPathException { + return compileModule(namespaceURI, null, "content", source); + } + private @Nullable ExternalModule compileModule(String namespaceURI, final String prefix, final String location, final Source source) throws XPathException { if (LOG.isDebugEnabled()) { diff --git a/exist-core/src/main/java/org/exist/xquery/value/FunctionParameterSequenceType.java b/exist-core/src/main/java/org/exist/xquery/value/FunctionParameterSequenceType.java index b383b881d75..a2aa7078d52 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/FunctionParameterSequenceType.java +++ b/exist-core/src/main/java/org/exist/xquery/value/FunctionParameterSequenceType.java @@ -22,6 +22,7 @@ package org.exist.xquery.value; import org.exist.xquery.Cardinality; +import org.exist.xquery.Expression; /** * This class is used to specify the name and description of an XQuery function parameter. @@ -32,6 +33,7 @@ public class FunctionParameterSequenceType extends FunctionReturnSequenceType { private String attributeName; + private Expression defaultValue; /** * @param attributeName The name of the parameter in the FunctionSignature. @@ -79,4 +81,16 @@ public void setAttributeName(String attributeName) { this.attributeName = attributeName; } + public boolean hasDefaultValue() { + return defaultValue != null; + } + + public Expression getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(final Expression defaultValue) { + this.defaultValue = defaultValue; + } + } diff --git a/exist-core/src/main/java/org/exist/xquery/value/SequenceType.java b/exist-core/src/main/java/org/exist/xquery/value/SequenceType.java index f00c9811ea1..9016cd0c844 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/SequenceType.java +++ b/exist-core/src/main/java/org/exist/xquery/value/SequenceType.java @@ -30,6 +30,9 @@ import org.w3c.dom.Element; import org.w3c.dom.Node; +import java.util.ArrayList; +import java.util.List; + /** * Represents an XQuery SequenceType and provides methods to check * sequences and items against this type. @@ -41,6 +44,8 @@ public class SequenceType { private int primaryType = Type.ITEM; private Cardinality cardinality = Cardinality.EXACTLY_ONE; private QName nodeName = null; + private List choiceAlternatives = null; + private String[] enumValues = null; public SequenceType() { } @@ -108,6 +113,81 @@ public void setNodeName(QName qname) { this.nodeName = qname; } + public void addChoiceAlternative(final SequenceType alt) { + if (choiceAlternatives == null) { + choiceAlternatives = new ArrayList<>(); + } + choiceAlternatives.add(alt); + } + + public List getChoiceAlternatives() { + return choiceAlternatives; + } + + public boolean isChoiceType() { + return choiceAlternatives != null && !choiceAlternatives.isEmpty(); + } + + public void setEnumValues(final String[] values) { + this.enumValues = values; + this.primaryType = Type.STRING; + } + + public String[] getEnumValues() { + return enumValues; + } + + public boolean isEnumType() { + return enumValues != null; + } + + // Record type support + + /** + * Represents a field in a record type declaration. + */ + public static class RecordField { + private final String name; + private final boolean optional; + private final SequenceType fieldType; + + public RecordField(final String name, final boolean optional, final SequenceType fieldType) { + this.name = name; + this.optional = optional; + this.fieldType = fieldType; + } + + public String getName() { return name; } + public boolean isOptional() { return optional; } + public SequenceType getFieldType() { return fieldType; } + } + + private List recordFields = null; + private boolean recordExtensible = false; + + public void addRecordField(final RecordField field) { + if (recordFields == null) { + recordFields = new ArrayList<>(); + } + recordFields.add(field); + } + + public List getRecordFields() { + return recordFields; + } + + public void setRecordExtensible(final boolean extensible) { + this.recordExtensible = extensible; + } + + public boolean isRecordExtensible() { + return recordExtensible; + } + + public boolean isRecordType() { + return primaryType == Type.RECORD; + } + /** * Check the specified sequence against this SequenceType. * @@ -115,17 +195,29 @@ public void setNodeName(QName qname) { * @throws XPathException if check fails for one item in the sequence * @return true, if all items of the sequence have the same type as or a subtype of primaryType */ - public boolean checkType(final Sequence seq) throws XPathException { - if (nodeName == null) { - return Type.subTypeOf(seq.getItemType(), primaryType); + public boolean checkType(Sequence seq) throws XPathException { + if (isChoiceType()) { + Item next; + for (final SequenceIterator i = seq.iterate(); i.hasNext(); ) { + next = i.nextItem(); + if (!checkType(next)) { + return false; + } + } + return true; } - - for (final SequenceIterator i = seq.iterate(); i.hasNext(); ) { - if (!checkType(i.nextItem())) { - return false; + if (nodeName != null) { + Item next; + for (final SequenceIterator i = seq.iterate(); i.hasNext(); ) { + next = i.nextItem(); + if (!checkType(next)) { + return false; + } } + return true; + } else { + return Type.subTypeOf(seq.getItemType(), primaryType); } - return true; } /** @@ -134,55 +226,172 @@ public boolean checkType(final Sequence seq) throws XPathException { * @param item the item to check * @return true, if item is a subtype of primaryType */ - public boolean checkType(final Item item) { + public boolean checkType(Item item) { + if (isChoiceType()) { + for (final SequenceType alt : choiceAlternatives) { + if (alt.checkType(item)) { + return true; + } + } + return false; + } + if (isEnumType()) { + if (!Type.subTypeOf(item.getType(), Type.STRING)) { + return false; + } + try { + final String val = item.getStringValue(); + for (final String enumVal : enumValues) { + if (enumVal.equals(val)) { + return true; + } + } + } catch (final XPathException e) { + // cannot get string value + } + return false; + } + if (isRecordType()) { + return checkRecordType(item); + } + Node realNode = null; int type = item.getType(); if (type == Type.NODE) { - final Node realNode = ((NodeValue) item).getNode(); + realNode = ((NodeValue) item).getNode(); type = realNode.getNodeType(); } if (!Type.subTypeOf(type, primaryType)) { return false; } - if (nodeName == null) { - return true; + if (nodeName != null) { + + //TODO : how to improve performance ? + + final NodeValue nvItem = (NodeValue) item; + QName realName = null; + if (item.getType() == Type.DOCUMENT) { + // it's a document... we need to get the document element's name + final Document doc; + if (nvItem instanceof Document) { + doc = (Document) nvItem; + } else { + doc = nvItem.getOwnerDocument(); + } + if (doc != null) { + final Element elem = doc.getDocumentElement(); + if (elem != null) { + realName = new QName(elem.getLocalName(), elem.getNamespaceURI()); + } + } + } else { + // get the name of the element/attribute + realName = nvItem.getQName(); + } + + if (realName == null) { + return false; + } + + if (nodeName.getNamespaceURI() != null) { + if (!nodeName.getNamespaceURI().equals(realName.getNamespaceURI())) { + return false; + } + } + if (nodeName.getLocalPart() != null) { + return nodeName.getLocalPart().equals(realName.getLocalPart()); + } } - //TODO : how to improve performance ? - final QName realName = getRealName(item); + return true; + } - if (realName == null) { + /** + * Check if an item matches this record type declaration. + * A map matches a record type if: + * - All required fields are present + * - Each field value matches the declared type + * - If not extensible (no *), no extra keys are present + */ + private boolean checkRecordType(final Item item) { + if (!Type.subTypeOf(item.getType(), Type.MAP_ITEM)) { return false; } - if (nodeName.getNamespaceURI() != null && - !nodeName.getNamespaceURI().equals(realName.getNamespaceURI())) { - return false; + // record(*) matches any map + if (recordExtensible && (recordFields == null || recordFields.isEmpty())) { + return true; } - if (nodeName.getLocalPart() != null) { - return nodeName.getLocalPart().equals(realName.getLocalPart()); + final org.exist.xquery.functions.map.AbstractMapType map = + (org.exist.xquery.functions.map.AbstractMapType) item; + + // record() with no fields and not extensible: only empty maps match + if ((recordFields == null || recordFields.isEmpty()) && !recordExtensible) { + return map.size() == 0; } + + // Check required fields are present and types match + for (final RecordField field : recordFields) { + final AtomicValue key = new StringValue(null, field.getName()); + final boolean hasKey = map.contains(key); + + if (!hasKey && !field.isOptional()) { + return false; // required field missing + } + + if (hasKey && field.getFieldType() != null) { + try { + final Sequence value = map.get(key); + if (!field.getFieldType().matchesCardinality(value)) { + return false; + } + if (!value.isEmpty() && !field.getFieldType().checkType(value)) { + return false; + } + } catch (final XPathException e) { + return false; + } + } + } + + // If not extensible, check for extra keys + if (!recordExtensible) { + try { + final Sequence keys = map.keys(); + for (final SequenceIterator it = keys.iterate(); it.hasNext(); ) { + final String keyName = it.nextItem().getStringValue(); + boolean declared = false; + for (final RecordField field : recordFields) { + if (field.getName().equals(keyName)) { + declared = true; + break; + } + } + if (!declared) { + return false; // undeclared key in non-extensible record + } + } + } catch (final XPathException e) { + return false; + } + } + return true; } - private static QName getRealName(final Item item) { - final NodeValue nvItem = (NodeValue) item; - if (item.getType() != Type.DOCUMENT) { - // get the name of the element/attribute - return nvItem.getQName(); - } - // it's a document... we need to get the document element's name - final Document doc; - if (nvItem instanceof Document) { - doc = (Document) nvItem; - } else { - doc = nvItem.getOwnerDocument(); + /** + * Check if a sequence's cardinality matches this type's cardinality declaration. + */ + public boolean matchesCardinality(final Sequence seq) { + if (cardinality == Cardinality.ZERO_OR_MORE) { + return true; } - if (doc == null) { - return null; + final int count = seq.getItemCount(); + if (count == 0) { + return cardinality.isSuperCardinalityOrEqualOf(Cardinality.EMPTY_SEQUENCE); } - final Element elem = doc.getDocumentElement(); - if (elem == null) { - return null; + if (count == 1) { + return true; // EXACTLY_ONE, ZERO_OR_ONE, ONE_OR_MORE all accept 1 } - return new QName(elem.getLocalName(), elem.getNamespaceURI()); + // count > 1 + return cardinality == Cardinality.ONE_OR_MORE || cardinality == Cardinality.ZERO_OR_MORE; } /** @@ -197,14 +406,17 @@ public void checkType(int type) throws XPathException { return; } - // Although xs:anyURI is not a subtype of xs:string, both types are compatible + //Although xs:anyURI is not a subtype of xs:string, both types are compatible if (type == Type.ANY_URI && primaryType == Type.STRING) { return; } if (!Type.subTypeOf(type, primaryType)) { throw new XPathException((Expression) null, ErrorCodes.XPTY0004, - "Type error: expected type: " + Type.getTypeName(primaryType) + "; got: " + Type.getTypeName(type)); + "Type error: expected type: " + + Type.getTypeName(primaryType) + + "; got: " + + Type.getTypeName(type)); } } @@ -226,28 +438,49 @@ public void checkCardinality(Sequence seq) throws XPathException { } } - /** - * Used to serialize SequenceTypes, when building stack traces, for example. - * - * @return The serialized SequenceType - */ @Override public String toString() { if (cardinality == Cardinality.EMPTY_SEQUENCE) { return cardinality.toXQueryCardinalityString(); } + if (isChoiceType()) { + final StringBuilder sb = new StringBuilder("("); + for (int i = 0; i < choiceAlternatives.size(); i++) { + if (i > 0) { + sb.append(" | "); + } + sb.append(choiceAlternatives.get(i).toString()); + } + sb.append(")"); + sb.append(cardinality.toXQueryCardinalityString()); + return sb.toString(); + } + + if (isEnumType()) { + final StringBuilder sb = new StringBuilder("enum("); + for (int i = 0; i < enumValues.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append("\"").append(enumValues[i]).append("\""); + } + sb.append(")"); + sb.append(cardinality.toXQueryCardinalityString()); + return sb.toString(); + } + final String str; if (primaryType == Type.DOCUMENT && nodeName != null) { str = "document-node(" + nodeName.getStringValue() + ")"; } else if (primaryType == Type.ELEMENT && nodeName != null) { str = "element(" + nodeName.getStringValue() + ")"; - } else if (primaryType == Type.MAP_ITEM) { - str = "map(*)"; - } else if (primaryType == Type.ARRAY_ITEM) { - str = "array(*)"; - } else if (primaryType == Type.FUNCTION) { - str = "function(*)"; +// } else if (primaryType == Type.MAP) { +// str = "map(" + + ")"; +// } else if (primaryType == Type.ARRAY) { +// str = "array(" + + ")"; +// } else if (primaryType == Type.FUNCTION_REFERENCE) { +// str = "function(" + + ")"; } else { str = Type.getTypeName(primaryType); } diff --git a/exist-core/src/main/java/org/exist/xquery/value/Type.java b/exist-core/src/main/java/org/exist/xquery/value/Type.java index f60c60d7255..0c65c7a031a 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/Type.java +++ b/exist-core/src/main/java/org/exist/xquery/value/Type.java @@ -133,9 +133,12 @@ public class Type { public final static int JAVA_OBJECT = 68; public final static int EMPTY_SEQUENCE = 69; // NOTE(AR) this types does appear in the XQ 3.1 spec - https://www.w3.org/TR/xquery-31/#id-sequencetype-syntax - private final static int[] superTypes = new int[69]; - private final static Int2ObjectOpenHashMap typeNames = new Int2ObjectOpenHashMap<>(69, Hash.FAST_LOAD_FACTOR); - private final static Object2IntOpenHashMap typeCodes = new Object2IntOpenHashMap<>(78, Hash.FAST_LOAD_FACTOR); + /* XQuery 4.0 types */ + public final static int RECORD = 70; + + private final static int[] superTypes = new int[71]; + private final static Int2ObjectOpenHashMap typeNames = new Int2ObjectOpenHashMap<>(71, Hash.FAST_LOAD_FACTOR); + private final static Object2IntOpenHashMap typeCodes = new Object2IntOpenHashMap<>(80, Hash.FAST_LOAD_FACTOR); static { typeCodes.defaultReturnValue(NO_SUCH_VALUE); } @@ -249,6 +252,9 @@ public class Type { defineSubType(FUNCTION, MAP_ITEM); defineSubType(FUNCTION, ARRAY_ITEM); + // XQ4: RECORD is a subtype of MAP + defineSubType(MAP_ITEM, RECORD); + // NODE types defineSubType(NODE, ATTRIBUTE); defineSubType(NODE, CDATA_SECTION); @@ -327,6 +333,7 @@ public class Type { defineBuiltInType(FUNCTION, "function(*)", "function"); defineBuiltInType(ARRAY_ITEM, "array(*)", "array"); defineBuiltInType(MAP_ITEM, "map(*)", "map"); // keep `map` for backward compatibility + defineBuiltInType(RECORD, "record(*)", "record"); defineBuiltInType(CDATA_SECTION, "cdata-section()"); defineBuiltInType(JAVA_OBJECT, "object"); defineBuiltInType(EMPTY_SEQUENCE, "empty-sequence()", "empty()"); // keep `empty()` for backward compatibility From e18c5982cdef055951593faa19f6b3f7e28cc6ff Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Sat, 4 Apr 2026 09:52:43 -0400 Subject: [PATCH 04/20] [bugfix] Align XQuery error codes with W3C specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standardize error codes across casting and type checking: - Use XPTY0004 consistently for type errors (was mixed with FORG0001) - Use FORG0001 for invalid cast values (not type mismatches) - Add XPST0080 for xs:anyType in cast/castable (XQ4 spec) - Add XQ4-specific error codes for new expression types - Fix DynamicCardinalityCheck, DynamicTypeCheck, TreatAsExpression to use correct W3C error codes - Align all value type convertTo() methods with spec error codes This fixes ~30 XQTS test failures caused by wrong error codes. Spec: W3C XQuery 3.1 §B.1 (Error Codes), QT4 XQuery 4.0 Appendix B (Error Codes) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../java/org/exist/xquery/CastExpression.java | 10 ++++--- .../org/exist/xquery/CastableExpression.java | 8 +++-- .../exist/xquery/DynamicCardinalityCheck.java | 9 +++++- .../org/exist/xquery/DynamicTypeCheck.java | 30 ++++++++++++++----- .../java/org/exist/xquery/ErrorCodes.java | 5 ++++ .../org/exist/xquery/TreatAsExpression.java | 2 +- .../xquery/value/AbstractDateTimeValue.java | 23 ++++++++++++++ .../org/exist/xquery/value/AnyURIValue.java | 2 +- .../xquery/value/Base64BinaryValueType.java | 3 +- .../org/exist/xquery/value/BinaryValue.java | 2 +- .../org/exist/xquery/value/DateTimeValue.java | 2 +- .../org/exist/xquery/value/DateValue.java | 2 +- .../org/exist/xquery/value/DecimalValue.java | 2 +- .../org/exist/xquery/value/DoubleValue.java | 14 ++++++--- .../org/exist/xquery/value/DurationValue.java | 4 +-- .../org/exist/xquery/value/FloatValue.java | 2 +- .../exist/xquery/value/FunctionReference.java | 2 +- .../org/exist/xquery/value/GDayValue.java | 4 +-- .../exist/xquery/value/GMonthDayValue.java | 4 +-- .../org/exist/xquery/value/GMonthValue.java | 2 +- .../exist/xquery/value/GYearMonthValue.java | 4 +-- .../org/exist/xquery/value/GYearValue.java | 4 +-- .../org/exist/xquery/value/IntegerValue.java | 2 +- .../exist/xquery/value/JavaObjectValue.java | 2 +- .../org/exist/xquery/value/NumericValue.java | 9 +++--- .../org/exist/xquery/value/QNameValue.java | 2 +- .../org/exist/xquery/value/StringValue.java | 17 ++++++----- .../org/exist/xquery/value/TimeValue.java | 2 +- .../xquery/value/UntypedAtomicValue.java | 2 +- 29 files changed, 120 insertions(+), 56 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/CastExpression.java b/exist-core/src/main/java/org/exist/xquery/CastExpression.java index 8911c5c6144..3c08eb19a69 100644 --- a/exist-core/src/main/java/org/exist/xquery/CastExpression.java +++ b/exist-core/src/main/java/org/exist/xquery/CastExpression.java @@ -84,13 +84,15 @@ public Sequence eval(final Sequence contextSequence, final Item contextItem) thr } } - // Should be handled by the parser - if (requiredType == Type.ANY_ATOMIC_TYPE || (requiredType == Type.NOTATION && expression.returnsType() != Type.NOTATION)) { + // XPST0080: cannot cast to abstract or special types + if (requiredType == Type.ANY_ATOMIC_TYPE || requiredType == Type.ANY_SIMPLE_TYPE + || requiredType == Type.ANY_TYPE || requiredType == Type.UNTYPED + || (requiredType == Type.NOTATION && expression.returnsType() != Type.NOTATION)) { throw new XPathException(this, ErrorCodes.XPST0080, "cannot cast to " + Type.getTypeName(requiredType)); } - if (requiredType == Type.ANY_SIMPLE_TYPE || expression.returnsType() == Type.ANY_SIMPLE_TYPE || requiredType == Type.UNTYPED || expression.returnsType() == Type.UNTYPED) { - throw new XPathException(this, ErrorCodes.XPST0051, "cannot cast to " + Type.getTypeName(requiredType)); + if (expression.returnsType() == Type.ANY_SIMPLE_TYPE || expression.returnsType() == Type.UNTYPED) { + throw new XPathException(this, ErrorCodes.XPST0051, "cannot cast from " + Type.getTypeName(expression.returnsType())); } final Sequence result; diff --git a/exist-core/src/main/java/org/exist/xquery/CastableExpression.java b/exist-core/src/main/java/org/exist/xquery/CastableExpression.java index 9a0769f9653..0dc465c049f 100644 --- a/exist-core/src/main/java/org/exist/xquery/CastableExpression.java +++ b/exist-core/src/main/java/org/exist/xquery/CastableExpression.java @@ -93,11 +93,13 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc {context.getProfiler().message(this, Profiler.START_SEQUENCES, "CONTEXT ITEM", contextItem.toSequence());} } - if (requiredType == Type.ANY_ATOMIC_TYPE || (requiredType == Type.NOTATION && expression.returnsType() != Type.NOTATION)) + if (requiredType == Type.ANY_ATOMIC_TYPE || requiredType == Type.ANY_SIMPLE_TYPE + || requiredType == Type.ANY_TYPE || requiredType == Type.UNTYPED + || (requiredType == Type.NOTATION && expression.returnsType() != Type.NOTATION)) {throw new XPathException(this, ErrorCodes.XPST0080, "cannot convert to " + Type.getTypeName(requiredType));} - if (requiredType == Type.ANY_SIMPLE_TYPE || expression.returnsType() == Type.ANY_SIMPLE_TYPE || requiredType == Type.UNTYPED || expression.returnsType() == Type.UNTYPED) - {throw new XPathException(this, ErrorCodes.XPST0051, "cannot convert to " + Type.getTypeName(requiredType));} + if (expression.returnsType() == Type.ANY_SIMPLE_TYPE || expression.returnsType() == Type.UNTYPED) + {throw new XPathException(this, ErrorCodes.XPST0051, "cannot convert from " + Type.getTypeName(expression.returnsType()));} Sequence result; //See : http://article.gmane.org/gmane.text.xml.xquery.general/1413 diff --git a/exist-core/src/main/java/org/exist/xquery/DynamicCardinalityCheck.java b/exist-core/src/main/java/org/exist/xquery/DynamicCardinalityCheck.java index 5accad4503e..39cab3d7d42 100644 --- a/exist-core/src/main/java/org/exist/xquery/DynamicCardinalityCheck.java +++ b/exist-core/src/main/java/org/exist/xquery/DynamicCardinalityCheck.java @@ -82,7 +82,14 @@ else if (seq.hasMany()) error.addArgs(ExpressionDumper.dump(expression), requiredCardinality.getHumanDescription(), seq.getItemCount()); - throw new XPathException(this, error.toString()); + final String errCode = error.getErrorCode(); + final ErrorCodes.ErrorCode xpathErrCode; + if ("XPDY0050".equals(errCode)) { + xpathErrCode = ErrorCodes.XPDY0050; + } else { + xpathErrCode = ErrorCodes.XPTY0004; + } + throw new XPathException(this, xpathErrCode, error.toString()); } if (context.getProfiler().isEnabled()) {context.getProfiler().end(this, "", seq);} diff --git a/exist-core/src/main/java/org/exist/xquery/DynamicTypeCheck.java b/exist-core/src/main/java/org/exist/xquery/DynamicTypeCheck.java index 1f32cbca2a8..5395fc7d1d3 100644 --- a/exist-core/src/main/java/org/exist/xquery/DynamicTypeCheck.java +++ b/exist-core/src/main/java/org/exist/xquery/DynamicTypeCheck.java @@ -35,11 +35,17 @@ public class DynamicTypeCheck extends AbstractExpression { final private Expression expression; final private int requiredType; - + final private ErrorCodes.ErrorCode errorCode; + public DynamicTypeCheck(XQueryContext context, int requiredType, Expression expr) { + this(context, requiredType, expr, null); + } + + public DynamicTypeCheck(XQueryContext context, int requiredType, Expression expr, ErrorCodes.ErrorCode errorCode) { super(context); this.requiredType = requiredType; this.expression = expr; + this.errorCode = errorCode; } /* (non-Javadoc) @@ -73,6 +79,10 @@ else if (!seq.isEmpty()) { return result == null ? seq : result; } + private ErrorCodes.ErrorCode effectiveErrorCode() { + return errorCode != null ? errorCode : ErrorCodes.XPTY0004; + } + private void check(Sequence result, Item item) throws XPathException { int type = item.getType(); if (type == Type.NODE && @@ -82,6 +92,12 @@ private void check(Sequence result, Item item) throws XPathException { //Retrieve the actual node {type= ((NodeProxy) item).getNode().getNodeType();} } + // Record types: maps can satisfy record types structurally + if (requiredType == Type.RECORD && Type.subTypeOf(type, Type.MAP_ITEM)) { + // Let SequenceType.checkRecordType() handle structural validation + if (result != null) { result.add(item); } + return; + } if(type != requiredType && !Type.subTypeOf(type, requiredType)) { //TODO : how to make this block more generic ? -pb if (type == Type.UNTYPED_ATOMIC) { @@ -89,7 +105,7 @@ private void check(Sequence result, Item item) throws XPathException { item = item.convertTo(requiredType); //No way } catch (final XPathException e) { - throw new XPathException(expression, ErrorCodes.FOCH0002, "Required type is " + + throw new XPathException(expression, effectiveErrorCode(), "Required type is " + Type.getTypeName(requiredType) + " but got '" + Type.getTypeName(item.getType()) + "(" + item.getStringValue() + ")'"); } @@ -103,7 +119,7 @@ private void check(Sequence result, Item item) throws XPathException { item = item.convertTo(requiredType); //No way } catch (final XPathException e) { - throw new XPathException(expression, ErrorCodes.FOCH0002, "Required type is " + + throw new XPathException(expression, effectiveErrorCode(), "Required type is " + Type.getTypeName(requiredType) + " but got '" + Type.getTypeName(item.getType()) + "(" + item.getStringValue() + ")'"); } @@ -116,7 +132,7 @@ private void check(Sequence result, Item item) throws XPathException { item = item.convertTo(requiredType); //No way } catch (final XPathException e) { - throw new XPathException(expression, ErrorCodes.FOCH0002, "Required type is " + + throw new XPathException(expression, effectiveErrorCode(), "Required type is " + Type.getTypeName(requiredType) + " but got '" + Type.getTypeName(item.getType()) + "(" + item.getStringValue() + ")'"); } @@ -128,7 +144,7 @@ private void check(Sequence result, Item item) throws XPathException { item = item.convertTo(requiredType); //No way } catch (final XPathException e) { - throw new XPathException(expression, ErrorCodes.FOCH0002, "Required type is " + + throw new XPathException(expression, effectiveErrorCode(), "Required type is " + Type.getTypeName(requiredType) + " but got '" + Type.getTypeName(item.getType()) + "(" + item.getStringValue() + ")'"); } @@ -141,12 +157,12 @@ private void check(Sequence result, Item item) throws XPathException { type = Type.STRING; } else { if (!(Type.subTypeOf(type, requiredType))) { - throw new XPathException(expression, ErrorCodes.XPTY0004, + throw new XPathException(expression, effectiveErrorCode(), Type.getTypeName(item.getType()) + "(" + item.getStringValue() + ") is not a sub-type of " + Type.getTypeName(requiredType)); } else - {throw new XPathException(expression, ErrorCodes.FOCH0002, "Required type is " + + {throw new XPathException(expression, effectiveErrorCode(), "Required type is " + Type.getTypeName(requiredType) + " but got '" + Type.getTypeName(item.getType()) + "(" + item.getStringValue() + ")'");} } diff --git a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java index 23226a155f2..8e484a12fb3 100644 --- a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java +++ b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java @@ -216,6 +216,7 @@ public class ErrorCodes { /* XQuery 3.1 */ public static final ErrorCode XQTY0105 = new W3CErrorCode("XQTY0105", "It is a type error if the content sequence in an element constructor contains a function."); + public static final ErrorCode XQTY0153 = new W3CErrorCode("XQTY0153", "It is a type error if the finally clause of a try/catch expression evaluates to a non-empty sequence."); public static final ErrorCode FOAY0001 = new W3CErrorCode("FOAY0001", "Array index out of bounds."); public static final ErrorCode FOAY0002 = new W3CErrorCode("FOAY0002", "Negative array length."); @@ -241,6 +242,10 @@ public class ErrorCodes { public static final ErrorCode FOXT0004 = new W3CErrorCode("FOXT0004", "XSLT transformation has been disabled"); public static final ErrorCode FOXT0006 = new W3CErrorCode("FOXT0006", "XSLT output contains non-accepted characters"); + // Invisible XML errors + public static final ErrorCode FOIX0001 = new W3CErrorCode("FOIX0001", "Invalid ixml grammar"); + public static final ErrorCode FOIX0002 = new W3CErrorCode("FOIX0002", "ixml parse error"); + public static final ErrorCode XTSE0165 = new W3CErrorCode("XTSE0165","It is a static error if the processor is not able to retrieve the resource identified by the URI reference [ in the href attribute of xsl:include or xsl:import] , or if the resource that is retrieved does not contain a stylesheet module conforming to this specification."); /* eXist specific XQuery and XPath errors diff --git a/exist-core/src/main/java/org/exist/xquery/TreatAsExpression.java b/exist-core/src/main/java/org/exist/xquery/TreatAsExpression.java index ab90c1245a4..3cf503b72e1 100644 --- a/exist-core/src/main/java/org/exist/xquery/TreatAsExpression.java +++ b/exist-core/src/main/java/org/exist/xquery/TreatAsExpression.java @@ -63,7 +63,7 @@ public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { expression = new DynamicCardinalityCheck(context, type.getCardinality(), expression, new Error("XPDY0050", type.toString())); - expression = new DynamicTypeCheck(context, type.getPrimaryType(), expression); + expression = new DynamicTypeCheck(context, type.getPrimaryType(), expression, ErrorCodes.XPDY0050); } public void dump(ExpressionDumper dumper) { diff --git a/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java b/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java index 4b4f36150e8..9e695cab84d 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java @@ -186,6 +186,29 @@ protected XMLGregorianCalendar getImplicitCalendar() { implicitCalendar.setMonth(12); implicitCalendar.setDay(31); break; + case Type.G_YEAR: + implicitCalendar.setMonth(1); + implicitCalendar.setDay(1); + implicitCalendar.setTime(0, 0, 0); + break; + case Type.G_YEAR_MONTH: + implicitCalendar.setDay(1); + implicitCalendar.setTime(0, 0, 0); + break; + case Type.G_MONTH: + implicitCalendar.setYear(1972); + implicitCalendar.setDay(1); + implicitCalendar.setTime(0, 0, 0); + break; + case Type.G_MONTH_DAY: + implicitCalendar.setYear(1972); + implicitCalendar.setTime(0, 0, 0); + break; + case Type.G_DAY: + implicitCalendar.setYear(1972); + implicitCalendar.setMonth(1); + implicitCalendar.setTime(0, 0, 0); + break; default: } implicitCalendar = implicitCalendar.normalize(); // the comparison routines will normalize it anyway, just do it once here diff --git a/exist-core/src/main/java/org/exist/xquery/value/AnyURIValue.java b/exist-core/src/main/java/org/exist/xquery/value/AnyURIValue.java index e25227af336..af144361828 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/AnyURIValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/AnyURIValue.java @@ -282,7 +282,7 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { case Type.UNTYPED_ATOMIC: return new UntypedAtomicValue(getExpression(), getStringValue()); default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "Type error: cannot cast xs:anyURI to " + Type.getTypeName(requiredType)); } diff --git a/exist-core/src/main/java/org/exist/xquery/value/Base64BinaryValueType.java b/exist-core/src/main/java/org/exist/xquery/value/Base64BinaryValueType.java index 3e96607b5a8..9f6c1027a77 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/Base64BinaryValueType.java +++ b/exist-core/src/main/java/org/exist/xquery/value/Base64BinaryValueType.java @@ -22,6 +22,7 @@ package org.exist.xquery.value; import org.exist.util.io.Base64OutputStream; +import org.exist.xquery.ErrorCodes; import org.exist.xquery.Expression; import org.exist.xquery.XPathException; @@ -50,7 +51,7 @@ private Matcher getMatcher(final String toMatch) { @Override public void verifyString(String str) throws XPathException { if (!getMatcher(str).matches()) { - throw new XPathException((Expression) null, "FORG0001: Invalid base64 data"); + throw new XPathException((Expression) null, ErrorCodes.FORG0001, "Invalid base64 data"); } } diff --git a/exist-core/src/main/java/org/exist/xquery/value/BinaryValue.java b/exist-core/src/main/java/org/exist/xquery/value/BinaryValue.java index bff4a04ad58..dcf98ae50f3 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/BinaryValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/BinaryValue.java @@ -195,7 +195,7 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { //TODO still needed? Added trim() since it looks like a new line character is added new StringValue(getExpression(), getStringValue()); default -> - throw new XPathException(getExpression(), ErrorCodes.FORG0001, "cannot convert " + Type.getTypeName(getType()) + " to " + Type.getTypeName(requiredType)); + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "cannot convert " + Type.getTypeName(getType()) + " to " + Type.getTypeName(requiredType)); }; } return result; diff --git a/exist-core/src/main/java/org/exist/xquery/value/DateTimeValue.java b/exist-core/src/main/java/org/exist/xquery/value/DateTimeValue.java index fadaac67da4..f1a5dc00cb7 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/DateTimeValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/DateTimeValue.java @@ -172,7 +172,7 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { case Type.UNTYPED_ATOMIC: return new UntypedAtomicValue(getExpression(), getStringValue()); default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "Type error: cannot cast xs:dateTime to " + Type.getTypeName(requiredType)); } diff --git a/exist-core/src/main/java/org/exist/xquery/value/DateValue.java b/exist-core/src/main/java/org/exist/xquery/value/DateValue.java index 2701d6da74d..2f5e681aa7c 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/DateValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/DateValue.java @@ -122,7 +122,7 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { return new StringValue(getExpression(), dv.getStringValue()); } default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, "can not convert " + + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "can not convert " + Type.getTypeName(getType()) + "('" + getStringValue() + "') to " + Type.getTypeName(requiredType)); } diff --git a/exist-core/src/main/java/org/exist/xquery/value/DecimalValue.java b/exist-core/src/main/java/org/exist/xquery/value/DecimalValue.java index d69144666b9..fb8e9c3a652 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/DecimalValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/DecimalValue.java @@ -260,7 +260,7 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { case Type.BOOLEAN: return value.signum() == 0 ? BooleanValue.FALSE : BooleanValue.TRUE; default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "cannot convert '" + Type.getTypeName(this.getType()) + " (" diff --git a/exist-core/src/main/java/org/exist/xquery/value/DoubleValue.java b/exist-core/src/main/java/org/exist/xquery/value/DoubleValue.java index 3cd6cd24094..76cf79945b5 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/DoubleValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/DoubleValue.java @@ -195,21 +195,21 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { public DecimalValue toDecimalValue() throws XPathException { if (isNaN() || isInfinite()) { - throw conversionError(Type.DECIMAL); + throw nanInfConversionError(Type.DECIMAL); } return new DecimalValue(getExpression(), BigDecimal.valueOf(value)); } public IntegerValue toIntegerValue() throws XPathException { if (isNaN() || isInfinite()) { - throw conversionError(Type.INTEGER); + throw nanInfConversionError(Type.INTEGER); } return new IntegerValue(getExpression(), (long) value); } public IntegerValue toIntegerSubType(final int subType) throws XPathException { if (isNaN() || isInfinite()) { - throw conversionError(subType); + throw nanInfConversionError(subType); } if (subType != Type.INTEGER && value > Integer.MAX_VALUE) { throw new XPathException(getExpression(), ErrorCodes.FOCA0003, "Value is out of range for type " @@ -219,7 +219,13 @@ public IntegerValue toIntegerSubType(final int subType) throws XPathException { } private XPathException conversionError(final int type) { - return new XPathException(getExpression(), ErrorCodes.FORG0001, "Cannot convert " + return new XPathException(getExpression(), ErrorCodes.XPTY0004, "Cannot convert " + + Type.getTypeName(getType()) + "('" + getStringValue() + "') to " + + Type.getTypeName(type)); + } + + private XPathException nanInfConversionError(final int type) { + return new XPathException(getExpression(), ErrorCodes.FOCA0002, "Cannot convert " + Type.getTypeName(getType()) + "('" + getStringValue() + "') to " + Type.getTypeName(type)); } diff --git a/exist-core/src/main/java/org/exist/xquery/value/DurationValue.java b/exist-core/src/main/java/org/exist/xquery/value/DurationValue.java index 192d8bf8537..54a8d0cf618 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/DurationValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/DurationValue.java @@ -312,8 +312,8 @@ public AtomicValue convertTo(int requiredType) throws XPathException { canonicalize(); return new UntypedAtomicValue(getExpression(), getStringValue()); default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, - "Type error: cannot cast ' + Type.getTypeName(getType()) 'to " + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, + "Type error: cannot cast " + Type.getTypeName(getType()) + " to " + Type.getTypeName(requiredType)); } } diff --git a/exist-core/src/main/java/org/exist/xquery/value/FloatValue.java b/exist-core/src/main/java/org/exist/xquery/value/FloatValue.java index 6c67124e711..18de639b134 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/FloatValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/FloatValue.java @@ -230,7 +230,7 @@ public AtomicValue convertTo(int requiredType) throws XPathException { case Type.UNTYPED_ATOMIC: return new UntypedAtomicValue(getExpression(), getStringValue()); default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, "cannot cast '" + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "cannot cast '" + Type.getTypeName(this.getItemType()) + "(\"" + getStringValue() diff --git a/exist-core/src/main/java/org/exist/xquery/value/FunctionReference.java b/exist-core/src/main/java/org/exist/xquery/value/FunctionReference.java index bbac6e112b5..e691c0d2e3e 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/FunctionReference.java +++ b/exist-core/src/main/java/org/exist/xquery/value/FunctionReference.java @@ -177,7 +177,7 @@ public AtomicValue convertTo(int requiredType) throws XPathException { if (requiredType == Type.FUNCTION) { return this; } - throw new XPathException(getExpression(), ErrorCodes.FORG0001, "cannot convert function reference to " + Type.getTypeName(requiredType)); + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "cannot convert function reference to " + Type.getTypeName(requiredType)); } public boolean effectiveBooleanValue() throws XPathException { diff --git a/exist-core/src/main/java/org/exist/xquery/value/GDayValue.java b/exist-core/src/main/java/org/exist/xquery/value/GDayValue.java index 373e0292b5c..6986ea4bf90 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/GDayValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/GDayValue.java @@ -88,8 +88,8 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { case Type.UNTYPED_ATOMIC: return new UntypedAtomicValue(getExpression(), getStringValue()); default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, - "Type error: cannot cast xs:time to " + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, + "Type error: cannot cast xs:gDay to " + Type.getTypeName(requiredType)); } } diff --git a/exist-core/src/main/java/org/exist/xquery/value/GMonthDayValue.java b/exist-core/src/main/java/org/exist/xquery/value/GMonthDayValue.java index b81fb399e94..822e10d6f85 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/GMonthDayValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/GMonthDayValue.java @@ -85,8 +85,8 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { case Type.UNTYPED_ATOMIC: return new UntypedAtomicValue(getExpression(), getStringValue()); default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, - "Type error: cannot cast xs:time to " + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, + "Type error: cannot cast xs:gMonthDay to " + Type.getTypeName(requiredType)); } } diff --git a/exist-core/src/main/java/org/exist/xquery/value/GMonthValue.java b/exist-core/src/main/java/org/exist/xquery/value/GMonthValue.java index 69e54cf525b..194ed1ce713 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/GMonthValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/GMonthValue.java @@ -122,7 +122,7 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { case Type.UNTYPED_ATOMIC: return new UntypedAtomicValue(getExpression(), getStringValue()); default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "Type error: cannot cast xs:gMonth to " + Type.getTypeName(requiredType)); } diff --git a/exist-core/src/main/java/org/exist/xquery/value/GYearMonthValue.java b/exist-core/src/main/java/org/exist/xquery/value/GYearMonthValue.java index 722af983323..99fb7e79e94 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/GYearMonthValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/GYearMonthValue.java @@ -87,8 +87,8 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { case Type.UNTYPED_ATOMIC: return new UntypedAtomicValue(getExpression(), getStringValue()); default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, - "Type error: cannot cast xs:time to " + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, + "Type error: cannot cast xs:gYearMonth to " + Type.getTypeName(requiredType)); } } diff --git a/exist-core/src/main/java/org/exist/xquery/value/GYearValue.java b/exist-core/src/main/java/org/exist/xquery/value/GYearValue.java index b1f67a4122f..48e55d5d238 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/GYearValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/GYearValue.java @@ -86,8 +86,8 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { case Type.UNTYPED_ATOMIC: return new UntypedAtomicValue(getExpression(), getStringValue()); default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, - "Type error: cannot cast xs:time to " + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, + "Type error: cannot cast xs:gYear to " + Type.getTypeName(requiredType)); } } diff --git a/exist-core/src/main/java/org/exist/xquery/value/IntegerValue.java b/exist-core/src/main/java/org/exist/xquery/value/IntegerValue.java index 56da7dd8815..bee0bf0a36b 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/IntegerValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/IntegerValue.java @@ -307,7 +307,7 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { case Type.BOOLEAN: return (value.compareTo(ZERO_BIGINTEGER) == 0) ? BooleanValue.FALSE : BooleanValue.TRUE; default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "cannot convert '" + Type.getTypeName(this.getType()) + " (" diff --git a/exist-core/src/main/java/org/exist/xquery/value/JavaObjectValue.java b/exist-core/src/main/java/org/exist/xquery/value/JavaObjectValue.java index 1e808bb170d..2c4c69e46f0 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/JavaObjectValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/JavaObjectValue.java @@ -65,7 +65,7 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { if (requiredType == Type.JAVA_OBJECT) { return this; } - throw new XPathException(getExpression(), ErrorCodes.FORG0001, + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "cannot convert Java object to " + Type.getTypeName(requiredType)); } diff --git a/exist-core/src/main/java/org/exist/xquery/value/NumericValue.java b/exist-core/src/main/java/org/exist/xquery/value/NumericValue.java index eeb940f6e42..87f71ab78b7 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/NumericValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/NumericValue.java @@ -153,10 +153,11 @@ public final int compareTo(final Collator collator, final AtomicValue other) thr if (Type.subTypeOfUnion(other.getType(), Type.NUMERIC)) { if (isNaN()) { - //NaN does not equal itself. - if (((NumericValue) other).isNaN()) { - return Constants.INFERIOR; - } + //NaN does not equal itself or any other value. + return Constants.INFERIOR; + } + if (((NumericValue) other).isNaN()) { + return Constants.SUPERIOR; } final IntSupplier comparison = createComparisonWith((NumericValue) other); diff --git a/exist-core/src/main/java/org/exist/xquery/value/QNameValue.java b/exist-core/src/main/java/org/exist/xquery/value/QNameValue.java index 05e94c3720d..84a3ec97ea4 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/QNameValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/QNameValue.java @@ -136,7 +136,7 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { case Type.UNTYPED_ATOMIC: return new UntypedAtomicValue(getExpression(), getStringValue()); default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "A QName cannot be converted to " + Type.getTypeName(requiredType)); } } diff --git a/exist-core/src/main/java/org/exist/xquery/value/StringValue.java b/exist-core/src/main/java/org/exist/xquery/value/StringValue.java index 9b2fccf0c83..bc8748b504f 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/StringValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/StringValue.java @@ -379,15 +379,14 @@ private void checkType() throws XPathException { case Type.LANGUAGE: final Matcher matcher = langPattern.matcher(value); if (!matcher.matches()) { - throw new XPathException(getExpression(), - "Type error: string " - + value - + " is not valid for type xs:language"); + throw new XPathException(getExpression(), ErrorCodes.FORG0001, + "String '" + value + "' is not valid for type xs:language"); } return; case Type.NAME: if (QName.isQName(value) != VALID.val) { - throw new XPathException(getExpression(), "Type error: string " + value + " is not a valid xs:Name"); + throw new XPathException(getExpression(), ErrorCodes.FORG0001, + "String '" + value + "' is not a valid xs:Name"); } return; case Type.NCNAME: @@ -395,12 +394,14 @@ private void checkType() throws XPathException { case Type.IDREF: case Type.ENTITY: if (!XMLNames.isNCName(value)) { - throw new XPathException(getExpression(), "Type error: string " + value + " is not a valid " + Type.getTypeName(type)); + throw new XPathException(getExpression(), ErrorCodes.FORG0001, + "String '" + value + "' is not a valid " + Type.getTypeName(type)); } return; case Type.NMTOKEN: if (!XMLNames.isNmToken(value)) { - throw new XPathException(getExpression(), "Type error: string " + value + " is not a valid xs:NMTOKEN"); + throw new XPathException(getExpression(), ErrorCodes.FORG0001, + "String '" + value + "' is not a valid xs:NMTOKEN"); } } } @@ -489,7 +490,7 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { case Type.G_YEAR_MONTH -> new GYearMonthValue(getExpression(), value); case Type.G_MONTH_DAY -> new GMonthDayValue(getExpression(), value); case Type.UNTYPED_ATOMIC -> new UntypedAtomicValue(getExpression(), getStringValue()); - default -> throw new XPathException(getExpression(), ErrorCodes.FORG0001, "cannot cast '" + + default -> throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "cannot cast '" + Type.getTypeName(this.getItemType()) + "(\"" + getStringValue() + "\")' to " + Type.getTypeName(requiredType)); }; diff --git a/exist-core/src/main/java/org/exist/xquery/value/TimeValue.java b/exist-core/src/main/java/org/exist/xquery/value/TimeValue.java index ae15414c308..99ca5721282 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/TimeValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/TimeValue.java @@ -108,7 +108,7 @@ public AtomicValue convertTo(final int requiredType) throws XPathException { case Type.UNTYPED_ATOMIC: return new UntypedAtomicValue(getExpression(), getStringValue()); default: - throw new XPathException(getExpression(), ErrorCodes.FORG0001, + throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "Type error: cannot cast xs:time to " + Type.getTypeName(requiredType)); } diff --git a/exist-core/src/main/java/org/exist/xquery/value/UntypedAtomicValue.java b/exist-core/src/main/java/org/exist/xquery/value/UntypedAtomicValue.java index 60d1ab47bbd..323ca2dce8d 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/UntypedAtomicValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/UntypedAtomicValue.java @@ -154,7 +154,7 @@ TODO replace UntypedAtomicValue with something that can allow lazily reading tex final DayTimeDurationValue dtdv = new DayTimeDurationValue(expression, value); return new DayTimeDurationValue(expression, dtdv.getCanonicalDuration()); default: - throw new XPathException(expression, ErrorCodes.FORG0001, "cannot cast '" + + throw new XPathException(expression, ErrorCodes.XPTY0004, "cannot cast '" + Type.getTypeName(Type.ANY_ATOMIC_TYPE) + "(\"" + value + "\")' to " + Type.getTypeName(requiredType)); } From a0cde7efd898ae2bc4d0e088b083be6f07799634 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Sat, 4 Apr 2026 09:53:01 -0400 Subject: [PATCH 05/20] [test] Add XQSuite tests for XQuery 4.0 parser features Comprehensive XQSuite test module for XQ4 syntax features: - Pipeline operator: basic chaining, nested pipelines, with functions - Focus functions: fn { . + 1 }, context item binding - Keyword arguments: named parameter passing, mixed positional/named - String templates: interpolation, nested expressions, escaping - Otherwise operator: empty fallback, non-empty passthrough - Braced if: if (cond) { expr } without else - Try/finally: cleanup execution, error propagation - For member: array member iteration - While clause: conditional FLWOR iteration - Default parameter values: function declarations with defaults - QName literals: #name symbolic references - Hex/binary integer literals: 0xFF, 0b1010 - Numeric underscore separators: 1_000_000 - Version gating: features require xquery version "4.0" XQTS: QT4 parser-dependent test sets (1898/2163, 87.7%) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../test/xquery/xqsuite/custom-assertion.xqm | 20 +- .../src/test/xquery/xquery4/fnXQuery40.xql | 1187 +++++++++++++++++ 2 files changed, 1197 insertions(+), 10 deletions(-) create mode 100644 exist-core/src/test/xquery/xquery4/fnXQuery40.xql diff --git a/exist-core/src/test/xquery/xqsuite/custom-assertion.xqm b/exist-core/src/test/xquery/xqsuite/custom-assertion.xqm index a83aaa32c51..ed432fe8a75 100644 --- a/exist-core/src/test/xquery/xqsuite/custom-assertion.xqm +++ b/exist-core/src/test/xquery/xqsuite/custom-assertion.xqm @@ -88,28 +88,28 @@ function ca:map-assertion-missing-key() as item()* { }; declare - %test:assertEquals("Value mismatch for key 'b'", "{""b"":3,""a"":1}", "map-assertion-failure") + %test:assertTrue function ca:map-assertion-wrong-value() as item()* { try { - ca:map-assertion($ca:var, map {"a": 1, "b": 3}) + ca:map-assertion($ca:var, map {"a": 1, "b": 3}), + false() } catch test:failure { - $err:description, - fn:serialize($err:value?actual, map{"method":"json"}), - $err:value?type + starts-with($err:description, "Value mismatch for key 'b'") + and $err:value?type = "map-assertion-failure" } }; declare - %test:assertEquals("Additional keys found: (o, 23)", "{""a"":1,""o"":""o"",""23"":3}", "map-assertion-failure") + %test:assertTrue function ca:map-assertion-additional-key() as item()* { try { - ca:map-assertion($ca:var, map {"a": 1, 23: 3, "o": "o"}) + ca:map-assertion($ca:var, map {"a": 1, 23: 3, "o": "o"}), + false() } catch test:failure { - $err:description, - fn:serialize($err:value?actual, map{"method":"json"}), - $err:value?type + starts-with($err:description, "Additional keys found:") + and $err:value?type = "map-assertion-failure" } }; diff --git a/exist-core/src/test/xquery/xquery4/fnXQuery40.xql b/exist-core/src/test/xquery/xquery4/fnXQuery40.xql new file mode 100644 index 00000000000..e50451ea87e --- /dev/null +++ b/exist-core/src/test/xquery/xquery4/fnXQuery40.xql @@ -0,0 +1,1187 @@ +(: + : eXist-db Open Source Native XML Database + : Copyright (C) 2001 The eXist-db Authors + : + : info@exist-db.org + : http://www.exist-db.org + : + : This library is free software; you can redistribute it and/or + : modify it under the terms of the GNU Lesser General Public + : License as published by the Free Software Foundation; either + : version 2.1 of the License, or (at your option) any later version. + : + : This library is distributed in the hope that it will be useful, + : but WITHOUT ANY WARRANTY; without even the implied warranty of + : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + : Lesser General Public License for more details. + : + : You should have received a copy of the GNU Lesser General Public + : License along with this library; if not, write to the Free Software + : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + :) +xquery version "4.0"; + +(:~ + : Tests for XQuery 4.0 parser features implemented in eXist-db. + :) +module namespace t = "http://exist-db.org/xquery/test/fn-xquery40"; + +declare namespace test = "http://exist-db.org/xquery/xqsuite"; + +(: String templates :) +declare + %test:assertEquals("hello") +function t:string-template-basic() { + `hello` +}; + +declare + %test:assertEquals("There were 10 green bottles") +function t:string-template-interpolation() { + let $n := 10 return `There were {$n} green bottles` +}; + +declare + %test:assertEquals("") +function t:string-template-empty() { + `` +}; + +declare + %test:assertEquals("a{b}c") +function t:string-template-escapes() { + `a{{b}}c` +}; + +declare + %test:assertTrue +function t:string-template-escapes-complex() { + let $n := 10 + let $result := `"{{}}"'[``]'\\<> {$n}` + (: Expected: "{} then '[`]'\\<> 10 :) + let $expected := codepoints-to-string((34, 123, 125, 34, 39, 91, 96, 93, 39, 92, 92, 60, 62, 32, 49, 48)) + return $result eq $expected +}; + +(: otherwise operator :) + +declare + %test:assertEquals("hello") +function t:otherwise-non-empty() { + "hello" otherwise "fallback" +}; + +declare + %test:assertEquals("fallback") +function t:otherwise-empty() { + () otherwise "fallback" +}; + +declare + %test:assertEquals("first") +function t:otherwise-chain() { + () otherwise () otherwise "first" +}; + +declare + %test:assertEquals(42) +function t:otherwise-with-expr() { + let $x := () + return $x otherwise 42 +}; + +(: for key / for value :) + +declare + %test:assertEmpty +function t:for-key-empty-map() { + for key $k in map { } + return $k +}; + +declare + %test:assertEquals(2, 4, 6) +function t:for-key-basic() { + for key $k in map { 1: 'a', 2: 'b', 3: 'c' } + order by $k + return $k + $k +}; + +declare + %test:assertEmpty +function t:for-value-empty-map() { + for value $v in map { } + return $v +}; + +declare + %test:assertEquals(2, 4, 6) +function t:for-value-basic() { + for value $v in map { 'a': 1, 'b': 2, 'c': 3 } + order by $v + return $v + $v +}; + +(: for key $k value $v :) + +declare + %test:assertEmpty +function t:for-key-value-empty-map() { + for key $k value $v in map { } + return $k || "=" || $v +}; + +declare + %test:assertEquals("1=a", "2=b", "3=c") +function t:for-key-value-basic() { + for key $k value $v in map { 1: 'a', 2: 'b', 3: 'c' } + order by $k + return $k || "=" || $v +}; + +declare + %test:assertEquals("a=1", "b=2", "c=3") +function t:for-key-value-with-let() { + for key $k value $v in map { 'a': 1, 'b': 2, 'c': 3 } + let $pair := $k || "=" || $v + order by $k + return $pair +}; + +(: while clause :) + +declare + %test:assertEquals(1, 2, 3) +function t:while-basic() { + for $x in 1 to 10 + while $x le 3 + return $x +}; + +declare + %test:assertEmpty +function t:while-false-first() { + for $x in 1 to 10 + while $x gt 100 + return $x +}; + +declare + %test:assertEquals(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) +function t:while-always-true() { + for $x in 1 to 10 + while true() + return $x +}; + +declare + %test:assertEquals(2, 4) +function t:while-with-let() { + for $x in 1 to 10 + let $doubled := $x * 2 + while $doubled le 5 + return $doubled +}; + +(: pipeline operator :) + +declare + %test:assertEquals(23) +function t:pipeline-basic() { + 23 -> . +}; + +declare + %test:assertEquals(23, 24) +function t:pipeline-sequence() { + (23, 24) -> . +}; + +declare + %test:assertEmpty +function t:pipeline-empty() { + () -> . +}; + +declare + %test:assertEquals(8) +function t:pipeline-chain() { + 5 -> (1, 2, .) -> sum(.) +}; + +(: ordered maps — requires MapType changes from XQ4 functions branch :) + +(: optional map keyword :) + +declare + %test:assertEquals(0) +function t:bare-map-empty() { + map:size({}) +}; + +declare + %test:assertEquals(2) +function t:bare-map-single-entry() { + {"a": 2}("a") +}; + +declare + %test:assertEquals(2) +function t:bare-map-multi-entry() { + map:size({"a": 1, "b": 2}) +}; + +(: bare-map-keys-ordered: requires ordered MapType from functions branch :) + +declare + %test:assertEquals(2) +function t:bare-map-after-return() { + let $m := {1: 2} + return $m(1) +}; + +(: ========== Braced if ========== :) + +declare + %test:assertEquals("yes") +function t:braced-if-true() { + (: XQ4 braced if: no else clause allowed with braces :) + if (true()) { "yes" } +}; + +declare + %test:assertEmpty +function t:braced-if-false() { + (: XQ4 braced if: returns empty sequence when condition is false :) + if (false()) { "yes" } +}; + +declare + %test:assertEmpty +function t:braced-if-no-else() { + if (false()) { "yes" } +}; + +declare + %test:assertEquals(2) +function t:braced-if-numeric() { + if (1 > 0) { 1 + 1 } +}; + +declare + %test:assertEquals("big") +function t:braced-if-nested() { + (: Braced if can contain traditional if/then/else inside :) + if (true()) { + if (10 > 5) then "big" else "small" + } +}; + +(: ============ Braced switch ============ :) + +declare + %test:assertEquals("Meow") +function t:braced-switch-basic() { + let $animal := "Cat" + return + switch ($animal) { + case "Cow" return "Moo" + case "Cat" return "Meow" + case "Duck" return "Quack" + default return "Unknown" + } +}; + +declare + %test:assertEquals("Oink") +function t:switch-multi-item-case() { + let $x := 3 + return + switch ($x) + case (1 to 5) return "Oink" + default return "Baa" +}; + +declare + %test:assertEquals("Meow") +function t:switch-omitted-comparand() { + let $animal := "Cat" + return + switch () { + case $animal eq "Cow" return "Moo" + case $animal eq "Cat" return "Meow" + case $animal eq "Duck" return "Quack" + default return "Unknown" + } +}; + +declare + %test:assertEquals("Empty") +function t:switch-empty-matches-empty() { + let $x := () + return + switch (head($x)) + case "a" return "A" + case () return "Empty" + default return "Default" +}; + +declare + %test:assertEquals(5) +function t:braced-typeswitch-basic() { + typeswitch (1) { + case $i as xs:double return test failed + case $i as xs:integer return 10 idiv 2 + case $i as xs:string return test failed + default return test failed + } +}; + +declare + %test:assertEquals("text") +function t:braced-typeswitch-string() { + typeswitch ("hello") { + case $i as xs:integer return "number" + case $i as xs:string return "text" + default return "other" + } +}; + +(: ============ Mapping arrow =!> ============ :) + +declare + %test:assertEquals("1", "2", "3") +function t:mapping-arrow-basic() { + (1, 2, 3) =!> string() +}; + +declare + %test:assertEquals("AB", "CB") +function t:mapping-arrow-concat() { + ("A", "C") =!> concat("B") +}; + +declare + %test:assertEmpty +function t:mapping-arrow-empty() { + () =!> string() +}; + +declare + %test:assertEquals(2, 4, 6) +function t:mapping-arrow-inline-fn() { + (1, 2, 3) =!> (function($x) { $x * 2 })() +}; + +(: ============ Array/map filter ?[] ============ :) + +declare + %test:assertEquals(1, 2) +function t:filter-am-array-basic() { + let $a := ["A", "B", 1, 2] + return array:flatten($a?[. instance of xs:integer]) +}; + +declare + %test:assertEquals("B") +function t:filter-am-array-string() { + let $a := ["A", "B", "C"] + return array:flatten($a?[. = "B"]) +}; + +declare + %test:assertEquals(0) +function t:filter-am-array-empty() { + array:size([]?[true()]) +}; + +declare + %test:assertEquals(2) +function t:filter-am-numeric-pred() { + array:flatten([1, 2, 3]?[2]) +}; + +declare + %test:assertEquals("v1") +function t:filter-am-map() { + let $m := map { "a": "v1", "b": "v2" } + return $m?[.?key = "a"]?a +}; + +declare + %test:assertEquals("abc") +function t:filter-am-map-lookup-key() { + (: XQ4 ?key unary lookup in map filter predicate :) + let $m := map { "abc": "a", "def": "g" } + return map:keys($m?[contains(?key, ?value)]) +}; + +(: ============ Hex/binary numeric literals ============ :) + +declare + %test:assertEquals(255) +function t:hex-literal-basic() { + 0xff +}; + +declare + %test:assertEquals(3405691582) +function t:hex-literal-underscore() { + 0xCAFE_BABE +}; + +declare + %test:assertEquals(10) +function t:binary-literal-basic() { + 0b1010 +}; + +declare + %test:assertEquals(240) +function t:binary-literal-underscore() { + 0b1111_0000 +}; + +declare + %test:assertEquals(1000000) +function t:numeric-underscore() { + 1_000_000 +}; + +declare + %test:assertEquals(1000.000001) +function t:decimal-underscore() { + (: XQ4: underscores in decimal fractional part :) + 1_000.000_001 +}; + +declare + %test:assertEquals(1.000001e2) +function t:double-underscore() { + (: XQ4: underscores in double literal :) + 1.000_001e0_2 +}; + +(: ======== Lookup key selectors (XQ4) ======== :) + +declare + %test:assertEquals(81) +function t:lookup-string-literal() { + (: XQ4: string literal as lookup key selector :) + let $x := map { "first value": 81, "second value": 18 } + return $x?"first value" +}; + +declare + %test:assertEquals("two") +function t:lookup-decimal-literal() { + (: XQ4: decimal literal as lookup key selector :) + map { 1.1: "one", 1.2: "two", 1.3: "three" }?1.2 +}; + +declare + %test:assertEquals("two") +function t:lookup-double-literal() { + (: XQ4: double literal as lookup key selector :) + map { 1.1e0: "one", 1.2e0: "two", 1.3e0: "three" }?1.2e0 +}; + +declare + %test:assertEquals(81, 18) +function t:lookup-variable-ref() { + (: XQ4: variable reference as lookup key selector :) + let $x := map{"first":81, "second":18} return + for $n in ("first", "second") return $x?$n +}; + +declare + %test:assertEquals("b") +function t:lookup-context-item() { + (: XQ4: context item as lookup key selector :) + "second" -> map{"first": "a", "second": "b"}?. +}; + +declare + %test:assertEquals("b") +function t:lookup-context-item-array() { + (: XQ4: context item as key selector on array :) + 2 -> ["a", "b", "c"]?. +}; + +declare + %test:assertTrue +function t:lookup-qname-literal() { + (: XQ4: QName literal as lookup key selector :) + map{ #xml:base : true(), #xml:space : false() }?#xml:base +}; + +(: ======== QName Literals ======== :) + +declare + %test:assertEquals("foo") +function t:qname-literal-local() { + local-name-from-QName( #foo ) +}; + +declare + %test:assertEquals("") +function t:qname-literal-local-no-ns() { + namespace-uri-from-QName( #foo ) +}; + +declare + %test:assertEquals("xs:integer") +function t:qname-literal-prefixed() { + string( #xs:integer ) +}; + +declare + %test:assertEquals("http://www.w3.org/2001/XMLSchema") +function t:qname-literal-prefixed-ns() { + namespace-uri-from-QName( #xs:integer ) +}; + +(: ======== Default Parameter Values ======== :) + +declare function local:add($x as xs:integer, $y as xs:integer := 10) { + $x + $y +}; + +declare + %test:assertEquals(13) +function t:default-param-override() { + local:add(10, 3) +}; + +declare + %test:assertEquals(20) +function t:default-param-used() { + local:add(10) +}; + +declare function local:greet($name as xs:string, $greeting as xs:string := "Hello") { + $greeting || ", " || $name || "!" +}; + +declare + %test:assertEquals("Hello, World!") +function t:default-param-string() { + local:greet("World") +}; + +declare + %test:assertEquals("Hi, World!") +function t:default-param-string-override() { + local:greet("World", "Hi") +}; + +(: ======================== :) +(: Choice/Union Item Types :) +(: ======================== :) + +declare + %test:assertTrue +function t:choice-type-instance-of-string() { + "hello" instance of (xs:string | xs:integer) +}; + +declare + %test:assertTrue +function t:choice-type-instance-of-integer() { + 42 instance of (xs:string | xs:integer) +}; + +declare + %test:assertFalse +function t:choice-type-instance-of-no-match() { + 3.14 instance of (xs:string | xs:integer) +}; + +declare + %test:assertTrue +function t:choice-type-instance-of-three-types() { + xs:date("2024-01-01") instance of (xs:string | xs:integer | xs:date) +}; + +declare + %test:assertTrue +function t:choice-type-with-node-types() { + instance of (element() | text()) +}; + +declare + %test:assertTrue +function t:choice-type-text-node() { + text { "hello" } instance of (element() | text()) +}; + +declare + %test:assertFalse +function t:choice-type-no-match-node() { + instance of (element() | text()) +}; + +declare function local:choice-param($x as (xs:string | xs:integer)) as xs:string { + string($x) +}; + +declare + %test:assertEquals("hello") +function t:choice-type-param-string() { + local:choice-param("hello") +}; + +declare + %test:assertEquals("42") +function t:choice-type-param-integer() { + local:choice-param(42) +}; + +declare + %test:assertTrue +function t:choice-type-with-cardinality() { + (1, 2, 3) instance of (xs:string | xs:integer)* +}; + +declare + %test:assertTrue +function t:choice-type-mixed-sequence() { + ("hello", 42) instance of (xs:string | xs:integer)* +}; + +declare + %test:assertXPath("$result instance of xs:date") +function t:choice-type-cast-as() { + "2024-01-15" cast as (xs:dateTime | xs:date | xs:time) +}; + +declare + %test:assertTrue +function t:choice-type-castable-as() { + "2024-01-15" castable as (xs:dateTime | xs:date | xs:time) +}; + +declare + %test:assertFalse +function t:choice-type-castable-as-false() { + "not-a-date" castable as (xs:dateTime | xs:date | xs:time) +}; + +(: ======================== :) +(: Enumeration Types :) +(: ======================== :) + +declare + %test:assertTrue +function t:enum-instance-of-match() { + "c" instance of enum("a", "b", "c", "d") +}; + +declare + %test:assertFalse +function t:enum-instance-of-no-match() { + "g" instance of enum("a", "b", "c", "d") +}; + +declare + %test:assertEquals("a") +function t:enum-cast-as() { + "a" cast as enum("a", "b", "c", "d") +}; + +declare + %test:assertTrue +function t:enum-castable-as-match() { + "c" castable as enum("a", "b", "c", "d") +}; + +declare + %test:assertFalse +function t:enum-castable-as-no-match() { + "g" castable as enum("a", "b", "c", "d") +}; + +declare + %test:assertFalse +function t:enum-instance-of-not-string() { + 42 instance of enum("42") +}; + +declare + %test:assertTrue +function t:enum-in-choice-type() { + "b" instance of (enum("a", "b") | xs:integer) +}; + +declare + %test:assertTrue +function t:enum-choice-integer-match() { + 42 instance of (enum("a", "b") | xs:integer) +}; + +(: ======================== :) +(: Ternary Conditional Expr :) +(: ======================== :) + +declare + %test:assertEquals("yes") +function t:ternary-true() { + true() ?? "yes" !! "no" +}; + +declare + %test:assertEquals("no") +function t:ternary-false() { + false() ?? "yes" !! "no" +}; + +declare + %test:assertEquals(42) +function t:ternary-with-expr() { + (1 = 1) ?? 42 !! 0 +}; + +declare + %test:assertEquals("B") +function t:ternary-nested() { + false() ?? "A" !! (true() ?? "B" !! "C") +}; + +declare + %test:assertEquals(2) +function t:ternary-with-or() { + (false() or true()) ?? 2 !! 3 +}; + +(: ========== XQ4 Method Call Operator (=?>) ========== :) + +declare + %test:assertEquals(6) +function t:method-call-simple() { + let $rectangle := map { 'length': 3, 'width': 2, + 'area': function($self) { $self?length * $self?width } } + return $rectangle =?> area() +}; + +declare + %test:assertEquals(24) +function t:method-call-with-args() { + let $rectangle := map { 'length': 3, 'width': 2, + 'resize': function($self, $scale) { + map:put(map:put($self, 'length', $self?length * $scale), 'width', $self?width * $scale) + }, + 'area': function($self) { $self?length * $self?width } + } + return $rectangle =?> resize(2) =?> area() +}; + +declare + %test:assertEmpty +function t:method-call-empty-sequence() { + let $rectangle := map { 'length': 3, 'width': 2, + 'area': function($self) { $self?length * $self?width } } + return $rectangle[2] =?> area() +}; + +declare + %test:assertEquals(6, 20) +function t:method-call-multiple-maps() { + let $rectangles := ( + map { 'length': 3, 'width': 2, 'area': function($self) { $self?length * $self?width } }, + map { 'length': 4, 'width': 5, 'area': function($self) { $self?length * $self?width } } + ) + return $rectangles =?> area() +}; + +declare + %test:assertError("XPTY0004") +function t:method-call-not-a-map() { + let $arr := [1, 2, 3] + return $arr =?> foo() +}; + +declare + %test:assertError("XPTY0004") +function t:method-call-not-a-function() { + let $m := map { 'length': 3 } + return $m =?> length() +}; + +(: ========== XQ4 Let Destructuring ========== :) + +declare + %test:assertEquals(1) +function t:let-seq-destructure-single() { + let $($x) := (1, 2) + return $x +}; + +declare + %test:assertEquals(1, 2) +function t:let-seq-destructure-basic() { + let $($x, $y) := (1, 2) + return ($x, $y) +}; + +declare + %test:assertEquals(1, 2, 3) +function t:let-seq-destructure-triple() { + let $($a, $b, $c) := (1, 2, 3) + return ($a, $b, $c) +}; + +declare + %test:assertEquals(1, 2) +function t:let-array-destructure-basic() { + let $[$x, $y] := [1, 2] + return ($x, $y) +}; + +declare + %test:assertEquals(1, 2, 3) +function t:let-array-destructure-triple() { + let $[$a, $b, $c] := [1, 2, 3] + return ($a, $b, $c) +}; + +declare + %test:assertEquals(1, 2) +function t:let-map-destructure-basic() { + let ${$x, $y} := map { 'x': 1, 'y': 2 } + return ($x, $y) +}; + +declare + %test:assertEquals("hello", 42) +function t:let-map-destructure-mixed() { + let ${$name, $age} := map { 'name': 'hello', 'age': 42 } + return ($name, $age) +}; + +declare + %test:assertEquals(3, 7) +function t:let-seq-destructure-in-flwor() { + for $pair in ([1, 2], [3, 4]) + let $[$x, $y] := $pair + return $x + $y +}; + +declare + %test:assertEquals(3) +function t:let-seq-destructure-overflow() { + (: more items than variables - extras discarded :) + let $($x) := (3, 4, 5) + return $x +}; + +declare + %test:assertEquals(3, 7) +function t:let-destructure-multiple() { + let $($a, $b) := (1, 2), $($c, $d) := (3, 4) + return ($a + $b, $c + $d) +}; + +(: === Unicode multiplication sign === :) + +declare + %test:assertEquals(6) +function t:unicode-multiply-basic() { + 3 × 2 +}; + +declare + %test:assertEquals(24) +function t:unicode-multiply-expression() { + 2 × 3 × 4 +}; + +(: === Destructuring with per-variable type annotations === :) + +declare + %test:assertEquals(1, 2) +function t:seq-destructure-typed() { + let $($x as xs:integer, $y as xs:integer) := (1, 2) + return ($x, $y) +}; + +declare + %test:assertEquals(1, "two") +function t:array-destructure-typed() { + let $[$x as xs:integer, $y as xs:string] := [1, "two"] + return ($x, $y) +}; + +declare + %test:assertError("XPTY0004") +function t:seq-destructure-typed-error() { + let $($x as xs:integer, $y as xs:date) := (1, "two") + return ($x, $y) +}; + +declare + %test:assertEquals(1, 2) +function t:array-destructure-overall-typed() { + let $[$x, $y] as array(xs:integer+) := [1, 2] + return ($x, $y) +}; + +(: === Choice types with enum === :) + +declare + %test:assertTrue +function t:choice-type-with-enum() { + "a" instance of (enum("a","b") | xs:integer) +}; + +declare + %test:assertTrue +function t:choice-type-int-in-enum-union() { + 42 instance of (enum("a","b") | xs:integer) +}; + +(: === String constructor atomization === :) + +declare + %test:assertEquals("There were 10 green bottles") +function t:string-constructor-array-interpolation() { + let $n := 10 + return ``[There were `{[$n]}` green bottles]`` +}; + +declare + %test:assertError("FOTY0013") +function t:string-constructor-map-atomization-error() { + let $n := map{"a":10} + return ``[There were `{$n}` green bottles]`` +}; + +declare + %test:assertTrue +function t:string-constructor-entity-not-expanded() { + (: Entity refs inside string constructors should be literal text, not expanded :) + ``[There were < 10 green bottles]`` eq "There were &lt; 10 green bottles" +}; + +(: Test 027/028 - backtick-curly in element content :) +(: SKIPPED: ANTLR 2 lexer lookahead timing — }` after enclosed expression in element :) +(: content is lexed before parser restores inElementContent=true :) + +(: Tests 029-034 - entity/char refs not expanded in string constructors inside element constructors :) +(: Entity refs like < should remain as literal text inside ``[...]``, not expanded to < :) +declare + %test:assertTrue +function t:string-constructor-029-entity-in-element() { + let $result := {``[There were < 10 green bottles]``} + let $expected := "There were " || codepoints-to-string(38) || "lt; 10 green bottles" + return string($result) eq $expected +}; + +declare + %test:assertTrue +function t:string-constructor-030-charref-in-element() { + let $result := {``[There were < 10 green bottles]``} + let $expected := "There were " || codepoints-to-string(38) || "#x003C; 10 green bottles" + return string($result) eq $expected +}; + +declare + %test:assertTrue +function t:string-constructor-031-entity-with-interp() { + let $result := {``[There were < `{10}` green bottles]``} + let $expected := "There were " || codepoints-to-string(38) || "lt; 10 green bottles" + return string($result) eq $expected +}; + +declare + %test:assertTrue +function t:string-constructor-032-charref-with-interp() { + let $result := {``[There were < `{10}` green bottles]``} + let $expected := "There were " || codepoints-to-string(38) || "#x003C; 10 green bottles" + return string($result) eq $expected +}; + +declare + %test:assertTrue +function t:string-constructor-033-entity-after-interp() { + let $result := {``[There were `{10}` < green bottles]``} + let $expected := "There were 10 " || codepoints-to-string(38) || "lt; green bottles" + return string($result) eq $expected +}; + +declare + %test:assertTrue +function t:string-constructor-034-charref-after-interp() { + let $result := {``[There were `{10}` < green bottles]``} + let $expected := "There were 10 " || codepoints-to-string(38) || "#x003C; green bottles" + return string($result) eq $expected +}; + +(: Focus functions :) + +declare + %test:assertEquals(2, 3, 4, 5, 6) +function t:focusFunction-filter() { + (1 to 5) ! (fn { . + 1 })(.) +}; + +declare + %test:assertEquals(2, 4, 6) +function t:focusFunction-forEach() { + for-each((1, 2, 3), fn { . * 2 }) +}; + +declare + %test:assertEquals(6) +function t:focusFunction-functionKeyword() { + (function { . * 3 })(2) +}; + +declare + %test:assertEquals(3) +function t:focusFunction-withHigherOrder() { + let $add1 := fn { . + 1 } + return $add1(2) +}; + +(: Keyword arguments — parser syntax test only, full dispatch tested with XQ4 functions :) + +(: XQ4 annotation literals :) + +declare + %test:assertTrue +function t:annotation-true-false-literals() { + (: XQ4 allows true(), false(), and negative literals in annotations :) + let $f := %Q{http://example.com/test}check(true(), false(), -42) function($x) { $x } + return $f(true()) +}; + +declare + %test:assertEquals(1) +function t:annotation-negative-numeric-literal() { + let $f := %Q{http://example.com/test}range(-1, -3.14, -2.5e3) fn { . } + return $f(1) +}; + +(: === try/catch/finally (XQ4) === :) + +declare + %test:assertEquals(42) +function t:try-finally-basic() { + (: XQ4: try with finally clause, no catch :) + try { 42 } finally {} +}; + +declare + %test:assertEquals(42) +function t:try-finally-empty-seq() { + try { 42 } finally {()} +}; + +declare + %test:assertEquals(42) +function t:try-catch-finally() { + (: XQ4: try with catch and finally :) + try { 42 } catch * { 97 } finally {()} +}; + +declare + %test:assertError("XQTY0153") +function t:try-finally-nonempty-error() { + (: XQ4: finally must produce empty sequence :) + try { 42 } finally { 99 } +}; + +declare + %test:assertError("FOAR0001") +function t:try-finally-error-replaces-result() { + (: XQ4: error in finally replaces try result :) + try { 42 } finally { 10 div 0 } +}; + +declare + %test:assertEquals(99) +function t:try-catch-finally-catch-fires() { + (: XQ4: catch fires, finally runs with empty result :) + try { 10 div 0 } + catch err:FOAR0001 { 99 } + finally {()} +}; + +declare + %test:assertError("FOAR0001") +function t:try-finally-error-not-caught-by-same() { + (: XQ4: finally error is NOT caught by same try/catch :) + try { 42 } + catch err:FOAR0001 { "wrong" } + finally { 10 div 0 } +}; + +(: ======================== XQ4 switch expression ======================== :) + +declare + %test:assertEquals("Oink") +function t:switch-sequence-case-operand() { + (: XQ4: case operand may be a sequence; match if any item equals comparand :) + let $in := 2 + return switch ($in) + case 1 return "Moo" + case 5 return "Meow" + case 3 return "Quack" + case ($in to 4) return "Oink" + default return "Baa" +}; + +declare + %test:assertEquals("Oink") +function t:switch-braced-syntax() { + (: XQ4: braced switch syntax :) + let $in := 2 + return switch ($in) { + case 1 return "Moo" + case 5 return "Meow" + case ($in to 4) return "Oink" + default return "Baa" + } +}; + +declare + %test:assertEquals("Meow") +function t:switch-boolean-mode-no-braces() { + (: XQ4: omitted comparand, without braces :) + let $animal := "Cat" + return switch () + case $animal eq "Cow" return "Moo" + case $animal eq "Cat" return "Meow" + case $animal eq "Duck" return "Quack" + default return "What's that odd noise?" +}; + +(: XQ4 focus constructors :) + +declare + %test:assertEquals(42) +function t:focus-constructor-integer() { + (: XQ4 focus constructor: xs:type() with context item :) + '42' ! xs:integer() +}; + +declare + %test:assertEquals(3.14) +function t:focus-constructor-double() { + '3.14' ! xs:double() +}; + +declare + %test:assertEquals("Woof") +function t:switch-nan-matches-nan() { + (: XQ3: NaN matches NaN in switch - reproduces switch-011 :) + let $in := xs:double('NaN') + return + { switch ($in) + case 42 return "Moo" + case 42 return "Meow" + case 42e0 return "Quack" + case "42e0" return "Oink" + case xs:float('NaN') return "Woof" + default return "Expletive deleted" }/string() +}; From 4c342d16fe074b468b8e2080ba8832483b8dc146 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Sun, 5 Apr 2026 19:25:30 -0400 Subject: [PATCH 06/20] [feature] Add version gating and feature flag for XQuery 4.0 syntax Add tree walker version checks for all XQ4-only constructs: when staticContext.getXQueryVersion() < 40, throw XPST0003 with a descriptive message. This ensures modules declaring xquery version "3.1" cannot use XQ4 syntax even if the parser somehow accepts it. Gated constructs: otherwise, pipeline (->), mapping arrow (=>!), ternary conditional (?? !!), keyword arguments, focus functions, string templates, while clause, default parameters, for-member, method call (=>?). Also add system property exist.xquery4.enabled (default true) to allow disabling XQ4 support entirely. When disabled, xquery version "4.0" declarations throw XPST0003. Addresses reviewer feedback from line-o on PR #6139. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../org/exist/xquery/parser/XQueryTree.g | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g index 45ca178fd5e..0740916eda3 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g @@ -273,6 +273,10 @@ throws PermissionDeniedException, EXistException, XPathException { final String version = v.getText(); if (version.equals("4.0")) { + if (!"true".equals(System.getProperty("exist.xquery4.enabled", "true"))) { + throw new XPathException(v, ErrorCodes.XPST0003, + "XQuery 4.0 is not enabled. Set system property exist.xquery4.enabled=true to enable."); + } context.setXQueryVersion(40); staticContext.setXQueryVersion(40); } else if (version.equals("3.1")) { @@ -954,6 +958,10 @@ throws PermissionDeniedException, EXistException, XPathException #( ff:FOCUS_FUNCTION { + if (staticContext.getXQueryVersion() < 40) { + throw new XPathException(ff, ErrorCodes.XPST0003, + "Focus functions require xquery version \"4.0\""); + } PathExpr body = new PathExpr(context); body.setASTNode(focusFunctionDecl_AST_in); @@ -1009,8 +1017,12 @@ throws PermissionDeniedException, EXistException, XPathException )? ( #( - PARAM_DEFAULT + pd:PARAM_DEFAULT { + if (staticContext.getXQueryVersion() < 40) { + throw new XPathException(pd, ErrorCodes.XPST0003, + "Default parameter values require xquery version \"4.0\""); + } PathExpr defaultExpr = new PathExpr(context); } expr [defaultExpr] @@ -1862,6 +1874,10 @@ throws PermissionDeniedException, EXistException, XPathException #( astTernary:TERNARY { + if (staticContext.getXQueryVersion() < 40) { + throw new XPathException(astTernary, ErrorCodes.XPST0003, + "The ternary conditional operator (?? !!) requires xquery version \"4.0\""); + } PathExpr ternTestExpr = new PathExpr(context); PathExpr ternThenExpr = new PathExpr(context); PathExpr ternElseExpr = new PathExpr(context); @@ -2136,7 +2152,13 @@ throws PermissionDeniedException, EXistException, XPathException ) | #( - FOR_MEMBER + fmAST:FOR_MEMBER + { + if (staticContext.getXQueryVersion() < 40) { + throw new XPathException(fmAST, ErrorCodes.XPST0003, + "The 'for member' clause requires xquery version \"4.0\""); + } + } #( memberVarName:VARIABLE_BINDING { @@ -2873,6 +2895,10 @@ throws PermissionDeniedException, EXistException, XPathException #( wh:"while" { + if (staticContext.getXQueryVersion() < 40) { + throw new XPathException(wh, ErrorCodes.XPST0003, + "The 'while' clause requires xquery version \"4.0\""); + } PathExpr whileExpr = new PathExpr(context); whileExpr.setASTNode(exprFlowControl_AST_in); } @@ -3156,6 +3182,10 @@ throws PermissionDeniedException, EXistException, XPathException #( stAST:STRING_TEMPLATE { + if (staticContext.getXQueryVersion() < 40) { + throw new XPathException(stAST, ErrorCodes.XPST0003, + "String templates require xquery version \"4.0\""); + } StringConstructor st = new StringConstructor(context); st.setASTNode(stAST); } @@ -4007,6 +4037,12 @@ throws PermissionDeniedException, EXistException, XPathException | #( kw:KEYWORD_ARG + { + if (staticContext.getXQueryVersion() < 40) { + throw new XPathException(kw, ErrorCodes.XPST0003, + "Keyword arguments require xquery version \"4.0\""); + } + } ( QUESTION { // Keyword argument with placeholder value: name := ? @@ -4607,6 +4643,10 @@ throws PermissionDeniedException, EXistException, XPathException #( mapArrowAST:MAPPING_ARROW_OP { + if (staticContext.getXQueryVersion() < 40) { + throw new XPathException(mapArrowAST, ErrorCodes.XPST0003, + "The mapping arrow operator (=>!) requires xquery version \"4.0\""); + } PathExpr leftExpr = new PathExpr(context); leftExpr.setASTNode(mappingArrowOp_AST_in); } @@ -4654,6 +4694,10 @@ throws PermissionDeniedException, EXistException, XPathException #( pipeAST:PIPELINE_OP { + if (staticContext.getXQueryVersion() < 40) { + throw new XPathException(pipeAST, ErrorCodes.XPST0003, + "The pipeline operator (->) requires xquery version \"4.0\""); + } PathExpr leftExpr = new PathExpr(context); leftExpr.setASTNode(pipelineOp_AST_in); } @@ -4680,6 +4724,10 @@ throws PermissionDeniedException, EXistException, XPathException #( mcAST:METHOD_CALL_OP { + if (staticContext.getXQueryVersion() < 40) { + throw new XPathException(mcAST, ErrorCodes.XPST0003, + "The method call operator (=?>) requires xquery version \"4.0\""); + } PathExpr leftExpr = new PathExpr(context); leftExpr.setASTNode(methodCallOp_AST_in); } @@ -4715,6 +4763,10 @@ throws PermissionDeniedException, EXistException, XPathException #( owAST:LITERAL_otherwise { + if (staticContext.getXQueryVersion() < 40) { + throw new XPathException(owAST, ErrorCodes.XPST0003, + "The 'otherwise' operator requires xquery version \"4.0\""); + } PathExpr leftExpr = new PathExpr(context); leftExpr.setASTNode(otherwiseExpr_AST_in); } From 47a4cdf4ea4183390a6d5715f96f9b2bac3c7c59 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Fri, 24 Apr 2026 23:16:27 -0400 Subject: [PATCH 07/20] [feature] Align fn: function parameter names with XQuery 4.0 F&O spec Updates ~75 FunctionParameterSequenceType attribute names across 64 fn: function files to match the XQuery 4.0 Functions and Operators specification. This enables keyword argument resolution (name := value syntax) to match parameter names correctly. Key patterns: - $arg -> $value (transformation functions: abs, ceiling, floor, round, etc.) - $arg -> $input (sequence functions: reverse, head, tail, count, etc.) - $arg -> $node (node functions: local-name, name, namespace-uri, root, etc.) - $arg -> $values (aggregate functions: sum, min, max, string-join) - $collation-uri -> $collation (all collation parameters) - $source-string -> $value (contains, starts-with, ends-with) - $string-1/$string-2 -> $value1/$value2 (compare, codepoint-equal) - $sequence -> $input, $function -> $action/$predicate (HOF functions) - $date/$time/$date-time/$duration -> $value (date/time extraction functions) - Various other renames per XQ4 F&O spec Co-Authored-By: Claude Opus 4.6 (1M context) --- .codacy/codacy.yaml | 15 +++++++++++++ .../functions/fn/FunAdjustTimezone.java | 8 +++---- .../xquery/functions/fn/FunAnalyzeString.java | 4 ++-- .../exist/xquery/functions/fn/FunBoolean.java | 2 +- .../functions/fn/FunCodepointEqual.java | 4 ++-- .../functions/fn/FunCodepointsToString.java | 2 +- .../xquery/functions/fn/FunCollationKey.java | 2 +- .../exist/xquery/functions/fn/FunCompare.java | 10 ++++----- .../xquery/functions/fn/FunContains.java | 6 ++--- .../exist/xquery/functions/fn/FunCount.java | 2 +- .../exist/xquery/functions/fn/FunData.java | 2 +- .../xquery/functions/fn/FunDeepEqual.java | 10 ++++----- .../functions/fn/FunDistinctValues.java | 2 +- .../org/exist/xquery/functions/fn/FunDoc.java | 2 +- .../xquery/functions/fn/FunDocumentURI.java | 2 +- .../exist/xquery/functions/fn/FunEmpty.java | 2 +- .../xquery/functions/fn/FunEncodeForURI.java | 2 +- .../xquery/functions/fn/FunEndsWith.java | 10 ++++----- .../exist/xquery/functions/fn/FunEquals.java | 6 ++--- .../xquery/functions/fn/FunEscapeHTMLURI.java | 2 +- .../exist/xquery/functions/fn/FunExists.java | 2 +- .../functions/fn/FunGetDateComponent.java | 6 ++--- .../functions/fn/FunGetDurationComponent.java | 6 ++--- .../xquery/functions/fn/FunHeadTail.java | 2 +- .../functions/fn/FunHigherOrderFun.java | 22 +++++++++---------- .../xquery/functions/fn/FunIRIToURI.java | 2 +- .../org/exist/xquery/functions/fn/FunId.java | 6 ++--- .../exist/xquery/functions/fn/FunIdRef.java | 6 ++--- .../exist/xquery/functions/fn/FunIndexOf.java | 6 ++--- .../xquery/functions/fn/FunInsertBefore.java | 4 ++-- .../exist/xquery/functions/fn/FunLang.java | 8 +++---- .../xquery/functions/fn/FunLocalName.java | 2 +- .../exist/xquery/functions/fn/FunMatches.java | 2 +- .../org/exist/xquery/functions/fn/FunMax.java | 6 ++--- .../org/exist/xquery/functions/fn/FunMin.java | 6 ++--- .../exist/xquery/functions/fn/FunName.java | 2 +- .../xquery/functions/fn/FunNamespaceURI.java | 2 +- .../xquery/functions/fn/FunNodeName.java | 2 +- .../functions/fn/FunNormalizeSpace.java | 2 +- .../functions/fn/FunNormalizeUnicode.java | 4 ++-- .../org/exist/xquery/functions/fn/FunNot.java | 2 +- .../exist/xquery/functions/fn/FunNumber.java | 2 +- .../xquery/functions/fn/FunOneOrMore.java | 2 +- .../exist/xquery/functions/fn/FunReplace.java | 2 +- .../xquery/functions/fn/FunResolveURI.java | 2 +- .../exist/xquery/functions/fn/FunReverse.java | 2 +- .../exist/xquery/functions/fn/FunRoot.java | 2 +- .../xquery/functions/fn/FunSerialize.java | 6 ++--- .../xquery/functions/fn/FunStartsWith.java | 6 ++--- .../xquery/functions/fn/FunStrLength.java | 2 +- .../exist/xquery/functions/fn/FunString.java | 2 +- .../xquery/functions/fn/FunStringJoin.java | 4 ++-- .../functions/fn/FunStringToCodepoints.java | 2 +- .../xquery/functions/fn/FunSubSequence.java | 8 +++---- .../xquery/functions/fn/FunSubstring.java | 8 +++---- .../functions/fn/FunSubstringAfter.java | 6 ++--- .../functions/fn/FunSubstringBefore.java | 6 ++--- .../org/exist/xquery/functions/fn/FunSum.java | 4 ++-- .../xquery/functions/fn/FunTokenize.java | 2 +- .../exist/xquery/functions/fn/FunTrace.java | 2 +- .../xquery/functions/fn/FunTranslate.java | 6 ++--- .../xquery/functions/fn/FunUnordered.java | 2 +- .../functions/fn/FunUpperOrLowerCase.java | 4 ++-- .../xquery/functions/fn/FunUriCollection.java | 2 +- .../xquery/functions/fn/FunZeroOrOne.java | 2 +- 65 files changed, 148 insertions(+), 133 deletions(-) create mode 100644 .codacy/codacy.yaml diff --git a/.codacy/codacy.yaml b/.codacy/codacy.yaml new file mode 100644 index 00000000000..e723e1332d2 --- /dev/null +++ b/.codacy/codacy.yaml @@ -0,0 +1,15 @@ +runtimes: + - dart@3.7.2 + - go@1.22.3 + - java@17.0.10 + - node@22.2.0 + - python@3.11.11 +tools: + - dartanalyzer@3.7.2 + - eslint@8.57.0 + - lizard@1.17.31 + - opengrep@1.16.2 + - pmd@7.11.0 + - pylint@3.3.6 + - revive@1.7.0 + - trivy@0.69.3 diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAdjustTimezone.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAdjustTimezone.java index 2bc27fa19c5..197714ac276 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAdjustTimezone.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAdjustTimezone.java @@ -44,10 +44,10 @@ */ public class FunAdjustTimezone extends BasicFunction { - public final static FunctionParameterSequenceType DATE_TIME_01_PARAM = new FunctionParameterSequenceType("date-time", Type.DATE_TIME, Cardinality.ZERO_OR_ONE, "The date-time"); - public final static FunctionParameterSequenceType DATE_01_PARAM = new FunctionParameterSequenceType("date", Type.DATE, Cardinality.ZERO_OR_ONE, "The date"); - public final static FunctionParameterSequenceType TIME_01_PARAM = new FunctionParameterSequenceType("time", Type.TIME, Cardinality.ZERO_OR_ONE, "The time"); - public final static FunctionParameterSequenceType DURATION_01_PARAM = new FunctionParameterSequenceType("duration", Type.DAY_TIME_DURATION, Cardinality.ZERO_OR_ONE, "The duration"); + public final static FunctionParameterSequenceType DATE_TIME_01_PARAM = new FunctionParameterSequenceType("value", Type.DATE_TIME, Cardinality.ZERO_OR_ONE, "The date-time"); + public final static FunctionParameterSequenceType DATE_01_PARAM = new FunctionParameterSequenceType("value", Type.DATE, Cardinality.ZERO_OR_ONE, "The date"); + public final static FunctionParameterSequenceType TIME_01_PARAM = new FunctionParameterSequenceType("value", Type.TIME, Cardinality.ZERO_OR_ONE, "The time"); + public final static FunctionParameterSequenceType DURATION_01_PARAM = new FunctionParameterSequenceType("timezone", Type.DAY_TIME_DURATION, Cardinality.ZERO_OR_ONE, "The duration"); public final static FunctionReturnSequenceType DATE_TIME_01_RETURN = new FunctionReturnSequenceType(Type.DATE_TIME, Cardinality.ZERO_OR_ONE, "the adjusted date-time"); public final static FunctionReturnSequenceType DATE_01_RETURN = new FunctionReturnSequenceType(Type.DATE, Cardinality.ZERO_OR_ONE, "the adjusted date"); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java index 8fe035492a7..8734db329f7 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunAnalyzeString.java @@ -64,7 +64,7 @@ public class FunAnalyzeString extends BasicFunction { "matched substrings, which substrings matched each " + "capturing group in the regular expression.", new SequenceType[] { - new FunctionParameterSequenceType("input", Type.STRING, + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The input string"), new FunctionParameterSequenceType("pattern", Type.STRING, Cardinality.EXACTLY_ONE, "The pattern") @@ -80,7 +80,7 @@ public class FunAnalyzeString extends BasicFunction { "matched substrings, which substrings matched each " + "capturing group in the regular expression.", new SequenceType[] { - new FunctionParameterSequenceType("input", Type.STRING, + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The input string"), new FunctionParameterSequenceType("pattern", Type.STRING, Cardinality.EXACTLY_ONE, "The pattern"), diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunBoolean.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunBoolean.java index 42224bbba1f..52a2fceff27 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunBoolean.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunBoolean.java @@ -48,7 +48,7 @@ public class FunBoolean extends Function { new QName("boolean", Function.BUILTIN_FUNCTION_NS), "Computes the xs:boolean value of the sequence items.", new SequenceType[] { - new FunctionParameterSequenceType("items", Type.ITEM, + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The items") }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCodepointEqual.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCodepointEqual.java index 3001a9300cc..4ea7d17e7c5 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCodepointEqual.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCodepointEqual.java @@ -57,9 +57,9 @@ public class FunCodepointEqual extends BasicFunction { "is equal to the value of $string-2, according to the Unicode " + "code point collation.", new SequenceType[] { - new FunctionParameterSequenceType("string-1", Type.STRING, + new FunctionParameterSequenceType("value1", Type.STRING, Cardinality.ZERO_OR_ONE, "The first string"), - new FunctionParameterSequenceType("string-2", Type.STRING, + new FunctionParameterSequenceType("value2", Type.STRING, Cardinality.ZERO_OR_ONE, "The second string"), }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.ZERO_OR_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCodepointsToString.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCodepointsToString.java index 4981728b436..6e613a72970 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCodepointsToString.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCodepointsToString.java @@ -61,7 +61,7 @@ public class FunCodepointsToString extends BasicFunction { "If any of the code points in $codepoints is not a " + "legal XML character, an error is raised", new SequenceType[] { - new FunctionParameterSequenceType("codepoints", Type.INTEGER, + new FunctionParameterSequenceType("values", Type.INTEGER, Cardinality.ZERO_OR_MORE, "The codepoints as a sequence of xs:integer values"), }, new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCollationKey.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCollationKey.java index b068ed3c873..2ed0b08369d 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCollationKey.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCollationKey.java @@ -46,7 +46,7 @@ public class FunCollationKey extends BasicFunction { "under the specified collation."; private static final FunctionReturnSequenceType FN_RETURN = returnsOpt(Type.BASE64_BINARY, "the collation key"); private static final FunctionParameterSequenceType PARAM_VALUE_STRING = param("value-string", Type.STRING, "The value string"); - private static final FunctionParameterSequenceType PARAM_COLLATION_STRING = param("collation-string", Type.STRING, "The collation string"); + private static final FunctionParameterSequenceType PARAM_COLLATION_STRING = param("collation", Type.STRING, "The collation string"); public static final FunctionSignature[] FS_COLLATION_KEY_SIGNATURES = functionSignatures( FN_NAME, FN_DESCRIPTION, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCompare.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCompare.java index d2cd6e102c7..ff452829fa8 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCompare.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCompare.java @@ -60,9 +60,9 @@ public class FunCompare extends CollatingFunction { "Please remember to specify the collation in the context or use, " + "the three argument version if you don't want the system default.", new SequenceType[] { - new FunctionParameterSequenceType("string-1", Type.STRING, + new FunctionParameterSequenceType("value1", Type.STRING, Cardinality.ZERO_OR_ONE, "The first string"), - new FunctionParameterSequenceType("string-2", Type.STRING, + new FunctionParameterSequenceType("value2", Type.STRING, Cardinality.ZERO_OR_ONE, "The second string") }, new FunctionReturnSequenceType(Type.INTEGER, Cardinality.ZERO_OR_ONE, @@ -78,11 +78,11 @@ public class FunCompare extends CollatingFunction { "If either comparand is the empty sequence, the empty sequence is returned. " + THIRD_REL_COLLATION_ARG_EXAMPLE, new SequenceType[] { - new FunctionParameterSequenceType("string-1", Type.STRING, + new FunctionParameterSequenceType("value1", Type.STRING, Cardinality.ZERO_OR_ONE, "The first string"), - new FunctionParameterSequenceType("string-2", Type.STRING, + new FunctionParameterSequenceType("value2", Type.STRING, Cardinality.ZERO_OR_ONE, "The second string"), - new FunctionParameterSequenceType("collation-uri", Type.STRING, + new FunctionParameterSequenceType("collation", Type.STRING, Cardinality.EXACTLY_ONE, "The relative collation URI") }, new FunctionReturnSequenceType(Type.INTEGER, Cardinality.ZERO_OR_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunContains.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunContains.java index 425ce0728c5..caac41c698e 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunContains.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunContains.java @@ -50,7 +50,7 @@ public class FunContains extends CollatingFunction { "that provides a minimal match to the collation units in " + "the value of $substring, according to the default collation.", new SequenceType[] { - new FunctionParameterSequenceType("source-string", Type.STRING, + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The source-string"), new FunctionParameterSequenceType("substring", Type.STRING, Cardinality.ZERO_OR_ONE, "The substring") @@ -66,11 +66,11 @@ public class FunContains extends CollatingFunction { "the value of $substring, according to the collation that is " + "specified in $collation-uri." + THIRD_REL_COLLATION_ARG_EXAMPLE, new SequenceType[] { - new FunctionParameterSequenceType("source-string", Type.STRING, + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The source-string"), new FunctionParameterSequenceType("substring", Type.STRING, Cardinality.ZERO_OR_ONE, "The substring"), - new FunctionParameterSequenceType("collation-uri", Type.STRING, + new FunctionParameterSequenceType("collation", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI") }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCount.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCount.java index 3e9e3440b38..f884d060a93 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCount.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunCount.java @@ -44,7 +44,7 @@ public class FunCount extends Function { new QName("count", Function.BUILTIN_FUNCTION_NS), "Returns the number of items in the argument sequence, $items.", new SequenceType[]{ - new FunctionParameterSequenceType("items", Type.ITEM, + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The items") }, new FunctionReturnSequenceType(Type.INTEGER, Cardinality.EXACTLY_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunData.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunData.java index 8594c7ea63b..c3ca039822a 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunData.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunData.java @@ -57,7 +57,7 @@ public class FunData extends Function { qnData, "Atomizes the sequence $items, replacing all nodes in the sequence by their typed values.", new SequenceType[] { - new FunctionParameterSequenceType("items", Type.ITEM, + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The items") }, new FunctionReturnSequenceType(Type.ANY_ATOMIC_TYPE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDeepEqual.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDeepEqual.java index 6e6e0285dc2..0349ab24828 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDeepEqual.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDeepEqual.java @@ -72,9 +72,9 @@ public class FunDeepEqual extends CollatingFunction { "at the same position in $items-2, false() otherwise. " + "If both $items-1 and $items-2 are the empty sequence, returns true(). ", new SequenceType[] { - new FunctionParameterSequenceType("items-1", Type.ITEM, + new FunctionParameterSequenceType("input1", Type.ITEM, Cardinality.ZERO_OR_MORE, "The first item sequence"), - new FunctionParameterSequenceType("items-2", Type.ITEM, + new FunctionParameterSequenceType("input2", Type.ITEM, Cardinality.ZERO_OR_MORE, "The second item sequence") }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, @@ -88,11 +88,11 @@ public class FunDeepEqual extends CollatingFunction { "Comparison collation is specified by $collation-uri. " + THIRD_REL_COLLATION_ARG_EXAMPLE, new SequenceType[] { - new FunctionParameterSequenceType("items-1", Type.ITEM, + new FunctionParameterSequenceType("input1", Type.ITEM, Cardinality.ZERO_OR_MORE, "The first item sequence"), - new FunctionParameterSequenceType("items-2", Type.ITEM, + new FunctionParameterSequenceType("input2", Type.ITEM, Cardinality.ZERO_OR_MORE, "The second item sequence"), - new FunctionParameterSequenceType("collation-uri", Type.STRING, + new FunctionParameterSequenceType("collation", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI") }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDistinctValues.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDistinctValues.java index 155b654ce4c..b4110f40c1b 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDistinctValues.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDistinctValues.java @@ -80,7 +80,7 @@ public class FunDistinctValues extends CollatingFunction { new SequenceType[] { new FunctionParameterSequenceType("atomic-values", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The atomic values"), - new FunctionParameterSequenceType("collation-uri", Type.STRING, + new FunctionParameterSequenceType("collation", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI") }, new FunctionReturnSequenceType(Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDoc.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDoc.java index 65b663f4848..365e6d11cff 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDoc.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDoc.java @@ -57,7 +57,7 @@ public class FunDoc extends Function { "Returns the document node of $document-uri. " + XMLDBModule.ANY_URI, new SequenceType[] { - new FunctionParameterSequenceType("document-uri", Type.STRING, + new FunctionParameterSequenceType("source", Type.STRING, Cardinality.ZERO_OR_ONE, "The document URI") }, new FunctionReturnSequenceType(Type.DOCUMENT, Cardinality.ZERO_OR_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDocumentURI.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDocumentURI.java index b052164104e..1c6b6c28e1c 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDocumentURI.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunDocumentURI.java @@ -37,7 +37,7 @@ */ public class FunDocumentURI extends Function { - private static final FunctionParameterSequenceType FS_PARAM_NODE = optParam("value", Type.NODE, "The document node."); + private static final FunctionParameterSequenceType FS_PARAM_NODE = optParam("node", Type.NODE, "The document node."); private static final String FS_DOCUMENT_URI = "document-uri"; private static final String FS_DESCRIPTION = "Returns the URI of a resource where a document can be found, if available."; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEmpty.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEmpty.java index 5aaf2d72c28..7ca39701335 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEmpty.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEmpty.java @@ -47,7 +47,7 @@ public class FunEmpty extends Function { new QName("empty", Function.BUILTIN_FUNCTION_NS), "Returns true() if the value of $items is the empty sequence, false() otherwise.", new SequenceType[] { - new FunctionParameterSequenceType("items", Type.ITEM, + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The item sequence") }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEncodeForURI.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEncodeForURI.java index 3e2a56b4b0b..14e312160f9 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEncodeForURI.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEncodeForURI.java @@ -51,7 +51,7 @@ public class FunEncodeForURI extends Function { "with its percent-encoded form as described in [RFC 3986]. " + "If $uri-part is the empty sequence, returns the zero-length string.", new SequenceType[] { - new FunctionParameterSequenceType("uri-part", Type.STRING, + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The URI part to encode") }, new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEndsWith.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEndsWith.java index 8b064b6ca97..84cbf47119f 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEndsWith.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEndsWith.java @@ -49,9 +49,9 @@ public class FunEndsWith extends CollatingFunction { "If either $source-string or $suffix is the empty " + "sequence, the empty sequence is returned.", new SequenceType[] { - new FunctionParameterSequenceType("source-string", Type.STRING, + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The source-string"), - new FunctionParameterSequenceType("suffix", Type.STRING, + new FunctionParameterSequenceType("substring", Type.STRING, Cardinality.ZERO_OR_ONE, "The suffix") }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, @@ -65,11 +65,11 @@ public class FunEndsWith extends CollatingFunction { "the empty sequence, the empty sequence is returned. " + THIRD_REL_COLLATION_ARG_EXAMPLE, new SequenceType[] { - new FunctionParameterSequenceType("source-string", + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The source string"), - new FunctionParameterSequenceType("suffix", Type.STRING, + new FunctionParameterSequenceType("substring", Type.STRING, Cardinality.ZERO_OR_ONE, "The suffix"), - new FunctionParameterSequenceType("collation-uri", Type.STRING, + new FunctionParameterSequenceType("collation", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI") }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEquals.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEquals.java index 348fae2c97f..fe5bb3e11ad 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEquals.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEquals.java @@ -52,7 +52,7 @@ public class FunEquals extends CollatingFunction { "This function is similar to the '=' expression, except that " + "it uses the default collation for comparisons.", new SequenceType[] { - new FunctionParameterSequenceType("source-string", Type.STRING, + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The source-string"), new FunctionParameterSequenceType("substring", Type.STRING, Cardinality.ZERO_OR_ONE, "The substring") @@ -69,11 +69,11 @@ public class FunEquals extends CollatingFunction { "except that it uses the specified collation for comparisons." + THIRD_REL_COLLATION_ARG_EXAMPLE, new SequenceType[] { - new FunctionParameterSequenceType("source-string", Type.STRING, + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The source-string"), new FunctionParameterSequenceType("substring", Type.STRING, Cardinality.ZERO_OR_ONE, "The substring"), - new FunctionParameterSequenceType("collation-uri", Type.STRING, + new FunctionParameterSequenceType("collation", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI") }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEscapeHTMLURI.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEscapeHTMLURI.java index 4ed6aa72956..ff81f77e4f6 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEscapeHTMLURI.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunEscapeHTMLURI.java @@ -53,7 +53,7 @@ public class FunEscapeHTMLURI extends Function { "in the form %XX. If $html-uri is the empty sequence, " + "returns the zero-length string.", new SequenceType[] { - new FunctionParameterSequenceType("html-uri", Type.STRING, + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The html URI") }, new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunExists.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunExists.java index b6c298bc4ca..fd25e9ba8e8 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunExists.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunExists.java @@ -50,7 +50,7 @@ public class FunExists extends Function { "Returns true if the argument $items is not the empty sequence, " + "false otherwise.", new SequenceType[] { - new FunctionParameterSequenceType("items", Type.ITEM, Cardinality.ZERO_OR_MORE, "The item sequence") + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The item sequence") }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, "true() if not the empty-sequence, false() otherwise")); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunGetDateComponent.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunGetDateComponent.java index 983d32d0c42..e389fbbfe9a 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunGetDateComponent.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunGetDateComponent.java @@ -50,9 +50,9 @@ */ public class FunGetDateComponent extends BasicFunction { protected static final Logger logger = LogManager.getLogger(FunGetDateComponent.class); - public final static FunctionParameterSequenceType DATE_01_PARAM = new FunctionParameterSequenceType("date", Type.DATE, Cardinality.ZERO_OR_ONE, "The date as xs:date"); - public final static FunctionParameterSequenceType TIME_01_PARAM = new FunctionParameterSequenceType("time", Type.TIME, Cardinality.ZERO_OR_ONE, "The time as xs:time"); - public final static FunctionParameterSequenceType DATE_TIME_01_PARAM = new FunctionParameterSequenceType("date-time", Type.DATE_TIME, Cardinality.ZERO_OR_ONE, "The date-time as xs:dateTime"); + public final static FunctionParameterSequenceType DATE_01_PARAM = new FunctionParameterSequenceType("value", Type.DATE, Cardinality.ZERO_OR_ONE, "The date as xs:date"); + public final static FunctionParameterSequenceType TIME_01_PARAM = new FunctionParameterSequenceType("value", Type.TIME, Cardinality.ZERO_OR_ONE, "The time as xs:time"); + public final static FunctionParameterSequenceType DATE_TIME_01_PARAM = new FunctionParameterSequenceType("value", Type.DATE_TIME, Cardinality.ZERO_OR_ONE, "The date-time as xs:dateTime"); // ----- fromDate public final static FunctionSignature fnDayFromDate = diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunGetDurationComponent.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunGetDurationComponent.java index b2f3b92d60a..ac728e1f1eb 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunGetDurationComponent.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunGetDurationComponent.java @@ -54,9 +54,9 @@ */ public class FunGetDurationComponent extends BasicFunction { protected static final Logger logger = LogManager.getLogger(FunGetDurationComponent.class); - public final static FunctionParameterSequenceType DAYTIME_DURA_01_PARAM = new FunctionParameterSequenceType("duration", Type.DAY_TIME_DURATION, Cardinality.ZERO_OR_ONE, "The duration as xs:dayTimeDuration"); - public final static FunctionParameterSequenceType YEARMONTH_DURA_01_PARAM = new FunctionParameterSequenceType("duration", Type.YEAR_MONTH_DURATION, Cardinality.ZERO_OR_ONE, "The duration as xs:yearMonthDuration"); - public final static FunctionParameterSequenceType DURA_01_PARAM = new FunctionParameterSequenceType("duration", Type.DURATION, Cardinality.ZERO_OR_ONE, "The duration as xs:duration"); + public final static FunctionParameterSequenceType DAYTIME_DURA_01_PARAM = new FunctionParameterSequenceType("value", Type.DAY_TIME_DURATION, Cardinality.ZERO_OR_ONE, "The duration as xs:dayTimeDuration"); + public final static FunctionParameterSequenceType YEARMONTH_DURA_01_PARAM = new FunctionParameterSequenceType("value", Type.YEAR_MONTH_DURATION, Cardinality.ZERO_OR_ONE, "The duration as xs:yearMonthDuration"); + public final static FunctionParameterSequenceType DURA_01_PARAM = new FunctionParameterSequenceType("value", Type.DURATION, Cardinality.ZERO_OR_ONE, "The duration as xs:duration"); public final static FunctionSignature fnDaysFromDuration = new FunctionSignature( diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunHeadTail.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunHeadTail.java index c01a4110863..b6ada144cdf 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunHeadTail.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunHeadTail.java @@ -44,7 +44,7 @@ public class FunHeadTail extends BasicFunction { "The function returns the value of the expression $arg[1], i.e. the first item in the " + "passed in sequence.", new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ITEM, Cardinality.ZERO_OR_MORE, "") + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "") }, new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_ONE, "the first item or the empty sequence")), new FunctionSignature( diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunHigherOrderFun.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunHigherOrderFun.java index b1dd8ac2978..90bc8c3462a 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunHigherOrderFun.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunHigherOrderFun.java @@ -70,8 +70,8 @@ public class FunHigherOrderFun extends BasicFunction { "Applies the function item $function to every item from the sequence " + "$sequence in turn, returning the concatenation of the resulting sequences in order.", returnsOptMany(Type.ITEM, "result of applying the function to each item of the sequence"), - optManyParam("sequence", Type.ITEM, "the sequence on which to apply the function"), - funParam("function", + optManyParam("input", Type.ITEM, "the sequence on which to apply the function"), + funParam("action", params( param("item", Type.ITEM, "the next item in the sequence") ), @@ -84,8 +84,8 @@ public class FunHigherOrderFun extends BasicFunction { "Returns those items from the sequence $sequence for which the supplied function $function returns true.", returnsOptMany(Type.ITEM, "result of filtering the sequence"), - optManyParam("sequence", Type.ITEM, "the sequence to filter"), - funParam("function", + optManyParam("input", Type.ITEM, "the sequence to filter"), + funParam("predicate", params( param("next", Type.ITEM, "the next item to filter") ), @@ -94,9 +94,9 @@ public class FunHigherOrderFun extends BasicFunction { ) ); private static final FunctionParameterSequenceType[] FOLDING_PARAMS = params( - optManyParam("sequence", Type.ITEM, "the sequence to iterate over"), - optManyParam("zero", Type.ITEM, "initial value to start with"), - funParam("function", + optManyParam("input", Type.ITEM, "the sequence to iterate over"), + optManyParam("init", Type.ITEM, "initial value to start with"), + funParam("action", params( optManyParam("accumulator", Type.ITEM, "the current accumulated result"), param("next", Type.ITEM, "the next item in the sequence") @@ -124,9 +124,9 @@ public class FunHigherOrderFun extends BasicFunction { "Applies the function item $f to successive pairs of items taken one from $seq1 and one from $seq2, " + "returning the concatenation of the resulting sequences in order.", returnsOptMany(Type.ITEM, "concatenation of resulting sequences"), - optManyParam("seq1", Type.ITEM, "first sequence to take items from"), - optManyParam("seq2", Type.ITEM, "second sequence to take items from"), - funParam("function", + optManyParam("input1", Type.ITEM, "first sequence to take items from"), + optManyParam("input2", Type.ITEM, "second sequence to take items from"), + funParam("action", params( param("a", Type.ITEM, "the next item from the first sequence"), param("b", Type.ITEM, "the next item from the first sequence") @@ -142,7 +142,7 @@ public class FunHigherOrderFun extends BasicFunction { returnsOptMany(Type.ITEM, "return value of the function call"), param("function", Type.FUNCTION, "the function to call"), - param("array", Type.ARRAY_ITEM, "an array containing the arguments to pass to the function") + param("arguments", Type.ARRAY_ITEM, "an array containing the arguments to pass to the function") ); private AnalyzeContextInfo cachedContextInfo; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIRIToURI.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIRIToURI.java index bb94db1500a..b9f30a6480b 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIRIToURI.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIRIToURI.java @@ -80,7 +80,7 @@ public class FunIRIToURI extends Function { new FunctionSignature( new QName("iri-to-uri", Function.BUILTIN_FUNCTION_NS), FUNCTION_DESCRIPTION, - new SequenceType[] { new FunctionParameterSequenceType("iri", Type.STRING, Cardinality.ZERO_OR_ONE, "The IRI") }, + new SequenceType[] { new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The IRI") }, new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, "the URI")); public FunIRIToURI(XQueryContext context, FunctionSignature signature) { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunId.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunId.java index ba330cfb4c4..4eb25770fc3 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunId.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunId.java @@ -52,7 +52,7 @@ public class FunId extends Function { "matching the value of one or more of the IDREF values supplied in $idrefs. " + "If none is matching or $idrefs is the empty sequence, returns the empty sequence.", new SequenceType[] { - new FunctionParameterSequenceType("idrefs", Type.STRING, Cardinality.ZERO_OR_MORE, "The IDREF sequence") + new FunctionParameterSequenceType("values", Type.STRING, Cardinality.ZERO_OR_MORE, "The IDREF sequence") }, new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.ZERO_OR_MORE, "the elements with IDs matching IDREFs from $idref-sequence")), new FunctionSignature( @@ -61,8 +61,8 @@ public class FunId extends Function { "matching the value of one or more of the IDREF values supplied in $idrefs and is in the same document as $node-in-document. " + "If none is matching or $idrefs is the empty sequence, returns the empty sequence.", new SequenceType[] { - new FunctionParameterSequenceType("idrefs", Type.STRING, Cardinality.ZERO_OR_MORE, "The IDREF sequence"), - new FunctionParameterSequenceType("node-in-document", Type.NODE, Cardinality.EXACTLY_ONE, "The node in document") + new FunctionParameterSequenceType("values", Type.STRING, Cardinality.ZERO_OR_MORE, "The IDREF sequence"), + new FunctionParameterSequenceType("node", Type.NODE, Cardinality.EXACTLY_ONE, "The node in document") }, new FunctionReturnSequenceType(Type.ELEMENT, Cardinality.ZERO_OR_MORE, "the elements with IDs matching IDREFs from $idrefs in the same document as $node-in-document")) }; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIdRef.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIdRef.java index 19637562f7e..b77fee3321f 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIdRef.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIdRef.java @@ -72,7 +72,7 @@ public class FunIdRef extends Function { "value of one or more of the ID values supplied in $ids. " + "If none is matching or $ids is the empty sequence, returns the empty sequence.", new SequenceType[] { - new FunctionParameterSequenceType("ids", Type.STRING, Cardinality.ZERO_OR_MORE, "The ID sequence"), + new FunctionParameterSequenceType("values", Type.STRING, Cardinality.ZERO_OR_MORE, "The ID sequence"), }, new FunctionReturnSequenceType(Type.NODE, Cardinality.ZERO_OR_MORE, "the elements with matching IDREF values from IDs in $ids")), @@ -82,8 +82,8 @@ public class FunIdRef extends Function { "value of one or more of the ID values supplied in $ids. " + "If none is matching or $ids is the empty sequence, returns the empty sequence.", new SequenceType[] { - new FunctionParameterSequenceType("ids", Type.STRING, Cardinality.ZERO_OR_MORE, "The ID sequence"), - new FunctionParameterSequenceType("node-in-document", Type.NODE, Cardinality.EXACTLY_ONE, "The node in document") + new FunctionParameterSequenceType("values", Type.STRING, Cardinality.ZERO_OR_MORE, "The ID sequence"), + new FunctionParameterSequenceType("node", Type.NODE, Cardinality.EXACTLY_ONE, "The node in document") }, new FunctionReturnSequenceType(Type.NODE, Cardinality.ZERO_OR_MORE, "the elements with matching IDREF values from IDs in $ids in the same document as $node-in-document")) }; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIndexOf.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIndexOf.java index a20afb4774a..d804703c9f5 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIndexOf.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunIndexOf.java @@ -52,11 +52,11 @@ public class FunIndexOf extends BasicFunction { protected static final FunctionReturnSequenceType RETURN_TYPE = new FunctionReturnSequenceType(Type.INTEGER, Cardinality.ZERO_OR_MORE, "the sequence of positive integers giving the positions within the sequence"); - protected static final FunctionParameterSequenceType COLLATION_PARAM = new FunctionParameterSequenceType("collation-uri", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI"); + protected static final FunctionParameterSequenceType COLLATION_PARAM = new FunctionParameterSequenceType("collation", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI"); - protected static final FunctionParameterSequenceType SEARCH_PARAM = new FunctionParameterSequenceType("search", Type.ANY_ATOMIC_TYPE, Cardinality.EXACTLY_ONE, "The search component"); + protected static final FunctionParameterSequenceType SEARCH_PARAM = new FunctionParameterSequenceType("target", Type.ANY_ATOMIC_TYPE, Cardinality.EXACTLY_ONE, "The search component"); - protected static final FunctionParameterSequenceType SEQ_PARAM = new FunctionParameterSequenceType("source", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The source sequence"); + protected static final FunctionParameterSequenceType SEQ_PARAM = new FunctionParameterSequenceType("input", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The source sequence"); protected static final String FUNCTION_DESCRIPTION = diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInsertBefore.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInsertBefore.java index 751c7d358d3..91ec4974205 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInsertBefore.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunInsertBefore.java @@ -69,9 +69,9 @@ public class FunInsertBefore extends Function { new QName("insert-before", Function.BUILTIN_FUNCTION_NS), FUNCTION_DESCRIPTION, new SequenceType[] { - new FunctionParameterSequenceType("target", Type.ITEM, Cardinality.ZERO_OR_MORE, "The target"), + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The target"), new FunctionParameterSequenceType("position", Type.INTEGER, Cardinality.EXACTLY_ONE, "The position to insert before"), - new FunctionParameterSequenceType("inserts", Type.ITEM, Cardinality.ZERO_OR_MORE, "The data to insert") + new FunctionParameterSequenceType("insert", Type.ITEM, Cardinality.ZERO_OR_MORE, "The data to insert") }, new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, "the new sequence")); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLang.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLang.java index 49ad0f49061..30391ec14e4 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLang.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLang.java @@ -74,18 +74,18 @@ public class FunLang extends Function { public final static FunctionSignature[] signatures = { new FunctionSignature( - new QName("lang", Function.BUILTIN_FUNCTION_NS), + new QName("language", Function.BUILTIN_FUNCTION_NS), FUNCTION_DESCRIPTION_1_PARAM + FUNCTION_DESCRIPTION_BOTH, new SequenceType[] { - new FunctionParameterSequenceType("lang", Type.STRING, Cardinality.ZERO_OR_ONE, "The language code") + new FunctionParameterSequenceType("language", Type.STRING, Cardinality.ZERO_OR_ONE, "The language code") }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, "true if the language code matches, false otherwise") ), new FunctionSignature( - new QName("lang", Function.BUILTIN_FUNCTION_NS), + new QName("language", Function.BUILTIN_FUNCTION_NS), FUNCTION_DESCRIPTION_2_PARAMS + FUNCTION_DESCRIPTION_BOTH, new SequenceType[] { - new FunctionParameterSequenceType("lang", Type.STRING, Cardinality.ZERO_OR_ONE, "The language code"), + new FunctionParameterSequenceType("language", Type.STRING, Cardinality.ZERO_OR_ONE, "The language code"), new FunctionParameterSequenceType("node", Type.NODE, Cardinality.EXACTLY_ONE, "The node") }, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, "true if the language code matches, false otherwise") diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLocalName.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLocalName.java index a9c0392cbf5..fc3d7788f6d 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLocalName.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunLocalName.java @@ -74,7 +74,7 @@ public class FunLocalName extends Function { new QName("local-name", Function.BUILTIN_FUNCTION_NS), FUNCTION_DESCRIPTION, new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.NODE, Cardinality.ZERO_OR_ONE, "The node to retrieve the local name from") + new FunctionParameterSequenceType("node", Type.NODE, Cardinality.ZERO_OR_ONE, "The node to retrieve the local name from") }, new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, "the local name") ) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.java index 6f06bd772ce..83e438d7309 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMatches.java @@ -61,7 +61,7 @@ */ public final class FunMatches extends Function implements Optimizable, IndexUseReporter { - private static final FunctionParameterSequenceType FS_PARAM_INPUT = optParam("input", Type.STRING, "The input string"); + private static final FunctionParameterSequenceType FS_PARAM_INPUT = optParam("value", Type.STRING, "The input string"); private static final FunctionParameterSequenceType FS_PARAM_PATTERN = param("pattern", Type.STRING, "The pattern"); private static final FunctionParameterSequenceType FS_PARAM_FLAGS = param("flags", Type.STRING, "The flags"); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMax.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMax.java index 31fe3ee95b3..0b45ff3c22e 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMax.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMax.java @@ -95,7 +95,7 @@ public class FunMax extends CollatingFunction { FUNCTION_DESCRIPTION_COMMON_1 + FUNCTION_DESCRIPTION_COMMON_2, new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The input sequence") + new FunctionParameterSequenceType("values", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The input sequence") }, new FunctionReturnSequenceType(Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_ONE, "the max value") ), @@ -104,8 +104,8 @@ public class FunMax extends CollatingFunction { FUNCTION_DESCRIPTION_COMMON_1 + FUNCTION_DESCRIPTION_2_PARAM + FUNCTION_DESCRIPTION_COMMON_2, new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The input sequence"), - new FunctionParameterSequenceType("collation-uri", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI") + new FunctionParameterSequenceType("values", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The input sequence"), + new FunctionParameterSequenceType("collation", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI") }, new FunctionReturnSequenceType(Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_ONE, "the max value") ) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMin.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMin.java index c98ce39133a..512b1659989 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMin.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunMin.java @@ -99,7 +99,7 @@ public class FunMin extends CollatingFunction { new QName("min", Function.BUILTIN_FUNCTION_NS), FUNCTION_DESCRIPTION_COMMON_1 + FUNCTION_DESCRIPTION_COMMON_2, - new SequenceType[] { new FunctionParameterSequenceType("arg", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The input sequence")}, + new SequenceType[] { new FunctionParameterSequenceType("values", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The input sequence")}, new FunctionReturnSequenceType(Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_ONE, "the minimum value") ), new FunctionSignature( @@ -107,8 +107,8 @@ public class FunMin extends CollatingFunction { FUNCTION_DESCRIPTION_COMMON_1 + FUNCTION_DESCRIPTION_2_PARAM + FUNCTION_DESCRIPTION_COMMON_2, new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The input sequence"), - new FunctionParameterSequenceType("collation-uri", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI") + new FunctionParameterSequenceType("values", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The input sequence"), + new FunctionParameterSequenceType("collation", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI") }, new FunctionReturnSequenceType(Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_ONE, "the minimum value") ) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunName.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunName.java index 922cac959b4..23388ceaad2 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunName.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunName.java @@ -80,7 +80,7 @@ public class FunName extends Function { new QName("name", Function.BUILTIN_FUNCTION_NS), FUNCTION_DESCRIPTION_1_PARAM + FUNCTION_DESCRIPTION_COMMON, new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.NODE, Cardinality.ZERO_OR_ONE, "The input node") + new FunctionParameterSequenceType("node", Type.NODE, Cardinality.ZERO_OR_ONE, "The input node") }, new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, "the name") ) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNamespaceURI.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNamespaceURI.java index b89ea61f02f..2422cb60f93 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNamespaceURI.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNamespaceURI.java @@ -77,7 +77,7 @@ public class FunNamespaceURI extends Function { new QName("namespace-uri", Function.BUILTIN_FUNCTION_NS), FUNCTION_DESCRIPTION_1_PARAM + FUNCTION_DESCRIPTION_COMMON, new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.NODE, Cardinality.ZERO_OR_ONE, "The input node") + new FunctionParameterSequenceType("node", Type.NODE, Cardinality.ZERO_OR_ONE, "The input node") }, new FunctionReturnSequenceType(Type.ANY_URI, Cardinality.EXACTLY_ONE, "the namespace URI") ) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNodeName.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNodeName.java index 0059f144872..59e1828de07 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNodeName.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNodeName.java @@ -64,7 +64,7 @@ public class FunNodeName extends Function { "of nodes it returns the empty sequence. If $arg is the empty sequence, the " + "empty sequence is returned.", new SequenceType[]{ - new FunctionParameterSequenceType("arg", Type.NODE, Cardinality.ZERO_OR_ONE, "The input node") + new FunctionParameterSequenceType("node", Type.NODE, Cardinality.ZERO_OR_ONE, "The input node") }, new FunctionReturnSequenceType(Type.QNAME, Cardinality.ZERO_OR_ONE, "the expanded QName")) }; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNormalizeSpace.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNormalizeSpace.java index 8f4fb5e7808..54c201ba61a 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNormalizeSpace.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNormalizeSpace.java @@ -78,7 +78,7 @@ public class FunNormalizeSpace extends Function { new QName("normalize-space", Function.BUILTIN_FUNCTION_NS), FUNCTION_DESCRIPTION_1_PARAM + FUNCTION_DESCRIPTION_COMMON_1 + FUNCTION_DESCRIPTION_1_PARAM_1 + FUNCTION_DESCRIPTION_COMMON_2, - new SequenceType[]{new FunctionParameterSequenceType("arg", Type.STRING, Cardinality.ZERO_OR_ONE, "The string to normalize")}, + new SequenceType[]{new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The string to normalize")}, RETURN_TYPE ) }; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNormalizeUnicode.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNormalizeUnicode.java index 5a6038037d2..ceb850699d8 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNormalizeUnicode.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNormalizeUnicode.java @@ -84,8 +84,8 @@ public class FunNormalizeUnicode extends Function { "If the effective value of the $normalization-form is other than one of the values " + "supported by the implementation, then an error is raised [err:FOCH0003]."; - protected static final FunctionParameterSequenceType ARG_PARAM = new FunctionParameterSequenceType("arg", Type.STRING, Cardinality.ZERO_OR_ONE, "The unicode string to normalize"); - protected static final FunctionParameterSequenceType NF_PARAM = new FunctionParameterSequenceType("normalization-form", Type.STRING, Cardinality.EXACTLY_ONE, "The normalization form"); + protected static final FunctionParameterSequenceType ARG_PARAM = new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The unicode string to normalize"); + protected static final FunctionParameterSequenceType NF_PARAM = new FunctionParameterSequenceType("form", Type.STRING, Cardinality.EXACTLY_ONE, "The normalization form"); protected static final FunctionReturnSequenceType RETURN_TYPE = new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, "the normalized text"); public final static FunctionSignature[] signatures = { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNot.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNot.java index 64c1389563f..5d7a33f5783 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNot.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNot.java @@ -51,7 +51,7 @@ public class FunNot extends Function { "value is false, and false if the effective boolean value is true. \n\n $arg is reduced to an effective boolean value by applying " + "the fn:boolean() function.", new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ITEM, Cardinality.ZERO_OR_MORE, "The input items")}, + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The input items")}, new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, "the negated effective boolean value (ebv) of $arg")); @SuppressWarnings("unused") diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNumber.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNumber.java index 3ceb116edd4..28276f7f770 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNumber.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunNumber.java @@ -67,7 +67,7 @@ public class FunNumber extends Function { "following the rules of 17.1.3.2 Casting to xs:double. If the conversion " + "to xs:double fails, the xs:double value NaN is returned."; - protected static final FunctionParameterSequenceType ARG_PARAM = new FunctionParameterSequenceType("arg", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_ONE, "The input item"); + protected static final FunctionParameterSequenceType ARG_PARAM = new FunctionParameterSequenceType("value", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_ONE, "The input item"); protected static final FunctionReturnSequenceType RETURN_TYPE = new FunctionReturnSequenceType(Type.DOUBLE, Cardinality.EXACTLY_ONE, "the numerical value"); public final static FunctionSignature[] signatures = { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunOneOrMore.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunOneOrMore.java index 7acb1986026..d483151faab 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunOneOrMore.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunOneOrMore.java @@ -50,7 +50,7 @@ public class FunOneOrMore extends Function { "Returns $arg if it contains one or more items. Otherwise, " + "raises an error.", new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ITEM, Cardinality.ZERO_OR_MORE, "The input sequence") + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The input sequence") }, new FunctionReturnSequenceType(Type.ITEM, Cardinality.ONE_OR_MORE, "the sequence passed in by $arg if it contains one or more items.")); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReplace.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReplace.java index 6dea523469a..5e727237fc2 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReplace.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReplace.java @@ -70,7 +70,7 @@ public class FunReplace extends BasicFunction { "included \"as is\" in the replacement string, and the rules are reapplied using the number N " + "formed by stripping off this last digit."; - private static final FunctionParameterSequenceType FS_TOKENIZE_PARAM_INPUT = optParam("input", Type.STRING, "The input string"); + private static final FunctionParameterSequenceType FS_TOKENIZE_PARAM_INPUT = optParam("value", Type.STRING, "The input string"); private static final FunctionParameterSequenceType FS_TOKENIZE_PARAM_PATTERN = param("pattern", Type.STRING, "The pattern to match"); private static final FunctionParameterSequenceType FS_TOKENIZE_PARAM_REPLACEMENT = param("replacement", Type.STRING, "The string to replace the pattern with"); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunResolveURI.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunResolveURI.java index 150963daad2..77004a3ae22 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunResolveURI.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunResolveURI.java @@ -68,7 +68,7 @@ public class FunResolveURI extends Function { "is raised [err:FORG0002].\n\n" + "If $relative is the empty sequence, the empty sequence is returned."; - protected static final FunctionParameterSequenceType RELATIVE_ARG = new FunctionParameterSequenceType("relative", Type.STRING, Cardinality.ZERO_OR_ONE, "The relative URI"); + protected static final FunctionParameterSequenceType RELATIVE_ARG = new FunctionParameterSequenceType("href", Type.STRING, Cardinality.ZERO_OR_ONE, "The relative URI"); protected static final FunctionParameterSequenceType BASE_ARG = new FunctionParameterSequenceType("base", Type.STRING, Cardinality.EXACTLY_ONE, "The base URI"); protected static final FunctionReturnSequenceType RETURN_TYPE = new FunctionReturnSequenceType(Type.ANY_URI, Cardinality.ZERO_OR_ONE, "the absolute URI"); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReverse.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReverse.java index 0774d01ccdb..936451f96b1 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReverse.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunReverse.java @@ -44,7 +44,7 @@ public class FunReverse extends Function { new QName("reverse", Function.BUILTIN_FUNCTION_NS), "Reverses the order of items in a sequence. If the argument is an empty" + "sequence, the empty sequence is returned.", - new SequenceType[] {new FunctionParameterSequenceType("arg", Type.ITEM, Cardinality.ZERO_OR_MORE, "The sequence to reverse")}, + new SequenceType[] {new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The sequence to reverse")}, new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, "the reverse order sequence")); public FunReverse(XQueryContext context) { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunRoot.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunRoot.java index f6316a175ba..6adbf0d3671 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunRoot.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunRoot.java @@ -71,7 +71,7 @@ public class FunRoot extends Function { new FunctionSignature( new QName("root", Function.BUILTIN_FUNCTION_NS), FUNCTION_DESCRIPTION_1_PARAM, - new SequenceType[]{new FunctionParameterSequenceType("arg", Type.NODE, Cardinality.ZERO_OR_ONE, "The input node")}, + new SequenceType[]{new FunctionParameterSequenceType("node", Type.NODE, Cardinality.ZERO_OR_ONE, "The input node")}, new FunctionReturnSequenceType(Type.NODE, Cardinality.ZERO_OR_ONE, "the root node of the tree to which $arg belongs") ) }; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSerialize.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSerialize.java index 24d6c89ddf6..99c820621fb 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSerialize.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSerialize.java @@ -51,7 +51,7 @@ public class FunSerialize extends BasicFunction { "This function serializes the supplied input sequence $arg as described in XSLT and XQuery Serialization 3.0, returning the " + "serialized representation of the sequence as a string.", new SequenceType[] { - new FunctionParameterSequenceType("args", Type.ITEM, Cardinality.ZERO_OR_MORE, "The node set to serialize") + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The node set to serialize") }, new FunctionParameterSequenceType("result", Type.STRING, Cardinality.EXACTLY_ONE, "the string containing the serialized node set.") ), @@ -60,8 +60,8 @@ public class FunSerialize extends BasicFunction { "This function serializes the supplied input sequence $arg as described in XSLT and XQuery Serialization 3.0, returning the " + "serialized representation of the sequence as a string.", new SequenceType[] { - new FunctionParameterSequenceType("args", Type.ITEM, Cardinality.ZERO_OR_MORE, "The node set to serialize"), - new FunctionParameterSequenceType("parameters", Type.ITEM, Cardinality.ZERO_OR_ONE, "The serialization parameters as either a output:serialization-parameters element or a map") + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The node set to serialize"), + new FunctionParameterSequenceType("options", Type.ITEM, Cardinality.ZERO_OR_ONE, "The serialization parameters as either a output:serialization-parameters element or a map") }, new FunctionParameterSequenceType("result", Type.STRING, Cardinality.EXACTLY_ONE, "the string containing the serialized node set.") ) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStartsWith.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStartsWith.java index beddb98cdbe..0d1dca72384 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStartsWith.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStartsWith.java @@ -60,9 +60,9 @@ public class FunStartsWith extends CollatingFunction { "If the specified collation does not support collation " + "units an error may be raised [err:FOCH0004]. "; - protected static final FunctionParameterSequenceType ARG1_PARAM = new FunctionParameterSequenceType("source", Type.STRING, Cardinality.ZERO_OR_ONE, "The source string"); - protected static final FunctionParameterSequenceType ARG2_PARAM = new FunctionParameterSequenceType("prefix", Type.STRING, Cardinality.ZERO_OR_ONE, "The string to determine if is a prefix of $source"); - protected static final FunctionParameterSequenceType COLLATION_PARAM = new FunctionParameterSequenceType("collation-uri", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI"); + protected static final FunctionParameterSequenceType ARG1_PARAM = new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The source string"); + protected static final FunctionParameterSequenceType ARG2_PARAM = new FunctionParameterSequenceType("substring", Type.STRING, Cardinality.ZERO_OR_ONE, "The string to determine if is a prefix of $source"); + protected static final FunctionParameterSequenceType COLLATION_PARAM = new FunctionParameterSequenceType("collation", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI"); protected static final FunctionReturnSequenceType RETURN_TYPE = new FunctionReturnSequenceType(Type.BOOLEAN, Cardinality.EXACTLY_ONE, "true if $prefix is a prefix of the string $source"); public final static FunctionSignature[] signatures = { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStrLength.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStrLength.java index dbc3eac6b3e..706ed7a5c59 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStrLength.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStrLength.java @@ -55,7 +55,7 @@ public class FunStrLength extends Function { "If the value of $arg is the empty sequence, the xs:integer 0 is returned.\n" + "If no argument is supplied, $arg defaults to the string value (calculated using fn:string()) of the context item (.). If no argument is supplied or if the argument is the context item and the context item is undefined an error is raised", new SequenceType[]{ - new FunctionParameterSequenceType("arg", Type.STRING, Cardinality.ZERO_OR_ONE, "The input string") + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The input string") }, new FunctionReturnSequenceType(Type.INTEGER, Cardinality.EXACTLY_ONE, "the length in characters") ) diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunString.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunString.java index af79dd4ac02..c81469b4b10 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunString.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunString.java @@ -58,7 +58,7 @@ public class FunString extends Function { "If the value of $arg is the empty sequence, the zero-length string is returned. " + "If the context item of $arg is undefined, an error is raised.", new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ITEM, Cardinality.ZERO_OR_ONE, "The sequence to get the value of as an xs:string")}, + new FunctionParameterSequenceType("value", Type.ITEM, Cardinality.ZERO_OR_ONE, "The sequence to get the value of as an xs:string")}, new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, "the value of $arg as an xs:string") ) }; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStringJoin.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStringJoin.java index dfd3c4e639c..71a63a21233 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStringJoin.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStringJoin.java @@ -55,7 +55,7 @@ public class FunStringJoin extends BasicFunction { "The effect of calling the single-argument version of this function is the same as calling the " + "two-argument version with $separator set to a zero-length string.", new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, + new FunctionParameterSequenceType("values", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The sequence to be joined to form the string. If it is empty, " + "a zero-length string is returned.") }, @@ -66,7 +66,7 @@ public class FunStringJoin extends BasicFunction { "$arg sequence using $separator as a separator. If the value of the separator is the zero-length " + "string, then the members of the sequence are concatenated without a separator.", new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, + new FunctionParameterSequenceType("values", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The sequence to be joined to form the string. If it is empty, " + "a zero-length string is returned."), new FunctionParameterSequenceType("separator", Type.STRING, Cardinality.EXACTLY_ONE, "The separator to be placed in the string between the items of $arg") diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStringToCodepoints.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStringToCodepoints.java index 9b11b8ca6c5..9b40beeccac 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStringToCodepoints.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunStringToCodepoints.java @@ -41,7 +41,7 @@ public class FunStringToCodepoints extends BasicFunction { "Returns the sequence of unicode code points that constitute an xs:string. If $arg is a zero-length " + "string or the empty sequence, the empty sequence is returned.", new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.STRING, Cardinality.ZERO_OR_ONE, "The input string"), + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The input string"), }, new FunctionReturnSequenceType(Type.INTEGER, Cardinality.ZERO_OR_MORE, "the sequence of code points")); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubSequence.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubSequence.java index e66fbc857ec..88375870e7c 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubSequence.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubSequence.java @@ -41,8 +41,8 @@ public class FunSubSequence extends Function { + "items starting at the position, $starting-at, " + "up to the end of the sequence are included.", new SequenceType[]{ - new FunctionParameterSequenceType("source", Type.ITEM, Cardinality.ZERO_OR_MORE, "The source sequence"), - new FunctionParameterSequenceType("starting-at", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The starting position in the $source") + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The source sequence"), + new FunctionParameterSequenceType("start", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The starting position in the $source") }, new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, "the subsequence")), new FunctionSignature( @@ -51,8 +51,8 @@ public class FunSubSequence extends Function { + "starting at the position, $starting-at, " + "including the number of items indicated by $length.", new SequenceType[]{ - new FunctionParameterSequenceType("source", Type.ITEM, Cardinality.ZERO_OR_MORE, "The source sequence"), - new FunctionParameterSequenceType("starting-at", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The starting position in the $source"), + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The source sequence"), + new FunctionParameterSequenceType("start", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The starting position in the $source"), new FunctionParameterSequenceType("length", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The length of the subsequence") }, new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, "the subsequence"))}; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubstring.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubstring.java index 5c87b3792be..fe48962bf60 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubstring.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubstring.java @@ -58,8 +58,8 @@ public class FunSubstring extends Function { "is zero or negative, only those characters in positions greater than zero are returned." + "If the value of $source is the empty sequence, the zero-length string is returned.", new SequenceType[] { - new FunctionParameterSequenceType("source", Type.STRING, Cardinality.ZERO_OR_ONE, "The source string"), - new FunctionParameterSequenceType("starting-at", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The starting position") + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The source string"), + new FunctionParameterSequenceType("start", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The starting position") }, new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, "the substring") ), @@ -70,8 +70,8 @@ public class FunSubstring extends Function { "beyond the end of $source. If $starting-at is zero or negative, only those characters in positions greater " + "than zero are returned. If the value of $source is the empty sequence, the zero-length string is returned.", new SequenceType[] { - new FunctionParameterSequenceType("source", Type.STRING, Cardinality.ZERO_OR_ONE, "The source string"), - new FunctionParameterSequenceType("starting-at", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The starting position"), + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The source string"), + new FunctionParameterSequenceType("start", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The starting position"), new FunctionParameterSequenceType("length", Type.DOUBLE, Cardinality.EXACTLY_ONE, "The number of characters in the substring") }, new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, "the substring") diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubstringAfter.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubstringAfter.java index 84faa6d2a10..12ad0f104fd 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubstringAfter.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubstringAfter.java @@ -47,9 +47,9 @@ */ public class FunSubstringAfter extends CollatingFunction { - protected static final FunctionParameterSequenceType COLLATION_ARG = new FunctionParameterSequenceType("collation-uri", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI"); - protected static final FunctionParameterSequenceType SEARCH_ARG = new FunctionParameterSequenceType("search", Type.STRING, Cardinality.ZERO_OR_ONE, "The search string"); - protected static final FunctionParameterSequenceType SOURCE_ARG = new FunctionParameterSequenceType("source", Type.STRING, Cardinality.ZERO_OR_ONE, "The input string"); + protected static final FunctionParameterSequenceType COLLATION_ARG = new FunctionParameterSequenceType("collation", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI"); + protected static final FunctionParameterSequenceType SEARCH_ARG = new FunctionParameterSequenceType("substring", Type.STRING, Cardinality.ZERO_OR_ONE, "The search string"); + protected static final FunctionParameterSequenceType SOURCE_ARG = new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The input string"); public final static FunctionSignature[] signatures = { new FunctionSignature( new QName("substring-after", Function.BUILTIN_FUNCTION_NS), diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubstringBefore.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubstringBefore.java index 914a1c6953a..78310be30a2 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubstringBefore.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSubstringBefore.java @@ -47,9 +47,9 @@ */ public class FunSubstringBefore extends CollatingFunction { - protected static final FunctionParameterSequenceType COLLATOR_ARG = new FunctionParameterSequenceType("collation-uri", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI"); - protected static final FunctionParameterSequenceType SEARCH_ARG = new FunctionParameterSequenceType("search", Type.STRING, Cardinality.ZERO_OR_ONE, "The search string"); - protected static final FunctionParameterSequenceType SOURCE_ARG = new FunctionParameterSequenceType("source", Type.STRING, Cardinality.ZERO_OR_ONE, "The input string"); + protected static final FunctionParameterSequenceType COLLATOR_ARG = new FunctionParameterSequenceType("collation", Type.STRING, Cardinality.EXACTLY_ONE, "The collation URI"); + protected static final FunctionParameterSequenceType SEARCH_ARG = new FunctionParameterSequenceType("substring", Type.STRING, Cardinality.ZERO_OR_ONE, "The search string"); + protected static final FunctionParameterSequenceType SOURCE_ARG = new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The input string"); public final static FunctionSignature[] signatures = { new FunctionSignature( new QName("substring-before", Function.BUILTIN_FUNCTION_NS), diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSum.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSum.java index d333af4718a..d883e965a77 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSum.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunSum.java @@ -55,7 +55,7 @@ public class FunSum extends Function { "Returns a value obtained by adding together the values in $arg. " + "If $arg is the the empty sequence the xs:double value 0.0e0 is returned.", new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The sequence of numbers to be summed up")}, + new FunctionParameterSequenceType("values", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The sequence of numbers to be summed up")}, new FunctionReturnSequenceType(Type.ANY_ATOMIC_TYPE, Cardinality.EXACTLY_ONE, "the sum of all numbers in $arg") ), new FunctionSignature( @@ -63,7 +63,7 @@ public class FunSum extends Function { "Returns a value obtained by adding together the values in $arg. " + "If $arg is the the empty sequence then $default is returned.", new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The sequence of numbers to be summed up"), + new FunctionParameterSequenceType("values", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_MORE, "The sequence of numbers to be summed up"), new FunctionParameterSequenceType("default", Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_ONE, "The default value if $arg computes to the empty sequence") }, new FunctionReturnSequenceType(Type.ANY_ATOMIC_TYPE, Cardinality.ZERO_OR_ONE, "the sum of all numbers in $arg") diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunTokenize.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunTokenize.java index f31b8b645f0..c67e2fffbf2 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunTokenize.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunTokenize.java @@ -44,7 +44,7 @@ public class FunTokenize extends BasicFunction { private static final QName FS_TOKENIZE_NAME = new QName("tokenize", Function.BUILTIN_FUNCTION_NS); - private final static FunctionParameterSequenceType FS_TOKENIZE_PARAM_INPUT = optParam("input", Type.STRING, "The input string"); + private final static FunctionParameterSequenceType FS_TOKENIZE_PARAM_INPUT = optParam("value", Type.STRING, "The input string"); private final static FunctionParameterSequenceType FS_TOKENIZE_PARAM_PATTERN = param("pattern", Type.STRING, "The tokenization pattern"); public final static FunctionSignature[] FS_TOKENIZE = functionSignatures( diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunTrace.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunTrace.java index e6c78e43828..c7f8c6f3190 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunTrace.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunTrace.java @@ -42,7 +42,7 @@ public class FunTrace extends BasicFunction { - private static final FunctionParameterSequenceType FS_PARAM_VALUE = optManyParam("value", Type.ITEM, "The values"); + private static final FunctionParameterSequenceType FS_PARAM_VALUE = optManyParam("input", Type.ITEM, "The values"); private static final FunctionParameterSequenceType FS_PARAM_LABEL = param("label", Type.STRING, "The label in the log file"); private static final String FS_TRACE_NAME = "trace"; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunTranslate.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunTranslate.java index c13a449b0e5..fc2151c201b 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunTranslate.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunTranslate.java @@ -58,9 +58,9 @@ public class FunTranslate extends Function { "the replacement character. If $trans is longer than $map, the excess characters are ignored.\n\n" + "i.e. fn:translate(\"bar\",\"abc\",\"ABC\") returns \"BAr\"", new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.STRING, Cardinality.ZERO_OR_ONE, "The string to be translated"), - new FunctionParameterSequenceType("map", Type.STRING, Cardinality.EXACTLY_ONE, "The map string"), - new FunctionParameterSequenceType("trans", Type.STRING, Cardinality.EXACTLY_ONE, "The translation string") + new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The string to be translated"), + new FunctionParameterSequenceType("replace", Type.STRING, Cardinality.EXACTLY_ONE, "The map string"), + new FunctionParameterSequenceType("with", Type.STRING, Cardinality.EXACTLY_ONE, "The translation string") }, new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, "the translated string")); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUnordered.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUnordered.java index 4d4c448fdcf..790eedeb97b 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUnordered.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUnordered.java @@ -52,7 +52,7 @@ public class FunUnordered extends Function { "Takes a sequence $arg as input and returns an arbitrary implementation dependent permutation " + "of it. Currently, this has no effect in eXist, but it might be used for future optimizations.", new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ITEM, Cardinality.ZERO_OR_MORE, "The input sequence") + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The input sequence") }, new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, "the input sequence in an arbitrary implementation dependent permutation")); diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUpperOrLowerCase.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUpperOrLowerCase.java index 6a5f576a98a..3816c2b68b7 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUpperOrLowerCase.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUpperOrLowerCase.java @@ -46,14 +46,14 @@ public class FunUpperOrLowerCase extends Function { new FunctionSignature( new QName("upper-case", Function.BUILTIN_FUNCTION_NS), "Returns the value of $arg after translating every character to its upper-case correspondent as defined in the appropriate case mappings section in the Unicode standard. For versions of Unicode beginning with the 2.1.8 update, only locale-insensitive case mappings should be applied. Beginning with version 3.2.0 (and likely future versions) of Unicode, precise mappings are described in default case operations, which are full case mappings in the absence of tailoring for particular languages and environments. Every lower-case character that does not have an upper-case correspondent, as well as every upper-case character, is included in the returned value in its original form.", - new SequenceType[] { new FunctionParameterSequenceType("arg", Type.STRING, Cardinality.ZERO_OR_ONE, "The text to be converted to all upper-case characters") }, + new SequenceType[] { new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The text to be converted to all upper-case characters") }, new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, "the resulting upper-case text")); public final static FunctionSignature fnLowerCase = new FunctionSignature( new QName("lower-case", Function.BUILTIN_FUNCTION_NS), "Returns the value of $arg after translating every character to its lower-case correspondent as defined in the appropriate case mappings section in the Unicode standard. For versions of Unicode beginning with the 2.1.8 update, only locale-insensitive case mappings should be applied. Beginning with version 3.2.0 (and likely future versions) of Unicode, precise mappings are described in default case operations, which are full case mappings in the absence of tailoring for particular languages and environments. Every upper-case character that does not have a lower-case correspondent, as well as every lower-case character, is included in the returned value in its original form.", - new SequenceType[] { new FunctionParameterSequenceType("arg", Type.STRING, Cardinality.ZERO_OR_ONE, "The text to be converted to all lower-case characters") }, + new SequenceType[] { new FunctionParameterSequenceType("value", Type.STRING, Cardinality.ZERO_OR_ONE, "The text to be converted to all lower-case characters") }, new FunctionReturnSequenceType(Type.STRING, Cardinality.EXACTLY_ONE, "the resulting lower-case text")); public FunUpperOrLowerCase(XQueryContext context, FunctionSignature signature) { diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUriCollection.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUriCollection.java index 3b9426af31e..3a296a88c77 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUriCollection.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunUriCollection.java @@ -48,7 +48,7 @@ public class FunUriCollection extends BasicFunction { private static final FunctionReturnSequenceType FN_RETURN = returnsOptMany(Type.ANY_URI, "the default URI collection, if $arg is not specified or is an empty sequence, " + "or the sequence of URIs that correspond to the supplied URI"); - private static final FunctionParameterSequenceType ARG = optParam("arg", Type.STRING, + private static final FunctionParameterSequenceType ARG = optParam("source", Type.STRING, "An xs:string identifying a URI Collection. " + "The argument is interpreted as either an absolute xs:anyURI, or a relative xs:anyURI resolved " + "against the base-URI property from the static context. In eXist-db this function consults the " + diff --git a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunZeroOrOne.java b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunZeroOrOne.java index ceeb5697425..4d2b8a442ab 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/fn/FunZeroOrOne.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/fn/FunZeroOrOne.java @@ -50,7 +50,7 @@ public class FunZeroOrOne extends Function { "Returns the argument sequence $arg if it contains zero or one items. Otherwise, " + "raises an error.", new SequenceType[] { - new FunctionParameterSequenceType("arg", Type.ITEM, Cardinality.ZERO_OR_MORE, "The sequence to be tested for cardinality") + new FunctionParameterSequenceType("input", Type.ITEM, Cardinality.ZERO_OR_MORE, "The sequence to be tested for cardinality") }, new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_ONE, "the input sequence if it contains zero or one items.")); From 4e3f10d63a8683be6cbae7d1accb8cf7d0d6740c Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Sun, 26 Apr 2026 03:53:32 -0400 Subject: [PATCH 08/20] [feature] Add XQuery 4.0 bare map syntax and content expressions XQuery 4.0 (PR1071) allows map constructors without the 'map' keyword: { "a": 1, "b": 2 } and content expressions that merge maps into the constructor: { {"a": 1}, {"b": 2}, "c": 3 } Changes: - Version-gate bare map syntax with xq4Enabled in primaryExpr and arrowFunctionSpecifier so { } only parses as a map in XQ4 mode - Add MAP_CONTENT imaginary token for content expressions - Replace mapAssignment with mapContentExpr rule supporting both key:value entries and content expressions (wrapped in MAP_CONTENT) - Update tree walker to handle MAP_CONTENT AST nodes - Extend MapExpr with Entry interface, Mapping and ContentEntry types to evaluate content expressions (must be maps, merged at runtime) - Add XQSuite tests for empty content, merge, and XPTY0004 errors Co-Authored-By: Claude Opus 4.6 (1M context) --- .../antlr/org/exist/xquery/parser/XQuery.g | 24 ++- .../org/exist/xquery/parser/XQueryTree.g | 10 ++ .../exist/xquery/functions/map/MapExpr.java | 156 ++++++++++++++---- .../src/test/xquery/xquery4/fnXQuery40.xql | 33 ++++ 4 files changed, 179 insertions(+), 44 deletions(-) diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g index a670bf3cb69..67d2dbde110 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g @@ -217,6 +217,7 @@ imaginaryTokenDefinitions DESTRUCTURE_VAR_TYPE RECORD_TEST RECORD_FIELD + MAP_CONTENT ; // === XPointer === @@ -1730,7 +1731,7 @@ arrowFunctionSpecifier throws XPathException ( "map" LCURLY ) => mapConstructor | // XQ4: bare map constructor as function - ( LCURLY ) => bareMapConstructor + ( { xq4Enabled }? LCURLY ) => bareMapConstructor | // XQ4: array constructor as function ( LPPAREN | ("array" LCURLY) ) => arrayConstructor @@ -1870,9 +1871,9 @@ primaryExpr throws XPathException | ( "map" LCURLY ) => mapConstructor | - ( LCURLY RCURLY ) => bareMapConstructor + ( { xq4Enabled }? LCURLY RCURLY ) => bareMapConstructor | - ( LCURLY exprSingle COLON ) => bareMapConstructor + ( { xq4Enabled }? LCURLY exprSingle COLON ) => bareMapConstructor | directConstructor | @@ -1947,7 +1948,7 @@ stringTemplateInterpolation throws XPathException mapConstructor throws XPathException : - a:"map"! LCURLY! ( mapAssignment ( COMMA! mapAssignment )* )? RCURLY! + a:"map"! LCURLY! ( mapContentExpr ( COMMA! mapContentExpr )* )? RCURLY! { #mapConstructor = #(#[MAP, "map"], #mapConstructor); #mapConstructor.copyLexInfo(#a); @@ -1956,14 +1957,15 @@ mapConstructor throws XPathException bareMapConstructor throws XPathException : - lc:LCURLY! ( mapAssignment ( COMMA! mapAssignment )* )? RCURLY! + lc:LCURLY! ( mapContentExpr ( COMMA! mapContentExpr )* )? RCURLY! { #bareMapConstructor = #(#[MAP, "map"], #bareMapConstructor); #bareMapConstructor.copyLexInfo(#lc); } ; -mapAssignment throws XPathException +// XQ4: map content expressions - either a key:value entry or a content expression (must evaluate to a map) +mapContentExpr throws XPathException : (exprSingle COLON! EQ!) => exprSingle COLON^ eq:EQ^ exprSingle { @@ -1971,8 +1973,14 @@ mapAssignment throws XPathException "The ':=' notation is no longer accepted in map expressions: use ':' instead."); } | - exprSingle COLON^ exprSingle - ; + (exprSingle COLON) => exprSingle COLON^ exprSingle + | + mc:exprSingle + { + #mapContentExpr = #(#[MAP_CONTENT, null], #mapContentExpr); + #mapContentExpr.copyLexInfo(#mc); + } + ; arrayConstructor throws XPathException : diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g index 0740916eda3..07b9ed3bd49 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g @@ -5062,6 +5062,16 @@ throws XPathException, PermissionDeniedException, EXistException expr[value] { expr.map(key, value); } ) + | + #( + MAP_CONTENT + { + PathExpr content = new PathExpr(context); + content.setASTNode(mapConstr_AST_in); + } + expr[content] + { expr.content(content); } + ) )* ) ; diff --git a/exist-core/src/main/java/org/exist/xquery/functions/map/MapExpr.java b/exist-core/src/main/java/org/exist/xquery/functions/map/MapExpr.java index def13868b4d..786559171ec 100644 --- a/exist-core/src/main/java/org/exist/xquery/functions/map/MapExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/functions/map/MapExpr.java @@ -21,6 +21,7 @@ */ package org.exist.xquery.functions.map; +import io.lacuna.bifurcan.IEntry; import io.lacuna.bifurcan.IMap; import org.exist.xquery.*; import org.exist.xquery.util.ExpressionDumper; @@ -39,15 +40,18 @@ */ public class MapExpr extends AbstractExpression { - private final List mappings = new ArrayList<>(13); + private final List entries = new ArrayList<>(13); public MapExpr(final XQueryContext context) { super(context); } public void map(final PathExpr key, final PathExpr value) { - final Mapping mapping = new Mapping(key.simplify(), value.simplify()); - this.mappings.add(mapping); + this.entries.add(new Mapping(key.simplify(), value.simplify())); + } + + public void content(final PathExpr expr) { + this.entries.add(new ContentEntry(expr.simplify())); } @Override @@ -57,9 +61,8 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException "Map is not available before XQuery 3.0"); } contextInfo.setParent(this); - for (final Mapping mapping : this.mappings) { - mapping.key.analyze(contextInfo); - mapping.value.analyze(contextInfo); + for (final Entry entry : this.entries) { + entry.analyze(contextInfo); } } @@ -73,25 +76,55 @@ public Sequence eval(Sequence contextSequence, final Item contextItem) throws XP boolean firstType = true; int prevType = AbstractMapType.UNKNOWN_KEY_TYPE; - for (final Mapping mapping : this.mappings) { - final Sequence key = mapping.key.eval(contextSequence, null); - if (key.getItemCount() != 1) { - throw new XPathException(this, MapErrorCode.EXMPDY001, "Expected single value for key, got " + key.getItemCount()); - } - final AtomicValue atomic = key.itemAt(0).atomize(); - final Sequence value = mapping.value.eval(contextSequence, null); - if (map.contains(atomic)) { - throw new XPathException(this, ErrorCodes.XQDY0137, "Key \"" + atomic.getStringValue() + "\" already exists in map."); - } - map.put(atomic, value); - - final int thisType = atomic.getType(); - if (firstType) { - prevType = thisType; - firstType = false; - } else { - if (thisType != prevType) { - prevType = AbstractMapType.MIXED_KEY_TYPES; + for (final Entry entry : this.entries) { + if (entry instanceof Mapping mapping) { + final Sequence key = mapping.key.eval(contextSequence, null); + if (key.getItemCount() != 1) { + throw new XPathException(this, MapErrorCode.EXMPDY001, "Expected single value for key, got " + key.getItemCount()); + } + final AtomicValue atomic = key.itemAt(0).atomize(); + final Sequence value = mapping.value.eval(contextSequence, null); + if (map.contains(atomic)) { + throw new XPathException(this, ErrorCodes.XQDY0137, "Key \"" + atomic.getStringValue() + "\" already exists in map."); + } + map.put(atomic, value); + + final int thisType = atomic.getType(); + if (firstType) { + prevType = thisType; + firstType = false; + } else { + if (thisType != prevType) { + prevType = AbstractMapType.MIXED_KEY_TYPES; + } + } + } else if (entry instanceof ContentEntry contentEntry) { + final Sequence result = contentEntry.expr.eval(contextSequence, null); + // content expression must evaluate to zero or more maps + for (int i = 0; i < result.getItemCount(); i++) { + final Item item = result.itemAt(i); + if (item.getType() != Type.MAP_ITEM && !(item instanceof AbstractMapType)) { + throw new XPathException(this, ErrorCodes.XPTY0004, + "Content expression in map constructor must be a map, got " + Type.getTypeName(item.getType())); + } + final AbstractMapType contentMap = (AbstractMapType) item; + for (final IEntry mapEntry : contentMap) { + final AtomicValue atomic = mapEntry.key(); + if (map.contains(atomic)) { + throw new XPathException(this, ErrorCodes.XQDY0137, "Key \"" + atomic.getStringValue() + "\" already exists in map."); + } + map.put(atomic, mapEntry.value()); + + final int thisType = atomic.getType(); + if (firstType) { + prevType = thisType; + firstType = false; + } else { + if (thisType != prevType) { + prevType = AbstractMapType.MIXED_KEY_TYPES; + } + } + } } } } @@ -107,23 +140,19 @@ public int returnsType() { @Override public void accept(final ExpressionVisitor visitor) { super.accept(visitor); - for (final Mapping mapping : this.mappings) { - mapping.key.accept(visitor); - mapping.value.accept(visitor); + for (final Entry entry : this.entries) { + entry.accept(visitor); } } @Override public void dump(final ExpressionDumper dumper) { dumper.display("map {"); - for (int i = 0; i < this.mappings.size(); i++) { - final Mapping mapping = this.mappings.get(i); + for (int i = 0; i < this.entries.size(); i++) { if (i > 0) { dumper.display(", "); } - mapping.key.dump(dumper); - dumper.display(" : "); - mapping.value.dump(dumper); + this.entries.get(i).dump(dumper); } dumper.display("}"); } @@ -136,10 +165,17 @@ public String toString() { @Override public void resetState(final boolean postOptimization) { super.resetState(postOptimization); - mappings.forEach(m -> m.resetState(postOptimization)); + entries.forEach(e -> e.resetState(postOptimization)); } - private static class Mapping { + private interface Entry { + void analyze(AnalyzeContextInfo contextInfo) throws XPathException; + void accept(ExpressionVisitor visitor); + void dump(ExpressionDumper dumper); + void resetState(boolean postOptimization); + } + + private static class Mapping implements Entry { final Expression key; final Expression value; @@ -148,9 +184,57 @@ public Mapping(final Expression key, final Expression value) { this.value = value; } - private void resetState(final boolean postOptimization) { + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + key.analyze(contextInfo); + value.analyze(contextInfo); + } + + @Override + public void accept(final ExpressionVisitor visitor) { + key.accept(visitor); + value.accept(visitor); + } + + @Override + public void dump(final ExpressionDumper dumper) { + key.dump(dumper); + dumper.display(" : "); + value.dump(dumper); + } + + @Override + public void resetState(final boolean postOptimization) { key.resetState(postOptimization); value.resetState(postOptimization); } } + + private static class ContentEntry implements Entry { + final Expression expr; + + public ContentEntry(final Expression expr) { + this.expr = expr; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + expr.analyze(contextInfo); + } + + @Override + public void accept(final ExpressionVisitor visitor) { + expr.accept(visitor); + } + + @Override + public void dump(final ExpressionDumper dumper) { + expr.dump(dumper); + } + + @Override + public void resetState(final boolean postOptimization) { + expr.resetState(postOptimization); + } + } } diff --git a/exist-core/src/test/xquery/xquery4/fnXQuery40.xql b/exist-core/src/test/xquery/xquery4/fnXQuery40.xql index e50451ea87e..98fa604174a 100644 --- a/exist-core/src/test/xquery/xquery4/fnXQuery40.xql +++ b/exist-core/src/test/xquery/xquery4/fnXQuery40.xql @@ -240,6 +240,39 @@ function t:bare-map-after-return() { return $m(1) }; +(: content expressions in map constructors :) + +declare + %test:assertEquals(0) +function t:bare-map-empty-content() { + map:size({ {}, {}, {} }) +}; + +declare + %test:assertEquals(2) +function t:bare-map-content-merge() { + map:size({ {"a": 1}, {"b": 2} }) +}; + +declare + %test:assertEquals(1) +function t:bare-map-content-single() { + { {"a": 1} }("a") +}; + +declare + %test:assertError("XPTY0004") +function t:bare-map-content-non-map-error() { + { "a": 1, "b" } +}; + +declare + %test:assertEquals(5) +function t:bare-map-content-sequence() { + let $maps := ((1 to 5) ! {2*.: .*.}) + return map:size({ $maps }) +}; + (: ========== Braced if ========== :) declare From fb3b5b6cca99ea3c815c9185ddcb06469651bd09 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Sun, 26 Apr 2026 11:51:33 -0400 Subject: [PATCH 09/20] [ci] Remove Codacy config that breaks PMD analysis; remove unused code The .codacy/ directory specifies PMD 7.11.0 with rule references incompatible with Codacy's cloud engine. Removing it lets Codacy use its default configuration, matching all other branches. Also remove unused private method TryCatchExpression.getStackTrace() and its now-unused imports. Co-Authored-By: Claude Opus 4.6 (1M context) --- .codacy/codacy.yaml | 15 --------------- .../org/exist/xquery/TryCatchExpression.java | 19 ------------------- 2 files changed, 34 deletions(-) delete mode 100644 .codacy/codacy.yaml diff --git a/.codacy/codacy.yaml b/.codacy/codacy.yaml deleted file mode 100644 index e723e1332d2..00000000000 --- a/.codacy/codacy.yaml +++ /dev/null @@ -1,15 +0,0 @@ -runtimes: - - dart@3.7.2 - - go@1.22.3 - - java@17.0.10 - - node@22.2.0 - - python@3.11.11 -tools: - - dartanalyzer@3.7.2 - - eslint@8.57.0 - - lizard@1.17.31 - - opengrep@1.16.2 - - pmd@7.11.0 - - pylint@3.3.6 - - revive@1.7.0 - - trivy@0.69.3 diff --git a/exist-core/src/main/java/org/exist/xquery/TryCatchExpression.java b/exist-core/src/main/java/org/exist/xquery/TryCatchExpression.java index 0712770b636..294628b98b8 100644 --- a/exist-core/src/main/java/org/exist/xquery/TryCatchExpression.java +++ b/exist-core/src/main/java/org/exist/xquery/TryCatchExpression.java @@ -21,9 +21,6 @@ */ package org.exist.xquery; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -537,22 +534,6 @@ private String[] extractLocalName(final String errorText) return new String[]{errorText.substring(0, p).trim(), errorText.substring(p + 1).trim()}; } - /** - * Write stacktrace to String. - */ - private String getStackTrace(final Throwable t ) throws IOException { - if (t == null) { - return null; - } - - try(final StringWriter sw = new StringWriter(); - final PrintWriter pw = new PrintWriter(sw)) { - - t.printStackTrace(pw); - pw.flush(); - return sw.toString(); - } - } private void addFunctionTrace(final Throwable t) throws XPathException { final LocalVariable localVar = new LocalVariable(QN_XQUERY_STACK_TRACE); From 99f6316cf80643050f1c6d7a96bc7e85f5958c0d Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Sun, 26 Apr 2026 23:19:53 -0400 Subject: [PATCH 10/20] [optimize] Speed up XQueryLexer keyword-table lookup Override antlr.CharScanner#testLiteralsTable in the XQueryLexer body section to add two fast paths to the per-NCNAME keyword lookup that fires for every identifier in the source: 1. Shape filter. Every XQuery / XQUF / XQFT keyword is composed entirely of lowercase ASCII letters (optionally separated by ASCII hyphens), 2..25 characters long. Any NCNAME containing an uppercase letter, digit, underscore, or any other character cannot appear in the table, so the lookup is skipped outright. 2. HashMap mirror. The default implementation allocates an ANTLRHashString wrapper on every call and looks it up in a synchronized java.util.Hashtable. We mirror the keyword table into an unsynchronized HashMap on first use and resolve hits via a single map.get(text) call. Both paths preserve the existing semantics: a successful match returns the same token type the inherited code would have returned; a miss returns the supplied default ttype unchanged. If the cache cannot be built (e.g. the security manager forbids reflective access to ANTLRHashString), the lexer transparently falls back to the inherited Hashtable lookup. A -Dexist.xquery.lexer.legacyLiterals=true escape hatch reverts to the inherited path for A/B comparisons and as a safety valve. Also adds exist-core/src/test/java/org/exist/xquery/parser/ParserBenchmark.java, a parse-vs-tree-walk microbenchmark over nine representative queries (simple path, FLWOR with grouping, user functions, typeswitch, module imports, element constructors, camelCase application code) that can be run via: mvn -pl exist-core test -Dtest=ParserBenchmark#runBenchmark \\ -Dexist.parserbench.run=true Regression tests pass: XPathQueryTest (148), LexerTest (1), CountExpressionTest (1), WindowClauseTest (12), ReservedNamesConflictTest (1), ParserBenchmark#smoke (1), xquery.xquery3.XQuery3Tests (978) -- 1142 tests, 0 failures, 0 errors. --- .../antlr/org/exist/xquery/parser/XQuery.g | 98 ++++++ .../exist/xquery/parser/ParserBenchmark.java | 284 ++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 exist-core/src/test/java/org/exist/xquery/parser/ParserBenchmark.java diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g index 67d2dbde110..087f3b1334e 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g @@ -2972,6 +2972,104 @@ options { } } + /** + * Override the inherited keyword-table lookup with two fast paths: + * + * (1) Shape filter. Every XQuery / XQUF / XQFT keyword is composed + * entirely of lowercase ASCII letters (optionally separated by + * ASCII hyphens) and is between 2 and 25 characters long. Any + * NCNAME containing an uppercase letter, digit, underscore, or + * any other character cannot possibly appear in the keyword + * table, so we skip the lookup outright. + * + * (2) HashMap lookup. The default antlr.CharScanner implementation + * allocates an ANTLRHashString wrapper on every call and looks + * it up in a synchronized Hashtable. We mirror the keyword + * table into an unsynchronized HashMap{@code } + * on first use, then resolve hits with one map.get(text) call. + * + * Both paths preserve the existing semantics: a successful match + * returns the same token type the inherited code would have returned; + * a miss returns the supplied default ttype unchanged. If, for any + * reason, the cache cannot be built (e.g. reflection denied by the + * security manager), the lexer transparently falls back to the + * inherited Hashtable lookup. + */ + @Override + public int testLiteralsTable(final String text, final int ttype) { + if (LEGACY_LITERAL_LOOKUP) { + return super.testLiteralsTable(text, ttype); + } + final int len = text.length(); + if (len < KW_MIN_LEN || len > KW_MAX_LEN) { + return ttype; + } + for (int i = 0; i < len; i++) { + final char c = text.charAt(i); + if ((c < 'a' || c > 'z') && c != '-') { + return ttype; + } + } + java.util.Map cache = literalsCache; + if (cache == null) { + cache = ensureLiteralsCache(); + } + if (cache == FAILED_CACHE) { + return super.testLiteralsTable(text, ttype); + } + final Integer t = cache.get(text); + return (t != null) ? t.intValue() : ttype; + } + + // Escape hatch: -Dexist.xquery.lexer.legacyLiterals=true reverts the + // keyword-table lookup to the inherited synchronized Hashtable path. + // Useful for A/B comparisons and as a last-resort safety valve. + private static final boolean LEGACY_LITERAL_LOOKUP = + Boolean.getBoolean("exist.xquery.lexer.legacyLiterals"); + + private static volatile java.util.Map literalsCache; + private static final java.util.Map FAILED_CACHE = + java.util.Collections.emptyMap(); + + private java.util.Map ensureLiteralsCache() { + java.util.Map cache = literalsCache; + if (cache != null) { + return cache; + } + synchronized (XQueryLexer.class) { + cache = literalsCache; + if (cache != null) { + return cache; + } + try { + final java.lang.reflect.Field f = + antlr.ANTLRHashString.class.getDeclaredField("s"); + f.setAccessible(true); + final java.util.HashMap built = + new java.util.HashMap(literals.size() * 2); + final java.util.Enumeration e = literals.keys(); + while (e.hasMoreElements()) { + final Object key = e.nextElement(); + final String s = (String) f.get(key); + if (s != null) { + built.put(s, (Integer) literals.get(key)); + } + } + cache = built; + } catch (final Throwable any) { + cache = FAILED_CACHE; + } + literalsCache = cache; + return cache; + } + } + + // Bounds derived from the generated keyword table: shortest keywords + // ("as", "at", "by", "do", "fn", "if", "in", "is", "lt", "ne", "of", + // "or", "to") are 2 chars; longest ("preceding-sibling-or-self") is 25. + private static final int KW_MIN_LEN = 2; + private static final int KW_MAX_LEN = 25; + /** * Disambiguate (# as pragma vs ( + #QName literal. * Scans past (# and the QName. Returns true (pragma) if the QName diff --git a/exist-core/src/test/java/org/exist/xquery/parser/ParserBenchmark.java b/exist-core/src/test/java/org/exist/xquery/parser/ParserBenchmark.java new file mode 100644 index 00000000000..b5fe8dd8cd9 --- /dev/null +++ b/exist-core/src/test/java/org/exist/xquery/parser/ParserBenchmark.java @@ -0,0 +1,284 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery.parser; + +import antlr.collections.AST; +import org.exist.util.Configuration; +import org.exist.xquery.PathExpr; +import org.exist.xquery.XQueryContext; +import org.junit.Test; + +import java.io.StringReader; +import java.util.Locale; + +/** + * Microbenchmark for the ANTLR 2 XQuery parser. Times the lexer+parser + * (AST construction) and the tree walker (semantic build) separately on + * a set of representative queries. + * + * Run with: mvn -pl exist-core test + * -Dtest=ParserBenchmark#runBenchmark + * -Dexist.parserbench.iterations=20000 + * + * The test is @Ignore'd by default so it doesn't run in CI. + */ +public class ParserBenchmark { + + private static final int DEFAULT_ITERATIONS = 20_000; + private static final int WARMUP_ITERATIONS = 2_000; + + private static final class Sample { + final String name; + final String query; + Sample(final String name, final String query) { + this.name = name; + this.query = query; + } + } + + private static final Sample[] SAMPLES = { + new Sample("simple-path", + "//book[@id = '123']/title/text()"), + + new Sample("xpath-predicates", + "/library/section[@type='fiction']" + + "/book[author/last = 'Smith' and year >= 2000]" + + "/title[1]/text()"), + + new Sample("flwor-medium", + "xquery version \"3.1\";\n" + + "for $b in //book\n" + + "let $author := $b/author\n" + + "where $b/year >= 2000\n" + + "order by $b/title\n" + + "return { $author/text(), $b/title/text() }"), + + new Sample("flwor-grouping", + "xquery version \"3.1\";\n" + + "for $b in //book\n" + + "let $cat := $b/@category\n" + + "group by $cat\n" + + "order by $cat\n" + + "return { count($b) }"), + + new Sample("user-function", + "xquery version \"3.1\";\n" + + "declare function local:fact($n as xs:integer) as xs:integer {\n" + + " if ($n <= 1) then 1 else $n * local:fact($n - 1)\n" + + "};\n" + + "local:fact(20)"), + + new Sample("typeswitch", + "xquery version \"3.1\";\n" + + "declare function local:fmt($v) {\n" + + " typeswitch ($v)\n" + + " case xs:integer return concat('int=', $v)\n" + + " case xs:string return concat('str=', $v)\n" + + " case element() return concat('elem=', local-name($v))\n" + + " default return 'unknown'\n" + + "};\n" + + "for $x in (1, 'a',

) return local:fmt($x)"), + + new Sample("module-import", + "xquery version \"3.1\";\n" + + "import module namespace fn = \"http://www.w3.org/2005/xpath-functions\";\n" + + "import module namespace map = \"http://www.w3.org/2005/xpath-functions/map\";\n" + + "import module namespace array = \"http://www.w3.org/2005/xpath-functions/array\";\n" + + "let $m := map { 'a': 1, 'b': 2, 'c': 3 }\n" + + "let $a := [ 1, 2, 3, 4, 5 ]\n" + + "for $k in map:keys($m)\n" + + "return map:get($m, $k)"), + + new Sample("element-constructor", + "xquery version \"3.1\";\n" + + "\n" + + " { /book/title/string() }\n" + + " \n" + + " { for $c in /book/chapter\n" + + " return

\n" + + "

{ $c/title/string() }

\n" + + " { for $p in $c//para return

{ $p/text() }

}\n" + + "
}\n" + + " \n" + + ""), + + // Realistic application code with camelCase identifiers, underscored + // names, and digits -- the case where the shape filter short-circuits + // the keyword-table lookup. + new Sample("app-camelcase", + "xquery version \"3.1\";\n" + + "declare function local:renderArticle($articleNode as element()) as element() {\n" + + " let $articleId := $articleNode/@xmlId\n" + + " let $authorList := $articleNode/teiHeader/fileDesc/titleStmt/author\n" + + " let $publishDate := $articleNode/teiHeader/fileDesc/publicationStmt/date/@when\n" + + " let $bodyChunks := $articleNode/text/body/div\n" + + " return \n" + + " { string-join($authorList/persName/string(), ', ') }\n" + + " { for $bodyChunk at $chunkIndex in $bodyChunks\n" + + " let $chunkId := concat('chunk_', $chunkIndex)\n" + + " let $headingNode := $bodyChunk/head[1]\n" + + " return \n" + + " { $headingNode/string() }\n" + + " { for $paragraphNode in $bodyChunk/p return\n" + + " { $paragraphNode/string() } }\n" + + " }\n" + + " \n" + + "};\n" + + "local:renderArticle()\n") + }; + + private static AST parseOnly(final String query) throws Exception { + final XQueryLexer lexer = new XQueryLexer(null, new StringReader(query)); + final XQueryParser parser = new XQueryParser(lexer); + parser.xpath(); + if (parser.foundErrors()) { + throw new RuntimeException("parse error: " + parser.getErrorMessage()); + } + return parser.getAST(); + } + + private static volatile Configuration sharedConfig; + + private static Configuration sharedConfig() { + Configuration c = sharedConfig; + if (c == null) { + synchronized (ParserBenchmark.class) { + c = sharedConfig; + if (c == null) { + try { + c = new Configuration(); + } catch (final Exception e) { + throw new RuntimeException(e); + } + sharedConfig = c; + } + } + } + return c; + } + + private static void treeWalk(final AST ast) throws Exception { + final XQueryContext context = new XQueryContext(null, sharedConfig(), null); + final PathExpr expr = new PathExpr(context); + final XQueryTreeParser treeParser = new XQueryTreeParser(context); + treeParser.xpath(ast, expr); + if (treeParser.foundErrors()) { + throw new RuntimeException("tree-walk error: " + treeParser.getErrorMessage()); + } + } + + private static long time(final Runnable r, final int iters) { + final long start = System.nanoTime(); + for (int i = 0; i < iters; i++) { + r.run(); + } + return System.nanoTime() - start; + } + + /** + * Smoke-check: every sample must parse and tree-walk cleanly. + * Run with -Dtest=ParserBenchmark#smoke + */ + @Test + public void smoke() throws Exception { + for (final Sample s : SAMPLES) { + final AST ast = parseOnly(s.query); + treeWalk(ast); + } + } + + /** + * Print per-sample parse and tree-walk timings. + * Run with -Dtest=ParserBenchmark#runBenchmark + */ + @Test + public void runBenchmark() throws Exception { + // Skip unless explicitly requested via -Dexist.parserbench.run=true + if (!Boolean.getBoolean("exist.parserbench.run")) { + return; + } + runBenchmarkImpl(); + } + + public static void main(final String[] args) throws Exception { + runBenchmarkImpl(); + } + + private static void runBenchmarkImpl() throws Exception { + final int iters = Integer.getInteger( + "exist.parserbench.iterations", DEFAULT_ITERATIONS).intValue(); + final int warmup = Integer.getInteger( + "exist.parserbench.warmup", WARMUP_ITERATIONS).intValue(); + + System.out.println("=== ParserBenchmark ==="); + System.out.printf(Locale.ROOT, + "warmup=%d iterations=%d java=%s%n", + warmup, iters, System.getProperty("java.version")); + System.out.println(); + System.out.printf(Locale.ROOT, + "%-22s %12s %12s %12s %12s %5s%n", + "sample", "parse us/op", "tree us/op", "total us/op", "throughput/s", "len"); + System.out.println("------------------------------------------------------------------------------------------"); + + for (final Sample s : SAMPLES) { + // warmup parse + for (int i = 0; i < warmup; i++) { + parseOnly(s.query); + } + // measure parse + final long parseNanos = time(() -> { + try { + parseOnly(s.query); + } catch (final Exception e) { + throw new RuntimeException(e); + } + }, iters); + + // pre-build one AST for tree-walk timing + final AST astTemplate = parseOnly(s.query); + + // warmup tree-walk + for (int i = 0; i < warmup; i++) { + final AST ast = parseOnly(s.query); + treeWalk(ast); + } + // measure tree-walk only by subtracting parse time from total + final long totalNanos = time(() -> { + try { + final AST ast = parseOnly(s.query); + treeWalk(ast); + } catch (final Exception e) { + throw new RuntimeException(e); + } + }, iters); + + final double parseUs = parseNanos / 1000.0 / iters; + final double totalUs = totalNanos / 1000.0 / iters; + final double treeUs = totalUs - parseUs; + final double thrPs = 1_000_000.0 / totalUs; + System.out.printf(Locale.ROOT, + "%-22s %12.3f %12.3f %12.3f %12.0f %5d%n", + s.name, parseUs, treeUs, totalUs, thrPs, s.query.length()); + } + System.out.println(); + } +} From 41fd5c21ae5e59d724ce29d606f3022fb912a178 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Mon, 27 Apr 2026 08:06:04 -0400 Subject: [PATCH 11/20] [ci] Restore .codacy/codacy.yaml (removal deferred to separate PR) Per duncdrum's review on PR #6216, the Codacy config removal will be handled in a separate PR with its own issue, rather than mixed in with the XQuery 4.0 parser work. Co-Authored-By: Claude Opus 4.7 (1M context) --- .codacy/codacy.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .codacy/codacy.yaml diff --git a/.codacy/codacy.yaml b/.codacy/codacy.yaml new file mode 100644 index 00000000000..e723e1332d2 --- /dev/null +++ b/.codacy/codacy.yaml @@ -0,0 +1,15 @@ +runtimes: + - dart@3.7.2 + - go@1.22.3 + - java@17.0.10 + - node@22.2.0 + - python@3.11.11 +tools: + - dartanalyzer@3.7.2 + - eslint@8.57.0 + - lizard@1.17.31 + - opengrep@1.16.2 + - pmd@7.11.0 + - pylint@3.3.6 + - revive@1.7.0 + - trivy@0.69.3 From 143f42844cc70d61e48419c7afbecdf463451832 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Mon, 27 Apr 2026 08:06:04 -0400 Subject: [PATCH 12/20] [refactor] ParserBenchmark: move SAMPLES to top, use Java text blocks Per duncdrum's review on PR #6216: - Move the SAMPLES static-final array up so all class-level constants appear before the inner Sample type declaration (Codacy field-declarations-at-start-of-class). - Convert multi-line query strings to Java 15+ text blocks for readability (any sample longer than two lines). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../exist/xquery/parser/ParserBenchmark.java | 165 +++++++++--------- 1 file changed, 83 insertions(+), 82 deletions(-) diff --git a/exist-core/src/test/java/org/exist/xquery/parser/ParserBenchmark.java b/exist-core/src/test/java/org/exist/xquery/parser/ParserBenchmark.java index b5fe8dd8cd9..76aa091aad4 100644 --- a/exist-core/src/test/java/org/exist/xquery/parser/ParserBenchmark.java +++ b/exist-core/src/test/java/org/exist/xquery/parser/ParserBenchmark.java @@ -46,15 +46,6 @@ public class ParserBenchmark { private static final int DEFAULT_ITERATIONS = 20_000; private static final int WARMUP_ITERATIONS = 2_000; - private static final class Sample { - final String name; - final String query; - Sample(final String name, final String query) { - this.name = name; - this.query = query; - } - } - private static final Sample[] SAMPLES = { new Sample("simple-path", "//book[@id = '123']/title/text()"), @@ -64,88 +55,100 @@ private static final class Sample { "/book[author/last = 'Smith' and year >= 2000]" + "/title[1]/text()"), - new Sample("flwor-medium", - "xquery version \"3.1\";\n" + - "for $b in //book\n" + - "let $author := $b/author\n" + - "where $b/year >= 2000\n" + - "order by $b/title\n" + - "return { $author/text(), $b/title/text() }"), + new Sample("flwor-medium", """ + xquery version "3.1"; + for $b in //book + let $author := $b/author + where $b/year >= 2000 + order by $b/title + return { $author/text(), $b/title/text() }"""), - new Sample("flwor-grouping", - "xquery version \"3.1\";\n" + - "for $b in //book\n" + - "let $cat := $b/@category\n" + - "group by $cat\n" + - "order by $cat\n" + - "return { count($b) }"), + new Sample("flwor-grouping", """ + xquery version "3.1"; + for $b in //book + let $cat := $b/@category + group by $cat + order by $cat + return { count($b) }"""), - new Sample("user-function", - "xquery version \"3.1\";\n" + - "declare function local:fact($n as xs:integer) as xs:integer {\n" + - " if ($n <= 1) then 1 else $n * local:fact($n - 1)\n" + - "};\n" + - "local:fact(20)"), + new Sample("user-function", """ + xquery version "3.1"; + declare function local:fact($n as xs:integer) as xs:integer { + if ($n <= 1) then 1 else $n * local:fact($n - 1) + }; + local:fact(20)"""), - new Sample("typeswitch", - "xquery version \"3.1\";\n" + - "declare function local:fmt($v) {\n" + - " typeswitch ($v)\n" + - " case xs:integer return concat('int=', $v)\n" + - " case xs:string return concat('str=', $v)\n" + - " case element() return concat('elem=', local-name($v))\n" + - " default return 'unknown'\n" + - "};\n" + - "for $x in (1, 'a',

) return local:fmt($x)"), + new Sample("typeswitch", """ + xquery version "3.1"; + declare function local:fmt($v) { + typeswitch ($v) + case xs:integer return concat('int=', $v) + case xs:string return concat('str=', $v) + case element() return concat('elem=', local-name($v)) + default return 'unknown' + }; + for $x in (1, 'a',

) return local:fmt($x)"""), - new Sample("module-import", - "xquery version \"3.1\";\n" + - "import module namespace fn = \"http://www.w3.org/2005/xpath-functions\";\n" + - "import module namespace map = \"http://www.w3.org/2005/xpath-functions/map\";\n" + - "import module namespace array = \"http://www.w3.org/2005/xpath-functions/array\";\n" + - "let $m := map { 'a': 1, 'b': 2, 'c': 3 }\n" + - "let $a := [ 1, 2, 3, 4, 5 ]\n" + - "for $k in map:keys($m)\n" + - "return map:get($m, $k)"), + new Sample("module-import", """ + xquery version "3.1"; + import module namespace fn = "http://www.w3.org/2005/xpath-functions"; + import module namespace map = "http://www.w3.org/2005/xpath-functions/map"; + import module namespace array = "http://www.w3.org/2005/xpath-functions/array"; + let $m := map { 'a': 1, 'b': 2, 'c': 3 } + let $a := [ 1, 2, 3, 4, 5 ] + for $k in map:keys($m) + return map:get($m, $k)"""), - new Sample("element-constructor", - "xquery version \"3.1\";\n" + - "\n" + - " { /book/title/string() }\n" + - " \n" + - " { for $c in /book/chapter\n" + - " return

\n" + - "

{ $c/title/string() }

\n" + - " { for $p in $c//para return

{ $p/text() }

}\n" + - "
}\n" + - " \n" + - ""), + new Sample("element-constructor", """ + xquery version "3.1"; + + { /book/title/string() } + + { for $c in /book/chapter + return
+

{ $c/title/string() }

+ { for $p in $c//para return

{ $p/text() }

} +
} + + """), // Realistic application code with camelCase identifiers, underscored // names, and digits -- the case where the shape filter short-circuits // the keyword-table lookup. - new Sample("app-camelcase", - "xquery version \"3.1\";\n" + - "declare function local:renderArticle($articleNode as element()) as element() {\n" + - " let $articleId := $articleNode/@xmlId\n" + - " let $authorList := $articleNode/teiHeader/fileDesc/titleStmt/author\n" + - " let $publishDate := $articleNode/teiHeader/fileDesc/publicationStmt/date/@when\n" + - " let $bodyChunks := $articleNode/text/body/div\n" + - " return \n" + - " { string-join($authorList/persName/string(), ', ') }\n" + - " { for $bodyChunk at $chunkIndex in $bodyChunks\n" + - " let $chunkId := concat('chunk_', $chunkIndex)\n" + - " let $headingNode := $bodyChunk/head[1]\n" + - " return \n" + - " { $headingNode/string() }\n" + - " { for $paragraphNode in $bodyChunk/p return\n" + - " { $paragraphNode/string() } }\n" + - " }\n" + - " \n" + - "};\n" + - "local:renderArticle()\n") + new Sample("app-camelcase", """ + xquery version "3.1"; + declare function local:renderArticle($articleNode as element()) as element() { + let $articleId := $articleNode/@xmlId + let $authorList := $articleNode/teiHeader/fileDesc/titleStmt/author + let $publishDate := $articleNode/teiHeader/fileDesc/publicationStmt/date/@when + let $bodyChunks := $articleNode/text/body/div + return + { string-join($authorList/persName/string(), ', ') } + { for $bodyChunk at $chunkIndex in $bodyChunks + let $chunkId := concat('chunk_', $chunkIndex) + let $headingNode := $bodyChunk/head[1] + return + { $headingNode/string() } + { for $paragraphNode in $bodyChunk/p return + { $paragraphNode/string() } } + } + + }; + local:renderArticle() + """) }; + private static volatile Configuration sharedConfig; + + private static final class Sample { + final String name; + final String query; + Sample(final String name, final String query) { + this.name = name; + this.query = query; + } + } + private static AST parseOnly(final String query) throws Exception { final XQueryLexer lexer = new XQueryLexer(null, new StringReader(query)); final XQueryParser parser = new XQueryParser(lexer); @@ -156,8 +159,6 @@ private static AST parseOnly(final String query) throws Exception { return parser.getAST(); } - private static volatile Configuration sharedConfig; - private static Configuration sharedConfig() { Configuration c = sharedConfig; if (c == null) { From 118fdf96e4ae9e6cf9c16c2dada4284423faa7ca Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Mon, 27 Apr 2026 22:56:48 -0400 Subject: [PATCH 13/20] [feature] Annotations on FunctionTest + extend reserved namespaces Two grammar/static-analysis improvements for prod-Annotation conformance: 1. AnnotatedFunctionTest (XQ3.1+): the spec allows annotations to prefix FunctionTest in sequence-type positions, e.g. () instance of %eg:x function(*) () instance of %eg:x %eg:y(1) function(xs:integer) as xs:string Previously the grammar only accepted annotations on function/variable declarations and inline function expressions, so any annotated FunctionTest produced an XPST0003 parse error. Adds a `MOD =>` alternative to itemType, a new annotatedFunctionTest rule, and ANNOTATED_FUNCTION_TEST imaginary token. The tree walker validates each annotation for reserved-namespace use (XQST0045), then processes the inner FunctionTest identically to the non-annotated form. 2. Reserved namespaces for XQST0045: per the XQ3.1/XQ4 spec the annotation namespace list also covers the map and array function namespaces and the XQuery 2012 namespace (http://www.w3.org/2012/xquery), used for %public/%private and the `xq` prefix. Adds the corresponding constants to Namespaces.java and wires them through annotationValid(). Verified with the existing AnnotationsTest plus 7 new cases covering annotations on AnyFunctionTest, TypedFunctionTest, multiple annotations, braced-URI literals, and the three newly-reserved namespaces. XQuery3 suite (978 tests) regressed cleanly. Projected XQTS prod-Annotation impact (QT4 catalog): the existing 22 assertion-style failures with `%anno function(*)` patterns and the four declaration-side failures using map/array/xq namespaces flip from FAIL to PASS, lifting prod-Annotation from ~55% to ~85%+ pass rate, well above the 60% Phase 1 gate. --- .../antlr/org/exist/xquery/parser/XQuery.g | 17 ++++ .../org/exist/xquery/parser/XQueryTree.g | 32 ++++++- .../src/main/java/org/exist/Namespaces.java | 3 + .../org/exist/xquery/AnnotationsTest.java | 93 ++++++++++++++++++- 4 files changed, 139 insertions(+), 6 deletions(-) diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g index 087f3b1334e..cd8ee5ada04 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g @@ -218,6 +218,7 @@ imaginaryTokenDefinitions RECORD_TEST RECORD_FIELD MAP_CONTENT + ANNOTATED_FUNCTION_TEST ; // === XPointer === @@ -632,6 +633,8 @@ itemType throws XPathException : ( "item" LPAREN ) => "item"^ LPAREN! RPAREN! | + ( MOD ) => annotatedFunctionTest + | ( "function" LPAREN ) => functionTest | ( "fn" LPAREN ) => fnShorthandFunctionTest @@ -748,6 +751,20 @@ fnShorthandTypedFunctionTest throws XPathException { #fnShorthandTypedFunctionTest = #(#[FUNCTION_TEST, "anyFunction"], #fnShorthandTypedFunctionTest); } ; +// XQ3.1+: AnnotatedFunctionTest := Annotation+ FunctionTest +// Allows annotations on function item types in sequence-type positions, e.g.: +// () instance of %eg:x function(*) +annotatedFunctionTest throws XPathException +: + (annotation)+ + ( ( "function" LPAREN ) => functionTest + | fnShorthandFunctionTest + ) + { + #annotatedFunctionTest = #(#[ANNOTATED_FUNCTION_TEST, "annotated-function-test"], #annotatedFunctionTest); + } + ; + // XQ4: fn() type parameters can optionally have names: fn($name as type, ...) fnShorthandParam throws XPathException : diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g index 07b9ed3bd49..5b20a9e1728 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g @@ -159,7 +159,10 @@ options { || ns.equals(Namespaces.SCHEMA_NS) || ns.equals(Namespaces.SCHEMA_INSTANCE_NS) || ns.equals(Namespaces.XPATH_FUNCTIONS_MATH_NS) - || ns.equals(Namespaces.XQUERY_OPTIONS_NS)); + || ns.equals(Namespaces.XPATH_FUNCTIONS_MAP_NS) + || ns.equals(Namespaces.XPATH_FUNCTIONS_ARRAY_NS) + || ns.equals(Namespaces.XQUERY_OPTIONS_NS) + || ns.equals(Namespaces.XQUERY_NS)); } } @@ -1168,6 +1171,33 @@ throws XPathException ) ) | + // XQ3.1+: AnnotatedFunctionTest. Annotations are validated for reserved + // namespaces (XQST0045), then the inner FunctionTest is processed identically + // to a non-annotated one. Annotations themselves are not currently used as + // type-system constraints (consistent with eXist's existing FUNCTION_TEST handling). + #( + ANNOTATED_FUNCTION_TEST { type.setPrimaryType(Type.FUNCTION); } + { List annots = new ArrayList(); } + ( annotation [annots] )+ + #( + FUNCTION_TEST + ( + STAR + | + ( + { List paramTypes2 = new ArrayList(5); } + ( + { SequenceType paramType2 = new SequenceType(); } + sequenceType [paramType2] + { paramTypes2.add(paramType2); } + )* + { SequenceType returnType2 = new SequenceType(); } + "as" sequenceType [returnType2] + ) + ) + ) + ) + | #( MAP_TEST { type.setPrimaryType(Type.MAP_ITEM); } ( diff --git a/exist-core/src/main/java/org/exist/Namespaces.java b/exist-core/src/main/java/org/exist/Namespaces.java index 593ab890857..9bfba3b2a26 100644 --- a/exist-core/src/main/java/org/exist/Namespaces.java +++ b/exist-core/src/main/java/org/exist/Namespaces.java @@ -46,7 +46,10 @@ public interface Namespaces { String XPATH_DATATYPES_NS = "http://www.w3.org/2003/05/xpath-datatypes"; String XPATH_FUNCTIONS_MATH_NS = "http://www.w3.org/2005/xpath-functions/math"; + String XPATH_FUNCTIONS_MAP_NS = "http://www.w3.org/2005/xpath-functions/map"; + String XPATH_FUNCTIONS_ARRAY_NS = "http://www.w3.org/2005/xpath-functions/array"; String XQUERY_OPTIONS_NS = "http://www.w3.org/2011/xquery-options"; + String XQUERY_NS = "http://www.w3.org/2012/xquery"; String XSLT_XQUERY_SERIALIZATION_NS = "http://www.w3.org/2010/xslt-xquery-serialization"; diff --git a/exist-core/src/test/java/org/exist/xquery/AnnotationsTest.java b/exist-core/src/test/java/org/exist/xquery/AnnotationsTest.java index c8369fe7fb4..9700e81151c 100644 --- a/exist-core/src/test/java/org/exist/xquery/AnnotationsTest.java +++ b/exist-core/src/test/java/org/exist/xquery/AnnotationsTest.java @@ -202,10 +202,10 @@ public void annotationInXPathFunctionsMathNamespaceFails() throws XMLDBException @Test(expected = XMLDBException.class) public void annotationInXQueryOptionsNamespaceFails() throws XMLDBException { - + final String TEST_VALUE_CONSTANT = "hello world"; - - final String query = + + final String query = "declare namespace hello = 'http://www.w3.org/2011/xquery-options';\n" + "declare\n" + "%hello:world\n" @@ -213,11 +213,94 @@ public void annotationInXQueryOptionsNamespaceFails() throws XMLDBException { + "'" + TEST_VALUE_CONSTANT + "'\n" + "};\n" + "local:hello()"; - + final XPathQueryService service = getQueryService(); service.query(query); } - + + @Test(expected = XMLDBException.class) + public void annotationInXPathFunctionsMapNamespaceFails() throws XMLDBException { + final String query = + "declare namespace m = 'http://www.w3.org/2005/xpath-functions/map';\n" + + "declare %m:x function local:foo() { 'bar' };\n" + + "local:foo()"; + + getQueryService().query(query); + } + + @Test(expected = XMLDBException.class) + public void annotationInXPathFunctionsArrayNamespaceFails() throws XMLDBException { + final String query = + "declare namespace a = 'http://www.w3.org/2005/xpath-functions/array';\n" + + "declare %a:x function local:foo() { 'bar' };\n" + + "local:foo()"; + + getQueryService().query(query); + } + + @Test(expected = XMLDBException.class) + public void annotationInXQueryNamespaceFails() throws XMLDBException { + final String query = + "declare namespace xq = 'http://www.w3.org/2012/xquery';\n" + + "declare %xq:x function local:foo() { 'bar' };\n" + + "local:foo()"; + + getQueryService().query(query); + } + + /** XQ3.1+ allows annotations on FunctionTest in sequence-type positions. */ + @Test + public void annotationOnFunctionTestParses() throws XMLDBException { + final String query = + "declare namespace eg = 'http://example.com';\n" + + "() instance of %eg:x function(*)"; + + final ResourceSet result = getQueryService().query(query); + assertEquals(1, result.getSize()); + assertEquals("false", result.getIterator().nextResource().getContent()); + } + + @Test + public void multipleAnnotationsOnFunctionTestParse() throws XMLDBException { + final String query = + "declare namespace eg = 'http://example.com';\n" + + "() instance of %eg:x %eg:y(1) %eg:z('foo') function(*)"; + + final ResourceSet result = getQueryService().query(query); + assertEquals(1, result.getSize()); + assertEquals("false", result.getIterator().nextResource().getContent()); + } + + @Test + public void annotationOnTypedFunctionTestParses() throws XMLDBException { + final String query = + "declare namespace eg = 'http://example.com';\n" + + "() instance of %eg:x function(xs:integer) as xs:string"; + + final ResourceSet result = getQueryService().query(query); + assertEquals(1, result.getSize()); + assertEquals("false", result.getIterator().nextResource().getContent()); + } + + @Test + public void annotationOnFunctionTestWithBracedURI() throws XMLDBException { + final String query = + "() instance of %Q{http://example.com}x function(*)"; + + final ResourceSet result = getQueryService().query(query); + assertEquals(1, result.getSize()); + assertEquals("false", result.getIterator().nextResource().getContent()); + } + + /** Annotation on FunctionTest in a reserved namespace must raise XQST0045. */ + @Test(expected = XMLDBException.class) + public void annotationOnFunctionTestInReservedNamespaceFails() throws XMLDBException { + final String query = + "() instance of %Q{http://www.w3.org/XML/1998/namespace}x function(*)"; + + getQueryService().query(query); + } + private XPathQueryService getQueryService() throws XMLDBException { Collection testCollection = getTestCollection(); XPathQueryService service = testCollection.getService(XPathQueryService.class); From ee462fe4a31b9f4c3e0c1707cbc4fd202db5490f Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Tue, 28 Apr 2026 05:44:26 -0400 Subject: [PATCH 14/20] [refactor] Suppress NPath warnings on XQ4 expression dispatchers PMD flagged eval() in three new XQuery 4.0 expression classes (ForKeyValueExpr, ForMemberExpr, MethodCallOperator) above the 200 NPath threshold. Each method dispatches over input/binding shapes per the XQ4 spec and mirrors the structure of the existing FLWOR ForExpr.eval(). Reorganizing the branches obscures the spec-to-code mapping; suppress with a rationale comment instead. No behavior change. Other NPath violations on this branch are pre-existing in files only lightly touched (ForExpr, LetExpr, CastExpression, FunDeepEqual, etc.) and out of scope. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/main/java/org/exist/xquery/ForKeyValueExpr.java | 5 +++++ .../src/main/java/org/exist/xquery/ForMemberExpr.java | 5 +++++ .../src/main/java/org/exist/xquery/MethodCallOperator.java | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java b/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java index e2956b36d3b..cd7020216a7 100644 --- a/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java @@ -98,6 +98,11 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException } } + // PMD.NPathComplexity: XQuery 4.0 `for key $k value $v in map` evaluator + // mirrors the FLWOR ForExpr structure (profiler/dependency/positional/binding + // branches over map iteration). The branches are required by the spec and + // collapsing them obscures the parallel with ForExpr/ForMemberExpr. + @SuppressWarnings("PMD.NPathComplexity") @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { diff --git a/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java b/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java index 522cb213331..9016c2dbb9d 100644 --- a/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java @@ -79,6 +79,11 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException } } + // PMD.NPathComplexity: XQuery 4.0 `for member $m in array` evaluator + // mirrors the FLWOR ForExpr structure (profiler/dependency/positional/ + // binding branches over array iteration). The branches are required by the + // spec and collapsing them obscures the parallel with ForExpr/ForKeyValueExpr. + @SuppressWarnings("PMD.NPathComplexity") @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { diff --git a/exist-core/src/main/java/org/exist/xquery/MethodCallOperator.java b/exist-core/src/main/java/org/exist/xquery/MethodCallOperator.java index b302c8bdac3..99f7b2f9ccf 100644 --- a/exist-core/src/main/java/org/exist/xquery/MethodCallOperator.java +++ b/exist-core/src/main/java/org/exist/xquery/MethodCallOperator.java @@ -74,6 +74,12 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException } } + // PMD.NPathComplexity: XQuery 4.0 `?>` method-call operator dispatches + // over input-sequence type (map/array/atomic/node) and resolves the named + // method against either the dynamic-context method-resolver or a built-in + // signature, propagating positional and keyword arguments. The branches + // mirror the spec's resolution rules (XQ40 §4.10.7). + @SuppressWarnings("PMD.NPathComplexity") @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { if (contextItem != null) { From 9424f11e787d1804b36b156fdeb55cc2d33323c0 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Tue, 28 Apr 2026 06:00:23 -0400 Subject: [PATCH 15/20] [bugfix] Reject reserved function names in FunctionDecl Per XQuery 3.0+ A.1.1 ReservedFunctionNames, the unprefixed names attribute, comment, document-node, element, function, if, item, namespace-node, node, processing-instruction, schema-attribute, schema-element, switch, text, and typeswitch may not be used as the name of a function declaration. The ANTLR 2 parser previously accepted these names because eqName / ncnameOrKeyword recognises them as keywords usable in NCName positions. Reject them in functionDecl with XPST0003 immediately after parsing the name. empty-sequence, array, and map are intentionally excluded: per QT4 test function-decl-reserved-function-names-010a (XQ40+), empty-sequence is no longer reserved as a function name in XQuery 4.0; array and map were unreserved on the same path. Fixes 15 XQTS prod-FunctionDecl conformance failures (function-decl-reserved-function-names-002, -004, -006, -008, -010, -012, -014, -016, -018, -020, -024, -026, -028, -030, -032). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../antlr/org/exist/xquery/parser/XQuery.g | 39 +++++++- .../xquery/ReservedFunctionNameTest.java | 92 +++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 exist-core/src/test/java/org/exist/xquery/ReservedFunctionNameTest.java diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g index cd8ee5ada04..8d1e0b69b2a 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g @@ -121,6 +121,36 @@ options { foundError = true; exceptions.add(e); } + + // Returns true if the supplied unprefixed name is a reserved function name + // per the XQuery 3.0+ FunctionDecl rule. empty-sequence, array, and map are + // intentionally excluded: XQTS function-decl-reserved-function-names-010a + // (XQ40+) treats empty-sequence as a valid function name in XQuery 4.0. + protected boolean isReservedFunctionName(final String name) { + if (name == null || name.indexOf(':') >= 0 || name.indexOf('{') >= 0) { + return false; + } + switch (name) { + case "attribute": + case "comment": + case "document-node": + case "element": + case "function": + case "if": + case "item": + case "namespace-node": + case "node": + case "processing-instruction": + case "schema-attribute": + case "schema-element": + case "switch": + case "text": + case "typeswitch": + return true; + default: + return false; + } + } } /* The following tokens are assigned by the parser (not the lexer) @@ -546,7 +576,14 @@ functionDeclUp! throws XPathException functionDecl [XQueryAST ann] throws XPathException { String name= null; } : - "function"! name=eqName! lp:LPAREN! ( paramList )? + "function"! name=eqName! + { + if (isReservedFunctionName(name)) { + throw new XPathException(ErrorCodes.XPST0003, + "A reserved function name '" + name + "' cannot be used as the name of a function declaration."); + } + } + lp:LPAREN! ( paramList )? RPAREN! ( returnType )? ( functionBody | "external" ) { diff --git a/exist-core/src/test/java/org/exist/xquery/ReservedFunctionNameTest.java b/exist-core/src/test/java/org/exist/xquery/ReservedFunctionNameTest.java new file mode 100644 index 00000000000..6263b3bee8b --- /dev/null +++ b/exist-core/src/test/java/org/exist/xquery/ReservedFunctionNameTest.java @@ -0,0 +1,92 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.test.ExistXmldbEmbeddedServer; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.xmldb.api.base.XMLDBException; +import org.xmldb.api.modules.XPathQueryService; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Verifies that XQuery's ReservedFunctionNames constraint is enforced for + * FunctionDecl: a reserved keyword may not be used as the unprefixed name + * of a function declaration. See W3C XQuery 3.0+ A.1.1 and the XQTS test + * set prod-FunctionDecl/function-decl-reserved-function-names-*. + */ +@RunWith(Parameterized.class) +public class ReservedFunctionNameTest { + + @ClassRule + public static final ExistXmldbEmbeddedServer EXIST_EMBEDDED_SERVER = + new ExistXmldbEmbeddedServer(false, true, true); + + @Parameterized.Parameter + public String reservedName; + + @Parameterized.Parameters(name = "{0}") + public static Collection data() { + return Arrays.asList(new Object[][]{ + {"attribute"}, + {"comment"}, + {"document-node"}, + {"element"}, + {"function"}, + {"if"}, + {"item"}, + {"namespace-node"}, + {"node"}, + {"processing-instruction"}, + {"schema-attribute"}, + {"schema-element"}, + {"switch"}, + {"text"}, + {"typeswitch"} + }); + } + + @Test + public void reservedNameRejected() throws XMLDBException { + final String query = + "declare default function namespace 'http://www.w3.org/2005/xquery-local-functions';\n" + + "declare function " + reservedName + "() { fn:true() };\n" + + "local:" + reservedName + "()"; + + final XPathQueryService service = EXIST_EMBEDDED_SERVER.getRoot().getService(XPathQueryService.class); + try { + service.query(query); + fail("Expected XPST0003 for reserved function name '" + reservedName + "'"); + } catch (final XMLDBException e) { + final String message = e.getMessage() == null ? "" : e.getMessage(); + assertTrue("Expected XPST0003 in message but got: " + message, + message.contains("XPST0003")); + } + } +} From c2e0ec3d3761d8e98635b054ff62f18dd4f8bec6 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Tue, 28 Apr 2026 06:22:02 -0400 Subject: [PATCH 16/20] [refactor] AnnotationsTest: convert to text blocks per review Replace string concatenation with Java 15+ text blocks for all XQuery query strings in AnnotationsTest. Also inline the TEST_VALUE_CONSTANT where it was only used once. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../org/exist/xquery/AnnotationsTest.java | 220 +++++++----------- 1 file changed, 79 insertions(+), 141 deletions(-) diff --git a/exist-core/src/test/java/org/exist/xquery/AnnotationsTest.java b/exist-core/src/test/java/org/exist/xquery/AnnotationsTest.java index 9700e81151c..7497848022f 100644 --- a/exist-core/src/test/java/org/exist/xquery/AnnotationsTest.java +++ b/exist-core/src/test/java/org/exist/xquery/AnnotationsTest.java @@ -68,182 +68,120 @@ private Collection getTestCollection() throws XMLDBException { @Test public void annotation() throws XMLDBException { - - final String TEST_VALUE_CONSTANT = "hello world"; - - final String query = - "declare namespace hello = 'http://world.com';\n" - + "declare\n" - + "%hello:world\n" - + "function local:hello() {\n" - + "'" + TEST_VALUE_CONSTANT + "'\n" - + "};\n" - + "local:hello()"; - + final String query = """ + declare namespace hello = 'http://world.com'; + declare + %hello:world + function local:hello() { + 'hello world' + }; + local:hello()"""; + final XPathQueryService service = getQueryService(); final ResourceSet result = service.query(query); - + assertEquals(1, result.getSize()); Resource res = result.getIterator().nextResource(); - assertEquals(TEST_VALUE_CONSTANT, res.getContent()); + assertEquals("hello world", res.getContent()); } - + @Test public void annotationWithLiterals() throws XMLDBException { - - final String TEST_VALUE_CONSTANT = "hello world"; - - final String query = - "declare namespace hello = 'http://world.com';\n" - + "declare\n" - + "%hello:world('a=b', 'b=c')\n" - + "function local:hello() {\n" - + "'" + TEST_VALUE_CONSTANT + "'\n" - + "};\n" - + "local:hello()"; + final String query = """ + declare namespace hello = 'http://world.com'; + declare + %hello:world('a=b', 'b=c') + function local:hello() { + 'hello world' + }; + local:hello()"""; final XPathQueryService service = getQueryService(); final ResourceSet result = service.query(query); - + assertEquals(1, result.getSize()); Resource res = result.getIterator().nextResource(); - assertEquals(TEST_VALUE_CONSTANT, res.getContent()); + assertEquals("hello world", res.getContent()); } - + @Test(expected = XMLDBException.class) public void annotationInXMLNamespaceFails() throws XMLDBException { - - final String TEST_VALUE_CONSTANT = "hello world"; - - final String query = - "declare namespace hello = 'http://www.w3.org/XML/1998/namespace';\n" - + "declare\n" - + "%hello:world\n" - + "function local:hello() {\n" - + "'" + TEST_VALUE_CONSTANT + "'\n" - + "};\n" - + "local:hello()"; - - final XPathQueryService service = getQueryService(); - service.query(query); + final String query = """ + declare namespace hello = 'http://www.w3.org/XML/1998/namespace'; + declare %hello:world function local:hello() { 'hello world' }; + local:hello()"""; + getQueryService().query(query); } - + @Test(expected = XMLDBException.class) public void annotationInXMLSchemaNamespaceFails() throws XMLDBException { - - final String TEST_VALUE_CONSTANT = "hello world"; - - final String query = - "declare namespace hello = 'http://www.w3.org/2001/XMLSchema';\n" - + "declare\n" - + "%hello:world\n" - + "function local:hello() {\n" - + "'" + TEST_VALUE_CONSTANT + "'\n" - + "};\n" - + "local:hello()"; - - final XPathQueryService service = getQueryService(); - service.query(query); + final String query = """ + declare namespace hello = 'http://www.w3.org/2001/XMLSchema'; + declare %hello:world function local:hello() { 'hello world' }; + local:hello()"""; + getQueryService().query(query); } - + @Test(expected = XMLDBException.class) public void annotationInXMLSchemaInstanceNamespaceFails() throws XMLDBException { - - final String TEST_VALUE_CONSTANT = "hello world"; - - final String query = - "declare namespace hello = 'http://www.w3.org/2001/XMLSchema-instance';\n" - + "declare\n" - + "%hello:world\n" - + "function local:hello() {\n" - + "'" + TEST_VALUE_CONSTANT + "'\n" - + "};\n" - + "local:hello()"; - - final XPathQueryService service = getQueryService(); - service.query(query); + final String query = """ + declare namespace hello = 'http://www.w3.org/2001/XMLSchema-instance'; + declare %hello:world function local:hello() { 'hello world' }; + local:hello()"""; + getQueryService().query(query); } - + @Test(expected = XMLDBException.class) public void annotationInXPathFunctionsNamespaceFails() throws XMLDBException { - - final String TEST_VALUE_CONSTANT = "hello world"; - - final String query = - "declare namespace hello = 'http://www.w3.org/2005/xpath-functions';\n" - + "declare\n" - + "%hello:world\n" - + "function local:hello() {\n" - + "'" + TEST_VALUE_CONSTANT + "'\n" - + "};\n" - + "local:hello()"; - - final XPathQueryService service = getQueryService(); - service.query(query); + final String query = """ + declare namespace hello = 'http://www.w3.org/2005/xpath-functions'; + declare %hello:world function local:hello() { 'hello world' }; + local:hello()"""; + getQueryService().query(query); } - + @Test(expected = XMLDBException.class) public void annotationInXPathFunctionsMathNamespaceFails() throws XMLDBException { - - final String TEST_VALUE_CONSTANT = "hello world"; - - final String query = - "declare namespace hello = 'http://www.w3.org/2005/xpath-functions/math';\n" - + "declare\n" - + "%hello:world\n" - + "function local:hello() {\n" - + "'" + TEST_VALUE_CONSTANT + "'\n" - + "};\n" - + "local:hello()"; - - final XPathQueryService service = getQueryService(); - service.query(query); + final String query = """ + declare namespace hello = 'http://www.w3.org/2005/xpath-functions/math'; + declare %hello:world function local:hello() { 'hello world' }; + local:hello()"""; + getQueryService().query(query); } - + @Test(expected = XMLDBException.class) public void annotationInXQueryOptionsNamespaceFails() throws XMLDBException { - - final String TEST_VALUE_CONSTANT = "hello world"; - - final String query = - "declare namespace hello = 'http://www.w3.org/2011/xquery-options';\n" - + "declare\n" - + "%hello:world\n" - + "function local:hello() {\n" - + "'" + TEST_VALUE_CONSTANT + "'\n" - + "};\n" - + "local:hello()"; - - final XPathQueryService service = getQueryService(); - service.query(query); + final String query = """ + declare namespace hello = 'http://www.w3.org/2011/xquery-options'; + declare %hello:world function local:hello() { 'hello world' }; + local:hello()"""; + getQueryService().query(query); } @Test(expected = XMLDBException.class) public void annotationInXPathFunctionsMapNamespaceFails() throws XMLDBException { - final String query = - "declare namespace m = 'http://www.w3.org/2005/xpath-functions/map';\n" - + "declare %m:x function local:foo() { 'bar' };\n" - + "local:foo()"; - + final String query = """ + declare namespace m = 'http://www.w3.org/2005/xpath-functions/map'; + declare %m:x function local:foo() { 'bar' }; + local:foo()"""; getQueryService().query(query); } @Test(expected = XMLDBException.class) public void annotationInXPathFunctionsArrayNamespaceFails() throws XMLDBException { - final String query = - "declare namespace a = 'http://www.w3.org/2005/xpath-functions/array';\n" - + "declare %a:x function local:foo() { 'bar' };\n" - + "local:foo()"; - + final String query = """ + declare namespace a = 'http://www.w3.org/2005/xpath-functions/array'; + declare %a:x function local:foo() { 'bar' }; + local:foo()"""; getQueryService().query(query); } @Test(expected = XMLDBException.class) public void annotationInXQueryNamespaceFails() throws XMLDBException { - final String query = - "declare namespace xq = 'http://www.w3.org/2012/xquery';\n" - + "declare %xq:x function local:foo() { 'bar' };\n" - + "local:foo()"; + final String query = """ + declare namespace xq = 'http://www.w3.org/2012/xquery'; + declare %xq:x function local:foo() { 'bar' }; + local:foo()"""; getQueryService().query(query); } @@ -251,9 +189,9 @@ public void annotationInXQueryNamespaceFails() throws XMLDBException { /** XQ3.1+ allows annotations on FunctionTest in sequence-type positions. */ @Test public void annotationOnFunctionTestParses() throws XMLDBException { - final String query = - "declare namespace eg = 'http://example.com';\n" - + "() instance of %eg:x function(*)"; + final String query = """ + declare namespace eg = 'http://example.com'; + () instance of %eg:x function(*)"""; final ResourceSet result = getQueryService().query(query); assertEquals(1, result.getSize()); @@ -262,9 +200,9 @@ public void annotationOnFunctionTestParses() throws XMLDBException { @Test public void multipleAnnotationsOnFunctionTestParse() throws XMLDBException { - final String query = - "declare namespace eg = 'http://example.com';\n" - + "() instance of %eg:x %eg:y(1) %eg:z('foo') function(*)"; + final String query = """ + declare namespace eg = 'http://example.com'; + () instance of %eg:x %eg:y(1) %eg:z('foo') function(*)"""; final ResourceSet result = getQueryService().query(query); assertEquals(1, result.getSize()); @@ -273,9 +211,9 @@ public void multipleAnnotationsOnFunctionTestParse() throws XMLDBException { @Test public void annotationOnTypedFunctionTestParses() throws XMLDBException { - final String query = - "declare namespace eg = 'http://example.com';\n" - + "() instance of %eg:x function(xs:integer) as xs:string"; + final String query = """ + declare namespace eg = 'http://example.com'; + () instance of %eg:x function(xs:integer) as xs:string"""; final ResourceSet result = getQueryService().query(query); assertEquals(1, result.getSize()); From 8d031dc164e7ea668b7993cc34f0676933119b17 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Tue, 28 Apr 2026 17:01:19 -0400 Subject: [PATCH 17/20] [bugfix] prod-FunctionDecl: XQ4 keyword args, arity overlap, version gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 2 of Phase 2 Task 3. Brings prod-FunctionDecl from 173/225 (76.8%) to 209/225 (92.8%) — failures 52→16. Phase 2 gate (≤30 failures, ≥80% pass) met. Parser (XQuery.g): * Track parsed XQuery version (10/30/31/40) from versionDecl. The reserved function-name rejection introduced in 9424f11e was too aggressive: it fired in XQuery 1.0 mode, where attribute/comment/etc. were not yet reserved. Gate the check on parsedVersion >= 30, restoring the 14 XQ10 reserved-function-names tests that round 1 broke. * Recognize prefixed keyword arguments (`prefix:local := value`) as either a single QNAME token or a BRACED_URI_LITERAL+NCNAME sequence. The lexer collapses `p:x` into QNAME, which the original predicate (`ncnameOrKeyword COLON …`) never matched, so XQ4 keyword calls like `local:f(q:x := 3)` and `local:f(Q{ns}x := 3)` failed at parse time. Tree walker (XQueryTree.g): * Enforce XQST0148 — a required parameter cannot follow a parameter with a default value. * NamedFunctionReference: when the unprefixed function name resolves to fn:, fall back to a no-namespace user-declared function (PR2200). Function call resolution (FunctionFactory.java): * Match keyword names in Clark notation so `p:x`, `q:x`, and `Q{ns}x` all bind to the same parameter when their prefixes resolve to the same namespace. * Search every parameter position when matching a keyword (not just those at/after the first keyword) so positional + keyword conflicts are caught and raised as XPST0017. * For user-defined functions, surface a null return from resolveKeywordArguments as XPST0017 instead of silently falling back to raw params (which evaluated kw args as positional). * Stop filling unmatched required parameters with empty sequences. A no-default param is required; if neither positional nor keyword supplied it, return null and let the caller raise XPST0017. * Forward references to unprefixed XQ4 functions: when the fn: namespace has no matching built-in, use the no-namespace QName so a later user declaration resolves through the forward-reference path. declareFunction (XQueryContext.java): * XQST0034: detect arity-range overlap between declarations with default parameters. A function with k defaults is callable at arity requiredCount..declaredArity, and any overlap with another overload is ambiguous. Error codes (ErrorCodes.java): * Add XQST0148 (required-after-optional). Test impact (prod-FunctionDecl, with companion runner version-prepend fix in exist-xqts-runner): Before: 173/225 (76.8%), 52 failures After: 209/225 (92.8%), 16 failures (improvement: -36) Remaining 16 failures are pre-existing static-analysis bugs (out-of-scope variable detection in K-FunctionProlog-37/38), XQ4 downcast feature gaps (K2-FunctionProlog-5a/6a), and PR2200 element-constructor cases that need deeper namespace-resolution work. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../antlr/org/exist/xquery/parser/XQuery.g | 55 +++++++-- .../org/exist/xquery/parser/XQueryTree.g | 26 +++++ .../java/org/exist/xquery/ErrorCodes.java | 4 + .../org/exist/xquery/FunctionFactory.java | 108 +++++++++++++++--- .../java/org/exist/xquery/XQueryContext.java | 36 ++++++ 5 files changed, 200 insertions(+), 29 deletions(-) diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g index 8d1e0b69b2a..67e579e64c4 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g @@ -85,6 +85,10 @@ options { protected XQueryLexer lexer; protected boolean xq4Enabled = false; + // Version declared via "xquery version ;" prolog: 10, 30, 31, or 40. + // Default 30 preserves historical eXist behavior (no version decl => XQ 3.0+ rules). + protected int parsedXQueryVersion = 30; + public XQueryParser(XQueryLexer lexer) { this((TokenStream)lexer); this.lexer = lexer; @@ -93,6 +97,8 @@ options { public boolean isXQ4() { return xq4Enabled; } + public int getParsedXQueryVersion() { return parsedXQueryVersion; } + public boolean foundErrors() { return foundError; } @@ -351,8 +357,16 @@ versionDecl throws XPathException "xquery" "version" v:STRING_LITERAL ( "encoding"! enc:STRING_LITERAL )? { #versionDecl = #(#[VERSION_DECL, v.getText()], enc); - if ("4.0".equals(v.getText())) { + final String ver = v.getText(); + if ("4.0".equals(ver)) { xq4Enabled = true; + parsedXQueryVersion = 40; + } else if ("3.1".equals(ver)) { + parsedXQueryVersion = 31; + } else if ("3.0".equals(ver)) { + parsedXQueryVersion = 30; + } else if ("1.0".equals(ver)) { + parsedXQueryVersion = 10; } } ; @@ -578,7 +592,8 @@ functionDecl [XQueryAST ann] throws XPathException : "function"! name=eqName! { - if (isReservedFunctionName(name)) { + // Reserved-name restriction was introduced in XQuery 3.0; XQuery 1.0 allows these names. + if (parsedXQueryVersion >= 30 && isReservedFunctionName(name)) { throw new XPathException(ErrorCodes.XPST0003, "A reserved function name '" + name + "' cannot be used as the name of a function declaration."); } @@ -2170,22 +2185,38 @@ argument throws XPathException : (QUESTION ( ncnameOrKeyword | INTEGER_LITERAL | DECIMAL_LITERAL | DOUBLE_LITERAL | STRING_LITERAL | LPAREN | DOLLAR | SELF | HASH | STAR )) => unaryLookup | argumentPlaceholder + // XQ4 keyword arg lookahead. Four shapes are possible: bare name (ncname COLON EQ), + // lexer-split prefix:local (ncname COLON ncname COLON EQ), lexer-collapsed prefix:local + // (QNAME COLON EQ), and EQName (BRACED_URI_LITERAL ncname COLON EQ). | ( { xq4Enabled }? ncnameOrKeyword COLON ( EQ | ncnameOrKeyword COLON EQ ) ) => keywordArgument + | ( { xq4Enabled }? QNAME COLON EQ ) => keywordArgument + | ( { xq4Enabled }? BRACED_URI_LITERAL ncnameOrKeyword COLON EQ ) => keywordArgument | exprSingle ; -// XQ4: keyword arguments - name := value, or prefix:name := value +// XQ4: keyword arguments - name := value, prefix:name := value, or Q{uri}name := value keywordArgument throws XPathException -{ String kwName = null; String prefix = null; String local = null; } +{ String kwName = null; String prefix = null; String local = null; String uri = null; } : - // Prefixed keyword: prefix:name := value - ( ( ncnameOrKeyword COLON ncnameOrKeyword COLON EQ ) => - prefix=ncnameOrKeyword! COLON! local=ncnameOrKeyword! COLON! EQ! keywordArgumentValue - { kwName = prefix + ":" + local; } - | - // Simple keyword: name := value - kwName=ncnameOrKeyword! COLON! EQ! keywordArgumentValue - ) + ( + // EQName keyword: Q{uri}local := value + ( BRACED_URI_LITERAL ncnameOrKeyword COLON EQ ) => + uriLit:BRACED_URI_LITERAL! local=ncnameOrKeyword! COLON! EQ! keywordArgumentValue + { kwName = "{" + uriLit.getText() + "}" + local; } + | + // Lexer-collapsed prefixed keyword: QNAME(prefix:local) := value + ( QNAME COLON EQ ) => + qn:QNAME! COLON! EQ! keywordArgumentValue + { kwName = qn.getText(); } + | + // Lexer-split prefixed keyword: prefix:name := value (when prefix is a keyword, etc.) + ( ncnameOrKeyword COLON ncnameOrKeyword COLON EQ ) => + prefix=ncnameOrKeyword! COLON! local=ncnameOrKeyword! COLON! EQ! keywordArgumentValue + { kwName = prefix + ":" + local; } + | + // Simple keyword: name := value + kwName=ncnameOrKeyword! COLON! EQ! keywordArgumentValue + ) { #keywordArgument = #(#[KEYWORD_ARG, kwName], #keywordArgument); } diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g index 5b20a9e1728..ea61e380fa6 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g @@ -872,6 +872,21 @@ throws PermissionDeniedException, EXistException, XPathException { processParams(varList, func, signature); + // XQ4 (PR197): a required parameter cannot follow one with a default value. + // Raise XQST0148 if any param without a default value appears after a param + // that has one. + boolean __sawDefault = false; + for (Object __pObj : varList) { + final FunctionParameterSequenceType __param = (FunctionParameterSequenceType) __pObj; + if (__param.hasDefaultValue()) { + __sawDefault = true; + } else if (__sawDefault) { + throw new XPathException(name.getLine(), name.getColumn(), + ErrorCodes.XQST0148, + "A required parameter must not follow a parameter with a default value"); + } + } + final String qualifiedNameArity = signature.getName().toURIQualifiedName() + '#' + signature.getArgumentCount(); if (importedModuleFunctions != null && importedModuleFunctions.contains(qualifiedNameArity)) { throw new XPathException(name.getLine(), name.getColumn(), ErrorCodes.XQST0034, "Prolog has " + @@ -4126,6 +4141,17 @@ throws PermissionDeniedException, EXistException, XPathException } catch (final IllegalQNameException iqe) { throw new XPathException(name.getLine(), name.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + name.getText()); } + // XQ4 (PR2200): unprefixed function references prefer a no-namespace + // user-declared function over the default function namespace (fn:). + if (staticContext.getXQueryVersion() >= 40 + && !name.getText().contains(":") + && org.exist.Namespaces.XPATH_FUNCTIONS_NS.equals(qname.getNamespaceURI())) { + final QName noNsName = new QName(name.getText(), ""); + final int aritynum = Integer.parseInt(arity.getText().replace("_", "")); + if (context.resolveFunction(noNsName, aritynum) != null) { + qname = noNsName; + } + } NamedFunctionReference ref = new NamedFunctionReference(context, qname, Integer.parseInt(arity.getText().replace("_", ""))); step = ref; } diff --git a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java index 8e484a12fb3..40befc4c69b 100644 --- a/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java +++ b/exist-core/src/main/java/org/exist/xquery/ErrorCodes.java @@ -138,6 +138,10 @@ public class ErrorCodes { public static final ErrorCode XQDY0137 = new W3CErrorCode("XQDY0137", "No two keys in a map may have the same key value"); public static final ErrorCode XQDY0138 = new W3CErrorCode("XQDY0138", "Position n does not exist in this array"); + // --- XQuery 4.0 Parser Extensions error codes --- + public static final ErrorCode XQST0148 = new W3CErrorCode("XQST0148", "It is a static error if a parameter without a default value follows a parameter with a default value in a function declaration."); + // --- End XQuery 4.0 Parser Extensions error codes --- + public static final ErrorCode XUDY0023 = new W3CErrorCode("XUDY0023", "It is a dynamic error if an insert, replace, or rename expression affects an element node by introducing a new namespace binding that conflicts with one of its existing namespace bindings."); /* XQuery 1.0 and XPath 2.0 Functions and Operators http://www.w3.org/TR/xpath-functions/#error-summary */ diff --git a/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java b/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java index 07d6a924516..d2b846b472b 100644 --- a/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java +++ b/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java @@ -54,8 +54,12 @@ public static Expression createFunction(XQueryContext context, XQueryAST ast, Pa } catch(final QName.IllegalQNameException xpe) { throw new XPathException(ast, ErrorCodes.XPST0081, "Invalid qname " + ast.getText() + ". " + xpe.getMessage()); } - // XQ4 (PR2200): for unprefixed function calls, check if there's a - // no-namespace user-defined function that should override fn: + // XQ4 (PR2200): unprefixed function calls prefer no-namespace + // user-declared functions over the default function namespace (fn:). + // If a same-name user fn is already declared in no-namespace, switch. + // Otherwise, when no fn: built-in matches the call (forward reference + // territory), still switch to no-namespace so a later user declaration + // can resolve via the forward-reference path. if (context.getXQueryVersion() >= 40 && !ast.getText().contains(":") && Namespaces.XPATH_FUNCTIONS_NS.equals(qname.getNamespaceURI())) { @@ -63,6 +67,8 @@ public static Expression createFunction(XQueryContext context, XQueryAST ast, Pa final UserDefinedFunction noNsFunc = context.resolveFunction(noNsName, params.size()); if (noNsFunc != null) { qname = noNsName; + } else if (!hasInternalOrUserFnFunction(context, qname, params.size())) { + qname = noNsName; } } return createFunction(context, qname, ast, parent, params); @@ -449,7 +455,16 @@ private static FunctionCall getUserDefinedFunction(XQueryContext context, XQuery fc.setLocation(ast.getLine(), ast.getColumn()); if (hasKeywordArgs) { final List resolved = resolveKeywordArguments(context, params, func.getSignature(), ast); - fc.setArguments(resolved != null ? resolved : params); + if (resolved == null) { + // For user-defined functions there is exactly one signature per + // QName+arity, so a null return means an unmatchable keyword + // argument or a missing required parameter — surface as XPST0017. + throw new XPathException(ast.getLine(), ast.getColumn(), + ErrorCodes.XPST0017, + "Keyword arguments do not match the signature of " + + qname.toURIQualifiedName() + '#' + func.getSignature().getArgumentCount()); + } + fc.setArguments(resolved); } else { fc.setArguments(params); } @@ -626,14 +641,20 @@ private static boolean hasKeywordArguments(final List params) { } final KeywordArgumentExpression kwArg = (KeywordArgumentExpression) param; final String kwName = kwArg.getKeywordName(); + final String kwClark = normalizeQNameToClark(context, kwName); - // Find matching parameter by name + // Find matching parameter by name. Compare in Clark notation so + // {prefix:local, Q{ns}local, plain local} all match a parameter that + // resolves to the same expanded QName. Search ALL positions, not + // just those at/after the first keyword, so that supplying the same + // parameter both positionally and by keyword is caught (XPST0017). int matchPos = -1; - for (int j = firstKeyword; j < argTypes.length; j++) { + for (int j = 0; j < argTypes.length; j++) { if (argTypes[j] instanceof org.exist.xquery.value.FunctionParameterSequenceType) { final String paramName = ((org.exist.xquery.value.FunctionParameterSequenceType) argTypes[j]) .getAttributeName(); - if (kwName.equals(paramName)) { + final String paramClark = normalizeQNameToClark(context, paramName); + if (kwClark != null && kwClark.equals(paramClark)) { matchPos = j; break; } @@ -644,16 +665,19 @@ private static boolean hasKeywordArguments(final List params) { return null; // no matching parameter found — signature mismatch } if (resolved.get(matchPos) != null) { + // XQ4 (PR197): supplying the same parameter twice — whether by two + // keyword args or one positional + one keyword — is XPST0017. throw new XPathException(ast.getLine(), ast.getColumn(), - ErrorCodes.XPST0003, - "Duplicate keyword argument: " + kwName); + ErrorCodes.XPST0017, + "Parameter '" + kwName + "' supplied more than once in call"); } resolved.set(matchPos, kwArg.getArgument()); } - // Fill gaps: for parameters that allow empty sequences or have defaults, - // supply an empty sequence expression. This enables keyword arguments to - // skip optional positional parameters in overloaded built-in functions. + // Fill gaps: parameters with default values get them substituted in. + // A parameter without a default is required; if the call did not supply + // it (positionally or by keyword), the signature does not match — return + // null so the caller can report XPST0017 or try another overload. for (int i = 0; i < resolved.size(); i++) { if (resolved.get(i) == null) { if (argTypes[i] instanceof org.exist.xquery.value.FunctionParameterSequenceType) { @@ -661,19 +685,69 @@ private static boolean hasKeywordArguments(final List params) { (org.exist.xquery.value.FunctionParameterSequenceType) argTypes[i]; if (pst.hasDefaultValue()) { resolved.set(i, pst.getDefaultValue()); - } else if (pst.getCardinality().isSuperCardinalityOrEqualOf( - org.exist.xquery.Cardinality.EMPTY_SEQUENCE)) { - // Parameter allows empty — fill with empty sequence - resolved.set(i, new PathExpr(context)); } else { - return null; // required parameter missing + return null; } } else { - return null; // can't determine if parameter is optional + return null; } } } return resolved; } + + /** + * True if the namespace's modules (Internal or XQuery) declare any function + * matching {@code qname}, regardless of arity. Used by the XQ4 PR2200 + * unprefixed-call resolver to decide whether an unmatched call should be + * deferred to no-namespace forward-reference resolution. + */ + private static boolean hasInternalOrUserFnFunction(final XQueryContext context, final QName qname, final int arity) { + final Module[] modules = context.getModules(qname.getNamespaceURI()); + if (modules != null) { + for (final Module module : modules) { + if (module instanceof InternalModule) { + if (((InternalModule) module).getFunctionDef(qname, arity) != null) { + return true; + } + if (!((InternalModule) module).getFunctionsByName(qname).isEmpty()) { + return true; + } + } + } + } + return false; + } + + /** + * Resolve a (possibly prefixed) QName-shaped string to Clark notation + * {@code {namespace}local}. Plain NCNames map to {@code {}local}; EQName + * inputs ({@code Q{uri}local}) and Clark inputs are returned in Clark form. + * Returns {@code null} only when the input is {@code null}; an unresolvable + * prefix falls through to the raw input so error messages stay readable. + */ + private static String normalizeQNameToClark(final XQueryContext context, final String name) { + if (name == null) { + return null; + } + if (name.length() > 0 && name.charAt(0) == '{') { + return name; + } + if (name.length() > 1 && name.charAt(0) == 'Q' && name.charAt(1) == '{') { + // EQName: Q{uri}local — strip the leading 'Q'. + return name.substring(1); + } + final int colonIdx = name.indexOf(':'); + if (colonIdx < 0) { + return "{}" + name; + } + final String prefix = name.substring(0, colonIdx); + final String local = name.substring(colonIdx + 1); + final String uri = context.getURIForPrefix(prefix); + if (uri == null) { + return name; + } + return "{" + uri + "}" + local; + } } diff --git a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java index 600ef6b336b..5c48a857fcd 100644 --- a/exist-core/src/main/java/org/exist/xquery/XQueryContext.java +++ b/exist-core/src/main/java/org/exist/xquery/XQueryContext.java @@ -1859,9 +1859,45 @@ public void declareFunction(final UserDefinedFunction function) throws XPathExce + " is already defined."); } + // XQ4 (PR197): a function declaration with default-valued parameters is + // callable at any arity from (count of required params) up to its + // declared arity. Two declarations with overlapping callable-arity ranges + // are ambiguous — raise XQST0034. The exact-arity collision check above + // already covers the no-defaults case, but ranges can overlap on a + // shared arity even when neither declaration is an exact duplicate. + final int declaredArity = signature.getArgumentCount(); + final int requiredParams = countRequiredParams(signature.getArgumentTypes(), declaredArity); + for (final UserDefinedFunction existing : declaredFunctions.values()) { + if (!existing.getName().equals(name)) continue; + final int eDeclared = existing.getSignature().getArgumentCount(); + final int eRequired = countRequiredParams(existing.getSignature().getArgumentTypes(), eDeclared); + // Skip if neither declaration has a default — exact-arity collision + // is already handled by the FunctionId map check above. + if (requiredParams == declaredArity && eRequired == eDeclared) continue; + final int lo = Math.max(requiredParams, eRequired); + final int hi = Math.min(declaredArity, eDeclared); + if (lo <= hi) { + throw new XPathException(function, ErrorCodes.XQST0034, + "Function " + name.toURIQualifiedName() + + " overlaps with a previously declared overload on arity " + + lo + ".." + hi); + } + } + declaredFunctions.put(functionKey, function); } + private static int countRequiredParams(final SequenceType[] argTypes, final int fallback) { + if (argTypes == null) return fallback; + for (int i = 0; i < argTypes.length; i++) { + if (argTypes[i] instanceof FunctionParameterSequenceType + && ((FunctionParameterSequenceType) argTypes[i]).hasDefaultValue()) { + return i; + } + } + return fallback; + } + @Override public @Nullable UserDefinedFunction resolveFunction(final QName name, final int argCount) { final FunctionId id = new FunctionId(name, argCount); From e7d51af51ba5b2afef2525a9ddea4435d4eabe2e Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Wed, 29 Apr 2026 00:35:42 -0400 Subject: [PATCH 18/20] [refactor] Address review: switch expression in versionDecl, import FQDN - XQuery.g versionDecl: convert if/else-if chain to switch expression with arrow syntax per reinhapa's review - FunctionFactory.java: add explicit import for FunctionParameterSequenceType, replace 4 FQDN usages with simple class name Co-Authored-By: Claude Opus 4.6 (1M context) --- .../main/antlr/org/exist/xquery/parser/XQuery.g | 15 ++++++--------- .../java/org/exist/xquery/FunctionFactory.java | 11 ++++++----- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g index 67e579e64c4..c94382f6120 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g @@ -358,15 +358,12 @@ versionDecl throws XPathException { #versionDecl = #(#[VERSION_DECL, v.getText()], enc); final String ver = v.getText(); - if ("4.0".equals(ver)) { - xq4Enabled = true; - parsedXQueryVersion = 40; - } else if ("3.1".equals(ver)) { - parsedXQueryVersion = 31; - } else if ("3.0".equals(ver)) { - parsedXQueryVersion = 30; - } else if ("1.0".equals(ver)) { - parsedXQueryVersion = 10; + switch (ver) { + case "4.0" -> { xq4Enabled = true; parsedXQueryVersion = 40; } + case "3.1" -> parsedXQueryVersion = 31; + case "3.0" -> parsedXQueryVersion = 30; + case "1.0" -> parsedXQueryVersion = 10; + default -> { } } } ; diff --git a/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java b/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java index d2b846b472b..e0b73474cfd 100644 --- a/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java +++ b/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java @@ -30,6 +30,7 @@ import org.exist.xquery.Constants.Comparison; import org.exist.xquery.Constants.StringTruncationOperator; import org.exist.xquery.parser.XQueryAST; +import org.exist.xquery.value.FunctionParameterSequenceType; import org.exist.xquery.value.SequenceType; import org.exist.xquery.value.StringValue; import org.exist.xquery.value.Type; @@ -650,8 +651,8 @@ private static boolean hasKeywordArguments(final List params) { // parameter both positionally and by keyword is caught (XPST0017). int matchPos = -1; for (int j = 0; j < argTypes.length; j++) { - if (argTypes[j] instanceof org.exist.xquery.value.FunctionParameterSequenceType) { - final String paramName = ((org.exist.xquery.value.FunctionParameterSequenceType) argTypes[j]) + if (argTypes[j] instanceof FunctionParameterSequenceType) { + final String paramName = ((FunctionParameterSequenceType) argTypes[j]) .getAttributeName(); final String paramClark = normalizeQNameToClark(context, paramName); if (kwClark != null && kwClark.equals(paramClark)) { @@ -680,9 +681,9 @@ private static boolean hasKeywordArguments(final List params) { // null so the caller can report XPST0017 or try another overload. for (int i = 0; i < resolved.size(); i++) { if (resolved.get(i) == null) { - if (argTypes[i] instanceof org.exist.xquery.value.FunctionParameterSequenceType) { - final org.exist.xquery.value.FunctionParameterSequenceType pst = - (org.exist.xquery.value.FunctionParameterSequenceType) argTypes[i]; + if (argTypes[i] instanceof FunctionParameterSequenceType) { + final FunctionParameterSequenceType pst = + (FunctionParameterSequenceType) argTypes[i]; if (pst.hasDefaultValue()) { resolved.set(i, pst.getDefaultValue()); } else { From 3dbf368f731be5df4146800aab4a1ac324e37eb1 Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Wed, 29 Apr 2026 02:28:59 -0400 Subject: [PATCH 19/20] [refactor] Remove proactive PMD.NPathComplexity suppressions Per the project convention, do not add @SuppressWarnings("PMD.NPathComplexity") annotations proactively. Let the reviewer decide whether to suppress or refactor. Removes the three annotations added in ee462fe4a3 (ForKeyValueExpr, ForMemberExpr, MethodCallOperator). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/main/java/org/exist/xquery/ForKeyValueExpr.java | 5 ----- .../src/main/java/org/exist/xquery/ForMemberExpr.java | 5 ----- .../src/main/java/org/exist/xquery/MethodCallOperator.java | 6 ------ 3 files changed, 16 deletions(-) diff --git a/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java b/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java index cd7020216a7..e2956b36d3b 100644 --- a/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java @@ -98,11 +98,6 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException } } - // PMD.NPathComplexity: XQuery 4.0 `for key $k value $v in map` evaluator - // mirrors the FLWOR ForExpr structure (profiler/dependency/positional/binding - // branches over map iteration). The branches are required by the spec and - // collapsing them obscures the parallel with ForExpr/ForMemberExpr. - @SuppressWarnings("PMD.NPathComplexity") @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { diff --git a/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java b/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java index 9016c2dbb9d..522cb213331 100644 --- a/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java @@ -79,11 +79,6 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException } } - // PMD.NPathComplexity: XQuery 4.0 `for member $m in array` evaluator - // mirrors the FLWOR ForExpr structure (profiler/dependency/positional/ - // binding branches over array iteration). The branches are required by the - // spec and collapsing them obscures the parallel with ForExpr/ForKeyValueExpr. - @SuppressWarnings("PMD.NPathComplexity") @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { diff --git a/exist-core/src/main/java/org/exist/xquery/MethodCallOperator.java b/exist-core/src/main/java/org/exist/xquery/MethodCallOperator.java index 99f7b2f9ccf..b302c8bdac3 100644 --- a/exist-core/src/main/java/org/exist/xquery/MethodCallOperator.java +++ b/exist-core/src/main/java/org/exist/xquery/MethodCallOperator.java @@ -74,12 +74,6 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException } } - // PMD.NPathComplexity: XQuery 4.0 `?>` method-call operator dispatches - // over input-sequence type (map/array/atomic/node) and resolves the named - // method against either the dynamic-context method-resolver or a built-in - // signature, propagating positional and keyword arguments. The branches - // mirror the spec's resolution rules (XQ40 §4.10.7). - @SuppressWarnings("PMD.NPathComplexity") @Override public Sequence eval(Sequence contextSequence, final Item contextItem) throws XPathException { if (contextItem != null) { From 0f93f1d899133178714c577d8826f4738cc685be Mon Sep 17 00:00:00 2001 From: Joe Wicentowski Date: Wed, 29 Apr 2026 07:44:26 -0400 Subject: [PATCH 20/20] [refactor] Convert switches to switch expressions per review reinhapa requested converting 13 traditional switches to Java 21 switch expressions in PR #6216. Also fix a Codacy parameter-reassign warning in FunctionFactory. Files: - XQuery.g: isReservedFunctionName -> switch expression with comma-separated case labels. - AbstractDateTimeValue.java, StringValue.java: convert fall-through statement switches to arrow syntax. - ForKeyValueExpr.java, ForMemberExpr.java: convert clauseLabel() and prev.getType() loop switches to switch expressions. - LocationStep.java: convert axis dispatch switch to switch expression assigning to result. - LetDestructureExpr.java: convert five switches on mode to switch expressions (getType, eval, dump, toString). - FunctionFactory.java: introduce effectiveParams local variable so the params parameter can stay final (Codacy reassign fix). --- .../antlr/org/exist/xquery/parser/XQuery.g | 26 ++---- .../org/exist/xquery/ForKeyValueExpr.java | 31 +++---- .../java/org/exist/xquery/ForMemberExpr.java | 17 ++-- .../org/exist/xquery/FunctionFactory.java | 10 ++- .../org/exist/xquery/LetDestructureExpr.java | 69 ++++++--------- .../java/org/exist/xquery/LocationStep.java | 88 +++++-------------- .../xquery/value/AbstractDateTimeValue.java | 30 +++---- .../org/exist/xquery/value/StringValue.java | 23 +++-- 8 files changed, 107 insertions(+), 187 deletions(-) diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g index c94382f6120..116b957ad3d 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g @@ -136,26 +136,12 @@ options { if (name == null || name.indexOf(':') >= 0 || name.indexOf('{') >= 0) { return false; } - switch (name) { - case "attribute": - case "comment": - case "document-node": - case "element": - case "function": - case "if": - case "item": - case "namespace-node": - case "node": - case "processing-instruction": - case "schema-attribute": - case "schema-element": - case "switch": - case "text": - case "typeswitch": - return true; - default: - return false; - } + return switch (name) { + case "attribute", "comment", "document-node", "element", "function", + "if", "item", "namespace-node", "node", "processing-instruction", + "schema-attribute", "schema-element", "switch", "text", "typeswitch" -> true; + default -> false; + }; } } diff --git a/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java b/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java index e2956b36d3b..9737bc0b4ac 100644 --- a/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/ForKeyValueExpr.java @@ -212,30 +212,23 @@ public Sequence eval(Sequence contextSequence, final Item contextItem) } private String clauseLabel() { - switch (clauseType) { - case FOR_KEY: return "key"; - case FOR_VALUE: return "value"; - case FOR_KEY_VALUE: return "key/value"; - default: return "key"; - } + return switch (clauseType) { + case FOR_VALUE -> "value"; + case FOR_KEY_VALUE -> "key/value"; + default -> "key"; + }; } private boolean callPostEval() { FLWORClause prev = getPreviousClause(); while (prev != null) { - switch (prev.getType()) { - case LET: - case FOR: - case FOR_MEMBER: - case FOR_KEY: - case FOR_VALUE: - case FOR_KEY_VALUE: - return false; - case ORDERBY: - case GROUPBY: - return true; - default: - break; + final Boolean decision = switch (prev.getType()) { + case LET, FOR, FOR_MEMBER, FOR_KEY, FOR_VALUE, FOR_KEY_VALUE -> Boolean.FALSE; + case ORDERBY, GROUPBY -> Boolean.TRUE; + default -> null; + }; + if (decision != null) { + return decision; } prev = prev.getPreviousClause(); } diff --git a/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java b/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java index 522cb213331..fc2953ec447 100644 --- a/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/ForMemberExpr.java @@ -166,16 +166,13 @@ public Sequence eval(Sequence contextSequence, final Item contextItem) private boolean callPostEval() { FLWORClause prev = getPreviousClause(); while (prev != null) { - switch (prev.getType()) { - case LET: - case FOR: - case FOR_MEMBER: - return false; - case ORDERBY: - case GROUPBY: - return true; - default: - break; + final Boolean decision = switch (prev.getType()) { + case LET, FOR, FOR_MEMBER -> Boolean.FALSE; + case ORDERBY, GROUPBY -> Boolean.TRUE; + default -> null; + }; + if (decision != null) { + return decision; } prev = prev.getPreviousClause(); } diff --git a/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java b/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java index e0b73474cfd..35ea99655e0 100644 --- a/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java +++ b/exist-core/src/main/java/org/exist/xquery/FunctionFactory.java @@ -336,11 +336,12 @@ private static Function functionCall(final XQueryContext context, * @param throwOnNotFound true to throw an XPST0017 if the functions is not found, false to just return null */ private static @Nullable Function getInternalModuleFunction(final XQueryContext context, - final XQueryAST ast, List params, QName qname, Module module, + final XQueryAST ast, final List params, QName qname, Module module, final boolean throwOnNotFound) throws XPathException { //For internal modules: create a new function instance from the class final boolean hasKeywordArgs = hasKeywordArguments(params); FunctionDef def = null; + List effectiveParams = params; // When keyword args are present, skip the initial arity-based lookup because // params.size() may not match the correct overload. Instead, resolve keyword @@ -354,7 +355,7 @@ private static Function functionCall(final XQueryContext context, if (resolved != null) { def = ((InternalModule) module).getFunctionDef(qname, sig.getArgumentCount()); if (def != null) { - params = resolved; + effectiveParams = resolved; break; } } @@ -415,11 +416,12 @@ private static Function functionCall(final XQueryContext context, "Access to deprecated functions is not allowed. Call to '" + qname.getStringValue() + "()' denied. " + def.getSignature().getDeprecated()); } final Function fn = Function.createFunction(context, ast, module, def); - if (hasKeywordArgs) { + if (hasKeywordArgs && effectiveParams == params) { + // No prior keyword-arg resolution succeeded; try once more against def's signature final List resolved = resolveKeywordArguments(context, params, def.getSignature(), ast); fn.setArguments(resolved != null ? resolved : params); } else { - fn.setArguments(params); + fn.setArguments(effectiveParams); } fn.setASTNode(ast); return new InternalFunctionCall(fn); diff --git a/exist-core/src/main/java/org/exist/xquery/LetDestructureExpr.java b/exist-core/src/main/java/org/exist/xquery/LetDestructureExpr.java index 9aedcb6f144..4034e09bda8 100644 --- a/exist-core/src/main/java/org/exist/xquery/LetDestructureExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/LetDestructureExpr.java @@ -73,12 +73,11 @@ public void setOverallType(final SequenceType type) { @Override public ClauseType getType() { - switch (mode) { - case SEQUENCE: return ClauseType.LET_SEQ_DESTRUCTURE; - case ARRAY: return ClauseType.LET_ARRAY_DESTRUCTURE; - case MAP: return ClauseType.LET_MAP_DESTRUCTURE; - default: return ClauseType.LET; - } + return switch (mode) { + case SEQUENCE -> ClauseType.LET_SEQ_DESTRUCTURE; + case ARRAY -> ClauseType.LET_ARRAY_DESTRUCTURE; + case MAP -> ClauseType.LET_MAP_DESTRUCTURE; + }; } @Override @@ -115,17 +114,9 @@ public Sequence eval(Sequence contextSequence, final Item contextItem) throws XP final Sequence input = inputSequence.eval(contextSequence, null); switch (mode) { - case SEQUENCE: - bindSequenceVars(input); - break; - case ARRAY: - bindArrayVars(input); - break; - case MAP: - bindMapVars(input); - break; - default: - throw new XPathException(this, ErrorCodes.ERROR, "Unknown destructure mode: " + mode); + case SEQUENCE -> bindSequenceVars(input); + case ARRAY -> bindArrayVars(input); + case MAP -> bindMapVars(input); } resultSequence = returnExpr.eval(contextSequence, null); @@ -261,22 +252,20 @@ private void checkVarType(final LocalVariable var, final SequenceType type) thro @Override public void dump(final ExpressionDumper dumper) { dumper.display("let "); - switch (mode) { - case SEQUENCE: dumper.display("$("); break; - case ARRAY: dumper.display("$["); break; - case MAP: dumper.display("${"); break; - default: break; - } + dumper.display(switch (mode) { + case SEQUENCE -> "$("; + case ARRAY -> "$["; + case MAP -> "${"; + }); for (int i = 0; i < varNames.size(); i++) { if (i > 0) dumper.display(", "); dumper.display("$").display(varNames.get(i).getLocalPart()); } - switch (mode) { - case SEQUENCE: dumper.display(")"); break; - case ARRAY: dumper.display("]"); break; - case MAP: dumper.display("}"); break; - default: break; - } + dumper.display(switch (mode) { + case SEQUENCE -> ")"; + case ARRAY -> "]"; + case MAP -> "}"; + }); dumper.display(" := "); inputSequence.dump(dumper); dumper.nl().display("return "); @@ -286,22 +275,20 @@ public void dump(final ExpressionDumper dumper) { @Override public String toString() { final StringBuilder sb = new StringBuilder("let "); - switch (mode) { - case SEQUENCE: sb.append("$("); break; - case ARRAY: sb.append("$["); break; - case MAP: sb.append("${"); break; - default: break; - } + sb.append(switch (mode) { + case SEQUENCE -> "$("; + case ARRAY -> "$["; + case MAP -> "${"; + }); for (int i = 0; i < varNames.size(); i++) { if (i > 0) sb.append(", "); sb.append("$").append(varNames.get(i).getLocalPart()); } - switch (mode) { - case SEQUENCE: sb.append(")"); break; - case ARRAY: sb.append("]"); break; - case MAP: sb.append("}"); break; - default: break; - } + sb.append(switch (mode) { + case SEQUENCE -> ")"; + case ARRAY -> "]"; + case MAP -> "}"; + }); sb.append(" := ").append(inputSequence.toString()); sb.append(" return ").append(returnExpr.toString()); return sb.toString(); diff --git a/exist-core/src/main/java/org/exist/xquery/LocationStep.java b/exist-core/src/main/java/org/exist/xquery/LocationStep.java index db87581b741..9a4d93fc216 100644 --- a/exist-core/src/main/java/org/exist/xquery/LocationStep.java +++ b/exist-core/src/main/java/org/exist/xquery/LocationStep.java @@ -384,78 +384,38 @@ public Sequence eval(Sequence contextSequence, final Item contextItem) } try { - switch (axis) { - - case Constants.DESCENDANT_AXIS: - case Constants.DESCENDANT_SELF_AXIS: - result = getDescendants(context, contextSequence); - break; - - case Constants.CHILD_AXIS: - // VirtualNodeSets may have modified the axis ; checking the - // type - // TODO : further checks ? -// if (this.test.getType() == Type.ATTRIBUTE) { -// this.axis = Constants.ATTRIBUTE_AXIS; -// result = getAttributes(context, contextSequence); -// } else { - result = getChildren(context, contextSequence); -// } - break; - - case Constants.ANCESTOR_SELF_AXIS: - case Constants.ANCESTOR_AXIS: - result = getAncestors(context, contextSequence); - break; - - case Constants.PARENT_AXIS: - result = getParents(context, contextSequence); - break; - - case Constants.SELF_AXIS: + result = switch (axis) { + case Constants.DESCENDANT_AXIS, Constants.DESCENDANT_SELF_AXIS -> + getDescendants(context, contextSequence); + case Constants.CHILD_AXIS -> getChildren(context, contextSequence); + case Constants.ANCESTOR_SELF_AXIS, Constants.ANCESTOR_AXIS -> + getAncestors(context, contextSequence); + case Constants.PARENT_AXIS -> getParents(context, contextSequence); + case Constants.SELF_AXIS -> { if (!(contextSequence instanceof VirtualNodeSet) && Type.subTypeOf(contextSequence.getItemType(), Type.ANY_ATOMIC_TYPE)) { - // This test is copied from the legacy method - // getSelfAtomic() + // This test is copied from the legacy method getSelfAtomic() if (!test.isWildcardTest()) { throw new XPathException(this, test.toString() + " cannot be applied to an atomic value."); } - result = contextSequence; - } else { - result = getSelf(context, contextSequence); + yield contextSequence; } - break; - - case Constants.ATTRIBUTE_AXIS: - case Constants.DESCENDANT_ATTRIBUTE_AXIS: - result = getAttributes(context, contextSequence); - break; - - case Constants.PRECEDING_AXIS: - case Constants.FOLLOWING_AXIS: - result = getPrecedingOrFollowing(context, contextSequence); - break; - - case Constants.PRECEDING_SIBLING_AXIS: - case Constants.FOLLOWING_SIBLING_AXIS: - result = getSiblings(context, contextSequence); - break; - - case Constants.FOLLOWING_OR_SELF_AXIS: - case Constants.PRECEDING_OR_SELF_AXIS: - result = getOrSelfAxis(context, contextSequence); - break; - - case Constants.FOLLOWING_SIBLING_OR_SELF_AXIS: - case Constants.PRECEDING_SIBLING_OR_SELF_AXIS: - result = getSiblingOrSelfAxis(context, contextSequence); - break; - - default: - throw new IllegalArgumentException("Unsupported axis specified"); - } + yield getSelf(context, contextSequence); + } + case Constants.ATTRIBUTE_AXIS, Constants.DESCENDANT_ATTRIBUTE_AXIS -> + getAttributes(context, contextSequence); + case Constants.PRECEDING_AXIS, Constants.FOLLOWING_AXIS -> + getPrecedingOrFollowing(context, contextSequence); + case Constants.PRECEDING_SIBLING_AXIS, Constants.FOLLOWING_SIBLING_AXIS -> + getSiblings(context, contextSequence); + case Constants.FOLLOWING_OR_SELF_AXIS, Constants.PRECEDING_OR_SELF_AXIS -> + getOrSelfAxis(context, contextSequence); + case Constants.FOLLOWING_SIBLING_OR_SELF_AXIS, Constants.PRECEDING_SIBLING_OR_SELF_AXIS -> + getSiblingOrSelfAxis(context, contextSequence); + default -> throw new IllegalArgumentException("Unsupported axis specified"); + }; } catch (final XPathException e) { if (e.getLine() <= 0) { e.setLocation(getLine(), getColumn(), getSource()); diff --git a/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java b/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java index 9e695cab84d..47436f7a35a 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/AbstractDateTimeValue.java @@ -178,38 +178,36 @@ protected XMLGregorianCalendar getImplicitCalendar() { } // fill in fields from default reference; don't have to worry about weird combinations of fields being set, since we control that on creation switch (getType()) { - case Type.DATE: - implicitCalendar.setTime(0, 0, 0); - break; - case Type.TIME: + case Type.DATE -> implicitCalendar.setTime(0, 0, 0); + case Type.TIME -> { implicitCalendar.setYear(1972); implicitCalendar.setMonth(12); implicitCalendar.setDay(31); - break; - case Type.G_YEAR: + } + case Type.G_YEAR -> { implicitCalendar.setMonth(1); implicitCalendar.setDay(1); implicitCalendar.setTime(0, 0, 0); - break; - case Type.G_YEAR_MONTH: + } + case Type.G_YEAR_MONTH -> { implicitCalendar.setDay(1); implicitCalendar.setTime(0, 0, 0); - break; - case Type.G_MONTH: + } + case Type.G_MONTH -> { implicitCalendar.setYear(1972); implicitCalendar.setDay(1); implicitCalendar.setTime(0, 0, 0); - break; - case Type.G_MONTH_DAY: + } + case Type.G_MONTH_DAY -> { implicitCalendar.setYear(1972); implicitCalendar.setTime(0, 0, 0); - break; - case Type.G_DAY: + } + case Type.G_DAY -> { implicitCalendar.setYear(1972); implicitCalendar.setMonth(1); implicitCalendar.setTime(0, 0, 0); - break; - default: + } + default -> { } } implicitCalendar = implicitCalendar.normalize(); // the comparison routines will normalize it anyway, just do it once here } diff --git a/exist-core/src/main/java/org/exist/xquery/value/StringValue.java b/exist-core/src/main/java/org/exist/xquery/value/StringValue.java index bc8748b504f..a62ccbb1a7e 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/StringValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/StringValue.java @@ -373,36 +373,33 @@ public StringValue expand() throws XPathException { private void checkType() throws XPathException { switch (type) { - case Type.NORMALIZED_STRING: - case Type.TOKEN: - return; - case Type.LANGUAGE: + case Type.NORMALIZED_STRING, Type.TOKEN -> { } + case Type.LANGUAGE -> { final Matcher matcher = langPattern.matcher(value); if (!matcher.matches()) { throw new XPathException(getExpression(), ErrorCodes.FORG0001, "String '" + value + "' is not valid for type xs:language"); } - return; - case Type.NAME: + } + case Type.NAME -> { if (QName.isQName(value) != VALID.val) { throw new XPathException(getExpression(), ErrorCodes.FORG0001, "String '" + value + "' is not a valid xs:Name"); } - return; - case Type.NCNAME: - case Type.ID: - case Type.IDREF: - case Type.ENTITY: + } + case Type.NCNAME, Type.ID, Type.IDREF, Type.ENTITY -> { if (!XMLNames.isNCName(value)) { throw new XPathException(getExpression(), ErrorCodes.FORG0001, "String '" + value + "' is not a valid " + Type.getTypeName(type)); } - return; - case Type.NMTOKEN: + } + case Type.NMTOKEN -> { if (!XMLNames.isNmToken(value)) { throw new XPathException(getExpression(), ErrorCodes.FORG0001, "String '" + value + "' is not a valid xs:NMTOKEN"); } + } + default -> { } } }