@@ -989,6 +989,9 @@ use rustc_hash::FxHashMap;
989989struct ItemInfo {
990990 span : Span ,
991991 path : String ,
992+ kind : & ' static str ,
993+ /// Whether we've already emitted an error for this item as the "first definition".
994+ first_error_emitted : bool ,
992995}
993996
994997/// Result of HIR validation.
@@ -1031,8 +1034,10 @@ fn validate_duplicate_names(db: &dyn Db, root: baml_workspace::Project) -> Vec<N
10311034 ItemId :: Function ( loc) => {
10321035 let file = loc. file ( db) ;
10331036 let item_tree = file_item_tree ( db, file) ;
1034- let func = & item_tree[ loc. id ( db) ] ;
1035- let span = Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ;
1037+ let local_id = loc. id ( db) ;
1038+ let func = & item_tree[ local_id] ;
1039+ let span = get_item_name_span ( db, file, "function" , func. name . as_str ( ) , local_id. index ( ) )
1040+ . unwrap_or_else ( || Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ) ;
10361041 let path = file. path ( db) . display ( ) . to_string ( ) ;
10371042 check_duplicate (
10381043 & mut seen,
@@ -1046,8 +1051,10 @@ fn validate_duplicate_names(db: &dyn Db, root: baml_workspace::Project) -> Vec<N
10461051 ItemId :: Class ( loc) => {
10471052 let file = loc. file ( db) ;
10481053 let item_tree = file_item_tree ( db, file) ;
1049- let class = & item_tree[ loc. id ( db) ] ;
1050- let span = Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ;
1054+ let local_id = loc. id ( db) ;
1055+ let class = & item_tree[ local_id] ;
1056+ let span = get_item_name_span ( db, file, "class" , class. name . as_str ( ) , local_id. index ( ) )
1057+ . unwrap_or_else ( || Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ) ;
10511058 let path = file. path ( db) . display ( ) . to_string ( ) ;
10521059 check_duplicate (
10531060 & mut seen,
@@ -1061,8 +1068,10 @@ fn validate_duplicate_names(db: &dyn Db, root: baml_workspace::Project) -> Vec<N
10611068 ItemId :: Enum ( loc) => {
10621069 let file = loc. file ( db) ;
10631070 let item_tree = file_item_tree ( db, file) ;
1064- let enum_def = & item_tree[ loc. id ( db) ] ;
1065- let span = Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ;
1071+ let local_id = loc. id ( db) ;
1072+ let enum_def = & item_tree[ local_id] ;
1073+ let span = get_item_name_span ( db, file, "enum" , enum_def. name . as_str ( ) , local_id. index ( ) )
1074+ . unwrap_or_else ( || Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ) ;
10661075 let path = file. path ( db) . display ( ) . to_string ( ) ;
10671076 check_duplicate (
10681077 & mut seen,
@@ -1076,8 +1085,10 @@ fn validate_duplicate_names(db: &dyn Db, root: baml_workspace::Project) -> Vec<N
10761085 ItemId :: TypeAlias ( loc) => {
10771086 let file = loc. file ( db) ;
10781087 let item_tree = file_item_tree ( db, file) ;
1079- let alias = & item_tree[ loc. id ( db) ] ;
1080- let span = Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ;
1088+ let local_id = loc. id ( db) ;
1089+ let alias = & item_tree[ local_id] ;
1090+ let span = get_item_name_span ( db, file, "type alias" , alias. name . as_str ( ) , local_id. index ( ) )
1091+ . unwrap_or_else ( || Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ) ;
10811092 let path = file. path ( db) . display ( ) . to_string ( ) ;
10821093 check_duplicate (
10831094 & mut seen,
@@ -1091,8 +1102,10 @@ fn validate_duplicate_names(db: &dyn Db, root: baml_workspace::Project) -> Vec<N
10911102 ItemId :: Client ( loc) => {
10921103 let file = loc. file ( db) ;
10931104 let item_tree = file_item_tree ( db, file) ;
1094- let client = & item_tree[ loc. id ( db) ] ;
1095- let span = Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ;
1105+ let local_id = loc. id ( db) ;
1106+ let client = & item_tree[ local_id] ;
1107+ let span = get_item_name_span ( db, file, "client" , client. name . as_str ( ) , local_id. index ( ) )
1108+ . unwrap_or_else ( || Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ) ;
10961109 let path = file. path ( db) . display ( ) . to_string ( ) ;
10971110 check_duplicate (
10981111 & mut seen,
@@ -1106,8 +1119,10 @@ fn validate_duplicate_names(db: &dyn Db, root: baml_workspace::Project) -> Vec<N
11061119 ItemId :: Generator ( loc) => {
11071120 let file = loc. file ( db) ;
11081121 let item_tree = file_item_tree ( db, file) ;
1109- let generator = & item_tree[ loc. id ( db) ] ;
1110- let span = Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ;
1122+ let local_id = loc. id ( db) ;
1123+ let generator = & item_tree[ local_id] ;
1124+ let span = get_item_name_span ( db, file, "generator" , generator. name . as_str ( ) , local_id. index ( ) )
1125+ . unwrap_or_else ( || Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ) ;
11111126 let path = file. path ( db) . display ( ) . to_string ( ) ;
11121127 check_duplicate (
11131128 & mut seen,
@@ -1122,8 +1137,10 @@ fn validate_duplicate_names(db: &dyn Db, root: baml_workspace::Project) -> Vec<N
11221137 // Tests are validated separately: only same name + same function is a duplicate
11231138 let file = loc. file ( db) ;
11241139 let item_tree = file_item_tree ( db, file) ;
1125- let test = & item_tree[ loc. id ( db) ] ;
1126- let span = Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ;
1140+ let local_id = loc. id ( db) ;
1141+ let test = & item_tree[ local_id] ;
1142+ let span = get_item_name_span ( db, file, "test" , test. name . as_str ( ) , local_id. index ( ) )
1143+ . unwrap_or_else ( || Span :: new ( file. file_id ( db) , TextRange :: empty ( 0 . into ( ) ) ) ) ;
11271144 let path = file. path ( db) . display ( ) . to_string ( ) ;
11281145
11291146 // Check each function reference in the test
@@ -1144,6 +1161,8 @@ fn validate_duplicate_names(db: &dyn Db, root: baml_workspace::Project) -> Vec<N
11441161 ItemInfo {
11451162 span,
11461163 path : path. clone ( ) ,
1164+ kind : "test" ,
1165+ first_error_emitted : false ,
11471166 } ,
11481167 ) ;
11491168 }
@@ -1199,7 +1218,21 @@ fn check_duplicate(
11991218 span : Span ,
12001219 path : String ,
12011220) {
1202- if let Some ( existing) = seen. get ( & name) {
1221+ if let Some ( existing) = seen. get_mut ( & name) {
1222+ // If this is the first duplicate we've seen, also emit an error for the first definition
1223+ if !existing. first_error_emitted {
1224+ errors. push ( NameError :: DuplicateName {
1225+ name : name. to_string ( ) ,
1226+ kind : existing. kind ,
1227+ first : span,
1228+ first_path : path. clone ( ) ,
1229+ second : existing. span ,
1230+ second_path : existing. path . clone ( ) ,
1231+ } ) ;
1232+ existing. first_error_emitted = true ;
1233+ }
1234+
1235+ // Emit error for the current (duplicate) definition
12031236 errors. push ( NameError :: DuplicateName {
12041237 name : name. to_string ( ) ,
12051238 kind,
@@ -1209,7 +1242,15 @@ fn check_duplicate(
12091242 second_path : path,
12101243 } ) ;
12111244 } else {
1212- seen. insert ( name, ItemInfo { span, path } ) ;
1245+ seen. insert (
1246+ name,
1247+ ItemInfo {
1248+ span,
1249+ path,
1250+ kind,
1251+ first_error_emitted : false ,
1252+ } ,
1253+ ) ;
12131254 }
12141255}
12151256
@@ -1318,6 +1359,122 @@ fn get_enum_variant_info(
13181359 None
13191360}
13201361
1362+ /// Look up the span of a top-level item's name from the syntax tree.
1363+ ///
1364+ /// This is used to get accurate spans for duplicate name errors, since the
1365+ /// ItemTree is position-independent and doesn't store spans.
1366+ ///
1367+ /// The `occurrence` parameter specifies which occurrence to return (0 = first, 1 = second, etc.)
1368+ /// when there are multiple items of the same kind with the same name in the file.
1369+ /// This corresponds to the collision index in `LocalItemId`.
1370+ fn get_item_name_span (
1371+ db : & dyn Db ,
1372+ file : baml_base:: files:: SourceFile ,
1373+ kind : & str ,
1374+ name : & str ,
1375+ occurrence : u16 ,
1376+ ) -> Option < Span > {
1377+ use baml_compiler_syntax:: {
1378+ SyntaxKind ,
1379+ ast:: { ClassDef , ClientDef , EnumDef , FunctionDef , GeneratorDef , TestDef , TypeAliasDef } ,
1380+ } ;
1381+
1382+ let tree = baml_compiler_parser:: syntax_tree ( db, file) ;
1383+ let file_id = file. file_id ( db) ;
1384+ let mut matches_found: u16 = 0 ;
1385+
1386+ for node in tree. children ( ) {
1387+ match kind {
1388+ "function" if node. kind ( ) == SyntaxKind :: FUNCTION_DEF => {
1389+ if let Some ( func) = FunctionDef :: cast ( node) {
1390+ if let Some ( name_token) = func. name ( ) {
1391+ if name_token. text ( ) == name {
1392+ if matches_found == occurrence {
1393+ return Some ( Span :: new ( file_id, name_token. text_range ( ) ) ) ;
1394+ }
1395+ matches_found += 1 ;
1396+ }
1397+ }
1398+ }
1399+ }
1400+ "class" if node. kind ( ) == SyntaxKind :: CLASS_DEF => {
1401+ if let Some ( class) = ClassDef :: cast ( node) {
1402+ if let Some ( name_token) = class. name ( ) {
1403+ if name_token. text ( ) == name {
1404+ if matches_found == occurrence {
1405+ return Some ( Span :: new ( file_id, name_token. text_range ( ) ) ) ;
1406+ }
1407+ matches_found += 1 ;
1408+ }
1409+ }
1410+ }
1411+ }
1412+ "enum" if node. kind ( ) == SyntaxKind :: ENUM_DEF => {
1413+ if let Some ( enum_def) = EnumDef :: cast ( node) {
1414+ if let Some ( name_token) = enum_def. name ( ) {
1415+ if name_token. text ( ) == name {
1416+ if matches_found == occurrence {
1417+ return Some ( Span :: new ( file_id, name_token. text_range ( ) ) ) ;
1418+ }
1419+ matches_found += 1 ;
1420+ }
1421+ }
1422+ }
1423+ }
1424+ "type alias" if node. kind ( ) == SyntaxKind :: TYPE_ALIAS_DEF => {
1425+ if let Some ( alias) = TypeAliasDef :: cast ( node) {
1426+ if let Some ( name_token) = alias. name ( ) {
1427+ if name_token. text ( ) == name {
1428+ if matches_found == occurrence {
1429+ return Some ( Span :: new ( file_id, name_token. text_range ( ) ) ) ;
1430+ }
1431+ matches_found += 1 ;
1432+ }
1433+ }
1434+ }
1435+ }
1436+ "client" if node. kind ( ) == SyntaxKind :: CLIENT_DEF => {
1437+ if let Some ( client) = ClientDef :: cast ( node) {
1438+ if let Some ( name_token) = client. name ( ) {
1439+ if name_token. text ( ) == name {
1440+ if matches_found == occurrence {
1441+ return Some ( Span :: new ( file_id, name_token. text_range ( ) ) ) ;
1442+ }
1443+ matches_found += 1 ;
1444+ }
1445+ }
1446+ }
1447+ }
1448+ "generator" if node. kind ( ) == SyntaxKind :: GENERATOR_DEF => {
1449+ if let Some ( generator) = GeneratorDef :: cast ( node) {
1450+ if let Some ( name_token) = generator. name ( ) {
1451+ if name_token. text ( ) == name {
1452+ if matches_found == occurrence {
1453+ return Some ( Span :: new ( file_id, name_token. text_range ( ) ) ) ;
1454+ }
1455+ matches_found += 1 ;
1456+ }
1457+ }
1458+ }
1459+ }
1460+ "test" if node. kind ( ) == SyntaxKind :: TEST_DEF => {
1461+ if let Some ( test) = TestDef :: cast ( node) {
1462+ if let Some ( name_token) = test. name ( ) {
1463+ if name_token. text ( ) == name {
1464+ if matches_found == occurrence {
1465+ return Some ( Span :: new ( file_id, name_token. text_range ( ) ) ) ;
1466+ }
1467+ matches_found += 1 ;
1468+ }
1469+ }
1470+ }
1471+ }
1472+ _ => { }
1473+ }
1474+ }
1475+ None
1476+ }
1477+
13211478/// Validate that field names and function parameters don't use reserved keywords.
13221479///
13231480/// This checks:
0 commit comments