summaryrefslogtreecommitdiff
path: root/src/diagnostics.rs
diff options
context:
space:
mode:
authorMica White <botahamec@outlook.com>2026-07-12 19:49:14 -0400
committerMica White <botahamec@outlook.com>2026-07-12 19:49:14 -0400
commit8399983fd45bc0f11eac9def280c0833ffb93c04 (patch)
treef84173ca30a3c62db7abb92a0d869ff5d5199c51 /src/diagnostics.rs
initial commitHEADmain
Diffstat (limited to 'src/diagnostics.rs')
-rw-r--r--src/diagnostics.rs217
1 files changed, 217 insertions, 0 deletions
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<Self> {
+ None
+ }
+
+ fn handle_attribute_token(_: &AttributeToken<'_>) -> Option<Self> {
+ None
+ }
+}
+
+trait AstDiagnostic: Sized + Send + Sync + 'static {
+ fn handle_document(_: &Document<'_>) -> Option<Self> {
+ None
+ }
+
+ fn handle_node(_: &Node<'_>) -> Option<Self> {
+ None
+ }
+
+ fn handle_element(_: &Element<'_>) -> Option<Self> {
+ None
+ }
+
+ fn handle_attribute(_: &Attribute<'_>) -> Option<Self> {
+ None
+ }
+}
+
+fn maybe_print_diagnostic<D: Diagnostic + Send + Sync + 'static>(
+ ctx: &DiagnosticsContext,
+ diagnostic: Option<D>,
+) {
+ 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<D: Diagnostic + TokenDiagnostic>(
+ 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<D: AstDiagnostic + 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::<non_void_self_closing_tag::NonVoidSelfClosingTag>(ctx, tokens);
+ run_token_diagnostic::<non_self_closing_void_tag::NonSelfClosingVoidTag>(ctx, tokens);
+ run_token_diagnostic::<end_tag_with_attributes::EndTagWithAttributes>(ctx, tokens);
+ run_token_diagnostic::<self_closing_end_tag::SelfClosingEndTag>(ctx, tokens);
+ run_token_diagnostic::<non_alphabetic_tag_name::NonAlphabeticTagName>(ctx, tokens);
+ run_token_diagnostic::<empty_tag_name::EmptyTagName>(ctx, tokens);
+ run_token_diagnostic::<unterminated_tag::UnterminatedTag>(ctx, tokens);
+ run_token_diagnostic::<unterminated_comment::UnterminatedComment>(ctx, tokens);
+ run_token_diagnostic::<non_alphabetic_attribute_name::NonAlphabeticAttributeName>(ctx, tokens);
+ // TODO invalid character reference
+}
+
+pub fn run_ast_diagnostics(ctx: &DiagnosticsContext, document: &Document<'_>) {
+ run_ast_diagnostic::<stray_equal_sign::StrayEqualSign>(ctx, document);
+ run_ast_diagnostic::<too_many_equal_signs::TooManyEqualSigns>(ctx, document);
+ run_ast_diagnostic::<duplicate_attribute_names::DuplicateAttributeNames>(ctx, document);
+ run_ast_diagnostic::<top_level_character_data::TopLevelCharacterData>(ctx, document);
+ run_ast_diagnostic::<forbidden_top_level_tag::ForbiddenTopLevelTag>(ctx, document);
+ run_ast_diagnostic::<stray_end_tag::StrayEndTag>(ctx, document);
+ run_ast_diagnostic::<unclosed_start_tag::UnclosedStartTag>(ctx, document);
+ run_ast_diagnostic::<html_tag::HtmlTag>(ctx, document);
+ run_ast_diagnostic::<image_tag::ImageTag>(ctx, document);
+ // TODO invalid <kuht-var />
+ // TODO invalid <import />
+ // TODO invalid <const />
+ // TODO invalid <block />
+ // 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")
+}