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    import_attributes: Vec<ImportAttributes<'s>>,
310    composes_local_classes: Vec<&'s str>,
311    composes_names: Vec<&'s str>,
312    value_at_rule_import_items: Vec<ValueAtRuleImportItem<'s>>,
313}
314
315impl<'s> DependencyContext<'s> {
316    pub fn new() -> Self {
317        Self::default()
318    }
319
320    pub(crate) fn reserve_estimated_capacity(&mut self, input_len: usize, mode: Mode) {
321        let estimate = DependencyContextCapacity::estimate(input_len, mode);
322        reserve_estimated_capacity(&mut self.dependencies, estimate.dependencies);
323        reserve_estimated_capacity(
324            &mut self.import_attributes,
325            estimate.import_attributes as usize,
326        );
327        reserve_estimated_capacity(
328            &mut self.composes_local_classes,
329            estimate.composes_local_classes as usize,
330        );
331        reserve_estimated_capacity(&mut self.composes_names, estimate.composes_names as usize);
332        reserve_estimated_capacity(
333            &mut self.value_at_rule_import_items,
334            estimate.value_at_rule_import_items as usize,
335        );
336    }
337
338    pub fn len(&self) -> usize {
339        self.dependencies.len()
340    }
341
342    pub fn is_empty(&self) -> bool {
343        self.dependencies.is_empty()
344    }
345
346    pub fn get(&self, index: usize) -> Option<&Dependency<'s>> {
347        self.dependencies.get(index)
348    }
349
350    pub fn iter(&self) -> std::slice::Iter<'_, Dependency<'s>> {
351        self.dependencies.iter()
352    }
353
354    pub fn dependencies(&self) -> &[Dependency<'s>] {
355        &self.dependencies
356    }
357
358    pub fn import_attributes(
359        &self,
360        index: DependencyIndex<ImportAttributes<'s>>,
361    ) -> &ImportAttributes<'s> {
362        &self.import_attributes[index.as_usize()]
363    }
364
365    pub fn composes_local_classes(&self, range: DependencyListRange<&'s str>) -> &[&'s str] {
366        &self.composes_local_classes[range.as_usize_range()]
367    }
368
369    pub fn composes_names(&self, range: DependencyListRange<&'s str>) -> &[&'s str] {
370        &self.composes_names[range.as_usize_range()]
371    }
372
373    pub fn value_at_rule_import_items(&self) -> &[ValueAtRuleImportItem<'s>] {
374        &self.value_at_rule_import_items
375    }
376
377    pub(crate) fn push_dependency(&mut self, dependency: Dependency<'s>) {
378        self.dependencies.push(dependency);
379    }
380
381    pub(crate) fn push_import(
382        &mut self,
383        request: &'s str,
384        range: Range,
385        layer: Option<&'s str>,
386        supports: Option<&'s str>,
387        media: Option<&'s str>,
388    ) {
389        let attributes = DependencyIndex::from_index(self.import_attributes.len());
390        self.import_attributes
391            .push(ImportAttributes::new(layer, supports, media));
392        self.dependencies.push(Dependency::Import {
393            request,
394            range,
395            attributes,
396        });
397    }
398
399    pub(crate) fn push_value_at_rule_import_item(&mut self, item: ValueAtRuleImportItem<'s>) {
400        self.value_at_rule_import_items.push(item);
401    }
402
403    pub(crate) fn value_at_rule_import_item(&self, index: usize) -> ValueAtRuleImportItem<'s> {
404        self.value_at_rule_import_items[index]
405    }
406
407    pub(crate) fn value_at_rule_import_items_checkpoint(&self) -> usize {
408        self.value_at_rule_import_items.len()
409    }
410
411    pub(crate) fn truncate_value_at_rule_import_items(&mut self, checkpoint: usize) {
412        self.value_at_rule_import_items.truncate(checkpoint);
413    }
414
415    pub(crate) fn finish_value_at_rule_import_items(
416        &self,
417        checkpoint: usize,
418    ) -> DependencyListRange<ValueAtRuleImportItem<'s>> {
419        debug_assert!(checkpoint <= self.value_at_rule_import_items.len());
420        DependencyListRange::from_bounds(checkpoint, self.value_at_rule_import_items.len())
421    }
422
423    pub(crate) fn push_composes(
424        &mut self,
425        local_classes: impl IntoIterator<Item = &'s str>,
426        names: impl IntoIterator<Item = &'s str>,
427        from: Option<&'s str>,
428        range: Range,
429    ) {
430        let local_classes_start = self.composes_local_classes.len();
431        self.composes_local_classes.extend(local_classes);
432        let local_classes = DependencyListRange::<&'s str>::from_bounds(
433            local_classes_start,
434            self.composes_local_classes.len(),
435        );
436
437        let names_start = self.composes_names.len();
438        self.composes_names.extend(names);
439        let names =
440            DependencyListRange::<&'s str>::from_bounds(names_start, self.composes_names.len());
441
442        self.dependencies.push(Dependency::Composes {
443            local_classes,
444            names,
445            from,
446            range,
447        });
448    }
449}
450
451impl PartialEq for DependencyContext<'_> {
452    fn eq(&self, other: &Self) -> bool {
453        self.dependencies == other.dependencies
454            && self.import_attributes == other.import_attributes
455            && self.composes_local_classes == other.composes_local_classes
456            && self.composes_names == other.composes_names
457            && self.value_at_rule_import_items == other.value_at_rule_import_items
458    }
459}
460
461impl Eq for DependencyContext<'_> {}
462
463impl Hash for DependencyContext<'_> {
464    fn hash<H: Hasher>(&self, state: &mut H) {
465        self.dependencies.hash(state);
466        self.import_attributes.hash(state);
467        self.composes_local_classes.hash(state);
468        self.composes_names.hash(state);
469        self.value_at_rule_import_items.hash(state);
470    }
471}
472
473#[derive(Debug, Clone, Copy, Default)]
474struct DependencyContextCapacity {
475    dependencies: usize,
476    import_attributes: u16,
477    composes_local_classes: u16,
478    composes_names: u16,
479    value_at_rule_import_items: u16,
480}
481
482impl DependencyContextCapacity {
483    fn estimate(input_len: usize, mode: Mode) -> Self {
484        let dependency_denominator = match mode {
485            Mode::Local | Mode::Pure => 32,
486            Mode::Global => 80,
487            Mode::Css => 4096,
488        };
489        let dependencies = estimate_capacity(input_len, dependency_denominator, 4, 8192);
490        let import_attributes = estimate_capacity(input_len, 8192, 2, 1024);
491        let (composes_local_classes, composes_names, value_at_rule_import_items) = match mode {
492            Mode::Local | Mode::Pure => (
493                estimate_capacity(input_len, 640, 2, 4096),
494                estimate_capacity(input_len, 576, 2, 4096),
495                estimate_capacity(input_len, 1792, 2, 4096),
496            ),
497            Mode::Global => (
498                estimate_capacity(input_len, 3880, 2, 4096),
499                estimate_capacity(input_len, 3880, 2, 4096),
500                estimate_capacity(input_len, 1774, 2, 4096),
501            ),
502            Mode::Css => (0, 0, 0),
503        };
504        Self {
505            dependencies,
506            import_attributes: import_attributes as u16,
507            composes_local_classes: composes_local_classes as u16,
508            composes_names: composes_names as u16,
509            value_at_rule_import_items: value_at_rule_import_items as u16,
510        }
511    }
512}
513
514fn estimate_capacity(
515    input_len: usize,
516    denominator: usize,
517    minimum: usize,
518    maximum: usize,
519) -> usize {
520    input_len
521        .checked_div(denominator)
522        .unwrap_or(0)
523        .clamp(minimum, maximum)
524}
525
526fn reserve_estimated_capacity<T>(values: &mut Vec<T>, estimated_capacity: usize) {
527    if values.capacity() < estimated_capacity {
528        values.reserve(estimated_capacity.saturating_sub(values.len()));
529    }
530}
531
532impl<'s> Index<usize> for DependencyContext<'s> {
533    type Output = Dependency<'s>;
534
535    fn index(&self, index: usize) -> &Self::Output {
536        &self.dependencies[index]
537    }
538}
539
540impl<'context, 's> IntoIterator for &'context DependencyContext<'s> {
541    type Item = &'context Dependency<'s>;
542    type IntoIter = std::slice::Iter<'context, Dependency<'s>>;
543
544    fn into_iter(self) -> Self::IntoIter {
545        self.dependencies.iter()
546    }
547}
548
549#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
550pub enum UrlRangeKind {
551    Function,
552    String,
553}
554
555#[derive(Debug, Clone, Hash, PartialEq, Eq)]
556pub struct Warning<'s> {
557    pub(crate) range: Range,
558    pub(crate) kind: WarningKind<'s>,
559}
560
561impl<'s> Warning<'s> {
562    pub fn new(range: Range, kind: WarningKind<'s>) -> Self {
563        Self { range, kind }
564    }
565
566    pub fn range(&self) -> &Range {
567        &self.range
568    }
569
570    pub fn kind(&self) -> &WarningKind<'s> {
571        &self.kind
572    }
573}
574
575#[derive(Debug, Clone, Hash, PartialEq, Eq)]
576pub enum WarningKind<'s> {
577    Unexpected { message: &'s str },
578    DuplicateUrl { when: &'s str },
579    NamespaceNotSupportedInBundledCss,
580    NotPrecededAtImport,
581    ExpectedUrl { when: &'s str },
582    ExpectedUrlBefore { when: &'s str },
583    ExpectedLayerBefore { when: &'s str },
584    InconsistentModeResult,
585    ExpectedNotInside { pseudo: &'s str },
586    NotPure { message: &'s str },
587    UnexpectedComposition { message: &'s str },
588}
589
590impl Display for Warning<'_> {
591    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
592        match self.kind {
593            WarningKind::Unexpected { message, .. } => write!(f, "{message}"),
594            WarningKind::DuplicateUrl { when, .. } => {
595                write!(f, "Duplicate of 'url(...)' in '{when}'")
596            }
597            WarningKind::NamespaceNotSupportedInBundledCss { .. } => {
598                write!(f, "'@namespace' is not supported in bundled CSS")
599            }
600            WarningKind::NotPrecededAtImport { .. } => {
601                write!(f, "Any '@import' rules must precede all other rules")
602            }
603            WarningKind::ExpectedUrl { when, .. } => write!(f, "Expected URL in '{when}'"),
604            WarningKind::ExpectedUrlBefore { when, .. } => {
605                write!(
606                    f,
607                    "An URL in '{when}' should be before 'layer(...)' or 'supports(...)'"
608                )
609            }
610            WarningKind::ExpectedLayerBefore { when, .. } => {
611                write!(
612                    f,
613                    "The 'layer(...)' in '{when}' should be before 'supports(...)'"
614                )
615            }
616            WarningKind::InconsistentModeResult { .. } => write!(
617                f,
618                "Inconsistent rule global/local (multiple selectors must result in the same mode for the rule)"
619            ),
620            WarningKind::ExpectedNotInside { pseudo, .. } => write!(
621                f,
622                "A '{pseudo}' is not allowed inside of a ':local()' or ':global()'"
623            ),
624            WarningKind::NotPure { message, .. } => {
625                write!(f, "Pure globals is not allowed in pure mode, {message}")
626            }
627            WarningKind::UnexpectedComposition { message, .. } => {
628                write!(f, "Composition is {message}")
629            }
630        }
631    }
632}
633
634#[cfg(test)]
635#[path = "../tests/unit/dependency_types_tests.rs"]
636mod tests;