Skip to content

Commit 897889d

Browse files
UnboundVariableUnboundVariable
andauthored
[ty] Added semantic token support for more identifiers (astral-sh#19473)
I noticed that the semantic token implementation was not handling identifiers in a few cases. This adds support for identifiers that appear in `except`, `case`, `nonlocal`, and `global` statements. Co-authored-by: UnboundVariable <[email protected]>
1 parent cb5a9ff commit 897889d

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed

crates/ty_ide/src/semantic_tokens.rs

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,26 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
664664
}
665665
}
666666
}
667+
ast::Stmt::Nonlocal(nonlocal_stmt) => {
668+
// Handle nonlocal statements - classify identifiers as variables
669+
for identifier in &nonlocal_stmt.names {
670+
self.add_token(
671+
identifier.range(),
672+
SemanticTokenType::Variable,
673+
SemanticTokenModifier::empty(),
674+
);
675+
}
676+
}
677+
ast::Stmt::Global(global_stmt) => {
678+
// Handle global statements - classify identifiers as variables
679+
for identifier in &global_stmt.names {
680+
self.add_token(
681+
identifier.range(),
682+
SemanticTokenType::Variable,
683+
SemanticTokenModifier::empty(),
684+
);
685+
}
686+
}
667687
_ => {
668688
// For all other statement types, let the default visitor handle them
669689
walk_stmt(self, stmt);
@@ -831,6 +851,71 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
831851
}
832852
}
833853
}
854+
855+
fn visit_except_handler(&mut self, except_handler: &ast::ExceptHandler) {
856+
match except_handler {
857+
ast::ExceptHandler::ExceptHandler(handler) => {
858+
// Visit the exception type expression if present
859+
if let Some(type_expr) = &handler.type_ {
860+
self.visit_expr(type_expr);
861+
}
862+
863+
// Handle the exception variable name (after "as")
864+
if let Some(name) = &handler.name {
865+
self.add_token(
866+
name.range(),
867+
SemanticTokenType::Variable,
868+
SemanticTokenModifier::empty(),
869+
);
870+
}
871+
872+
// Visit the handler body
873+
self.visit_body(&handler.body);
874+
}
875+
}
876+
}
877+
878+
fn visit_pattern(&mut self, pattern: &ast::Pattern) {
879+
match pattern {
880+
ast::Pattern::MatchAs(pattern_as) => {
881+
// Visit the nested pattern first to maintain source order
882+
if let Some(nested_pattern) = &pattern_as.pattern {
883+
self.visit_pattern(nested_pattern);
884+
}
885+
886+
// Now add the "as" variable name token
887+
if let Some(name) = &pattern_as.name {
888+
self.add_token(
889+
name.range(),
890+
SemanticTokenType::Variable,
891+
SemanticTokenModifier::empty(),
892+
);
893+
}
894+
}
895+
ast::Pattern::MatchMapping(pattern_mapping) => {
896+
// Visit keys and patterns in source order by interleaving them
897+
for (key, nested_pattern) in
898+
pattern_mapping.keys.iter().zip(&pattern_mapping.patterns)
899+
{
900+
self.visit_expr(key);
901+
self.visit_pattern(nested_pattern);
902+
}
903+
904+
// Handle the rest parameter (after "**") - this comes last
905+
if let Some(rest_name) = &pattern_mapping.rest {
906+
self.add_token(
907+
rest_name.range(),
908+
SemanticTokenType::Variable,
909+
SemanticTokenModifier::empty(),
910+
);
911+
}
912+
}
913+
_ => {
914+
// For all other pattern types, use the default walker
915+
ruff_python_ast::visitor::source_order::walk_pattern(self, pattern);
916+
}
917+
}
918+
}
834919
}
835920

836921
#[cfg(test)]
@@ -1942,4 +2027,200 @@ complex_fstring = f"User: {name.upper()}, Count: {len(data)}, Hex: {value:x}"<CU
19422027
"x" @ 414..415: String
19432028
"#);
19442029
}
2030+
2031+
#[test]
2032+
fn test_nonlocal_and_global_statements() {
2033+
let test = cursor_test(
2034+
r#"
2035+
x = "global_value"
2036+
y = "another_global"
2037+
2038+
def outer():
2039+
x = "outer_value"
2040+
z = "outer_local"
2041+
2042+
def inner():
2043+
nonlocal x, z # These should be variable tokens
2044+
global y # This should be a variable token
2045+
x = "modified"
2046+
y = "modified_global"
2047+
z = "modified_local"
2048+
2049+
def deeper():
2050+
nonlocal x # Variable token
2051+
global y, x # Both should be variable tokens
2052+
return x + y
2053+
2054+
return deeper
2055+
2056+
return inner<CURSOR>
2057+
"#,
2058+
);
2059+
2060+
let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
2061+
2062+
assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
2063+
"x" @ 1..2: Variable
2064+
"/"global_value/"" @ 5..19: String
2065+
"y" @ 20..21: Variable
2066+
"/"another_global/"" @ 24..40: String
2067+
"outer" @ 46..51: Function [definition]
2068+
"x" @ 59..60: Variable
2069+
"/"outer_value/"" @ 63..76: String
2070+
"z" @ 81..82: Variable
2071+
"/"outer_local/"" @ 85..98: String
2072+
"inner" @ 112..117: Function [definition]
2073+
"x" @ 138..139: Variable
2074+
"z" @ 141..142: Variable
2075+
"y" @ 193..194: Variable
2076+
"x" @ 243..244: Variable
2077+
"/"modified/"" @ 247..257: String
2078+
"y" @ 266..267: Variable
2079+
"/"modified_global/"" @ 270..287: String
2080+
"z" @ 296..297: Variable
2081+
"/"modified_local/"" @ 300..316: String
2082+
"deeper" @ 338..344: Function [definition]
2083+
"x" @ 369..370: Variable
2084+
"y" @ 410..411: Variable
2085+
"x" @ 413..414: Variable
2086+
"x" @ 469..470: Variable
2087+
"y" @ 473..474: Variable
2088+
"deeper" @ 499..505: Function
2089+
"inner" @ 522..527: Function
2090+
"#);
2091+
}
2092+
2093+
#[test]
2094+
fn test_nonlocal_global_edge_cases() {
2095+
let test = cursor_test(
2096+
r#"
2097+
# Single variable statements
2098+
def test():
2099+
global x
2100+
nonlocal y
2101+
2102+
# Multiple variables in one statement
2103+
global a, b, c
2104+
nonlocal d, e, f
2105+
2106+
return x + y + a + b + c + d + e + f<CURSOR>
2107+
"#,
2108+
);
2109+
2110+
let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
2111+
2112+
assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
2113+
"test" @ 34..38: Function [definition]
2114+
"x" @ 53..54: Variable
2115+
"y" @ 68..69: Variable
2116+
"a" @ 128..129: Variable
2117+
"b" @ 131..132: Variable
2118+
"c" @ 134..135: Variable
2119+
"d" @ 149..150: Variable
2120+
"e" @ 152..153: Variable
2121+
"f" @ 155..156: Variable
2122+
"x" @ 173..174: Variable
2123+
"y" @ 177..178: Variable
2124+
"a" @ 181..182: Variable
2125+
"b" @ 185..186: Variable
2126+
"c" @ 189..190: Variable
2127+
"d" @ 193..194: Variable
2128+
"e" @ 197..198: Variable
2129+
"f" @ 201..202: Variable
2130+
"#);
2131+
}
2132+
2133+
#[test]
2134+
fn test_pattern_matching() {
2135+
let test = cursor_test(
2136+
r#"
2137+
def process_data(data):
2138+
match data:
2139+
case {"name": name, "age": age, **rest} as person:
2140+
print(f"Person {name}, age {age}, extra: {rest}")
2141+
return person
2142+
case [first, *remaining] as sequence:
2143+
print(f"First: {first}, remaining: {remaining}")
2144+
return sequence
2145+
case value as fallback:
2146+
print(f"Fallback: {fallback}")
2147+
return fallback<CURSOR>
2148+
"#,
2149+
);
2150+
2151+
let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
2152+
2153+
assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
2154+
"process_data" @ 5..17: Function [definition]
2155+
"data" @ 18..22: Parameter
2156+
"data" @ 35..39: Variable
2157+
"/"name/"" @ 55..61: String
2158+
"name" @ 63..67: Variable
2159+
"/"age/"" @ 69..74: String
2160+
"age" @ 76..79: Variable
2161+
"rest" @ 83..87: Variable
2162+
"person" @ 92..98: Variable
2163+
"print" @ 112..117: Function
2164+
"Person " @ 120..127: String
2165+
"name" @ 128..132: Variable
2166+
", age " @ 133..139: String
2167+
"age" @ 140..143: Variable
2168+
", extra: " @ 144..153: String
2169+
"rest" @ 154..158: Variable
2170+
"person" @ 181..187: Variable
2171+
"first" @ 202..207: Variable
2172+
"sequence" @ 224..232: Variable
2173+
"print" @ 246..251: Function
2174+
"First: " @ 254..261: String
2175+
"first" @ 262..267: Variable
2176+
", remaining: " @ 268..281: String
2177+
"remaining" @ 282..291: Variable
2178+
"sequence" @ 314..322: Variable
2179+
"value" @ 336..341: Variable
2180+
"fallback" @ 345..353: Variable
2181+
"print" @ 367..372: Function
2182+
"Fallback: " @ 375..385: String
2183+
"fallback" @ 386..394: Variable
2184+
"fallback" @ 417..425: Variable
2185+
"#);
2186+
}
2187+
2188+
#[test]
2189+
fn test_exception_handlers() {
2190+
let test = cursor_test(
2191+
r#"
2192+
try:
2193+
x = 1 / 0
2194+
except ValueError as ve:
2195+
print(ve)
2196+
except (TypeError, RuntimeError) as re:
2197+
print(re)
2198+
except Exception as e:
2199+
print(e)
2200+
finally:
2201+
pass<CURSOR>
2202+
"#,
2203+
);
2204+
2205+
let tokens = semantic_tokens_full_file(&test.db, test.cursor.file);
2206+
2207+
assert_snapshot!(semantic_tokens_to_snapshot(&test.db, test.cursor.file, &tokens), @r#"
2208+
"x" @ 10..11: Variable
2209+
"1" @ 14..15: Number
2210+
"0" @ 18..19: Number
2211+
"ValueError" @ 27..37: Class
2212+
"ve" @ 41..43: Variable
2213+
"print" @ 49..54: Function
2214+
"ve" @ 55..57: Variable
2215+
"TypeError" @ 67..76: Class
2216+
"RuntimeError" @ 78..90: Class
2217+
"re" @ 95..97: Variable
2218+
"print" @ 103..108: Function
2219+
"re" @ 109..111: Variable
2220+
"Exception" @ 120..129: Class
2221+
"e" @ 133..134: Variable
2222+
"print" @ 140..145: Function
2223+
"e" @ 146..147: Variable
2224+
"#);
2225+
}
19452226
}

0 commit comments

Comments
 (0)