mirror of
				https://github.com/astral-sh/ruff.git
				synced 2025-10-31 12:05:57 +00:00 
			
		
		
		
	[ty] Prefer declared base class attribute over inferred attribute on subclass (#20764)
## Summary When accessing an (instance) attribute on a given class, we were previously traversing its MRO, and building a union of types (if the attribute was available on multiple classes in the MRO) until we found a *definitely bound* symbol. The idea was that possibly unbound symbols in a subclass might only partially shadow the underlying base class attribute. This behavior was problematic for two reasons: * if the attribute was definitely bound on a class (e.g. `self.x = None`), we would have stopped iterating, even if there might be a `x: str | None` declaration in a base class (the bug reported in https://github.com/astral-sh/ty/issues/1067). * if the attribute originated from an implicit instance attribute assignment (e.g. `self.x = 1` in method `Sub.foo`), we might stop looking and miss another implicit instance attribute assignment in a base class method (e.g. `self.x = 2` in method `Base.bar`). With this fix, we still iterate the MRO of the class, but we only stop iterating if we find a *definitely declared* symbol. In this case, we only return the declared attribute type. Otherwise, we keep building a union of inferred attribute types. The implementation here seemed to be the easiest fix for https://github.com/astral-sh/ty/issues/1067 that also kept the ecosystem impact low (the changes that I see all look correct). However, as the Markdown tests show, there are other things to fix in this area. For example, we should do a similar thing for *class attributes*. This is more involved, though (affects many different areas and probably involves a change to our descriptor protocol implementation), so I'd like to postpone this to a follow-up. closes https://github.com/astral-sh/ty/issues/1067 ## Test Plan Updated Markdown tests, including a regression test for https://github.com/astral-sh/ty/issues/1067.
This commit is contained in:
		
							parent
							
								
									c80ee1a50b
								
							
						
					
					
						commit
						9b9c9ae092
					
				
					 5 changed files with 169 additions and 86 deletions
				
			
		|  | @ -1,6 +1,7 @@ | |||
| use ruff_db::files::File; | ||||
| 
 | ||||
| use crate::dunder_all::dunder_all_names; | ||||
| use crate::member::Member; | ||||
| use crate::module_resolver::{KnownModule, file_to_module}; | ||||
| use crate::semantic_index::definition::{Definition, DefinitionState}; | ||||
| use crate::semantic_index::place::{PlaceExprRef, ScopedPlaceId}; | ||||
|  | @ -232,13 +233,9 @@ pub(crate) fn place<'db>( | |||
|     ) | ||||
| } | ||||
| 
 | ||||
| /// Infer the public type of a class symbol (its type as seen from outside its scope) in the given
 | ||||
| /// Infer the public type of a class member/symbol (its type as seen from outside its scope) in the given
 | ||||
| /// `scope`.
 | ||||
| pub(crate) fn class_symbol<'db>( | ||||
|     db: &'db dyn Db, | ||||
|     scope: ScopeId<'db>, | ||||
|     name: &str, | ||||
| ) -> PlaceAndQualifiers<'db> { | ||||
| pub(crate) fn class_member<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Member<'db> { | ||||
|     place_table(db, scope) | ||||
|         .symbol_id(name) | ||||
|         .map(|symbol_id| { | ||||
|  | @ -252,7 +249,7 @@ pub(crate) fn class_symbol<'db>( | |||
| 
 | ||||
|             if !place_and_quals.place.is_unbound() && !place_and_quals.is_init_var() { | ||||
|                 // Trust the declared type if we see a class-level declaration
 | ||||
|                 return place_and_quals; | ||||
|                 return Member::declared(place_and_quals); | ||||
|             } | ||||
| 
 | ||||
|             if let PlaceAndQualifiers { | ||||
|  | @ -267,14 +264,14 @@ pub(crate) fn class_symbol<'db>( | |||
| 
 | ||||
|                 // TODO: we should not need to calculate inferred type second time. This is a temporary
 | ||||
|                 // solution until the notion of Boundness and Declaredness is split. See #16036, #16264
 | ||||
|                 match inferred { | ||||
|                 Member::inferred(match inferred { | ||||
|                     Place::Unbound => Place::Unbound.with_qualifiers(qualifiers), | ||||
|                     Place::Type(_, boundness) => { | ||||
|                         Place::Type(ty, boundness).with_qualifiers(qualifiers) | ||||
|                     } | ||||
|                 } | ||||
|                 }) | ||||
|             } else { | ||||
|                 Place::Unbound.into() | ||||
|                 Member::unbound() | ||||
|             } | ||||
|         }) | ||||
|         .unwrap_or_default() | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 David Peter
						David Peter