···289289 def transform_JoinedStr(self, node: ast.JoinedStr) -> FormatString:
290290 format_string_parts: ListT[str] = []
291291 names = []
292292+ # Tracks the unescaped length of the parts seen so far, so error carets stay positioned
293293+ # the same way even though literal braces are doubled in `format_string_parts` below.
294294+ unescaped_length = 0
292295293296 for value in node.values:
294297 if isinstance(value, ast.Str):
295298 assert isinstance(value.s, str)
296296- format_string_parts.append(value.s)
299299+ # Escape literal braces so the reconstructed `format_string` is a valid
300300+ # `str.format()` template; interpolated names are added as `{name}` fields below.
301301+ format_string_parts.append(value.s.replace('{', '{{').replace('}', '}}'))
302302+ unescaped_length += len(value.s)
297303 continue
298304299305 if isinstance(value, ast.FormattedValue):
300300- offset_by = sum(len(n) for n in format_string_parts) + 3
306306+ offset_by = unescaped_length + 3
301307 self.invariant(
302308 value.format_spec is None and value.conversion == -1,
303309 'a format string cannot have a format spec',
···328334 node=node,
329335 )
330336 names.append(name)
331331- format_string_parts.append(f'{{{name.identifier}}}')
337337+ placeholder = f'{{{name.identifier}}}'
338338+ format_string_parts.append(placeholder)
339339+ unescaped_length += len(placeholder)
332340333341 format_string = ''.join(format_string_parts)
334342 return FormatString(span=self.span_for(node), format_string=format_string, names=names)