Skip to main content

css_module_lexer/
dependency_types.rs

1//! Public dependency result types and their typed side-table storage.
2
3use std::{
4  fmt::Display,
5  hash::{Hash, Hasher},
6  marker::PhantomData,
7  ops::Index,
8};
9
10use crate::lexer::Pos;
11
12#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
13pub struct Range {
14  pub start: Pos,
15  pub end: Pos,
16}
17
18impl Range {
19  pub fn new(start: Pos, end: Pos) -> Self {
20    Self { start, end }
21  }
22}
23
24/// A half-open index range into one of [`DependencyContext`]'s flat payload
25/// vectors. Keeping list payloads out of [`Dependency`] prevents rare, large
26/// variants from determining the size of every dependency value. `T` binds the
27/// range to its payload element type without adding runtime storage.
28#[derive(Debug, Hash, PartialEq, Eq)]
29pub struct DependencyListRange<T> {
30  start: u32,
31  end: u32,
32  marker: PhantomData<fn() -> T>,
33}
34
35impl<T> Copy for DependencyListRange<T> {}
36
37impl<T> Clone for DependencyListRange<T> {
38  fn clone(&self) -> Self {
39    *self
40  }
41}
42
43impl<T> DependencyListRange<T> {
44  pub(crate) fn from_bounds(start: usize, end: usize) -> Self {
45    assert!(start <= end, "dependency list range is reversed");
46    assert!(
47      u32::try_from(end).is_ok(),
48      "dependency list storage is too large"
49    );
50    Self {
51      start: start as u32,
52      end: end as u32,
53      marker: PhantomData,
54    }
55  }
56
57  pub(crate) fn as_usize_range(self) -> std::ops::Range<usize> {
58    self.start as usize..self.end as usize
59  }
60
61  pub fn start(self) -> u32 {
62    self.start
63  }
64
65  pub fn end(self) -> u32 {
66    self.end
67  }
68
69  pub fn len(self) -> usize {
70    (self.end - self.start) as usize
71  }
72
73  pub fn is_empty(self) -> bool {
74    self.start == self.end
75  }
76}
77
78/// A typed index into one of [`DependencyContext`]'s side tables.
79///
80/// `T` identifies the target table without adding runtime storage. Keeping
81/// rare payloads behind an index prevents them from determining the size of
82/// every [`Dependency`] value.
83#[derive(Debug, Hash, PartialEq, Eq)]
84pub struct DependencyIndex<T> {
85  index: u32,
86  marker: PhantomData<fn() -> T>,
87}
88
89impl<T> Copy for DependencyIndex<T> {}
90
91impl<T> Clone for DependencyIndex<T> {
92  fn clone(&self) -> Self {
93    *self
94  }
95}
96
97impl<T> DependencyIndex<T> {
98  pub(crate) fn from_index(index: usize) -> Self {
99    assert!(
100      u32::try_from(index).is_ok(),
101      "dependency side table is too large"
102    );
103    Self {
104      index: index as u32,
105      marker: PhantomData,
106    }
107  }
108
109  pub(crate) fn as_usize(self) -> usize {
110    self.index as usize
111  }
112
113  pub fn index(self) -> u32 {
114    self.index
115  }
116}
117
118#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
119pub enum Mode {
120  Local,
121  Global,
122  Pure,
123  Css,
124}
125
126#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
127pub struct ValueAtRuleImportItem<'s> {
128  local_name: &'s str,
129  import_name: &'s str,
130}
131
132impl<'s> ValueAtRuleImportItem<'s> {
133  pub(crate) fn new(local_name: &'s str, import_name: &'s str) -> Self {
134    Self {
135      local_name,
136      import_name,
137    }
138  }
139
140  pub fn local_name(&self) -> &'s str {
141    self.local_name
142  }
143
144  pub fn import_name(&self) -> &'s str {
145    self.import_name
146  }
147}
148
149#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
150pub struct ImportAttributes<'s> {
151  layer: Option<&'s str>,
152  supports: Option<&'s str>,
153  media: Option<&'s str>,
154}
155
156impl<'s> ImportAttributes<'s> {
157  pub(crate) fn new(
158    layer: Option<&'s str>,
159    supports: Option<&'s str>,
160    media: Option<&'s str>,
161  ) -> Self {
162    Self {
163      layer,
164      supports,
165      media,
166    }
167  }
168
169  pub fn layer(&self) -> Option<&'s str> {
170    self.layer
171  }
172
173  pub fn supports(&self) -> Option<&'s str> {
174    self.supports
175  }
176
177  pub fn media(&self) -> Option<&'s str> {
178    self.media
179  }
180}
181
182#[derive(Debug, Clone, Hash, PartialEq, Eq)]
183pub enum Dependency<'s> {
184  Url {
185    request: &'s str,
186    range: Range,
187    kind: UrlRangeKind,
188  },
189  Import {
190    request: &'s str,
191    range: Range,
192    attributes: DependencyIndex<ImportAttributes<'s>>,
193  },
194  ICSSImportUrl {
195    name: &'s str,
196    range: Range,
197    name_range: Range,
198  },
199  Replace {
200    content: &'s str,
201    range: Range,
202  },
203  Charset {
204    value: &'s str,
205    range: Range,
206  },
207  LocalClass {
208    name: &'s str,
209    range: Range,
210    explicit: bool,
211  },
212  LocalId {
213    name: &'s str,
214    range: Range,
215    explicit: bool,
216  },
217  LocalVar {
218    name: &'s str,
219    range: Range,
220    from: Option<&'s str>,
221  },
222  LocalVarDecl {
223    name: &'s str,
224    range: Range,
225  },
226  LocalPropertyDecl {
227    name: &'s str,
228    range: Range,
229  },
230  LocalKeyframes {
231    name: &'s str,
232    range: Range,
233  },
234  LocalKeyframesDecl {
235    name: &'s str,
236    range: Range,
237  },
238  LocalCounterStyle {
239    name: &'s str,
240    range: Range,
241  },
242  LocalCounterStyleDecl {
243    name: &'s str,
244    range: Range,
245  },
246  LocalFontPalette {
247    name: &'s str,
248    range: Range,
249  },
250  LocalFontPaletteDecl {
251    name: &'s str,
252    range: Range,
253  },
254  LocalContainer {
255    name: &'s str,
256    range: Range,
257  },
258  LocalContainerDecl {
259    name: &'s str,
260    range: Range,
261  },
262  LocalFunction {
263    name: &'s str,
264    range: Range,
265  },
266  LocalFunctionDecl {
267    name: &'s str,
268    range: Range,
269  },
270  LocalGrid {
271    name: &'s str,
272    range: Range,
273  },
274  LocalGridDecl {
275    name: &'s str,
276    range: Range,
277  },
278  Composes {
279    local_classes: DependencyListRange<&'s str>,
280    names: DependencyListRange<&'s str>,
281    from: Option<&'s str>,
282    range: Range,
283  },
284  ICSSImportFrom {
285    path: &'s str,
286  },
287  ICSSImportValue {
288    prop: &'s str,
289    value: &'s str,
290  },
291  ICSSExportValue {
292    prop: &'s str,
293    value: &'s str,
294  },
295  ICSSSymbol {
296    name: &'s str,
297    range: Range,
298  },
299}
300
301/// Owns dependencies and the side-table payloads referenced by them.
302///
303/// `Dependency` values are intentionally cheap to move. Rare or variable-length
304/// data is appended to context vectors and referenced through [`DependencyIndex`]
305/// or [`DependencyListRange`]. Consumers should resolve those handles through
306/// this type rather than retaining raw indices independently.
307#[derive(Debug, Clone, Default)]
308pub struct DependencyContext<'s> {
309  dependencies: Vec<Dependency<'s>>,
310  dashed_ident_occurrences: Vec<Range>,
311  import_attributes: Vec<ImportAttributes<'s>>,
312  composes_local_classes: Vec<&'s str>,
313  composes_names: Vec<&'s str>,
314  value_at_rule_import_items: Vec<ValueAtRuleImportItem<'s>>,
315}
316
317impl<'s> DependencyContext<'s> {
318  pub fn new() -> Self {
319    Self::default()
320  }
321
322  pub(crate) fn reserve_estimated_capacity(&mut self, input_len: usize, mode: Mode) {
323    let estimate = DependencyContextCapacity::estimate(input_len, mode);
324    reserve_estimated_capacity(&mut self.dependencies, estimate.dependencies);
325    reserve_estimated_capacity(
326      &mut self.import_attributes,
327      estimate.import_attributes as usize,
328    );
329    reserve_estimated_capacity(
330      &mut self.composes_local_classes,
331      estimate.composes_local_classes as usize,
332    );
333    reserve_estimated_capacity(&mut self.composes_names, estimate.composes_names as usize);
334    reserve_estimated_capacity(
335      &mut self.value_at_rule_import_items,
336      estimate.value_at_rule_import_items as usize,
337    );
338  }
339
340  pub fn len(&self) -> usize {
341    self.dependencies.len()
342  }
343
344  pub fn is_empty(&self) -> bool {
345    self.dependencies.is_empty()
346  }
347
348  pub fn get(&self, index: usize) -> Option<&Dependency<'s>> {
349    self.dependencies.get(index)
350  }
351
352  pub fn iter(&self) -> std::slice::Iter<'_, Dependency<'s>> {
353    self.dependencies.iter()
354  }
355
356  pub fn dependencies(&self) -> &[Dependency<'s>] {
357    &self.dependencies
358  }
359
360  pub fn dashed_ident_occurrences(&self) -> &[Range] {
361    &self.dashed_ident_occurrences
362  }
363
364  pub(crate) fn set_dashed_ident_occurrences(&mut self, occurrences: Vec<Range>) {
365    self.dashed_ident_occurrences = occurrences;
366  }
367
368  pub(crate) fn estimated_dashed_ident_capacity(input_len: usize, mode: Mode) -> usize {
369    DependencyContextCapacity::estimate(input_len, mode).dashed_ident_occurrences
370  }
371
372  pub fn import_attributes(
373    &self,
374    index: DependencyIndex<ImportAttributes<'s>>,
375  ) -> &ImportAttributes<'s> {
376    &self.import_attributes[index.as_usize()]
377  }
378
379  pub fn composes_local_classes(&self, range: DependencyListRange<&'s str>) -> &[&'s str] {
380    &self.composes_local_classes[range.as_usize_range()]
381  }
382
383  pub fn composes_names(&self, range: DependencyListRange<&'s str>) -> &[&'s str] {
384    &self.composes_names[range.as_usize_range()]
385  }
386
387  pub fn value_at_rule_import_items(&self) -> &[ValueAtRuleImportItem<'s>] {
388    &self.value_at_rule_import_items
389  }
390
391  pub(crate) fn push_dependency(&mut self, dependency: Dependency<'s>) {
392    self.dependencies.push(dependency);
393  }
394
395  pub(crate) fn push_import(
396    &mut self,
397    request: &'s str,
398    range: Range,
399    layer: Option<&'s str>,
400    supports: Option<&'s str>,
401    media: Option<&'s str>,
402  ) {
403    let attributes = DependencyIndex::from_index(self.import_attributes.len());
404    self
405      .import_attributes
406      .push(ImportAttributes::new(layer, supports, media));
407    self.dependencies.push(Dependency::Import {
408      request,
409      range,
410      attributes,
411    });
412  }
413
414  pub(crate) fn push_value_at_rule_import_item(&mut self, item: ValueAtRuleImportItem<'s>) {
415    self.value_at_rule_import_items.push(item);
416  }
417
418  pub(crate) fn value_at_rule_import_item(&self, index: usize) -> ValueAtRuleImportItem<'s> {
419    self.value_at_rule_import_items[index]
420  }
421
422  pub(crate) fn value_at_rule_import_items_checkpoint(&self) -> usize {
423    self.value_at_rule_import_items.len()
424  }
425
426  pub(crate) fn truncate_value_at_rule_import_items(&mut self, checkpoint: usize) {
427    self.value_at_rule_import_items.truncate(checkpoint);
428  }
429
430  pub(crate) fn finish_value_at_rule_import_items(
431    &self,
432    checkpoint: usize,
433  ) -> DependencyListRange<ValueAtRuleImportItem<'s>> {
434    debug_assert!(checkpoint <= self.value_at_rule_import_items.len());
435    DependencyListRange::from_bounds(checkpoint, self.value_at_rule_import_items.len())
436  }
437
438  pub(crate) fn push_composes(
439    &mut self,
440    local_classes: impl IntoIterator<Item = &'s str>,
441    names: impl IntoIterator<Item = &'s str>,
442    from: Option<&'s str>,
443    range: Range,
444  ) {
445    let local_classes_start = self.composes_local_classes.len();
446    self.composes_local_classes.extend(local_classes);
447    let local_classes = DependencyListRange::<&'s str>::from_bounds(
448      local_classes_start,
449      self.composes_local_classes.len(),
450    );
451
452    let names_start = self.composes_names.len();
453    self.composes_names.extend(names);
454    let names = DependencyListRange::<&'s str>::from_bounds(names_start, self.composes_names.len());
455
456    self.dependencies.push(Dependency::Composes {
457      local_classes,
458      names,
459      from,
460      range,
461    });
462  }
463}
464
465impl PartialEq for DependencyContext<'_> {
466  fn eq(&self, other: &Self) -> bool {
467    self.dependencies == other.dependencies
468      && self.dashed_ident_occurrences == other.dashed_ident_occurrences
469      && self.import_attributes == other.import_attributes
470      && self.composes_local_classes == other.composes_local_classes
471      && self.composes_names == other.composes_names
472      && self.value_at_rule_import_items == other.value_at_rule_import_items
473  }
474}
475
476impl Eq for DependencyContext<'_> {}
477
478impl Hash for DependencyContext<'_> {
479  fn hash<H: Hasher>(&self, state: &mut H) {
480    self.dependencies.hash(state);
481    self.dashed_ident_occurrences.hash(state);
482    self.import_attributes.hash(state);
483    self.composes_local_classes.hash(state);
484    self.composes_names.hash(state);
485    self.value_at_rule_import_items.hash(state);
486  }
487}
488
489#[derive(Debug, Clone, Copy, Default)]
490struct DependencyContextCapacity {
491  dependencies: usize,
492  dashed_ident_occurrences: usize,
493  import_attributes: u16,
494  composes_local_classes: u16,
495  composes_names: u16,
496  value_at_rule_import_items: u16,
497}
498
499impl DependencyContextCapacity {
500  fn estimate(input_len: usize, mode: Mode) -> Self {
501    let dependency_denominator = match mode {
502      Mode::Local | Mode::Pure => 32,
503      Mode::Global => 80,
504      Mode::Css => 4096,
505    };
506    let dependencies = estimate_capacity(input_len, dependency_denominator, 4, 8192);
507    let dashed_ident_occurrences = if mode != Mode::Css {
508      estimate_capacity(input_len, 64, 2, 8192)
509    } else {
510      0
511    };
512    let import_attributes = estimate_capacity(input_len, 8192, 2, 1024);
513    let (composes_local_classes, composes_names, value_at_rule_import_items) = match mode {
514      Mode::Local | Mode::Pure => (
515        estimate_capacity(input_len, 640, 2, 4096),
516        estimate_capacity(input_len, 576, 2, 4096),
517        estimate_capacity(input_len, 1792, 2, 4096),
518      ),
519      Mode::Global => (
520        estimate_capacity(input_len, 3880, 2, 4096),
521        estimate_capacity(input_len, 3880, 2, 4096),
522        estimate_capacity(input_len, 1774, 2, 4096),
523      ),
524      Mode::Css => (0, 0, 0),
525    };
526    Self {
527      dependencies,
528      dashed_ident_occurrences,
529      import_attributes: import_attributes as u16,
530      composes_local_classes: composes_local_classes as u16,
531      composes_names: composes_names as u16,
532      value_at_rule_import_items: value_at_rule_import_items as u16,
533    }
534  }
535}
536
537fn estimate_capacity(
538  input_len: usize,
539  denominator: usize,
540  minimum: usize,
541  maximum: usize,
542) -> usize {
543  input_len
544    .checked_div(denominator)
545    .unwrap_or(0)
546    .clamp(minimum, maximum)
547}
548
549fn reserve_estimated_capacity<T>(values: &mut Vec<T>, estimated_capacity: usize) {
550  if values.capacity() < estimated_capacity {
551    values.reserve(estimated_capacity.saturating_sub(values.len()));
552  }
553}
554
555impl<'s> Index<usize> for DependencyContext<'s> {
556  type Output = Dependency<'s>;
557
558  fn index(&self, index: usize) -> &Self::Output {
559    &self.dependencies[index]
560  }
561}
562
563impl<'context, 's> IntoIterator for &'context DependencyContext<'s> {
564  type Item = &'context Dependency<'s>;
565  type IntoIter = std::slice::Iter<'context, Dependency<'s>>;
566
567  fn into_iter(self) -> Self::IntoIter {
568    self.dependencies.iter()
569  }
570}
571
572#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
573pub enum UrlRangeKind {
574  Function,
575  String,
576}
577
578#[derive(Debug, Clone, Hash, PartialEq, Eq)]
579pub struct Warning<'s> {
580  pub(crate) range: Range,
581  pub(crate) kind: WarningKind<'s>,
582}
583
584impl<'s> Warning<'s> {
585  pub fn new(range: Range, kind: WarningKind<'s>) -> Self {
586    Self { range, kind }
587  }
588
589  pub fn range(&self) -> &Range {
590    &self.range
591  }
592
593  pub fn kind(&self) -> &WarningKind<'s> {
594    &self.kind
595  }
596}
597
598#[derive(Debug, Clone, Hash, PartialEq, Eq)]
599pub enum WarningKind<'s> {
600  Unexpected { message: &'s str },
601  DuplicateUrl { when: &'s str },
602  NamespaceNotSupportedInBundledCss,
603  NotPrecededAtImport,
604  ExpectedUrl { when: &'s str },
605  ExpectedUrlBefore { when: &'s str },
606  ExpectedLayerBefore { when: &'s str },
607  InconsistentModeResult,
608  ExpectedNotInside { pseudo: &'s str },
609  NotPure { message: &'s str },
610  UnexpectedComposition { message: &'s str },
611}
612
613impl Display for Warning<'_> {
614  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
615    match self.kind {
616      WarningKind::Unexpected { message, .. } => write!(f, "{message}"),
617      WarningKind::DuplicateUrl { when, .. } => {
618        write!(f, "Duplicate of 'url(...)' in '{when}'")
619      }
620      WarningKind::NamespaceNotSupportedInBundledCss { .. } => {
621        write!(f, "'@namespace' is not supported in bundled CSS")
622      }
623      WarningKind::NotPrecededAtImport { .. } => {
624        write!(f, "Any '@import' rules must precede all other rules")
625      }
626      WarningKind::ExpectedUrl { when, .. } => write!(f, "Expected URL in '{when}'"),
627      WarningKind::ExpectedUrlBefore { when, .. } => {
628        write!(
629          f,
630          "An URL in '{when}' should be before 'layer(...)' or 'supports(...)'"
631        )
632      }
633      WarningKind::ExpectedLayerBefore { when, .. } => {
634        write!(
635          f,
636          "The 'layer(...)' in '{when}' should be before 'supports(...)'"
637        )
638      }
639      WarningKind::InconsistentModeResult { .. } => write!(
640        f,
641        "Inconsistent rule global/local (multiple selectors must result in the same mode for the rule)"
642      ),
643      WarningKind::ExpectedNotInside { pseudo, .. } => write!(
644        f,
645        "A '{pseudo}' is not allowed inside of a ':local()' or ':global()'"
646      ),
647      WarningKind::NotPure { message, .. } => {
648        write!(f, "Pure globals is not allowed in pure mode, {message}")
649      }
650      WarningKind::UnexpectedComposition { message, .. } => {
651        write!(f, "Composition is {message}")
652      }
653    }
654  }
655}
656
657#[cfg(test)]
658#[path = "../tests/dependency_types_tests.rs"]
659mod tests;