[ty] handle Annotated metadata in semantic tokens (#24890)

Fixes https://github.com/astral-sh/ty/issues/3362

## Summary

Fix semantic token classification for `typing.Annotated` metadata.

## Test Plan

Added test.
This commit is contained in:
Denys Zhak
2026-04-28 11:44:28 +02:00
committed by GitHub
parent 87443c22d4
commit 5a74b374a0
+69 -7
View File
@@ -14,9 +14,6 @@
//!
//! TODO: Need to handle semantic tokens within quoted annotations.
//!
//! TODO: Need to properly handle Annotated expressions. All type arguments other
//! than the first should be treated as value expressions, not as type expressions.
//!
//! TODO: Properties (or perhaps more generally, descriptor objects?) should be
//! classified as property tokens rather than just variables.
//!
@@ -625,6 +622,35 @@ impl<'db> SemanticTokenVisitor<'db> {
}
}
}
fn visit_expr_with_type_form(&mut self, expr: &Expr, in_type_form: bool) {
let prev_in_type_form = self.in_type_form;
self.in_type_form = in_type_form;
self.visit_expr(expr);
self.in_type_form = prev_in_type_form;
}
fn visit_value_expression(&mut self, expr: &Expr) {
self.visit_expr_with_type_form(expr, false);
}
fn visit_annotated_arguments(&mut self, slice: &Expr) {
let ast::Expr::Tuple(tuple) = slice else {
self.visit_annotation(slice);
return;
};
let Some((annotation, metadata)) = tuple.elts.split_first() else {
self.visit_annotation(slice);
return;
};
self.visit_annotation(annotation);
for metadata_element in metadata {
self.visit_value_expression(metadata_element);
}
}
}
impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
@@ -882,10 +908,7 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
/// Visit an annotation or other expression that should be interpreted as a type form.
fn visit_annotation(&mut self, expr: &'_ Expr) {
let prev_in_type_form = self.in_type_form;
self.in_type_form = true;
self.visit_expr(expr);
self.in_type_form = prev_in_type_form;
self.visit_expr_with_type_form(expr, true);
}
fn visit_expr(&mut self, expr: &Expr) {
@@ -962,6 +985,15 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
walk_expr(self, expr);
}
}
ast::Expr::Subscript(subscript)
if matches!(
subscript.value.inferred_type(self.model),
Some(Type::SpecialForm(SpecialFormType::Annotated))
) =>
{
self.visit_expr(subscript.value.as_ref());
self.visit_annotated_arguments(subscript.slice.as_ref());
}
ast::Expr::Call(call) => {
self.visit_expr(call.func.as_ref());
@@ -1226,6 +1258,36 @@ mod tests {
assert_snapshot!(test.to_snapshot(&tokens), @r#""Foo" @ 6..9: Class [definition]"#);
}
#[test]
fn semantic_tokens_annotated_metadata() {
let test = SemanticTokenTest::new(
"
from typing import Annotated
class Metadata:
field = 1
def f(x: Annotated[int, Metadata.field]): ...
",
);
let tokens = test.highlight_file();
assert_snapshot!(test.to_snapshot(&tokens), @r#"
"typing" @ 6..12: Namespace
"Annotated" @ 20..29: Variable
"Metadata" @ 37..45: Class [definition]
"field" @ 51..56: Variable [definition]
"1" @ 59..60: Number
"f" @ 66..67: Function [definition]
"x" @ 68..69: Parameter [definition]
"Annotated" @ 71..80: Variable
"int" @ 81..84: Class
"Metadata" @ 86..94: Class
"field" @ 95..100: Variable
"#);
}
#[test]
fn semantic_tokens_match_class_pattern_keyword_before_positional() {
// Regression test for https://github.com/astral-sh/ty/issues/2417