2024-04-22 07:07:34 +00:00
|
|
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This script checks that all exported functions have JSDoc comments with
|
|
|
|
* `@param`, `@return`, and `@example` tags, according to the contributing
|
|
|
|
* guidelines.
|
|
|
|
*
|
|
|
|
* @see {@link https://github.com/denoland/deno_std/blob/main/.github/CONTRIBUTING.md#documentation}
|
|
|
|
*
|
|
|
|
* TODO(iuioiua): Add support for classes and methods.
|
|
|
|
*/
|
2024-04-29 23:37:05 +00:00
|
|
|
import { doc } from "@deno/doc";
|
2024-05-06 03:28:15 +00:00
|
|
|
import type {
|
|
|
|
DocNodeBase,
|
|
|
|
DocNodeFunction,
|
|
|
|
JsDocTag,
|
|
|
|
JsDocTagDocRequired,
|
|
|
|
} from "@deno/doc/types";
|
2024-04-22 07:07:34 +00:00
|
|
|
|
|
|
|
const ENTRY_POINTS = [
|
|
|
|
"../bytes/mod.ts",
|
|
|
|
"../datetime/mod.ts",
|
2024-05-06 07:51:20 +00:00
|
|
|
"../collections/mod.ts",
|
2024-04-22 07:07:34 +00:00
|
|
|
] as const;
|
|
|
|
|
2024-05-06 03:28:15 +00:00
|
|
|
const MD_SNIPPET = /(?<=```ts\n)(\n|.)*(?=\n```)/g;
|
|
|
|
|
2024-05-08 06:18:26 +00:00
|
|
|
class DocumentError extends Error {
|
2024-04-22 07:07:34 +00:00
|
|
|
constructor(message: string, document: DocNodeBase) {
|
|
|
|
super(message, {
|
|
|
|
cause: `${document.location.filename}:${document.location.line}`,
|
|
|
|
});
|
|
|
|
this.name = this.constructor.name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function assert(
|
|
|
|
condition: boolean,
|
|
|
|
message: string,
|
|
|
|
document: DocNodeBase,
|
|
|
|
): asserts condition {
|
|
|
|
if (!condition) {
|
2024-05-08 06:18:26 +00:00
|
|
|
throw new DocumentError(message, document);
|
2024-04-22 07:07:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-08 06:18:26 +00:00
|
|
|
/**
|
|
|
|
* We only check functions that have JSDocs. We know that exported functions
|
|
|
|
* have JSDocs thanks to `deno doc --lint`, which is used in the `lint:docs`
|
|
|
|
* task.
|
|
|
|
*/
|
2024-04-22 07:07:34 +00:00
|
|
|
function isFunctionDoc(document: DocNodeBase): document is DocNodeFunction {
|
2024-05-08 06:18:26 +00:00
|
|
|
return document.kind === "function" && document.jsDoc !== undefined;
|
2024-04-22 07:07:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function isExported(document: DocNodeBase) {
|
|
|
|
return document.declarationKind === "export";
|
|
|
|
}
|
|
|
|
|
|
|
|
function assertHasTag(tags: JsDocTag[], kind: string, document: DocNodeBase) {
|
|
|
|
const tag = tags.find((tag) => tag.kind === kind);
|
|
|
|
assert(tag !== undefined, `Symbol must have a @${kind} tag`, document);
|
|
|
|
assert(
|
|
|
|
// @ts-ignore doc is defined
|
|
|
|
tag.doc !== undefined,
|
|
|
|
`@${kind} tag must have a description`,
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function assertHasParamTag(
|
|
|
|
tags: JsDocTag[],
|
|
|
|
param: string,
|
|
|
|
document: DocNodeBase,
|
|
|
|
) {
|
|
|
|
const tag = tags.find((tag) => tag.kind === "param" && tag.name === param);
|
|
|
|
assert(
|
|
|
|
tag !== undefined,
|
|
|
|
`Symbol must have a @param tag for ${param}`,
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
assert(
|
|
|
|
// @ts-ignore doc is defined
|
|
|
|
tag.doc !== undefined,
|
|
|
|
`@param tag for ${param} must have a description`,
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-05-06 03:28:15 +00:00
|
|
|
function assertHasExampleTag(tags: JsDocTag[], document: DocNodeBase) {
|
|
|
|
tags = tags.filter((tag) => tag.kind === "example");
|
|
|
|
if (tags.length === 0) {
|
2024-05-08 06:18:26 +00:00
|
|
|
throw new DocumentError("Symbol must have an @example tag", document);
|
2024-05-06 03:28:15 +00:00
|
|
|
}
|
|
|
|
for (const tag of (tags as JsDocTagDocRequired[])) {
|
|
|
|
assert(
|
|
|
|
tag.doc !== undefined,
|
|
|
|
"@example tag must have a description",
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
const snippets = tag.doc.match(MD_SNIPPET);
|
|
|
|
if (snippets === null) {
|
2024-05-08 06:18:26 +00:00
|
|
|
throw new DocumentError(
|
2024-05-06 03:28:15 +00:00
|
|
|
"@example tag must have a code snippet",
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
for (const snippet of snippets) {
|
|
|
|
const command = new Deno.Command(Deno.execPath(), {
|
|
|
|
args: [
|
|
|
|
"eval",
|
2024-05-08 06:18:26 +00:00
|
|
|
"--ext=ts",
|
2024-05-06 03:28:15 +00:00
|
|
|
snippet,
|
|
|
|
],
|
2024-05-08 06:18:26 +00:00
|
|
|
stderr: "piped",
|
2024-05-06 03:28:15 +00:00
|
|
|
});
|
|
|
|
// TODO(iuioiua): Use `await command.output()`
|
2024-05-08 06:18:26 +00:00
|
|
|
const { success, stderr } = command.outputSync();
|
2024-05-06 03:28:15 +00:00
|
|
|
assert(
|
|
|
|
success,
|
2024-05-08 06:18:26 +00:00
|
|
|
`Example code snippet failed to execute: \n${snippet}\n${
|
|
|
|
new TextDecoder().decode(stderr)
|
|
|
|
}`,
|
2024-05-06 03:28:15 +00:00
|
|
|
document,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-02 07:47:00 +00:00
|
|
|
function assertHasTemplateTags(
|
|
|
|
tags: JsDocTag[],
|
|
|
|
template: string,
|
|
|
|
document: DocNodeBase,
|
|
|
|
) {
|
|
|
|
const tag = tags.find((tag) =>
|
|
|
|
tag.kind === "template" && tag.name === template
|
|
|
|
);
|
|
|
|
assert(
|
|
|
|
tag !== undefined,
|
|
|
|
`Symbol must have a @template tag for ${template}`,
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
assert(
|
|
|
|
// @ts-ignore doc is defined
|
|
|
|
tag.doc !== undefined,
|
|
|
|
`@template tag for ${template} must have a description`,
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-04-22 07:07:34 +00:00
|
|
|
function assertFunctionDocs(document: DocNodeFunction) {
|
|
|
|
assert(
|
|
|
|
document.jsDoc !== undefined,
|
|
|
|
"Symbol must have a JSDoc block",
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
const { tags } = document.jsDoc;
|
|
|
|
assert(tags !== undefined, "JSDoc block must have tags", document);
|
|
|
|
for (const param of document.functionDef.params) {
|
|
|
|
if (param.kind === "identifier") {
|
|
|
|
assertHasParamTag(tags, param.name, document);
|
|
|
|
}
|
|
|
|
if (param.kind === "assign") {
|
|
|
|
// @ts-ignore Trust me
|
|
|
|
assertHasParamTag(tags, param.left.name, document);
|
|
|
|
}
|
|
|
|
}
|
2024-05-02 07:47:00 +00:00
|
|
|
for (const typeParam of document.functionDef.typeParams) {
|
|
|
|
assertHasTemplateTags(tags, typeParam.name, document);
|
|
|
|
}
|
2024-04-22 07:07:34 +00:00
|
|
|
assertHasTag(tags, "return", document);
|
2024-05-06 03:28:15 +00:00
|
|
|
assertHasExampleTag(tags, document);
|
2024-04-22 07:07:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function checkDocs(specifier: string) {
|
|
|
|
const docs = await doc(specifier);
|
2024-05-06 03:28:15 +00:00
|
|
|
for (const document of docs.filter(isExported)) {
|
|
|
|
if (isFunctionDoc(document)) {
|
|
|
|
assertFunctionDocs(document);
|
|
|
|
}
|
|
|
|
}
|
2024-04-22 07:07:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const promises = [];
|
|
|
|
for (const entry of ENTRY_POINTS) {
|
|
|
|
const { href } = new URL(entry, import.meta.url);
|
|
|
|
promises.push(checkDocs(href));
|
|
|
|
}
|
|
|
|
await Promise.all(promises);
|