Skip to content

Commit 331c8a9

Browse files
committed
Improve deserialization error messages
Fixes parcel-bundler#399
1 parent 856d460 commit 331c8a9

File tree

4 files changed

+116
-6
lines changed

4 files changed

+116
-6
lines changed

node/src/transformer.rs

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::ops::{Index, IndexMut};
1+
use std::{
2+
marker::PhantomData,
3+
ops::{Index, IndexMut},
4+
};
25

36
use lightningcss::{
47
media_query::MediaFeatureValue,
@@ -487,7 +490,7 @@ impl<'i> Visitor<'i, AtRule<'i>> for JsVisitor {
487490
.as_ref()
488491
.and_then(|v| self.env.get_reference_value_unchecked::<JsFunction>(v).ok())
489492
{
490-
map(&mut selectors.0, |value| {
493+
map::<_, _, _, true>(&mut selectors.0, |value| {
491494
let js_value = self.env.to_js_value(value)?;
492495
let res = visit.call(None, &[js_value])?;
493496
self.env.from_js_value(res).map(serde_detach::detach)
@@ -664,7 +667,7 @@ fn visit_list<
664667
})
665668
}
666669

667-
fn map<V, L: List<V>, F: FnMut(&mut V) -> napi::Result<Option<ValueOrVec<V>>>>(
670+
fn map<V, L: List<V>, F: FnMut(&mut V) -> napi::Result<Option<ValueOrVec<V, IS_VEC>>>, const IS_VEC: bool>(
668671
list: &mut L,
669672
mut f: F,
670673
) -> napi::Result<()> {
@@ -694,13 +697,70 @@ fn map<V, L: List<V>, F: FnMut(&mut V) -> napi::Result<Option<ValueOrVec<V>>>>(
694697
Ok(())
695698
}
696699

697-
#[derive(serde::Serialize, serde::Deserialize)]
700+
#[derive(serde::Serialize)]
698701
#[serde(untagged)]
699-
enum ValueOrVec<V> {
702+
enum ValueOrVec<V, const IS_VEC: bool = false> {
700703
Value(V),
701704
Vec(Vec<V>),
702705
}
703706

707+
// Manually implemented deserialize for better error messages.
708+
// https://github.com/serde-rs/serde/issues/773
709+
impl<'de, V: serde::Deserialize<'de>, const IS_VEC: bool> serde::Deserialize<'de> for ValueOrVec<V, IS_VEC> {
710+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
711+
where
712+
D: serde::Deserializer<'de>,
713+
{
714+
use serde::Deserializer;
715+
let content = serde::__private::de::Content::deserialize(deserializer)?;
716+
let de: serde::__private::de::ContentRefDeserializer<D::Error> =
717+
serde::__private::de::ContentRefDeserializer::new(&content);
718+
719+
// Try to deserialize as a sequence first.
720+
let mut was_seq = false;
721+
let res = de.deserialize_seq(SeqVisitor {
722+
was_seq: &mut was_seq,
723+
phantom: PhantomData,
724+
});
725+
726+
if was_seq {
727+
// Allow fallback if we know the value is also a list (e.g. selector).
728+
if res.is_ok() || !IS_VEC {
729+
return res.map(ValueOrVec::Vec);
730+
}
731+
}
732+
733+
// If it wasn't a sequence, try a value.
734+
let de = serde::__private::de::ContentRefDeserializer::new(&content);
735+
return V::deserialize(de).map(ValueOrVec::Value);
736+
737+
struct SeqVisitor<'a, V> {
738+
was_seq: &'a mut bool,
739+
phantom: PhantomData<V>,
740+
}
741+
742+
impl<'a, 'de, V: serde::Deserialize<'de>> serde::de::Visitor<'de> for SeqVisitor<'a, V> {
743+
type Value = Vec<V>;
744+
745+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
746+
formatter.write_str("a sequence")
747+
}
748+
749+
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
750+
where
751+
A: serde::de::SeqAccess<'de>,
752+
{
753+
*self.was_seq = true;
754+
let mut vec = Vec::with_capacity(seq.size_hint().unwrap_or(1));
755+
while let Some(v) = seq.next_element()? {
756+
vec.push(v);
757+
}
758+
Ok(vec)
759+
}
760+
}
761+
}
762+
}
763+
704764
trait List<V>: Index<usize, Output = V> + IndexMut<usize, Output = V> {
705765
fn len(&self) -> usize;
706766
fn remove(&mut self, i: usize);

node/test/customAtRules.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,5 @@ test('bundler', () => {
274274

275275
assert.equal(res.code.toString(), '.foo{color:red}.foo.bar{color:#ff0}');
276276
});
277+
278+
test.run();

node/test/visitor.test.mjs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,4 +866,37 @@ test('returning string values', () => {
866866
assert.equal(res.code.toString(), '*{visibility:hidden;--custom:hi;background:#ff0;-moz-transition:test .2s;-webkit-animation:3s foo}');
867867
});
868868

869+
test('errors on invalid dashed idents', () => {
870+
assert.throws(() => {
871+
transform({
872+
filename: 'test.css',
873+
minify: true,
874+
code: Buffer.from(`
875+
.foo {
876+
background: opacity(abcdef);
877+
}
878+
`),
879+
visitor: {
880+
Function(fn) {
881+
if (fn.arguments[0].type === 'token' && fn.arguments[0].value.type === 'ident') {
882+
fn.arguments = [
883+
{
884+
type: 'var',
885+
value: {
886+
name: { ident: fn.arguments[0].value.value }
887+
}
888+
}
889+
];
890+
}
891+
892+
return {
893+
type: 'function',
894+
value: fn
895+
}
896+
}
897+
}
898+
})
899+
}, 'Dashed idents must start with --');
900+
});
901+
869902
test.run();

src/values/ident.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ pub type CustomIdentList<'i> = SmallVec<[CustomIdent<'i>; 1]>;
6464
#[cfg_attr(feature = "visitor", derive(Visit))]
6565
#[cfg_attr(feature = "into_owned", derive(lightningcss_derive::IntoOwned))]
6666
#[cfg_attr(feature = "visitor", visit(visit_dashed_ident, DASHED_IDENTS))]
67-
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
67+
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
6868
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
6969
pub struct DashedIdent<'i>(#[cfg_attr(feature = "serde", serde(borrow))] pub CowArcStr<'i>);
7070

@@ -89,6 +89,21 @@ impl<'i> ToCss for DashedIdent<'i> {
8989
}
9090
}
9191

92+
#[cfg(feature = "serde")]
93+
impl<'i, 'de: 'i> serde::Deserialize<'de> for DashedIdent<'i> {
94+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95+
where
96+
D: serde::Deserializer<'de>,
97+
{
98+
let ident = CowArcStr::deserialize(deserializer)?;
99+
if !ident.starts_with("--") {
100+
return Err(serde::de::Error::custom("Dashed idents must start with --"));
101+
}
102+
103+
Ok(DashedIdent(ident))
104+
}
105+
}
106+
92107
/// A CSS [`<dashed-ident>`](https://www.w3.org/TR/css-values-4/#dashed-idents) reference.
93108
///
94109
/// Dashed idents are used in cases where an identifier can be either author defined _or_ CSS-defined.

0 commit comments

Comments
 (0)