Repository: obsidian-tasks-group/obsidian-tasks
Pull request #3075 (2024-09-11 02:49:55)

fix: Make 'filter by function' show statement in error messages
# Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)

Internal changes:

- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Description

When an exception is thrown whilst filtering tasks, include the problem statement in the error text.

I've noticed a growing number of users using `filter by function`, often for simple cases that can be done using built-in instructions.

Try to make it easier for users to figure out which instruction is causing the problem - and to aid in user support too.

## Motivation and Context

Make it easier to work out which instruction caused "Search failed" messages in queries that have more than one filter.
This is especially relevant when there are multiple `filter by function` statements in a Query.

## How has this been tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. -->

- Updating existing test
- Adding new content to the test vault, to see the changed behaviour

## Screenshots (if appropriate)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follows this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/resources/sample_vaults/Tasks-Demo/Functions/Custom Filters - Demo.md b/resources/sample_vaults/Tasks-Demo/Functions/Custom Filters - Demo.md
index f6c5d5b7..ce08ca65 100644
--- a/resources/sample_vaults/Tasks-Demo/Functions/Custom Filters - Demo.md
+++ b/resources/sample_vaults/Tasks-Demo/Functions/Custom Filters - Demo.md
@@ -43,3 +43,34 @@ filename includes Custom Filters - Demo
 # Infer tag from heading
 filter by function task.heading.includes('#context/home') || task.tags.find( (tag) => tag === '#context/home' ) && true || false
 ```
+
+## Demonstrate Error Handling
+
+### Parsing Errors
+
+This section demonstrate how Tasks handles errors when reading `filter by function` instructions.
+
+#### SyntaxError
+
+```tasks
+filter by function \
+    task.due.formatAsDate(
+```
+
+### Evaluation Errors
+
+This section demonstrate how Tasks handles when evaluating `filter by function` instructions during searches.
+
+#### ReferenceError
+
+```tasks
+filter by function \
+    hello
+```
+
+#### Non-existent task field
+
+```tasks
+filter by function \
+    task.nonExistentField
+```
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index b3ff5c27..ef1d4178 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -246,11 +246,15 @@ ${source}`;
     }
 
     private setError(message: string, statement: Statement) {
+        this._error = Query.generateErrorMessage(statement, message);
+    }
+
+    private static generateErrorMessage(statement: Statement, message: string) {
         if (statement.allLinesIdentical()) {
-            this._error = `${message}
+            return `${message}
 Problem line: "${statement.rawInstruction}"`;
         } else {
-            this._error = `${message}
+            return `${message}
 Problem statement:
 ${statement.explainStatement('    ')}
 `;
@@ -265,10 +269,16 @@ ${statement.explainStatement('    ')}
         this.debug(`Executing query: ${this.formatQueryForLogging()}`);
 
         const searchInfo = new SearchInfo(this.tasksFile, tasks);
+
+        // Custom filter (filter by function) does not report the instruction line in any exceptions,
+        // for performance reasons. So we keep track of it here.
+        let possiblyBrokenStatement: Statement | undefined = undefined;
         try {
             this.filters.forEach((filter) => {
+                possiblyBrokenStatement = filter.statement;
                 tasks = tasks.filter((task) => filter.filterFunction(task, searchInfo));
             });
+            possiblyBrokenStatement = undefined;
 
             const { debugSettings } = getSettings();
             const tasksSorted = debugSettings.ignoreSortInstructions ? tasks : Sort.by(this.sorting, tasks, searchInfo);
@@ -283,7 +293,12 @@ ${statement.explainStatement('    ')}
             return new QueryResult(taskGroups, tasksSorted.length);
         } catch (e) {
             const description = 'Search failed';
-            return QueryResult.fromError(errorMessageForException(description, e));
+            let message = errorMessageForException(description, e);
+
+            if (possiblyBrokenStatement) {
+                message = Query.generateErrorMessage(possiblyBrokenStatement, message);
+            }
+            return QueryResult.fromError(message);
         }
     }
 


Test patch:
diff --git a/tests/Query/Query.test.ts b/tests/Query/Query.test.ts
index 74fa93a0..8e9551d2 100644
--- a/tests/Query/Query.test.ts
+++ b/tests/Query/Query.test.ts
@@ -1542,7 +1542,8 @@ describe('Query', () => {
     describe('error handling', () => {
         it('should catch an exception that occurs during searching', () => {
             // Arrange
-            const source = 'filter by function wibble';
+            const source = `filter by function \\
+wibble`;
             const query = new Query(source);
             const queryUpper = new Query(source.toUpperCase());
             const task = TaskBuilder.createFullyPopulatedTask();
@@ -1553,10 +1554,26 @@ describe('Query', () => {
 
             // Assert
             expect(queryResult.searchErrorMessage).toEqual(
-                'Error: Search failed.\nThe error message was:\n    "ReferenceError: wibble is not defined"',
+                `Error: Search failed.
+The error message was:
+    "ReferenceError: wibble is not defined"
+Problem statement:
+    filter by function \\
+    wibble
+     =>
+    filter by function wibble
+`,
             );
             expect(queryResultUpper.searchErrorMessage).toEqual(
-                'Error: Search failed.\nThe error message was:\n    "ReferenceError: WIBBLE is not defined"',
+                `Error: Search failed.
+The error message was:
+    "ReferenceError: WIBBLE is not defined"
+Problem statement:
+    FILTER BY FUNCTION \\
+    WIBBLE
+     =>
+    FILTER BY FUNCTION WIBBLE
+`,
             );
         });
     });