From 58da1a2cc890fb18b72ec7eb427a80c31e6cb30b Mon Sep 17 00:00:00 2001 From: undergroundwires Date: Sun, 12 Nov 2023 03:22:05 +0100 Subject: [PATCH] Refactor code with and 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. --- cypress.config.ts | 10 +- docs/tests.md | 2 +- src/application/ApplicationFactory.ts | 3 - src/application/Common/Array.ts | 4 - src/application/Common/CustomError.ts | 23 +- src/application/Common/Enum.ts | 3 - .../ScriptingLanguageFactory.ts | 7 +- src/application/Context/ApplicationContext.ts | 13 +- .../Context/ApplicationContextFactory.ts | 15 +- .../Context/State/Code/ApplicationCode.ts | 3 - .../State/Code/Event/CodeChangedEvent.ts | 6 +- .../State/Code/Generation/CodeBuilder.ts | 4 +- .../Code/Generation/UserScriptGenerator.ts | 4 +- .../State/Filter/Event/FilterChange.ts | 38 +-- .../Filter/Event/IFilterChangeDetails.ts | 19 +- .../Context/State/Selection/UserSelection.ts | 8 +- src/application/Parser/ApplicationParser.ts | 5 +- .../Parser/CategoryCollectionParser.ts | 5 +- src/application/Parser/CategoryParser.ts | 22 +- src/application/Parser/DocumentationParser.ts | 3 - .../Parser/NodeValidation/NodeValidator.ts | 2 +- .../Script/CategoryCollectionParseContext.ts | 3 +- .../Expressions/Expression/Expression.ts | 9 - .../Expression/ExpressionEvaluationContext.ts | 3 - .../Expression/ExpressionPositionFactory.ts | 16 ++ .../Expressions/ExpressionsCompiler.ts | 7 +- .../Expressions/IExpressionsCompiler.ts | 5 +- .../Parser/CompositeExpressionParser.ts | 5 +- .../Expressions/Parser/Regex/RegexParser.ts | 10 +- .../Compiler/Expressions/Pipes/PipeFactory.ts | 11 +- .../Expressions/SyntaxParsers/WithParser.ts | 26 +-- .../FunctionCallArgumentCollection.ts | 3 - .../NewlineCodeSegmentMerger.ts | 10 +- .../Compiler/FunctionCallSequenceCompiler.ts | 4 +- .../Strategies/InlineFunctionCallCompiler.ts | 20 +- .../Strategies/NestedFunctionCallCompiler.ts | 6 +- .../Function/Call/FunctionCallParser.ts | 16 +- .../Function/Call/ParsedFunctionCall.ts | 3 - .../Compiler/Function/ISharedFunction.ts | 16 +- .../Parameter/FunctionParameterCollection.ts | 3 - .../Compiler/Function/SharedFunction.ts | 29 ++- .../Function/SharedFunctionCollection.ts | 1 - .../Function/SharedFunctionsParser.ts | 53 +++-- .../Parser/Script/Compiler/ScriptCompiler.ts | 31 ++- src/application/Parser/Script/ScriptParser.ts | 44 ++-- .../Parser/Script/Validation/CodeValidator.ts | 2 +- .../Validation/Rules/NoDuplicatedLines.ts | 4 +- .../ScriptingDefinition/CodeSubstituter.ts | 1 - .../ScriptingDefinitionParser.ts | 2 - .../collections/collection.yaml.d.ts | 35 ++- src/domain/Application.ts | 17 +- src/domain/Category.ts | 11 +- src/domain/CategoryCollection.ts | 22 +- src/domain/IApplication.ts | 2 +- src/domain/ICategory.ts | 4 +- src/domain/ICategoryCollection.ts | 4 +- src/domain/IScriptCode.ts | 2 +- src/domain/Script.ts | 3 - src/domain/ScriptCode.ts | 6 +- src/infrastructure/CodeRunner.ts | 3 + .../EnvironmentVariablesValidator.ts | 5 +- .../Events/EventSubscriptionCollection.ts | 5 +- src/infrastructure/Log/ConsoleLogger.ts | 10 +- src/infrastructure/Log/ElectronLogger.ts | 7 +- .../Log/WindowInjectedLogger.ts | 4 +- src/infrastructure/Repository/IRepository.ts | 2 +- .../Repository/InMemoryRepository.ts | 12 +- .../BrowserOs/BrowserOsDetector.ts | 3 - .../BrowserOs/DetectorBuilder.ts | 4 +- .../RuntimeEnvironment/RuntimeEnvironment.ts | 2 +- .../RuntimeSanity/Common/FactoryValidator.ts | 3 - .../RuntimeSanity/SanityChecks.ts | 19 +- src/infrastructure/SaveFileDialog.ts | 16 +- .../SystemOperations/ISystemOperations.ts | 2 +- .../SystemOperations/NodeSystemOperations.ts | 10 +- src/infrastructure/Threading/AsyncLazy.ts | 61 +++-- .../WindowVariables/WindowVariables.ts | 8 +- .../WindowVariablesValidator.ts | 2 +- src/presentation/components/App.vue | 25 +- .../Code/CodeButtons/CodeRunButton.vue | 2 +- .../Code/CodeButtons/Save/CodeSaveButton.vue | 15 +- .../InstructionListDataFactory.ts | 8 +- .../components/Code/TheCodeArea.vue | 13 +- .../Menu/Selector/SelectionTypeHandler.ts | 5 +- .../components/Scripts/Menu/TheOsChanger.vue | 12 +- .../Scripts/Slider/HorizontalResizeSlider.vue | 8 +- .../Scripts/Slider/SliderHandle.vue | 5 +- .../Scripts/View/Cards/CardList.vue | 7 +- .../Scripts/View/Cards/CardListItem.vue | 15 +- .../View/Cards/CardSelectionIndicator.vue | 2 +- .../Scripts/View/TheScriptsView.vue | 5 +- .../Documentation/DocumentationText.vue | 2 +- .../Documentation/MarkdownRenderer.ts | 6 +- .../View/Tree/NodeContent/NodeMetadata.ts | 2 +- .../NodeContent/Reverter/CategoryReverter.ts | 5 +- .../Scripts/View/Tree/ScriptsTree.vue | 2 +- .../TreeView/Bindings/TreeInputFilterEvent.ts | 18 +- .../Node/Hierarchy/TreeNodeHierarchy.ts | 2 +- .../View/Tree/TreeView/TreeRoot/TreeRoot.vue | 3 + .../TreeView/UseTreeKeyboardNavigation.ts | 28 ++- .../View/Tree/TreeView/UseTreeQueryFilter.ts | 28 ++- .../CategoryNodeMetadataConverter.ts | 11 +- .../TreeNodeMetadataConverter.ts | 4 +- .../TreeViewAdapter/UseTreeViewFilterEvent.ts | 2 +- .../components/Shared/Hooks/UseApplication.ts | 3 - .../Shared/Hooks/UseCollectionState.ts | 16 -- .../Shared/Hooks/UseRuntimeEnvironment.ts | 3 - .../components/Shared/Icon/UseSvgLoader.ts | 2 +- .../Modal/Hooks/UseCurrentFocusToggle.ts | 4 +- .../components/Shared/SizeObserver.vue | 74 +++--- .../components/Shared/Throttle.ts | 10 +- .../components/TheFooter/DownloadUrlList.vue | 4 +- .../TheFooter/DownloadUrlListItem.vue | 2 +- src/presentation/components/TheSearchBar.vue | 7 +- .../electron/main/Update/ManualUpdater.ts | 9 +- src/presentation/electron/main/index.ts | 4 +- .../app/app-logs.ts | 21 +- .../app/check-for-errors.ts | 6 +- .../extractors/common/app-artifact-locator.ts | 4 +- .../app/extractors/macos.ts | 10 +- .../app/runner.ts | 18 +- .../system-capture/window-title-capture.ts | 3 +- .../check-desktop-runtime-errors/utils/npm.ts | 5 +- .../utils/platform.ts | 4 +- .../utils/run-command.ts | 2 +- .../utils/text.ts | 5 + .../StatusChecker/BatchStatusChecker.ts | 16 +- .../ExponentialBackOffRetryHandler.ts | 3 + .../StatusChecker/FetchFollow.ts | 24 +- .../external-urls/StatusChecker/Requestor.ts | 27 +-- tests/e2e/revert-toggle.cy.ts | 10 +- tests/e2e/tsconfig.json | 4 +- .../RuntimeSanity/SanityChecks.spec.ts | 4 + .../Validators/ValidatorTestRunner.ts | 3 - .../preload/WindowVariablesProvider.spec.ts | 2 +- .../Assertions/ExpectDeepThrowsError.ts | 43 ++-- tests/shared/Assertions/ExpectExists.ts | 11 + tests/shared/Assertions/ExpectThrowsAsync.ts | 7 +- .../application/ApplicationFactory.spec.ts | 14 -- tests/unit/application/Common/Array.spec.ts | 57 +---- .../application/Common/CustomError.spec.ts | 16 +- tests/unit/application/Common/Enum.spec.ts | 14 +- .../application/Common/EnumRangeTestRunner.ts | 26 +-- .../ScriptingLanguageFactory.spec.ts | 15 -- .../ScriptingLanguageFactoryTestRunner.ts | 3 +- .../Context/ApplicationContext.spec.ts | 59 ++--- .../Context/ApplicationContextFactory.spec.ts | 47 ++-- .../State/CategoryCollectionState.spec.ts | 6 +- .../State/Code/ApplicationCode.spec.ts | 51 +---- .../State/Code/Event/CodeChangedEvent.spec.ts | 11 + .../Generation/UserScriptGenerator.spec.ts | 39 +--- .../State/Filter/Event/FilterChange.spec.ts | 37 +-- .../Context/State/Filter/UserFilter.spec.ts | 14 +- .../Selection/UserSelectionTestRunner.ts | 2 +- .../Parser/ApplicationParser.spec.ts | 26 +-- .../Parser/CategoryCollectionParser.spec.ts | 44 +--- .../application/Parser/CategoryParser.spec.ts | 36 +-- .../Parser/DocumentationParser.spec.ts | 14 +- .../NodeValidation/NodeValidator.spec.ts | 2 +- .../NodeValidation/NodeValidatorTestRunner.ts | 2 +- .../CategoryCollectionParseContext.spec.ts | 25 +- .../Expressions/Expression/Expression.spec.ts | 55 ++--- .../ExpressionEvaluationContext.spec.ts | 14 -- .../ExpressiontPositionFactory.spec.ts | 102 +++++++++ .../Expressions/ExpressionsCompiler.spec.ts | 24 +- .../Parser/CompositeExpressionParser.spec.ts | 21 +- .../Parser/Regex/RegexParser.spec.ts | 13 +- .../EscapeDoubleQuotes.spec.ts | 11 +- .../PipeDefinitions/InlinePowerShell.spec.ts | 12 +- .../Expressions/Pipes/PipeFactory.spec.ts | 33 +-- .../Pipes/PipelineCompiler.spec.ts | 28 +-- .../SyntaxParsers/WithParser.spec.ts | 30 +-- .../Argument/FunctionCallArgument.spec.ts | 2 +- .../FunctionCallArgumentCollection.spec.ts | 18 +- .../NewlineCodeSegmentMerger.spec.ts | 53 +++-- .../FunctionCallSequenceCompiler.spec.ts | 46 +--- .../NestedFunctionCallCompiler.spec.ts | 19 +- .../AdaptiveFunctionCallCompiler.spec.ts | 9 +- .../NestedFunctionArgumentCompiler.spec.ts | 17 +- .../InlineFunctionCallCompiler.spec.ts | 128 +++++++---- .../Function/Call/FunctionCallParser.spec.ts | 29 +-- .../Function/Call/ParsedFunctionCall.spec.ts | 17 +- .../Function/ExpectFunctionBodyType.ts | 29 +++ .../FunctionParameterCollection.spec.ts | 15 -- .../Compiler/Function/SharedFunction.spec.ts | 58 ++--- .../Function/SharedFunctionCollection.spec.ts | 29 +-- .../Function/SharedFunctionsParser.spec.ts | 92 +++----- .../Compiler/ParameterNameTestRunner.ts | 29 +-- .../Script/Compiler/ScriptCompiler.spec.ts | 112 +++------ .../Parser/Script/ScriptParser.spec.ts | 64 ++---- .../Script/Validation/CodeValidator.spec.ts | 8 +- .../Rules/NoDuplicatedLines.spec.ts | 16 +- .../Validation/Rules/NoEmptyLines.spec.ts | 6 - .../CodeSubstituter.spec.ts | 45 ++-- .../ScriptingDefinitionParser.spec.ts | 29 --- tests/unit/domain/Application.spec.ts | 43 ++-- tests/unit/domain/Category.spec.ts | 2 +- tests/unit/domain/CategoryCollection.spec.ts | 90 +++++--- tests/unit/domain/ProjectInformation.spec.ts | 3 +- tests/unit/domain/Script.spec.ts | 30 +-- tests/unit/domain/ScriptCode.spec.ts | 60 +++-- tests/unit/domain/ScriptingDefinition.spec.ts | 22 +- tests/unit/domain/Version.spec.ts | 24 +- tests/unit/infrastructure/CodeRunner.spec.ts | 45 +++- .../EnvironmentVariablesFactory.ts | 4 +- .../EnvironmentVariablesValidator.spec.ts | 12 - .../EventSubscriptionCollection.spec.ts | 24 +- .../infrastructure/InMemoryRepository.spec.ts | 37 +-- .../infrastructure/Log/ConsoleLogger.spec.ts | 19 +- .../infrastructure/Log/ElectronLogger.spec.ts | 43 ++-- .../infrastructure/Log/LoggerTestRunner.ts | 6 +- .../Log/WindowInjectedLogger.spec.ts | 2 +- .../BrowserOs/BrowserOsDetector.spec.ts | 4 +- .../RuntimeEnvironment.spec.ts | 4 +- .../WindowVariablesValidator.spec.ts | 2 +- .../Common/FactoryValidator.spec.ts | 14 -- .../RuntimeSanity/SanityChecks.spec.ts | 31 +-- .../WindowInjectedSystemOperations.spec.ts | 4 +- .../Modules/RuntimeSanityValidator.spec.ts | 2 +- .../Code/CodeButtons/CodeCopyButton.spec.ts | 5 +- .../Save/Instructions/CodeInstruction.spec.ts | 3 +- .../Data/InstructionsBuilder.spec.ts | 32 +-- .../InstructionListDataFactory.spec.ts | 31 +-- .../Selector/SelectionTypeHandler.spec.ts | 13 +- .../View/Cards/NonCollapsingDirective.spec.ts | 15 +- .../Scripts/View/TheScriptsView.spec.ts | 24 +- .../Bindings/TreeViewFilterEvent.spec.ts | 43 +--- .../Node/Hierarchy/TreeNodeHierarchy.spec.ts | 2 +- .../TreeView/Node/State/TreeNodeState.spec.ts | 5 +- .../TreeView/Node/TreeNodeManager.spec.ts | 10 +- .../Node/UseKeyboardInteractionState.spec.ts | 5 +- .../Tree/TreeView/Node/UseNodeState.spec.ts | 5 +- .../NodeCollection/TreeInputParser.spec.ts | 13 -- .../TreeNodeInitializerAndUpdater.spec.ts | 5 +- .../UseAutoUpdateChildrenCheckState.spec.ts | 4 +- .../Tree/TreeView/UseCurrentTreeNodes.spec.ts | 5 +- .../UseNodeStateChangeAggregator.spec.ts | 20 +- .../CategoryNodeMetadataConverter.spec.ts | 42 +++- .../TreeNodeMetadataConverter.spec.ts | 35 +-- .../UseTreeViewFilterEvent.spec.ts | 47 ++-- .../UseTreeViewNodeInput.spec.ts | 6 +- .../Hooks/Clipboard/BrowserClipboard.spec.ts | 3 +- .../Hooks/Clipboard/UseClipboard.spec.ts | 3 +- .../Shared/Hooks/UseApplication.spec.ts | 13 -- .../Shared/Hooks/UseCollectionState.spec.ts | 78 +------ .../Hooks/UseRuntimeEnvironment.spec.ts | 13 -- .../components/Shared/Throttle.spec.ts | 34 +-- .../electron/preload/NodeOsMapper.spec.ts | 15 +- tests/unit/shared/ExceptionCollector.ts | 12 + tests/unit/shared/Stubs/ApplicationStub.ts | 10 +- .../Stubs/CategoryCollectionParserStub.ts | 7 +- .../shared/Stubs/CategoryCollectionStub.ts | 21 +- tests/unit/shared/Stubs/CategoryDataStub.ts | 4 +- tests/unit/shared/Stubs/CategoryStub.ts | 20 +- tests/unit/shared/Stubs/CodeValidatorStub.ts | 2 +- tests/unit/shared/Stubs/CollectionDataStub.ts | 17 +- tests/unit/shared/Stubs/DelaySchedulerStub.ts | 2 +- .../unit/shared/Stubs/ExpressionParserStub.ts | 5 +- tests/unit/shared/Stubs/ExpressionStub.ts | 2 +- .../shared/Stubs/ExpressionsCompilerStub.ts | 26 ++- tests/unit/shared/Stubs/FileSystemOpsStub.ts | 4 +- .../shared/Stubs/FilterChangeDetailsStub.ts | 27 ++- tests/unit/shared/Stubs/FunctionCodeStub.ts | 4 +- tests/unit/shared/Stubs/FunctionDataStub.ts | 65 ++++-- tests/unit/shared/Stubs/LanguageSyntaxStub.ts | 4 +- tests/unit/shared/Stubs/LocationOpsStub.ts | 12 +- tests/unit/shared/Stubs/NodeMetadataStub.ts | 2 +- .../Stubs/NodeStateChangeEventArgsStub.ts | 2 +- .../Stubs/ProjectInformationParserStub.ts | 2 +- .../shared/Stubs/RuntimeEnvironmentStub.ts | 4 +- tests/unit/shared/Stubs/ScriptCompilerStub.ts | 17 +- tests/unit/shared/Stubs/ScriptDataStub.ts | 67 +++--- tests/unit/shared/Stubs/ScriptStub.ts | 21 +- .../shared/Stubs/ScriptingDefinitionStub.ts | 6 +- .../Stubs/SharedFunctionCollectionStub.ts | 11 +- tests/unit/shared/Stubs/SharedFunctionStub.ts | 79 +++++-- .../shared/Stubs/SharedFunctionsParserStub.ts | 2 +- tests/unit/shared/Stubs/TreeNodeStub.ts | 2 +- tests/unit/shared/Stubs/UserFilterStub.ts | 7 +- .../unit/shared/Stubs/WindowVariablesStub.ts | 16 +- tests/unit/shared/TestCases/AbsentTests.ts | 216 +++++++++++++----- tests/unit/shared/TestCases/TransientFacto | 0 tsconfig.json | 1 + 283 files changed, 2353 insertions(+), 2606 deletions(-) create mode 100644 src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory.ts rename tests/{unit => }/shared/Assertions/ExpectDeepThrowsError.ts (54%) create mode 100644 tests/shared/Assertions/ExpectExists.ts create mode 100644 tests/unit/application/Parser/Script/Compiler/Expressions/Expression/ExpressiontPositionFactory.spec.ts create mode 100644 tests/unit/application/Parser/Script/Compiler/Function/ExpectFunctionBodyType.ts create mode 100644 tests/unit/shared/ExceptionCollector.ts delete mode 100644 tests/unit/shared/TestCases/TransientFacto diff --git a/cypress.config.ts b/cypress.config.ts index 55a351323..225323d8c 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -11,8 +11,16 @@ export default defineConfig({ videosFolder: `${CYPRESS_BASE_DIR}/videos`, e2e: { - baseUrl: `http://localhost:${ViteConfig.server.port}/`, + baseUrl: `http://localhost:${getApplicationPort()}/`, specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx} supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`, }, }); + +function getApplicationPort(): number { + const port = ViteConfig.server?.port; + if (port === undefined) { + throw new Error('Unknown application port'); + } + return port; +} diff --git a/docs/tests.md b/docs/tests.md index 715f69670..d67360809 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -68,13 +68,13 @@ These checks validate various qualities like runtime execution, building process - [`./src/`](./../src/): Contains the code subject to testing. - [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories. - [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests. + - [`Assertions/`](./../tests/shared/Assertions/): Contains common assertion functions, prefixed with `expect`. - [`./tests/unit/`](./../tests/unit/) - Stores unit test code. - The directory structure mirrors [`./src/`](./../src). - E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts). - [`shared/`](./../tests/unit/shared/) - Contains shared unit test functionalities. - - [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`. - [`TestCases/`](./../tests/unit/shared/TestCases/) - Shared test cases. - Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix. diff --git a/src/application/ApplicationFactory.ts b/src/application/ApplicationFactory.ts index 6e80451ec..e9b7a60db 100644 --- a/src/application/ApplicationFactory.ts +++ b/src/application/ApplicationFactory.ts @@ -12,9 +12,6 @@ export class ApplicationFactory implements IApplicationFactory { private readonly getter: AsyncLazy; protected constructor(costlyGetter: ApplicationGetterType) { - if (!costlyGetter) { - throw new Error('missing getter'); - } this.getter = new AsyncLazy(() => Promise.resolve(costlyGetter())); } diff --git a/src/application/Common/Array.ts b/src/application/Common/Array.ts index a3f95dfdf..c48b831a0 100644 --- a/src/application/Common/Array.ts +++ b/src/application/Common/Array.ts @@ -1,7 +1,5 @@ // Compares to Array objects for equality, ignoring order export function scrambledEqual(array1: readonly T[], array2: readonly T[]) { - if (!array1) { throw new Error('missing first array'); } - if (!array2) { throw new Error('missing second array'); } const sortedArray1 = sort(array1); const sortedArray2 = sort(array2); return sequenceEqual(sortedArray1, sortedArray2); @@ -12,8 +10,6 @@ export function scrambledEqual(array1: readonly T[], array2: readonly T[]) { // Compares to Array objects for equality in same order export function sequenceEqual(array1: readonly T[], array2: readonly T[]) { - if (!array1) { throw new Error('missing first array'); } - if (!array2) { throw new Error('missing second array'); } if (array1.length !== array2.length) { return false; } diff --git a/src/application/Common/CustomError.ts b/src/application/Common/CustomError.ts index f572cd06e..b219a945e 100644 --- a/src/application/Common/CustomError.ts +++ b/src/application/Common/CustomError.ts @@ -20,23 +20,30 @@ export abstract class CustomError extends Error { } } -export const Environment = { +interface ErrorPrototypeManipulation { + getSetPrototypeOf: () => (typeof Object.setPrototypeOf | undefined); + getCaptureStackTrace: () => (typeof Error.captureStackTrace | undefined); +} + +export const PlatformErrorPrototypeManipulation: ErrorPrototypeManipulation = { getSetPrototypeOf: () => Object.setPrototypeOf, getCaptureStackTrace: () => Error.captureStackTrace, }; function fixPrototype(target: Error, prototype: CustomError) { - // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget - const setPrototypeOf = Environment.getSetPrototypeOf(); - if (!functionExists(setPrototypeOf)) { + // This is recommended by TypeScript guidelines. + // Source: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + // Snapshots: https://web.archive.org/web/20231111234849/https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget, https://archive.ph/tr7cX#support-for-newtarget + const setPrototypeOf = PlatformErrorPrototypeManipulation.getSetPrototypeOf(); + if (!isFunction(setPrototypeOf)) { return; } setPrototypeOf(target, prototype); } function ensureStackTrace(target: Error) { - const captureStackTrace = Environment.getCaptureStackTrace(); - if (!functionExists(captureStackTrace)) { + const captureStackTrace = PlatformErrorPrototypeManipulation.getCaptureStackTrace(); + if (!isFunction(captureStackTrace)) { // captureStackTrace is only available on V8, if it's not available // modern JS engines will usually generate a stack trace on error objects when they're thrown. return; @@ -44,7 +51,7 @@ function ensureStackTrace(target: Error) { captureStackTrace(target, target.constructor); } -function functionExists(func: unknown): boolean { - // Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness +// eslint-disable-next-line @typescript-eslint/ban-types +function isFunction(func: unknown): func is Function { return typeof func === 'function'; } diff --git a/src/application/Common/Enum.ts b/src/application/Common/Enum.ts index 624a52ab7..b7f37da73 100644 --- a/src/application/Common/Enum.ts +++ b/src/application/Common/Enum.ts @@ -54,9 +54,6 @@ export function assertInRange( value: TEnumValue, enumVariable: EnumVariable, ) { - if (value === undefined || value === null) { - throw new Error('absent enum value'); - } if (!(value in enumVariable)) { throw new RangeError(`enum value "${value}" is out of range`); } diff --git a/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts b/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts index 6ae46fdf2..284152394 100644 --- a/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts +++ b/src/application/Common/ScriptingLanguage/ScriptingLanguageFactory.ts @@ -9,19 +9,16 @@ export abstract class ScriptingLanguageFactory implements IScriptingLanguageF public create(language: ScriptingLanguage): T { assertInRange(language, ScriptingLanguage); - if (!this.getters.has(language)) { + const getter = this.getters.get(language); + if (!getter) { throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`); } - const getter = this.getters.get(language); const instance = getter(); return instance; } protected registerGetter(language: ScriptingLanguage, getter: Getter) { assertInRange(language, ScriptingLanguage); - if (!getter) { - throw new Error('missing getter'); - } if (this.getters.has(language)) { throw new Error(`${ScriptingLanguage[language]} is already registered`); } diff --git a/src/application/Context/ApplicationContext.ts b/src/application/Context/ApplicationContext.ts index 2ed02b5f3..284664f3f 100644 --- a/src/application/Context/ApplicationContext.ts +++ b/src/application/Context/ApplicationContext.ts @@ -26,7 +26,6 @@ export class ApplicationContext implements IApplicationContext { public readonly app: IApplication, initialContext: OperatingSystem, ) { - validateApp(app); this.states = initializeStates(app); this.changeContext(initialContext); } @@ -36,10 +35,8 @@ export class ApplicationContext implements IApplicationContext { if (this.currentOs === os) { return; } - this.collection = this.app.getCollection(os); - if (!this.collection) { - throw new Error(`os "${OperatingSystem[os]}" is not defined in application`); - } + const collection = this.app.getCollection(os); + this.collection = collection; const event: IApplicationContextChangedEvent = { newState: this.states[os], oldState: this.states[this.currentOs], @@ -49,12 +46,6 @@ export class ApplicationContext implements IApplicationContext { } } -function validateApp(app: IApplication) { - if (!app) { - throw new Error('missing app'); - } -} - function initializeStates(app: IApplication): StateMachine { const machine = new Map(); for (const collection of app.collections) { diff --git a/src/application/Context/ApplicationContextFactory.ts b/src/application/Context/ApplicationContextFactory.ts index 44350ef5f..264c94b6e 100644 --- a/src/application/Context/ApplicationContextFactory.ts +++ b/src/application/Context/ApplicationContextFactory.ts @@ -10,20 +10,25 @@ export async function buildContext( factory: IApplicationFactory = ApplicationFactory.Current, environment = RuntimeEnvironment.CurrentEnvironment, ): Promise { - if (!factory) { throw new Error('missing factory'); } - if (!environment) { throw new Error('missing environment'); } const app = await factory.getApp(); const os = getInitialOs(app, environment.os); return new ApplicationContext(app, os); } -function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem { +function getInitialOs( + app: IApplication, + currentOs: OperatingSystem | undefined, +): OperatingSystem { const supportedOsList = app.getSupportedOsList(); - if (supportedOsList.includes(currentOs)) { + if (currentOs !== undefined && supportedOsList.includes(currentOs)) { return currentOs; } + return getMostSupportedOs(supportedOsList, app); +} + +function getMostSupportedOs(supportedOsList: OperatingSystem[], app: IApplication) { supportedOsList.sort((os1, os2) => { - const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts; + const getPriority = (os: OperatingSystem) => app.getCollection(os)?.totalScripts ?? 0; return getPriority(os2) - getPriority(os1); }); return supportedOsList[0]; diff --git a/src/application/Context/State/Code/ApplicationCode.ts b/src/application/Context/State/Code/ApplicationCode.ts index 165d809b9..91cf795f0 100644 --- a/src/application/Context/State/Code/ApplicationCode.ts +++ b/src/application/Context/State/Code/ApplicationCode.ts @@ -21,9 +21,6 @@ export class ApplicationCode implements IApplicationCode { private readonly scriptingDefinition: IScriptingDefinition, private readonly generator: IUserScriptGenerator = new UserScriptGenerator(), ) { - if (!userSelection) { throw new Error('missing userSelection'); } - if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); } - if (!generator) { throw new Error('missing generator'); } this.setCode(userSelection.selectedScripts); userSelection.changed.on((scripts) => { this.setCode(scripts); diff --git a/src/application/Context/State/Code/Event/CodeChangedEvent.ts b/src/application/Context/State/Code/Event/CodeChangedEvent.ts index 52cbb5322..681df9261 100644 --- a/src/application/Context/State/Code/Event/CodeChangedEvent.ts +++ b/src/application/Context/State/Code/Event/CodeChangedEvent.ts @@ -36,7 +36,11 @@ export class CodeChangedEvent implements ICodeChangedEvent { } public getScriptPositionInCode(script: IScript): ICodePosition { - return this.scripts.get(script); + const position = this.scripts.get(script); + if (!position) { + throw new Error('Unknown script: Position could not be found for the script'); + } + return position; } } diff --git a/src/application/Context/State/Code/Generation/CodeBuilder.ts b/src/application/Context/State/Code/Generation/CodeBuilder.ts index 22fa6ea13..4264c8763 100644 --- a/src/application/Context/State/Code/Generation/CodeBuilder.ts +++ b/src/application/Context/State/Code/Generation/CodeBuilder.ts @@ -16,7 +16,9 @@ export abstract class CodeBuilder implements ICodeBuilder { return this; } const lines = code.match(/[^\r\n]+/g); - this.lines.push(...lines); + if (lines) { + this.lines.push(...lines); + } return this; } diff --git a/src/application/Context/State/Code/Generation/UserScriptGenerator.ts b/src/application/Context/State/Code/Generation/UserScriptGenerator.ts index 7743942b5..abad29e8c 100644 --- a/src/application/Context/State/Code/Generation/UserScriptGenerator.ts +++ b/src/application/Context/State/Code/Generation/UserScriptGenerator.ts @@ -17,8 +17,6 @@ export class UserScriptGenerator implements IUserScriptGenerator { selectedScripts: ReadonlyArray, scriptingDefinition: IScriptingDefinition, ): IUserScript { - if (!selectedScripts) { throw new Error('missing scripts'); } - if (!scriptingDefinition) { throw new Error('missing definition'); } if (!selectedScripts.length) { return { code: '', scriptPositions: new Map() }; } @@ -68,7 +66,7 @@ function appendSelection( function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder { const { script } = selection; const name = selection.revert ? `${script.name} (revert)` : script.name; - const scriptCode = selection.revert ? script.code.revert : script.code.execute; + const scriptCode = selection.revert ? (script.code.revert ?? '') : script.code.execute; return builder .appendLine() .appendFunction(name, scriptCode); diff --git a/src/application/Context/State/Filter/Event/FilterChange.ts b/src/application/Context/State/Filter/Event/FilterChange.ts index e90eb2a9d..bed54dc8c 100644 --- a/src/application/Context/State/Filter/Event/FilterChange.ts +++ b/src/application/Context/State/Filter/Event/FilterChange.ts @@ -1,37 +1,37 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'; import { FilterActionType } from './FilterActionType'; -import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from './IFilterChangeDetails'; +import { + IFilterChangeDetails, IFilterChangeDetailsVisitor, + ApplyFilterAction, ClearFilterAction, +} from './IFilterChangeDetails'; export class FilterChange implements IFilterChangeDetails { - public static forApply(filter: IFilterResult) { - if (!filter) { - throw new Error('missing filter'); - } - return new FilterChange(FilterActionType.Apply, filter); + public static forApply( + filter: IFilterResult, + ): IFilterChangeDetails { + return new FilterChange({ type: FilterActionType.Apply, filter }); } - public static forClear() { - return new FilterChange(FilterActionType.Clear); + public static forClear(): IFilterChangeDetails { + return new FilterChange({ type: FilterActionType.Clear }); } - private constructor( - public readonly actionType: FilterActionType, - public readonly filter?: IFilterResult, - ) { } + private constructor(public readonly action: ApplyFilterAction | ClearFilterAction) { } public visit(visitor: IFilterChangeDetailsVisitor): void { - if (!visitor) { - throw new Error('missing visitor'); - } - switch (this.actionType) { + switch (this.action.type) { case FilterActionType.Apply: - visitor.onApply(this.filter); + if (visitor.onApply) { + visitor.onApply(this.action.filter); + } break; case FilterActionType.Clear: - visitor.onClear(); + if (visitor.onClear) { + visitor.onClear(); + } break; default: - throw new Error(`Unknown action type: ${this.actionType}`); + throw new Error(`Unknown action: ${this.action}`); } } } diff --git a/src/application/Context/State/Filter/Event/IFilterChangeDetails.ts b/src/application/Context/State/Filter/Event/IFilterChangeDetails.ts index 2bb32bbe3..1cf343e14 100644 --- a/src/application/Context/State/Filter/Event/IFilterChangeDetails.ts +++ b/src/application/Context/State/Filter/Event/IFilterChangeDetails.ts @@ -2,13 +2,22 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult' import { FilterActionType } from './FilterActionType'; export interface IFilterChangeDetails { - readonly actionType: FilterActionType; - readonly filter?: IFilterResult; - + readonly action: FilterAction; visit(visitor: IFilterChangeDetailsVisitor): void; } export interface IFilterChangeDetailsVisitor { - onClear(): void; - onApply(filter: IFilterResult): void; + readonly onClear?: () => void; + readonly onApply?: (filter: IFilterResult) => void; } + +export type ApplyFilterAction = { + readonly type: FilterActionType.Apply, + readonly filter: IFilterResult; +}; + +export type ClearFilterAction = { + readonly type: FilterActionType.Clear, +}; + +export type FilterAction = ApplyFilterAction | ClearFilterAction; diff --git a/src/application/Context/State/Selection/UserSelection.ts b/src/application/Context/State/Selection/UserSelection.ts index 1aee15f5e..a3c0a55f6 100644 --- a/src/application/Context/State/Selection/UserSelection.ts +++ b/src/application/Context/State/Selection/UserSelection.ts @@ -43,7 +43,7 @@ export class UserSelection implements IUserSelection { } public removeAllInCategory(categoryId: number): void { - const category = this.collection.findCategory(categoryId); + const category = this.collection.getCategory(categoryId); const scriptsToRemove = category.getAllScriptsRecursively() .filter((script) => this.scripts.exists(script.id)); if (!scriptsToRemove.length) { @@ -57,7 +57,7 @@ export class UserSelection implements IUserSelection { public addOrUpdateAllInCategory(categoryId: number, revert = false): void { const scriptsToAddOrUpdate = this.collection - .findCategory(categoryId) + .getCategory(categoryId) .getAllScriptsRecursively() .filter( (script) => !this.scripts.exists(script.id) @@ -74,7 +74,7 @@ export class UserSelection implements IUserSelection { } public addSelectedScript(scriptId: string, revert: boolean): void { - const script = this.collection.findScript(scriptId); + const script = this.collection.getScript(scriptId); if (!script) { throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`); } @@ -84,7 +84,7 @@ export class UserSelection implements IUserSelection { } public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void { - const script = this.collection.findScript(scriptId); + const script = this.collection.getScript(scriptId); const selectedScript = new SelectedScript(script, revert); this.scripts.addOrUpdateItem(selectedScript); this.changed.notify(this.scripts.getItems()); diff --git a/src/application/Parser/ApplicationParser.ts b/src/application/Parser/ApplicationParser.ts index e5be8c57a..1f6c24fae 100644 --- a/src/application/Parser/ApplicationParser.ts +++ b/src/application/Parser/ApplicationParser.ts @@ -32,10 +32,7 @@ const PreParsedCollections: readonly CollectionData [] = [ ]; function validateCollectionsData(collections: readonly CollectionData[]) { - if (!collections?.length) { + if (!collections.length) { throw new Error('missing collections'); } - if (collections.some((collection) => !collection)) { - throw new Error('missing collection provided'); - } } diff --git a/src/application/Parser/CategoryCollectionParser.ts b/src/application/Parser/CategoryCollectionParser.ts index 9bc86504b..691f8fab6 100644 --- a/src/application/Parser/CategoryCollectionParser.ts +++ b/src/application/Parser/CategoryCollectionParser.ts @@ -28,10 +28,7 @@ export function parseCategoryCollection( } function validate(content: CollectionData): void { - if (!content) { - throw new Error('missing content'); - } - if (!content.actions || content.actions.length <= 0) { + if (!content.actions?.length) { throw new Error('content does not define any action'); } } diff --git a/src/application/Parser/CategoryParser.ts b/src/application/Parser/CategoryParser.ts index 10f8d2efb..c424a5f06 100644 --- a/src/application/Parser/CategoryParser.ts +++ b/src/application/Parser/CategoryParser.ts @@ -1,5 +1,5 @@ import type { - CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder, + CategoryData, ScriptData, CategoryOrScriptData, } from '@/application/collections/'; import { Script } from '@/domain/Script'; import { Category } from '@/domain/Category'; @@ -16,7 +16,6 @@ export function parseCategory( context: ICategoryCollectionParseContext, factory: CategoryFactoryType = CategoryFactory, ): Category { - if (!context) { throw new Error('missing context'); } return parseCategoryRecursively({ categoryData: category, context, @@ -30,8 +29,8 @@ interface ICategoryParseContext { readonly factory: CategoryFactoryType, readonly parentCategory?: CategoryData, } -// eslint-disable-next-line consistent-return -function parseCategoryRecursively(context: ICategoryParseContext): Category { + +function parseCategoryRecursively(context: ICategoryParseContext): Category | never { ensureValidCategory(context.categoryData, context.parentCategory); const children: ICategoryChildren = { subCategories: new Array(), @@ -55,7 +54,7 @@ function parseCategoryRecursively(context: ICategoryParseContext): Category { /* scripts: */ children.subScripts, ); } catch (err) { - new NodeValidator({ + return new NodeValidator({ type: NodeType.Category, selfNode: context.categoryData, parentNode: context.parentCategory, @@ -72,7 +71,7 @@ function ensureValidCategory(category: CategoryData, parentCategory?: CategoryDa .assertDefined(category) .assertValidName(category.category) .assert( - () => category.children && category.children.length > 0, + () => category.children.length > 0, `"${category.category}" has no children.`, ); } @@ -94,14 +93,14 @@ function parseNode(context: INodeParseContext) { validator.assertDefined(context.nodeData); if (isCategory(context.nodeData)) { const subCategory = parseCategoryRecursively({ - categoryData: context.nodeData as CategoryData, + categoryData: context.nodeData, context: context.context, factory: context.factory, parentCategory: context.parent, }); context.children.subCategories.push(subCategory); } else if (isScript(context.nodeData)) { - const script = parseScript(context.nodeData as ScriptData, context.context); + const script = parseScript(context.nodeData, context.context); context.children.subScripts.push(script); } else { validator.throw('Node is neither a category or a script.'); @@ -109,19 +108,18 @@ function parseNode(context: INodeParseContext) { } function isScript(data: CategoryOrScriptData): data is ScriptData { - const holder = (data as InstructionHolder); - return hasCode(holder) || hasCall(holder); + return hasCode(data) || hasCall(data); } function isCategory(data: CategoryOrScriptData): data is CategoryData { return hasProperty(data, 'category'); } -function hasCode(data: InstructionHolder): boolean { +function hasCode(data: unknown): boolean { return hasProperty(data, 'code'); } -function hasCall(data: InstructionHolder) { +function hasCall(data: unknown) { return hasProperty(data, 'call'); } diff --git a/src/application/Parser/DocumentationParser.ts b/src/application/Parser/DocumentationParser.ts index 0639dfad1..9d1b0ca3e 100644 --- a/src/application/Parser/DocumentationParser.ts +++ b/src/application/Parser/DocumentationParser.ts @@ -1,9 +1,6 @@ import type { DocumentableData, DocumentationData } from '@/application/collections/'; export function parseDocs(documentable: DocumentableData): readonly string[] { - if (!documentable) { - throw new Error('missing documentable'); - } const { docs } = documentable; if (!docs) { return []; diff --git a/src/application/Parser/NodeValidation/NodeValidator.ts b/src/application/Parser/NodeValidation/NodeValidator.ts index 0fce365c2..4c154dbd4 100644 --- a/src/application/Parser/NodeValidation/NodeValidator.ts +++ b/src/application/Parser/NodeValidation/NodeValidator.ts @@ -32,7 +32,7 @@ export class NodeValidator { return this; } - public throw(errorMessage: string) { + public throw(errorMessage: string): never { throw new NodeDataError(errorMessage, this.context); } } diff --git a/src/application/Parser/Script/CategoryCollectionParseContext.ts b/src/application/Parser/Script/CategoryCollectionParseContext.ts index fc02301f6..68245e561 100644 --- a/src/application/Parser/Script/CategoryCollectionParseContext.ts +++ b/src/application/Parser/Script/CategoryCollectionParseContext.ts @@ -17,8 +17,7 @@ export class CategoryCollectionParseContext implements ICategoryCollectionParseC scripting: IScriptingDefinition, syntaxFactory: ISyntaxFactory = new SyntaxFactory(), ) { - if (!scripting) { throw new Error('missing scripting'); } this.syntax = syntaxFactory.create(scripting.language); - this.compiler = new ScriptCompiler(functionsData, this.syntax); + this.compiler = new ScriptCompiler(functionsData ?? [], this.syntax); } } diff --git a/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts b/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts index f44fa287c..279f398dc 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts @@ -15,19 +15,10 @@ export class Expression implements IExpression { public readonly evaluator: ExpressionEvaluator, parameters?: IReadOnlyFunctionParameterCollection, ) { - if (!position) { - throw new Error('missing position'); - } - if (!evaluator) { - throw new Error('missing evaluator'); - } this.parameters = parameters ?? new FunctionParameterCollection(); } public evaluate(context: IExpressionEvaluationContext): string { - if (!context) { - throw new Error('missing context'); - } validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args); const args = filterUnusedArguments(this.parameters, context.args); const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler); diff --git a/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts b/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts index 743623204..4685a49da 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts @@ -12,8 +12,5 @@ export class ExpressionEvaluationContext implements IExpressionEvaluationContext public readonly args: IReadOnlyFunctionCallArgumentCollection, public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(), ) { - if (!args) { - throw new Error('missing args, send empty collection instead.'); - } } } diff --git a/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory.ts b/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory.ts new file mode 100644 index 000000000..10eb73ae6 --- /dev/null +++ b/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPositionFactory.ts @@ -0,0 +1,16 @@ +import { ExpressionPosition } from './ExpressionPosition'; + +export function createPositionFromRegexFullMatch( + match: RegExpMatchArray, +): ExpressionPosition { + const startPos = match.index; + if (startPos === undefined) { + throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`); + } + const fullMatch = match[0]; + if (!fullMatch.length) { + throw new Error(`Regex match is empty: ${JSON.stringify(match)}`); + } + const endPos = startPos + fullMatch.length; + return new ExpressionPosition(startPos, endPos); +} diff --git a/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts b/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts index ec81a581d..bcb130245 100644 --- a/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts +++ b/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts @@ -11,14 +11,11 @@ export class ExpressionsCompiler implements IExpressionsCompiler { ) { } public compileExpressions( - code: string | undefined, + code: string, args: IReadOnlyFunctionCallArgumentCollection, ): string { - if (!args) { - throw new Error('missing args, send empty collection instead.'); - } if (!code) { - return code; + return ''; } const context = new ExpressionEvaluationContext(args); const compiledCode = compileRecursively(code, context, this.extractor); diff --git a/src/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler.ts b/src/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler.ts index 87b4671d0..0c4e82a3c 100644 --- a/src/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler.ts +++ b/src/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler.ts @@ -2,6 +2,7 @@ import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argume export interface IExpressionsCompiler { compileExpressions( - code: string | undefined, - args: IReadOnlyFunctionCallArgumentCollection): string; + code: string, + args: IReadOnlyFunctionCallArgumentCollection, + ): string; } diff --git a/src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts b/src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts index aa75c4804..d564f33df 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts @@ -10,12 +10,9 @@ const Parsers = [ export class CompositeExpressionParser implements IExpressionParser { public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) { - if (!leafs) { + if (!leafs.length) { throw new Error('missing leafs'); } - if (leafs.some((leaf) => !leaf)) { - throw new Error('missing leaf'); - } } public findExpressions(code: string): IExpression[] { diff --git a/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts b/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts index 7bc53d0c5..8745c44c6 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts @@ -1,9 +1,9 @@ import { IExpressionParser } from '../IExpressionParser'; -import { ExpressionPosition } from '../../Expression/ExpressionPosition'; import { IExpression } from '../../Expression/IExpression'; import { Expression, ExpressionEvaluator } from '../../Expression/Expression'; import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter'; import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection'; +import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory'; export abstract class RegexParser implements IExpressionParser { protected abstract readonly regex: RegExp; @@ -21,7 +21,7 @@ export abstract class RegexParser implements IExpressionParser { const matches = code.matchAll(this.regex); for (const match of matches) { const primitiveExpression = this.buildExpression(match); - const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code); + const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code); const parameters = createParameters(primitiveExpression); const expression = new Expression(position, primitiveExpression.evaluator, parameters); yield expression; @@ -37,12 +37,6 @@ export abstract class RegexParser implements IExpressionParser { } } -function createPosition(match: RegExpMatchArray): ExpressionPosition { - const startPos = match.index; - const endPos = startPos + match[0].length; - return new ExpressionPosition(startPos, endPos); -} - function createParameters( expression: IPrimitiveExpression, ): FunctionParameterCollection { diff --git a/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeFactory.ts b/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeFactory.ts index 5b47a9dbb..a96e8cffe 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeFactory.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Pipes/PipeFactory.ts @@ -15,12 +15,6 @@ export class PipeFactory implements IPipeFactory { private readonly pipes = new Map(); constructor(pipes: readonly IPipe[] = RegisteredPipes) { - if (!pipes) { - throw new Error('missing pipes'); - } - if (pipes.some((pipe) => !pipe)) { - throw new Error('missing pipe in list'); - } for (const pipe of pipes) { this.registerPipe(pipe); } @@ -28,10 +22,11 @@ export class PipeFactory implements IPipeFactory { public get(pipeName: string): IPipe { validatePipeName(pipeName); - if (!this.pipes.has(pipeName)) { + const pipe = this.pipes.get(pipeName); + if (!pipe) { throw new Error(`Unknown pipe: "${pipeName}"`); } - return this.pipes.get(pipeName); + return pipe; } private registerPipe(pipe: IPipe): void { diff --git a/src/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts b/src/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts index 4c165828f..e47151b99 100644 --- a/src/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts +++ b/src/application/Parser/Script/Compiler/Expressions/SyntaxParsers/WithParser.ts @@ -5,6 +5,7 @@ import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function import { IExpression } from '../Expression/IExpression'; import { ExpressionPosition } from '../Expression/ExpressionPosition'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; +import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory'; export class WithParser implements IExpressionParser { public findExpressions(code: string): IExpression[] { @@ -42,31 +43,25 @@ function parseAllWithExpressions( expressions.push({ type: WithStatementType.Start, parameterName: match[1], - position: createPosition(match), + position: createPositionFromRegexFullMatch(match), }); } for (const match of input.matchAll(WithStatementEndRegEx)) { expressions.push({ type: WithStatementType.End, - position: createPosition(match), + position: createPositionFromRegexFullMatch(match), }); } for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) { expressions.push({ type: WithStatementType.ContextVariable, - position: createPosition(match), + position: createPositionFromRegexFullMatch(match), pipeline: match[1], }); } return expressions; } -function createPosition(match: RegExpMatchArray): ExpressionPosition { - const startPos = match.index; - const endPos = startPos + match[0].length; - return new ExpressionPosition(startPos, endPos); -} - class WithStatementBuilder { private readonly contextVariables = new Array<{ readonly positionInScope: ExpressionPosition; @@ -125,7 +120,7 @@ class WithStatementBuilder { private substituteContextVariables( scope: string, - substituter: (pipeline: string) => string, + substituter: (pipeline?: string) => string, ): string { if (!this.contextVariables.length) { return scope; @@ -157,7 +152,7 @@ function parseWithExpressions(input: string): IExpression[] { .sort((a, b) => b.position.start - a.position.start); const expressions = new Array(); const builders = new Array(); - const throwWithContext = (message: string) => { + const throwWithContext = (message: string): never => { throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`); }; while (sortedStatements.length > 0) { @@ -178,12 +173,15 @@ function parseWithExpressions(input: string): IExpression[] { } builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline); break; - case WithStatementType.End: - if (builders.length === 0) { + case WithStatementType.End: { + const builder = builders.pop(); + if (!builder) { throwWithContext('Redundant `end` statement, missing `with`?'); + break; } - expressions.push(builders.pop().buildExpression(statement.position, input)); + expressions.push(builder.buildExpression(statement.position, input)); break; + } } } if (builders.length > 0) { diff --git a/src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts b/src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts index 9775b6b56..47e4fd108 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts @@ -5,9 +5,6 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl private readonly arguments = new Map(); public addArgument(argument: IFunctionCallArgument): void { - if (!argument) { - throw new Error('missing argument'); - } if (this.hasArgument(argument.parameterName)) { throw new Error(`argument value for parameter ${argument.parameterName} is already provided`); } diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts index fab561eae..5f0949785 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/CodeSegmentJoin/NewlineCodeSegmentMerger.ts @@ -3,18 +3,22 @@ import { CodeSegmentMerger } from './CodeSegmentMerger'; export class NewlineCodeSegmentMerger implements CodeSegmentMerger { public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode { - if (!codeSegments?.length) { + if (!codeSegments.length) { throw new Error('missing segments'); } return { code: joinCodeParts(codeSegments.map((f) => f.code)), - revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)), + revertCode: joinCodeParts( + codeSegments + .map((f) => f.revertCode) + .filter((code): code is string => Boolean(code)), + ), }; } } function joinCodeParts(codeSegments: readonly string[]): string { return codeSegments - .filter((segment) => segment?.length > 0) + .filter((segment) => segment.length > 0) .join('\n'); } diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.ts index 85ea8b736..12d0a8ec3 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallSequenceCompiler.ts @@ -21,9 +21,7 @@ export class FunctionCallSequenceCompiler implements FunctionCallCompiler { calls: readonly FunctionCall[], functions: ISharedFunctionCollection, ): CompiledCode { - if (!functions) { throw new Error('missing functions'); } - if (!calls?.length) { throw new Error('missing calls'); } - if (calls.some((f) => !f)) { throw new Error('missing function call'); } + if (!calls.length) { throw new Error('missing calls'); } const context: FunctionCallCompilationContext = { allFunctions: functions, rootCallSequence: calls, diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts index 7886723d5..442833ca1 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/InlineFunctionCallCompiler.ts @@ -1,6 +1,6 @@ import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; -import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy'; @@ -12,19 +12,33 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy { } public canCompile(func: ISharedFunction): boolean { - return func.body.code !== undefined; + return func.body.type === FunctionBodyType.Code; } public compileFunction( calledFunction: ISharedFunction, callToFunction: FunctionCall, ): CompiledCode[] { + if (calledFunction.body.type !== FunctionBodyType.Code) { + throw new Error([ + 'Unexpected function body type.', + `\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`, + `\tActual: "${FunctionBodyType[calledFunction.body.type]}"`, + 'Function:', + `\t${JSON.stringify(callToFunction)}`, + ].join('\n')); + } const { code } = calledFunction.body; const { args } = callToFunction; return [ { code: this.expressionsCompiler.compileExpressions(code.execute, args), - revertCode: this.expressionsCompiler.compileExpressions(code.revert, args), + revertCode: (() => { + if (!code.revert) { + return undefined; + } + return this.expressionsCompiler.compileExpressions(code.revert, args); + })(), }, ]; } diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts index baaea0038..c10422357 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts @@ -1,4 +1,4 @@ -import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { CallFunctionBody, FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; @@ -13,7 +13,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy { } public canCompile(func: ISharedFunction): boolean { - return func.body.calls !== undefined; + return func.body.type === FunctionBodyType.Calls; } public compileFunction( @@ -21,7 +21,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy { callToFunction: FunctionCall, context: FunctionCallCompilationContext, ): CompiledCode[] { - const nestedCalls = calledFunction.body.calls; + const nestedCalls = (calledFunction.body as CallFunctionBody).calls; return nestedCalls.map((nestedCall) => { try { const compiledParentCall = this.argumentCompiler diff --git a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts index bb1f696a4..59c8382ea 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts @@ -5,9 +5,6 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument'; import { ParsedFunctionCall } from './ParsedFunctionCall'; export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] { - if (calls === undefined) { - throw new Error('missing call data'); - } const sequence = getCallSequence(calls); return sequence.map((call) => parseFunctionCall(call)); } @@ -19,22 +16,21 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] { if (calls instanceof Array) { return calls as FunctionCallData[]; } - return [calls as FunctionCallData]; + const singleCall = calls; + return [singleCall]; } function parseFunctionCall(call: FunctionCallData): FunctionCall { - if (!call) { - throw new Error('missing call data'); - } const callArgs = parseArgs(call.parameters); return new ParsedFunctionCall(call.function, callArgs); } function parseArgs( - parameters: FunctionCallParametersData, + parameters: FunctionCallParametersData | undefined, ): FunctionCallArgumentCollection { - return Object.keys(parameters || {}) - .map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName])) + const parametersMap = parameters ?? {}; + return Object.keys(parametersMap) + .map((parameterName) => new FunctionCallArgument(parameterName, parametersMap[parameterName])) .reduce((args, arg) => { args.addArgument(arg); return args; diff --git a/src/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.ts b/src/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.ts index 71b414698..e4c7a4238 100644 --- a/src/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall.ts @@ -9,8 +9,5 @@ export class ParsedFunctionCall implements FunctionCall { if (!functionName) { throw new Error('missing function name in function call'); } - if (!args) { - throw new Error('missing args'); - } } } diff --git a/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts b/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts index de0267f53..2d9d15360 100644 --- a/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts +++ b/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts @@ -4,15 +4,21 @@ import { FunctionCall } from './Call/FunctionCall'; export interface ISharedFunction { readonly name: string; readonly parameters: IReadOnlyFunctionParameterCollection; - readonly body: ISharedFunctionBody; + readonly body: SharedFunctionBody; } -export interface ISharedFunctionBody { - readonly type: FunctionBodyType; - readonly code: IFunctionCode | undefined; - readonly calls: readonly FunctionCall[] | undefined; +export interface CallFunctionBody { + readonly type: FunctionBodyType.Calls, + readonly calls: readonly FunctionCall[], } +export interface CodeFunctionBody { + readonly type: FunctionBodyType.Code; + readonly code: IFunctionCode, +} + +export type SharedFunctionBody = CallFunctionBody | CodeFunctionBody; + export enum FunctionBodyType { Code, Calls, diff --git a/src/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts b/src/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts index f680b9e49..d740d1aff 100644 --- a/src/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts +++ b/src/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection.ts @@ -18,9 +18,6 @@ export class FunctionParameterCollection implements IFunctionParameterCollection } private ensureValidParameter(parameter: IFunctionParameter) { - if (!parameter) { - throw new Error('missing parameter'); - } if (this.includesName(parameter.name)) { throw new Error(`duplicate parameter name: "${parameter.name}"`); } diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunction.ts b/src/application/Parser/Script/Compiler/Function/SharedFunction.ts index 5108a5102..753b5ab5e 100644 --- a/src/application/Parser/Script/Compiler/Function/SharedFunction.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunction.ts @@ -1,7 +1,7 @@ import { FunctionCall } from './Call/FunctionCall'; import { - FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody, + FunctionBodyType, IFunctionCode, ISharedFunction, SharedFunctionBody, } from './ISharedFunction'; import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; @@ -10,7 +10,7 @@ export function createCallerFunction( parameters: IReadOnlyFunctionParameterCollection, callSequence: readonly FunctionCall[], ): ISharedFunction { - if (!callSequence || !callSequence.length) { + if (!callSequence.length) { throw new Error(`missing call sequence in function "${name}"`); } return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls); @@ -33,7 +33,7 @@ export function createFunctionWithInlineCode( } class SharedFunction implements ISharedFunction { - public readonly body: ISharedFunctionBody; + public readonly body: SharedFunctionBody; constructor( public readonly name: string, @@ -42,11 +42,22 @@ class SharedFunction implements ISharedFunction { bodyType: FunctionBodyType, ) { if (!name) { throw new Error('missing function name'); } - if (!parameters) { throw new Error('missing parameters'); } - this.body = { - type: bodyType, - code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined, - calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined, - }; + + switch (bodyType) { + case FunctionBodyType.Code: + this.body = { + type: FunctionBodyType.Code, + code: content as IFunctionCode, + }; + break; + case FunctionBodyType.Calls: + this.body = { + type: FunctionBodyType.Calls, + calls: content as readonly FunctionCall[], + }; + break; + default: + throw new Error(`unknown body type: ${FunctionBodyType[bodyType]}`); + } } } diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts b/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts index 9cba7948d..653e796ed 100644 --- a/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts @@ -5,7 +5,6 @@ export class SharedFunctionCollection implements ISharedFunctionCollection { private readonly functionsByName = new Map(); public addFunction(func: ISharedFunction): void { - if (!func) { throw new Error('missing function'); } if (this.has(func.name)) { throw new Error(`function with name ${func.name} already exists`); } diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts b/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts index aa3f482a0..21c1f9edd 100644 --- a/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts @@ -1,4 +1,6 @@ -import type { FunctionData, InstructionHolder } from '@/application/collections/'; +import type { + FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction, +} from '@/application/collections/'; import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator'; import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines'; @@ -23,9 +25,8 @@ export class SharedFunctionsParser implements ISharedFunctionsParser { functions: readonly FunctionData[], syntax: ILanguageSyntax, ): ISharedFunctionCollection { - if (!syntax) { throw new Error('missing syntax'); } const collection = new SharedFunctionCollection(); - if (!functions || !functions.length) { + if (!functions.length) { return collection; } ensureValidFunctions(functions); @@ -55,16 +56,18 @@ function parseFunction( } function validateCode( - data: FunctionData, + data: CodeFunctionData, syntax: ILanguageSyntax, validator: ICodeValidator, ): void { - [data.code, data.revertCode].forEach( - (code) => validator.throwIfInvalid( - code, - [new NoEmptyLines(), new NoDuplicatedLines(syntax)], - ), - ); + [data.code, data.revertCode] + .filter((code): code is string => Boolean(code)) + .forEach( + (code) => validator.throwIfInvalid( + code, + [new NoEmptyLines(), new NoDuplicatedLines(syntax)], + ), + ); } function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection { @@ -85,19 +88,18 @@ function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollecti }, new FunctionParameterCollection()); } -function hasCode(data: FunctionData): boolean { - return Boolean(data.code); +function hasCode(data: FunctionData): data is CodeFunctionData { + return (data as CodeInstruction).code !== undefined; } -function hasCall(data: FunctionData): boolean { - return Boolean(data.call); +function hasCall(data: FunctionData): data is CallFunctionData { + return (data as CallInstruction).call !== undefined; } function ensureValidFunctions(functions: readonly FunctionData[]) { - ensureNoUndefinedItem(functions); ensureNoDuplicatesInFunctionNames(functions); - ensureNoDuplicateCode(functions); ensureEitherCallOrCodeIsDefined(functions); + ensureNoDuplicateCode(functions); ensureExpectedParametersType(functions); } @@ -105,7 +107,7 @@ function printList(list: readonly string[]): string { return `"${list.join('","')}"`; } -function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) { +function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) { // Ensure functions do not define both call and code const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder)); if (withBothCallAndCode.length) { @@ -132,7 +134,7 @@ function isArrayOfObjects(value: unknown): boolean { && value.every((item) => typeof item === 'object'); } -function printNames(holders: readonly InstructionHolder[]) { +function printNames(holders: readonly FunctionData[]) { return printList(holders.map((holder) => holder.name)); } @@ -144,22 +146,19 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) { } } -function ensureNoUndefinedItem(functions: readonly FunctionData[]) { - if (functions.some((func) => !func)) { - throw new Error('some functions are undefined'); - } -} - function ensureNoDuplicateCode(functions: readonly FunctionData[]) { - const duplicateCodes = getDuplicates(functions + const callFunctions = functions + .filter((func) => hasCode(func)) + .map((func) => func as CodeFunctionData); + const duplicateCodes = getDuplicates(callFunctions .map((func) => func.code) .filter((code) => code)); if (duplicateCodes.length > 0) { throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`); } - const duplicateRevertCodes = getDuplicates(functions + const duplicateRevertCodes = getDuplicates(callFunctions .filter((func) => func.revertCode) - .map((func) => func.revertCode)); + .map((func) => func.revertCode as string)); if (duplicateRevertCodes.length > 0) { throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`); } diff --git a/src/application/Parser/Script/Compiler/ScriptCompiler.ts b/src/application/Parser/Script/Compiler/ScriptCompiler.ts index 38c07a9c8..e333a72b9 100644 --- a/src/application/Parser/Script/Compiler/ScriptCompiler.ts +++ b/src/application/Parser/Script/Compiler/ScriptCompiler.ts @@ -1,4 +1,4 @@ -import type { FunctionData, ScriptData } from '@/application/collections/'; +import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/'; import { IScriptCode } from '@/domain/IScriptCode'; import { ScriptCode } from '@/domain/ScriptCode'; import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; @@ -18,27 +18,24 @@ export class ScriptCompiler implements IScriptCompiler { private readonly functions: ISharedFunctionCollection; constructor( - functions: readonly FunctionData[] | undefined, + functions: readonly FunctionData[], syntax: ILanguageSyntax, sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance, private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance, private readonly codeValidator: ICodeValidator = CodeValidator.instance, ) { - if (!syntax) { throw new Error('missing syntax'); } this.functions = sharedFunctionsParser.parseFunctions(functions, syntax); } public canCompile(script: ScriptData): boolean { - if (!script) { throw new Error('missing script'); } - if (!script.call) { - return false; - } - return true; + return hasCall(script); } public compile(script: ScriptData): IScriptCode { - if (!script) { throw new Error('missing script'); } try { + if (!hasCall(script)) { + throw new Error('Script does include any calls.'); + } const calls = parseFunctionCalls(script.call); const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions); validateCompiledCode(compiledCode, this.codeValidator); @@ -53,7 +50,17 @@ export class ScriptCompiler implements IScriptCompiler { } function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void { - [compiledCode.code, compiledCode.revertCode].forEach( - (code) => validator.throwIfInvalid(code, [new NoEmptyLines()]), - ); + [compiledCode.code, compiledCode.revertCode] + .filter((code): code is string => Boolean(code)) + .map((code) => code as string) + .forEach( + (code) => validator.throwIfInvalid( + code, + [new NoEmptyLines()], + ), + ); +} + +function hasCall(data: ScriptData): data is ScriptData & CallInstruction { + return (data as CallInstruction).call !== undefined; } diff --git a/src/application/Parser/Script/ScriptParser.ts b/src/application/Parser/Script/ScriptParser.ts index 5e8946025..adb3d20f0 100644 --- a/src/application/Parser/Script/ScriptParser.ts +++ b/src/application/Parser/Script/ScriptParser.ts @@ -1,4 +1,4 @@ -import type { ScriptData } from '@/application/collections/'; +import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/'; import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines'; import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import { Script } from '@/domain/Script'; @@ -14,7 +14,6 @@ import { ICategoryCollectionParseContext } from './ICategoryCollectionParseConte import { CodeValidator } from './Validation/CodeValidator'; import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines'; -// eslint-disable-next-line consistent-return export function parseScript( data: ScriptData, context: ICategoryCollectionParseContext, @@ -24,7 +23,6 @@ export function parseScript( ): Script { const validator = new NodeValidator({ type: NodeType.Script, selfNode: data }); validateScript(data, validator); - if (!context) { throw new Error('missing context'); } try { const script = scriptFactory( /* name: */ data.name, @@ -34,12 +32,12 @@ export function parseScript( ); return script; } catch (err) { - validator.throw(err.message); + return validator.throw(err.message); } } function parseLevel( - level: string, + level: string | undefined, parser: IEnumParser, ): RecommendationLevel | undefined { if (!level) { @@ -56,39 +54,45 @@ function parseCode( if (context.compiler.canCompile(script)) { return context.compiler.compile(script); } - const code = new ScriptCode(script.code, script.revertCode); + const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled + const code = new ScriptCode(codeScript.code, codeScript.revertCode); validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax); return code; } function validateHardcodedCodeWithoutCalls( scriptCode: ScriptCode, - codeValidator: ICodeValidator, + validator: ICodeValidator, syntax: ILanguageSyntax, ) { - [scriptCode.execute, scriptCode.revert].forEach( - (code) => codeValidator.throwIfInvalid( - code, - [new NoEmptyLines(), new NoDuplicatedLines(syntax)], - ), - ); + [scriptCode.execute, scriptCode.revert] + .filter((code): code is string => Boolean(code)) + .forEach( + (code) => validator.throwIfInvalid( + code, + [new NoEmptyLines(), new NoDuplicatedLines(syntax)], + ), + ); } -function validateScript(script: ScriptData, validator: NodeValidator) { +function validateScript( + script: ScriptData, + validator: NodeValidator, +): asserts script is NonNullable { validator .assertDefined(script) .assertValidName(script.name) .assert( - () => Boolean(script.code || script.call), - 'Must define either "call" or "code".', + () => Boolean((script as CodeScriptData).code || (script as CallScriptData).call), + 'Neither "call" or "code" is defined.', ) .assert( - () => !(script.code && script.call), - 'Cannot define both "call" and "code".', + () => !((script as CodeScriptData).code && (script as CallScriptData).call), + 'Both "call" and "code" are defined.', ) .assert( - () => !(script.revertCode && script.call), - 'Cannot define "revertCode" if "call" is defined.', + () => !((script as CodeScriptData).revertCode && (script as CallScriptData).call), + 'Both "call" and "revertCode" are defined.', ); } diff --git a/src/application/Parser/Script/Validation/CodeValidator.ts b/src/application/Parser/Script/Validation/CodeValidator.ts index 130a93e7c..42f3e22d8 100644 --- a/src/application/Parser/Script/Validation/CodeValidator.ts +++ b/src/application/Parser/Script/Validation/CodeValidator.ts @@ -9,7 +9,7 @@ export class CodeValidator implements ICodeValidator { code: string, rules: readonly ICodeValidationRule[], ): void { - if (!rules || rules.length === 0) { throw new Error('missing rules'); } + if (rules.length === 0) { throw new Error('missing rules'); } if (!code) { return; } diff --git a/src/application/Parser/Script/Validation/Rules/NoDuplicatedLines.ts b/src/application/Parser/Script/Validation/Rules/NoDuplicatedLines.ts index ebea7dc6d..0d0ce03d3 100644 --- a/src/application/Parser/Script/Validation/Rules/NoDuplicatedLines.ts +++ b/src/application/Parser/Script/Validation/Rules/NoDuplicatedLines.ts @@ -3,9 +3,7 @@ import { ICodeLine } from '../ICodeLine'; import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule'; export class NoDuplicatedLines implements ICodeValidationRule { - constructor(private readonly syntax: ILanguageSyntax) { - if (!syntax) { throw new Error('missing syntax'); } - } + constructor(private readonly syntax: ILanguageSyntax) { } public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] { return lines diff --git a/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts b/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts index 00af3ee6f..fefadd222 100644 --- a/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts +++ b/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts @@ -17,7 +17,6 @@ export class CodeSubstituter implements ICodeSubstituter { public substitute(code: string, info: IProjectInformation): string { if (!code) { throw new Error('missing code'); } - if (!info) { throw new Error('missing info'); } const args = new FunctionCallArgumentCollection(); const substitute = (name: string, value: string) => args .addArgument(new FunctionCallArgument(name, value)); diff --git a/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts b/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts index 6fa20e908..2c25d6dbb 100644 --- a/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts +++ b/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts @@ -18,8 +18,6 @@ export class ScriptingDefinitionParser { definition: ScriptingDefinitionData, info: IProjectInformation, ): IScriptingDefinition { - if (!info) { throw new Error('missing info'); } - if (!definition) { throw new Error('missing definition'); } const language = this.languageParser.parseEnum(definition.language, 'language'); const startCode = this.codeSubstituter.substitute(definition.startCode, info); const endCode = this.codeSubstituter.substitute(definition.endCode, info); diff --git a/src/application/collections/collection.yaml.d.ts b/src/application/collections/collection.yaml.d.ts index 6e7f0bce6..b429420bb 100644 --- a/src/application/collections/collection.yaml.d.ts +++ b/src/application/collections/collection.yaml.d.ts @@ -12,29 +12,38 @@ declare module '@/application/collections/*' { } export type CategoryOrScriptData = CategoryData | ScriptData; - export type DocumentationData = ReadonlyArray | string; + export type DocumentationData = ReadonlyArray | string | undefined; export interface DocumentableData { readonly docs?: DocumentationData; } - export interface InstructionHolder { - readonly name: string; - - readonly code?: string; + export interface CodeInstruction { + readonly code: string; readonly revertCode?: string; + } - readonly call?: FunctionCallsData; + export interface CallInstruction { + readonly call: FunctionCallsData; } + export type InstructionHolder = CodeInstruction | CallInstruction; + export interface ParameterDefinitionData { readonly name: string; readonly optional?: boolean; } - export interface FunctionData extends InstructionHolder { + export type FunctionDefinition = { + readonly name: string; readonly parameters?: readonly ParameterDefinitionData[]; - } + }; + + export type CodeFunctionData = FunctionDefinition & CodeInstruction; + + export type CallFunctionData = FunctionDefinition & CallInstruction; + + export type FunctionData = CodeFunctionData | CallFunctionData; export interface FunctionCallParametersData { readonly [index: string]: string; @@ -47,10 +56,16 @@ declare module '@/application/collections/*' { export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined; - export interface ScriptData extends InstructionHolder, DocumentableData { + export type ScriptDefinition = DocumentableData & { readonly name: string; readonly recommend?: string; - } + }; + + export type CodeScriptData = ScriptDefinition & CodeInstruction; + + export type CallScriptData = ScriptDefinition & CallInstruction; + + export type ScriptData = CodeScriptData | CallScriptData; export interface ScriptingDefinitionData { readonly language: string; diff --git a/src/domain/Application.ts b/src/domain/Application.ts index 633db8720..afbb710cd 100644 --- a/src/domain/Application.ts +++ b/src/domain/Application.ts @@ -8,7 +8,6 @@ export class Application implements IApplication { public info: IProjectInformation, public collections: readonly ICategoryCollection[], ) { - validateInformation(info); validateCollections(collections); } @@ -16,19 +15,17 @@ export class Application implements IApplication { return this.collections.map((collection) => collection.os); } - public getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined { - return this.collections.find((collection) => collection.os === operatingSystem); - } -} - -function validateInformation(info: IProjectInformation) { - if (!info) { - throw new Error('missing project information'); + public getCollection(operatingSystem: OperatingSystem): ICategoryCollection { + const collection = this.collections.find((c) => c.os === operatingSystem); + if (!collection) { + throw new Error(`Operating system "${OperatingSystem[operatingSystem]}" is not defined in application`); + } + return collection; } } function validateCollections(collections: readonly ICategoryCollection[]) { - if (!collections || !collections.length) { + if (!collections.length) { throw new Error('missing collections'); } if (collections.filter((c) => !c).length > 0) { diff --git a/src/domain/Category.ts b/src/domain/Category.ts index 9e9e35e62..2ce6c9d7b 100644 --- a/src/domain/Category.ts +++ b/src/domain/Category.ts @@ -3,14 +3,14 @@ import { IScript } from './IScript'; import { ICategory } from './ICategory'; export class Category extends BaseEntity implements ICategory { - private allSubScripts: ReadonlyArray = undefined; + private allSubScripts?: ReadonlyArray = undefined; constructor( id: number, public readonly name: string, public readonly docs: ReadonlyArray, - public readonly subCategories?: ReadonlyArray, - public readonly scripts?: ReadonlyArray, + public readonly subCategories: ReadonlyArray, + public readonly scripts: ReadonlyArray, ) { super(id); validateCategory(this); @@ -39,10 +39,7 @@ function validateCategory(category: ICategory) { if (!category.name) { throw new Error('missing name'); } - if ( - (!category.subCategories || category.subCategories.length === 0) - && (!category.scripts || category.scripts.length === 0) - ) { + if (category.subCategories.length === 0 && category.scripts.length === 0) { throw new Error('A category must have at least one sub-category or script'); } } diff --git a/src/domain/CategoryCollection.ts b/src/domain/CategoryCollection.ts index 0a627dd23..245d803ec 100644 --- a/src/domain/CategoryCollection.ts +++ b/src/domain/CategoryCollection.ts @@ -19,9 +19,6 @@ export class CategoryCollection implements ICategoryCollection { public readonly actions: ReadonlyArray, public readonly scripting: IScriptingDefinition, ) { - if (!scripting) { - throw new Error('missing scripting definition'); - } this.queryable = makeQueryable(actions); assertInRange(os, OperatingSystem); ensureValid(this.queryable); @@ -29,17 +26,26 @@ export class CategoryCollection implements ICategoryCollection { ensureNoDuplicates(this.queryable.allScripts); } - public findCategory(categoryId: number): ICategory | undefined { - return this.queryable.allCategories.find((category) => category.id === categoryId); + public getCategory(categoryId: number): ICategory { + const category = this.queryable.allCategories.find((c) => c.id === categoryId); + if (!category) { + throw new Error(`Missing category with ID: "${categoryId}"`); + } + return category; } public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] { assertInRange(level, RecommendationLevel); - return this.queryable.scriptsByLevel.get(level); + const scripts = this.queryable.scriptsByLevel.get(level); + return scripts ?? []; } - public findScript(scriptId: string): IScript | undefined { - return this.queryable.allScripts.find((script) => script.id === scriptId); + public getScript(scriptId: string): IScript { + const script = this.queryable.allScripts.find((s) => s.id === scriptId); + if (!script) { + throw new Error(`missing script: ${scriptId}`); + } + return script; } public getAllScripts(): IScript[] { diff --git a/src/domain/IApplication.ts b/src/domain/IApplication.ts index 61bc3de20..4ff92fae1 100644 --- a/src/domain/IApplication.ts +++ b/src/domain/IApplication.ts @@ -7,5 +7,5 @@ export interface IApplication { readonly collections: readonly ICategoryCollection[]; getSupportedOsList(): OperatingSystem[]; - getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined; + getCollection(operatingSystem: OperatingSystem): ICategoryCollection; } diff --git a/src/domain/ICategory.ts b/src/domain/ICategory.ts index 8f44a424d..2413cd09e 100644 --- a/src/domain/ICategory.ts +++ b/src/domain/ICategory.ts @@ -5,8 +5,8 @@ import { IDocumentable } from './IDocumentable'; export interface ICategory extends IEntity, IDocumentable { readonly id: number; readonly name: string; - readonly subCategories?: ReadonlyArray; - readonly scripts?: ReadonlyArray; + readonly subCategories: ReadonlyArray; + readonly scripts: ReadonlyArray; includes(script: IScript): boolean; getAllScriptsRecursively(): ReadonlyArray; } diff --git a/src/domain/ICategoryCollection.ts b/src/domain/ICategoryCollection.ts index 95e6e4f02..913d8b00a 100644 --- a/src/domain/ICategoryCollection.ts +++ b/src/domain/ICategoryCollection.ts @@ -12,8 +12,8 @@ export interface ICategoryCollection { readonly actions: ReadonlyArray; getScriptsByLevel(level: RecommendationLevel): ReadonlyArray; - findCategory(categoryId: number): ICategory | undefined; - findScript(scriptId: string): IScript | undefined; + getCategory(categoryId: number): ICategory; + getScript(scriptId: string): IScript; getAllScripts(): ReadonlyArray; getAllCategories(): ReadonlyArray; } diff --git a/src/domain/IScriptCode.ts b/src/domain/IScriptCode.ts index 06af81655..c2c8900c5 100644 --- a/src/domain/IScriptCode.ts +++ b/src/domain/IScriptCode.ts @@ -1,4 +1,4 @@ export interface IScriptCode { readonly execute: string; - readonly revert: string; + readonly revert?: string; } diff --git a/src/domain/Script.ts b/src/domain/Script.ts index 6f8d5ebc9..a852effcd 100644 --- a/src/domain/Script.ts +++ b/src/domain/Script.ts @@ -11,9 +11,6 @@ export class Script extends BaseEntity implements IScript { public readonly level?: RecommendationLevel, ) { super(name); - if (!code) { - throw new Error('missing code'); - } validateLevel(level); } diff --git a/src/domain/ScriptCode.ts b/src/domain/ScriptCode.ts index 89791ac8f..51eb56bd6 100644 --- a/src/domain/ScriptCode.ts +++ b/src/domain/ScriptCode.ts @@ -3,14 +3,14 @@ import { IScriptCode } from './IScriptCode'; export class ScriptCode implements IScriptCode { constructor( public readonly execute: string, - public readonly revert: string, + public readonly revert: string | undefined, ) { validateCode(execute); validateRevertCode(revert, execute); } } -function validateRevertCode(revertCode: string, execute: string) { +function validateRevertCode(revertCode: string | undefined, execute: string) { if (!revertCode) { return; } @@ -25,7 +25,7 @@ function validateRevertCode(revertCode: string, execute: string) { } function validateCode(code: string): void { - if (!code || code.length === 0) { + if (code.length === 0) { throw new Error('missing code'); } } diff --git a/src/infrastructure/CodeRunner.ts b/src/infrastructure/CodeRunner.ts index 30ae771cd..2a4371e01 100644 --- a/src/infrastructure/CodeRunner.ts +++ b/src/infrastructure/CodeRunner.ts @@ -14,6 +14,9 @@ export class CodeRunner { public async runCode(code: string, folderName: string, fileExtension: string): Promise { const { os } = this.environment; + if (os === undefined) { + throw new Error('Unidentified operating system'); + } const dir = this.system.location.combinePaths( this.system.operatingSystem.getTempDirectory(), folderName, diff --git a/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts b/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts index 70676ce48..2f01af127 100644 --- a/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts +++ b/src/infrastructure/EnvironmentVariables/EnvironmentVariablesValidator.ts @@ -2,9 +2,6 @@ import { IEnvironmentVariables } from './IEnvironmentVariables'; /* Validation is externalized to keep the environment objects simple */ export function validateEnvironmentVariables(environment: IEnvironmentVariables): void { - if (!environment) { - throw new Error('missing environment'); - } const keyValues = capturePropertyValues(environment); if (!Object.keys(keyValues).length) { throw new Error('Unable to capture key/value pairs'); @@ -30,7 +27,7 @@ function getKeysMissingValues(keyValuePairs: Record): string[] * Necessary because code transformations can make class getters non-enumerable during bundling. * This ensures that even if getters are non-enumerable, their values are still captured and used. */ -function capturePropertyValues(instance: unknown): Record { +function capturePropertyValues(instance: object): Record { const obj: Record = {}; const descriptors = Object.getOwnPropertyDescriptors(instance.constructor.prototype); diff --git a/src/infrastructure/Events/EventSubscriptionCollection.ts b/src/infrastructure/Events/EventSubscriptionCollection.ts index df06163ec..0d299f083 100644 --- a/src/infrastructure/Events/EventSubscriptionCollection.ts +++ b/src/infrastructure/Events/EventSubscriptionCollection.ts @@ -9,12 +9,9 @@ export class EventSubscriptionCollection implements IEventSubscriptionCollection } public register(subscriptions: IEventSubscription[]) { - if (!subscriptions || subscriptions.length === 0) { + if (subscriptions.length === 0) { throw new Error('missing subscriptions'); } - if (subscriptions.some((subscription) => !subscription)) { - throw new Error('missing subscription in list'); - } this.subscriptions.push(...subscriptions); } diff --git a/src/infrastructure/Log/ConsoleLogger.ts b/src/infrastructure/Log/ConsoleLogger.ts index 4a1bc6bdb..818b91734 100644 --- a/src/infrastructure/Log/ConsoleLogger.ts +++ b/src/infrastructure/Log/ConsoleLogger.ts @@ -1,13 +1,17 @@ import { ILogger } from './ILogger'; export class ConsoleLogger implements ILogger { - constructor(private readonly globalConsole: Partial = console) { - if (!globalConsole) { + constructor(private readonly consoleProxy: Partial = console) { + if (!consoleProxy) { // do not trust strict null checks for global objects throw new Error('missing console'); } } public info(...params: unknown[]): void { - this.globalConsole.info(...params); + const logFunction = this.consoleProxy?.info; + if (!logFunction) { + throw new Error('missing "info" function'); + } + logFunction.call(this.consoleProxy, ...params); } } diff --git a/src/infrastructure/Log/ElectronLogger.ts b/src/infrastructure/Log/ElectronLogger.ts index 5a1dc1aed..3c44ca802 100644 --- a/src/infrastructure/Log/ElectronLogger.ts +++ b/src/infrastructure/Log/ElectronLogger.ts @@ -7,6 +7,11 @@ export function createElectronLogger(logger: Partial): ILogger { throw new Error('missing logger'); } return { - info: (...params) => logger.info(...params), + info: (...params) => { + if (!logger.info) { + throw new Error('missing "info" function'); + } + logger.info(...params); + }, }; } diff --git a/src/infrastructure/Log/WindowInjectedLogger.ts b/src/infrastructure/Log/WindowInjectedLogger.ts index c3642dbea..0be0b8ef2 100644 --- a/src/infrastructure/Log/WindowInjectedLogger.ts +++ b/src/infrastructure/Log/WindowInjectedLogger.ts @@ -4,8 +4,8 @@ import { ILogger } from './ILogger'; export class WindowInjectedLogger implements ILogger { private readonly logger: ILogger; - constructor(windowVariables: WindowVariables = window) { - if (!windowVariables) { + constructor(windowVariables: WindowVariables | undefined | null = window) { + if (!windowVariables) { // do not trust strict null checks for global objects throw new Error('missing window'); } if (!windowVariables.log) { diff --git a/src/infrastructure/Repository/IRepository.ts b/src/infrastructure/Repository/IRepository.ts index a69280ef9..e7c14e0b2 100644 --- a/src/infrastructure/Repository/IRepository.ts +++ b/src/infrastructure/Repository/IRepository.ts @@ -3,7 +3,7 @@ import { IEntity } from '../Entity/IEntity'; export interface IRepository> { readonly length: number; getItems(predicate?: (entity: TEntity) => boolean): TEntity[]; - getById(id: TKey): TEntity | undefined; + getById(id: TKey): TEntity; addItem(item: TEntity): void; addOrUpdateItem(item: TEntity): void; removeItem(id: TKey): void; diff --git a/src/infrastructure/Repository/InMemoryRepository.ts b/src/infrastructure/Repository/InMemoryRepository.ts index 1096e9d2a..af0da4959 100644 --- a/src/infrastructure/Repository/InMemoryRepository.ts +++ b/src/infrastructure/Repository/InMemoryRepository.ts @@ -6,7 +6,7 @@ implements IRepository { private readonly items: TEntity[]; constructor(items?: TEntity[]) { - this.items = items || new Array(); + this.items = items ?? new Array(); } public get length(): number { @@ -17,18 +17,15 @@ implements IRepository { return predicate ? this.items.filter(predicate) : this.items; } - public getById(id: TKey): TEntity | undefined { + public getById(id: TKey): TEntity { const items = this.getItems((entity) => entity.id === id); if (!items.length) { - return undefined; + throw new Error(`missing item: ${id}`); } return items[0]; } public addItem(item: TEntity): void { - if (!item) { - throw new Error('missing item'); - } if (this.exists(item.id)) { throw new Error(`Cannot add (id: ${item.id}) as it is already exists`); } @@ -36,9 +33,6 @@ implements IRepository { } public addOrUpdateItem(item: TEntity): void { - if (!item) { - throw new Error('missing item'); - } if (this.exists(item.id)) { this.removeItem(item.id); } diff --git a/src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsDetector.ts b/src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsDetector.ts index 3022c3d30..af744a241 100644 --- a/src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsDetector.ts +++ b/src/infrastructure/RuntimeEnvironment/BrowserOs/BrowserOsDetector.ts @@ -6,9 +6,6 @@ export class BrowserOsDetector implements IBrowserOsDetector { private readonly detectors = BrowserDetectors; public detect(userAgent: string): OperatingSystem | undefined { - if (!userAgent) { - return undefined; - } for (const detector of this.detectors) { const os = detector.detect(userAgent); if (os !== undefined) { diff --git a/src/infrastructure/RuntimeEnvironment/BrowserOs/DetectorBuilder.ts b/src/infrastructure/RuntimeEnvironment/BrowserOs/DetectorBuilder.ts index 9e8382ca4..3d81edb3e 100644 --- a/src/infrastructure/RuntimeEnvironment/BrowserOs/DetectorBuilder.ts +++ b/src/infrastructure/RuntimeEnvironment/BrowserOs/DetectorBuilder.ts @@ -25,9 +25,9 @@ export class DetectorBuilder { }; } - private detect(userAgent: string): OperatingSystem { + private detect(userAgent: string): OperatingSystem | undefined { if (!userAgent) { - throw new Error('missing userAgent'); + return undefined; } if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) { return undefined; diff --git a/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts b/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts index fa088a155..87c4d0213 100644 --- a/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts +++ b/src/infrastructure/RuntimeEnvironment/RuntimeEnvironment.ts @@ -37,7 +37,7 @@ export class RuntimeEnvironment implements IRuntimeEnvironment { } } -function getUserAgent(window: Partial): string { +function getUserAgent(window: Partial): string | undefined { return window?.navigator?.userAgent; } diff --git a/src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts b/src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts index 46f0030a2..1372e81a7 100644 --- a/src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts +++ b/src/infrastructure/RuntimeSanity/Common/FactoryValidator.ts @@ -7,9 +7,6 @@ export abstract class FactoryValidator implements ISanityValidator { private readonly factory: FactoryFunction; protected constructor(factory: FactoryFunction) { - if (!factory) { - throw new Error('missing factory'); - } this.factory = factory; } diff --git a/src/infrastructure/RuntimeSanity/SanityChecks.ts b/src/infrastructure/RuntimeSanity/SanityChecks.ts index 36f44bf96..9019ae15e 100644 --- a/src/infrastructure/RuntimeSanity/SanityChecks.ts +++ b/src/infrastructure/RuntimeSanity/SanityChecks.ts @@ -11,7 +11,9 @@ export function validateRuntimeSanity( options: ISanityCheckOptions, validators: readonly ISanityValidator[] = DefaultSanityValidators, ): void { - validateContext(options, validators); + if (!validators.length) { + throw new Error('missing validators'); + } const errorMessages = validators.reduce((errors, validator) => { if (validator.shouldValidate(options)) { const errorMessage = getErrorMessage(validator); @@ -26,21 +28,6 @@ export function validateRuntimeSanity( } } -function validateContext( - options: ISanityCheckOptions, - validators: readonly ISanityValidator[], -) { - if (!options) { - throw new Error('missing options'); - } - if (!validators?.length) { - throw new Error('missing validators'); - } - if (validators.some((validator) => !validator)) { - throw new Error('missing validator in validators'); - } -} - function getErrorMessage(validator: ISanityValidator): string | undefined { const errorMessages = [...validator.collectErrors()]; if (!errorMessages.length) { diff --git a/src/infrastructure/SaveFileDialog.ts b/src/infrastructure/SaveFileDialog.ts index 29678df63..c00bf5999 100644 --- a/src/infrastructure/SaveFileDialog.ts +++ b/src/infrastructure/SaveFileDialog.ts @@ -6,17 +6,21 @@ export enum FileType { } export class SaveFileDialog { - public static saveFile(text: string, fileName: string, type: FileType): void { - const mimeType = this.mimeTypes.get(type); + public static saveFile( + text: string, + fileName: string, + type: FileType, + ): void { + const mimeType = this.mimeTypes[type]; this.saveBlob(text, mimeType, fileName); } - private static readonly mimeTypes = new Map([ + private static readonly mimeTypes: Record = { // Some browsers (including firefox + IE) require right mime type // otherwise they ignore extension and save the file as text. - [FileType.BatchFile, 'application/bat'], // https://en.wikipedia.org/wiki/Batch_file - [FileType.ShellScript, 'text/x-shellscript'], // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ - ]); + [FileType.BatchFile]: 'application/bat', // https://en.wikipedia.org/wiki/Batch_file + [FileType.ShellScript]: 'text/x-shellscript', // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ + }; private static saveBlob(file: BlobPart, fileType: string, fileName: string): void { try { diff --git a/src/infrastructure/SystemOperations/ISystemOperations.ts b/src/infrastructure/SystemOperations/ISystemOperations.ts index 217da5873..8915d0f50 100644 --- a/src/infrastructure/SystemOperations/ISystemOperations.ts +++ b/src/infrastructure/SystemOperations/ISystemOperations.ts @@ -19,6 +19,6 @@ export interface ICommandOps { export interface IFileSystemOps { setFilePermissions(filePath: string, mode: string | number): Promise; - createDirectory(directoryPath: string, isRecursive?: boolean): Promise; + createDirectory(directoryPath: string, isRecursive?: boolean): Promise; writeToFile(filePath: string, data: string): Promise; } diff --git a/src/infrastructure/SystemOperations/NodeSystemOperations.ts b/src/infrastructure/SystemOperations/NodeSystemOperations.ts index 3377fb3bc..6979ec610 100644 --- a/src/infrastructure/SystemOperations/NodeSystemOperations.ts +++ b/src/infrastructure/SystemOperations/NodeSystemOperations.ts @@ -17,10 +17,16 @@ export function createNodeSystemOperations(): ISystemOperations { filePath: string, mode: string | number, ) => chmod(filePath, mode), - createDirectory: ( + createDirectory: async ( directoryPath: string, isRecursive?: boolean, - ) => mkdir(directoryPath, { recursive: isRecursive }), + ) => { + await mkdir(directoryPath, { recursive: isRecursive }); + // Ignoring the return value from `mkdir`, which is the first directory created + // when `recursive` is true. The function contract is to not return any value, + // and we avoid handling this inconsistent behavior. + // See https://github.com/nodejs/node/pull/31530 + }, writeToFile: ( filePath: string, data: string, diff --git a/src/infrastructure/Threading/AsyncLazy.ts b/src/infrastructure/Threading/AsyncLazy.ts index b024169be..0912ecf28 100644 --- a/src/infrastructure/Threading/AsyncLazy.ts +++ b/src/infrastructure/Threading/AsyncLazy.ts @@ -1,13 +1,9 @@ import { EventSource } from '../Events/EventSource'; export class AsyncLazy { - private valueCreated = new EventSource(); + private valueCreated = new EventSource(); - private isValueCreated = false; - - private isCreatingValue = false; - - private value: T | undefined; + private state: ValueState = { status: ValueStatus.NotRequested }; constructor(private valueFactory: () => Promise) {} @@ -15,23 +11,44 @@ export class AsyncLazy { this.valueFactory = valueFactory; } - public async getValue(): Promise { - // If value is already created, return the value directly - if (this.isValueCreated) { - return Promise.resolve(this.value); + public getValue(): Promise { + if (this.state.status === ValueStatus.Created) { + return Promise.resolve(this.state.value); } - // If value is being created, wait until the value is created and then return it. - if (this.isCreatingValue) { - return new Promise((resolve) => { - // Return/result when valueCreated event is triggered. - this.valueCreated.on(() => resolve(this.value)); - }); + if (this.state.status === ValueStatus.BeingCreated) { + return this.state.value; } - this.isCreatingValue = true; - this.value = await this.valueFactory(); - this.isCreatingValue = false; - this.isValueCreated = true; - this.valueCreated.notify(null); - return Promise.resolve(this.value); + const valuePromise = this.valueFactory(); + this.state = { + status: ValueStatus.BeingCreated, + value: valuePromise, + }; + valuePromise.then((value) => { + this.state = { + status: ValueStatus.Created, + value, + }; + this.valueCreated.notify(value); + }); + return valuePromise; } } + +enum ValueStatus { + NotRequested, + BeingCreated, + Created, +} + +type ValueState = + | { + readonly status: ValueStatus.NotRequested; + } + | { + readonly status: ValueStatus.BeingCreated; + readonly value: Promise; + } + | { + readonly status: ValueStatus.Created; + readonly value: T + }; diff --git a/src/infrastructure/WindowVariables/WindowVariables.ts b/src/infrastructure/WindowVariables/WindowVariables.ts index 06485ce64..a5a6ed24d 100644 --- a/src/infrastructure/WindowVariables/WindowVariables.ts +++ b/src/infrastructure/WindowVariables/WindowVariables.ts @@ -4,8 +4,8 @@ import { ILogger } from '@/infrastructure/Log/ILogger'; /* Primary entry point for platform-specific injections */ export interface WindowVariables { - readonly system: ISystemOperations; - readonly isDesktop: boolean; - readonly os: OperatingSystem; - readonly log: ILogger; + readonly isDesktop?: boolean; + readonly system?: ISystemOperations; + readonly os?: OperatingSystem; + readonly log?: ILogger; } diff --git a/src/infrastructure/WindowVariables/WindowVariablesValidator.ts b/src/infrastructure/WindowVariables/WindowVariablesValidator.ts index 66d764e9b..8d2a98b7d 100644 --- a/src/infrastructure/WindowVariables/WindowVariablesValidator.ts +++ b/src/infrastructure/WindowVariables/WindowVariablesValidator.ts @@ -17,7 +17,7 @@ export function validateWindowVariables(variables: Partial) { function* testEveryProperty(variables: Partial): Iterable { const tests: { - [K in PropertyKeys]: boolean; + [K in PropertyKeys>]: boolean; } = { os: testOperatingSystem(variables.os), isDesktop: testIsDesktop(variables.isDesktop), diff --git a/src/presentation/components/App.vue b/src/presentation/components/App.vue index b7ca298d9..d40f48cd0 100644 --- a/src/presentation/components/App.vue +++ b/src/presentation/components/App.vue @@ -7,22 +7,18 @@ - +