Compare commits

...

31 Commits

Author SHA1 Message Date
undergroundwires
1430d5215a win: add more Edge scripts including AI & ads
This commit improves the scripts configuring Edge. It improves their
categorization, naming and adds scripts to disable Bing ads and Search
bar along with others to disable ads/data collection.

Changes:

- Add new scripts to configure Edge, such as blocking ads and AI
  features that collects data.
- Improve categorization and documentation consistency.
2024-06-26 16:48:49 +02:00
undergroundwires
c09c5ffa47 win, linux, mac: fix typos #373
This commit fixes typos, corrects markdown syntax, and archived URLs.

Co-authored-by: RainRat <rainrat78@yahoo.ca>
2024-06-26 08:13:13 +02:00
undergroundwires
ed7e69c07e win: add disabling Edge/WebView2 auto-updates #309
This commit adds scripts to block automatic updates for Microsoft Edge
and WebView2 on Windows, aimed at enhancing user privacy as per the
issue #309.

Changes:

- Create a new category for scripts targeting Edge and WebView2 updates.
- Add scripts for disabling automatic update services, scheduled tasks,
  and executable blocking, along with registry configurations.

Other supporting changes:

- Remove comments in code that indicates reusing of small text parts.
  This approach does not encourage creating unique content.
2024-06-25 12:23:55 +02:00
undergroundwires
f286f92b1f win: categorize, rename, doc Chrome & Edge scripts
This commit improves the script organization, documentation and code for
Edge and Chrome browser by simplifying naming, categorizing
configurations, and unifying documentation and generated code.

Changes:

- Rename "Edge (Chromium)" to "Edge" for clarity, with "Edge (Legacy)"
  detailed explicitly in the script titles.
- Flatten Edge settings under a unified "Configure" category.
- Enhance script documentation to improve clarity.
- Move "Your browser is managed" warning from script titles to script
  documentation.
- Introduce shared functions for configuring Edge and Chrome, leading to
  better consistency in generated code.
- Update scripts to include restart suggestions in generated code.
- Improve documentation of affected scripts.
- Split some scripts for increased granularity, easier maintenance and
  focused documentation.
- Fix some Windows UI scripts being incorrectly categorized as Edge
  configurations.
2024-06-24 20:27:52 +02:00
undergroundwires
e7031a3ae4 win: fix latest Edge removal on Windows 10 #309
This commit introduces a placeholder ifle creation step necessary for
the uninstallation process of Microsoft Edge on Windows 10, as discussed
in #309. The file simulates the presence of Microsoft Edge (Legacy),
which newer uninstallers check for before proceeding with the
uninstallation.

This change resolves the observed issue where the uninstaller fails to
recognize the absence of Legacy Edge, hindering the uninstallation
process.

Changes:

- Add placeholder file creation/removal for legacy Edge.
- Update and improve the documentation.
2024-06-23 12:51:15 +02:00
undergroundwires
2f828735a8 win: fix errors due to missing Edge uninstaller
If Edge is uninstalled using an existing installer, it may delete other
installers. When the script attempts to use these deleted installers,
it results in an error: `The system cannot find the file specified.`.

This commit addresses the issue by checking for the existence of the
uninstaller during the iteration and handling cases where it is missing.
2024-06-22 14:01:06 +02:00
undergroundwires
78c62cfc95 Trim compiler error output for better readability
Previously, compiler outputted whole executable in error context. This
caused long and hard to read error messages, especially when the
executable is a long category with many children. This commit improves
readability by trimming the error output.

Changes:

- Trim the error output (max characters: 1000).
- Improve indenting and newlines.
2024-06-21 12:46:30 +02:00
undergroundwires
ed93614ca3 Bump Electron to latest
- Bump Electron to latest.
- Adjust types to new Electron types.
2024-06-20 10:49:24 +02:00
undergroundwires
fac26a6ca0 Add type validation for parameters and fix types
This commit introduces type validation for parameter values within the
parser/compiler, aligning with the YAML schema. It aims to eliminate
dependencies on side effects in the collection files.

This update changes the treatment of data types in the Windows
collection, moving away from unintended type casting by the compiler.
Previously, numeric and boolean values were used even though only
string types were supported. This behavior was unstable and untested,
and has now been adjusted to use strings exclusively.

Changes ensure that parameter values are correctly validated
as strings, enhancing stability and maintainability.
2024-06-19 17:01:27 +02:00
undergroundwires
48761f62a2 win: fix incomplete VSCEIP, location scripts
This commit improves the validation logic in parser, corrects Windows
collection files to adhere to expected structure. This validation helps
catch errors that previously led to incomplete generated code in scripts
for disabling VSCEIP and location settings.

Changes:

- Add type validation for function call structures in the
  parser/compiler. This helps prevent runtime errors by ensuring that
  only correctly structured data is processed.
- Fix scripts in the Windows collection that previoulsy had incomplete
  `code` or `revertCode` values. These corrections ensure that the
  scripts function as intended.
- Refactor related logic within the compiler/parser to improve
  testability and maintainability.
2024-06-18 17:59:32 +02:00
undergroundwires
dc03bff324 Add schema validation for collection files #369
This commit improves collection file editing and error detection
directly in the IDE. It adds YAML schema, IDE configuration and
automatic tests to validate it.

- Introduce a YAML schema for collection file.
- Use `yaml-language-server` for enhanced YAML support in VSCode.
- Add telemetry disabling in `configure_vscode.py` to respect user
  privacy.
- Add automated checks to validate YAML file structure against the
  schema.
- Remove unused properties and do not allow them in compiler.
2024-06-17 14:01:07 +02:00
undergroundwires
e9a52859f6 mac: document, improve, encourage clearing logs
Previously, scripts under the 'Clear operating system logs' category for
macOS were misaligned due to a lack of individual script
recommendations, as the category itself wrongly used the `recommend:
strict` property. This misconfiguration caused none of these scripts to
appear recommended.

This commit assigns accurate `recommend:` values to each script
within the category.

Key changes:

- Introduce individual recommendations for each script.
- Document scripts to justify recommendations.
- Standardize deletion operations through shared functions.
- Improve script and category naming for clarity.
- Simplify code by unifying redundant path references.
- Add comments in generated user script code.
- Fix specific issue where clearing daily os logs inadvertently affected
  configuration files.
2024-06-16 11:27:48 +02:00
undergroundwires
1a10cf2e5f win: fix text and handwriting script omission #369
This commit corrects a syntax error that prevented the 'Disable text
and handwriting data collection' script from being included since
version 0.13.4. The error was identified in a previous syntax validation
update (commit 6ecfa9b954).

Changes:

- Add a missing dash before 'Disable location access' category. This
  fixes the script omission as reported in the issue #369.
- Remove the dash from `revertCode` of `HarvestContacts` registry
  modification code, fixing the revert code for 'Disable text and
  handwriting data collection' script.

This fix addresses the build errors introduced by stricter syntax checks
and ensures that the script is now properly recognized and executed.
This fix is part of ongoing efforts to improve data handling robustness
and management in script processing.
2024-06-15 08:32:58 +02:00
undergroundwires
1c2d82dc9b win: fix missing app access recommendations #369
This script fixes the recommendation property syntax in Windows script
collection. This syntax error prevented the application from
recommending these scripts, even though they were intended to be
recommended.

Affected scripts:

- Disable app access to physical movement
- Disable app access to eye tracking
- Disable app access to human presence
- Disable app access to screen capture

Previously, these scripts used the unsupported 'recommended: standard'
property, which was identified as incorrect after implementing stricter
property validation.
Related commit: 6ecfa9b954
Related issue: #369

This change update these properties to the correct 'recommend:
standard', resolving issues where scripts were not being recommended as
expected.
2024-06-14 12:36:15 +02:00
undergroundwires
6ecfa9b954 Add object property validation in parser #369
This commit introduces stricter type validation across the application
to reject objects with unexpected properties, enhancing the robustness
and predictability of data handling.

Changes include:

- Implement a common utility to validate object types.
- Refactor across various parsers and data handlers to utilize the new
  validations.
- Update error messages for better clarity and troubleshooting.
2024-06-13 22:26:57 +02:00
undergroundwires
c138f74460 Refactor to unify scripts/categories as Executable
This commit consolidates scripts and categories under a unified
'Executable' concept. This simplifies the architecture and improves code
readability.

- Introduce subfolders within `src/domain` to segregate domain elements.
- Update class and interface names by removing the 'I' prefix in
  alignment with new coding standards.
- Replace 'Node' with 'Executable' to clarify usage; reserve 'Node'
  exclusively for the UI's tree component.
2024-06-12 12:36:40 +02:00
undergroundwires
8becc7dbc4 win: fix revert scripts for removing shortcuts
Revert scripts for removing shortcuts previously used hardcoded paths.
These paths are now replaced with system environment variables to allow
for broader configuration compatibility.
2024-06-11 12:06:46 +02:00
undergroundwires
b29cd7b5f7 mac: discourage and document captive portal script
This commit adjusts the recommendation level for disabling captive
portal detection from 'Standard' to 'Strict'. This aligns macOS settings
with equivalent recommendations for Linux and Windows.

It improves documentation to provide additional context on implications,
facilitating a better understanding of the change.
2024-06-10 13:22:32 +02:00
undergroundwires
f21ef9250a win: improve executable blocking, Chrome reporting
This commit improves blocking of execution of executables, providing a
more reliable way to stop execution of unwanted executables.

Introduce a new function to block shell execution of an executable. This
logic is extracted from disabling Chrome Software Reporter tool with
improved logic which does no longer or reset if there is other rules.
This resolves potential issues if there was a blocking rule using same
number which privacy.sexy before overwrote or restored on revert.

Other scripts which terminated executables on launch does now block
their shell execution too for more reliability. A common function is
introduced which streamlines blocking execution of an executable all
known ways which is now reused by these scripts.

This commit additionally improves the Google Software Reporter disabling
script. It removes the code that adds denies permissions on its
installation directory as the new way of preventing executable from
running should be enough. It also adds missing documentation to the
related scripts.
2024-06-09 13:59:06 +02:00
undergroundwires
fa2a92bf89 Add image to README.md to thank supporters 2024-06-08 11:49:04 +02:00
undergroundwires
8341411be4 win: document and improve Firefox telemetry #259
This commit improves the existing Firefox privacy scripts and improves
the categorization and documentation to be simpler and more clear.

Changes:

- Rename and reorganize scripts for disabling browser telemetry and
  default browser agent reporting to simplify the structure.
- Improve documentation across scripts to provide clearer guidance on
  how the changes improve user privacy.
- Fix revert scripts by removing unnecessary registry key configuration.
2024-05-29 08:17:50 +02:00
undergroundwires
22d6c7991e ci/cd: centralize and bump artifact uploads
- Upgrade `actions/upload-artifact` to `v4` to address deprecation
  warnings related to Node.js 16, improving compatibility with GitHub
  runners. This resolves the following warning from the runners:
  > Node.js 16 actions are deprecated. Please update the following actions
  > to use Node.js 20: actions/upload-artifact@v3.
- Centralize the use of the `upload-artifact` action through a new
  custom action, improving maintainability and consistency across
  workflows.
2024-05-28 12:53:45 +02:00
undergroundwires-bot
795b7f0321 ⬆️ bump everywhere to 0.13.4 2024-05-27 13:45:04 +00:00
undergroundwires
9e34e64449 win, mac, linux: fix typos and dead URLs #367
- Fix multiple spelling errors in various scripts.
- Fix dead URLs with archived versions.
- Fix incorrect registry keys previously introduced in commit
  cec0b4b4f6.

Co-authored-by: RainRat <rainrat78@yahoo.ca>
2024-05-27 14:54:17 +02:00
undergroundwires
ce4cfdd169 win: add script to disable Recall feature 2024-05-27 10:44:11 +02:00
undergroundwires
12b1f183f7 win: document disabling firewall #115 #152 #364
This commit updates documentation to clarify the impacts of disabling
firewall services, specifically how they affect Windows Sandbox, Docker
and WSL.

This update responds to user feedback from issues #115, #152, #364. The
documentation now guides users more clearly on the consequences of their
actions, potentially preventing unintended service disruptions.

Changes include:

- Expand the caution notes to explicitly mention the impact on
  virtualization and isolation features like Windows Sandbox, Docker and
  WSL.
- Expand script titles to briefly mention affects on these features.
- Expand documentation to suggest system restart.
- Add an informative message to restart the computer in terminal outputs
  after service changes to ensure the settings are applied.
2024-05-26 13:42:25 +02:00
undergroundwires
4212c7b9e0 Improve context for errors thrown by compiler
This commit introduces a custom error object to provide additional
context for errors throwing during parsing and compiling operations,
improving troubleshooting.

By integrating error context handling, the error messages become more
informative and user-friendly, providing sequence of trace with context
to aid in troubleshooting.

Changes include:

- Introduce custom error object that extends errors with contextual
  information. This replaces previous usages of `AggregateError` which
  is not displayed well by browsers when logged.
- Improve parsing functions to encapsulate error context with more
  details.
- Increase unit test coverage and refactor the related code to be more
  testable.
2024-05-25 13:55:30 +02:00
undergroundwires
7794846185 win: discourage blocking app access #121 #339 #350
This commit adjusts the recommendation level for scripts that disable
UWP app access to accommodate user issues #121, #339, #350. It also
extends their documentation to reflect the new changes and with
cautions.

Changes:

- Add caution text for all scripts about potential impacts.
- Move disabling app access to notifications from 'Standard' to
  'Strict'. This addresses #121 and #339, where users report lack of
  notification as unintended side-effects.
- Move disabling app access to phone calls from 'Standard' to 'Strict'.
  This addresses #350 where its effect on the Phone Link app was
  reported as an unintended side-effect.
2024-05-24 10:45:23 +02:00
undergroundwires
150e067039 win: improve printing removal /w Print Queue #279
- Consolidate removal of printing UIs under the same category.
- Improve documentation for printing app removal scripts.
- Add removal of previously unlisted 'Print Queue' app, #279.
- Combine removal of `Microsoft.Print3D` and `Windows.Print3D`
  into a single script.
- Highlight the importance of removing 'Print 3D' app due to
  security risks and recommend it on 'Standard'.
2024-05-23 09:27:14 +02:00
undergroundwires
f347fde0c8 win: document and discourage RSA key script #363
This commit improves the documentation of RSA key handling script and
changes its recommendation level to address potential issues with
Hyper-V (as reported in #363).

Changes:

- Add documentation to describe potential disruptions caused by stronger
  RSA key requirements.
- Move RSA key script from 'Standard' to 'Strict' due to its impact on
  Hyper-V VMs.
- Use bullet points for easier expansion in cautions of secret key
  hardening scripts.
2024-05-22 08:10:37 +02:00
undergroundwires
ff3d5c4841 win: improve app access disabling and docs #138
This commit improves disabling app access by correcting minor issues,
adding missing access control configurations and adding more
documentation to increase maintainability and user understanding of
privacy settings, resolving #138.

- Introduce shared functions to streamline modifications of app access,
  improving maintainability and simplifying the codebase.
- Move disabling app access to first position in the category.
- Improve code comments for better clarity on generated outputs.
- Resolve error display in revert codes due to incorrect use of `reg
  delete` commands.
- Fix disabling app access to trusted devices disables access to account
  information, name and picture on older versions of Windows.
- Add missing privacy settings and configurations.
- Add more documentation to scripts
- Rename script names for consistency and clarity.
- Move disabling access to SMS/MMS to phone access disablement category.
- Set empty `REG_MULTI_SZ` values to null for GPO access settings to
  maintain registry integrity.
2024-05-21 13:02:48 +02:00
312 changed files with 13478 additions and 4925 deletions

View File

@@ -0,0 +1,15 @@
inputs:
name:
required: true
path:
required: true
runs:
using: composite
steps:
-
name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}

View File

@@ -70,7 +70,7 @@ jobs:
- -
name: Upload screenshot name: Upload screenshot
if: always() # Run even if previous step fails if: always() # Run even if previous step fails
uses: actions/upload-artifact@v3 uses: ./.github/actions/upload-artifact
with: with:
name: screenshot-${{ matrix.os }} name: screenshot-${{ matrix.os }}
path: screenshot.png path: screenshot.png

View File

@@ -74,3 +74,28 @@ jobs:
- -
name: Analyzing the code with pylint name: Analyzing the code with pylint
run: npm run lint:pylint run: npm run lint:pylint
validate-collection-files:
runs-on: ${{ matrix.os }}-latest
strategy:
matrix:
os: [ macos, ubuntu, windows ]
fail-fast: false # Still interested to see results from other combinations
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Setup node
uses: ./.github/actions/setup-node
-
name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
-
name: Install dependencies
run: python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
-
name: Validate
run: python3 ./scripts/validate-collections-yaml

View File

@@ -51,14 +51,14 @@ jobs:
- -
name: Upload screenshots name: Upload screenshots
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
uses: actions/upload-artifact@v3 uses: ./.github/actions/upload-artifact
with: with:
name: e2e-screenshots-${{ matrix.os }} name: e2e-screenshots-${{ matrix.os }}
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }} path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
- -
name: Upload videos name: Upload videos
if: always() # Run even if previous steps fail because test run video is always captured if: always() # Run even if previous steps fail because test run video is always captured
uses: actions/upload-artifact@v3 uses: ./.github/actions/upload-artifact
with: with:
name: e2e-videos-${{ matrix.os }} name: e2e-videos-${{ matrix.os }}
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }} path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}

4
.gitignore vendored
View File

@@ -14,3 +14,7 @@ node_modules
# macOS # macOS
.DS_Store .DS_Store
# Python
__pycache__
.venv

View File

@@ -5,8 +5,10 @@
"wengerk.highlight-bad-chars", // Highlights bad chars. "wengerk.highlight-bad-chars", // Highlights bad chars.
"wayou.vscode-todo-highlight", // Highlights TODO. "wayou.vscode-todo-highlight", // Highlights TODO.
"wix.vscode-import-cost", // Shows in KB how much a require include in code. "wix.vscode-import-cost", // Shows in KB how much a require include in code.
// Documentation // Markdown
"davidanson.vscode-markdownlint", // Lints markdown. "davidanson.vscode-markdownlint", // Lints markdown.
// YAML
"redhat.vscode-yaml", // Lints YAML files, validates against schema.
// TypeScript / JavaScript // TypeScript / JavaScript
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript. "dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports. "pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.

View File

@@ -1,5 +1,27 @@
# Changelog # Changelog
## 0.13.4 (2024-05-27)
* Add specific empty function name compiler error | [870120b](https://github.com/undergroundwires/privacy.sexy/commit/870120bc13909a3681e0f0a2351806849476342f)
* ci/cd: fix recent Docker build failures on macOS | [a1922c5](https://github.com/undergroundwires/privacy.sexy/commit/a1922c50c12b3b7806e9e681ace842194a178bda)
* win: standardize registry edit + delete on revert | [cec0b4b](https://github.com/undergroundwires/privacy.sexy/commit/cec0b4b4f63c3563a0e7923ce6324a38d71a3955)
* Fix e2e test failing on Windows | [4a7efa2](https://github.com/undergroundwires/privacy.sexy/commit/4a7efa27c8df73ef9b7960afed29f216b066cba2)
* Add support for macOS universal binary #348, #362 | [d25c4e8](https://github.com/undergroundwires/privacy.sexy/commit/d25c4e8c812b8d012010ba38070a2931dcd28908)
* Migrate to GitHub issue forms | [9ab3ff7](https://github.com/undergroundwires/privacy.sexy/commit/9ab3ff75b0a69ac2ba27dd02e82db9b5bd76ea0f)
* ci/cd: fix quality checks not running on all OSes | [2390530](https://github.com/undergroundwires/privacy.sexy/commit/2390530d929fb92c266558c52376569a0ecb90c1)
* Bump Vue to latest and fix universal selector CSS | [aae5434](https://github.com/undergroundwires/privacy.sexy/commit/aae54344511ec51d17ad0420a92cb5a064e0e7bb)
* Centralize and optimize `ResizeObserver` usage | [2923621](https://github.com/undergroundwires/privacy.sexy/commit/292362135db0519ec1050bab80ed373aad115731)
* win: improve app access disabling and docs #138 | [ff3d5c4](https://github.com/undergroundwires/privacy.sexy/commit/ff3d5c48419f663379f5aba8936636c22f2c5de8)
* win: document and discourage RSA key script #363 | [f347fde](https://github.com/undergroundwires/privacy.sexy/commit/f347fde0c85f8b51b0060fdea0a2724b042aaeed)
* win: improve printing removal /w Print Queue #279 | [150e067](https://github.com/undergroundwires/privacy.sexy/commit/150e0670392bb62348c20ec644a4ed8a6bbffe74)
* win: discourage blocking app access #121 #339 #350 | [7794846](https://github.com/undergroundwires/privacy.sexy/commit/77948461856e6837ddfbcbbef72a1bf9fc706b4e)
* Improve context for errors thrown by compiler | [4212c7b](https://github.com/undergroundwires/privacy.sexy/commit/4212c7b9e0b1500378a1e4e88efc2d59f39f3d29)
* win: document disabling firewall #115 #152 #364 | [12b1f18](https://github.com/undergroundwires/privacy.sexy/commit/12b1f183f7ce966d6ce090d98aeea7ec491f8c7c)
* win: add script to disable Recall feature | [ce4cfdd](https://github.com/undergroundwires/privacy.sexy/commit/ce4cfdd169b7da0edc3da61143c988ed5f3c976e)
* win, mac, linux: fix typos and dead URLs #367 | [9e34e64](https://github.com/undergroundwires/privacy.sexy/commit/9e34e644493674ca709b64a47206763d5d4bd60c)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.3...0.13.4)
## 0.13.3 (2024-05-11) ## 0.13.3 (2024-05-11)
* win: organize and document network disablement | [2eed6f4](https://github.com/undergroundwires/privacy.sexy/commit/2eed6f4afb6cf85fdc1d6acb808f82405a35cafd) * win: organize and document network disablement | [2eed6f4](https://github.com/undergroundwires/privacy.sexy/commit/2eed6f4afb6cf85fdc1d6acb808f82405a35cafd)

View File

@@ -122,7 +122,7 @@
## Get started ## Get started
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy). - 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-Setup-0.13.3.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-0.13.3.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-0.13.3.AppImage). For more options, see [here](#additional-install-options). - 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.4/privacy.sexy-Setup-0.13.4.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.4/privacy.sexy-0.13.4.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.4/privacy.sexy-0.13.4.AppImage). For more options, see [here](#additional-install-options).
See also: See also:
@@ -186,3 +186,7 @@ Check [architecture.md](./docs/architecture.md) for an overview of design and ho
Security is a top priority at privacy.sexy. Security is a top priority at privacy.sexy.
An extensive commitment to security verification ensures this priority. An extensive commitment to security verification ensures this priority.
For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md). For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md).
## Supporters
[![Supporters appreciation banner showing the supporters](https://undergroundwires.dev/img/supporters.jpg)](https://undergroundwires.dev/supporters)

View File

@@ -41,5 +41,5 @@ Application layer compiles templating syntax during parsing to create the end sc
The steps to extend the templating syntax: The steps to extend the templating syntax:
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more. 1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts). 2. Register your in [CompositeExpressionParser](./../src/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).

View File

@@ -1,11 +1,11 @@
# Collection files # Collection files
privacy.sexy is a data-driven application that reads YAML files. privacy.sexy is a data-driven application that reads YAML files.
This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model. This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model. The YAML schema [`.schema.yaml`](./../src/application/collections/.schema.yaml) is provided to provide better IDE support and be used in automated validations.
Related documentation: Related documentation:
- 📖 [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts) outlines code types. - 📖 [`Collections README`](./../src/application/collections/README.md) includes references to code as documentation.
- 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices. - 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices.
## Objects ## Objects
@@ -28,11 +28,20 @@ Related documentation:
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)** - `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
- Sets the scripting language for all inline code used within the collection. - Sets the scripting language for all inline code used within the collection.
### `Category` ### Executables
An Executable is a logical entity that can
- execute once compiled,
- include a `docs` property for documentation.
It's either [Category](#category) or a [Script](#script).
#### `Category`
Represents a logical group of scripts and subcategories. Represents a logical group of scripts and subcategories.
#### `Category` syntax ##### `Category` syntax
- `category:` *`string`* **(required)** - `category:` *`string`* **(required)**
- Name of the category. - Name of the category.
@@ -43,7 +52,7 @@ Represents a logical group of scripts and subcategories.
- `docs`: *`string`* | `[`*`string`*`, ... ]` - `docs`: *`string`* | `[`*`string`*`, ... ]`
- Markdown-formatted documentation related to the category. - Markdown-formatted documentation related to the category.
### `Script` #### `Script`
Represents an individual tweak. Represents an individual tweak.
@@ -58,7 +67,7 @@ Types (like [functions](#function)):
📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md). 📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md).
#### `Script` syntax ##### `Script` syntax
- `name`: *`string`* **(required)** - `name`: *`string`* **(required)**
- Script name. - Script name.

View File

@@ -80,8 +80,10 @@ See [ci-cd.md](./ci-cd.md) for more information.
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js): - [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features. - Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies. - For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
- [**`python ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py): - [**`python3 ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py):
- Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment. - Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment.
- [**`python3 ./scripts/validate-collections-yaml`**](../scripts/validate-collections-yaml/README.md):
- Validates the syntax and structure of collection YAML files.
#### Automation scripts #### Automation scripts

19
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.13.3", "version": "0.13.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.13.3", "version": "0.13.4",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@floating-ui/vue": "^1.0.6", "@floating-ui/vue": "^1.0.6",
@@ -34,7 +34,7 @@
"@vue/test-utils": "^2.4.5", "@vue/test-utils": "^2.4.5",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"cypress": "^13.7.3", "cypress": "^13.7.3",
"electron": "^29.3.0", "electron": "^31.0.2",
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-vite": "^2.1.0", "electron-vite": "^2.1.0",
@@ -6645,11 +6645,12 @@
} }
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "29.3.0", "version": "31.0.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-29.3.0.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-31.0.2.tgz",
"integrity": "sha512-ZxFKm0/v48GSoBuO3DdnMlCYXefEUKUHLMsKxyXY4nZGgzbBKpF/X8haZa2paNj23CLfsCKBOtfc2vsEQiOOsA==", "integrity": "sha512-55efQ5yfLN+AQHcFC00AXQqtxC3iAGaxX2GQ3EDbFJ0ca9GHNOdSXkcrdBElLleiDrR2hpXNkQxN1bDn0oxe6w==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",
"@types/node": "^20.9.0", "@types/node": "^20.9.0",
@@ -22338,9 +22339,9 @@
} }
}, },
"electron": { "electron": {
"version": "29.3.0", "version": "31.0.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-29.3.0.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-31.0.2.tgz",
"integrity": "sha512-ZxFKm0/v48GSoBuO3DdnMlCYXefEUKUHLMsKxyXY4nZGgzbBKpF/X8haZa2paNj23CLfsCKBOtfc2vsEQiOOsA==", "integrity": "sha512-55efQ5yfLN+AQHcFC00AXQqtxC3iAGaxX2GQ3EDbFJ0ca9GHNOdSXkcrdBElLleiDrR2hpXNkQxN1bDn0oxe6w==",
"dev": true, "dev": true,
"requires": { "requires": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.13.3", "version": "0.13.4",
"private": true, "private": true,
"slogan": "Privacy is sexy", "slogan": "Privacy is sexy",
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy.", "description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy.",
@@ -59,7 +59,7 @@
"@vue/test-utils": "^2.4.5", "@vue/test-utils": "^2.4.5",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"cypress": "^13.7.3", "cypress": "^13.7.3",
"electron": "^29.3.0", "electron": "^31.0.2",
"electron-builder": "^24.13.3", "electron-builder": "^24.13.3",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-vite": "^2.1.0", "electron-vite": "^2.1.0",

View File

@@ -58,6 +58,10 @@ def add_or_update_settings() -> None:
# Details: # pylint: disable-next=line-too-long # Details: # pylint: disable-next=line-too-long
# - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274 # - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274
# Disable telemetry
configure_setting_key('redhat.telemetry.enabled', False)
configure_setting_key('gitlens.telemetry.enabled', False)
def configure_setting_key(configuration_key: str, desired_value: Any) -> None: def configure_setting_key(configuration_key: str, desired_value: Any) -> None:
try: try:
with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file: with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file:

View File

@@ -0,0 +1,51 @@
# validate-collections-yaml
This script validates YAML collection files against a predefined schema to ensure their integrity.
## Prerequisites
- Python 3.x installed on your system.
## Running in a Virtual Environment (Recommended)
Using a virtual environment isolates dependencies and prevents conflicts.
1. **Create a virtual environment:**
```bash
python3 -m venv ./scripts/validate-collections-yaml/.venv
```
2. **Activate the virtual environment:**
```bash
source ./scripts/validate-collections-yaml/.venv/bin/activate
```
3. **Install dependencies:**
```bash
python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
```
4. **Run the script:**
```bash
python3 ./scripts/validate-collections-yaml
```
## Running Globally
Running the script globally is less recommended due to potential dependency conflicts.
1. **Install dependencies:**
```bash
python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
```
2. **Run the script:**
```bash
python3 ./scripts/validate-collections-yaml
```

View File

@@ -0,0 +1,62 @@
"""
Description:
This script validates collection YAML files against the expected schema.
Usage:
python3 ./scripts/validate-collections-yaml
Notes:
This script requires the `jsonschema` and `pyyaml` packages (see requirements.txt).
"""
# pylint: disable=missing-function-docstring
from os import path
import sys
from glob import glob
from typing import List
from jsonschema import exceptions, validate # pylint: disable=import-error
import yaml # pylint: disable=import-error
SCHEMA_FILE_PATH = './src/application/collections/.schema.yaml'
COLLECTIONS_GLOB_PATTERN = './src/application/collections/*.yaml'
def main() -> None:
schema_yaml = read_file(SCHEMA_FILE_PATH)
schema_json = convert_yaml_to_json(schema_yaml)
collection_file_paths = find_collection_files(COLLECTIONS_GLOB_PATTERN)
print(f'Found {len(collection_file_paths)} YAML files to validate.')
total_invalid_files = 0
for collection_file_path in collection_file_paths:
file_name = path.basename(collection_file_path)
print(f'Validating {file_name}...')
collection_yaml = read_file(collection_file_path)
collection_json = convert_yaml_to_json(collection_yaml)
try:
validate(instance=collection_json, schema=schema_json)
print(f'Success: {file_name} is valid.')
except exceptions.ValidationError as err:
print(f'Error: Validation failed for {file_name}.', file=sys.stderr)
print(str(err), file=sys.stderr)
total_invalid_files += 1
if total_invalid_files > 0:
print(f'Validation complete with {total_invalid_files} invalid files.', file=sys.stderr)
sys.exit(1)
else:
print('Validation complete. All files are valid.')
sys.exit(0)
def read_file(file_path: str) -> str:
with open(file_path, 'r', encoding='utf-8') as file:
return file.read()
def find_collection_files(glob_pattern: str) -> List[str]:
files = glob(glob_pattern)
filtered_files = [f for f in files if not path.basename(f).startswith('.')]
return filtered_files
def convert_yaml_to_json(yaml_content: str) -> dict:
return yaml.safe_load(yaml_content)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,6 @@
attrs==23.2.0
jsonschema==4.22.0
jsonschema-specifications==2023.12.1
PyYAML==6.0.1
referencing==0.35.1
rpds-py==0.18.1

View File

@@ -1,4 +1,4 @@
import { isFunction } from '@/TypeHelpers'; import { isFunction, type ConstructorArguments } from '@/TypeHelpers';
/* /*
Provides a unified and resilient way to extend errors across platforms. Provides a unified and resilient way to extend errors across platforms.
@@ -12,8 +12,8 @@ import { isFunction } from '@/TypeHelpers';
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work > https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
*/ */
export abstract class CustomError extends Error { export abstract class CustomError extends Error {
constructor(message?: string, options?: ErrorOptions) { constructor(...args: ConstructorArguments<typeof Error>) {
super(message, options); super(...args);
fixPrototype(this, new.target.prototype); fixPrototype(this, new.target.prototype);
ensureStackTrace(this); ensureStackTrace(this);

View File

@@ -5,13 +5,13 @@ export type EnumType = number | string;
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
= { [key in T]: TEnumValue }; = { [key in T]: TEnumValue };
export interface IEnumParser<TEnum> { export interface EnumParser<TEnum> {
parseEnum(value: string, propertyName: string): TEnum; parseEnum(value: string, propertyName: string): TEnum;
} }
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>( export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>, enumVariable: EnumVariable<T, TEnumValue>,
): IEnumParser<TEnumValue> { ): EnumParser<TEnumValue> {
return { return {
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable), parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
}; };

View File

@@ -1,4 +1,4 @@
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import type { ICodeChangedEvent } from './ICodeChangedEvent'; import type { ICodeChangedEvent } from './ICodeChangedEvent';
@@ -6,13 +6,13 @@ import type { ICodeChangedEvent } from './ICodeChangedEvent';
export class CodeChangedEvent implements ICodeChangedEvent { export class CodeChangedEvent implements ICodeChangedEvent {
public readonly code: string; public readonly code: string;
public readonly addedScripts: ReadonlyArray<IScript>; public readonly addedScripts: ReadonlyArray<Script>;
public readonly removedScripts: ReadonlyArray<IScript>; public readonly removedScripts: ReadonlyArray<Script>;
public readonly changedScripts: ReadonlyArray<IScript>; public readonly changedScripts: ReadonlyArray<Script>;
private readonly scripts: Map<IScript, ICodePosition>; private readonly scripts: Map<Script, ICodePosition>;
constructor( constructor(
code: string, code: string,
@@ -25,7 +25,7 @@ export class CodeChangedEvent implements ICodeChangedEvent {
this.addedScripts = selectIfNotExists(newScripts, oldScripts); this.addedScripts = selectIfNotExists(newScripts, oldScripts);
this.removedScripts = selectIfNotExists(oldScripts, newScripts); this.removedScripts = selectIfNotExists(oldScripts, newScripts);
this.changedScripts = getChangedScripts(oldScripts, newScripts); this.changedScripts = getChangedScripts(oldScripts, newScripts);
this.scripts = new Map<IScript, ICodePosition>(); this.scripts = new Map<Script, ICodePosition>();
scripts.forEach((position, selection) => { scripts.forEach((position, selection) => {
this.scripts.set(selection.script, position); this.scripts.set(selection.script, position);
}); });
@@ -35,7 +35,7 @@ export class CodeChangedEvent implements ICodeChangedEvent {
return this.scripts.size === 0; return this.scripts.size === 0;
} }
public getScriptPositionInCode(script: IScript): ICodePosition { public getScriptPositionInCode(script: Script): ICodePosition {
return this.getPositionById(script.id); return this.getPositionById(script.id);
} }
@@ -65,7 +65,7 @@ function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodeP
function getChangedScripts( function getChangedScripts(
oldScripts: ReadonlyArray<SelectedScript>, oldScripts: ReadonlyArray<SelectedScript>,
newScripts: ReadonlyArray<SelectedScript>, newScripts: ReadonlyArray<SelectedScript>,
): ReadonlyArray<IScript> { ): ReadonlyArray<Script> {
return newScripts return newScripts
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id .filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
&& oldScript.revert !== newScript.revert)) && oldScript.revert !== newScript.revert))

View File

@@ -1,11 +1,11 @@
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
export interface ICodeChangedEvent { export interface ICodeChangedEvent {
readonly code: string; readonly code: string;
readonly addedScripts: ReadonlyArray<IScript>; readonly addedScripts: ReadonlyArray<Script>;
readonly removedScripts: ReadonlyArray<IScript>; readonly removedScripts: ReadonlyArray<Script>;
readonly changedScripts: ReadonlyArray<IScript>; readonly changedScripts: ReadonlyArray<Script>;
isEmpty(): boolean; isEmpty(): boolean;
getScriptPositionInCode(script: IScript): ICodePosition; getScriptPositionInCode(script: Script): ICodePosition;
} }

View File

@@ -1,11 +1,11 @@
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import type { ICategory } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { FilterResult } from './FilterResult'; import type { FilterResult } from './FilterResult';
export class AppliedFilterResult implements FilterResult { export class AppliedFilterResult implements FilterResult {
constructor( constructor(
public readonly scriptMatches: ReadonlyArray<IScript>, public readonly scriptMatches: ReadonlyArray<Script>,
public readonly categoryMatches: ReadonlyArray<ICategory>, public readonly categoryMatches: ReadonlyArray<Category>,
public readonly query: string, public readonly query: string,
) { ) {
if (!query) { throw new Error('Query is empty or undefined'); } if (!query) { throw new Error('Query is empty or undefined'); }

View File

@@ -1,8 +1,9 @@
import type { IScript, ICategory } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { Script } from '@/domain/Executables/Script/Script';
export interface FilterResult { export interface FilterResult {
readonly categoryMatches: ReadonlyArray<ICategory>; readonly categoryMatches: ReadonlyArray<Category>;
readonly scriptMatches: ReadonlyArray<IScript>; readonly scriptMatches: ReadonlyArray<Script>;
readonly query: string; readonly query: string;
hasAnyMatches(): boolean; hasAnyMatches(): boolean;
} }

View File

@@ -1,7 +1,8 @@
import type { ICategory, IScript } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { IScriptCode } from '@/domain/IScriptCode'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
import type { IDocumentable } from '@/domain/IDocumentable'; import type { Documentable } from '@/domain/Executables/Documentable';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { Script } from '@/domain/Executables/Script/Script';
import { AppliedFilterResult } from '../Result/AppliedFilterResult'; import { AppliedFilterResult } from '../Result/AppliedFilterResult';
import type { FilterStrategy } from './FilterStrategy'; import type { FilterStrategy } from './FilterStrategy';
import type { FilterResult } from '../Result/FilterResult'; import type { FilterResult } from '../Result/FilterResult';
@@ -24,7 +25,7 @@ export class LinearFilterStrategy implements FilterStrategy {
} }
function matchesCategory( function matchesCategory(
category: ICategory, category: Category,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
return matchesAny( return matchesAny(
@@ -34,7 +35,7 @@ function matchesCategory(
} }
function matchesScript( function matchesScript(
script: IScript, script: Script,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
return matchesAny( return matchesAny(
@@ -58,7 +59,7 @@ function matchName(
} }
function matchCode( function matchCode(
code: IScriptCode, code: ScriptCode,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
if (code.execute.toLowerCase().includes(filterLowercase)) { if (code.execute.toLowerCase().includes(filterLowercase)) {
@@ -71,7 +72,7 @@ function matchCode(
} }
function matchDocumentation( function matchDocumentation(
documentable: IDocumentable, documentable: Documentable,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
return documentable.docs.some( return documentable.docs.some(

View File

@@ -1,9 +1,9 @@
import type { ICategory } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { CategorySelectionChangeCommand } from './CategorySelectionChange'; import type { CategorySelectionChangeCommand } from './CategorySelectionChange';
export interface ReadonlyCategorySelection { export interface ReadonlyCategorySelection {
areAllScriptsSelected(category: ICategory): boolean; areAllScriptsSelected(category: Category): boolean;
isAnyScriptSelected(category: ICategory): boolean; isAnyScriptSelected(category: Category): boolean;
} }
export interface CategorySelection extends ReadonlyCategorySelection { export interface CategorySelection extends ReadonlyCategorySelection {

View File

@@ -1,4 +1,4 @@
import type { ICategory } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange'; import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
import type { CategorySelection } from './CategorySelection'; import type { CategorySelection } from './CategorySelection';
@@ -13,7 +13,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
} }
public areAllScriptsSelected(category: ICategory): boolean { public areAllScriptsSelected(category: Category): boolean {
const { selectedScripts } = this.scriptSelection; const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) { if (selectedScripts.length === 0) {
return false; return false;
@@ -27,7 +27,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
); );
} }
public isAnyScriptSelected(category: ICategory): boolean { public isAnyScriptSelected(category: Category): boolean {
const { selectedScripts } = this.scriptSelection; const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) { if (selectedScripts.length === 0) {
return false; return false;

View File

@@ -1,5 +1,5 @@
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository'; import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
@@ -80,7 +80,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
}); });
} }
public selectOnly(scripts: readonly IScript[]): void { public selectOnly(scripts: readonly Script[]): void {
assertNonEmptyScriptSelection(scripts); assertNonEmptyScriptSelection(scripts);
this.processChanges({ this.processChanges({
changes: [ changes: [
@@ -145,7 +145,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
} }
} }
function assertNonEmptyScriptSelection(selectedItems: readonly IScript[]) { function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) {
if (selectedItems.length === 0) { if (selectedItems.length === 0) {
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.'); throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
} }
@@ -153,7 +153,7 @@ function assertNonEmptyScriptSelection(selectedItems: readonly IScript[]) {
function getScriptIdsToBeSelected( function getScriptIdsToBeSelected(
existingItems: ReadonlyRepository<string, SelectedScript>, existingItems: ReadonlyRepository<string, SelectedScript>,
desiredScripts: readonly IScript[], desiredScripts: readonly Script[],
): string[] { ): string[] {
return desiredScripts return desiredScripts
.filter((script) => !existingItems.exists(script.id)) .filter((script) => !existingItems.exists(script.id))
@@ -162,7 +162,7 @@ function getScriptIdsToBeSelected(
function getScriptIdsToBeDeselected( function getScriptIdsToBeDeselected(
existingItems: ReadonlyRepository<string, SelectedScript>, existingItems: ReadonlyRepository<string, SelectedScript>,
desiredScripts: readonly IScript[], desiredScripts: readonly Script[],
): string[] { ): string[] {
return existingItems return existingItems
.getItems() .getItems()

View File

@@ -1,5 +1,5 @@
import type { IEventSource } from '@/infrastructure/Events/IEventSource'; import type { IEventSource } from '@/infrastructure/Events/IEventSource';
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import type { SelectedScript } from './SelectedScript'; import type { SelectedScript } from './SelectedScript';
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange'; import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
@@ -10,7 +10,7 @@ export interface ReadonlyScriptSelection {
} }
export interface ScriptSelection extends ReadonlyScriptSelection { export interface ScriptSelection extends ReadonlyScriptSelection {
selectOnly(scripts: readonly IScript[]): void; selectOnly(scripts: readonly Script[]): void;
selectAll(): void; selectAll(): void;
deselectAll(): void; deselectAll(): void;
processChanges(action: ScriptSelectionChangeCommand): void; processChanges(action: ScriptSelectionChangeCommand): void;

View File

@@ -1,9 +1,9 @@
import type { IEntity } from '@/infrastructure/Entity/IEntity'; import type { IEntity } from '@/infrastructure/Entity/IEntity';
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
type ScriptId = IScript['id']; type ScriptId = Script['id'];
export interface SelectedScript extends IEntity<ScriptId> { export interface SelectedScript extends IEntity<ScriptId> {
readonly script: IScript; readonly script: Script;
readonly revert: boolean; readonly revert: boolean;
} }

View File

@@ -1,12 +1,12 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity'; import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import type { SelectedScript } from './SelectedScript'; import type { SelectedScript } from './SelectedScript';
type SelectedScriptId = SelectedScript['id']; type SelectedScriptId = SelectedScript['id'];
export class UserSelectedScript extends BaseEntity<SelectedScriptId> { export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
constructor( constructor(
public readonly script: IScript, public readonly script: Script,
public readonly revert: boolean, public readonly revert: boolean,
) { ) {
super(script.id); super(script.id);

View File

@@ -1,40 +1,48 @@
import type { CollectionData } from '@/application/collections/'; import type { CollectionData } from '@/application/collections/';
import type { IApplication } from '@/domain/IApplication'; import type { IApplication } from '@/domain/IApplication';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import WindowsData from '@/application/collections/windows.yaml'; import WindowsData from '@/application/collections/windows.yaml';
import MacOsData from '@/application/collections/macos.yaml'; import MacOsData from '@/application/collections/macos.yaml';
import LinuxData from '@/application/collections/linux.yaml'; import LinuxData from '@/application/collections/linux.yaml';
import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser'; import { parseProjectDetails, type ProjectDetailsParser } from '@/application/Parser/ProjectDetailsParser';
import { Application } from '@/domain/Application'; import { Application } from '@/domain/Application';
import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata'; import { parseCategoryCollection, type CategoryCollectionParser } from './CategoryCollectionParser';
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory'; import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
import { parseCategoryCollection } from './CategoryCollectionParser';
export function parseApplication( export function parseApplication(
categoryParser = parseCategoryCollection, collectionsData: readonly CollectionData[] = PreParsedCollections,
projectDetailsParser = parseProjectDetails, utilities: ApplicationParserUtilities = DefaultUtilities,
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
collectionsData = PreParsedCollections,
): IApplication { ): IApplication {
validateCollectionsData(collectionsData); validateCollectionsData(collectionsData, utilities.validator);
const projectDetails = projectDetailsParser(metadata); const projectDetails = utilities.parseProjectDetails();
const collections = collectionsData.map( const collections = collectionsData.map(
(collection) => categoryParser(collection, projectDetails), (collection) => utilities.parseCategoryCollection(collection, projectDetails),
); );
const app = new Application(projectDetails, collections); const app = new Application(projectDetails, collections);
return app; return app;
} }
export type CategoryCollectionParserType const PreParsedCollections: readonly CollectionData[] = [
= (file: CollectionData, projectDetails: ProjectDetails) => ICategoryCollection;
const PreParsedCollections: readonly CollectionData [] = [
WindowsData, MacOsData, LinuxData, WindowsData, MacOsData, LinuxData,
]; ];
function validateCollectionsData(collections: readonly CollectionData[]) { function validateCollectionsData(
if (!collections.length) { collections: readonly CollectionData[],
throw new Error('missing collections'); validator: TypeValidator,
} ) {
validator.assertNonEmptyCollection({
value: collections,
valueName: 'collections',
});
} }
interface ApplicationParserUtilities {
readonly parseCategoryCollection: CategoryCollectionParser;
readonly validator: TypeValidator;
readonly parseProjectDetails: ProjectDetailsParser;
}
const DefaultUtilities: ApplicationParserUtilities = {
parseCategoryCollection,
parseProjectDetails,
validator: createTypeValidator(),
};

View File

@@ -3,32 +3,73 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategoryCollection } from '@/domain/CategoryCollection'; import { CategoryCollection } from '@/domain/CategoryCollection';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import { createEnumParser } from '../Common/Enum'; import { createEnumParser, type EnumParser } from '../Common/Enum';
import { parseCategory } from './CategoryParser'; import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext'; import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser'; import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
import { createCollectionUtilities, type CategoryCollectionSpecificUtilitiesFactory } from './Executable/CategoryCollectionSpecificUtilities';
export function parseCategoryCollection( export const parseCategoryCollection: CategoryCollectionParser = (
content: CollectionData, content,
projectDetails: ProjectDetails, projectDetails,
osParser = createEnumParser(OperatingSystem), utilities: CategoryCollectionParserUtilities = DefaultUtilities,
): ICategoryCollection { ) => {
validate(content); validateCollection(content, utilities.validator);
const scripting = new ScriptingDefinitionParser() const scripting = utilities.parseScriptingDefinition(content.scripting, projectDetails);
.parse(content.scripting, projectDetails); const collectionUtilities = utilities.createUtilities(content.functions, scripting);
const context = new CategoryCollectionParseContext(content.functions, scripting); const categories = content.actions.map(
const categories = content.actions.map((action) => parseCategory(action, context)); (action) => utilities.parseCategory(action, collectionUtilities),
const os = osParser.parseEnum(content.os, 'os');
const collection = new CategoryCollection(
os,
categories,
scripting,
); );
const os = utilities.osParser.parseEnum(content.os, 'os');
const collection = utilities.createCategoryCollection({
os, actions: categories, scripting,
});
return collection; return collection;
};
export type CategoryCollectionFactory = (
...parameters: ConstructorParameters<typeof CategoryCollection>
) => ICategoryCollection;
export interface CategoryCollectionParser {
(
content: CollectionData,
projectDetails: ProjectDetails,
utilities?: CategoryCollectionParserUtilities,
): ICategoryCollection;
} }
function validate(content: CollectionData): void { function validateCollection(
if (!content.actions.length) { content: CollectionData,
throw new Error('content does not define any action'); validator: TypeValidator,
} ): void {
validator.assertObject({
value: content,
valueName: 'collection',
allowedProperties: [
'os', 'scripting', 'actions', 'functions',
],
});
validator.assertNonEmptyCollection({
value: content.actions,
valueName: '"actions" in collection',
});
} }
interface CategoryCollectionParserUtilities {
readonly osParser: EnumParser<OperatingSystem>;
readonly validator: TypeValidator;
readonly parseScriptingDefinition: ScriptingDefinitionParser;
readonly createUtilities: CategoryCollectionSpecificUtilitiesFactory;
readonly parseCategory: CategoryParser;
readonly createCategoryCollection: CategoryCollectionFactory;
}
const DefaultUtilities: CategoryCollectionParserUtilities = {
osParser: createEnumParser(OperatingSystem),
validator: createTypeValidator(),
parseScriptingDefinition,
createUtilities: createCollectionUtilities,
parseCategory,
createCategoryCollection: (...args) => new CategoryCollection(...args),
};

View File

@@ -1,133 +0,0 @@
import type {
CategoryData, ScriptData, CategoryOrScriptData,
} from '@/application/collections/';
import { Script } from '@/domain/Script';
import { Category } from '@/domain/Category';
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
import { parseDocs } from './DocumentationParser';
import { parseScript } from './Script/ScriptParser';
import type { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
let categoryIdCounter = 0;
export function parseCategory(
category: CategoryData,
context: ICategoryCollectionParseContext,
factory: CategoryFactoryType = CategoryFactory,
): Category {
return parseCategoryRecursively({
categoryData: category,
context,
factory,
});
}
interface ICategoryParseContext {
readonly categoryData: CategoryData,
readonly context: ICategoryCollectionParseContext,
readonly factory: CategoryFactoryType,
readonly parentCategory?: CategoryData,
}
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
ensureValidCategory(context.categoryData, context.parentCategory);
const children: ICategoryChildren = {
subCategories: new Array<Category>(),
subScripts: new Array<Script>(),
};
for (const data of context.categoryData.children) {
parseNode({
nodeData: data,
children,
parent: context.categoryData,
factory: context.factory,
context: context.context,
});
}
try {
return context.factory(
/* id: */ categoryIdCounter++,
/* name: */ context.categoryData.category,
/* docs: */ parseDocs(context.categoryData),
/* categories: */ children.subCategories,
/* scripts: */ children.subScripts,
);
} catch (err) {
return new NodeValidator({
type: NodeType.Category,
selfNode: context.categoryData,
parentNode: context.parentCategory,
}).throw(err.message);
}
}
function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
new NodeValidator({
type: NodeType.Category,
selfNode: category,
parentNode: parentCategory,
})
.assertDefined(category)
.assertValidName(category.category)
.assert(
() => category.children.length > 0,
`"${category.category}" has no children.`,
);
}
interface ICategoryChildren {
subCategories: Category[];
subScripts: Script[];
}
interface INodeParseContext {
readonly nodeData: CategoryOrScriptData;
readonly children: ICategoryChildren;
readonly parent: CategoryData;
readonly factory: CategoryFactoryType;
readonly context: ICategoryCollectionParseContext;
}
function parseNode(context: INodeParseContext) {
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
validator.assertDefined(context.nodeData);
if (isCategory(context.nodeData)) {
const subCategory = parseCategoryRecursively({
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, context.context);
context.children.subScripts.push(script);
} else {
validator.throw('Node is neither a category or a script.');
}
}
function isScript(data: CategoryOrScriptData): data is ScriptData {
return hasCode(data) || hasCall(data);
}
function isCategory(data: CategoryOrScriptData): data is CategoryData {
return hasProperty(data, 'category');
}
function hasCode(data: unknown): boolean {
return hasProperty(data, 'code');
}
function hasCall(data: unknown) {
return hasProperty(data, 'call');
}
function hasProperty(object: unknown, propertyName: string) {
return Object.prototype.hasOwnProperty.call(object, propertyName);
}
export type CategoryFactoryType = (
...parameters: ConstructorParameters<typeof Category>) => Category;
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);

View File

@@ -0,0 +1,42 @@
import { CustomError } from '@/application/Common/CustomError';
export interface ErrorWithContextWrapper {
(
error: Error,
additionalContext: string,
): Error;
}
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
error: Error,
additionalContext: string,
) => {
return (error instanceof ContextualError ? error : new ContextualError(error))
.withAdditionalContext(additionalContext);
};
/* AggregateError is similar but isn't well-serialized or displayed by browsers */
class ContextualError extends CustomError {
private readonly additionalContext = new Array<string>();
constructor(
public readonly innerError: Error,
) {
super();
}
public withAdditionalContext(additionalContext: string): this {
this.additionalContext.push(additionalContext);
return this;
}
public get message(): string { // toString() is not used when Chromium logs it on console
return [
'\n',
this.innerError.message,
'\n',
'Additional context:',
...this.additionalContext.map((context, index) => `${index + 1}: ${context}`),
].join('\n');
}
}

View File

@@ -0,0 +1,131 @@
import type { PropertyKeys } from '@/TypeHelpers';
import {
isNullOrUndefined, isArray, isPlainObject, isString,
} from '@/TypeHelpers';
export interface TypeValidator {
assertObject<T>(assertion: ObjectAssertion<T>): void;
assertNonEmptyCollection(assertion: NonEmptyCollectionAssertion): void;
assertNonEmptyString(assertion: NonEmptyStringAssertion): void;
}
export interface NonEmptyCollectionAssertion {
readonly value: unknown;
readonly valueName: string;
}
export interface RegexValidationRule {
readonly expectedMatch: RegExp;
readonly errorMessage: string;
}
export interface NonEmptyStringAssertion {
readonly value: unknown;
readonly valueName: string;
readonly rule?: RegexValidationRule;
}
export interface ObjectAssertion<T> {
readonly value: T | unknown;
readonly valueName: string;
readonly allowedProperties?: readonly PropertyKeys<T>[];
}
export function createTypeValidator(): TypeValidator {
return {
assertObject: (assertion) => {
assertDefined(assertion.value, assertion.valueName);
assertPlainObject(assertion.value, assertion.valueName);
assertNoEmptyProperties(assertion.value, assertion.valueName);
if (assertion.allowedProperties !== undefined) {
const allowedProperties = assertion.allowedProperties.map((p) => p as string);
assertAllowedProperties(assertion.value, assertion.valueName, allowedProperties);
}
},
assertNonEmptyCollection: (assertion) => {
assertDefined(assertion.value, assertion.valueName);
assertArray(assertion.value, assertion.valueName);
assertNonEmpty(assertion.value, assertion.valueName);
},
assertNonEmptyString: (assertion) => {
assertDefined(assertion.value, assertion.valueName);
assertString(assertion.value, assertion.valueName);
if (assertion.value.length === 0) {
throw new Error(`'${assertion.valueName}' is missing.`);
}
if (assertion.rule) {
if (!assertion.value.match(assertion.rule.expectedMatch)) {
throw new Error(assertion.rule.errorMessage);
}
}
},
};
}
function assertDefined<T>(
value: T,
valueName: string,
): asserts value is NonNullable<T> {
if (isNullOrUndefined(value)) {
throw new Error(`'${valueName}' is missing.`);
}
}
function assertPlainObject(
value: unknown,
valueName: string,
): asserts value is object {
if (!isPlainObject(value)) {
throw new Error(`'${valueName}' is not an object.`);
}
}
function assertNoEmptyProperties(
value: object,
valueName: string,
): void {
if (Object.keys(value).length === 0) {
throw new Error(`'${valueName}' is an empty object without properties.`);
}
}
function assertAllowedProperties(
value: object,
valueName: string,
allowedProperties: readonly string[],
): void {
const properties = Object.keys(value).map((p) => p as string);
const disallowedProperties = properties.filter(
(prop) => !allowedProperties.map((p) => p as string).includes(prop),
);
if (disallowedProperties.length > 0) {
throw new Error(`'${valueName}' has disallowed properties: ${disallowedProperties.join(', ')}.`);
}
}
function assertArray(
value: unknown,
valueName: string,
): asserts value is Array<unknown> {
if (!isArray(value)) {
throw new Error(`'${valueName}' should be of type 'array', but is of type '${typeof value}'.`);
}
}
function assertString(
value: unknown,
valueName: string,
): asserts value is string {
if (!isString(value)) {
throw new Error(`'${valueName}' should be of type 'string', but is of type '${typeof value}'.`);
}
}
function assertNonEmpty(
value: Array<unknown>,
valueName: string,
): void {
if (value.length === 0) {
throw new Error(`'${valueName}' cannot be an empty array.`);
}
}

View File

@@ -0,0 +1,35 @@
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import type { FunctionData } from '@/application/collections/';
import { ScriptCompiler } from './Script/Compiler/ScriptCompiler';
import { SyntaxFactory } from './Script/Validation/Syntax/SyntaxFactory';
import type { IScriptCompiler } from './Script/Compiler/IScriptCompiler';
import type { ILanguageSyntax } from './Script/Validation/Syntax/ILanguageSyntax';
import type { ISyntaxFactory } from './Script/Validation/Syntax/ISyntaxFactory';
export interface CategoryCollectionSpecificUtilities {
readonly compiler: IScriptCompiler;
readonly syntax: ILanguageSyntax;
}
export const createCollectionUtilities: CategoryCollectionSpecificUtilitiesFactory = (
functionsData: ReadonlyArray<FunctionData> | undefined,
scripting: IScriptingDefinition,
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
) => {
const syntax = syntaxFactory.create(scripting.language);
return {
compiler: new ScriptCompiler({
functions: functionsData ?? [],
syntax,
}),
syntax,
};
};
export interface CategoryCollectionSpecificUtilitiesFactory {
(
functionsData: ReadonlyArray<FunctionData> | undefined,
scripting: IScriptingDefinition,
syntaxFactory?: ISyntaxFactory,
): CategoryCollectionSpecificUtilities;
}

View File

@@ -0,0 +1,187 @@
import type {
CategoryData, ScriptData, ExecutableData,
} from '@/application/collections/';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import type { Category } from '@/domain/Executables/Category/Category';
import { CollectionCategory } from '@/domain/Executables/Category/CollectionCategory';
import type { Script } from '@/domain/Executables/Script/Script';
import { parseDocs, type DocsParser } from './DocumentationParser';
import { parseScript, type ScriptParser } from './Script/ScriptParser';
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
import { ExecutableType } from './Validation/ExecutableType';
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
let categoryIdCounter = 0;
export const parseCategory: CategoryParser = (
category: CategoryData,
collectionUtilities: CategoryCollectionSpecificUtilities,
categoryUtilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
) => {
return parseCategoryRecursively({
categoryData: category,
collectionUtilities,
categoryUtilities,
});
};
export interface CategoryParser {
(
category: CategoryData,
collectionUtilities: CategoryCollectionSpecificUtilities,
categoryUtilities?: CategoryParserUtilities,
): Category;
}
interface CategoryParseContext {
readonly categoryData: CategoryData;
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
readonly parentCategory?: CategoryData;
readonly categoryUtilities: CategoryParserUtilities;
}
function parseCategoryRecursively(
context: CategoryParseContext,
): Category | never {
const validator = ensureValidCategory(context);
const children: CategoryChildren = {
subcategories: new Array<Category>(),
subscripts: new Array<Script>(),
};
for (const data of context.categoryData.children) {
parseUnknownExecutable({
data,
children,
parent: context.categoryData,
categoryUtilities: context.categoryUtilities,
collectionUtilities: context.collectionUtilities,
});
}
try {
return context.categoryUtilities.createCategory({
id: categoryIdCounter++,
name: context.categoryData.category,
docs: context.categoryUtilities.parseDocs(context.categoryData),
subcategories: children.subcategories,
scripts: children.subscripts,
});
} catch (error) {
throw context.categoryUtilities.wrapError(
error,
validator.createContextualErrorMessage('Failed to parse category.'),
);
}
}
function ensureValidCategory(
context: CategoryParseContext,
): ExecutableValidator {
const category = context.categoryData;
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
type: ExecutableType.Category,
self: context.categoryData,
parentCategory: context.parentCategory,
});
validator.assertType((v) => v.assertObject({
value: category,
valueName: category.category ?? 'category',
allowedProperties: [
'docs', 'children', 'category',
],
}));
validator.assertValidName(category.category);
validator.assertType((v) => v.assertNonEmptyCollection({
value: category.children,
valueName: category.category,
}));
return validator;
}
interface CategoryChildren {
readonly subcategories: Category[];
readonly subscripts: Script[];
}
interface ExecutableParseContext {
readonly data: ExecutableData;
readonly children: CategoryChildren;
readonly parent: CategoryData;
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
readonly categoryUtilities: CategoryParserUtilities;
}
function parseUnknownExecutable(context: ExecutableParseContext) {
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
self: context.data,
parentCategory: context.parent,
});
validator.assertType((v) => v.assertObject({
value: context.data,
valueName: 'Executable',
}));
validator.assert(
() => isCategory(context.data) || isScript(context.data),
'Executable is neither a category or a script.',
);
if (isCategory(context.data)) {
const subCategory = parseCategoryRecursively({
categoryData: context.data,
collectionUtilities: context.collectionUtilities,
parentCategory: context.parent,
categoryUtilities: context.categoryUtilities,
});
context.children.subcategories.push(subCategory);
} else { // A script
const script = context.categoryUtilities.parseScript(context.data, context.collectionUtilities);
context.children.subscripts.push(script);
}
}
function isScript(data: ExecutableData): data is ScriptData {
return hasCode(data) || hasCall(data);
}
function isCategory(data: ExecutableData): data is CategoryData {
return hasProperty(data, 'category');
}
function hasCode(data: unknown): boolean {
return hasProperty(data, 'code');
}
function hasCall(data: unknown) {
return hasProperty(data, 'call');
}
function hasProperty(
object: unknown,
propertyName: string,
): object is NonNullable<object> {
if (typeof object !== 'object') {
return false;
}
if (object === null) { // `typeof object` is `null`
return false;
}
return Object.prototype.hasOwnProperty.call(object, propertyName);
}
export type CategoryFactory = (
...parameters: ConstructorParameters<typeof CollectionCategory>
) => Category;
interface CategoryParserUtilities {
readonly createCategory: CategoryFactory;
readonly wrapError: ErrorWithContextWrapper;
readonly createValidator: ExecutableValidatorFactory;
readonly parseScript: ScriptParser;
readonly parseDocs: DocsParser;
}
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
createCategory: (...parameters) => new CollectionCategory(...parameters),
wrapError: wrapErrorWithAdditionalContext,
createValidator: createExecutableDataValidator,
parseScript,
parseDocs,
};

View File

@@ -1,7 +1,7 @@
import type { DocumentableData, DocumentationData } from '@/application/collections/'; import type { DocumentableData, DocumentationData } from '@/application/collections/';
import { isString, isArray } from '@/TypeHelpers'; import { isString, isArray } from '@/TypeHelpers';
export function parseDocs(documentable: DocumentableData): readonly string[] { export const parseDocs: DocsParser = (documentable) => {
const { docs } = documentable; const { docs } = documentable;
if (!docs) { if (!docs) {
return []; return [];
@@ -9,6 +9,12 @@ export function parseDocs(documentable: DocumentableData): readonly string[] {
let result = new DocumentationContainer(); let result = new DocumentationContainer();
result = addDocs(docs, result); result = addDocs(docs, result);
return result.getAll(); return result.getAll();
};
export interface DocsParser {
(
documentable: DocumentableData,
): readonly string[];
} }
function addDocs( function addDocs(
@@ -44,5 +50,5 @@ class DocumentationContainer {
} }
function throwInvalidType(): never { function throwInvalidType(): never {
throw new Error('docs field (documentation) must be an array of strings'); throw new Error('docs field (documentation) must be a single string or an array of strings.');
} }

View File

@@ -1,4 +1,4 @@
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection'; import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
import { ExpressionEvaluationContext, type IExpressionEvaluationContext } from './ExpressionEvaluationContext'; import { ExpressionEvaluationContext, type IExpressionEvaluationContext } from './ExpressionEvaluationContext';
import { ExpressionPosition } from './ExpressionPosition'; import { ExpressionPosition } from './ExpressionPosition';
@@ -7,15 +7,18 @@ import type { IReadOnlyFunctionParameterCollection } from '../../Function/Parame
import type { IExpression } from './IExpression'; import type { IExpression } from './IExpression';
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string; export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
export class Expression implements IExpression { export class Expression implements IExpression {
public readonly parameters: IReadOnlyFunctionParameterCollection; public readonly parameters: IReadOnlyFunctionParameterCollection;
constructor( public readonly position: ExpressionPosition;
public readonly position: ExpressionPosition,
public readonly evaluator: ExpressionEvaluator, public readonly evaluator: ExpressionEvaluator;
parameters?: IReadOnlyFunctionParameterCollection,
) { constructor(parameters: ExpressionInitParameters) {
this.parameters = parameters ?? new FunctionParameterCollection(); this.parameters = parameters.parameters ?? new FunctionParameterCollection();
this.evaluator = parameters.evaluator;
this.position = parameters.position;
} }
public evaluate(context: IExpressionEvaluationContext): string { public evaluate(context: IExpressionEvaluationContext): string {
@@ -26,6 +29,12 @@ export class Expression implements IExpression {
} }
} }
export interface ExpressionInitParameters {
readonly position: ExpressionPosition,
readonly evaluator: ExpressionEvaluator,
readonly parameters?: IReadOnlyFunctionParameterCollection,
}
function validateThatAllRequiredParametersAreSatisfied( function validateThatAllRequiredParametersAreSatisfied(
parameters: IReadOnlyFunctionParameterCollection, parameters: IReadOnlyFunctionParameterCollection,
args: IReadOnlyFunctionCallArgumentCollection, args: IReadOnlyFunctionCallArgumentCollection,

View File

@@ -1,8 +1,13 @@
import { ExpressionPosition } from './ExpressionPosition'; import { ExpressionPosition } from './ExpressionPosition';
export function createPositionFromRegexFullMatch( export interface ExpressionPositionFactory {
match: RegExpMatchArray, (
): ExpressionPosition { match: RegExpMatchArray,
): ExpressionPosition
}
export const createPositionFromRegexFullMatch
: ExpressionPositionFactory = (match) => {
const startPos = match.index; const startPos = match.index;
if (startPos === undefined) { if (startPos === undefined) {
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`); throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
@@ -13,4 +18,4 @@ export function createPositionFromRegexFullMatch(
} }
const endPos = startPos + fullMatch.length; const endPos = startPos + fullMatch.length;
return new ExpressionPosition(startPos, endPos); return new ExpressionPosition(startPos, endPos);
} };

View File

@@ -1,4 +1,4 @@
import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext'; import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser'; import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
import type { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection'; import type { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
import type { IExpressionsCompiler } from './IExpressionsCompiler'; import type { IExpressionsCompiler } from './IExpressionsCompiler';

View File

@@ -3,10 +3,10 @@ import { WithParser } from '../SyntaxParsers/WithParser';
import type { IExpression } from '../Expression/IExpression'; import type { IExpression } from '../Expression/IExpression';
import type { IExpressionParser } from './IExpressionParser'; import type { IExpressionParser } from './IExpressionParser';
const Parsers = [ const Parsers: readonly IExpressionParser[] = [
new ParameterSubstitutionParser(), new ParameterSubstitutionParser(),
new WithParser(), new WithParser(),
]; ] as const;
export class CompositeExpressionParser implements IExpressionParser { export class CompositeExpressionParser implements IExpressionParser {
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) { public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {

View File

@@ -0,0 +1,127 @@
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory';
import type { IExpressionParser } from '../IExpressionParser';
import type { IExpression } from '../../Expression/IExpression';
import type { FunctionParameter } from '../../../Function/Parameter/FunctionParameter';
import type { IFunctionParameterCollection, IReadOnlyFunctionParameterCollection } from '../../../Function/Parameter/IFunctionParameterCollection';
export interface RegexParserUtilities {
readonly wrapError: ErrorWithContextWrapper;
readonly createPosition: ExpressionPositionFactory;
readonly createExpression: ExpressionFactory;
readonly createParameterCollection: FunctionParameterCollectionFactory;
}
export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp;
public constructor(
private readonly utilities: RegexParserUtilities = DefaultRegexParserUtilities,
) {
}
public findExpressions(code: string): IExpression[] {
return Array.from(this.findRegexExpressions(code));
}
protected abstract buildExpression(match: RegExpMatchArray): PrimitiveExpression;
private* findRegexExpressions(code: string): Iterable<IExpression> {
if (!code) {
throw new Error(
this.buildErrorMessageWithContext({ errorMessage: 'missing code', code: 'EMPTY' }),
);
}
const createErrorContext = (message: string): ErrorContext => ({ code, errorMessage: message });
const matches = this.doOrRethrow(
() => code.matchAll(this.regex),
createErrorContext('Failed to match regex.'),
);
for (const match of matches) {
const primitiveExpression = this.doOrRethrow(
() => this.buildExpression(match),
createErrorContext('Failed to build expression.'),
);
const position = this.doOrRethrow(
() => this.utilities.createPosition(match),
createErrorContext('Failed to create position.'),
);
const parameters = this.doOrRethrow(
() => createParameters(
primitiveExpression,
this.utilities.createParameterCollection(),
),
createErrorContext('Failed to create parameters.'),
);
const expression = this.doOrRethrow(
() => this.utilities.createExpression({
position,
evaluator: primitiveExpression.evaluator,
parameters,
}),
createErrorContext('Failed to create expression.'),
);
yield expression;
}
}
private doOrRethrow<T>(
action: () => T,
context: ErrorContext,
): T {
try {
return action();
} catch (error) {
throw this.utilities.wrapError(
error,
this.buildErrorMessageWithContext(context),
);
}
}
private buildErrorMessageWithContext(context: ErrorContext): string {
return [
context.errorMessage,
`Class name: ${this.constructor.name}`,
`Regex pattern used: ${this.regex}`,
`Code: ${context.code}`,
].join('\n');
}
}
interface ErrorContext {
readonly errorMessage: string,
readonly code: string,
}
function createParameters(
expression: PrimitiveExpression,
parameterCollection: IFunctionParameterCollection,
): IReadOnlyFunctionParameterCollection {
return (expression.parameters || [])
.reduce((parameters, parameter) => {
parameters.addParameter(parameter);
return parameters;
}, parameterCollection);
}
export interface PrimitiveExpression {
readonly evaluator: ExpressionEvaluator;
readonly parameters?: readonly FunctionParameter[];
}
export interface ExpressionFactory {
(
...args: ConstructorParameters<typeof Expression>
): IExpression;
}
const DefaultRegexParserUtilities: RegexParserUtilities = {
wrapError: wrapErrorWithAdditionalContext,
createPosition: createPositionFromRegexFullMatch,
createExpression: (...args) => new Expression(...args),
createParameterCollection: createFunctionParameterCollection,
};

View File

@@ -1,5 +1,4 @@
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter'; import { RegexParser, type PrimitiveExpression } from '../Parser/Regex/RegexParser';
import { RegexParser, type IPrimitiveExpression } from '../Parser/Regex/RegexParser';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
export class ParameterSubstitutionParser extends RegexParser { export class ParameterSubstitutionParser extends RegexParser {
@@ -12,11 +11,14 @@ export class ParameterSubstitutionParser extends RegexParser {
.expectExpressionEnd() .expectExpressionEnd()
.buildRegExp(); .buildRegExp();
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression { protected buildExpression(match: RegExpMatchArray): PrimitiveExpression {
const parameterName = match[1]; const parameterName = match[1];
const pipeline = match[2]; const pipeline = match[2];
return { return {
parameters: [new FunctionParameter(parameterName, false)], parameters: [{
name: parameterName,
isOptional: false,
}],
evaluator: (context) => { evaluator: (context) => {
const { argumentValue } = context.args.getArgument(parameterName); const { argumentValue } = context.args.getArgument(parameterName);
if (!pipeline) { if (!pipeline) {

View File

@@ -1,7 +1,6 @@
// eslint-disable-next-line max-classes-per-file // eslint-disable-next-line max-classes-per-file
import type { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser'; import type { IExpressionParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/IExpressionParser';
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection'; import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
import { ExpressionPosition } from '../Expression/ExpressionPosition'; import { ExpressionPosition } from '../Expression/ExpressionPosition';
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder'; import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory'; import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
@@ -84,7 +83,10 @@ class WithStatementBuilder {
public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression { public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression {
const parameters = new FunctionParameterCollection(); const parameters = new FunctionParameterCollection();
parameters.addParameter(new FunctionParameter(this.parameterName, true)); parameters.addParameter({
name: this.parameterName,
isOptional: true,
});
const position = new ExpressionPosition( const position = new ExpressionPosition(
this.startExpressionPosition.start, this.startExpressionPosition.start,
endExpressionPosition.end, endExpressionPosition.end,

View File

@@ -0,0 +1,41 @@
import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator';
import { validateParameterName, type ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator';
export interface FunctionCallArgument {
readonly parameterName: string;
readonly argumentValue: string;
}
export interface FunctionCallArgumentFactory {
(
parameterName: string,
argumentValue: string,
utilities?: FunctionCallArgumentFactoryUtilities,
): FunctionCallArgument;
}
export const createFunctionCallArgument: FunctionCallArgumentFactory = (
parameterName: string,
argumentValue: string,
utilities: FunctionCallArgumentFactoryUtilities = DefaultUtilities,
): FunctionCallArgument => {
utilities.validateParameterName(parameterName);
utilities.typeValidator.assertNonEmptyString({
value: argumentValue,
valueName: `Missing argument value for the parameter "${parameterName}".`,
});
return {
parameterName,
argumentValue,
};
};
interface FunctionCallArgumentFactoryUtilities {
readonly typeValidator: TypeValidator;
readonly validateParameterName: ParameterNameValidator;
}
const DefaultUtilities: FunctionCallArgumentFactoryUtilities = {
typeValidator: createTypeValidator(),
validateParameterName,
};

View File

@@ -1,10 +1,10 @@
import type { IFunctionCallArgument } from './IFunctionCallArgument'; import type { FunctionCallArgument } from './FunctionCallArgument';
import type { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection'; import type { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection { export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
private readonly arguments = new Map<string, IFunctionCallArgument>(); private readonly arguments = new Map<string, FunctionCallArgument>();
public addArgument(argument: IFunctionCallArgument): void { public addArgument(argument: FunctionCallArgument): void {
if (this.hasArgument(argument.parameterName)) { if (this.hasArgument(argument.parameterName)) {
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`); throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
} }
@@ -22,7 +22,7 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
return this.arguments.has(parameterName); return this.arguments.has(parameterName);
} }
public getArgument(parameterName: string): IFunctionCallArgument { public getArgument(parameterName: string): FunctionCallArgument {
if (!parameterName) { if (!parameterName) {
throw new Error('missing parameter name'); throw new Error('missing parameter name');
} }

View File

@@ -1,11 +1,11 @@
import type { IFunctionCallArgument } from './IFunctionCallArgument'; import type { FunctionCallArgument } from './FunctionCallArgument';
export interface IReadOnlyFunctionCallArgumentCollection { export interface IReadOnlyFunctionCallArgumentCollection {
getArgument(parameterName: string): IFunctionCallArgument; getArgument(parameterName: string): FunctionCallArgument;
getAllParameterNames(): string[]; getAllParameterNames(): string[];
hasArgument(parameterName: string): boolean; hasArgument(parameterName: string): boolean;
} }
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection { export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
addArgument(argument: IFunctionCallArgument): void; addArgument(argument: FunctionCallArgument): void;
} }

View File

@@ -1,4 +1,4 @@
import type { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; import type { ISharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionCollection';
import type { FunctionCall } from '../FunctionCall'; import type { FunctionCall } from '../FunctionCall';
import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler'; import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler';

View File

@@ -1,4 +1,4 @@
import type { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; import type { ISharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionCollection';
import type { CompiledCode } from './CompiledCode'; import type { CompiledCode } from './CompiledCode';
import type { FunctionCall } from '../FunctionCall'; import type { FunctionCall } from '../FunctionCall';

View File

@@ -1,4 +1,4 @@
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import { NewlineCodeSegmentMerger } from './CodeSegmentJoin/NewlineCodeSegmentMerger'; import { NewlineCodeSegmentMerger } from './CodeSegmentJoin/NewlineCodeSegmentMerger';
import { AdaptiveFunctionCallCompiler } from './SingleCall/AdaptiveFunctionCallCompiler'; import { AdaptiveFunctionCallCompiler } from './SingleCall/AdaptiveFunctionCallCompiler';
import type { ISharedFunctionCollection } from '../../ISharedFunctionCollection'; import type { ISharedFunctionCollection } from '../../ISharedFunctionCollection';

View File

@@ -72,7 +72,7 @@ function throwIfUnexpectedParametersExist(
// eslint-disable-next-line prefer-template // eslint-disable-next-line prefer-template
`Function "${functionName}" has unexpected parameter(s) provided: ` `Function "${functionName}" has unexpected parameter(s) provided: `
+ `"${unexpectedParameters.join('", "')}"` + `"${unexpectedParameters.join('", "')}"`
+ '. Expected parameter(s): ' + '.\nExpected parameter(s): '
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'), + (expectedParameters.length ? `"${expectedParameters.join('", "')}".` : 'none'),
); );
} }

View File

@@ -1,5 +1,5 @@
import type { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import type { ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { CompiledCode } from '../CompiledCode'; import type { CompiledCode } from '../CompiledCode';
import type { FunctionCallCompilationContext } from '../FunctionCallCompilationContext'; import type { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';

View File

@@ -1,5 +1,5 @@
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
export interface ArgumentCompiler { export interface ArgumentCompiler {
createCompiledNestedCall( createCompiledNestedCall(

View File

@@ -1,17 +1,16 @@
import type { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; import type { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; import { FunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection'; import { ExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler';
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler';
import type { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; import { ParsedFunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall';
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall'; import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { createFunctionCallArgument, type FunctionCallArgument, type FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import type { ArgumentCompiler } from './ArgumentCompiler'; import type { ArgumentCompiler } from './ArgumentCompiler';
export class NestedFunctionArgumentCompiler implements ArgumentCompiler { export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
constructor( constructor(private readonly utilities: ArgumentCompilationUtilities = DefaultUtilities) { }
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
) { }
public createCompiledNestedCall( public createCompiledNestedCall(
nestedFunction: FunctionCall, nestedFunction: FunctionCall,
@@ -22,18 +21,24 @@ export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
nestedFunction, nestedFunction,
parentFunction.args, parentFunction.args,
context, context,
this.expressionsCompiler, this.utilities,
); );
const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs); const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs);
return compiledCall; return compiledCall;
} }
} }
interface ArgumentCompilationUtilities {
readonly expressionsCompiler: IExpressionsCompiler,
readonly wrapError: ErrorWithContextWrapper;
readonly createCallArgument: FunctionCallArgumentFactory;
}
function compileNestedFunctionArguments( function compileNestedFunctionArguments(
nestedFunction: FunctionCall, nestedFunction: FunctionCall,
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection, parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
context: FunctionCallCompilationContext, context: FunctionCallCompilationContext,
expressionsCompiler: IExpressionsCompiler, utilities: ArgumentCompilationUtilities,
): IReadOnlyFunctionCallArgumentCollection { ): IReadOnlyFunctionCallArgumentCollection {
const requiredParameterNames = context const requiredParameterNames = context
.allFunctions .allFunctions
@@ -47,7 +52,7 @@ function compileNestedFunctionArguments(
paramName, paramName,
nestedFunction, nestedFunction,
parentFunctionArgs, parentFunctionArgs,
expressionsCompiler, utilities,
), ),
})) }))
// Filter out arguments with absent values // Filter out arguments with absent values
@@ -67,7 +72,7 @@ function compileNestedFunctionArguments(
.map(({ .map(({
parameterName, parameterName,
compiledArgumentValue, compiledArgumentValue,
}) => new FunctionCallArgument(parameterName, compiledArgumentValue)); }) => utilities.createCallArgument(parameterName, compiledArgumentValue));
return buildArgumentCollectionFromArguments(compiledArguments); return buildArgumentCollectionFromArguments(compiledArguments);
} }
@@ -89,13 +94,13 @@ function compileArgument(
parameterName: string, parameterName: string,
nestedFunction: FunctionCall, nestedFunction: FunctionCall,
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection, parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
expressionsCompiler: IExpressionsCompiler, utilities: ArgumentCompilationUtilities,
): string { ): string {
try { try {
const { argumentValue: codeInArgument } = nestedFunction.args.getArgument(parameterName); const { argumentValue: codeInArgument } = nestedFunction.args.getArgument(parameterName);
return expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs); return utilities.expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs);
} catch (err) { } catch (error) {
throw new AggregateError([err], `Error when compiling argument for "${parameterName}"`); throw utilities.wrapError(error, `Error when compiling argument for "${parameterName}"`);
} }
} }
@@ -107,3 +112,9 @@ function buildArgumentCollectionFromArguments(
return compiledArgs; return compiledArgs;
}, new FunctionCallArgumentCollection()); }, new FunctionCallArgumentCollection());
} }
const DefaultUtilities: ArgumentCompilationUtilities = {
expressionsCompiler: new ExpressionsCompiler(),
wrapError: wrapErrorWithAdditionalContext,
createCallArgument: createFunctionCallArgument,
};

View File

@@ -1,8 +1,8 @@
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; import { ExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler';
import type { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler';
import { FunctionBodyType, type ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { FunctionBodyType, type ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy'; import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy { export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {

View File

@@ -1,14 +1,21 @@
import { type CallFunctionBody, FunctionBodyType, type ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import {
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; type CallFunctionBody, FunctionBodyType,
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; type ISharedFunction,
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler'; import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler';
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy'; import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
import type { ArgumentCompiler } from './Argument/ArgumentCompiler'; import type { ArgumentCompiler } from './Argument/ArgumentCompiler';
export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy { export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
public constructor( public constructor(
private readonly argumentCompiler: ArgumentCompiler = new NestedFunctionArgumentCompiler(), private readonly argumentCompiler: ArgumentCompiler
= new NestedFunctionArgumentCompiler(),
private readonly wrapError: ErrorWithContextWrapper
= wrapErrorWithAdditionalContext,
) { ) {
} }
@@ -29,8 +36,11 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
const compiledNestedCall = context.singleCallCompiler const compiledNestedCall = context.singleCallCompiler
.compileSingleCall(compiledParentCall, context); .compileSingleCall(compiledParentCall, context);
return compiledNestedCall; return compiledNestedCall;
} catch (err) { } catch (error) {
throw new AggregateError([err], `Error with call to "${nestedCall.functionName}" function from "${callToFunction.functionName}" function`); throw this.wrapError(
error,
`Failed to call '${nestedCall.functionName}' (callee function) from '${callToFunction.functionName}' (caller function).`,
);
} }
}).flat(); }).flat();
} }

View File

@@ -0,0 +1,80 @@
import type {
FunctionCallData,
FunctionCallsData,
FunctionCallParametersData,
} from '@/application/collections/';
import { isArray, isPlainObject } from '@/TypeHelpers';
import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator';
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
import { ParsedFunctionCall } from './ParsedFunctionCall';
import { createFunctionCallArgument, type FunctionCallArgumentFactory } from './Argument/FunctionCallArgument';
import type { FunctionCall } from './FunctionCall';
export interface FunctionCallsParser {
(
calls: FunctionCallsData,
utilities?: FunctionCallParsingUtilities,
): FunctionCall[];
}
interface FunctionCallParsingUtilities {
readonly typeValidator: TypeValidator;
readonly createCallArgument: FunctionCallArgumentFactory;
}
const DefaultUtilities: FunctionCallParsingUtilities = {
typeValidator: createTypeValidator(),
createCallArgument: createFunctionCallArgument,
};
export const parseFunctionCalls: FunctionCallsParser = (
calls,
utilities = DefaultUtilities,
) => {
const sequence = getCallSequence(calls, utilities.typeValidator);
return sequence.map((call) => parseFunctionCall(call, utilities));
};
function getCallSequence(calls: FunctionCallsData, validator: TypeValidator): FunctionCallData[] {
if (!isPlainObject(calls) && !isArray(calls)) {
throw new Error('called function(s) must be an object or array');
}
if (isArray(calls)) {
validator.assertNonEmptyCollection({
value: calls,
valueName: 'function call sequence',
});
return calls as FunctionCallData[];
}
const singleCall = calls as FunctionCallData;
return [singleCall];
}
function parseFunctionCall(
call: FunctionCallData,
utilities: FunctionCallParsingUtilities,
): FunctionCall {
utilities.typeValidator.assertObject({
value: call,
valueName: 'function call',
allowedProperties: ['function', 'parameters'],
});
const callArgs = parseArgs(call.parameters, utilities.createCallArgument);
return new ParsedFunctionCall(call.function, callArgs);
}
function parseArgs(
parameters: FunctionCallParametersData | undefined,
createArgument: FunctionCallArgumentFactory,
): FunctionCallArgumentCollection {
const parametersMap = parameters ?? {};
return Object.keys(parametersMap)
.map((parameterName) => {
const argumentValue = parametersMap[parameterName];
return createArgument(parameterName, argumentValue);
})
.reduce((args, arg) => {
args.addArgument(arg);
return args;
}, new FunctionCallArgumentCollection());
}

View File

@@ -1,4 +1,4 @@
export interface IFunctionParameter { export interface FunctionParameter {
readonly name: string; readonly name: string;
readonly isOptional: boolean; readonly isOptional: boolean;
} }

View File

@@ -1,14 +1,14 @@
import type { IFunctionParameterCollection } from './IFunctionParameterCollection'; import type { IFunctionParameterCollection } from './IFunctionParameterCollection';
import type { IFunctionParameter } from './IFunctionParameter'; import type { FunctionParameter } from './FunctionParameter';
export class FunctionParameterCollection implements IFunctionParameterCollection { export class FunctionParameterCollection implements IFunctionParameterCollection {
private parameters = new Array<IFunctionParameter>(); private parameters = new Array<FunctionParameter>();
public get all(): readonly IFunctionParameter[] { public get all(): readonly FunctionParameter[] {
return this.parameters; return this.parameters;
} }
public addParameter(parameter: IFunctionParameter) { public addParameter(parameter: FunctionParameter) {
this.ensureValidParameter(parameter); this.ensureValidParameter(parameter);
this.parameters.push(parameter); this.parameters.push(parameter);
} }
@@ -17,7 +17,7 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
return this.parameters.find((existingParameter) => existingParameter.name === name); return this.parameters.find((existingParameter) => existingParameter.name === name);
} }
private ensureValidParameter(parameter: IFunctionParameter) { private ensureValidParameter(parameter: FunctionParameter) {
if (this.includesName(parameter.name)) { if (this.includesName(parameter.name)) {
throw new Error(`duplicate parameter name: "${parameter.name}"`); throw new Error(`duplicate parameter name: "${parameter.name}"`);
} }

View File

@@ -0,0 +1,12 @@
import { FunctionParameterCollection } from './FunctionParameterCollection';
import type { IFunctionParameterCollection } from './IFunctionParameterCollection';
export interface FunctionParameterCollectionFactory {
(
...args: ConstructorParameters<typeof FunctionParameterCollection>
): IFunctionParameterCollection;
}
export const createFunctionParameterCollection: FunctionParameterCollectionFactory = (...args) => {
return new FunctionParameterCollection(...args);
};

View File

@@ -0,0 +1,21 @@
import type { ParameterDefinitionData } from '@/application/collections/';
import { validateParameterName, type ParameterNameValidator } from '../Shared/ParameterNameValidator';
import type { FunctionParameter } from './FunctionParameter';
export interface FunctionParameterParser {
(
data: ParameterDefinitionData,
validator?: ParameterNameValidator,
): FunctionParameter;
}
export const parseFunctionParameter: FunctionParameterParser = (
data,
validator = validateParameterName,
) => {
validator(data.name);
return {
name: data.name,
isOptional: data.optional || false,
};
};

View File

@@ -0,0 +1,9 @@
import type { FunctionParameter } from './FunctionParameter';
export interface IReadOnlyFunctionParameterCollection {
readonly all: readonly FunctionParameter[];
}
export interface IFunctionParameterCollection extends IReadOnlyFunctionParameterCollection {
addParameter(parameter: FunctionParameter): void;
}

View File

@@ -0,0 +1,22 @@
import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator';
export interface ParameterNameValidator {
(
parameterName: string,
typeValidator?: TypeValidator,
): void;
}
export const validateParameterName = (
parameterName: string,
typeValidator = createTypeValidator(),
) => {
typeValidator.assertNonEmptyString({
value: parameterName,
valueName: 'parameter name',
rule: {
expectedMatch: /^[0-9a-zA-Z]+$/,
errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`,
},
});
};

View File

@@ -15,7 +15,7 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
if (!name) { throw Error('missing function name'); } if (!name) { throw Error('missing function name'); }
const func = this.functionsByName.get(name); const func = this.functionsByName.get(name);
if (!func) { if (!func) {
throw new Error(`called function is not defined "${name}"`); throw new Error(`Called function is not defined: "${name}"`);
} }
return func; return func;
} }

View File

@@ -1,58 +1,79 @@
import type { import type {
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction, FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData,
CallInstruction, ParameterDefinitionData,
} from '@/application/collections/'; } from '@/application/collections/';
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator'; import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines'; import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines'; import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator'; import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers'; import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction'; import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
import { SharedFunctionCollection } from './SharedFunctionCollection'; import { SharedFunctionCollection } from './SharedFunctionCollection';
import { FunctionParameter } from './Parameter/FunctionParameter'; import { parseFunctionCalls, type FunctionCallsParser } from './Call/FunctionCallsParser';
import { FunctionParameterCollection } from './Parameter/FunctionParameterCollection'; import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from './Parameter/FunctionParameterCollectionFactory';
import { parseFunctionCalls } from './Call/FunctionCallParser'; import { parseFunctionParameter, type FunctionParameterParser } from './Parameter/FunctionParameterParser';
import type { FunctionParameter } from './Parameter/FunctionParameter';
import type { ISharedFunctionCollection } from './ISharedFunctionCollection'; import type { ISharedFunctionCollection } from './ISharedFunctionCollection';
import type { ISharedFunctionsParser } from './ISharedFunctionsParser';
import type { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; import type { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
import type { ISharedFunction } from './ISharedFunction'; import type { ISharedFunction } from './ISharedFunction';
export class SharedFunctionsParser implements ISharedFunctionsParser { export interface SharedFunctionsParser {
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser(); (
constructor(private readonly codeValidator: ICodeValidator = CodeValidator.instance) { }
public parseFunctions(
functions: readonly FunctionData[], functions: readonly FunctionData[],
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
): ISharedFunctionCollection { utilities?: SharedFunctionsParsingUtilities,
const collection = new SharedFunctionCollection(); ): ISharedFunctionCollection;
if (!functions.length) { }
return collection;
} export const parseSharedFunctions: SharedFunctionsParser = (
ensureValidFunctions(functions); functions: readonly FunctionData[],
return functions syntax: ILanguageSyntax,
.map((func) => parseFunction(func, syntax, this.codeValidator)) utilities = DefaultUtilities,
.reduce((acc, func) => { ) => {
acc.addFunction(func); const collection = new SharedFunctionCollection();
return acc; if (!functions.length) {
}, collection); return collection;
} }
ensureValidFunctions(functions);
return functions
.map((func) => parseFunction(func, syntax, utilities))
.reduce((acc, func) => {
acc.addFunction(func);
return acc;
}, collection);
};
const DefaultUtilities: SharedFunctionsParsingUtilities = {
wrapError: wrapErrorWithAdditionalContext,
parseParameter: parseFunctionParameter,
codeValidator: CodeValidator.instance,
createParameterCollection: createFunctionParameterCollection,
parseFunctionCalls,
};
interface SharedFunctionsParsingUtilities {
readonly wrapError: ErrorWithContextWrapper;
readonly parseParameter: FunctionParameterParser;
readonly codeValidator: ICodeValidator;
readonly createParameterCollection: FunctionParameterCollectionFactory;
readonly parseFunctionCalls: FunctionCallsParser;
} }
function parseFunction( function parseFunction(
data: FunctionData, data: FunctionData,
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
validator: ICodeValidator, utilities: SharedFunctionsParsingUtilities,
): ISharedFunction { ): ISharedFunction {
const { name } = data; const { name } = data;
const parameters = parseParameters(data); const parameters = parseParameters(data, utilities);
if (hasCode(data)) { if (hasCode(data)) {
validateCode(data, syntax, validator); validateCode(data, syntax, utilities.codeValidator);
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode); return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
} }
// Has call // Has call
const calls = parseFunctionCalls(data.call); const calls = utilities.parseFunctionCalls(data.call);
return createCallerFunction(name, parameters, calls); return createCallerFunction(name, parameters, calls);
} }
@@ -71,22 +92,35 @@ function validateCode(
); );
} }
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection { function parseParameters(
data: FunctionData,
utilities: SharedFunctionsParsingUtilities,
): IReadOnlyFunctionParameterCollection {
return (data.parameters || []) return (data.parameters || [])
.map((parameter) => { .map((parameter) => parseParameterWithContextualError(
try { data.name,
return new FunctionParameter( parameter,
parameter.name, utilities,
parameter.optional || false, ))
);
} catch (err) {
throw new Error(`"${data.name}": ${err.message}`);
}
})
.reduce((parameters, parameter) => { .reduce((parameters, parameter) => {
parameters.addParameter(parameter); parameters.addParameter(parameter);
return parameters; return parameters;
}, new FunctionParameterCollection()); }, utilities.createParameterCollection());
}
function parseParameterWithContextualError(
functionName: string,
parameterData: ParameterDefinitionData,
utilities: SharedFunctionsParsingUtilities,
): FunctionParameter {
try {
return utilities.parseParameter(parameterData);
} catch (err) {
throw utilities.wrapError(
err,
`Failed to create parameter: ${parameterData.name} for function "${functionName}"`,
);
}
} }
function hasCode(data: FunctionData): data is CodeFunctionData { function hasCode(data: FunctionData): data is CodeFunctionData {

View File

@@ -1,7 +1,7 @@
import type { ScriptData } from '@/application/collections/'; import type { ScriptData } from '@/application/collections/';
import type { IScriptCode } from '@/domain/IScriptCode'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
export interface IScriptCompiler { export interface IScriptCompiler {
canCompile(script: ScriptData): boolean; canCompile(script: ScriptData): boolean;
compile(script: ScriptData): IScriptCode; compile(script: ScriptData): ScriptCode;
} }

View File

@@ -0,0 +1,87 @@
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
import { parseFunctionCalls } from './Function/Call/FunctionCallsParser';
import { parseSharedFunctions, type SharedFunctionsParser } from './Function/SharedFunctionsParser';
import type { CompiledCode } from './Function/Call/Compiler/CompiledCode';
import type { IScriptCompiler } from './IScriptCompiler';
import type { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
import type { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';
interface ScriptCompilerUtilities {
readonly sharedFunctionsParser: SharedFunctionsParser;
readonly callCompiler: FunctionCallCompiler;
readonly codeValidator: ICodeValidator;
readonly wrapError: ErrorWithContextWrapper;
readonly scriptCodeFactory: ScriptCodeFactory;
}
const DefaultUtilities: ScriptCompilerUtilities = {
sharedFunctionsParser: parseSharedFunctions,
callCompiler: FunctionCallSequenceCompiler.instance,
codeValidator: CodeValidator.instance,
wrapError: wrapErrorWithAdditionalContext,
scriptCodeFactory: createScriptCode,
};
interface CategoryCollectionDataContext {
readonly functions: readonly FunctionData[];
readonly syntax: ILanguageSyntax;
}
export class ScriptCompiler implements IScriptCompiler {
private readonly functions: ISharedFunctionCollection;
constructor(
categoryContext: CategoryCollectionDataContext,
private readonly utilities: ScriptCompilerUtilities = DefaultUtilities,
) {
this.functions = this.utilities.sharedFunctionsParser(
categoryContext.functions,
categoryContext.syntax,
);
}
public canCompile(script: ScriptData): boolean {
return hasCall(script);
}
public compile(script: ScriptData): ScriptCode {
try {
if (!hasCall(script)) {
throw new Error('Script does include any calls.');
}
const calls = parseFunctionCalls(script.call);
const compiledCode = this.utilities.callCompiler.compileFunctionCalls(calls, this.functions);
validateCompiledCode(compiledCode, this.utilities.codeValidator);
return this.utilities.scriptCodeFactory(
compiledCode.code,
compiledCode.revertCode,
);
} catch (error) {
throw this.utilities.wrapError(error, `Failed to compile script: ${script.name}`);
}
}
}
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
[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;
}

View File

@@ -0,0 +1,151 @@
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import { CollectionScript } from '@/domain/Executables/Script/CollectionScript';
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
import type { Script } from '@/domain/Executables/Script/Script';
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
import { parseDocs, type DocsParser } from '../DocumentationParser';
import { ExecutableType } from '../Validation/ExecutableType';
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
import { CodeValidator } from './Validation/CodeValidator';
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
import type { CategoryCollectionSpecificUtilities } from '../CategoryCollectionSpecificUtilities';
export interface ScriptParser {
(
data: ScriptData,
collectionUtilities: CategoryCollectionSpecificUtilities,
scriptUtilities?: ScriptParserUtilities,
): Script;
}
export const parseScript: ScriptParser = (
data,
collectionUtilities,
scriptUtilities = DefaultUtilities,
) => {
const validator = scriptUtilities.createValidator({
type: ExecutableType.Script,
self: data,
});
validateScript(data, validator);
try {
const script = scriptUtilities.createScript({
name: data.name,
code: parseCode(
data,
collectionUtilities,
scriptUtilities.codeValidator,
scriptUtilities.createCode,
),
docs: scriptUtilities.parseDocs(data),
level: parseLevel(data.recommend, scriptUtilities.levelParser),
});
return script;
} catch (error) {
throw scriptUtilities.wrapError(
error,
validator.createContextualErrorMessage('Failed to parse script.'),
);
}
};
function parseLevel(
level: string | undefined,
parser: EnumParser<RecommendationLevel>,
): RecommendationLevel | undefined {
if (!level) {
return undefined;
}
return parser.parseEnum(level, 'level');
}
function parseCode(
script: ScriptData,
collectionUtilities: CategoryCollectionSpecificUtilities,
codeValidator: ICodeValidator,
createCode: ScriptCodeFactory,
): ScriptCode {
if (collectionUtilities.compiler.canCompile(script)) {
return collectionUtilities.compiler.compile(script);
}
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
const code = createCode(codeScript.code, codeScript.revertCode);
validateHardcodedCodeWithoutCalls(code, codeValidator, collectionUtilities.syntax);
return code;
}
function validateHardcodedCodeWithoutCalls(
scriptCode: ScriptCode,
validator: ICodeValidator,
syntax: ILanguageSyntax,
) {
[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: ExecutableValidator,
): asserts script is NonNullable<ScriptData> {
validator.assertType((v) => v.assertObject<CallScriptData & CodeScriptData>({
value: script,
valueName: script.name ?? 'script',
allowedProperties: [
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
],
}));
validator.assertValidName(script.name);
validator.assert(
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
'Neither "call" or "code" is defined.',
);
validator.assert(
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
'Both "call" and "code" are defined.',
);
validator.assert(
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
'Both "call" and "revertCode" are defined.',
);
}
interface ScriptParserUtilities {
readonly levelParser: EnumParser<RecommendationLevel>;
readonly createScript: ScriptFactory;
readonly codeValidator: ICodeValidator;
readonly wrapError: ErrorWithContextWrapper;
readonly createValidator: ExecutableValidatorFactory;
readonly createCode: ScriptCodeFactory;
readonly parseDocs: DocsParser;
}
export type ScriptFactory = (
...parameters: ConstructorParameters<typeof CollectionScript>
) => Script;
const createScript: ScriptFactory = (...parameters) => {
return new CollectionScript(...parameters);
};
const DefaultUtilities: ScriptParserUtilities = {
levelParser: createEnumParser(RecommendationLevel),
createScript,
codeValidator: CodeValidator.instance,
wrapError: wrapErrorWithAdditionalContext,
createValidator: createExecutableDataValidator,
createCode: createScriptCode,
parseDocs,
};

View File

@@ -1,4 +1,4 @@
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import type { ICodeLine } from '../ICodeLine'; import type { ICodeLine } from '../ICodeLine';
import type { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule'; import type { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';

View File

@@ -1,4 +1,4 @@
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
const BatchFileCommonCodeParts = ['(', ')', 'else', '||']; const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
const PowerShellCommonCodeParts = ['{', '}']; const PowerShellCommonCodeParts = ['{', '}'];

View File

@@ -1,4 +1,4 @@
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
export class ShellScriptSyntax implements ILanguageSyntax { export class ShellScriptSyntax implements ILanguageSyntax {
public readonly commentDelimiters = ['#']; public readonly commentDelimiters = ['#'];

Some files were not shown because too many files have changed in this diff Show More