From 957bea481f70124abd42b660f463a0d5344de86b Mon Sep 17 00:00:00 2001 From: Don Isaac Date: Sun, 4 Aug 2024 22:11:36 -0400 Subject: [PATCH] test(linter): more documentation testing --- Cargo.lock | 342 +++++++++++++++++- crates/oxc_linter/Cargo.toml | 1 + crates/oxc_linter/src/rule.rs | 38 +- .../src/rules/eslint/for_direction.rs | 2 +- crates/oxc_linter/src/table.rs | 46 +++ tasks/website/Cargo.toml | 9 +- tasks/website/src/linter/rules/mod.rs | 5 + tasks/website/src/linter/rules/test.rs | 118 ++++++ 8 files changed, 549 insertions(+), 12 deletions(-) create mode 100644 tasks/website/src/linter/rules/test.rs diff --git a/Cargo.lock b/Cargo.lock index fdbf6286dd174..4e5d5417394a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -192,6 +193,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.6.0" @@ -388,6 +395,29 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.11.2", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "ctor" version = "0.2.8" @@ -440,6 +470,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -450,6 +491,21 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + [[package]] name = "dunce" version = "1.0.4" @@ -462,6 +518,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "ego-tree" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" + [[package]] name = "either" version = "1.13.0" @@ -559,6 +621,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures" version = "0.3.30" @@ -648,6 +720,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -658,6 +739,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -762,6 +852,20 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "httparse" version = "1.9.4" @@ -937,7 +1041,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -997,6 +1101,35 @@ dependencies = [ "url", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markdown" +version = "1.0.0-alpha.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e61c5c85b392273c4d4ea546e6399ace3e3db172ab01b6de8f3d398d1dbd2ec" +dependencies = [ + "unicode-id", +] + +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.2", + "phf_codegen 0.11.2", + "string_cache", + "string_cache_codegen", + "tendril", +] + [[package]] name = "memchr" version = "2.7.4" @@ -1147,6 +1280,12 @@ dependencies = [ "libloading", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nom" version = "7.1.3" @@ -1437,7 +1576,7 @@ dependencies = [ "oxc_span", "oxc_tasks_common", "oxc_transformer", - "phf", + "phf 0.11.2", "pico-args", "project-root", "rayon", @@ -1525,6 +1664,7 @@ dependencies = [ "json-strip-comments", "language-tags", "lazy_static", + "markdown", "memchr", "mime_guess", "once_cell", @@ -1540,7 +1680,7 @@ dependencies = [ "oxc_semantic", "oxc_span", "oxc_syntax", - "phf", + "phf 0.11.2", "project-root", "rayon", "regex", @@ -1728,7 +1868,7 @@ dependencies = [ "oxc_parser", "oxc_span", "oxc_syntax", - "phf", + "phf 0.11.2", "rustc-hash", "serde", "serde_json", @@ -1773,7 +1913,7 @@ dependencies = [ "oxc_ast_macros", "oxc_index", "oxc_span", - "phf", + "phf 0.11.2", "rustc-hash", "ryu-js", "serde", @@ -1980,6 +2120,15 @@ dependencies = [ "indexmap", ] +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + [[package]] name = "phf" version = "0.11.2" @@ -1987,7 +2136,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ "phf_macros", - "phf_shared", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand", ] [[package]] @@ -1996,7 +2175,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" dependencies = [ - "phf_shared", + "phf_shared 0.11.2", "rand", ] @@ -2006,13 +2185,22 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.11.2", + "phf_shared 0.11.2", "proc-macro2", "quote", "syn", ] +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + [[package]] name = "phf_shared" version = "0.11.2" @@ -2066,6 +2254,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.20" @@ -2119,6 +2322,18 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", "rand_core", ] @@ -2127,6 +2342,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] [[package]] name = "rayon" @@ -2374,6 +2592,41 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scraper" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90460b31bfe1fc07be8262e42c665ad97118d4585869de9345a84d501a9eaf0" +dependencies = [ + "ahash", + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "once_cell", + "selectors", + "tendril", +] + +[[package]] +name = "selectors" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" +dependencies = [ + "bitflags 2.6.0", + "cssparser", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf 0.10.1", + "phf_codegen 0.10.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + [[package]] name = "semver" version = "1.0.23" @@ -2463,6 +2716,15 @@ dependencies = [ "syn", ] +[[package]] +name = "servo_arc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2541,6 +2803,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2553,6 +2821,32 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9557cb6521e8d009c51a8666f09356f4b817ba9ba0981a305bd86aee47bd35c" +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2592,6 +2886,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "textwrap" version = "0.16.1" @@ -2879,6 +3184,12 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +[[package]] +name = "unicode-id" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b6def86329695390197b82c1e244a54a131ceb66c996f2088a3876e2ae083f" + [[package]] name = "unicode-id-start" version = "1.2.0" @@ -2953,6 +3264,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "valuable" version = "0.1.0" @@ -3067,10 +3384,16 @@ dependencies = [ "bpaf", "handlebars", "insta", + "markdown", + "oxc_allocator", + "oxc_diagnostics", "oxc_linter", + "oxc_parser", + "oxc_span", "oxlint", "pico-args", "schemars", + "scraper", "serde", "serde_json", ] @@ -3257,6 +3580,7 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] diff --git a/crates/oxc_linter/Cargo.toml b/crates/oxc_linter/Cargo.toml index fd82b48b5e359..e25ac5120ec00 100644 --- a/crates/oxc_linter/Cargo.toml +++ b/crates/oxc_linter/Cargo.toml @@ -57,3 +57,4 @@ schemars = { workspace = true, features = ["indexmap2"] } static_assertions = { workspace = true } insta = { workspace = true } project-root = { workspace = true } +markdown = { version = "1.0.0-alpha.18" } diff --git a/crates/oxc_linter/src/rule.rs b/crates/oxc_linter/src/rule.rs index 3fb081fb1315d..111be46be01e1 100644 --- a/crates/oxc_linter/src/rule.rs +++ b/crates/oxc_linter/src/rule.rs @@ -235,12 +235,48 @@ impl RuleWithSeverity { #[cfg(test)] mod test { use crate::rules::RULES; + use itertools::Itertools as _; + use markdown::{to_html_with_options, Options}; + use oxc_allocator::Allocator; + use oxc_diagnostics::Error; + use oxc_parser::Parser; + use oxc_span::SourceType; #[test] fn ensure_documentation() { assert!(!RULES.is_empty()); + let options = Options::gfm(); + let source_type = SourceType::default().with_jsx(true).with_always_strict(true); + for rule in RULES.iter() { - assert!(rule.documentation().is_some_and(|s| !s.is_empty()), "{}", rule.name()); + let name = rule.name(); + assert!( + rule.documentation().is_some_and(|s| !s.is_empty()), + "Rule '{name}' is missing documentation." + ); + // will panic if provided invalid markdown + let html = to_html_with_options(rule.documentation().unwrap(), &options).unwrap(); + assert!(!html.is_empty()); + + // convert HTML to JSX, then use the parser to ensure valid HTML was generated + let jsx = + format!("const Documentation = <>{};", html.replace("class=", "className=")); + let allocator = Allocator::default(); + let ret = Parser::new(&allocator, &jsx, source_type).parse(); + + let has_errors = !ret.errors.is_empty(); + let errors = ret + .errors + .into_iter() + .map(|error| Error::new(error).with_source_code(jsx.clone())) + .map(|e| format!("{e:?}")) + .join("\n\n"); + + assert!( + !ret.panicked && !has_errors, + "Documentation for rule '{name}' has invalid syntax: {errors}" + ); + assert!(!ret.program.body.is_empty(), "Documentation for rule '{name}' is empty."); } } } diff --git a/crates/oxc_linter/src/rules/eslint/for_direction.rs b/crates/oxc_linter/src/rules/eslint/for_direction.rs index 13dd1219f629a..b227ed8bc716c 100644 --- a/crates/oxc_linter/src/rules/eslint/for_direction.rs +++ b/crates/oxc_linter/src/rules/eslint/for_direction.rs @@ -35,7 +35,7 @@ declare_oxc_lint!( /// ```javascript /// for (var i = 0; i < 10; i--) {} /// - /// for (var = 10; i >= 0; i++) {} + /// for (var i = 10; i >= 0; i++) {} /// ``` ForDirection, correctness, diff --git a/crates/oxc_linter/src/table.rs b/crates/oxc_linter/src/table.rs index ab45d824a6bd7..c61aae271e6da 100644 --- a/crates/oxc_linter/src/table.rs +++ b/crates/oxc_linter/src/table.rs @@ -125,3 +125,49 @@ impl RuleTableSection { s } } + +#[cfg(test)] +mod test { + use super::*; + use markdown::{to_html_with_options, Options}; + use std::sync::OnceLock; + + static TABLE: OnceLock = OnceLock::new(); + + fn table() -> &'static RuleTable { + TABLE.get_or_init(|| RuleTable::new()) + } + + #[test] + fn test_table_no_links() { + let options = Options::gfm(); + for section in &table().sections { + let rendered_table = section.render_markdown_table(None); + assert!(!rendered_table.is_empty()); + assert_eq!(rendered_table.split("\n").count(), 5 + section.rows.len()); + + let html = to_html_with_options(&rendered_table, &options).unwrap(); + assert!(!html.is_empty()); + assert!(html.contains("")); + } + } + + #[test] + fn test_table_with_links() { + const PREFIX: &str = "/foo/bar"; + const PREFIX_WITH_SLASH: &str = "/foo/bar/"; + + let options = Options::gfm(); + + for section in &table().sections { + let rendered_table = section.render_markdown_table(Some(PREFIX)); + assert!(!rendered_table.is_empty()); + assert_eq!(rendered_table.split("\n").count(), 5 + section.rows.len()); + + let html = to_html_with_options(&rendered_table, &options).unwrap(); + assert!(!html.is_empty()); + assert!(html.contains("
")); + assert!(html.contains(PREFIX_WITH_SLASH)); + } + } +} diff --git a/tasks/website/Cargo.toml b/tasks/website/Cargo.toml index 934333cbf7222..c35428112dd35 100644 --- a/tasks/website/Cargo.toml +++ b/tasks/website/Cargo.toml @@ -27,7 +27,14 @@ serde = { workspace = true } bpaf = { workspace = true, features = ["docgen"] } [dev-dependencies] -insta = { workspace = true } +oxc_allocator = { workspace = true } +oxc_diagnostics = { workspace = true } +oxc_parser = { workspace = true } +oxc_span = { workspace = true } + +insta = { workspace = true } +markdown = { version = "1.0.0-alpha.18" } +scraper = { version = "0.20.0" } [package.metadata.cargo-shear] ignored = ["bpaf"] diff --git a/tasks/website/src/linter/rules/mod.rs b/tasks/website/src/linter/rules/mod.rs index 151a658d8c0a8..88cafd8dfd6eb 100644 --- a/tasks/website/src/linter/rules/mod.rs +++ b/tasks/website/src/linter/rules/mod.rs @@ -1,6 +1,8 @@ mod doc_page; mod html; mod table; +#[cfg(test)] +mod test; use std::{ borrow::Cow, @@ -95,6 +97,9 @@ fn write_rule_doc_pages(table: &RuleTable, outdir: &Path) { let plugin_path = outdir.join(&rule.plugin); fs::create_dir_all(&plugin_path).unwrap(); let page_path = plugin_path.join(format!("{}.md", rule.name)); + if page_path.exists() { + fs::remove_file(&page_path).unwrap(); + } println!("{}", page_path.display()); let docs = render_rule_docs_page(rule).unwrap(); fs::write(&page_path, docs).unwrap(); diff --git a/tasks/website/src/linter/rules/test.rs b/tasks/website/src/linter/rules/test.rs new file mode 100644 index 0000000000000..d310dad25b27c --- /dev/null +++ b/tasks/website/src/linter/rules/test.rs @@ -0,0 +1,118 @@ +use markdown::{to_html, to_html_with_options, Options}; +use oxc_diagnostics::NamedSource; +use scraper::{ElementRef, Html, Selector}; +use std::sync::{Arc, OnceLock}; + +use oxc_allocator::Allocator; +use oxc_linter::table::RuleTable; +use oxc_parser::Parser; +use oxc_span::SourceType; + +use super::{render_rule_docs_page, render_rules_table}; + +static TABLE: OnceLock = OnceLock::new(); + +fn table() -> &'static RuleTable { + TABLE.get_or_init(|| RuleTable::new()) +} + +fn parse(filename: &str, jsx: &str) -> Result<(), String> { + let filename = format!("{filename}.tsx"); + let source_type = SourceType::from_path(&filename).unwrap(); + parse_type(&filename, jsx, source_type) +} + +fn parse_type(filename: &str, source_text: &str, source_type: SourceType) -> Result<(), String> { + let alloc = Allocator::default(); + let ret = Parser::new(&alloc, source_text, source_type).parse(); + + if ret.errors.is_empty() { + Ok(()) + } else { + let num_errs = ret.errors.len(); + let source = Arc::new(NamedSource::new(filename, source_text.to_string())); + ret.errors + .into_iter() + .map(|e| e.with_source_code(Arc::clone(&source))) + .for_each(|e| println!("{e:?}")); + Err(format!("{} errors occurred while parsing {filename}.jsx", num_errs)) + } +} + +#[test] +fn test_rules_table() { + const PREFIX: &str = "/docs/guide/usage/linter/rules"; + let rendered_table = render_rules_table(table(), PREFIX); + let html = to_html(&rendered_table); + let jsx = format!("const Table = () => <>{html}"); + parse("rules-table", &jsx).unwrap(); +} + +#[test] +fn test_doc_pages() { + let mut options = Options::gfm(); + options.compile.allow_dangerous_html = true; + + for section in &table().sections { + let category = section.category; + let code = Selector::parse("code").unwrap(); + + for row in §ion.rows { + let filename = format!("{category}/{}/{}", row.plugin, row.name); + let docs = render_rule_docs_page(row).unwrap(); + let docs = to_html_with_options(&docs, &options).unwrap(); + let docs = if let Some(end_of_autogen_comment) = docs.find("-->") { + &docs[end_of_autogen_comment + 4..] + } else { + &docs + }; + + // ensure the docs are valid JSX + { + let jsx = format!("const Docs = () => <>{docs}"); + parse(&filename, &jsx).unwrap(); + } + + // ensure code examples are valid + { + let html = Html::parse_fragment(&docs); + assert!(html.errors.is_empty(), "HTML parsing errors: {:#?}", html.errors); + for code_el in html.select(&code) { + let inner = code_el.inner_html(); + assert!( + !inner.trim().is_empty(), + "Rule '{}' has an empty code snippet", + row.name + ); + let inner = inner.replace("<", "<").replace(">", ">"); + let filename = filename.clone() + "/code-snippet"; + let source_type = source_type_from_code_element(code_el); + parse_type(&filename, &inner, source_type).unwrap(); + } + } + } + } +} + +fn default_source() -> SourceType { + SourceType::default().with_typescript(true).with_jsx(true).with_always_strict(true) +} +fn source_type_from_code_element(code: ElementRef) -> SourceType { + let Some(class) = code.attr("class") else { + return default_source(); + }; + let maybe_class = class.split('-').collect::>(); + let ["language", lang] = maybe_class.as_slice() else { + return default_source(); + }; + + match *lang { + "javascript" | "js" => SourceType::default().with_always_strict(true), + "jsx" => SourceType::default().with_jsx(true).with_always_strict(true), + "typescript" | "ts" => SourceType::default().with_typescript(true).with_always_strict(true), + "tsx" => { + SourceType::default().with_typescript(true).with_jsx(true).with_always_strict(true) + } + _ => default_source(), + } +}