Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2710 (2024-03-16 06:01:19)

refactor: Introduce Statement class
<!--- Provide a general summary of your changes in the Title above -->

# Description

This refactoring ended up introducing a new class, `Statement`.

Filter now stores the instruction line in a `Statement` instead of `string`.

_Note: the branch name `better-exlain`_ is misleading: I thought I could fix the bug in one step but it was too much work in the end.

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

Preparation to fix:
- https://github.com/obsidian-tasks-group/obsidian-tasks/issues/2349

## How has this been tested?

<!--- Please describe in detail how you tested your changes. -->
<!--- Include details of your testing environment, tests ran to see how -->
<!--- your change affects other areas of the code, etc. -->

Exploratory testing and automated 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)
- [x] **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`.
- [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/Filter.ts b/src/Query/Filter/Filter.ts
index 6655b717..870a8289 100644
--- a/src/Query/Filter/Filter.ts
+++ b/src/Query/Filter/Filter.ts
@@ -1,6 +1,7 @@
 import type { Task } from '../../Task/Task';
 import type { Explanation } from '../Explain/Explanation';
 import type { SearchInfo } from '../SearchInfo';
+import { Statement } from '../Statement';
 
 /**
  * A filtering function, that takes a Task object and returns
@@ -14,24 +15,45 @@ export type FilterFunction = (task: Task, searchInfo: SearchInfo) => boolean;
 /**
  * A class that represents a parsed filtering instruction from a tasks code block.
  *
- * Currently it provides access to:
+ * It provides access to:
  *
- * - The original {@link instruction}
+ * - The original {@link instruction}, after processing of continuation lines and placeholders.
+ * - An {@link explanation}, showing how the instruction was interpreted.
+ * - A {@link statement}, which is a {@link Statement} object that gives access to the original text,
+ *   for filters that were created by a {@link Query}.
  * - The {@link filterFunction} - a {@link FilterFunction} which tests whether a task matches the filter
- *
- * Later, the plan is to add a human-readable explanation of the filter.
  */
 export class Filter {
-    readonly instruction: string;
+    /** _statement may be updated later with {@link setStatement} */
+    private _statement: Statement;
+
     readonly explanation: Explanation;
     public filterFunction: FilterFunction;
 
     public constructor(instruction: string, filterFunction: FilterFunction, explanation: Explanation) {
-        this.instruction = instruction;
+        this._statement = new Statement(instruction, instruction);
         this.explanation = explanation;
         this.filterFunction = filterFunction;
     }
 
+    public get statement(): Statement {
+        return this._statement;
+    }
+
+    /**
+     * Optionally record more detail about the source statement.
+     *
+     * In tests, we only care about the actual instruction being parsed and executed.
+     * However, in {@link Query}, we want the ability to show user more information.
+     */
+    public setStatement(statement: Statement) {
+        this._statement = statement;
+    }
+
+    public get instruction(): string {
+        return this._statement.anyPlaceholdersExpanded;
+    }
+
     public explainFilterIndented(indent: string) {
         const explanation = this.explanation;
         const unindentedExplanation = explanation.asString();
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index be77e48a..85e95d2d 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -13,10 +13,11 @@ import * as FilterParser from './FilterParser';
 import type { Grouper } from './Group/Grouper';
 import { TaskGroups } from './Group/TaskGroups';
 import { QueryResult } from './QueryResult';
-import { scan } from './Scanner';
+import { continueLines } from './Scanner';
 import { SearchInfo } from './SearchInfo';
 import { Sort } from './Sort/Sort';
 import type { Sorter } from './Sort/Sorter';
+import type { Statement } from './Statement';
 
 export class Query implements IQuery {
     /** Note: source is the raw source, before expanding any placeholders */
@@ -56,15 +57,15 @@ export class Query implements IQuery {
 
         this.debug(`Creating query: ${this.formatQueryForLogging()}`);
 
-        scan(source).forEach((rawLine: string) => {
-            const line = this.expandPlaceholders(rawLine, path);
+        continueLines(source).forEach((statement: Statement) => {
+            const line = this.expandPlaceholders(statement, path);
             if (this.error !== undefined) {
                 // There was an error expanding placeholders.
                 return;
             }
 
             try {
-                this.parseLine(line);
+                this.parseLine(line, statement);
             } catch (e) {
                 let message;
                 if (e instanceof Error) {
@@ -79,7 +80,7 @@ export class Query implements IQuery {
         });
     }
 
-    private parseLine(line: string) {
+    private parseLine(line: string, statement: Statement) {
         switch (true) {
             case this.shortModeRegexp.test(line):
                 this._queryLayoutOptions.shortMode = true;
@@ -106,7 +107,7 @@ export class Query implements IQuery {
             case this.commentRegexp.test(line):
                 // Comment lines are ignored
                 break;
-            case this.parseFilter(line):
+            case this.parseFilter(line, statement):
                 break;
             default:
                 this.setError('do not understand query', line);
@@ -117,7 +118,8 @@ export class Query implements IQuery {
         return `[${this.source.split('\n').join(' ; ')}]`;
     }
 
-    private expandPlaceholders(source: string, path: string | undefined) {
+    private expandPlaceholders(statement: Statement, path: string | undefined) {
+        const source = statement.anyContinuationLinesRemoved;
         if (source.includes('{{') && source.includes('}}')) {
             if (this.filePath === undefined) {
                 this._error = `The query looks like it contains a placeholder, with "{{" and "}}"
@@ -145,6 +147,9 @@ ${source}`;
                 return source;
             }
         }
+
+        // Save any expanded text back in to the statement:
+        statement.recordExpandedPlaceholders(expandedSource);
         return expandedSource;
     }
 
@@ -327,10 +332,14 @@ Problem line: "${line}"`;
         }
     }
 
-    private parseFilter(line: string) {
+    private parseFilter(line: string, statement: Statement) {
         const filterOrError = FilterParser.parseFilter(line);
         if (filterOrError != null) {
             if (filterOrError.filter) {
+                // Overwrite the filter's statement, to preserve details of any
+                // continuation lines and placeholder expansions.
+                filterOrError.filter.setStatement(statement);
+
                 this._filters.push(filterOrError.filter);
             } else {
                 this.setError(filterOrError.error ?? 'Unknown error', line);
diff --git a/src/Query/Scanner.ts b/src/Query/Scanner.ts
index fd46084e..582fe4c3 100644
--- a/src/Query/Scanner.ts
+++ b/src/Query/Scanner.ts
@@ -1,3 +1,5 @@
+import { Statement } from './Statement';
+
 function endsWith1Slash(inputLine: string) {
     return inputLine.endsWith('\\');
 }
@@ -34,27 +36,48 @@ function adjustLine(inputLine: string, continuePreviousLine: boolean) {
     return adjustedLine;
 }
 
-function saveLine(outputLines: string[], continuePreviousLine: boolean, adjustedLine: string) {
-    if (continuePreviousLine) {
-        outputLines[outputLines.length - 1] += ' ' + adjustedLine;
-    } else {
-        outputLines.push(adjustedLine);
-    }
+/**
+ * Removes newlines escaped by a backslash.
+ * A trailing backslash at the end of a line can be escaped by doubling it.
+ *
+ * @param input input string
+ * @returns modified input, as a string
+ *
+ * @see continueLines
+ */
+export function continueLinesFlattened(input: string): string {
+    return continueLines(input)
+        .map((instruction) => instruction.anyContinuationLinesRemoved)
+        .join('\n');
 }
 
 /**
  * Removes newlines escaped by a backslash.
  * A trailing backslash at the end of a line can be escaped by doubling it.
  *
+ * Instruction lines are not trimmed.
+ * But instructions that are empty or only contain whitespace are discarded.
+ *
  * @param input input string
- * @returns modified input
+ * @returns modified input, as a list of strings
+ *
+ * @see continueLinesFlattened
  */
-export function continue_lines(input: string): string {
-    const outputLines: string[] = [];
+export function continueLines(input: string): Statement[] {
+    const instructions: Statement[] = [];
     let continuePreviousLine = false;
+
+    let currentStatementRaw = '';
+    let currentStatementProcessed = '';
     for (const inputLine of input.split('\n')) {
         const adjustedLine = adjustLine(inputLine, continuePreviousLine);
-        saveLine(outputLines, continuePreviousLine, adjustedLine);
+        if (continuePreviousLine) {
+            currentStatementRaw += '\n' + inputLine;
+            currentStatementProcessed += ' ' + adjustedLine;
+        } else {
+            currentStatementRaw = inputLine;
+            currentStatementProcessed = adjustedLine;
+        }
 
         // Decide what to do with the next line:
         if (endsWith2Slashes(inputLine)) {
@@ -62,8 +85,15 @@ export function continue_lines(input: string): string {
         } else {
             continuePreviousLine = endsWith1Slash(inputLine);
         }
+        if (!continuePreviousLine) {
+            if (currentStatementProcessed.trim() !== '') {
+                instructions.push(new Statement(currentStatementRaw, currentStatementProcessed));
+            }
+            currentStatementRaw = '';
+            currentStatementProcessed = '';
+        }
     }
-    return outputLines.join('\n');
+    return instructions;
 }
 
 /**
@@ -76,8 +106,5 @@ export function continue_lines(input: string): string {
  * @returns List of statements
  */
 export function scan(input: string): string[] {
-    return continue_lines(input)
-        .split('\n')
-        .map((rawLine: string) => rawLine.trim())
-        .filter((line) => line !== '');
+    return continueLines(input).map((instruction: Statement) => instruction.anyContinuationLinesRemoved);
 }
diff --git a/src/Query/Statement.ts b/src/Query/Statement.ts
new file mode 100644
index 00000000..346ce112
--- /dev/null
+++ b/src/Query/Statement.ts
@@ -0,0 +1,55 @@
+/**
+ * A single logical input statement in a Tasks Query block.
+ *
+ * This may represent multiple lines with continuation characters.
+ *
+ * - {@link anyContinuationLinesRemoved} is the statement after continuation lines have been applied.
+ *                                       It may contain placeholders, however.
+ * - If continuation lines were used, {@link rawInstruction} represents the multi-line input.
+ * - {@link anyPlaceholdersExpanded} will differ from {@link anyContinuationLinesRemoved} if there were any placeholders.
+ *
+ * In tests, generally all 3 fields are identical, as the continuation characters and placeholders
+ * are only applied in {@link Query}.
+ */
+export class Statement {
+    private readonly _rawInstruction: string;
+    private readonly _anyContinuationLinesRemoved: string;
+    private _anyPlaceholdersExpanded: string; // May be updated in recordExpandedPlaceholders() after construction
+
+    /**
+     *
+     * @param rawInstruction - If the query used continuation lines for this statement, rawInstruction represents the multi-line input.
+     * @param instruction - whitespace is trimmed
+     */
+    constructor(rawInstruction: string, instruction: string) {
+        this._rawInstruction = rawInstruction;
+        this._anyContinuationLinesRemoved = instruction.trim();
+        this._anyPlaceholdersExpanded = this._anyContinuationLinesRemoved;
+    }
+
+    public recordExpandedPlaceholders(line: string) {
+        this._anyPlaceholdersExpanded = line;
+    }
+
+    /**
+     * The original text of this statement, with all original whitespace.
+     *
+     * If there were continuation lines in the query, this will contain newline characters.
+     */
+    public get rawInstruction(): string {
+        return this._rawInstruction;
+    }
+
+    /**
+     * The text of this statement after any continuation lines have been removed.
+     *
+     * Currently, the line is trimmed, although that may change.
+     */
+    public get anyContinuationLinesRemoved(): string {
+        return this._anyContinuationLinesRemoved;
+    }
+
+    public get anyPlaceholdersExpanded(): string {
+        return this._anyPlaceholdersExpanded;
+    }
+}


Test patch:
diff --git a/tests/Query/Filter/Filter.test.ts b/tests/Query/Filter/Filter.test.ts
index dea2db4c..b4a112bf 100644
--- a/tests/Query/Filter/Filter.test.ts
+++ b/tests/Query/Filter/Filter.test.ts
@@ -15,6 +15,13 @@ describe('Filter', () => {
         expect(filter.instruction).toEqual(line);
         expect(filter.explanation.asString()).toEqual(line);
         expect(filter.filterFunction).not.toBeUndefined();
+
+        expect(filter.statement.rawInstruction).toEqual(line);
+        expect(filter.statement.anyContinuationLinesRemoved).toEqual(line);
+        expect(filter.statement.anyPlaceholdersExpanded).toEqual(line);
+
+        // Note: there is also a test in Query.test.ts that verifies correct populating
+        //       of filter.statement via the Query constructor.
     });
 
     it('should create a Filter object with different explanation', () => {
diff --git a/tests/Query/Query.test.ts b/tests/Query/Query.test.ts
index 2279d0ec..f0509560 100644
--- a/tests/Query/Query.test.ts
+++ b/tests/Query/Query.test.ts
@@ -1459,6 +1459,23 @@ describe('Query', () => {
     });
 
     describe('line continuations', () => {
+        it('should save the source correctly in a Statement object', () => {
+            const source = String.raw`(path includes A) OR \
+                (path includes {{query.file.path}})`;
+            const query = new Query(source, 'Test.md');
+
+            expect(query.error).toBeUndefined();
+            const filter = query.filters[0];
+            expect(filter.statement.rawInstruction).toEqual(source);
+            expect(filter.statement.anyContinuationLinesRemoved).toEqual(
+                '(path includes A) OR (path includes {{query.file.path}})',
+            );
+            expect(filter.statement.anyPlaceholdersExpanded).toEqual('(path includes A) OR (path includes Test.md)');
+
+            // Self-consistency check:
+            expect(filter.statement.anyPlaceholdersExpanded).toEqual(filter.instruction);
+        });
+
         it('should work in group by functions', () => {
             const source = String.raw`group by function \
                 const date = task.due.moment; \
@@ -1471,6 +1488,7 @@ describe('Query', () => {
             const query = new Query(source);
             expect(query.error).toBeUndefined();
         });
+
         it('should be explained correctly in boolean queries', () => {
             const source = String.raw`explain
 (description includes line 1) OR        \
diff --git a/tests/Query/Scanner.test.continue_lines_visualise_continue_lines.approved.txt b/tests/Query/Scanner.test.continue_lines_visualise_continue_lines.approved.txt
index 018406e6..d9f447ea 100644
--- a/tests/Query/Scanner.test.continue_lines_visualise_continue_lines.approved.txt
+++ b/tests/Query/Scanner.test.continue_lines_visualise_continue_lines.approved.txt
@@ -34,37 +34,118 @@ description includes 15 hello world\
 description includes 16 hello world \ 
 description includes 17 hello world \\ 
 
+  description includes 17 hello world  
 
 -------------------------------------
 
-result after calling continue_lines():
+result after calling continueLines():
 -------------------------------------
 
-description includes 1 hello world    
-
-description includes 2 hello world   
-
-description includes 3 hello world
-
-description includes 4 hello world
-
-description includes 5 hello    \
-description includes 6 world
-
-description includes 7 hello world 
-description includes 8 hello world\
-
-description includes 9 hello world description includes 10 hello world\
-
-description includes 11 hello world\
-description includes 12 hello world
-
-description includes 13 hello world\
-description includes 14 hello world\
-
-description includes 15 hello world 
-description includes 16 hello world \ 
-description includes 17 hello world \\ 
-
-
+"description includes 1 hello world    "
+=>
+"description includes 1 hello world"
+=>
+"description includes 1 hello world"
+
+"description includes 2 hello   \
+   world   "
+=>
+"description includes 2 hello world"
+=>
+"description includes 2 hello world"
+
+"description includes 3 hello \
+world"
+=>
+"description includes 3 hello world"
+=>
+"description includes 3 hello world"
+
+"description includes 4 hello    \
+world"
+=>
+"description includes 4 hello world"
+=>
+"description includes 4 hello world"
+
+"description includes 5 hello    \\"
+=>
+"description includes 5 hello    \"
+=>
+"description includes 5 hello    \"
+
+"description includes 6 world"
+=>
+"description includes 6 world"
+=>
+"description includes 6 world"
+
+"description includes 7 hello world\
+"
+=>
+"description includes 7 hello world"
+=>
+"description includes 7 hello world"
+
+"description includes 8 hello world\\"
+=>
+"description includes 8 hello world\"
+=>
+"description includes 8 hello world\"
+
+"description includes 9 hello world\
+description includes 10 hello world\\"
+=>
+"description includes 9 hello world description includes 10 hello world\"
+=>
+"description includes 9 hello world description includes 10 hello world\"
+
+"description includes 11 hello world\\"
+=>
+"description includes 11 hello world\"
+=>
+"description includes 11 hello world\"
+
+"description includes 12 hello world"
+=>
+"description includes 12 hello world"
+=>
+"description includes 12 hello world"
+
+"description includes 13 hello world\\"
+=>
+"description includes 13 hello world\"
+=>
+"description includes 13 hello world\"
+
+"description includes 14 hello world\\"
+=>
+"description includes 14 hello world\"
+=>
+"description includes 14 hello world\"
+
+"description includes 15 hello world\
+"
+=>
+"description includes 15 hello world"
+=>
+"description includes 15 hello world"
+
+"description includes 16 hello world \ "
+=>
+"description includes 16 hello world \"
+=>
+"description includes 16 hello world \"
+
+"description includes 17 hello world \\ "
+=>
+"description includes 17 hello world \\"
+=>
+"description includes 17 hello world \\"
+
+"  description includes 17 hello world  "
+=>
+"description includes 17 hello world"
+=>
+"description includes 17 hello world"
 -------------------------------------
diff --git a/tests/Query/Scanner.test.scan_visualise_scanning.approved.txt b/tests/Query/Scanner.test.scan_visualise_scanning.approved.txt
index 59d3fe87..d2dbb4b9 100644
--- a/tests/Query/Scanner.test.scan_visualise_scanning.approved.txt
+++ b/tests/Query/Scanner.test.scan_visualise_scanning.approved.txt
@@ -34,6 +34,7 @@ description includes 15 hello world\
 description includes 16 hello world \ 
 description includes 17 hello world \\ 
 
+  description includes 17 hello world  
 
 -------------------------------------
 
@@ -55,4 +56,5 @@ description includes 14 hello world\
 description includes 15 hello world
 description includes 16 hello world \
 description includes 17 hello world \\
+description includes 17 hello world
 -------------------------------------
diff --git a/tests/Query/Scanner.test.ts b/tests/Query/Scanner.test.ts
index 09e8b7f7..1b80e27e 100644
--- a/tests/Query/Scanner.test.ts
+++ b/tests/Query/Scanner.test.ts
@@ -1,5 +1,5 @@
 import { verify } from 'approvals/lib/Providers/Jest/JestApprovals';
-import { continue_lines, scan } from '../../src/Query/Scanner';
+import { continueLines, continueLinesFlattened, scan } from '../../src/Query/Scanner';
 
 // There is no way to have a literal \ at the end of a raw string.
 // In such cases, we use substitution instead.
@@ -40,6 +40,7 @@ const querySource = [
     'description includes 16 hello world \\ ',
     'description includes 17 hello world \\\\ ',
     '',
+    '  description includes 17 hello world  ',
     '',
 ].join('\n');
 
@@ -50,7 +51,7 @@ describe('continue_lines', () => {
             'not done',
             'due this week',
         ].join('\n');
-        expect(continue_lines(text)).toEqual(text);
+        expect(continueLinesFlattened(text)).toEqual(text);
     });
 
     it('removes backslashed newlines', () => {
@@ -59,7 +60,7 @@ describe('continue_lines', () => {
             String.raw`line1 ${bs}`,
             'continued',
         ].join('\n');
-        expect(continue_lines(text)).toEqual('line1 continued');
+        expect(continueLinesFlattened(text)).toEqual('line1 continued');
     });
 
     it('only consumes one backslash', () => {
@@ -69,11 +70,10 @@ describe('continue_lines', () => {
             '',
             'line2',
         ].join('\n');
-        expect(continue_lines(text)).toEqual(
+        expect(continueLinesFlattened(text)).toEqual(
             [
                 // force linebreak
                 String.raw`line1 ${bs}`,
-                '',
                 'line2',
             ].join('\n'),
         );
@@ -84,14 +84,12 @@ describe('continue_lines', () => {
             // force linebreak
             String.raw`line\1 ${bs}`,
             `continued ${bs}${bs}${bs}`,
-            '',
             'line2',
         ].join('\n');
-        expect(continue_lines(text)).toEqual(
+        expect(continueLinesFlattened(text)).toEqual(
             [
                 // force linebreak
                 String.raw`line\1 continued \\`,
-                '',
                 'line2',
             ].join('\n'),
         );
@@ -101,14 +99,12 @@ describe('continue_lines', () => {
         const text = [
             // force linebreak
             String.raw`line1${bs}${bs}`,
-            '',
             'line2',
         ].join('\n');
-        expect(continue_lines(text)).toEqual(
+        expect(continueLinesFlattened(text)).toEqual(
             [
                 // force linebreak
                 String.raw`line1${bs}`,
-                '',
                 'line2',
             ].join('\n'),
         );
@@ -120,7 +116,7 @@ describe('continue_lines', () => {
             String.raw`line1    ${bs}`,
             'continued',
         ].join('\n');
-        expect(continue_lines(text)).toEqual(String.raw`line1 continued`);
+        expect(continueLinesFlattened(text)).toEqual(String.raw`line1 continued`);
     });
 
     it('compresses surrounding tabs', () => {
@@ -131,10 +127,10 @@ describe('continue_lines', () => {
             '',
             'line2',
         ].join('\n');
-        expect(continue_lines(text)).toEqual(
+        expect(continueLinesFlattened(text)).toEqual(
             [
                 // force linebreak
-                'line1 continued ',
+                'line1 continued',
                 'line2',
             ].join('\n'),
         );
@@ -151,7 +147,7 @@ describe('continue_lines', () => {
             '   five \\',
             '   six',
         ].join('\n');
-        expect(continue_lines(text)).toEqual('description includes one two three four five six');
+        expect(continueLinesFlattened(text)).toEqual('description includes one two three four five six');
     });
 
     it('visualise continue_lines', () => {
@@ -161,9 +157,18 @@ input:
 ${querySource}
 -------------------------------------
 
-result after calling continue_lines():
+result after calling continueLines():
 -------------------------------------
-${continue_lines(querySource)}
+${continueLines(querySource)
+    .map(
+        (statement) => `
+"${statement.rawInstruction}"
+=>
+"${statement.anyContinuationLinesRemoved}"
+=>
+"${statement.anyPlaceholdersExpanded}"`,
+    )
+    .join('\n')}
 -------------------------------------
 `;
         verify(output);
@@ -185,9 +190,6 @@ describe('scan', () => {
             // force line break
             ' not done   ',
             '        due this week',
-            '',
-            '        ',
-            '        ',
         ].join('\n');
         expect(scan(text)).toEqual(['not done', 'due this week']);
     });
diff --git a/tests/Query/Statement.test.ts b/tests/Query/Statement.test.ts
new file mode 100644
index 00000000..5565a92a
--- /dev/null
+++ b/tests/Query/Statement.test.ts
@@ -0,0 +1,13 @@
+import { Statement } from '../../src/Query/Statement';
+
+describe('Statement', () => {
+    it('should store the instruction', () => {
+        const raw = `hello \\
+world`;
+        const final = 'hello world';
+        const instruction = new Statement(raw, final);
+        expect(instruction.rawInstruction).toEqual(raw);
+        expect(instruction.anyContinuationLinesRemoved).toEqual('hello world');
+        expect(instruction.anyPlaceholdersExpanded).toEqual('hello world');
+    });
+});
diff --git a/tests/Scripting/Expression.test.ts b/tests/Scripting/Expression.test.ts
index 28b0b648..c1d1841c 100644
--- a/tests/Scripting/Expression.test.ts
+++ b/tests/Scripting/Expression.test.ts
@@ -6,7 +6,7 @@ import moment from 'moment';
 import { evaluateExpression, evaluateExpressionOrCatch, parseExpression } from '../../src/Scripting/Expression';
 import { TaskBuilder } from '../TestingTools/TaskBuilder';
 import { verifyMarkdownForDocs } from '../TestingTools/VerifyMarkdown';
-import { continue_lines } from '../../src/Query/Scanner';
+import { continueLinesFlattened } from '../../src/Query/Scanner';
 import { constructArguments, parseAndEvaluateExpression } from '../../src/Scripting/TaskExpression';
 import { makeQueryContext } from '../../src/Scripting/QueryContext';
 import { formatToRepresentType } from './ScriptingTestHelpers';
@@ -112,7 +112,7 @@ describe('Expression', () => {
         markdown +=
             expressions
                 .map((expression) => {
-                    const result = parseAndEvaluateExpression(task, continue_lines(expression), queryContext);
+                    const result = parseAndEvaluateExpression(task, continueLinesFlattened(expression), queryContext);
                     return `${expression}${resultSeparator}=> ${formatToRepresentType(result)}`;
                 })
                 .join(separator) + '\n';