From 57d2bcaceab62c2a54d871444e9be7283f9e2d82 Mon Sep 17 00:00:00 2001 From: Boshen <1430279+Boshen@users.noreply.github.com> Date: Wed, 22 May 2024 16:09:33 +0000 Subject: [PATCH] feat(tasks/website): start generating linter config markdown from json schema (#3386) --- Cargo.lock | 2 + crates/oxc_linter/src/config/env.rs | 1 + tasks/website/Cargo.toml | 2 + tasks/website/src/linter.rs | 23 ----- tasks/website/src/linter/cli.rs | 8 ++ tasks/website/src/linter/json_schema.rs | 129 ++++++++++++++++++++++++ tasks/website/src/linter/mod.rs | 9 ++ tasks/website/src/linter/rules.rs | 7 ++ tasks/website/src/main.rs | 3 +- 9 files changed, 160 insertions(+), 24 deletions(-) delete mode 100644 tasks/website/src/linter.rs create mode 100644 tasks/website/src/linter/cli.rs create mode 100644 tasks/website/src/linter/json_schema.rs create mode 100644 tasks/website/src/linter/mod.rs create mode 100644 tasks/website/src/linter/rules.rs diff --git a/Cargo.lock b/Cargo.lock index 3295003b3..b2c1a507c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2973,10 +2973,12 @@ name = "website" version = "0.0.0" dependencies = [ "bpaf", + "handlebars", "oxc_cli", "oxc_linter", "pico-args", "schemars", + "serde", "serde_json", ] diff --git a/crates/oxc_linter/src/config/env.rs b/crates/oxc_linter/src/config/env.rs index d8d2eea68..ef214dac5 100644 --- a/crates/oxc_linter/src/config/env.rs +++ b/crates/oxc_linter/src/config/env.rs @@ -2,6 +2,7 @@ use rustc_hash::FxHashMap; use schemars::JsonSchema; use serde::Deserialize; +/// Env // TODO: list the keys we support #[derive(Debug, Clone, Deserialize, JsonSchema)] pub struct ESLintEnv(FxHashMap); diff --git a/tasks/website/Cargo.toml b/tasks/website/Cargo.toml index 45ce8ea8d..151a5e925 100644 --- a/tasks/website/Cargo.toml +++ b/tasks/website/Cargo.toml @@ -20,3 +20,5 @@ bpaf = { workspace = true, features = ["docgen"] } pico-args = { workspace = true } serde_json = { workspace = true } schemars = { workspace = true } +handlebars = { workspace = true } +serde = { workspace = true } diff --git a/tasks/website/src/linter.rs b/tasks/website/src/linter.rs deleted file mode 100644 index 2ccf9eda3..000000000 --- a/tasks/website/src/linter.rs +++ /dev/null @@ -1,23 +0,0 @@ -// -pub fn generate_cli() { - use bpaf::Parser; - use oxc_cli::lint_options; - let markdown = lint_options().to_options().render_markdown("oxlint"); - println!("{markdown}"); -} - -// -pub fn generate_rules() { - use oxc_linter::Linter; - let mut v = vec![]; - Linter::print_rules(&mut v); - println!("{}", String::from_utf8(v).unwrap()); -} - -pub fn generate_json_schema() { - use schemars::schema_for; - - use oxc_linter::ESLintConfig; - let schema = schema_for!(ESLintConfig); - println!("{}", serde_json::to_string_pretty(&schema).unwrap()); -} diff --git a/tasks/website/src/linter/cli.rs b/tasks/website/src/linter/cli.rs new file mode 100644 index 000000000..b665472c6 --- /dev/null +++ b/tasks/website/src/linter/cli.rs @@ -0,0 +1,8 @@ +use bpaf::Parser; +use oxc_cli::lint_options; + +// +pub fn generate_cli() { + let markdown = lint_options().to_options().render_markdown("oxlint"); + println!("{markdown}"); +} diff --git a/tasks/website/src/linter/json_schema.rs b/tasks/website/src/linter/json_schema.rs new file mode 100644 index 000000000..485a7a68c --- /dev/null +++ b/tasks/website/src/linter/json_schema.rs @@ -0,0 +1,129 @@ +use handlebars::Handlebars; +use schemars::{ + schema::{RootSchema, Schema, SchemaObject}, + schema_for, +}; +use serde::Serialize; + +use oxc_linter::ESLintConfig; + +pub fn generate_schema_json() { + let schema = schema_for!(ESLintConfig); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} + +pub fn generate_schema_markdown() { + let rendered = Renderer::new().render(); + println!("{rendered}"); +} + +const ROOT: &str = " +# {{title}} + +{{> section}} +"; + +const SECTION: &str = " +{{#each sections}} +{{level}} `{{title}}` + +type: `{{instance_type}}` + +{{description}} + +{{> section}} + +{{/each}} +"; + +#[derive(Serialize)] +struct Root { + title: String, + sections: Vec
, +} + +#[derive(Serialize)] +struct Section { + level: String, + title: String, + instance_type: String, + description: String, + sections: Vec
, +} + +struct Renderer { + handlebars: Handlebars<'static>, + root_schema: RootSchema, +} + +impl Renderer { + fn new() -> Self { + let mut handlebars = Handlebars::new(); + handlebars.register_escape_fn(handlebars::no_escape); + assert!(handlebars.register_template_string("root", ROOT).is_ok()); + assert!(handlebars.register_template_string("section", SECTION).is_ok()); + let root_schema = schema_for!(ESLintConfig); + Self { handlebars, root_schema } + } + + fn render(self) -> String { + let root = self.render_root_schema(&self.root_schema); + self.handlebars.render("root", &root).unwrap() + } + + fn get_schema_object(schema: &Schema) -> &SchemaObject { + match schema { + Schema::Object(schema_object) => schema_object, + Schema::Bool(_) => panic!("definition must be an object."), + } + } + + fn get_referenced_schema(&self, reference: &str) -> &SchemaObject { + let definitions = &self.root_schema.definitions; + let definition = definitions.get(reference.trim_start_matches("#/definitions/")); + definition.map(Self::get_schema_object).unwrap() + } + + fn render_root_schema(&self, root_schema: &RootSchema) -> Root { + let title = root_schema + .schema + .metadata + .as_ref() + .and_then(|m| m.description.clone()) + .unwrap_or_default(); + let sections = self.render_properties(1, None, &root_schema.schema); + Root { title, sections } + } + + fn render_properties( + &self, + depth: usize, + parent_key: Option<&str>, + schema: &SchemaObject, + ) -> Vec
{ + let Some(object) = &schema.object else { return vec![] }; + object + .properties + .iter() + .map(|(key, schema)| { + let key = parent_key.map_or_else(|| key.clone(), |k| format!("{k}.{key}")); + self.render_schema(depth + 1, &key, Self::get_schema_object(schema)) + }) + .collect::>() + } + + fn render_schema(&self, depth: usize, key: &str, schema: &SchemaObject) -> Section { + let schema = schema.reference.as_ref().map_or(schema, |r| self.get_referenced_schema(r)); + Section { + level: "#".repeat(depth), + title: key.into(), + instance_type: serde_json::to_string(&schema.instance_type).unwrap().replace('"', ""), + description: schema + .metadata + .as_ref() + .and_then(|m| m.description.clone()) + .unwrap_or_default(), + sections: self.render_properties(depth, Some(key), schema), + } + } +} diff --git a/tasks/website/src/linter/mod.rs b/tasks/website/src/linter/mod.rs new file mode 100644 index 000000000..b24766966 --- /dev/null +++ b/tasks/website/src/linter/mod.rs @@ -0,0 +1,9 @@ +mod cli; +mod json_schema; +mod rules; + +pub use self::{ + cli::generate_cli, + json_schema::{generate_schema_json, generate_schema_markdown}, + rules::generate_rules, +}; diff --git a/tasks/website/src/linter/rules.rs b/tasks/website/src/linter/rules.rs new file mode 100644 index 000000000..545661ae0 --- /dev/null +++ b/tasks/website/src/linter/rules.rs @@ -0,0 +1,7 @@ +// +pub fn generate_rules() { + use oxc_linter::Linter; + let mut v = vec![]; + Linter::print_rules(&mut v); + println!("{}", String::from_utf8(v).unwrap()); +} diff --git a/tasks/website/src/main.rs b/tasks/website/src/main.rs index 526d57abd..8f7987cc3 100644 --- a/tasks/website/src/main.rs +++ b/tasks/website/src/main.rs @@ -9,7 +9,8 @@ fn main() { let task = command.as_deref().unwrap_or("default"); match task { - "linter-json-schema" => linter::generate_json_schema(), + "linter-schema-json" => linter::generate_schema_json(), + "linter-schema-markdown" => linter::generate_schema_markdown(), "linter-cli" => linter::generate_cli(), "linter-rules" => linter::generate_rules(), _ => println!("Missing task command."),