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