Rename types through the AST, not the token stream
The token-level rename was too blunt, and the full dependency run showed exactly how: `Iterator<Item = T>` became `Iterator<swash_Item = T>`, `Deref<Target = X>` became `Deref<zbus_Target = X>`, and `A::Adapter` became `A::wgpu_Adapter`. `Item`, `Target`, `Output` and `Error` are all names crates declare *and* names of associated types -- different things that happen to be spelled alike, which is a distinction an AST has and a token stream does not. A `VisitMut` now renames only where a type name of this crate can appear: the declarations themselves, and paths whose type segment is not qualified by another type. An associated type's name, an `impl` block's `type Output`, and a field's name are left alone; only their values are visited. Token renaming stays where it is still right -- function-local types, where the body is the entire scope and every spelling of the name inside it does mean that type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8204755 parent: 113d857 modified
Cargo.toml +1 -1 | @@ -6,7 +6,7 @@ edition = "2024" | ||
| 6 | 6 | [dependencies] |
| 7 | 7 | quote = "1" |
| 8 | 8 | proc-macro2 = "1" |
| 9 | -syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] } | |
| 9 | +syn = { version = "3.0.5", features = ["full", "extra-traits", "visit", "visit-mut"] } | |
| 10 | 10 | |
| 11 | 11 | # Used only by the test corpus: a case carrying `//@ extern: bitflags` is |
| 12 | 12 | # compiled by the oracle against the real crate, so the `bitflags!` lowering is |
| @@ -6,7 +6,7 @@ edition = "2024" | |||
| 6 | [dependencies] | 6 | [dependencies] |
| 7 | quote = "1" | 7 | quote = "1" |
| 8 | proc-macro2 = "1" | 8 | proc-macro2 = "1" |
| 9 | -syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] } | 9 | +syn = { version = "3.0.5", features = ["full", "extra-traits", "visit", "visit-mut"] } |
| 10 | 10 | ||
| 11 | # Used only by the test corpus: a case carrying `//@ extern: bitflags` is | 11 | # Used only by the test corpus: a case carrying `//@ extern: bitflags` is |
| 12 | # compiled by the oracle against the real crate, so the `bitflags!` lowering is | 12 | # compiled by the oracle against the real crate, so the `bitflags!` lowering is |
modified
src/lower.rs +97 -5 | @@ -8184,12 +8184,104 @@ fn namespace_types(files: &mut [(String, syn::File)]) { | ||
| 8184 | 8184 | if renames.is_empty() { |
| 8185 | 8185 | continue; |
| 8186 | 8186 | } |
| 8187 | - let mut ts = quote::quote!(#f); | |
| 8188 | - for (from, to) in &renames { | |
| 8189 | - ts = rename_ident(ts, from, to); | |
| 8187 | + syn::visit_mut::VisitMut::visit_file_mut(&mut Renamer { renames: &renames }, f); | |
| 8188 | + } | |
| 8189 | +} | |
| 8190 | + | |
| 8191 | +/// Rename type names across a crate, in the places a *type* name can appear. | |
| 8192 | +/// | |
| 8193 | +/// Deliberately not a token substitution. `Item`, `Target`, `Output` and | |
| 8194 | +/// `Error` are all names crates declare and also names of associated types, so | |
| 8195 | +/// rewriting every token spelled `Item` turns `Iterator<Item = T>` into | |
| 8196 | +/// `Iterator<swash_Item = T>` and `A::Adapter` into `A::wgpu_Adapter`. Those | |
| 8197 | +/// are different things that happen to be spelled alike, which is exactly the | |
| 8198 | +/// distinction an AST has and a token stream does not. | |
| 8199 | +struct Renamer<'a> { | |
| 8200 | + renames: &'a HashMap<String, String>, | |
| 8201 | +} | |
| 8202 | + | |
| 8203 | +impl Renamer<'_> { | |
| 8204 | + fn rename(&self, id: &mut proc_macro2::Ident) { | |
| 8205 | + if let Some(to) = self.renames.get(&id.to_string()) { | |
| 8206 | + *id = proc_macro2::Ident::new(to, id.span()); | |
| 8190 | 8207 | } |
| 8191 | - if let Ok(parsed) = syn::parse2::<syn::File>(ts) { | |
| 8192 | - *f = parsed; | |
| 8208 | + } | |
| 8209 | + | |
| 8210 | + /// The first segment of a path, or the last when the path is qualified by | |
| 8211 | + /// a module. Anything after a segment that is not the crate root is an | |
| 8212 | + /// associated item and belongs to whatever the qualifier resolves to, not | |
| 8213 | + /// to this crate's namespace. | |
| 8214 | + fn rename_path(&self, p: &mut syn::Path) { | |
| 8215 | + let n = p.segments.len(); | |
| 8216 | + if n == 0 { | |
| 8217 | + return; | |
| 8218 | + } | |
| 8219 | + if n == 1 { | |
| 8220 | + self.rename(&mut p.segments[0].ident); | |
| 8221 | + return; | |
| 8222 | + } | |
| 8223 | + // `module::Type` and `module::Type::CONST` -- rename the segment that | |
| 8224 | + // names a type, never one that follows it. | |
| 8225 | + for i in 0..n { | |
| 8226 | + let seg = &p.segments[i]; | |
| 8227 | + let is_type = seg.ident.to_string().starts_with(char::is_uppercase); | |
| 8228 | + if is_type { | |
| 8229 | + // Only if what precedes it is lowercase (a module path). A | |
| 8230 | + // preceding uppercase segment means this is an associated | |
| 8231 | + // item: `A::Adapter`, `Self::Output`. | |
| 8232 | + let qualified_by_type = i > 0 | |
| 8233 | + && p.segments[i - 1] | |
| 8234 | + .ident | |
| 8235 | + .to_string() | |
| 8236 | + .starts_with(char::is_uppercase); | |
| 8237 | + if !qualified_by_type { | |
| 8238 | + let mut id = seg.ident.clone(); | |
| 8239 | + self.rename(&mut id); | |
| 8240 | + p.segments[i].ident = id; | |
| 8241 | + } | |
| 8242 | + break; | |
| 8243 | + } | |
| 8193 | 8244 | } |
| 8194 | 8245 | } |
| 8195 | 8246 | } |
| 8247 | + | |
| 8248 | +impl syn::visit_mut::VisitMut for Renamer<'_> { | |
| 8249 | + fn visit_item_struct_mut(&mut self, i: &mut syn::ItemStruct) { | |
| 8250 | + self.rename(&mut i.ident); | |
| 8251 | + syn::visit_mut::visit_item_struct_mut(self, i); | |
| 8252 | + } | |
| 8253 | + fn visit_item_enum_mut(&mut self, i: &mut syn::ItemEnum) { | |
| 8254 | + self.rename(&mut i.ident); | |
| 8255 | + syn::visit_mut::visit_item_enum_mut(self, i); | |
| 8256 | + } | |
| 8257 | + fn visit_item_type_mut(&mut self, i: &mut syn::ItemType) { | |
| 8258 | + self.rename(&mut i.ident); | |
| 8259 | + syn::visit_mut::visit_item_type_mut(self, i); | |
| 8260 | + } | |
| 8261 | + fn visit_item_union_mut(&mut self, i: &mut syn::ItemUnion) { | |
| 8262 | + self.rename(&mut i.ident); | |
| 8263 | + syn::visit_mut::visit_item_union_mut(self, i); | |
| 8264 | + } | |
| 8265 | + fn visit_path_mut(&mut self, p: &mut syn::Path) { | |
| 8266 | + self.rename_path(p); | |
| 8267 | + syn::visit_mut::visit_path_mut(self, p); | |
| 8268 | + } | |
| 8269 | + /// `Iterator<Item = T>`: `Item` names an associated type of `Iterator`, | |
| 8270 | + /// not a type in this crate. Only the value is visited. | |
| 8271 | + fn visit_assoc_type_mut(&mut self, a: &mut syn::AssocType) { | |
| 8272 | + syn::visit_mut::visit_type_mut(self, &mut a.ty); | |
| 8273 | + } | |
| 8274 | + fn visit_assoc_const_mut(&mut self, a: &mut syn::AssocConst) { | |
| 8275 | + syn::visit_mut::visit_expr_mut(self, &mut a.value); | |
| 8276 | + } | |
| 8277 | + /// An `impl` block's own associated items keep their names: `type Output` | |
| 8278 | + /// in `impl Add for X` is the trait's name for it. | |
| 8279 | + fn visit_impl_item_type_mut(&mut self, i: &mut syn::ImplItemType) { | |
| 8280 | + syn::visit_mut::visit_type_mut(self, &mut i.ty); | |
| 8281 | + } | |
| 8282 | + fn visit_trait_item_type_mut(&mut self, _: &mut syn::TraitItemType) {} | |
| 8283 | + /// A field or variant named like a type is still a field or a variant. | |
| 8284 | + fn visit_field_mut(&mut self, f: &mut syn::Field) { | |
| 8285 | + syn::visit_mut::visit_type_mut(self, &mut f.ty); | |
| 8286 | + } | |
| 8287 | +} | |
| @@ -8184,12 +8184,104 @@ fn namespace_types(files: &mut [(String, syn::File)]) { | |||
| 8184 | if renames.is_empty() { | 8184 | if renames.is_empty() { |
| 8185 | continue; | 8185 | continue; |
| 8186 | } | 8186 | } |
| 8187 | - let mut ts = quote::quote!(#f); | 8187 | + syn::visit_mut::VisitMut::visit_file_mut(&mut Renamer { renames: &renames }, f); |
| 8188 | - for (from, to) in &renames { | 8188 | + } |
| 8189 | - ts = rename_ident(ts, from, to); | 8189 | +} |
| 8190 | + | ||
| 8191 | +/// Rename type names across a crate, in the places a *type* name can appear. | ||
| 8192 | +/// | ||
| 8193 | +/// Deliberately not a token substitution. `Item`, `Target`, `Output` and | ||
| 8194 | +/// `Error` are all names crates declare and also names of associated types, so | ||
| 8195 | +/// rewriting every token spelled `Item` turns `Iterator<Item = T>` into | ||
| 8196 | +/// `Iterator<swash_Item = T>` and `A::Adapter` into `A::wgpu_Adapter`. Those | ||
| 8197 | +/// are different things that happen to be spelled alike, which is exactly the | ||
| 8198 | +/// distinction an AST has and a token stream does not. | ||
| 8199 | +struct Renamer<'a> { | ||
| 8200 | + renames: &'a HashMap<String, String>, | ||
| 8201 | +} | ||
| 8202 | + | ||
| 8203 | +impl Renamer<'_> { | ||
| 8204 | + fn rename(&self, id: &mut proc_macro2::Ident) { | ||
| 8205 | + if let Some(to) = self.renames.get(&id.to_string()) { | ||
| 8206 | + *id = proc_macro2::Ident::new(to, id.span()); | ||
| 8190 | } | 8207 | } |
| 8191 | - if let Ok(parsed) = syn::parse2::<syn::File>(ts) { | 8208 | + } |
| 8192 | - *f = parsed; | 8209 | + |
| 8210 | + /// The first segment of a path, or the last when the path is qualified by | ||
| 8211 | + /// a module. Anything after a segment that is not the crate root is an | ||
| 8212 | + /// associated item and belongs to whatever the qualifier resolves to, not | ||
| 8213 | + /// to this crate's namespace. | ||
| 8214 | + fn rename_path(&self, p: &mut syn::Path) { | ||
| 8215 | + let n = p.segments.len(); | ||
| 8216 | + if n == 0 { | ||
| 8217 | + return; | ||
| 8218 | + } | ||
| 8219 | + if n == 1 { | ||
| 8220 | + self.rename(&mut p.segments[0].ident); | ||
| 8221 | + return; | ||
| 8222 | + } | ||
| 8223 | + // `module::Type` and `module::Type::CONST` -- rename the segment that | ||
| 8224 | + // names a type, never one that follows it. | ||
| 8225 | + for i in 0..n { | ||
| 8226 | + let seg = &p.segments[i]; | ||
| 8227 | + let is_type = seg.ident.to_string().starts_with(char::is_uppercase); | ||
| 8228 | + if is_type { | ||
| 8229 | + // Only if what precedes it is lowercase (a module path). A | ||
| 8230 | + // preceding uppercase segment means this is an associated | ||
| 8231 | + // item: `A::Adapter`, `Self::Output`. | ||
| 8232 | + let qualified_by_type = i > 0 | ||
| 8233 | + && p.segments[i - 1] | ||
| 8234 | + .ident | ||
| 8235 | + .to_string() | ||
| 8236 | + .starts_with(char::is_uppercase); | ||
| 8237 | + if !qualified_by_type { | ||
| 8238 | + let mut id = seg.ident.clone(); | ||
| 8239 | + self.rename(&mut id); | ||
| 8240 | + p.segments[i].ident = id; | ||
| 8241 | + } | ||
| 8242 | + break; | ||
| 8243 | + } | ||
| 8193 | } | 8244 | } |
| 8194 | } | 8245 | } |
| 8195 | } | 8246 | } |
| 8247 | + | ||
| 8248 | +impl syn::visit_mut::VisitMut for Renamer<'_> { | ||
| 8249 | + fn visit_item_struct_mut(&mut self, i: &mut syn::ItemStruct) { | ||
| 8250 | + self.rename(&mut i.ident); | ||
| 8251 | + syn::visit_mut::visit_item_struct_mut(self, i); | ||
| 8252 | + } | ||
| 8253 | + fn visit_item_enum_mut(&mut self, i: &mut syn::ItemEnum) { | ||
| 8254 | + self.rename(&mut i.ident); | ||
| 8255 | + syn::visit_mut::visit_item_enum_mut(self, i); | ||
| 8256 | + } | ||
| 8257 | + fn visit_item_type_mut(&mut self, i: &mut syn::ItemType) { | ||
| 8258 | + self.rename(&mut i.ident); | ||
| 8259 | + syn::visit_mut::visit_item_type_mut(self, i); | ||
| 8260 | + } | ||
| 8261 | + fn visit_item_union_mut(&mut self, i: &mut syn::ItemUnion) { | ||
| 8262 | + self.rename(&mut i.ident); | ||
| 8263 | + syn::visit_mut::visit_item_union_mut(self, i); | ||
| 8264 | + } | ||
| 8265 | + fn visit_path_mut(&mut self, p: &mut syn::Path) { | ||
| 8266 | + self.rename_path(p); | ||
| 8267 | + syn::visit_mut::visit_path_mut(self, p); | ||
| 8268 | + } | ||
| 8269 | + /// `Iterator<Item = T>`: `Item` names an associated type of `Iterator`, | ||
| 8270 | + /// not a type in this crate. Only the value is visited. | ||
| 8271 | + fn visit_assoc_type_mut(&mut self, a: &mut syn::AssocType) { | ||
| 8272 | + syn::visit_mut::visit_type_mut(self, &mut a.ty); | ||
| 8273 | + } | ||
| 8274 | + fn visit_assoc_const_mut(&mut self, a: &mut syn::AssocConst) { | ||
| 8275 | + syn::visit_mut::visit_expr_mut(self, &mut a.value); | ||
| 8276 | + } | ||
| 8277 | + /// An `impl` block's own associated items keep their names: `type Output` | ||
| 8278 | + /// in `impl Add for X` is the trait's name for it. | ||
| 8279 | + fn visit_impl_item_type_mut(&mut self, i: &mut syn::ImplItemType) { | ||
| 8280 | + syn::visit_mut::visit_type_mut(self, &mut i.ty); | ||
| 8281 | + } | ||
| 8282 | + fn visit_trait_item_type_mut(&mut self, _: &mut syn::TraitItemType) {} | ||
| 8283 | + /// A field or variant named like a type is still a field or a variant. | ||
| 8284 | + fn visit_field_mut(&mut self, f: &mut syn::Field) { | ||
| 8285 | + syn::visit_mut::visit_type_mut(self, &mut f.ty); | ||
| 8286 | + } | ||
| 8287 | +} | ||