diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index 8b6339a..0000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index ba2b3cb..0000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,24 +0,0 @@
-module.exports = {
- root: true,
- parser: '@typescript-eslint/parser',
- parserOptions: {
- tsconfigRootDir: __dirname, // this is the reason this is a .js file
- project: ['./tsconfig.eslint.json'],
- },
- extends: [
- '@rubensworks'
- ],
- rules: {
- '@typescript-eslint/naming-convention': [
- 'error',
- {
- 'selector': 'interface',
- 'format': ['PascalCase'],
- 'custom': {
- 'regex': '^[A-Z]',
- 'match': true
- }
- }
- ],
- }
-};
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..55df9c3
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,37 @@
+This page contains several pointers for people that want to contribute to this project.
+
+## Setup development environment
+
+Start by cloning this repository.
+
+```bash
+$ git clone git@github.com:LinkedSoftwareDependencies/Components-Generator.js.git
+```
+
+This project requires [Node.js](https://nodejs.org/en/) `>=18.12` and [Yarn](https://yarnpkg.com/) `>=4` to be installed. Preferable, use the Yarn version provided and managed by Node.js' integrated [CorePack](https://yarnpkg.com/corepack) by running `corepack enable`.
+
+After that, you can install the project by running `yarn install`. This will automatically also run `yarn build`, which you can run again at any time to compile any changed code.
+
+## Continuous integration
+
+Given the critical nature of this project, we require a full (100%) test coverage.
+Additionally, we have configured strict linting rules.
+
+These checks are run automatically upon each commit, and via continuous integration.
+
+You can run them manually as follows:
+```bash
+$ yarn test
+$ yarn lint
+```
+
+## Code architecture
+
+The architecture is decomposed into 5 main packages:
+
+1. [`config`](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/tree/master/lib/config): Loading a generator from configuration files.
+2. [`generate`](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/tree/master/lib/generate): Generating component files by parsing type information from TypeScript and serializing it into JSON-LD.
+3. [`parse`](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/tree/master/lib/parse): Parsing components from TypeScript.
+4. [`resolution`](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/tree/master/lib/resolution): Resolution of dependencies.
+5. [`serialize`](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/tree/master/lib/serialize): Serializing components to JSON-LD.
+6. [`util`](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/tree/master/lib/util): Various utilities.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 270a72b..2742427 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,62 +1,86 @@
name: CI
on: [push, pull_request]
-jobs:
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+env:
+ DEFAULT_NODE_VERSION: 22.x
+
+jobs:
lint:
+ name: Lint
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - uses: actions/cache@v2
+ - name: Use Node.js ${{ env.DEFAULT_NODE_VERSION }}
+ uses: actions/setup-node@v6
with:
- path: '**/node_modules'
- key: ${{ runner.os }}-lint-modules-${{ hashFiles('**/yarn.lock') }}
- - uses: actions/setup-node@v2
+ node-version: ${{ env.DEFAULT_NODE_VERSION }}
+ - name: Enable corepack
+ run: corepack enable
+ - name: Ensure line endings are consistent
+ run: git config --global core.autocrlf input
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ - name: Load dependency cache
+ uses: actions/cache@v4
with:
- node-version: 14.x
- - run: yarn install
- - run: yarn run lint
-
+ path: '**/node_modules'
+ key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
+ - name: Install dependencies
+ run: yarn install --immutable
+ - name: Run ESLint
+ run: yarn run lint
test:
+ name: Test
+ needs: lint
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest, windows-latest, macos-latest]
+ os:
+ - ubuntu-latest
+ - windows-latest
+ - macos-latest
node-version:
- - 10.x
- - 12.x
- - 14.x
+ - 18.x
+ - 20.x
+ - 22.x
steps:
- name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
+ - name: Enable corepack
+ run: corepack enable
- name: Ensure line endings are consistent
run: git config --global core.autocrlf input
- - name: Check out repository
- uses: actions/checkout@v2
- - uses: actions/cache@v2
+ - name: Checkout repository
+ uses: actions/checkout@v5
+ - name: Load dependency cache
+ uses: actions/cache@v4
with:
path: '**/node_modules'
- key: ${{ runner.os }}-test-modules-${{ hashFiles('**/yarn.lock') }}
+ key: ${{ runner.os }}-modules-${{ hashFiles('yarn.lock') }}
- name: Install dependencies
- run: yarn install
+ run: yarn install --immutable
- name: Build project
run: yarn run build
- name: Run tests
run: yarn run test
- name: Submit coverage results
- uses: coverallsapp/github-action@master
+ uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.github_token }}
- flag-name: run-${{ matrix.node-version }}
+ flag-name: ${{ matrix.node-version }}-${{ matrix.os }}
parallel: true
-
coveralls:
+ name: Coverage
needs: test
runs-on: ubuntu-latest
steps:
- name: Consolidate test coverage from different jobs
- uses: coverallsapp/github-action@master
+ uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.github_token }}
parallel-finished: true
diff --git a/.gitignore b/.gitignore
index 5bdc52f..54d337c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,22 +1,12 @@
-**/.idea/
-**/node_modules/
-**/coverage/
-**/comunica_temp/
-**/local_temp/
-test_files/
-output/
.eslintcache
+.yarn/
+
+node_modules/
+coverage/
**/lib/**/*.js
**/lib/**/*.js.map
**/lib/**/*.d.ts
-**/test/**/*.js
-**/test/**/*.js.map
-**/test/**/*.d.ts
-!/test/data/**/*.d.ts
**/bin/**/*.js
**/bin/**/*.js.map
**/bin/**/*.d.ts
-**/index.js
-**/index.js.map
-**/index.d.ts
diff --git a/.husky/.gitignore b/.husky/.gitignore
deleted file mode 100644
index 31354ec..0000000
--- a/.husky/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-_
diff --git a/.husky/pre-commit b/.husky/pre-commit
index af44751..f4e15be 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,4 +1,3 @@
-#!/bin/sh
-. "$(dirname "$0")/_/husky.sh"
-
-npm run build && npm run lint && npm run test
+yarn run build
+yarn run lint
+yarn run test
diff --git a/.yarnrc.yml b/.yarnrc.yml
new file mode 100644
index 0000000..e8c211e
--- /dev/null
+++ b/.yarnrc.yml
@@ -0,0 +1,3 @@
+compressionLevel: mixed
+enableGlobalCache: false
+nodeLinker: node-modules
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bbac654..05eb77b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,230 @@
# Changelog
All notable changes to this project will be documented in this file.
+
+## [v4.3.0](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v4.2.0...v4.3.0) - 2024-10-23
+
+### Added
+* [Support @json ranges in arrays](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/10d7e48c489f3d5fcaa59015a4f409ee31047ba1)
+
+
+## [v4.2.0](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v4.1.0...v4.2.0) - 2024-09-27
+
+### Changed
+* [Update to jsonld-context-parser v3](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/26fbe7e60df4cbaa4a41845b34ee14d73d72e4b6)
+
+
+## [v4.1.0](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v4.0.1...v4.1.0) - 2024-09-17
+
+### Added
+* [Output the default values of generic types](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/7b85bcd0f59fd1b3a284a286dad7e6c9307cf4e2)
+
+
+## [v4.0.1](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v4.0.0...v4.0.1) - 2024-03-05
+
+### Fixed
+* [Fix invalid postinstall script](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/de5b89666a661a21b13c54e256551e2f720bf61f)
+
+
+## [v4.0.0](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.1.2...v4.0.0) - 2024-03-05
+
+### Changed
+* [Bump @typescript-eslint/typescript-estree to v7](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/e96ef49eba3c3c682348888f01cdf5a317274b08)
+* [Update to Components.js v6](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/dc60e5ce3a0097276971393312ba34b58da19d8d)
+
+
+## [v3.1.2](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.1.1...v3.1.2) - 2023-06-15
+
+### Fixed
+* [Fix failing ESM imports with file extension](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/7301c3e40b11f811fc377cefe8a984ef262eaff4)
+
+
+## [v3.1.1](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.1.0...v3.1.1) - 2023-06-01
+
+### Fixed
+* [Ensure paths are treated correctly on Windows (#116)](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/2a2a766bbc474954235cbea85558411ae03ecaf4)
+
+
+## [v3.1.0](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.4...v3.1.0) - 2022-08-12
+
+### Changed
+* [Prevent ClassLoader from throwing error on invalid packages, Closes #95](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/5f8e23c98f6bd4a9b436461ef8832bbb4e336705)
+* [Prevent ExternalModulesLoader from throwing error on invalid packages, Closes #95](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/016a371b4bddef005319cf151de9e07ed15c1307)
+
+
+## [v3.0.5](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.4...v3.0.5) - 2022-06-27
+
+### Fixed
+* [Fix incorrect handling of recursive types](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/5f7e8d48783e59a1dcec416175f8ef657d3b7470)
+
+
+## [v3.0.4](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.3...v3.0.4) - 2022-06-01
+
+### Fixed
+* [Fix process halting on recursive types](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/9375d1478b361ade2a61958f7310ff29efd525d2)
+
+
+## [v3.0.3](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.1...v3.0.3) - 2022-04-27
+
+### Fixed
+* [Fix resolution of an index.d.ts inside a directory, Closes #99](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/8b8229090a73dcc1b7762d358952c2a1e2a5209b)
+
+
+## [v3.0.2](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.1...v3.0.2) - 2022-03-15
+
+### Fixed
+* [Add path expansion for Windows users, Closes #96](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/3a0d109d6b9d46f976c4212826c02d0e3d448489)
+
+
+## [v3.0.1](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.0...v3.0.1) - 2022-03-02
+
+### Fixed
+* [Fix Components.js version range being too strict](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/7c64fc71d80ead5fcc7fc84e5f51a4df0c1e6551)
+
+
+## [v3.0.0](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.0-beta.7...v3.0.0) - 2022-03-01
+
+### Changed
+* [Update to CJS 5](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/f2be05ba2065116c9b2bf405fd84e8d1ce0155c1)
+* [Update dependency @types/node to v16](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/66dfae6b27d923c795f6bc455272fb8c95e0a048)
+
+
+## [v3.0.0-beta.7](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.0-beta.6...v3.0.0-beta.7) - 2022-02-08
+
+### Added
+* [Add support for never param types as wildcard, Closes #85](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/67b4972e66db8710469b0f678852f5c9fa52e5f5)
+
+
+## [v3.0.0-beta.6](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.0-beta.5...v3.0.0-beta.6) - 2022-01-29
+
+### Added
+* [Add lenient generation mode to ignore unsupported lang features](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/6654e77c8d9ff98cdbbd9639b64e79d9817087d1)
+* [Support indexed access type param ranges](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/823deb987b50efadaa0e22fb2ea7e637a7ea37ae)
+* [Support keyof typeof enum as union of enum keys](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/bc15b19c7789d70e3639be9194dd81054cdde561)
+
+### Changed
+* [Emit typed memberFields instead of memberKeys](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/d58bfa24097617ca75e066f027b91589b91f5e4b)
+* [Ignore TSMappedType during generation](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/82082ffa9af45bde8e8bbbde720990664f886a44)
+* [Ignore unsupported function types](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/e40fef9f936cc0d39e25703f092aad61717fac6e)
+
+### Fixed
+* [Fix empty context shortcuts being generated in some cases](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/458d2dea522c6898984baa0dd1b05e3835aad394)
+
+
+## [v3.0.0-beta.5](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.0-beta.4...v3.0.0-beta.5) - 2022-01-17
+
+### Added
+* [Generate wildcard parameter ranges](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/193ea080f4bfbc4e4591b2609eb5a4bf554f605e)
+* Improve generics support:
+ * [Output generic type instances on class/iface extensions](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/3c7528ea4b774a808d053a8e55bc1f513ca9f022)
+ * [Generate wrapped comp extensions as GenericComponentExtension](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/110e3832e45958e4a974fd7a057b292b2be18f6b)
+* [Handle references to external packages that do no expose components](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/22c43c03ed5fa3da7b4c4b6e06cd71e9eb0c048e)
+* [Cache interface range resolution](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/c184e4722c5cda54aafce7ca92bf070551d6baee)
+
+### Fixed
+* [Inherit qualified path on class/iface chains](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/75d962c52423890ffb4ec9d27f88bf29a712149b)
+* [Fix resolution not working on type-only packages, Closes #83](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/e1c0126a7a396cd7792385bb1bdda9984cfd1626)
+* [Don't resolve as nested fields when handling extension data](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/a7401ef79e84d3ddef88ee7bf2008cdb91506b5e)
+* [Emit @type of ExtensionDefinitions](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/5545a4ebb116ea06f13a374dd65c658bf85dd15c)
+* [Fix qualified path incorrectly propagating to extension defs](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/2b12135a83d7132545b426cf8c2ecb75fd0a0d22)
+* [Don't throw on hash range when getNestedFields is false](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/f87ea4c03aca4bc7b2741aebbac669b2d84c6ca6)
+* [Fix generics not being generated for interfaces](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/facf5fc164321d9a2ec1be92c75d2f493f722120)
+
+
+## [v3.0.0-beta.4](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.0-beta.3...v3.0.0-beta.4) - 2021-12-09
+
+### Added
+* [Handle keyof param ranges](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/4e0df62cf6d2189a0c338f7b33f7d61b47209ab1)
+* [Export component member keys, for keyof checking](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/391aa833632b5bc3aac4eaeb020ed60b7695c845)
+* [Handle generics in type aliases](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/1983f478d52e7ebbb1c5e9bab99f6fe5172d53ba)
+* [Allow fieldNameToId to refer to other packages](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/3a25ad74a91cb8a149d80f02f8cdc186d5e8db57)
+* Support qualified paths:
+ * [Support qualified paths in ignored components](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/d4703c82c017fc43974b59e5be62015df4e55b1b)
+ * [Support parameters referring to components with qualified paths](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/8a4e112ccf776b6aa025925947e6c42aeecffa7d)
+* [Support enum and enum value parameter ranges](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/96e86972b3f20fdfa387a4891ee832ffdddb6da9)
+
+### Changed
+* [Return undefined for nested parameter ranges](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/de7cde1f2cef6a339eb427bce0536ce31fbaba97)
+* * [Improve error message for unsupported nested fields](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/05b9e150cd5312c2cc424b0be30317e06ff3bb5b)
+
+### Fixed
+* [Fix stackoverflow on generic remapping with self-references](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/326f31fb5ceab75bf238448693893b4ceba212b5)
+
+
+
+## [v3.0.0-beta.3](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.0-beta.2...v3.0.0-beta.3) - 2021-12-07
+
+### Added
+* [Generate all relevant generic type data of classes and params](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/4245abd23b6b16363fa5cce47f72a61d351f9884)
+
+
+## [v3.0.0-beta.2](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.0-beta.1...v3.0.0-beta.2) - 2021-12-02
+
+### Fixed
+* [Fix class types not being overridable, Closs #82](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/1be3a39ca317d6e64648566c716769da1c8c8826)
+
+
+## [v3.0.0-beta.1](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v3.0.0-beta.0...v3.0.0-beta.1) - 2021-12-02
+
+### Added
+* [Allow type aliases for interfaces and classes](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/c480f717ad35ec0e8c1cc67dbacf9e7eac69fd25)
+
+
+## [v3.0.0-beta.0](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v2.6.0...v3.0.0-beta.0) - 2021-11-30
+
+_Requires Components.js >= 5.0.0_
+
+### BREAKING CHANGES
+* [Enable type scoped context functionality by default](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/40bcc44c99674e467b7c3b0c9b9d6b0c563f283b): This means that the `--typeScopedContexts` CLI option should not be passed anymore.
+* [Make component URLs dereferenceable](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/783670cc6ff91b5d33bb5431b7f817e73acd9548): This improves the URL strategy for components, and results in different component URLs.
+* Align with Components.js range changes:
+ * [Remove 'unique' field option in favor of array param type](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/8d60dcd0479b960857f87758bf02ddf6fdf44b47)
+ * [Remove 'required' field option in favor of union with undefined](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/e65f0e8bc8f1ea0b05472e630cebb784fe2ea525)
+ * [Explicitly serialize undefined parameter ranges](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/cb37fb5e82292e46eab76e96d364db25e2d21f67)
+* [Set minimum Node version to 12](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/23bf2634dba4ec95955293f4e91545e340cea1eb)
+
+### Added
+* [Allow configuration using config file](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/bbdd36b0dd38a49151c467f46d3bd2014933b820)
+ * [Allow package paths to be ignored](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/a6d47ce00ab8c3c5b0e70bcdc8cdf8f07e6c34d4)
+* Improve TypeScript language support:
+ * [Support components defined within namespaces via export assignment](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/d5582d2126138643f88cf0398218e1eb9883a227)
+ * [Support imports for packages that have external @types packages](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/12996b70c2b2a3b82dbfe99dbceb943ea1294cf4)
+ * [Support type literals and type aliases](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/ff2fdc103cb6a658ac1741322686832e8cc88d96)
+ * [Generate param ranges with tuples and rest types](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/348b80dcbb47d5888cd1ac8a724bc848209e16c1)
+ * [Generate param ranges with union and intersection types](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/7796c816295886a4702e8037f441fac9559891a6)
+* Improve tag support:
+ * [Allow multiple @default values to be defined](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/6b0177210f7608b0d284da87cf40d432c5c4132b)
+ * [Fix @ symbols not being allowed in comment data](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/26e2f405db6d0cf6de5d3fd1e7d46681205e9e07)
+ * [Allow setting default JSON values](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/61e0b13bfaced9e9c8b2c36f3c89d588cf76f9b2)
+ * [Allow default IRI values to be relative](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/d400e1fc56aca4c52f194213a1090651604e8941)
+ * [Allow defaultNested tags to be added with typed values](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/0ec8fa9d6cf470965c0ce6ce148f65cbf751916c)
+ * [Keep structural param type information on overrides](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/b05cb60473ec839718c9a1fa3ac42ee670a21224)
+ * [Allow default values to be IRIs when wrapped in <>](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/7275824082cb180c9abe5f4f8084b48c4ee5e98f)
+* [Add shorter param entries in type-scoped context when possible.](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/3b21fb75448cac9288045ca12f330040cefa2a08)
+* [Enable generation of multiple packages in bulk](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/86467d2b43dd1006aff937644afa51f7cdcb522d): This allows this generator to be used in monorepos
+* [Add option to dump debug state](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/ae0b5278bb9d93f513f70fb5731d727817b46d97)
+* [Allow constructor comment data inheritance from supers](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/3527ceb4d8f8e0d259dffba0dacc393b011cd4c1)
+* [Allow interface args to extend from other interfaces, Closes #73](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/b5724be7b30fb3d6a57f25bbe4e3fc534c9db4f5)
+* [Also consider recursive deps when loading external modules](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/a6596e0b87600359be7b6daa04adda25066fc062)
+* [Allow components from other packages to be re-exported](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/1ddb44afbf594973971829efcdcae7d1855950c8)
+
+### Fixed
+* [Fix default values not being in an RDF list when needed](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/e9761232748ea7bb1d2a6e58664011a6092ec092)
+* [Fix @list not being applied on optional arrays](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/078f78c9ed2bc33e27344d28307df4fd776eca57)
+* [Fix crash when loading fields from super interface chains](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/3de032af5adef3a2a2c1bfcf1a350d4306be38b7)
+
+### Changed
+* [Reduce unneeded logging](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/75ff04cb1f040e7e25bc1d3486bb946e0c37d364)
+* [Ignore imports that fail](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/f73798f6b9bcd8d9d2ba35d9963d2869a783e8b0)
+* [Allow components to be part of multiple modules](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/32350fa72830892adf27304619fad1b64e8b2eae)
+* [Fix param range resources using @type instead of @id](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/1fbad2f8f91c91ce0528b85df690a1356fbd6ed0)
+
+
+## [v2.6.1](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v2.6.0...v2.6.1) - 2021-09-29
+
+### Fixed
+* [Fix optional types not always being parsed correctly, Closes #74](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/commit/c95294a929452faed872838d5e8bbd2bcde13e3d)
+
## [v2.6.0](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/compare/v2.5.0...v2.6.0) - 2021-07-20
diff --git a/README.md b/README.md
index b38d0c8..448b4b5 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@
[](https://github.com/LinkedSoftwareDependencies/Components-Generator.js/actions?query=workflow%3ACI)
[](https://coveralls.io/github/LinkedSoftwareDependencies/Components-Generator.js?branch=master)
[](https://www.npmjs.com/package/componentsjs-generator)
+[](https://doi.org/10.5281/zenodo.5644902)
This is a tool to automatically generate `.jsonld` component files from TypeScript classes
for the [Components.js](https://github.com/LinkedSoftwareDependencies/Components.js) dependency injection framework.
@@ -90,10 +91,14 @@ When invoking `componentsjs-generator`,
this tool will automatically generate `.jsonld` components files for all TypeScript files
that are exported by the current package.
+For monorepos, multiple package paths may be provided.
+
```bash
Generates component file for a package
Usage:
componentsjs-generator
+ Arguments:
+ path/to/package The directories of the packages to look in, defaults to working directory
Options:
-p path/to/package The directory of the package to look in, defaults to working directory
-s lib Relative path to directory containing source files, defaults to 'lib'
@@ -101,19 +106,42 @@ Usage:
-e jsonld Extension for components files (without .), defaults to 'jsonld'
-i ignore-classes.json Relative path to an optional file with class names to ignore
-r prefix Optional custom JSON-LD module prefix
+ --lenient If unsupported language features must produce a warning instead of an error
+ --debugState If a 'componentsjs-generator-debug-state.json' file should be created with debug information
--help Show information about this command
-
- Experimental options:
- --typeScopedContexts If a type-scoped context for each component is to be generated with parameter name aliases
```
**Note:** This generator will read `.d.ts` files,
so it is important that you invoke the TypeScript compiler (`tsc`) _before_ using this tool.
+### Configuration files
+
+While options passed to the CLI tool will always take precedence,
+it is possible to add a `.componentsjs-generator-config.json` file to your project to define your configuration.
+
+The following shows an example of the possible options:
+```json
+{
+ "source": "lib",
+ "destination": "components",
+ "extension": "jsonld",
+ "ignorePackagePaths": ["path/to/package-ignored1", "path/to/package-ignored2"],
+ "ignoreComponents": ["Class1", "Class2"],
+ "logLevel": "info",
+ "modulePrefix": "myprefix",
+ "debugState": "true",
+ "hardErrorUnsupported": false
+}
+```
+
+When invoking `componentsjs-generator`, the tool will look for `.componentsjs-generator-config.json` in the current working directory.
+If it can not find one, it will recursively go look into the parent directories until it either finds one or is at the root.
+
### Ignoring classes
If you don't want components to be generated for certain classes,
-then you can pass a JSON file to the `-i` option containing an array of class names to skip.
+then you can either add it to the `ignoreComponents` array of the `.componentsjs-generator-config.json` file (as explained above),
+or you can pass a JSON file to the `-i` option containing an array of class names to skip.
For example, invoking `componentsjs-generator -i ignore-classes.json` will skip `BadClass` if the contents of `ignore-classes.json` are:
```json
@@ -141,8 +169,8 @@ export class MyClass extends OtherClass {
/**
* @param paramA - My parameter
*/
- constructor(paramA: boolean, paramB: number) {
-
+ constructor(paramA: boolean, paramB: number, paramC: string[]) {
+
}
}
```
@@ -165,20 +193,24 @@ Component file:
{
"@id": "ex:MyFile#MyClass_paramA",
"range": "xsd:boolean",
- "comment": "My parameter",
- "unique": true,
- "required": true
+ "comment": "My parameter"
},
{
"@id": "ex:MyFile#MyClass_paramB",
- "range": "xsd:integer",
- "unique": true,
- "required": true
+ "range": "xsd:integer"
+ },
+ {
+ "@id": "ex:MyFile#MyClass_paramC",
+ "range": {
+ "@type": "ParameterRangeArray",
+ "parameterRangeValue": "xsd:integer"
+ }
}
],
"constructorArguments": [
{ "@id": "ex:MyFile#MyClass_paramA" },
- { "@id": "ex:MyFile#MyClass_paramB" }
+ { "@id": "ex:MyFile#MyClass_paramB" },
+ { "@id": "ex:MyFile#MyClass_paramC" }
]
}
]
@@ -189,29 +221,38 @@ Component file:
Each argument in the constructor of the class must be one of the following:
-* A primitive type such as `boolean, number, string`, which will be mapped to an [XSD type](https://componentsjs.readthedocs.io/en/latest/configuration/components/parameters/)
+* A primitive type such as `boolean, number, string`, which will be mapped to an [XSD type](https://componentsjs.readthedocs.io/en/latest/configuration/components/parameters/)
* Another class, which will be mapped to the component `@id`.
-* A hash or interface containing key-value pairs where each value matches one of the possible options. Nesting is allowed.
-* An array of any of the allowed types.
-
-Here is an example that showcases all of these options:
+* A record or interface containing key-value pairs where each value matches one of the possible options. Nesting is allowed.
+* Reference to a generic type that is defined on the class.
+* An array, `keyof`, tuple, union, or intersection over any of the allowed types.
+
+Here is an example that showcases all of these options:
```typescript
import {Logger} from "@comunica/core";
export class SampleActor {
- constructor(args:HashArg, testArray:HashArg[], numberSample: number, componentExample: Logger) {}
+ constructor(
+ args: HashArg,
+ number: number,
+ component: Logger,
+ array: HashArg[],
+ complexComposition: (SomeClass & OtherClass) | string,
+ complexTuple: [ number, SomeClass, ...string[] ],
+ optional?: number,
+ ) {}
}
export interface HashArg {
args: NestedHashArg;
- arraySample: NestedHashArg[];
+ array: NestedHashArg[];
}
export interface NestedHashArg extends ExtendsTest {
test: boolean;
- componentTest: Logger;
+ component: Logger;
}
export interface ExtendsTest {
- stringTest: String;
+ string: string;
}
-```
+```
### Argument tags
@@ -222,8 +263,18 @@ Using comment tags, arguments can be customized.
| Tag | Action
|---|---
| `@ignored` | This field will be ignored.
-| `@default {}` | The `default` attribute of the parameter will be set to ``
-| `@range {}` | The `range` attribute of the parameter will be set to ``. You can only use values that fit the type of field. Options: `json, boolean, int, integer, number, byte, long, float, decimal, double, string`. For example, if your field has the type `number`, you could explicitly mark it as a `float` by using `@range {float}`. See [the documentation](https://componentsjs.readthedocs.io/en/latest/configuration/components/parameters/).
+| `@default {value}` | The `default` attribute of the parameter will be set to `value`. See section below for acceptable values.
+| `@defaultNested {value} path_to_args` | When the given parameter accepts a nested object (child links delimited by `_`), the `default` attribute of this nested field will be set to `value`. See section below for acceptable values.
+| `@range {type}` | The `range` attribute of the parameter will be set to `type`. You can only use values that fit the type of field. Options: `json, boolean, int, integer, number, byte, long, float, decimal, double, string`. For example, if your field has the type `number`, you could explicitly mark it as a `float` by using `@range {float}`. See [the documentation](https://componentsjs.readthedocs.io/en/latest/configuration/components/parameters/).
+
+##### Default values
+
+Default values accept a microsyntax, in which several types of values may be provided:
+
+* Literal values: `@default {abc}`
+* IRI values: `@default {}`
+* Blank-node-based instantiation: `@default {a }`
+* IRI-based instantiation: `@default { a }`
#### Examples
@@ -235,7 +286,7 @@ export class MyActor {
/**
* @param myByte - This is an array of bytes @range {byte}
* @param ignoredArg - @ignored
- */
+ */
constructor(myByte: number[], ignoredArg: string) {
}
@@ -250,9 +301,10 @@ Component file:
"parameters": [
{
"@id": "my-actor#TestClass#myByte",
- "range": "xsd:byte",
- "required": false,
- "unique": false,
+ "range": {
+ "@type": "ParameterRangeArray",
+ "parameterRangeValue": "xsd:byte"
+ },
"comment": "This is an array of bytes"
}
],
@@ -274,7 +326,7 @@ export class MyActor {
/**
* @param myValue - Values will be passed as parsed JSON @range {json}
* @param ignoredArg - @ignored
- */
+ */
constructor(myValue: any, ignoredArg: string) {
}
@@ -290,8 +342,6 @@ Component file:
{
"@id": "my-actor#TestClass#myValue",
"range": "rdf:JSON",
- "required": false,
- "unique": false,
"comment": "Values will be passed as parsed JSON"
}
],
@@ -314,7 +364,7 @@ When instantiating TestClass as follows, its JSON value will be passed directly
"someKey": {
"someOtherKey1": 1,
"someOtherKey2": "abc"
- }
+ }
}
}
```
@@ -335,7 +385,7 @@ export interface IActorBindingArgs {
* @range {float}
* @default {5.0}
*/
- floatField?: number;
+ floatField: number;
}
```
@@ -348,8 +398,6 @@ Component file:
{
"@id": "my-actor#floatField",
"range": "xsd:float",
- "required": false,
- "unique": true,
"default": "5.0",
"comment": "This field is very important"
}
diff --git a/bin/componentsjs-generator.ts b/bin/componentsjs-generator.ts
index e2d6258..aa9f702 100755
--- a/bin/componentsjs-generator.ts
+++ b/bin/componentsjs-generator.ts
@@ -1,59 +1,58 @@
#!/usr/bin/env node
-import * as fs from 'fs';
-import * as Path from 'path';
+import * as fs from 'node:fs';
import * as minimist from 'minimist';
-import { Generator } from '../lib/generate/Generator';
+import { GeneratorFactory } from '../lib/config/GeneratorFactory';
import { ResolutionContext } from '../lib/resolution/ResolutionContext';
+import { joinFilePath, normalizeFilePath } from '../lib/util/PathUtil';
function showHelp(): void {
process.stderr.write(`Generates components files for TypeScript files in a package
Usage:
componentsjs-generator
+ Arguments:
+ path/to/package The directories of the packages to look in, defaults to working directory
Options:
- -p path/to/package The directory of the package to look in, defaults to working directory
-s lib Relative path to directory containing source files, defaults to 'lib'
-c components Relative path to directory that will contain components files, defaults to 'components'
-e jsonld Extension for components files (without .), defaults to 'jsonld'
-i ignore-classes.json Relative path to an optional file with class names to ignore
-l info The logger level
-r prefix Optional custom JSON-LD module prefix
+ --lenient If unsupported language features must produce a warning instead of an error
+ --debugState If a 'componentsjs-generator-debug-state.json' file should be created with debug information
--help Show information about this command
-
- Experimental options:
- --typeScopedContexts If a type-scoped context for each component is to be generated with parameter name aliases
`);
process.exit(1);
}
const args = minimist(process.argv.slice(2));
+
+// TODO: remove in next major version
+if (args.typeScopedContexts) {
+ process.stderr.write(`The flag '--typeScopedContexts' must not be used anymore, as this is default behaviour as of version 3.x\n`);
+ process.exit(1);
+}
+
if (args.help) {
showHelp();
} else {
- const packageRootDirectory = Path.posix.join(process.cwd(), args.p || '');
- const generator = new Generator({
- resolutionContext: new ResolutionContext(),
- pathDestination: {
- packageRootDirectory,
- originalPath: Path.posix.join(packageRootDirectory, args.s || 'lib'),
- replacementPath: Path.posix.join(packageRootDirectory, args.c || 'components'),
- },
- fileExtension: args.e || 'jsonld',
- typeScopedContexts: args.typeScopedContexts,
- logLevel: args.l || 'info',
- prefix: args.r,
- ignoreClasses: args.i ?
- // eslint-disable-next-line no-sync
- JSON.parse(fs.readFileSync(args.i, 'utf8')).reduce((acc: Record, entry: string) => {
- acc[entry] = true;
- return acc;
- }, {}) :
- [],
- });
- generator
- .generateComponents()
+ const packageRootDirectories = (args._.length > 0 ? args._ : [ '' ])
+ .map(path => joinFilePath(normalizeFilePath(process.cwd()), path))
+ .flatMap((path) => {
+ // Since path expansion does not work on Windows, we may receive wildcard paths, so let's expand those here
+ if (path.endsWith('*')) {
+ path = path.slice(0, -1);
+ // eslint-disable-next-line no-sync
+ return fs.readdirSync(path)
+ .map(subFile => joinFilePath(path, subFile));
+ }
+ return path;
+ });
+ new GeneratorFactory({ resolutionContext: new ResolutionContext() })
+ .createGenerator(normalizeFilePath(process.cwd()), args, packageRootDirectories)
+ .then(generator => generator.generateComponents())
.catch((error: Error) => {
process.stderr.write(`${error.message}\n`);
process.exit(1);
});
}
-
diff --git a/debug b/debug
deleted file mode 100644
index a0eb262..0000000
--- a/debug
+++ /dev/null
@@ -1,136 +0,0 @@
-{
- "@context": [
- "https://linkedsoftwaredependencies.org/bundles/npm/@comunica/actor-init-sparql/^1.0.0/components/context.jsonld",
- "https://linkedsoftwaredependencies.org/bundles/npm/@comunica/core/^1.0.0/components/context.jsonld",
- "https://linkedsoftwaredependencies.org/bundles/npm/@comunica/bus-init/^1.0.0/components/context.jsonld"
- ],
- "@id": "npmd:@comunica/actor-init-sparql",
- "components": [
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql",
- "requireElement": "ActorInitSparql",
- "@type": "Class",
- "comment": "A comunica SPARQL Init Actor.",
- "parameters": [
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorOptimizeQueryOperation",
- "required": true,
- "unique": true,
- "range": "cc:Mediator"
- },
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorQueryOperation",
- "required": true,
- "unique": true,
- "range": "cc:Mediator"
- },
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorSparqlParse",
- "required": true,
- "unique": true,
- "range": "cc:Mediator"
- },
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorSparqlSerialize",
- "required": true,
- "unique": true,
- "range": "cc:Mediator"
- },
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorSparqlSerializeMediaTypeCombiner",
- "required": true,
- "unique": true,
- "range": "cc:Mediator"
- },
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorContextPreprocess",
- "required": true,
- "unique": true,
- "range": "cc:Mediator"
- },
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorHttpInvalidate",
- "required": true,
- "unique": true,
- "range": "cc:Mediator"
- },
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql#logger",
- "required": true,
- "unique": true,
- "range": "cc:Logger"
- },
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql#queryString",
- "required": false,
- "unique": true,
- "range": "xsd:string"
- },
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql#defaultQueryInputFormat",
- "required": false,
- "unique": true,
- "range": "xsd:string"
- },
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql#context",
- "required": false,
- "unique": true,
- "range": "xsd:string"
- }
- ],
- "constructorArguments": [
- {
- "@id": "npmd:@comunica/actor-init-sparql/ActorInitSparql#constructorArgumentsObject",
- "extends": "cbi:Actor/Init/constructorArgumentsObject",
- "fields": [
- {
- "keyRaw": "mediatorOptimizeQueryOperation",
- "value": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorOptimizeQueryOperation"
- },
- {
- "keyRaw": "mediatorQueryOperation",
- "value": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorQueryOperation"
- },
- {
- "keyRaw": "mediatorSparqlParse",
- "value": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorSparqlParse"
- },
- {
- "keyRaw": "mediatorSparqlSerialize",
- "value": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorSparqlSerialize"
- },
- {
- "keyRaw": "mediatorSparqlSerializeMediaTypeCombiner",
- "value": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorSparqlSerializeMediaTypeCombiner"
- },
- {
- "keyRaw": "mediatorContextPreprocess",
- "value": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorContextPreprocess"
- },
- {
- "keyRaw": "mediatorHttpInvalidate",
- "value": "npmd:@comunica/actor-init-sparql/ActorInitSparql#mediatorHttpInvalidate"
- },
- {
- "keyRaw": "logger",
- "value": "npmd:@comunica/actor-init-sparql/ActorInitSparql#logger"
- },
- {
- "keyRaw": "queryString",
- "value": "npmd:@comunica/actor-init-sparql/ActorInitSparql#queryString"
- },
- {
- "keyRaw": "defaultQueryInputFormat",
- "value": "npmd:@comunica/actor-init-sparql/ActorInitSparql#defaultQueryInputFormat"
- },
- {
- "keyRaw": "context",
- "value": "npmd:@comunica/actor-init-sparql/ActorInitSparql#context"
- }
- ]
- }
- ]
- }
- ]
-}
\ No newline at end of file
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..40b572b
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,56 @@
+const config = require('@rubensworks/eslint-config');
+
+module.exports = config([
+ {
+ files: [ '**/*.ts' ],
+ languageOptions: {
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: [ './tsconfig.eslint.json' ],
+ },
+ },
+ },
+ {
+ files: [ '**/*.ts' ],
+ rules: {
+ 'import/no-nodejs-modules': 'off',
+ 'ts/naming-convention': [
+ 'error',
+ {
+ selector: 'interface',
+ format: [ 'PascalCase' ],
+ custom: {
+ regex: '^[A-Z]',
+ match: true,
+ },
+ },
+ ],
+ // TODO: check if we can enable the following
+ 'ts/no-require-imports': 'off',
+ 'ts/no-unsafe-assignment': 'off',
+ 'ts/no-unsafe-argument': 'off',
+ 'ts/no-unsafe-return': 'off',
+ },
+ },
+ {
+ // Specific rules for NodeJS-specific files
+ files: [
+ '**/test/**/*.ts',
+ ],
+ rules: {
+ 'import/no-nodejs-modules': 'off',
+ 'unused-imports/no-unused-vars': 'off',
+ 'ts/no-require-imports': 'off',
+ 'ts/no-var-requires': 'off',
+ 'ts/no-extraneous-class': 'off',
+ // TODO: check if we can enable the following
+ 'node/no-path-concat': 'off',
+ },
+ },
+ {
+ // Files that do not require linting
+ ignores: [
+ '**/file-invalid.d.ts',
+ ],
+ },
+]);
diff --git a/index.ts b/index.ts
deleted file mode 100644
index e7d6fcc..0000000
--- a/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './lib/generate/Generator';
diff --git a/jest.config.ts b/jest.config.ts
new file mode 100644
index 0000000..3744bf3
--- /dev/null
+++ b/jest.config.ts
@@ -0,0 +1,31 @@
+import type { Config } from '@jest/types';
+
+const config: Config.InitialOptions = {
+ collectCoverage: true,
+ coveragePathIgnorePatterns: [
+ '/test/',
+ ],
+ coverageProvider: 'babel',
+ coverageThreshold: {
+ global: {
+ branches: 100,
+ functions: 100,
+ lines: 100,
+ statements: 100,
+ },
+ },
+ moduleFileExtensions: [
+ 'ts',
+ 'js',
+ ],
+ testEnvironment: 'node',
+ testMatch: [
+ '/test/**/*.test.ts',
+ '/packages/*/test/**/*-test.ts',
+ ],
+ transform: {
+ '\\.ts$': 'ts-jest',
+ },
+};
+
+export default config;
diff --git a/lib/config/FileConfigLoader.ts b/lib/config/FileConfigLoader.ts
new file mode 100644
index 0000000..7f13539
--- /dev/null
+++ b/lib/config/FileConfigLoader.ts
@@ -0,0 +1,52 @@
+import type { ResolutionContext } from '../resolution/ResolutionContext';
+import { joinFilePath } from '../util/PathUtil';
+import type { GeneratorConfig } from './GeneratorConfig';
+
+/**
+ * Loads the `.componentsjs-generator-config.json` config file from the file system.
+ */
+export class FileConfigLoader {
+ public static readonly DEFAULT_CONFIG_NAME = '.componentsjs-generator-config.json';
+
+ private readonly resolutionContext: ResolutionContext;
+
+ public constructor(args: FileConfigLoaderArgs) {
+ this.resolutionContext = args.resolutionContext;
+ }
+
+ /**
+ * Get the closest config file, starting from the current working directory and following parent directory links.
+ * @param cwd The current working directory
+ */
+ public async getClosestConfigFile(cwd: string): Promise | undefined> {
+ for (const directory of this.getConsideredDirectories(cwd)) {
+ const configPath = joinFilePath(directory, FileConfigLoader.DEFAULT_CONFIG_NAME);
+ try {
+ const textContents = await this.resolutionContext.getFileContent(configPath);
+ return JSON.parse(textContents);
+ } catch {
+ // Ignore error
+ }
+ }
+ }
+
+ /**
+ * All directories that need to be considered when looking for the config file.
+ * @param cwd The current working directory
+ */
+ public getConsideredDirectories(cwd: string): string[] {
+ // Since Windows paths can have `/` or `\` depending on the operations done so far
+ // it is safest to split on both possible separators.
+ const sections: string[] = cwd.split(/[/\\]/u);
+ const paths: string[] = [];
+ for (let i = sections.length; i > 1; i--) {
+ // Slash is valid on both platforms and keeps results consistent
+ paths.push(sections.slice(0, i).join('/'));
+ }
+ return paths;
+ }
+}
+
+export interface FileConfigLoaderArgs {
+ resolutionContext: ResolutionContext;
+}
diff --git a/lib/config/GeneratorConfig.ts b/lib/config/GeneratorConfig.ts
new file mode 100644
index 0000000..001e164
--- /dev/null
+++ b/lib/config/GeneratorConfig.ts
@@ -0,0 +1,44 @@
+/**
+ * Represents a generator config
+ */
+export interface GeneratorConfig {
+ /**
+ * Relative path to directory containing source files, defaults to 'lib'.
+ */
+ source: string;
+ /**
+ * Relative path to directory that will contain components files, defaults to 'components'.
+ */
+ destination: string;
+ /**
+ * Extension for components files (without .), defaults to 'jsonld'.
+ */
+ extension: string;
+ /**
+ * Paths to packages that should be excluded from generation.
+ * This can be used in monorepos where not all packages require component generation.
+ */
+ ignorePackagePaths: string[];
+ /**
+ * Relative path to an optional file with class names to ignore.
+ */
+ ignoreComponents: string[];
+ /**
+ * The logger level, defaults to 'info'.
+ */
+ logLevel: string;
+ /**
+ * Optional custom JSON-LD module prefix, defaults to an auto-generated value.
+ * May also be a mapping from package name to prefix.
+ */
+ modulePrefix: string | undefined | Record;
+ /**
+ * If a 'componentsjs-generator-debug-state.json' file should be created with debug information.
+ */
+ debugState: boolean;
+ /**
+ * If unsupported language features should cause a hard crash.
+ * Otherwise they are emitted as warning instead of error.
+ */
+ hardErrorUnsupported: boolean;
+}
diff --git a/lib/config/GeneratorFactory.ts b/lib/config/GeneratorFactory.ts
new file mode 100644
index 0000000..51122a8
--- /dev/null
+++ b/lib/config/GeneratorFactory.ts
@@ -0,0 +1,109 @@
+import type { LogLevel } from 'componentsjs';
+import { Generator } from '../generate/Generator';
+import type { ResolutionContext } from '../resolution/ResolutionContext';
+import { joinFilePath } from '../util/PathUtil';
+import { FileConfigLoader } from './FileConfigLoader';
+import type { GeneratorConfig } from './GeneratorConfig';
+
+/**
+ * Constructs a {@link Generator} with the proper configs.
+ *
+ * It will consider the following configs in order of priority:
+ * * CLI arguments
+ * * .componentsjs-generator-config.json
+ * * Default values
+ */
+export class GeneratorFactory {
+ private readonly resolutionContext: ResolutionContext;
+
+ public constructor(args: GeneratorFactoryArgs) {
+ this.resolutionContext = args.resolutionContext;
+ }
+
+ public async createGenerator(
+ cwd: string,
+ cliArgs: Record,
+ packageRootDirectories: string[],
+ ): Promise {
+ const config = await this.getConfig(cwd, cliArgs);
+ return new Generator({
+ resolutionContext: this.resolutionContext,
+ pathDestinations: packageRootDirectories
+ .filter(packageRootDirectory => !config.ignorePackagePaths
+ .some(ignorePackagePath => packageRootDirectory.startsWith(joinFilePath(cwd, ignorePackagePath))))
+ .map(packageRootDirectory => ({
+ packageRootDirectory,
+ originalPath: joinFilePath(packageRootDirectory, config.source),
+ replacementPath: joinFilePath(packageRootDirectory, config.destination),
+ })),
+ fileExtension: config.extension,
+ logLevel: config.logLevel,
+ debugState: config.debugState,
+ prefixes: config.modulePrefix,
+ ignoreClasses: config.ignoreComponents.reduce((acc: Record, entry: string) => {
+ acc[entry] = true;
+ return acc;
+ }, {}),
+ hardErrorUnsupported: config.hardErrorUnsupported,
+ });
+ }
+
+ public async getConfig(cwd: string, cliArgs: Record): Promise {
+ const defaultConfig = this.getDefaultConfig();
+ const fileConfig = await new FileConfigLoader({ resolutionContext: this.resolutionContext })
+ .getClosestConfigFile(cwd);
+ const cliConfig = await this.getCliConfig(cliArgs);
+ return {
+ ...defaultConfig,
+ ...fileConfig,
+ ...cliConfig,
+ };
+ }
+
+ public getDefaultConfig(): GeneratorConfig {
+ return {
+ source: 'lib',
+ destination: 'components',
+ extension: 'jsonld',
+ ignorePackagePaths: [],
+ ignoreComponents: [],
+ logLevel: 'info',
+ modulePrefix: undefined,
+ debugState: false,
+ hardErrorUnsupported: true,
+ };
+ }
+
+ public async getCliConfig(cliArgs: Record): Promise> {
+ const config: Partial = {};
+ if (cliArgs.s) {
+ config.source = cliArgs.s;
+ }
+ if (cliArgs.c) {
+ config.destination = cliArgs.c;
+ }
+ if (cliArgs.e) {
+ config.extension = cliArgs.e;
+ }
+ if (cliArgs.i) {
+ config.ignoreComponents = JSON.parse(await this.resolutionContext.getFileContent(cliArgs.i));
+ }
+ if (cliArgs.l) {
+ config.logLevel = cliArgs.l;
+ }
+ if (cliArgs.r) {
+ config.modulePrefix = cliArgs.r;
+ }
+ if (cliArgs.debugState) {
+ config.debugState = cliArgs.debugState;
+ }
+ if (cliArgs.lenient) {
+ config.hardErrorUnsupported = false;
+ }
+ return config;
+ }
+}
+
+export interface GeneratorFactoryArgs {
+ resolutionContext: ResolutionContext;
+}
diff --git a/lib/generate/Generator.ts b/lib/generate/Generator.ts
index cdfbff8..3a8ebcb 100644
--- a/lib/generate/Generator.ts
+++ b/lib/generate/Generator.ts
@@ -2,11 +2,17 @@ import { ComponentsManagerBuilder } from 'componentsjs/lib/loading/ComponentsMan
import { PrefetchedDocumentLoader } from 'componentsjs/lib/rdf/PrefetchedDocumentLoader';
import type { LogLevel } from 'componentsjs/lib/util/LogLevel';
import { ContextParser } from 'jsonld-context-parser';
+import { BulkPackageMetadataLoader } from '../parse/BulkPackageMetadataLoader';
import { ClassFinder } from '../parse/ClassFinder';
import { ClassIndexer } from '../parse/ClassIndexer';
import { ClassLoader } from '../parse/ClassLoader';
+import { CommentLoader } from '../parse/CommentLoader';
import { ConstructorLoader } from '../parse/ConstructorLoader';
+import { GenericsLoader } from '../parse/GenericsLoader';
+import { MemberLoader } from '../parse/MemberLoader';
+import type { PackageMetadata } from '../parse/PackageMetadataLoader';
import { PackageMetadataLoader } from '../parse/PackageMetadataLoader';
+import { ParameterLoader } from '../parse/ParameterLoader';
import { ParameterResolver } from '../parse/ParameterResolver';
import { ExternalModulesLoader } from '../resolution/ExternalModulesLoader';
import type { ResolutionContext } from '../resolution/ResolutionContext';
@@ -20,98 +26,142 @@ import { ContextConstructor } from '../serialize/ContextConstructor';
*/
export class Generator {
private readonly resolutionContext: ResolutionContext;
- private readonly pathDestination: PathDestinationDefinition;
+ private readonly pathDestinations: PathDestinationDefinition[];
private readonly fileExtension: string;
private readonly ignoreClasses: Record;
- private readonly typeScopedContexts: boolean;
private readonly logLevel: LogLevel;
- private readonly prefix?: string;
+ private readonly debugState: boolean;
+ private readonly prefixes?: string | Record;
+ private readonly hardErrorUnsupported: boolean;
public constructor(args: GeneratorArgs) {
this.resolutionContext = args.resolutionContext;
- this.pathDestination = args.pathDestination;
+ this.pathDestinations = args.pathDestinations;
this.fileExtension = args.fileExtension;
this.ignoreClasses = args.ignoreClasses;
- this.typeScopedContexts = args.typeScopedContexts;
this.logLevel = args.logLevel;
- this.prefix = args.prefix;
+ this.debugState = args.debugState;
+ this.prefixes = args.prefixes;
+ this.hardErrorUnsupported = args.hardErrorUnsupported;
}
public async generateComponents(): Promise {
const logger = ComponentsManagerBuilder.createLogger(this.logLevel);
- // Load package metadata
- const packageMetadata = await new PackageMetadataLoader({ resolutionContext: this.resolutionContext,
- prefix: this.prefix })
- .load(this.pathDestination.packageRootDirectory);
-
- const classLoader = new ClassLoader({ resolutionContext: this.resolutionContext, logger });
+ const commentLoader = new CommentLoader();
+ const classLoader = new ClassLoader({ resolutionContext: this.resolutionContext, logger, commentLoader });
const classFinder = new ClassFinder({ classLoader });
const classIndexer = new ClassIndexer({ classLoader, classFinder, ignoreClasses: this.ignoreClasses, logger });
+ const parameterLoader = new ParameterLoader({
+ commentLoader,
+ hardErrorUnsupported: this.hardErrorUnsupported,
+ logger,
+ });
+ const parameterResolver = new ParameterResolver({
+ classLoader,
+ parameterLoader,
+ ignoreClasses: this.ignoreClasses,
+ });
+
+ // Preload package metadata for all provided paths
+ const { packageMetadatas, pathMetadatas } = await new BulkPackageMetadataLoader({
+ packageMetadataLoader: new PackageMetadataLoader({
+ resolutionContext: this.resolutionContext,
+ prefixes: this.prefixes,
+ }),
+ logger,
+ }).load(this.pathDestinations);
- // Find all relevant classes
- const packageExports = await classFinder.getPackageExports(packageMetadata.name, packageMetadata.typesPath);
- const classAndInterfaceIndex = await classIndexer.createIndex(packageExports);
+ logger.info(`Generating components for ${Object.keys(packageMetadatas).length} package${Object.keys(packageMetadatas).length > 1 ? 's' : ''}`);
- // Load constructor data
- const constructorsUnresolved = new ConstructorLoader().getConstructors(classAndInterfaceIndex);
- const constructors = await new ParameterResolver({ classLoader, ignoreClasses: this.ignoreClasses })
- .resolveAllConstructorParameters(constructorsUnresolved, classAndInterfaceIndex);
+ // Generate components for all provided paths
+ for (const pathDestination of this.pathDestinations) {
+ // Load package metadata
+ const packageMetadata: PackageMetadata = pathMetadatas[pathDestination.packageRootDirectory];
+ if (!packageMetadata) {
+ continue;
+ }
- // Load external components
- const externalModulesLoader = new ExternalModulesLoader({
- pathDestination: this.pathDestination,
- packageMetadata,
- logger,
- });
- const externalPackages = externalModulesLoader.findExternalPackages(classAndInterfaceIndex, constructors);
- const externalComponents = await externalModulesLoader.loadExternalComponents(require, externalPackages);
+ // Find all relevant classes
+ const packageExports = await classFinder.getPackageExports(packageMetadata.name, packageMetadata.typesPath);
+ const classAndInterfaceIndex = await classIndexer.createIndex(packageExports);
- // Create components
- const contextConstructor = new ContextConstructor({
- packageMetadata,
- typeScopedContexts: this.typeScopedContexts,
- });
- const componentConstructor = new ComponentConstructor({
- packageMetadata,
- contextConstructor,
- pathDestination: this.pathDestination,
- classAndInterfaceIndex,
- classConstructors: constructors,
- externalComponents,
- contextParser: new ContextParser({
- documentLoader: new PrefetchedDocumentLoader({
- contexts: externalComponents.moduleState.contexts,
- logger,
+ // Load constructor data
+ const constructorsUnresolved = new ConstructorLoader({ parameterLoader }).getConstructors(classAndInterfaceIndex);
+ const constructors = await parameterResolver.resolveAllConstructorParameters(constructorsUnresolved);
+
+ // Load generics data
+ const genericsUnresolved = new GenericsLoader({ parameterLoader }).getGenerics(classAndInterfaceIndex);
+ const generics = await parameterResolver.resolveAllGenericTypeParameterData(genericsUnresolved);
+
+ // Load extensions data
+ const extensionsUnresolved = parameterLoader.loadAllExtensionData(classAndInterfaceIndex);
+ const extensions = await parameterResolver.resolveAllExtensionData(extensionsUnresolved, classAndInterfaceIndex);
+
+ // Load members data
+ const membersUnresolved = new MemberLoader({ parameterLoader }).getMembers(classAndInterfaceIndex);
+ const members = await parameterResolver.resolveAllMemberParameterData(membersUnresolved);
+
+ // Load external components
+ const externalModulesLoader = new ExternalModulesLoader({
+ pathDestination,
+ packageMetadata,
+ packagesBeingGenerated: packageMetadatas,
+ resolutionContext: this.resolutionContext,
+ debugState: this.debugState,
+ logger,
+ });
+ const externalPackages = externalModulesLoader.findExternalPackages(classAndInterfaceIndex, constructors);
+ const externalComponents = await externalModulesLoader.loadExternalComponents(require, externalPackages);
+
+ // Create components
+ const contextConstructor = new ContextConstructor({ packageMetadata });
+ const componentConstructor = new ComponentConstructor({
+ packageMetadata,
+ fileExtension: this.fileExtension,
+ contextConstructor,
+ pathDestination,
+ classAndInterfaceIndex,
+ classConstructors: constructors,
+ classGenerics: generics,
+ classExtensions: extensions,
+ classMembers: members,
+ externalComponents,
+ contextParser: new ContextParser({
+ documentLoader: new PrefetchedDocumentLoader({
+ contexts: externalComponents.moduleState.contexts,
+ logger,
+ }),
+ skipValidation: true,
}),
- skipValidation: true,
- }),
- });
- const components = await componentConstructor.constructComponents();
- const componentsIndex = await componentConstructor.constructComponentsIndex(components, this.fileExtension);
-
- // Serialize components
- const componentSerializer = new ComponentSerializer({
- resolutionContext: this.resolutionContext,
- pathDestination: this.pathDestination,
- fileExtension: this.fileExtension,
- indentation: ' ',
- });
- await componentSerializer.serializeComponents(components);
- await componentSerializer.serializeComponentsIndex(componentsIndex);
+ });
+ const components = await componentConstructor.constructComponents();
+ const componentsIndex = await componentConstructor.constructComponentsIndex(components);
+
+ // Serialize components
+ const componentSerializer = new ComponentSerializer({
+ resolutionContext: this.resolutionContext,
+ pathDestination,
+ fileExtension: this.fileExtension,
+ indentation: ' ',
+ });
+ await componentSerializer.serializeComponents(components);
+ await componentSerializer.serializeComponentsIndex(componentsIndex);
- // Serialize context
- const context = contextConstructor.constructContext(components);
- await componentSerializer.serializeContext(context);
+ // Serialize context
+ const context = contextConstructor.constructContext(components);
+ await componentSerializer.serializeContext(context);
+ }
}
}
export interface GeneratorArgs {
resolutionContext: ResolutionContext;
- pathDestination: PathDestinationDefinition;
+ pathDestinations: PathDestinationDefinition[];
fileExtension: string;
ignoreClasses: Record;
- typeScopedContexts: boolean;
logLevel: LogLevel;
- prefix?: string;
+ debugState: boolean;
+ prefixes?: string | Record;
+ hardErrorUnsupported: boolean;
}
diff --git a/lib/index.ts b/lib/index.ts
new file mode 100644
index 0000000..f9ec371
--- /dev/null
+++ b/lib/index.ts
@@ -0,0 +1 @@
+export * from './generate/Generator';
diff --git a/lib/parse/BulkPackageMetadataLoader.ts b/lib/parse/BulkPackageMetadataLoader.ts
new file mode 100644
index 0000000..244464d
--- /dev/null
+++ b/lib/parse/BulkPackageMetadataLoader.ts
@@ -0,0 +1,74 @@
+import { PrefetchedDocumentLoader } from 'componentsjs';
+import { ContextParser } from 'jsonld-context-parser';
+import type { Logger } from 'winston';
+import type { PackageMetadataScope } from '../resolution/ExternalModulesLoader';
+import type { PathDestinationDefinition } from '../serialize/ComponentConstructor';
+import { ContextConstructor } from '../serialize/ContextConstructor';
+import type { PackageMetadata, PackageMetadataLoader } from './PackageMetadataLoader';
+
+/**
+ * Load metadata from multiple packages in bulk.
+ */
+export class BulkPackageMetadataLoader {
+ private readonly packageMetadataLoader: PackageMetadataLoader;
+ private readonly logger: Logger;
+
+ public constructor(args: BulkPackageMetadataLoaderArgs) {
+ this.packageMetadataLoader = args.packageMetadataLoader;
+ this.logger = args.logger;
+ }
+
+ /**
+ * Load the metadata from the given packages.
+ * @param pathDestinations Package paths.
+ */
+ public async load(pathDestinations: PathDestinationDefinition[]): Promise {
+ const packageMetadatas: Record = {};
+ const pathMetadatas: Record = {};
+ const minimalContextParser = new ContextParser({
+ documentLoader: new PrefetchedDocumentLoader({
+ contexts: {},
+ logger: this.logger,
+ }),
+ skipValidation: true,
+ });
+
+ for (const pathDestination of pathDestinations) {
+ let packageMetadata: PackageMetadata;
+ try {
+ // Load package metadata
+ packageMetadata = await this.packageMetadataLoader.load(pathDestination.packageRootDirectory);
+ const contextConstructor = new ContextConstructor({ packageMetadata });
+
+ // Save the metadata for later use
+ packageMetadatas[packageMetadata.name] = {
+ packageMetadata,
+ pathDestination,
+ minimalContext: await minimalContextParser.parse(contextConstructor.constructContext()),
+ };
+ pathMetadatas[pathDestination.packageRootDirectory] = packageMetadata;
+ } catch (error: unknown) {
+ // Skip invalid packages
+ this.logger.warn(`Skipped generating invalid package at "${pathDestination.packageRootDirectory}": ${( error).message}`);
+ }
+ }
+
+ return { packageMetadatas, pathMetadatas };
+ }
+}
+
+export interface BulkPackageMetadataLoaderArgs {
+ packageMetadataLoader: PackageMetadataLoader;
+ logger: Logger;
+}
+
+export interface BulkPackageMetadataOutput {
+ /**
+ * Maps package name to scoped package metadata.
+ */
+ packageMetadatas: Record;
+ /**
+ * Maps package root path to package metadata.
+ */
+ pathMetadatas: Record;
+}
diff --git a/lib/parse/ClassFinder.ts b/lib/parse/ClassFinder.ts
index b94308e..1a04877 100644
--- a/lib/parse/ClassFinder.ts
+++ b/lib/parse/ClassFinder.ts
@@ -42,6 +42,7 @@ export class ClassFinder {
Promise<{ named: ClassIndex; unnamed: { packageName: string; fileName: string }[] }> {
// Load the elements of the class
const {
+ resolvedPath,
exportedClasses,
exportedInterfaces,
exportedImportedElements,
@@ -52,22 +53,28 @@ export class ClassFinder {
importedElements,
} = await this.classLoader.loadClassElements(packageName, fileName);
const exportDefinitions:
- { named: ClassIndex; unnamed: { packageName: string; fileName: string }[] } =
- { named: {}, unnamed: []};
+ {
+ named: ClassIndex;
+ unnamed: { packageName: string; fileName: string; fileNameReferenced: string }[];
+ } = { named: {}, unnamed: []};
// Get all named exports
for (const localName in exportedClasses) {
exportDefinitions.named[localName] = {
packageName,
localName,
- fileName,
+ qualifiedPath: undefined,
+ fileName: resolvedPath,
+ fileNameReferenced: resolvedPath,
};
}
for (const localName in exportedInterfaces) {
exportDefinitions.named[localName] = {
packageName,
localName,
- fileName,
+ qualifiedPath: undefined,
+ fileName: resolvedPath,
+ fileNameReferenced: resolvedPath,
};
}
@@ -76,7 +83,9 @@ export class ClassFinder {
exportDefinitions.named[exportedName] = {
packageName,
localName,
+ qualifiedPath: undefined,
fileName: importedFileName,
+ fileNameReferenced: resolvedPath,
};
}
@@ -89,7 +98,9 @@ export class ClassFinder {
exportDefinitions.named[exportedName] = {
packageName,
localName,
- fileName,
+ qualifiedPath: undefined,
+ fileName: resolvedPath,
+ fileNameReferenced: resolvedPath,
};
break;
}
@@ -99,7 +110,9 @@ export class ClassFinder {
exportDefinitions.named[exportedName] = {
packageName,
localName,
- fileName,
+ qualifiedPath: undefined,
+ fileName: resolvedPath,
+ fileNameReferenced: resolvedPath,
};
break;
}
diff --git a/lib/parse/ClassIndex.ts b/lib/parse/ClassIndex.ts
index 3860b16..7a83b7b 100644
--- a/lib/parse/ClassIndex.ts
+++ b/lib/parse/ClassIndex.ts
@@ -1,5 +1,4 @@
-import type { ClassDeclaration, TSInterfaceDeclaration, TypeNode } from '@typescript-eslint/types/dist/ts-estree';
-import type { AST, TSESTreeOptions } from '@typescript-eslint/typescript-estree';
+import type { AST, TSESTreeOptions, TSESTree } from '@typescript-eslint/typescript-estree';
/**
* A collection of classes, with exported name as key.
@@ -14,14 +13,23 @@ export interface ClassReference {
packageName: string;
// The name of the class within the file.
localName: string;
+ // Qualified path to the class.
+ qualifiedPath?: string[];
// The name of the file the class is defined in.
fileName: string;
+ // The first name of the file this class was referenced from, in a chain of imports/exports (in top-down order)
+ fileNameReferenced: string;
}
/**
* A loaded reference.
*/
-export type ClassReferenceLoaded = ClassLoaded | InterfaceLoaded;
+export type ClassReferenceLoaded = ClassLoaded | InterfaceLoaded | TypeLoaded | EnumLoaded;
+
+/**
+ * A loaded reference without type aliases and enums.
+ */
+export type ClassReferenceLoadedClassOrInterface = ClassLoaded | InterfaceLoaded;
/**
* A loaded class with a full class declaration.
@@ -33,13 +41,13 @@ export interface ClassLoaded extends ClassReference {
// The name of the file the class is defined in.
fileName: string;
// The loaded class declaration.
- declaration: ClassDeclaration;
+ declaration: TSESTree.ClassDeclaration;
// The full AST the class was present in.
ast: AST;
// A super class reference if the class has one
- superClass?: ClassLoaded;
+ superClass?: GenericallyTyped;
// Interface or (abstract) class references if the class implements them
- implementsInterfaces?: ClassReferenceLoaded[];
+ implementsInterfaces?: GenericallyTyped[];
// If this class is an abstract class that can not be instantiated directly
abstract?: boolean;
// The tsdoc comment of this class
@@ -51,7 +59,17 @@ export interface ClassLoaded extends ClassReference {
/**
* A hash of generic type name to its properties.
*/
-export type GenericTypes = Record;
+export type GenericTypes = Record;
+
+/**
+ * Something (like a class or interface) that may have generic types assigned to it as instantiation.
+ */
+export interface GenericallyTyped {
+ // The typed value
+ value: T;
+ // The generic types of this value
+ genericTypeInstantiations?: TSESTree.TSTypeParameterInstantiation;
+}
/**
* A loaded interface with a full interface declaration.
@@ -63,13 +81,57 @@ export interface InterfaceLoaded extends ClassReference {
// The name of the file the interface is defined in.
fileName: string;
// The loaded interface declaration.
- declaration: TSInterfaceDeclaration;
+ declaration: TSESTree.TSInterfaceDeclaration;
// The full AST the interface was present in.
ast: AST;
// Super interface references if the interface has them
- superInterfaces?: InterfaceLoaded[];
+ superInterfaces?: GenericallyTyped[];
// The tsdoc comment of this class
comment?: string;
// The generic types of this class
generics: GenericTypes;
}
+
+/**
+ * A member field of a class or interface.
+ */
+export interface MemberField {
+ name: string;
+ range: TSESTree.TypeNode | undefined;
+}
+
+/**
+ * A loaded type alias with a full type declaration.
+ */
+export interface TypeLoaded extends ClassReference {
+ type: 'type';
+ // The name of the interface within the file.
+ localName: string;
+ // The name of the file the interface is defined in.
+ fileName: string;
+ // The loaded type declaration.
+ declaration: TSESTree.TSTypeAliasDeclaration;
+ // The full AST the interface was present in.
+ ast: AST;
+ // The tsdoc comment of this class
+ comment?: string;
+ // The generic types of this class
+ generics: GenericTypes;
+}
+
+/**
+ * A loaded enum with a full type declaration.
+ */
+export interface EnumLoaded extends ClassReference {
+ type: 'enum';
+ // The name of the interface within the file.
+ localName: string;
+ // The name of the file the interface is defined in.
+ fileName: string;
+ // The loaded enum declaration.
+ declaration: TSESTree.TSEnumDeclaration;
+ // The full AST the interface was present in.
+ ast: AST;
+ // The tsdoc comment of this class
+ comment?: string;
+}
diff --git a/lib/parse/ClassIndexer.ts b/lib/parse/ClassIndexer.ts
index a14b0c8..11ae549 100644
--- a/lib/parse/ClassIndexer.ts
+++ b/lib/parse/ClassIndexer.ts
@@ -3,7 +3,15 @@
*/
import type { Logger } from 'winston';
import type { ClassFinder } from './ClassFinder';
-import type { ClassIndex, ClassReference, ClassReferenceLoaded, InterfaceLoaded } from './ClassIndex';
+import type {
+ ClassIndex,
+ ClassReference,
+ ClassReferenceLoaded,
+ ClassReferenceLoadedClassOrInterface,
+ InterfaceLoaded,
+ GenericallyTyped,
+} from './ClassIndex';
+
import type { ClassLoader } from './ClassLoader';
export class ClassIndexer {
@@ -23,8 +31,10 @@ export class ClassIndexer {
* Load all class references in the given class index.
* @param classReferences An index of class references.
*/
- public async createIndex(classReferences: ClassIndex): Promise> {
- const classIndex: ClassIndex = {};
+ public async createIndex(
+ classReferences: ClassIndex,
+ ): Promise> {
+ const classIndex: ClassIndex = {};
for (const [ className, classReference ] of Object.entries(classReferences)) {
if (!(className in this.ignoreClasses)) {
@@ -40,77 +50,95 @@ export class ClassIndexer {
* such as its declaration and loaded super class referenced.
* @param classReference The reference to a class or interface.
*/
- public async loadClassChain(classReference: ClassReference): Promise {
+ public async loadClassChain(classReference: ClassReference): Promise {
// Load the class declaration
const classReferenceLoaded: ClassReferenceLoaded = await this.classLoader
- .loadClassDeclaration(classReference, true);
+ .loadClassDeclaration(classReference, true, false);
if (classReferenceLoaded.type === 'class') {
// If the class has a super class, load it recursively
- const superClassName = this.classLoader.getSuperClassName(classReferenceLoaded.declaration,
- classReferenceLoaded.fileName);
- if (superClassName && !(superClassName in this.ignoreClasses)) {
+ const superClassName = this.classLoader
+ .getSuperClassName(classReferenceLoaded.declaration, classReferenceLoaded.fileName);
+ if (superClassName && !(superClassName.value in this.ignoreClasses)) {
let superClassLoaded;
try {
superClassLoaded = await this.loadClassChain({
packageName: classReferenceLoaded.packageName,
- localName: superClassName,
+ localName: superClassName.value,
+ qualifiedPath: classReferenceLoaded.qualifiedPath,
fileName: classReferenceLoaded.fileName,
+ fileNameReferenced: classReferenceLoaded.fileNameReferenced,
});
} catch (error: unknown) {
- throw new Error(`Failed to load super class ${superClassName} of ${classReference.localName} in ${classReference.fileName}:\n${(error).message}`);
+ throw new Error(`Failed to load super class ${superClassName.value} of ${classReference.localName} in ${classReference.fileName}:\n${(error).message}`);
}
if (superClassLoaded.type !== 'class') {
- throw new Error(`Detected non-class ${superClassName} extending from a class ${classReference.localName} in ${classReference.fileName}`);
+ throw new Error(`Detected non-class ${superClassName.value} extending from a class ${classReference.localName} in ${classReference.fileName}`);
}
- classReferenceLoaded.superClass = superClassLoaded;
+ classReferenceLoaded.superClass = {
+ value: superClassLoaded,
+ genericTypeInstantiations: superClassName.genericTypeInstantiations,
+ };
}
// If the class implements interfaces, load them
- const interfaceNames = this.classLoader.getClassInterfaceNames(classReferenceLoaded.declaration,
- classReferenceLoaded.fileName);
- classReferenceLoaded.implementsInterfaces = (await Promise.all(interfaceNames
- .filter(interfaceName => !(interfaceName in this.ignoreClasses))
- .map(async interfaceName => {
- let interfaceOrClassLoaded;
- try {
- interfaceOrClassLoaded = await this.classLoader.loadClassDeclaration({
- packageName: classReferenceLoaded.packageName,
- localName: interfaceName,
- fileName: classReferenceLoaded.fileName,
- }, true);
- } catch (error: unknown) {
- // Ignore interfaces that we don't understand
- this.logger.debug(`Ignored interface ${interfaceName} implemented by ${classReference.localName} in ${classReference.fileName}:\n${( error).message}`);
- return;
- }
- return interfaceOrClassLoaded;
- })))
- .filter(iface => Boolean(iface));
+ const interfaceNames = this.classLoader
+ .getClassInterfaceNames(classReferenceLoaded.declaration, classReferenceLoaded.fileName);
+ classReferenceLoaded.implementsInterfaces = []> (
+ await Promise
+ .all(interfaceNames
+ .filter(interfaceName => !(interfaceName.value in this.ignoreClasses))
+ .map(async(interfaceName) => {
+ let interfaceOrClassLoaded;
+ try {
+ interfaceOrClassLoaded = await this.classLoader.loadClassDeclaration({
+ packageName: classReferenceLoaded.packageName,
+ localName: interfaceName.value,
+ qualifiedPath: classReferenceLoaded.qualifiedPath,
+ fileName: classReferenceLoaded.fileName,
+ fileNameReferenced: classReferenceLoaded.fileNameReferenced,
+ }, true, false);
+ } catch (error: unknown) {
+ // Ignore interfaces that we don't understand
+ this.logger.debug(`Ignored interface ${interfaceName.value} implemented by ${classReference.localName} in ${classReference.fileName}:\n${( error).message}`);
+ return;
+ }
+ return {
+ value: interfaceOrClassLoaded,
+ genericTypeInstantiations: interfaceName.genericTypeInstantiations,
+ };
+ })))
+ .filter(Boolean);
} else {
const superInterfaceNames = this.classLoader
.getSuperInterfaceNames(classReferenceLoaded.declaration, classReferenceLoaded.fileName);
- classReferenceLoaded.superInterfaces = (await Promise.all(superInterfaceNames
- .filter(interfaceName => !(interfaceName in this.ignoreClasses))
- .map(async interfaceName => {
- let superInterface;
- try {
- superInterface = await this.loadClassChain({
- packageName: classReferenceLoaded.packageName,
- localName: interfaceName,
- fileName: classReferenceLoaded.fileName,
- });
- } catch (error: unknown) {
+ classReferenceLoaded.superInterfaces = []> (await Promise
+ .all(superInterfaceNames
+ .filter(interfaceName => !(interfaceName.value in this.ignoreClasses))
+ .map(async(interfaceName) => {
+ let superInterface;
+ try {
+ superInterface = await this.loadClassChain({
+ packageName: classReferenceLoaded.packageName,
+ localName: interfaceName.value,
+ qualifiedPath: classReferenceLoaded.qualifiedPath,
+ fileName: classReferenceLoaded.fileName,
+ fileNameReferenced: classReferenceLoaded.fileNameReferenced,
+ });
+ } catch (error: unknown) {
// Ignore interfaces that we don't understand
- this.logger.debug(`Ignored interface ${interfaceName} extended by ${classReference.localName} in ${classReference.fileName}:\n${( error).message}`);
- return;
- }
- if (superInterface.type !== 'interface') {
- throw new Error(`Detected non-interface ${classReferenceLoaded.localName} extending from a class ${interfaceName} in ${classReference.fileName}`);
- }
- return superInterface;
- })))
- .filter(iface => Boolean(iface));
+ this.logger.debug(`Ignored interface ${interfaceName.value} extended by ${classReference.localName} in ${classReference.fileName}:\n${( error).message}`);
+ return;
+ }
+ if (superInterface.type !== 'interface') {
+ throw new Error(`Detected non-interface ${classReferenceLoaded.localName} extending from a class ${interfaceName.value} in ${classReference.fileName}`);
+ }
+ return {
+ value: superInterface,
+ genericTypeInstantiations: interfaceName.genericTypeInstantiations,
+ };
+ })))
+ .filter(Boolean);
}
return classReferenceLoaded;
diff --git a/lib/parse/ClassLoader.ts b/lib/parse/ClassLoader.ts
index 5b6513d..92958e7 100644
--- a/lib/parse/ClassLoader.ts
+++ b/lib/parse/ClassLoader.ts
@@ -1,11 +1,19 @@
-import * as Path from 'path';
-import type { ClassDeclaration, TSInterfaceDeclaration } from '@typescript-eslint/types/dist/ts-estree';
-import type { AST, TSESTreeOptions } from '@typescript-eslint/typescript-estree';
+import type { AST, TSESTreeOptions, TSESTree } from '@typescript-eslint/typescript-estree';
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
import type { Logger } from 'winston';
import type { ResolutionContext } from '../resolution/ResolutionContext';
-import type { ClassLoaded, ClassReference, ClassReferenceLoaded, GenericTypes, InterfaceLoaded } from './ClassIndex';
-import { CommentLoader } from './CommentLoader';
+import { filePathDirName, joinFilePath } from '../util/PathUtil';
+import type {
+ ClassLoaded,
+ ClassReference,
+ ClassReferenceLoaded,
+ EnumLoaded,
+ GenericTypes,
+ InterfaceLoaded,
+ TypeLoaded,
+ GenericallyTyped,
+} from './ClassIndex';
+import type { CommentLoader } from './CommentLoader';
/**
* Loads typescript classes from class references.
@@ -13,10 +21,12 @@ import { CommentLoader } from './CommentLoader';
export class ClassLoader {
private readonly resolutionContext: ResolutionContext;
private readonly logger: Logger;
+ private readonly commentLoader: CommentLoader;
public constructor(args: ClassLoaderArgs) {
this.resolutionContext = args.resolutionContext;
this.logger = args.logger;
+ this.commentLoader = args.commentLoader;
}
/**
@@ -25,13 +35,19 @@ export class ClassLoader {
* @param declaration A class declaration.
* @param fileName The file name of the current class.
*/
- public getSuperClassName(declaration: ClassDeclaration, fileName: string): string | undefined {
+ public getSuperClassName(
+ declaration: TSESTree.ClassDeclaration,
+ fileName: string,
+ ): GenericallyTyped | undefined {
if (!declaration.superClass) {
return;
}
if (declaration.superClass.type === AST_NODE_TYPES.Identifier) {
// Extensions in the form of `class A extends B`
- return declaration.superClass.name;
+ return {
+ value: declaration.superClass.name,
+ genericTypeInstantiations: declaration.superTypeArguments,
+ };
}
if (declaration.superClass.type === AST_NODE_TYPES.MemberExpression &&
declaration.superClass.property.type === AST_NODE_TYPES.Identifier &&
@@ -46,21 +62,27 @@ export class ClassLoader {
* Find the super interfaces of the given interface.
* Throws an error for interface definitions that could not be interpreted.
* @param declaration An interface declaration.
- * @param fileName The file name of the current class.
+ * @param _fileName The file name of the current class.
*/
- public getSuperInterfaceNames(declaration: TSInterfaceDeclaration, fileName: string): string[] {
- return (declaration.extends || [])
+ public getSuperInterfaceNames(
+ declaration: TSESTree.TSInterfaceDeclaration,
+ _fileName: string,
+ ): GenericallyTyped[] {
+ return []> declaration.extends
// eslint-disable-next-line array-callback-return
- .map(extendsExpression => {
+ .map((extendsExpression) => {
if (extendsExpression.type === AST_NODE_TYPES.TSInterfaceHeritage &&
extendsExpression.expression.type === AST_NODE_TYPES.Identifier) {
// Extensions in the form of `interface A extends B`
- return extendsExpression.expression.name;
+ return {
+ value: extendsExpression.expression.name,
+ genericTypeInstantiations: extendsExpression.typeArguments,
+ };
}
// Ignore interfaces that we don't understand
this.logger.debug(`Ignored an interface expression of unknown type ${extendsExpression.expression.type} on ${declaration.id.name}`);
})
- .filter(iface => Boolean(iface));
+ .filter(Boolean);
}
/**
@@ -68,14 +90,20 @@ export class ClassLoader {
* @param declaration A class declaration.
* @param fileName The file name of the current class.
*/
- public getClassInterfaceNames(declaration: ClassDeclaration, fileName: string): string[] {
- const interfaceNames = [];
+ public getClassInterfaceNames(
+ declaration: TSESTree.ClassDeclaration,
+ fileName: string,
+ ): GenericallyTyped[] {
+ const interfaceNames: GenericallyTyped[] = [];
if (declaration.implements) {
for (const implement of declaration.implements) {
if (implement.expression.type !== AST_NODE_TYPES.Identifier) {
throw new Error(`Could not interpret the implements type on a class in ${fileName} on line ${implement.expression.loc.start.line} column ${implement.expression.loc.start.column}`);
}
- interfaceNames.push(implement.expression.name);
+ interfaceNames.push({
+ value: implement.expression.name,
+ genericTypeInstantiations: implement.typeArguments,
+ });
}
}
return interfaceNames;
@@ -86,112 +114,337 @@ export class ClassLoader {
* Classes can either be defined in this file (exported or not), or imported from another file.
* @param classReference The reference to a class.
* @param considerInterfaces If the class reference is allows to refer to an interface, as well as a class.
+ * @param considerOthers If the class reference is allows to refer to refer to other things,
+ * such as a type alias or enum.
*/
- public async loadClassDeclaration(classReference: ClassReference, considerInterfaces: CI):
- Promise {
+ public async loadClassDeclaration(
+ classReference: ClassReference,
+ considerInterfaces: CI,
+ considerOthers: CT,
+ ): Promise {
+ let targetString = 'class';
+ if (considerInterfaces) {
+ targetString += ' or interface';
+ }
+ if (considerOthers) {
+ targetString += ' or other type';
+ }
+
// Load the class as an AST
let ast;
try {
ast = await this.resolutionContext.parseTypescriptFile(classReference.fileName);
} catch (error: unknown) {
- throw new Error(`Could not load ${considerInterfaces ? 'class or interface' : 'class'} ${classReference.localName} from ${classReference.fileName}:\n${( error).message}`);
+ const name = `${classReference.qualifiedPath && classReference.qualifiedPath.length > 0 ? `${classReference.qualifiedPath.join('.')}.` : ''}${classReference.localName}`;
+ throw new Error(`Could not load ${targetString} ${name} from ${classReference.fileName}:\n${( error).message}`);
}
+ return this.loadClassDeclarationFromAst(ast, targetString, classReference, considerInterfaces, considerOthers);
+ }
+
+ /**
+ * Load the referenced class, and obtain its full class declaration.
+ * Classes can either be defined in this file (exported or not), or imported from another file.
+ * @param ast An abstract syntax tree.
+ * @param targetString A string for error reporting on the considered scope.
+ * @param classReference The reference to a class.
+ * @param considerInterfaces If the class reference is allows to refer to an interface, as well as a class.
+ * @param considerOthers If the class reference is allows to refer to refer to other things,
+ * such as a type alias or enum.
+ */
+ public async loadClassDeclarationFromAst(
+ ast: AST | TSESTree.TSModuleBlock,
+ targetString: string,
+ classReference: ClassReference,
+ considerInterfaces: CI,
+ considerOthers: CT,
+ ): Promise {
const {
exportedClasses,
exportedInterfaces,
+ exportedTypes,
+ exportedEnums,
+ exportedImportedAllNamed,
declaredClasses,
declaredInterfaces,
+ declaredTypes,
+ declaredEnums,
+ declaredNamespaces,
importedElements,
+ importedElementsAllNamed,
exportedImportedAll,
exportedImportedElements,
+ exportAssignment,
} = this.getClassElements(classReference.packageName, classReference.fileName, ast);
- // If the class has been exported in this file, return directly
- if (classReference.localName in exportedClasses) {
- const declaration = exportedClasses[classReference.localName];
- return this.enhanceLoadedWithComment( {
- type: 'class',
- ...classReference,
- declaration,
- ast,
- abstract: declaration.abstract,
- generics: this.collectGenericTypes(declaration),
- });
- }
+ let componentName: string;
+ let qualifiedPathInner: string[] | undefined;
+ if (classReference.qualifiedPath && classReference.qualifiedPath.length > 0) {
+ // In all following code, look for the qualified path head
+ componentName = classReference.qualifiedPath[0];
- // If the class has been declared in this file, return directly
- if (classReference.localName in declaredClasses) {
- const declaration = declaredClasses[classReference.localName];
- return this.enhanceLoadedWithComment( {
- type: 'class',
- ...classReference,
- declaration,
- ast,
- abstract: declaration.abstract,
- generics: this.collectGenericTypes(declaration),
- });
- }
+ // For recursive calls to getClassElements, we'll have to slice off the head
+ qualifiedPathInner = classReference.qualifiedPath.slice(1);
+ } else {
+ // Otherwise if we don't have a qualified path, look for the class name
+ componentName = classReference.localName;
- // Only consider interfaces if explicitly enabled
- if (considerInterfaces) {
- // If the interface has been exported in this file, return directly
- if (classReference.localName in exportedInterfaces) {
- const declaration = exportedInterfaces[classReference.localName];
- return this.enhanceLoadedWithComment( {
- type: 'interface',
+ // If the class has been exported in this file, return directly
+ if (componentName in exportedClasses) {
+ const declaration = exportedClasses[componentName];
+ return this.enhanceLoadedWithComment({
+ type: 'class',
...classReference,
declaration,
ast,
+ abstract: declaration.abstract,
generics: this.collectGenericTypes(declaration),
});
}
- // If the interface has been declared in this file, return directly
- if (classReference.localName in declaredInterfaces) {
- const declaration = declaredInterfaces[classReference.localName];
- return this.enhanceLoadedWithComment( {
- type: 'interface',
+ // If the class has been declared in this file, return directly
+ if (componentName in declaredClasses) {
+ const declaration = declaredClasses[componentName];
+ return this.enhanceLoadedWithComment({
+ type: 'class',
...classReference,
declaration,
ast,
+ abstract: declaration.abstract,
generics: this.collectGenericTypes(declaration),
});
}
+
+ // Only consider interfaces if explicitly enabled
+ if (considerInterfaces) {
+ // If the interface has been exported in this file, return directly
+ if (componentName in exportedInterfaces) {
+ const declaration = exportedInterfaces[componentName];
+ return this.enhanceLoadedWithComment({
+ type: 'interface',
+ ...classReference,
+ declaration,
+ ast,
+ generics: this.collectGenericTypes(declaration),
+ });
+ }
+
+ // If the interface has been declared in this file, return directly
+ if (componentName in declaredInterfaces) {
+ const declaration = declaredInterfaces[componentName];
+ return this.enhanceLoadedWithComment({
+ type: 'interface',
+ ...classReference,
+ declaration,
+ ast,
+ generics: this.collectGenericTypes(declaration),
+ });
+ }
+ }
+
+ // Only consider other types if explicitly enabled
+ if (considerOthers) {
+ // Check types
+ if (componentName in exportedTypes) {
+ const declaration = exportedTypes[componentName];
+ return this.enhanceLoadedWithComment({
+ type: 'type',
+ ...classReference,
+ declaration,
+ ast,
+ generics: this.collectGenericTypes(declaration),
+ });
+ }
+ if (componentName in declaredTypes) {
+ const declaration = declaredTypes[componentName];
+ return this.enhanceLoadedWithComment({
+ type: 'type',
+ ...classReference,
+ declaration,
+ ast,
+ generics: this.collectGenericTypes(declaration),
+ });
+ }
+
+ // Check enums
+ if (componentName in exportedEnums) {
+ const declaration = exportedEnums[componentName];
+ return this.enhanceLoadedWithComment({
+ type: 'enum',
+ ...classReference,
+ declaration,
+ ast,
+ });
+ }
+ if (componentName in declaredEnums) {
+ const declaration = declaredEnums[componentName];
+ return this.enhanceLoadedWithComment({
+ type: 'enum',
+ ...classReference,
+ declaration,
+ ast,
+ });
+ }
+ }
}
+ // If we haven't found anything so far, we will follow import/export links.
+
// If the class is available via an import, follow that import link
- if (classReference.localName in importedElements) {
- return this.loadClassDeclaration(importedElements[classReference.localName], considerInterfaces);
+ if (componentName in importedElements) {
+ const entry = importedElements[componentName];
+ let localNameInner: string;
+ if (qualifiedPathInner) {
+ localNameInner = classReference.localName;
+ qualifiedPathInner = [ entry.localName, ...qualifiedPathInner ];
+ } else {
+ localNameInner = entry.localName;
+ }
+ return this.loadClassDeclaration(
+ {
+ ...entry,
+ localName: localNameInner,
+ qualifiedPath: qualifiedPathInner,
+ fileNameReferenced: classReference.fileNameReferenced,
+ },
+ considerInterfaces,
+ considerOthers,
+ );
}
// If the class is available via an exported import, follow that import link
- if (classReference.localName in exportedImportedElements) {
- return this.loadClassDeclaration(exportedImportedElements[classReference.localName], considerInterfaces);
+ if (componentName in exportedImportedElements) {
+ const entry = exportedImportedElements[componentName];
+ let localNameInner: string;
+ if (qualifiedPathInner) {
+ localNameInner = classReference.localName;
+ qualifiedPathInner = [ entry.localName, ...qualifiedPathInner ];
+ } else {
+ localNameInner = entry.localName;
+ }
+ return this.loadClassDeclaration(
+ {
+ ...entry,
+ localName: localNameInner,
+ qualifiedPath: qualifiedPathInner,
+ fileNameReferenced: classReference.fileNameReferenced,
+ },
+ considerInterfaces,
+ considerOthers,
+ );
+ }
+
+ // Check for named exported elements
+ if (componentName in exportedImportedAllNamed) {
+ return await this.loadClassDeclaration({
+ localName: classReference.localName,
+ qualifiedPath: qualifiedPathInner,
+ ...exportedImportedAllNamed[componentName],
+ fileNameReferenced: classReference.fileNameReferenced,
+ }, considerInterfaces, considerOthers);
+ }
+
+ // Follow named import links
+ if (componentName in importedElementsAllNamed) {
+ return await this.loadClassDeclaration({
+ localName: classReference.localName,
+ qualifiedPath: qualifiedPathInner,
+ ...importedElementsAllNamed[componentName],
+ fileNameReferenced: classReference.fileNameReferenced,
+ }, considerInterfaces, considerOthers);
+ }
+
+ // Check enum values
+ if (classReference.qualifiedPath && classReference.qualifiedPath.length === 1) {
+ const enumName = classReference.qualifiedPath[0];
+ const enumKey = classReference.localName;
+ const enumDeclaration = exportedEnums[enumName] || declaredEnums[enumName];
+ if (enumDeclaration) {
+ for (const enumMember of enumDeclaration.members) {
+ if (enumMember.id.type === AST_NODE_TYPES.Identifier && enumMember.id.name === enumKey &&
+ enumMember.initializer && enumMember.initializer.type === AST_NODE_TYPES.Literal) {
+ // Expose the enum entry as type alias
+ const typeNode: TSESTree.TSTypeAliasDeclaration = {
+ type: AST_NODE_TYPES.TSTypeAliasDeclaration,
+ id: {
+ type: AST_NODE_TYPES.Identifier,
+ name: enumKey,
+ loc: undefined,
+ range: undefined,
+ parent: undefined,
+ typeAnnotation: undefined,
+ optional: undefined,
+ decorators: undefined,
+ },
+ typeAnnotation: {
+ type: AST_NODE_TYPES.TSLiteralType,
+ literal: enumMember.initializer,
+ loc: undefined,
+ range: undefined,
+ parent: undefined,
+ },
+ loc: undefined,
+ range: undefined,
+ parent: undefined,
+ declare: undefined,
+ typeParameters: undefined,
+ };
+ return {
+ type: 'type',
+ ...classReference,
+ declaration: typeNode,
+ ast,
+ };
+ }
+ }
+ }
}
// If we still haven't found the class, iterate over all export all's
for (const subFile of exportedImportedAll) {
try {
- return await this.loadClassDeclaration({ localName: classReference.localName, ...subFile },
- considerInterfaces);
+ return await this.loadClassDeclaration({
+ localName: classReference.localName,
+ qualifiedPath: qualifiedPathInner,
+ ...subFile,
+ fileNameReferenced: classReference.fileNameReferenced,
+ }, considerInterfaces, considerOthers);
} catch {
// Ignore class not found errors
}
}
- throw new Error(`Could not load ${considerInterfaces ? 'class or interface' : 'class'} ${classReference.localName} from ${classReference.fileName}`);
+ // Check if the export assignment refers to a namespace
+ if (exportAssignment && typeof exportAssignment === 'string' && exportAssignment in declaredNamespaces) {
+ const namespace: TSESTree.TSModuleDeclaration = declaredNamespaces[exportAssignment];
+ return this.loadClassDeclarationFromAst(
+ namespace.body,
+ targetString,
+ classReference,
+ considerInterfaces,
+ considerOthers,
+ );
+ }
+
+ const name = `${classReference.qualifiedPath && classReference.qualifiedPath.length > 0 ? `${classReference.qualifiedPath.join('.')}.` : ''}${classReference.localName}`;
+ throw new Error(`Could not load ${targetString} ${name} from ${classReference.fileName}`);
}
/**
* Create a hash of generic types in the given class declaration.
* @param classDeclaration A class or interface declaration.
*/
- public collectGenericTypes(classDeclaration: ClassDeclaration | TSInterfaceDeclaration): GenericTypes {
+ public collectGenericTypes(
+ classDeclaration: TSESTree.ClassDeclaration | TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration,
+ ): GenericTypes {
const genericTypes: GenericTypes = {};
if (classDeclaration.typeParameters) {
for (const param of classDeclaration.typeParameters.params) {
- genericTypes[param.name.name] = { type: param.constraint };
+ genericTypes[param.name.name] = { type: param.constraint, default: param.default };
}
}
return genericTypes;
@@ -202,8 +455,7 @@ export class ClassLoader {
* @param classLoaded A loaded class or interface.
*/
public enhanceLoadedWithComment(classLoaded: ClassReferenceLoaded): ClassReferenceLoaded {
- const commentData = new CommentLoader({ classLoaded })
- .getCommentDataFromClassOrInterface(classLoaded.declaration);
+ const commentData = this.commentLoader.getCommentDataFromClassOrInterface(classLoaded);
if (commentData.description) {
classLoaded.comment = commentData.description;
}
@@ -213,15 +465,22 @@ export class ClassLoader {
/**
* Load a class, and get all class elements from it.
* @param packageName Package name we are importing from.
- * @param fileName A file path.
+ * @param filePath A file path.
+ * @returns {Promise} Promise of the class elements along with
+ * the resolved file path that was used to load these class elements.
*/
- public async loadClassElements(packageName: string, fileName: string): Promise {
- const ast = await this.resolutionContext.parseTypescriptFile(fileName);
- return this.getClassElements(packageName, fileName, ast);
+ public async loadClassElements(packageName: string, filePath: string): Promise<
+ ClassElements & { resolvedPath: string }
+ > {
+ const resolvedPath = await this.resolutionContext.resolveTypesPath(filePath);
+ const ast = await this.resolutionContext.parseTypescriptFile(resolvedPath);
+ const classElements = this.getClassElements(packageName, resolvedPath, ast);
+ return { ...classElements, resolvedPath };
}
/**
* Convert the given import path to an absolute file path, coupled with the module it is part of.
+ * Result is `undefined` if there was an error resolving the package.
* @param currentPackageName Package name we are importing from.
* @param currentFilePath Absolute path to a file in which the import path occurs.
* @param importPath Possibly relative path that is being imported.
@@ -230,12 +489,18 @@ export class ClassLoader {
currentPackageName: string,
currentFilePath: string,
importPath: string,
- ): { packageName: string; fileName: string } {
+ ): { packageName: string; fileName: string; fileNameReferenced: string } | undefined {
// Handle import paths within the current package
if (importPath.startsWith('.')) {
+ const fileName = joinFilePath(filePathDirName(currentFilePath), importPath);
+ const indexOfExtension = fileName.indexOf('.', fileName.lastIndexOf('/'));
+ const fileNameWithoutExtension = indexOfExtension === -1 ?
+ fileName :
+ fileName.slice(0, indexOfExtension);
return {
packageName: currentPackageName,
- fileName: Path.join(Path.dirname(currentFilePath), importPath),
+ fileName: fileNameWithoutExtension,
+ fileNameReferenced: currentFilePath,
};
}
@@ -269,13 +534,20 @@ export class ClassLoader {
}
// Resolve paths
- const packageRoot = this.resolutionContext.resolvePackageIndex(packageName, currentFilePath);
+ let packageRoot: string;
+ try {
+ packageRoot = this.resolutionContext.resolvePackageIndex(packageName, currentFilePath);
+ } catch (error: unknown) {
+ this.logger.warn(`Ignoring invalid package "${packageName}": ${( error).message}`);
+ return;
+ }
const remoteFilePath = packagePath ?
- Path.join(Path.dirname(packageRoot), packagePath) :
- packageRoot.slice(0, packageRoot.lastIndexOf('.'));
+ joinFilePath(filePathDirName(packageRoot), packagePath) :
+ packageRoot.slice(0, packageRoot.indexOf('.', packageRoot.lastIndexOf('/')));
return {
packageName,
fileName: remoteFilePath,
+ fileNameReferenced: currentFilePath,
};
}
@@ -285,15 +557,30 @@ export class ClassLoader {
* @param fileName A file path.
* @param ast The parsed file.
*/
- public getClassElements(packageName: string, fileName: string, ast: AST): ClassElements {
- const exportedClasses: Record = {};
- const exportedInterfaces: Record = {};
+ public getClassElements(
+ packageName: string,
+ fileName: string,
+ ast: AST | TSESTree.TSModuleBlock,
+ ): ClassElements {
+ const exportedClasses: Record = {};
+ const exportedInterfaces: Record = {};
+ const exportedTypes: Record = {};
+ const exportedEnums: Record = {};
+ const exportedNamespaces: Record = {};
const exportedImportedElements: Record = {};
- const exportedImportedAll: { packageName: string; fileName: string }[] = [];
+ const exportedImportedAll: { packageName: string; fileName: string; fileNameReferenced: string }[] = [];
+ const exportedImportedAllNamed:
+ Record = {};
const exportedUnknowns: Record = {};
- const declaredClasses: Record = {};
- const declaredInterfaces: Record = {};
+ const declaredClasses: Record = {};
+ const declaredInterfaces: Record = {};
+ const declaredTypes: Record = {};
+ const declaredEnums: Record = {};
+ const declaredNamespaces: Record = {};
const importedElements: Record = {};
+ const importedElementsAllNamed:
+ Record = {};
+ let exportAssignment: string | TSESTree.ClassDeclaration | undefined;
for (const statement of ast.body) {
if (statement.type === AST_NODE_TYPES.ExportNamedDeclaration) {
@@ -308,15 +595,29 @@ export class ClassLoader {
statement.declaration.type === AST_NODE_TYPES.TSInterfaceDeclaration) {
// Form: `export interface A{}`
exportedInterfaces[statement.declaration.id.name] = statement.declaration;
+ } else if (statement.declaration && statement.declaration.type === AST_NODE_TYPES.TSTypeAliasDeclaration) {
+ // Form: `export type A = ...`
+ exportedTypes[statement.declaration.id.name] = statement.declaration;
+ } else if (statement.declaration && statement.declaration.type === AST_NODE_TYPES.TSEnumDeclaration) {
+ // Form: `export enum A {...}`
+ exportedEnums[statement.declaration.id.name] = statement.declaration;
+ } else if (statement.declaration && statement.declaration.type === AST_NODE_TYPES.TSModuleDeclaration &&
+ 'name' in statement.declaration.id) {
+ // Form: `export namespace A { ... }`
+ exportedNamespaces[statement.declaration.id.name] = statement.declaration;
} else if (statement.source &&
statement.source.type === AST_NODE_TYPES.Literal &&
typeof statement.source.value === 'string') {
// Form: `export { A as B } from "b"`
for (const specifier of statement.specifiers) {
- exportedImportedElements[specifier.exported.name] = {
- localName: specifier.local.name,
- ...this.importTargetToAbsolutePath(packageName, fileName, statement.source.value),
- };
+ const entry = this.importTargetToAbsolutePath(packageName, fileName, statement.source.value);
+ if (entry) {
+ exportedImportedElements[specifier.exported.name] = {
+ localName: specifier.local.name,
+ qualifiedPath: undefined,
+ ...entry,
+ };
+ }
}
} else {
// Form: `export { A as B }`
@@ -325,11 +626,28 @@ export class ClassLoader {
}
}
} else if (statement.type === AST_NODE_TYPES.ExportAllDeclaration) {
- // Form: `export * from "b"`
+ // Form: `export * from "b"` or `export * as B from "b"`
if (statement.source &&
statement.source.type === AST_NODE_TYPES.Literal &&
typeof statement.source.value === 'string') {
- exportedImportedAll.push(this.importTargetToAbsolutePath(packageName, fileName, statement.source.value));
+ const entry = this.importTargetToAbsolutePath(packageName, fileName, statement.source.value);
+ if (entry) {
+ if (statement.exported) {
+ exportedImportedAllNamed[statement.exported.name] = entry;
+ } else {
+ exportedImportedAll.push(entry);
+ }
+ }
+ }
+ } else if (statement.type === AST_NODE_TYPES.TSExportAssignment) {
+ // Form: `export = ...`
+ if (statement.expression.type === AST_NODE_TYPES.Identifier) {
+ exportAssignment = statement.expression.name;
+ } else if (statement.expression.type === AST_NODE_TYPES.ClassExpression) {
+ exportAssignment = {
+ ...statement.expression,
+ type: AST_NODE_TYPES.ClassDeclaration,
+ };
}
} else if (statement.type === AST_NODE_TYPES.ClassDeclaration && statement.id) {
// Form: `declare class A {}`
@@ -337,16 +655,32 @@ export class ClassLoader {
} else if (statement.type === AST_NODE_TYPES.TSInterfaceDeclaration && statement.id) {
// Form: `declare interface A {}`
declaredInterfaces[statement.id.name] = statement;
+ } else if (statement.type === AST_NODE_TYPES.TSTypeAliasDeclaration && statement.id) {
+ // Form: `declare type A = ...`
+ declaredTypes[statement.id.name] = statement;
+ } else if (statement.type === AST_NODE_TYPES.TSEnumDeclaration && statement.id) {
+ // Form: `declare enum A {...}`
+ declaredEnums[statement.id.name] = statement;
+ } else if (statement.type === AST_NODE_TYPES.TSModuleDeclaration && statement.id && 'name' in statement.id) {
+ // Form `declare namespace A { ... }
+ declaredNamespaces[statement.id.name] = statement;
} else if (statement.type === AST_NODE_TYPES.ImportDeclaration &&
statement.source.type === AST_NODE_TYPES.Literal &&
typeof statement.source.value === 'string') {
- // Form: `import {A} from './lib/A'`
- for (const specifier of statement.specifiers) {
- if (specifier.type === AST_NODE_TYPES.ImportSpecifier) {
- importedElements[specifier.local.name] = {
- localName: specifier.imported.name,
- ...this.importTargetToAbsolutePath(packageName, fileName, statement.source.value),
- };
+ const entry = this.importTargetToAbsolutePath(packageName, fileName, statement.source.value);
+ if (entry) {
+ for (const specifier of statement.specifiers) {
+ if (specifier.type === AST_NODE_TYPES.ImportSpecifier) {
+ // Form: `import {A} from './lib/A'`
+ importedElements[specifier.local.name] = {
+ localName: specifier.imported.name,
+ qualifiedPath: undefined,
+ ...entry,
+ };
+ } else if (specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier) {
+ // Form: `import * as A from './lib/A'`
+ importedElementsAllNamed[specifier.local.name] = entry;
+ }
}
}
}
@@ -355,12 +689,21 @@ export class ClassLoader {
return {
exportedClasses,
exportedInterfaces,
+ exportedTypes,
+ exportedEnums,
+ exportedNamespaces,
exportedImportedElements,
exportedImportedAll,
+ exportedImportedAllNamed,
exportedUnknowns,
declaredClasses,
declaredInterfaces,
+ declaredTypes,
+ declaredEnums,
+ declaredNamespaces,
importedElements,
+ importedElementsAllNamed,
+ exportAssignment,
};
}
}
@@ -368,6 +711,7 @@ export class ClassLoader {
export interface ClassLoaderArgs {
resolutionContext: ResolutionContext;
logger: Logger;
+ commentLoader: CommentLoader;
}
/**
@@ -375,19 +719,37 @@ export interface ClassLoaderArgs {
*/
export interface ClassElements {
// Classes that have been declared in a file via `export class A`
- exportedClasses: Record;
+ exportedClasses: Record;
// Interfaces that have been declared in a file via `export interface A`
- exportedInterfaces: Record;
+ exportedInterfaces: Record;
+ // Types that have been declared in a file via `export type A = ...`
+ exportedTypes: Record;
+ // Enums that have been declared in a file via `export enum A {...}`
+ exportedEnums: Record;
+ // Namespaces that have been declared in a file via `export namespace A { ... }`
+ exportedNamespaces: Record;
// Elements that have been exported via `export { A as B } from "b"`
exportedImportedElements: Record;
// Exports via `export * from "b"`
- exportedImportedAll: { packageName: string; fileName: string }[];
+ exportedImportedAll: { packageName: string; fileName: string; fileNameReferenced: string }[];
+ // Exports via `export * as A from "b"`
+ exportedImportedAllNamed: Record;
// Things that have been exported via `export {A as B}`, where the target is not known
exportedUnknowns: Record;
// Classes that have been declared in a file via `declare class A`
- declaredClasses: Record;
+ declaredClasses: Record;
// Interfaces that have been declared in a file via `declare interface A`
- declaredInterfaces: Record;
+ declaredInterfaces: Record;
+ // Types that have been declared in a file via `declare type A = ...`
+ declaredTypes: Record;
+ // Enums that have been declared in a file via `declare enum A {...}`
+ declaredEnums: Record;
+ // Namespaces that have been declared in a file via `declare namespace A { ... }`
+ declaredNamespaces: Record;
// Elements that are imported from elsewhere via `import {A} from ''`
importedElements: Record;
+ // Elements that are imported from elsewhere via `import * as A from ''`
+ importedElementsAllNamed: Record;
+ // Element exported via `export = ...`
+ exportAssignment: string | TSESTree.ClassDeclaration | undefined;
}
diff --git a/lib/parse/CommentLoader.ts b/lib/parse/CommentLoader.ts
index b73c271..d498815 100644
--- a/lib/parse/CommentLoader.ts
+++ b/lib/parse/CommentLoader.ts
@@ -1,28 +1,62 @@
-import type { ClassDeclaration, TSInterfaceDeclaration,
- MethodDefinition, TSPropertySignature, TSIndexSignature, BaseNode } from '@typescript-eslint/types/dist/ts-estree';
-import * as commentParse from 'comment-parser';
+import type { TSESTree } from '@typescript-eslint/typescript-estree';
+import { parse } from 'comment-parser';
import type { ClassReference, ClassReferenceLoaded } from './ClassIndex';
-import type { ParameterRangeUnresolved } from './ParameterLoader';
+import type { ConstructorHolder } from './ConstructorLoader';
+import type { DefaultNested, DefaultValue, ParameterRangeUnresolved } from './ParameterLoader';
/**
* Loads comments from fields in a given class.
*/
export class CommentLoader {
- private readonly classLoaded: ClassReferenceLoaded;
-
- public constructor(args: CommentLoaderArgs) {
- this.classLoaded = args.classLoaded;
+ /**
+ * Extract comment data from the given constructor inheritance chain.
+ * @param constructorChain An array of constructors within the class inheritance chain.
+ */
+ public getCommentDataFromConstructor(constructorChain: ConstructorHolder[]): ConstructorCommentData {
+ // Merge comment data about each field so that the closest classes in the inheritance chain have
+ // the highest priority in setting comment data.
+ return constructorChain
+ .map(constructorHolder => this.getCommentDataFromConstructorSingle(
+ constructorHolder.classLoaded.value,
+ constructorHolder.constructor,
+ ))
+ .reduce((acc, commentData) => {
+ for (const [ key, value ] of Object.entries(commentData)) {
+ if (key in acc) {
+ acc[key] = {
+ // eslint-disable-next-line ts/prefer-nullish-coalescing
+ range: acc[key].range || value.range,
+ // eslint-disable-next-line ts/prefer-nullish-coalescing
+ defaults: [ ...acc[key].defaults || [], ...value.defaults || [] ],
+ // eslint-disable-next-line ts/prefer-nullish-coalescing
+ ignored: acc[key].ignored || value.ignored,
+ // eslint-disable-next-line ts/prefer-nullish-coalescing
+ description: acc[key].description || value.description,
+ params: { ...acc[key].params, ...value.params },
+ // eslint-disable-next-line ts/prefer-nullish-coalescing
+ defaultNested: [ ...acc[key].defaultNested || [], ...value.defaultNested || [] ],
+ };
+ } else {
+ acc[key] = value;
+ }
+ }
+ return acc;
+ }, {});
}
/**
* Extract comment data from the given constructor.
+ * @param classLoaded The loaded class in which the constructor is defined.
* @param constructor A constructor.
*/
- public getCommentDataFromConstructor(constructor: MethodDefinition): ConstructorCommentData {
+ public getCommentDataFromConstructorSingle(
+ classLoaded: ClassReferenceLoaded,
+ constructor: TSESTree.MethodDefinition,
+ ): ConstructorCommentData {
// Get the constructor comment
- const comment = this.getCommentRaw(constructor);
+ const comment = this.getCommentRaw(classLoaded, constructor);
if (comment) {
- return CommentLoader.getCommentDataFromConstructorComment(comment, this.classLoaded);
+ return CommentLoader.getCommentDataFromConstructorComment(comment, classLoaded);
}
return {};
@@ -40,7 +74,16 @@ export class CommentLoader {
const commentData = CommentLoader.getCommentDataFromComment(comment, clazz);
if (commentData.params) {
for (const [ key, value ] of Object.entries(commentData.params)) {
- data[key] = CommentLoader.getCommentDataFromComment(`/**${value.replace(/@/gu, '\n * @')}*/`, clazz);
+ const subCommentData = CommentLoader.getCommentDataFromComment(`/**${value.replaceAll(' @', '\n * @')}*/`, clazz);
+
+ // Since we're in the scope of a param (key), prepend the defaultNested paramPath array with the current param.
+ if (subCommentData.defaultNested) {
+ for (const defaultNested of subCommentData.defaultNested) {
+ defaultNested.paramPath.unshift(key);
+ }
+ }
+
+ data[key] = subCommentData;
}
}
@@ -49,24 +92,28 @@ export class CommentLoader {
/**
* Extract comment data from the given field.
+ * @param classLoaded The loaded class in which the field is defined.
* @param field A field.
*/
- public getCommentDataFromField(field: TSPropertySignature | TSIndexSignature): CommentData {
- const comment = this.getCommentRaw(field);
+ public getCommentDataFromField(
+ classLoaded: ClassReferenceLoaded,
+ field: TSESTree.TSPropertySignature | TSESTree.TSIndexSignature,
+ ): CommentData {
+ const comment = this.getCommentRaw(classLoaded, field);
if (comment) {
- return CommentLoader.getCommentDataFromComment(comment, this.classLoaded);
+ return CommentLoader.getCommentDataFromComment(comment, classLoaded);
}
return {};
}
/**
* Extract comment data from the given class.
- * @param clazz A class or interface.
+ * @param classLoaded The loaded class or interface.
*/
- public getCommentDataFromClassOrInterface(clazz: ClassDeclaration | TSInterfaceDeclaration): CommentData {
- const comment = this.getCommentRaw(clazz);
+ public getCommentDataFromClassOrInterface(classLoaded: ClassReferenceLoaded): CommentData {
+ const comment = this.getCommentRaw(classLoaded, classLoaded.declaration);
if (comment) {
- return CommentLoader.getCommentDataFromComment(comment, this.classLoaded);
+ return CommentLoader.getCommentDataFromComment(comment, classLoaded);
}
return {};
}
@@ -79,11 +126,11 @@ export class CommentLoader {
public static getCommentDataFromComment(comment: string, clazz: ClassReference): CommentData {
const data: CommentData = {};
- const commentParsed = commentParse(comment)[0];
+ const commentParsed = parse(comment)[0];
if (commentParsed) {
// Extract description
if (commentParsed.description.length > 0) {
- data.description = commentParsed.description.replace(/\n/gu, ' ');
+ data.description = commentParsed.description.replaceAll('\n', ' ');
}
// Extract tags
@@ -102,7 +149,10 @@ export class CommentLoader {
if (tag.type.length === 0) {
throw new Error(`Missing @default value {something} on a field in class ${clazz.localName} at ${clazz.fileName}`);
}
- data.default = tag.type;
+ if (!data.defaults) {
+ data.defaults = [];
+ }
+ data.defaults.push(CommentLoader.getDefaultValue(tag.type, clazz));
break;
case 'ignored':
data.ignored = true;
@@ -116,6 +166,18 @@ export class CommentLoader {
data.params[tag.name] = data.params[tag.name].slice(2);
}
break;
+ case 'defaultnested':
+ if (tag.type.length === 0 || tag.name.length === 0) {
+ throw new Error(`Invalid @defaultNested syntax on a field in class ${clazz.localName} at ${clazz.fileName}: expected @defaultNested { a } path_to_param`);
+ }
+ if (!data.defaultNested) {
+ data.defaultNested = [];
+ }
+ data.defaultNested.push({
+ paramPath: tag.name.split('_'),
+ value: CommentLoader.getDefaultValue(tag.type, clazz),
+ });
+ break;
}
}
}
@@ -123,13 +185,54 @@ export class CommentLoader {
return data;
}
+ /**
+ * Parse the microsyntax of a default value.
+ *
+ * Can be one of:
+ * * raw value: "abc"
+ * * iri value: ""
+ * * type value: "a "
+ * * iri and type value: " a "
+ *
+ * @param value A default value string.
+ * @param clazz The class reference this value is loaded in.
+ */
+ public static getDefaultValue(value: string, clazz: ClassReference): DefaultValue {
+ if (!value.startsWith('<') && !value.startsWith('a ')) {
+ return {
+ type: 'raw',
+ value,
+ };
+ }
+
+ const [ idRaw, typeRaw ] = value.startsWith('a ') ?
+ [ undefined, value.slice(2) ] :
+ value.split(' a ');
+ return {
+ type: 'iri',
+ value: idRaw ? CommentLoader.getIriValue(idRaw) : undefined,
+ typeIri: typeRaw ? CommentLoader.getIriValue(typeRaw) : undefined,
+ baseComponent: clazz,
+ };
+ }
+
+ /**
+ * Unbox an IRI wrapped in <>
+ * @param iriBoxed An iri string within <>
+ */
+ public static getIriValue(iriBoxed: string): string | undefined {
+ const match = /^<([^>]*)>$/u.exec(iriBoxed);
+ return match ? match[1] : undefined;
+ }
+
/**
* Get the comment string from the given node.
+ * @param classLoaded The loaded class in which the field is defined.
* @param node A node, such as a field or constructor.
*/
- public getCommentRaw(node: BaseNode): string | undefined {
+ public getCommentRaw(classLoaded: ClassReferenceLoaded, node: TSESTree.BaseNode): string | undefined {
const line = node.loc.start.line;
- for (const comment of this.classLoaded.ast.comments || []) {
+ for (const comment of classLoaded.ast.comments ?? []) {
if (comment.loc.end.line === line - 1) {
return `/*${comment.value}*/`;
}
@@ -137,10 +240,9 @@ export class CommentLoader {
}
}
-export interface CommentLoaderArgs {
- classLoaded: ClassReferenceLoaded;
-}
-
+/**
+ * Maps field keys to comments.
+ */
export type ConstructorCommentData = Record;
export interface CommentData {
@@ -149,9 +251,9 @@ export interface CommentData {
*/
range?: ParameterRangeUnresolved;
/**
- * The default value.
+ * The default values.
*/
- default?: string;
+ defaults?: DefaultValue[];
/**
* If the field referenced by this comment should be ignored.
*/
@@ -164,4 +266,8 @@ export interface CommentData {
* Parameters that were defined in this comment.
*/
params?: Record;
+ /**
+ * The nested default values on parameters.
+ */
+ defaultNested?: DefaultNested[];
}
diff --git a/lib/parse/ConstructorLoader.ts b/lib/parse/ConstructorLoader.ts
index 9b32a31..87bc4ea 100644
--- a/lib/parse/ConstructorLoader.ts
+++ b/lib/parse/ConstructorLoader.ts
@@ -1,14 +1,18 @@
-import type { ClassDeclaration, MethodDefinition } from '@typescript-eslint/types/dist/ts-estree';
-import type { AST, TSESTreeOptions } from '@typescript-eslint/typescript-estree';
+import type { AST, TSESTreeOptions, TSESTree } from '@typescript-eslint/typescript-estree';
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
-import type { ClassIndex, ClassLoaded, ClassReferenceLoaded } from './ClassIndex';
-import type { ParameterDataField, ParameterRangeUnresolved } from './ParameterLoader';
-import { ParameterLoader } from './ParameterLoader';
+import type { ClassIndex, ClassLoaded, ClassReferenceLoaded, GenericallyTyped } from './ClassIndex';
+import type { ParameterDataField, ParameterRangeUnresolved, ParameterLoader } from './ParameterLoader';
/**
* Loads the constructor data of classes.
*/
export class ConstructorLoader {
+ private readonly parameterLoader: ParameterLoader;
+
+ public constructor(args: ConstructorLoaderArgs) {
+ this.parameterLoader = args.parameterLoader;
+ }
+
/**
* Create a class index containing all constructor data from the classes in the given index.
* @param classIndex An index of loaded classes.
@@ -17,42 +21,66 @@ export class ConstructorLoader {
classIndex: ClassIndex,
): ClassIndex> {
const constructorDataIndex: ClassIndex> = {};
- for (const [ className, classLoaded ] of Object.entries(classIndex)) {
- const constructor = classLoaded.type === 'class' ? this.getConstructor(classLoaded) : undefined;
- if (constructor) {
- const parameterLoader = new ParameterLoader({ classLoaded });
- constructorDataIndex[className] = parameterLoader.loadConstructorFields(constructor);
- } else {
- constructorDataIndex[className] = { parameters: []};
+ for (const [ className, classLoadedRoot ] of Object.entries(classIndex)) {
+ // Initialize default value
+ constructorDataIndex[className] = {
+ parameters: [],
+ classLoaded: classLoadedRoot,
+ };
+
+ // Fill in constructor data if we're loading a class, and we find a constructor in the inheritance chain.
+ if (classLoadedRoot.type === 'class') {
+ const constructorChain = this.getConstructorChain({ value: classLoadedRoot });
+ if (constructorChain.length > 0) {
+ constructorDataIndex[className] = this.parameterLoader.loadConstructorFields(constructorChain);
+ }
}
}
return constructorDataIndex;
}
+ /**
+ * Load the superclass chain of constructor holders starting from the given class.
+ * @param classLoaded The class to start from.
+ */
+ public getConstructorChain(classLoaded: GenericallyTyped): ConstructorHolder[] {
+ const constructorData = this.getConstructor(classLoaded);
+ const chain: ConstructorHolder[] = [];
+ if (constructorData) {
+ chain.push(constructorData);
+ if (constructorData.classLoaded.value.superClass) {
+ chain.push(...this.getConstructorChain(constructorData.classLoaded.value.superClass));
+ }
+ }
+ return chain;
+ }
+
/**
* Retrieve the constructor in the given class, or its super class.
* Can be undefined if no explicit constructor exists in this class or any of its super classes.
* @param classLoaded A loaded class reference.
*/
- public getConstructor(classLoaded: ClassLoaded): MethodDefinition | undefined {
+ public getConstructor(classLoaded: GenericallyTyped): ConstructorHolder | undefined {
// First look for the constructor in this class
- let constructor = this.getConstructorInClass(classLoaded.declaration);
+ let constructor: TSESTree.MethodDefinition | undefined = this.getConstructorInClass(classLoaded.value.declaration);
// If no constructor was found, look in the super class
- if (!constructor) {
- if (classLoaded.superClass) {
- constructor = this.getConstructor(classLoaded.superClass);
+ if (!constructor && classLoaded.value.superClass) {
+ const constructorDataSuper = this.getConstructor(classLoaded.value.superClass);
+ if (constructorDataSuper) {
+ constructor = constructorDataSuper.constructor;
+ classLoaded = constructorDataSuper.classLoaded;
}
}
- return constructor;
+ return constructor ? { constructor, classLoaded } : undefined;
}
/**
* Retrieve the constructor in the given class, or undefined if it could not be found.
* @param declaration A class declaration
*/
- public getConstructorInClass(declaration: ClassDeclaration): MethodDefinition | undefined {
+ public getConstructorInClass(declaration: TSESTree.ClassDeclaration): TSESTree.MethodDefinition | undefined {
for (const element of declaration.body.body) {
if (element.type === AST_NODE_TYPES.MethodDefinition &&
element.kind === 'constructor') {
@@ -68,7 +96,7 @@ export class ConstructorLoader {
* @param ast A parsed typescript file
* @param fileName The file name, for error reporting.
*/
- public getClass(className: string, ast: AST, fileName: string): ClassDeclaration {
+ public getClass(className: string, ast: AST, fileName: string): TSESTree.ClassDeclaration {
for (const statement of ast.body) {
// Classes in the form of `declare class A {}`
if (statement.type === AST_NODE_TYPES.ClassDeclaration &&
@@ -89,9 +117,22 @@ export class ConstructorLoader {
}
}
+export interface ConstructorLoaderArgs {
+ parameterLoader: ParameterLoader;
+}
+
/**
* Constructor parameter information
*/
export interface ConstructorData {
parameters: ParameterDataField[];
+ classLoaded: ClassReferenceLoaded;
+}
+
+/**
+ * Datastructure for holding a constructor and the class it is part of.
+ */
+export interface ConstructorHolder {
+ constructor: TSESTree.MethodDefinition;
+ classLoaded: GenericallyTyped;
}
diff --git a/lib/parse/GenericsLoader.ts b/lib/parse/GenericsLoader.ts
new file mode 100644
index 0000000..a16fbeb
--- /dev/null
+++ b/lib/parse/GenericsLoader.ts
@@ -0,0 +1,45 @@
+import type { ClassIndex, ClassReferenceLoaded } from './ClassIndex';
+import type {
+ GenericTypeParameterData,
+ ParameterLoader,
+ ParameterRangeUnresolved,
+} from './ParameterLoader';
+
+/**
+ * Loads the generics data of classes.
+ */
+export class GenericsLoader {
+ private readonly parameterLoader: ParameterLoader;
+
+ public constructor(args: GenericsLoaderArgs) {
+ this.parameterLoader = args.parameterLoader;
+ }
+
+ /**
+ * Create a class index containing all generics data from the classes in the given index.
+ * @param classIndex An index of loaded classes.
+ */
+ public getGenerics(
+ classIndex: ClassIndex,
+ ): ClassIndex> {
+ const genericsDataIndex: ClassIndex> = {};
+ for (const [ className, classLoadedRoot ] of Object.entries(classIndex)) {
+ if (classLoadedRoot.type === 'class' || classLoadedRoot.type === 'interface') {
+ genericsDataIndex[className] = this.parameterLoader.loadClassGenerics(classLoadedRoot);
+ }
+ }
+ return genericsDataIndex;
+ }
+}
+
+export interface GenericsLoaderArgs {
+ parameterLoader: ParameterLoader;
+}
+
+/**
+ * Generics parameter information
+ */
+export interface GenericsData {
+ genericTypeParameters: GenericTypeParameterData[];
+ classLoaded: ClassReferenceLoaded;
+}
diff --git a/lib/parse/MemberLoader.ts b/lib/parse/MemberLoader.ts
new file mode 100644
index 0000000..753db6e
--- /dev/null
+++ b/lib/parse/MemberLoader.ts
@@ -0,0 +1,83 @@
+import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
+import type { ClassReferenceLoaded, ClassReferenceLoadedClassOrInterface, ClassIndex } from './ClassIndex';
+
+import type { MemberParameterData, ParameterLoader, ParameterRangeUnresolved } from './ParameterLoader';
+
+/**
+ * Loads the member data of classes.
+ */
+export class MemberLoader {
+ private readonly parameterLoader: ParameterLoader;
+
+ public constructor(args: MemberLoaderArgs) {
+ this.parameterLoader = args.parameterLoader;
+ }
+
+ /**
+ * Create a class index containing all member data from the classes in the given index.
+ * @param classIndex An index of loaded classes.
+ */
+ public getMembers(
+ classIndex: ClassIndex,
+ ): ClassIndex> {
+ const membersIndex: ClassIndex> = {};
+ for (const [ className, classLoadedRoot ] of Object.entries(classIndex)) {
+ if (classLoadedRoot.type === 'class' || classLoadedRoot.type === 'interface') {
+ membersIndex[className] = {
+ members: this.collectClassFields(classLoadedRoot),
+ classLoaded: classLoadedRoot,
+ };
+ }
+ }
+ return membersIndex;
+ }
+
+ /**
+ * Obtain the class member fields.
+ * This should correspond to the keys that are available within the `keyof` range of this class
+ * @param classLoaded A class or interface
+ */
+ public collectClassFields(
+ classLoaded: ClassReferenceLoadedClassOrInterface,
+ ): MemberParameterData[] {
+ const members: MemberParameterData[] = [];
+ for (const element of classLoaded.declaration.body.body) {
+ // eslint-disable-next-line ts/switch-exhaustiveness-check
+ switch (element.type) {
+ case AST_NODE_TYPES.PropertyDefinition:
+ case AST_NODE_TYPES.TSAbstractPropertyDefinition:
+ case AST_NODE_TYPES.MethodDefinition:
+ case AST_NODE_TYPES.TSAbstractMethodDefinition:
+ case AST_NODE_TYPES.TSPropertySignature:
+ case AST_NODE_TYPES.TSMethodSignature:
+ if (element.key.type === 'Identifier') {
+ // TODO: more types may be needed here, such as AST_NODE_TYPES.TSPropertySignature
+ const typeNode = element.type === AST_NODE_TYPES.PropertyDefinition ||
+ element.type === AST_NODE_TYPES.TSAbstractPropertyDefinition ?
+ element.typeAnnotation?.typeAnnotation :
+ undefined;
+ members.push({
+ name: element.key.name,
+ range: typeNode ?
+ this.parameterLoader.getRangeFromTypeNode(classLoaded, typeNode, `field ${element.key.name}`) :
+ undefined,
+ });
+ }
+ break;
+ }
+ }
+ return members;
+ }
+}
+
+/**
+ * Member parameter information
+ */
+export interface MemberData {
+ members: MemberParameterData[];
+ classLoaded: ClassReferenceLoaded;
+}
+
+export interface MemberLoaderArgs {
+ parameterLoader: ParameterLoader;
+}
diff --git a/lib/parse/PackageMetadataLoader.ts b/lib/parse/PackageMetadataLoader.ts
index 0dc8644..5777651 100644
--- a/lib/parse/PackageMetadataLoader.ts
+++ b/lib/parse/PackageMetadataLoader.ts
@@ -1,17 +1,17 @@
-import * as Path from 'path';
import semverMajor = require('semver/functions/major');
import type { ResolutionContext } from '../resolution/ResolutionContext';
+import { joinFilePath } from '../util/PathUtil';
/**
- * Load metadata from packages.
+ * Load metadata from a package.
*/
export class PackageMetadataLoader {
private readonly resolutionContext: ResolutionContext;
- private readonly prefix?: string;
+ private readonly prefixes?: string | Record;
public constructor(args: PackageMetadataLoaderArgs) {
this.resolutionContext = args.resolutionContext;
- this.prefix = args.prefix;
+ this.prefixes = args.prefixes;
}
/**
@@ -20,7 +20,7 @@ export class PackageMetadataLoader {
*/
public async load(packageRootDirectory: string): Promise {
// Read package.json
- const packageJsonPath = Path.join(packageRootDirectory, 'package.json');
+ const packageJsonPath = joinFilePath(packageRootDirectory, 'package.json');
const packageJsonRaw = await this.resolutionContext.getFileContent(packageJsonPath);
let packageJson: any;
try {
@@ -54,7 +54,7 @@ export class PackageMetadataLoader {
if (!('lsd:components' in packageJson)) {
throw new Error(`Invalid package: Missing 'lsd:components' in ${packageJsonPath}`);
}
- const componentsPath = Path.join(packageRootDirectory, packageJson['lsd:components']);
+ const componentsPath = joinFilePath(packageRootDirectory, packageJson['lsd:components']);
if (!('lsd:contexts' in packageJson)) {
throw new Error(`Invalid package: Missing 'lsd:contexts' in ${packageJsonPath}`);
}
@@ -66,11 +66,14 @@ export class PackageMetadataLoader {
if (!('types' in packageJson) && !('typings' in packageJson)) {
throw new Error(`Invalid package: Missing 'types' or 'typings' in ${packageJsonPath}`);
}
- let typesPath = Path.join(packageRootDirectory, packageJson.types || packageJson.typings);
+ let typesPath = joinFilePath(packageRootDirectory, packageJson.types || packageJson.typings);
if (typesPath.endsWith('.d.ts')) {
typesPath = typesPath.slice(0, -5);
}
+ // Determine prefixes
+ const prefix = !this.prefixes || typeof this.prefixes === 'string' ? this.prefixes : this.prefixes[name];
+
// Construct metadata hash
return {
name,
@@ -80,14 +83,14 @@ export class PackageMetadataLoader {
contexts,
importPaths,
typesPath,
- prefix: this.prefix,
+ prefix,
};
}
}
export interface PackageMetadataLoaderArgs {
resolutionContext: ResolutionContext;
- prefix?: string;
+ prefixes?: string | Record;
}
export interface PackageMetadata {
diff --git a/lib/parse/ParameterLoader.ts b/lib/parse/ParameterLoader.ts
index 337036a..73d8691 100644
--- a/lib/parse/ParameterLoader.ts
+++ b/lib/parse/ParameterLoader.ts
@@ -1,19 +1,16 @@
-import type {
- Identifier,
- MethodDefinition,
- TSPropertySignature,
- TSTypeLiteral,
- TypeElement,
- TypeNode,
- TSIndexSignature,
- TSTypeReference,
- Parameter,
-} from '@typescript-eslint/types/dist/ts-estree';
+import type { TSESTree } from '@typescript-eslint/typescript-estree';
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
-import type { ClassReferenceLoaded, InterfaceLoaded } from './ClassIndex';
-import type { CommentData, ConstructorCommentData } from './CommentLoader';
-import { CommentLoader } from './CommentLoader';
-import type { ConstructorData } from './ConstructorLoader';
+import type { Logger } from 'winston';
+import type {
+ ClassReferenceLoaded,
+ InterfaceLoaded,
+ ClassReference,
+ ClassReferenceLoadedClassOrInterface,
+ ClassIndex,
+} from './ClassIndex';
+import type { CommentData, ConstructorCommentData, CommentLoader } from './CommentLoader';
+import type { ConstructorData, ConstructorHolder } from './ConstructorLoader';
+import type { GenericsData } from './GenericsLoader';
import type { TypeReferenceOverride } from './typereferenceoverride/TypeReferenceOverride';
import { TypeReferenceOverrideAliasRecord } from './typereferenceoverride/TypeReferenceOverrideAliasRecord';
@@ -25,50 +22,178 @@ export class ParameterLoader {
new TypeReferenceOverrideAliasRecord(),
];
- private readonly classLoaded: ClassReferenceLoaded;
private readonly commentLoader: CommentLoader;
+ private readonly hardErrorUnsupported: boolean;
+ private readonly logger: Logger;
public constructor(args: ParameterLoaderArgs) {
- this.classLoaded = args.classLoaded;
- this.commentLoader = new CommentLoader({ classLoaded: this.classLoaded });
+ this.commentLoader = args.commentLoader;
+ this.hardErrorUnsupported = args.hardErrorUnsupported;
+ this.logger = args.logger;
+ }
+
+ /**
+ * Create a class index containing all constructor data from the classes in the given index.
+ * @param classIndex An index of loaded classes.
+ */
+ public loadAllExtensionData(
+ classIndex: ClassIndex,
+ ): ClassIndex[]> {
+ const newIndex: ClassIndex[]> = {};
+ for (const [ key, classLoaded ] of Object.entries(classIndex)) {
+ if (classLoaded.type === 'class' || classLoaded.type === 'interface') {
+ newIndex[key] = this.loadExtensionData(classLoaded);
+ }
+ }
+ return newIndex;
+ }
+
+ /**
+ * Load the extension data of the given class or interface.
+ * @param classReference A loaded class or interface reference.
+ */
+ public loadExtensionData(
+ classReference: ClassReferenceLoadedClassOrInterface,
+ ): ExtensionData[] {
+ const extensionDatas: ExtensionData[] = [];
+ if (classReference.type === 'class') {
+ if (classReference.superClass) {
+ extensionDatas.push({
+ classLoaded: classReference.superClass.value,
+ genericTypeInstantiations: classReference.superClass.genericTypeInstantiations ?
+ this.getGenericTypeParameterInstantiations(
+ classReference.superClass.genericTypeInstantiations,
+ classReference,
+ ) :
+ [],
+ });
+ }
+ if (classReference.implementsInterfaces) {
+ for (const iface of classReference.implementsInterfaces) {
+ extensionDatas.push({
+ classLoaded: iface.value,
+ genericTypeInstantiations: iface.genericTypeInstantiations ?
+ this.getGenericTypeParameterInstantiations(iface.genericTypeInstantiations, classReference) :
+ [],
+ });
+ }
+ }
+ } else if (classReference.superInterfaces) {
+ for (const iface of classReference.superInterfaces) {
+ extensionDatas.push({
+ classLoaded: iface.value,
+ genericTypeInstantiations: iface.genericTypeInstantiations ?
+ this.getGenericTypeParameterInstantiations(iface.genericTypeInstantiations, classReference) :
+ [],
+ });
+ }
+ }
+ return extensionDatas;
}
/**
- * Load all parameter data from all fields in the given constructor.
- * @param constructor A constructor
+ * Load all parameter data from all fields in the given constructor inheritance chain.
+ * @param constructorChain An array of constructors within the class inheritance chain.
*/
- public loadConstructorFields(constructor: MethodDefinition): ConstructorData {
+ public loadConstructorFields(
+ constructorChain: ConstructorHolder[],
+ ): ConstructorData {
+ const classLoaded = constructorChain[0].classLoaded.value;
+
// Load the constructor comment
- const constructorCommentData = this.commentLoader.getCommentDataFromConstructor(constructor);
+ const constructorCommentData = this.commentLoader.getCommentDataFromConstructor(constructorChain);
// Load all constructor parameters
const parameters: ParameterDataField[] = [];
- for (const field of constructor.value.params) {
- this.loadConstructorField(parameters, constructorCommentData, field);
+ for (const field of constructorChain[0].constructor.value.params) {
+ this.loadConstructorField(classLoaded, parameters, constructorCommentData, field);
+ }
+
+ return {
+ parameters,
+ classLoaded,
+ };
+ }
+
+ /**
+ * Load generics types from the given class.
+ * @param classLoaded A loaded class.
+ */
+ public loadClassGenerics(classLoaded: ClassReferenceLoadedClassOrInterface): GenericsData {
+ // Load all generic type parameters
+ const genericTypeParameters: GenericTypeParameterData[] = [];
+ for (const [ genericName, genericType ] of Object.entries(classLoaded.generics)) {
+ this.loadClassGeneric(
+ classLoaded,
+ genericTypeParameters,
+ genericName,
+ genericType.type,
+ genericType.default,
+ );
}
- return { parameters };
+
+ return {
+ genericTypeParameters,
+ classLoaded,
+ };
+ }
+
+ /**
+ * Load the generic type parameter data from the given generic in a class.
+ * @param classLoaded The loaded class in which the field is defined.
+ * @param genericTypeParameters The array of generic type parameters that will be appended to.
+ * @param genericName The generic type name.
+ * @param genericType The optional generic type range.
+ * @param genericDefault The optional generic default value.
+ */
+ public loadClassGeneric(
+ classLoaded: ClassReferenceLoaded,
+ genericTypeParameters: GenericTypeParameterData[],
+ genericName: string,
+ genericType: TSESTree.TypeNode | undefined,
+ genericDefault: TSESTree.TypeNode | undefined,
+ ): void {
+ genericTypeParameters.push({
+ name: genericName,
+ ...genericType ?
+ { range: this.getRangeFromTypeNode(
+ classLoaded,
+ genericType,
+ this.getErrorIdentifierGeneric(classLoaded, genericName),
+ ) } :
+ {},
+ ...genericDefault ?
+ { default: this.getRangeFromTypeNode(
+ classLoaded,
+ genericDefault,
+ this.getErrorIdentifierGeneric(classLoaded, genericName),
+ ) } :
+ {},
+ });
}
/**
* Load the parameter data from the given field in a constructor.
+ * @param classLoaded The loaded class in which the field is defined.
* @param parameters The array of parameters that will be appended to.
* @param constructorCommentData Comment data from the constructor.
* @param field The field to load.
*/
public loadConstructorField(
+ classLoaded: ClassReferenceLoaded,
parameters: ParameterDataField[],
constructorCommentData: ConstructorCommentData,
- field: Parameter,
+ field: TSESTree.Parameter,
): void {
if (field.type === AST_NODE_TYPES.Identifier) {
const commentData = constructorCommentData[field.name] || {};
if (!commentData.ignored) {
- parameters.push(this.loadField(field, commentData));
+ parameters.push(this.loadField(classLoaded, field, commentData));
}
} else if (field.type === AST_NODE_TYPES.TSParameterProperty) {
- this.loadConstructorField(parameters, constructorCommentData, field.parameter);
+ this.loadConstructorField(classLoaded, parameters, constructorCommentData, field.parameter);
} else {
- throw new Error(`Could not understand constructor parameter type ${field.type} in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
+ this.throwOrWarn(new Error(`Could not understand constructor parameter type ${field.type} in ${classLoaded.localName} at ${classLoaded.fileName}`));
}
}
@@ -78,67 +203,80 @@ export class ParameterLoader {
* @param iface An interface
*/
public loadInterfaceFields(iface: InterfaceLoaded): ParameterData[] {
- return []> iface.declaration.body.body
- .map(field => this.loadTypeElementField(field))
+ let fields: ParameterData[] = []> iface
+ .declaration.body.body
+ .map(field => this.loadTypeElementField(iface, field))
.filter(Boolean);
+ if (iface.superInterfaces && iface.superInterfaces.length > 0) {
+ // TODO: pass down superIface.genericTypeInstantiations to loadInterfaceFields
+ // eslint-disable-next-line unicorn/prefer-spread
+ fields = fields.concat(...iface.superInterfaces.map(superIface => this.loadInterfaceFields(superIface.value)));
+ }
+ return fields;
}
/**
* Load all parameter data from all fields in the given hash.
+ * @param classLoaded The loaded class in which the field is defined.
* @param hash An hash element.
*/
- public loadHashFields(hash: TSTypeLiteral): ParameterData[] {
+ public loadHashFields(
+ classLoaded: ClassReferenceLoaded,
+ hash: TSESTree.TSTypeLiteral,
+ ): ParameterData[] {
return []> hash.members
- .map(field => this.loadTypeElementField(field))
+ .map(field => this.loadTypeElementField(classLoaded, field))
.filter(Boolean);
}
/**
* Load the parameter data from the given type element.
+ * @param classLoaded The loaded class in which the field is defined.
* @param typeElement A type element, such as an interface or hash field.
*/
- public loadTypeElementField(typeElement: TypeElement): ParameterData | undefined {
+ public loadTypeElementField(
+ classLoaded: ClassReferenceLoaded,
+ typeElement: TSESTree.TypeElement,
+ ): ParameterData | undefined {
let commentData;
switch (typeElement.type) {
case AST_NODE_TYPES.TSPropertySignature:
- commentData = this.commentLoader.getCommentDataFromField(typeElement);
+ commentData = this.commentLoader.getCommentDataFromField(classLoaded, typeElement);
if (!commentData.ignored) {
- return this.loadField(typeElement, commentData);
+ return this.loadField(classLoaded, typeElement, commentData);
}
return;
case AST_NODE_TYPES.TSIndexSignature:
- commentData = this.commentLoader.getCommentDataFromField(typeElement);
+ commentData = this.commentLoader.getCommentDataFromField(classLoaded, typeElement);
if (!commentData.ignored) {
- return this.loadIndex(typeElement, commentData);
+ return this.loadIndex(classLoaded, typeElement, commentData);
}
return;
default:
- throw new Error(`Unsupported field type ${typeElement.type} in ${this.classLoaded.localName} in ${this.classLoaded.fileName}`);
+ this.throwOrWarn(new Error(`Unsupported field type ${typeElement.type} in ${classLoaded.localName} in ${classLoaded.fileName}`));
}
}
/**
* Load the parameter data from the given field.
+ * @param classLoaded The loaded class in which the field is defined.
* @param field A field.
* @param commentData Comment data about the given field.
*/
- public loadField(field: Identifier | TSPropertySignature, commentData: CommentData):
- ParameterDataField {
+ public loadField(
+ classLoaded: ClassReferenceLoaded,
+ field: TSESTree.Identifier | TSESTree.TSPropertySignature,
+ commentData: CommentData,
+ ): ParameterDataField {
// Required data
const parameterData: ParameterDataField = {
type: 'field',
- name: this.getFieldName(field),
- unique: this.isFieldUnique(field),
- required: this.isFieldRequired(field),
- range: this.getFieldRange(field, commentData),
+ name: this.getFieldName(classLoaded, field),
+ range: this.getFieldRange(classLoaded, field, commentData),
+ defaults: commentData.defaults,
+ defaultNested: commentData.defaultNested,
};
- // Optional data
- const defaultValue = this.getFieldDefault(commentData);
- if (defaultValue) {
- parameterData.default = defaultValue;
- }
-
const comment = this.getFieldComment(commentData);
if (comment) {
parameterData.comment = comment;
@@ -147,7 +285,10 @@ export class ParameterLoader {
return parameterData;
}
- public getFieldName(field: Identifier | TSPropertySignature): string {
+ public getFieldName(
+ classLoaded: ClassReferenceLoaded,
+ field: TSESTree.Identifier | TSESTree.TSPropertySignature,
+ ): string {
if ('name' in field) {
// If Identifier
return field.name;
@@ -156,26 +297,18 @@ export class ParameterLoader {
if (field.key.type === AST_NODE_TYPES.Identifier) {
return field.key.name;
}
- throw new Error(`Unsupported field key type ${field.key.type} in interface ${this.classLoaded.localName} in ${this.classLoaded.fileName}`);
+ throw new Error(`Unsupported field key type ${field.key.type} in interface ${classLoaded.localName} in ${classLoaded.fileName}`);
}
- public isFieldIndexedHash(field: Identifier | TSPropertySignature): boolean {
- return Boolean(field.typeAnnotation &&
- field.typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSTypeLiteral &&
- field.typeAnnotation.typeAnnotation.members.some(member => member.type === AST_NODE_TYPES.TSIndexSignature));
+ public getErrorIdentifierGeneric(classLoaded: ClassReferenceLoaded, genericName: string): string {
+ return `generic type ${genericName}`;
}
- public isFieldUnique(field: Identifier | TSPropertySignature): boolean {
- return !(field.typeAnnotation && field.typeAnnotation.typeAnnotation.type === AST_NODE_TYPES.TSArrayType) &&
- !this.isFieldIndexedHash(field);
- }
-
- public isFieldRequired(field: Identifier | TSPropertySignature): boolean {
- return !field.optional && !this.isFieldIndexedHash(field);
- }
-
- public getErrorIdentifierField(field: Identifier | TSPropertySignature): string {
- return `field ${this.getFieldName(field)}`;
+ public getErrorIdentifierField(
+ classLoaded: ClassReferenceLoaded,
+ field: TSESTree.Identifier | TSESTree.TSPropertySignature,
+ ): string {
+ return `field ${this.getFieldName(classLoaded, field)}`;
}
public getErrorIdentifierIndex(): string {
@@ -183,18 +316,12 @@ export class ParameterLoader {
}
public getRangeFromTypeNode(
- typeNode: TypeNode,
+ classLoaded: ClassReferenceLoaded,
+ typeNode: TSESTree.TypeNode,
errorIdentifier: string,
- nestedArrays = 0,
): ParameterRangeUnresolved {
- // Don't allow arrays to be nested
- if (nestedArrays > 1) {
- throw new Error(`Detected illegal nested array type for ${errorIdentifier
- } in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
- }
-
let typeAliasOverride: ParameterRangeUnresolved | undefined;
- // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
+ // eslint-disable-next-line ts/switch-exhaustiveness-check
switch (typeNode.type) {
case AST_NODE_TYPES.TSTypeReference:
if (typeNode.typeName.type === AST_NODE_TYPES.Identifier) {
@@ -207,20 +334,22 @@ export class ParameterLoader {
case 'String':
return { type: 'raw', value: 'string' };
case 'Array':
- if (typeNode.typeParameters && typeNode.typeParameters.params.length === 1) {
- return this.getRangeFromTypeNode(typeNode.typeParameters.params[0], errorIdentifier, nestedArrays + 1);
+ if (typeNode.typeArguments && typeNode.typeArguments.params.length === 1) {
+ return {
+ type: 'array',
+ value: this.getRangeFromTypeNode(classLoaded, typeNode.typeArguments.params[0], errorIdentifier),
+ };
}
- throw new Error(`Found invalid Array field type at ${errorIdentifier
- } in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
+ this.throwOrWarn(new Error(`Found invalid Array field type at ${errorIdentifier
+ } in ${classLoaded.localName} at ${classLoaded.fileName}`));
+ return { type: 'wildcard' };
default:
- // First check if the type is be a generic type
- if (typeNode.typeName.name in this.classLoaded.generics) {
- const genericProperties = this.classLoaded.generics[typeNode.typeName.name];
- if (!genericProperties.type) {
- throw new Error(`Found untyped generic field type at ${errorIdentifier
- } in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
- }
- return this.getRangeFromTypeNode(genericProperties.type, errorIdentifier);
+ // First check if the type is a direct generic type
+ if (classLoaded.type !== 'enum' && typeNode.typeName.name in classLoaded.generics) {
+ return {
+ type: 'genericTypeReference',
+ value: typeNode.typeName.name,
+ };
}
// Check if this node is a predefined type alias
@@ -230,50 +359,234 @@ export class ParameterLoader {
}
// Otherwise, assume we have an interface/class parameter
- return { type: 'interface', value: typeNode.typeName.name };
+ return {
+ type: 'interface',
+ value: typeNode.typeName.name,
+ genericTypeParameterInstantiations: typeNode.typeArguments ?
+ this.getGenericTypeParameterInstantiations(typeNode.typeArguments, classLoaded) :
+ undefined,
+ origin: classLoaded,
+ };
}
+ } else {
+ // Case: typeNode.typeName.type === AST_NODE_TYPES.TSQualifiedName
+ return {
+ type: 'interface',
+ value: ( typeNode.typeName).right.name,
+ qualifiedPath: this.getQualifiedPath(( typeNode.typeName).left),
+ genericTypeParameterInstantiations: typeNode.typeArguments ?
+ this.getGenericTypeParameterInstantiations(typeNode.typeArguments, classLoaded) :
+ undefined,
+ origin: classLoaded,
+ };
}
- break;
- case AST_NODE_TYPES.TSArrayType:
- return this.getRangeFromTypeNode(typeNode.elementType, errorIdentifier, nestedArrays + 1);
case AST_NODE_TYPES.TSBooleanKeyword:
return { type: 'raw', value: 'boolean' };
case AST_NODE_TYPES.TSNumberKeyword:
return { type: 'raw', value: 'number' };
case AST_NODE_TYPES.TSStringKeyword:
return { type: 'raw', value: 'string' };
+ case AST_NODE_TYPES.TSLiteralType:
+ if (typeNode.literal.type !== AST_NODE_TYPES.UnaryExpression &&
+ typeNode.literal.type !== AST_NODE_TYPES.UpdateExpression &&
+ 'value' in typeNode.literal &&
+ (typeof typeNode.literal.value === 'number' ||
+ typeof typeNode.literal.value === 'string' ||
+ typeof typeNode.literal.value === 'boolean')) {
+ return { type: 'literal', value: typeNode.literal.value };
+ }
+ break;
case AST_NODE_TYPES.TSTypeLiteral:
return { type: 'hash', value: typeNode };
- case AST_NODE_TYPES.TSUnknownKeyword:
+ case AST_NODE_TYPES.TSUnionType:
+ case AST_NODE_TYPES.TSIntersectionType:
+ return {
+ type: typeNode.type === AST_NODE_TYPES.TSUnionType ? 'union' : 'intersection',
+ elements: typeNode.types
+ .map(type => this.getRangeFromTypeNode(classLoaded, type, errorIdentifier)),
+ };
case AST_NODE_TYPES.TSUndefinedKeyword:
+ return { type: 'undefined' };
+ case AST_NODE_TYPES.TSUnknownKeyword:
case AST_NODE_TYPES.TSVoidKeyword:
case AST_NODE_TYPES.TSNullKeyword:
case AST_NODE_TYPES.TSAnyKeyword:
- case AST_NODE_TYPES.TSUnionType:
+ return { type: 'wildcard' };
+ case AST_NODE_TYPES.TSFunctionType:
+ case AST_NODE_TYPES.TSImportType:
+ case AST_NODE_TYPES.TSMappedType:
+ case AST_NODE_TYPES.TSNeverKeyword:
+ // TODO: These types are explicitly not supported at the moment
+ return { type: 'wildcard' };
case AST_NODE_TYPES.TSTupleType:
- return { type: 'undefined' };
+ return {
+ type: 'tuple',
+ elements: typeNode.elementTypes
+ .map(type => this.getRangeFromTypeNode(classLoaded, type, errorIdentifier)),
+ };
+ case AST_NODE_TYPES.TSArrayType:
+ return {
+ type: 'array',
+ value: this.getRangeFromTypeNode(classLoaded, typeNode.elementType, errorIdentifier),
+ };
+ case AST_NODE_TYPES.TSRestType:
+ return {
+ type: 'rest',
+ value: this.getRangeFromTypeNode(classLoaded, typeNode.typeAnnotation, errorIdentifier),
+ };
+ case AST_NODE_TYPES.TSTypeOperator:
+ if (typeNode.operator === 'keyof' && typeNode.typeAnnotation) {
+ return {
+ type: 'keyof',
+ value: this.getRangeFromTypeNode(classLoaded, typeNode.typeAnnotation, errorIdentifier),
+ };
+ }
+ break;
+ case AST_NODE_TYPES.TSTypeQuery:
+ if (typeNode.exprName.type === AST_NODE_TYPES.Identifier) {
+ return {
+ type: 'typeof',
+ value: typeNode.exprName.name,
+ origin: classLoaded,
+ };
+ }
+ if (typeNode.exprName.type === AST_NODE_TYPES.TSQualifiedName) {
+ // Otherwise we have a qualified name: AST_NODE_TYPES.TSQualifiedName
+ return {
+ type: 'typeof',
+ value: typeNode.exprName.right.name,
+ qualifiedPath: this.getQualifiedPath(typeNode.exprName.left),
+ origin: classLoaded,
+ };
+ }
+ break;
+ case AST_NODE_TYPES.TSIndexedAccessType:
+ return {
+ type: 'indexed',
+ object: this.getRangeFromTypeNode(classLoaded, typeNode.objectType, errorIdentifier),
+ index: this.getRangeFromTypeNode(classLoaded, typeNode.indexType, errorIdentifier),
+ };
}
- throw new Error(`Could not understand parameter type ${typeNode.type} of ${errorIdentifier
- } in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
+ this.throwOrWarn(new Error(`Could not understand parameter type ${typeNode.type} of ${errorIdentifier
+ } in ${classLoaded.localName} at ${classLoaded.fileName}`));
+ return { type: 'wildcard' };
}
- public getFieldRange(field: Identifier | TSPropertySignature, commentData: CommentData): ParameterRangeUnresolved {
- // Check comment data
- if (commentData.range) {
- return commentData.range;
+ protected getGenericTypeParameterInstantiations(
+ typeParameters: TSESTree.TSTypeParameterInstantiation,
+ classLoaded: ClassReferenceLoaded,
+ ): ParameterRangeUnresolved[] {
+ return typeParameters.params
+ .map(genericTypeParameter => this.getRangeFromTypeNode(
+ classLoaded,
+ genericTypeParameter,
+ `generic type instantiation on ${classLoaded.localName} in ${classLoaded.fileName}`,
+ ));
+ }
+
+ protected getQualifiedPath(qualifiedEntity: TSESTree.EntityName): string[] {
+ switch (qualifiedEntity.type) {
+ case AST_NODE_TYPES.TSQualifiedName:
+ return [ ...this.getQualifiedPath(qualifiedEntity.left), qualifiedEntity.right.name ];
+ case AST_NODE_TYPES.Identifier:
+ return [ qualifiedEntity.name ];
+ case AST_NODE_TYPES.ThisExpression: {
+ throw new Error('Not implemented yet: AST_NODE_TYPES.ThisExpression case');
+ }
}
+ }
+
+ public getFieldRange(
+ classLoaded: ClassReferenceLoaded,
+ field: TSESTree.Identifier | TSESTree.TSPropertySignature,
+ commentData: CommentData,
+ ): ParameterRangeUnresolved {
+ let range: ParameterRangeUnresolved | undefined;
// Check the typescript raw field type
if (field.typeAnnotation) {
- return this.getRangeFromTypeNode(field.typeAnnotation.typeAnnotation, this.getErrorIdentifierField(field));
+ range = this.getRangeFromTypeNode(
+ classLoaded,
+ field.typeAnnotation.typeAnnotation,
+ this.getErrorIdentifierField(classLoaded, field),
+ );
+ }
+
+ // Throw if no range was found
+ if (!range) {
+ this.throwOrWarn(new Error(`Missing field type on ${this.getFieldName(classLoaded, field)
+ } in ${classLoaded.localName} at ${classLoaded.fileName}`));
+ return { type: 'wildcard' };
+ }
+
+ // If the field has the '?' annotation, explicitly allow undefined as value to make it be considered optional.
+ if (field.optional) {
+ if (range.type === 'union') {
+ // Don't add undefined element if it is already present
+ if (!range.elements.some(element => element.type === 'undefined')) {
+ range.elements.push({ type: 'undefined' });
+ }
+ } else {
+ range = {
+ type: 'union',
+ elements: [
+ range,
+ { type: 'undefined' },
+ ],
+ };
+ }
+ }
+
+ // Check comment data
+ if (commentData.range) {
+ range = this.overrideRawRange(range, commentData.range);
}
- throw new Error(`Missing field type on ${this.getFieldName(field)
- } in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
+ return range;
}
- public getFieldDefault(commentData: CommentData): string | undefined {
- return commentData.default;
+ /**
+ * Apply a range override on the given range
+ * @param range The range to override in.
+ * @param override The range set set.
+ */
+ public overrideRawRange(
+ range: ParameterRangeUnresolved,
+ override: ParameterRangeUnresolved,
+ ): ParameterRangeUnresolved {
+ switch (range.type) {
+ case 'raw':
+ case 'literal':
+ case 'hash':
+ case 'interface':
+ case 'genericTypeReference':
+ case 'typeof':
+ case 'indexed':
+ // Replace these types
+ return override;
+ case 'undefined':
+ case 'wildcard':
+ case 'override':
+ // Override has no effect here
+ return range;
+ case 'union':
+ case 'intersection':
+ case 'tuple':
+ // Recursively apply override operation on elements
+ return {
+ type: range.type,
+ elements: range.elements.map(element => this.overrideRawRange(element, override)),
+ };
+ case 'rest':
+ case 'array':
+ case 'keyof':
+ // Recursively apply override operation on value
+ return {
+ type: range.type,
+ // TODO: remove the following any cast when TS bug is fixed
+ value: this.overrideRawRange(range.value, override),
+ };
+ }
}
public getFieldComment(commentData: CommentData): string | undefined {
@@ -282,23 +595,24 @@ export class ParameterLoader {
/**
* Load the parameter data from the given index signature.
+ * @param classLoaded The loaded class in which the field is defined.
* @param indexSignature An index signature.
* @param commentData Comment data about the given field.
*/
- public loadIndex(indexSignature: TSIndexSignature, commentData: CommentData):
- ParameterDataIndex {
+ public loadIndex(
+ classLoaded: ClassReferenceLoaded,
+ indexSignature: TSESTree.TSIndexSignature,
+ commentData: CommentData,
+ ): ParameterDataIndex {
// Required data
const parameterData: ParameterDataIndex = {
type: 'index',
- domain: this.getIndexDomain(indexSignature),
- range: this.getIndexRange(indexSignature, commentData),
+ domain: this.getIndexDomain(classLoaded, indexSignature),
+ range: this.getIndexRange(classLoaded, indexSignature, commentData),
};
// Optional data
- const defaultValue = this.getFieldDefault(commentData);
- if (defaultValue) {
- parameterData.default = defaultValue;
- }
+ parameterData.defaults = commentData.defaults;
const comment = this.getFieldComment(commentData);
if (comment) {
@@ -308,29 +622,39 @@ export class ParameterLoader {
return parameterData;
}
- public getIndexDomain(indexSignature: TSIndexSignature): 'string' | 'number' | 'boolean' {
+ public getIndexDomain(
+ classLoaded: ClassReferenceLoaded,
+ indexSignature: TSESTree.TSIndexSignature,
+ ): 'string' | 'number' | 'boolean' {
if (indexSignature.parameters.length !== 1) {
throw new Error(`Expected exactly one key in index signature in ${
- this.classLoaded.localName} at ${this.classLoaded.fileName}`);
+ classLoaded.localName} at ${classLoaded.fileName}`);
}
if (indexSignature.parameters[0].type !== 'Identifier') {
throw new Error(`Only identifier-based index signatures are allowed in ${
- this.classLoaded.localName} at ${this.classLoaded.fileName}`);
+ classLoaded.localName} at ${classLoaded.fileName}`);
}
if (!indexSignature.parameters[0].typeAnnotation) {
throw new Error(`Missing key type annotation in index signature in ${
- this.classLoaded.localName} at ${this.classLoaded.fileName}`);
+ classLoaded.localName} at ${classLoaded.fileName}`);
}
- const type = this.getRangeFromTypeNode(indexSignature.parameters[0].typeAnnotation.typeAnnotation,
- this.getErrorIdentifierIndex());
+ const type = this.getRangeFromTypeNode(
+ classLoaded,
+ indexSignature.parameters[0].typeAnnotation.typeAnnotation,
+ this.getErrorIdentifierIndex(),
+ );
if (type.type !== 'raw') {
throw new Error(`Only raw types are allowed in index signature keys in ${
- this.classLoaded.localName} at ${this.classLoaded.fileName}`);
+ classLoaded.localName} at ${classLoaded.fileName}`);
}
return type.value;
}
- public getIndexRange(indexSignature: TSIndexSignature, commentData: CommentData): ParameterRangeUnresolved {
+ public getIndexRange(
+ classLoaded: ClassReferenceLoaded,
+ indexSignature: TSESTree.TSIndexSignature,
+ commentData: CommentData,
+ ): ParameterRangeUnresolved {
// Check comment data
if (commentData.range) {
return commentData.range;
@@ -338,18 +662,23 @@ export class ParameterLoader {
// Check the typescript raw field type
if (indexSignature.typeAnnotation) {
- return this.getRangeFromTypeNode(indexSignature.typeAnnotation.typeAnnotation, this.getErrorIdentifierIndex());
+ return this.getRangeFromTypeNode(
+ classLoaded,
+ indexSignature.typeAnnotation.typeAnnotation,
+ this.getErrorIdentifierIndex(),
+ );
}
- throw new Error(`Missing field type on ${this.getErrorIdentifierIndex()
- } in ${this.classLoaded.localName} at ${this.classLoaded.fileName}`);
+ this.throwOrWarn(new Error(`Missing field type on ${this.getErrorIdentifierIndex()
+ } in ${classLoaded.localName} at ${classLoaded.fileName}`));
+ return { type: 'wildcard' };
}
/**
* Iterate over all type reference override handler to see if one of them overrides the given type.
* @param typeNode A type reference node.
*/
- public handleTypeOverride(typeNode: TSTypeReference): ParameterRangeUnresolved | undefined {
+ public handleTypeOverride(typeNode: TSESTree.TSTypeReference): ParameterRangeUnresolved | undefined {
for (const typeReferenceOverride of ParameterLoader.typeReferenceOverrides) {
const handled = typeReferenceOverride.handle(typeNode);
if (handled) {
@@ -357,10 +686,20 @@ export class ParameterLoader {
}
}
}
+
+ protected throwOrWarn(error: Error): void {
+ if (this.hardErrorUnsupported) {
+ throw error;
+ } else {
+ this.logger.error(error.message);
+ }
+ }
}
export interface ParameterLoaderArgs {
- classLoaded: ClassReferenceLoaded;
+ commentLoader: CommentLoader;
+ hardErrorUnsupported: boolean;
+ logger: Logger;
}
export type ParameterData = ParameterDataField | ParameterDataIndex;
@@ -374,27 +713,22 @@ export interface ParameterDataField {
* The parameter name.
*/
name: string;
- /**
- * If only one value for the given parameter can exist.
- * This is always false for an array.
- */
- unique: boolean;
- /**
- * If the parameter MUST have a value.
- */
- required: boolean;
/**
* The range of the parameter values.
*/
range: R;
/**
- * The default value.
+ * The default values.
*/
- default?: string;
+ defaults?: DefaultValue[];
/**
* The human-readable description of this parameter.
*/
comment?: string;
+ /**
+ * The nested default values on parameters.
+ */
+ defaultNested?: DefaultNested[];
}
export interface ParameterDataIndex {
@@ -410,44 +744,205 @@ export interface ParameterDataIndex {
* The range of the parameter values.
*/
range: R;
+ /**
+ * The default values.
+ */
+ defaults?: DefaultValue[];
+ /**
+ * The human-readable description of this parameter.
+ */
+ comment?: string;
+}
+
+export interface GenericTypeParameterData {
+ /**
+ * The generic type parameter name.
+ */
+ name: string;
+ /**
+ * The range of the generic type parameter.
+ */
+ range?: R;
/**
* The default value.
*/
- default?: string;
+ default?: R;
/**
* The human-readable description of this parameter.
*/
comment?: string;
}
+export interface MemberParameterData {
+ /**
+ * The member name.
+ */
+ name: string;
+ /**
+ * The range of the member parameter.
+ */
+ range?: R;
+ /**
+ * The human-readable description of this member.
+ */
+ comment?: string;
+}
+
+/**
+ * Extension information
+ */
+export interface ExtensionData {
+ classLoaded: ClassReferenceLoaded;
+ genericTypeInstantiations: R[];
+}
+
export type ParameterRangeUnresolved = {
type: 'raw';
value: 'boolean' | 'number' | 'string';
+} | {
+ type: 'literal';
+ value: boolean | number | string;
} | {
type: 'override';
value: string;
} | {
type: 'interface';
value: string;
+ /**
+ * For qualified names, this array contains the path segments.
+ */
+ qualifiedPath?: string[];
+ genericTypeParameterInstantiations: ParameterRangeUnresolved[] | undefined;
+ /**
+ * The place from which the interface was referenced.
+ */
+ origin: ClassReferenceLoaded;
} | {
type: 'hash';
- value: TSTypeLiteral;
+ value: TSESTree.TSTypeLiteral;
} | {
type: 'undefined';
+} | {
+ type: 'wildcard';
+} | {
+ type: 'union';
+ elements: ParameterRangeUnresolved[];
+} | {
+ type: 'intersection';
+ elements: ParameterRangeUnresolved[];
+} | {
+ type: 'tuple';
+ elements: ParameterRangeUnresolved[];
+} | {
+ type: 'rest';
+ value: ParameterRangeUnresolved;
+} | {
+ type: 'array';
+ value: ParameterRangeUnresolved;
+} | {
+ type: 'keyof';
+ value: ParameterRangeUnresolved;
+} | {
+ type: 'genericTypeReference';
+ value: string;
+} | {
+ type: 'typeof';
+ value: string;
+ /**
+ * For qualified names, this array contains the path segments.
+ */
+ qualifiedPath?: string[];
+ /**
+ * The place from which the interface was referenced.
+ */
+ origin: ClassReferenceLoaded;
+} | {
+ type: 'indexed';
+ object: ParameterRangeUnresolved;
+ index: ParameterRangeUnresolved;
};
export type ParameterRangeResolved = {
type: 'raw';
value: 'boolean' | 'number' | 'string';
+} | {
+ type: 'literal';
+ value: boolean | number | string;
} | {
type: 'override';
value: string;
} | {
type: 'class';
value: ClassReferenceLoaded;
+ genericTypeParameterInstances: ParameterRangeResolved[] | undefined;
} | {
type: 'nested';
value: ParameterData[];
} | {
type: 'undefined';
+} | {
+ type: 'wildcard';
+} | {
+ type: 'union';
+ elements: ParameterRangeResolved[];
+} | {
+ type: 'intersection';
+ elements: ParameterRangeResolved[];
+} | {
+ type: 'tuple';
+ elements: ParameterRangeResolved[];
+} | {
+ type: 'rest';
+ value: ParameterRangeResolved;
+} | {
+ type: 'array';
+ value: ParameterRangeResolved;
+} | {
+ type: 'keyof';
+ value: ParameterRangeResolved;
+} | {
+ type: 'genericTypeReference';
+ value: string;
+ /**
+ * The place in which the generic type was defined.
+ */
+ origin: ClassReferenceLoaded;
+} | {
+ type: 'typeof';
+ value: ParameterRangeResolved;
+} | {
+ type: 'indexed';
+ object: ParameterRangeResolved;
+ index: ParameterRangeResolved;
+};
+
+/**
+ * Represents a default value that is to be set on a nested parameter,
+ * indicated by a path of parameter keys.
+ */
+export interface DefaultNested {
+ /**
+ * The path of parameter keys in which the default value applies.
+ */
+ paramPath: string[];
+ /**
+ * A default value for the path.
+ */
+ value: DefaultValue;
+}
+
+/**
+ * A default value
+ */
+export type DefaultValue = {
+ type: 'raw';
+ value: string;
+} | {
+ type: 'iri';
+ value?: string;
+ typeIri?: string;
+ /**
+ * The component reference for relative IRIs.
+ */
+ baseComponent: ClassReference;
};
diff --git a/lib/parse/ParameterResolver.ts b/lib/parse/ParameterResolver.ts
index 027845f..7cc172e 100644
--- a/lib/parse/ParameterResolver.ts
+++ b/lib/parse/ParameterResolver.ts
@@ -1,42 +1,53 @@
-import type { TSTypeLiteral } from '@typescript-eslint/types/dist/ts-estree';
+import type { TSESTree } from '@typescript-eslint/typescript-estree';
import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
-import type { ClassIndex, ClassLoaded, ClassReference, ClassReferenceLoaded, InterfaceLoaded } from './ClassIndex';
+import { LRUCache } from 'lru-cache';
+import type {
+ ClassIndex,
+ ClassReference,
+ ClassReferenceLoaded,
+ InterfaceLoaded,
+} from './ClassIndex';
import type { ClassLoader } from './ClassLoader';
import type { ConstructorData } from './ConstructorLoader';
-import type { ParameterData,
- ParameterDataField,
+import type { GenericsData } from './GenericsLoader';
+import type { MemberData } from './MemberLoader';
+import type {
+ ExtensionData,
+ GenericTypeParameterData,
+ ParameterData,
ParameterRangeResolved,
- ParameterRangeUnresolved } from './ParameterLoader';
-import {
+ ParameterRangeUnresolved,
ParameterLoader,
+ MemberParameterData,
} from './ParameterLoader';
export class ParameterResolver {
private readonly classLoader: ClassLoader;
+ private readonly parameterLoader: ParameterLoader;
private readonly ignoreClasses: Record;
+ private readonly cacheInterfaceRange: LRUCache>;
public constructor(args: ParameterResolverArgs) {
this.classLoader = args.classLoader;
+ this.parameterLoader = args.parameterLoader;
this.ignoreClasses = args.ignoreClasses;
+ this.cacheInterfaceRange = new LRUCache({ max: 2_048 });
}
/**
* Resolve all constructor parameters of a given constructor index.
* @param unresolvedParametersIndex An index of unresolved constructor data.
- * @param classIndex A class index.
*/
public async resolveAllConstructorParameters(
unresolvedParametersIndex: ClassIndex>,
- classIndex: ClassIndex,
): Promise>> {
const resolvedParametersIndex: ClassIndex> = {};
// Resolve parameters for the different constructors in parallel
await Promise.all(Object.entries(unresolvedParametersIndex)
.map(async([ className, parameters ]) => {
- const classOrInterface = classIndex[className];
- if (classOrInterface.type === 'class') {
- resolvedParametersIndex[className] = await this.resolveConstructorParameters(parameters, classOrInterface);
+ if (parameters.classLoaded.type === 'class') {
+ resolvedParametersIndex[className] = await this.resolveConstructorParameters(parameters);
}
}));
@@ -46,88 +57,495 @@ export class ParameterResolver {
/**
* Resolve all parameters of a given constructor.
* @param unresolvedConstructorData Unresolved constructor data.
- * @param owningClass The class in which the given constructor is declared.
*/
public async resolveConstructorParameters(
unresolvedConstructorData: ConstructorData,
- owningClass: ClassLoaded,
): Promise> {
return {
- parameters: await this.resolveParameterData(unresolvedConstructorData.parameters, owningClass),
+ parameters: (await this.resolveParameterData(
+ unresolvedConstructorData.parameters,
+ unresolvedConstructorData.classLoaded,
+ {},
+ new Set(),
+ )).filter(parameter => parameter.type === 'field'),
+ classLoaded: unresolvedConstructorData.classLoaded,
};
}
+ /**
+ * Resolve all generic type parameters of a given constructor index.
+ * @param unresolvedParametersIndex An index of unresolved constructor data.
+ */
+ public async resolveAllGenericTypeParameterData(
+ unresolvedParametersIndex: ClassIndex>,
+ ): Promise>> {
+ const resolvedGenericsIndex: ClassIndex> = {};
+
+ // Resolve parameters for the different constructors in parallel
+ await Promise.all(Object.entries(unresolvedParametersIndex)
+ .map(async([ className, unresolvedGenericsData ]) => {
+ resolvedGenericsIndex[className] = {
+ genericTypeParameters: await this.resolveGenericTypeParameterData(
+ unresolvedGenericsData.genericTypeParameters,
+ unresolvedGenericsData.classLoaded,
+ {},
+ ),
+ classLoaded: unresolvedGenericsData.classLoaded,
+ };
+ }));
+
+ return resolvedGenericsIndex;
+ }
+
+ /**
+ * Resolve the given array of generic type parameter data in parallel.
+ * @param genericTypeParameters An array of unresolved generic type parameters.
+ * @param owningClass The class in which the given generic type parameters are declared.
+ * @param genericTypeRemappings A remapping of generic type names.
+ */
+ public async resolveGenericTypeParameterData(
+ genericTypeParameters: GenericTypeParameterData[],
+ owningClass: ClassReferenceLoaded,
+ genericTypeRemappings: Record,
+ ): Promise[]> {
+ return await Promise.all(genericTypeParameters
+ .map(async generic => ({
+ ...generic,
+ range: generic.range ?
+ await this.resolveRange(generic.range, owningClass, genericTypeRemappings, false, new Set()) :
+ undefined,
+ default: generic.default ?
+ await this.resolveRange(generic.default, owningClass, genericTypeRemappings, false, new Set()) :
+ undefined,
+ })));
+ }
+
+ /**
+ * Resolve all member parameters of a given constructor index.
+ * @param unresolvedParametersIndex An index of unresolved constructor data.
+ */
+ public async resolveAllMemberParameterData(
+ unresolvedParametersIndex: ClassIndex>,
+ ): Promise>> {
+ const resolvedIndex: ClassIndex> = {};
+
+ // Resolve parameters for the different constructors in parallel
+ await Promise.all(Object.entries(unresolvedParametersIndex)
+ .map(async([ className, unresolvedData ]) => {
+ resolvedIndex[className] = {
+ members: await this.resolveMemberParameterData(unresolvedData.members, unresolvedData.classLoaded, {}),
+ classLoaded: unresolvedData.classLoaded,
+ };
+ }));
+
+ return resolvedIndex;
+ }
+
+ /**
+ * Resolve the given array of member parameter data in parallel.
+ * @param members An array of unresolved members.
+ * @param owningClass The class in which the given generic type parameters are declared.
+ * @param genericTypeRemappings A remapping of generic type names.
+ */
+ public async resolveMemberParameterData(
+ members: MemberParameterData