@@ -378,14 +378,30 @@ impl CommandAnalyzer {
378378 Ok ( ( ) )
379379 }
380380
381- // Find type paths from external crates
381+ // Find type paths from external crates.
382+ //
383+ // Walks the Cargo registry source tree and, for each `.rs` file, looks for a
384+ // `struct` or `enum` item whose identifier exactly matches `type_name`. The
385+ // match is performed on the parsed AST (via `syn`) rather than with a raw
386+ // substring search, so it correctly handles visibility modifiers
387+ // (`pub`, `pub(crate)`), attributes (`#[derive(...)]`), generics
388+ // (`struct X<T>`), and multi-line declarations — all of which the previous
389+ // `content.contains("struct X")` heuristic missed or matched incorrectly
390+ // (see issue #82). A cheap `contains` pre-filter keeps the registry walk fast
391+ // by skipping files that cannot possibly declare the type.
382392 fn find_external_type_path ( & self , type_name : & str ) -> Option < PathBuf > {
383393 // Resolve Cargo home – fall back to $HOME/.cargo if the env var is missing.
384394 let cargo_home: String = env:: var ( "CARGO_HOME" )
385395 . or_else ( |_| env:: var ( "HOME" ) . map ( |h| format ! ( "{}/.cargo" , h) ) )
386396 . ok ( ) ?;
387397 let src_dir: PathBuf = PathBuf :: from ( cargo_home) . join ( "registry/src" ) ;
388398
399+ // A cheap substring pre-filter: only the identifier, without the
400+ // `struct`/`enum` keyword, so that `pub struct X`, `pub(crate) enum X`,
401+ // and `struct\n X` all pass through to the AST check. This avoids parsing
402+ // the vast majority of registry files that cannot contain the type.
403+ let needle: String = type_name. to_string ( ) ;
404+
389405 // Walk the registry tree depth‑first.
390406 let mut dirs: Vec < PathBuf > = vec ! [ src_dir] ;
391407 while let Some ( dir) = dirs. pop ( ) {
@@ -399,19 +415,56 @@ impl CommandAnalyzer {
399415 if path. extension ( ) . and_then ( |s| s. to_str ( ) ) != Some ( "rs" ) {
400416 continue ;
401417 }
402- // Cheap heuristic – read the file and look for the exact identifier.
403- if let Ok ( content) = fs:: read_to_string ( & path) {
404- let struct_pat: String = format ! ( "struct {}" , type_name) ;
405- let enum_pat: String = format ! ( "enum {}" , type_name) ;
406- if content. contains ( & struct_pat) || content. contains ( & enum_pat) {
407- return Some ( path) ;
408- }
418+ // Cheap pre-filter on the raw source before paying for a full parse.
419+ let content: String = match fs:: read_to_string ( & path) {
420+ Ok ( c) => c,
421+ Err ( _) => continue ,
422+ } ;
423+ if !content. contains ( & needle) {
424+ continue ;
425+ }
426+ // Authoritative check on the parsed AST.
427+ if Self :: file_declares_type ( & content, type_name) {
428+ return Some ( path) ;
409429 }
410430 }
411431 }
412432 None
413433 }
414434
435+ /// Returns `true` if `source` declares a `struct` or `enum` item whose
436+ /// identifier equals `type_name`. Items nested inside `mod` blocks are
437+ /// considered as well, mirroring how the analyzer indexes local modules.
438+ /// Files that fail to parse (e.g. macro-heavy or generated sources) yield
439+ /// `false` so they are simply skipped during the registry walk.
440+ fn file_declares_type ( source : & str , type_name : & str ) -> bool {
441+ let file: syn:: File = match syn:: parse_file ( source) {
442+ Ok ( f) => f,
443+ Err ( _) => return false ,
444+ } ;
445+ Self :: items_declare_type ( & file. items , type_name)
446+ }
447+
448+ fn items_declare_type ( items : & [ syn:: Item ] , type_name : & str ) -> bool {
449+ for item in items {
450+ let declares = match item {
451+ syn:: Item :: Struct ( s) => s. ident == type_name,
452+ syn:: Item :: Enum ( e) => e. ident == type_name,
453+ // Recurse into inline modules so types declared in `mod x { ... }`
454+ // blocks within a single file are still discovered.
455+ syn:: Item :: Mod ( m) => match & m. content {
456+ Some ( ( _, inner) ) => Self :: items_declare_type ( inner, type_name) ,
457+ None => false ,
458+ } ,
459+ _ => false ,
460+ } ;
461+ if declares {
462+ return true ;
463+ }
464+ }
465+ false
466+ }
467+
415468 /// Extract a specific type from a cached AST
416469 fn extract_type_from_ast (
417470 & mut self ,
@@ -1173,31 +1226,190 @@ mod tests {
11731226
11741227 mod external_type_lookup {
11751228 use super :: * ;
1229+ use serial_test:: serial;
11761230 use std:: env;
11771231 use std:: fs;
11781232 use std:: path:: PathBuf ;
11791233
1234+ /// Build a fake Cargo registry rooted at a temp dir, write the given
1235+ /// source files into `registry/src/dummy-0.1.0/`, point `CARGO_HOME` at
1236+ /// it, and return the analyzer + the path that a declaration in
1237+ /// `lib.rs` should resolve to.
1238+ struct FakeRegistry {
1239+ _root : PathBuf ,
1240+ cargo_home : PathBuf ,
1241+ }
1242+
1243+ impl FakeRegistry {
1244+ fn new ( files : & [ ( & str , & str ) ] ) -> ( Self , CommandAnalyzer ) {
1245+ let root: PathBuf = std:: env:: temp_dir ( ) . join ( format ! (
1246+ "tauri_typegen_external_test_{}" ,
1247+ std:: time:: SystemTime :: now( )
1248+ . duration_since( std:: time:: UNIX_EPOCH )
1249+ . unwrap( )
1250+ . as_nanos( ) ,
1251+ ) ) ;
1252+ let _ = std:: fs:: remove_dir_all ( & root) ;
1253+ let cargo_home: PathBuf = root. join ( ".cargo" ) ;
1254+ let src_dir: PathBuf = cargo_home. join ( "registry/src" ) ;
1255+ let crate_dir: PathBuf = src_dir. join ( "dummy-0.1.0" ) ;
1256+ fs:: create_dir_all ( & crate_dir) . expect ( "create temp crate dir" ) ;
1257+ for ( name, content) in files {
1258+ fs:: write ( crate_dir. join ( name) , content) . expect ( "write file" ) ;
1259+ }
1260+ env:: set_var ( "CARGO_HOME" , & cargo_home) ;
1261+ let fake = FakeRegistry {
1262+ _root : root. clone ( ) ,
1263+ cargo_home,
1264+ } ;
1265+ ( fake, CommandAnalyzer :: default ( ) )
1266+ }
1267+ }
1268+
1269+ impl Drop for FakeRegistry {
1270+ fn drop ( & mut self ) {
1271+ let _ = std:: fs:: remove_dir_all ( & self . _root ) ;
1272+ }
1273+ }
1274+
11801275 #[ test]
1181- fn test_find_external_type_path ( ) {
1182- let tmp_root : PathBuf = std :: env :: temp_dir ( ) . join ( "tauri_typegen_external_test" ) ;
1183- let _ = std :: fs :: remove_dir_all ( & tmp_root ) ;
1184- let cargo_home : PathBuf = tmp_root . join ( ".cargo ") ;
1185- let src_dir : PathBuf = cargo_home. join ( "registry/src" ) ;
1186- let crate_dir : PathBuf = src_dir . join ( "dummy-0.1.0 ") ;
1187- fs :: create_dir_all ( & crate_dir ) . expect ( "create temp crate dir" ) ;
1276+ # [ serial ]
1277+ fn test_find_external_type_path_pub_struct ( ) {
1278+ let ( reg , analyzer ) = FakeRegistry :: new ( & [ ( "lib.rs" , "pub struct ExternalFoo;" ) ] ) ;
1279+ let found = analyzer . find_external_type_path ( "ExternalFoo ") ;
1280+ let expected = reg . cargo_home . join ( "registry/src/dummy-0.1.0/lib.rs " ) ;
1281+ assert_eq ! ( found . unwrap ( ) , expected , "pub struct should be located ") ;
1282+ }
11881283
1189- let file_path: PathBuf = crate_dir. join ( "lib.rs" ) ;
1190- fs:: write ( & file_path, "pub struct ExternalFoo;" ) . expect ( "write dummy crate file" ) ;
1284+ /// Regression for issue #82: visibility modifiers such as `pub(crate)`
1285+ /// must not prevent discovery.
1286+ #[ test]
1287+ #[ serial]
1288+ fn test_find_external_type_path_pub_crate_visibility ( ) {
1289+ let ( reg, analyzer) = FakeRegistry :: new ( & [ ( "lib.rs" , "pub(crate) struct VisCrate;" ) ] ) ;
1290+ let found = analyzer. find_external_type_path ( "VisCrate" ) ;
1291+ let expected = reg. cargo_home . join ( "registry/src/dummy-0.1.0/lib.rs" ) ;
1292+ assert_eq ! (
1293+ found. unwrap( ) ,
1294+ expected,
1295+ "pub(crate) struct should be located"
1296+ ) ;
1297+ }
11911298
1192- env:: set_var ( "CARGO_HOME" , & cargo_home) ;
1299+ /// Regression for issue #82: derive attributes preceding the
1300+ /// declaration must not prevent discovery.
1301+ #[ test]
1302+ #[ serial]
1303+ fn test_find_external_type_path_with_derive_attributes ( ) {
1304+ let ( reg, analyzer) = FakeRegistry :: new ( & [ (
1305+ "lib.rs" ,
1306+ "#[derive(Debug, Clone)]\n pub struct WithDerives { field: i32 }" ,
1307+ ) ] ) ;
1308+ let found = analyzer. find_external_type_path ( "WithDerives" ) ;
1309+ let expected = reg. cargo_home . join ( "registry/src/dummy-0.1.0/lib.rs" ) ;
1310+ assert_eq ! (
1311+ found. unwrap( ) ,
1312+ expected,
1313+ "#[derive(...)] pub struct should be located"
1314+ ) ;
1315+ }
1316+
1317+ /// Regression for issue #82: generic parameters must not prevent
1318+ /// discovery.
1319+ #[ test]
1320+ #[ serial]
1321+ fn test_find_external_type_path_with_generics ( ) {
1322+ let ( reg, analyzer) =
1323+ FakeRegistry :: new ( & [ ( "lib.rs" , "pub struct Generic<T, U> { a: T, b: U }" ) ] ) ;
1324+ let found = analyzer. find_external_type_path ( "Generic" ) ;
1325+ let expected = reg. cargo_home . join ( "registry/src/dummy-0.1.0/lib.rs" ) ;
1326+ assert_eq ! (
1327+ found. unwrap( ) ,
1328+ expected,
1329+ "generic pub struct should be located"
1330+ ) ;
1331+ }
11931332
1194- let analyzer: CommandAnalyzer = CommandAnalyzer :: default ( ) ;
1195- let found: Option < PathBuf > = analyzer. find_external_type_path ( "ExternalFoo" ) ;
1333+ /// Multi-line declarations (keyword and identifier on different lines)
1334+ /// must be discovered — the old `contains("struct X")` heuristic missed
1335+ /// these.
1336+ #[ test]
1337+ #[ serial]
1338+ fn test_find_external_type_path_multiline ( ) {
1339+ let ( reg, analyzer) =
1340+ FakeRegistry :: new ( & [ ( "lib.rs" , "pub\n struct\n Multiline\n {\n x: i32,\n }" ) ] ) ;
1341+ let found = analyzer. find_external_type_path ( "Multiline" ) ;
1342+ let expected = reg. cargo_home . join ( "registry/src/dummy-0.1.0/lib.rs" ) ;
1343+ assert_eq ! (
1344+ found. unwrap( ) ,
1345+ expected,
1346+ "multi-line struct should be located"
1347+ ) ;
1348+ }
1349+
1350+ /// Enums with attributes and visibility must be discovered too.
1351+ #[ test]
1352+ #[ serial]
1353+ fn test_find_external_type_path_enum_with_attributes ( ) {
1354+ let ( reg, analyzer) =
1355+ FakeRegistry :: new ( & [ ( "lib.rs" , "#[derive(Debug)]\n pub enum EnumAttr { A, B }" ) ] ) ;
1356+ let found = analyzer. find_external_type_path ( "EnumAttr" ) ;
1357+ let expected = reg. cargo_home . join ( "registry/src/dummy-0.1.0/lib.rs" ) ;
1358+ assert_eq ! (
1359+ found. unwrap( ) ,
1360+ expected,
1361+ "pub enum with derive should be located"
1362+ ) ;
1363+ }
11961364
1365+ /// Types declared inside an inline `mod` block should still be found.
1366+ #[ test]
1367+ #[ serial]
1368+ fn test_find_external_type_path_nested_module ( ) {
1369+ let ( reg, analyzer) =
1370+ FakeRegistry :: new ( & [ ( "lib.rs" , "mod inner {\n pub struct Nested;\n }\n " ) ] ) ;
1371+ let found = analyzer. find_external_type_path ( "Nested" ) ;
1372+ let expected = reg. cargo_home . join ( "registry/src/dummy-0.1.0/lib.rs" ) ;
11971373 assert_eq ! (
11981374 found. unwrap( ) ,
1199- file_path,
1200- "the external‑type lookup should locate the dummy `ExternalFoo`"
1375+ expected,
1376+ "struct inside an inline mod should be located"
1377+ ) ;
1378+ }
1379+
1380+ /// The lookup must not return false positives: a struct whose name only
1381+ /// *starts with* the searched identifier (e.g. `ExternalFooBar` when
1382+ /// searching for `ExternalFoo`) must not match. The old substring
1383+ /// heuristic would incorrectly match this.
1384+ #[ test]
1385+ #[ serial]
1386+ fn test_find_external_type_path_no_false_positive_prefix ( ) {
1387+ let ( _reg, analyzer) = FakeRegistry :: new ( & [ ( "lib.rs" , "pub struct ExternalFooBar;" ) ] ) ;
1388+ let found = analyzer. find_external_type_path ( "ExternalFoo" ) ;
1389+ assert ! (
1390+ found. is_none( ) ,
1391+ "a prefix-named struct must not match the shorter identifier"
1392+ ) ;
1393+ }
1394+
1395+ /// An absent type must resolve to `None`.
1396+ #[ test]
1397+ #[ serial]
1398+ fn test_find_external_type_path_missing ( ) {
1399+ let ( _reg, analyzer) = FakeRegistry :: new ( & [ ( "lib.rs" , "pub struct SomethingElse;" ) ] ) ;
1400+ let found = analyzer. find_external_type_path ( "ExternalFoo" ) ;
1401+ assert ! ( found. is_none( ) , "a missing type must resolve to None" ) ;
1402+ }
1403+
1404+ /// A file that fails to parse must be skipped, not panic.
1405+ #[ test]
1406+ #[ serial]
1407+ fn test_find_external_type_path_skips_unparseable_file ( ) {
1408+ let ( _reg, analyzer) = FakeRegistry :: new ( & [ ( "lib.rs" , "this is not valid rust !!!" ) ] ) ;
1409+ let found = analyzer. find_external_type_path ( "ExternalFoo" ) ;
1410+ assert ! (
1411+ found. is_none( ) ,
1412+ "an unparseable file must be skipped without panicking"
12011413 ) ;
12021414 }
12031415 }
0 commit comments