diff --git a/src/Query/Query.ts b/src/Query/Query.ts index 30f0a11756..21d7a18ac1 100644 --- a/src/Query/Query.ts +++ b/src/Query/Query.ts @@ -13,6 +13,7 @@ import type { Grouper } from './Grouper'; import type { Filter } from './Filter/Filter'; import { QueryResult } from './QueryResult'; import { scan } from './Scanner'; +import { SearchInfo } from './SearchInfo'; export class Query implements IQuery { /** Note: source is the raw source, before expanding any placeholders */ @@ -232,9 +233,10 @@ Problem line: "${line}"`; } public applyQueryToTasks(tasks: Task[]): QueryResult { + const searchInfo = new SearchInfo(tasks); try { this.filters.forEach((filter) => { - tasks = tasks.filter(filter.filterFunction); + tasks = tasks.filter((task) => filter.filterFunction(task, searchInfo)); }); const { debugSettings } = getSettings(); diff --git a/src/Query/SearchInfo.ts b/src/Query/SearchInfo.ts index 0fdbcdd02e..7ec6f58767 100644 --- a/src/Query/SearchInfo.ts +++ b/src/Query/SearchInfo.ts @@ -1,9 +1,18 @@ +import type { Task } from '../Task'; + /** - * SearchInfo will soon contain selected data passed in from the {@link Query} being executed. + * SearchInfo contains 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. + * This is the Parameter Object pattern: it is a container for information that needs + * to be passed down through multiple levels of code, without having to update + * the function signatures of all the layers in between. */ -export class SearchInfo {} +export class SearchInfo { + /** The list of tasks being searched. + */ + public readonly allTasks: Readonly; + + public constructor(allTasks: Task[]) { + this.allTasks = [...allTasks]; + } +} diff --git a/tests/CustomMatchers/CustomMatchersForFilters.test.ts b/tests/CustomMatchers/CustomMatchersForFilters.test.ts index 04cf75cda6..4313558110 100644 --- a/tests/CustomMatchers/CustomMatchersForFilters.test.ts +++ b/tests/CustomMatchers/CustomMatchersForFilters.test.ts @@ -8,7 +8,8 @@ import { Explanation } from '../../src/Query/Explain/Explanation'; describe('CustomMatchersForFilters', () => { it('should check filter with supplied SearchInfo', () => { // Arrange - const initialSearchInfo = new SearchInfo(); + const task = new TaskBuilder().build(); + const initialSearchInfo = new SearchInfo([task]); const checkSearchInfoPassedThrough = (_task: Task, searchInfo: SearchInfo) => { return Object.is(initialSearchInfo, searchInfo); }; @@ -17,6 +18,6 @@ describe('CustomMatchersForFilters', () => { ); // Act, Assert - expect(filter).toMatchTaskWithSearchInfo(new TaskBuilder().build(), initialSearchInfo); + expect(filter).toMatchTaskWithSearchInfo(task, initialSearchInfo); }); }); diff --git a/tests/CustomMatchers/CustomMatchersForFilters.ts b/tests/CustomMatchers/CustomMatchersForFilters.ts index 42da8ae578..cf4c5f7130 100644 --- a/tests/CustomMatchers/CustomMatchersForFilters.ts +++ b/tests/CustomMatchers/CustomMatchersForFilters.ts @@ -171,7 +171,7 @@ with filter: "${filter.instruction}"`, } export function toMatchTask(filter: FilterOrErrorMessage, task: Task) { - return toMatchTaskWithSearchInfo(filter, task, new SearchInfo()); + return toMatchTaskWithSearchInfo(filter, task, new SearchInfo([task])); } export function toMatchTaskFromLine(filter: FilterOrErrorMessage, line: string) { diff --git a/tests/DocumentationSamples/DocsSamplesForStatuses.test.ts b/tests/DocumentationSamples/DocsSamplesForStatuses.test.ts index 75006213e2..1a28721ead 100644 --- a/tests/DocumentationSamples/DocsSamplesForStatuses.test.ts +++ b/tests/DocumentationSamples/DocsSamplesForStatuses.test.ts @@ -227,8 +227,9 @@ function verifyTransitionsAsMarkdownTable(statuses: Status[]) { function filterAllStatuses(filter: FilterOrErrorMessage) { const cells: string[] = [`Matches \`${filter!.instruction}\``]; + const searchInfo = new SearchInfo(tasks); tasks.forEach((task) => { - const matchedText = filter!.filter?.filterFunction(task, new SearchInfo()) ? 'YES' : 'no'; + const matchedText = filter!.filter?.filterFunction(task, searchInfo) ? 'YES' : 'no'; cells.push(matchedText); }); table.addRow(cells); diff --git a/tests/Query.test.ts b/tests/Query.test.ts index 17339437c2..3e132a0b3c 100644 --- a/tests/Query.test.ts +++ b/tests/Query.test.ts @@ -11,6 +11,9 @@ 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 { FilterOrErrorMessage } from '../src/Query/Filter/FilterOrErrorMessage'; +import { Explanation } from '../src/Query/Explain/Explanation'; +import { Filter } from '../src/Query/Filter/Filter'; import { createTasksFromMarkdown, fromLine } from './TestHelpers'; import { shouldSupportFiltering } from './TestingTools/FilterTestHelpers'; import type { FilteringCase } from './TestingTools/FilterTestHelpers'; @@ -205,7 +208,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, new SearchInfo())).toBeTruthy(); + expect(query.filters[0].filterFunction(task, new SearchInfo([task]))).toBeTruthy(); }); }); @@ -615,8 +618,9 @@ describe('Query', () => { // Act let filteredTasks = [...tasks]; + const searchInfo = new SearchInfo(tasks); query.filters.forEach((filter) => { - filteredTasks = filteredTasks.filter(filter.filterFunction); + filteredTasks = filteredTasks.filter((task) => filter.filterFunction(task, searchInfo)); }); // Assert @@ -1026,6 +1030,29 @@ describe('Query', () => { }); }); + describe('SearchInfo', () => { + it('should pass SearchInfo through to filter functions', () => { + // Arrange + const same1 = new TaskBuilder().description('duplicate').build(); + const same2 = new TaskBuilder().description('duplicate').build(); + const different = new TaskBuilder().description('different').build(); + const allTasks = [same1, same2, different]; + + const moreThanOneTaskHasThisDescription = (task: Task, searchInfo: SearchInfo) => { + return searchInfo.allTasks.filter((t) => t.description === task.description).length > 1; + }; + const filter = FilterOrErrorMessage.fromFilter( + new Filter('stuff', moreThanOneTaskHasThisDescription, new Explanation('explanation of stuff')), + ); + + // Act, Assert + const searchInfo = new SearchInfo(allTasks); + expect(filter).toMatchTaskWithSearchInfo(same1, searchInfo); + expect(filter).toMatchTaskWithSearchInfo(same2, searchInfo); + expect(filter).not.toMatchTaskWithSearchInfo(different, searchInfo); + }); + }); + describe('sorting', () => { const doneTask = new TaskBuilder().status(Status.DONE).build(); const todoTask = new TaskBuilder().status(Status.TODO).build(); diff --git a/tests/Query/Filter/FunctionField.test.ts b/tests/Query/Filter/FunctionField.test.ts index 5bfffbe4b2..b1902a4535 100644 --- a/tests/Query/Filter/FunctionField.test.ts +++ b/tests/Query/Filter/FunctionField.test.ts @@ -44,7 +44,8 @@ describe('FunctionField - filtering', () => { // Assert expect(filter).toBeValid(); const t = () => { - filter.filterFunction!(new TaskBuilder().build(), new SearchInfo()); + const task = new TaskBuilder().build(); + filter.filterFunction!(task, new SearchInfo([task])); }; expect(t).toThrow(Error); expect(t).toThrowError('filtering function must return true or false. This returned "undefined".'); diff --git a/tests/Query/SearchInfo.test.ts b/tests/Query/SearchInfo.test.ts new file mode 100644 index 0000000000..b199dd1b39 --- /dev/null +++ b/tests/Query/SearchInfo.test.ts @@ -0,0 +1,25 @@ +import { SearchInfo } from '../../src/Query/SearchInfo'; +import { TaskBuilder } from '../TestingTools/TaskBuilder'; + +describe('SearchInfo', () => { + it('should not be able to modify SearchInfo.allTasks directly', () => { + const tasks = [new TaskBuilder().build()]; + const searchInfo = new SearchInfo(tasks); + expect(searchInfo.allTasks.length).toEqual(1); + + // Success: Does not compile + // searchInfo.allTasks.push(new TaskBuilder().description('I should not be allowed').build()); + + // Success: Does not compile + // searchInfo.allTasks[0] = new TaskBuilder().description('cannot replace a task').build(); + }); + + it('should not be able to modify SearchInfo.allTasks indirectly', () => { + const tasks = [new TaskBuilder().build()]; + const searchInfo = new SearchInfo(tasks); + + // Check that updating the original list of tasks does not change the tasks saved in searchInfo.allTasks + tasks.push(new TaskBuilder().description('I should not be added to searchInfo.allTasks').build()); + expect(searchInfo.allTasks.length).toEqual(1); + }); +}); diff --git a/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts b/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts index c41a21544d..c1419666a4 100644 --- a/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts +++ b/tests/Scripting/ScriptingReference/VerifyFunctionFieldSamples.ts @@ -91,8 +91,9 @@ export function verifyFunctionFieldFilterSamplesOnTasks(filters: QueryInstructio const filterFunction = filterOrErrorMessage.filterFunction!; const matchingTasks: string[] = []; + const searchInfo = new SearchInfo(tasks); for (const task of tasks) { - const matches = filterFunction(task, new SearchInfo()); + const matches = filterFunction(task, searchInfo); if (matches) { matchingTasks.push(task.toFileLineString()); } diff --git a/tests/TestingTools/FilterTestHelpers.ts b/tests/TestingTools/FilterTestHelpers.ts index 88b53b28cd..8cbae68924 100644 --- a/tests/TestingTools/FilterTestHelpers.ts +++ b/tests/TestingTools/FilterTestHelpers.ts @@ -28,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, new SearchInfo())).toEqual(expected); + expect(filter.filterFunction!(task, new SearchInfo([task]))).toEqual(expected); } /** @@ -50,8 +50,9 @@ export function testTaskFilterViaQuery(filter: string, task: Task, expected: boo // Act let filteredTasks = [...tasks]; + const searchInfo = new SearchInfo(tasks); query.filters.forEach((filter) => { - filteredTasks = filteredTasks.filter(filter.filterFunction); + filteredTasks = filteredTasks.filter((task) => filter.filterFunction(task, searchInfo)); }); const matched = filteredTasks.length === 1; @@ -84,8 +85,9 @@ export function shouldSupportFiltering( // Act let filteredTasks = [...tasks]; + const searchInfo = new SearchInfo(tasks); query.filters.forEach((filter) => { - filteredTasks = filteredTasks.filter(filter.filterFunction); + filteredTasks = filteredTasks.filter((task) => filter.filterFunction(task, searchInfo)); }); // Assert