Skip to content

Commit

Permalink
Merge pull request #2374 from obsidian-tasks-group/custom-filters-acc…
Browse files Browse the repository at this point in the history
…ess-all-tasks

recfactor: SearchInfo now contains list of tasks being searched
  • Loading branch information
claremacrae authored Oct 27, 2023
2 parents a326092 + 85eac9b commit 9f4227b
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 18 deletions.
4 changes: 3 additions & 1 deletion src/Query/Query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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();
Expand Down
21 changes: 15 additions & 6 deletions src/Query/SearchInfo.ts
Original file line number Diff line number Diff line change
@@ -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<Task[]>;

public constructor(allTasks: Task[]) {
this.allTasks = [...allTasks];
}
}
5 changes: 3 additions & 2 deletions tests/CustomMatchers/CustomMatchersForFilters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand All @@ -17,6 +18,6 @@ describe('CustomMatchersForFilters', () => {
);

// Act, Assert
expect(filter).toMatchTaskWithSearchInfo(new TaskBuilder().build(), initialSearchInfo);
expect(filter).toMatchTaskWithSearchInfo(task, initialSearchInfo);
});
});
2 changes: 1 addition & 1 deletion tests/CustomMatchers/CustomMatchersForFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion tests/DocumentationSamples/DocsSamplesForStatuses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
31 changes: 29 additions & 2 deletions tests/Query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
});
});

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
3 changes: 2 additions & 1 deletion tests/Query/Filter/FunctionField.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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".');
Expand Down
25 changes: 25 additions & 0 deletions tests/Query/SearchInfo.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
8 changes: 5 additions & 3 deletions tests/TestingTools/FilterTestHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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;

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 9f4227b

Please sign in to comment.