1use crate::std::borrow::Borrow;
5use crate::std::fmt;
6use crate::std::hash::{Hash, Hasher};
7use crate::std::ops::Deref;
8use crate::{Result, WasmFeatures};
9use alloc::borrow::ToOwned;
10use alloc::string::{String, ToString};
11use semver::Version;
12
13#[derive(Debug, Eq)]
22#[repr(transparent)]
23pub struct KebabStr(str);
24
25impl KebabStr {
26 pub fn new<'a>(s: impl AsRef<str> + 'a) -> Option<&'a Self> {
30 let s = Self::new_unchecked(s);
31 if s.is_kebab_case() {
32 Some(s)
33 } else {
34 None
35 }
36 }
37
38 pub(crate) fn new_unchecked<'a>(s: impl AsRef<str> + 'a) -> &'a Self {
39 #[allow(unsafe_code)]
42 unsafe {
43 crate::std::mem::transmute::<_, &Self>(s.as_ref())
44 }
45 }
46
47 pub fn as_str(&self) -> &str {
49 &self.0
50 }
51
52 pub fn to_kebab_string(&self) -> KebabString {
54 KebabString(self.to_string())
55 }
56
57 fn is_kebab_case(&self) -> bool {
58 let mut lower = false;
59 let mut upper = false;
60 for c in self.chars() {
61 match c {
62 'a'..='z' if !lower && !upper => lower = true,
63 'A'..='Z' if !lower && !upper => upper = true,
64 'a'..='z' if lower => {}
65 'A'..='Z' if upper => {}
66 '0'..='9' if lower || upper => {}
67 '-' if lower || upper => {
68 lower = false;
69 upper = false;
70 }
71 _ => return false,
72 }
73 }
74
75 !self.is_empty() && !self.ends_with('-')
76 }
77}
78
79impl Deref for KebabStr {
80 type Target = str;
81
82 fn deref(&self) -> &str {
83 self.as_str()
84 }
85}
86
87impl PartialEq for KebabStr {
88 fn eq(&self, other: &Self) -> bool {
89 if self.len() != other.len() {
90 return false;
91 }
92
93 self.chars()
94 .zip(other.chars())
95 .all(|(a, b)| a.to_ascii_lowercase() == b.to_ascii_lowercase())
96 }
97}
98
99impl PartialEq<KebabString> for KebabStr {
100 fn eq(&self, other: &KebabString) -> bool {
101 self.eq(other.as_kebab_str())
102 }
103}
104
105impl Hash for KebabStr {
106 fn hash<H: Hasher>(&self, state: &mut H) {
107 self.len().hash(state);
108
109 for b in self.chars() {
110 b.to_ascii_lowercase().hash(state);
111 }
112 }
113}
114
115impl fmt::Display for KebabStr {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 (self as &str).fmt(f)
118 }
119}
120
121impl ToOwned for KebabStr {
122 type Owned = KebabString;
123
124 fn to_owned(&self) -> Self::Owned {
125 self.to_kebab_string()
126 }
127}
128
129#[derive(Debug, Clone, Eq)]
138pub struct KebabString(String);
139
140impl KebabString {
141 pub fn new(s: impl Into<String>) -> Option<Self> {
145 let s = s.into();
146 if KebabStr::new(&s).is_some() {
147 Some(Self(s))
148 } else {
149 None
150 }
151 }
152
153 pub fn as_str(&self) -> &str {
155 self.0.as_str()
156 }
157
158 pub fn as_kebab_str(&self) -> &KebabStr {
160 KebabStr::new_unchecked(self.as_str())
162 }
163}
164
165impl Deref for KebabString {
166 type Target = KebabStr;
167
168 fn deref(&self) -> &Self::Target {
169 self.as_kebab_str()
170 }
171}
172
173impl Borrow<KebabStr> for KebabString {
174 fn borrow(&self) -> &KebabStr {
175 self.as_kebab_str()
176 }
177}
178
179impl PartialEq for KebabString {
180 fn eq(&self, other: &Self) -> bool {
181 self.as_kebab_str().eq(other.as_kebab_str())
182 }
183}
184
185impl PartialEq<KebabStr> for KebabString {
186 fn eq(&self, other: &KebabStr) -> bool {
187 self.as_kebab_str().eq(other)
188 }
189}
190
191impl Hash for KebabString {
192 fn hash<H: Hasher>(&self, state: &mut H) {
193 self.as_kebab_str().hash(state)
194 }
195}
196
197impl fmt::Display for KebabString {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 self.as_kebab_str().fmt(f)
200 }
201}
202
203impl From<KebabString> for String {
204 fn from(s: KebabString) -> String {
205 s.0
206 }
207}
208
209#[derive(Clone)]
229pub struct ComponentName {
230 raw: String,
231 kind: ParsedComponentNameKind,
232}
233
234#[derive(Copy, Clone)]
235enum ParsedComponentNameKind {
236 Label,
237 Constructor,
238 Method,
239 Static,
240 Interface,
241 Dependency,
242 Url,
243 Hash,
244}
245
246#[derive(Debug, Clone)]
248pub enum ComponentNameKind<'a> {
249 Label(&'a KebabStr),
251 Constructor(&'a KebabStr),
253 #[allow(missing_docs)]
255 Method(ResourceFunc<'a>),
256 #[allow(missing_docs)]
258 Static(ResourceFunc<'a>),
259 #[allow(missing_docs)]
261 Interface(InterfaceName<'a>),
262 #[allow(missing_docs)]
264 Dependency(DependencyName<'a>),
265 #[allow(missing_docs)]
267 Url(UrlName<'a>),
268 #[allow(missing_docs)]
270 Hash(HashName<'a>),
271}
272
273const CONSTRUCTOR: &str = "[constructor]";
274const METHOD: &str = "[method]";
275const STATIC: &str = "[static]";
276
277impl ComponentName {
278 pub fn new(name: &str, offset: usize) -> Result<ComponentName> {
281 Self::new_with_features(
282 name,
283 offset,
284 WasmFeatures {
285 component_model: true,
286 ..Default::default()
287 },
288 )
289 }
290
291 pub fn new_with_features(name: &str, offset: usize, features: WasmFeatures) -> Result<Self> {
297 let mut parser = ComponentNameParser {
298 next: name,
299 offset,
300 features,
301 };
302 let kind = parser.parse()?;
303 if !parser.next.is_empty() {
304 bail!(offset, "trailing characters found: `{}`", parser.next);
305 }
306 Ok(ComponentName {
307 raw: name.to_string(),
308 kind,
309 })
310 }
311
312 pub fn kind(&self) -> ComponentNameKind<'_> {
314 use ComponentNameKind::*;
315 use ParsedComponentNameKind as PK;
316 match self.kind {
317 PK::Label => Label(KebabStr::new_unchecked(&self.raw)),
318 PK::Constructor => Constructor(KebabStr::new_unchecked(&self.raw[CONSTRUCTOR.len()..])),
319 PK::Method => Method(ResourceFunc(&self.raw[METHOD.len()..])),
320 PK::Static => Static(ResourceFunc(&self.raw[STATIC.len()..])),
321 PK::Interface => Interface(InterfaceName(&self.raw)),
322 PK::Dependency => Dependency(DependencyName(&self.raw)),
323 PK::Url => Url(UrlName(&self.raw)),
324 PK::Hash => Hash(HashName(&self.raw)),
325 }
326 }
327
328 pub fn as_str(&self) -> &str {
330 &self.raw
331 }
332}
333
334impl From<ComponentName> for String {
335 fn from(name: ComponentName) -> String {
336 name.raw
337 }
338}
339
340impl Hash for ComponentName {
341 fn hash<H: Hasher>(&self, hasher: &mut H) {
342 self.kind().hash(hasher)
343 }
344}
345
346impl PartialEq for ComponentName {
347 fn eq(&self, other: &ComponentName) -> bool {
348 self.kind().eq(&other.kind())
349 }
350}
351
352impl Eq for ComponentName {}
353
354impl fmt::Display for ComponentName {
355 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
356 self.raw.fmt(f)
357 }
358}
359
360impl fmt::Debug for ComponentName {
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 self.raw.fmt(f)
363 }
364}
365
366impl Hash for ComponentNameKind<'_> {
367 fn hash<H: Hasher>(&self, hasher: &mut H) {
368 use ComponentNameKind::*;
369 match self {
370 Label(name) => (0u8, name).hash(hasher),
371 Constructor(name) => (1u8, name).hash(hasher),
372 Method(name) | Static(name) => (2u8, name).hash(hasher),
374 Interface(name) => (3u8, name).hash(hasher),
375 Dependency(name) => (4u8, name).hash(hasher),
376 Url(name) => (5u8, name).hash(hasher),
377 Hash(name) => (6u8, name).hash(hasher),
378 }
379 }
380}
381
382impl PartialEq for ComponentNameKind<'_> {
383 fn eq(&self, other: &ComponentNameKind<'_>) -> bool {
384 use ComponentNameKind::*;
385 match (self, other) {
386 (Label(a), Label(b)) => a == b,
387 (Label(_), _) => false,
388 (Constructor(a), Constructor(b)) => a == b,
389 (Constructor(_), _) => false,
390
391 (Method(a), Method(b))
394 | (Static(a), Static(b))
395 | (Method(a), Static(b))
396 | (Static(a), Method(b)) => a == b,
397
398 (Method(_), _) => false,
399 (Static(_), _) => false,
400
401 (Interface(a), Interface(b)) => a == b,
402 (Interface(_), _) => false,
403 (Dependency(a), Dependency(b)) => a == b,
404 (Dependency(_), _) => false,
405 (Url(a), Url(b)) => a == b,
406 (Url(_), _) => false,
407 (Hash(a), Hash(b)) => a == b,
408 (Hash(_), _) => false,
409 }
410 }
411}
412
413impl Eq for ComponentNameKind<'_> {}
414
415#[derive(Debug, Clone, Hash, Eq, PartialEq)]
417pub struct ResourceFunc<'a>(&'a str);
418
419impl<'a> ResourceFunc<'a> {
420 pub fn as_str(&self) -> &'a str {
422 self.0
423 }
424
425 pub fn resource(&self) -> &'a KebabStr {
427 let dot = self.0.find('.').unwrap();
428 KebabStr::new_unchecked(&self.0[..dot])
429 }
430}
431
432#[derive(Debug, Clone, Hash, Eq, PartialEq)]
434pub struct InterfaceName<'a>(&'a str);
435
436impl<'a> InterfaceName<'a> {
437 pub fn as_str(&self) -> &'a str {
439 self.0
440 }
441
442 pub fn namespace(&self) -> &'a KebabStr {
444 let colon = self.0.rfind(':').unwrap();
445 KebabStr::new_unchecked(&self.0[..colon])
446 }
447
448 pub fn package(&self) -> &'a KebabStr {
450 let colon = self.0.rfind(':').unwrap();
451 let slash = self.0.find('/').unwrap();
452 KebabStr::new_unchecked(&self.0[colon + 1..slash])
453 }
454
455 pub fn interface(&self) -> &'a KebabStr {
457 let projection = self.projection();
458 let slash = projection.find('/').unwrap_or(projection.len());
459 KebabStr::new_unchecked(&projection[..slash])
460 }
461
462 pub fn projection(&self) -> &'a KebabStr {
464 let slash = self.0.find('/').unwrap();
465 let at = self.0.find('@').unwrap_or(self.0.len());
466 KebabStr::new_unchecked(&self.0[slash + 1..at])
467 }
468
469 pub fn version(&self) -> Option<Version> {
471 let at = self.0.find('@')?;
472 Some(Version::parse(&self.0[at + 1..]).unwrap())
473 }
474}
475
476#[derive(Debug, Clone, Hash, Eq, PartialEq)]
479pub struct DependencyName<'a>(&'a str);
480
481impl<'a> DependencyName<'a> {
482 pub fn as_str(&self) -> &'a str {
484 self.0
485 }
486}
487
488#[derive(Debug, Clone, Hash, Eq, PartialEq)]
491pub struct UrlName<'a>(&'a str);
492
493impl<'a> UrlName<'a> {
494 pub fn as_str(&self) -> &'a str {
496 self.0
497 }
498}
499
500#[derive(Debug, Clone, Hash, Eq, PartialEq)]
502pub struct HashName<'a>(&'a str);
503
504impl<'a> HashName<'a> {
505 pub fn as_str(&self) -> &'a str {
507 self.0
508 }
509}
510
511struct ComponentNameParser<'a> {
517 next: &'a str,
518 offset: usize,
519 features: WasmFeatures,
520}
521
522impl<'a> ComponentNameParser<'a> {
523 fn parse(&mut self) -> Result<ParsedComponentNameKind> {
524 if self.eat_str(CONSTRUCTOR) {
525 self.expect_kebab()?;
526 return Ok(ParsedComponentNameKind::Constructor);
527 }
528 if self.eat_str(METHOD) {
529 let resource = self.take_until('.')?;
530 self.kebab(resource)?;
531 self.expect_kebab()?;
532 return Ok(ParsedComponentNameKind::Method);
533 }
534 if self.eat_str(STATIC) {
535 let resource = self.take_until('.')?;
536 self.kebab(resource)?;
537 self.expect_kebab()?;
538 return Ok(ParsedComponentNameKind::Static);
539 }
540
541 if self.eat_str("unlocked-dep=") {
543 self.expect_str("<")?;
544 self.pkg_name_query()?;
545 self.expect_str(">")?;
546 return Ok(ParsedComponentNameKind::Dependency);
547 }
548
549 if self.eat_str("locked-dep=") {
551 self.expect_str("<")?;
552 self.pkg_name(false)?;
553 self.expect_str(">")?;
554 self.eat_optional_hash()?;
555 return Ok(ParsedComponentNameKind::Dependency);
556 }
557
558 if self.eat_str("url=") {
560 self.expect_str("<")?;
561 let url = self.take_up_to('>')?;
562 if url.contains('<') {
563 bail!(self.offset, "url cannot contain `<`");
564 }
565 self.expect_str(">")?;
566 self.eat_optional_hash()?;
567 return Ok(ParsedComponentNameKind::Url);
568 }
569 if self.eat_str("relative-url=") {
571 self.expect_str("<")?;
572 let url = self.take_up_to('>')?;
573 if url.contains('<') {
574 bail!(self.offset, "relative-url cannot contain `<`");
575 }
576 self.expect_str(">")?;
577 self.eat_optional_hash()?;
578 return Ok(ParsedComponentNameKind::Url);
579 }
580
581 if self.eat_str("integrity=") {
583 self.expect_str("<")?;
584 let _hash = self.parse_hash()?;
585 self.expect_str(">")?;
586 return Ok(ParsedComponentNameKind::Hash);
587 }
588
589 if self.next.contains(':') {
590 self.pkg_name(true)?;
591 Ok(ParsedComponentNameKind::Interface)
592 } else {
593 self.expect_kebab()?;
594 Ok(ParsedComponentNameKind::Label)
595 }
596 }
597
598 fn pkg_name_query(&mut self) -> Result<()> {
600 self.pkg_path(false)?;
601
602 if self.eat_str("@") {
603 if self.eat_str("*") {
604 return Ok(());
605 }
606
607 self.expect_str("{")?;
608 let range = self.take_up_to('}')?;
609 self.expect_str("}")?;
610 self.semver_range(range)?;
611 }
612
613 Ok(())
614 }
615
616 fn pkg_name(&mut self, require_projection: bool) -> Result<()> {
618 self.pkg_path(require_projection)?;
619
620 if self.eat_str("@") {
621 let version = match self.eat_up_to('>') {
622 Some(version) => version,
623 None => self.take_rest(),
624 };
625
626 self.semver(version)?;
627 }
628
629 Ok(())
630 }
631
632 fn pkg_path(&mut self, require_projection: bool) -> Result<()> {
634 self.take_kebab()?;
636 self.expect_str(":")?;
637 self.take_kebab()?;
638
639 if self.features.component_model_nested_names {
640 while self.next.starts_with(':') {
642 self.expect_str(":")?;
643 self.take_kebab()?;
644 }
645 }
646
647 if self.next.starts_with('/') {
649 self.expect_str("/")?;
650 self.take_kebab()?;
651
652 if self.features.component_model_nested_names {
653 while self.next.starts_with('/') {
654 self.expect_str("/")?;
655 self.take_kebab()?;
656 }
657 }
658 } else if require_projection {
659 bail!(self.offset, "expected `/` after package name");
660 }
661
662 Ok(())
663 }
664
665 fn semver_range(&self, range: &str) -> Result<()> {
672 if range == "*" {
673 return Ok(());
674 }
675
676 if let Some(range) = range.strip_prefix(">=") {
677 let (lower, upper) = range
678 .split_once(' ')
679 .map(|(l, u)| (l, Some(u)))
680 .unwrap_or((range, None));
681 self.semver(lower)?;
682
683 if let Some(upper) = upper {
684 match upper.strip_prefix('<') {
685 Some(upper) => {
686 self.semver(upper)?;
687 }
688 None => bail!(
689 self.offset,
690 "expected `<` at start of version range upper bounds"
691 ),
692 }
693 }
694 } else if let Some(upper) = range.strip_prefix('<') {
695 self.semver(upper)?;
696 } else {
697 bail!(
698 self.offset,
699 "expected `>=` or `<` at start of version range"
700 );
701 }
702
703 Ok(())
704 }
705
706 fn parse_hash(&mut self) -> Result<&'a str> {
707 let integrity = self.take_up_to('>')?;
708 let mut any = false;
709 for hash in integrity.split_whitespace() {
710 any = true;
711 let rest = hash
712 .strip_prefix("sha256")
713 .or_else(|| hash.strip_prefix("sha384"))
714 .or_else(|| hash.strip_prefix("sha512"));
715 let rest = match rest {
716 Some(s) => s,
717 None => bail!(self.offset, "unrecognized hash algorithm: `{hash}`"),
718 };
719 let rest = match rest.strip_prefix('-') {
720 Some(s) => s,
721 None => bail!(self.offset, "expected `-` after hash algorithm: {hash}"),
722 };
723 let (base64, _options) = match rest.find('?') {
724 Some(i) => (&rest[..i], Some(&rest[i + 1..])),
725 None => (rest, None),
726 };
727 if !is_base64(base64) {
728 bail!(self.offset, "not valid base64: `{base64}`");
729 }
730 }
731 if !any {
732 bail!(self.offset, "integrity hash cannot be empty");
733 }
734 Ok(integrity)
735 }
736
737 fn eat_optional_hash(&mut self) -> Result<Option<&'a str>> {
738 if !self.eat_str(",") {
739 return Ok(None);
740 }
741 self.expect_str("integrity=<")?;
742 let ret = self.parse_hash()?;
743 self.expect_str(">")?;
744 Ok(Some(ret))
745 }
746
747 fn eat_str(&mut self, prefix: &str) -> bool {
748 match self.next.strip_prefix(prefix) {
749 Some(rest) => {
750 self.next = rest;
751 true
752 }
753 None => false,
754 }
755 }
756
757 fn expect_str(&mut self, prefix: &str) -> Result<()> {
758 if self.eat_str(prefix) {
759 Ok(())
760 } else {
761 bail!(self.offset, "expected `{prefix}` at `{}`", self.next);
762 }
763 }
764
765 fn eat_until(&mut self, c: char) -> Option<&'a str> {
766 let ret = self.eat_up_to(c);
767 if ret.is_some() {
768 self.next = &self.next[c.len_utf8()..];
769 }
770 ret
771 }
772
773 fn eat_up_to(&mut self, c: char) -> Option<&'a str> {
774 let i = self.next.find(c)?;
775 let (a, b) = self.next.split_at(i);
776 self.next = b;
777 Some(a)
778 }
779
780 fn kebab(&self, s: &'a str) -> Result<&'a KebabStr> {
781 match KebabStr::new(s) {
782 Some(name) => Ok(name),
783 None => bail!(self.offset, "`{s}` is not in kebab case"),
784 }
785 }
786
787 fn semver(&self, s: &str) -> Result<Version> {
788 match Version::parse(s) {
789 Ok(v) => Ok(v),
790 Err(e) => bail!(self.offset, "`{s}` is not a valid semver: {e}"),
791 }
792 }
793
794 fn take_until(&mut self, c: char) -> Result<&'a str> {
795 match self.eat_until(c) {
796 Some(s) => Ok(s),
797 None => bail!(self.offset, "failed to find `{c}` character"),
798 }
799 }
800
801 fn take_up_to(&mut self, c: char) -> Result<&'a str> {
802 match self.eat_up_to(c) {
803 Some(s) => Ok(s),
804 None => bail!(self.offset, "failed to find `{c}` character"),
805 }
806 }
807
808 fn take_rest(&mut self) -> &'a str {
809 let ret = self.next;
810 self.next = "";
811 ret
812 }
813
814 fn take_kebab(&mut self) -> Result<&'a KebabStr> {
815 self.next
816 .find(|c| !matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-'))
817 .map(|i| {
818 let (kebab, next) = self.next.split_at(i);
819 self.next = next;
820 self.kebab(kebab)
821 })
822 .unwrap_or_else(|| self.expect_kebab())
823 }
824
825 fn expect_kebab(&mut self) -> Result<&'a KebabStr> {
826 let s = self.take_rest();
827 self.kebab(s)
828 }
829}
830
831fn is_base64(s: &str) -> bool {
832 if s.is_empty() {
833 return false;
834 }
835 let mut equals = 0;
836 for (i, byte) in s.as_bytes().iter().enumerate() {
837 match byte {
838 b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'+' | b'/' if equals == 0 => {}
839 b'=' if i > 0 && equals < 2 => equals += 1,
840 _ => return false,
841 }
842 }
843 true
844}
845
846#[cfg(test)]
847mod tests {
848 use super::*;
849 use hashbrown::HashSet;
850
851 fn parse_kebab_name(s: &str) -> Option<ComponentName> {
852 ComponentName::new(s, 0).ok()
853 }
854
855 #[test]
856 fn kebab_smoke() {
857 assert!(KebabStr::new("").is_none());
858 assert!(KebabStr::new("a").is_some());
859 assert!(KebabStr::new("aB").is_none());
860 assert!(KebabStr::new("a-B").is_some());
861 assert!(KebabStr::new("a-").is_none());
862 assert!(KebabStr::new("-").is_none());
863 assert!(KebabStr::new("ΒΆ").is_none());
864 assert!(KebabStr::new("0").is_none());
865 assert!(KebabStr::new("a0").is_some());
866 assert!(KebabStr::new("a-0").is_none());
867 }
868
869 #[test]
870 fn name_smoke() {
871 assert!(parse_kebab_name("a").is_some());
872 assert!(parse_kebab_name("[foo]a").is_none());
873 assert!(parse_kebab_name("[constructor]a").is_some());
874 assert!(parse_kebab_name("[method]a").is_none());
875 assert!(parse_kebab_name("[method]a.b").is_some());
876 assert!(parse_kebab_name("[method]a.b.c").is_none());
877 assert!(parse_kebab_name("[static]a.b").is_some());
878 assert!(parse_kebab_name("[static]a").is_none());
879 }
880
881 #[test]
882 fn name_equality() {
883 assert_eq!(parse_kebab_name("a"), parse_kebab_name("a"));
884 assert_ne!(parse_kebab_name("a"), parse_kebab_name("b"));
885 assert_eq!(
886 parse_kebab_name("[constructor]a"),
887 parse_kebab_name("[constructor]a")
888 );
889 assert_ne!(
890 parse_kebab_name("[constructor]a"),
891 parse_kebab_name("[constructor]b")
892 );
893 assert_eq!(
894 parse_kebab_name("[method]a.b"),
895 parse_kebab_name("[method]a.b")
896 );
897 assert_ne!(
898 parse_kebab_name("[method]a.b"),
899 parse_kebab_name("[method]b.b")
900 );
901 assert_eq!(
902 parse_kebab_name("[static]a.b"),
903 parse_kebab_name("[static]a.b")
904 );
905 assert_ne!(
906 parse_kebab_name("[static]a.b"),
907 parse_kebab_name("[static]b.b")
908 );
909
910 assert_eq!(
911 parse_kebab_name("[static]a.b"),
912 parse_kebab_name("[method]a.b")
913 );
914 assert_eq!(
915 parse_kebab_name("[method]a.b"),
916 parse_kebab_name("[static]a.b")
917 );
918
919 assert_ne!(
920 parse_kebab_name("[method]b.b"),
921 parse_kebab_name("[static]a.b")
922 );
923
924 let mut s = HashSet::new();
925 assert!(s.insert(parse_kebab_name("a")));
926 assert!(s.insert(parse_kebab_name("[constructor]a")));
927 assert!(s.insert(parse_kebab_name("[method]a.b")));
928 assert!(!s.insert(parse_kebab_name("[static]a.b")));
929 assert!(s.insert(parse_kebab_name("[static]b.b")));
930 }
931}