Repository: FontoXML/fontoxpath
Pull request #627 (2024-01-11 02:02:25)

Bump follow-redirects from 1.15.3 to 1.15.4
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.3 to 1.15.4.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/follow-redirects/follow-redirects/commit/65858205e59f1e23c9bf173348a7a7cbb8ac47f5"><code>6585820</code></a> Release version 1.15.4 of the npm package.</li>
<li><a href="https://github.com/follow-redirects/follow-redirects/commit/7a6567e16dfa9ad18a70bfe91784c28653fbf19d"><code>7a6567e</code></a> Disallow bracketed hostnames.</li>
<li><a href="https://github.com/follow-redirects/follow-redirects/commit/05629af696588b90d64e738bc2e809a97a5f92fc"><code>05629af</code></a> Prefer native URL instead of deprecated url.parse.</li>
<li><a href="https://github.com/follow-redirects/follow-redirects/commit/1cba8e85fa73f563a439fe460cf028688e4358df"><code>1cba8e8</code></a> Prefer native URL instead of legacy url.resolve.</li>
<li><a href="https://github.com/follow-redirects/follow-redirects/commit/72bc2a4229bc18dc9fbd57c60579713e6264cb92"><code>72bc2a4</code></a> Simplify _processResponse error handling.</li>
<li><a href="https://github.com/follow-redirects/follow-redirects/commit/3d42aecdca39b144a0a2f27ea134b4cf67dd796a"><code>3d42aec</code></a> Add bracket tests.</li>
<li><a href="https://github.com/follow-redirects/follow-redirects/commit/bcbb096b32686ecad6cd34235358ed6f2217d4f0"><code>bcbb096</code></a> Do not directly set Error properties.</li>
<li>See full diff in <a href="https://github.com/follow-redirects/follow-redirects/compare/v1.15.3...v1.15.4">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=follow-redirects&package-manager=npm_and_yarn&previous-version=1.15.3&new-version=1.15.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/FontoXML/fontoxpath/network/alerts).

</details>

bundlemon[bot]: <!-- bundlemon -->
## BundleMon

  
  
  
  
  
  
  
<details>
<summary>Unchanged files (2)</summary>

Status | Path | Size | Limits
:------------: | ------------ | :------------: | :------------:
:white_check_mark: | fontoxpath.esm.js<br/> | 77.95KB | -
:white_check_mark: | fontoxpath.js<br/> | 77.84KB | -
</details>



No change in files bundle size

Final result: :white_check_mark:

[View report in BundleMon website ➡️](https://app.bundlemon.dev/projects/6437c8f89a7cb205c434db8d/reports/659f4c6d19c7cb89c814be29)

---
<p align="center"><a href="https://app.bundlemon.dev/projects/6437c8f89a7cb205c434db8d/reports?branch=dependabot%2Fnpm_and_yarn%2Ffollow-redirects-1.15.4&resolution=all" target="_blank" rel="noreferrer noopener">Current branch size history</a> | <a href="https://app.bundlemon.dev/projects/6437c8f89a7cb205c434db8d/reports?branch=master&resolution=days" target="_blank" rel="noreferrer noopener">Target branch size history</a></p>
coveralls: 
[![Coverage Status](https://coveralls.io/builds/65005225/badge)](https://coveralls.io/builds/65005225)

coverage: 91.581%. remained the same
when pulling **452722fc0586cab631990262944d670d87b85f72 on dependabot/npm_and_yarn/follow-redirects-1.15.4**
into **07c10cb15d96c8c3dd1153bc318a76bb4b814a74 on master**.


Patch:
diff --git a/package-lock.json b/package-lock.json
index c4ab928c..62b603eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
 	"name": "fontoxpath",
-	"version": "3.30.2",
+	"version": "3.30.3",
 	"lockfileVersion": 2,
 	"requires": true,
 	"packages": {
 		"": {
 			"name": "fontoxpath",
-			"version": "3.30.2",
+			"version": "3.30.3",
 			"license": "MIT",
 			"dependencies": {
 				"prsc": "4.0.0",
@@ -4325,9 +4325,9 @@
 			}
 		},
 		"node_modules/follow-redirects": {
-			"version": "1.15.3",
-			"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
-			"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
+			"version": "1.15.4",
+			"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
+			"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
 			"dev": true,
 			"funding": [
 				{
@@ -13482,9 +13482,9 @@
 			}
 		},
 		"follow-redirects": {
-			"version": "1.15.3",
-			"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
-			"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
+			"version": "1.15.4",
+			"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
+			"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
 			"dev": true
 		},
 		"for-each": {
diff --git a/package.json b/package.json
index 00bf11ae..b16373e9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "fontoxpath",
-	"version": "3.30.2",
+	"version": "3.30.3",
 	"description": "A minimalistic XPath 3.1 engine in JavaScript",
 	"main": "dist/fontoxpath.js",
 	"module": "dist/fontoxpath.esm.js",
diff --git a/src/expressions/dataTypes/Value.ts b/src/expressions/dataTypes/Value.ts
index 857fb6a6..311b94c3 100644
--- a/src/expressions/dataTypes/Value.ts
+++ b/src/expressions/dataTypes/Value.ts
@@ -81,10 +81,11 @@ export const enum ValueType {
 }
 
 /**
- * Handles the occurances in the XPath specs.
- * Zero or one matches to '?';
- * One or more matches to '+';
- * Zero or more matches to '*'
+ * Handles the occurences in the XPath specs.
+ *
+ * * Zero or one matches to '?';
+ * * One or more matches to '+';
+ * * Zero or more matches to '*';
  *
  * @private
  */
diff --git a/src/expressions/xquery/SwitchExpression.ts b/src/expressions/xquery/SwitchExpression.ts
new file mode 100644
index 00000000..fb77e6f8
--- /dev/null
+++ b/src/expressions/xquery/SwitchExpression.ts
@@ -0,0 +1,135 @@
+import ISequence from '../dataTypes/ISequence';
+import sequenceFactory from '../dataTypes/sequenceFactory';
+import { SequenceType } from '../dataTypes/Value';
+import DynamicContext from '../DynamicContext';
+import ExecutionParameters from '../ExecutionParameters';
+import Expression, { RESULT_ORDERINGS } from '../Expression';
+import sequenceDeepEqual, {
+	itemDeepEqual,
+} from '../functions/builtInFunctions_sequences_deepEqual';
+import PossiblyUpdatingExpression, { SequenceCallbacks } from '../PossiblyUpdatingExpression';
+import Specificity from '../Specificity';
+import StaticContext from '../StaticContext';
+import atomizeSequence from '../util/atomizeSequence';
+import { IterationHint } from '../util/iterators';
+import { errXUST0001 } from '../xquery-update/XQueryUpdateFacilityErrors';
+import { errXPTY0004 } from './XQueryErrors';
+
+export type SwitchExpressionClause = {
+	caseClauseExpression: PossiblyUpdatingExpression;
+	tests: Expression[];
+};
+
+class SwitchExpression extends PossiblyUpdatingExpression {
+	private _amountOfCases: number;
+	private _argExpression: Expression;
+	private _testsByCase: Expression[][];
+
+	constructor(
+		argExpression: Expression,
+		caseClauses: SwitchExpressionClause[],
+		defaultExpression: PossiblyUpdatingExpression,
+		type: SequenceType,
+	) {
+		const specificity = new Specificity({});
+		super(
+			specificity,
+			[
+				argExpression,
+				defaultExpression,
+				...caseClauses.map((clause) => clause.caseClauseExpression),
+			].concat(...caseClauses.map((clause) => clause.tests.map((test) => test))),
+			{
+				canBeStaticallyEvaluated: false,
+				peer: false,
+				resultOrder: RESULT_ORDERINGS.UNSORTED,
+				subtree: false,
+			},
+			type,
+		);
+		this._argExpression = argExpression;
+		this._amountOfCases = caseClauses.length;
+		this._testsByCase = caseClauses.map((clause) => clause.tests);
+	}
+
+	public performFunctionalEvaluation(
+		dynamicContext: DynamicContext,
+		executionParameters: ExecutionParameters,
+		sequenceCallbacks: ((dynamicContext: DynamicContext) => ISequence)[],
+	) {
+		// Pick the argumentExpression.
+		const evaluatedExpression = atomizeSequence(
+			sequenceCallbacks[0](dynamicContext),
+			executionParameters,
+		);
+		const [
+			_argSequenceCallback,
+			defaultSequenceCallback,
+			...caseClauseAndTestSequenceCallbacks
+		] = sequenceCallbacks;
+		// Map over all values the type test, and return the result.
+		return evaluatedExpression.switchCases({
+			multiple: () => {
+				throw errXPTY0004(
+					'The operand for a switch expression should result in zero or one item',
+				);
+			},
+			default: () => {
+				const singleValue = evaluatedExpression.first();
+				const isEmpty = !singleValue;
+
+				for (let i = 0; i < this._amountOfCases; i++) {
+					const results = this._testsByCase[i].map((x) =>
+						x.evaluateMaybeStatically(dynamicContext, executionParameters),
+					);
+
+					for (const result of results) {
+						const atomizedResult = atomizeSequence(result, executionParameters);
+						if (atomizedResult.isEmpty()) {
+							if (isEmpty) {
+								return caseClauseAndTestSequenceCallbacks[i](dynamicContext);
+							}
+							continue;
+						}
+
+						if (!atomizedResult.isSingleton()) {
+							throw errXPTY0004(
+								'The operand for a switch case should result in zero or one item',
+							);
+						}
+
+						if (isEmpty) {
+							continue;
+						}
+
+						const first = atomizedResult.first();
+
+						if (
+							itemDeepEqual(
+								dynamicContext,
+								executionParameters,
+								null,
+								singleValue,
+								first,
+							).next(IterationHint.NONE).value
+						) {
+							return caseClauseAndTestSequenceCallbacks[i](dynamicContext);
+						}
+					}
+				}
+
+				return defaultSequenceCallback(dynamicContext);
+			},
+		});
+	}
+
+	public performStaticEvaluation(staticContext: StaticContext) {
+		super.performStaticEvaluation(staticContext);
+
+		if (this._argExpression.isUpdating) {
+			throw errXUST0001();
+		}
+	}
+}
+
+export default SwitchExpression;
diff --git a/src/parsing/compileAstToExpression.ts b/src/parsing/compileAstToExpression.ts
index babe7aec..39fb3c10 100644
--- a/src/parsing/compileAstToExpression.ts
+++ b/src/parsing/compileAstToExpression.ts
@@ -68,6 +68,7 @@ import ElementConstructor from '../expressions/xquery/ElementConstructor';
 import PIConstructor from '../expressions/xquery/PIConstructor';
 import TextConstructor from '../expressions/xquery/TextConstructor';
 import TypeSwitchExpression from '../expressions/xquery/TypeSwitchExpression';
+import SwitchExpression from '../expressions/xquery/SwitchExpression';
 import astHelper, { IAST } from './astHelper';
 
 const COMPILATION_OPTIONS = {
@@ -204,6 +205,8 @@ function compile(ast: IAST, compilationOptions: CompilationOptions): Expression
 
 		case 'typeswitchExpr':
 			return typeswitchExpr(ast, compilationOptions);
+		case 'switchExpr':
+			return switchExpr(ast, compilationOptions);
 
 		// XQuery node constructors
 		case 'elementConstructor':
@@ -1481,7 +1484,7 @@ function typeswitchExpr(ast: IAST, compilationOptions: CompilationOptions) {
 	const caseClause = astHelper.getChildren(ast, 'typeswitchExprCaseClause');
 
 	const caseClauseExpressions = caseClause.map((caseClauseExpression) => {
-		const sequenceTypesAstNodes: IAST[] =
+		const caseNodes: IAST[] =
 			astHelper.getChildren(caseClauseExpression, 'sequenceTypeUnion').length === 0
 				? [astHelper.getFirstChild(caseClauseExpression, 'sequenceType')]
 				: astHelper.getChildren(
@@ -1496,7 +1499,7 @@ function typeswitchExpr(ast: IAST, compilationOptions: CompilationOptions) {
 
 		return {
 			caseClauseExpression: resultExpression,
-			typeTests: sequenceTypesAstNodes.map((sequenceTypeAstNode: IAST) => {
+			typeTests: caseNodes.map((sequenceTypeAstNode: IAST) => {
 				const occurrenceIndicator = astHelper.getFirstChild(
 					sequenceTypeAstNode,
 					'occurrenceIndicator',
@@ -1522,6 +1525,44 @@ function typeswitchExpr(ast: IAST, compilationOptions: CompilationOptions) {
 	return new TypeSwitchExpression(argExpr, caseClauseExpressions, defaultExpression, type);
 }
 
+function switchExpr(ast: IAST, compilationOptions: CompilationOptions) {
+	if (!compilationOptions.allowXQuery) {
+		throw new Error('XPST0003: Use of XQuery functionality is not allowed in XPath context');
+	}
+
+	const type = astHelper.getAttribute(ast, 'type');
+
+	const argExpr = compile(
+		astHelper.getFirstChild(astHelper.getFirstChild(ast, 'argExpr'), '*'),
+		compilationOptions,
+	);
+
+	const caseClauses = astHelper.getChildren(ast, 'switchExprCaseClause');
+
+	const caseClauseExpressions = caseClauses.map((caseClauseExpression) => {
+		const caseNodes: IAST[] = astHelper.getChildren(caseClauseExpression, 'switchCaseExpr');
+
+		const resultExpression = compile(
+			astHelper.followPath(caseClauseExpression, ['resultExpr', '*']),
+			compilationOptions,
+		) as PossiblyUpdatingExpression;
+
+		return {
+			caseClauseExpression: resultExpression,
+			tests: caseNodes.map((caseNode) =>
+				compile(astHelper.getFirstChild(caseNode, '*'), compilationOptions),
+			),
+		};
+	});
+
+	const defaultExpression = compile(
+		astHelper.followPath(ast, ['switchExprDefaultClause', 'resultExpr', '*']),
+		compilationOptions,
+	) as PossiblyUpdatingExpression;
+
+	return new SwitchExpression(argExpr, caseClauseExpressions, defaultExpression, type);
+}
+
 export default function (xPathAst: IAST, compilationOptions: CompilationOptions): Expression {
 	return compile(xPathAst, compilationOptions);
 }
diff --git a/src/parsing/prscParser.ts b/src/parsing/prscParser.ts
index 1287f7a3..7f62f52f 100644
--- a/src/parsing/prscParser.ts
+++ b/src/parsing/prscParser.ts
@@ -1813,25 +1813,33 @@ function generateParser(options: { outputDebugInfo: boolean; xquery: boolean }):
 
 	const switchCaseClause: Parser<IAST> = then(
 		plus(
-			map(precededMultiple([tokens.CASE, whitespacePlus], switchCaseOperand), (x) => [
-				'switchCaseExpr',
-				x,
-			]),
+			then(
+				map(
+					precededMultiple([tokens.CASE, whitespacePlus], cut(switchCaseOperand)),
+					(x) => ['switchCaseExpr', x],
+				),
+				cut(whitespacePlus),
+				(x, _) => x,
+			),
 		),
-		precededMultiple([whitespacePlus, tokens.RETURN, whitespacePlus], exprSingle),
+		cut(precededMultiple([tokens.RETURN, whitespacePlus], cut(exprSingle))),
 		(operands, exprPart) =>
 			['switchExprCaseClause', ...operands, ['resultExpr', exprPart]] as IAST,
 	);
 
 	const switchExpr: Parser<IAST> = then3(
-		precededMultiple([tokens.SWITCH, whitespace, tokens.BRACE_OPEN], expr),
-		precededMultiple(
-			[whitespace, tokens.BRACE_CLOSE, whitespace],
-			plus(followed(switchCaseClause, whitespace)),
+		precededMultiple([tokens.SWITCH, whitespace, tokens.BRACE_OPEN], cut(expr)),
+		cut(
+			precededMultiple(
+				[whitespace, tokens.BRACE_CLOSE, cut(whitespace)],
+				plus(followed(switchCaseClause, whitespace)),
+			),
 		),
-		precededMultiple(
-			[tokens.DEFAULT, whitespacePlus, tokens.RETURN, whitespacePlus],
-			exprSingle,
+		cut(
+			precededMultiple(
+				[tokens.DEFAULT, whitespacePlus, tokens.RETURN, whitespacePlus],
+				exprSingle,
+			),
 		),
 		(exprPart, clauses, resultExpr) => [
 			'switchExpr',
diff --git a/src/registerXQueryModule.ts b/src/registerXQueryModule.ts
index 25004bba..d0c7055b 100644
--- a/src/registerXQueryModule.ts
+++ b/src/registerXQueryModule.ts
@@ -49,10 +49,15 @@ export default function registerXQueryModule(
 	moduleString: string,
 	options: { debug: boolean } = { debug: false },
 ): string {
-	const parsedModule = parseExpression(moduleString, {
-		allowXQuery: true,
-		debug: options['debug'],
-	});
+	let parsedModule;
+	try {
+		parsedModule = parseExpression(moduleString, {
+			allowXQuery: true,
+			debug: options['debug'],
+		});
+	} catch (error) {
+		printAndRethrowError(moduleString, error);
+	}
 
 	annotateAst(parsedModule, new AnnotationContext(undefined));
 


Test patch:
diff --git a/test/assets/failingXQueryXTestNames.csv b/test/assets/failingXQueryXTestNames.csv
index 91e30755..049247f6 100644
--- a/test/assets/failingXQueryXTestNames.csv
+++ b/test/assets/failingXQueryXTestNames.csv
@@ -2670,8 +2670,6 @@ string-constructor-026,Parse error
 string-constructor-910,Parse error
 string-constructor-911,Parse error
 string-constructor-912,Parse error
-switch-004,Parse error
-switch-005,Parse error
 try-001,Parse error
 try-002,Parse error
 try-003,Parse error
diff --git a/test/assets/runnableTestSets.csv b/test/assets/runnableTestSets.csv
index 09794f25..9b8ff536 100644
--- a/test/assets/runnableTestSets.csv
+++ b/test/assets/runnableTestSets.csv
@@ -408,7 +408,7 @@ prod-SequenceType,true
 prod-SquareArrayConstructor,true
 prod-StepExpr,true
 prod-StringConstructor,true
-prod-SwitchExpr,false
+prod-SwitchExpr,true
 prod-TreatExpr,false
 prod-TryCatchExpr,false
 prod-TypeswitchExpr,true
diff --git a/test/assets/unrunnableTestCases.csv b/test/assets/unrunnableTestCases.csv
index 013e0a8e..da65a04c 100644
--- a/test/assets/unrunnableTestCases.csv
+++ b/test/assets/unrunnableTestCases.csv
@@ -3443,6 +3443,7 @@ string-constructor-028,Error: 1: ( 2:         declare variable $n external := <e
 string-constructor-910,AssertionError: expected [Function] to throw error matching /FOTY0013/ but got '1: \n2:         declare variable $n e…'
 string-constructor-911,AssertionError: expected [Function] to throw error matching /FOTY0013/ but got '1: \n2:         declare variable $n e…'
 string-constructor-912,Error: 1:  2:         declare variable $n as xs:integer external := 10;  3:         ``[There were `{1 to $n}` green bottles]``            ^ 4:        = "There were 1 2 3 4 5 6 7 8 9 10 green bottles"  Error: XPST0003: Failed to parse script. Expected -,+,validate,(#,(,#,parent::,ancestor::,preceding-sibling::,preceding::,ancestor-or-self::,..,child::,descendant::,attribute::,self::,descendant-or-self::,following-sibling::,following::,document-node(,element,attribute,schema-element,schema-attribute(,processing-instruction(,processing-instruction(),comment(),text(),namespace-node(),node(),:*,Q,[A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD],[\uD800-\uDB7F],/,//   at <>:3:9 - 3:10
+switch-018,AssertionError: Expected XPath            let $v := xs:time('00:00:00Z')          return switch($v)          case xs:time('24:00:00Z') return 1          default return 2        to resolve to 1: expected false to be true
 typeswitchhc16,Error: XPST0008, The variable i is not in scope.
 typeswitchhc20,Error: XPST0008, The variable i is not in scope.
 K-sequenceExprTypeswitch-6,Error: No selector counterpart for: voidSequenceType.
@@ -3876,7 +3877,6 @@ UseCaseR31-002,Error: Not implemented: groupByClause is not implemented yet.
 UseCaseR31-003,Error: Not implemented: groupByClause is not implemented yet.
 UseCaseR31-004,Error: 1: let $result := (        ^ 2:         declare namespace map="http://www.w3.org/2005/xpath-functions/map"; 3:   Error: XPST0003: Failed to parse script. Expected end of input   at <>:1:5 - 1:6
 UseCaseR31-009,Error: 1: let $result := (        ^ 2:       declare namespace map="http://www.w3.org/2005/xpath-functions/map"; 3:       declare variable $book:= (<book>  Error: XPST0003: Failed to parse script. Expected end of input   at <>:1:5 - 1:6
-UseCaseR31-012,Error: No selector counterpart for: switchExpr.
 UseCaseR31-015,Error: Not implemented: groupByClause is not implemented yet.
 UseCaseR31-016,Error: 1: deep-equal((              ^ 2:               declare variable $s :=[['A', 'DT'], ['bride', 'NN'], [',', ','], ['you', 'PRP'], ['know', 'VBP'], [',', ','], ['must', 'MD'], 3:  ['appear', 'VB'], ['like', 'IN'], ['a', 'DT'], ['bride', 'NN'], [',', ','], ['but', 'CC'], ['my', 'PRP$'],  Error: XPST0003: Failed to parse script. Expected end of input   at <>:1:11 - 1:12
 UseCaseR31-017,Error: 1: deep-equal((              ^ 2:               declare variable $s :=[['A', 'DT'], ['bride', 'NN'], [',', ','], ['you', 'PRP'], ['know', 'VBP'], [',', ','], ['must', 'MD'], 3:  ['appear', 'VB'], ['like', 'IN'], ['a', 'DT'], ['bride', 'NN'], [',', ','], ['but', 'CC'], ['my', 'PRP$'],  Error: XPST0003: Failed to parse script. Expected end of input   at <>:1:11 - 1:12
diff --git a/test/specs/parsing/registerXQueryModule.tests.ts b/test/specs/parsing/registerXQueryModule.tests.ts
index be06d1c1..f4834560 100644
--- a/test/specs/parsing/registerXQueryModule.tests.ts
+++ b/test/specs/parsing/registerXQueryModule.tests.ts
@@ -345,4 +345,13 @@ declare function bar:baz() {
 			'XPST0017',
 		);
 	});
+
+	it('throws a proper error for parse errors', () => {
+		chai.expect(() => {
+			registerXQueryModule(
+				`declare function local:one() { 1 }(:missing semicolon:)
+				declare function local:two() { 2 };`,
+			);
+		}).to.throw('XPST0003: Failed to parse script. Expected ;');
+	});
 });
diff --git a/test/specs/parsing/xquery/SwitchExpression.tests.ts b/test/specs/parsing/xquery/SwitchExpression.tests.ts
new file mode 100644
index 00000000..dcc6ad0c
--- /dev/null
+++ b/test/specs/parsing/xquery/SwitchExpression.tests.ts
@@ -0,0 +1,102 @@
+import * as chai from 'chai';
+
+import { evaluateXPath, evaluateXPathToNumber } from 'fontoxpath';
+
+describe('Switch', () => {
+	it('works', () =>
+		chai.assert.equal(
+			evaluateXPathToNumber(
+				`
+switch (1)
+case 1 return 1
+default return 0`,
+
+				null,
+				null,
+				null,
+				{
+					debug: true,
+					language: evaluateXPath.XQUERY_3_1_LANGUAGE,
+				},
+			),
+			1,
+		));
+
+	it('works hitting the default case', () =>
+		chai.assert.equal(
+			evaluateXPathToNumber(
+				`
+switch (0)
+case 1 return 1
+default return 0`,
+
+				null,
+				null,
+				null,
+				{
+					debug: true,
+					language: evaluateXPath.XQUERY_3_1_LANGUAGE,
+				},
+			),
+			0,
+		));
+
+	it('works hitting the empty sequence case', () =>
+		chai.assert.equal(
+			evaluateXPathToNumber(
+				`
+switch (())
+case () return 1
+default return 0`,
+
+				null,
+				null,
+				null,
+				{
+					debug: true,
+					language: evaluateXPath.XQUERY_3_1_LANGUAGE,
+				},
+			),
+			1,
+		));
+
+	it('works hitting the empty sequence case as a default', () =>
+		chai.assert.equal(
+			evaluateXPathToNumber(
+				`
+switch (())
+case 1 return 1
+default return 0`,
+
+				null,
+				null,
+				null,
+				{
+					debug: true,
+					language: evaluateXPath.XQUERY_3_1_LANGUAGE,
+				},
+			),
+			0,
+		));
+
+	it('works hitting overload cases', () =>
+		chai.assert.equal(
+			evaluateXPathToNumber(
+				`
+switch (1)
+case ()
+case 1 return 1
+
+default return 0`,
+
+				null,
+				null,
+				null,
+				{
+					debug: true,
+					language: evaluateXPath.XQUERY_3_1_LANGUAGE,
+				},
+			),
+			1,
+		));
+});