Skip to content

Commit

Permalink
feat(extract): adds recognition of jsdoc @import type imports (#965)
Browse files Browse the repository at this point in the history
## Description

- detects [jsdoc @import
imports](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#the-jsdoc-import-tag)
added in TS5.5
- adds a new `jsdoc`, `jsdoc-import-tag` dependency types
- adds unit tests
- dog foods the `@import` jsdoc tag
- [x] feature switches it in the configuration (otherwise it would be a
breaking change) -
- [x] feature switch true implies `parser` set to `tsc`
- [x] document it (and add caveats (TS 5.5 and up - `parser` attribute
set to `tsc`)
- [x] adds e2e test
- 🏕️ disables the yarn berry integration test - for #reasons it now acts
up on the ci (node 23 issue? Something else?), where it runs perfectly
fine on local machine - something to figure out later.

Future work:
- also detect jsdoc-bracket-import (`/** @type {import('yadda').bla}
*/`) statements in other jsdoc tags - see [typescript jsdoc
reference](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html)
for a list => likely better in a _separate PR_.

## Motivation and Context

Addresses #964 

## How Has This Been Tested?

- [x] green ci
- [x] additional automated non-regression tests

## Types of changes

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Documentation only change
- [ ] Refactor (non-breaking change which fixes an issue without
changing functionality)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
  • Loading branch information
sverweij authored Nov 17, 2024
1 parent 69b59b6 commit 09e9e41
Show file tree
Hide file tree
Showing 93 changed files with 787 additions and 268 deletions.
3 changes: 2 additions & 1 deletion .dependency-cruiser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,8 @@ export default {
// },
/* Experimental: the parser to use
*/
parser: "tsc", // acorn, tsc
// parser: "tsc", // acorn, tsc
detectJSDocImports: true, // implies parser: "tsc"
experimentalStats: true,
metrics: true,
enhancedResolveOptions: {
Expand Down
68 changes: 35 additions & 33 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,36 +113,38 @@ jobs:
- run: npm run depcruise
- run: npx mocha --invert --fgrep "#do-not-run-on-windows"

check-berry-integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: |
.yarn
.yarnrc.yml
.pnp.js
yarn.lock
key: ${{env.NODE_LATEST}}@${{env.PLATFORM}}-build-${{hashFiles('package.json')}}
restore-keys: |
${{env.NODE_LATEST}}@${{env.PLATFORM}}-build-
- uses: actions/setup-node@v4
with:
node-version: ${{env.NODE_LATEST}}
- name: install & build
run: |
rm -f .npmrc
yarn set version berry
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn
- name: forbidden dependency check
run: |
yarn --version
yarn depcruise
# testing doesn't work as the tests are esm and berry, with pnp enabled,
# doesn't support esm yet.
# - name: test coverage
# run: |
# node --version
# yarn --version
# yarn test:cover
# for #reasons the run step takes forever to complete on the ci - while
# running fine locally. Something to figure out another time.
# check-berry-integration:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: actions/cache@v4
# with:
# path: |
# .yarn
# .yarnrc.yml
# .pnp.js
# yarn.lock
# key: ${{env.NODE_LATEST}}@${{env.PLATFORM}}-build-${{hashFiles('package.json')}}
# restore-keys: |
# ${{env.NODE_LATEST}}@${{env.PLATFORM}}-build-
# - uses: actions/setup-node@v4
# with:
# node-version: ${{env.NODE_LATEST}}
# - name: install & build
# run: |
# rm -f .npmrc
# yarn set version berry
# YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn
# - name: forbidden dependency check
# run: |
# yarn --version
# yarn depcruise
# # testing doesn't work as the tests are esm and berry, with pnp enabled,
# # doesn't support esm yet.
# # - name: test coverage
# # run: |
# # node --version
# # yarn --version
# # yarn test:cover
29 changes: 29 additions & 0 deletions doc/options-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- [`tsConfig`: use a TypeScript configuration file ('project')](#tsconfig-use-a-typescript-configuration-file-project)
- [`babelConfig`: use a babel configuration file](#babelconfig-use-a-babel-configuration-file)
- [`webpackConfig`: use (the resolution options of) a webpack configuration](#webpackconfig-use-the-resolution-options-of-a-webpack-configuration)
- [`detectJSDocImports`: detect dependencies in JSDoc comments](#detectjsdocimports-detect-dependencies-in-jsdoc-comments)
- [Yarn Plug'n'Play support - `externalModuleResolutionStrategy`](#yarn-plugnplay-support---externalmoduleresolutionstrategy)
- [`prefix`: prefix links in reports](#prefix-prefix-links-in-reports)
- [`reporterOptions`](#reporteroptions)
Expand Down Expand Up @@ -784,6 +785,34 @@ you can provide the parameters like so:
- :bulb: For more information check out the the [webpack resolve](https://webpack.js.org/configuration/resolve/)
documentation.

### `detectJSDocImports`: detect dependencies in JSDoc comments

> :shell: there is no command line equivalent for this
If you have dependencies in JSDoc comments that you want to take into account
you can set this option to `true`. This will make dependency-cruiser look at
TypeScript 5.5+ [`@import` tags](https://devblogs.microsoft.com/typescript/announcing-typescript-5-5/#the-jsdoc-import-tag).

In the near future it will also look to bracket style imports (e.g. `/** @type {import('./thing').SomeType} */`)
in all JSDoc tags they can occur in (e.g. `@param`, `@returns`, `@type`, `@typedef` etc).

As currently on the TypeScript compiler (`tsc`) can detect these imports, switching
on this option implies dependency-cruiser will set `options.parser` to `tsc` so
it uses the TypeScript compiler to parse not only TypeScript but also JavaScript.

```javascript
options: {
detectJSDocImports: true; // implies `parser: "tsc"`
}
```

#### Usage notes

- :bulb: Only TypeScript compilers 5.5 and up can detect `@import` tags.
- :bulb: If you want to take imports in JSDoc comments in consideration you
will need the `typescript` compiler in your (dev-)dependencies as it's currently
the only parser that supports these.

### Yarn Plug'n'Play support - `externalModuleResolutionStrategy`

> :shell: there is no command line equivalent for this
Expand Down
53 changes: 28 additions & 25 deletions doc/rules-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -984,31 +984,34 @@ the dependency was declared. One or more of these can occur at the same time. E.
dependency which resolves to a base url in a tsconfig.json you'll see `import`, `aliased` as well as
`aliased-tsconfig` and `aliased-tsconfig-base-url`.
| dependency type | meaning the module was imported ... | example |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| aliased | via an alias of some sort (e.g. tsconfig paths, subpath imports, npm workspace or webpack aliases) | "~/hello.ts" |
| aliased-subpath-import | via a [subpath import](https://nodejs.org/api/packages.html#subpath-imports) | "#thing/hello.mjs" |
| aliased-tsconfig | via a typescript compilerOptions.paths or compilerOptions.baseUrl setting in tsconfig. | "@thing/hello" |
| aliased-tsconfig-base-url | via a typescript [compilerOptions.baseUrl setting in tsconfig](https://www.typescriptlang.org/tsconfig#baseUrl) | "libs/utensils/src/hello.js" |
| aliased-tsconfig-paths | via a typescript [compilerOptions.paths setting in tsconfig](https://www.typescriptlang.org/tsconfig#paths) | "@thing/hello" |
| aliased-webpack | via a [webpack resolve alias](https://webpack.js.org/configuration/resolve/#resolvealias) | "Utilities" |
| aliased-workspace | via a [workspace](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#workspaces) | "local-workspace-package" |
| amd-define | with an AMD `define` wrapper (popularized by requirejs) | `define(["./thing"], function(thing){ /* do stuff */ })` |
| amd-require | with a require statement within an AMD module | `define(function(require, exports, module){ var one = require('./thing')})` |
| amd-exotic-require | with a require statement within an AMD module (but with the first parameter baring an insensible non-standard name) | `define(function(want, exports, module){ var one = want('./thing')})` |
| type-only | as 'type only' - only available for TypeScript sources, only for tsPreCompilationDeps !== false. | `import type { IThing } from "./things"` |
| export | implicitly via a module export | `export { thing } from "./things"` |
| import | with a 'regular' ES import | `import { thing } from "./things` |
| dynamic-import | with a dynamic import statement | `const { thing } = await import("./things")` |
| import-equals | with an 'import equals' statement | `import fs = require("fs")` |
| type-import | as part of a type declaration | `const lAThing: import('./things').IThing = {}` |
| require | with a commonjs 'require' statement | `const memoize = require("lodash/memoize")` |
| exotic-require | with a statement that isn't 'require' see [exoticallyRequired](#exoticallyrequired-exoticrequire-and-exoticrequirenot) | `const { thing } = want("./thing")` |
| triple-slash-directive | with a triple slash directive (oldskool TypeScript) | |
| triple-slash-file-reference | with a triple slash directive, specifically importing another module | `/// <reference path="./ts-thing" />` |
| triple-slash-type-reference | with a triple slash directive, specifically importing types | `/// <reference types="./ts-thing-types" />` |
| triple-slash-amd-dependency | with a triple slash directive, specifically declaring an AMD dependency | `/// <amd-dependency path="./ts-thing-types" />` |
| pre-compilation-only | but the dependency will disappear at runtime. See [preCompilationOnly](#preCompilationOnly) | `import { thing } from "./things"` // and continue to not use `thing` |
| dependency type | meaning the module was imported ... | example |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| aliased | via an alias of some sort (e.g. tsconfig paths, subpath imports, npm workspace or webpack aliases) | "~/hello.ts" |
| aliased-subpath-import | via a [subpath import](https://nodejs.org/api/packages.html#subpath-imports) | "#thing/hello.mjs" |
| aliased-tsconfig | via a typescript compilerOptions.paths or compilerOptions.baseUrl setting in tsconfig. | "@thing/hello" |
| aliased-tsconfig-base-url | via a typescript [compilerOptions.baseUrl setting in tsconfig](https://www.typescriptlang.org/tsconfig#baseUrl) | "libs/utensils/src/hello.js" |
| aliased-tsconfig-paths | via a typescript [compilerOptions.paths setting in tsconfig](https://www.typescriptlang.org/tsconfig#paths) | "@thing/hello" |
| aliased-webpack | via a [webpack resolve alias](https://webpack.js.org/configuration/resolve/#resolvealias) | "Utilities" |
| aliased-workspace | via a [workspace](https://docs.npmjs.com/cli/v10/configuring-npm/package-json#workspaces) | "local-workspace-package" |
| amd-define | with an AMD `define` wrapper (popularized by requirejs) | `define(["./thing"], function(thing){ /* do stuff */ })` |
| amd-exotic-require | with a require statement within an AMD module (but with the first parameter baring an insensible non-standard name) | `define(function(want, exports, module){ var one = want('./thing')})` |
| amd-require | with a require statement within an AMD module | `define(function(require, exports, module){ var one = require('./thing')})` |
| dynamic-import | with a dynamic import statement | `const { thing } = await import("./things")` |
| exotic-require | with a statement that isn't 'require' see [exoticallyRequired](#exoticallyrequired-exoticrequire-and-exoticrequirenot) | `const { thing } = want("./thing")` |
| export | implicitly via a module export | `export { thing } from "./things"` |
| import | with a 'regular' ES import | `import { thing } from "./things` |
| import-equals | with an 'import equals' statement | `import fs = require("fs")` |
| jsdoc | in jsdoc. See `jsdoc-bracket-import` and `jsdoc-import-tag`. Needs [detectJSDocImports](options-reference.md#detectjsdocimports-detect-dependencies-in-jsdoc-comments) set to `true` | `/** @type {import('./things').thing} */`, `/** @import { thing } from "things" */` |
| jsdoc-bracket-import | in a jsdoc comment with a 'bracket' style import. Always `type-only`, also has the `jsdoc` dependency type. FUTURE FEATURE | `/** @type {import('./things').thing} */` |
| jsdoc-import-tag | in a jsdoc comment with an @import tag. Always `type-only`, also has the `jsdoc` dependency type. | `/** @import { thing } from "things" */` |
| pre-compilation-only | but the dependency will disappear at runtime. See [preCompilationOnly](#preCompilationOnly) | `import { thing } from "./things"` // and continue to not use `thing` |
| require | with a commonjs 'require' statement | `const memoize = require("lodash/memoize")` |
| triple-slash-amd-dependency | with a triple slash directive, specifically declaring an AMD dependency | `/// <amd-dependency path="./ts-thing-types" />` |
| triple-slash-directive | with a triple slash directive (oldskool TypeScript) | |
| triple-slash-file-reference | with a triple slash directive, specifically importing another module | `/// <reference path="./ts-thing" />` |
| triple-slash-type-reference | with a triple slash directive, specifically importing types | `/// <reference types="./ts-thing-types" />` |
| type-import | as part of a type declaration | `const lAThing: import('./things').IThing = {}` |
| type-only | as 'type only' - only available for TypeScript sources, only for tsPreCompilationDeps !== false. | `import type { IThing } from "./things"` |
### `dynamic`
Expand Down
7 changes: 3 additions & 4 deletions src/cache/cache.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ import { scannableExtensions } from "#extract/transpile/meta.mjs";
import { bus } from "#utl/bus.mjs";

/**
* @typedef {import("../../types/dependency-cruiser.mjs").IRevisionData} IRevisionData
* @typedef {import("../../types/strict-options.mjs").IStrictCruiseOptions} IStrictCruiseOptions
* @typedef {import("../../types/dependency-cruiser.mjs").ICruiseResult} ICruiseResult
* @typedef {import("../../types/cache-options.mjs").cacheStrategyType} cacheStrategyType
* @import { IRevisionData, ICruiseResult } from "../../types/dependency-cruiser.mjs";
* @import { IStrictCruiseOptions } from "../../types/strict-options.mjs";
* @import { cacheStrategyType } from "../../types/cache-options.mjs";
*/

const CACHE_FILE_NAME = "cache.json";
Expand Down
Loading

0 comments on commit 09e9e41

Please sign in to comment.