From d4cb7e6f5889851cafd82f86ec7f092e8fe4e005 Mon Sep 17 00:00:00 2001 From: DonIsaac <22823424+DonIsaac@users.noreply.github.com> Date: Tue, 15 Oct 2024 20:35:45 +0000 Subject: [PATCH] fix(website/linter): better md rendering for primitive arrays (#6602) Improves how config docs are rendered for the website. If a config property is an array of primitive values, we now skip subsection rendering and render a better type, e.g. `string[]` instead of `array`. --- tasks/website/src/linter/json_schema.rs | 81 ++++++++++++++++--- .../src/linter/snapshots/schema_markdown.snap | 38 +-------- 2 files changed, 69 insertions(+), 50 deletions(-) diff --git a/tasks/website/src/linter/json_schema.rs b/tasks/website/src/linter/json_schema.rs index 53a40a0fe..4d7c98cb1 100644 --- a/tasks/website/src/linter/json_schema.rs +++ b/tasks/website/src/linter/json_schema.rs @@ -1,7 +1,7 @@ use handlebars::Handlebars; use oxc_linter::Oxlintrc; use schemars::{ - schema::{RootSchema, Schema, SchemaObject, SingleOrVec, SubschemaValidation}, + schema::{InstanceType, RootSchema, Schema, SchemaObject, SingleOrVec, SubschemaValidation}, schema_for, }; use serde::Serialize; @@ -49,16 +49,17 @@ type: `{{instance_type}}` {{description}} {{> section}} - {{/each}} "; +/// Data passed to [`ROOT`] hbs template #[derive(Serialize)] struct Root { title: String, sections: Vec
, } +/// Data passed to [`SECTION`] hbs template #[derive(Serialize)] struct Section { level: String, @@ -77,13 +78,17 @@ impl Renderer { fn new(root_schema: RootSchema) -> 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()); + handlebars + .register_template_string("root", ROOT) + .expect("Failed to register root template."); + handlebars + .register_template_string("section", SECTION) + .expect("Failed to register section template."); Self { handlebars, root_schema } } fn render(self) -> String { - let mut root = self.render_root_schema(&self.root_schema); + let mut root = self.render_root_schema(); root.sanitize(); self.handlebars.render("root", &root).unwrap() } @@ -105,17 +110,19 @@ impl Renderer { } } - fn render_root_schema(&self, root_schema: &RootSchema) -> Root { - let title = root_schema + fn render_root_schema(&self) -> Root { + let title = self + .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); + let sections = self.render_properties(1, None, &self.root_schema.schema); Root { title, sections } } + /// Recursively render a subschema's properties into sections. fn render_properties( &self, depth: usize, @@ -182,19 +189,34 @@ impl Renderer { fn render_schema(&self, depth: usize, key: &str, schema: &SchemaObject) -> Section { let schema = self.get_referenced_schema(schema); + + let (instance_type, sections) = if let Some(item) = as_primitive_array(schema) { + // e.g. "string[]" instead of "array" + let instance_type = serde_json::to_string_pretty(item.instance_type.as_ref().unwrap()) + .unwrap() + .replace('"', "") + + "[]"; + // Do not render subsections for primitive arrays + (Some(instance_type), vec![]) + } else { + let instance_type = schema + .instance_type + .as_ref() + .map(|t| serde_json::to_string_pretty(t).unwrap().replace('"', "")); + let sections = self.render_properties(depth, Some(key), schema); + (instance_type, sections) + }; + Section { level: "#".repeat(depth), title: key.into(), - instance_type: schema - .instance_type - .as_ref() - .map(|t| serde_json::to_string_pretty(t).unwrap().replace('"', "")), + instance_type, description: schema .metadata .as_ref() .and_then(|m| m.description.clone()) .unwrap_or_default(), - sections: self.render_properties(depth, Some(key), schema), + sections, } } } @@ -220,3 +242,36 @@ fn sanitize(s: &mut String) { let json = format!("\n{json}\n"); s.replace_range(start..start + end, &json); } + +/// If `schema` is an array of primitive data types, returns [`Some`] with the +/// primitive schema (i.e. the array item schema). +fn as_primitive_array(schema: &SchemaObject) -> Option<&SchemaObject> { + let arr = schema.array.as_ref()?; + let SingleOrVec::Single(item) = arr.items.as_ref()? else { + return None; + }; + let Schema::Object(item) = &**item else { + return None; + }; + + as_primitive(item) +} + +/// If `schema` has a primitive data type (e.g. `string`, `integer`), returns +/// `Some(schema)`. +fn as_primitive(schema: &SchemaObject) -> Option<&SchemaObject> { + // null intentionally omitted + const PRIMITIVE_TYPES: [InstanceType; 4] = + [InstanceType::Boolean, InstanceType::Integer, InstanceType::Number, InstanceType::String]; + + let is_primitive = !schema.is_ref() + && PRIMITIVE_TYPES.iter().any(|t| { + // schema.has_type(*t) + schema.instance_type.as_ref().is_some_and(|ty| { + let SingleOrVec::Single(single) = ty else { return false }; + single.as_ref() == t + }) + }); + + is_primitive.then_some(schema) +} diff --git a/tasks/website/src/linter/snapshots/schema_markdown.snap b/tasks/website/src/linter/snapshots/schema_markdown.snap index 25a874d4d..5da84e002 100644 --- a/tasks/website/src/linter/snapshots/schema_markdown.snap +++ b/tasks/website/src/linter/snapshots/schema_markdown.snap @@ -62,50 +62,42 @@ Rules enabled or disabled this way will be overwritten by individual rules in th - ### categories.nursery - ### categories.pedantic - ### categories.perf - ### categories.restriction - ### categories.style - ### categories.suspicious - - ## env type: `object` @@ -115,7 +107,6 @@ Predefine global variables. Environments specify what global variables are predefined. See [ESLint's list of environments](https://eslint.org/docs/v8.x/use/configure/language-options#specifying-environments) for what environments are available and what each one provides. - ## globals type: `object` @@ -140,19 +131,9 @@ Globals can be disabled by setting their value to `"off"`. For example, in an en You may also use `"readable"` or `false` to represent `"readonly"`, and `"writeable"` or `true` to represent `"writable"`. - ## plugins -type: `array` - - - - -### plugins[n] - -type: `string` - - +type: `string[]` @@ -164,7 +145,6 @@ type: `object` See [Oxlint Rules](https://oxc.rs/docs/guide/usage/linter/rules.html) - ## settings type: `object` @@ -186,7 +166,6 @@ type: `boolean` Only for `require-(yields|returns|description|example|param|throws)` rule - #### settings.jsdoc.exemptDestructuredRootsFromChecks type: `boolean` @@ -194,7 +173,6 @@ type: `boolean` Only for `require-param-type` and `require-param-description` rule - #### settings.jsdoc.ignoreInternal type: `boolean` @@ -202,7 +180,6 @@ type: `boolean` For all rules but NOT apply to `empty-tags` rule - #### settings.jsdoc.ignorePrivate type: `boolean` @@ -210,7 +187,6 @@ type: `boolean` For all rules but NOT apply to `check-access` and `empty-tags` rule - #### settings.jsdoc.ignoreReplacesDocs type: `boolean` @@ -218,7 +194,6 @@ type: `boolean` Only for `require-(yields|returns|description|example|param|throws)` rule - #### settings.jsdoc.implementsReplacesDocs type: `boolean` @@ -226,7 +201,6 @@ type: `boolean` Only for `require-(yields|returns|description|example|param|throws)` rule - #### settings.jsdoc.overrideReplacesDocs type: `boolean` @@ -234,7 +208,6 @@ type: `boolean` Only for `require-(yields|returns|description|example|param|throws)` rule - #### settings.jsdoc.tagNamePreference type: `object` @@ -242,8 +215,6 @@ type: `object` - - ### settings.jsx-a11y type: `object` @@ -258,7 +229,6 @@ type: `object` - #### settings.jsx-a11y.polymorphicPropName type: `[ @@ -269,8 +239,6 @@ type: `[ - - ### settings.next type: `object` @@ -284,8 +252,6 @@ type: `object` - - ### settings.react type: `object` @@ -306,8 +272,6 @@ type: `array` - - #### settings.react.linkComponents type: `array`