Repository: FontoXML/fontoxpath (ts)

Issue:
Bad error when passing a non-node as a context item into `path`
Seems like the normal type checking is skipped in that case.

Example:

```xquery
'a'!path()
```

[Playground link](https://xpath.playground.fontoxml.com/?state=H4sIADwI0GcAA21RwWrDMAz9Fc2XbFDSnUsW2L2DbWzQwS5uojRmthVsuSSU%2FvvkZKWHFQx%2BsqSnJ7%2BTctSi2jyu1FEHo%2FcWo9qo01mt1OiswEqu%2BtsDVGzYYj0OmvtysHo6BEq%2BLTvyTFJUNuSq9VI018fknA5T%2FdGbCHI0WNTBG38AJrLQUYDdy3YFu1ehBO1b2L0lDFNZrS%2B9f4OHOKMFg2mfCmwNF%2FUXJWi0hxwBHqWX%2B8xPHrhHmddxljT86x61G2TVK4NuGowRHAWESxa6QE5kO%2FQJzELJNEAwh%2F4274DBaWv8T1E%2Fe5L6APvEvMgRZoscYZKRsdcSCgrAGBlSzLJFx%2Bf79so8o7x6tc4uZE%2FmDxJbCl3cZSfuH%2BS1EQtw5EtOnX8BWxRB5dcBAAA%3D)

Expected:
```
1: 'a'!path()
   ^^^^^^^^^^

Error: XPTY0004 Unable to cast argument of type xs:string to type node()? while calling path
  at <simpleMapExpr>:1:1 - 1:16
```

Actual:

```
1: 'a'!path()
   ^^^^^^^^^^

TypeError: Cannot read properties of undefined (reading 'Ta')
  at <simpleMapExpr>:1:1 - 1:11
```

Pull request description:
Fix contextItemAsFirstArgument to check type of contextItem
This makes errors for some queries like `1!path()` a lot more readable

Fixes #670

Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index f13e8f57..5cd68726 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -12,9 +12,9 @@ jobs:
             fail-fast: false
             matrix:
                 node-version:
+                    - 22
                     - 20
                     - 18
-                    - 16
         steps:
             - uses: actions/checkout@v2
               with:
diff --git a/src/expressions/functions/argumentHelper.ts b/src/expressions/functions/argumentHelper.ts
index 1d521fab..a13bf1f0 100644
--- a/src/expressions/functions/argumentHelper.ts
+++ b/src/expressions/functions/argumentHelper.ts
@@ -3,6 +3,7 @@ import castToType from '../dataTypes/castToType';
 import ISequence from '../dataTypes/ISequence';
 import isSubtypeOf from '../dataTypes/isSubtypeOf';
 import promoteToType from '../dataTypes/promoteToType';
+import sequenceFactory from '../dataTypes/sequenceFactory';
 import Value, {
 	SequenceMultiplicity,
 	SequenceType,
@@ -10,7 +11,11 @@ import Value, {
 	ValueType,
 	valueTypeToString,
 } from '../dataTypes/Value';
+import DynamicContext from '../DynamicContext';
 import ExecutionParameters from '../ExecutionParameters';
+import StaticContext from '../StaticContext';
+import { errXPDY0002 } from '../XPathErrors';
+import FunctionDefinitionType from './FunctionDefinitionType';
 
 function mapItem(
 	argumentItem: Value,
@@ -149,3 +154,31 @@ export const performFunctionConversion = (
 		},
 	});
 };
+
+export function contextItemAsFirstArgument(
+	functionName: string,
+	parameterType: ValueType,
+	fn: FunctionDefinitionType,
+): FunctionDefinitionType {
+	return (
+		dynamicContext: DynamicContext,
+		executionParameters: ExecutionParameters,
+		staticContext: StaticContext,
+	): ISequence => {
+		if (dynamicContext.contextItem === null) {
+			throw errXPDY0002(
+				`The function ${functionName} depends on dynamic context, which is absent.`,
+			);
+		}
+
+		const contextItemArgument = performFunctionConversion(
+			{ type: parameterType, mult: SequenceMultiplicity.EXACTLY_ONE },
+			sequenceFactory.singleton(dynamicContext.contextItem),
+			executionParameters,
+			functionName,
+			false,
+		);
+
+		return fn(dynamicContext, executionParameters, staticContext, contextItemArgument);
+	};
+}
diff --git a/src/expressions/functions/builtInFunctions_node.ts b/src/expressions/functions/builtInFunctions_node.ts
index 42006831..8f4be779 100644
--- a/src/expressions/functions/builtInFunctions_node.ts
+++ b/src/expressions/functions/builtInFunctions_node.ts
@@ -19,40 +19,18 @@ import isSubtypeOf from '../dataTypes/isSubtypeOf';
 import sequenceFactory from '../dataTypes/sequenceFactory';
 import Value, { SequenceMultiplicity, ValueType } from '../dataTypes/Value';
 import QName from '../dataTypes/valueTypes/QName';
-import DynamicContext from '../DynamicContext';
-import ExecutionParameters from '../ExecutionParameters';
 import arePointersEqual from '../operators/compares/arePointersEqual';
 import { BUILT_IN_NAMESPACE_URIS } from '../staticallyKnownNamespaces';
-import StaticContext from '../StaticContext';
 import { IterationHint } from '../util/iterators';
 import zipSingleton from '../util/zipSingleton';
 import { errXPDY0002 } from '../XPathErrors';
+import { contextItemAsFirstArgument } from './argumentHelper';
 import { BuiltinDeclarationType } from './builtInFunctions';
 import builtinStringFunctions from './builtInFunctions_string';
 import FunctionDefinitionType from './FunctionDefinitionType';
 import generateId from './generateId';
 const fnString = builtinStringFunctions.functions.string;
 
-function contextItemAsFirstArgument(
-	functionName: string,
-	fn: FunctionDefinitionType,
-	dynamicContext: DynamicContext,
-	executionParameters: ExecutionParameters,
-	staticContext: StaticContext,
-) {
-	if (dynamicContext.contextItem === null) {
-		throw errXPDY0002(
-			`The function ${functionName} depends on dynamic context, which is absent.`,
-		);
-	}
-	return fn(
-		dynamicContext,
-		executionParameters,
-		staticContext,
-		sequenceFactory.singleton(dynamicContext.contextItem),
-	);
-}
-
 const fnNodeName: FunctionDefinitionType = (
 	_dynamicContext,
 	executionParameters,
@@ -499,7 +477,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, 'name', fnName),
+		callFunction: contextItemAsFirstArgument('name', ValueType.NODE, fnName),
 		localName: 'name',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
@@ -515,7 +493,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, 'namespace-uri', fnNamespaceURI),
+		callFunction: contextItemAsFirstArgument('namespace-uri', ValueType.NODE, fnNamespaceURI),
 		localName: 'namespace-uri',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSANYURI, mult: SequenceMultiplicity.EXACTLY_ONE },
@@ -550,7 +528,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, 'has-children', fnHasChildren),
+		callFunction: contextItemAsFirstArgument('has-children', ValueType.NODE, fnHasChildren),
 		localName: 'has-children',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSBOOLEAN, mult: SequenceMultiplicity.EXACTLY_ONE },
@@ -566,7 +544,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, 'path', fnPath),
+		callFunction: contextItemAsFirstArgument('path', ValueType.NODE, fnPath),
 		localName: 'path',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.ZERO_OR_ONE },
@@ -582,7 +560,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, 'node-name', fnNodeName),
+		callFunction: contextItemAsFirstArgument('node-name', ValueType.NODE, fnNodeName),
 		localName: 'node-name',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSQNAME, mult: SequenceMultiplicity.ZERO_OR_ONE },
@@ -598,7 +576,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, 'local-name', fnLocalName),
+		callFunction: contextItemAsFirstArgument('local-name', ValueType.NODE, fnLocalName),
 		localName: 'local-name',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
@@ -614,7 +592,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, 'root', fnRoot),
+		callFunction: contextItemAsFirstArgument('root', ValueType.NODE, fnRoot),
 		localName: 'root',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.NODE, mult: SequenceMultiplicity.ZERO_OR_ONE },
@@ -622,7 +600,7 @@ const declarations: BuiltinDeclarationType[] = [
 
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, 'data', fnData),
+		callFunction: contextItemAsFirstArgument('data', ValueType.ITEM, fnData),
 		localName: 'data',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSANYATOMICTYPE, mult: SequenceMultiplicity.ZERO_OR_MORE },
@@ -654,7 +632,7 @@ const declarations: BuiltinDeclarationType[] = [
 	},
 	{
 		argumentTypes: [],
-		callFunction: contextItemAsFirstArgument.bind(null, 'generate-id', fnGenerateId),
+		callFunction: contextItemAsFirstArgument('generate-id', ValueType.NODE, fnGenerateId),
 		localName: 'generate-id',
 		namespaceURI: BUILT_IN_NAMESPACE_URIS.FUNCTIONS_NAMESPACE_URI,
 		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
diff --git a/src/expressions/functions/builtInFunctions_string.ts b/src/expressions/functions/builtInFunctions_string.ts
index 291c775b..5e7d4f85 100644
--- a/src/expressions/functions/builtInFunctions_string.ts
+++ b/src/expressions/functions/builtInFunctions_string.ts
@@ -16,6 +16,7 @@ import { errXPDY0002 } from '../XPathErrors';
 import { BuiltinDeclarationType } from './builtInFunctions';
 import builtInNumericFunctions from './builtInFunctions_numeric';
 import FunctionDefinitionType from './FunctionDefinitionType';
+import { contextItemAsFirstArgument } from './argumentHelper';
 
 const fnRound = builtInNumericFunctions.functions.round;
 
@@ -23,25 +24,6 @@ function collationError(): ISequence {
 	throw new Error('FOCH0002: No collations are supported');
 }
 
-function contextItemAsFirstArgument(
-	fn: FunctionDefinitionType,
-	dynamicContext: DynamicContext,
-	executionParameters: ExecutionParameters,
-	staticContext: StaticContext,
-) {
-	if (dynamicContext.contextItem === null) {
-		throw errXPDY0002(
-			'The function which was called depends on dynamic context, which is absent.',
-		);
-	}
-	return fn(
-		dynamicContext,
-		executionParameters,
-		staticContext,
-		sequenceFactory.singleton(dynamicContext.contextItem),
-	);
-}
-
 const fnCompare: FunctionDefinitionType = (
 	_dynamicContext,
 	_executionParameters,
@@ -767,8 +749,9 @@ const declarations: BuiltinDeclarationType[] = [
 		localName: 'normalize-space',
 		argumentTypes: [],
 		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
-		callFunction: contextItemAsFirstArgument.bind(
-			null,
+		callFunction: contextItemAsFirstArgument(
+			'normalize-space',
+			ValueType.XSSTRING,
 			(
 				dynamicContext: DynamicContext,
 				executionParameters: ExecutionParameters,
@@ -820,7 +803,7 @@ const declarations: BuiltinDeclarationType[] = [
 		localName: 'string',
 		argumentTypes: [],
 		returnType: { type: ValueType.XSSTRING, mult: SequenceMultiplicity.EXACTLY_ONE },
-		callFunction: contextItemAsFirstArgument.bind(null, fnString),
+		callFunction: contextItemAsFirstArgument('string', ValueType.ITEM, fnString),
 	},
 
 	{
@@ -926,8 +909,11 @@ const declarations: BuiltinDeclarationType[] = [
 		localName: 'string-length',
 		argumentTypes: [],
 		returnType: { type: ValueType.XSINTEGER, mult: SequenceMultiplicity.EXACTLY_ONE },
-		callFunction: contextItemAsFirstArgument.bind(
-			null,
+		callFunction: contextItemAsFirstArgument(
+			'string-length',
+			// Note: deviate from the spec to allow expressions line `1!fn:string-length()` to
+			// atomize the input still
+			ValueType.XSANYATOMICTYPE,
 			(
 				dynamicContext: DynamicContext,
 				executionParameters: ExecutionParameters,


Test patch:
diff --git a/test/assets/unrunnableTestCases.csv b/test/assets/unrunnableTestCases.csv
index 3dcc816c..80dc0725 100644
--- a/test/assets/unrunnableTestCases.csv
+++ b/test/assets/unrunnableTestCases.csv
@@ -726,11 +726,8 @@ generate-id-015,Error: No selector counterpart for: computedDocumentConstructor.
 generate-id-016,Error: No selector counterpart for: computedDocumentConstructor.
 generate-id-017,Error: No selector counterpart for: computedDocumentConstructor.
 generate-id-018,Error: XPTY0004: Multiplicity of function argument of type node()? for generate-id is incorrect. Expected "?", but got "+".
-fn-has-children-007,AssertionError: Expected executing the XPath "(1)[fn:has-children()]" to resolve to one of the expected results, but got AssertionError: expected [Function] to throw error matching /XPTY0004/ but got 'Cannot read properties of undefined (…'.
-fn-has-children-008,AssertionError: Expected executing the XPath "(fn:concat#2)[fn:has-children()]" to resolve to one of the expected results, but got AssertionError: expected [Function] to throw error matching /XPTY0004/ but got 'Cannot read properties of undefined (…'.
 fn-has-children-012,AssertionError: expected [Function] to throw an error
 fn-has-children-013,AssertionError: expected [Function] to throw an error
-fn-has-children-014,AssertionError: expected [Function] to throw error matching /XPTY0004/ but got 'Cannot read properties of undefined (…'
 fn-hours-from-dateTime-3,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}adjust-dateTime-to-timezone with arity of 2 not registered. Did you mean "Q{test}custom-dateTime-function ()"?
 fn-hours-from-time-4,Error: XPST0017: Function Q{http://www.w3.org/2005/xpath-functions}adjust-time-to-timezone with arity of 2 not registered. No similar functions found.
 fn-id-4,Error: No selector counterpart for: computedDocumentConstructor.
@@ -981,8 +978,6 @@ fn-node-name-15,Error: No selector counterpart for: computedDocumentConstructor.
 fn-node-name-16,Error: No selector counterpart for: computedDocumentConstructor.
 fn-node-name-17,Error: No selector counterpart for: computedDocumentConstructor.
 fn-node-name-28,Error: No selector counterpart for: computedNamespaceConstructor.
-fn-node-name-30,AssertionError: expected [Function] to throw error matching /XPTY0004/ but got 'Cannot read properties of undefined (…'
-fn-node-name-31,AssertionError: expected [Function] to throw error matching /XPTY0004/ but got 'Cannot read properties of undefined (…'
 fn-notintg1args-1,Error: FOCA0003: can not cast -999999999999999999 to xs:integer, it is out of bounds for JavaScript numbers.
 fn-notintg1args-2,Error: FOCA0003: can not cast 830993497117024304 to xs:integer, it is out of bounds for JavaScript numbers.
 fn-notintg1args-3,Error: FOCA0003: can not cast 999999999999999999 to xs:integer, it is out of bounds for JavaScript numbers.
@@ -2461,8 +2456,8 @@ cbcl-cast-gYearMonth-003,AssertionError: expected [Function] to throw an error
 XQueryComment014,Error: 3:  4:  5: "10" cast as (: type comment :) xs:integer ?                                               ^ 6:  7:  = 10  Error: XPST0003: Failed to parse script. Expected end of input   at <>:5:44 - 5:45
 Constr-compattr-compname-20,Error: XQDY0074: The value "Q{http://example.com/x}y" of a name expressions cannot be converted to an expanded QName.
 Constr-compattr-compname-21,Error: XQDY0074: The value " Q{}y " of a name expressions cannot be converted to an expanded QName.
-Constr-compattr-compname-22,Error: XQDY0074: The value " Q{http://example.com/x}y2024" of a name expressions cannot be converted to an expanded QName.
-Constr-compattr-compname-23,Error: XQDY0074: The value "Q{}y2024" of a name expressions cannot be converted to an expanded QName.
+Constr-compattr-compname-22,Error: XQDY0074: The value " Q{http://example.com/x}y2025" of a name expressions cannot be converted to an expanded QName.
+Constr-compattr-compname-23,Error: XQDY0074: The value "Q{}y2025" of a name expressions cannot be converted to an expanded QName.
 Constr-compattr-id-2,AssertionError: Expected executing the XPath "element elem {attribute xml:id {" ab c d "}}" to resolve to one of the expected results, but got AssertionError: Expected XPath element elem {attribute xml:id {" ab c d "}} to resolve to the given XML. Expected <elem xml:id=" ab c d "/> to equal <elem xml:id="ab c d"/>, AssertionError: expected [Function] to throw an error.
 K2-ComputeConAttr-34,AssertionError: Expected XPath <e> { attribute name {xs:hexBinary("ff")}, attribute name2 {"content"} } </e> to resolve to the given XML. Expected <e name="ff" name2="content"/> to equal <e name="FF" name2="content"/>
 K2-ComputeConAttr-48,AssertionError: Expected executing the XPath "string(attribute xml:id {" ab c d "})" to resolve to one of the expected results, but got AssertionError: string(attribute xml:id {" ab c d "}): expected ' ab c d ' to equal 'ab c d', AssertionError: expected [Function] to throw an error.
@@ -2548,8 +2543,8 @@ cbcl-constr-compcomment-001,AssertionError: expected [Function] to throw an erro
 cbcl-constr-compcomment-002,AssertionError: expected [Function] to throw an error
 Constr-compelem-name-20,Error: XQDY0074: The value "Q{http://example.com/x}x" of a name expressions cannot be converted to an expanded QName.
 Constr-compelem-name-21,Error: XQDY0074: The value "Q{}x" of a name expressions cannot be converted to an expanded QName.
-Constr-compelem-name-22,Error: XQDY0074: The value " Q{http://example.com/x}x2024" of a name expressions cannot be converted to an expanded QName.
-Constr-compelem-name-23,Error: XQDY0074: The value " Q{}x2024 " of a name expressions cannot be converted to an expanded QName.
+Constr-compelem-name-22,Error: XQDY0074: The value " Q{http://example.com/x}x2025" of a name expressions cannot be converted to an expanded QName.
+Constr-compelem-name-23,Error: XQDY0074: The value " Q{}x2025 " of a name expressions cannot be converted to an expanded QName.
 Constr-compelem-name-24,AssertionError: element {" x" || year-from-date(current-date()) || " "} {}: expected false to be true
 Constr-compelem-constrmod-3,AssertionError: expected [Function] to throw error matching /FORG0001/ but got 'Not implemented: only module imports,…'
 Constr-compelem-constrmod-4,AssertionError: Expected executing the XPath "declare construction preserve; (element elem {xs:decimal((//decimal[1]))}) cast as xs:integer" to resolve to one of the expected results, but got Error: Not implemented: only module imports, namespace declarations, and function declarations are implemented in XQuery modules, AssertionError: expected [Function] to throw error matching /FORG0001/ but got 'Not implemented: only module imports,…'.
diff --git a/test/specs/parsing/functions/functions.node.tests.ts b/test/specs/parsing/functions/functions.node.tests.ts
index 520a8444..70562ebe 100644
--- a/test/specs/parsing/functions/functions.node.tests.ts
+++ b/test/specs/parsing/functions/functions.node.tests.ts
@@ -502,6 +502,22 @@ return $node/root() = $element`,
 			);
 		});
 
+		it('returns the correct error if the context argument is of the wrong type', () => {
+			documentNode = slimdom.parseXmlDocument(
+				`<p xmlns="http://example.com/one" xml:lang="de" author="Friedrich von Schiller">
+				Freude, schöner Götterfunken,<br/>
+				Tochter aus Elysium,<br/>
+				Wir betreten feuertrunken,<br/>
+				Himmlische, dein Heiligtum.</p>`,
+			);
+
+			chai.assert.throws(() => {
+				evaluateXPathToString(`'string'!path()`, documentNode, null, null, {
+					language: evaluateXPath.XQUERY_3_1_LANGUAGE,
+				});
+			}, 'XPTY0004');
+		});
+
 		it('returns "/Q{http://example.com/one}p[1]" for the <p> element of the document', () => {
 			documentNode = slimdom.parseXmlDocument(
 				`<p xmlns="http://example.com/one" xml:lang="de" author="Friedrich von Schiller">