From 8399983fd45bc0f11eac9def280c0833ffb93c04 Mon Sep 17 00:00:00 2001 From: Mica White Date: Sun, 12 Jul 2026 19:49:14 -0400 Subject: initial commit --- src/diagnostics.rs | 217 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 src/diagnostics.rs (limited to 'src/diagnostics.rs') diff --git a/src/diagnostics.rs b/src/diagnostics.rs new file mode 100644 index 0000000..514818a --- /dev/null +++ b/src/diagnostics.rs @@ -0,0 +1,217 @@ +use miette::{Diagnostic, Error}; +use thiserror::Error; + +use crate::{ + parse::{Attribute, Document, Element, Node}, + tokenize::{AttributeToken, Token, TokenType}, +}; + +mod duplicate_attribute_names; +mod empty_tag_name; +mod end_tag_with_attributes; +mod forbidden_top_level_tag; +mod html_tag; +mod image_tag; +mod non_alphabetic_attribute_name; +mod non_alphabetic_tag_name; +mod non_self_closing_void_tag; +mod non_void_self_closing_tag; +mod self_closing_end_tag; +mod stray_end_tag; +mod stray_equal_sign; +mod too_many_equal_signs; +mod top_level_character_data; +mod unclosed_start_tag; +mod unterminated_comment; +mod unterminated_tag; + +const VOID_TAGS: &[&str] = &[ + "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", + "track", "wbr", +]; +const ALLOWED_TOP_LEVEL_TAGS: &[&str] = &["import", "const", "block", "head", "body"]; + +pub struct DiagnosticsContext { + source_code: &'static str, +} + +trait TokenDiagnostic: Sized + Send + Sync + 'static { + fn handle_token(_: &Token<'_>) -> Option { + None + } + + fn handle_attribute_token(_: &AttributeToken<'_>) -> Option { + None + } +} + +trait AstDiagnostic: Sized + Send + Sync + 'static { + fn handle_document(_: &Document<'_>) -> Option { + None + } + + fn handle_node(_: &Node<'_>) -> Option { + None + } + + fn handle_element(_: &Element<'_>) -> Option { + None + } + + fn handle_attribute(_: &Attribute<'_>) -> Option { + None + } +} + +fn maybe_print_diagnostic( + ctx: &DiagnosticsContext, + diagnostic: Option, +) { + if let Some(diagnostic) = diagnostic { + eprintln!( + "{}", + Error::new(diagnostic).with_source_code(ctx.source_code) + ); + } +} + +#[derive(Debug, Error, Diagnostic)] +#[error("This file is not valid UTF-8")] +#[diagnostic(code(NonUtf8), severity(Error))] +struct NonUtf8; + +#[derive(Debug, Error, Diagnostic)] +#[error("Forbidden NUL character found")] +#[diagnostic(code(NonUtf8), severity(Error))] +struct NulCharacter { + #[label("Forbidden NUL byte found here")] + location: usize, +} + +#[derive(Debug, Error, Diagnostic)] +#[error("Forbidden control character found")] +#[diagnostic(code(NonUtf8), severity(Error))] +struct ControlCharacter { + #[label("Control character found")] + location: usize, +} + +pub fn run_character_diagnostics(source: &'static [u8]) -> Option<&'static str> { + let Ok(source) = str::from_utf8(source) else { + eprintln!("{}", Error::new(NonUtf8)); + return None; + }; + let ctx = DiagnosticsContext { + source_code: source, + }; + for (i, char) in source.char_indices() { + maybe_print_diagnostic(&ctx, (char == '\0').then_some(NulCharacter { location: i })); + maybe_print_diagnostic( + &ctx, + (char.is_control() && !char.is_ascii_whitespace()) + .then_some(ControlCharacter { location: i }), + ); + } + + Some(source) +} + +fn run_token_diagnostic( + ctx: &DiagnosticsContext, + tokens: &[Token<'_>], +) { + for token in tokens { + maybe_print_diagnostic(ctx, D::handle_token(token)); + + let TokenType::Tag { + attribute_tokens, .. + } = token.ty + else { + continue; + }; + + for token in attribute_tokens { + maybe_print_diagnostic(ctx, D::handle_attribute_token(token)); + } + } +} + +fn run_ast_diagnostic( + ctx: &DiagnosticsContext, + document: &Document<'_>, +) { + maybe_print_diagnostic(ctx, D::handle_document(document)); + for node in document.children { + maybe_print_diagnostic(ctx, D::handle_node(node)); + + let Node::Element(element) = node else { + continue; + }; + maybe_print_diagnostic(ctx, D::handle_element(element)); + + for attribute in element.attributes { + maybe_print_diagnostic(ctx, D::handle_attribute(attribute)); + } + } +} + +pub fn run_token_diagnostics(ctx: &DiagnosticsContext, tokens: &[Token<'_>]) { + run_token_diagnostic::(ctx, tokens); + run_token_diagnostic::(ctx, tokens); + run_token_diagnostic::(ctx, tokens); + run_token_diagnostic::(ctx, tokens); + run_token_diagnostic::(ctx, tokens); + run_token_diagnostic::(ctx, tokens); + run_token_diagnostic::(ctx, tokens); + run_token_diagnostic::(ctx, tokens); + run_token_diagnostic::(ctx, tokens); + // TODO invalid character reference +} + +pub fn run_ast_diagnostics(ctx: &DiagnosticsContext, document: &Document<'_>) { + run_ast_diagnostic::(ctx, document); + run_ast_diagnostic::(ctx, document); + run_ast_diagnostic::(ctx, document); + run_ast_diagnostic::(ctx, document); + run_ast_diagnostic::(ctx, document); + run_ast_diagnostic::(ctx, document); + run_ast_diagnostic::(ctx, document); + run_ast_diagnostic::(ctx, document); + run_ast_diagnostic::(ctx, document); + // TODO invalid + // TODO invalid + // TODO invalid + // TODO invalid + // TODO deprecated tag + // TODO deprecated tag attribute + // + // TODO forbidden tag in noscript + // TODO head tag outside head + // TODO html tag in body + // TODO heading inside heading + // TODO form inside form + // TODO button inside button + // TODO anchor inside anchor + // TODO head inside body + // TODO caption, col, colgroup, tbody, tfoot, thead, tr outside table + // TODO table inside table + // TODO non-hidden input inside table + // TODO form inside table + // TODO body inside body + // TODO col, colgroup, tbody, tfoot, thead, tr inside caption + // TODO th, td outside tr + // TODO caption, col, colgroup, tr in tbody + // TODO caption, col, colgroup, tbody, td, tfoot, th, thead, tr inside table cell + // TODO select inside select + // TODO input, keygen, textarea inside select + // TODO caption, table, tbody, tfoot, thread, tr, td, th inside select + // + // TODO anchor with invalid href + // TODO dt or dd outside dl + // TODO li outside ul, ol, menu + // TODO invalid dl, ul, menu + // TODO invalid attribute values (see microsyntaxes) + // TODO aria tags don't match tag semantics + // TODO empty or missing image src + // TODO missing alt text for image (unless inside figure with caption, or has title, or meta name = "generator") +} -- cgit v1.2.3