Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2373 (2023-10-27 10:11:39)

refactor: Introduce (empty) SearchInfo class
<!--- Provide a general summary of your changes in the Title above -->

# Description

`SearchInfo` will soon contain selected data passed in from the Query being executed.

This is the [Parameter Object pattern](https://wiki.c2.com/?ParameterObject): it is a container for information that will be passed down through multiple levels of code, in order to be able to in future pass through more data, without having to update the function signatures of all the layers in between.

**Note:** Lots of the `FilterFunction` use don't need the 2nd parameter added, as they only act on the `Task`.

For now, I've taken the decision to only update the locations that needed to be updated in order to compile, rather than having loads of unused variables.

## Motivation and Context

- I found when working with @DanielTMolloy919 on the dependencies code that we needed to pass `Task[]` down from `Query` to the filter functions
- Now I want to be able to pass the query file name down as well, in order to allow `filter by function` to have access to the path of the query block - to provide `query.file.path` etc in custom filtering and grouping.

Now we pass through a `SearchInfo` object, rather than keeping on adding more parameters to pass down through all the layers from `Query` to `FilterFunction`.

And in future `Query` can add more data to `SearchInfo`, and the filters that need that information can use it.

## How has this been tested?

- By running tests
- By checking some of the searches in the Tasks-Demo vault.

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Internal changes:

- [ ] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [ ] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## 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`.
- [ ] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [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 follow 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/src/Query/Filter/BooleanField.ts b/src/Query/Filter/BooleanField.ts
index 53b91c60..9866658e 100644
--- a/src/Query/Filter/BooleanField.ts
+++ b/src/Query/Filter/BooleanField.ts
@@ -4,6 +4,7 @@ import type { PostfixExpression } from 'boon-js';
 import { parseFilter } from '../FilterParser';
 import type { Task } from '../../Task';
 import { Explanation } from '../Explain/Explanation';
+import type { SearchInfo } from '../SearchInfo';
 import { Field } from './Field';
 import { FilterOrErrorMessage } from './FilterOrErrorMessage';
 import { Filter } from './Filter';
@@ -94,8 +95,8 @@ export class BooleanField extends Field {
                 }
             }
             // Return the filter with filter function that can run the complete query
-            const filterFunction = (task: Task) => {
-                return this.filterTaskWithParsedQuery(task, postfixExpression);
+            const filterFunction = (task: Task, searchInfo: SearchInfo) => {
+                return this.filterTaskWithParsedQuery(task, postfixExpression, searchInfo);
             };
             const explanation = this.constructExplanation(postfixExpression);
             return FilterOrErrorMessage.fromFilter(new Filter(line, filterFunction, explanation));
@@ -122,7 +123,11 @@ export class BooleanField extends Field {
      * See here how it works: http://www.btechsmartclass.com/data_structures/postfix-evaluation.html
      * Another reference: https://www.tutorialspoint.com/Evaluate-Postfix-Expression
      */
-    private filterTaskWithParsedQuery(task: Task, postfixExpression: PostfixExpression): boolean {
+    private filterTaskWithParsedQuery(
+        task: Task,
+        postfixExpression: PostfixExpression,
+        searchInfo: SearchInfo,
+    ): boolean {
         const toBool = (s: string | undefined) => {
             return s === 'true';
         };
@@ -137,7 +142,7 @@ export class BooleanField extends Field {
                 // task for each identifier that we find in the postfix expression.
                 if (token.value == null) throw Error('null token value'); // This should not happen
                 const filter = this.subFields[token.value.trim()];
-                const result = filter.filterFunction(task);
+                const result = filter.filterFunction(task, searchInfo);
                 booleanStack.push(toString(result));
             } else if (token.name === 'OPERATOR') {
                 // To evaluate an operator we need to pop the required number of items from the boolean stack,
diff --git a/src/Query/Filter/Filter.ts b/src/Query/Filter/Filter.ts
index dd74a4d1..c7b4c4aa 100644
--- a/src/Query/Filter/Filter.ts
+++ b/src/Query/Filter/Filter.ts
@@ -1,11 +1,15 @@
 import type { Task } from '../../Task';
 import type { Explanation } from '../Explain/Explanation';
+import type { SearchInfo } from '../SearchInfo';
 
 /**
  * A filtering function, that takes a Task object and returns
  * whether it matches a particular filtering instruction.
+ *
+ * SearchInfo is being introduced as a Parameter Object, in order to later allow
+ * more data to be passed from the Query down in to the individual filters.
  */
-export type FilterFunction = (task: Task) => boolean;
+export type FilterFunction = (task: Task, searchInfo: SearchInfo) => boolean;
 
 /**
  * A class that represents a parsed filtering instruction from a tasks code block.
diff --git a/src/Query/SearchInfo.ts b/src/Query/SearchInfo.ts
new file mode 100644
index 00000000..0fdbcdd0
--- /dev/null
+++ b/src/Query/SearchInfo.ts
@@ -0,0 +1,9 @@
+/**
+ * SearchInfo will soon contain selected data passed in from the {@link Query} being executed.
+ *
+ * This is the Parameter Object pattern: it is a container for information that will
+ * be passed down through multiple levels of code, in order to be able to in future
+ * pass through more data, without having to update the function signatures of all
+ * the layers in between.
+ */
+export class SearchInfo {}


Test patch:
diff --git a/tests/CustomMatchers/CustomMatchersForFilters.test.ts b/tests/CustomMatchers/CustomMatchersForFilters.test.ts
new file mode 100644
index 00000000..04cf75cd
--- /dev/null
+++ b/tests/CustomMatchers/CustomMatchersForFilters.test.ts
@@ -0,0 +1,22 @@
+import { SearchInfo } from '../../src/Query/SearchInfo';
+import type { Task } from '../../src/Task';
+import { TaskBuilder } from '../TestingTools/TaskBuilder';
+import { FilterOrErrorMessage } from '../../src/Query/Filter/FilterOrErrorMessage';
+import { Filter } from '../../src/Query/Filter/Filter';
+import { Explanation } from '../../src/Query/Explain/Explanation';
+
+describe('CustomMatchersForFilters', () => {
+    it('should check filter with supplied SearchInfo', () => {
+        // Arrange
+        const initialSearchInfo = new SearchInfo();
+        const checkSearchInfoPassedThrough = (_task: Task, searchInfo: SearchInfo) => {
+            return Object.is(initialSearchInfo, searchInfo);
+        };
+        const filter = FilterOrErrorMessage.fromFilter(
+            new Filter('stuff', checkSearchInfoPassedThrough, new Explanation('explanation of stuff')),
+        );
+
+        // Act, Assert
+        expect(filter).toMatchTaskWithSearchInfo(new TaskBuilder().build(), initialSearchInfo);
+    });
+});
diff --git a/tests/CustomMatchers/CustomMatchersForFilters.ts b/tests/CustomMatchers/CustomMatchersForFilters.ts
index 6f23230e..42da8ae5 100644
--- a/tests/CustomMatchers/CustomMatchersForFilters.ts
+++ b/tests/CustomMatchers/CustomMatchersForFilters.ts
@@ -5,6 +5,7 @@ import { fromLine } from '../TestHelpers';
 import { TaskBuilder } from '../TestingTools/TaskBuilder';
 import type { StatusConfiguration } from '../../src/StatusConfiguration';
 import { Status } from '../../src/Status';
+import { SearchInfo } from '../../src/Query/SearchInfo';
 
 /**
  @summary
@@ -65,6 +66,7 @@ declare global {
         interface Matchers<R> {
             toBeValid(): R;
             toHaveExplanation(expectedExplanation: string): R;
+            toMatchTaskWithSearchInfo(task: Task, searchInfo: SearchInfo): R;
             toMatchTask(task: Task): R;
             toMatchTaskFromLine(line: string): R;
             toMatchTaskWithDescription(description: string): R;
@@ -76,6 +78,7 @@ declare global {
         interface Expect {
             toBeValid(): any;
             toHaveExplanation(expectedExplanation: string): any;
+            toMatchTaskWithSearchInfo(task: Task, searchInfo: SearchInfo): any;
             toMatchTask(task: Task): any;
             toMatchTaskFromLine(line: string): any;
             toMatchTaskWithDescription(description: string): any;
@@ -87,6 +90,7 @@ declare global {
         interface InverseAsymmetricMatchers {
             toBeValid(): any;
             toHaveExplanation(expectedExplanation: string): any;
+            toMatchTaskWithSearchInfo(task: Task, searchInfo: SearchInfo): any;
             toMatchTask(task: Task): any;
             toMatchTaskFromLine(line: string): any;
             toMatchTaskWithDescription(description: string): any;
@@ -141,8 +145,14 @@ export function toHaveExplanation(filter: FilterOrErrorMessage, expectedExplanat
     };
 }
 
-export function toMatchTask(filter: FilterOrErrorMessage, task: Task) {
-    const matches = filter.filterFunction!(task);
+/**
+ * Use this test matcher for any filters that need access to any data from the search.
+ * @param filter
+ * @param task
+ * @param searchInfo
+ */
+export function toMatchTaskWithSearchInfo(filter: FilterOrErrorMessage, task: Task, searchInfo: SearchInfo) {
+    const matches = filter.filterFunction!(task, searchInfo);
     if (!matches) {
         return {
             message: () => `unexpected failure to match
@@ -160,6 +170,10 @@ with filter: "${filter.instruction}"`,
     };
 }
 
+export function toMatchTask(filter: FilterOrErrorMessage, task: Task) {
+    return toMatchTaskWithSearchInfo(filter, task, new SearchInfo());
+}
+
 export function toMatchTaskFromLine(filter: FilterOrErrorMessage, line: string) {
     const task = fromLine({
         line: line,
diff --git a/tests/CustomMatchers/jest.custom_matchers.setup.ts b/tests/CustomMatchers/jest.custom_matchers.setup.ts
index 35582cff..057f4c56 100644
--- a/tests/CustomMatchers/jest.custom_matchers.setup.ts
+++ b/tests/CustomMatchers/jest.custom_matchers.setup.ts
@@ -25,6 +25,7 @@ import {
     toMatchTaskWithDescription,
     toMatchTaskWithHeading,
     toMatchTaskWithPath,
+    toMatchTaskWithSearchInfo,
     toMatchTaskWithStatus,
 } from './CustomMatchersForFilters';
 expect.extend({
@@ -35,6 +36,7 @@ expect.extend({
     toMatchTaskWithDescription,
     toMatchTaskWithHeading,
     toMatchTaskWithPath,
+    toMatchTaskWithSearchInfo,
     toMatchTaskWithStatus,
 });
 
diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.ts b/tests/DocumentationSamples/DocsSamplesForStatuses.test.ts
index 5f72852f..75006213 100644
--- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.ts
+++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.ts
@@ -15,6 +15,7 @@ import { TaskBuilder } from '../TestingTools/TaskBuilder';
 import { MarkdownTable, verifyMarkdownForDocs } from '../TestingTools/VerifyMarkdownTable';
 import { StatusRegistry } from '../../src/StatusRegistry';
 import { verifyWithFileExtension } from '../TestingTools/ApprovalTestHelpers';
+import { SearchInfo } from '../../src/Query/SearchInfo';
 
 function getPrintableSymbol(symbol: string) {
     const result = symbol !== ' ' ? symbol : 'space';
@@ -227,7 +228,7 @@ function verifyTransitionsAsMarkdownTable(statuses: Status[]) {
     function filterAllStatuses(filter: FilterOrErrorMessage) {
         const cells: string[] = [`Matches \`${filter!.instruction}\``];
         tasks.forEach((task) => {
-            const matchedText = filter!.filter?.filterFunction(task) ? 'YES' : 'no';
+            const matchedText = filter!.filter?.filterFunction(task, new SearchInfo()) ? 'YES' : 'no';
             cells.push(matchedText);
         });
         table.addRow(cells);
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index 1a9454fb..17339437 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -10,6 +10,7 @@ import { TaskLocation } from '../src/TaskLocation';
 import { fieldCreators } from '../src/Query/FilterParser';
 import type { Field } from '../src/Query/Filter/Field';
 import type { BooleanField } from '../src/Query/Filter/BooleanField';
+import { SearchInfo } from '../src/Query/SearchInfo';
 import { createTasksFromMarkdown, fromLine } from './TestHelpers';
 import { shouldSupportFiltering } from './TestingTools/FilterTestHelpers';
 import type { FilteringCase } from './TestingTools/FilterTestHelpers';
@@ -204,7 +205,7 @@ describe('Query parsing', () => {
             expect(query.filters.length).toEqual(1);
             expect(query.filters[0]).toBeDefined();
             // If the boolean query and its sub-query are parsed correctly, the expression should always be true
-            expect(query.filters[0].filterFunction(task)).toBeTruthy();
+            expect(query.filters[0].filterFunction(task, new SearchInfo())).toBeTruthy();
         });
     });
 
diff --git a/tests/Query/Filter/FunctionField.test.ts b/tests/Query/Filter/FunctionField.test.ts
index ff49caa2..5bfffbe4 100644
--- a/tests/Query/Filter/FunctionField.test.ts
+++ b/tests/Query/Filter/FunctionField.test.ts
@@ -8,6 +8,7 @@ import { Status } from '../../../src/Status';
 import { Priority } from '../../../src/Task';
 import { toGroupTaskFromBuilder, toGroupTaskWithPath } from '../../CustomMatchers/CustomMatchersForGrouping';
 import { TaskBuilder } from '../../TestingTools/TaskBuilder';
+import { SearchInfo } from '../../../src/Query/SearchInfo';
 
 window.moment = moment;
 
@@ -43,7 +44,7 @@ describe('FunctionField - filtering', () => {
         // Assert
         expect(filter).toBeValid();
         const t = () => {
-            filter.filterFunction!(new TaskBuilder().build());
+            filter.filterFunction!(new TaskBuilder().build(), new SearchInfo());
         };
         expect(t).toThrow(Error);
         expect(t).toThrowError('filtering function must return true or false. This returned "undefined".');
diff --git a/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts b/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts
index ff3de209..c41a2154 100644
--- a/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts
+++ b/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts
@@ -6,6 +6,7 @@ import { verifyMarkdownForDocs } from '../../TestingTools/VerifyMarkdownTable';
 import { expandPlaceholders } from '../../../src/Scripting/ExpandPlaceholders';
 import { makeQueryContext } from '../../../src/Scripting/QueryContext';
 import { scan } from '../../../src/Query/Scanner';
+import { SearchInfo } from '../../../src/Query/SearchInfo';
 
 /** For example, 'task.due' */
 type TaskPropertyName = string;
@@ -91,7 +92,7 @@ export function verifyFunctionFieldFilterSamplesOnTasks(filters: QueryInstructio
         const filterFunction = filterOrErrorMessage.filterFunction!;
         const matchingTasks: string[] = [];
         for (const task of tasks) {
-            const matches = filterFunction(task);
+            const matches = filterFunction(task, new SearchInfo());
             if (matches) {
                 matchingTasks.push(task.toFileLineString());
             }
diff --git a/tests/TestingTools/FilterTestHelpers.ts b/tests/TestingTools/FilterTestHelpers.ts
index 35d1ec7a..88b53b28 100644
--- a/tests/TestingTools/FilterTestHelpers.ts
+++ b/tests/TestingTools/FilterTestHelpers.ts
@@ -2,6 +2,7 @@ import type { FilterOrErrorMessage } from '../../src/Query/Filter/FilterOrErrorM
 import { Task } from '../../src/Task';
 import { Query } from '../../src/Query/Query';
 import { TaskLocation } from '../../src/TaskLocation';
+import { SearchInfo } from '../../src/Query/SearchInfo';
 import type { TaskBuilder } from './TaskBuilder';
 
 /**
@@ -27,7 +28,7 @@ export function testFilter(filter: FilterOrErrorMessage, taskBuilder: TaskBuilde
 export function testTaskFilter(filter: FilterOrErrorMessage, task: Task, expected: boolean) {
     expect(filter.filterFunction).toBeDefined();
     expect(filter.error).toBeUndefined();
-    expect(filter.filterFunction!(task)).toEqual(expected);
+    expect(filter.filterFunction!(task, new SearchInfo())).toEqual(expected);
 }
 
 /**