mirror of
https://github.com/denoland/std.git
synced 2024-11-21 20:50:22 +00:00
ci: handle classes in check_docs.ts (#4800)
This commit is contained in:
parent
a5bc643e8e
commit
006b96a586
@ -4,14 +4,21 @@
|
||||
* This script checks that all public symbols documentation aligns with the
|
||||
* {@link ./CONTRIBUTING.md#documentation | documentation guidelines}.
|
||||
*
|
||||
* TODO(iuioiua): Add support for classes and methods.
|
||||
* TODO(lucacasonato): Add support for variables, interfaces, namespaces, and type aliases.
|
||||
*/
|
||||
import {
|
||||
type ClassConstructorDef,
|
||||
type ClassMethodDef,
|
||||
type ClassPropertyDef,
|
||||
doc,
|
||||
type DocNode,
|
||||
type DocNodeBase,
|
||||
type DocNodeClass,
|
||||
type DocNodeFunction,
|
||||
type JsDoc,
|
||||
type JsDocTagDocRequired,
|
||||
type Location,
|
||||
type TsTypeDef,
|
||||
} from "@deno/doc";
|
||||
|
||||
type DocNodeWithJsDoc<T = DocNodeBase> = T & {
|
||||
@ -30,7 +37,10 @@ const TS_SNIPPET = /```ts[\s\S]*?```/g;
|
||||
const NEWLINE = "\n";
|
||||
|
||||
class DocumentError extends Error {
|
||||
constructor(message: string, document: DocNodeBase) {
|
||||
constructor(
|
||||
message: string,
|
||||
document: { location: Location },
|
||||
) {
|
||||
super(message, {
|
||||
cause: `${document.location.filename}:${document.location.line}`,
|
||||
});
|
||||
@ -41,7 +51,7 @@ class DocumentError extends Error {
|
||||
function assert(
|
||||
condition: boolean,
|
||||
message: string,
|
||||
document: DocNodeBase,
|
||||
document: { location: Location },
|
||||
): asserts condition {
|
||||
if (!condition) {
|
||||
throw new DocumentError(message, document);
|
||||
@ -52,13 +62,19 @@ function isExported(document: DocNodeBase) {
|
||||
return document.declarationKind === "export";
|
||||
}
|
||||
|
||||
function isFunctionDoc(
|
||||
document: DocNodeBase,
|
||||
): document is DocNodeWithJsDoc<DocNodeFunction> {
|
||||
return document.kind === "function" && document.jsDoc !== undefined;
|
||||
function isVoidOrPromiseVoid(returnType: TsTypeDef) {
|
||||
return isVoid(returnType) ||
|
||||
(returnType.kind === "typeRef" &&
|
||||
returnType.typeRef.typeName === "Promise" &&
|
||||
returnType.typeRef.typeParams?.length === 1 &&
|
||||
isVoid(returnType.typeRef.typeParams[0]!));
|
||||
}
|
||||
|
||||
function assertHasReturnTag(document: DocNodeWithJsDoc) {
|
||||
function isVoid(returnType: TsTypeDef) {
|
||||
return returnType.kind === "keyword" && returnType.keyword === "void";
|
||||
}
|
||||
|
||||
function assertHasReturnTag(document: { jsDoc: JsDoc; location: Location }) {
|
||||
const tag = document.jsDoc.tags?.find((tag) => tag.kind === "return");
|
||||
assert(tag !== undefined, "Symbol must have a @return tag", document);
|
||||
assert(
|
||||
@ -70,7 +86,7 @@ function assertHasReturnTag(document: DocNodeWithJsDoc) {
|
||||
}
|
||||
|
||||
function assertHasParamTag(
|
||||
document: DocNodeWithJsDoc,
|
||||
document: { jsDoc: JsDoc; location: Location },
|
||||
param: string,
|
||||
) {
|
||||
const tag = document.jsDoc.tags?.find((tag) =>
|
||||
@ -89,7 +105,7 @@ function assertHasParamTag(
|
||||
);
|
||||
}
|
||||
|
||||
function assertHasExampleTag(document: DocNodeWithJsDoc) {
|
||||
function assertHasExampleTag(document: { jsDoc: JsDoc; location: Location }) {
|
||||
const tags = document.jsDoc.tags?.filter((tag) => tag.kind === "example");
|
||||
if (tags === undefined || tags.length === 0) {
|
||||
throw new DocumentError("Symbol must have an @example tag", document);
|
||||
@ -133,7 +149,7 @@ function assertHasExampleTag(document: DocNodeWithJsDoc) {
|
||||
}
|
||||
|
||||
function assertHasTypeParamTags(
|
||||
document: DocNodeWithJsDoc,
|
||||
document: { jsDoc: JsDoc; location: Location },
|
||||
typeParamName: string,
|
||||
) {
|
||||
const tag = document.jsDoc.tags?.find((tag) =>
|
||||
@ -160,28 +176,126 @@ function assertHasTypeParamTags(
|
||||
* - At least one {@linkcode https://jsdoc.app/tags-example | @example} tag with
|
||||
* a code snippet that executes successfully.
|
||||
*/
|
||||
function assertFunctionDocs(document: DocNodeWithJsDoc<DocNodeFunction>) {
|
||||
function assertFunctionDocs(
|
||||
document: DocNodeWithJsDoc<DocNodeFunction | ClassMethodDef>,
|
||||
) {
|
||||
for (const param of document.functionDef.params) {
|
||||
if (param.kind === "identifier") {
|
||||
assertHasParamTag(document, param.name);
|
||||
}
|
||||
if (param.kind === "assign") {
|
||||
// @ts-ignore Trust me
|
||||
if (param.kind === "assign" && param.left.kind === "identifier") {
|
||||
assertHasParamTag(document, param.left.name);
|
||||
}
|
||||
}
|
||||
for (const typeParam of document.functionDef.typeParams) {
|
||||
assertHasTypeParamTags(document, typeParam.name);
|
||||
}
|
||||
assertHasReturnTag(document);
|
||||
if (
|
||||
document.functionDef.returnType !== undefined &&
|
||||
!isVoidOrPromiseVoid(document.functionDef.returnType)
|
||||
) {
|
||||
assertHasReturnTag(document);
|
||||
}
|
||||
assertHasExampleTag(document);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>) {
|
||||
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) {
|
||||
throw new DocumentError(
|
||||
"Do not use `public`, `protected`, or `private` fields in classes",
|
||||
property,
|
||||
);
|
||||
}
|
||||
assertClassPropertyDocs(property as DocNodeWithJsDoc<ClassPropertyDef>);
|
||||
}
|
||||
for (const method of document.classDef.methods) {
|
||||
if (method.jsDoc === undefined) continue; // this is caught by `deno doc --lint`
|
||||
if (method.accessibility !== undefined) {
|
||||
throw new DocumentError(
|
||||
"Do not use `public`, `protected`, or `private` methods in classes",
|
||||
method,
|
||||
);
|
||||
}
|
||||
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) {
|
||||
throw new DocumentError(
|
||||
"Do not use `public`, `protected`, or `private` constructors in classes",
|
||||
constructor,
|
||||
);
|
||||
}
|
||||
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.
|
||||
*/
|
||||
function assertClassPropertyDocs(property: DocNodeWithJsDoc<ClassPropertyDef>) {
|
||||
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) {
|
||||
if (param.accessibility !== undefined) {
|
||||
throw new DocumentError(
|
||||
"Do not use `public`, `protected`, or `private` parameters in constructors",
|
||||
constructor,
|
||||
);
|
||||
}
|
||||
if (param.kind === "identifier") {
|
||||
assertHasParamTag(constructor, param.name);
|
||||
}
|
||||
if (param.kind === "assign" && param.left.kind === "identifier") {
|
||||
assertHasParamTag(constructor, param.left.name);
|
||||
}
|
||||
}
|
||||
assertHasExampleTag(constructor);
|
||||
}
|
||||
|
||||
async function checkDocs(specifier: string) {
|
||||
const docs = await doc(specifier);
|
||||
for (const document of docs.filter(isExported)) {
|
||||
if (isFunctionDoc(document)) {
|
||||
assertFunctionDocs(document);
|
||||
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) {
|
||||
case "function": {
|
||||
assertFunctionDocs(document);
|
||||
break;
|
||||
}
|
||||
case "class": {
|
||||
assertClassDocs(document);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -191,4 +305,18 @@ for (const entry of ENTRY_POINTS) {
|
||||
const { href } = new URL(entry, import.meta.url);
|
||||
promises.push(checkDocs(href));
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
} catch (error) {
|
||||
if (error instanceof DocumentError) {
|
||||
console.error(
|
||||
`%c[error] %c${error.message} %cat ${error.cause}`,
|
||||
"color: red",
|
||||
"",
|
||||
"color: gray",
|
||||
);
|
||||
Deno.exit(1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user