Skip to content

Commit 8a2b659

Browse files
bartlomiejuclaude
andauthored
fix: vendor check_docs to avoid raw GitHub URL rate limits (#3704)
## Summary - The CI `check:docs` task imports from `raw.githubusercontent.com/denoland/std/.../_tools/check_docs.ts` which intermittently fails with **429 Too Many Requests**, breaking CI on unrelated PRs - Vendor the script locally as `tools/check_docs_lib.ts` (from `denoland/std`'s `_tools/check_docs.ts` + inlined `resolve` from `_tools/utils.ts`) to eliminate the external dependency - Both `@deno/doc` and `@std/collections` were already in the import map, so no new dependencies Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 93356c8 commit 8a2b659

2 files changed

Lines changed: 383 additions & 1 deletion

File tree

tools/check_docs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { checkDocs } from "https://github.com/denoland/std/raw/refs/heads/main/_tools/check_docs.ts";
1+
import { checkDocs } from "./check_docs_lib.ts";
22

33
await checkDocs([
44
import.meta.resolve("../packages/fresh/src/error.ts"),

tools/check_docs_lib.ts

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
// Vendored from https://github.com/denoland/std/blob/main/_tools/check_docs.ts
2+
// Copyright 2018-2026 the Deno authors. MIT license.
3+
4+
import {
5+
type ClassConstructorDef,
6+
type ClassMethodDef,
7+
type ClassPropertyDef,
8+
doc,
9+
type DocNode,
10+
type DocNodeBase,
11+
type DocNodeClass,
12+
type DocNodeFunction,
13+
type DocNodeModuleDoc,
14+
type JsDoc,
15+
type JsDocTagDocRequired,
16+
type JsDocTagParam,
17+
type Location,
18+
type TsTypeDef,
19+
} from "@deno/doc";
20+
import { distinctBy } from "@std/collections/distinct-by";
21+
22+
type DocNodeWithJsDoc<T = DocNodeBase> = T & {
23+
jsDoc: JsDoc;
24+
};
25+
26+
const TS_SNIPPET_REGEXP = /```ts[\s\S]*?```/g;
27+
const ASSERTION_IMPORT_REGEXP =
28+
/from "@std\/(assert(\/[a-z-]+)?|expect(\/[a-z-]+)?|testing\/(mock|snapshot|types))"/g;
29+
const NEWLINE = "\n";
30+
const diagnostics: DocumentError[] = [];
31+
32+
class DocumentError extends Error {
33+
constructor(
34+
message: string,
35+
document: { location: Location },
36+
) {
37+
super(message, {
38+
cause: `${document.location.filename}:${document.location.line}`,
39+
});
40+
this.name = this.constructor.name;
41+
}
42+
}
43+
44+
function assert(
45+
condition: boolean,
46+
message: string,
47+
document: { location: Location },
48+
): asserts condition {
49+
if (!condition) {
50+
diagnostics.push(new DocumentError(message, document));
51+
}
52+
}
53+
54+
function isVoidOrPromiseVoid(returnType: TsTypeDef) {
55+
return isVoid(returnType) ||
56+
(returnType.kind === "typeRef" &&
57+
returnType.typeRef.typeName === "Promise" &&
58+
returnType.typeRef.typeParams?.length === 1 &&
59+
isVoid(returnType.typeRef.typeParams[0]!));
60+
}
61+
62+
function isTypeAsserts(returnType: TsTypeDef) {
63+
return returnType.kind === "typePredicate" &&
64+
returnType.typePredicate.asserts;
65+
}
66+
67+
function isVoid(returnType: TsTypeDef) {
68+
return returnType.kind === "keyword" && returnType.keyword === "void";
69+
}
70+
71+
function assertHasReturnTag(document: { jsDoc: JsDoc; location: Location }) {
72+
const tag = document.jsDoc.tags?.find((tag) => tag.kind === "return");
73+
assert(
74+
tag !== undefined,
75+
"Symbol must have a @return or @returns tag",
76+
document,
77+
);
78+
if (tag === undefined) return;
79+
assert(
80+
tag.doc !== undefined,
81+
"@return tag must have a description",
82+
document,
83+
);
84+
}
85+
86+
function assertHasParamDefinition(
87+
document: DocNodeWithJsDoc<DocNodeFunction | ClassMethodDef>,
88+
param: JsDocTagParam,
89+
) {
90+
const paramDoc = document.functionDef.params.find((paramDoc) => {
91+
if (paramDoc.kind === "identifier") {
92+
return paramDoc.name === param.name;
93+
} else if (paramDoc.kind === "rest" && paramDoc.arg.kind === "identifier") {
94+
return paramDoc.arg.name === param.name;
95+
} else if (
96+
paramDoc.kind === "assign" && paramDoc.left.kind === "identifier"
97+
) {
98+
return paramDoc.left.name === param.name;
99+
}
100+
return false;
101+
});
102+
103+
assert(
104+
paramDoc !== undefined,
105+
`@param ${param.name} must have a corresponding function parameter definition.`,
106+
document,
107+
);
108+
}
109+
110+
function assertHasParamTag(
111+
document: { jsDoc: JsDoc; location: Location },
112+
param: string,
113+
) {
114+
const tag = document.jsDoc.tags?.find((tag) =>
115+
tag.kind === "param" && tag.name === param
116+
);
117+
assert(
118+
tag !== undefined,
119+
`Symbol must have a @param tag for ${param}`,
120+
document,
121+
);
122+
if (tag === undefined) return;
123+
assert(
124+
// @ts-ignore doc is defined
125+
tag.doc !== undefined,
126+
`@param tag for ${param} must have a description`,
127+
document,
128+
);
129+
}
130+
131+
function assertHasSnippets(
132+
doc: string,
133+
document: { jsDoc: JsDoc; location: Location },
134+
) {
135+
const snippets = doc.match(TS_SNIPPET_REGEXP);
136+
assert(
137+
snippets !== null,
138+
"@example tag must have a TypeScript code snippet",
139+
document,
140+
);
141+
if (snippets === null) return;
142+
for (let snippet of snippets) {
143+
const delim = snippet.split(NEWLINE)[0];
144+
snippet = snippet.split(NEWLINE).slice(1, -1).join(NEWLINE);
145+
if (!(delim?.includes("no-assert") || delim?.includes("ignore"))) {
146+
assert(
147+
snippet.match(ASSERTION_IMPORT_REGEXP) !== null,
148+
"Snippet must contain assertion from `@std/assert`, `@std/expect` or `@std/testing`",
149+
document,
150+
);
151+
}
152+
}
153+
}
154+
155+
function assertHasExampleTag(
156+
document: { jsDoc: JsDoc; location: Location },
157+
) {
158+
const exampleTags = document.jsDoc.tags?.filter((tag) =>
159+
tag.kind === "example"
160+
) as JsDocTagDocRequired[];
161+
assert(exampleTags?.length > 0, "Symbol must have an @example tag", document);
162+
if (exampleTags === undefined) return;
163+
for (const tag of exampleTags) {
164+
assert(
165+
tag.doc !== undefined,
166+
"@example tag must have a title and TypeScript code snippet",
167+
document,
168+
);
169+
if (tag.doc === undefined) continue;
170+
assert(
171+
!tag.doc.startsWith("```ts"),
172+
"@example tag must have a title",
173+
document,
174+
);
175+
assertHasSnippets(tag.doc, document);
176+
}
177+
}
178+
179+
function assertHasTypeParamTags(
180+
document: { jsDoc: JsDoc; location: Location },
181+
typeParamName: string,
182+
) {
183+
const tag = document.jsDoc.tags?.find((tag) =>
184+
tag.kind === "template" && tag.name === typeParamName
185+
);
186+
assert(
187+
tag !== undefined,
188+
`Symbol must have a @typeParam tag for ${typeParamName}`,
189+
document,
190+
);
191+
if (tag === undefined) return;
192+
assert(
193+
// @ts-ignore doc is defined
194+
tag.doc !== undefined,
195+
`@typeParam tag for ${typeParamName} must have a description`,
196+
document,
197+
);
198+
}
199+
200+
function assertFunctionDocs(
201+
document: DocNodeWithJsDoc<DocNodeFunction | ClassMethodDef>,
202+
) {
203+
for (const param of document.functionDef.params) {
204+
if (param.kind === "identifier") {
205+
assertHasParamTag(document, param.name);
206+
}
207+
if (param.kind === "rest" && param.arg.kind === "identifier") {
208+
assertHasParamTag(document, param.arg.name);
209+
}
210+
if (param.kind === "assign" && param.left.kind === "identifier") {
211+
assertHasParamTag(document, param.left.name);
212+
}
213+
}
214+
215+
const documentedParams = document.jsDoc.tags?.filter((
216+
tag,
217+
): tag is JsDocTagParam => tag.kind === "param" && !tag.name.includes(".")) ??
218+
[];
219+
for (const param of documentedParams) {
220+
assertHasParamDefinition(document, param);
221+
}
222+
223+
for (const typeParam of document.functionDef.typeParams) {
224+
assertHasTypeParamTags(document, typeParam.name);
225+
}
226+
if (
227+
document.functionDef.returnType !== undefined &&
228+
!isVoidOrPromiseVoid(document.functionDef.returnType) &&
229+
!isTypeAsserts(document.functionDef.returnType)
230+
) {
231+
assertHasReturnTag(document);
232+
}
233+
assertHasExampleTag(document);
234+
}
235+
236+
function assertClassDocs(document: DocNodeWithJsDoc<DocNodeClass>) {
237+
for (const typeParam of document.classDef.typeParams) {
238+
assertHasTypeParamTags(document, typeParam.name);
239+
}
240+
if (!document.jsDoc.tags?.some((tag) => tag.kind === "example")) {
241+
assertHasExampleTag(document);
242+
}
243+
244+
for (const property of document.classDef.properties) {
245+
if (property.jsDoc === undefined) continue;
246+
assert(
247+
property.accessibility === undefined,
248+
"Do not use `public`, `protected`, or `private` fields in classes",
249+
property,
250+
);
251+
assertClassPropertyDocs(
252+
property as DocNodeWithJsDoc<ClassPropertyDef>,
253+
);
254+
}
255+
for (const method of document.classDef.methods) {
256+
if (method.jsDoc === undefined) continue;
257+
assert(
258+
method.accessibility === undefined,
259+
"Do not use `public`, `protected`, or `private` methods in classes",
260+
document,
261+
);
262+
assertFunctionDocs(method as DocNodeWithJsDoc<ClassMethodDef>);
263+
}
264+
for (const constructor of document.classDef.constructors) {
265+
if (constructor.jsDoc === undefined) continue;
266+
assert(
267+
constructor.accessibility === undefined,
268+
"Do not use `public`, `protected`, or `private` constructors in classes",
269+
constructor,
270+
);
271+
assertConstructorDocs(
272+
constructor as DocNodeWithJsDoc<ClassConstructorDef>,
273+
);
274+
}
275+
}
276+
277+
function assertClassPropertyDocs(
278+
property: DocNodeWithJsDoc<ClassPropertyDef>,
279+
) {
280+
assertHasExampleTag(property);
281+
}
282+
283+
function assertConstructorDocs(
284+
constructor: DocNodeWithJsDoc<ClassConstructorDef>,
285+
) {
286+
for (const param of constructor.params) {
287+
assert(
288+
param.accessibility === undefined,
289+
"Do not use `public`, `protected`, or `private` parameters in constructors",
290+
constructor,
291+
);
292+
if (param.kind === "identifier") {
293+
assertHasParamTag(constructor, param.name);
294+
}
295+
if (param.kind === "assign" && param.left.kind === "identifier") {
296+
assertHasParamTag(constructor, param.left.name);
297+
}
298+
}
299+
}
300+
301+
function assertModuleDoc(document: DocNodeWithJsDoc<DocNodeModuleDoc>) {
302+
assertHasSnippets(document.jsDoc.doc!, document);
303+
}
304+
305+
function assertHasDeprecationDesc(document: DocNodeWithJsDoc<DocNode>) {
306+
const tags = document.jsDoc?.tags;
307+
if (!tags) return;
308+
for (const tag of tags) {
309+
if (tag.kind !== "deprecated") continue;
310+
assert(
311+
tag.doc !== undefined,
312+
"@deprecated tag must have a description",
313+
document,
314+
);
315+
}
316+
}
317+
318+
function resolve(
319+
specifier: string,
320+
referrer: string,
321+
) {
322+
return (specifier.startsWith("./") || specifier.startsWith("../"))
323+
? new URL(specifier, referrer).href
324+
: import.meta.resolve(specifier);
325+
}
326+
327+
async function assertDocs(specifiers: string[]) {
328+
const docs = await doc(specifiers, { resolve });
329+
for (const d of Object.values(docs).flat()) {
330+
if (d.jsDoc === undefined || d.declarationKind !== "export") continue;
331+
332+
const document = d as DocNodeWithJsDoc<DocNode>;
333+
assertHasDeprecationDesc(document);
334+
switch (document.kind) {
335+
case "moduleDoc": {
336+
if (document.location.filename.endsWith("/mod.ts")) {
337+
assertModuleDoc(document);
338+
}
339+
break;
340+
}
341+
case "function": {
342+
assertFunctionDocs(document);
343+
break;
344+
}
345+
case "class": {
346+
assertClassDocs(document);
347+
break;
348+
}
349+
}
350+
}
351+
}
352+
353+
export async function checkDocs(specifiers: string[]) {
354+
const { success, stderr } = await new Deno.Command(Deno.execPath(), {
355+
args: ["doc", "--lint", ...specifiers],
356+
stdin: "inherit",
357+
stdout: "inherit",
358+
stderr: "piped",
359+
}).output();
360+
if (!success) {
361+
throw new Error(new TextDecoder().decode(stderr));
362+
}
363+
364+
await assertDocs(specifiers);
365+
366+
if (diagnostics.length > 0) {
367+
const errors = distinctBy(diagnostics, (e) => e.message + e.cause);
368+
for (const error of errors) {
369+
// deno-lint-ignore no-console
370+
console.error(
371+
`%c[error] %c${error.message} %cat ${error.cause}`,
372+
"color: red",
373+
"",
374+
"color: gray",
375+
);
376+
}
377+
378+
// deno-lint-ignore no-console
379+
console.log(`%c${errors.length} errors found`, "color: red");
380+
Deno.exit(1);
381+
}
382+
}

0 commit comments

Comments
 (0)