Skip to main content

syntect/parsing/
scope.rs

1// see DESIGN.md
2use std::cmp::{min, Ordering};
3use std::collections::HashMap;
4use std::fmt;
5use std::mem;
6use std::str::FromStr;
7use std::sync::{Mutex, MutexGuard};
8
9use serde::de::{Deserialize, Deserializer, Error, Visitor};
10use serde::ser::{Serialize, Serializer};
11use serde_derive::{Deserialize, Serialize};
12use std::sync::LazyLock;
13
14/// Scope related errors
15#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum ScopeError {
18    #[error("Tried to restore cleared scopes, but none were cleared")]
19    NoClearedScopesToRestore,
20}
21
22/// Multiplier on the power of 2 for MatchPower. This is only useful if you compute your own
23/// [`MatchPower`] scores
24///
25/// [`MatchPower`]: struct.MatchPower.html
26pub const ATOM_LEN_BITS: u16 = 3;
27
28/// The global scope repo, exposed in case you want to minimize locking and unlocking.
29///
30/// Ths shouldn't be necessary for you to use. See the [`ScopeRepository`] docs.
31///
32/// [`ScopeRepository`]: struct.ScopeRepository.html
33#[deprecated(
34    since = "5.3.0",
35    note = "\
36    Deprecated in anticipation of removal in the next semver-breaking release under the \
37    justification that it's incredibly niche functionality to expose. If you rely on this \
38    functionality then please express your particular use-case in the github issue: \
39    https://github.com/trishume/syntect/issues/575\
40    "
41)]
42pub static SCOPE_REPO: LazyLock<Mutex<ScopeRepository>> =
43    LazyLock::new(|| Mutex::new(ScopeRepository::new()));
44
45pub(crate) fn lock_global_scope_repo() -> MutexGuard<'static, ScopeRepository> {
46    #[allow(deprecated)]
47    SCOPE_REPO.lock().unwrap()
48}
49
50/// A hierarchy of atoms with semi-standardized names used to accord semantic information to a
51/// specific piece of text.
52///
53/// These are generally written with the atoms separated by dots, and - by convention - atoms are
54/// all lowercase alphanumeric.
55///
56/// Example scopes: `text.plain`, `punctuation.definition.string.begin.ruby`,
57/// `meta.function.parameters.rust`
58///
59/// `syntect` uses an optimized format for storing these that allows super fast comparison and
60/// determining if one scope is a prefix of another. It also always takes 16 bytes of space. It
61/// accomplishes this by using a global repository to store string values and using bit-packed 16
62/// bit numbers to represent and compare atoms. Like "atoms" or "symbols" in other languages. This
63/// means that while comparing and prefix are fast, extracting a string is relatively slower but
64/// ideally should be very rare.
65#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Default, Hash)]
66pub struct Scope {
67    a: u64,
68    b: u64,
69}
70
71/// Not all strings are valid scopes
72#[derive(Debug, thiserror::Error)]
73#[non_exhaustive]
74pub enum ParseScopeError {
75    /// Due to a limitation of the current optimized internal representation
76    /// scopes can be at most 8 atoms long
77    #[error("Too long scope. Scopes can be at most 8 atoms long.")]
78    TooLong,
79    /// The internal representation uses 16 bits per atom, so if all scopes ever
80    /// used by the program have more than 2^16-2 atoms, things break
81    #[error("Too many atoms. Max 2^16-2 atoms allowed.")]
82    TooManyAtoms,
83}
84
85/// The structure used to keep track of the mapping between scope atom numbers and their string
86/// names
87///
88/// It is only exposed in case you want to lock [`SCOPE_REPO`] and then allocate a bunch of scopes
89/// at once without thrashing the lock. In general, you should just use [`Scope::new()`].
90///
91/// Only [`Scope`]s created by the same repository have valid comparison results.
92///
93/// [`SCOPE_REPO`]: struct.SCOPE_REPO.html
94/// [`Scope::new()`]: struct.Scope.html#method.new
95/// [`Scope`]: struct.Scope.html
96#[derive(Debug)]
97pub struct ScopeRepository {
98    atoms: Vec<String>,
99    atom_index_map: HashMap<String, usize>,
100}
101
102/// A stack/sequence of scopes for representing hierarchies for a given token of text
103///
104/// This is also used within [`ScopeSelectors`].
105///
106/// In Sublime Text, the scope stack at a given point can be seen by pressing `ctrl+shift+p`. Also
107/// see [the TextMate docs](https://manual.macromates.com/en/scope_selectors).
108///
109/// Example for a JS string inside a script tag in a Rails `ERB` file:
110/// `text.html.ruby text.html.basic source.js.embedded.html string.quoted.double.js`
111///
112/// [`ScopeSelectors`]: ../highlighting/struct.ScopeSelectors.html
113#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
114pub struct ScopeStack {
115    clear_stack: Vec<Vec<Scope>>,
116    pub scopes: Vec<Scope>,
117}
118
119#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
120pub enum ClearAmount {
121    TopN(usize),
122    All,
123}
124
125/// A change to a scope stack
126///
127/// Generally, `Noop` is only used internally and you won't need to worry about getting one back
128/// from calling a public function.
129///
130/// The change from a `ScopeStackOp` can be applied via [`ScopeStack::apply`].
131///
132/// [`ScopeStack::apply`]: struct.ScopeStack.html#method.apply
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub enum ScopeStackOp {
135    Push(Scope),
136    Pop(usize),
137    /// Used for the `clear_scopes` feature
138    Clear(ClearAmount),
139    /// Restores cleared scopes
140    Restore,
141    Noop,
142}
143
144/// Used for [`ScopeStack::apply_with_hook`]
145///
146/// [`ScopeStack::apply_with_hook`]: struct.ScopeStack.html#method.apply_with_hook
147#[derive(Debug, Clone, PartialEq, Eq)]
148pub enum BasicScopeStackOp {
149    Push(Scope),
150    Pop,
151}
152
153fn pack_as_u16s(atoms: &[usize]) -> Result<Scope, ParseScopeError> {
154    let mut res = Scope { a: 0, b: 0 };
155
156    for (i, &n) in atoms.iter().enumerate() {
157        if n >= (u16::MAX as usize) - 2 {
158            return Err(ParseScopeError::TooManyAtoms);
159        }
160        let small = (n + 1) as u64; // +1 since we reserve 0 for unused
161
162        if i < 4 {
163            let shift = (3 - i) * 16;
164            res.a |= small << shift;
165        } else {
166            let shift = (7 - i) * 16;
167            res.b |= small << shift;
168        }
169    }
170    Ok(res)
171}
172
173impl ScopeRepository {
174    fn new() -> ScopeRepository {
175        ScopeRepository {
176            atoms: Vec::new(),
177            atom_index_map: HashMap::new(),
178        }
179    }
180
181    pub fn build(&mut self, s: &str) -> Result<Scope, ParseScopeError> {
182        if s.is_empty() {
183            return Ok(Scope { a: 0, b: 0 });
184        }
185        let parts: Vec<usize> = s
186            .trim_end_matches('.')
187            .split('.')
188            .map(|a| self.atom_to_index(a))
189            .collect();
190        // The internal representation supports at most 8 atoms (packed into
191        // two u64 fields). Scopes with more atoms are silently truncated to
192        // 8; the first 8 atoms still provide meaningful scope matching for
193        // any realistic selector depth.
194        let len = parts.len().min(8);
195        pack_as_u16s(&parts[..len])
196    }
197
198    pub fn to_string(&self, scope: Scope) -> String {
199        let mut s = String::new();
200        for i in 0..8 {
201            let atom_number = scope.atom_at(i);
202            // println!("atom {} of {:x}-{:x} = {:x}",
203            //     i, scope.a, scope.b, atom_number);
204            if atom_number == 0 {
205                break;
206            }
207            if i != 0 {
208                s.push('.');
209            }
210            s.push_str(self.atom_str(atom_number));
211        }
212        s
213    }
214
215    fn atom_to_index(&mut self, atom: &str) -> usize {
216        if let Some(index) = self.atom_index_map.get(atom) {
217            return *index;
218        }
219
220        self.atoms.push(atom.to_owned());
221        let index = self.atoms.len() - 1;
222        self.atom_index_map.insert(atom.to_owned(), index);
223
224        index
225    }
226
227    /// Return the string for an atom number returned by [`Scope::atom_at`]
228    ///
229    /// [`Scope::atom_at`]: struct.Scope.html#method.atom_at
230    pub fn atom_str(&self, atom_number: u16) -> &str {
231        &self.atoms[(atom_number - 1) as usize]
232    }
233}
234
235impl Scope {
236    /// Parses a `Scope` from a series of atoms separated by dot (`.`) characters
237    ///
238    /// Example: `Scope::new("meta.rails.controller")`
239    pub fn new(s: &str) -> Result<Scope, ParseScopeError> {
240        let mut repo = lock_global_scope_repo();
241        repo.build(s.trim())
242    }
243
244    /// Gets the atom number at a given index.
245    ///
246    /// I can't think of any reason you'd find this useful. It is used internally for turning a
247    /// scope back into a string.
248    pub fn atom_at(self, index: usize) -> u16 {
249        #[allow(clippy::panic)]
250        // The below panic is too much of an edge-case for it to be worth propagating
251        let shifted = if index < 4 {
252            self.a >> ((3 - index) * 16)
253        } else if index < 8 {
254            self.b >> ((7 - index) * 16)
255        } else {
256            panic!("atom index out of bounds {:?}", index);
257        };
258        (shifted & 0xFFFF) as u16
259    }
260
261    #[inline]
262    fn missing_atoms(self) -> u32 {
263        let trail = if self.b == 0 {
264            self.a.trailing_zeros() + 64
265        } else {
266            self.b.trailing_zeros()
267        };
268        trail / 16
269    }
270
271    /// Returns the number of atoms in the scope
272    #[inline(always)]
273    pub fn len(self) -> u32 {
274        8 - self.missing_atoms()
275    }
276
277    pub fn is_empty(self) -> bool {
278        self.len() == 0
279    }
280
281    /// Returns a string representation of this scope
282    ///
283    /// This requires locking a global repo and shouldn't be done frequently.
284    pub fn build_string(self) -> String {
285        let repo = lock_global_scope_repo();
286        repo.to_string(self)
287    }
288
289    /// Tests if this scope is a prefix of another scope. Note that the empty scope is always a
290    /// prefix.
291    ///
292    /// This operation uses bitwise operations and is very fast
293    /// # Examples
294    ///
295    /// ```
296    /// use syntect::parsing::Scope;
297    /// assert!( Scope::new("string").unwrap()
298    ///         .is_prefix_of(Scope::new("string.quoted").unwrap()));
299    /// assert!( Scope::new("string.quoted").unwrap()
300    ///         .is_prefix_of(Scope::new("string.quoted").unwrap()));
301    /// assert!( Scope::new("").unwrap()
302    ///         .is_prefix_of(Scope::new("meta.rails.controller").unwrap()));
303    /// assert!(!Scope::new("source.php").unwrap()
304    ///         .is_prefix_of(Scope::new("source").unwrap()));
305    /// assert!(!Scope::new("source.php").unwrap()
306    ///         .is_prefix_of(Scope::new("source.ruby").unwrap()));
307    /// assert!(!Scope::new("meta.php").unwrap()
308    ///         .is_prefix_of(Scope::new("source.php").unwrap()));
309    /// assert!(!Scope::new("meta.php").unwrap()
310    ///         .is_prefix_of(Scope::new("source.php.wow").unwrap()));
311    /// ```
312    pub fn is_prefix_of(self, s: Scope) -> bool {
313        let pref_missing = self.missing_atoms();
314
315        // TODO: test optimization - use checked shl and then mult carry flag as int by -1
316        let mask: (u64, u64) = if pref_missing == 8 {
317            (0, 0)
318        } else if pref_missing == 4 {
319            (u64::MAX, 0)
320        } else if pref_missing > 4 {
321            (u64::MAX << ((pref_missing - 4) * 16), 0)
322        } else {
323            (u64::MAX, u64::MAX << (pref_missing * 16))
324        };
325
326        // xor to find the difference
327        let ax = (self.a ^ s.a) & mask.0;
328        let bx = (self.b ^ s.b) & mask.1;
329        // println!("{:x}-{:x} is_pref {:x}-{:x}: missing {} mask {:x}-{:x} xor {:x}-{:x}",
330        //     self.a, self.b, s.a, s.b, pref_missing, mask.0, mask.1, ax, bx);
331
332        ax == 0 && bx == 0
333    }
334}
335
336impl FromStr for Scope {
337    type Err = ParseScopeError;
338
339    fn from_str(s: &str) -> Result<Scope, ParseScopeError> {
340        Scope::new(s)
341    }
342}
343
344impl fmt::Display for Scope {
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        let s = self.build_string();
347        write!(f, "{}", s)
348    }
349}
350
351impl fmt::Debug for Scope {
352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353        let s = self.build_string();
354        write!(f, "<{}>", s)
355    }
356}
357
358impl Serialize for Scope {
359    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
360    where
361        S: Serializer,
362    {
363        let s = self.build_string();
364        serializer.serialize_str(&s)
365    }
366}
367
368impl<'de> Deserialize<'de> for Scope {
369    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
370    where
371        D: Deserializer<'de>,
372    {
373        struct ScopeVisitor;
374
375        impl Visitor<'_> for ScopeVisitor {
376            type Value = Scope;
377
378            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
379                formatter.write_str("a string")
380            }
381
382            fn visit_str<E>(self, v: &str) -> Result<Scope, E>
383            where
384                E: Error,
385            {
386                Scope::new(v).map_err(|e| Error::custom(format!("Invalid scope: {:?}", e)))
387            }
388        }
389
390        deserializer.deserialize_str(ScopeVisitor)
391    }
392}
393
394/// Wrapper to get around the fact Rust `f64` doesn't implement `Ord` and there is no non-NaN
395/// float type
396#[derive(Debug, Copy, Clone, PartialEq)]
397pub struct MatchPower(pub f64);
398
399impl Eq for MatchPower {}
400
401impl Ord for MatchPower {
402    fn cmp(&self, other: &Self) -> Ordering {
403        self.0.partial_cmp(&other.0).unwrap()
404    }
405}
406
407impl PartialOrd for MatchPower {
408    fn partial_cmp(&self, other: &MatchPower) -> Option<Ordering> {
409        Some(self.cmp(other))
410    }
411}
412
413impl ScopeStack {
414    pub fn new() -> ScopeStack {
415        ScopeStack {
416            clear_stack: Vec::new(),
417            scopes: Vec::new(),
418        }
419    }
420
421    /// Note: creating a ScopeStack with this doesn't contain information
422    /// on what to do when `clear_scopes` contexts end.
423    pub fn from_vec(v: Vec<Scope>) -> ScopeStack {
424        ScopeStack {
425            clear_stack: Vec::new(),
426            scopes: v,
427        }
428    }
429
430    #[inline]
431    pub fn push(&mut self, s: Scope) {
432        self.scopes.push(s);
433    }
434
435    #[inline]
436    pub fn pop(&mut self) {
437        self.scopes.pop();
438    }
439
440    /// Modifies this stack according to the operation given
441    ///
442    /// Use this to create a stack from a `Vec` of changes given by the parser.
443    pub fn apply(&mut self, op: &ScopeStackOp) -> Result<(), ScopeError> {
444        self.apply_with_hook(op, |_, _| {})
445    }
446
447    /// Modifies this stack according to the operation given and calls the hook for each basic operation.
448    ///
449    /// Like [`apply`] but calls `hook` for every basic modification (as defined by
450    /// [`BasicScopeStackOp`]). Use this to do things only when the scope stack changes.
451    ///
452    /// [`apply`]: #method.apply
453    /// [`BasicScopeStackOp`]: enum.BasicScopeStackOp.html
454    #[inline]
455    pub fn apply_with_hook<F>(&mut self, op: &ScopeStackOp, mut hook: F) -> Result<(), ScopeError>
456    where
457        F: FnMut(BasicScopeStackOp, &[Scope]),
458    {
459        match *op {
460            ScopeStackOp::Push(scope) => {
461                self.scopes.push(scope);
462                hook(BasicScopeStackOp::Push(scope), self.as_slice());
463            }
464            ScopeStackOp::Pop(count) => {
465                for _ in 0..count {
466                    self.scopes.pop();
467                    hook(BasicScopeStackOp::Pop, self.as_slice());
468                }
469            }
470            ScopeStackOp::Clear(amount) => {
471                let cleared = match amount {
472                    ClearAmount::TopN(n) => {
473                        // don't try to clear more scopes than are on the stack
474                        let to_leave = self.scopes.len() - min(n, self.scopes.len());
475                        self.scopes.split_off(to_leave)
476                    }
477                    ClearAmount::All => {
478                        let mut cleared = Vec::new();
479                        mem::swap(&mut cleared, &mut self.scopes);
480                        cleared
481                    }
482                };
483                let clear_amount = cleared.len();
484                self.clear_stack.push(cleared);
485                for _ in 0..clear_amount {
486                    hook(BasicScopeStackOp::Pop, self.as_slice());
487                }
488            }
489            ScopeStackOp::Restore => match self.clear_stack.pop() {
490                Some(ref mut to_push) => {
491                    for s in to_push {
492                        self.scopes.push(*s);
493                        hook(BasicScopeStackOp::Push(*s), self.as_slice());
494                    }
495                }
496                None => return Err(ScopeError::NoClearedScopesToRestore),
497            },
498            ScopeStackOp::Noop => (),
499        }
500
501        Ok(())
502    }
503
504    /// Prints out each scope in the stack separated by spaces
505    /// and then a newline. Top of the stack at the end.
506    pub fn debug_print(&self, repo: &ScopeRepository) {
507        for s in &self.scopes {
508            print!("{} ", repo.to_string(*s));
509        }
510        println!();
511    }
512
513    /// Returns the bottom `n` elements of the stack.
514    ///
515    /// Equivalent to `&scopes[0..n]` on a `Vec`
516    pub fn bottom_n(&self, n: usize) -> &[Scope] {
517        &self.scopes[0..n]
518    }
519
520    /// Return a slice of the scopes in this stack
521    #[inline]
522    pub fn as_slice(&self) -> &[Scope] {
523        &self.scopes[..]
524    }
525
526    /// Return the height/length of this stack
527    #[inline]
528    pub fn len(&self) -> usize {
529        self.scopes.len()
530    }
531
532    #[inline]
533    pub fn is_empty(&self) -> bool {
534        self.len() == 0
535    }
536
537    /// Checks if this stack as a selector matches the given stack, returning the match score if so
538    ///
539    /// Higher match scores indicate stronger matches. Scores are ordered according to the rules
540    /// found at [https://manual.macromates.com/en/scope_selectors](https://manual.macromates.com/en/scope_selectors)
541    ///
542    /// It accomplishes this ordering through some floating point math ensuring deeper and longer
543    /// matches matter. Unfortunately it is only guaranteed to return perfectly accurate results up
544    /// to stack depths of 17, but it should be reasonably good even afterwards. TextMate has the
545    /// exact same limitation, dunno about Sublime Text.
546    ///
547    /// # Examples
548    /// ```
549    /// use syntect::parsing::{ScopeStack, MatchPower};
550    /// use std::str::FromStr;
551    /// assert_eq!(ScopeStack::from_str("a.b c e.f").unwrap()
552    ///     .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
553    ///     Some(MatchPower(0o212u64 as f64)));
554    /// assert_eq!(ScopeStack::from_str("a c.d.e").unwrap()
555    ///     .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
556    ///     None);
557    /// ```
558    pub fn does_match(&self, stack: &[Scope]) -> Option<MatchPower> {
559        let mut sel_index: usize = 0;
560        let mut score: f64 = 0.0;
561        for (i, scope) in stack.iter().enumerate() {
562            let sel_scope = self.scopes[sel_index];
563            if sel_scope.is_prefix_of(*scope) {
564                let len = sel_scope.len();
565                // equivalent to score |= len << (ATOM_LEN_BITS*i) on a large unsigned
566                score += f64::from(len) * f64::from(ATOM_LEN_BITS * (i as u16)).exp2();
567                sel_index += 1;
568                if sel_index >= self.scopes.len() {
569                    return Some(MatchPower(score));
570                }
571            }
572        }
573        None
574    }
575}
576
577impl FromStr for ScopeStack {
578    type Err = ParseScopeError;
579
580    /// Parses a scope stack from a whitespace separated list of scopes.
581    fn from_str(s: &str) -> Result<ScopeStack, ParseScopeError> {
582        let mut scopes = Vec::new();
583        for name in s.split_whitespace() {
584            scopes.push(Scope::from_str(name)?)
585        }
586        Ok(ScopeStack::from_vec(scopes))
587    }
588}
589
590impl fmt::Display for ScopeStack {
591    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
592        for s in &self.scopes {
593            write!(f, "{} ", s)?;
594        }
595        Ok(())
596    }
597}
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602
603    #[test]
604    fn misc() {
605        // use std::mem;
606        // use std::rc::{Rc};
607        // use scope::*;
608        // assert_eq!(8, mem::size_of::<Rc<Scope>>());
609        // assert_eq!(Scope::new("source.php"), Scope::new("source.php"));
610    }
611
612    #[test]
613    fn repo_works() {
614        let mut repo = ScopeRepository::new();
615        assert_eq!(
616            repo.build("source.php").unwrap(),
617            repo.build("source.php").unwrap()
618        );
619        assert_eq!(
620            repo.build("source.php.wow.hi.bob.troll.clock.5").unwrap(),
621            repo.build("source.php.wow.hi.bob.troll.clock.5").unwrap()
622        );
623        assert_eq!(repo.build("").unwrap(), repo.build("").unwrap());
624        let s1 = repo.build("").unwrap();
625        assert_eq!(repo.to_string(s1), "");
626        let s2 = repo.build("source.php.wow").unwrap();
627        assert_eq!(repo.to_string(s2), "source.php.wow");
628        assert!(repo.build("source.php").unwrap() != repo.build("source.perl").unwrap());
629        assert!(repo.build("source.php").unwrap() != repo.build("source.php.wagon").unwrap());
630        assert_eq!(
631            repo.build("comment.line.").unwrap(),
632            repo.build("comment.line").unwrap()
633        );
634    }
635
636    #[test]
637    fn global_repo_works() {
638        use std::str::FromStr;
639        assert_eq!(
640            Scope::new("source.php").unwrap(),
641            Scope::new("source.php").unwrap()
642        );
643        assert!(Scope::from_str("1.2.3.4.5.6.7.8").is_ok());
644        // Scopes with >8 atoms are silently truncated to 8 rather than
645        // rejected, so a 9-atom scope succeeds and compares equal to
646        // its 8-atom prefix.
647        let nine = Scope::from_str("1.2.3.4.5.6.7.8.9").unwrap();
648        let eight = Scope::from_str("1.2.3.4.5.6.7.8").unwrap();
649        assert_eq!(nine, eight);
650    }
651
652    #[test]
653    fn prefixes_work() {
654        assert!(Scope::new("1.2.3.4.5.6.7.8")
655            .unwrap()
656            .is_prefix_of(Scope::new("1.2.3.4.5.6.7.8").unwrap()));
657        assert!(Scope::new("1.2.3.4.5.6")
658            .unwrap()
659            .is_prefix_of(Scope::new("1.2.3.4.5.6.7.8").unwrap()));
660        assert!(Scope::new("1.2.3.4")
661            .unwrap()
662            .is_prefix_of(Scope::new("1.2.3.4.5.6.7.8").unwrap()));
663        assert!(!Scope::new("1.2.3.4.5.6.a")
664            .unwrap()
665            .is_prefix_of(Scope::new("1.2.3.4.5.6.7.8").unwrap()));
666        assert!(!Scope::new("1.2.a.4.5.6.7")
667            .unwrap()
668            .is_prefix_of(Scope::new("1.2.3.4.5.6.7.8").unwrap()));
669        assert!(!Scope::new("1.2.a.4.5.6.7")
670            .unwrap()
671            .is_prefix_of(Scope::new("1.2.3.4.5").unwrap()));
672        assert!(!Scope::new("1.2.a")
673            .unwrap()
674            .is_prefix_of(Scope::new("1.2.3.4.5.6.7.8").unwrap()));
675    }
676
677    #[test]
678    fn matching_works() {
679        use std::str::FromStr;
680        assert_eq!(
681            ScopeStack::from_str("string")
682                .unwrap()
683                .does_match(ScopeStack::from_str("string.quoted").unwrap().as_slice()),
684            Some(MatchPower(0o1u64 as f64))
685        );
686        assert_eq!(
687            ScopeStack::from_str("source")
688                .unwrap()
689                .does_match(ScopeStack::from_str("string.quoted").unwrap().as_slice()),
690            None
691        );
692        assert_eq!(
693            ScopeStack::from_str("a.b e.f")
694                .unwrap()
695                .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
696            Some(MatchPower(0o202u64 as f64))
697        );
698        assert_eq!(
699            ScopeStack::from_str("c e.f")
700                .unwrap()
701                .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
702            Some(MatchPower(0o210u64 as f64))
703        );
704        assert_eq!(
705            ScopeStack::from_str("c.d e.f")
706                .unwrap()
707                .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
708            Some(MatchPower(0o220u64 as f64))
709        );
710        assert_eq!(
711            ScopeStack::from_str("a.b c e.f")
712                .unwrap()
713                .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
714            Some(MatchPower(0o212u64 as f64))
715        );
716        assert_eq!(
717            ScopeStack::from_str("a c.d")
718                .unwrap()
719                .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
720            Some(MatchPower(0o021u64 as f64))
721        );
722        assert_eq!(
723            ScopeStack::from_str("a c.d.e")
724                .unwrap()
725                .does_match(ScopeStack::from_str("a.b c.d e.f.g").unwrap().as_slice()),
726            None
727        );
728    }
729}