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    /// Whether `from` is the unquoted `global` keyword rather than a request.
222    from_is_global: bool,
223  },
224  LocalVarDecl {
225    name: &'s str,
226    range: Range,
227  },
228  LocalPropertyDecl {
229    name: &'s str,
230    range: Range,
231  },
232  LocalKeyframes {
233    name: &'s str,
234    range: Range,
235  },
236  LocalKeyframesDecl {
237    name: &'s str,
238    range: Range,
239  },
240  LocalCounterStyle {
241    name: &'s str,
242    range: Range,
243  },
244  LocalCounterStyleDecl {
245    name: &'s str,
246    range: Range,
247  },
248  LocalFontPalette {
249    name: &'s str,
250    range: Range,
251  },
252  LocalFontPaletteDecl {
253    name: &'s str,
254    range: Range,
255  },
256  LocalContainer {
257    name: &'s str,
258    range: Range,
259  },
260  LocalContainerDecl {
261    name: &'s str,
262    range: Range,
263  },
264  LocalFunction {
265    name: &'s str,
266    range: Range,
267  },
268  LocalFunctionDecl {
269    name: &'s str,
270    range: Range,
271  },
272  LocalGrid {
273    name: &'s str,
274    range: Range,
275  },
276  LocalGridDecl {
277    name: &'s str,
278    range: Range,
279  },
280  Composes {
281    local_classes: DependencyListRange<&'s str>,
282    names: DependencyListRange<&'s str>,
283    from: Option<&'s str>,
284    /// Whether `from` is the unquoted `global` keyword rather than a request.
285    from_is_global: bool,
286    range: Range,
287  },
288  ICSSImportFrom {
289    path: &'s str,
290  },
291  ICSSImportValue {
292    prop: &'s str,
293    value: &'s str,
294  },
295  ICSSExportValue {
296    prop: &'s str,
297    value: &'s str,
298  },
299  ICSSSymbol {
300    name: &'s str,
301    range: Range,
302  },
303}
304
305/// Owns dependencies and the side-table payloads referenced by them.
306///
307/// `Dependency` values are intentionally cheap to move. Rare or variable-length
308/// data is appended to context vectors and referenced through [`DependencyIndex`]
309/// or [`DependencyListRange`]. Consumers should resolve those handles through
310/// this type rather than retaining raw indices independently.
311#[derive(Debug, Clone, Default)]
312pub struct DependencyContext<'s> {
313  dependencies: Vec<Dependency<'s>>,
314  dashed_ident_occurrences: Vec<Range>,
315  import_attributes: Vec<ImportAttributes<'s>>,
316  composes_local_classes: Vec<&'s str>,
317  composes_names: Vec<&'s str>,
318  value_at_rule_import_items: Vec<ValueAtRuleImportItem<'s>>,
319}
320
321impl<'s> DependencyContext<'s> {
322  pub fn new() -> Self {
323    Self::default()
324  }
325
326  pub(crate) fn reserve_estimated_capacity(&mut self, input_len: usize, mode: Mode) {
327    let estimate = DependencyContextCapacity::estimate(input_len, mode);
328    reserve_estimated_capacity(&mut self.dependencies, estimate.dependencies);
329    reserve_estimated_capacity(
330      &mut self.import_attributes,
331      estimate.import_attributes as usize,
332    );
333    reserve_estimated_capacity(
334      &mut self.composes_local_classes,
335      estimate.composes_local_classes as usize,
336    );
337    reserve_estimated_capacity(&mut self.composes_names, estimate.composes_names as usize);
338    reserve_estimated_capacity(
339      &mut self.value_at_rule_import_items,
340      estimate.value_at_rule_import_items as usize,
341    );
342  }
343
344  pub fn len(&self) -> usize {
345    self.dependencies.len()
346  }
347
348  pub fn is_empty(&self) -> bool {
349    self.dependencies.is_empty()
350  }
351
352  pub fn get(&self, index: usize) -> Option<&Dependency<'s>> {
353    self.dependencies.get(index)
354  }
355
356  pub fn iter(&self) -> std::slice::Iter<'_, Dependency<'s>> {
357    self.dependencies.iter()
358  }
359
360  pub fn dependencies(&self) -> &[Dependency<'s>] {
361    &self.dependencies
362  }
363
364  pub fn dashed_ident_name_ranges(&self) -> &[Range] {
365    &self.dashed_ident_occurrences
366  }
367
368  pub(crate) fn set_dashed_ident_occurrences(&mut self, occurrences: Vec<Range>) {
369    self.dashed_ident_occurrences = occurrences;
370  }
371
372  pub(crate) fn estimated_dashed_ident_capacity(input_len: usize, mode: Mode) -> usize {
373    DependencyContextCapacity::estimate(input_len, mode).dashed_ident_occurrences
374  }
375
376  pub fn import_attributes(
377    &self,
378    index: DependencyIndex<ImportAttributes<'s>>,
379  ) -> &ImportAttributes<'s> {
380    &self.import_attributes[index.as_usize()]
381  }
382
383  pub fn composes_local_classes(&self, range: DependencyListRange<&'s str>) -> &[&'s str] {
384    &self.composes_local_classes[range.as_usize_range()]
385  }
386
387  pub fn composes_names(&self, range: DependencyListRange<&'s str>) -> &[&'s str] {
388    &self.composes_names[range.as_usize_range()]
389  }
390
391  pub fn value_at_rule_import_items(&self) -> &[ValueAtRuleImportItem<'s>] {
392    &self.value_at_rule_import_items
393  }
394
395  pub(crate) fn push_dependency(&mut self, dependency: Dependency<'s>) {
396    self.dependencies.push(dependency);
397  }
398
399  pub(crate) fn push_import(
400    &mut self,
401    request: &'s str,
402    range: Range,
403    layer: Option<&'s str>,
404    supports: Option<&'s str>,
405    media: Option<&'s str>,
406  ) {
407    let attributes = DependencyIndex::from_index(self.import_attributes.len());
408    self
409      .import_attributes
410      .push(ImportAttributes::new(layer, supports, media));
411    self.dependencies.push(Dependency::Import {
412      request,
413      range,
414      attributes,
415    });
416  }
417
418  pub(crate) fn push_value_at_rule_import_item(&mut self, item: ValueAtRuleImportItem<'s>) {
419    self.value_at_rule_import_items.push(item);
420  }
421
422  pub(crate) fn value_at_rule_import_item(&self, index: usize) -> ValueAtRuleImportItem<'s> {
423    self.value_at_rule_import_items[index]
424  }
425
426  pub(crate) fn value_at_rule_import_items_checkpoint(&self) -> usize {
427    self.value_at_rule_import_items.len()
428  }
429
430  pub(crate) fn truncate_value_at_rule_import_items(&mut self, checkpoint: usize) {
431    self.value_at_rule_import_items.truncate(checkpoint);
432  }
433
434  pub(crate) fn finish_value_at_rule_import_items(
435    &self,
436    checkpoint: usize,
437  ) -> DependencyListRange<ValueAtRuleImportItem<'s>> {
438    debug_assert!(checkpoint <= self.value_at_rule_import_items.len());
439    DependencyListRange::from_bounds(checkpoint, self.value_at_rule_import_items.len())
440  }
441
442  pub(crate) fn push_composes(
443    &mut self,
444    local_classes: impl IntoIterator<Item = &'s str>,
445    names: impl IntoIterator<Item = &'s str>,
446    from: Option<&'s str>,
447    from_is_global: bool,
448    range: Range,
449  ) {
450    let local_classes_start = self.composes_local_classes.len();
451    self.composes_local_classes.extend(local_classes);
452    let local_classes = DependencyListRange::<&'s str>::from_bounds(
453      local_classes_start,
454      self.composes_local_classes.len(),
455    );
456
457    let names_start = self.composes_names.len();
458    self.composes_names.extend(names);
459    let names = DependencyListRange::<&'s str>::from_bounds(names_start, self.composes_names.len());
460
461    self.dependencies.push(Dependency::Composes {
462      local_classes,
463      names,
464      from,
465      from_is_global,
466      range,
467    });
468  }
469}
470
471impl PartialEq for DependencyContext<'_> {
472  fn eq(&self, other: &Self) -> bool {
473    self.dependencies == other.dependencies
474      && self.dashed_ident_occurrences == other.dashed_ident_occurrences
475      && self.import_attributes == other.import_attributes
476      && self.composes_local_classes == other.composes_local_classes
477      && self.composes_names == other.composes_names
478      && self.value_at_rule_import_items == other.value_at_rule_import_items
479  }
480}
481
482impl Eq for DependencyContext<'_> {}
483
484impl Hash for DependencyContext<'_> {
485  fn hash<H: Hasher>(&self, state: &mut H) {
486    self.dependencies.hash(state);
487    self.dashed_ident_occurrences.hash(state);
488    self.import_attributes.hash(state);
489    self.composes_local_classes.hash(state);
490    self.composes_names.hash(state);
491    self.value_at_rule_import_items.hash(state);
492  }
493}
494
495#[derive(Debug, Clone, Copy, Default)]
496struct DependencyContextCapacity {
497  dependencies: usize,
498  dashed_ident_occurrences: usize,
499  import_attributes: u16,
500  composes_local_classes: u16,
501  composes_names: u16,
502  value_at_rule_import_items: u16,
503}
504
505impl DependencyContextCapacity {
506  fn estimate(input_len: usize, mode: Mode) -> Self {
507    let dependency_denominator = match mode {
508      Mode::Local | Mode::Pure => 32,
509      Mode::Global => 80,
510      Mode::Css => 4096,
511    };
512    let dependencies = estimate_capacity(input_len, dependency_denominator, 4, 8192);
513    let dashed_ident_occurrences = if mode != Mode::Css {
514      estimate_capacity(input_len, 64, 2, 8192)
515    } else {
516      0
517    };
518    let import_attributes = estimate_capacity(input_len, 8192, 2, 1024);
519    let (composes_local_classes, composes_names, value_at_rule_import_items) = match mode {
520      Mode::Local | Mode::Pure => (
521        estimate_capacity(input_len, 640, 2, 4096),
522        estimate_capacity(input_len, 576, 2, 4096),
523        estimate_capacity(input_len, 1792, 2, 4096),
524      ),
525      Mode::Global => (
526        estimate_capacity(input_len, 3880, 2, 4096),
527        estimate_capacity(input_len, 3880, 2, 4096),
528        estimate_capacity(input_len, 1774, 2, 4096),
529      ),
530      Mode::Css => (0, 0, 0),
531    };
532    Self {
533      dependencies,
534      dashed_ident_occurrences,
535      import_attributes: import_attributes as u16,
536      composes_local_classes: composes_local_classes as u16,
537      composes_names: composes_names as u16,
538      value_at_rule_import_items: value_at_rule_import_items as u16,
539    }
540  }
541}
542
543fn estimate_capacity(
544  input_len: usize,
545  denominator: usize,
546  minimum: usize,
547  maximum: usize,
548) -> usize {
549  input_len
550    .checked_div(denominator)
551    .unwrap_or(0)
552    .clamp(minimum, maximum)
553}
554
555fn reserve_estimated_capacity<T>(values: &mut Vec<T>, estimated_capacity: usize) {
556  if values.capacity() < estimated_capacity {
557    values.reserve(estimated_capacity.saturating_sub(values.len()));
558  }
559}
560
561impl<'s> Index<usize> for DependencyContext<'s> {
562  type Output = Dependency<'s>;
563
564  fn index(&self, index: usize) -> &Self::Output {
565    &self.dependencies[index]
566  }
567}
568
569impl<'context, 's> IntoIterator for &'context DependencyContext<'s> {
570  type Item = &'context Dependency<'s>;
571  type IntoIter = std::slice::Iter<'context, Dependency<'s>>;
572
573  fn into_iter(self) -> Self::IntoIter {
574    self.dependencies.iter()
575  }
576}
577
578#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
579pub enum UrlRangeKind {
580  Function,
581  String,
582}
583
584#[derive(Debug, Clone, Hash, PartialEq, Eq)]
585pub struct Warning<'s> {
586  pub(crate) range: Range,
587  pub(crate) kind: WarningKind<'s>,
588}
589
590impl<'s> Warning<'s> {
591  pub fn new(range: Range, kind: WarningKind<'s>) -> Self {
592    Self { range, kind }
593  }
594
595  pub fn range(&self) -> &Range {
596    &self.range
597  }
598
599  pub fn kind(&self) -> &WarningKind<'s> {
600    &self.kind
601  }
602}
603
604#[derive(Debug, Clone, Hash, PartialEq, Eq)]
605pub enum WarningKind<'s> {
606  Unexpected { message: &'s str },
607  DuplicateUrl { when: &'s str },
608  NamespaceNotSupportedInBundledCss,
609  NotPrecededAtImport,
610  ExpectedUrl { when: &'s str },
611  ExpectedUrlBefore { when: &'s str },
612  ExpectedLayerBefore { when: &'s str },
613  InconsistentModeResult,
614  ExpectedNotInside { pseudo: &'s str },
615  NotPure { message: &'s str },
616  UnexpectedComposition { message: &'s str },
617}
618
619impl Display for Warning<'_> {
620  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621    match self.kind {
622      WarningKind::Unexpected { message, .. } => write!(f, "{message}"),
623      WarningKind::DuplicateUrl { when, .. } => {
624        write!(f, "Duplicate of 'url(...)' in '{when}'")
625      }
626      WarningKind::NamespaceNotSupportedInBundledCss { .. } => {
627        write!(f, "'@namespace' is not supported in bundled CSS")
628      }
629      WarningKind::NotPrecededAtImport { .. } => {
630        write!(f, "Any '@import' rules must precede all other rules")
631      }
632      WarningKind::ExpectedUrl { when, .. } => write!(f, "Expected URL in '{when}'"),
633      WarningKind::ExpectedUrlBefore { when, .. } => {
634        write!(
635          f,
636          "An URL in '{when}' should be before 'layer(...)' or 'supports(...)'"
637        )
638      }
639      WarningKind::ExpectedLayerBefore { when, .. } => {
640        write!(
641          f,
642          "The 'layer(...)' in '{when}' should be before 'supports(...)'"
643        )
644      }
645      WarningKind::InconsistentModeResult { .. } => write!(
646        f,
647        "Inconsistent rule global/local (multiple selectors must result in the same mode for the rule)"
648      ),
649      WarningKind::ExpectedNotInside { pseudo, .. } => write!(
650        f,
651        "A '{pseudo}' is not allowed inside of a ':local()' or ':global()'"
652      ),
653      WarningKind::NotPure { message, .. } => {
654        write!(f, "Pure globals is not allowed in pure mode, {message}")
655      }
656      WarningKind::UnexpectedComposition { message, .. } => {
657        write!(f, "Composition is {message}")
658      }
659    }
660  }
661}
662
663#[cfg(test)]
664#[path = "../tests/dependency_types_tests.rs"]
665mod tests;