Skip to main content

css_module_lexer/
dependency_types.rs

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