Refactor to enforce strictNullChecks

This commit applies `strictNullChecks` to the entire codebase to improve
maintainability and type safety. Key changes include:

- Remove some explicit null-checks where unnecessary.
- Add necessary null-checks.
- Refactor static factory functions for a more functional approach.
- Improve some test names and contexts for better debugging.
- Add unit tests for any additional logic introduced.
- Refactor `createPositionFromRegexFullMatch` to its own function as the
  logic is reused.
- Prefer `find` prefix on functions that may return `undefined` and
  `get` prefix for those that always return a value.
This commit is contained in:
undergroundwires
2023-11-12 22:54:00 +01:00
parent 7ab16ecccb
commit 949fac1a7c
294 changed files with 2477 additions and 2738 deletions

View File

@@ -9,25 +9,13 @@ import { ExpressionEvaluationContextStub } from '@tests/unit/shared/Stubs/Expres
import { IPipelineCompiler } from '@/application/Parser/Script/Compiler/Expressions/Pipes/IPipelineCompiler';
import { PipelineCompilerStub } from '@tests/unit/shared/Stubs/PipelineCompilerStub';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { getAbsentObjectTestCases, itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
describe('Expression', () => {
describe('ctor', () => {
describe('position', () => {
describe('throws when missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing position';
const position = absentValue;
// act
const act = () => new ExpressionBuilder()
.withPosition(position)
.build();
// assert
expect(act).to.throw(expectedError);
});
});
it('sets as expected', () => {
// arrange
const expected = new ExpressionPosition(0, 5);
@@ -52,7 +40,7 @@ describe('Expression', () => {
expect(actual.parameters);
expect(actual.parameters.all);
expect(actual.parameters.all.length).to.equal(0);
});
}, { excludeNull: true });
});
it('sets as expected', () => {
// arrange
@@ -67,21 +55,6 @@ describe('Expression', () => {
expect(actual.parameters).to.deep.equal(expected);
});
});
describe('evaluator', () => {
describe('throws if missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing evaluator';
const evaluator = absentValue;
// act
const act = () => new ExpressionBuilder()
.withEvaluator(evaluator)
.build();
// assert
expect(act).to.throw(expectedError);
});
});
});
});
describe('evaluate', () => {
describe('throws with invalid arguments', () => {
@@ -91,11 +64,6 @@ describe('Expression', () => {
expectedError: string,
sutBuilder?: (builder: ExpressionBuilder) => ExpressionBuilder,
}[] = [
...getAbsentObjectTestCases().map((testCase) => ({
name: `throws if arguments is ${testCase.valueName}`,
context: testCase.absentValue,
expectedError: 'missing context',
})),
{
name: 'throws when some of the required args are not provided',
sutBuilder: (i: ExpressionBuilder) => i.withParameterNames(['a', 'b', 'c'], false),
@@ -159,7 +127,7 @@ describe('Expression', () => {
const expected = new PipelineCompilerStub();
const context = new ExpressionEvaluationContextStub()
.withPipelineCompiler(expected);
let actual: IPipelineCompiler;
let actual: IPipelineCompiler | undefined;
const evaluatorMock: ExpressionEvaluator = (c) => {
actual = c.pipelineCompiler;
return '';
@@ -170,6 +138,7 @@ describe('Expression', () => {
// arrange
sut.evaluate(context);
// assert
expectExists(actual);
expect(expected).to.equal(actual);
});
describe('filters unused parameters', () => {
@@ -202,7 +171,7 @@ describe('Expression', () => {
];
for (const testCase of testCases) {
it(testCase.name, () => {
let actual: IReadOnlyFunctionCallArgumentCollection;
let actual: IReadOnlyFunctionCallArgumentCollection | undefined;
const evaluatorMock: ExpressionEvaluator = (c) => {
actual = c.args;
return '';
@@ -216,8 +185,10 @@ describe('Expression', () => {
// act
sut.evaluate(context);
// assert
const actualArguments = actual.getAllParameterNames()
.map((name) => actual.getArgument(name));
expectExists(actual);
const collection = actual;
const actualArguments = collection.getAllParameterNames()
.map((name) => collection.getArgument(name));
expect(actualArguments).to.deep.equal(testCase.expectedArguments);
});
}
@@ -228,7 +199,7 @@ describe('Expression', () => {
class ExpressionBuilder {
private position: ExpressionPosition = new ExpressionPosition(0, 5);
private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
private parameters?: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
public withPosition(position: ExpressionPosition) {
this.position = position;
@@ -240,7 +211,7 @@ class ExpressionBuilder {
return this;
}
public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
public withParameters(parameters: IReadOnlyFunctionParameterCollection | undefined) {
this.parameters = parameters;
return this;
}
@@ -261,5 +232,5 @@ class ExpressionBuilder {
return new Expression(this.position, this.evaluator, this.parameters);
}
private evaluator: ExpressionEvaluator = () => '';
private evaluator: ExpressionEvaluator = () => `[${ExpressionBuilder.name}] evaluated-expression`;
}

View File

@@ -4,24 +4,10 @@ import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Sc
import { IPipelineCompiler } from '@/application/Parser/Script/Compiler/Expressions/Pipes/IPipelineCompiler';
import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentCollectionStub';
import { PipelineCompilerStub } from '@tests/unit/shared/Stubs/PipelineCompilerStub';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('ExpressionEvaluationContext', () => {
describe('ctor', () => {
describe('args', () => {
describe('throws if args is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing args, send empty collection instead.';
const args = absentValue;
// act
const act = () => new ExpressionEvaluationContextBuilder()
.withArgs(args)
.build();
// assert
expect(act).throw(expectedError);
});
});
it('sets as expected', () => {
// arrange
const expected = new FunctionCallArgumentCollectionStub()

View File

@@ -0,0 +1,102 @@
import { describe, it, expect } from 'vitest';
import { createPositionFromRegexFullMatch } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory';
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
describe('ExpressionPositionFactory', () => {
describe('createPositionFromRegexFullMatch', () => {
it(`creates ${ExpressionPosition.name} instance`, () => {
// arrange
const expectedType = ExpressionPosition;
const fakeMatch = createRegexMatch({
fullMatch: 'matched string',
matchIndex: 5,
});
// act
const position = createPositionFromRegexFullMatch(fakeMatch);
// assert
expect(position).to.be.instanceOf(expectedType);
});
it('creates a position with the correct start position', () => {
// arrange
const expectedStartPosition = 5;
const fakeMatch = createRegexMatch({
fullMatch: 'matched string',
matchIndex: expectedStartPosition,
});
// act
const position = createPositionFromRegexFullMatch(fakeMatch);
// assert
expect(position.start).toBe(expectedStartPosition);
});
it('creates a position with the correct end position', () => {
// arrange
const startPosition = 3;
const matchedString = 'matched string';
const expectedEndPosition = startPosition + matchedString.length;
const fakeMatch = createRegexMatch({
fullMatch: matchedString,
matchIndex: startPosition,
});
// act
const position = createPositionFromRegexFullMatch(fakeMatch);
// assert
expect(position.end).to.equal(expectedEndPosition);
});
it('creates correct position with capturing groups', () => {
// arrange
const startPosition = 20;
const fakeMatch = createRegexMatch({
fullMatch: 'matched string',
capturingGroups: ['group1', 'group2'],
matchIndex: startPosition,
});
// act
const position = createPositionFromRegexFullMatch(fakeMatch);
// assert
expect(position.start).toBe(startPosition);
expect(position.end).toBe(startPosition + fakeMatch[0].length);
});
describe('invalid values', () => {
it('throws an error if match.index is undefined', () => {
// arrange
const fakeMatch = createRegexMatch({
fullMatch: 'matched string',
matchIndex: undefined,
});
const expectedError = `Regex match did not yield any results: ${JSON.stringify(fakeMatch)}`;
// act
const act = () => createPositionFromRegexFullMatch(fakeMatch);
// assert
expect(act).to.throw(expectedError);
});
it('throws an error for empty match', () => {
// arrange
const fakeMatch = createRegexMatch({
fullMatch: '',
matchIndex: 0,
});
const expectedError = `Regex match is empty: ${JSON.stringify(fakeMatch)}`;
// act
const act = () => createPositionFromRegexFullMatch(fakeMatch);
// assert
expect(act).to.throw(expectedError);
});
});
});
});
function createRegexMatch(options?: {
readonly fullMatch?: string,
readonly capturingGroups?: readonly string[],
readonly matchIndex?: number,
}): RegExpMatchArray {
const fullMatch = options?.fullMatch ?? 'fake match';
const capturingGroups = options?.capturingGroups ?? [];
const fakeMatch: RegExpMatchArray = [fullMatch, ...capturingGroups];
fakeMatch.index = options?.matchIndex;
return fakeMatch;
}