Skip to content

[ty] Add go-to definition for dataclass/TypedDict/NamedTuple fields from keyword arguments#24378

Closed
Viicos wants to merge 2 commits intoastral-sh:mainfrom
Viicos:goto-dc-td-nt-kw-args
Closed

[ty] Add go-to definition for dataclass/TypedDict/NamedTuple fields from keyword arguments#24378
Viicos wants to merge 2 commits intoastral-sh:mainfrom
Viicos:goto-dc-td-nt-kw-args

Conversation

@Viicos
Copy link
Copy Markdown
Contributor

@Viicos Viicos commented Apr 2, 2026

Fixes astral-sh/ty#3207.

Also fixes a small unrelated typo.

Summary

Test Plan

@astral-sh-bot astral-sh-bot Bot added the ty Multi-file analysis & type inference label Apr 2, 2026
if resolved_definitions.is_empty()
&& let Some(class_literal) = func_type.as_class_literal()
&& let Some(static_class_literal) = class_literal.as_static()
&& let Some(field_policy) = CodeGeneratorKind::from_class(db, class_literal, None)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: this is a bit of a leaky abstraction, because it assumes CodeGeneratorKind can only be created for classes that support this goto logic. Maybe we could also guard with:

Suggested change
&& let Some(field_policy) = CodeGeneratorKind::from_class(db, class_literal, None)
&& (static_class_literal.is_dataclass_like(db) || static_class_literal.is_typed_dict(db) || static_class_literal.has_named_tuple_class_in_mro(db))
&& let Some(field_policy) = CodeGeneratorKind::from_class(db, class_literal, None)

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Apr 2, 2026

Typing conformance results

No changes detected ✅

Current numbers
The percentage of diagnostics emitted that were expected errors held steady at 87.92%. The percentage of expected errors that received a diagnostic held steady at 83.11%. The number of fully passing files held steady at 78/133.

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Apr 2, 2026

Memory usage report

Memory usage unchanged ✅

Comment on lines +529 to +530
// Go to field definitions if the type is a dataclass-like type, a named tuple or a typed dict. Only do so if no definition was found,
// as having existing definition(s) would mean a custom constructor is defined and as such takes priority:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think we should explicitly check if a custom constructor is set, because with the following:

@dataclass
class A:
    a: int

    def __init__(self, b: int) -> None: ...

A(a<CURSOR>=1)

It would still go to the a field. Maybe not worth the added complexity?

@astral-sh-bot
Copy link
Copy Markdown

astral-sh-bot Bot commented Apr 2, 2026

ecosystem-analyzer results

No diagnostic changes detected ✅

Full report with detailed diff (timing results)

@carljm carljm removed their request for review April 3, 2026 17:17
@MichaReiser MichaReiser assigned MichaReiser and unassigned BurntSushi Apr 3, 2026
@sharkdp sharkdp changed the title Add go-to definition for dataclass/TypedDict/NamedTuple fields from keyword arguments [ty] Add go-to definition for dataclass/TypedDict/NamedTuple fields from keyword arguments Apr 7, 2026
@MichaReiser
Copy link
Copy Markdown
Member

Thank you. I'm not sure this is the right approach as it results in needing multiple special casing:

  1. Here, to make go to parameter work
  2. In hover: we want to show the constructor signature when hovering a typed dict constructor, including all its optional arguments
  3. In signature completion: The same as for hover, we want to suggest all optional parameters.

I'm not entirely sure how to integrate this best. Ideally, we'd do something when resolving the constructor call. @oconnor663 I think you worked on TypedDict. Do you think we could synthesize the constructor function somehow during type inference and if so, where would we add this?

@AlexWaygood
Copy link
Copy Markdown
Member

Do you think we could synthesize the constructor function somehow during type inference and if so, where would we add this?

We already do this -- here for dataclasses:

(CodeGeneratorKind::DataclassLike(_), "__init__") => {
if !self.has_dataclass_param(db, field_policy, DataclassFlags::INIT) {
return None;
}
let self_parameter = Parameter::positional_or_keyword(Name::new_static("self"))
// TODO: could be `Self`.
.with_annotated_type(instance_ty);
signature_from_fields(vec![self_parameter], Type::none(db))
}

Here for class-based NamedTuples:

(
CodeGeneratorKind::NamedTuple,
"__new__" | "__init__" | "_replace" | "__replace__" | "_fields",
) if self.namedtuple_base_has_unknown_fields(db) => {
// When the namedtuple base has unknown fields, fall back to NamedTupleFallback
// which has generic signatures that accept any arguments.
KnownClass::NamedTupleFallback
.to_class_literal(db)
.as_class_literal()?
.as_static()?
.own_class_member(db, inherited_generic_context, None, name)
.ignore_possibly_undefined()
.map(|ty| {
ty.apply_type_mapping(
db,
&TypeMapping::ReplaceSelf {
new_upper_bound: instance_ty,
},
TypeContext::default(),
)
})
}
(
CodeGeneratorKind::NamedTuple,
"__new__" | "_replace" | "__replace__" | "_fields" | "__slots__",
) => {
let fields = self.fields(db, specialization, field_policy);
let fields_iter = fields.iter().map(|(name, field)| {
let default_ty = match &field.kind {
FieldKind::NamedTuple { default_ty } => *default_ty,
_ => None,
};
NamedTupleField {
name: name.clone(),
ty: field.declared_ty,
default: default_ty,
}
});
synthesize_namedtuple_class_member(
db,
name,
instance_ty,
fields_iter,
specialization.map(|s| s.generic_context(db)),
)
}

Here for functional namedtuples:

let result = synthesize_namedtuple_class_member(
db,
name,
instance_ty,
self.fields(db).iter().cloned(),
None,
);
// For fallback members from NamedTupleFallback, apply type mapping to handle
// `Self` types. The explicitly synthesized members (__new__, _fields, _replace,
// __replace__) don't need this mapping.
if matches!(
name,
"__new__" | "_fields" | "_replace" | "__replace__" | "__slots__"
) {
result
} else {
result.map(|ty| {
ty.apply_type_mapping(
db,
&TypeMapping::ReplaceSelf {
new_upper_bound: instance_ty,
},
TypeContext::default(),
)
})
}
}

I'm not sure where TypedDict constructors get synthesized off the top of my head, but there must be a similar hook somewhere in type inference

@Viicos
Copy link
Copy Markdown
Contributor Author

Viicos commented Apr 12, 2026

Yes to clarify, this doesn't require the synthesized signatures to be available at all. The definitions_for_keyword_argument() function I extended here already did this for non-synthesized (i.e. explicitly defined in code) constructors:

class CustomClass:
    def __init__(self, a: int) -> None: ...

# Trigger go-to on a keyworg argument will go to `a` parameter in `CustomClass.__init__()`:
CustomClass(a=1)

Playground

For synthesized methods, this condition in definitions_for_keyword_argument():

if let Some(parameter_range) =
find_parameter_range(&function_node.parameters, keyword_name_str)
{

is never reached because synthesized constructors parameters don't have any range. We end up with an empty resolved_definitions vec, and so my added code path gets triggered. It only relies on the field definitions, not the constructor.

@Viicos Viicos force-pushed the goto-dc-td-nt-kw-args branch from 83a6ef7 to fa7784d Compare April 12, 2026 11:43
@MichaReiser
Copy link
Copy Markdown
Member

That makes sense. I still think it would be nice to avoid special casing typed dict in the IDE code and instead expose the necessary information in synthesized constructors.

For example, what if we add a origin_definition: Option<Definition> to Parameter and set it when synthesizing the constructor? We could even consider doing this for non-synthesized constructors too (could it then even be required). @carljm does this sound reasonable?

@carljm
Copy link
Copy Markdown
Contributor

carljm commented Apr 14, 2026

Tracking an optional definition with Parameter seems reasonable to me. I suspect we can't make it required, because e.g. a Callable[...] type annotation defines parameters, but there is no associated Definition available.

@MichaReiser
Copy link
Copy Markdown
Member

I went ahead and implemented my described approach in #24897

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Go to definition for dataclass/TypedDict/NamedTuple fields

5 participants