Skip to content

Commit

Permalink
ci: handle classes in check_docs.ts (#4800)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucacasonato committed May 21, 2024
1 parent a5bc643 commit 006b96a
Showing 1 changed file with 147 additions and 19 deletions.
166 changes: 147 additions & 19 deletions _tools/check_docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand All @@ -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}`,
});
Expand All @@ -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);
Expand All @@ -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(
Expand All @@ -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) =>
Expand All @@ -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);
Expand Down Expand Up @@ -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) =>
Expand All @@ -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;
}
}
}
}
Expand All @@ -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;
}

0 comments on commit 006b96a

Please sign in to comment.