Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!!: Disallow mixing delimiter types in Boolean queries #2762

Merged
merged 22 commits into from Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8abcc87
refactor: - Add BooleanDelimiters.fromInstructionLine()
claremacrae Apr 9, 2024
d785fd4
fix: Make BooleanDelimiters support 'NOT (not done)'
claremacrae Apr 9, 2024
edb4631
test: - Add failing test to detect mixed delimiters
claremacrae Apr 9, 2024
9f259af
test: - Group related tests together
claremacrae Apr 9, 2024
163c1e1
test: - Add tests with binary operators
claremacrae Apr 9, 2024
6db5d18
test: . Extract variables for the lines being tested
claremacrae Apr 9, 2024
9aeef41
test: . extract function shouldDelimitWithParentheses()
claremacrae Apr 7, 2024
89fcbdb
test: . extract function shouldDelimitWithDoubleQuotes()
claremacrae Apr 7, 2024
0c2bd11
test: . extract function shouldThrow()
claremacrae Apr 7, 2024
ddea985
test: . inline variables now I've removed repetition
claremacrae Apr 7, 2024
e057b57
test: . Fix inputs to a test to make it pass, and for the right reason
claremacrae Apr 7, 2024
5c308a4
refactor: - Use a regular expression to find the closing delimiter
claremacrae Apr 7, 2024
7228356
fix: Make detecting of Boolean delimiters more thorough.
claremacrae Apr 7, 2024
4987a56
test: - Test behaviour if the input line is too short
claremacrae Apr 7, 2024
fa165a2
test: - Improve test names
claremacrae Apr 7, 2024
38bb2b3
test: Show that only the outer delimiters are checked
claremacrae Apr 7, 2024
b8d8062
jsdoc: Document that only the outer delimiters are checked
claremacrae Apr 7, 2024
0bc48ae
refactor: Pass delimiters in to two BooleanPreprocessor methods
claremacrae Apr 7, 2024
b65553b
refactor: Pass delimiters in to BooleanPreprocessor.getFiltersAndSimp…
claremacrae Apr 9, 2024
1e368e5
feat!: Disallow mixing () and "" delimiters in Boolean queries
claremacrae Apr 9, 2024
6bf5a77
refactor: Revert accidentally making BooleanDelimiters constructor pu…
claremacrae Apr 9, 2024
13f391a
feat: Improve message shown for multiple delimiter types
claremacrae Apr 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 30 additions & 1 deletion src/Query/Filter/BooleanDelimiters.ts
Expand Up @@ -5,7 +5,9 @@ export function anyOfTheseChars(allowedChars: string): string {
/**
* A class to try to identify the type of delimiter used between Boolean operators.
*
* Currently under development
* Note that this only checks the first and last non-operator characters on the line,
* so where there is more than one binary operator, it is still possible for the user
* to mix delimiters, and for the error to not be detected until later in the parsing process.
*/
export class BooleanDelimiters {
public readonly openFilterChars;
Expand All @@ -28,4 +30,31 @@ export class BooleanDelimiters {
public static allSupportedDelimiters(): BooleanDelimiters {
return new BooleanDelimiters('("', ')"', '()"');
}

public static fromInstructionLine(instruction: string) {
const trimmedInstruction = instruction.trim();

// We use a set of capitals and spaces as a short-cut to match AND, OR, NOT, AND NOT etc.
// The only valid initial operator is NOT, so this may be worth tightening up, if
// further tests show that it would be worthwhile.
const findAnyInitialUnaryOperator = /^[A-Z ]*\s*(.*)/;
const matches = findAnyInitialUnaryOperator.exec(trimmedInstruction);
if (matches) {
const instructionWithoutAnyLeadingOperators = matches[1];
const firstChar = instructionWithoutAnyLeadingOperators[0];
const lastChar = instructionWithoutAnyLeadingOperators.slice(-1);

if (firstChar === '(' && lastChar === ')') {
return new BooleanDelimiters('(', ')', '()');
}

if (firstChar === '"' && lastChar === '"') {
return new BooleanDelimiters('"', '"', '"');
}
}

throw new Error(
'All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.',
);
}
}
10 changes: 9 additions & 1 deletion src/Query/Filter/BooleanField.ts
Expand Up @@ -75,7 +75,15 @@ export class BooleanField extends Field {
return FilterOrErrorMessage.fromError(line, 'empty line');
}

const parseResult = BooleanPreprocessor.preprocessExpression(line);
let delimiters;
try {
delimiters = BooleanDelimiters.fromInstructionLine(line);
} catch (error) {
const message = error instanceof Error ? error.message : 'unknown error type';
return FilterOrErrorMessage.fromError(line, this.helpMessageFromSimpleError(line, message));
}

const parseResult = BooleanPreprocessor.preprocessExpression(line, delimiters);
const simplifiedLine = parseResult.simplifiedLine;
const filters = parseResult.filters;
try {
Expand Down
14 changes: 5 additions & 9 deletions src/Query/Filter/BooleanPreprocessor.ts
Expand Up @@ -6,14 +6,12 @@ export type BooleanPreprocessorResult = {
};

export class BooleanPreprocessor {
public static preprocessExpression(line: string): BooleanPreprocessorResult {
const parts = BooleanPreprocessor.splitLine(line);
return BooleanPreprocessor.getFiltersAndSimplifiedLine(parts);
public static preprocessExpression(line: string, delimiters: BooleanDelimiters): BooleanPreprocessorResult {
const parts = BooleanPreprocessor.splitLine(line, delimiters);
return BooleanPreprocessor.getFiltersAndSimplifiedLine(parts, delimiters);
}

public static splitLine(line: string) {
const delimiters = BooleanDelimiters.allSupportedDelimiters();

public static splitLine(line: string, delimiters: BooleanDelimiters) {
// Here, we split the input line in to separate operators-plus-adjacent-delimiters
// and the remaining filter text.

Expand Down Expand Up @@ -69,9 +67,7 @@ export class BooleanPreprocessor {
.filter((substring) => substring !== '');
}

private static getFiltersAndSimplifiedLine(parts: string[]) {
const delimiters = BooleanDelimiters.allSupportedDelimiters();

private static getFiltersAndSimplifiedLine(parts: string[], delimiters: BooleanDelimiters) {
// Holds the reconstructed expression with placeholders
let simplifiedLine = '';
let currentIndex = 1; // Placeholder index starts at 1
Expand Down
73 changes: 73 additions & 0 deletions tests/Query/Filter/BooleanDelimiters.test.ts
@@ -1,5 +1,31 @@
import { BooleanDelimiters } from '../../../src/Query/Filter/BooleanDelimiters';

function shouldDelimitWithParentheses(line: string) {
const delimiters = BooleanDelimiters.fromInstructionLine(line);

expect(delimiters.openFilterChars).toEqual('(');
expect(delimiters.closeFilterChars).toEqual(')');
expect(delimiters.openAndCloseFilterChars).toEqual('()');
}

function shouldDelimitWithDoubleQuotes(line: string) {
const delimiters = BooleanDelimiters.fromInstructionLine(line);

expect(delimiters.openFilterChars).toEqual('"');
expect(delimiters.closeFilterChars).toEqual('"');
expect(delimiters.openAndCloseFilterChars).toEqual('"');
}

function shouldThrow(line: string) {
const t = () => {
BooleanDelimiters.fromInstructionLine(line);
};
expect(t).toThrow(Error);
expect(t).toThrowError(
'All filters in a Boolean instruction must be inside one of these pairs of delimiter characters: (...) or "...". Combinations of those delimiters are no longer supported.',
);
}

describe('BooleanDelimiters', () => {
it('construction - all delimiters', () => {
const delimiters = BooleanDelimiters.allSupportedDelimiters();
Expand All @@ -12,4 +38,51 @@ describe('BooleanDelimiters', () => {

expect(delimiters.openAndCloseFilterChars).toEqual('()"');
});

describe('construct from line with binary operators', () => {
it('should recognise () delimiters', () => {
shouldDelimitWithParentheses('(not done) OR (done)');
});

it('does not recognise inconsistent delimiters in middle of line', () => {
shouldDelimitWithParentheses('(not done) OR "done" OR (done)');
});

it('should recognise "" delimiters', () => {
shouldDelimitWithDoubleQuotes('"not done" OR "done"');
});

it('should reject line with mixed delimiters', () => {
shouldThrow('(not done) OR "done"');
});

it('should reject line with unknown delimiters', () => {
shouldThrow('{not done} OR {done}');
});
});

describe('construct from line starting with NOT', () => {
it('should recognise () delimiters', () => {
shouldDelimitWithParentheses('NOT (not done)');
});

it('should recognise "" delimiters', () => {
shouldDelimitWithDoubleQuotes('NOT "not done"');
});

it('should reject line with mixed delimiters', () => {
shouldThrow('NOT (not done"');
});

it('should reject line with unknown delimiters', () => {
shouldThrow('NOT {not done}');
});
});

describe('error cases', () => {
it('should throw if line is too short', () => {
shouldThrow('');
shouldThrow('x');
});
});
});