Skip to content

Commit c60eda9

Browse files
committed
fix(wit-parser): validate case insensitivity
1 parent 44b5ea4 commit c60eda9

File tree

3 files changed

+85
-6
lines changed

3 files changed

+85
-6
lines changed

crates/wit-parser/src/ast/resolve.rs

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -630,9 +630,29 @@ impl<'a> Resolver<'a> {
630630

631631
let mut export_spans = Vec::new();
632632
let mut import_spans = Vec::new();
633+
// Track case-normalized names to catch case-insensitive duplicates
634+
let mut import_names: HashMap<String, (String, Span)> = HashMap::new();
635+
let mut export_names: HashMap<String, (String, Span)> = HashMap::new();
636+
633637
for (name, (item, span)) in self.type_lookup.iter() {
634638
match *item {
635639
TypeOrItem::Type(id) => {
640+
// Check for case-insensitive duplicate
641+
let lowercase_name = name.to_lowercase();
642+
if let Some((existing_name, _existing_span)) = import_names.get(&lowercase_name)
643+
{
644+
// Only error on case-insensitive duplicates (e.g., "foo" vs "FOO").
645+
// Exact duplicates fall through to the existing check below.
646+
if existing_name != name {
647+
bail!(Error::new(
648+
*span,
649+
format!(
650+
"import `{name}` conflicts with import `{existing_name}` (kebab-case identifiers are case-insensitive)"
651+
),
652+
))
653+
}
654+
}
655+
636656
let prev = self.worlds[world_id]
637657
.imports
638658
.insert(WorldKey::Name(name.to_string()), WorldItem::Type(id));
@@ -642,6 +662,7 @@ impl<'a> Resolver<'a> {
642662
format!("import `{name}` conflicts with prior import of same name"),
643663
))
644664
}
665+
import_names.insert(lowercase_name, (name.to_string(), *span));
645666
import_spans.push(*span);
646667
}
647668
TypeOrItem::Item(_) => unreachable!(),
@@ -674,16 +695,36 @@ impl<'a> Resolver<'a> {
674695
ty: ast::Type::Resource(r),
675696
..
676697
}) => {
677-
for func in r.funcs.iter() {
678-
import_spans.push(func.named_func().name.span);
679-
let func = self.resolve_resource_func(func, name)?;
680-
let prev = self.worlds[world_id]
681-
.imports
682-
.insert(WorldKey::Name(func.name.clone()), WorldItem::Function(func));
698+
for ast_func in r.funcs.iter() {
699+
let func_span = ast_func.named_func().name.span;
700+
import_spans.push(func_span);
701+
let func = self.resolve_resource_func(ast_func, name)?;
702+
703+
// Check for case-insensitive duplicate
704+
let lowercase_name = func.name.to_lowercase();
705+
if let Some((existing_name, _existing_span)) =
706+
import_names.get(&lowercase_name)
707+
{
708+
if existing_name != &func.name {
709+
bail!(Error::new(
710+
func_span,
711+
format!(
712+
"import `{}` conflicts with import `{existing_name}` (kebab-case identifiers are case-insensitive)",
713+
func.name
714+
),
715+
))
716+
}
717+
}
718+
719+
let prev = self.worlds[world_id].imports.insert(
720+
WorldKey::Name(func.name.clone()),
721+
WorldItem::Function(func.clone()),
722+
);
683723
// Resource names themselves are unique, and methods are
684724
// uniquely named, so this should be possible to assert
685725
// at this point and never trip.
686726
assert!(prev.is_none());
727+
import_names.insert(lowercase_name, (func.name.clone(), func_span));
687728
}
688729
continue;
689730
}
@@ -724,6 +765,30 @@ impl<'a> Resolver<'a> {
724765
))
725766
}
726767
}
768+
769+
// Check for case-insensitive duplicate names.
770+
if let WorldKey::Name(ref name) = key {
771+
let lowercase_name = name.to_lowercase();
772+
let names = if desc == "import" {
773+
&mut import_names
774+
} else {
775+
&mut export_names
776+
};
777+
778+
if let Some((existing_name, _existing_span)) = names.get(&lowercase_name) {
779+
// Only error on case-insensitive duplicates (e.g., "foo" vs "FOO").
780+
if existing_name != name {
781+
bail!(Error::new(
782+
kind.span(),
783+
format!(
784+
"{desc} `{name}` conflicts with {desc} `{existing_name}` (kebab-case identifiers are case-insensitive)"
785+
),
786+
))
787+
}
788+
}
789+
names.insert(lowercase_name, (name.clone(), kind.span()));
790+
}
791+
727792
let dst = if desc == "import" {
728793
&mut self.worlds[world_id].imports
729794
} else {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// parse-fail
2+
3+
package poc:demo;
4+
5+
world example {
6+
import get-data: func() -> string;
7+
export get-user: func() -> string;
8+
export GET-USER: func() -> string;
9+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export `GET-USER` conflicts with export `get-user` (kebab-case identifiers are case-insensitive)
2+
--> tests/ui/parse-fail/case-insensitive-duplicates.wit:8:10
3+
|
4+
8 | export GET-USER: func() -> string;
5+
| ^-------

0 commit comments

Comments
 (0)