summaryrefslogtreecommitdiff
path: root/src/diagnostics.rs
blob: 514818a8be56ee31b50f95d1905db7d086eb5e07 (plain)
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")
}