Repository: obsidian-tasks-group/obsidian-tasks
Pull request #3228 (2024-12-07 23:04:24)

refactor: Add ListItem methods findClosestParentTask() and isTask()
# 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)
- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Description

This fixes a problem that @ilandikov and I had, where we wanted to move `findClosestParentTask()` from `QueryResultsRenderer.ts` to `ListItem.ts`, but when we did so, the Tasks plugin crashed on start-up.

It is more convenient to be able to to write `listItem.findClosestParentTask()` and that now works.

I thought that the `isTask` accessor would be needed to make this work, but in fact it wasn't needed. However, it may still be useful eventually in custom filters and groups.

## Motivation and Context

Move code to a more logical place.

## How has this been tested?

- Running the tests
- Running the plugin and confirming that Tasks renderers `Parent-Child relationships - Searches - Tasks.md` correctly


## 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 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/src/Renderer/QueryResultsRenderer.ts b/src/Renderer/QueryResultsRenderer.ts
index e140540c..7c55aa04 100644
--- a/src/Renderer/QueryResultsRenderer.ts
+++ b/src/Renderer/QueryResultsRenderer.ts
@@ -28,23 +28,6 @@ export interface QueryRendererParameters {
     editTaskPencilClickHandler: EditButtonClickHandler;
 }
 
-/**
- * We want this function to be a method of ListItem but that causes a circular dependency
- * which makes the plugin fail to load in Obsidian.
- *
- * Note: the tests are in ListItem.test.ts
- *
- * @param listItem
- */
-export function findClosestParentTask(listItem: ListItem) {
-    // Try to find the closest parent that is a task
-    let closestParentTask = listItem.parent;
-    while (closestParentTask !== null && !(closestParentTask instanceof Task)) {
-        closestParentTask = closestParentTask.parent;
-    }
-    return closestParentTask;
-}
-
 export class QueryResultsRenderer {
     /**
      * The complete text in the instruction block, such as:
@@ -274,7 +257,7 @@ export class QueryResultsRenderer {
     }
 
     private willBeRenderedLater(listItem: ListItem, renderedListItems: Set<ListItem>, listItems: ListItem[]) {
-        const closestParentTask = findClosestParentTask(listItem);
+        const closestParentTask = listItem.findClosestParentTask();
         if (!closestParentTask) {
             return false;
         }
@@ -349,7 +332,7 @@ export class QueryResultsRenderer {
         await this.textRenderer(
             listItem.description,
             span,
-            findClosestParentTask(listItem)?.path ?? '',
+            listItem.findClosestParentTask()?.path ?? '',
             this.obsidianComponent,
         );
 
diff --git a/src/Task/ListItem.ts b/src/Task/ListItem.ts
index d6fa7c90..3a293c6b 100644
--- a/src/Task/ListItem.ts
+++ b/src/Task/ListItem.ts
@@ -1,4 +1,5 @@
 import { TaskRegularExpressions } from './TaskRegularExpressions';
+import type { Task } from './Task';
 
 export class ListItem {
     // The original line read from file.
@@ -46,6 +47,28 @@ export class ListItem {
         return this.parent === null;
     }
 
+    /**
+     * Find to find the closest parent that is a {@link Task}
+     */
+    public findClosestParentTask(): Task | null {
+        let closestParentTask = this.parent;
+
+        while (closestParentTask !== null) {
+            // Lazy load the Task class to avoid circular dependencies
+            const { Task } = require('./Task');
+            if (closestParentTask instanceof Task) {
+                return closestParentTask as Task;
+            }
+            closestParentTask = closestParentTask.parent;
+        }
+
+        return null;
+    }
+
+    get isTask() {
+        return false;
+    }
+
     /**
      * Compare all the fields in another ListItem, to detect any differences from this one.
      *
diff --git a/src/Task/Task.ts b/src/Task/Task.ts
index 3d516c90..85524f3e 100644
--- a/src/Task/Task.ts
+++ b/src/Task/Task.ts
@@ -492,6 +492,15 @@ export class Task extends ListItem {
         return recurrenceOnNextLine ? potentiallyPrunedTasks.reverse() : potentiallyPrunedTasks;
     }
 
+    /**
+     * Return whether this object is a {@link Task}.
+     *
+     * This is useful at run-time to discover whether a {@link ListItem} reference is in fact a {@link Task}.
+     */
+    get isTask() {
+        return true;
+    }
+
     /**
      * Return whether the task is considered done.
      * @returns true if the status type is {@link StatusType.DONE}, {@link StatusType.CANCELLED} or {@link StatusType.NON_TASK}, and false otherwise.


Test patch:
diff --git a/tests/Task/ListItem.test.ts b/tests/Task/ListItem.test.ts
index cab4b924..2c4e2458 100644
--- a/tests/Task/ListItem.test.ts
+++ b/tests/Task/ListItem.test.ts
@@ -8,7 +8,6 @@ import { TaskLocation } from '../../src/Task/TaskLocation';
 import { ListItem } from '../../src/Task/ListItem';
 import { TaskBuilder } from '../TestingTools/TaskBuilder';
 import { fromLine } from '../TestingTools/TestHelpers';
-import { findClosestParentTask } from '../../src/Renderer/QueryResultsRenderer';
 import { createChildListItem } from './ListItemHelpers';
 
 window.moment = moment;
@@ -80,6 +79,11 @@ describe('list item tests', () => {
         expect(child.isRoot).toEqual(false);
     });
 
+    it('should not be a task', () => {
+        const listItem = new ListItem('- list item', null);
+        expect(listItem.isTask).toBe(false);
+    });
+
     it.each([
         ['- ', true],
         ['* ', true],
@@ -107,9 +111,9 @@ describe('related items', () => {
         const item = new ListItem('- item', null);
         const childOfItem = new ListItem('- child of item', item);
 
-        expect(findClosestParentTask(task)).toEqual(null);
-        expect(findClosestParentTask(item)).toEqual(null);
-        expect(findClosestParentTask(childOfItem)).toEqual(null);
+        expect(task.findClosestParentTask()).toEqual(null);
+        expect(item.findClosestParentTask()).toEqual(null);
+        expect(childOfItem.findClosestParentTask()).toEqual(null);
     });
 
     it('should find the closest parent task', () => {
@@ -117,9 +121,9 @@ describe('related items', () => {
         const child = new ListItem('- item', parentTask);
         const grandChild = new ListItem('- item', child);
 
-        expect(findClosestParentTask(parentTask)).toEqual(null);
-        expect(findClosestParentTask(child)).toEqual(parentTask);
-        expect(findClosestParentTask(grandChild)).toEqual(parentTask);
+        expect(parentTask.findClosestParentTask()).toEqual(null);
+        expect(child.findClosestParentTask()).toEqual(parentTask);
+        expect(grandChild.findClosestParentTask()).toEqual(parentTask);
     });
 });
 
diff --git a/tests/Task/Task.test.ts b/tests/Task/Task.test.ts
index 99c1ce78..54c47133 100644
--- a/tests/Task/Task.test.ts
+++ b/tests/Task/Task.test.ts
@@ -67,6 +67,13 @@ describe('immutability', () => {
     });
 });
 
+describe('list item-related tests', () => {
+    it('should be a task', () => {
+        const task = fromLine({ line: '- [ ] A Task' });
+        expect(task.isTask).toBe(true);
+    });
+});
+
 describe('parsing', () => {
     it('parses a task from a line starting with hyphen', () => {
         // Arrange