Skip to content

chore: improve lint:docs script #6625

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 28, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 73 additions & 121 deletions _tools/check_docs.ts
Original file line number Diff line number Diff line change
@@ -52,7 +52,7 @@ function assert(
condition: boolean,
message: string,
document: { location: Location },
) {
): asserts condition {
if (!condition) {
diagnostics.push(new DocumentError(message, document));
}
@@ -77,18 +77,17 @@ function isVoid(returnType: TsTypeDef) {

function assertHasReturnTag(document: { jsDoc: JsDoc; location: Location }) {
const tag = document.jsDoc.tags?.find((tag) => tag.kind === "return");
if (tag === undefined) {
diagnostics.push(
new DocumentError("Symbol must have a @return or @returns tag", document),
);
} else {
assert(
// @ts-ignore doc is defined
tag.doc !== undefined,
"@return tag must have a description",
document,
);
}
assert(
tag !== undefined,
"Symbol must have a @return or @returns tag",
document,
);
if (tag === undefined) return;
assert(
tag.doc !== undefined,
"@return tag must have a description",
document,
);
}

/**
@@ -111,14 +110,11 @@ function assertHasParamDefinition(
return false;
});

if (!paramDoc) {
diagnostics.push(
new DocumentError(
`@param ${param.name} must have a corresponding named function parameter definition.`,
document,
),
);
}
assert(
paramDoc !== undefined,
`@param ${param.name} must have a corresponding function parameter definition.`,
document,
);
}

function assertHasParamTag(
@@ -128,37 +124,31 @@ function assertHasParamTag(
const tag = document.jsDoc.tags?.find((tag) =>
tag.kind === "param" && tag.name === param
);
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,
);
}
assert(
tag !== undefined,
`Symbol must have a @param tag for ${param}`,
document,
);
if (tag === undefined) return;
assert(
// @ts-ignore doc is defined
tag.doc !== undefined,
`@param tag for ${param} must have a description`,
document,
);
}

function assertHasSnippets(
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;
}
assert(
snippets !== null,
"@example tag must have a TypeScript code snippet",
document,
);
if (snippets === null) return;
for (let snippet of snippets) {
const delim = snippet.split(NEWLINE)[0];
// Trim the code block delimiters
@@ -179,17 +169,7 @@ function assertHasExampleTag(
const exampleTags = document.jsDoc.tags?.filter((tag) =>
tag.kind === "example"
) as JsDocTagDocRequired[];
const hasNoExampleTags = exampleTags === undefined ||
exampleTags.length === 0;
if (
hasNoExampleTags &&
!document.jsDoc.tags?.some((tag) => tag.kind === "private")
) {
diagnostics.push(
new DocumentError("Symbol must have an @example tag", document),
);
return;
}
assert(exampleTags?.length > 0, "Symbol must have an @example tag", document);
for (const tag of exampleTags) {
assert(
tag.doc !== undefined,
@@ -216,21 +196,17 @@ function assertHasTypeParamTags(
const tag = document.jsDoc.tags?.find((tag) =>
tag.kind === "template" && tag.name === typeParamName
);
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,
);
}
assert(
tag !== undefined,
`Symbol must have a @typeParam tag for ${typeParamName}`,
document,
);
assert(
// @ts-ignore doc is defined
tag.doc !== undefined,
`@typeParam tag for ${typeParamName} must have a description`,
document,
);
}

/**
@@ -245,7 +221,6 @@ function assertHasTypeParamTags(
function assertFunctionDocs(
document: DocNodeWithJsDoc<DocNodeFunction | ClassMethodDef>,
) {
assertHasSnippets(document.jsDoc.doc!, document, false);
for (const param of document.functionDef.params) {
if (param.kind === "identifier") {
assertHasParamTag(document, param.name);
@@ -289,7 +264,6 @@ function assertFunctionDocs(
* - Documentation on all properties, methods, and constructors.
*/
function assertClassDocs(document: DocNodeWithJsDoc<DocNodeClass>) {
assertHasSnippets(document.jsDoc.doc!, document, false);
for (const typeParam of document.classDef.typeParams) {
assertHasTypeParamTags(document, typeParam.name);
}
@@ -299,41 +273,31 @@ function assertClassDocs(document: DocNodeWithJsDoc<DocNodeClass>) {

for (const property of document.classDef.properties) {
if (property.jsDoc === undefined) continue; // this is caught by `deno doc --lint`
if (property.accessibility !== undefined) {
diagnostics.push(
new DocumentError(
"Do not use `public`, `protected`, or `private` fields in classes",
property,
),
);
continue;
}
assert(
property.accessibility === undefined,
"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) {
diagnostics.push(
new DocumentError(
"Do not use `public`, `protected`, or `private` methods in classes",
method,
),
);
}
assert(
method.accessibility === undefined,
"Do not use `public`, `protected`, or `private` methods in classes",
document,
);
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) {
diagnostics.push(
new DocumentError(
"Do not use `public`, `protected`, or `private` constructors in classes",
constructor,
),
);
}
assert(
constructor.accessibility === undefined,
"Do not use `public`, `protected`, or `private` constructors in classes",
constructor,
);
assertConstructorDocs(
constructor as DocNodeWithJsDoc<ClassConstructorDef>,
);
@@ -392,14 +356,11 @@ function assertModuleDoc(document: DocNodeWithJsDoc<DocNodeModuleDoc>) {
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,
),
);
}
assert(
prop.jsDoc?.tags?.find((tag) => tag.kind === "default") !== undefined,
"Optional interface properties should have default values",
document,
);
}
}

@@ -417,14 +378,11 @@ function assertHasDeprecationDesc(document: DocNodeWithJsDoc<DocNode>) {
if (!tags) return;
for (const tag of tags) {
if (tag.kind !== "deprecated") continue;
if (tag.doc === undefined) {
diagnostics.push(
new DocumentError(
"@deprecated tag must have a description",
document,
),
);
}
assert(
tag.doc !== undefined,
"@deprecated tag must have a description",
document,
);
}
}

@@ -434,30 +392,24 @@ async function assertDocs(specifiers: string[]) {
if (d.jsDoc === undefined || d.declarationKind !== "export") continue; // this is caught by other checks

const document = d as DocNodeWithJsDoc<DocNode>;
assertHasDeprecationDesc(document);
switch (document.kind) {
case "moduleDoc": {
if (document.location.filename.endsWith("/mod.ts")) {
assertModuleDoc(document);
assertHasDeprecationDesc(document);
}
break;
}
case "function": {
assertFunctionDocs(document);
assertHasDeprecationDesc(document);
break;
}
case "class": {
assertClassDocs(document);
assertHasDeprecationDesc(document);
break;
}
case "interface":
assertInterfaceDocs(document);
assertHasDeprecationDesc(document);
break;
case "variable":
assertHasDeprecationDesc(document);
break;
}
}

Unchanged files with check annotations Beta

try {
const snapshotFileUrl = this.#snapshotFileUrl.toString();
const { snapshot } = await import(snapshotFileUrl);

Check warning on line 401 in testing/snapshot.ts

GitHub Actions / test (canary, ubuntu-latest)

unable to analyze dynamic import
this.#currentSnapshots = typeof snapshot === "undefined"
? new Map()
: new Map(
});
`);
const { snapshot } = await import(toFileUrl(snapshotFilePath).toString());

Check warning on line 329 in testing/snapshot_test.ts

GitHub Actions / test (canary, ubuntu-latest)

unable to analyze dynamic import
await assertSnapshot(t, snapshot[`${snapshotName} 1`]);
await assertSnapshot(t, formatTestOutput(result.output));