Skip to content

Commit

Permalink
test(linter): more documentation testing
Browse files Browse the repository at this point in the history
  • Loading branch information
DonIsaac committed Aug 9, 2024
1 parent 2fe39dd commit 957bea4
Show file tree
Hide file tree
Showing 8 changed files with 549 additions and 12 deletions.
342 changes: 333 additions & 9 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/oxc_linter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
38 changes: 37 additions & 1 deletion crates/oxc_linter/src/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
}
}
2 changes: 1 addition & 1 deletion crates/oxc_linter/src/rules/eslint/for_direction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
46 changes: 46 additions & 0 deletions crates/oxc_linter/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RuleTable> = 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("<table>"));
}
}

#[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("<table>"));
assert!(html.contains(PREFIX_WITH_SLASH));
}
}
}
9 changes: 8 additions & 1 deletion tasks/website/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
5 changes: 5 additions & 0 deletions tasks/website/src/linter/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod doc_page;
mod html;
mod table;
#[cfg(test)]
mod test;

use std::{
borrow::Cow,
Expand Down Expand Up @@ -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();
Expand Down
118 changes: 118 additions & 0 deletions tasks/website/src/linter/rules/test.rs
Original file line number Diff line number Diff line change
@@ -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<RuleTable> = 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 &section.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("&lt;", "<").replace("&gt;", ">");
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::<Vec<_>>();
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(),
}
}

0 comments on commit 957bea4

Please sign in to comment.