mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 12:03:46 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			165 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			165 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env node
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| const fs = require("fs");
 | |
| 
 | |
| const Diff = require("diff");
 | |
| const ExampleValidator = require("openapi-examples-validator");
 | |
| const Prettier = require("prettier");
 | |
| const SwaggerParser = require("swagger-parser");
 | |
| const {Composer, CST, LineCounter, Parser, Scalar, YAMLMap, YAMLSeq, visit} = require("yaml");
 | |
| const yargs = require("yargs");
 | |
| 
 | |
| const {argv} = yargs.option("fix", {
 | |
|     type: "boolean",
 | |
|     description: "Automatically fix some problems",
 | |
| });
 | |
| 
 | |
| async function checkFile(file) {
 | |
|     const yaml = await fs.promises.readFile(file, "utf8");
 | |
|     const lineCounter = new LineCounter();
 | |
|     const tokens = [...new Parser(lineCounter.addNewLine).parse(yaml)];
 | |
|     const docs = [...new Composer().compose(tokens)];
 | |
|     if (docs.length !== 1) {
 | |
|         return;
 | |
|     }
 | |
|     const [doc] = docs;
 | |
|     if (doc.errors.length > 0) {
 | |
|         for (const error of doc.errors) {
 | |
|             console.error("%s: %s", file, error.message);
 | |
|         }
 | |
|         process.exitCode = 1;
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     const root = doc.contents;
 | |
|     if (!(root instanceof YAMLMap && root.has("openapi"))) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     let ok = true;
 | |
|     const reformats = new Map();
 | |
|     const promises = [];
 | |
| 
 | |
|     visit(doc, {
 | |
|         Map(key, node) {
 | |
|             if (node.has("$ref") && node.items.length !== 1) {
 | |
|                 const {line, col} = lineCounter.linePos(node.range[0]);
 | |
|                 console.error("%s:%d:%d: Siblings of $ref have no effect", file, line, col);
 | |
|                 ok = false;
 | |
|             }
 | |
|         },
 | |
| 
 | |
|         Pair(key, node) {
 | |
|             if (
 | |
|                 node.key instanceof Scalar &&
 | |
|                 node.key.value === "allOf" &&
 | |
|                 node.value instanceof YAMLSeq &&
 | |
|                 node.value.items.filter(
 | |
|                     (subschema) => !(subschema instanceof YAMLMap && subschema.has("$ref")),
 | |
|                 ).length > 1
 | |
|             ) {
 | |
|                 const {line, col} = lineCounter.linePos(node.value.range[0]);
 | |
|                 console.error("%s:%d:%d: Too many inline allOf subschemas", file, line, col);
 | |
|                 ok = false;
 | |
|             }
 | |
| 
 | |
|             if (
 | |
|                 node.key instanceof Scalar &&
 | |
|                 node.key.value === "description" &&
 | |
|                 node.value instanceof Scalar &&
 | |
|                 typeof node.value.value === "string"
 | |
|             ) {
 | |
|                 promises.push(
 | |
|                     (async () => {
 | |
|                         let formatted = await Prettier.format(node.value.value, {
 | |
|                             parser: "markdown",
 | |
|                         });
 | |
|                         if (
 | |
|                             ![Scalar.BLOCK_FOLDED, Scalar.BLOCK_LITERAL].includes(node.value.type)
 | |
|                         ) {
 | |
|                             formatted = formatted.replace(/\n$/, "");
 | |
|                         }
 | |
|                         if (formatted !== node.value.value) {
 | |
|                             if (argv.fix) {
 | |
|                                 reformats.set(node.value.range[0], {
 | |
|                                     value: formatted,
 | |
|                                     context: {afterKey: true},
 | |
|                                 });
 | |
|                             } else {
 | |
|                                 ok = false;
 | |
|                                 const {line, col} = lineCounter.linePos(node.value.range[0]);
 | |
|                                 console.error(
 | |
|                                     "%s:%d:%d: Format description with Prettier:",
 | |
|                                     file,
 | |
|                                     line,
 | |
|                                     col,
 | |
|                                 );
 | |
|                                 let diff = "";
 | |
|                                 for (const part of Diff.diffLines(node.value.value, formatted)) {
 | |
|                                     const prefix = part.added
 | |
|                                         ? "\u001B[32m+"
 | |
|                                         : part.removed
 | |
|                                         ? "\u001B[31m-"
 | |
|                                         : "\u001B[34m ";
 | |
|                                     diff += prefix;
 | |
|                                     diff += part.value
 | |
|                                         .replace(/\n$/, "")
 | |
|                                         .replaceAll("\n", "\n" + prefix);
 | |
|                                     diff += "\n";
 | |
|                                 }
 | |
|                                 diff += "\u001B[0m";
 | |
|                                 console.error(diff);
 | |
|                             }
 | |
|                         }
 | |
|                     })(),
 | |
|                 );
 | |
|             }
 | |
|         },
 | |
|     });
 | |
|     await Promise.all(promises);
 | |
| 
 | |
|     if (!ok) {
 | |
|         process.exitCode = 1;
 | |
|     }
 | |
|     if (reformats.size > 0) {
 | |
|         console.log("%s: Fixing problems", file);
 | |
|         for (const token of tokens) {
 | |
|             CST.visit(token, ({value}) => {
 | |
|                 if (CST.isScalar(value) && reformats.has(value.offset)) {
 | |
|                     const reformat = reformats.get(value.offset);
 | |
|                     CST.setScalarValue(value, reformat.value, reformat.context);
 | |
|                 }
 | |
|             });
 | |
|         }
 | |
|         await fs.promises.writeFile(file, tokens.map((token) => CST.stringify(token)).join(""));
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|         await SwaggerParser.validate(file);
 | |
|     } catch (error) {
 | |
|         if (!(error instanceof SyntaxError)) {
 | |
|             throw error;
 | |
|         }
 | |
|         console.error("%s: %s", file, error.message);
 | |
|         process.exitCode = 1;
 | |
|     }
 | |
|     const res = await ExampleValidator.validateFile(file);
 | |
|     if (!res.valid) {
 | |
|         for (const error of res.errors) {
 | |
|             console.error(error);
 | |
|         }
 | |
|         process.exitCode = 1;
 | |
|     }
 | |
| }
 | |
| 
 | |
| (async () => {
 | |
|     for (const file of argv._) {
 | |
|         await checkFile(file);
 | |
|     }
 | |
| })().catch((error) => {
 | |
|     console.error(error);
 | |
|     process.exit(1);
 | |
| });
 |