check-openapi: Convert to TypeScript.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg
2025-09-03 22:16:25 -07:00
committed by Anders Kaseorg
parent 29dd592f38
commit 0fc5f6994f
5 changed files with 36 additions and 22 deletions

View File

@@ -12,7 +12,7 @@ trim_trailing_whitespace = true
binary_next_line = true binary_next_line = true
switch_case_indent = true switch_case_indent = true
[{*.{cjs,cts,js,json,mjs,mts,ts},check-openapi}] [*.{cjs,cts,js,json,mjs,mts,ts}]
max_line_length = 100 max_line_length = 100
[*.{py,pyi}] [*.{py,pyi}]

View File

@@ -264,7 +264,7 @@ above.
You can check your formatting using these helpful tools. You can check your formatting using these helpful tools.
- `tools/check-openapi` will verify the syntax of `zerver/openapi/zulip.yaml`. - `tools/check-openapi.ts` will verify the syntax of `zerver/openapi/zulip.yaml`.
- `tools/test-backend zerver/tests/test_openapi.py`; this test compares - `tools/test-backend zerver/tests/test_openapi.py`; this test compares
your documentation against the code and can find many common your documentation against the code and can find many common
mistakes in how arguments are declared. mistakes in how arguments are declared.

View File

@@ -14,9 +14,6 @@ import tseslint from "typescript-eslint";
const compat = new FlatCompat({baseDirectory: import.meta.dirname}); const compat = new FlatCompat({baseDirectory: import.meta.dirname});
export default tseslint.config( export default tseslint.config(
{
files: ["tools/check-openapi"],
},
{ {
// This is intended for generated files and vendored third-party files. // This is intended for generated files and vendored third-party files.
// For our source code, instead of adding files here, consider using // For our source code, instead of adding files here, consider using

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
import assert from "node:assert/strict";
import * as fs from "node:fs"; import * as fs from "node:fs";
import {parseArgs} from "node:util"; import {parseArgs} from "node:util";
@@ -9,7 +10,7 @@ import ExampleValidator from "openapi-examples-validator";
import {format as prettierFormat} from "prettier"; import {format as prettierFormat} from "prettier";
import {CST, Composer, LineCounter, Parser, Scalar, YAMLMap, YAMLSeq, visit} from "yaml"; import {CST, Composer, LineCounter, Parser, Scalar, YAMLMap, YAMLSeq, visit} from "yaml";
const usage = "Usage: check-openapi [--fix] <file>..."; const usage = "Usage: check-openapi.ts [--fix] <file>...";
const { const {
values: {fix, help}, values: {fix, help},
positionals: files, positionals: files,
@@ -20,7 +21,7 @@ if (help) {
process.exit(0); process.exit(0);
} }
async function checkFile(file) { async function checkFile(file: string): Promise<void> {
const yaml = await fs.promises.readFile(file, "utf8"); const yaml = await fs.promises.readFile(file, "utf8");
const lineCounter = new LineCounter(); const lineCounter = new LineCounter();
const tokens = [...new Parser(lineCounter.addNewLine).parse(yaml)]; const tokens = [...new Parser(lineCounter.addNewLine).parse(yaml)];
@@ -29,6 +30,7 @@ async function checkFile(file) {
return; return;
} }
const [doc] = docs; const [doc] = docs;
assert.ok(doc !== undefined);
if (doc.errors.length > 0) { if (doc.errors.length > 0) {
for (const error of doc.errors) { for (const error of doc.errors) {
console.error("%s: %s", file, error.message); console.error("%s: %s", file, error.message);
@@ -43,12 +45,16 @@ async function checkFile(file) {
} }
let ok = true; let ok = true;
const reformats = new Map(); const reformats = new Map<
const promises = []; number,
{value: string; context: Parameters<typeof CST.setScalarValue>[2]}
>();
const promises: Promise<void>[] = [];
visit(doc, { visit(doc, {
Map(_key, node) { Map(_key, node) {
if (node.has("$ref") && node.items.length !== 1) { if (node.has("$ref") && node.items.length !== 1) {
assert.ok(node.range);
const {line, col} = lineCounter.linePos(node.range[0]); const {line, col} = lineCounter.linePos(node.range[0]);
console.error("%s:%d:%d: Siblings of $ref have no effect", file, line, col); console.error("%s:%d:%d: Siblings of $ref have no effect", file, line, col);
ok = false; ok = false;
@@ -58,6 +64,7 @@ async function checkFile(file) {
node.has(combinator), node.has(combinator),
); );
if (node.has("nullable") && combinator !== undefined) { if (node.has("nullable") && combinator !== undefined) {
assert.ok(node.range);
const {line, col} = lineCounter.linePos(node.range[0]); const {line, col} = lineCounter.linePos(node.range[0]);
console.error( console.error(
`%s:%d:%d: nullable has no effect as a sibling of ${combinator}`, `%s:%d:%d: nullable has no effect as a sibling of ${combinator}`,
@@ -78,6 +85,7 @@ async function checkFile(file) {
(subschema) => !(subschema instanceof YAMLMap && subschema.has("$ref")), (subschema) => !(subschema instanceof YAMLMap && subschema.has("$ref")),
).length > 1 ).length > 1
) { ) {
assert.ok(node.value.range);
const {line, col} = lineCounter.linePos(node.value.range[0]); const {line, col} = lineCounter.linePos(node.value.range[0]);
console.error("%s:%d:%d: Too many inline allOf subschemas", file, line, col); console.error("%s:%d:%d: Too many inline allOf subschemas", file, line, col);
ok = false; ok = false;
@@ -89,25 +97,29 @@ async function checkFile(file) {
node.value instanceof Scalar && node.value instanceof Scalar &&
typeof node.value.value === "string" typeof node.value.value === "string"
) { ) {
const value = node.value;
const description = node.value.value;
promises.push( promises.push(
(async () => { (async () => {
let formatted = await prettierFormat(node.value.value, { let formatted = await prettierFormat(description, {
parser: "markdown", parser: "markdown",
}); });
if ( if (
![Scalar.BLOCK_FOLDED, Scalar.BLOCK_LITERAL].includes(node.value.type) value.type !== Scalar.BLOCK_FOLDED &&
value.type !== Scalar.BLOCK_LITERAL
) { ) {
formatted = formatted.replace(/\n$/, ""); formatted = formatted.replace(/\n$/, "");
} }
if (formatted !== node.value.value) { if (formatted !== description) {
assert.ok(value.range);
if (fix) { if (fix) {
reformats.set(node.value.range[0], { reformats.set(value.range[0], {
value: formatted, value: formatted,
context: {afterKey: true}, context: {afterKey: true},
}); });
} else { } else {
ok = false; ok = false;
const {line, col} = lineCounter.linePos(node.value.range[0]); const {line, col} = lineCounter.linePos(value.range[0]);
console.error( console.error(
"%s:%d:%d: Format description with Prettier:", "%s:%d:%d: Format description with Prettier:",
file, file,
@@ -115,7 +127,7 @@ async function checkFile(file) {
col, col,
); );
let diff = ""; let diff = "";
for (const part of Diff.diffLines(node.value.value, formatted)) { for (const part of Diff.diffLines(description, formatted)) {
const prefix = part.added const prefix = part.added
? "\u001B[32m+" ? "\u001B[32m+"
: part.removed : part.removed
@@ -144,12 +156,17 @@ async function checkFile(file) {
if (reformats.size > 0) { if (reformats.size > 0) {
console.log("%s: Fixing problems", file); console.log("%s: Fixing problems", file);
for (const token of tokens) { for (const token of tokens) {
CST.visit(token, ({value}) => { if (token.type === "document") {
if (CST.isScalar(value) && reformats.has(value.offset)) { CST.visit(token, ({value}) => {
const reformat = reformats.get(value.offset); let reformat;
CST.setScalarValue(value, reformat.value, reformat.context); if (
} CST.isScalar(value) &&
}); (reformat = reformats.get(value.offset)) !== undefined
) {
CST.setScalarValue(value, reformat.value, reformat.context);
}
});
}
} }
await fs.promises.writeFile(file, tokens.map((token) => CST.stringify(token)).join("")); await fs.promises.writeFile(file, tokens.map((token) => CST.stringify(token)).join(""));
} }

View File

@@ -110,7 +110,7 @@ def run() -> None:
) )
linter_config.external_linter( linter_config.external_linter(
"openapi", "openapi",
["node", "tools/check-openapi"], ["node", "tools/check-openapi.ts"],
["yaml"], ["yaml"],
description="Validates our OpenAPI/Swagger API documentation (zerver/openapi/zulip.yaml) ", description="Validates our OpenAPI/Swagger API documentation (zerver/openapi/zulip.yaml) ",
fix_arg="--fix", fix_arg="--fix",