use std::fmt;
use std::fmt::Write as _;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Severity {
Error,
Warning,
}
impl Severity {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Severity::Error => "error",
Severity::Warning => "warning",
}
}
}
pub mod codes {
pub const MISSING_FOR_EACH: &str = "missing-for-each";
pub const INVALID_FOR_EACH: &str = "invalid-for-each";
pub const INVALID_FOR_IDENTIFIER: &str = "invalid-for-identifier";
pub const INVALID_FOR_KEY: &str = "invalid-for-key";
pub const MISSING_IF_CONDITION: &str = "missing-if-condition";
pub const INVALID_IF_CONDITION: &str = "invalid-if-condition";
pub const UNKNOWN_COMPONENT: &str = "unknown-component";
pub const INVALID_EVENT_HANDLER: &str = "invalid-event-handler";
pub const SCRIPTLESS_EVENT_HANDLER: &str = "scriptless-event-handler";
pub const INVALID_W_REF: &str = "invalid-w-ref";
pub const UNCLOSED_HTML_TAG: &str = "unclosed-html-tag";
pub const MALFORMED_HTML_TAG: &str = "malformed-html-tag";
pub const UNEXPECTED_CLOSING_TAG: &str = "unexpected-closing-tag";
pub const LIGHT_DOM_SLOT: &str = "light-dom-slot";
pub const INVALID_SHADOW_ROOT_MODE: &str = "invalid-shadow-root-mode";
#[deprecated(note = "Light DOM no longer generates compiler-owned markers")]
pub const RESERVED_LIGHT_DOM_MARKER: &str = "reserved-light-dom-marker";
#[deprecated(note = "Light DOM no longer generates compiler-owned markers")]
pub const LIGHT_SCOPE_COLLISION: &str = "light-scope-collision";
pub const UNSUPPORTED_LIGHT_CSS: &str = "unsupported-light-css";
pub const FAST_LIGHT_DOM_UNSUPPORTED: &str =
crate::plugin::fast::diagnostic::FAST_LIGHT_DOM_UNSUPPORTED;
#[deprecated(note = "Light DOM no longer namespaces component-local keyframes")]
pub const DYNAMIC_LIGHT_KEYFRAME: &str = "dynamic-light-keyframe";
pub const UNTERMINATED_HTML_COMMENT: &str = "unterminated-html-comment";
pub const UNTERMINATED_HTML_DECLARATION: &str = "unterminated-html-declaration";
pub const EXCESSIVE_NESTING: &str = "excessive-nesting";
pub const RECURSIVE_TEMPLATE: &str = "recursive-template";
pub const INVALID_CSS: &str = "invalid-css";
pub const MISSING_THEME_TOKEN: &str = "missing-theme-token";
pub const UNTHEMED_TOKEN: &str = "unthemed-token";
pub const MISSING_BOUNDARY_NAME: &str = "missing-boundary-name";
pub const INVALID_BOUNDARY_NAME: &str = "invalid-boundary-name";
pub const DUPLICATE_BOUNDARY_NAME: &str = "duplicate-boundary-name";
pub const MISSING_BOUNDARY_KEY: &str = "missing-boundary-key";
pub const BOUNDARY_IN_REPEAT: &str = "boundary-in-repeat";
pub const INVALID_BOUNDARY_KEY: &str = "invalid-boundary-key";
pub const TOO_MANY_BOUNDARIES: &str = "too-many-boundaries";
pub const NESTED_BOUNDARY: &str = "nested-boundary";
pub const BOUNDARY_CROSSES_SCOPE: &str = "boundary-crosses-scope";
pub const BOUNDARY_OUTSIDE_BODY: &str = "boundary-outside-body";
pub const BOUNDARY_IN_FOSTER_CONTEXT: &str = "boundary-in-foster-context";
pub const INVALID_ROUTE_BOUNDARY_PLACEMENT: &str = "invalid-route-boundary-placement";
pub const AUTHORED_WEBUI_HYDRATE: &str = "authored-webui-hydrate";
pub const STREAMING_WITHOUT_PROJECTION: &str = "streaming-without-projection";
pub const AMBIGUOUS_MODULE_ENTRY: &str = "ambiguous-module-entry";
pub const EXCESSIVE_MODULE_PRELOADS: &str = "excessive-module-preloads";
pub const UNSAFE_MODULE_PRELOAD: &str = "unsafe-module-preload";
pub const COMPONENT_ASSETS_WITH_ROUTES: &str = "component-assets-with-routes";
pub const INVALID_COMPONENT_RENDER_POLICY: &str = "invalid-component-render-policy";
pub const MISSING_RENDER_RESERVATION: &str = "missing-render-reservation";
pub const INVALID_RENDER_RESERVATION: &str = "invalid-render-reservation";
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
severity: Severity,
code: Option<&'static str>,
title: String,
component: Option<String>,
element: Option<String>,
snippet: Option<String>,
help: Option<String>,
position: Option<(usize, usize)>,
}
impl Diagnostic {
#[must_use]
pub fn error(title: impl Into<String>) -> Self {
Self::new(Severity::Error, title)
}
#[must_use]
pub fn warning(title: impl Into<String>) -> Self {
Self::new(Severity::Warning, title)
}
fn new(severity: Severity, title: impl Into<String>) -> Self {
Self {
severity,
code: None,
title: title.into(),
component: None,
element: None,
snippet: None,
help: None,
position: None,
}
}
#[must_use]
pub fn code(mut self, code: &'static str) -> Self {
self.code = Some(code);
self
}
#[must_use]
pub fn component(mut self, component: impl Into<String>) -> Self {
self.component = Some(component.into());
self
}
#[must_use]
pub fn element(mut self, element: impl Into<String>) -> Self {
self.element = Some(element.into());
self
}
#[must_use]
pub fn snippet(mut self, snippet: impl Into<String>) -> Self {
self.snippet = Some(snippet.into());
self
}
#[must_use]
pub fn help(mut self, help: impl Into<String>) -> Self {
self.help = Some(help.into());
self
}
#[must_use]
pub fn position(mut self, line: usize, column: usize) -> Self {
self.position = Some((line, column));
self
}
#[must_use]
pub fn at_offset(self, source: &str, offset: usize) -> Self {
let (line, column) = line_column(source, offset);
self.position(line, column)
}
#[must_use]
pub fn severity(&self) -> Severity {
self.severity
}
#[must_use]
pub fn error_code(&self) -> Option<&'static str> {
self.code
}
#[must_use]
pub fn title(&self) -> &str {
&self.title
}
#[must_use]
pub fn component_name(&self) -> Option<&str> {
self.component.as_deref()
}
#[must_use]
pub fn element_name(&self) -> Option<&str> {
self.element.as_deref()
}
#[must_use]
pub fn snippet_text(&self) -> Option<&str> {
self.snippet.as_deref()
}
#[must_use]
pub fn help_text(&self) -> Option<&str> {
self.help.as_deref()
}
#[must_use]
pub fn position_line_column(&self) -> Option<(usize, usize)> {
self.position
}
#[must_use]
pub fn location(&self) -> Option<String> {
if let Some((line, column)) = self.position {
return Some(match &self.component {
Some(c) => format!("--> {c}:{line}:{column}"),
None => format!("--> {line}:{column}"),
});
}
match (&self.component, &self.element) {
(Some(c), Some(e)) => Some(format!("in component <{c}> · element <{e}>")),
(Some(c), None) => Some(format!("in component <{c}>")),
(None, Some(e)) => Some(format!("in element <{e}>")),
(None, None) => None,
}
}
#[must_use]
pub fn body(&self) -> String {
let mut out = String::with_capacity(96);
out.push_str(&self.title);
if let Some(code) = self.code {
let _ = write!(out, " [{code}]");
}
if let Some(location) = self.location() {
let _ = write!(out, "\n {location}");
}
if let Some(snippet) = &self.snippet {
let _ = write!(out, "\n {snippet}");
}
if let Some(help) = &self.help {
let _ = write!(out, "\n help: {help}");
}
out
}
}
#[must_use]
pub(crate) fn line_column(source: &str, offset: usize) -> (usize, usize) {
let offset = offset.min(source.len());
let mut line = 1usize;
let mut line_start = 0usize;
for (i, &b) in source.as_bytes()[..offset].iter().enumerate() {
if b == b'\n' {
line += 1;
line_start = i + 1;
}
}
let column = source[line_start..offset].chars().count() + 1;
(line, column)
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.severity.label(), self.body())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn renders_full_error_compact() {
let diag = Diagnostic::error("invalid @pointerdown handler")
.component("mail-inbox-page")
.element("button")
.snippet(r#"@pointerdown="e.preventDefault()""#)
.help(r#"use @pointerdown="{handler()}" to pass the event"#);
let expected = "error: invalid @pointerdown handler\n \
in component <mail-inbox-page> · element <button>\n \
@pointerdown=\"e.preventDefault()\"\n \
help: use @pointerdown=\"{handler()}\" to pass the event";
assert_eq!(diag.to_string(), expected);
}
#[test]
fn omits_optional_sections_when_absent() {
let diag = Diagnostic::warning("something minor").component("my-card");
assert_eq!(
diag.to_string(),
"warning: something minor\n in component <my-card>"
);
}
#[test]
fn element_only_location() {
let diag = Diagnostic::error("x").element("button");
assert_eq!(diag.to_string(), "error: x\n in element <button>");
}
#[test]
fn getters_expose_structured_fields() {
let diag = Diagnostic::error("boom")
.component("c")
.element("e")
.snippet("snip")
.help("do this");
assert_eq!(diag.severity(), Severity::Error);
assert_eq!(diag.title(), "boom");
assert_eq!(diag.component_name(), Some("c"));
assert_eq!(diag.element_name(), Some("e"));
assert_eq!(diag.snippet_text(), Some("snip"));
assert_eq!(diag.help_text(), Some("do this"));
assert_eq!(
diag.location().as_deref(),
Some("in component <c> · element <e>")
);
}
#[test]
fn position_renders_rustc_style_location() {
let diag = Diagnostic::error("boom")
.component("index.html")
.element("for")
.position(3, 7);
assert_eq!(diag.position_line_column(), Some((3, 7)));
assert_eq!(diag.location().as_deref(), Some("--> index.html:3:7"));
assert!(diag.to_string().contains("--> index.html:3:7"));
}
#[test]
fn bare_position_renders_without_component_or_element() {
let diag = Diagnostic::error("boom").position(5, 1);
assert_eq!(diag.location().as_deref(), Some("--> 5:1"));
}
#[test]
fn line_column_counts_lines_and_chars() {
let src = "<div>\n <for each=\"x\">\n</div>";
let offset = src.find("<for").unwrap();
assert_eq!(line_column(src, offset), (2, 3));
assert_eq!(line_column(src, 0), (1, 1));
}
#[test]
fn line_column_uses_char_columns_for_multibyte() {
let src = "📚x";
let offset = src.find('x').unwrap();
assert_eq!(line_column(src, offset), (1, 2));
}
}