diff --git a/.gitignore b/.gitignore index 501cab1..63cb2a5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ node_modules/ /coverage/ /dist/ +/docs/ *.log diff --git a/package.json b/package.json index ee7b899..18ac5de 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,11 @@ "rollup-plugin-dts": "5.2.0", "semantic-release": "20.1.0", "ts-node": "10.9.1", + "typedoc": "^0.23.25", "typescript": "4.9.5" }, + "peerDependencies": { + "typedoc": "^0.23.25" + }, "packageManager": "pnpm@7.27.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b42d7a..d997a07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,7 @@ specifiers: rollup-plugin-dts: 5.2.0 semantic-release: 20.1.0 ts-node: 10.9.1 + typedoc: ^0.23.25 typescript: 4.9.5 devDependencies: @@ -98,6 +99,7 @@ devDependencies: rollup-plugin-dts: 5.2.0_vi3xdhr63abcxdtwtptol35g5u semantic-release: 20.1.0 ts-node: 10.9.1_5lyanyhwyggrikgtn7dsw3wuei + typedoc: 0.23.25_typescript@4.9.5 typescript: 4.9.5 packages: @@ -1585,6 +1587,10 @@ packages: engines: {node: '>=12'} dev: true + /ansi-sequence-parser/1.1.0: + resolution: {integrity: sha512-lEm8mt52to2fT8GhciPCGeCXACSz2UwIN4X2e2LJSnZ5uAbn2/dsYdOmUXq0AtWS5cpAupysIneExOgH0Vd2TQ==} + dev: true + /ansi-styles/3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -4787,6 +4793,10 @@ packages: engines: {node: '>=12'} dev: true + /lunr/2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + dev: true + /magic-string/0.16.0: resolution: {integrity: sha512-c4BEos3y6G2qO0B9X7K0FVLOPT9uGrjYwYRLFmDqyl5YMboUviyecnXWp94fJTSMwPw2/sf+CEYt5AGpmklkkQ==} dependencies: @@ -6174,6 +6184,15 @@ packages: resolution: {integrity: sha512-lT297f1WLAdq0A4O+AknIFRP6kkiI3s8C913eJ0XqBxJbZPGWUNkRQk2u8zk4bEAjUJ5i+fSLwB6z1HzeT+DEg==} dev: true + /shiki/0.14.1: + resolution: {integrity: sha512-+Jz4nBkCBe0mEDqo1eKRcCdjRtrCjozmcbTUjbPTX7OOJfEbTZzlUWlZtGe3Gb5oV1/jnojhG//YZc3rs9zSEw==} + dependencies: + ansi-sequence-parser: 1.1.0 + jsonc-parser: 3.2.0 + vscode-oniguruma: 1.7.0 + vscode-textmate: 8.0.0 + dev: true + /side-channel/1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -6745,6 +6764,20 @@ packages: is-typedarray: 1.0.0 dev: true + /typedoc/0.23.25_typescript@4.9.5: + resolution: {integrity: sha512-O1he153qVyoCgJYSvIyY3bPP1wAJTegZfa6tL3APinSZhJOf8CSd8F/21M6ex8pUY/fuY6n0jAsT4fIuMGA6sA==} + engines: {node: '>= 14.14'} + hasBin: true + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x + dependencies: + lunr: 2.3.9 + marked: 4.2.12 + minimatch: 6.2.0 + shiki: 0.14.1 + typescript: 4.9.5 + dev: true + /typescript/4.9.5: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} @@ -6842,6 +6875,14 @@ packages: resolution: {integrity: sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==} dev: true + /vscode-oniguruma/1.7.0: + resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} + dev: true + + /vscode-textmate/8.0.0: + resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} + dev: true + /vscode-uri/3.0.7: resolution: {integrity: sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==} dev: true diff --git a/src/index.ts b/src/index.ts index d4a9007..40c3afc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,99 @@ -/* eslint-disable eslint-comments/disable-enable-pair */ -/* eslint-disable unicorn/no-empty-file */ +import type { Application, ProjectReflection, Reflection } from "typedoc"; +import { ParameterType, Validator, ReflectionKind } from "typedoc"; -// Write me +/** + * Extend typedoc's options with the plugin's option using declaration merging. + */ +declare module "typedoc" { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions, jsdoc/require-jsdoc + export interface TypeDocOptionMap { + requireTags: { + byKind: ByKindEntry[]; + }; + } +} + +export type ByKindEntry = { + kind: keyof typeof ReflectionKind; + tags: string[]; +}; + +export function load(app: Readonly) { + // @ts-expect-error -- FIXME: ??? + app.options.addDeclaration({ + name: "requireTags", + help: "The configuration object of the require-tags plugin.", + type: ParameterType.Object, + }); + + app.validator.on( + Validator.EVENT_RUN, + (project: Readonly) => { + const requireTagsOptions = app.options.getValue( + "requireTags" + ) as unknown as { + byKind: ByKindEntry[]; + }; + + let m_kinds = requireTagsOptions.byKind.reduce( + (prev, cur) => prev | ReflectionKind[cur.kind], + 0 + ); + + const reflectionKindReplacements: Array< + [oldKind: number, newKind: number] + > = [ + [ReflectionKind.FunctionOrMethod, ReflectionKind.CallSignature], + [ReflectionKind.Constructor, ReflectionKind.ConstructorSignature], + [ + ReflectionKind.Accessor, + ReflectionKind.GetSignature | ReflectionKind.SetSignature, + ], + ]; + + for (const [oldKind, newKind] of reflectionKindReplacements) { + m_kinds = (m_kinds | newKind) & ~oldKind; + } + + const requireTagsByKind = new Map( + requireTagsOptions.byKind.map( + ({ kind: kindString, tags }): [number, string[]] => { + const kind = ReflectionKind[kindString]; + const realKind = + reflectionKindReplacements.find( + ([oldKind]) => (oldKind & kind) !== 0 + )?.[1] ?? kind; + return [realKind, tags]; + } + ) + ); + + const reflections = project.getReflectionsByKind(m_kinds); + const seen = new Set(); + + for (const reflection of reflections) { + if (seen.has(reflection)) { + continue; + } + + seen.add(reflection); + + if (!reflection.hasComment()) { + app.logger.warn( + `${reflection.getFriendlyFullName()} does not have any documentation.` + ); + continue; + } + + for (const tagName of requireTagsByKind.get(reflection.kind)!) { + const tag = `@${tagName}` as const; + if (reflection.comment!.getTags(tag).length === 0) { + app.logger.warn( + `${reflection.getFriendlyFullName()} does not have any ${tag} tags.` + ); + } + } + } + } + ); +}