2024-04-22 07:07:34 +00:00
|
|
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
|
|
|
|
|
|
|
/**
|
2024-05-21 05:34:08 +00:00
|
|
|
* This script checks that all public symbols documentation aligns with the
|
|
|
|
* {@link ./CONTRIBUTING.md#documentation | documentation guidelines}.
|
2024-04-22 07:07:34 +00:00
|
|
|
*
|
2024-05-21 14:09:42 +00:00
|
|
|
* TODO(lucacasonato): Add support for variables, interfaces, namespaces, and type aliases.
|
2024-04-22 07:07:34 +00:00
|
|
|
*/
|
2024-05-14 19:36:08 +00:00
|
|
|
import {
|
2024-05-21 14:09:42 +00:00
|
|
|
type ClassConstructorDef,
|
|
|
|
type ClassMethodDef,
|
|
|
|
type ClassPropertyDef,
|
2024-05-14 19:36:08 +00:00
|
|
|
doc,
|
2024-05-21 14:09:42 +00:00
|
|
|
type DocNode,
|
2024-05-14 19:36:08 +00:00
|
|
|
type DocNodeBase,
|
2024-05-21 14:09:42 +00:00
|
|
|
type DocNodeClass,
|
2024-05-14 19:36:08 +00:00
|
|
|
type DocNodeFunction,
|
2024-07-11 09:21:37 +00:00
|
|
|
type DocNodeInterface,
|
docs(assert,cli,data-structures,expect,fmt,front-matter,html,http,jsonc,semver,streams,text,toml,webgpu): add snippet checks in module, function and class docs to doc checker (#4855)
* chore: add snippet checks to module docs
* fix
* work
* tweak
2024-05-31 02:01:46 +00:00
|
|
|
type DocNodeModuleDoc,
|
2024-05-21 05:34:08 +00:00
|
|
|
type JsDoc,
|
2024-05-14 19:36:08 +00:00
|
|
|
type JsDocTagDocRequired,
|
2024-10-30 02:39:19 +00:00
|
|
|
type JsDocTagParam,
|
2024-05-21 14:09:42 +00:00
|
|
|
type Location,
|
|
|
|
type TsTypeDef,
|
2024-05-14 19:36:08 +00:00
|
|
|
} from "@deno/doc";
|
2024-08-22 04:10:17 +00:00
|
|
|
import { pooledMap } from "@std/async/pool";
|
2024-04-22 07:07:34 +00:00
|
|
|
|
2024-05-21 05:34:08 +00:00
|
|
|
type DocNodeWithJsDoc<T = DocNodeBase> = T & {
|
|
|
|
jsDoc: JsDoc;
|
|
|
|
};
|
|
|
|
|
2024-04-22 07:07:34 +00:00
|
|
|
const ENTRY_POINTS = [
|
2024-05-30 02:38:16 +00:00
|
|
|
"../assert/mod.ts",
|
2024-09-12 03:59:12 +00:00
|
|
|
"../assert/unstable_never.ts",
|
2024-05-22 00:40:43 +00:00
|
|
|
"../async/mod.ts",
|
2024-05-22 13:42:59 +00:00
|
|
|
"../bytes/mod.ts",
|
2024-08-08 16:12:24 +00:00
|
|
|
"../cache/mod.ts",
|
2024-10-09 14:17:23 +00:00
|
|
|
"../cbor/mod.ts",
|
2024-05-23 16:53:13 +00:00
|
|
|
"../cli/mod.ts",
|
2024-09-12 03:52:06 +00:00
|
|
|
"../cli/unstable_spinner.ts",
|
2024-05-31 02:39:47 +00:00
|
|
|
"../crypto/mod.ts",
|
2024-05-06 07:51:20 +00:00
|
|
|
"../collections/mod.ts",
|
2024-06-03 03:32:09 +00:00
|
|
|
"../csv/mod.ts",
|
2024-11-14 06:51:46 +00:00
|
|
|
"../csv/unstable_stringify.ts",
|
2024-05-27 05:30:26 +00:00
|
|
|
"../data_structures/mod.ts",
|
2024-09-12 03:58:23 +00:00
|
|
|
"../data_structures/unstable_bidirectional_map.ts",
|
2024-05-22 13:42:59 +00:00
|
|
|
"../datetime/mod.ts",
|
2024-06-21 04:21:26 +00:00
|
|
|
"../dotenv/mod.ts",
|
2024-05-24 02:23:24 +00:00
|
|
|
"../encoding/mod.ts",
|
2024-09-12 05:51:34 +00:00
|
|
|
"../encoding/unstable_base64_stream.ts",
|
2024-09-12 05:45:35 +00:00
|
|
|
"../encoding/unstable_base32hex_stream.ts",
|
2024-09-12 05:40:43 +00:00
|
|
|
"../encoding/unstable_base32_stream.ts",
|
2024-09-12 06:46:23 +00:00
|
|
|
"../encoding/unstable_base64url_stream.ts",
|
2024-09-12 06:31:01 +00:00
|
|
|
"../encoding/unstable_base32hex_stream.ts",
|
2024-09-12 06:22:51 +00:00
|
|
|
"../encoding/unstable_hex_stream.ts",
|
2024-05-27 09:19:27 +00:00
|
|
|
"../expect/mod.ts",
|
2024-05-28 06:14:34 +00:00
|
|
|
"../fmt/bytes.ts",
|
|
|
|
"../fmt/colors.ts",
|
|
|
|
"../fmt/duration.ts",
|
|
|
|
"../fmt/printf.ts",
|
2024-05-28 01:14:52 +00:00
|
|
|
"../front_matter/mod.ts",
|
2024-09-12 09:55:44 +00:00
|
|
|
"../front_matter/unstable_yaml.ts",
|
2024-06-07 03:54:50 +00:00
|
|
|
"../fs/mod.ts",
|
2024-05-29 09:15:48 +00:00
|
|
|
"../html/mod.ts",
|
2024-09-12 04:02:20 +00:00
|
|
|
"../html/unstable_is_valid_custom_element_name.ts",
|
2024-05-27 09:19:27 +00:00
|
|
|
"../http/mod.ts",
|
2024-09-12 03:59:12 +00:00
|
|
|
"../http/unstable_header.ts",
|
|
|
|
"../http/unstable_method.ts",
|
|
|
|
"../http/unstable_signed_cookie.ts",
|
2024-06-11 07:05:04 +00:00
|
|
|
"../ini/mod.ts",
|
2024-05-15 07:57:20 +00:00
|
|
|
"../internal/mod.ts",
|
2024-08-08 14:20:43 +00:00
|
|
|
"../io/mod.ts",
|
2024-06-18 10:10:57 +00:00
|
|
|
"../json/mod.ts",
|
2024-05-22 13:42:59 +00:00
|
|
|
"../jsonc/mod.ts",
|
2024-09-27 03:31:58 +00:00
|
|
|
"../log/base_handler.ts",
|
2024-11-06 03:48:51 +00:00
|
|
|
"../log/file_handler.ts",
|
2024-09-18 05:04:33 +00:00
|
|
|
"../log/warn.ts",
|
2024-09-27 03:30:09 +00:00
|
|
|
"../log/critical.ts",
|
|
|
|
"../log/debug.ts",
|
|
|
|
"../log/error.ts",
|
|
|
|
"../log/info.ts",
|
2024-10-02 07:46:14 +00:00
|
|
|
"../log/console_handler.ts",
|
2024-09-29 06:38:09 +00:00
|
|
|
"../log/formatters.ts",
|
2024-10-03 12:54:16 +00:00
|
|
|
"../log/get_logger.ts",
|
2024-11-07 03:43:02 +00:00
|
|
|
"../log/logger.ts",
|
2024-05-15 06:33:05 +00:00
|
|
|
"../media_types/mod.ts",
|
2024-06-12 03:49:23 +00:00
|
|
|
"../msgpack/mod.ts",
|
2024-06-07 03:54:15 +00:00
|
|
|
"../net/mod.ts",
|
2024-09-12 04:05:40 +00:00
|
|
|
"../net/unstable_get_network_address.ts",
|
2024-06-02 02:46:36 +00:00
|
|
|
"../path/mod.ts",
|
2024-09-12 06:19:05 +00:00
|
|
|
"../path/unstable_basename.ts",
|
2024-09-12 05:33:12 +00:00
|
|
|
"../path/unstable_dirname.ts",
|
2024-09-12 06:19:05 +00:00
|
|
|
"../path/unstable_extname.ts",
|
2024-09-12 07:32:06 +00:00
|
|
|
"../path/unstable_join.ts",
|
2024-09-12 10:04:50 +00:00
|
|
|
"../path/unstable_normalize.ts",
|
2024-06-02 02:46:36 +00:00
|
|
|
"../path/posix/mod.ts",
|
|
|
|
"../path/windows/mod.ts",
|
2024-09-05 05:17:10 +00:00
|
|
|
"../random/mod.ts",
|
2024-06-11 07:06:12 +00:00
|
|
|
"../regexp/mod.ts",
|
2024-05-27 10:03:20 +00:00
|
|
|
"../semver/mod.ts",
|
2024-05-28 01:27:40 +00:00
|
|
|
"../streams/mod.ts",
|
2024-09-12 04:50:22 +00:00
|
|
|
"../streams/unstable_fixed_chunk_stream.ts",
|
2024-09-12 04:04:21 +00:00
|
|
|
"../streams/unstable_to_lines.ts",
|
2024-09-18 09:57:38 +00:00
|
|
|
"../streams/unstable_to_bytes.ts",
|
2024-09-04 08:28:07 +00:00
|
|
|
"../tar/mod.ts",
|
2024-05-23 17:01:10 +00:00
|
|
|
"../text/mod.ts",
|
2024-09-12 06:36:08 +00:00
|
|
|
"../text/unstable_slugify.ts",
|
2024-09-12 04:51:21 +00:00
|
|
|
"../text/unstable_to_constant_case.ts",
|
2024-06-17 02:31:31 +00:00
|
|
|
"../testing/bdd.ts",
|
|
|
|
"../testing/mock.ts",
|
|
|
|
"../testing/snapshot.ts",
|
|
|
|
"../testing/time.ts",
|
|
|
|
"../testing/types.ts",
|
2024-05-30 04:08:02 +00:00
|
|
|
"../toml/mod.ts",
|
2024-05-22 13:42:59 +00:00
|
|
|
"../ulid/mod.ts",
|
2024-05-29 05:47:42 +00:00
|
|
|
"../uuid/mod.ts",
|
2024-09-12 03:59:12 +00:00
|
|
|
"../uuid/unstable_v7.ts",
|
2024-05-21 17:33:10 +00:00
|
|
|
"../webgpu/mod.ts",
|
2024-06-28 10:28:55 +00:00
|
|
|
"../yaml/mod.ts",
|
2024-04-22 07:07:34 +00:00
|
|
|
] as const;
|
|
|
|
|
2024-05-21 10:12:55 +00:00
|
|
|
const TS_SNIPPET = /```ts[\s\S]*?```/g;
|
2024-06-17 02:31:31 +00:00
|
|
|
const ASSERTION_IMPORT =
|
|
|
|
/from "@std\/(assert(\/[a-z-]+)?|testing\/(mock|snapshot|types))"/g;
|
2024-05-21 10:12:55 +00:00
|
|
|
const NEWLINE = "\n";
|
2024-05-22 15:58:40 +00:00
|
|
|
const diagnostics: DocumentError[] = [];
|
2024-08-22 04:10:17 +00:00
|
|
|
const snippetPromises: (() => Promise<void>)[] = [];
|
2024-05-06 03:28:15 +00:00
|
|
|
|
2024-05-08 06:18:26 +00:00
|
|
|
class DocumentError extends Error {
|
2024-05-21 14:09:42 +00:00
|
|
|
constructor(
|
|
|
|
message: string,
|
|
|
|
document: { location: Location },
|
|
|
|
) {
|
2024-04-22 07:07:34 +00:00
|
|
|
super(message, {
|
|
|
|
cause: `${document.location.filename}:${document.location.line}`,
|
|
|
|
});
|
|
|
|
this.name = this.constructor.name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function assert(
|
|
|
|
condition: boolean,
|
|
|
|
message: string,
|
2024-05-21 14:09:42 +00:00
|
|
|
document: { location: Location },
|
2024-05-22 15:58:40 +00:00
|
|
|
) {
|
2024-04-22 07:07:34 +00:00
|
|
|
if (!condition) {
|
2024-05-22 15:58:40 +00:00
|
|
|
diagnostics.push(new DocumentError(message, document));
|
2024-04-22 07:07:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function isExported(document: DocNodeBase) {
|
|
|
|
return document.declarationKind === "export";
|
|
|
|
}
|
|
|
|
|
2024-05-21 14:09:42 +00:00
|
|
|
function isVoidOrPromiseVoid(returnType: TsTypeDef) {
|
|
|
|
return isVoid(returnType) ||
|
|
|
|
(returnType.kind === "typeRef" &&
|
|
|
|
returnType.typeRef.typeName === "Promise" &&
|
|
|
|
returnType.typeRef.typeParams?.length === 1 &&
|
|
|
|
isVoid(returnType.typeRef.typeParams[0]!));
|
2024-05-21 05:34:08 +00:00
|
|
|
}
|
|
|
|
|
2024-05-29 07:33:37 +00:00
|
|
|
function isTypeAsserts(returnType: TsTypeDef) {
|
|
|
|
return returnType.kind === "typePredicate" &&
|
|
|
|
returnType.typePredicate.asserts;
|
|
|
|
}
|
|
|
|
|
2024-05-21 14:09:42 +00:00
|
|
|
function isVoid(returnType: TsTypeDef) {
|
|
|
|
return returnType.kind === "keyword" && returnType.keyword === "void";
|
|
|
|
}
|
|
|
|
|
|
|
|
function assertHasReturnTag(document: { jsDoc: JsDoc; location: Location }) {
|
2024-05-21 05:34:08 +00:00
|
|
|
const tag = document.jsDoc.tags?.find((tag) => tag.kind === "return");
|
2024-05-22 15:58:40 +00:00
|
|
|
if (tag === undefined) {
|
|
|
|
diagnostics.push(
|
2024-05-23 17:01:10 +00:00
|
|
|
new DocumentError("Symbol must have a @return or @returns tag", document),
|
2024-05-22 15:58:40 +00:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
assert(
|
|
|
|
// @ts-ignore doc is defined
|
|
|
|
tag.doc !== undefined,
|
|
|
|
"@return tag must have a description",
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
}
|
2024-04-22 07:07:34 +00:00
|
|
|
}
|
|
|
|
|
2024-10-30 02:39:19 +00:00
|
|
|
/**
|
|
|
|
* Asserts that a @param tag has a corresponding function definition.
|
|
|
|
*/
|
|
|
|
function assertHasParamDefinition(
|
|
|
|
document: DocNodeWithJsDoc<DocNodeFunction | ClassMethodDef>,
|
|
|
|
param: JsDocTagParam,
|
|
|
|
) {
|
|
|
|
const paramDoc = document.functionDef.params.find((paramDoc) => {
|
|
|
|
if (paramDoc.kind === "identifier") {
|
|
|
|
return paramDoc.name === param.name;
|
|
|
|
} else if (paramDoc.kind === "rest" && paramDoc.arg.kind === "identifier") {
|
|
|
|
return paramDoc.arg.name === param.name;
|
|
|
|
} else if (
|
|
|
|
paramDoc.kind === "assign" && paramDoc.left.kind === "identifier"
|
|
|
|
) {
|
|
|
|
return paramDoc.left.name === param.name;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!paramDoc) {
|
|
|
|
diagnostics.push(
|
|
|
|
new DocumentError(
|
|
|
|
`@param ${param.name} must have a corresponding named function parameter definition.`,
|
|
|
|
document,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-22 07:07:34 +00:00
|
|
|
function assertHasParamTag(
|
2024-05-21 14:09:42 +00:00
|
|
|
document: { jsDoc: JsDoc; location: Location },
|
2024-04-22 07:07:34 +00:00
|
|
|
param: string,
|
|
|
|
) {
|
2024-05-21 05:34:08 +00:00
|
|
|
const tag = document.jsDoc.tags?.find((tag) =>
|
|
|
|
tag.kind === "param" && tag.name === param
|
|
|
|
);
|
2024-05-22 15:58:40 +00:00
|
|
|
if (!tag) {
|
|
|
|
diagnostics.push(
|
|
|
|
new DocumentError(`Symbol must have a @param tag for ${param}`, document),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
assert(
|
|
|
|
// @ts-ignore doc is defined
|
|
|
|
tag.doc !== undefined,
|
|
|
|
`@param tag for ${param} must have a description`,
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
}
|
2024-04-22 07:07:34 +00:00
|
|
|
}
|
|
|
|
|
docs(assert,cli,data-structures,expect,fmt,front-matter,html,http,jsonc,semver,streams,text,toml,webgpu): add snippet checks in module, function and class docs to doc checker (#4855)
* chore: add snippet checks to module docs
* fix
* work
* tweak
2024-05-31 02:01:46 +00:00
|
|
|
function assertSnippetsWork(
|
|
|
|
doc: string,
|
|
|
|
document: { jsDoc: JsDoc; location: Location },
|
|
|
|
required = true,
|
|
|
|
) {
|
|
|
|
const snippets = doc.match(TS_SNIPPET);
|
|
|
|
if (snippets === null) {
|
|
|
|
if (required) {
|
|
|
|
diagnostics.push(
|
|
|
|
new DocumentError(
|
|
|
|
"@example tag must have a TypeScript code snippet",
|
|
|
|
document,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (let snippet of snippets) {
|
2024-06-03 04:10:27 +00:00
|
|
|
const delim = snippet.split(NEWLINE)[0];
|
docs(assert,cli,data-structures,expect,fmt,front-matter,html,http,jsonc,semver,streams,text,toml,webgpu): add snippet checks in module, function and class docs to doc checker (#4855)
* chore: add snippet checks to module docs
* fix
* work
* tweak
2024-05-31 02:01:46 +00:00
|
|
|
// Trim the code block delimiters
|
|
|
|
snippet = snippet.split(NEWLINE).slice(1, -1).join(NEWLINE);
|
2024-09-19 23:29:31 +00:00
|
|
|
if (!(delim?.includes("no-assert") || delim?.includes("ignore"))) {
|
2024-06-03 04:10:27 +00:00
|
|
|
assert(
|
|
|
|
snippet.match(ASSERTION_IMPORT) !== null,
|
|
|
|
"Snippet must contain assertion from '@std/assert'",
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
}
|
docs(assert,cli,data-structures,expect,fmt,front-matter,html,http,jsonc,semver,streams,text,toml,webgpu): add snippet checks in module, function and class docs to doc checker (#4855)
* chore: add snippet checks to module docs
* fix
* work
* tweak
2024-05-31 02:01:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-22 15:58:40 +00:00
|
|
|
function assertHasExampleTag(
|
|
|
|
document: { jsDoc: JsDoc; location: Location },
|
|
|
|
) {
|
docs(assert,cli,data-structures,expect,fmt,front-matter,html,http,jsonc,semver,streams,text,toml,webgpu): add snippet checks in module, function and class docs to doc checker (#4855)
* chore: add snippet checks to module docs
* fix
* work
* tweak
2024-05-31 02:01:46 +00:00
|
|
|
const tags = document.jsDoc.tags?.filter((tag) =>
|
|
|
|
tag.kind === "example"
|
|
|
|
) as JsDocTagDocRequired[];
|
2024-05-21 05:34:08 +00:00
|
|
|
if (tags === undefined || tags.length === 0) {
|
2024-05-22 15:58:40 +00:00
|
|
|
diagnostics.push(
|
|
|
|
new DocumentError("Symbol must have an @example tag", document),
|
|
|
|
);
|
|
|
|
return;
|
2024-05-06 03:28:15 +00:00
|
|
|
}
|
docs(assert,cli,data-structures,expect,fmt,front-matter,html,http,jsonc,semver,streams,text,toml,webgpu): add snippet checks in module, function and class docs to doc checker (#4855)
* chore: add snippet checks to module docs
* fix
* work
* tweak
2024-05-31 02:01:46 +00:00
|
|
|
for (const tag of tags) {
|
2024-05-06 03:28:15 +00:00
|
|
|
assert(
|
|
|
|
tag.doc !== undefined,
|
2024-05-22 05:08:36 +00:00
|
|
|
"@example tag must have a title and TypeScript code snippet",
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
/**
|
|
|
|
* Otherwise, if the example title is undefined, it is given the title
|
|
|
|
* "Example #" by default.
|
|
|
|
*/
|
|
|
|
assert(
|
|
|
|
!tag.doc.startsWith("```ts"),
|
|
|
|
"@example tag must have a title",
|
2024-05-06 03:28:15 +00:00
|
|
|
document,
|
|
|
|
);
|
docs(assert,cli,data-structures,expect,fmt,front-matter,html,http,jsonc,semver,streams,text,toml,webgpu): add snippet checks in module, function and class docs to doc checker (#4855)
* chore: add snippet checks to module docs
* fix
* work
* tweak
2024-05-31 02:01:46 +00:00
|
|
|
assertSnippetsWork(tag.doc, document);
|
2024-05-06 03:28:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-20 07:34:47 +00:00
|
|
|
function assertHasTypeParamTags(
|
2024-05-21 14:09:42 +00:00
|
|
|
document: { jsDoc: JsDoc; location: Location },
|
2024-05-20 07:34:47 +00:00
|
|
|
typeParamName: string,
|
2024-05-02 07:47:00 +00:00
|
|
|
) {
|
2024-05-21 05:34:08 +00:00
|
|
|
const tag = document.jsDoc.tags?.find((tag) =>
|
2024-05-20 07:34:47 +00:00
|
|
|
tag.kind === "template" && tag.name === typeParamName
|
2024-05-02 07:47:00 +00:00
|
|
|
);
|
2024-05-22 15:58:40 +00:00
|
|
|
if (tag === undefined) {
|
|
|
|
diagnostics.push(
|
|
|
|
new DocumentError(
|
|
|
|
`Symbol must have a @typeParam tag for ${typeParamName}`,
|
|
|
|
document,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
assert(
|
|
|
|
// @ts-ignore doc is defined
|
|
|
|
tag.doc !== undefined,
|
|
|
|
`@typeParam tag for ${typeParamName} must have a description`,
|
|
|
|
document,
|
|
|
|
);
|
|
|
|
}
|
2024-05-02 07:47:00 +00:00
|
|
|
}
|
|
|
|
|
2024-05-21 05:34:08 +00:00
|
|
|
/**
|
|
|
|
* Asserts that a function document has:
|
|
|
|
* - A `@typeParam` tag for each type parameter.
|
|
|
|
* - A {@linkcode https://jsdoc.app/tags-param | @param} tag for each parameter.
|
2024-10-30 02:39:19 +00:00
|
|
|
* - A parameter definition inside the function for each @param tag.
|
2024-05-21 05:34:08 +00:00
|
|
|
* - A {@linkcode https://jsdoc.app/tags-returns | @returns} tag.
|
|
|
|
* - At least one {@linkcode https://jsdoc.app/tags-example | @example} tag with
|
|
|
|
* a code snippet that executes successfully.
|
|
|
|
*/
|
2024-05-21 14:09:42 +00:00
|
|
|
function assertFunctionDocs(
|
|
|
|
document: DocNodeWithJsDoc<DocNodeFunction | ClassMethodDef>,
|
|
|
|
) {
|
docs(assert,cli,data-structures,expect,fmt,front-matter,html,http,jsonc,semver,streams,text,toml,webgpu): add snippet checks in module, function and class docs to doc checker (#4855)
* chore: add snippet checks to module docs
* fix
* work
* tweak
2024-05-31 02:01:46 +00:00
|
|
|
assertSnippetsWork(document.jsDoc.doc!, document, false);
|
2024-04-22 07:07:34 +00:00
|
|
|
for (const param of document.functionDef.params) {
|
|
|
|
if (param.kind === "identifier") {
|
2024-05-21 05:34:08 +00:00
|
|
|
assertHasParamTag(document, param.name);
|
2024-04-22 07:07:34 +00:00
|
|
|
}
|
2024-10-29 05:24:18 +00:00
|
|
|
if (param.kind === "rest" && param.arg.kind === "identifier") {
|
|
|
|
assertHasParamTag(document, param.arg.name);
|
|
|
|
}
|
2024-05-21 14:09:42 +00:00
|
|
|
if (param.kind === "assign" && param.left.kind === "identifier") {
|
2024-05-21 05:34:08 +00:00
|
|
|
assertHasParamTag(document, param.left.name);
|
2024-04-22 07:07:34 +00:00
|
|
|
}
|
|
|
|
}
|
2024-10-30 02:39:19 +00:00
|
|
|
|
|
|
|
const documentedParams = document.jsDoc.tags?.filter((
|
|
|
|
tag,
|
|
|
|
): tag is JsDocTagParam =>
|
|
|
|
// Filter nested definitions like options.root as it is still documenting options parameter
|
|
|
|
tag.kind === "param" && !tag.name.includes(".")
|
|
|
|
) ?? [];
|
|
|
|
for (const param of documentedParams) {
|
|
|
|
assertHasParamDefinition(document, param);
|
|
|
|
}
|
|
|
|
|
2024-05-02 07:47:00 +00:00
|
|
|
for (const typeParam of document.functionDef.typeParams) {
|
2024-05-21 05:34:08 +00:00
|
|
|
assertHasTypeParamTags(document, typeParam.name);
|
2024-05-02 07:47:00 +00:00
|
|
|
}
|
2024-05-21 14:09:42 +00:00
|
|
|
if (
|
|
|
|
document.functionDef.returnType !== undefined &&
|
2024-05-29 07:33:37 +00:00
|
|
|
!isVoidOrPromiseVoid(document.functionDef.returnType) &&
|
|
|
|
!isTypeAsserts(document.functionDef.returnType)
|
2024-05-21 14:09:42 +00:00
|
|
|
) {
|
|
|
|
assertHasReturnTag(document);
|
|
|
|
}
|
2024-05-21 05:34:08 +00:00
|
|
|
assertHasExampleTag(document);
|
2024-04-22 07:07:34 +00:00
|
|
|
}
|
|
|
|
|
2024-05-21 14:09:42 +00:00
|
|
|
/**
|
|
|
|
* Asserts that a class document has:
|
|
|
|
* - A `@typeParam` tag for each type parameter.
|
|
|
|
* - At least one {@linkcode https://jsdoc.app/tags-example | @example} tag with
|
|
|
|
* a code snippet that executes successfully.
|
|
|
|
* - Documentation on all properties, methods, and constructors.
|
|
|
|
*/
|
|
|
|
function assertClassDocs(document: DocNodeWithJsDoc<DocNodeClass>) {
|
docs(assert,cli,data-structures,expect,fmt,front-matter,html,http,jsonc,semver,streams,text,toml,webgpu): add snippet checks in module, function and class docs to doc checker (#4855)
* chore: add snippet checks to module docs
* fix
* work
* tweak
2024-05-31 02:01:46 +00:00
|
|
|
assertSnippetsWork(document.jsDoc.doc!, document, false);
|
2024-05-21 14:09:42 +00:00
|
|
|
for (const typeParam of document.classDef.typeParams) {
|
|
|
|
assertHasTypeParamTags(document, typeParam.name);
|
|
|
|
}
|
|
|
|
assertHasExampleTag(document);
|
|
|
|
|
|
|
|
for (const property of document.classDef.properties) {
|
|
|
|
if (property.jsDoc === undefined) continue; // this is caught by `deno doc --lint`
|
|
|
|
if (property.accessibility !== undefined) {
|
2024-05-22 15:58:40 +00:00
|
|
|
diagnostics.push(
|
|
|
|
new DocumentError(
|
|
|
|
"Do not use `public`, `protected`, or `private` fields in classes",
|
|
|
|
property,
|
|
|
|
),
|
2024-05-21 14:09:42 +00:00
|
|
|
);
|
2024-05-22 15:58:40 +00:00
|
|
|
continue;
|
2024-05-21 14:09:42 +00:00
|
|
|
}
|
2024-05-22 15:58:40 +00:00
|
|
|
assertClassPropertyDocs(
|
|
|
|
property as DocNodeWithJsDoc<ClassPropertyDef>,
|
|
|
|
);
|
2024-05-21 14:09:42 +00:00
|
|
|
}
|
|
|
|
for (const method of document.classDef.methods) {
|
|
|
|
if (method.jsDoc === undefined) continue; // this is caught by `deno doc --lint`
|
|
|
|
if (method.accessibility !== undefined) {
|
2024-05-22 15:58:40 +00:00
|
|
|
diagnostics.push(
|
|
|
|
new DocumentError(
|
|
|
|
"Do not use `public`, `protected`, or `private` methods in classes",
|
|
|
|
method,
|
|
|
|
),
|
2024-05-21 14:09:42 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
assertFunctionDocs(method as DocNodeWithJsDoc<ClassMethodDef>);
|
|
|
|
}
|
|
|
|
for (const constructor of document.classDef.constructors) {
|
|
|
|
if (constructor.jsDoc === undefined) continue; // this is caught by `deno doc --lint`
|
|
|
|
if (constructor.accessibility !== undefined) {
|
2024-05-22 15:58:40 +00:00
|
|
|
diagnostics.push(
|
|
|
|
new DocumentError(
|
|
|
|
"Do not use `public`, `protected`, or `private` constructors in classes",
|
|
|
|
constructor,
|
|
|
|
),
|
2024-05-21 14:09:42 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
assertConstructorDocs(
|
|
|
|
constructor as DocNodeWithJsDoc<ClassConstructorDef>,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Asserts that a class property document has:
|
|
|
|
* - At least one {@linkcode https://jsdoc.app/tags-example | @example} tag with
|
|
|
|
* a code snippet that executes successfully.
|
|
|
|
*/
|
2024-05-22 15:58:40 +00:00
|
|
|
function assertClassPropertyDocs(
|
|
|
|
property: DocNodeWithJsDoc<ClassPropertyDef>,
|
|
|
|
) {
|
2024-05-21 14:09:42 +00:00
|
|
|
assertHasExampleTag(property);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks a constructor document for:
|
|
|
|
* - No TypeScript parameters marked with `public`, `protected`, or `private`.
|
|
|
|
* - A {@linkcode https://jsdoc.app/tags-param | @param} tag for each parameter.
|
|
|
|
* - At least one {@linkcode https://jsdoc.app/tags-example | @example} tag with
|
|
|
|
* a code snippet that executes successfully.
|
|
|
|
*/
|
|
|
|
function assertConstructorDocs(
|
|
|
|
constructor: DocNodeWithJsDoc<ClassConstructorDef>,
|
|
|
|
) {
|
|
|
|
for (const param of constructor.params) {
|
2024-05-22 15:58:40 +00:00
|
|
|
assert(
|
|
|
|
param.accessibility === undefined,
|
|
|
|
"Do not use `public`, `protected`, or `private` parameters in constructors",
|
|
|
|
constructor,
|
|
|
|
);
|
2024-05-21 14:09:42 +00:00
|
|
|
if (param.kind === "identifier") {
|
|
|
|
assertHasParamTag(constructor, param.name);
|
|
|
|
}
|
|
|
|
if (param.kind === "assign" && param.left.kind === "identifier") {
|
|
|
|
assertHasParamTag(constructor, param.left.name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
docs(assert,cli,data-structures,expect,fmt,front-matter,html,http,jsonc,semver,streams,text,toml,webgpu): add snippet checks in module, function and class docs to doc checker (#4855)
* chore: add snippet checks to module docs
* fix
* work
* tweak
2024-05-31 02:01:46 +00:00
|
|
|
/**
|
|
|
|
* Checks a module document for:
|
|
|
|
* - Code snippets that execute successfully.
|
|
|
|
*/
|
|
|
|
function assertModuleDoc(document: DocNodeWithJsDoc<DocNodeModuleDoc>) {
|
|
|
|
assertSnippetsWork(document.jsDoc.doc!, document);
|
|
|
|
}
|
|
|
|
|
2024-07-11 09:21:37 +00:00
|
|
|
/**
|
|
|
|
* Ensures an interface document:
|
|
|
|
* - Has `@default` tags for all optional properties.
|
|
|
|
*/
|
|
|
|
// deno-lint-ignore no-unused-vars
|
|
|
|
function assertHasDefaultTags(document: DocNodeWithJsDoc<DocNodeInterface>) {
|
|
|
|
for (const prop of document.interfaceDef.properties) {
|
|
|
|
if (!prop.optional) continue;
|
|
|
|
if (!prop.jsDoc?.tags?.find((tag) => tag.kind === "default")) {
|
|
|
|
diagnostics.push(
|
|
|
|
new DocumentError(
|
|
|
|
"Optional interface properties should have default values",
|
|
|
|
document,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// deno-lint-ignore no-unused-vars
|
|
|
|
function assertInterfaceDocs(document: DocNodeWithJsDoc<DocNodeInterface>) {
|
|
|
|
// TODO(iuioiua): This is currently disabled deliberately, as it throws errors
|
|
|
|
// for interface properties that don't have a `@default` tag. Re-enable this
|
|
|
|
// when checking for `@default` tags again, or when a solution is found for
|
|
|
|
// ignoring some properties (those that don't require a `@default` tag).
|
|
|
|
// assertHasDefaultTags(document);
|
|
|
|
}
|
|
|
|
|
2024-05-29 02:55:37 +00:00
|
|
|
function resolve(specifier: string, referrer: string): string {
|
2024-06-30 08:27:29 +00:00
|
|
|
if (specifier.startsWith("@std/")) {
|
|
|
|
specifier = specifier.replace("@std/", "../").replaceAll("-", "_");
|
|
|
|
const parts = specifier.split("/");
|
|
|
|
if (parts.length === 2) {
|
|
|
|
specifier += "/mod.ts";
|
|
|
|
} else if (parts.length > 2) {
|
|
|
|
specifier += ".ts";
|
|
|
|
}
|
2024-05-29 02:55:37 +00:00
|
|
|
}
|
|
|
|
return new URL(specifier, referrer).href;
|
|
|
|
}
|
|
|
|
|
2024-04-22 07:07:34 +00:00
|
|
|
async function checkDocs(specifier: string) {
|
2024-05-29 02:55:37 +00:00
|
|
|
const docs = await doc(specifier, { resolve });
|
2024-05-21 14:09:42 +00:00
|
|
|
for (const d of docs.filter(isExported)) {
|
|
|
|
if (d.jsDoc === undefined) continue; // this is caught by other checks
|
|
|
|
const document = d as DocNodeWithJsDoc<DocNode>;
|
|
|
|
switch (document.kind) {
|
docs(assert,cli,data-structures,expect,fmt,front-matter,html,http,jsonc,semver,streams,text,toml,webgpu): add snippet checks in module, function and class docs to doc checker (#4855)
* chore: add snippet checks to module docs
* fix
* work
* tweak
2024-05-31 02:01:46 +00:00
|
|
|
case "moduleDoc": {
|
|
|
|
assertModuleDoc(document);
|
|
|
|
break;
|
|
|
|
}
|
2024-05-21 14:09:42 +00:00
|
|
|
case "function": {
|
|
|
|
assertFunctionDocs(document);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "class": {
|
|
|
|
assertClassDocs(document);
|
|
|
|
break;
|
|
|
|
}
|
2024-07-11 09:21:37 +00:00
|
|
|
case "interface":
|
|
|
|
assertInterfaceDocs(document);
|
2024-05-06 03:28:15 +00:00
|
|
|
}
|
|
|
|
}
|
2024-04-22 07:07:34 +00:00
|
|
|
}
|
|
|
|
|
2024-05-22 13:42:59 +00:00
|
|
|
const ENTRY_POINT_URLS = ENTRY_POINTS.map((entry) =>
|
|
|
|
new URL(entry, import.meta.url).href
|
|
|
|
);
|
|
|
|
|
|
|
|
const lintStatus = await new Deno.Command(Deno.execPath(), {
|
|
|
|
args: ["doc", "--lint", ...ENTRY_POINT_URLS],
|
|
|
|
stdin: "inherit",
|
|
|
|
stdout: "inherit",
|
|
|
|
stderr: "inherit",
|
|
|
|
}).output();
|
|
|
|
if (!lintStatus.success) {
|
2024-09-13 05:43:13 +00:00
|
|
|
// deno-lint-ignore no-console
|
2024-05-22 13:42:59 +00:00
|
|
|
console.error(
|
|
|
|
`%c[error] %c'deno doc --lint' failed`,
|
|
|
|
"color: red",
|
|
|
|
"",
|
|
|
|
);
|
|
|
|
Deno.exit(1);
|
|
|
|
}
|
|
|
|
|
2024-08-22 04:10:17 +00:00
|
|
|
await Promise.all(ENTRY_POINT_URLS.map(checkDocs));
|
2024-05-21 14:09:42 +00:00
|
|
|
|
2024-08-22 04:10:17 +00:00
|
|
|
const iter = pooledMap(
|
|
|
|
navigator.hardwareConcurrency,
|
|
|
|
snippetPromises,
|
|
|
|
(fn) => fn(),
|
|
|
|
);
|
|
|
|
for await (const _ of iter) {
|
|
|
|
// noop
|
|
|
|
}
|
2024-05-22 15:58:40 +00:00
|
|
|
if (diagnostics.length > 0) {
|
|
|
|
for (const error of diagnostics) {
|
2024-09-13 05:43:13 +00:00
|
|
|
// deno-lint-ignore no-console
|
2024-05-21 14:09:42 +00:00
|
|
|
console.error(
|
|
|
|
`%c[error] %c${error.message} %cat ${error.cause}`,
|
|
|
|
"color: red",
|
|
|
|
"",
|
|
|
|
"color: gray",
|
|
|
|
);
|
|
|
|
}
|
2024-05-29 06:21:43 +00:00
|
|
|
|
2024-09-13 05:43:13 +00:00
|
|
|
// deno-lint-ignore no-console
|
2024-05-29 06:21:43 +00:00
|
|
|
console.log(`%c${diagnostics.length} errors found`, "color: red");
|
2024-05-22 15:58:40 +00:00
|
|
|
Deno.exit(1);
|
2024-05-21 14:09:42 +00:00
|
|
|
}
|