@@ -328,9 +328,13 @@ impl CompileError {
328328 ( ParseErrorType :: OtherError ( msg) , loc, end_loc)
329329 }
330330
331- // Dict literal: `{1:}` / `{1: 2, 3: 4, 5: }` — missing value.
331+ // Dict literal: `{1:}` / `{1: 2, 3: 4, 5: }` — *missing* value. CPython's
332+ // "expression expected after dictionary key and ':'" is specific to a
333+ // genuinely empty value; a present-but-unparseable value (`{1: *}`,
334+ // `{1: 2*}`, `{1: +}`) is plain "invalid syntax", so require emptiness.
332335 ParseErrorType :: ExpectedExpression
333- if is_dict_value_position ( source_text, error. location ) =>
336+ if is_dict_value_position ( source_text, error. location )
337+ && dict_value_is_empty ( source_text, error. location ) =>
334338 {
335339 let ( loc, end_loc) = adjusted_locations ( & source_code, error. location ) ;
336340 let msg = "expression expected after dictionary key and ':'" . to_owned ( ) ;
@@ -1185,42 +1189,55 @@ fn is_bare_star_in_call(source: &str, range: ruff_text_size::TextRange) -> bool
11851189 {
11861190 return false ;
11871191 }
1188- // The token immediately before the error must be `*`.
1189- prefix. trim_end ( ) . ends_with ( '*' )
1192+ // The current argument slot (after the enclosing `(` or the last depth-0
1193+ // comma) must begin with a bare `*`; a `*` after an operand (`f(a *)`, a
1194+ // binary multiply) is plain "invalid syntax".
1195+ let args = & prefix[ open_idx + 1 ..] ;
1196+ let mut d = 0i32 ;
1197+ let mut slot_start = 0 ;
1198+ for ( j, c) in args. char_indices ( ) {
1199+ match c {
1200+ '(' | '[' | '{' => d += 1 ,
1201+ ')' | ']' | '}' => d -= 1 ,
1202+ ',' if d == 0 => slot_start = j + 1 ,
1203+ _ => { }
1204+ }
1205+ }
1206+ slot_starts_with_bare_star ( & args[ slot_start..] )
11901207}
11911208
11921209/// Detect a bare single `*` as the leading element of a set/dict display
11931210/// `{ ... }` or a non-call parenthesised group/tuple `( ... )` — CPython's
1194- /// "Invalid star expression" (`{*}`, `{*, 1}`, `(*)`, `(*,)`, `(*, 1)`). A
1195- /// non-leading star (`{1, *}`, `(1, *)`), a dict value (`{1: *}`), a double
1196- /// star (`{**}`), a subscript/list (`[*]`, handled elsewhere), or a call
1197- /// (`f(*)`, handled elsewhere) is intentionally excluded.
1211+ /// "Invalid star expression" (`{*}`, `{*, 1}`, `(*)`, `(*,)`, `(*, 1)`). The
1212+ /// star must START the (leading) slot: a star following an operand (`{1 *}`, a
1213+ /// binary multiply), a comma (`{1, *}`), or a dict colon (`{1: *}`), as well as
1214+ /// a double star (`{**}`), a subscript/list (`[*]`), or a call (`f(*)`) are
1215+ /// excluded (the last two are handled by `is_invalid_star_in_subscript` /
1216+ /// `is_bare_star_in_call`).
11981217fn is_bare_star_first_in_group ( source : & str , range : ruff_text_size:: TextRange ) -> bool {
11991218 let start: usize = range. start ( ) . into ( ) ;
12001219 let prefix = & source[ ..start] ;
1201- let trimmed = prefix. trim_end ( ) ;
1202- // The token immediately before the error must be a single `*` (not `**`).
1203- if !trimmed. ends_with ( '*' ) || trimmed. ends_with ( "**" ) {
1204- return false ;
1205- }
1206- // Walk back to the nearest depth-0 opener. Bail at a depth-0 `,` (the star
1207- // is not the leading element), `:` (a dict value), or `[` (subscript/list,
1208- // handled by `is_invalid_star_in_subscript`).
1220+ // Walk back to the nearest depth-0 opener; the leading slot (opener..error)
1221+ // must begin with a bare `*`. Bail at a depth-0 `,`/`:` (non-leading slot /
1222+ // dict value) or `[` (subscript/list, handled by `is_invalid_star_in_subscript`).
12091223 let mut depth = 0i32 ;
12101224 for ( i, c) in prefix. char_indices ( ) . rev ( ) {
12111225 match c {
12121226 ')' | ']' | '}' => depth += 1 ,
12131227 ',' | ':' if depth == 0 => return false ,
12141228 '[' if depth == 0 => return false ,
1215- '{' if depth == 0 => return true ,
1229+ '{' if depth == 0 => return slot_starts_with_bare_star ( & prefix [ i + 1 .. ] ) ,
12161230 '(' if depth == 0 => {
12171231 // Only a non-call group: the token before `(` must not be a
12181232 // callee (identifier / `)` / `]`).
12191233 let before = prefix[ ..i] . chars ( ) . rev ( ) . find ( |c| !c. is_whitespace ( ) ) ;
1220- return ! matches ! (
1234+ if matches ! (
12211235 before,
12221236 Some ( c) if c. is_ascii_alphanumeric( ) || c == '_' || c == ')' || c == ']'
1223- ) ;
1237+ ) {
1238+ return false ;
1239+ }
1240+ return slot_starts_with_bare_star ( & prefix[ i + 1 ..] ) ;
12241241 }
12251242 '(' | '{' | '[' => depth -= 1 ,
12261243 _ => { }
@@ -1229,6 +1246,13 @@ fn is_bare_star_first_in_group(source: &str, range: ruff_text_size::TextRange) -
12291246 false
12301247}
12311248
1249+ /// Whether `slot` (the text from an opener to the error) begins with a single
1250+ /// bare `*` (not `**`) — i.e. a leading starred element with no operand.
1251+ fn slot_starts_with_bare_star ( slot : & str ) -> bool {
1252+ let s = slot. trim_start ( ) ;
1253+ s. starts_with ( '*' ) && !s. starts_with ( "**" )
1254+ }
1255+
12321256/// Detect bad target in `except[*] T as <bad>:`. Returns the matching CPython
12331257/// message (e.g. "cannot use except statement with attribute") or `None`.
12341258fn except_as_bad_target_message ( source : & str , range : ruff_text_size:: TextRange ) -> Option < String > {
@@ -1396,6 +1420,35 @@ fn is_dict_value_position(source: &str, range: ruff_text_size::TextRange) -> boo
13961420 matches ! ( last_relevant, Some ( ':' ) )
13971421}
13981422
1423+ /// Whether the dict value position (after the nearest depth-0 `:` inside the
1424+ /// enclosing `{`) is empty — the next non-whitespace character is the `,`/`}`
1425+ /// delimiter (or end of input): `{1:}`, `{1: }`, `{1: ,}`. Only then does
1426+ /// CPython emit "expression expected after dictionary key and ':'"; a value
1427+ /// that is present but fails to parse (`{1: *}`, `{1: 2*}`, `{1: +}`) is plain
1428+ /// "invalid syntax". Used together with [`is_dict_value_position`], which
1429+ /// guarantees the nearest depth-0 separator is the `:`.
1430+ fn dict_value_is_empty ( source : & str , range : ruff_text_size:: TextRange ) -> bool {
1431+ let start: usize = range. start ( ) . into ( ) ;
1432+ let prefix = & source[ ..start] ;
1433+ let mut depth = 0i32 ;
1434+ for ( i, c) in prefix. char_indices ( ) . rev ( ) {
1435+ match c {
1436+ ')' | ']' | '}' => depth += 1 ,
1437+ ',' if depth == 0 => return false ,
1438+ ':' if depth == 0 => {
1439+ return matches ! (
1440+ source[ i + 1 ..] . trim_start( ) . chars( ) . next( ) ,
1441+ None | Some ( ',' | '}' )
1442+ ) ;
1443+ }
1444+ '(' | '[' | '{' if depth == 0 => return false ,
1445+ '(' | '[' | '{' => depth -= 1 ,
1446+ _ => { }
1447+ }
1448+ }
1449+ false
1450+ }
1451+
13991452/// Detect `X=Y` in tuple/list/set literals (i.e. not a function call).
14001453/// CPython suggests "invalid syntax. Maybe you meant '==' or ':=' instead of
14011454/// '='?".
0 commit comments