Repository: obsidian-tasks-group/obsidian-tasks
Pull request #1698 (2023-02-26 22:34:08)

refactor: Add TaskLocation to encapsulate position data
<!--- Provide a general summary of your changes in the Title above -->

# Description

This moves 4 pieces of data from Task in to a new class `TaskLocation`:

```ts
    public readonly path: string;
    public readonly sectionStart: number;
    public readonly sectionIndex: number;
    public readonly precedingHeader: string | null;
```

## Motivation and Context

I wish to refactor `File.ts` function `replaceTaskWithTasks()` to make it testable, and reliable in more situations.

This may, for example, replace the complex, fragile and unreliable storage of `sectionStart` and `sectionIndex` with a simpler storage of the task's line number.

By first adding storage for this data, and later adding some algorithms to it, the later refactorings should be easier, I hope.

## How has this been tested?

- Running existing tests
- Adding new tests

## 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:

- [x] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)

## 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://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#updating-documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md#maintaining-the-tests).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md) 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://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CONTRIBUTING.md)
- [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/Cache.ts b/src/Cache.ts
index 60efa401..5cf5f2a1 100644
--- a/src/Cache.ts
+++ b/src/Cache.ts
@@ -8,6 +8,7 @@ import type { TasksEvents } from './TasksEvents';
 import { DateFallback } from './DateFallback';
 import { getSettings } from './Config/Settings';
 import { Lazy } from './lib/Lazy';
+import { TaskLocation } from './TaskLocation';
 
 export enum State {
     Cold = 'Cold',
@@ -147,7 +148,10 @@ export class Cache {
                 this.tasks = this.tasks.map((task: Task): Task => {
                     if (task.path === oldPath) {
                         if (!useFilenameAsScheduledDate) {
-                            return new Task({ ...task, path: file.path });
+                            return new Task({
+                                ...task,
+                                taskLocation: task.taskLocation.fromRenamedFile(file.path),
+                            });
                         } else {
                             return DateFallback.updateTaskPath(task, file.path, fallbackDate.value);
                         }
@@ -294,10 +298,12 @@ export class Cache {
                 try {
                     task = Task.fromLine({
                         line,
-                        path: file.path,
-                        sectionStart: currentSection.position.start.line,
-                        sectionIndex,
-                        precedingHeader: Cache.getPrecedingHeader(lineNumber, fileCache.headings),
+                        taskLocation: new TaskLocation(
+                            file.path,
+                            currentSection.position.start.line,
+                            sectionIndex,
+                            Cache.getPrecedingHeader(lineNumber, fileCache.headings),
+                        ),
                         fallbackDate: dateFromFileName.value,
                     });
                 } catch (e) {
diff --git a/src/Commands/CreateOrEditTaskParser.ts b/src/Commands/CreateOrEditTaskParser.ts
index 70695350..e15bf12f 100644
--- a/src/Commands/CreateOrEditTaskParser.ts
+++ b/src/Commands/CreateOrEditTaskParser.ts
@@ -2,6 +2,7 @@ import { Status } from '../Status';
 import { Priority, Task, TaskRegularExpressions } from '../Task';
 import { DateFallback } from '../DateFallback';
 import { StatusRegistry } from '../StatusRegistry';
+import { TaskLocation } from '../TaskLocation';
 
 /**
  * Read any markdown line and treat it as a task, for the purposes of
@@ -21,10 +22,7 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
 
     const task = Task.fromLine({
         line,
-        path,
-        sectionStart: 0, // We don't need this to toggle it here in the editor.
-        sectionIndex: 0, // We don't need this to toggle it here in the editor.
-        precedingHeader: null, // We don't need this to toggle it here in the editor.
+        taskLocation: TaskLocation.fromUnknownPosition(path), // We don't need precise location to toggle it here in the editor.
         fallbackDate, // set the scheduled date from the filename, so it can be displayed in the dialog
     });
 
@@ -41,7 +39,8 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
         return new Task({
             status: Status.TODO,
             description: '',
-            path,
+            // We don't need the location fields except file to edit here in the editor.
+            taskLocation: TaskLocation.fromUnknownPosition(path),
             indentation: '',
             listMarker: '-',
             priority: Priority.None,
@@ -50,10 +49,6 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
             dueDate: null,
             doneDate: null,
             recurrence: null,
-            // We don't need the following fields to edit here in the editor.
-            sectionStart: 0,
-            sectionIndex: 0,
-            precedingHeader: null,
             blockLink: '',
             tags: [],
             originalMarkdown: '',
@@ -78,7 +73,8 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
     return new Task({
         status,
         description,
-        path,
+        // We don't need the location fields except file to edit here in the editor.
+        taskLocation: TaskLocation.fromUnknownPosition(path),
         indentation,
         listMarker,
         blockLink,
@@ -88,10 +84,6 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
         dueDate: null,
         doneDate: null,
         recurrence: null,
-        // We don't need the following fields to edit here in the editor.
-        sectionStart: 0,
-        sectionIndex: 0,
-        precedingHeader: null,
         tags: [],
         originalMarkdown: '',
         // Not needed since the inferred status is always re-computed after submitting.
diff --git a/src/Commands/ToggleDone.ts b/src/Commands/ToggleDone.ts
index bb76df30..831ce335 100644
--- a/src/Commands/ToggleDone.ts
+++ b/src/Commands/ToggleDone.ts
@@ -2,6 +2,7 @@ import { Editor, MarkdownView, View } from 'obsidian';
 import { StatusRegistry } from '../StatusRegistry';
 
 import { Task, TaskRegularExpressions } from '../Task';
+import { TaskLocation } from '../TaskLocation';
 
 export const toggleDone = (checking: boolean, editor: Editor, view: View) => {
     if (checking) {
@@ -58,10 +59,7 @@ export const toggleLine = (line: string, path: string) => {
     const task = Task.fromLine({
         // Why are we using Task.fromLine instead of the Cache here?
         line,
-        path,
-        sectionStart: 0, // We don't need this to toggle it here in the editor.
-        sectionIndex: 0, // We don't need this to toggle it here in the editor.
-        precedingHeader: null, // We don't need this to toggle it here in the editor.
+        taskLocation: TaskLocation.fromUnknownPosition(path), // We don't need precise location to toggle it here in the editor.
         fallbackDate: null, // We don't need this to toggle it here in the editor.
     });
     if (task !== null) {
diff --git a/src/DateFallback.ts b/src/DateFallback.ts
index dc13a238..add323b2 100644
--- a/src/DateFallback.ts
+++ b/src/DateFallback.ts
@@ -114,7 +114,7 @@ export class DateFallback {
 
         return new Task({
             ...task,
-            path: newPath,
+            taskLocation: task.taskLocation.fromRenamedFile(newPath),
             scheduledDate,
             scheduledDateIsInferred,
         });
diff --git a/src/InlineRenderer.ts b/src/InlineRenderer.ts
index 4264310b..6dd0d8b9 100644
--- a/src/InlineRenderer.ts
+++ b/src/InlineRenderer.ts
@@ -2,6 +2,7 @@ import { getSettings } from 'Config/Settings';
 
 import type { MarkdownPostProcessorContext, Plugin } from 'obsidian';
 import { Task } from './Task';
+import { TaskLocation } from './TaskLocation';
 
 export class InlineRenderer {
     constructor({ plugin }: { plugin: Plugin }) {
@@ -66,10 +67,12 @@ export class InlineRenderer {
 
             const task = Task.fromLine({
                 line,
-                path,
-                sectionStart: section.lineStart,
-                sectionIndex,
-                precedingHeader: null, // We don't need the preceding header for in-line rendering.
+                taskLocation: new TaskLocation(
+                    path,
+                    section.lineStart,
+                    sectionIndex,
+                    null, // We don't need the preceding header for in-line rendering.
+                ),
                 fallbackDate: null, // We don't need the fallback date for in-line rendering
             });
             if (task !== null) {
diff --git a/src/LivePreviewExtension.ts b/src/LivePreviewExtension.ts
index 8795ba22..2363fe1a 100644
--- a/src/LivePreviewExtension.ts
+++ b/src/LivePreviewExtension.ts
@@ -3,6 +3,7 @@ import type { PluginValue } from '@codemirror/view';
 import { Notice } from 'obsidian';
 
 import { Task } from './Task';
+import { TaskLocation } from './TaskLocation';
 
 export const newLivePreviewExtension = () => {
     return ViewPlugin.fromClass(LivePreviewExtension);
@@ -59,10 +60,7 @@ class LivePreviewExtension implements PluginValue {
             // None of this data is relevant here.
             // The task is created, toggled, and written back to the CM6 document,
             // replacing the old task in-place.
-            path: '',
-            sectionStart: 0,
-            sectionIndex: 0,
-            precedingHeader: null,
+            taskLocation: TaskLocation.fromUnknownPosition(''),
             fallbackDate: null,
         });
 
diff --git a/src/Task.ts b/src/Task.ts
index 0aa65d3b..7c31cdb6 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -1,6 +1,7 @@
 import type { Moment } from 'moment';
 import { LayoutOptions, TaskLayout } from './TaskLayout';
 import type { TaskLayoutComponent } from './TaskLayout';
+import type { TaskLocation } from './TaskLocation';
 import { Recurrence } from './Recurrence';
 import { getSettings } from './Config/Settings';
 import { StatusRegistry } from './StatusRegistry';
@@ -118,14 +119,10 @@ export class TaskRegularExpressions {
 export class Task {
     public readonly status: Status;
     public readonly description: string;
-    public readonly path: string;
     public readonly indentation: string;
     public readonly listMarker: string;
-    /** Line number where the section starts that contains this task. */
-    public readonly sectionStart: number;
-    /** The index of the nth task in its section. */
-    public readonly sectionIndex: number;
-    public readonly precedingHeader: string | null;
+
+    public readonly taskLocation: TaskLocation;
 
     public readonly tags: string[];
 
@@ -153,12 +150,9 @@ export class Task {
     constructor({
         status,
         description,
-        path,
+        taskLocation,
         indentation,
         listMarker,
-        sectionStart,
-        sectionIndex,
-        precedingHeader,
         priority,
         startDate,
         scheduledDate,
@@ -172,12 +166,9 @@ export class Task {
     }: {
         status: Status;
         description: string;
-        path: string;
+        taskLocation: TaskLocation;
         indentation: string;
         listMarker: string;
-        sectionStart: number;
-        sectionIndex: number;
-        precedingHeader: string | null;
         priority: Priority;
         startDate: moment.Moment | null;
         scheduledDate: moment.Moment | null;
@@ -191,12 +182,9 @@ export class Task {
     }) {
         this.status = status;
         this.description = description;
-        this.path = path;
         this.indentation = indentation;
         this.listMarker = listMarker;
-        this.sectionStart = sectionStart;
-        this.sectionIndex = sectionIndex;
-        this.precedingHeader = precedingHeader;
+        this.taskLocation = taskLocation;
 
         this.tags = tags;
 
@@ -219,27 +207,18 @@ export class Task {
      *
      * @static
      * @param {string} line - The full line in the note to parse.
-     * @param {string} path - Path to the note in obsidian.
-     * @param {number} sectionStart - Line number where the section starts that contains this task.
-     * @param {number} sectionIndex - The index of the nth task in its section.
-     * @param {(string | null)} precedingHeader - The header before this task.
+     * @param {TaskLocation} taskLocation - The location of the task line
      * @param {(Moment | null)} fallbackDate - The date to use as the scheduled date if no other date is set
      * @return {*}  {(Task | null)}
      * @memberof Task
      */
     public static fromLine({
         line,
-        path,
-        sectionStart,
-        sectionIndex,
-        precedingHeader,
+        taskLocation,
         fallbackDate,
     }: {
         line: string;
-        path: string;
-        sectionStart: number;
-        sectionIndex: number;
-        precedingHeader: string | null;
+        taskLocation: TaskLocation;
         fallbackDate: Moment | null;
     }): Task | null {
         // Check the line to see if it is a markdown task.
@@ -402,12 +381,9 @@ export class Task {
         return new Task({
             status,
             description,
-            path,
             indentation,
             listMarker,
-            sectionStart,
-            sectionIndex,
-            precedingHeader,
+            taskLocation: taskLocation,
             priority,
             startDate,
             scheduledDate,
@@ -572,6 +548,10 @@ export class Task {
         return this._urgency;
     }
 
+    public get path(): string {
+        return this.taskLocation.path;
+    }
+
     /**
      * Return the name of the file containing the task, with the .md extension removed.
      */
@@ -584,6 +564,18 @@ export class Task {
         }
     }
 
+    get sectionStart(): number {
+        return this.taskLocation.sectionStart;
+    }
+
+    get sectionIndex(): number {
+        return this.taskLocation.sectionIndex;
+    }
+
+    public get precedingHeader(): string | null {
+        return this.taskLocation.precedingHeader;
+    }
+
     /**
      * Returns the text that should be displayed to the user when linking to the origin of the task
      *
diff --git a/src/TaskLocation.ts b/src/TaskLocation.ts
new file mode 100644
index 00000000..9a3d3302
--- /dev/null
+++ b/src/TaskLocation.ts
@@ -0,0 +1,51 @@
+/**
+ * TaskLocation is the place where all information about a task line's location
+ * in a markdown file is stored, so that testable algorithms can then be added here.
+ */
+export class TaskLocation {
+    private readonly _path: string;
+    private readonly _sectionStart: number;
+    private readonly _sectionIndex: number;
+    private readonly _precedingHeader: string | null;
+
+    public constructor(path: string, sectionStart: number, sectionIndex: number, precedingHeader: string | null) {
+        this._path = path;
+        this._sectionStart = sectionStart;
+        this._sectionIndex = sectionIndex;
+        this._precedingHeader = precedingHeader;
+    }
+
+    /**
+     * Constructor, for use when the Task's exact location in a file is either unknown, or not needed.
+     * @param path
+     */
+    public static fromUnknownPosition(path: string): TaskLocation {
+        return new TaskLocation(path, 0, 0, null);
+    }
+
+    /**
+     * Constructor, for when the file has been renamed, and all other data remains the same.
+     * @param newPath
+     */
+    fromRenamedFile(newPath: string) {
+        return new TaskLocation(newPath, this.sectionStart, this.sectionIndex, this.precedingHeader);
+    }
+
+    public get path(): string {
+        return this._path;
+    }
+
+    /** Line number where the section starts that contains this task. */
+    get sectionStart(): number {
+        return this._sectionStart;
+    }
+
+    /** The index of the nth task in its section. */
+    get sectionIndex(): number {
+        return this._sectionIndex;
+    }
+
+    public get precedingHeader(): string | null {
+        return this._precedingHeader;
+    }
+}


Test patch:
diff --git a/tests/DateFallback.test.ts b/tests/DateFallback.test.ts
index a980e7a9..6bd148bd 100644
--- a/tests/DateFallback.test.ts
+++ b/tests/DateFallback.test.ts
@@ -6,6 +6,7 @@ import moment from 'moment';
 import { Task } from '../src/Task';
 import { resetSettings, updateSettings } from '../src/Config/Settings';
 import { DateFallback } from '../src/DateFallback';
+import { TaskLocation } from '../src/TaskLocation';
 import { TaskBuilder } from './TestingTools/TaskBuilder';
 
 jest.mock('obsidian');
@@ -185,10 +186,7 @@ describe('extract date from filename', () => {
 function constructTaskFromLine(line: string, fallbackDate: string | null) {
     return Task.fromLine({
         line,
-        path: 'file.md', // filename must be parsed before calling Task.fromLine, so irrelevant for these tests
-        sectionStart: 0,
-        sectionIndex: 0,
-        precedingHeader: '',
+        taskLocation: TaskLocation.fromUnknownPosition('file.md'), // filename must be parsed before calling Task.fromLine, so irrelevant for these tests
         fallbackDate: date(fallbackDate),
     });
 }
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index aedd2001..c4c9dc49 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -6,6 +6,7 @@ import { Query } from '../src/Query/Query';
 import { Status } from '../src/Status';
 import { Priority, Task } from '../src/Task';
 import { resetSettings, updateSettings } from '../src/Config/Settings';
+import { TaskLocation } from '../src/TaskLocation';
 import { createTasksFromMarkdown, fromLine } from './TestHelpers';
 import { shouldSupportFiltering } from './TestingTools/FilterTestHelpers';
 import type { FilteringCase } from './TestingTools/FilterTestHelpers';
@@ -279,12 +280,9 @@ describe('Query', () => {
                 new Task({
                     status: Status.TODO,
                     description: 'description',
-                    path: 'Ab/C D',
+                    taskLocation: TaskLocation.fromUnknownPosition('Ab/C D'),
                     indentation: '',
                     listMarker: '-',
-                    sectionStart: 0,
-                    sectionIndex: 0,
-                    precedingHeader: null,
                     priority: Priority.None,
                     startDate: null,
                     scheduledDate: null,
@@ -299,12 +297,9 @@ describe('Query', () => {
                 new Task({
                     status: Status.TODO,
                     description: 'description',
-                    path: 'FF/C D',
+                    taskLocation: TaskLocation.fromUnknownPosition('FF/C D'),
                     indentation: '',
                     listMarker: '-',
-                    sectionStart: 0,
-                    sectionIndex: 0,
-                    precedingHeader: null,
                     priority: Priority.None,
                     startDate: null,
                     scheduledDate: null,
diff --git a/tests/StatusRegistry.test.ts b/tests/StatusRegistry.test.ts
index 9361efca..20dd82dc 100644
--- a/tests/StatusRegistry.test.ts
+++ b/tests/StatusRegistry.test.ts
@@ -6,6 +6,7 @@ import { StatusRegistry } from '../src/StatusRegistry';
 import { Status } from '../src/Status';
 import { StatusConfiguration, StatusType } from '../src/StatusConfiguration';
 import { Task } from '../src/Task';
+import { TaskLocation } from '../src/TaskLocation';
 import * as TestHelpers from './TestHelpers';
 
 jest.mock('obsidian');
@@ -157,10 +158,7 @@ describe('StatusRegistry', () => {
             const precedingHeader = 'Eloquent Section';
             const task = Task.fromLine({
                 line,
-                path,
-                sectionStart,
-                sectionIndex,
-                precedingHeader,
+                taskLocation: new TaskLocation(path, sectionStart, sectionIndex, precedingHeader),
                 fallbackDate: null,
             });
 
@@ -188,10 +186,7 @@ describe('StatusRegistry', () => {
             const precedingHeader = 'Eloquent Section';
             const task = Task.fromLine({
                 line,
-                path,
-                sectionStart,
-                sectionIndex,
-                precedingHeader,
+                taskLocation: new TaskLocation(path, sectionStart, sectionIndex, precedingHeader),
                 fallbackDate: null,
             });
 
@@ -225,10 +220,7 @@ describe('StatusRegistry', () => {
             const precedingHeader = 'Eloquent Section';
             const task = Task.fromLine({
                 line,
-                path,
-                sectionStart,
-                sectionIndex,
-                precedingHeader,
+                taskLocation: new TaskLocation(path, sectionStart, sectionIndex, precedingHeader),
                 fallbackDate: null,
             });
 
diff --git a/tests/Task.test.ts b/tests/Task.test.ts
index 008082bc..c9432c66 100644
--- a/tests/Task.test.ts
+++ b/tests/Task.test.ts
@@ -8,6 +8,7 @@ import { Priority, Task } from '../src/Task';
 import { resetSettings, updateSettings } from '../src/Config/Settings';
 import type { StatusCollection } from '../src/StatusCollection';
 import { StatusRegistry } from '../src/StatusRegistry';
+import { TaskLocation } from '../src/TaskLocation';
 import { fromLine } from './TestHelpers';
 import { TaskBuilder } from './TestingTools/TaskBuilder';
 import { RecurrenceBuilder } from './TestingTools/RecurrenceBuilder';
@@ -279,10 +280,7 @@ type TagParsingExpectations = {
 function constructTaskFromLine(line: string, path: string = 'file.md', fallbackDate: Moment | null = null) {
     return Task.fromLine({
         line,
-        path: path,
-        sectionStart: 0,
-        sectionIndex: 0,
-        precedingHeader: '',
+        taskLocation: TaskLocation.fromUnknownPosition(path),
         fallbackDate,
     });
 }
diff --git a/tests/TaskLocation.test.ts b/tests/TaskLocation.test.ts
new file mode 100644
index 00000000..4401acbe
--- /dev/null
+++ b/tests/TaskLocation.test.ts
@@ -0,0 +1,53 @@
+import { TaskLocation } from '../src/TaskLocation';
+
+describe('TaskLocation', () => {
+    it('should store the full task location', () => {
+        // Arrange
+        const path = 'a/b/c.md';
+        const sectionStart = 3;
+        const sectionIndex = 7;
+        const precedingHeader = 'My Header';
+
+        // Act
+        const taskLocation = new TaskLocation(path, sectionStart, sectionIndex, precedingHeader);
+
+        // Assert
+        expect(taskLocation.path).toStrictEqual(path);
+        expect(taskLocation.sectionStart).toStrictEqual(sectionStart);
+        expect(taskLocation.sectionIndex).toStrictEqual(sectionIndex);
+        expect(taskLocation.precedingHeader).toStrictEqual(precedingHeader);
+    });
+
+    it('should construct from only the file path', () => {
+        // Arrange
+        const path = 'a/b/c.md';
+
+        // Act
+        const taskLocation = TaskLocation.fromUnknownPosition(path);
+
+        // Assert
+        expect(taskLocation.path).toStrictEqual(path);
+        expect(taskLocation.sectionStart).toStrictEqual(0);
+        expect(taskLocation.sectionIndex).toStrictEqual(0);
+        expect(taskLocation.precedingHeader).toBeNull();
+    });
+
+    it('should provide convenient renaming of path', () => {
+        // Arrange
+        const path = 'a/b/c.md';
+        const sectionStart = 13;
+        const sectionIndex = 10;
+        const precedingHeader = 'My Previous Header';
+        const taskLocation = new TaskLocation(path, sectionStart, sectionIndex, precedingHeader);
+
+        // Act
+        const newPath = 'd/e/f.md';
+        const newLocation = taskLocation.fromRenamedFile(newPath);
+
+        // Assert
+        expect(newLocation.path).toStrictEqual(newPath);
+        expect(newLocation.sectionStart).toStrictEqual(sectionStart);
+        expect(newLocation.sectionIndex).toStrictEqual(sectionIndex);
+        expect(newLocation.precedingHeader).toStrictEqual(precedingHeader);
+    });
+});
diff --git a/tests/TestHelpers.ts b/tests/TestHelpers.ts
index eaa84eba..5bc13537 100644
--- a/tests/TestHelpers.ts
+++ b/tests/TestHelpers.ts
@@ -1,4 +1,5 @@
 import { Task } from '../src/Task';
+import { TaskLocation } from '../src/TaskLocation';
 
 export function fromLine({
     line,
@@ -11,10 +12,7 @@ export function fromLine({
 }) {
     return Task.fromLine({
         line,
-        path,
-        precedingHeader,
-        sectionIndex: 0,
-        sectionStart: 0,
+        taskLocation: new TaskLocation(path, 0, 0, precedingHeader),
         fallbackDate: null,
     })!;
 }
@@ -25,10 +23,7 @@ export function createTasksFromMarkdown(tasksAsMarkdown: string, path: string, p
     for (const line of taskLines) {
         const task = Task.fromLine({
             line: line,
-            path: path,
-            precedingHeader: precedingHeader,
-            sectionIndex: 0,
-            sectionStart: 0,
+            taskLocation: new TaskLocation(path, 0, 0, precedingHeader),
             fallbackDate: null,
         });
         if (task) {
diff --git a/tests/TestingTools/FilterTestHelpers.ts b/tests/TestingTools/FilterTestHelpers.ts
index 5cf339d4..1babf70a 100644
--- a/tests/TestingTools/FilterTestHelpers.ts
+++ b/tests/TestingTools/FilterTestHelpers.ts
@@ -1,6 +1,7 @@
 import type { FilterOrErrorMessage } from '../../src/Query/Filter/Filter';
 import { Task } from '../../src/Task';
 import { Query } from '../../src/Query/Query';
+import { TaskLocation } from '../../src/TaskLocation';
 import type { TaskBuilder } from './TaskBuilder';
 
 /**
@@ -75,10 +76,7 @@ export function shouldSupportFiltering(
         (taskLine) =>
             Task.fromLine({
                 line: taskLine,
-                sectionStart: 0,
-                sectionIndex: 0,
-                path: '',
-                precedingHeader: '',
+                taskLocation: TaskLocation.fromUnknownPosition(''),
                 fallbackDate: null, // For tests scheduled date needs to be set explicitly
             }) as Task,
     );
diff --git a/tests/TestingTools/TaskBuilder.ts b/tests/TestingTools/TaskBuilder.ts
index 461eeaf8..cabe9306 100644
--- a/tests/TestingTools/TaskBuilder.ts
+++ b/tests/TestingTools/TaskBuilder.ts
@@ -5,6 +5,7 @@ import { Priority, Task } from '../../src/Task';
 import type { Recurrence } from '../../src/Recurrence';
 import { DateParser } from '../../src/Query/DateParser';
 import { StatusConfiguration, StatusType } from '../../src/StatusConfiguration';
+import { TaskLocation } from '../../src/TaskLocation';
 
 /**
  * A fluent class for creating tasks for tests.
@@ -62,12 +63,9 @@ export class TaskBuilder {
         return new Task({
             status: this._status,
             description: description,
-            path: this._path,
+            taskLocation: new TaskLocation(this._path, this._sectionStart, this._sectionIndex, this._precedingHeader),
             indentation: this._indentation,
             listMarker: this._listMarker,
-            sectionStart: this._sectionStart,
-            sectionIndex: this._sectionIndex,
-            precedingHeader: this._precedingHeader,
             priority: this._priority,
             startDate: this._startDate,
             scheduledDate: this._scheduledDate,