lightningcss/
error.rs

1//! Error types.
2
3use crate::properties::custom::Token;
4use crate::rules::Location;
5use crate::values::string::CowArcStr;
6use cssparser::{BasicParseErrorKind, ParseError, ParseErrorKind};
7use parcel_selectors::parser::SelectorParseErrorKind;
8#[cfg(any(feature = "serde", feature = "nodejs"))]
9use serde::Serialize;
10#[cfg(feature = "into_owned")]
11use static_self::IntoOwned;
12use std::fmt;
13
14/// An error with a source location.
15#[derive(Debug, PartialEq, Clone)]
16#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))]
17#[cfg_attr(any(feature = "serde"), derive(serde::Deserialize))]
18#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
19pub struct Error<T> {
20  /// The type of error that occurred.
21  pub kind: T,
22  /// The location where the error occurred.
23  pub loc: Option<ErrorLocation>,
24}
25
26impl<T: fmt::Display> fmt::Display for Error<T> {
27  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28    self.kind.fmt(f)?;
29    if let Some(loc) = &self.loc {
30      write!(f, " at {}", loc)?;
31    }
32    Ok(())
33  }
34}
35
36impl<T: fmt::Display + fmt::Debug> std::error::Error for Error<T> {}
37
38/// A line and column location within a source file.
39#[derive(Debug, PartialEq, Clone)]
40#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))]
41#[cfg_attr(any(feature = "serde"), derive(serde::Deserialize))]
42#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
43pub struct ErrorLocation {
44  /// The filename in which the error occurred.
45  pub filename: String,
46  /// The line number, starting from 0.
47  pub line: u32,
48  /// The column number, starting from 1.
49  pub column: u32,
50}
51
52impl ErrorLocation {
53  /// Create a new error location from a source location and filename.
54  pub fn new(loc: Location, filename: String) -> Self {
55    ErrorLocation {
56      filename,
57      line: loc.line,
58      column: loc.column,
59    }
60  }
61}
62
63impl fmt::Display for ErrorLocation {
64  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65    write!(f, "{}:{}:{}", self.filename, self.line, self.column)
66  }
67}
68
69/// A parser error.
70#[derive(Debug, PartialEq, Clone)]
71#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
72#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
73#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(tag = "type", content = "value"))]
74pub enum ParserError<'i> {
75  /// An at rule body was invalid.
76  AtRuleBodyInvalid,
77  /// An at rule prelude was invalid
78  AtRulePreludeInvalid,
79  /// An unknown or unsupported at rule was encountered.
80  AtRuleInvalid(CowArcStr<'i>),
81  /// Unexpectedly encountered the end of input data.
82  EndOfInput,
83  /// A declaration was invalid.
84  InvalidDeclaration,
85  /// A media query was invalid.
86  InvalidMediaQuery,
87  /// Invalid CSS nesting.
88  InvalidNesting,
89  /// The @nest rule is deprecated.
90  DeprecatedNestRule,
91  /// An invalid selector in an `@page` rule.
92  InvalidPageSelector,
93  /// An invalid value was encountered.
94  InvalidValue,
95  /// Invalid qualified rule.
96  QualifiedRuleInvalid,
97  /// A selector was invalid.
98  SelectorError(SelectorError<'i>),
99  /// An `@import` rule was encountered after any rule besides `@charset` or `@layer`.
100  UnexpectedImportRule,
101  /// A `@namespace` rule was encountered after any rules besides `@charset`, `@import`, or `@layer`.
102  UnexpectedNamespaceRule,
103  /// An unexpected token was encountered.
104  UnexpectedToken(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
105  /// Maximum nesting depth was reached.
106  MaximumNestingDepth,
107}
108
109impl<'i> fmt::Display for ParserError<'i> {
110  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111    use ParserError::*;
112    match self {
113      AtRuleBodyInvalid => write!(f, "Invalid @ rule body"),
114      AtRulePreludeInvalid => write!(f, "Invalid @ rule prelude"),
115      AtRuleInvalid(name) => write!(f, "Unknown at rule: @{}", name),
116      EndOfInput => write!(f, "Unexpected end of input"),
117      InvalidDeclaration => write!(f, "Invalid declaration"),
118      InvalidMediaQuery => write!(f, "Invalid media query"),
119      InvalidNesting => write!(f, "Invalid nesting"),
120      DeprecatedNestRule => write!(f, "The @nest rule is deprecated"),
121      InvalidPageSelector => write!(f, "Invalid page selector"),
122      InvalidValue => write!(f, "Invalid value"),
123      QualifiedRuleInvalid => write!(f, "Invalid qualified rule"),
124      SelectorError(s) => s.fmt(f),
125      UnexpectedImportRule => write!(
126        f,
127        "@import rules must precede all rules aside from @charset and @layer statements"
128      ),
129      UnexpectedNamespaceRule => write!(
130        f,
131        "@namespaces rules must precede all rules aside from @charset, @import, and @layer statements"
132      ),
133      UnexpectedToken(token) => write!(f, "Unexpected token {:?}", token),
134      MaximumNestingDepth => write!(f, "Overflowed the maximum nesting depth"),
135    }
136  }
137}
138
139impl<'i> Error<ParserError<'i>> {
140  /// Creates an error from a cssparser error.
141  pub fn from(err: ParseError<'i, ParserError<'i>>, filename: String) -> Error<ParserError<'i>> {
142    let kind = match err.kind {
143      ParseErrorKind::Basic(b) => match &b {
144        BasicParseErrorKind::UnexpectedToken(t) => ParserError::UnexpectedToken(t.into()),
145        BasicParseErrorKind::EndOfInput => ParserError::EndOfInput,
146        BasicParseErrorKind::AtRuleInvalid(a) => ParserError::AtRuleInvalid(a.into()),
147        BasicParseErrorKind::AtRuleBodyInvalid => ParserError::AtRuleBodyInvalid,
148        BasicParseErrorKind::QualifiedRuleInvalid => ParserError::QualifiedRuleInvalid,
149      },
150      ParseErrorKind::Custom(c) => c,
151    };
152
153    Error {
154      kind,
155      loc: Some(ErrorLocation {
156        filename,
157        line: err.location.line,
158        column: err.location.column,
159      }),
160    }
161  }
162
163  /// Consumes the value and returns an owned clone.
164  #[cfg(feature = "into_owned")]
165  pub fn into_owned<'x>(self) -> Error<ParserError<'static>> {
166    Error {
167      kind: self.kind.into_owned(),
168      loc: self.loc,
169    }
170  }
171}
172
173impl<'i> From<SelectorParseErrorKind<'i>> for ParserError<'i> {
174  fn from(err: SelectorParseErrorKind<'i>) -> ParserError<'i> {
175    ParserError::SelectorError(err.into())
176  }
177}
178
179impl<'i> ParserError<'i> {
180  #[deprecated(note = "use `ParserError::to_string()` or `fmt::Display` instead")]
181  #[allow(missing_docs)]
182  pub fn reason(&self) -> String {
183    self.to_string()
184  }
185}
186
187/// A selector parsing error.
188#[derive(Debug, PartialEq, Clone)]
189#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
190#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
191#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(tag = "type", content = "value"))]
192pub enum SelectorError<'i> {
193  /// An unexpected token was found in an attribute selector.
194  BadValueInAttr(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
195  /// An unexpected token was found in a class selector.
196  ClassNeedsIdent(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
197  /// A dangling combinator was found.
198  DanglingCombinator,
199  /// An empty selector.
200  EmptySelector,
201  /// A `|` was expected in an attribute selector.
202  ExpectedBarInAttr(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
203  /// A namespace was expected.
204  ExpectedNamespace(CowArcStr<'i>),
205  /// An unexpected token was encountered in a namespace.
206  ExplicitNamespaceUnexpectedToken(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
207  /// An invalid pseudo class was encountered after a pseudo element.
208  InvalidPseudoClassAfterPseudoElement,
209  /// An invalid pseudo class was encountered after a `-webkit-scrollbar` pseudo element.
210  InvalidPseudoClassAfterWebKitScrollbar,
211  /// A `-webkit-scrollbar` state was encountered before a `-webkit-scrollbar` pseudo element.
212  InvalidPseudoClassBeforeWebKitScrollbar,
213  /// Invalid qualified name in attribute selector.
214  InvalidQualNameInAttr(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
215  /// The current token is not allowed in this state.
216  InvalidState,
217  /// The selector is required to have the `&` nesting selector at the start.
218  MissingNestingPrefix,
219  /// The selector is missing a `&` nesting selector.
220  MissingNestingSelector,
221  /// No qualified name in attribute selector.
222  NoQualifiedNameInAttributeSelector(
223    #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>,
224  ),
225  /// An Invalid token was encountered in a pseudo element.
226  PseudoElementExpectedIdent(#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>),
227  /// An unexpected identifier was encountered.
228  UnexpectedIdent(CowArcStr<'i>),
229  /// An unexpected token was encountered inside an attribute selector.
230  UnexpectedTokenInAttributeSelector(
231    #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>,
232  ),
233  /// An unsupported pseudo class or pseudo element was encountered.
234  UnsupportedPseudoClassOrElement(CowArcStr<'i>),
235
236  /// Ambiguous CSS module class.
237  AmbiguousCssModuleClass(CowArcStr<'i>),
238
239  /// An unexpected token was encountered after a pseudo element.
240  UnexpectedSelectorAfterPseudoElement(
241    #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>,
242  ),
243}
244
245impl<'i> fmt::Display for SelectorError<'i> {
246  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247    use SelectorError::*;
248    match self {
249      InvalidState => write!(f, "Invalid state"),
250      BadValueInAttr(token) => write!(f, "Invalid value in attribute selector: {:?}", token),
251      ClassNeedsIdent(token) => write!(f, "Expected identifier in class selector, got {:?}", token),
252      DanglingCombinator => write!(f, "Invalid dangling combinator in selector"),
253      EmptySelector => write!(f, "Invalid empty selector"),
254      ExpectedBarInAttr(name) => write!(f, "Expected | in attribute, got {:?}", name),
255      ExpectedNamespace(name) => write!(f, "Expected namespace: {}", name),
256      ExplicitNamespaceUnexpectedToken(token) => write!(f, "Unexpected token in namespace selector: {:?}", token),
257      InvalidPseudoClassAfterPseudoElement => write!(f, "Invalid pseudo class after pseudo element, only user action pseudo classes (e.g. :hover, :active) are allowed"),
258      InvalidPseudoClassAfterWebKitScrollbar => write!(f, "Invalid pseudo class after ::-webkit-scrollbar pseudo element"),
259      InvalidPseudoClassBeforeWebKitScrollbar => write!(f, "Pseudo class must be prefixed by a ::-webkit-scrollbar pseudo element"),
260      InvalidQualNameInAttr(token) => write!(f, "Invalid qualified name in attribute selector: {:?}", token),
261      MissingNestingPrefix => write!(f, "A nested rule must start with a nesting selector (&) as prefix of each selector, or start with @nest"),
262      MissingNestingSelector => write!(f, "A nesting selector (&) is required in each selector of a @nest rule"),
263      NoQualifiedNameInAttributeSelector(token) => write!(f, "No qualified name in attribute selector: {:?}.", token),
264      PseudoElementExpectedIdent(token) => write!(f, "Invalid token in pseudo element: {:?}", token),
265      UnexpectedIdent(name) => write!(f, "Unexpected identifier: {}", name),
266      UnexpectedTokenInAttributeSelector(token) => write!(f, "Unexpected token in attribute selector: {:?}", token),
267      UnsupportedPseudoClassOrElement(name) => write!(f, "Unsupported pseudo class or element: {}", name),
268      AmbiguousCssModuleClass(_) => write!(f, "Ambiguous CSS module class not supported"),
269      UnexpectedSelectorAfterPseudoElement(token) => {
270        write!(
271          f,
272          "Pseudo-elements like '::before' or '::after' can't be followed by selectors like '{token:?}'"
273        )
274      },
275    }
276  }
277}
278
279impl<'i> From<SelectorParseErrorKind<'i>> for SelectorError<'i> {
280  fn from(err: SelectorParseErrorKind<'i>) -> Self {
281    match &err {
282      SelectorParseErrorKind::NoQualifiedNameInAttributeSelector(t) => {
283        SelectorError::NoQualifiedNameInAttributeSelector(t.into())
284      }
285      SelectorParseErrorKind::EmptySelector => SelectorError::EmptySelector,
286      SelectorParseErrorKind::DanglingCombinator => SelectorError::DanglingCombinator,
287      SelectorParseErrorKind::InvalidPseudoClassBeforeWebKitScrollbar => {
288        SelectorError::InvalidPseudoClassBeforeWebKitScrollbar
289      }
290      SelectorParseErrorKind::InvalidPseudoClassAfterWebKitScrollbar => {
291        SelectorError::InvalidPseudoClassAfterWebKitScrollbar
292      }
293      SelectorParseErrorKind::InvalidPseudoClassAfterPseudoElement => {
294        SelectorError::InvalidPseudoClassAfterPseudoElement
295      }
296      SelectorParseErrorKind::InvalidState => SelectorError::InvalidState,
297      SelectorParseErrorKind::MissingNestingSelector => SelectorError::MissingNestingSelector,
298      SelectorParseErrorKind::MissingNestingPrefix => SelectorError::MissingNestingPrefix,
299      SelectorParseErrorKind::UnexpectedTokenInAttributeSelector(t) => {
300        SelectorError::UnexpectedTokenInAttributeSelector(t.into())
301      }
302      SelectorParseErrorKind::PseudoElementExpectedIdent(t) => SelectorError::PseudoElementExpectedIdent(t.into()),
303      SelectorParseErrorKind::UnsupportedPseudoClassOrElement(t) => {
304        SelectorError::UnsupportedPseudoClassOrElement(t.into())
305      }
306      SelectorParseErrorKind::UnexpectedIdent(t) => SelectorError::UnexpectedIdent(t.into()),
307      SelectorParseErrorKind::ExpectedNamespace(t) => SelectorError::ExpectedNamespace(t.into()),
308      SelectorParseErrorKind::ExpectedBarInAttr(t) => SelectorError::ExpectedBarInAttr(t.into()),
309      SelectorParseErrorKind::BadValueInAttr(t) => SelectorError::BadValueInAttr(t.into()),
310      SelectorParseErrorKind::InvalidQualNameInAttr(t) => SelectorError::InvalidQualNameInAttr(t.into()),
311      SelectorParseErrorKind::ExplicitNamespaceUnexpectedToken(t) => {
312        SelectorError::ExplicitNamespaceUnexpectedToken(t.into())
313      }
314      SelectorParseErrorKind::ClassNeedsIdent(t) => SelectorError::ClassNeedsIdent(t.into()),
315      SelectorParseErrorKind::AmbiguousCssModuleClass(name) => SelectorError::AmbiguousCssModuleClass(name.into()),
316      SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(t) => {
317        SelectorError::UnexpectedSelectorAfterPseudoElement(t.into())
318      }
319    }
320  }
321}
322
323#[derive(Debug, PartialEq)]
324pub(crate) struct ErrorWithLocation<T> {
325  pub kind: T,
326  pub loc: Location,
327}
328
329impl<T: fmt::Display> fmt::Display for ErrorWithLocation<T> {
330  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331    self.kind.fmt(f)
332  }
333}
334
335impl<T: fmt::Display + fmt::Debug> std::error::Error for ErrorWithLocation<T> {}
336
337pub(crate) type MinifyError = ErrorWithLocation<MinifyErrorKind>;
338
339/// A transformation error.
340#[derive(Debug, PartialEq)]
341#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
342#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(tag = "type"))]
343pub enum MinifyErrorKind {
344  /// A circular `@custom-media` rule was detected.
345  CircularCustomMedia {
346    /// The name of the `@custom-media` rule that was referenced circularly.
347    name: String,
348  },
349  /// Attempted to reference a custom media rule that doesn't exist.
350  CustomMediaNotDefined {
351    /// The name of the `@custom-media` rule that was not defined.
352    name: String,
353  },
354  /// Boolean logic with media types in @custom-media rules is not supported.
355  UnsupportedCustomMediaBooleanLogic {
356    /// The source location of the `@custom-media` rule with unsupported boolean logic.
357    custom_media_loc: Location,
358  },
359  /// A CSS module selector did not contain at least one class or id selector.
360  ImpureCSSModuleSelector,
361}
362
363impl fmt::Display for MinifyErrorKind {
364  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
365    use MinifyErrorKind::*;
366    match self {
367      CircularCustomMedia { name } => write!(f, "Circular custom media query {} detected", name),
368      CustomMediaNotDefined { name } => write!(f, "Custom media query {} is not defined", name),
369      UnsupportedCustomMediaBooleanLogic { .. } => write!(
370        f,
371        "Boolean logic with media types in @custom-media rules is not supported by Lightning CSS"
372      ),
373      ImpureCSSModuleSelector => write!(
374        f,
375        "A selector in CSS modules should contain at least one class or ID selector"
376      ),
377    }
378  }
379}
380
381impl MinifyErrorKind {
382  #[deprecated(note = "use `MinifyErrorKind::to_string()` or `fmt::Display` instead")]
383  #[allow(missing_docs)]
384  pub fn reason(&self) -> String {
385    self.to_string()
386  }
387}
388
389/// A printer error.
390pub type PrinterError = Error<PrinterErrorKind>;
391
392/// A printer error type.
393#[derive(Debug, PartialEq)]
394#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))]
395#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(tag = "type"))]
396pub enum PrinterErrorKind {
397  /// An ambiguous relative `url()` was encountered in a custom property declaration.
398  AmbiguousUrlInCustomProperty {
399    /// The ambiguous URL.
400    url: String,
401  },
402  /// A [std::fmt::Error](std::fmt::Error) was encountered in the underlying destination.
403  FmtError,
404  /// The CSS modules `composes` property cannot be used within nested rules.
405  InvalidComposesNesting,
406  /// The CSS modules `composes` property cannot be used with a simple class selector.
407  InvalidComposesSelector,
408  /// The CSS modules pattern must end with `[local]` for use in CSS grid.
409  InvalidCssModulesPatternInGrid,
410}
411
412impl From<fmt::Error> for PrinterError {
413  fn from(_: fmt::Error) -> PrinterError {
414    PrinterError {
415      kind: PrinterErrorKind::FmtError,
416      loc: None,
417    }
418  }
419}
420
421impl fmt::Display for PrinterErrorKind {
422  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
423    use PrinterErrorKind::*;
424    match self {
425      AmbiguousUrlInCustomProperty { url } => write!(f, "Ambiguous url('{}') in custom property. Relative paths are resolved from the location the var() is used, not where the custom property is defined. Use an absolute URL instead", url),
426      FmtError => write!(f, "Printer error"),
427      InvalidComposesNesting => write!(f, "The `composes` property cannot be used within nested rules"),
428      InvalidComposesSelector => write!(f, "The `composes` property cannot be used with a simple class selector"),
429      InvalidCssModulesPatternInGrid => write!(f, "The CSS modules `pattern` config must end with `[local]` for use in CSS grid line names."),
430    }
431  }
432}
433
434impl PrinterErrorKind {
435  #[deprecated(note = "use `PrinterErrorKind::to_string()` or `fmt::Display` instead")]
436  #[allow(missing_docs)]
437  pub fn reason(&self) -> String {
438    self.to_string()
439  }
440}