From 9bbf4987e86690b7181aaa7992d7c757e2fac64d Mon Sep 17 00:00:00 2001 From: Dylan Date: Fri, 30 May 2025 15:00:56 -0500 Subject: [PATCH] Implement template strings (#17851) This PR implements template strings (t-strings) in the parser and formatter for Ruff. Minimal changes necessary to compile were made in other parts of the code (e.g. ty, the linter, etc.). These will be covered properly in follow-up PRs. --- .../test/fixtures/flake8_bandit/S104.py | 5 + .../test/fixtures/flake8_bandit/S108.py | 4 + .../test/fixtures/flake8_bandit/S608.py | 10 + .../test/fixtures/flake8_pyi/PYI053.py | 2 + .../test/fixtures/flake8_pyi/PYI053.pyi | 4 + .../fixtures/flake8_quotes/doubles_escaped.py | 24 + .../fixtures/flake8_quotes/singles_escaped.py | 22 + .../test/fixtures/pycodestyle/W605_1.py | 71 +- crates/ruff_linter/src/checkers/ast/mod.rs | 21 +- .../rules/hardcoded_bind_all_interfaces.rs | 3 + .../rules/hardcoded_sql_expression.rs | 11 +- .../rules/hardcoded_tmp_directory.rs | 3 + .../rules/suspicious_function_call.rs | 6 +- ...s__flake8_bandit__tests__S104_S104.py.snap | 7 +- ...s__flake8_bandit__tests__S608_S608.py.snap | 9 + .../rules/string_or_bytes_too_long.rs | 11 +- .../flake8_pytest_style/rules/helpers.rs | 16 +- .../rules/avoidable_escaped_quote.rs | 90 +- .../rules/check_string_quotes.rs | 5 +- .../rules/unnecessary_escaped_quote.rs | 35 +- ...quire_doubles_over_singles_escaped.py.snap | 183 + ...re_doubles_over_singles_escaped_py311.snap | 179 +- ...quire_singles_over_doubles_escaped.py.snap | 204 ++ ...re_singles_over_doubles_escaped_py311.snap | 200 +- crates/ruff_linter/src/rules/flynt/helpers.rs | 30 +- .../flynt/rules/static_join_to_fstring.rs | 4 +- .../rules/invalid_escape_sequence.rs | 83 +- ...s__pycodestyle__tests__W605_W605_1.py.snap | 345 +- .../rules/f_string_missing_placeholders.rs | 2 +- .../pylint/rules/assert_on_string_literal.rs | 16 +- .../pyupgrade/rules/use_pep604_annotation.rs | 1 + .../src/rules/refurb/rules/bit_count.rs | 1 + .../ruff/rules/ambiguous_unicode_character.rs | 7 +- .../ruff/rules/assert_with_print_message.rs | 50 +- .../explicit_f_string_type_conversion.rs | 4 +- .../invalid_formatter_suppression_comment.rs | 8 +- .../ruff/rules/missing_fstring_syntax.rs | 2 +- crates/ruff_python_ast/ast.toml | 22 +- crates/ruff_python_ast/generate.py | 5 +- crates/ruff_python_ast/src/comparable.rs | 200 +- crates/ruff_python_ast/src/expression.rs | 59 +- crates/ruff_python_ast/src/generated.rs | 296 +- crates/ruff_python_ast/src/helpers.rs | 111 +- crates/ruff_python_ast/src/node.rs | 56 +- crates/ruff_python_ast/src/nodes.rs | 518 ++- .../src/operator_precedence.rs | 3 +- crates/ruff_python_ast/src/python_version.rs | 7 + crates/ruff_python_ast/src/relocate.rs | 3 + crates/ruff_python_ast/src/str.rs | 2 +- crates/ruff_python_ast/src/str_prefix.rs | 47 + crates/ruff_python_ast/src/visitor.rs | 49 +- .../src/visitor/source_order.rs | 41 +- .../src/visitor/transformer.rs | 49 +- .../tests/comparable.rs | 91 +- .../snapshots/source_order__f_strings.snap | 13 +- .../snapshots/source_order__t_strings.snap | 17 + .../tests/snapshots/visitor__f_strings.snap | 13 +- .../tests/snapshots/visitor__t_strings.snap | 16 + .../tests/source_order.rs | 9 + .../tests/visitor.rs | 28 +- crates/ruff_python_codegen/src/generator.rs | 64 +- crates/ruff_python_formatter/generate.py | 6 +- .../test/fixtures/ruff/expression/binary.py | 13 + .../join_implicit_concatenated_string.py | 65 + ...implicit_concatenated_string_assignment.py | 151 +- .../ruff/expression/tstring.options.json | 1 + .../test/fixtures/ruff/expression/tstring.py | 731 ++++ crates/ruff_python_formatter/src/builders.rs | 4 +- .../src/comments/placement.rs | 19 +- crates/ruff_python_formatter/src/context.rs | 49 +- .../src/expression/expr_f_string.rs | 8 +- .../src/expression/expr_t_string.rs | 59 + .../src/expression/mod.rs | 8 + crates/ruff_python_formatter/src/generated.rs | 64 + .../src/other/f_string.rs | 88 +- .../src/other/interpolated_string.rs | 73 + ...ment.rs => interpolated_string_element.rs} | 77 +- crates/ruff_python_formatter/src/other/mod.rs | 4 +- .../src/other/t_string.rs | 40 + .../ruff_python_formatter/src/pattern/mod.rs | 3 +- crates/ruff_python_formatter/src/range.rs | 8 +- .../src/statement/stmt_assign.rs | 194 +- .../src/string/implicit.rs | 104 +- .../ruff_python_formatter/src/string/mod.rs | 90 +- .../src/string/normalize.rs | 219 +- .../ruff_python_formatter/tests/normalizer.rs | 26 +- .../format@expression__binary.py.snap | 31 + ..._join_implicit_concatenated_string.py.snap | 127 +- ...cit_concatenated_string_assignment.py.snap | 318 +- .../format@expression__tstring.py.snap | 1536 ++++++++ .../inline/err/t_string_empty_expression.py | 3 + ...string_invalid_conversion_flag_name_tok.py | 2 + ...tring_invalid_conversion_flag_other_tok.py | 3 + .../err/t_string_invalid_starred_expr.py | 5 + .../t_string_lambda_without_parentheses.py | 2 + .../inline/err/t_string_unclosed_lbrace.py | 6 + ...t_string_unclosed_lbrace_in_format_spec.py | 3 + .../inline/ok/param_with_annotation.py | 1 - .../inline/ok/pep750_t_string_py314.py | 10 + .../resources/valid/expressions/t_string.py | 74 + crates/ruff_python_parser/src/error.rs | 58 +- crates/ruff_python_parser/src/lexer.rs | 384 +- .../{fstring.rs => interpolated_string.rs} | 68 +- crates/ruff_python_parser/src/lib.rs | 4 +- .../src/parser/expression.rs | 337 +- .../ruff_python_parser/src/parser/helpers.rs | 1 + crates/ruff_python_parser/src/parser/mod.rs | 61 +- .../ruff_python_parser/src/parser/pattern.rs | 2 +- .../src/parser/statement.rs | 2 +- ..._parser__lexer__tests__empty_tstrings.snap | 98 + ..._python_parser__lexer__tests__fstring.snap | 9 +- ...arser__lexer__tests__fstring_comments.snap | 5 +- ...ser__lexer__tests__fstring_conversion.snap | 9 +- ..._parser__lexer__tests__fstring_escape.snap | 7 +- ...__lexer__tests__fstring_escape_braces.snap | 9 +- ...ser__lexer__tests__fstring_escape_raw.snap | 7 +- ...__tests__fstring_expression_multiline.snap | 5 +- ...rser__lexer__tests__fstring_multiline.snap | 11 +- ...__lexer__tests__fstring_named_unicode.snap | 3 +- ...xer__tests__fstring_named_unicode_raw.snap | 5 +- ..._parser__lexer__tests__fstring_nested.snap | 15 +- ...er__lexer__tests__fstring_parentheses.snap | 17 +- ...__fstring_single_quote_escape_mac_eol.snap | 3 +- ..._fstring_single_quote_escape_unix_eol.snap | 3 +- ...tring_single_quote_escape_windows_eol.snap | 3 +- ...exer__tests__fstring_with_format_spec.snap | 17 +- ...ests__fstring_with_ipy_escape_command.snap | 5 +- ...s__fstring_with_multiline_format_spec.snap | 25 +- ..._tests__fstring_with_named_expression.snap | 9 +- ...__lexer__tests__fstring_with_nul_char.snap | 3 +- ...r__lexer__tests__nested_t_and_fstring.snap | 226 ++ ..._python_parser__lexer__tests__tstring.snap | 105 + ...arser__lexer__tests__tstring_comments.snap | 71 + ...ser__lexer__tests__tstring_conversion.snap | 133 + ..._parser__lexer__tests__tstring_escape.snap | 86 + ...__lexer__tests__tstring_escape_braces.snap | 133 + ...ser__lexer__tests__tstring_escape_raw.snap | 86 + ...__tests__tstring_expression_multiline.snap | 85 + ...rser__lexer__tests__tstring_multiline.snap | 136 + ...__lexer__tests__tstring_named_unicode.snap | 36 + ...xer__tests__tstring_named_unicode_raw.snap | 59 + ..._parser__lexer__tests__tstring_nested.snap | 216 ++ ...er__lexer__tests__tstring_parentheses.snap | 209 ++ ..._parser__lexer__tests__tstring_prefix.snap | 153 + ...__tstring_single_quote_escape_mac_eol.snap | 36 + ..._tstring_single_quote_escape_unix_eol.snap | 36 + ...tring_single_quote_escape_windows_eol.snap | 36 + ...exer__tests__tstring_with_format_spec.snap | 289 ++ ...ests__tstring_with_ipy_escape_command.snap | 63 + ...tests__tstring_with_lambda_expression.snap | 125 + ...s__tstring_with_multiline_format_spec.snap | 295 ++ ..._tests__tstring_with_named_expression.snap | 187 + ...__lexer__tests__tstring_with_nul_char.snap | 36 + ...string__tests__fstring_constant_range.snap | 15 +- ...ing__tests__fstring_escaped_character.snap | 7 +- ...tring__tests__fstring_escaped_newline.snap | 7 +- ...ing__tests__fstring_line_continuation.snap | 7 +- ...__fstring_parse_self_documenting_base.snap | 5 +- ...ring_parse_self_documenting_base_more.snap | 13 +- ...fstring_parse_self_documenting_format.snap | 9 +- ...ing__tests__fstring_unescaped_newline.snap | 7 +- ...r__string__tests__parse_empty_fstring.snap | 1 - ...r__string__tests__parse_empty_tstring.snap | 31 + ...tring__tests__parse_f_string_concat_1.snap | 3 +- ...tring__tests__parse_f_string_concat_2.snap | 3 +- ...tring__tests__parse_f_string_concat_3.snap | 7 +- ...tring__tests__parse_f_string_concat_4.snap | 7 +- ...ing__tests__parse_f_t_string_concat_1.snap | 58 + ...ing__tests__parse_f_t_string_concat_2.snap | 69 + ..._parser__string__tests__parse_fstring.snap | 11 +- ...__string__tests__parse_fstring_equals.snap | 5 +- ...ring_nested_concatenation_string_spec.snap | 11 +- ...ing__tests__parse_fstring_nested_spec.snap | 11 +- ...sts__parse_fstring_nested_string_spec.snap | 11 +- ...ring__tests__parse_fstring_not_equals.snap | 5 +- ..._tests__parse_fstring_not_nested_spec.snap | 9 +- ...ts__parse_fstring_self_doc_prec_space.snap | 5 +- ...parse_fstring_self_doc_trailing_space.snap | 5 +- ...ring__tests__parse_fstring_yield_expr.snap | 5 +- ...tring__tests__parse_t_string_concat_1.snap | 51 + ...tring__tests__parse_t_string_concat_2.snap | 51 + ...tring__tests__parse_t_string_concat_3.snap | 77 + ...tring__tests__parse_t_string_concat_4.snap | 88 + ..._parser__string__tests__parse_tstring.snap | 68 + ...__string__tests__parse_tstring_equals.snap | 66 + ...ring_nested_concatenation_string_spec.snap | 93 + ...ing__tests__parse_tstring_nested_spec.snap | 68 + ...sts__parse_tstring_nested_string_spec.snap | 79 + ...ring__tests__parse_tstring_not_equals.snap | 66 + ..._tests__parse_tstring_not_nested_spec.snap | 59 + ...ts__parse_tstring_self_doc_prec_space.snap | 52 + ...parse_tstring_self_doc_trailing_space.snap | 52 + ...ring__tests__parse_tstring_yield_expr.snap | 46 + ...ing__tests__parse_u_f_string_concat_1.snap | 3 +- ...ing__tests__parse_u_f_string_concat_2.snap | 3 +- ...ing__tests__parse_u_t_string_concat_1.snap | 51 + ...ing__tests__parse_u_t_string_concat_2.snap | 62 + ...on_parser__string__tests__raw_fstring.snap | 5 +- ...on_parser__string__tests__raw_tstring.snap | 49 + ...ing__tests__triple_quoted_raw_fstring.snap | 5 +- ...ing__tests__triple_quoted_raw_tstring.snap | 49 + ...string__tests__tstring_constant_range.snap | 80 + ...ing__tests__tstring_escaped_character.snap | 53 + ...tring__tests__tstring_escaped_newline.snap | 53 + ...ing__tests__tstring_line_continuation.snap | 55 + ...__tstring_parse_self_documenting_base.snap | 52 + ...ring_parse_self_documenting_base_more.snap | 84 + ...tstring_parse_self_documenting_format.snap | 64 + ...ing__tests__tstring_unescaped_newline.snap | 53 + crates/ruff_python_parser/src/string.rs | 313 +- crates/ruff_python_parser/src/token.rs | 62 +- crates/ruff_python_parser/tests/fixtures.rs | 4 +- ...ann_assign_stmt_invalid_annotation.py.snap | 21 +- ...d_syntax@f_string_empty_expression.py.snap | 9 +- ...g_invalid_conversion_flag_name_tok.py.snap | 5 +- ..._invalid_conversion_flag_other_tok.py.snap | 9 +- ...ntax@f_string_invalid_starred_expr.py.snap | 13 +- ..._string_lambda_without_parentheses.py.snap | 9 +- ...id_syntax@f_string_unclosed_lbrace.py.snap | 20 +- ...ing_unclosed_lbrace_in_format_spec.py.snap | 19 +- ...x@function_def_invalid_return_expr.py.snap | 10 + ...y_concatenated_unterminated_string.py.snap | 6 +- ...ated_unterminated_string_multiline.py.snap | 10 +- ...syntax@invalid_annotation_function.py.snap | 16 +- ...ax@invalid_fstring_literal_element.py.snap | 5 +- ...mixed_bytes_and_non_bytes_literals.py.snap | 5 +- ...ntax@param_with_invalid_annotation.py.snap | 10 + ...param_with_invalid_star_annotation.py.snap | 11 + ...valid_syntax@pep701_f_string_py311.py.snap | 72 +- ...nvalid_syntax@re_lex_logical_token.py.snap | 6 +- ...x@re_lexing__fstring_format_spec_1.py.snap | 30 +- ...re_lexing__triple_quoted_fstring_1.py.snap | 6 +- ...re_lexing__triple_quoted_fstring_2.py.snap | 9 +- ...re_lexing__triple_quoted_fstring_3.py.snap | 9 +- ...ements__invalid_assignment_targets.py.snap | 14 +- ...nvalid_augmented_assignment_target.py.snap | 14 +- ...d_syntax@t_string_empty_expression.py.snap | 113 + ...g_invalid_conversion_flag_name_tok.py.snap | 63 + ..._invalid_conversion_flag_other_tok.py.snap | 113 + ...ntax@t_string_invalid_starred_expr.py.snap | 205 ++ ..._string_lambda_without_parentheses.py.snap | 118 + ...id_syntax@t_string_unclosed_lbrace.py.snap | 359 ++ ...ing_unclosed_lbrace_in_format_spec.py.snap | 142 + ...erminated_fstring_newline_recovery.py.snap | 20 +- ...invalid_syntax@write_to_debug_expr.py.snap | 4 +- ...valid_syntax@expressions__f_string.py.snap | 275 +- ...valid_syntax@expressions__t_string.py.snap | 3127 +++++++++++++++++ ...tax@fstring_format_spec_terminator.py.snap | 23 +- ...syntax@match_classify_as_keyword_1.py.snap | 7 +- ...valid_syntax@param_with_annotation.py.snap | 68 +- ...valid_syntax@pep701_f_string_py311.py.snap | 70 +- ...valid_syntax@pep701_f_string_py312.py.snap | 60 +- ...valid_syntax@pep750_t_string_py314.py.snap | 584 +++ ...atement__ambiguous_lpar_with_items.py.snap | 13 +- .../valid_syntax@statement__match.py.snap | 4 +- .../valid_syntax@statement__try.py.snap | 37 +- .../src/analyze/type_inference.rs | 1 + crates/ruff_python_semantic/src/model.rs | 20 +- .../src/semantic_index/re_exports.rs | 2 + .../ty_python_semantic/src/semantic_model.rs | 2 + crates/ty_python_semantic/src/types/infer.rs | 70 +- 261 files changed, 18023 insertions(+), 1802 deletions(-) create mode 100644 crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__t_strings.snap create mode 100644 crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__t_strings.snap create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.options.json create mode 100644 crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py create mode 100644 crates/ruff_python_formatter/src/expression/expr_t_string.rs create mode 100644 crates/ruff_python_formatter/src/other/interpolated_string.rs rename crates/ruff_python_formatter/src/other/{f_string_element.rs => interpolated_string_element.rs} (80%) create mode 100644 crates/ruff_python_formatter/src/other/t_string.rs create mode 100644 crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap create mode 100644 crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py create mode 100644 crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py create mode 100644 crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py create mode 100644 crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py create mode 100644 crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py create mode 100644 crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py create mode 100644 crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py create mode 100644 crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py create mode 100644 crates/ruff_python_parser/resources/valid/expressions/t_string.py rename crates/ruff_python_parser/src/lexer/{fstring.rs => interpolated_string.rs} (61%) create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap create mode 100644 crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap create mode 100644 crates/ruff_python_parser/tests/snapshots/valid_syntax@pep750_t_string_py314.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S104.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S104.py index d7f9716ef1..9dcd08ecec 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S104.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S104.py @@ -22,3 +22,8 @@ def my_func(): # Implicit string concatenation "0.0.0.0" f"0.0.0.0{expr}0.0.0.0" + +# t-strings - all ok +t"0.0.0.0" +"0.0.0.0" t"0.0.0.0{expr}0.0.0.0" +"0.0.0.0" f"0.0.0.0{expr}0.0.0.0" t"0.0.0.0{expr}0.0.0.0" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S108.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S108.py index 610a6700cd..ca73cd6879 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S108.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S108.py @@ -40,3 +40,7 @@ with tempfile.TemporaryDirectory(dir="/dev/shm") as d: with TemporaryDirectory(dir="/tmp") as d: pass + +# ok (runtime error from t-string) +with open(t"/foo/bar", "w") as f: + f.write("def") diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py index 447e46dcf2..620a18c038 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_bandit/S608.py @@ -169,3 +169,13 @@ query60 = f""" # https://github.com/astral-sh/ruff/issues/17967 query61 = f"SELECT * FROM table" # skip expressionless f-strings + +# t-strings +query62 = t"SELECT * FROM table" +query63 = t""" + SELECT *, + foo + FROM ({user_input}) raw +""" +query64 = f"update {t"{table}"} set var = {t"{var}"}" +query65 = t"update {f"{table}"} set var = {f"{var}"}" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.py b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.py index 12cac66d13..b3fbf5ab9e 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.py @@ -72,3 +72,5 @@ def not_warnings_dot_deprecated( @not_warnings_dot_deprecated("Not warnings.deprecated, so this one *should* lead to PYI053 in a stub!") def not_a_deprecated_function() -> None: ... + +baz: str = t"51 character stringgggggggggggggggggggggggggggggggg" diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi index 5cb3585c57..caf9f55e97 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pyi/PYI053.pyi @@ -80,3 +80,7 @@ x: TypeAlias = Literal["fooooooooooooooooooooooooooooooooooooooooooooooooooooooo # Ok y: TypeAlias = Annotated[int, "metadataaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"] + +ttoo: str = t"50 character stringggggggggggggggggggggggggggggggg" # OK + +tbar: str = t"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053 diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped.py index 7f789a22fb..92a2744f4b 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/doubles_escaped.py @@ -39,3 +39,27 @@ f'\'normal\' {f'nested'} normal' # Q003 f'\'normal\' {f'nested'} "double quotes"' f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003 f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l + + + +# Same as above, but with t-strings +t'This is a \'string\'' # Q003 +t'This is \\ a \\\'string\'' # Q003 +t'"This" is a \'string\'' +f"This is a 'string'" +f"\"This\" is a 'string'" +fr'This is a \'string\'' +fR'This is a \'string\'' +foo = ( + t'This is a' + t'\'string\'' # Q003 +) +t'\'foo\' {'nested'}' # Q003 +t'\'foo\' {t'nested'}' # Q003 +t'\'foo\' {t'\'nested\''} \'\'' # Q003 + +t'normal {t'nested'} normal' +t'\'normal\' {t'nested'} normal' # Q003 +t'\'normal\' {t'nested'} "double quotes"' +t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 +t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped.py b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped.py index 815db5bdb7..d68017f380 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_quotes/singles_escaped.py @@ -37,3 +37,25 @@ f"\"normal\" {f"nested"} normal" # Q003 f"\"normal\" {f"nested"} 'single quotes'" f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003 f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003 + + +# Same as above, but with t-strings +t"This is a \"string\"" +t"'This' is a \"string\"" +f'This is a "string"' +f'\'This\' is a "string"' +fr"This is a \"string\"" +fR"This is a \"string\"" +foo = ( + t"This is a" + t"\"string\"" +) +t"\"foo\" {"foo"}" # Q003 +t"\"foo\" {t"foo"}" # Q003 +t"\"foo\" {t"\"foo\""} \"\"" # Q003 + +t"normal {t"nested"} normal" +t"\"normal\" {t"nested"} normal" # Q003 +t"\"normal\" {t"nested"} 'single quotes'" +t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 +t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py index 735881301e..c29b7d53ec 100644 --- a/crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/W605_1.py @@ -1,4 +1,4 @@ -# Same as `W605_0.py` but using f-strings instead. +# Same as `W605_0.py` but using f-strings and t-strings instead. #: W605:1:10 regex = f'\.png$' @@ -66,3 +66,72 @@ s = f"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n" # Debug text (should trigger) t = f"{'\InHere'=}" + + + +#: W605:1:10 +regex = t'\.png$' + +#: W605:2:1 +regex = t''' +\.png$ +''' + +#: W605:2:6 +f( + t'\_' +) + +#: W605:4:6 +t""" +multi-line +literal +with \_ somewhere +in the middle +""" + +#: W605:1:38 +value = t'new line\nand invalid escape \_ here' + + +#: Okay +regex = fr'\.png$' +regex = t'\\.png$' +regex = fr''' +\.png$ +''' +regex = fr''' +\\.png$ +''' +s = t'\\' +regex = t'\w' # noqa +regex = t''' +\w +''' # noqa + +regex = t'\\\_' +value = t'\{{1}}' +value = t'\{1}' +value = t'{1:\}' +value = t"{t"\{1}"}" +value = rt"{t"\{1}"}" + +# Okay +value = rt'\{{1}}' +value = rt'\{1}' +value = rt'{1:\}' +value = t"{rt"\{1}"}" + +# Regression tests for https://github.com/astral-sh/ruff/issues/10434 +t"{{}}+-\d" +t"\n{{}}+-\d+" +t"\n{{}}�+-\d+" + +# See https://github.com/astral-sh/ruff/issues/11491 +total = 10 +ok = 7 +incomplete = 3 +s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n" + +# Debug text (should trigger) +t = t"{'\InHere'=}" diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index db57ecc87f..3ca0b6ed5b 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -37,8 +37,8 @@ use ruff_python_ast::str::Quote; use ruff_python_ast::visitor::{Visitor, walk_except_handler, walk_pattern}; use ruff_python_ast::{ self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr, - ExprContext, FStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters, Pattern, - PythonVersion, Stmt, Suite, UnaryOp, + ExprContext, InterpolatedStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters, + Pattern, PythonVersion, Stmt, Suite, UnaryOp, }; use ruff_python_ast::{PySourceType, helpers, str, visitor}; use ruff_python_codegen::{Generator, Stylist}; @@ -338,6 +338,7 @@ impl<'a> Checker<'a> { ast::BytesLiteralFlags::empty().with_quote_style(self.preferred_quote()) } + // TODO(dylan) add similar method for t-strings /// Return the default f-string flags a generated `FString` node should use, given where we are /// in the AST. pub(crate) fn default_fstring_flags(&self) -> ast::FStringFlags { @@ -1897,6 +1898,10 @@ impl<'a> Visitor<'a> for Checker<'a> { self.semantic.flags |= SemanticModelFlags::F_STRING; visitor::walk_expr(self, expr); } + Expr::TString(_) => { + self.semantic.flags |= SemanticModelFlags::T_STRING; + visitor::walk_expr(self, expr); + } Expr::Named(ast::ExprNamed { target, value, @@ -1930,6 +1935,7 @@ impl<'a> Visitor<'a> for Checker<'a> { } Expr::BytesLiteral(bytes_literal) => analyze::string_like(bytes_literal.into(), self), Expr::FString(f_string) => analyze::string_like(f_string.into(), self), + Expr::TString(t_string) => analyze::string_like(t_string.into(), self), _ => {} } @@ -2119,12 +2125,15 @@ impl<'a> Visitor<'a> for Checker<'a> { } } - fn visit_f_string_element(&mut self, f_string_element: &'a FStringElement) { + fn visit_interpolated_string_element( + &mut self, + interpolated_string_element: &'a InterpolatedStringElement, + ) { let snapshot = self.semantic.flags; - if f_string_element.is_expression() { - self.semantic.flags |= SemanticModelFlags::F_STRING_REPLACEMENT_FIELD; + if interpolated_string_element.is_interpolation() { + self.semantic.flags |= SemanticModelFlags::INTERPOLATED_STRING_REPLACEMENT_FIELD; } - visitor::walk_f_string_element(self, f_string_element); + visitor::walk_interpolated_string_element(self, interpolated_string_element); self.semantic.flags = snapshot; } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs index 9b9628bb03..843dd5c3b6 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_bind_all_interfaces.rs @@ -63,6 +63,9 @@ pub(crate) fn hardcoded_bind_all_interfaces(checker: &Checker, string: StringLik } } } + StringLike::Bytes(_) => (), + // TODO(dylan): decide whether to trigger here + StringLike::TString(_) => (), } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs index 7080cb57f4..76baa9c479 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_sql_expression.rs @@ -101,10 +101,11 @@ pub(crate) fn hardcoded_sql_expression(checker: &Checker, expr: &Expr) { // f"select * from table where val = {val}" Expr::FString(f_string) - if f_string - .value - .f_strings() - .any(|fs| fs.elements.iter().any(ast::FStringElement::is_expression)) => + if f_string.value.f_strings().any(|fs| { + fs.elements + .iter() + .any(ast::InterpolatedStringElement::is_interpolation) + }) => { concatenated_f_string(f_string, checker.locator()) } @@ -175,6 +176,8 @@ fn is_explicit_concatenation(expr: &Expr) -> Option { Expr::DictComp(_) => Some(false), Expr::Compare(_) => Some(false), Expr::FString(_) => Some(true), + // TODO(dylan): decide whether to trigger here + Expr::TString(_) => Some(false), Expr::StringLiteral(_) => Some(true), Expr::BytesLiteral(_) => Some(false), Expr::NoneLiteral(_) => Some(false), diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs index 963314df64..03df078819 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/hardcoded_tmp_directory.rs @@ -75,7 +75,10 @@ pub(crate) fn hardcoded_tmp_directory(checker: &Checker, string: StringLike) { } } } + // These are not actually strings StringLike::Bytes(_) => (), + // TODO(dylan) - verify that we should skip these + StringLike::TString(_) => (), } } diff --git a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs index 28944882b4..f558707fec 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs +++ b/crates/ruff_linter/src/rules/flake8_bandit/rules/suspicious_function_call.rs @@ -1006,9 +1006,9 @@ fn suspicious_function( // Ex) f"foo" Expr::FString(ast::ExprFString { value, .. }) => { value.elements().next().and_then(|element| { - if let ast::FStringElement::Literal(ast::FStringLiteralElement { - value, .. - }) = element + if let ast::InterpolatedStringElement::Literal( + ast::InterpolatedStringLiteralElement { value, .. }, + ) = element { Some(Either::Right(value.chars())) } else { diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S104_S104.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S104_S104.py.snap index 15fec6bd70..bcad262f2f 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S104_S104.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S104_S104.py.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs -snapshot_kind: text --- S104.py:9:1: S104 Possible binding to all interfaces | @@ -48,6 +47,8 @@ S104.py:24:1: S104 Possible binding to all interfaces 23 | # Implicit string concatenation 24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0" | ^^^^^^^^^ S104 +25 | +26 | # t-strings - all ok | S104.py:24:13: S104 Possible binding to all interfaces @@ -55,6 +56,8 @@ S104.py:24:13: S104 Possible binding to all interfaces 23 | # Implicit string concatenation 24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0" | ^^^^^^^ S104 +25 | +26 | # t-strings - all ok | S104.py:24:26: S104 Possible binding to all interfaces @@ -62,4 +65,6 @@ S104.py:24:26: S104 Possible binding to all interfaces 23 | # Implicit string concatenation 24 | "0.0.0.0" f"0.0.0.0{expr}0.0.0.0" | ^^^^^^^ S104 +25 | +26 | # t-strings - all ok | diff --git a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap index 2170539c17..b19b9631f6 100644 --- a/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap +++ b/crates/ruff_linter/src/rules/flake8_bandit/snapshots/ruff_linter__rules__flake8_bandit__tests__S608_S608.py.snap @@ -604,3 +604,12 @@ S608.py:164:11: S608 Possible SQL injection vector through string-based query co 169 | 170 | # https://github.com/astral-sh/ruff/issues/17967 | + +S608.py:180:11: S608 Possible SQL injection vector through string-based query construction + | +178 | FROM ({user_input}) raw +179 | """ +180 | query64 = f"update {t"{table}"} set var = {t"{var}"}" + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S608 +181 | query65 = t"update {f"{table}"} set var = {f"{var}"}" + | diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs index 76ae0e1e0b..a739f91d36 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/string_or_bytes_too_long.rs @@ -67,6 +67,11 @@ pub(crate) fn string_or_bytes_too_long(checker: &Checker, string: StringLike) { StringLike::String(ast::ExprStringLiteral { value, .. }) => value.chars().count(), StringLike::Bytes(ast::ExprBytesLiteral { value, .. }) => value.len(), StringLike::FString(node) => count_f_string_chars(node), + // TODO(dylan): decide how to count chars, especially + // if interpolations are of different type than `str` + StringLike::TString(_) => { + return; + } }; if length <= 50 { return; @@ -91,8 +96,10 @@ fn count_f_string_chars(f_string: &ast::ExprFString) -> usize { .elements .iter() .map(|element| match element { - ast::FStringElement::Literal(string) => string.chars().count(), - ast::FStringElement::Expression(expr) => expr.range().len().to_usize(), + ast::InterpolatedStringElement::Literal(string) => string.chars().count(), + ast::InterpolatedStringElement::Interpolation(expr) => { + expr.range().len().to_usize() + } }) .sum(), }) diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs index 7b886dd5cc..24d3a987a6 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs @@ -106,19 +106,23 @@ pub(super) fn is_empty_or_null_string(expr: &Expr) -> bool { ast::FStringPart::FString(f_string) => f_string .elements .iter() - .all(is_empty_or_null_fstring_element), + .all(is_empty_or_null_interpolated_string_element), }) } _ => false, } } -fn is_empty_or_null_fstring_element(element: &ast::FStringElement) -> bool { +fn is_empty_or_null_interpolated_string_element(element: &ast::InterpolatedStringElement) -> bool { match element { - ast::FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => value.is_empty(), - ast::FStringElement::Expression(ast::FStringExpressionElement { expression, .. }) => { - is_empty_or_null_string(expression) - } + ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { + value, + .. + }) => value.is_empty(), + ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { + expression, + .. + }) => is_empty_or_null_string(expression), } } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs index b5b0ceadab..47972e53af 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/avoidable_escaped_quote.rs @@ -1,7 +1,7 @@ use flake8_quotes::helpers::{contains_escaped_quote, raw_contents, unescape_string}; use flake8_quotes::settings::Quote; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::visitor::{Visitor, walk_f_string}; +use ruff_python_ast::visitor::{Visitor, walk_f_string, walk_t_string}; use ruff_python_ast::{self as ast, AnyStringFlags, PythonVersion, StringFlags, StringLike}; use ruff_text_size::{Ranged, TextRange, TextSize}; @@ -54,7 +54,7 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike // This rule has support for strings nested inside another f-strings but they're checked // via the outermost f-string. This means that we shouldn't be checking any nested string // or f-string. - || checker.semantic().in_f_string_replacement_field() + || checker.semantic().in_interpolated_string_replacement_field() { return; } @@ -70,6 +70,7 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike rule_checker.visit_bytes_literal(bytes_literal); } ast::StringLikePart::FString(f_string) => rule_checker.visit_f_string(f_string), + ast::StringLikePart::TString(t_string) => rule_checker.visit_t_string(t_string), } } } @@ -179,25 +180,70 @@ impl Visitor<'_> for AvoidableEscapedQuoteChecker<'_, '_> { .literals() .any(|literal| contains_quote(literal, opposite_quote_char)) { - check_f_string(self.checker, self.quotes_settings, f_string); + check_interpolated_string( + self.checker, + self.quotes_settings, + AnyStringFlags::from(f_string.flags), + &f_string.elements, + f_string.range, + ); } walk_f_string(self, f_string); } + + fn visit_t_string(&mut self, t_string: &'_ ast::TString) { + let opposite_quote_char = self.quotes_settings.inline_quotes.opposite().as_char(); + + // If any literal part of this t-string contains the quote character which is opposite to + // the configured inline quotes, we can't change the quote style for this t-string. For + // example: + // + // ```py + // t"\"hello\" {x} 'world'" + // ``` + // + // If we try to fix the above example, the t-string will end in the middle and "world" will + // be considered as a variable which is outside this t-string: + // + // ```py + // t'"hello" {x} 'world'' + // # ^ + // # t-string ends here now + // ``` + // + // The check is local to this t-string and it shouldn't check for any literal parts of any + // nested t-string. + if !t_string + .elements + .literals() + .any(|literal| contains_quote(literal, opposite_quote_char)) + { + check_interpolated_string( + self.checker, + self.quotes_settings, + AnyStringFlags::from(t_string.flags), + &t_string.elements, + t_string.range, + ); + } + + walk_t_string(self, t_string); + } } /// Checks for unnecessary escaped quotes in a string or bytes literal. /// /// # Panics /// -/// If the string kind is an f-string. +/// If the string kind is an f-string or a t-string. fn check_string_or_bytes( checker: &Checker, quotes_settings: &flake8_quotes::settings::Settings, range: TextRange, flags: AnyStringFlags, ) { - assert!(!flags.is_f_string()); + assert!(!flags.is_interpolated_string()); let locator = checker.locator(); @@ -231,16 +277,14 @@ fn check_string_or_bytes( ))); } -/// Checks for unnecessary escaped quotes in an f-string. -fn check_f_string( +/// Checks for unnecessary escaped quotes in an f-string or t-string. +fn check_interpolated_string( checker: &Checker, quotes_settings: &flake8_quotes::settings::Settings, - f_string: &ast::FString, + flags: ast::AnyStringFlags, + elements: &ast::InterpolatedStringElements, + range: TextRange, ) { - let locator = checker.locator(); - - let ast::FString { flags, range, .. } = f_string; - if flags.is_triple_quoted() || flags.prefix().is_raw() { return; } @@ -254,8 +298,8 @@ fn check_f_string( let opposite_quote_char = quotes_settings.inline_quotes.opposite().as_char(); let mut edits = vec![]; - for literal in f_string.elements.literals() { - let content = locator.slice(literal); + for literal in elements.literals() { + let content = checker.locator().slice(literal); if !contains_escaped_quote(content, quote_char) { continue; } @@ -269,10 +313,10 @@ fn check_f_string( return; } - // Replacement for the f-string opening quote. We don't perform the check for raw and + // Replacement for the f/t-string opening quote. We don't perform the check for raw and // triple-quoted f-strings, so no need to account for them. let start_edit = Edit::range_replacement( - format!("f{opposite_quote_char}"), + format!("{}{opposite_quote_char}", flags.prefix()), TextRange::at( range.start(), // Prefix + quote char @@ -280,16 +324,15 @@ fn check_f_string( ), ); - // Replacement for the f-string ending quote. We don't perform the check for triple-quoted + // Replacement for the f/t-string ending quote. We don't perform the check for triple-quoted // f-string, so no need to account for them. edits.push(Edit::range_replacement( opposite_quote_char.to_string(), TextRange::at( // Offset would either be the end offset of the start edit in case there are no - // elements in the f-string (e.g., `f""`) or the end offset of the last f-string + // elements in the f/t-string (e.g., `f""`) or the end offset of the last f/t-string // element (e.g., `f"hello"`). - f_string - .elements + elements .last() .map_or_else(|| start_edit.end(), Ranged::end), // Quote char @@ -298,7 +341,7 @@ fn check_f_string( )); checker - .report_diagnostic(AvoidableEscapedQuote, *range) + .report_diagnostic(AvoidableEscapedQuote, range) .set_fix(Fix::safe_edits(start_edit, edits)); } @@ -320,6 +363,11 @@ impl Visitor<'_> for ContainsAnyString { self.result = true; // We don't need to recurse into this f-string now that we already know the result. } + + fn visit_t_string(&mut self, _: &'_ ast::TString) { + self.result = true; + // We don't need to recurse into this t-string now that we already know the result. + } } /// Return `true` if the haystack contains the quote. diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs index 6d8ae3237e..2607d3d2d1 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/check_string_quotes.rs @@ -444,7 +444,10 @@ pub(crate) fn check_string_quotes(checker: &Checker, string_like: StringLike) { } // TODO(dhruvmanila): Support checking for escaped quotes in f-strings. - if checker.semantic().in_f_string_replacement_field() { + if checker + .semantic() + .in_interpolated_string_replacement_field() + { return; } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs b/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs index 32ea80a749..5c7d6c91d0 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs +++ b/crates/ruff_linter/src/rules/flake8_quotes/rules/unnecessary_escaped_quote.rs @@ -1,5 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::{self as ast, AnyStringFlags, StringFlags, StringLike}; +use ruff_python_ast::{ + self as ast, AnyStringFlags, InterpolatedStringElements, StringFlags, StringLike, +}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -62,7 +64,20 @@ pub(crate) fn unnecessary_escaped_quote(checker: &Checker, string_like: StringLi bytes_literal.range(), AnyStringFlags::from(bytes_literal.flags), ), - ast::StringLikePart::FString(f_string) => check_f_string(checker, f_string), + ast::StringLikePart::FString(ast::FString { + elements, + range, + flags, + }) => { + check_interpolated_string(checker, AnyStringFlags::from(*flags), *range, elements); + } + ast::StringLikePart::TString(ast::TString { + elements, + range, + flags, + }) => { + check_interpolated_string(checker, AnyStringFlags::from(*flags), *range, elements); + } } } } @@ -73,7 +88,7 @@ pub(crate) fn unnecessary_escaped_quote(checker: &Checker, string_like: StringLi /// /// If the string kind is an f-string. fn check_string_or_bytes(checker: &Checker, range: TextRange, flags: AnyStringFlags) { - assert!(!flags.is_f_string()); + assert!(!flags.is_interpolated_string()); if flags.is_triple_quoted() || flags.is_raw_string() { return; @@ -96,9 +111,13 @@ fn check_string_or_bytes(checker: &Checker, range: TextRange, flags: AnyStringFl ))); } -/// Checks for unnecessary escaped quotes in an f-string. -fn check_f_string(checker: &Checker, f_string: &ast::FString) { - let ast::FString { flags, range, .. } = f_string; +/// Checks for unnecessary escaped quotes in an f-string or t-string. +fn check_interpolated_string( + checker: &Checker, + flags: AnyStringFlags, + range: TextRange, + elements: &InterpolatedStringElements, +) { if flags.is_triple_quoted() || flags.prefix().is_raw() { return; } @@ -106,7 +125,7 @@ fn check_f_string(checker: &Checker, f_string: &ast::FString) { let opposite_quote_char = flags.quote_style().opposite().as_char(); let mut edits = vec![]; - for literal in f_string.elements.literals() { + for literal in elements.literals() { let content = checker.locator().slice(literal); if !contains_escaped_quote(content, opposite_quote_char) { continue; @@ -122,6 +141,6 @@ fn check_f_string(checker: &Checker, f_string: &ast::FString) { return; }; - let mut diagnostic = checker.report_diagnostic(UnnecessaryEscapedQuote, *range); + let mut diagnostic = checker.report_diagnostic(UnnecessaryEscapedQuote, range); diagnostic.set_fix(Fix::safe_edits(first, edits_iter)); } diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap index a1099a8e0b..260805955b 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped.py.snap @@ -197,6 +197,8 @@ singles_escaped.py:38:15: Q003 [*] Change outer quotes to avoid escaping inner q 38 |-f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003 38 |+f"\"normal\" {f'"nested" {"other"} normal'} 'single quotes'" # Q003 39 39 | f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003 +40 40 | +41 41 | singles_escaped.py:39:1: Q003 [*] Change outer quotes to avoid escaping inner quotes | @@ -213,3 +215,184 @@ singles_escaped.py:39:1: Q003 [*] Change outer quotes to avoid escaping inner qu 38 38 | f"\"normal\" {f"\"nested\" {"other"} normal"} 'single quotes'" # Q003 39 |-f"\"normal\" {f"\"nested\" {"other"} 'single quotes'"} normal" # Q003 39 |+f'"normal" {f"\"nested\" {"other"} 'single quotes'"} normal' # Q003 +40 40 | +41 41 | +42 42 | # Same as above, but with t-strings + +singles_escaped.py:43:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +42 | # Same as above, but with t-strings +43 | t"This is a \"string\"" + | ^^^^^^^^^^^^^^^^^^^^^^^ Q003 +44 | t"'This' is a \"string\"" +45 | f'This is a "string"' + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +40 40 | +41 41 | +42 42 | # Same as above, but with t-strings +43 |-t"This is a \"string\"" + 43 |+t'This is a "string"' +44 44 | t"'This' is a \"string\"" +45 45 | f'This is a "string"' +46 46 | f'\'This\' is a "string"' + +singles_escaped.py:51:5: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +49 | foo = ( +50 | t"This is a" +51 | t"\"string\"" + | ^^^^^^^^^^^^^ Q003 +52 | ) +53 | t"\"foo\" {"foo"}" # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +48 48 | fR"This is a \"string\"" +49 49 | foo = ( +50 50 | t"This is a" +51 |- t"\"string\"" + 51 |+ t'"string"' +52 52 | ) +53 53 | t"\"foo\" {"foo"}" # Q003 +54 54 | t"\"foo\" {t"foo"}" # Q003 + +singles_escaped.py:53:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +51 | t"\"string\"" +52 | ) +53 | t"\"foo\" {"foo"}" # Q003 + | ^^^^^^^^^^^^^^^^^^ Q003 +54 | t"\"foo\" {t"foo"}" # Q003 +55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +50 50 | t"This is a" +51 51 | t"\"string\"" +52 52 | ) +53 |-t"\"foo\" {"foo"}" # Q003 + 53 |+t'"foo" {"foo"}' # Q003 +54 54 | t"\"foo\" {t"foo"}" # Q003 +55 55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 +56 56 | + +singles_escaped.py:54:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +52 | ) +53 | t"\"foo\" {"foo"}" # Q003 +54 | t"\"foo\" {t"foo"}" # Q003 + | ^^^^^^^^^^^^^^^^^^^ Q003 +55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +51 51 | t"\"string\"" +52 52 | ) +53 53 | t"\"foo\" {"foo"}" # Q003 +54 |-t"\"foo\" {t"foo"}" # Q003 + 54 |+t'"foo" {t"foo"}' # Q003 +55 55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 +56 56 | +57 57 | t"normal {t"nested"} normal" + +singles_escaped.py:55:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +53 | t"\"foo\" {"foo"}" # Q003 +54 | t"\"foo\" {t"foo"}" # Q003 +55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +56 | +57 | t"normal {t"nested"} normal" + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +52 52 | ) +53 53 | t"\"foo\" {"foo"}" # Q003 +54 54 | t"\"foo\" {t"foo"}" # Q003 +55 |-t"\"foo\" {t"\"foo\""} \"\"" # Q003 + 55 |+t'"foo" {t"\"foo\""} ""' # Q003 +56 56 | +57 57 | t"normal {t"nested"} normal" +58 58 | t"\"normal\" {t"nested"} normal" # Q003 + +singles_escaped.py:55:12: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +53 | t"\"foo\" {"foo"}" # Q003 +54 | t"\"foo\" {t"foo"}" # Q003 +55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 + | ^^^^^^^^^^ Q003 +56 | +57 | t"normal {t"nested"} normal" + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +52 52 | ) +53 53 | t"\"foo\" {"foo"}" # Q003 +54 54 | t"\"foo\" {t"foo"}" # Q003 +55 |-t"\"foo\" {t"\"foo\""} \"\"" # Q003 + 55 |+t"\"foo\" {t'"foo"'} \"\"" # Q003 +56 56 | +57 57 | t"normal {t"nested"} normal" +58 58 | t"\"normal\" {t"nested"} normal" # Q003 + +singles_escaped.py:58:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +57 | t"normal {t"nested"} normal" +58 | t"\"normal\" {t"nested"} normal" # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +59 | t"\"normal\" {t"nested"} 'single quotes'" +60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +55 55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 +56 56 | +57 57 | t"normal {t"nested"} normal" +58 |-t"\"normal\" {t"nested"} normal" # Q003 + 58 |+t'"normal" {t"nested"} normal' # Q003 +59 59 | t"\"normal\" {t"nested"} 'single quotes'" +60 60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 +61 61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 + +singles_escaped.py:60:15: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +58 | t"\"normal\" {t"nested"} normal" # Q003 +59 | t"\"normal\" {t"nested"} 'single quotes'" +60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +57 57 | t"normal {t"nested"} normal" +58 58 | t"\"normal\" {t"nested"} normal" # Q003 +59 59 | t"\"normal\" {t"nested"} 'single quotes'" +60 |-t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 + 60 |+t"\"normal\" {t'"nested" {"other"} normal'} 'single quotes'" # Q003 +61 61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 + +singles_escaped.py:61:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +59 | t"\"normal\" {t"nested"} 'single quotes'" +60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 +61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +58 58 | t"\"normal\" {t"nested"} normal" # Q003 +59 59 | t"\"normal\" {t"nested"} 'single quotes'" +60 60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 +61 |-t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 + 61 |+t'"normal" {t"\"nested\" {"other"} 'single quotes'"} normal' # Q003 diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_py311.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_py311.snap index 0ca32efbb5..cf7a8b3c70 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_py311.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_doubles_over_singles_escaped_py311.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -snapshot_kind: text --- singles_escaped.py:1:26: Q003 [*] Change outer quotes to avoid escaping inner quotes | @@ -77,3 +76,181 @@ singles_escaped.py:21:5: Q003 [*] Change outer quotes to avoid escaping inner qu 22 22 | ) 23 23 | 24 24 | # Nested f-strings (Python 3.12+) + +singles_escaped.py:43:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +42 | # Same as above, but with t-strings +43 | t"This is a \"string\"" + | ^^^^^^^^^^^^^^^^^^^^^^^ Q003 +44 | t"'This' is a \"string\"" +45 | f'This is a "string"' + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +40 40 | +41 41 | +42 42 | # Same as above, but with t-strings +43 |-t"This is a \"string\"" + 43 |+t'This is a "string"' +44 44 | t"'This' is a \"string\"" +45 45 | f'This is a "string"' +46 46 | f'\'This\' is a "string"' + +singles_escaped.py:51:5: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +49 | foo = ( +50 | t"This is a" +51 | t"\"string\"" + | ^^^^^^^^^^^^^ Q003 +52 | ) +53 | t"\"foo\" {"foo"}" # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +48 48 | fR"This is a \"string\"" +49 49 | foo = ( +50 50 | t"This is a" +51 |- t"\"string\"" + 51 |+ t'"string"' +52 52 | ) +53 53 | t"\"foo\" {"foo"}" # Q003 +54 54 | t"\"foo\" {t"foo"}" # Q003 + +singles_escaped.py:53:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +51 | t"\"string\"" +52 | ) +53 | t"\"foo\" {"foo"}" # Q003 + | ^^^^^^^^^^^^^^^^^^ Q003 +54 | t"\"foo\" {t"foo"}" # Q003 +55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +50 50 | t"This is a" +51 51 | t"\"string\"" +52 52 | ) +53 |-t"\"foo\" {"foo"}" # Q003 + 53 |+t'"foo" {"foo"}' # Q003 +54 54 | t"\"foo\" {t"foo"}" # Q003 +55 55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 +56 56 | + +singles_escaped.py:54:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +52 | ) +53 | t"\"foo\" {"foo"}" # Q003 +54 | t"\"foo\" {t"foo"}" # Q003 + | ^^^^^^^^^^^^^^^^^^^ Q003 +55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +51 51 | t"\"string\"" +52 52 | ) +53 53 | t"\"foo\" {"foo"}" # Q003 +54 |-t"\"foo\" {t"foo"}" # Q003 + 54 |+t'"foo" {t"foo"}' # Q003 +55 55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 +56 56 | +57 57 | t"normal {t"nested"} normal" + +singles_escaped.py:55:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +53 | t"\"foo\" {"foo"}" # Q003 +54 | t"\"foo\" {t"foo"}" # Q003 +55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +56 | +57 | t"normal {t"nested"} normal" + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +52 52 | ) +53 53 | t"\"foo\" {"foo"}" # Q003 +54 54 | t"\"foo\" {t"foo"}" # Q003 +55 |-t"\"foo\" {t"\"foo\""} \"\"" # Q003 + 55 |+t'"foo" {t"\"foo\""} ""' # Q003 +56 56 | +57 57 | t"normal {t"nested"} normal" +58 58 | t"\"normal\" {t"nested"} normal" # Q003 + +singles_escaped.py:55:12: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +53 | t"\"foo\" {"foo"}" # Q003 +54 | t"\"foo\" {t"foo"}" # Q003 +55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 + | ^^^^^^^^^^ Q003 +56 | +57 | t"normal {t"nested"} normal" + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +52 52 | ) +53 53 | t"\"foo\" {"foo"}" # Q003 +54 54 | t"\"foo\" {t"foo"}" # Q003 +55 |-t"\"foo\" {t"\"foo\""} \"\"" # Q003 + 55 |+t"\"foo\" {t'"foo"'} \"\"" # Q003 +56 56 | +57 57 | t"normal {t"nested"} normal" +58 58 | t"\"normal\" {t"nested"} normal" # Q003 + +singles_escaped.py:58:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +57 | t"normal {t"nested"} normal" +58 | t"\"normal\" {t"nested"} normal" # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +59 | t"\"normal\" {t"nested"} 'single quotes'" +60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +55 55 | t"\"foo\" {t"\"foo\""} \"\"" # Q003 +56 56 | +57 57 | t"normal {t"nested"} normal" +58 |-t"\"normal\" {t"nested"} normal" # Q003 + 58 |+t'"normal" {t"nested"} normal' # Q003 +59 59 | t"\"normal\" {t"nested"} 'single quotes'" +60 60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 +61 61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 + +singles_escaped.py:60:15: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +58 | t"\"normal\" {t"nested"} normal" # Q003 +59 | t"\"normal\" {t"nested"} 'single quotes'" +60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +57 57 | t"normal {t"nested"} normal" +58 58 | t"\"normal\" {t"nested"} normal" # Q003 +59 59 | t"\"normal\" {t"nested"} 'single quotes'" +60 |-t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 + 60 |+t"\"normal\" {t'"nested" {"other"} normal'} 'single quotes'" # Q003 +61 61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 + +singles_escaped.py:61:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +59 | t"\"normal\" {t"nested"} 'single quotes'" +60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 +61 | t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +58 58 | t"\"normal\" {t"nested"} normal" # Q003 +59 59 | t"\"normal\" {t"nested"} 'single quotes'" +60 60 | t"\"normal\" {t"\"nested\" {"other"} normal"} 'single quotes'" # Q003 +61 |-t"\"normal\" {t"\"nested\" {"other"} 'single quotes'"} normal" # Q003 + 61 |+t'"normal" {t"\"nested\" {"other"} 'single quotes'"} normal' # Q003 diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap index feff195fb7..014b383162 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped.py.snap @@ -236,6 +236,8 @@ doubles_escaped.py:40:15: Q003 [*] Change outer quotes to avoid escaping inner q 40 |-f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003 40 |+f'\'normal\' {f"'nested' {'other'} normal"} "double quotes"' # Q003 41 41 | f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l +42 42 | +43 43 | doubles_escaped.py:41:1: Q003 [*] Change outer quotes to avoid escaping inner quotes | @@ -252,3 +254,205 @@ doubles_escaped.py:41:1: Q003 [*] Change outer quotes to avoid escaping inner qu 40 40 | f'\'normal\' {f'\'nested\' {'other'} normal'} "double quotes"' # Q003 41 |-f'\'normal\' {f'\'nested\' {'other'} "double quotes"'} normal' # Q00l 41 |+f"'normal' {f'\'nested\' {'other'} "double quotes"'} normal" # Q00l +42 42 | +43 43 | +44 44 | + +doubles_escaped.py:46:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +45 | # Same as above, but with t-strings +46 | t'This is a \'string\'' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^ Q003 +47 | t'This is \\ a \\\'string\'' # Q003 +48 | t'"This" is a \'string\'' + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +43 43 | +44 44 | +45 45 | # Same as above, but with t-strings +46 |-t'This is a \'string\'' # Q003 + 46 |+t"This is a 'string'" # Q003 +47 47 | t'This is \\ a \\\'string\'' # Q003 +48 48 | t'"This" is a \'string\'' +49 49 | f"This is a 'string'" + +doubles_escaped.py:47:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +45 | # Same as above, but with t-strings +46 | t'This is a \'string\'' # Q003 +47 | t'This is \\ a \\\'string\'' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +48 | t'"This" is a \'string\'' +49 | f"This is a 'string'" + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +44 44 | +45 45 | # Same as above, but with t-strings +46 46 | t'This is a \'string\'' # Q003 +47 |-t'This is \\ a \\\'string\'' # Q003 + 47 |+t"This is \\ a \\'string'" # Q003 +48 48 | t'"This" is a \'string\'' +49 49 | f"This is a 'string'" +50 50 | f"\"This\" is a 'string'" + +doubles_escaped.py:55:5: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +53 | foo = ( +54 | t'This is a' +55 | t'\'string\'' # Q003 + | ^^^^^^^^^^^^^ Q003 +56 | ) +57 | t'\'foo\' {'nested'}' # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +52 52 | fR'This is a \'string\'' +53 53 | foo = ( +54 54 | t'This is a' +55 |- t'\'string\'' # Q003 + 55 |+ t"'string'" # Q003 +56 56 | ) +57 57 | t'\'foo\' {'nested'}' # Q003 +58 58 | t'\'foo\' {t'nested'}' # Q003 + +doubles_escaped.py:57:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +55 | t'\'string\'' # Q003 +56 | ) +57 | t'\'foo\' {'nested'}' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^ Q003 +58 | t'\'foo\' {t'nested'}' # Q003 +59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +54 54 | t'This is a' +55 55 | t'\'string\'' # Q003 +56 56 | ) +57 |-t'\'foo\' {'nested'}' # Q003 + 57 |+t"'foo' {'nested'}" # Q003 +58 58 | t'\'foo\' {t'nested'}' # Q003 +59 59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 +60 60 | + +doubles_escaped.py:58:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +56 | ) +57 | t'\'foo\' {'nested'}' # Q003 +58 | t'\'foo\' {t'nested'}' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^ Q003 +59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +55 55 | t'\'string\'' # Q003 +56 56 | ) +57 57 | t'\'foo\' {'nested'}' # Q003 +58 |-t'\'foo\' {t'nested'}' # Q003 + 58 |+t"'foo' {t'nested'}" # Q003 +59 59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 +60 60 | +61 61 | t'normal {t'nested'} normal' + +doubles_escaped.py:59:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +57 | t'\'foo\' {'nested'}' # Q003 +58 | t'\'foo\' {t'nested'}' # Q003 +59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +60 | +61 | t'normal {t'nested'} normal' + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +56 56 | ) +57 57 | t'\'foo\' {'nested'}' # Q003 +58 58 | t'\'foo\' {t'nested'}' # Q003 +59 |-t'\'foo\' {t'\'nested\''} \'\'' # Q003 + 59 |+t"'foo' {t'\'nested\''} ''" # Q003 +60 60 | +61 61 | t'normal {t'nested'} normal' +62 62 | t'\'normal\' {t'nested'} normal' # Q003 + +doubles_escaped.py:59:12: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +57 | t'\'foo\' {'nested'}' # Q003 +58 | t'\'foo\' {t'nested'}' # Q003 +59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 + | ^^^^^^^^^^^^^ Q003 +60 | +61 | t'normal {t'nested'} normal' + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +56 56 | ) +57 57 | t'\'foo\' {'nested'}' # Q003 +58 58 | t'\'foo\' {t'nested'}' # Q003 +59 |-t'\'foo\' {t'\'nested\''} \'\'' # Q003 + 59 |+t'\'foo\' {t"'nested'"} \'\'' # Q003 +60 60 | +61 61 | t'normal {t'nested'} normal' +62 62 | t'\'normal\' {t'nested'} normal' # Q003 + +doubles_escaped.py:62:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +61 | t'normal {t'nested'} normal' +62 | t'\'normal\' {t'nested'} normal' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +63 | t'\'normal\' {t'nested'} "double quotes"' +64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +59 59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 +60 60 | +61 61 | t'normal {t'nested'} normal' +62 |-t'\'normal\' {t'nested'} normal' # Q003 + 62 |+t"'normal' {t'nested'} normal" # Q003 +63 63 | t'\'normal\' {t'nested'} "double quotes"' +64 64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 +65 65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l + +doubles_escaped.py:64:15: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +62 | t'\'normal\' {t'nested'} normal' # Q003 +63 | t'\'normal\' {t'nested'} "double quotes"' +64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +61 61 | t'normal {t'nested'} normal' +62 62 | t'\'normal\' {t'nested'} normal' # Q003 +63 63 | t'\'normal\' {t'nested'} "double quotes"' +64 |-t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 + 64 |+t'\'normal\' {t"'nested' {'other'} normal"} "double quotes"' # Q003 +65 65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l + +doubles_escaped.py:65:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +63 | t'\'normal\' {t'nested'} "double quotes"' +64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 +65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +62 62 | t'\'normal\' {t'nested'} normal' # Q003 +63 63 | t'\'normal\' {t'nested'} "double quotes"' +64 64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 +65 |-t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l + 65 |+t"'normal' {t'\'nested\' {'other'} "double quotes"'} normal" # Q00l diff --git a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_py311.snap b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_py311.snap index f6768a4b6f..6b0b2348ca 100644 --- a/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_py311.snap +++ b/crates/ruff_linter/src/rules/flake8_quotes/snapshots/ruff_linter__rules__flake8_quotes__tests__require_singles_over_doubles_escaped_py311.snap @@ -1,6 +1,5 @@ --- source: crates/ruff_linter/src/rules/flake8_quotes/mod.rs -snapshot_kind: text --- doubles_escaped.py:1:26: Q003 [*] Change outer quotes to avoid escaping inner quotes | @@ -116,3 +115,202 @@ doubles_escaped.py:23:5: Q003 [*] Change outer quotes to avoid escaping inner qu 24 24 | ) 25 25 | 26 26 | # Nested f-strings (Python 3.12+) + +doubles_escaped.py:46:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +45 | # Same as above, but with t-strings +46 | t'This is a \'string\'' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^ Q003 +47 | t'This is \\ a \\\'string\'' # Q003 +48 | t'"This" is a \'string\'' + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +43 43 | +44 44 | +45 45 | # Same as above, but with t-strings +46 |-t'This is a \'string\'' # Q003 + 46 |+t"This is a 'string'" # Q003 +47 47 | t'This is \\ a \\\'string\'' # Q003 +48 48 | t'"This" is a \'string\'' +49 49 | f"This is a 'string'" + +doubles_escaped.py:47:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +45 | # Same as above, but with t-strings +46 | t'This is a \'string\'' # Q003 +47 | t'This is \\ a \\\'string\'' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +48 | t'"This" is a \'string\'' +49 | f"This is a 'string'" + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +44 44 | +45 45 | # Same as above, but with t-strings +46 46 | t'This is a \'string\'' # Q003 +47 |-t'This is \\ a \\\'string\'' # Q003 + 47 |+t"This is \\ a \\'string'" # Q003 +48 48 | t'"This" is a \'string\'' +49 49 | f"This is a 'string'" +50 50 | f"\"This\" is a 'string'" + +doubles_escaped.py:55:5: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +53 | foo = ( +54 | t'This is a' +55 | t'\'string\'' # Q003 + | ^^^^^^^^^^^^^ Q003 +56 | ) +57 | t'\'foo\' {'nested'}' # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +52 52 | fR'This is a \'string\'' +53 53 | foo = ( +54 54 | t'This is a' +55 |- t'\'string\'' # Q003 + 55 |+ t"'string'" # Q003 +56 56 | ) +57 57 | t'\'foo\' {'nested'}' # Q003 +58 58 | t'\'foo\' {t'nested'}' # Q003 + +doubles_escaped.py:57:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +55 | t'\'string\'' # Q003 +56 | ) +57 | t'\'foo\' {'nested'}' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^ Q003 +58 | t'\'foo\' {t'nested'}' # Q003 +59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +54 54 | t'This is a' +55 55 | t'\'string\'' # Q003 +56 56 | ) +57 |-t'\'foo\' {'nested'}' # Q003 + 57 |+t"'foo' {'nested'}" # Q003 +58 58 | t'\'foo\' {t'nested'}' # Q003 +59 59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 +60 60 | + +doubles_escaped.py:58:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +56 | ) +57 | t'\'foo\' {'nested'}' # Q003 +58 | t'\'foo\' {t'nested'}' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^ Q003 +59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +55 55 | t'\'string\'' # Q003 +56 56 | ) +57 57 | t'\'foo\' {'nested'}' # Q003 +58 |-t'\'foo\' {t'nested'}' # Q003 + 58 |+t"'foo' {t'nested'}" # Q003 +59 59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 +60 60 | +61 61 | t'normal {t'nested'} normal' + +doubles_escaped.py:59:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +57 | t'\'foo\' {'nested'}' # Q003 +58 | t'\'foo\' {t'nested'}' # Q003 +59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +60 | +61 | t'normal {t'nested'} normal' + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +56 56 | ) +57 57 | t'\'foo\' {'nested'}' # Q003 +58 58 | t'\'foo\' {t'nested'}' # Q003 +59 |-t'\'foo\' {t'\'nested\''} \'\'' # Q003 + 59 |+t"'foo' {t'\'nested\''} ''" # Q003 +60 60 | +61 61 | t'normal {t'nested'} normal' +62 62 | t'\'normal\' {t'nested'} normal' # Q003 + +doubles_escaped.py:59:12: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +57 | t'\'foo\' {'nested'}' # Q003 +58 | t'\'foo\' {t'nested'}' # Q003 +59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 + | ^^^^^^^^^^^^^ Q003 +60 | +61 | t'normal {t'nested'} normal' + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +56 56 | ) +57 57 | t'\'foo\' {'nested'}' # Q003 +58 58 | t'\'foo\' {t'nested'}' # Q003 +59 |-t'\'foo\' {t'\'nested\''} \'\'' # Q003 + 59 |+t'\'foo\' {t"'nested'"} \'\'' # Q003 +60 60 | +61 61 | t'normal {t'nested'} normal' +62 62 | t'\'normal\' {t'nested'} normal' # Q003 + +doubles_escaped.py:62:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +61 | t'normal {t'nested'} normal' +62 | t'\'normal\' {t'nested'} normal' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +63 | t'\'normal\' {t'nested'} "double quotes"' +64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +59 59 | t'\'foo\' {t'\'nested\''} \'\'' # Q003 +60 60 | +61 61 | t'normal {t'nested'} normal' +62 |-t'\'normal\' {t'nested'} normal' # Q003 + 62 |+t"'normal' {t'nested'} normal" # Q003 +63 63 | t'\'normal\' {t'nested'} "double quotes"' +64 64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 +65 65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l + +doubles_escaped.py:64:15: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +62 | t'\'normal\' {t'nested'} normal' # Q003 +63 | t'\'normal\' {t'nested'} "double quotes"' +64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 +65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +61 61 | t'normal {t'nested'} normal' +62 62 | t'\'normal\' {t'nested'} normal' # Q003 +63 63 | t'\'normal\' {t'nested'} "double quotes"' +64 |-t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 + 64 |+t'\'normal\' {t"'nested' {'other'} normal"} "double quotes"' # Q003 +65 65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l + +doubles_escaped.py:65:1: Q003 [*] Change outer quotes to avoid escaping inner quotes + | +63 | t'\'normal\' {t'nested'} "double quotes"' +64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 +65 | t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Q003 + | + = help: Change outer quotes to avoid escaping inner quotes + +ℹ Safe fix +62 62 | t'\'normal\' {t'nested'} normal' # Q003 +63 63 | t'\'normal\' {t'nested'} "double quotes"' +64 64 | t'\'normal\' {t'\'nested\' {'other'} normal'} "double quotes"' # Q003 +65 |-t'\'normal\' {t'\'nested\' {'other'} "double quotes"'} normal' # Q00l + 65 |+t"'normal' {t'\'nested\' {'other'} "double quotes"'} normal" # Q00l diff --git a/crates/ruff_linter/src/rules/flynt/helpers.rs b/crates/ruff_linter/src/rules/flynt/helpers.rs index a71b369b69..4afb764c4c 100644 --- a/crates/ruff_linter/src/rules/flynt/helpers.rs +++ b/crates/ruff_linter/src/rules/flynt/helpers.rs @@ -2,8 +2,8 @@ use ruff_python_ast::{self as ast, Arguments, ConversionFlag, Expr}; use ruff_text_size::TextRange; /// Wrap an expression in a [`ast::FStringElement::Expression`] with no special formatting. -fn to_f_string_expression_element(inner: &Expr) -> ast::FStringElement { - ast::FStringElement::Expression(ast::FStringExpressionElement { +fn to_interpolated_string_interpolation_element(inner: &Expr) -> ast::InterpolatedStringElement { + ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { expression: Box::new(inner.clone()), debug_text: None, conversion: ConversionFlag::None, @@ -12,9 +12,9 @@ fn to_f_string_expression_element(inner: &Expr) -> ast::FStringElement { }) } -/// Convert a string to a [`ast::FStringElement::Literal`]. -pub(super) fn to_f_string_literal_element(s: &str) -> ast::FStringElement { - ast::FStringElement::Literal(ast::FStringLiteralElement { +/// Convert a string to a [`ast::InterpolatedStringLiteralElement `]. +pub(super) fn to_interpolated_string_literal_element(s: &str) -> ast::InterpolatedStringElement { + ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { value: Box::from(s), range: TextRange::default(), }) @@ -48,20 +48,24 @@ fn is_simple_callee(func: &Expr) -> bool { } } -/// Convert an expression to a f-string element (if it looks like a good idea). -pub(super) fn to_f_string_element(expr: &Expr) -> Option { +/// Convert an expression to an f-string or t-string element (if it looks like a good idea). +pub(super) fn to_interpolated_string_element( + expr: &Expr, +) -> Option { match expr { - Expr::StringLiteral(ast::ExprStringLiteral { value, range }) => { - Some(ast::FStringElement::Literal(ast::FStringLiteralElement { + Expr::StringLiteral(ast::ExprStringLiteral { value, range }) => Some( + ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { value: value.to_string().into_boxed_str(), range: *range, - })) - } + }), + ), // These should be pretty safe to wrap in a formatted value. Expr::NumberLiteral(_) | Expr::BooleanLiteral(_) | Expr::Name(_) | Expr::Attribute(_) => { - Some(to_f_string_expression_element(expr)) + Some(to_interpolated_string_interpolation_element(expr)) + } + Expr::Call(_) if is_simple_call(expr) => { + Some(to_interpolated_string_interpolation_element(expr)) } - Expr::Call(_) if is_simple_call(expr) => Some(to_f_string_expression_element(expr)), _ => None, } } diff --git a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs index 968c386b51..aa07cb58af 100644 --- a/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff_linter/src/rules/flynt/rules/static_join_to_fstring.rs @@ -105,9 +105,9 @@ fn build_fstring(joiner: &str, joinees: &[Expr], flags: FStringFlags) -> Option< return None; } if !std::mem::take(&mut first) { - f_string_elements.push(helpers::to_f_string_literal_element(joiner)); + f_string_elements.push(helpers::to_interpolated_string_literal_element(joiner)); } - f_string_elements.push(helpers::to_f_string_element(expr)?); + f_string_elements.push(helpers::to_interpolated_string_element(expr)?); } let node = ast::FString { diff --git a/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs b/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs index d5e0107e16..1e0fe8507a 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/rules/invalid_escape_sequence.rs @@ -1,7 +1,10 @@ use memchr::memchr_iter; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::{AnyStringFlags, FStringElement, StringLike, StringLikePart}; +use ruff_python_ast::{ + AnyStringFlags, InterpolatedStringElement, InterpolatedStringElements, StringLike, + StringLikePart, +}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::Locator; @@ -70,39 +73,16 @@ pub(crate) fn invalid_escape_sequence(checker: &Checker, string_like: StringLike StringLikePart::String(_) | StringLikePart::Bytes(_) => { analyze_escape_chars(locator, part.range(), part.flags()) } - StringLikePart::FString(f_string) => { - let flags = AnyStringFlags::from(f_string.flags); - let mut escape_chars_state = EscapeCharsState::default(); - // Whether we suggest converting to a raw string or - // adding backslashes depends on the presence of valid - // escape characters in the entire f-string. Therefore, - // we must analyze escape characters in each f-string - // element before pushing a diagnostic and fix. - for element in &f_string.elements { - match element { - FStringElement::Literal(literal) => { - escape_chars_state.update(analyze_escape_chars( - locator, - literal.range(), - flags, - )); - } - FStringElement::Expression(expression) => { - let Some(format_spec) = expression.format_spec.as_ref() else { - continue; - }; - for literal in format_spec.elements.literals() { - escape_chars_state.update(analyze_escape_chars( - locator, - literal.range(), - flags, - )); - } - } - } - } - escape_chars_state - } + StringLikePart::FString(f_string) => analyze_escape_chars_in_interpolated_string( + AnyStringFlags::from(f_string.flags), + &f_string.elements, + locator, + ), + StringLikePart::TString(t_string) => analyze_escape_chars_in_interpolated_string( + AnyStringFlags::from(t_string.flags), + &t_string.elements, + locator, + ), }; check(checker, locator, part.start(), part.flags(), state); } @@ -146,7 +126,7 @@ fn analyze_escape_chars( let next_char = match source[i + 1..].chars().next() { Some(next_char) => next_char, - None if flags.is_f_string() => { + None if flags.is_interpolated_string() => { // If we're at the end of a f-string middle token, the next character // is actually emitted as a different token. For example, // @@ -230,6 +210,39 @@ fn analyze_escape_chars( } } +fn analyze_escape_chars_in_interpolated_string( + flags: AnyStringFlags, + elements: &InterpolatedStringElements, + locator: &Locator, +) -> EscapeCharsState { + let mut escape_chars_state = EscapeCharsState::default(); + // Whether we suggest converting to a raw string or + // adding backslashes depends on the presence of valid + // escape characters in the entire f/t-string. Therefore, + // we must analyze escape characters in each f/t-string + // element before pushing a diagnostic and fix. + for element in elements { + match element { + InterpolatedStringElement::Literal(literal) => { + escape_chars_state.update(analyze_escape_chars(locator, literal.range(), flags)); + } + InterpolatedStringElement::Interpolation(interpolation) => { + let Some(format_spec) = interpolation.format_spec.as_ref() else { + continue; + }; + for literal in format_spec.elements.literals() { + escape_chars_state.update(analyze_escape_chars( + locator, + literal.range(), + flags, + )); + } + } + } + } + escape_chars_state +} + /// Pushes a diagnostic and fix depending on escape characters seen so far. /// /// If we have not seen any valid escape characters, we convert to diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap index 8d763a16a3..4ca58ba4ec 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__W605_W605_1.py.snap @@ -12,7 +12,7 @@ W605_1.py:4:11: W605 [*] Invalid escape sequence: `\.` = help: Use a raw string literal ℹ Safe fix -1 1 | # Same as `W605_0.py` but using f-strings instead. +1 1 | # Same as `W605_0.py` but using f-strings and t-strings instead. 2 2 | 3 3 | #: W605:1:10 4 |-regex = f'\.png$' @@ -320,3 +320,346 @@ W605_1.py:68:9: W605 [*] Invalid escape sequence: `\I` 67 67 | # Debug text (should trigger) 68 |-t = f"{'\InHere'=}" 68 |+t = f"{r'\InHere'=}" +69 69 | +70 70 | +71 71 | + +W605_1.py:73:11: W605 [*] Invalid escape sequence: `\.` + | +72 | #: W605:1:10 +73 | regex = t'\.png$' + | ^^ W605 +74 | +75 | #: W605:2:1 + | + = help: Use a raw string literal + +ℹ Safe fix +70 70 | +71 71 | +72 72 | #: W605:1:10 +73 |-regex = t'\.png$' + 73 |+regex = rt'\.png$' +74 74 | +75 75 | #: W605:2:1 +76 76 | regex = t''' + +W605_1.py:77:1: W605 [*] Invalid escape sequence: `\.` + | +75 | #: W605:2:1 +76 | regex = t''' +77 | \.png$ + | ^^ W605 +78 | ''' + | + = help: Use a raw string literal + +ℹ Safe fix +73 73 | regex = t'\.png$' +74 74 | +75 75 | #: W605:2:1 +76 |-regex = t''' + 76 |+regex = rt''' +77 77 | \.png$ +78 78 | ''' +79 79 | + +W605_1.py:82:7: W605 [*] Invalid escape sequence: `\_` + | +80 | #: W605:2:6 +81 | f( +82 | t'\_' + | ^^ W605 +83 | ) + | + = help: Use a raw string literal + +ℹ Safe fix +79 79 | +80 80 | #: W605:2:6 +81 81 | f( +82 |- t'\_' + 82 |+ rt'\_' +83 83 | ) +84 84 | +85 85 | #: W605:4:6 + +W605_1.py:89:6: W605 [*] Invalid escape sequence: `\_` + | +87 | multi-line +88 | literal +89 | with \_ somewhere + | ^^ W605 +90 | in the middle +91 | """ + | + = help: Use a raw string literal + +ℹ Safe fix +83 83 | ) +84 84 | +85 85 | #: W605:4:6 +86 |-t""" + 86 |+rt""" +87 87 | multi-line +88 88 | literal +89 89 | with \_ somewhere + +W605_1.py:94:40: W605 [*] Invalid escape sequence: `\_` + | +93 | #: W605:1:38 +94 | value = t'new line\nand invalid escape \_ here' + | ^^ W605 + | + = help: Add backslash to escape sequence + +ℹ Safe fix +91 91 | """ +92 92 | +93 93 | #: W605:1:38 +94 |-value = t'new line\nand invalid escape \_ here' + 94 |+value = t'new line\nand invalid escape \\_ here' +95 95 | +96 96 | +97 97 | #: Okay + +W605_1.py:109:1: W605 [*] Invalid escape sequence: `\w` + | +107 | regex = t'\w' # noqa +108 | regex = t''' +109 | \w + | ^^ W605 +110 | ''' # noqa + | + = help: Use a raw string literal + +ℹ Safe fix +105 105 | ''' +106 106 | s = t'\\' +107 107 | regex = t'\w' # noqa +108 |-regex = t''' + 108 |+regex = rt''' +109 109 | \w +110 110 | ''' # noqa +111 111 | + +W605_1.py:112:13: W605 [*] Invalid escape sequence: `\_` + | +110 | ''' # noqa +111 | +112 | regex = t'\\\_' + | ^^ W605 +113 | value = t'\{{1}}' +114 | value = t'\{1}' + | + = help: Add backslash to escape sequence + +ℹ Safe fix +109 109 | \w +110 110 | ''' # noqa +111 111 | +112 |-regex = t'\\\_' + 112 |+regex = t'\\\\_' +113 113 | value = t'\{{1}}' +114 114 | value = t'\{1}' +115 115 | value = t'{1:\}' + +W605_1.py:113:11: W605 [*] Invalid escape sequence: `\{` + | +112 | regex = t'\\\_' +113 | value = t'\{{1}}' + | ^^ W605 +114 | value = t'\{1}' +115 | value = t'{1:\}' + | + = help: Use a raw string literal + +ℹ Safe fix +110 110 | ''' # noqa +111 111 | +112 112 | regex = t'\\\_' +113 |-value = t'\{{1}}' + 113 |+value = rt'\{{1}}' +114 114 | value = t'\{1}' +115 115 | value = t'{1:\}' +116 116 | value = t"{t"\{1}"}" + +W605_1.py:114:11: W605 [*] Invalid escape sequence: `\{` + | +112 | regex = t'\\\_' +113 | value = t'\{{1}}' +114 | value = t'\{1}' + | ^^ W605 +115 | value = t'{1:\}' +116 | value = t"{t"\{1}"}" + | + = help: Use a raw string literal + +ℹ Safe fix +111 111 | +112 112 | regex = t'\\\_' +113 113 | value = t'\{{1}}' +114 |-value = t'\{1}' + 114 |+value = rt'\{1}' +115 115 | value = t'{1:\}' +116 116 | value = t"{t"\{1}"}" +117 117 | value = rt"{t"\{1}"}" + +W605_1.py:115:14: W605 [*] Invalid escape sequence: `\}` + | +113 | value = t'\{{1}}' +114 | value = t'\{1}' +115 | value = t'{1:\}' + | ^^ W605 +116 | value = t"{t"\{1}"}" +117 | value = rt"{t"\{1}"}" + | + = help: Use a raw string literal + +ℹ Safe fix +112 112 | regex = t'\\\_' +113 113 | value = t'\{{1}}' +114 114 | value = t'\{1}' +115 |-value = t'{1:\}' + 115 |+value = rt'{1:\}' +116 116 | value = t"{t"\{1}"}" +117 117 | value = rt"{t"\{1}"}" +118 118 | + +W605_1.py:116:14: W605 [*] Invalid escape sequence: `\{` + | +114 | value = t'\{1}' +115 | value = t'{1:\}' +116 | value = t"{t"\{1}"}" + | ^^ W605 +117 | value = rt"{t"\{1}"}" + | + = help: Use a raw string literal + +ℹ Safe fix +113 113 | value = t'\{{1}}' +114 114 | value = t'\{1}' +115 115 | value = t'{1:\}' +116 |-value = t"{t"\{1}"}" + 116 |+value = t"{rt"\{1}"}" +117 117 | value = rt"{t"\{1}"}" +118 118 | +119 119 | # Okay + +W605_1.py:117:15: W605 [*] Invalid escape sequence: `\{` + | +115 | value = t'{1:\}' +116 | value = t"{t"\{1}"}" +117 | value = rt"{t"\{1}"}" + | ^^ W605 +118 | +119 | # Okay + | + = help: Use a raw string literal + +ℹ Safe fix +114 114 | value = t'\{1}' +115 115 | value = t'{1:\}' +116 116 | value = t"{t"\{1}"}" +117 |-value = rt"{t"\{1}"}" + 117 |+value = rt"{rt"\{1}"}" +118 118 | +119 119 | # Okay +120 120 | value = rt'\{{1}}' + +W605_1.py:126:9: W605 [*] Invalid escape sequence: `\d` + | +125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434 +126 | t"{{}}+-\d" + | ^^ W605 +127 | t"\n{{}}+-\d+" +128 | t"\n{{}}�+-\d+" + | + = help: Use a raw string literal + +ℹ Safe fix +123 123 | value = t"{rt"\{1}"}" +124 124 | +125 125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434 +126 |-t"{{}}+-\d" + 126 |+rt"{{}}+-\d" +127 127 | t"\n{{}}+-\d+" +128 128 | t"\n{{}}�+-\d+" +129 129 | + +W605_1.py:127:11: W605 [*] Invalid escape sequence: `\d` + | +125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434 +126 | t"{{}}+-\d" +127 | t"\n{{}}+-\d+" + | ^^ W605 +128 | t"\n{{}}�+-\d+" + | + = help: Add backslash to escape sequence + +ℹ Safe fix +124 124 | +125 125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434 +126 126 | t"{{}}+-\d" +127 |-t"\n{{}}+-\d+" + 127 |+t"\n{{}}+-\\d+" +128 128 | t"\n{{}}�+-\d+" +129 129 | +130 130 | # See https://github.com/astral-sh/ruff/issues/11491 + +W605_1.py:128:12: W605 [*] Invalid escape sequence: `\d` + | +126 | t"{{}}+-\d" +127 | t"\n{{}}+-\d+" +128 | t"\n{{}}�+-\d+" + | ^^ W605 +129 | +130 | # See https://github.com/astral-sh/ruff/issues/11491 + | + = help: Add backslash to escape sequence + +ℹ Safe fix +125 125 | # Regression tests for https://github.com/astral-sh/ruff/issues/10434 +126 126 | t"{{}}+-\d" +127 127 | t"\n{{}}+-\d+" +128 |-t"\n{{}}�+-\d+" + 128 |+t"\n{{}}�+-\\d+" +129 129 | +130 130 | # See https://github.com/astral-sh/ruff/issues/11491 +131 131 | total = 10 + +W605_1.py:134:31: W605 [*] Invalid escape sequence: `\I` + | +132 | ok = 7 +133 | incomplete = 3 +134 | s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n" + | ^^ W605 +135 | +136 | # Debug text (should trigger) + | + = help: Add backslash to escape sequence + +ℹ Safe fix +131 131 | total = 10 +132 132 | ok = 7 +133 133 | incomplete = 3 +134 |-s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n" + 134 |+s = t"TOTAL: {total}\nOK: {ok}\\INCOMPLETE: {incomplete}\n" +135 135 | +136 136 | # Debug text (should trigger) +137 137 | t = t"{'\InHere'=}" + +W605_1.py:137:9: W605 [*] Invalid escape sequence: `\I` + | +136 | # Debug text (should trigger) +137 | t = t"{'\InHere'=}" + | ^^ W605 + | + = help: Use a raw string literal + +ℹ Safe fix +134 134 | s = t"TOTAL: {total}\nOK: {ok}\INCOMPLETE: {incomplete}\n" +135 135 | +136 136 | # Debug text (should trigger) +137 |-t = t"{'\InHere'=}" + 137 |+t = t"{r'\InHere'=}" diff --git a/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs b/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs index 4262a7baa9..3e15f4cfe3 100644 --- a/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs +++ b/crates/ruff_linter/src/rules/pyflakes/rules/f_string_missing_placeholders.rs @@ -73,7 +73,7 @@ pub(crate) fn f_string_missing_placeholders(checker: &Checker, expr: &ast::ExprF f_string .elements .iter() - .any(ast::FStringElement::is_expression) + .any(ast::InterpolatedStringElement::is_interpolation) }) { return; } diff --git a/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs b/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs index 3e75254309..64db3f1e40 100644 --- a/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs +++ b/crates/ruff_linter/src/rules/pylint/rules/assert_on_string_literal.rs @@ -77,10 +77,10 @@ pub(crate) fn assert_on_string_literal(checker: &Checker, test: &Expr) { ast::FStringPart::Literal(literal) => literal.is_empty(), ast::FStringPart::FString(f_string) => { f_string.elements.iter().all(|element| match element { - ast::FStringElement::Literal(ast::FStringLiteralElement { - value, .. - }) => value.is_empty(), - ast::FStringElement::Expression(_) => false, + ast::InterpolatedStringElement::Literal( + ast::InterpolatedStringLiteralElement { value, .. }, + ) => value.is_empty(), + ast::InterpolatedStringElement::Interpolation(_) => false, }) } }) { @@ -89,10 +89,10 @@ pub(crate) fn assert_on_string_literal(checker: &Checker, test: &Expr) { ast::FStringPart::Literal(literal) => !literal.is_empty(), ast::FStringPart::FString(f_string) => { f_string.elements.iter().any(|element| match element { - ast::FStringElement::Literal(ast::FStringLiteralElement { - value, .. - }) => !value.is_empty(), - ast::FStringElement::Expression(_) => false, + ast::InterpolatedStringElement::Literal( + ast::InterpolatedStringLiteralElement { value, .. }, + ) => !value.is_empty(), + ast::InterpolatedStringElement::Interpolation(_) => false, }) } }) { diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs index 12a7798ad3..b1b888cf4c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/use_pep604_annotation.rs @@ -252,6 +252,7 @@ fn is_allowed_value(expr: &Expr) -> bool { | Expr::Compare(_) | Expr::Call(_) | Expr::FString(_) + | Expr::TString(_) | Expr::StringLiteral(_) | Expr::BytesLiteral(_) | Expr::NumberLiteral(_) diff --git a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs index 93946dcb1f..1dd8d9c89e 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/bit_count.rs @@ -137,6 +137,7 @@ pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) { Expr::StringLiteral(inner) => inner.value.is_implicit_concatenated(), Expr::BytesLiteral(inner) => inner.value.is_implicit_concatenated(), Expr::FString(inner) => inner.value.is_implicit_concatenated(), + Expr::TString(inner) => inner.value.is_implicit_concatenated(), Expr::Await(_) | Expr::Starred(_) diff --git a/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs b/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs index d1b1ef67ed..19228605b8 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/ambiguous_unicode_character.rs @@ -3,7 +3,7 @@ use std::fmt; use bitflags::bitflags; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::{self as ast, StringLike}; +use ruff_python_ast::{self as ast, FString, StringLike, TString}; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::Locator; @@ -211,8 +211,9 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &Checker, string_like: } } ast::StringLikePart::Bytes(_) => {} - ast::StringLikePart::FString(f_string) => { - for literal in f_string.elements.literals() { + ast::StringLikePart::FString(FString { elements, .. }) + | ast::StringLikePart::TString(TString { elements, .. }) => { + for literal in elements.literals() { let text = checker.locator().slice(literal); for candidate in ambiguous_unicode_character(text, literal.range(), checker.settings) diff --git a/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs b/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs index 5c956340e1..8b3fad38dc 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/assert_with_print_message.rs @@ -88,9 +88,9 @@ pub(crate) fn assert_with_print_message(checker: &Checker, stmt: &ast::StmtAsser mod print_arguments { use itertools::Itertools; use ruff_python_ast::{ - Arguments, ConversionFlag, Expr, ExprFString, FString, FStringElement, FStringElements, - FStringExpressionElement, FStringFlags, FStringLiteralElement, FStringValue, StringLiteral, - StringLiteralFlags, + Arguments, ConversionFlag, Expr, ExprFString, FString, FStringFlags, FStringValue, + InterpolatedElement, InterpolatedStringElement, InterpolatedStringElements, + InterpolatedStringLiteralElement, StringLiteral, StringLiteralFlags, }; use ruff_text_size::TextRange; @@ -103,14 +103,14 @@ mod print_arguments { /// `FStringLiteralElement`. /// - if the expression is an f-string, the elements will be returned as-is. /// - otherwise, the expression will be wrapped in a `FStringExpressionElement`. - fn expr_to_fstring_elements(expr: &Expr) -> Vec { + fn expr_to_fstring_elements(expr: &Expr) -> Vec { match expr { // If the expression is a string literal, convert each part to a `FStringLiteralElement`. Expr::StringLiteral(string) => string .value .iter() .map(|part| { - FStringElement::Literal(FStringLiteralElement { + InterpolatedStringElement::Literal(InterpolatedStringLiteralElement { value: part.value.clone(), range: TextRange::default(), }) @@ -122,13 +122,15 @@ mod print_arguments { // Otherwise, return the expression as a single `FStringExpressionElement` wrapping // the expression. - expr => vec![FStringElement::Expression(FStringExpressionElement { - expression: Box::new(expr.clone()), - debug_text: None, - conversion: ConversionFlag::None, - format_spec: None, - range: TextRange::default(), - })], + expr => vec![InterpolatedStringElement::Interpolation( + InterpolatedElement { + expression: Box::new(expr.clone()), + debug_text: None, + conversion: ConversionFlag::None, + format_spec: None, + range: TextRange::default(), + }, + )], } } @@ -140,11 +142,11 @@ mod print_arguments { /// checking if the `sep` and `args` arguments to `print` are all string /// literals. fn fstring_elements_to_string_literals<'a>( - mut elements: impl ExactSizeIterator, + mut elements: impl ExactSizeIterator, flags: StringLiteralFlags, ) -> Option> { elements.try_fold(Vec::with_capacity(elements.len()), |mut acc, element| { - if let FStringElement::Literal(literal) = element { + if let InterpolatedStringElement::Literal(literal) = element { acc.push(StringLiteral { value: literal.value.clone(), flags, @@ -162,8 +164,8 @@ mod print_arguments { /// This function will return [`None`] if any of the arguments are not string literals, /// or if there are no arguments at all. fn args_to_string_literal_expr<'a>( - args: impl ExactSizeIterator>, - sep: impl ExactSizeIterator, + args: impl ExactSizeIterator>, + sep: impl ExactSizeIterator, flags: StringLiteralFlags, ) -> Option { // If there are no arguments, short-circuit and return `None` @@ -220,8 +222,8 @@ mod print_arguments { /// Also note that the iterator arguments of this function are consumed, /// as opposed to the references taken by [`args_to_string_literal_expr`]. fn args_to_fstring_expr( - mut args: impl ExactSizeIterator>, - sep: impl ExactSizeIterator, + mut args: impl ExactSizeIterator>, + sep: impl ExactSizeIterator, flags: FStringFlags, ) -> Option { // If there are no arguments, short-circuit and return `None` @@ -236,7 +238,7 @@ mod print_arguments { Some(Expr::FString(ExprFString { value: FStringValue::single(FString { - elements: FStringElements::from(fstring_elements), + elements: InterpolatedStringElements::from(fstring_elements), flags, range: TextRange::default(), }), @@ -273,10 +275,12 @@ mod print_arguments { ) .map(expr_to_fstring_elements) .unwrap_or_else(|| { - vec![FStringElement::Literal(FStringLiteralElement { - range: TextRange::default(), - value: " ".into(), - })] + vec![InterpolatedStringElement::Literal( + InterpolatedStringLiteralElement { + range: TextRange::default(), + value: " ".into(), + }, + )] }); let args = arguments diff --git a/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs b/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs index 7318e8e683..fa88d0b083 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/explicit_f_string_type_conversion.rs @@ -54,11 +54,11 @@ impl AlwaysFixableViolation for ExplicitFStringTypeConversion { /// RUF010 pub(crate) fn explicit_f_string_type_conversion(checker: &Checker, f_string: &ast::FString) { for (index, element) in f_string.elements.iter().enumerate() { - let Some(ast::FStringExpressionElement { + let Some(ast::InterpolatedElement { expression, conversion, .. - }) = element.as_expression() + }) = element.as_interpolation() else { continue; }; diff --git a/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs b/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs index f5152cf1af..f691b3b9af 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/invalid_formatter_suppression_comment.rs @@ -303,10 +303,11 @@ const fn is_valid_enclosing_node(node: AnyNodeRef) -> bool { | AnyNodeRef::ExprYieldFrom(_) | AnyNodeRef::ExprCompare(_) | AnyNodeRef::ExprCall(_) - | AnyNodeRef::FStringExpressionElement(_) - | AnyNodeRef::FStringLiteralElement(_) - | AnyNodeRef::FStringFormatSpec(_) + | AnyNodeRef::InterpolatedElement(_) + | AnyNodeRef::InterpolatedStringLiteralElement(_) + | AnyNodeRef::InterpolatedStringFormatSpec(_) | AnyNodeRef::ExprFString(_) + | AnyNodeRef::ExprTString(_) | AnyNodeRef::ExprStringLiteral(_) | AnyNodeRef::ExprBytesLiteral(_) | AnyNodeRef::ExprNumberLiteral(_) @@ -344,6 +345,7 @@ const fn is_valid_enclosing_node(node: AnyNodeRef) -> bool { | AnyNodeRef::TypeParamTypeVarTuple(_) | AnyNodeRef::TypeParamParamSpec(_) | AnyNodeRef::FString(_) + | AnyNodeRef::TString(_) | AnyNodeRef::StringLiteral(_) | AnyNodeRef::BytesLiteral(_) | AnyNodeRef::Identifier(_) => false, diff --git a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs index 359b8472f8..e13cb03dd5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs @@ -214,7 +214,7 @@ fn should_be_fstring( for f_string in value.f_strings() { let mut has_name = false; - for element in f_string.elements.expressions() { + for element in f_string.elements.interpolations() { if let ast::Expr::Name(ast::ExprName { id, .. }) = element.expression.as_ref() { if arg_names.contains(id) { return false; diff --git a/crates/ruff_python_ast/ast.toml b/crates/ruff_python_ast/ast.toml index 8d2b9e067d..38a9415515 100644 --- a/crates/ruff_python_ast/ast.toml +++ b/crates/ruff_python_ast/ast.toml @@ -433,6 +433,18 @@ See also [JoinedStr](https://docs.python.org/3/library/ast.html#ast.JoinedStr)"" fields = [{ name = "value", type = "FStringValue" }] custom_source_order = true +[Expr.nodes.ExprTString] +doc = """An AST node that represents either a single-part t-string literal +or an implicitly concatenated t-string literal. + +This type differs from the original Python AST `TemplateStr` in that it +doesn't join the implicitly concatenated parts into a single string. Instead, +it keeps them separate and provide various methods to access the parts. + +See also [TemplateStr](https://docs.python.org/3/library/ast.html#ast.TemplateStr)""" +fields = [{ name = "value", type = "TStringValue" }] +custom_source_order = true + [Expr.nodes.ExprStringLiteral] doc = """An AST node that represents either a single-part string literal or an implicitly concatenated string literal.""" @@ -539,9 +551,10 @@ doc = "See also [excepthandler](https://docs.python.org/3/library/ast.html#ast.e [ExceptHandler.nodes] ExceptHandlerExceptHandler = {} -[FStringElement.nodes] -FStringExpressionElement = { variant = "Expression" } -FStringLiteralElement = { variant = "Literal" } +[InterpolatedStringElement.nodes] +InterpolatedElement = { variant = "Interpolation" } +InterpolatedStringLiteralElement = { variant = "Literal" } + [Pattern] doc = "See also [pattern](https://docs.python.org/3/library/ast.html#ast.pattern)" @@ -565,7 +578,7 @@ TypeParamTypeVarTuple = {} TypeParamParamSpec = {} [ungrouped.nodes] -FStringFormatSpec = {} +InterpolatedStringFormatSpec = {} PatternArguments = {} PatternKeyword = {} Comprehension = {} @@ -581,6 +594,7 @@ Decorator = {} ElifElseClause = {} TypeParams = {} FString = {} +TString = {} StringLiteral = {} BytesLiteral = {} Identifier = {} diff --git a/crates/ruff_python_ast/generate.py b/crates/ruff_python_ast/generate.py index f6afab68cc..981e23fa09 100644 --- a/crates/ruff_python_ast/generate.py +++ b/crates/ruff_python_ast/generate.py @@ -15,7 +15,7 @@ from typing import Any import tomllib # Types that require `crate::`. We can slowly remove these types as we move them to generate scripts. -types_requiring_create_prefix = { +types_requiring_crate_prefix = { "IpyEscapeKind", "ExprContext", "Identifier", @@ -23,6 +23,7 @@ types_requiring_create_prefix = { "BytesLiteralValue", "StringLiteralValue", "FStringValue", + "TStringValue", "Arguments", "CmpOp", "Comprehension", @@ -762,7 +763,7 @@ def write_node(out: list[str], ast: Ast) -> None: ty = field.parsed_ty rust_ty = f"{field.parsed_ty.name}" - if ty.name in types_requiring_create_prefix: + if ty.name in types_requiring_crate_prefix: rust_ty = f"crate::{rust_ty}" if ty.slice_: rust_ty = f"[{rust_ty}]" diff --git a/crates/ruff_python_ast/src/comparable.rs b/crates/ruff_python_ast/src/comparable.rs index 5197bfd38a..a759ef208a 100644 --- a/crates/ruff_python_ast/src/comparable.rs +++ b/crates/ruff_python_ast/src/comparable.rs @@ -512,48 +512,57 @@ impl<'a> From<&'a ast::ExceptHandler> for ComparableExceptHandler<'a> { } #[derive(Debug, PartialEq, Eq, Hash)] -pub enum ComparableFStringElement<'a> { +pub enum ComparableInterpolatedStringElement<'a> { Literal(Cow<'a, str>), - FStringExpressionElement(FStringExpressionElement<'a>), + InterpolatedElement(InterpolatedElement<'a>), } #[derive(Debug, PartialEq, Eq, Hash)] -pub struct FStringExpressionElement<'a> { +pub struct InterpolatedElement<'a> { expression: ComparableExpr<'a>, debug_text: Option<&'a ast::DebugText>, conversion: ast::ConversionFlag, - format_spec: Option>>, + format_spec: Option>>, } -impl<'a> From<&'a ast::FStringElement> for ComparableFStringElement<'a> { - fn from(fstring_element: &'a ast::FStringElement) -> Self { - match fstring_element { - ast::FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => { - Self::Literal(value.as_ref().into()) +impl<'a> From<&'a ast::InterpolatedStringElement> for ComparableInterpolatedStringElement<'a> { + fn from(interpolated_string_element: &'a ast::InterpolatedStringElement) -> Self { + match interpolated_string_element { + ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { + value, + .. + }) => Self::Literal(value.as_ref().into()), + ast::InterpolatedStringElement::Interpolation(formatted_value) => { + formatted_value.into() } - ast::FStringElement::Expression(formatted_value) => formatted_value.into(), } } } -impl<'a> From<&'a ast::FStringExpressionElement> for ComparableFStringElement<'a> { - fn from(fstring_expression_element: &'a ast::FStringExpressionElement) -> Self { - let ast::FStringExpressionElement { +impl<'a> From<&'a ast::InterpolatedElement> for InterpolatedElement<'a> { + fn from(interpolated_element: &'a ast::InterpolatedElement) -> Self { + let ast::InterpolatedElement { expression, debug_text, conversion, format_spec, range: _, - } = fstring_expression_element; + } = interpolated_element; - Self::FStringExpressionElement(FStringExpressionElement { + Self { expression: (expression).into(), debug_text: debug_text.as_ref(), conversion: *conversion, format_spec: format_spec .as_ref() .map(|spec| spec.elements.iter().map(Into::into).collect()), - }) + } + } +} + +impl<'a> From<&'a ast::InterpolatedElement> for ComparableInterpolatedStringElement<'a> { + fn from(interpolated_element: &'a ast::InterpolatedElement) -> Self { + Self::InterpolatedElement(interpolated_element.into()) } } @@ -610,7 +619,7 @@ impl<'a> From> for ComparableLiteral<'a> { #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableFString<'a> { - elements: Box<[ComparableFStringElement<'a>]>, + elements: Box<[ComparableInterpolatedStringElement<'a>]>, } impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> { @@ -637,7 +646,7 @@ impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> { fn from(value: &'a ast::FStringValue) -> Self { #[derive(Default)] struct Collector<'a> { - elements: Vec>, + elements: Vec>, } impl<'a> Collector<'a> { @@ -647,17 +656,17 @@ impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> { // `elements` vector, while subsequent strings // are concatenated onto this top string. fn push_literal(&mut self, literal: &'a str) { - if let Some(ComparableFStringElement::Literal(existing_literal)) = + if let Some(ComparableInterpolatedStringElement::Literal(existing_literal)) = self.elements.last_mut() { existing_literal.to_mut().push_str(literal); } else { self.elements - .push(ComparableFStringElement::Literal(literal.into())); + .push(ComparableInterpolatedStringElement::Literal(literal.into())); } } - fn push_expression(&mut self, expression: &'a ast::FStringExpressionElement) { + fn push_expression(&mut self, expression: &'a ast::InterpolatedElement) { self.elements.push(expression.into()); } } @@ -672,10 +681,10 @@ impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> { ast::FStringPart::FString(fstring) => { for element in &fstring.elements { match element { - ast::FStringElement::Literal(literal) => { + ast::InterpolatedStringElement::Literal(literal) => { collector.push_literal(&literal.value); } - ast::FStringElement::Expression(expression) => { + ast::InterpolatedStringElement::Interpolation(expression) => { collector.push_expression(expression); } } @@ -690,6 +699,133 @@ impl<'a> From<&'a ast::FStringValue> for ComparableFString<'a> { } } +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ComparableTString<'a> { + strings: Box<[ComparableInterpolatedStringElement<'a>]>, + interpolations: Box<[InterpolatedElement<'a>]>, +} + +impl<'a> From<&'a ast::TStringValue> for ComparableTString<'a> { + // The approach taken below necessarily deviates from the + // corresponding implementation for [`ast::FStringValue`]. + // The reason is that a t-string value is composed of _three_ + // non-comparable parts: literals, f-string expressions, and + // t-string interpolations. Since we have merged the AST nodes + // that capture f-string expressions and t-string interpolations + // into the shared [`ast::InterpolatedElement`], we must + // be careful to distinguish between them here. + // + // Consequently, we model a [`ComparableTString`] on the actual + // [CPython implementation] of a `string.templatelib.Template` object: + // it is composed of `strings` and `interpolations`. In CPython, + // the `strings` field is a tuple of honest strings (since f-strings + // are evaluated). Our `strings` field will house both f-string + // expressions and string literals. + // + // Finally, as in CPython, we must be careful to ensure that the length + // of `strings` is always one more than the length of `interpolations` - + // that way we can recover the original reading order by interleaving + // starting with `strings`. This is how we can tell the + // difference between, e.g. `t"{foo}bar"` and `t"bar{foo}"`. + // + // - [CPython implementation](https://github.com/python/cpython/blob/c91ad5da9d92eac4718e4da8d53689c3cc24535e/Python/codegen.c#L4052-L4103) + fn from(value: &'a ast::TStringValue) -> Self { + struct Collector<'a> { + strings: Vec>, + interpolations: Vec>, + } + + impl Default for Collector<'_> { + fn default() -> Self { + Self { + strings: vec![ComparableInterpolatedStringElement::Literal("".into())], + interpolations: vec![], + } + } + } + + impl<'a> Collector<'a> { + // The logic for concatenating adjacent string literals + // occurs here, implicitly: when we encounter a sequence + // of string literals, the first gets pushed to the + // `strings` vector, while subsequent strings + // are concatenated onto this top string. + fn push_literal(&mut self, literal: &'a str) { + if let Some(ComparableInterpolatedStringElement::Literal(existing_literal)) = + self.strings.last_mut() + { + existing_literal.to_mut().push_str(literal); + } else { + self.strings + .push(ComparableInterpolatedStringElement::Literal(literal.into())); + } + } + + fn start_new_literal(&mut self) { + self.strings + .push(ComparableInterpolatedStringElement::Literal("".into())); + } + + fn push_fstring_expression(&mut self, expression: &'a ast::InterpolatedElement) { + if let Some(ComparableInterpolatedStringElement::Literal(last_literal)) = + self.strings.last() + { + // Recall that we insert empty strings after + // each interpolation. If we encounter an f-string + // expression, we replace the empty string with it. + if last_literal.is_empty() { + self.strings.pop(); + } + } + self.strings.push(expression.into()); + } + fn push_tstring_interpolation(&mut self, expression: &'a ast::InterpolatedElement) { + self.interpolations.push(expression.into()); + self.start_new_literal(); + } + } + + let mut collector = Collector::default(); + + for part in value { + match part { + ast::TStringPart::Literal(string_literal) => { + collector.push_literal(&string_literal.value); + } + ast::TStringPart::TString(fstring) => { + for element in &fstring.elements { + match element { + ast::InterpolatedStringElement::Literal(literal) => { + collector.push_literal(&literal.value); + } + ast::InterpolatedStringElement::Interpolation(interpolation) => { + collector.push_tstring_interpolation(interpolation); + } + } + } + } + ast::TStringPart::FString(fstring) => { + for element in &fstring.elements { + match element { + ast::InterpolatedStringElement::Literal(literal) => { + collector.push_literal(&literal.value); + } + ast::InterpolatedStringElement::Interpolation(expression) => { + collector.push_fstring_expression(expression); + } + } + } + } + } + } + + Self { + strings: collector.strings.into_boxed_slice(), + interpolations: collector.interpolations.into_boxed_slice(), + } + } +} + #[derive(Debug, PartialEq, Eq, Hash)] pub struct ComparableStringLiteral<'a> { value: &'a str, @@ -833,11 +969,11 @@ pub struct ExprCall<'a> { } #[derive(Debug, PartialEq, Eq, Hash)] -pub struct ExprFStringExpressionElement<'a> { +pub struct ExprInterpolatedElement<'a> { value: Box>, debug_text: Option<&'a ast::DebugText>, conversion: ast::ConversionFlag, - format_spec: Vec>, + format_spec: Vec>, } #[derive(Debug, PartialEq, Eq, Hash)] @@ -845,6 +981,11 @@ pub struct ExprFString<'a> { value: ComparableFString<'a>, } +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct ExprTString<'a> { + value: ComparableTString<'a>, +} + #[derive(Debug, PartialEq, Eq, Hash)] pub struct ExprStringLiteral<'a> { value: ComparableStringLiteral<'a>, @@ -929,8 +1070,10 @@ pub enum ComparableExpr<'a> { YieldFrom(ExprYieldFrom<'a>), Compare(ExprCompare<'a>), Call(ExprCall<'a>), - FStringExpressionElement(ExprFStringExpressionElement<'a>), + FStringExpressionElement(ExprInterpolatedElement<'a>), FString(ExprFString<'a>), + TStringInterpolationElement(ExprInterpolatedElement<'a>), + TString(ExprTString<'a>), StringLiteral(ExprStringLiteral<'a>), BytesLiteral(ExprBytesLiteral<'a>), NumberLiteral(ExprNumberLiteral<'a>), @@ -1089,6 +1232,11 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> { value: value.into(), }) } + ast::Expr::TString(ast::ExprTString { value, range: _ }) => { + Self::TString(ExprTString { + value: value.into(), + }) + } ast::Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ }) => { Self::StringLiteral(ExprStringLiteral { value: ComparableStringLiteral { diff --git a/crates/ruff_python_ast/src/expression.rs b/crates/ruff_python_ast/src/expression.rs index 48a0342971..14fe4e1170 100644 --- a/crates/ruff_python_ast/src/expression.rs +++ b/crates/ruff_python_ast/src/expression.rs @@ -4,7 +4,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::{ self as ast, AnyNodeRef, AnyStringFlags, Expr, ExprBytesLiteral, ExprFString, ExprRef, - ExprStringLiteral, StringFlags, + ExprStringLiteral, ExprTString, StringFlags, }; impl<'a> From<&'a Box> for ExprRef<'a> { @@ -80,17 +80,18 @@ impl LiteralExpressionRef<'_> { } /// An enum that holds a reference to a string-like expression from the AST. This includes string -/// literals, bytes literals, and f-strings. +/// literals, bytes literals, f-strings, and t-strings. #[derive(Copy, Clone, Debug, PartialEq)] pub enum StringLike<'a> { String(&'a ast::ExprStringLiteral), Bytes(&'a ast::ExprBytesLiteral), FString(&'a ast::ExprFString), + TString(&'a ast::ExprTString), } impl<'a> StringLike<'a> { - pub const fn is_fstring(self) -> bool { - matches!(self, Self::FString(_)) + pub const fn is_interpolated_string(self) -> bool { + matches!(self, Self::TString(_) | Self::FString(_)) } /// Returns an iterator over the [`StringLikePart`] contained in this string-like expression. @@ -99,6 +100,7 @@ impl<'a> StringLike<'a> { StringLike::String(expr) => StringLikePartIter::String(expr.value.iter()), StringLike::Bytes(expr) => StringLikePartIter::Bytes(expr.value.iter()), StringLike::FString(expr) => StringLikePartIter::FString(expr.value.iter()), + StringLike::TString(expr) => StringLikePartIter::TString(expr.value.iter()), } } @@ -108,6 +110,7 @@ impl<'a> StringLike<'a> { Self::String(ExprStringLiteral { value, .. }) => value.is_implicit_concatenated(), Self::Bytes(ExprBytesLiteral { value, .. }) => value.is_implicit_concatenated(), Self::FString(ExprFString { value, .. }) => value.is_implicit_concatenated(), + Self::TString(ExprTString { value, .. }) => value.is_implicit_concatenated(), } } @@ -116,6 +119,7 @@ impl<'a> StringLike<'a> { StringLike::String(expr) => ExprRef::StringLiteral(expr), StringLike::Bytes(expr) => ExprRef::BytesLiteral(expr), StringLike::FString(expr) => ExprRef::FString(expr), + StringLike::TString(expr) => ExprRef::TString(expr), } } } @@ -138,12 +142,19 @@ impl<'a> From<&'a ast::ExprFString> for StringLike<'a> { } } +impl<'a> From<&'a ast::ExprTString> for StringLike<'a> { + fn from(value: &'a ast::ExprTString) -> Self { + StringLike::TString(value) + } +} + impl<'a> From<&StringLike<'a>> for ExprRef<'a> { fn from(value: &StringLike<'a>) -> Self { match value { StringLike::String(expr) => ExprRef::StringLiteral(expr), StringLike::Bytes(expr) => ExprRef::BytesLiteral(expr), StringLike::FString(expr) => ExprRef::FString(expr), + StringLike::TString(expr) => ExprRef::TString(expr), } } } @@ -160,6 +171,7 @@ impl<'a> From<&StringLike<'a>> for AnyNodeRef<'a> { StringLike::String(expr) => AnyNodeRef::ExprStringLiteral(expr), StringLike::Bytes(expr) => AnyNodeRef::ExprBytesLiteral(expr), StringLike::FString(expr) => AnyNodeRef::ExprFString(expr), + StringLike::TString(expr) => AnyNodeRef::ExprTString(expr), } } } @@ -172,6 +184,7 @@ impl<'a> TryFrom<&'a Expr> for StringLike<'a> { Expr::StringLiteral(value) => Ok(Self::String(value)), Expr::BytesLiteral(value) => Ok(Self::Bytes(value)), Expr::FString(value) => Ok(Self::FString(value)), + Expr::TString(value) => Ok(Self::TString(value)), _ => Err(()), } } @@ -185,6 +198,7 @@ impl<'a> TryFrom> for StringLike<'a> { AnyNodeRef::ExprStringLiteral(value) => Ok(Self::String(value)), AnyNodeRef::ExprBytesLiteral(value) => Ok(Self::Bytes(value)), AnyNodeRef::ExprFString(value) => Ok(Self::FString(value)), + AnyNodeRef::ExprTString(value) => Ok(Self::TString(value)), _ => Err(()), } } @@ -196,6 +210,7 @@ impl Ranged for StringLike<'_> { StringLike::String(literal) => literal.range(), StringLike::Bytes(literal) => literal.range(), StringLike::FString(literal) => literal.range(), + StringLike::TString(literal) => literal.range(), } } } @@ -206,6 +221,7 @@ pub enum StringLikePart<'a> { String(&'a ast::StringLiteral), Bytes(&'a ast::BytesLiteral), FString(&'a ast::FString), + TString(&'a ast::TString), } impl<'a> StringLikePart<'a> { @@ -215,6 +231,7 @@ impl<'a> StringLikePart<'a> { StringLikePart::String(string) => AnyStringFlags::from(string.flags), StringLikePart::Bytes(bytes) => AnyStringFlags::from(bytes.flags), StringLikePart::FString(f_string) => AnyStringFlags::from(f_string.flags), + StringLikePart::TString(t_string) => AnyStringFlags::from(t_string.flags), } } @@ -238,8 +255,8 @@ impl<'a> StringLikePart<'a> { } } - pub const fn is_fstring(self) -> bool { - matches!(self, Self::FString(_)) + pub const fn is_interpolated_string(self) -> bool { + matches!(self, Self::FString(_) | Self::TString(_)) } } @@ -261,6 +278,12 @@ impl<'a> From<&'a ast::FString> for StringLikePart<'a> { } } +impl<'a> From<&'a ast::TString> for StringLikePart<'a> { + fn from(value: &'a ast::TString) -> Self { + StringLikePart::TString(value) + } +} + impl<'a> From<&StringLikePart<'a>> for AnyNodeRef<'a> { fn from(value: &StringLikePart<'a>) -> Self { AnyNodeRef::from(*value) @@ -273,6 +296,7 @@ impl<'a> From> for AnyNodeRef<'a> { StringLikePart::String(part) => AnyNodeRef::StringLiteral(part), StringLikePart::Bytes(part) => AnyNodeRef::BytesLiteral(part), StringLikePart::FString(part) => AnyNodeRef::FString(part), + StringLikePart::TString(part) => AnyNodeRef::TString(part), } } } @@ -283,6 +307,7 @@ impl Ranged for StringLikePart<'_> { StringLikePart::String(part) => part.range(), StringLikePart::Bytes(part) => part.range(), StringLikePart::FString(part) => part.range(), + StringLikePart::TString(part) => part.range(), } } } @@ -295,6 +320,7 @@ pub enum StringLikePartIter<'a> { String(std::slice::Iter<'a, ast::StringLiteral>), Bytes(std::slice::Iter<'a, ast::BytesLiteral>), FString(std::slice::Iter<'a, ast::FStringPart>), + TString(std::slice::Iter<'a, ast::TStringPart>), } impl<'a> Iterator for StringLikePartIter<'a> { @@ -313,6 +339,16 @@ impl<'a> Iterator for StringLikePartIter<'a> { ast::FStringPart::FString(f_string) => StringLikePart::FString(f_string), } } + StringLikePartIter::TString(inner) => { + let part = inner.next()?; + match part { + ast::TStringPart::Literal(string_literal) => { + StringLikePart::String(string_literal) + } + ast::TStringPart::TString(t_string) => StringLikePart::TString(t_string), + ast::TStringPart::FString(f_string) => StringLikePart::FString(f_string), + } + } }; Some(part) @@ -323,6 +359,7 @@ impl<'a> Iterator for StringLikePartIter<'a> { StringLikePartIter::String(inner) => inner.size_hint(), StringLikePartIter::Bytes(inner) => inner.size_hint(), StringLikePartIter::FString(inner) => inner.size_hint(), + StringLikePartIter::TString(inner) => inner.size_hint(), } } } @@ -341,6 +378,16 @@ impl DoubleEndedIterator for StringLikePartIter<'_> { ast::FStringPart::FString(f_string) => StringLikePart::FString(f_string), } } + StringLikePartIter::TString(inner) => { + let part = inner.next_back()?; + match part { + ast::TStringPart::Literal(string_literal) => { + StringLikePart::String(string_literal) + } + ast::TStringPart::TString(t_string) => StringLikePart::TString(t_string), + ast::TStringPart::FString(f_string) => StringLikePart::FString(f_string), + } + } }; Some(part) diff --git a/crates/ruff_python_ast/src/generated.rs b/crates/ruff_python_ast/src/generated.rs index 166878d973..7fd7db5072 100644 --- a/crates/ruff_python_ast/src/generated.rs +++ b/crates/ruff_python_ast/src/generated.rs @@ -1270,6 +1270,7 @@ pub enum Expr { Compare(crate::ExprCompare), Call(crate::ExprCall), FString(crate::ExprFString), + TString(crate::ExprTString), StringLiteral(crate::ExprStringLiteral), BytesLiteral(crate::ExprBytesLiteral), NumberLiteral(crate::ExprNumberLiteral), @@ -1394,6 +1395,12 @@ impl From for Expr { } } +impl From for Expr { + fn from(node: crate::ExprTString) -> Self { + Self::TString(node) + } +} + impl From for Expr { fn from(node: crate::ExprStringLiteral) -> Self { Self::StringLiteral(node) @@ -1499,6 +1506,7 @@ impl ruff_text_size::Ranged for Expr { Self::Compare(node) => node.range(), Self::Call(node) => node.range(), Self::FString(node) => node.range(), + Self::TString(node) => node.range(), Self::StringLiteral(node) => node.range(), Self::BytesLiteral(node) => node.range(), Self::NumberLiteral(node) => node.range(), @@ -2185,6 +2193,43 @@ impl Expr { } } + #[inline] + pub const fn is_t_string_expr(&self) -> bool { + matches!(self, Self::TString(_)) + } + + #[inline] + pub fn t_string_expr(self) -> Option { + match self { + Self::TString(val) => Some(val), + _ => None, + } + } + + #[inline] + pub fn expect_t_string_expr(self) -> crate::ExprTString { + match self { + Self::TString(val) => val, + _ => panic!("called expect on {self:?}"), + } + } + + #[inline] + pub fn as_t_string_expr_mut(&mut self) -> Option<&mut crate::ExprTString> { + match self { + Self::TString(val) => Some(val), + _ => None, + } + } + + #[inline] + pub fn as_t_string_expr(&self) -> Option<&crate::ExprTString> { + match self { + Self::TString(val) => Some(val), + _ => None, + } + } + #[inline] pub const fn is_string_literal_expr(&self) -> bool { matches!(self, Self::StringLiteral(_)) @@ -2761,67 +2806,67 @@ impl ExceptHandler { } #[derive(Clone, Debug, PartialEq)] -pub enum FStringElement { - Expression(crate::FStringExpressionElement), - Literal(crate::FStringLiteralElement), +pub enum InterpolatedStringElement { + Interpolation(crate::InterpolatedElement), + Literal(crate::InterpolatedStringLiteralElement), } -impl From for FStringElement { - fn from(node: crate::FStringExpressionElement) -> Self { - Self::Expression(node) +impl From for InterpolatedStringElement { + fn from(node: crate::InterpolatedElement) -> Self { + Self::Interpolation(node) } } -impl From for FStringElement { - fn from(node: crate::FStringLiteralElement) -> Self { +impl From for InterpolatedStringElement { + fn from(node: crate::InterpolatedStringLiteralElement) -> Self { Self::Literal(node) } } -impl ruff_text_size::Ranged for FStringElement { +impl ruff_text_size::Ranged for InterpolatedStringElement { fn range(&self) -> ruff_text_size::TextRange { match self { - Self::Expression(node) => node.range(), + Self::Interpolation(node) => node.range(), Self::Literal(node) => node.range(), } } } #[allow(dead_code, clippy::match_wildcard_for_single_variants)] -impl FStringElement { +impl InterpolatedStringElement { #[inline] - pub const fn is_expression(&self) -> bool { - matches!(self, Self::Expression(_)) + pub const fn is_interpolation(&self) -> bool { + matches!(self, Self::Interpolation(_)) } #[inline] - pub fn expression(self) -> Option { + pub fn interpolation(self) -> Option { match self { - Self::Expression(val) => Some(val), + Self::Interpolation(val) => Some(val), _ => None, } } #[inline] - pub fn expect_expression(self) -> crate::FStringExpressionElement { + pub fn expect_interpolation(self) -> crate::InterpolatedElement { match self { - Self::Expression(val) => val, + Self::Interpolation(val) => val, _ => panic!("called expect on {self:?}"), } } #[inline] - pub fn as_expression_mut(&mut self) -> Option<&mut crate::FStringExpressionElement> { + pub fn as_interpolation_mut(&mut self) -> Option<&mut crate::InterpolatedElement> { match self { - Self::Expression(val) => Some(val), + Self::Interpolation(val) => Some(val), _ => None, } } #[inline] - pub fn as_expression(&self) -> Option<&crate::FStringExpressionElement> { + pub fn as_interpolation(&self) -> Option<&crate::InterpolatedElement> { match self { - Self::Expression(val) => Some(val), + Self::Interpolation(val) => Some(val), _ => None, } } @@ -2832,7 +2877,7 @@ impl FStringElement { } #[inline] - pub fn literal(self) -> Option { + pub fn literal(self) -> Option { match self { Self::Literal(val) => Some(val), _ => None, @@ -2840,7 +2885,7 @@ impl FStringElement { } #[inline] - pub fn expect_literal(self) -> crate::FStringLiteralElement { + pub fn expect_literal(self) -> crate::InterpolatedStringLiteralElement { match self { Self::Literal(val) => val, _ => panic!("called expect on {self:?}"), @@ -2848,7 +2893,7 @@ impl FStringElement { } #[inline] - pub fn as_literal_mut(&mut self) -> Option<&mut crate::FStringLiteralElement> { + pub fn as_literal_mut(&mut self) -> Option<&mut crate::InterpolatedStringLiteralElement> { match self { Self::Literal(val) => Some(val), _ => None, @@ -2856,7 +2901,7 @@ impl FStringElement { } #[inline] - pub fn as_literal(&self) -> Option<&crate::FStringLiteralElement> { + pub fn as_literal(&self) -> Option<&crate::InterpolatedStringLiteralElement> { match self { Self::Literal(val) => Some(val), _ => None, @@ -3659,6 +3704,12 @@ impl ruff_text_size::Ranged for crate::ExprFString { } } +impl ruff_text_size::Ranged for crate::ExprTString { + fn range(&self) -> ruff_text_size::TextRange { + self.range + } +} + impl ruff_text_size::Ranged for crate::ExprStringLiteral { fn range(&self) -> ruff_text_size::TextRange { self.range @@ -3749,13 +3800,13 @@ impl ruff_text_size::Ranged for crate::ExceptHandlerExceptHandler { } } -impl ruff_text_size::Ranged for crate::FStringExpressionElement { +impl ruff_text_size::Ranged for crate::InterpolatedElement { fn range(&self) -> ruff_text_size::TextRange { self.range } } -impl ruff_text_size::Ranged for crate::FStringLiteralElement { +impl ruff_text_size::Ranged for crate::InterpolatedStringLiteralElement { fn range(&self) -> ruff_text_size::TextRange { self.range } @@ -3827,7 +3878,7 @@ impl ruff_text_size::Ranged for crate::TypeParamParamSpec { } } -impl ruff_text_size::Ranged for crate::FStringFormatSpec { +impl ruff_text_size::Ranged for crate::InterpolatedStringFormatSpec { fn range(&self) -> ruff_text_size::TextRange { self.range } @@ -3923,6 +3974,12 @@ impl ruff_text_size::Ranged for crate::FString { } } +impl ruff_text_size::Ranged for crate::TString { + fn range(&self) -> ruff_text_size::TextRange { + self.range + } +} + impl ruff_text_size::Ranged for crate::StringLiteral { fn range(&self) -> ruff_text_size::TextRange { self.range @@ -4015,6 +4072,7 @@ impl Expr { Expr::Compare(node) => node.visit_source_order(visitor), Expr::Call(node) => node.visit_source_order(visitor), Expr::FString(node) => node.visit_source_order(visitor), + Expr::TString(node) => node.visit_source_order(visitor), Expr::StringLiteral(node) => node.visit_source_order(visitor), Expr::BytesLiteral(node) => node.visit_source_order(visitor), Expr::NumberLiteral(node) => node.visit_source_order(visitor), @@ -4045,15 +4103,15 @@ impl ExceptHandler { } } -impl FStringElement { +impl InterpolatedStringElement { #[allow(unused)] pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) where V: crate::visitor::source_order::SourceOrderVisitor<'a> + ?Sized, { match self { - FStringElement::Expression(node) => node.visit_source_order(visitor), - FStringElement::Literal(node) => node.visit_source_order(visitor), + InterpolatedStringElement::Interpolation(node) => node.visit_source_order(visitor), + InterpolatedStringElement::Literal(node) => node.visit_source_order(visitor), } } } @@ -4436,6 +4494,8 @@ pub enum ExprRef<'a> { Call(&'a crate::ExprCall), #[is(name = "f_string_expr")] FString(&'a crate::ExprFString), + #[is(name = "t_string_expr")] + TString(&'a crate::ExprTString), #[is(name = "string_literal_expr")] StringLiteral(&'a crate::ExprStringLiteral), #[is(name = "bytes_literal_expr")] @@ -4487,6 +4547,7 @@ impl<'a> From<&'a Expr> for ExprRef<'a> { Expr::Compare(node) => ExprRef::Compare(node), Expr::Call(node) => ExprRef::Call(node), Expr::FString(node) => ExprRef::FString(node), + Expr::TString(node) => ExprRef::TString(node), Expr::StringLiteral(node) => ExprRef::StringLiteral(node), Expr::BytesLiteral(node) => ExprRef::BytesLiteral(node), Expr::NumberLiteral(node) => ExprRef::NumberLiteral(node), @@ -4613,6 +4674,12 @@ impl<'a> From<&'a crate::ExprFString> for ExprRef<'a> { } } +impl<'a> From<&'a crate::ExprTString> for ExprRef<'a> { + fn from(node: &'a crate::ExprTString) -> Self { + Self::TString(node) + } +} + impl<'a> From<&'a crate::ExprStringLiteral> for ExprRef<'a> { fn from(node: &'a crate::ExprStringLiteral) -> Self { Self::StringLiteral(node) @@ -4718,6 +4785,7 @@ impl ruff_text_size::Ranged for ExprRef<'_> { Self::Compare(node) => node.range(), Self::Call(node) => node.range(), Self::FString(node) => node.range(), + Self::TString(node) => node.range(), Self::StringLiteral(node) => node.range(), Self::BytesLiteral(node) => node.range(), Self::NumberLiteral(node) => node.range(), @@ -4765,36 +4833,38 @@ impl ruff_text_size::Ranged for ExceptHandlerRef<'_> { } #[derive(Clone, Copy, Debug, PartialEq, is_macro::Is)] -pub enum FStringElementRef<'a> { - Expression(&'a crate::FStringExpressionElement), - Literal(&'a crate::FStringLiteralElement), +pub enum InterpolatedStringElementRef<'a> { + Interpolation(&'a crate::InterpolatedElement), + Literal(&'a crate::InterpolatedStringLiteralElement), } -impl<'a> From<&'a FStringElement> for FStringElementRef<'a> { - fn from(node: &'a FStringElement) -> Self { +impl<'a> From<&'a InterpolatedStringElement> for InterpolatedStringElementRef<'a> { + fn from(node: &'a InterpolatedStringElement) -> Self { match node { - FStringElement::Expression(node) => FStringElementRef::Expression(node), - FStringElement::Literal(node) => FStringElementRef::Literal(node), + InterpolatedStringElement::Interpolation(node) => { + InterpolatedStringElementRef::Interpolation(node) + } + InterpolatedStringElement::Literal(node) => InterpolatedStringElementRef::Literal(node), } } } -impl<'a> From<&'a crate::FStringExpressionElement> for FStringElementRef<'a> { - fn from(node: &'a crate::FStringExpressionElement) -> Self { - Self::Expression(node) +impl<'a> From<&'a crate::InterpolatedElement> for InterpolatedStringElementRef<'a> { + fn from(node: &'a crate::InterpolatedElement) -> Self { + Self::Interpolation(node) } } -impl<'a> From<&'a crate::FStringLiteralElement> for FStringElementRef<'a> { - fn from(node: &'a crate::FStringLiteralElement) -> Self { +impl<'a> From<&'a crate::InterpolatedStringLiteralElement> for InterpolatedStringElementRef<'a> { + fn from(node: &'a crate::InterpolatedStringLiteralElement) -> Self { Self::Literal(node) } } -impl ruff_text_size::Ranged for FStringElementRef<'_> { +impl ruff_text_size::Ranged for InterpolatedStringElementRef<'_> { fn range(&self) -> ruff_text_size::TextRange { match self { - Self::Expression(node) => node.range(), + Self::Interpolation(node) => node.range(), Self::Literal(node) => node.range(), } } @@ -4984,6 +5054,7 @@ pub enum AnyNodeRef<'a> { ExprCompare(&'a crate::ExprCompare), ExprCall(&'a crate::ExprCall), ExprFString(&'a crate::ExprFString), + ExprTString(&'a crate::ExprTString), ExprStringLiteral(&'a crate::ExprStringLiteral), ExprBytesLiteral(&'a crate::ExprBytesLiteral), ExprNumberLiteral(&'a crate::ExprNumberLiteral), @@ -4999,8 +5070,8 @@ pub enum AnyNodeRef<'a> { ExprSlice(&'a crate::ExprSlice), ExprIpyEscapeCommand(&'a crate::ExprIpyEscapeCommand), ExceptHandlerExceptHandler(&'a crate::ExceptHandlerExceptHandler), - FStringExpressionElement(&'a crate::FStringExpressionElement), - FStringLiteralElement(&'a crate::FStringLiteralElement), + InterpolatedElement(&'a crate::InterpolatedElement), + InterpolatedStringLiteralElement(&'a crate::InterpolatedStringLiteralElement), PatternMatchValue(&'a crate::PatternMatchValue), PatternMatchSingleton(&'a crate::PatternMatchSingleton), PatternMatchSequence(&'a crate::PatternMatchSequence), @@ -5012,7 +5083,7 @@ pub enum AnyNodeRef<'a> { TypeParamTypeVar(&'a crate::TypeParamTypeVar), TypeParamTypeVarTuple(&'a crate::TypeParamTypeVarTuple), TypeParamParamSpec(&'a crate::TypeParamParamSpec), - FStringFormatSpec(&'a crate::FStringFormatSpec), + InterpolatedStringFormatSpec(&'a crate::InterpolatedStringFormatSpec), PatternArguments(&'a crate::PatternArguments), PatternKeyword(&'a crate::PatternKeyword), Comprehension(&'a crate::Comprehension), @@ -5028,6 +5099,7 @@ pub enum AnyNodeRef<'a> { ElifElseClause(&'a crate::ElifElseClause), TypeParams(&'a crate::TypeParams), FString(&'a crate::FString), + TString(&'a crate::TString), StringLiteral(&'a crate::StringLiteral), BytesLiteral(&'a crate::BytesLiteral), Identifier(&'a crate::Identifier), @@ -5181,6 +5253,7 @@ impl<'a> From<&'a Expr> for AnyNodeRef<'a> { Expr::Compare(node) => AnyNodeRef::ExprCompare(node), Expr::Call(node) => AnyNodeRef::ExprCall(node), Expr::FString(node) => AnyNodeRef::ExprFString(node), + Expr::TString(node) => AnyNodeRef::ExprTString(node), Expr::StringLiteral(node) => AnyNodeRef::ExprStringLiteral(node), Expr::BytesLiteral(node) => AnyNodeRef::ExprBytesLiteral(node), Expr::NumberLiteral(node) => AnyNodeRef::ExprNumberLiteral(node), @@ -5220,6 +5293,7 @@ impl<'a> From> for AnyNodeRef<'a> { ExprRef::Compare(node) => AnyNodeRef::ExprCompare(node), ExprRef::Call(node) => AnyNodeRef::ExprCall(node), ExprRef::FString(node) => AnyNodeRef::ExprFString(node), + ExprRef::TString(node) => AnyNodeRef::ExprTString(node), ExprRef::StringLiteral(node) => AnyNodeRef::ExprStringLiteral(node), ExprRef::BytesLiteral(node) => AnyNodeRef::ExprBytesLiteral(node), ExprRef::NumberLiteral(node) => AnyNodeRef::ExprNumberLiteral(node), @@ -5259,6 +5333,7 @@ impl<'a> AnyNodeRef<'a> { Self::ExprCompare(node) => Some(ExprRef::Compare(node)), Self::ExprCall(node) => Some(ExprRef::Call(node)), Self::ExprFString(node) => Some(ExprRef::FString(node)), + Self::ExprTString(node) => Some(ExprRef::TString(node)), Self::ExprStringLiteral(node) => Some(ExprRef::StringLiteral(node)), Self::ExprBytesLiteral(node) => Some(ExprRef::BytesLiteral(node)), Self::ExprNumberLiteral(node) => Some(ExprRef::NumberLiteral(node)), @@ -5305,29 +5380,39 @@ impl<'a> AnyNodeRef<'a> { } } -impl<'a> From<&'a FStringElement> for AnyNodeRef<'a> { - fn from(node: &'a FStringElement) -> AnyNodeRef<'a> { +impl<'a> From<&'a InterpolatedStringElement> for AnyNodeRef<'a> { + fn from(node: &'a InterpolatedStringElement) -> AnyNodeRef<'a> { match node { - FStringElement::Expression(node) => AnyNodeRef::FStringExpressionElement(node), - FStringElement::Literal(node) => AnyNodeRef::FStringLiteralElement(node), + InterpolatedStringElement::Interpolation(node) => AnyNodeRef::InterpolatedElement(node), + InterpolatedStringElement::Literal(node) => { + AnyNodeRef::InterpolatedStringLiteralElement(node) + } } } } -impl<'a> From> for AnyNodeRef<'a> { - fn from(node: FStringElementRef<'a>) -> AnyNodeRef<'a> { +impl<'a> From> for AnyNodeRef<'a> { + fn from(node: InterpolatedStringElementRef<'a>) -> AnyNodeRef<'a> { match node { - FStringElementRef::Expression(node) => AnyNodeRef::FStringExpressionElement(node), - FStringElementRef::Literal(node) => AnyNodeRef::FStringLiteralElement(node), + InterpolatedStringElementRef::Interpolation(node) => { + AnyNodeRef::InterpolatedElement(node) + } + InterpolatedStringElementRef::Literal(node) => { + AnyNodeRef::InterpolatedStringLiteralElement(node) + } } } } impl<'a> AnyNodeRef<'a> { - pub fn as_f_string_element_ref(self) -> Option> { + pub fn as_interpolated_string_element_ref(self) -> Option> { match self { - Self::FStringExpressionElement(node) => Some(FStringElementRef::Expression(node)), - Self::FStringLiteralElement(node) => Some(FStringElementRef::Literal(node)), + Self::InterpolatedElement(node) => { + Some(InterpolatedStringElementRef::Interpolation(node)) + } + Self::InterpolatedStringLiteralElement(node) => { + Some(InterpolatedStringElementRef::Literal(node)) + } _ => None, } @@ -5683,6 +5768,12 @@ impl<'a> From<&'a crate::ExprFString> for AnyNodeRef<'a> { } } +impl<'a> From<&'a crate::ExprTString> for AnyNodeRef<'a> { + fn from(node: &'a crate::ExprTString) -> AnyNodeRef<'a> { + AnyNodeRef::ExprTString(node) + } +} + impl<'a> From<&'a crate::ExprStringLiteral> for AnyNodeRef<'a> { fn from(node: &'a crate::ExprStringLiteral) -> AnyNodeRef<'a> { AnyNodeRef::ExprStringLiteral(node) @@ -5773,15 +5864,15 @@ impl<'a> From<&'a crate::ExceptHandlerExceptHandler> for AnyNodeRef<'a> { } } -impl<'a> From<&'a crate::FStringExpressionElement> for AnyNodeRef<'a> { - fn from(node: &'a crate::FStringExpressionElement) -> AnyNodeRef<'a> { - AnyNodeRef::FStringExpressionElement(node) +impl<'a> From<&'a crate::InterpolatedElement> for AnyNodeRef<'a> { + fn from(node: &'a crate::InterpolatedElement) -> AnyNodeRef<'a> { + AnyNodeRef::InterpolatedElement(node) } } -impl<'a> From<&'a crate::FStringLiteralElement> for AnyNodeRef<'a> { - fn from(node: &'a crate::FStringLiteralElement) -> AnyNodeRef<'a> { - AnyNodeRef::FStringLiteralElement(node) +impl<'a> From<&'a crate::InterpolatedStringLiteralElement> for AnyNodeRef<'a> { + fn from(node: &'a crate::InterpolatedStringLiteralElement) -> AnyNodeRef<'a> { + AnyNodeRef::InterpolatedStringLiteralElement(node) } } @@ -5851,9 +5942,9 @@ impl<'a> From<&'a crate::TypeParamParamSpec> for AnyNodeRef<'a> { } } -impl<'a> From<&'a crate::FStringFormatSpec> for AnyNodeRef<'a> { - fn from(node: &'a crate::FStringFormatSpec) -> AnyNodeRef<'a> { - AnyNodeRef::FStringFormatSpec(node) +impl<'a> From<&'a crate::InterpolatedStringFormatSpec> for AnyNodeRef<'a> { + fn from(node: &'a crate::InterpolatedStringFormatSpec) -> AnyNodeRef<'a> { + AnyNodeRef::InterpolatedStringFormatSpec(node) } } @@ -5947,6 +6038,12 @@ impl<'a> From<&'a crate::FString> for AnyNodeRef<'a> { } } +impl<'a> From<&'a crate::TString> for AnyNodeRef<'a> { + fn from(node: &'a crate::TString) -> AnyNodeRef<'a> { + AnyNodeRef::TString(node) + } +} + impl<'a> From<&'a crate::StringLiteral> for AnyNodeRef<'a> { fn from(node: &'a crate::StringLiteral) -> AnyNodeRef<'a> { AnyNodeRef::StringLiteral(node) @@ -6013,6 +6110,7 @@ impl ruff_text_size::Ranged for AnyNodeRef<'_> { AnyNodeRef::ExprCompare(node) => node.range(), AnyNodeRef::ExprCall(node) => node.range(), AnyNodeRef::ExprFString(node) => node.range(), + AnyNodeRef::ExprTString(node) => node.range(), AnyNodeRef::ExprStringLiteral(node) => node.range(), AnyNodeRef::ExprBytesLiteral(node) => node.range(), AnyNodeRef::ExprNumberLiteral(node) => node.range(), @@ -6028,8 +6126,8 @@ impl ruff_text_size::Ranged for AnyNodeRef<'_> { AnyNodeRef::ExprSlice(node) => node.range(), AnyNodeRef::ExprIpyEscapeCommand(node) => node.range(), AnyNodeRef::ExceptHandlerExceptHandler(node) => node.range(), - AnyNodeRef::FStringExpressionElement(node) => node.range(), - AnyNodeRef::FStringLiteralElement(node) => node.range(), + AnyNodeRef::InterpolatedElement(node) => node.range(), + AnyNodeRef::InterpolatedStringLiteralElement(node) => node.range(), AnyNodeRef::PatternMatchValue(node) => node.range(), AnyNodeRef::PatternMatchSingleton(node) => node.range(), AnyNodeRef::PatternMatchSequence(node) => node.range(), @@ -6041,7 +6139,7 @@ impl ruff_text_size::Ranged for AnyNodeRef<'_> { AnyNodeRef::TypeParamTypeVar(node) => node.range(), AnyNodeRef::TypeParamTypeVarTuple(node) => node.range(), AnyNodeRef::TypeParamParamSpec(node) => node.range(), - AnyNodeRef::FStringFormatSpec(node) => node.range(), + AnyNodeRef::InterpolatedStringFormatSpec(node) => node.range(), AnyNodeRef::PatternArguments(node) => node.range(), AnyNodeRef::PatternKeyword(node) => node.range(), AnyNodeRef::Comprehension(node) => node.range(), @@ -6057,6 +6155,7 @@ impl ruff_text_size::Ranged for AnyNodeRef<'_> { AnyNodeRef::ElifElseClause(node) => node.range(), AnyNodeRef::TypeParams(node) => node.range(), AnyNodeRef::FString(node) => node.range(), + AnyNodeRef::TString(node) => node.range(), AnyNodeRef::StringLiteral(node) => node.range(), AnyNodeRef::BytesLiteral(node) => node.range(), AnyNodeRef::Identifier(node) => node.range(), @@ -6112,6 +6211,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprCompare(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::ExprCall(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::ExprFString(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::ExprTString(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::ExprStringLiteral(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::ExprBytesLiteral(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::ExprNumberLiteral(node) => std::ptr::NonNull::from(*node).cast(), @@ -6127,8 +6227,10 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprSlice(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::ExprIpyEscapeCommand(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::ExceptHandlerExceptHandler(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::FStringExpressionElement(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::FStringLiteralElement(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::InterpolatedElement(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::InterpolatedStringLiteralElement(node) => { + std::ptr::NonNull::from(*node).cast() + } AnyNodeRef::PatternMatchValue(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::PatternMatchSingleton(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::PatternMatchSequence(node) => std::ptr::NonNull::from(*node).cast(), @@ -6140,7 +6242,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::TypeParamTypeVar(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::TypeParamTypeVarTuple(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::TypeParamParamSpec(node) => std::ptr::NonNull::from(*node).cast(), - AnyNodeRef::FStringFormatSpec(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::InterpolatedStringFormatSpec(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::PatternArguments(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::PatternKeyword(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::Comprehension(node) => std::ptr::NonNull::from(*node).cast(), @@ -6156,6 +6258,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::ElifElseClause(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::TypeParams(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::FString(node) => std::ptr::NonNull::from(*node).cast(), + AnyNodeRef::TString(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::StringLiteral(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::BytesLiteral(node) => std::ptr::NonNull::from(*node).cast(), AnyNodeRef::Identifier(node) => std::ptr::NonNull::from(*node).cast(), @@ -6215,6 +6318,7 @@ impl<'a> AnyNodeRef<'a> { AnyNodeRef::ExprCompare(node) => node.visit_source_order(visitor), AnyNodeRef::ExprCall(node) => node.visit_source_order(visitor), AnyNodeRef::ExprFString(node) => node.visit_source_order(visitor), + AnyNodeRef::ExprTString(node) => node.visit_source_order(visitor), AnyNodeRef::ExprStringLiteral(node) => node.visit_source_order(visitor), AnyNodeRef::ExprBytesLiteral(node) => node.visit_source_order(visitor), AnyNodeRef::ExprNumberLiteral(node) => node.visit_source_order(visitor), @@ -6230,8 +6334,8 @@ impl<'a> AnyNodeRef<'a> { AnyNodeRef::ExprSlice(node) => node.visit_source_order(visitor), AnyNodeRef::ExprIpyEscapeCommand(node) => node.visit_source_order(visitor), AnyNodeRef::ExceptHandlerExceptHandler(node) => node.visit_source_order(visitor), - AnyNodeRef::FStringExpressionElement(node) => node.visit_source_order(visitor), - AnyNodeRef::FStringLiteralElement(node) => node.visit_source_order(visitor), + AnyNodeRef::InterpolatedElement(node) => node.visit_source_order(visitor), + AnyNodeRef::InterpolatedStringLiteralElement(node) => node.visit_source_order(visitor), AnyNodeRef::PatternMatchValue(node) => node.visit_source_order(visitor), AnyNodeRef::PatternMatchSingleton(node) => node.visit_source_order(visitor), AnyNodeRef::PatternMatchSequence(node) => node.visit_source_order(visitor), @@ -6243,7 +6347,7 @@ impl<'a> AnyNodeRef<'a> { AnyNodeRef::TypeParamTypeVar(node) => node.visit_source_order(visitor), AnyNodeRef::TypeParamTypeVarTuple(node) => node.visit_source_order(visitor), AnyNodeRef::TypeParamParamSpec(node) => node.visit_source_order(visitor), - AnyNodeRef::FStringFormatSpec(node) => node.visit_source_order(visitor), + AnyNodeRef::InterpolatedStringFormatSpec(node) => node.visit_source_order(visitor), AnyNodeRef::PatternArguments(node) => node.visit_source_order(visitor), AnyNodeRef::PatternKeyword(node) => node.visit_source_order(visitor), AnyNodeRef::Comprehension(node) => node.visit_source_order(visitor), @@ -6259,6 +6363,7 @@ impl<'a> AnyNodeRef<'a> { AnyNodeRef::ElifElseClause(node) => node.visit_source_order(visitor), AnyNodeRef::TypeParams(node) => node.visit_source_order(visitor), AnyNodeRef::FString(node) => node.visit_source_order(visitor), + AnyNodeRef::TString(node) => node.visit_source_order(visitor), AnyNodeRef::StringLiteral(node) => node.visit_source_order(visitor), AnyNodeRef::BytesLiteral(node) => node.visit_source_order(visitor), AnyNodeRef::Identifier(node) => node.visit_source_order(visitor), @@ -6330,6 +6435,7 @@ impl AnyNodeRef<'_> { | AnyNodeRef::ExprCompare(_) | AnyNodeRef::ExprCall(_) | AnyNodeRef::ExprFString(_) + | AnyNodeRef::ExprTString(_) | AnyNodeRef::ExprStringLiteral(_) | AnyNodeRef::ExprBytesLiteral(_) | AnyNodeRef::ExprNumberLiteral(_) @@ -6355,10 +6461,10 @@ impl AnyNodeRef<'_> { } impl AnyNodeRef<'_> { - pub const fn is_f_string_element(self) -> bool { + pub const fn is_interpolated_string_element(self) -> bool { matches!( self, - AnyNodeRef::FStringExpressionElement(_) | AnyNodeRef::FStringLiteralElement(_) + AnyNodeRef::InterpolatedElement(_) | AnyNodeRef::InterpolatedStringLiteralElement(_) ) } } @@ -6437,6 +6543,7 @@ pub enum NodeKind { ExprCompare, ExprCall, ExprFString, + ExprTString, ExprStringLiteral, ExprBytesLiteral, ExprNumberLiteral, @@ -6452,8 +6559,8 @@ pub enum NodeKind { ExprSlice, ExprIpyEscapeCommand, ExceptHandlerExceptHandler, - FStringExpressionElement, - FStringLiteralElement, + InterpolatedElement, + InterpolatedStringLiteralElement, PatternMatchValue, PatternMatchSingleton, PatternMatchSequence, @@ -6465,7 +6572,7 @@ pub enum NodeKind { TypeParamTypeVar, TypeParamTypeVarTuple, TypeParamParamSpec, - FStringFormatSpec, + InterpolatedStringFormatSpec, PatternArguments, PatternKeyword, Comprehension, @@ -6481,6 +6588,7 @@ pub enum NodeKind { ElifElseClause, TypeParams, FString, + TString, StringLiteral, BytesLiteral, Identifier, @@ -6534,6 +6642,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprCompare(_) => NodeKind::ExprCompare, AnyNodeRef::ExprCall(_) => NodeKind::ExprCall, AnyNodeRef::ExprFString(_) => NodeKind::ExprFString, + AnyNodeRef::ExprTString(_) => NodeKind::ExprTString, AnyNodeRef::ExprStringLiteral(_) => NodeKind::ExprStringLiteral, AnyNodeRef::ExprBytesLiteral(_) => NodeKind::ExprBytesLiteral, AnyNodeRef::ExprNumberLiteral(_) => NodeKind::ExprNumberLiteral, @@ -6549,8 +6658,10 @@ impl AnyNodeRef<'_> { AnyNodeRef::ExprSlice(_) => NodeKind::ExprSlice, AnyNodeRef::ExprIpyEscapeCommand(_) => NodeKind::ExprIpyEscapeCommand, AnyNodeRef::ExceptHandlerExceptHandler(_) => NodeKind::ExceptHandlerExceptHandler, - AnyNodeRef::FStringExpressionElement(_) => NodeKind::FStringExpressionElement, - AnyNodeRef::FStringLiteralElement(_) => NodeKind::FStringLiteralElement, + AnyNodeRef::InterpolatedElement(_) => NodeKind::InterpolatedElement, + AnyNodeRef::InterpolatedStringLiteralElement(_) => { + NodeKind::InterpolatedStringLiteralElement + } AnyNodeRef::PatternMatchValue(_) => NodeKind::PatternMatchValue, AnyNodeRef::PatternMatchSingleton(_) => NodeKind::PatternMatchSingleton, AnyNodeRef::PatternMatchSequence(_) => NodeKind::PatternMatchSequence, @@ -6562,7 +6673,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::TypeParamTypeVar(_) => NodeKind::TypeParamTypeVar, AnyNodeRef::TypeParamTypeVarTuple(_) => NodeKind::TypeParamTypeVarTuple, AnyNodeRef::TypeParamParamSpec(_) => NodeKind::TypeParamParamSpec, - AnyNodeRef::FStringFormatSpec(_) => NodeKind::FStringFormatSpec, + AnyNodeRef::InterpolatedStringFormatSpec(_) => NodeKind::InterpolatedStringFormatSpec, AnyNodeRef::PatternArguments(_) => NodeKind::PatternArguments, AnyNodeRef::PatternKeyword(_) => NodeKind::PatternKeyword, AnyNodeRef::Comprehension(_) => NodeKind::Comprehension, @@ -6578,6 +6689,7 @@ impl AnyNodeRef<'_> { AnyNodeRef::ElifElseClause(_) => NodeKind::ElifElseClause, AnyNodeRef::TypeParams(_) => NodeKind::TypeParams, AnyNodeRef::FString(_) => NodeKind::FString, + AnyNodeRef::TString(_) => NodeKind::TString, AnyNodeRef::StringLiteral(_) => NodeKind::StringLiteral, AnyNodeRef::BytesLiteral(_) => NodeKind::BytesLiteral, AnyNodeRef::Identifier(_) => NodeKind::Identifier, @@ -7023,6 +7135,20 @@ pub struct ExprFString { pub value: crate::FStringValue, } +/// An AST node that represents either a single-part t-string literal +/// or an implicitly concatenated t-string literal. +/// +/// This type differs from the original Python AST `TemplateStr` in that it +/// doesn't join the implicitly concatenated parts into a single string. Instead, +/// it keeps them separate and provide various methods to access the parts. +/// +/// See also [TemplateStr](https://docs.python.org/3/library/ast.html#ast.TemplateStr) +#[derive(Clone, Debug, PartialEq)] +pub struct ExprTString { + pub range: ruff_text_size::TextRange, + pub value: crate::TStringValue, +} + /// An AST node that represents either a single-part string literal /// or an implicitly concatenated string literal. #[derive(Clone, Debug, PartialEq)] diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 0464c636f4..ba0b08bfe6 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -12,8 +12,8 @@ use crate::parenthesize::parenthesized_range; use crate::statement_visitor::StatementVisitor; use crate::visitor::Visitor; use crate::{ - self as ast, Arguments, CmpOp, DictItem, ExceptHandler, Expr, FStringElement, MatchCase, - Operator, Pattern, Stmt, TypeParam, + self as ast, Arguments, CmpOp, DictItem, ExceptHandler, Expr, InterpolatedStringElement, + MatchCase, Operator, Pattern, Stmt, TypeParam, }; use crate::{AnyNodeRef, ExprContext}; @@ -138,7 +138,10 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool { } Expr::FString(ast::ExprFString { value, .. }) => value .elements() - .any(|expr| any_over_f_string_element(expr, func)), + .any(|expr| any_over_interpolated_string_element(expr, func)), + Expr::TString(ast::ExprTString { value, .. }) => value + .elements() + .any(|expr| any_over_interpolated_string_element(expr, func)), Expr::Named(ast::ExprNamed { target, value, @@ -315,22 +318,22 @@ pub fn any_over_pattern(pattern: &Pattern, func: &dyn Fn(&Expr) -> bool) -> bool } } -pub fn any_over_f_string_element( - element: &ast::FStringElement, +pub fn any_over_interpolated_string_element( + element: &ast::InterpolatedStringElement, func: &dyn Fn(&Expr) -> bool, ) -> bool { match element { - ast::FStringElement::Literal(_) => false, - ast::FStringElement::Expression(ast::FStringExpressionElement { + ast::InterpolatedStringElement::Literal(_) => false, + ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { expression, format_spec, .. }) => { any_over_expr(expression, func) || format_spec.as_ref().is_some_and(|spec| { - spec.elements - .iter() - .any(|spec_element| any_over_f_string_element(spec_element, func)) + spec.elements.iter().any(|spec_element| { + any_over_interpolated_string_element(spec_element, func) + }) }) } } @@ -1304,6 +1307,8 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool { // These literals may or may not be empty. Expr::FString(f_string) => is_non_empty_f_string(f_string), + // These literals may or may not be empty. + Expr::TString(f_string) => is_non_empty_t_string(f_string), Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(), Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(), } @@ -1313,8 +1318,78 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool { ast::FStringPart::Literal(string_literal) => !string_literal.is_empty(), ast::FStringPart::FString(f_string) => { f_string.elements.iter().all(|element| match element { - FStringElement::Literal(string_literal) => !string_literal.is_empty(), - FStringElement::Expression(f_string) => inner(&f_string.expression), + InterpolatedStringElement::Literal(string_literal) => !string_literal.is_empty(), + InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression), + }) + } + }) +} + +/// Returns `true` if the expression definitely resolves to a non-empty string, when used as an +/// f-string expression, or `false` if the expression may resolve to an empty string. +fn is_non_empty_t_string(expr: &ast::ExprTString) -> bool { + fn inner(expr: &Expr) -> bool { + match expr { + // When stringified, these expressions are always non-empty. + Expr::Lambda(_) => true, + Expr::Dict(_) => true, + Expr::Set(_) => true, + Expr::ListComp(_) => true, + Expr::SetComp(_) => true, + Expr::DictComp(_) => true, + Expr::Compare(_) => true, + Expr::NumberLiteral(_) => true, + Expr::BooleanLiteral(_) => true, + Expr::NoneLiteral(_) => true, + Expr::EllipsisLiteral(_) => true, + Expr::List(_) => true, + Expr::Tuple(_) => true, + + // These expressions must resolve to the inner expression. + Expr::If(ast::ExprIf { body, orelse, .. }) => inner(body) && inner(orelse), + Expr::Named(ast::ExprNamed { value, .. }) => inner(value), + + // These expressions are complex. We can't determine whether they're empty or not. + Expr::BoolOp(ast::ExprBoolOp { .. }) => false, + Expr::BinOp(ast::ExprBinOp { .. }) => false, + Expr::UnaryOp(ast::ExprUnaryOp { .. }) => false, + Expr::Generator(_) => false, + Expr::Await(_) => false, + Expr::Yield(_) => false, + Expr::YieldFrom(_) => false, + Expr::Call(_) => false, + Expr::Attribute(_) => false, + Expr::Subscript(_) => false, + Expr::Starred(_) => false, + Expr::Name(_) => false, + Expr::Slice(_) => false, + Expr::IpyEscapeCommand(_) => false, + + // These literals may or may not be empty. + Expr::FString(f_string) => is_non_empty_f_string(f_string), + // These literals may or may not be empty. + Expr::TString(t_string) => is_non_empty_t_string(t_string), + Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(), + Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(), + } + } + + expr.value.iter().any(|part| match part { + ast::TStringPart::Literal(string_literal) => !string_literal.is_empty(), + ast::TStringPart::TString(t_string) => { + t_string.elements.iter().all(|element| match element { + ast::InterpolatedStringElement::Literal(string_literal) => { + !string_literal.is_empty() + } + ast::InterpolatedStringElement::Interpolation(t_string) => { + inner(&t_string.expression) + } + }) + } + ast::TStringPart::FString(f_string) => { + f_string.elements.iter().all(|element| match element { + InterpolatedStringElement::Literal(string_literal) => !string_literal.is_empty(), + InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression), }) } }) @@ -1331,10 +1406,10 @@ fn is_empty_f_string(expr: &ast::ExprFString) -> bool { value .elements() .all(|f_string_element| match f_string_element { - FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => { - value.is_empty() - } - FStringElement::Expression(ast::FStringExpressionElement { + InterpolatedStringElement::Literal( + ast::InterpolatedStringLiteralElement { value, .. }, + ) => value.is_empty(), + InterpolatedStringElement::Interpolation(ast::InterpolatedElement { expression, .. }) => inner(expression), @@ -1348,8 +1423,8 @@ fn is_empty_f_string(expr: &ast::ExprFString) -> bool { ast::FStringPart::Literal(string_literal) => string_literal.is_empty(), ast::FStringPart::FString(f_string) => { f_string.elements.iter().all(|element| match element { - FStringElement::Literal(string_literal) => string_literal.is_empty(), - FStringElement::Expression(f_string) => inner(&f_string.expression), + InterpolatedStringElement::Literal(string_literal) => string_literal.is_empty(), + InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression), }) } }) diff --git a/crates/ruff_python_ast/src/node.rs b/crates/ruff_python_ast/src/node.rs index 8e7a10bc52..52912c6d66 100644 --- a/crates/ruff_python_ast/src/node.rs +++ b/crates/ruff_python_ast/src/node.rs @@ -85,23 +85,23 @@ impl ast::ExprCompare { } } -impl ast::FStringFormatSpec { +impl ast::InterpolatedStringFormatSpec { pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) where V: SourceOrderVisitor<'a> + ?Sized, { for element in &self.elements { - visitor.visit_f_string_element(element); + visitor.visit_interpolated_string_element(element); } } } -impl ast::FStringExpressionElement { +impl ast::InterpolatedElement { pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::FStringExpressionElement { + let ast::InterpolatedElement { expression, format_spec, .. @@ -110,18 +110,18 @@ impl ast::FStringExpressionElement { if let Some(format_spec) = format_spec { for spec_part in &format_spec.elements { - visitor.visit_f_string_element(spec_part); + visitor.visit_interpolated_string_element(spec_part); } } } } -impl ast::FStringLiteralElement { +impl ast::InterpolatedStringLiteralElement { pub(crate) fn visit_source_order<'a, V>(&'a self, _visitor: &mut V) where V: SourceOrderVisitor<'a> + ?Sized, { - let ast::FStringLiteralElement { range: _, value: _ } = self; + let ast::InterpolatedStringLiteralElement { range: _, value: _ } = self; } } @@ -145,6 +145,29 @@ impl ast::ExprFString { } } +impl ast::ExprTString { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let ast::ExprTString { value, range: _ } = self; + + for t_string_part in value { + match t_string_part { + ast::TStringPart::Literal(string_literal) => { + visitor.visit_string_literal(string_literal); + } + ast::TStringPart::FString(f_string) => { + visitor.visit_f_string(f_string); + } + ast::TStringPart::TString(t_string) => { + visitor.visit_t_string(t_string); + } + } + } + } +} + impl ast::ExprStringLiteral { pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) where @@ -615,7 +638,24 @@ impl ast::FString { } = self; for fstring_element in elements { - visitor.visit_f_string_element(fstring_element); + visitor.visit_interpolated_string_element(fstring_element); + } + } +} + +impl ast::TString { + pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V) + where + V: SourceOrderVisitor<'a> + ?Sized, + { + let ast::TString { + elements, + range: _, + flags: _, + } = self; + + for tstring_element in elements { + visitor.visit_interpolated_string_element(tstring_element); } } } diff --git a/crates/ruff_python_ast/src/nodes.rs b/crates/ruff_python_ast/src/nodes.rs index 40e3dd8f00..54235b2bbe 100644 --- a/crates/ruff_python_ast/src/nodes.rs +++ b/crates/ruff_python_ast/src/nodes.rs @@ -2,7 +2,7 @@ use crate::generated::{ ExprBytesLiteral, ExprDict, ExprFString, ExprList, ExprName, ExprSet, ExprStringLiteral, - ExprTuple, StmtClassDef, + ExprTString, ExprTuple, StmtClassDef, }; use std::borrow::Cow; use std::fmt; @@ -17,10 +17,12 @@ use itertools::Itertools; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; -use crate::str_prefix::{AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix}; +use crate::str_prefix::{ + AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, TStringPrefix, +}; use crate::{ - Expr, ExprRef, FStringElement, LiteralExpressionRef, OperatorPrecedence, Pattern, Stmt, - TypeParam, int, + Expr, ExprRef, InterpolatedStringElement, LiteralExpressionRef, OperatorPrecedence, Pattern, + Stmt, TypeParam, int, name::Name, str::{Quote, TripleQuotes}, }; @@ -312,35 +314,35 @@ impl<'a> IntoIterator for &'a ExprSet { } #[derive(Clone, Debug, PartialEq)] -pub struct FStringFormatSpec { +pub struct InterpolatedStringFormatSpec { pub range: TextRange, - pub elements: FStringElements, + pub elements: InterpolatedStringElements, } /// See also [FormattedValue](https://docs.python.org/3/library/ast.html#ast.FormattedValue) #[derive(Clone, Debug, PartialEq)] -pub struct FStringExpressionElement { +pub struct InterpolatedElement { pub range: TextRange, pub expression: Box, pub debug_text: Option, pub conversion: ConversionFlag, - pub format_spec: Option>, + pub format_spec: Option>, } /// An `FStringLiteralElement` with an empty `value` is an invalid f-string element. #[derive(Clone, Debug, PartialEq)] -pub struct FStringLiteralElement { +pub struct InterpolatedStringLiteralElement { pub range: TextRange, pub value: Box, } -impl FStringLiteralElement { +impl InterpolatedStringLiteralElement { pub fn is_valid(&self) -> bool { !self.value.is_empty() } } -impl Deref for FStringLiteralElement { +impl Deref for InterpolatedStringLiteralElement { type Target = str; fn deref(&self) -> &Self::Target { @@ -483,7 +485,7 @@ impl FStringValue { self.iter().filter_map(|part| part.as_f_string()) } - /// Returns an iterator over all the [`FStringElement`] contained in this value. + /// Returns an iterator over all the [`InterpolatedStringElement`] contained in this value. /// /// An f-string element is what makes up an [`FString`] i.e., it is either a /// string literal or an expression. In the following example, @@ -494,7 +496,7 @@ impl FStringValue { /// /// The f-string elements returned would be string literal (`"bar "`), /// expression (`x`) and string literal (`"qux"`). - pub fn elements(&self) -> impl Iterator { + pub fn elements(&self) -> impl Iterator { self.f_strings().flat_map(|fstring| fstring.elements.iter()) } } @@ -554,6 +556,181 @@ impl Ranged for FStringPart { } } +impl ExprTString { + /// Returns the single [`TString`] if the t-string isn't implicitly concatenated, [`None`] + /// otherwise. + pub const fn as_single_part_tstring(&self) -> Option<&TString> { + match &self.value.inner { + TStringValueInner::Single(TStringPart::TString(tstring)) => Some(tstring), + _ => None, + } + } +} + +/// The value representing an [`ExprTString`]. +#[derive(Clone, Debug, PartialEq)] +pub struct TStringValue { + inner: TStringValueInner, +} + +impl TStringValue { + /// Creates a new t-string literal with a single [`TString`] part. + pub fn single(value: TString) -> Self { + Self { + inner: TStringValueInner::Single(TStringPart::TString(value)), + } + } + + /// Creates a new t-string with the given values that represents an implicitly + /// concatenated t-string. + /// + /// # Panics + /// + /// Panics if `values` has less than 2 elements. + /// Use [`TStringValue::single`] instead. + pub fn concatenated(values: Vec) -> Self { + assert!( + values.len() > 1, + "Use `TStringValue::single` to create single-part t-strings" + ); + Self { + inner: TStringValueInner::Concatenated(values), + } + } + + /// Returns `true` if the t-string is implicitly concatenated, `false` otherwise. + pub fn is_implicit_concatenated(&self) -> bool { + matches!(self.inner, TStringValueInner::Concatenated(_)) + } + + /// Returns a slice of all the [`TStringPart`]s contained in this value. + pub fn as_slice(&self) -> &[TStringPart] { + match &self.inner { + TStringValueInner::Single(part) => std::slice::from_ref(part), + TStringValueInner::Concatenated(parts) => parts, + } + } + + /// Returns a mutable slice of all the [`TStringPart`]s contained in this value. + fn as_mut_slice(&mut self) -> &mut [TStringPart] { + match &mut self.inner { + TStringValueInner::Single(part) => std::slice::from_mut(part), + TStringValueInner::Concatenated(parts) => parts, + } + } + + /// Returns an iterator over all the [`TStringPart`]s contained in this value. + pub fn iter(&self) -> Iter { + self.as_slice().iter() + } + + /// Returns an iterator over all the [`TStringPart`]s contained in this value + /// that allows modification. + pub fn iter_mut(&mut self) -> IterMut { + self.as_mut_slice().iter_mut() + } + + /// Returns an iterator over the [`StringLiteral`] parts contained in this value. + /// + /// Note that this doesn't recurse into the t-string parts. For example, + /// + /// ```python + /// "foo" t"bar {x}" "baz" t"qux" + /// ``` + /// + /// Here, the string literal parts returned would be `"foo"` and `"baz"`. + pub fn literals(&self) -> impl Iterator { + self.iter().filter_map(|part| part.as_literal()) + } + + /// Returns an iterator over the [`TString`] parts contained in this value. + /// + /// Note that this doesn't recurse into the t-string parts. For example, + /// + /// ```python + /// "foo" t"bar {x}" "baz" t"qux" + /// ``` + /// + /// Here, the t-string parts returned would be `f"bar {x}"` and `f"qux"`. + pub fn t_strings(&self) -> impl Iterator { + self.iter().filter_map(|part| part.as_t_string()) + } + + /// Returns an iterator over all the [`InterpolatedStringElement`] contained in this value. + /// + /// An t-string element is what makes up an [`TString`] i.e., it is either a + /// string literal or an interpolation. In the following example, + /// + /// ```python + /// "foo" t"bar {x}" "baz" t"qux" + /// ``` + /// + /// The t-string elements returned would be string literal (`"bar "`), + /// interpolation (`x`) and string literal (`"qux"`). + pub fn elements(&self) -> impl Iterator { + self.t_strings().flat_map(|fstring| fstring.elements.iter()) + } +} + +impl<'a> IntoIterator for &'a TStringValue { + type Item = &'a TStringPart; + type IntoIter = Iter<'a, TStringPart>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a> IntoIterator for &'a mut TStringValue { + type Item = &'a mut TStringPart; + type IntoIter = IterMut<'a, TStringPart>; + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +/// An internal representation of [`TStringValue`]. +#[derive(Clone, Debug, PartialEq)] +enum TStringValueInner { + /// A single t-string i.e., `t"foo"`. + /// + /// This is always going to be `TStringPart::TString` variant which is + /// maintained by the `TStringValue::single` constructor. + Single(TStringPart), + + /// An implicitly concatenated t-string i.e., `"foo" t"bar {x}"`. + Concatenated(Vec), +} + +/// An t-string part which is either a string literal, an f-string, +/// or a t-string. +#[derive(Clone, Debug, PartialEq, is_macro::Is)] +pub enum TStringPart { + Literal(StringLiteral), + FString(FString), + TString(TString), +} + +impl TStringPart { + pub fn quote_style(&self) -> Quote { + match self { + Self::Literal(string_literal) => string_literal.flags.quote_style(), + Self::FString(f_string) => f_string.flags.quote_style(), + Self::TString(t_string) => t_string.flags.quote_style(), + } + } +} + +impl Ranged for TStringPart { + fn range(&self) -> TextRange { + match self { + TStringPart::Literal(string_literal) => string_literal.range(), + TStringPart::FString(f_string) => f_string.range(), + TStringPart::TString(t_string) => t_string.range(), + } + } +} + pub trait StringFlags: Copy { /// Does the string use single or double quotes in its opener and closer? fn quote_style(self) -> Quote; @@ -635,7 +812,7 @@ impl std::fmt::Display for DisplayFlags<'_> { bitflags! { #[derive(Default, Copy, Clone, PartialEq, Eq, Hash)] - struct FStringFlagsInner: u8 { + struct InterpolatedStringFlagsInner: u8 { /// The f-string uses double quotes (`"`) for its opener and closer. /// If this flag is not set, the f-string uses single quotes (`'`) /// for its opener and closer. @@ -662,6 +839,11 @@ bitflags! { /// Flags that can be queried to obtain information /// regarding the prefixes and quotes used for an f-string. /// +/// Note: This is identical to [`TStringFlags`] except that +/// the implementation of the `prefix` method of the +/// [`StringFlags`] trait returns a variant of +/// `AnyStringPrefix::Format`. +/// /// ## Notes on usage /// /// If you're using a `Generator` from the `ruff_python_codegen` crate to generate a lint-rule fix @@ -671,7 +853,7 @@ bitflags! { /// will properly handle nested f-strings. For usage that doesn't fit into one of these categories, /// the public constructor [`FStringFlags::empty`] can be used. #[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct FStringFlags(FStringFlagsInner); +pub struct FStringFlags(InterpolatedStringFlagsInner); impl FStringFlags { /// Construct a new [`FStringFlags`] with **no flags set**. @@ -684,42 +866,60 @@ impl FStringFlags { /// situations in which alternative ways to construct this struct should be used, especially /// when writing lint rules. pub fn empty() -> Self { - Self(FStringFlagsInner::empty()) + Self(InterpolatedStringFlagsInner::empty()) } #[must_use] pub fn with_quote_style(mut self, quote_style: Quote) -> Self { - self.0 - .set(FStringFlagsInner::DOUBLE, quote_style.is_double()); + self.0.set( + InterpolatedStringFlagsInner::DOUBLE, + quote_style.is_double(), + ); self } #[must_use] pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self { - self.0 - .set(FStringFlagsInner::TRIPLE_QUOTED, triple_quotes.is_yes()); + self.0.set( + InterpolatedStringFlagsInner::TRIPLE_QUOTED, + triple_quotes.is_yes(), + ); self } #[must_use] pub fn with_prefix(mut self, prefix: FStringPrefix) -> Self { match prefix { - FStringPrefix::Regular => { - Self(self.0 - FStringFlagsInner::R_PREFIX_LOWER - FStringFlagsInner::R_PREFIX_UPPER) - } + FStringPrefix::Regular => Self( + self.0 + - InterpolatedStringFlagsInner::R_PREFIX_LOWER + - InterpolatedStringFlagsInner::R_PREFIX_UPPER, + ), FStringPrefix::Raw { uppercase_r } => { - self.0.set(FStringFlagsInner::R_PREFIX_UPPER, uppercase_r); - self.0.set(FStringFlagsInner::R_PREFIX_LOWER, !uppercase_r); + self.0 + .set(InterpolatedStringFlagsInner::R_PREFIX_UPPER, uppercase_r); + self.0 + .set(InterpolatedStringFlagsInner::R_PREFIX_LOWER, !uppercase_r); self } } } pub const fn prefix(self) -> FStringPrefix { - if self.0.contains(FStringFlagsInner::R_PREFIX_LOWER) { - debug_assert!(!self.0.contains(FStringFlagsInner::R_PREFIX_UPPER)); + if self + .0 + .contains(InterpolatedStringFlagsInner::R_PREFIX_LOWER) + { + debug_assert!( + !self + .0 + .contains(InterpolatedStringFlagsInner::R_PREFIX_UPPER) + ); FStringPrefix::Raw { uppercase_r: false } - } else if self.0.contains(FStringFlagsInner::R_PREFIX_UPPER) { + } else if self + .0 + .contains(InterpolatedStringFlagsInner::R_PREFIX_UPPER) + { FStringPrefix::Raw { uppercase_r: true } } else { FStringPrefix::Regular @@ -727,12 +927,108 @@ impl FStringFlags { } } +// TODO(dylan): the documentation about using +// `Checker::default_tstring_flags` is not yet +// correct. This method does not yet exist because +// introducing it would emit a dead code warning +// until we call it in lint rules. +/// Flags that can be queried to obtain information +/// regarding the prefixes and quotes used for an f-string. +/// +/// Note: This is identical to [`FStringFlags`] except that +/// the implementation of the `prefix` method of the +/// [`StringFlags`] trait returns a variant of +/// `AnyStringPrefix::Template`. +/// +/// ## Notes on usage +/// +/// If you're using a `Generator` from the `ruff_python_codegen` crate to generate a lint-rule fix +/// from an existing t-string literal, consider passing along the [`FString::flags`] field. If you +/// don't have an existing literal but have a `Checker` from the `ruff_linter` crate available, +/// consider using `Checker::default_tstring_flags` to create instances of this struct; this method +/// will properly handle nested t-strings. For usage that doesn't fit into one of these categories, +/// the public constructor [`TStringFlags::empty`] can be used. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct TStringFlags(InterpolatedStringFlagsInner); + +impl TStringFlags { + /// Construct a new [`TStringFlags`] with **no flags set**. + /// + /// See [`TStringFlags::with_quote_style`], [`TStringFlags::with_triple_quotes`], and + /// [`TStringFlags::with_prefix`] for ways of setting the quote style (single or double), + /// enabling triple quotes, and adding prefixes (such as `r`), respectively. + /// + /// See the documentation for [`TStringFlags`] for additional caveats on this constructor, and + /// situations in which alternative ways to construct this struct should be used, especially + /// when writing lint rules. + pub fn empty() -> Self { + Self(InterpolatedStringFlagsInner::empty()) + } + + #[must_use] + pub fn with_quote_style(mut self, quote_style: Quote) -> Self { + self.0.set( + InterpolatedStringFlagsInner::DOUBLE, + quote_style.is_double(), + ); + self + } + + #[must_use] + pub fn with_triple_quotes(mut self, triple_quotes: TripleQuotes) -> Self { + self.0.set( + InterpolatedStringFlagsInner::TRIPLE_QUOTED, + triple_quotes.is_yes(), + ); + self + } + + #[must_use] + pub fn with_prefix(mut self, prefix: TStringPrefix) -> Self { + match prefix { + TStringPrefix::Regular => Self( + self.0 + - InterpolatedStringFlagsInner::R_PREFIX_LOWER + - InterpolatedStringFlagsInner::R_PREFIX_UPPER, + ), + TStringPrefix::Raw { uppercase_r } => { + self.0 + .set(InterpolatedStringFlagsInner::R_PREFIX_UPPER, uppercase_r); + self.0 + .set(InterpolatedStringFlagsInner::R_PREFIX_LOWER, !uppercase_r); + self + } + } + } + + pub const fn prefix(self) -> TStringPrefix { + if self + .0 + .contains(InterpolatedStringFlagsInner::R_PREFIX_LOWER) + { + debug_assert!( + !self + .0 + .contains(InterpolatedStringFlagsInner::R_PREFIX_UPPER) + ); + TStringPrefix::Raw { uppercase_r: false } + } else if self + .0 + .contains(InterpolatedStringFlagsInner::R_PREFIX_UPPER) + { + TStringPrefix::Raw { uppercase_r: true } + } else { + TStringPrefix::Regular + } + } +} + impl StringFlags for FStringFlags { /// Return `true` if the f-string is triple-quoted, i.e., /// it begins and ends with three consecutive quote characters. /// For example: `f"""{bar}"""` fn triple_quotes(self) -> TripleQuotes { - if self.0.contains(FStringFlagsInner::TRIPLE_QUOTED) { + if self.0.contains(InterpolatedStringFlagsInner::TRIPLE_QUOTED) { TripleQuotes::Yes } else { TripleQuotes::No @@ -744,7 +1040,7 @@ impl StringFlags for FStringFlags { /// - `f"{"a"}"` -> `QuoteStyle::Double` /// - `f'{"a"}'` -> `QuoteStyle::Single` fn quote_style(self) -> Quote { - if self.0.contains(FStringFlagsInner::DOUBLE) { + if self.0.contains(InterpolatedStringFlagsInner::DOUBLE) { Quote::Double } else { Quote::Single @@ -766,11 +1062,50 @@ impl fmt::Debug for FStringFlags { } } +impl StringFlags for TStringFlags { + /// Return `true` if the t-string is triple-quoted, i.e., + /// it begins and ends with three consecutive quote characters. + /// For example: `t"""{bar}"""` + fn triple_quotes(self) -> TripleQuotes { + if self.0.contains(InterpolatedStringFlagsInner::TRIPLE_QUOTED) { + TripleQuotes::Yes + } else { + TripleQuotes::No + } + } + + /// Return the quoting style (single or double quotes) + /// used by the t-string's opener and closer: + /// - `t"{"a"}"` -> `QuoteStyle::Double` + /// - `t'{"a"}'` -> `QuoteStyle::Single` + fn quote_style(self) -> Quote { + if self.0.contains(InterpolatedStringFlagsInner::DOUBLE) { + Quote::Double + } else { + Quote::Single + } + } + + fn prefix(self) -> AnyStringPrefix { + AnyStringPrefix::Template(self.prefix()) + } +} + +impl fmt::Debug for TStringFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("TStringFlags") + .field("quote_style", &self.quote_style()) + .field("prefix", &self.prefix()) + .field("triple_quoted", &self.is_triple_quoted()) + .finish() + } +} + /// An AST node that represents a single f-string which is part of an [`ExprFString`]. #[derive(Clone, Debug, PartialEq)] pub struct FString { pub range: TextRange, - pub elements: FStringElements, + pub elements: InterpolatedStringElements, pub flags: FStringFlags, } @@ -784,66 +1119,84 @@ impl From for Expr { } } -/// A newtype wrapper around a list of [`FStringElement`]. +/// A newtype wrapper around a list of [`InterpolatedStringElement`]. #[derive(Clone, Default, PartialEq)] -pub struct FStringElements(Vec); +pub struct InterpolatedStringElements(Vec); -impl FStringElements { - /// Returns an iterator over all the [`FStringLiteralElement`] nodes contained in this f-string. - pub fn literals(&self) -> impl Iterator { +impl InterpolatedStringElements { + /// Returns an iterator over all the [`InterpolatedStringLiteralElement`] nodes contained in this f-string. + pub fn literals(&self) -> impl Iterator { self.iter().filter_map(|element| element.as_literal()) } - /// Returns an iterator over all the [`FStringExpressionElement`] nodes contained in this f-string. - pub fn expressions(&self) -> impl Iterator { - self.iter().filter_map(|element| element.as_expression()) + /// Returns an iterator over all the [`InterpolatedElement`] nodes contained in this f-string. + pub fn interpolations(&self) -> impl Iterator { + self.iter().filter_map(|element| element.as_interpolation()) } } -impl From> for FStringElements { - fn from(elements: Vec) -> Self { - FStringElements(elements) +impl From> for InterpolatedStringElements { + fn from(elements: Vec) -> Self { + InterpolatedStringElements(elements) } } -impl<'a> IntoIterator for &'a FStringElements { - type IntoIter = Iter<'a, FStringElement>; - type Item = &'a FStringElement; +impl<'a> IntoIterator for &'a InterpolatedStringElements { + type IntoIter = Iter<'a, InterpolatedStringElement>; + type Item = &'a InterpolatedStringElement; fn into_iter(self) -> Self::IntoIter { self.iter() } } -impl<'a> IntoIterator for &'a mut FStringElements { - type IntoIter = IterMut<'a, FStringElement>; - type Item = &'a mut FStringElement; +impl<'a> IntoIterator for &'a mut InterpolatedStringElements { + type IntoIter = IterMut<'a, InterpolatedStringElement>; + type Item = &'a mut InterpolatedStringElement; fn into_iter(self) -> Self::IntoIter { self.iter_mut() } } -impl Deref for FStringElements { - type Target = [FStringElement]; +impl Deref for InterpolatedStringElements { + type Target = [InterpolatedStringElement]; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for FStringElements { +impl DerefMut for InterpolatedStringElements { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -impl fmt::Debug for FStringElements { +impl fmt::Debug for InterpolatedStringElements { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) } } +/// An AST node that represents a single t-string which is part of an [`ExprTString`]. +#[derive(Clone, Debug, PartialEq)] +pub struct TString { + pub range: TextRange, + pub elements: InterpolatedStringElements, + pub flags: TStringFlags, +} + +impl From for Expr { + fn from(payload: TString) -> Self { + ExprTString { + range: payload.range, + value: TStringValue::single(payload), + } + .into() + } +} + impl ExprStringLiteral { /// Return `Some(literal)` if the string only consists of a single `StringLiteral` part /// (indicating that it is not implicitly concatenated). Otherwise, return `None`. @@ -1662,18 +2015,23 @@ bitflags! { /// but can have no other prefixes. const F_PREFIX = 1 << 4; + /// The string has a `t` or `T` prefix, meaning it is a t-string. + /// T-strings can also be raw strings, + /// but can have no other prefixes. + const T_PREFIX = 1 << 5; + /// The string has an `r` prefix, meaning it is a raw string. /// F-strings and byte-strings can be raw, /// as can strings with no other prefixes. /// U-strings cannot be raw. - const R_PREFIX_LOWER = 1 << 5; + const R_PREFIX_LOWER = 1 << 6; /// The string has an `R` prefix, meaning it is a raw string. /// The casing of the `r`/`R` has no semantic significance at runtime; /// see https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#r-strings-and-r-strings /// for why we track the casing of the `r` prefix, /// but not for any other prefix - const R_PREFIX_UPPER = 1 << 6; + const R_PREFIX_UPPER = 1 << 7; } } @@ -1711,6 +2069,15 @@ impl AnyStringFlags { AnyStringPrefix::Format(FStringPrefix::Raw { uppercase_r: true }) => { AnyStringFlagsInner::F_PREFIX.union(AnyStringFlagsInner::R_PREFIX_UPPER) } + + // t-strings + AnyStringPrefix::Template(TStringPrefix::Regular) => AnyStringFlagsInner::T_PREFIX, + AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: false }) => { + AnyStringFlagsInner::T_PREFIX.union(AnyStringFlagsInner::R_PREFIX_LOWER) + } + AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: true }) => { + AnyStringFlagsInner::T_PREFIX.union(AnyStringFlagsInner::R_PREFIX_UPPER) + } }; self } @@ -1734,9 +2101,10 @@ impl AnyStringFlags { ) } - /// Does the string have an `f` or `F` prefix? - pub const fn is_f_string(self) -> bool { - self.0.contains(AnyStringFlagsInner::F_PREFIX) + /// Does the string have an `f`,`F`,`t`, or `T` prefix? + pub const fn is_interpolated_string(self) -> bool { + self.0 + .intersects(AnyStringFlagsInner::F_PREFIX.union(AnyStringFlagsInner::T_PREFIX)) } /// Does the string have a `b` or `B` prefix? @@ -1793,6 +2161,17 @@ impl StringFlags for AnyStringFlags { return AnyStringPrefix::Format(FStringPrefix::Regular); } + // t-strings + if flags.contains(AnyStringFlagsInner::T_PREFIX) { + if flags.contains(AnyStringFlagsInner::R_PREFIX_LOWER) { + return AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: false }); + } + if flags.contains(AnyStringFlagsInner::R_PREFIX_UPPER) { + return AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: true }); + } + return AnyStringPrefix::Template(TStringPrefix::Regular); + } + // bytestrings if flags.contains(AnyStringFlagsInner::B_PREFIX) { if flags.contains(AnyStringFlagsInner::R_PREFIX_LOWER) { @@ -1872,7 +2251,7 @@ impl From for AnyStringFlags { impl From for FStringFlags { fn from(value: AnyStringFlags) -> FStringFlags { - let AnyStringPrefix::Format(fstring_prefix) = value.prefix() else { + let AnyStringPrefix::Format(prefix) = value.prefix() else { unreachable!( "Should never attempt to convert {} into an f-string", value.prefix() @@ -1880,7 +2259,7 @@ impl From for FStringFlags { }; FStringFlags::empty() .with_quote_style(value.quote_style()) - .with_prefix(fstring_prefix) + .with_prefix(prefix) .with_triple_quotes(value.triple_quotes()) } } @@ -1891,6 +2270,27 @@ impl From for AnyStringFlags { } } +impl From for TStringFlags { + fn from(value: AnyStringFlags) -> TStringFlags { + let AnyStringPrefix::Template(prefix) = value.prefix() else { + unreachable!( + "Should never attempt to convert {} into a t-string", + value.prefix() + ) + }; + TStringFlags::empty() + .with_quote_style(value.quote_style()) + .with_prefix(prefix) + .with_triple_quotes(value.triple_quotes()) + } +} + +impl From for AnyStringFlags { + fn from(value: TStringFlags) -> Self { + value.as_any_string_flags() + } +} + #[derive(Clone, Debug, PartialEq, is_macro::Is)] pub enum Number { Int(int::Int), diff --git a/crates/ruff_python_ast/src/operator_precedence.rs b/crates/ruff_python_ast/src/operator_precedence.rs index 750ef7f719..6b652847e9 100644 --- a/crates/ruff_python_ast/src/operator_precedence.rs +++ b/crates/ruff_python_ast/src/operator_precedence.rs @@ -72,7 +72,8 @@ impl OperatorPrecedence { | ExprRef::BooleanLiteral(_) | ExprRef::NoneLiteral(_) | ExprRef::EllipsisLiteral(_) - | ExprRef::FString(_) => Self::Atomic, + | ExprRef::FString(_) + | ExprRef::TString(_) => Self::Atomic, // Subscription, slicing, call, attribute reference ExprRef::Attribute(_) | ExprRef::Subscript(_) diff --git a/crates/ruff_python_ast/src/python_version.rs b/crates/ruff_python_ast/src/python_version.rs index 0658bc8dee..bc5a2689de 100644 --- a/crates/ruff_python_ast/src/python_version.rs +++ b/crates/ruff_python_ast/src/python_version.rs @@ -59,6 +59,13 @@ impl PythonVersion { Self::PY313 } + /// The latest Python version supported in preview + pub fn latest_preview() -> Self { + let latest_preview = Self::PY314; + debug_assert!(latest_preview >= Self::latest()); + latest_preview + } + pub const fn latest_ty() -> Self { // Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version. Self::PY313 diff --git a/crates/ruff_python_ast/src/relocate.rs b/crates/ruff_python_ast/src/relocate.rs index 7410b9fc5e..c985003069 100644 --- a/crates/ruff_python_ast/src/relocate.rs +++ b/crates/ruff_python_ast/src/relocate.rs @@ -72,6 +72,9 @@ impl Transformer for Relocator { Expr::FString(ast::ExprFString { range, .. }) => { *range = self.range; } + Expr::TString(ast::ExprTString { range, .. }) => { + *range = self.range; + } Expr::StringLiteral(ast::ExprStringLiteral { range, .. }) => { *range = self.range; } diff --git a/crates/ruff_python_ast/src/str.rs b/crates/ruff_python_ast/src/str.rs index 5a8dd1093e..a9096a5218 100644 --- a/crates/ruff_python_ast/src/str.rs +++ b/crates/ruff_python_ast/src/str.rs @@ -5,7 +5,7 @@ use std::sync::LazyLock; use ruff_text_size::{TextLen, TextRange}; /// Enumeration of the two kinds of quotes that can be used -/// for Python string/f-string/bytestring literals +/// for Python string/f/t-string/bytestring literals #[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq, is_macro::Is)] pub enum Quote { /// E.g. `'` diff --git a/crates/ruff_python_ast/src/str_prefix.rs b/crates/ruff_python_ast/src/str_prefix.rs index 37f8421711..a00b02fb46 100644 --- a/crates/ruff_python_ast/src/str_prefix.rs +++ b/crates/ruff_python_ast/src/str_prefix.rs @@ -91,6 +91,47 @@ impl fmt::Display for FStringPrefix { } } +/// Enumeration of the valid prefixes a t-string literal can have. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum TStringPrefix { + /// Just a regular t-string with no other prefixes, e.g. t"{bar}" + Regular, + + /// A "raw" template string, that has an `r` or `R` prefix, + /// e.g. `rt"{bar}"` or `Rt"{bar}"` + Raw { uppercase_r: bool }, +} + +impl TStringPrefix { + /// Return a `str` representation of the prefix + pub const fn as_str(self) -> &'static str { + match self { + Self::Regular => "t", + Self::Raw { uppercase_r: true } => "Rt", + Self::Raw { uppercase_r: false } => "rt", + } + } + + pub const fn text_len(self) -> TextSize { + match self { + Self::Regular => TextSize::new(1), + Self::Raw { .. } => TextSize::new(2), + } + } + + /// Return true if this prefix indicates a "raw t-string", + /// e.g. `rt"{bar}"` or `Rt"{bar}"` + pub const fn is_raw(self) -> bool { + matches!(self, Self::Raw { .. }) + } +} + +impl fmt::Display for TStringPrefix { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + /// Enumeration of the valid prefixes a bytestring literal can have. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum ByteStringPrefix { @@ -151,6 +192,9 @@ pub enum AnyStringPrefix { /// Prefixes that indicate the string is an f-string Format(FStringPrefix), + /// Prefixes that indicate the string is a t-string + Template(TStringPrefix), + /// All other prefixes Regular(StringLiteralPrefix), } @@ -161,6 +205,7 @@ impl AnyStringPrefix { Self::Regular(regular_prefix) => regular_prefix.as_str(), Self::Bytes(bytestring_prefix) => bytestring_prefix.as_str(), Self::Format(fstring_prefix) => fstring_prefix.as_str(), + Self::Template(tstring_prefix) => tstring_prefix.as_str(), } } @@ -169,6 +214,7 @@ impl AnyStringPrefix { Self::Regular(regular_prefix) => regular_prefix.text_len(), Self::Bytes(bytestring_prefix) => bytestring_prefix.text_len(), Self::Format(fstring_prefix) => fstring_prefix.text_len(), + Self::Template(tstring_prefix) => tstring_prefix.text_len(), } } @@ -177,6 +223,7 @@ impl AnyStringPrefix { Self::Regular(regular_prefix) => regular_prefix.is_raw(), Self::Bytes(bytestring_prefix) => bytestring_prefix.is_raw(), Self::Format(fstring_prefix) => fstring_prefix.is_raw(), + Self::Template(tstring_prefix) => tstring_prefix.is_raw(), } } } diff --git a/crates/ruff_python_ast/src/visitor.rs b/crates/ruff_python_ast/src/visitor.rs index 8bb2013efc..1e8c5ebf36 100644 --- a/crates/ruff_python_ast/src/visitor.rs +++ b/crates/ruff_python_ast/src/visitor.rs @@ -5,10 +5,10 @@ pub mod transformer; use crate::{ self as ast, Alias, AnyParameterRef, Arguments, BoolOp, BytesLiteral, CmpOp, Comprehension, - Decorator, ElifElseClause, ExceptHandler, Expr, ExprContext, FString, FStringElement, - FStringPart, Keyword, MatchCase, Operator, Parameter, Parameters, Pattern, PatternArguments, - PatternKeyword, Stmt, StringLiteral, TypeParam, TypeParamParamSpec, TypeParamTypeVar, - TypeParamTypeVarTuple, TypeParams, UnaryOp, WithItem, + Decorator, ElifElseClause, ExceptHandler, Expr, ExprContext, FString, FStringPart, + InterpolatedStringElement, Keyword, MatchCase, Operator, Parameter, Parameters, Pattern, + PatternArguments, PatternKeyword, Stmt, StringLiteral, TString, TStringPart, TypeParam, + TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, TypeParams, UnaryOp, WithItem, }; /// A trait for AST visitors. Visits all nodes in the AST recursively in evaluation-order. @@ -99,8 +99,14 @@ pub trait Visitor<'a> { fn visit_f_string(&mut self, f_string: &'a FString) { walk_f_string(self, f_string); } - fn visit_f_string_element(&mut self, f_string_element: &'a FStringElement) { - walk_f_string_element(self, f_string_element); + fn visit_interpolated_string_element( + &mut self, + interpolated_string_element: &'a InterpolatedStringElement, + ) { + walk_interpolated_string_element(self, interpolated_string_element); + } + fn visit_t_string(&mut self, t_string: &'a TString) { + walk_t_string(self, t_string); } fn visit_string_literal(&mut self, string_literal: &'a StringLiteral) { walk_string_literal(self, string_literal); @@ -484,6 +490,17 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) { } } } + Expr::TString(ast::ExprTString { value, .. }) => { + for part in value { + match part { + TStringPart::Literal(string_literal) => { + visitor.visit_string_literal(string_literal); + } + TStringPart::FString(f_string) => visitor.visit_f_string(f_string), + TStringPart::TString(t_string) => visitor.visit_t_string(t_string), + } + } + } Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => { for string_literal in value { visitor.visit_string_literal(string_literal); @@ -739,30 +756,36 @@ pub fn walk_pattern_keyword<'a, V: Visitor<'a> + ?Sized>( } pub fn walk_f_string<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, f_string: &'a FString) { - for f_string_element in &f_string.elements { - visitor.visit_f_string_element(f_string_element); + for interpolated_string_element in &f_string.elements { + visitor.visit_interpolated_string_element(interpolated_string_element); } } -pub fn walk_f_string_element<'a, V: Visitor<'a> + ?Sized>( +pub fn walk_interpolated_string_element<'a, V: Visitor<'a> + ?Sized>( visitor: &mut V, - f_string_element: &'a FStringElement, + interpolated_string_element: &'a InterpolatedStringElement, ) { - if let ast::FStringElement::Expression(ast::FStringExpressionElement { + if let ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { expression, format_spec, .. - }) = f_string_element + }) = interpolated_string_element { visitor.visit_expr(expression); if let Some(format_spec) = format_spec { for spec_element in &format_spec.elements { - visitor.visit_f_string_element(spec_element); + visitor.visit_interpolated_string_element(spec_element); } } } } +pub fn walk_t_string<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, t_string: &'a TString) { + for t_string_element in &t_string.elements { + visitor.visit_interpolated_string_element(t_string_element); + } +} + pub fn walk_expr_context<'a, V: Visitor<'a> + ?Sized>( _visitor: &V, _expr_context: &'a ExprContext, diff --git a/crates/ruff_python_ast/src/visitor/source_order.rs b/crates/ruff_python_ast/src/visitor/source_order.rs index 5e6ca022a0..af57ee48db 100644 --- a/crates/ruff_python_ast/src/visitor/source_order.rs +++ b/crates/ruff_python_ast/src/visitor/source_order.rs @@ -1,8 +1,8 @@ use crate::{ Alias, Arguments, BoolOp, BytesLiteral, CmpOp, Comprehension, Decorator, ElifElseClause, - ExceptHandler, Expr, FString, FStringElement, Keyword, MatchCase, Mod, Operator, Parameter, - ParameterWithDefault, Parameters, Pattern, PatternArguments, PatternKeyword, Singleton, Stmt, - StringLiteral, TypeParam, TypeParams, UnaryOp, WithItem, + ExceptHandler, Expr, FString, InterpolatedStringElement, Keyword, MatchCase, Mod, Operator, + Parameter, ParameterWithDefault, Parameters, Pattern, PatternArguments, PatternKeyword, + Singleton, Stmt, StringLiteral, TString, TypeParam, TypeParams, UnaryOp, WithItem, }; use crate::{AnyNodeRef, Identifier}; @@ -157,8 +157,16 @@ pub trait SourceOrderVisitor<'a> { } #[inline] - fn visit_f_string_element(&mut self, f_string_element: &'a FStringElement) { - walk_f_string_element(self, f_string_element); + fn visit_interpolated_string_element( + &mut self, + interpolated_string_element: &'a InterpolatedStringElement, + ) { + walk_interpolated_string_element(self, interpolated_string_element); + } + + #[inline] + fn visit_t_string(&mut self, t_string: &'a TString) { + walk_t_string(self, t_string); } #[inline] @@ -272,6 +280,7 @@ where Expr::Compare(expr) => expr.visit_source_order(visitor), Expr::Call(expr) => expr.visit_source_order(visitor), Expr::FString(expr) => expr.visit_source_order(visitor), + Expr::TString(expr) => expr.visit_source_order(visitor), Expr::StringLiteral(expr) => expr.visit_source_order(visitor), Expr::BytesLiteral(expr) => expr.visit_source_order(visitor), Expr::NumberLiteral(expr) => expr.visit_source_order(visitor), @@ -497,15 +506,17 @@ where visitor.leave_node(node); } -pub fn walk_f_string_element<'a, V: SourceOrderVisitor<'a> + ?Sized>( +pub fn walk_interpolated_string_element<'a, V: SourceOrderVisitor<'a> + ?Sized>( visitor: &mut V, - f_string_element: &'a FStringElement, + f_string_element: &'a InterpolatedStringElement, ) { let node = AnyNodeRef::from(f_string_element); if visitor.enter_node(node).is_traverse() { match f_string_element { - FStringElement::Expression(element) => element.visit_source_order(visitor), - FStringElement::Literal(element) => element.visit_source_order(visitor), + InterpolatedStringElement::Interpolation(element) => { + element.visit_source_order(visitor); + } + InterpolatedStringElement::Literal(element) => element.visit_source_order(visitor), } } visitor.leave_node(node); @@ -550,6 +561,18 @@ where visitor.leave_node(node); } +#[inline] +pub fn walk_t_string<'a, V>(visitor: &mut V, t_string: &'a TString) +where + V: SourceOrderVisitor<'a> + ?Sized, +{ + let node = AnyNodeRef::from(t_string); + if visitor.enter_node(node).is_traverse() { + t_string.visit_source_order(visitor); + } + visitor.leave_node(node); +} + #[inline] pub fn walk_string_literal<'a, V>(visitor: &mut V, string_literal: &'a StringLiteral) where diff --git a/crates/ruff_python_ast/src/visitor/transformer.rs b/crates/ruff_python_ast/src/visitor/transformer.rs index f3e1c30bbc..07b098eb43 100644 --- a/crates/ruff_python_ast/src/visitor/transformer.rs +++ b/crates/ruff_python_ast/src/visitor/transformer.rs @@ -1,8 +1,8 @@ use crate::{ self as ast, Alias, Arguments, BoolOp, BytesLiteral, CmpOp, Comprehension, Decorator, - ElifElseClause, ExceptHandler, Expr, ExprContext, FString, FStringElement, Keyword, MatchCase, - Operator, Parameter, Parameters, Pattern, PatternArguments, PatternKeyword, Stmt, - StringLiteral, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, + ElifElseClause, ExceptHandler, Expr, ExprContext, FString, InterpolatedStringElement, Keyword, + MatchCase, Operator, Parameter, Parameters, Pattern, PatternArguments, PatternKeyword, Stmt, + StringLiteral, TString, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, TypeParams, UnaryOp, WithItem, }; @@ -86,8 +86,14 @@ pub trait Transformer { fn visit_f_string(&self, f_string: &mut FString) { walk_f_string(self, f_string); } - fn visit_f_string_element(&self, f_string_element: &mut FStringElement) { - walk_f_string_element(self, f_string_element); + fn visit_interpolated_string_element( + &self, + interpolated_string_element: &mut InterpolatedStringElement, + ) { + walk_interpolated_string_element(self, interpolated_string_element); + } + fn visit_t_string(&self, t_string: &mut TString) { + walk_t_string(self, t_string); } fn visit_string_literal(&self, string_literal: &mut StringLiteral) { walk_string_literal(self, string_literal); @@ -470,6 +476,21 @@ pub fn walk_expr(visitor: &V, expr: &mut Expr) { } } } + Expr::TString(ast::ExprTString { value, .. }) => { + for t_string_part in value.iter_mut() { + match t_string_part { + ast::TStringPart::Literal(string_literal) => { + visitor.visit_string_literal(string_literal); + } + ast::TStringPart::FString(f_string) => { + visitor.visit_f_string(f_string); + } + ast::TStringPart::TString(t_string) => { + visitor.visit_t_string(t_string); + } + } + } + } Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => { for string_literal in value.iter_mut() { visitor.visit_string_literal(string_literal); @@ -744,29 +765,35 @@ pub fn walk_pattern_keyword( pub fn walk_f_string(visitor: &V, f_string: &mut FString) { for element in &mut f_string.elements { - visitor.visit_f_string_element(element); + visitor.visit_interpolated_string_element(element); } } -pub fn walk_f_string_element( +pub fn walk_interpolated_string_element( visitor: &V, - f_string_element: &mut FStringElement, + interpolated_string_element: &mut InterpolatedStringElement, ) { - if let ast::FStringElement::Expression(ast::FStringExpressionElement { + if let ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { expression, format_spec, .. - }) = f_string_element + }) = interpolated_string_element { visitor.visit_expr(expression); if let Some(format_spec) = format_spec { for spec_element in &mut format_spec.elements { - visitor.visit_f_string_element(spec_element); + visitor.visit_interpolated_string_element(spec_element); } } } } +pub fn walk_t_string(visitor: &V, t_string: &mut TString) { + for element in &mut t_string.elements { + visitor.visit_interpolated_string_element(element); + } +} + pub fn walk_expr_context(_visitor: &V, _expr_context: &mut ExprContext) {} pub fn walk_bool_op(_visitor: &V, _bool_op: &mut BoolOp) {} diff --git a/crates/ruff_python_ast_integration_tests/tests/comparable.rs b/crates/ruff_python_ast_integration_tests/tests/comparable.rs index 89155a58a0..8f792f1bdd 100644 --- a/crates/ruff_python_ast_integration_tests/tests/comparable.rs +++ b/crates/ruff_python_ast_integration_tests/tests/comparable.rs @@ -1,19 +1,36 @@ use ruff_python_ast::comparable::ComparableExpr; use ruff_python_parser::{ParseError, parse_expression}; +#[track_caller] +fn assert_comparable(left: &str, right: &str) -> Result<(), ParseError> { + let left_parsed = parse_expression(left)?; + let right_parsed = parse_expression(right)?; + + let left_compr = ComparableExpr::from(left_parsed.expr()); + let right_compr = ComparableExpr::from(right_parsed.expr()); + + assert_eq!(left_compr, right_compr); + Ok(()) +} + +#[track_caller] +fn assert_noncomparable(left: &str, right: &str) -> Result<(), ParseError> { + let left_parsed = parse_expression(left)?; + let right_parsed = parse_expression(right)?; + + let left_compr = ComparableExpr::from(left_parsed.expr()); + let right_compr = ComparableExpr::from(right_parsed.expr()); + + assert_ne!(left_compr, right_compr); + Ok(()) +} + #[test] fn concatenated_strings_compare_equal() -> Result<(), ParseError> { let split_contents = r#"'a' 'b' r'\n raw'"#; let value_contents = r#"'ab\\n raw'"#; - let split_parsed = parse_expression(split_contents)?; - let value_parsed = parse_expression(value_contents)?; - - let split_compr = ComparableExpr::from(split_parsed.expr()); - let value_compr = ComparableExpr::from(value_parsed.expr()); - - assert_eq!(split_compr, value_compr); - Ok(()) + assert_comparable(split_contents, value_contents) } #[test] @@ -21,14 +38,7 @@ fn concatenated_bytes_compare_equal() -> Result<(), ParseError> { let split_contents = r#"b'a' b'b'"#; let value_contents = r#"b'ab'"#; - let split_parsed = parse_expression(split_contents)?; - let value_parsed = parse_expression(value_contents)?; - - let split_compr = ComparableExpr::from(split_parsed.expr()); - let value_compr = ComparableExpr::from(value_parsed.expr()); - - assert_eq!(split_compr, value_compr); - Ok(()) + assert_comparable(split_contents, value_contents) } #[test] @@ -36,12 +46,45 @@ fn concatenated_fstrings_compare_equal() -> Result<(), ParseError> { let split_contents = r#"f"{foo!r} this" r"\n raw" f" and {bar!s} that""#; let value_contents = r#"f"{foo!r} this\\n raw and {bar!s} that""#; - let split_parsed = parse_expression(split_contents)?; - let value_parsed = parse_expression(value_contents)?; - - let split_compr = ComparableExpr::from(split_parsed.expr()); - let value_compr = ComparableExpr::from(value_parsed.expr()); - - assert_eq!(split_compr, value_compr); - Ok(()) + assert_comparable(split_contents, value_contents) +} + +#[test] +fn concatenated_tstrings_compare_equal() -> Result<(), ParseError> { + let split_contents = r#"t"{foo!r} this" r"\n raw" t" and {bar!s} that""#; + let value_contents = r#"t"{foo!r} this\\n raw and {bar!s} that""#; + + assert_comparable(split_contents, value_contents) +} + +#[test] +fn concatenated_f_and_t_strings_interwoven_compare_equal() -> Result<(), ParseError> { + let split_contents = r#"f"{foo} this " t"{bar}" "baz""#; + let value_contents = r#"f"{foo}" t" this {bar}" "baz""#; + + assert_comparable(split_contents, value_contents) +} + +#[test] +fn concatenated_f_and_t_strings_compare_unequal_when_swapped() -> Result<(), ParseError> { + let f_then_t_contents = r#"f"{foo!r} this" r"\n raw" t" and {bar!s} that""#; + let t_then_f_contents = r#"t"{foo!r} this" r"\n raw" f" and {bar!s} that""#; + + assert_noncomparable(f_then_t_contents, t_then_f_contents) +} + +#[test] +fn t_strings_literal_order_matters_compare_unequal() -> Result<(), ParseError> { + let interp_then_literal_contents = r#"t"{foo}bar""#; + let literal_then_interp_contents = r#"t"bar{foo}""#; + + assert_noncomparable(interp_then_literal_contents, literal_then_interp_contents) +} + +#[test] +fn t_strings_empty_concat_equal() -> Result<(), ParseError> { + let empty_literal = r#""" t"hey{foo}""#; + let empty_f_string = r#"f""t"hey{foo}""#; + + assert_comparable(empty_literal, empty_f_string) } diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__f_strings.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__f_strings.snap index 84266e3745..87e9af87a6 100644 --- a/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__f_strings.snap +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__f_strings.snap @@ -1,18 +1,17 @@ --- source: crates/ruff_python_ast_integration_tests/tests/source_order.rs expression: trace -snapshot_kind: text --- - ModModule - StmtExpr - ExprFString - StringLiteral - FString - - FStringLiteralElement - - FStringExpressionElement + - InterpolatedStringLiteralElement + - InterpolatedElement - ExprName - - FStringLiteralElement - - FStringExpressionElement + - InterpolatedStringLiteralElement + - InterpolatedElement - ExprName - - FStringLiteralElement - - FStringLiteralElement + - InterpolatedStringLiteralElement + - InterpolatedStringLiteralElement diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__t_strings.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__t_strings.snap new file mode 100644 index 0000000000..75e4f537b2 --- /dev/null +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/source_order__t_strings.snap @@ -0,0 +1,17 @@ +--- +source: crates/ruff_python_ast_integration_tests/tests/source_order.rs +expression: trace +--- +- ModModule + - StmtExpr + - ExprTString + - StringLiteral + - TString + - InterpolatedStringLiteralElement + - InterpolatedElement + - ExprName + - InterpolatedStringLiteralElement + - InterpolatedElement + - ExprName + - InterpolatedStringLiteralElement + - InterpolatedStringLiteralElement diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__f_strings.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__f_strings.snap index f379b791d7..4d81357f9e 100644 --- a/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__f_strings.snap +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__f_strings.snap @@ -1,17 +1,16 @@ --- source: crates/ruff_python_ast_integration_tests/tests/visitor.rs expression: trace -snapshot_kind: text --- - StmtExpr - ExprFString - StringLiteral - FString - - FStringLiteralElement - - FStringExpressionElement + - InterpolatedStringLiteralElement + - InterpolatedElement - ExprName - - FStringLiteralElement - - FStringExpressionElement + - InterpolatedStringLiteralElement + - InterpolatedElement - ExprName - - FStringLiteralElement - - FStringLiteralElement + - InterpolatedStringLiteralElement + - InterpolatedStringLiteralElement diff --git a/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__t_strings.snap b/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__t_strings.snap new file mode 100644 index 0000000000..58def387aa --- /dev/null +++ b/crates/ruff_python_ast_integration_tests/tests/snapshots/visitor__t_strings.snap @@ -0,0 +1,16 @@ +--- +source: crates/ruff_python_ast_integration_tests/tests/visitor.rs +expression: trace +--- +- StmtExpr + - ExprTString + - StringLiteral + - TString + - InterpolatedStringLiteralElement + - InterpolatedElement + - ExprName + - InterpolatedStringLiteralElement + - InterpolatedElement + - ExprName + - InterpolatedStringLiteralElement + - InterpolatedStringLiteralElement diff --git a/crates/ruff_python_ast_integration_tests/tests/source_order.rs b/crates/ruff_python_ast_integration_tests/tests/source_order.rs index 1f01d91c7f..ea257eb279 100644 --- a/crates/ruff_python_ast_integration_tests/tests/source_order.rs +++ b/crates/ruff_python_ast_integration_tests/tests/source_order.rs @@ -146,6 +146,15 @@ fn f_strings() { assert_snapshot!(trace); } +#[test] +fn t_strings() { + let source = r"'pre' t'foo {bar:.{x}f} baz'"; + + let trace = trace_source_order_visitation(source); + + assert_snapshot!(trace); +} + fn trace_source_order_visitation(source: &str) -> String { let parsed = parse(source, ParseOptions::from(Mode::Module)).unwrap(); diff --git a/crates/ruff_python_ast_integration_tests/tests/visitor.rs b/crates/ruff_python_ast_integration_tests/tests/visitor.rs index c99365898b..9cdc7d998a 100644 --- a/crates/ruff_python_ast_integration_tests/tests/visitor.rs +++ b/crates/ruff_python_ast_integration_tests/tests/visitor.rs @@ -4,13 +4,14 @@ use insta::assert_snapshot; use ruff_python_ast::visitor::{ Visitor, walk_alias, walk_bytes_literal, walk_comprehension, walk_except_handler, walk_expr, - walk_f_string, walk_f_string_element, walk_keyword, walk_match_case, walk_parameter, - walk_parameters, walk_pattern, walk_stmt, walk_string_literal, walk_type_param, walk_with_item, + walk_f_string, walk_interpolated_string_element, walk_keyword, walk_match_case, walk_parameter, + walk_parameters, walk_pattern, walk_stmt, walk_string_literal, walk_t_string, walk_type_param, + walk_with_item, }; use ruff_python_ast::{ self as ast, Alias, AnyNodeRef, BoolOp, BytesLiteral, CmpOp, Comprehension, ExceptHandler, - Expr, FString, FStringElement, Keyword, MatchCase, Operator, Parameter, Parameters, Pattern, - Stmt, StringLiteral, TypeParam, UnaryOp, WithItem, + Expr, FString, InterpolatedStringElement, Keyword, MatchCase, Operator, Parameter, Parameters, + Pattern, Stmt, StringLiteral, TString, TypeParam, UnaryOp, WithItem, }; use ruff_python_parser::{Mode, ParseOptions, parse}; @@ -154,6 +155,15 @@ fn f_strings() { assert_snapshot!(trace); } +#[test] +fn t_strings() { + let source = r"'pre' t'foo {bar:.{x}f} baz'"; + + let trace = trace_visitation(source); + + assert_snapshot!(trace); +} + fn trace_visitation(source: &str) -> String { let parsed = parse(source, ParseOptions::from(Mode::Module)).unwrap(); @@ -318,9 +328,15 @@ impl Visitor<'_> for RecordVisitor { self.exit_node(); } - fn visit_f_string_element(&mut self, f_string_element: &FStringElement) { + fn visit_interpolated_string_element(&mut self, f_string_element: &InterpolatedStringElement) { self.enter_node(f_string_element); - walk_f_string_element(self, f_string_element); + walk_interpolated_string_element(self, f_string_element); + self.exit_node(); + } + + fn visit_t_string(&mut self, t_string: &TString) { + self.enter_node(t_string); + walk_t_string(self, t_string); self.exit_node(); } } diff --git a/crates/ruff_python_codegen/src/generator.rs b/crates/ruff_python_codegen/src/generator.rs index d435ff5c22..8933205731 100644 --- a/crates/ruff_python_codegen/src/generator.rs +++ b/crates/ruff_python_codegen/src/generator.rs @@ -5,9 +5,9 @@ use std::ops::Deref; use ruff_python_ast::{ self as ast, Alias, AnyStringFlags, ArgOrKeyword, BoolOp, BytesLiteralFlags, CmpOp, - Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, FStringFlags, Identifier, - MatchCase, Operator, Parameter, Parameters, Pattern, Singleton, Stmt, StringFlags, Suite, - TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, WithItem, + Comprehension, ConversionFlag, DebugText, ExceptHandler, Expr, Identifier, MatchCase, Operator, + Parameter, Parameters, Pattern, Singleton, Stmt, StringFlags, Suite, TypeParam, + TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, WithItem, }; use ruff_python_ast::{ParameterWithDefault, TypeParams}; use ruff_python_literal::escape::{AsciiEscape, Escape, UnicodeEscape}; @@ -1112,6 +1112,9 @@ impl<'a> Generator<'a> { Expr::FString(ast::ExprFString { value, .. }) => { self.unparse_f_string_value(value); } + Expr::TString(ast::ExprTString { value, .. }) => { + self.unparse_t_string_value(value); + } Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => { self.unparse_string_literal_value(value); } @@ -1326,24 +1329,24 @@ impl<'a> Generator<'a> { self.unparse_string_literal(string_literal); } ast::FStringPart::FString(f_string) => { - self.unparse_f_string(&f_string.elements, f_string.flags); + self.unparse_interpolated_string(&f_string.elements, f_string.flags.into()); } } } } - fn unparse_f_string_body(&mut self, values: &[ast::FStringElement]) { + fn unparse_interpolated_string_body(&mut self, values: &[ast::InterpolatedStringElement]) { for value in values { - self.unparse_f_string_element(value); + self.unparse_interpolated_string_element(value); } } - fn unparse_f_string_expression_element( + fn unparse_interpolated_element( &mut self, val: &Expr, debug_text: Option<&DebugText>, conversion: ConversionFlag, - spec: Option<&ast::FStringFormatSpec>, + spec: Option<&ast::InterpolatedStringFormatSpec>, ) { let mut generator = Generator::new(self.indent, self.line_ending); generator.unparse_expr(val, precedence::FORMATTED_VALUE); @@ -1379,18 +1382,21 @@ impl<'a> Generator<'a> { self.p("}"); } - fn unparse_f_string_element(&mut self, element: &ast::FStringElement) { + fn unparse_interpolated_string_element(&mut self, element: &ast::InterpolatedStringElement) { match element { - ast::FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => { - self.unparse_f_string_literal_element(value); + ast::InterpolatedStringElement::Literal(ast::InterpolatedStringLiteralElement { + value, + .. + }) => { + self.unparse_interpolated_string_literal_element(value); } - ast::FStringElement::Expression(ast::FStringExpressionElement { + ast::InterpolatedStringElement::Interpolation(ast::InterpolatedElement { expression, debug_text, conversion, format_spec, range: _, - }) => self.unparse_f_string_expression_element( + }) => self.unparse_interpolated_element( expression, debug_text.as_ref(), *conversion, @@ -1399,24 +1405,46 @@ impl<'a> Generator<'a> { } } - fn unparse_f_string_literal_element(&mut self, s: &str) { + fn unparse_interpolated_string_literal_element(&mut self, s: &str) { let s = s.replace('{', "{{").replace('}', "}}"); self.p(&s); } - fn unparse_f_string_specifier(&mut self, values: &[ast::FStringElement]) { - self.unparse_f_string_body(values); + fn unparse_f_string_specifier(&mut self, values: &[ast::InterpolatedStringElement]) { + self.unparse_interpolated_string_body(values); } /// Unparse `values` with [`Generator::unparse_f_string_body`], using `quote` as the preferred /// surrounding quote style. - fn unparse_f_string(&mut self, values: &[ast::FStringElement], flags: FStringFlags) { + fn unparse_interpolated_string( + &mut self, + values: &[ast::InterpolatedStringElement], + flags: AnyStringFlags, + ) { let mut generator = Generator::new(self.indent, self.line_ending); - generator.unparse_f_string_body(values); + generator.unparse_interpolated_string_body(values); let body = &generator.buffer; self.p_str_repr(body, flags); } + fn unparse_t_string_value(&mut self, value: &ast::TStringValue) { + let mut first = true; + for t_string_part in value { + self.p_delim(&mut first, " "); + match t_string_part { + ast::TStringPart::Literal(string_literal) => { + self.unparse_string_literal(string_literal); + } + ast::TStringPart::FString(f_string) => { + self.unparse_interpolated_string(&f_string.elements, f_string.flags.into()); + } + ast::TStringPart::TString(t_string) => { + self.unparse_interpolated_string(&t_string.elements, t_string.flags.into()); + } + } + } + } + fn unparse_alias(&mut self, alias: &Alias) { self.p_id(&alias.name); if let Some(asname) = &alias.asname { diff --git a/crates/ruff_python_formatter/generate.py b/crates/ruff_python_formatter/generate.py index ba8354c2aa..ded6ae5b00 100755 --- a/crates/ruff_python_formatter/generate.py +++ b/crates/ruff_python_formatter/generate.py @@ -38,9 +38,9 @@ for node_line in node_lines: # `FStringLiteralElement`, `FStringFormatSpec` and `FStringExpressionElement` are # handled by the `FString` implementation. if node in ( - "FStringLiteralElement", - "FStringExpressionElement", - "FStringFormatSpec", + "InterpolatedStringLiteralElement", + "InterpolatedElement", + "InterpolatedStringFormatSpec", "Identifier", ): continue diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py index 8b27e3cd9f..439d0f1cc1 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/binary.py @@ -431,3 +431,16 @@ f"hellooooooooooooooooooooooo \ worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) aaaaaaaaaaa = f"hellooooooooooooooooooooooo \ worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) + +# This t-string should be flattened +xxxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaaaaaaa { + expression } bbbbbbbbbbbbbbbbbbbbbbbb" + ( + yyyyyyyyyyyyyy + zzzzzzzzzzz +) + +# This is not a multiline t-string, but the expression is too long so it should be +# wrapped in parentheses. +t"hellooooooooooooooooooooooo \ + worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) +aaaaaaaaaaa = t"hellooooooooooooooooooooooo \ + worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py index ede789e997..e945211bec 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string.py @@ -100,6 +100,55 @@ f"{10 + len('bar')=}" f'no debug{10}' f"{10 + len('bar')=}" f"{10 + len('bar')=}" f'{10 + len("bar")=}' +############################################################################## +# T-strings +############################################################################## + +# Escape `{` and `}` when merging a t-string with a string +"a {not_a_variable}" t"b {10}" "c" + +# Join, and break expressions +t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{ +expression +}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more" + +# Join, but don't break the expressions +t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more" + +t"test{ +expression +}flat" t"can be { +joined +} together" + +aaaaaaaaaaa = t"test{ +expression +}flat" t"cean beeeeeeee { +joined +} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + + +t"single quoted '{x}'" t'double quoted "{x}"' # Same number of quotes => use preferred quote style +t"single quote ' {x}" t'double quoted "{x}"' # More double quotes => use single quotes +t"single quoted '{x}'" t'double quote " {x}"' # More single quotes => use double quotes + +# Different triple quoted strings +t"{'''test'''}" t'{"""other"""}' + +# Now with inner quotes +t"{'''test ' '''}" t'{"""other " """}' +t"{some_where_nested('''test ' ''')}" t'{"""other " """ + "more"}' +t"{b'''test ' '''}" t'{b"""other " """}' +t"{t'''test ' '''}" t'{t"""other " """}' + +# debug expressions containing quotes +t"{10 + len('bar')=}" t"{10 + len('bar')=}" +t"{10 + len('bar')=}" t'no debug{10}' t"{10 + len('bar')=}" + +# We can't safely merge this pre Python 3.12 without altering the debug expression. +t"{10 + len('bar')=}" t'{10 + len("bar")=}' + + ############################################################################## # Don't join raw strings ############################################################################## @@ -110,6 +159,9 @@ R"a" "normal" f"test" fr"test" f"test" fR"test" +t"test" tr"test" +t"test" tR"test" + ############################################################################## # Don't join triple quoted strings @@ -119,9 +171,22 @@ f"test" fR"test" "single" f""""single""" +"single" t""""single""" + b"single" b"""triple""" +############################################################################## +# Don't join t-strings and f-strings +############################################################################## + +t"{interp}" f"{expr}" + +f"{expr}" t"{interp}" + +f"{expr}" "string" t"{interp}" + + ############################################################################## # Join strings in with statements ############################################################################## diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py index 642ad83c27..c70233ea52 100644 --- a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/join_implicit_concatenated_string_assignment.py @@ -293,6 +293,155 @@ aaaaa[aaaaaaaaaaa] = ( ) +############################################################# +# T-Strings +############################################################# + +# Flatten and join the t-string +aaaaaaaaaaa = t"test{ +expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline + +# Parenthesize the value and join it, inline the comment +aaaaaaaaaaa = t"test{ +expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# Parenthesize the t-string and keep it multiline because it doesn't fit on a single line including the comment +aaaaaaaaaaa = t"test{ +expression +}flat" t"cean beeeeeeee { +joined +} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + + +# The target splits because of a magic trailing comma +# The string is joined and not parenthesized because it just fits into the line length (including comment). +a[ + aaaaaaa, + b, +] = t"ccccc{ +expression}ccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment + + +# Same but starting with a joined string. They should both result in the same formatting. +[ + aaaaaaa, + b, +] = t"ccccc{ +expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment + +# The target splits because of the magic trailing comma +# The string is **not** joined because it with the inlined comment exceeds the line length limit. +a[ + aaaaaaa, + b, +] = t"ccccc{ +expression}cccccccccccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment + + +# The target should be flat +# The string should be joined because it fits into the line length +a[ + aaaaaaa, + b +] = ( + t"ccccc{ + expression}ccccccccccc" "cccccccccccccccccccccccc" # comment +) + +# Same but starting with a joined string. They should both result in the same formatting. +a[ + aaaaaaa, + b +] = t"ccccc{ +expression}ccccccccccccccccccccccccccccccccccc" # comment + +# The target should be flat +# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit. +a[ + aaaaaaa, + b +] = t"ccccc{ +expression}ccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment + + +# Split an overlong target, but join the string if it fits +a[ + aaaaaaa, + b +].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( + t"ccccc{ + expression}ccccccccccc" "cccccccccccccccccccccccccccccc" # comment +) + +# Split both if necessary and keep multiline +a[ + aaaaaaa, + b +].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( + t"ccccc{ + expression}cccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment +) + +# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment +) + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment + +aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment +) + +# Don't inline t-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +# Don't inline t-strings with multiline debug expressions: +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + + # Trailing last-part comments a = ( @@ -374,4 +523,4 @@ self._attr_unique_id = ( return ( f"Exception in {call_back_name} when handling msg on " f"'{msg.topic}': '{msg.payload}'" # type: ignore[str-bytes-safe] -) \ No newline at end of file +) diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.options.json b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.options.json new file mode 100644 index 0000000000..c485014b9b --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.options.json @@ -0,0 +1 @@ +[{"target_version": "3.14"}] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py new file mode 100644 index 0000000000..31087f1610 --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py @@ -0,0 +1,731 @@ +( + t'{one}' + t'{two}' +) + + +rt"Not-so-tricky \"quote" + +# Regression test for tstrings dropping comments +result_f = ( + 'Traceback (most recent call last):\n' + t' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n' + ' f()\n' + t' File "{__file__}", line {lineno_f+1}, in f\n' + ' f()\n' + t' File "{__file__}", line {lineno_f+1}, in f\n' + ' f()\n' + t' File "{__file__}", line {lineno_f+1}, in f\n' + ' f()\n' + # XXX: The following line changes depending on whether the tests + # are run through the interactive interpreter or with -m + # It also varies depending on the platform (stack size) + # Fortunately, we don't care about exactness here, so we use regex + r' \[Previous line repeated (\d+) more times\]' '\n' + 'RecursionError: maximum recursion depth exceeded\n' +) + + +# Regression for tstring dropping comments that were accidentally attached to +# an expression inside a formatted value +( + t'{1}' + # comment 1 + '' +) + +( + t'{1}' # comment 2 + t'{2}' +) + +( + t'{1}' + t'{2}' # comment 3 +) + +( + 1, ( # comment 4 + t'{2}' + ) +) + +( + ( + t'{1}' + # comment 5 + ), + 2 +) + +# https://github.com/astral-sh/ruff/issues/6841 +x = t'''a{""}b''' +y = t'''c{1}d"""e''' +z = t'''a{""}b''' t'''c{1}d"""e''' + +# T-String formatting test cases (Preview) + +# Simple expression with a mix of debug expression and comments. +x = t"{a}" +x = t"{ + a = }" +x = t"{ # comment 6 + a }" +x = t"{ # comment 7 + a = }" + +# Remove the parentheses as adding them doesn't make then fit within the line length limit. +# This is similar to how we format it before t-string formatting. +aaaaaaaaaaa = ( + t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd } cccccccccc" +) +# Here, we would use the best fit layout to put the t-string indented on the next line +# similar to the next example. +aaaaaaaaaaa = t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc" +aaaaaaaaaaa = ( + t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc" +) + +# This should never add the optional parentheses because even after adding them, the +# t-string exceeds the line length limit. +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9 + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc" + +# Multiple larger expressions which exceeds the line length limit. Here, we need to decide +# whether to split at the first or second expression. This should work similarly to the +# assignment statement formatting where we split from right to left in preview mode. +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" + +# The above example won't split but when we start introducing line breaks: +x = t"aaaaaaaaaaaa { + bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb + } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { + ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd + } eeeeeeeeeeeeee" + +# But, in case comments are present, we would split at the expression containing the +# comments: +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb # comment 10 + } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb + } cccccccccccccccccccc { # comment 11 + ddddddddddddddd } eeeeeeeeeeeeee" + +# Here, the expression part itself starts with a curly brace so we need to add an extra +# space between the opening curly brace and the expression. +x = t"{ {'x': 1, 'y': 2} }" +# Although the extra space isn't required before the ending curly brace, we add it for +# consistency. +x = t"{ {'x': 1, 'y': 2}}" +x = t"{ {'x': 1, 'y': 2} = }" +x = t"{ # comment 12 + {'x': 1, 'y': 2} }" +x = t"{ # comment 13 + {'x': 1, 'y': 2} = }" +# But, if there's a format specifier or a conversion flag then we don't need to add +# any whitespace at the end +x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}!s} bbbbbb" +x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}:.3f} bbbbbb" + +# But, in this case, we would split the expression itself because it exceeds the line +# length limit so we need not add the extra space. +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'} +}" +# And, split the expression itself because it exceeds the line length. +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +############################################################################################# +# Quotes +############################################################################################# +t"foo 'bar' {x}" +t"foo \"bar\" {x}" +t'foo "bar" {x}' +t'foo \'bar\' {x}' +t"foo {"bar"}" + +t"single quoted '{x}' double quoted \"{x}\"" # Same number of quotes => use preferred quote style +t"single quote ' {x} double quoted \"{x}\"" # More double quotes => use single quotes +t"single quoted '{x}' double quote \" {x}" # More single quotes => use double quotes + +fr"single quotes ' {x}" # Keep double because `'` can't be escaped +fr'double quotes " {x}' # Keep single because `"` can't be escaped +fr'flip quotes {x}' # Use preferred quotes, because raw string contains now quotes. + +# Here, the formatter will remove the escapes +t"foo {'\'bar\''}" +t"foo {'\"bar\"'}" + +# Quotes inside the expressions have no impact on the quote selection of the outer string. +# Required so that the following two examples result in the same formatting. +t'foo {10 + len("bar")}' +t"foo {10 + len('bar')}" + +# Pre 312, preserve the outer quotes if the t-string contains quotes in the debug expression +t'foo {10 + len("bar")=}' +t'''foo {10 + len('''bar''')=}''' +t'''foo {10 + len('bar')=}''' # Fine to change the quotes because it uses triple quotes + +# Triple-quoted strings +# It's ok to use the same quote char for the inner string if it's single-quoted. +t"""test {'inner'}""" +t"""test {"inner"}""" +# But if the inner string is also triple-quoted then we should preserve the existing quotes. +t"""test {'''inner'''}""" + +# It's not okay to change the quote style if the inner string is triple quoted and contains a quote. +t'{"""other " """}' +t'{"""other " """ + "more"}' +t'{b"""other " """}' +t'{t"""other " """}' + +t"""test {t'inner {'''inner inner'''}'}""" +t"""test {t'''inner {"""inner inner"""}'''}""" + +# Magic trailing comma +# +# The expression formatting will result in breaking it across multiple lines with a +# trailing comma but as the expression isn't already broken, we will remove all the line +# breaks which results in the trailing comma being present. This test case makes sure +# that the trailing comma is removed as well. +t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa" + +# And, if the trailing comma is already present, we still need to remove it. +t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee',]} aaaaaaa" + +# Keep this Multiline by breaking it at the square brackets. +t"""aaaaaa {[ + xxxxxxxx, + yyyyyyyy, +]} ccc""" + +# Add the magic trailing comma because the elements don't fit within the line length limit +# when collapsed. +t"aaaaaa {[ + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + yyyyyyyyyyyy +]} ccccccc" + +# Remove the parentheses because they aren't required +xxxxxxxxxxxxxxx = ( + t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbb { + xxxxxxxxxxx # comment 14 + + yyyyyyyyyy + } dddddddddd" +) + +# Comments + +# No comments should be dropped! +t"{ # comment 15 + # comment 16 + foo # comment 17 + # comment 18 +}" # comment 19 +# comment 20 + +# Single-quoted t-strings with a format specificer can be multiline +t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable:.3f} ddddddddddddddd eeeeeeee" + +# But, if it's triple-quoted then we can't or the format specificer will have a +# trailing newline +t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable:.3f} ddddddddddddddd eeeeeeee""" + +# But, we can break the ones which don't have a format specifier +t"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr { + xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { xxxxxxxxxxxxxxxxxxxx } bbbbbbbbbbbb""" + +# Throw in a random comment in it but surprise, this is not a comment but just a text +# which is part of the format specifier +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f + # comment +} cccccccccc""" +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f + # comment} cccccccccc""" + +# Conversion flags +# +# This is not a valid Python code because of the additional whitespace between the `!` +# and conversion type. But, our parser isn't strict about this. This should probably be +# removed once we have a strict parser. +x = t"aaaaaaaaa { x ! r }" + +# Even in the case of debug expressions, we only need to preserve the whitespace within +# the expression part of the replacement field. +x = t"aaaaaaaaa { x = ! r }" + +# Combine conversion flags with format specifiers +x = t"{x = ! s + :>0 + + }" +# This is interesting. There can be a comment after the format specifier but only if it's +# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. +# We'll format is as trailing comments. +x = t"{x !s + :>0 + # comment 21 + }" + +x = t""" +{ # comment 22 + x = :.0{y # comment 23 + }f}""" + +# Here, the debug expression is in a nested t-string so we should start preserving +# whitespaces from that point onwards. This means we should format the outer t-string. +x = t"""{"foo " + # comment 24 + t"{ x = + + }" # comment 25 + } + """ + +# Mix of various features. +t"{ # comment 26 + foo # after foo + :>{ + x # after x + } + # comment 27 + # comment 28 +} woah {x}" + +# Assignment statement + +# Even though this t-string has multiline expression, thus allowing us to break it at the +# curly braces, the t-string fits on a single line if it's moved inside the parentheses. +# We should prefer doing that instead. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" + +# Same as above +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Similar to the previous example, but the t-string will exceed the line length limit, +# we shouldn't add any parentheses here. +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Same as above but with an inline comment. The t-string should be formatted inside the +# parentheses and the comment should be part of the line inside the parentheses. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Similar to the previous example but this time parenthesizing doesn't work because it +# exceeds the line length. So, avoid parenthesizing this t-string. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# Similar to the previous example but we start with the parenthesized layout. This should +# remove the parentheses and format the t-string on a single line. This shows that the +# final layout for the formatter is same for this and the previous case. The only +# difference is that in the previous case the expression is already mulitline which means +# the formatter can break it further at the curly braces. +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment loooooooong +) + +# The following t-strings are going to break because of the trailing comma so we should +# avoid using the best fit layout and instead use the default layout. +# left-to-right +aaaa = t"aaaa {[ + 1, 2, +]} bbbb" +# right-to-left +aaaa, bbbb = t"aaaa {[ + 1, 2, +]} bbbb" + +# Using the right-to-left assignment statement variant. +aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Here, the t-string layout is flat but it exceeds the line length limit. This shouldn't +# try the custom best fit layout because the t-string doesn't have any split points. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +# Same as above but without the parentheses to test that it gets formatted to the same +# layout as the previous example. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" + +# But, the following t-string does have a split point because of the multiline expression. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + dddddddddddddddddddddddddddd} ddddddddddddddddddd" +) + +# This is an implicitly concatenated t-string but it cannot be joined because otherwise +# it'll exceed the line length limit. So, the two t-strings will be inside parentheses +# instead and the inline comment should be outside the parentheses. +a = t"test{ + expression +}flat" t"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# Similar to the above example but this fits within the line length limit. +a = t"test{ + expression +}flat" t"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# The following test cases are adopted from implicit string concatenation but for a +# single t-string instead. + +# Don't inline t-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment +) + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment +) + +# Don't inline t-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# Don't inline t-strings with multiline debug expressions or format specifiers +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# This is not a multiline t-string even though it has a newline after the format specifier. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest" # comment +) + +# The newline is only considered when it's a tripled-quoted t-string. +aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment +) + +# Remove the parentheses here +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + # comment + ]}moee" # comment +) +# ... but not here because of the ownline comment +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + ]}moee" + # comment +) + +# t-strings in other positions + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if ( + t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }" +): pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if ( + t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }" +): + pass + +if t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +# For loops +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# With statements +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# Assert statements +assert t"aaaaaaaaa{ + expression}bbbbbbbbbbbb", t"cccccccccc{ + expression}dddddddddd" + +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{ + expression}dddddddddddddddd" + +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{expression}dddddddddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccccccccccccccccc { + expression} dddddddddddddddddddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd" + +# t-strings as a single argument to a call expression to test whether it's huggable or not. +call(t"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(t"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(t"{ # comment + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""") + +call(t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""") + +call( + t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""" +) + +call(t"{ + aaaaaa + + '''test + more''' +}") + +# Indentation + +# What should be the indentation? +# https://github.com/astral-sh/ruff/discussions/9785#discussioncomment-8470590 +if indent0: + if indent1: + if indent2: + foo = t"""hello world +hello { + t"aaaaaaa { + [ + 'aaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbb', + 'ccccccccccccccccccccc', + 'ddddddddddddddddddddd' + ] + } bbbbbbbb" + + [ + 'aaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbb', + 'ccccccccccccccccccccc', + 'ddddddddddddddddddddd' + ] + } -------- +""" + + +# Implicit concatenated t-string containing quotes +_ = ( + 'This string should change its quotes to double quotes' + t'This string uses double quotes in an expression {"it's a quote"}' + t'This t-string does not use any quotes.' +) + +# Regression test for https://github.com/astral-sh/ruff/issues/14487 +t"aaaaaaaaaaaaaaaaaaaaaaaaaa {10**27} bbbbbbbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccccc" + +# Regression test for https://github.com/astral-sh/ruff/issues/14778 +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}" +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}" + +# Quotes reuse +t"{'a'}" + +# 312+, it's okay to change the outer quotes even when there's a debug expression using the same quotes +t'foo {10 + len("bar")=}' +t'''foo {10 + len("""bar""")=}''' + +# 312+, it's okay to change the quotes here without creating an invalid t-string +t'{"""other " """}' +t'{"""other " """ + "more"}' +t'{b"""other " """}' +t'{t"""other " """}' + + +# Regression tests for https://github.com/astral-sh/ruff/issues/13935 +t'{1: hy "user"}' +t'{1:hy "user"}' +t'{1: abcd "{1}" }' +t'{1: abcd "{'aa'}" }' +t'{1=: "abcd {'aa'}}' +t'{x:a{z:hy "user"}} \'\'\'' + +# Changing the outer quotes is fine because the format-spec is in a nested expression. +t'{t'{z=:hy "user"}'} \'\'\'' + + +# We have to be careful about changing the quotes if the t-string has a debug expression because it is inserted verbatim. +t'{1=: "abcd \'\'}' # Don't change the outer quotes, or it results in a syntax error +t'{1=: abcd \'\'}' # Changing the quotes here is fine because the inner quotes aren't the opposite quotes +t'{1=: abcd \"\"}' # Changing the quotes here is fine because the inner quotes are escaped +# Don't change the quotes in the following cases: +t'{x=:hy "user"} \'\'\'' +t'{x=:a{y:hy "user"}} \'\'\'' +t'{x=:a{y:{z:hy "user"}}} \'\'\'' +t'{x:a{y=:{z:hy "user"}}} \'\'\'' + +# This is fine because the debug expression and format spec are in a nested expression + +t"""{1=: "this" is fine}""" +t'''{1=: "this" is fine}''' # Change quotes to double quotes because they're preferred +t'{1=: {'ab"cd"'}}' # It's okay if the quotes are in an expression part. + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15459 +print(t"{ {1, 2, 3} - {2} }") +print(t"{ {1: 2}.keys() }") +print(t"{({1, 2, 3}) - ({2})}") +print(t"{1, 2, {3} }") +print(t"{(1, 2, {3})}") + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15535 +print(t"{ {}, }") # A single item tuple gets parenthesized +print(t"{ {}.values(), }") +print(t"{ {}, 1 }") # A tuple with multiple elements doesn't get parenthesized +print(t"{ # Tuple with multiple elements that doesn't fit on a single line gets parenthesized + {}, 1, +}") + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15536 +print(t"{ {}, 1, }") diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 6d7c79e33d..8d7aeb502b 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -205,14 +205,14 @@ impl<'fmt, 'ast, 'buf> JoinCommaSeparatedBuilder<'fmt, 'ast, 'buf> { pub(crate) fn finish(&mut self) -> FormatResult<()> { self.result.and_then(|()| { - // Don't add a magic trailing comma when formatting an f-string expression + // Don't add a magic trailing comma when formatting an f-string or t-string expression // that always must be flat because the `expand_parent` forces enclosing // groups to expand, e.g. `print(f"{(a,)} ")` would format the f-string in // flat mode but the `print` call gets expanded because of the `expand_parent`. if self .fmt .context() - .f_string_state() + .interpolated_string_state() .can_contain_line_breaks() == Some(false) { diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index 43ebbfa68e..23cc3ee996 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -314,15 +314,14 @@ fn handle_enclosed_comment<'a>( AnyNodeRef::StmtImportFrom(import_from) => handle_import_from_comment(comment, import_from), AnyNodeRef::StmtWith(with_) => handle_with_comment(comment, with_), AnyNodeRef::ExprCall(_) => handle_call_comment(comment), - AnyNodeRef::ExprStringLiteral(_) => { - if let Some(AnyNodeRef::FString(fstring)) = comment.enclosing_parent() { - CommentPlacement::dangling(fstring, comment) - } else { - CommentPlacement::Default(comment) - } - } + AnyNodeRef::ExprStringLiteral(_) => match comment.enclosing_parent() { + Some(AnyNodeRef::FString(fstring)) => CommentPlacement::dangling(fstring, comment), + Some(AnyNodeRef::TString(tstring)) => CommentPlacement::dangling(tstring, comment), + _ => CommentPlacement::Default(comment), + }, AnyNodeRef::FString(fstring) => CommentPlacement::dangling(fstring, comment), - AnyNodeRef::FStringExpressionElement(_) => { + AnyNodeRef::TString(tstring) => CommentPlacement::dangling(tstring, comment), + AnyNodeRef::InterpolatedElement(_) => { // Handle comments after the format specifier (should be rare): // // ```python @@ -336,7 +335,8 @@ fn handle_enclosed_comment<'a>( if matches!( comment.preceding_node(), Some( - AnyNodeRef::FStringExpressionElement(_) | AnyNodeRef::FStringLiteralElement(_) + AnyNodeRef::InterpolatedElement(_) + | AnyNodeRef::InterpolatedStringLiteralElement(_) ) ) { CommentPlacement::trailing(comment.enclosing_node(), comment) @@ -344,6 +344,7 @@ fn handle_enclosed_comment<'a>( handle_bracketed_end_of_line_comment(comment, source) } } + AnyNodeRef::ExprList(_) | AnyNodeRef::ExprSet(_) | AnyNodeRef::ExprListComp(_) diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 946deaf1a6..b1b3a7941a 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -7,7 +7,7 @@ use ruff_python_parser::Tokens; use crate::PyFormatOptions; use crate::comments::Comments; -use crate::other::f_string_element::FStringExpressionElementContext; +use crate::other::interpolated_string_element::InterpolatedElementContext; pub struct PyFormatContext<'a> { options: PyFormatOptions, @@ -25,8 +25,8 @@ pub struct PyFormatContext<'a> { /// quote style that is inverted from the one here in order to ensure that /// the formatted Python code will be valid. docstring: Option, - /// The state of the formatter with respect to f-strings. - f_string_state: FStringState, + /// The state of the formatter with respect to f-strings and t-strings. + interpolated_string_state: InterpolatedStringState, } impl<'a> PyFormatContext<'a> { @@ -44,7 +44,7 @@ impl<'a> PyFormatContext<'a> { node_level: NodeLevel::TopLevel(TopLevelStatementPosition::Other), indent_level: IndentLevel::new(0), docstring: None, - f_string_state: FStringState::Outside, + interpolated_string_state: InterpolatedStringState::Outside, } } @@ -97,12 +97,15 @@ impl<'a> PyFormatContext<'a> { } } - pub(crate) fn f_string_state(&self) -> FStringState { - self.f_string_state + pub(crate) fn interpolated_string_state(&self) -> InterpolatedStringState { + self.interpolated_string_state } - pub(crate) fn set_f_string_state(&mut self, f_string_state: FStringState) { - self.f_string_state = f_string_state; + pub(crate) fn set_interpolated_string_state( + &mut self, + interpolated_string_state: InterpolatedStringState, + ) { + self.interpolated_string_state = interpolated_string_state; } /// Returns `true` if preview mode is enabled. @@ -135,24 +138,24 @@ impl Debug for PyFormatContext<'_> { } #[derive(Clone, Copy, Debug, Default)] -pub(crate) enum FStringState { +pub(crate) enum InterpolatedStringState { /// The formatter is inside an f-string expression element i.e., between the /// curly brace in `f"foo {x}"`. /// /// The containing `FStringContext` is the surrounding f-string context. - InsideExpressionElement(FStringExpressionElementContext), + InsideInterpolatedElement(InterpolatedElementContext), /// The formatter is outside an f-string. #[default] Outside, } -impl FStringState { +impl InterpolatedStringState { pub(crate) fn can_contain_line_breaks(self) -> Option { match self { - FStringState::InsideExpressionElement(context) => { + InterpolatedStringState::InsideInterpolatedElement(context) => { Some(context.can_contain_line_breaks()) } - FStringState::Outside => None, + InterpolatedStringState::Outside => None, } } } @@ -375,25 +378,25 @@ where } } -pub(crate) struct WithFStringState<'a, B, D> +pub(crate) struct WithInterpolatedStringState<'a, B, D> where D: DerefMut, B: Buffer>, { buffer: D, - saved_location: FStringState, + saved_location: InterpolatedStringState, } -impl<'a, B, D> WithFStringState<'a, B, D> +impl<'a, B, D> WithInterpolatedStringState<'a, B, D> where D: DerefMut, B: Buffer>, { - pub(crate) fn new(expr_location: FStringState, mut buffer: D) -> Self { + pub(crate) fn new(expr_location: InterpolatedStringState, mut buffer: D) -> Self { let context = buffer.state_mut().context_mut(); - let saved_location = context.f_string_state(); + let saved_location = context.interpolated_string_state(); - context.set_f_string_state(expr_location); + context.set_interpolated_string_state(expr_location); Self { buffer, @@ -402,7 +405,7 @@ where } } -impl<'a, B, D> Deref for WithFStringState<'a, B, D> +impl<'a, B, D> Deref for WithInterpolatedStringState<'a, B, D> where D: DerefMut, B: Buffer>, @@ -414,7 +417,7 @@ where } } -impl<'a, B, D> DerefMut for WithFStringState<'a, B, D> +impl<'a, B, D> DerefMut for WithInterpolatedStringState<'a, B, D> where D: DerefMut, B: Buffer>, @@ -424,7 +427,7 @@ where } } -impl<'a, B, D> Drop for WithFStringState<'a, B, D> +impl<'a, B, D> Drop for WithInterpolatedStringState<'a, B, D> where D: DerefMut, B: Buffer>, @@ -433,6 +436,6 @@ where self.buffer .state_mut() .context_mut() - .set_f_string_state(self.saved_location); + .set_interpolated_string_state(self.saved_location); } } diff --git a/crates/ruff_python_formatter/src/expression/expr_f_string.rs b/crates/ruff_python_formatter/src/expression/expr_f_string.rs index 90db025694..ad559e102a 100644 --- a/crates/ruff_python_formatter/src/expression/expr_f_string.rs +++ b/crates/ruff_python_formatter/src/expression/expr_f_string.rs @@ -3,7 +3,7 @@ use ruff_python_ast::{AnyNodeRef, ExprFString, StringLike}; use crate::expression::parentheses::{ NeedsParentheses, OptionalParentheses, in_parentheses_only_group, }; -use crate::other::f_string::FStringLayout; +use crate::other::interpolated_string::InterpolatedStringLayout; use crate::prelude::*; use crate::string::StringLikeExtensions; use crate::string::implicit::{ @@ -41,7 +41,11 @@ impl NeedsParentheses for ExprFString { if let Some(fstring_part) = self.as_single_part_fstring() { // The f-string is not implicitly concatenated if StringLike::FString(self).is_multiline(context) - || FStringLayout::from_f_string(fstring_part, context.source()).is_multiline() + || InterpolatedStringLayout::from_interpolated_string_elements( + &fstring_part.elements, + context.source(), + ) + .is_multiline() { OptionalParentheses::Never } else { diff --git a/crates/ruff_python_formatter/src/expression/expr_t_string.rs b/crates/ruff_python_formatter/src/expression/expr_t_string.rs new file mode 100644 index 0000000000..d937338baf --- /dev/null +++ b/crates/ruff_python_formatter/src/expression/expr_t_string.rs @@ -0,0 +1,59 @@ +use ruff_python_ast::{AnyNodeRef, ExprTString, StringLike}; + +use crate::expression::parentheses::{ + NeedsParentheses, OptionalParentheses, in_parentheses_only_group, +}; +use crate::other::interpolated_string::InterpolatedStringLayout; +use crate::prelude::*; +use crate::string::StringLikeExtensions; +use crate::string::implicit::{ + FormatImplicitConcatenatedString, FormatImplicitConcatenatedStringFlat, +}; + +#[derive(Default)] +pub struct FormatExprTString; + +impl FormatNodeRule for FormatExprTString { + fn fmt_fields(&self, item: &ExprTString, f: &mut PyFormatter) -> FormatResult<()> { + if let Some(t_string) = item.as_single_part_tstring() { + t_string.format().fmt(f) + } else { + // Always join tstrings that aren't parenthesized and thus, are always on a single line. + if !f.context().node_level().is_parenthesized() { + if let Some(format_flat) = + FormatImplicitConcatenatedStringFlat::new(item.into(), f.context()) + { + return format_flat.fmt(f); + } + } + + in_parentheses_only_group(&FormatImplicitConcatenatedString::new(item)).fmt(f) + } + } +} + +impl NeedsParentheses for ExprTString { + fn needs_parentheses( + &self, + _parent: AnyNodeRef, + context: &PyFormatContext, + ) -> OptionalParentheses { + if let Some(tstring_part) = self.as_single_part_tstring() { + // The t-string is not implicitly concatenated + if StringLike::TString(self).is_multiline(context) + || InterpolatedStringLayout::from_interpolated_string_elements( + &tstring_part.elements, + context.source(), + ) + .is_multiline() + { + OptionalParentheses::Never + } else { + OptionalParentheses::BestFit + } + } else { + // The t-string is implicitly concatenated + OptionalParentheses::Multiline + } + } +} diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index aa5be13813..025c4e7513 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -50,6 +50,7 @@ pub(crate) mod expr_slice; pub(crate) mod expr_starred; pub(crate) mod expr_string_literal; pub(crate) mod expr_subscript; +pub(crate) mod expr_t_string; pub(crate) mod expr_tuple; pub(crate) mod expr_unary_op; pub(crate) mod expr_yield; @@ -94,6 +95,7 @@ impl FormatRule> for FormatExpr { Expr::Compare(expr) => expr.format().fmt(f), Expr::Call(expr) => expr.format().fmt(f), Expr::FString(expr) => expr.format().fmt(f), + Expr::TString(expr) => expr.format().fmt(f), Expr::StringLiteral(expr) => expr.format().fmt(f), Expr::BytesLiteral(expr) => expr.format().fmt(f), Expr::NumberLiteral(expr) => expr.format().fmt(f), @@ -282,6 +284,7 @@ fn format_with_parentheses_comments( Expr::Compare(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), Expr::Call(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), Expr::FString(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), + Expr::TString(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), Expr::StringLiteral(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), Expr::BytesLiteral(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), Expr::NumberLiteral(expr) => FormatNodeRule::fmt_fields(expr.format().rule(), expr, f), @@ -480,6 +483,7 @@ impl NeedsParentheses for Expr { Expr::Compare(expr) => expr.needs_parentheses(parent, context), Expr::Call(expr) => expr.needs_parentheses(parent, context), Expr::FString(expr) => expr.needs_parentheses(parent, context), + Expr::TString(expr) => expr.needs_parentheses(parent, context), Expr::StringLiteral(expr) => expr.needs_parentheses(parent, context), Expr::BytesLiteral(expr) => expr.needs_parentheses(parent, context), Expr::NumberLiteral(expr) => expr.needs_parentheses(parent, context), @@ -775,6 +779,7 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> { // Terminal nodes or nodes that wrap a sub-expression (where the sub expression can never be at the end). Expr::FString(_) + | Expr::TString(_) | Expr::StringLiteral(_) | Expr::BytesLiteral(_) | Expr::NumberLiteral(_) @@ -1126,6 +1131,7 @@ pub(crate) fn is_expression_huggable(expr: &Expr, context: &PyFormatContext) -> | Expr::StringLiteral(_) | Expr::BytesLiteral(_) | Expr::FString(_) + | Expr::TString(_) | Expr::EllipsisLiteral(_) => false, } } @@ -1221,6 +1227,7 @@ pub(crate) fn is_splittable_expression(expr: &Expr, context: &PyFormatContext) - // String like literals can expand if they are implicit concatenated. Expr::FString(fstring) => fstring.value.is_implicit_concatenated(), + Expr::TString(tstring) => tstring.value.is_implicit_concatenated(), Expr::StringLiteral(string) => string.value.is_implicit_concatenated(), Expr::BytesLiteral(bytes) => bytes.value.is_implicit_concatenated(), @@ -1278,6 +1285,7 @@ pub(crate) fn left_most<'expr>( | Expr::Name(_) | Expr::Starred(_) | Expr::FString(_) + | Expr::TString(_) | Expr::StringLiteral(_) | Expr::BytesLiteral(_) | Expr::NumberLiteral(_) diff --git a/crates/ruff_python_formatter/src/generated.rs b/crates/ruff_python_formatter/src/generated.rs index 9bb79d80ac..3abc77a538 100644 --- a/crates/ruff_python_formatter/src/generated.rs +++ b/crates/ruff_python_formatter/src/generated.rs @@ -1562,6 +1562,42 @@ impl<'ast> IntoFormat> for ast::ExprFString { } } +impl FormatRule> + for crate::expression::expr_t_string::FormatExprTString +{ + #[inline] + fn fmt(&self, node: &ast::ExprTString, f: &mut PyFormatter) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> for ast::ExprTString { + type Format<'a> = FormatRefWithRule< + 'a, + ast::ExprTString, + crate::expression::expr_t_string::FormatExprTString, + PyFormatContext<'ast>, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::expression::expr_t_string::FormatExprTString::default(), + ) + } +} +impl<'ast> IntoFormat> for ast::ExprTString { + type Format = FormatOwnedWithRule< + ast::ExprTString, + crate::expression::expr_t_string::FormatExprTString, + PyFormatContext<'ast>, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::expression::expr_t_string::FormatExprTString::default(), + ) + } +} + impl FormatRule> for crate::expression::expr_string_literal::FormatExprStringLiteral { @@ -2963,6 +2999,34 @@ impl<'ast> IntoFormat> for ast::FString { } } +impl FormatRule> for crate::other::t_string::FormatTString { + #[inline] + fn fmt(&self, node: &ast::TString, f: &mut PyFormatter) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl<'ast> AsFormat> for ast::TString { + type Format<'a> = FormatRefWithRule< + 'a, + ast::TString, + crate::other::t_string::FormatTString, + PyFormatContext<'ast>, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new(self, crate::other::t_string::FormatTString::default()) + } +} +impl<'ast> IntoFormat> for ast::TString { + type Format = FormatOwnedWithRule< + ast::TString, + crate::other::t_string::FormatTString, + PyFormatContext<'ast>, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new(self, crate::other::t_string::FormatTString::default()) + } +} + impl FormatRule> for crate::other::string_literal::FormatStringLiteral { diff --git a/crates/ruff_python_formatter/src/other/f_string.rs b/crates/ruff_python_formatter/src/other/f_string.rs index c423f39f34..a02cd4a086 100644 --- a/crates/ruff_python_formatter/src/other/f_string.rs +++ b/crates/ruff_python_formatter/src/other/f_string.rs @@ -1,12 +1,9 @@ -use ruff_formatter::write; -use ruff_python_ast::{AnyStringFlags, FString, StringFlags}; -use ruff_source_file::LineRanges; -use ruff_text_size::Ranged; - +use super::interpolated_string_element::FormatInterpolatedStringElement; +use crate::other::interpolated_string::{InterpolatedStringContext, InterpolatedStringLayout}; use crate::prelude::*; use crate::string::{StringNormalizer, StringQuotes}; - -use super::f_string_element::FormatFStringElement; +use ruff_formatter::write; +use ruff_python_ast::{FString, StringFlags}; /// Formats an f-string which is part of a larger f-string expression. /// @@ -21,9 +18,12 @@ impl FormatNodeRule for FormatFString { let string_kind = normalizer.choose_quotes(item.into()).flags(); - let context = FStringContext::new( + let context = InterpolatedStringContext::new( string_kind, - FStringLayout::from_f_string(item, f.context().source()), + InterpolatedStringLayout::from_interpolated_string_elements( + &item.elements, + f.context().source(), + ), ); // Starting prefix and quote @@ -31,78 +31,10 @@ impl FormatNodeRule for FormatFString { write!(f, [string_kind.prefix(), quotes])?; for element in &item.elements { - FormatFStringElement::new(element, context).fmt(f)?; + FormatInterpolatedStringElement::new(element, context).fmt(f)?; } // Ending quote quotes.fmt(f) } } - -#[derive(Clone, Copy, Debug)] -pub(crate) struct FStringContext { - /// The string flags of the enclosing f-string part. - enclosing_flags: AnyStringFlags, - layout: FStringLayout, -} - -impl FStringContext { - pub(crate) const fn new(flags: AnyStringFlags, layout: FStringLayout) -> Self { - Self { - enclosing_flags: flags, - layout, - } - } - - pub(crate) fn flags(self) -> AnyStringFlags { - self.enclosing_flags - } - - pub(crate) const fn layout(self) -> FStringLayout { - self.layout - } -} - -#[derive(Copy, Clone, Debug)] -pub(crate) enum FStringLayout { - /// Original f-string is flat. - /// Don't break expressions to keep the string flat. - Flat, - /// Original f-string has multiline expressions in the replacement fields. - /// Allow breaking expressions across multiple lines. - Multiline, -} - -impl FStringLayout { - pub(crate) fn from_f_string(f_string: &FString, source: &str) -> Self { - // Heuristic: Allow breaking the f-string expressions across multiple lines - // only if there already is at least one multiline expression. This puts the - // control in the hands of the user to decide if they want to break the - // f-string expressions across multiple lines or not. This is similar to - // how Prettier does it for template literals in JavaScript. - // - // If it's single quoted f-string and it contains a multiline expression, then we - // assume that the target version of Python supports it (3.12+). If there are comments - // used in any of the expression of the f-string, then it's always going to be multiline - // and we assume that the target version of Python supports it (3.12+). - // - // Reference: https://prettier.io/docs/en/next/rationale.html#template-literals - if f_string - .elements - .expressions() - .any(|expr| source.contains_line_break(expr.range())) - { - Self::Multiline - } else { - Self::Flat - } - } - - pub(crate) const fn is_flat(self) -> bool { - matches!(self, FStringLayout::Flat) - } - - pub(crate) const fn is_multiline(self) -> bool { - matches!(self, FStringLayout::Multiline) - } -} diff --git a/crates/ruff_python_formatter/src/other/interpolated_string.rs b/crates/ruff_python_formatter/src/other/interpolated_string.rs new file mode 100644 index 0000000000..7a0c8b3c1c --- /dev/null +++ b/crates/ruff_python_formatter/src/other/interpolated_string.rs @@ -0,0 +1,73 @@ +use ruff_python_ast::{AnyStringFlags, InterpolatedStringElements}; +use ruff_source_file::LineRanges; +use ruff_text_size::Ranged; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct InterpolatedStringContext { + /// The string flags of the enclosing f/t-string part. + enclosing_flags: AnyStringFlags, + layout: InterpolatedStringLayout, +} + +impl InterpolatedStringContext { + pub(crate) const fn new(flags: AnyStringFlags, layout: InterpolatedStringLayout) -> Self { + Self { + enclosing_flags: flags, + layout, + } + } + + pub(crate) fn flags(self) -> AnyStringFlags { + self.enclosing_flags + } + + pub(crate) const fn layout(self) -> InterpolatedStringLayout { + self.layout + } +} + +#[derive(Copy, Clone, Debug)] +pub(crate) enum InterpolatedStringLayout { + /// Original f/t-string is flat. + /// Don't break expressions to keep the string flat. + Flat, + /// Original f/t-string has multiline expressions in the replacement fields. + /// Allow breaking expressions across multiple lines. + Multiline, +} + +impl InterpolatedStringLayout { + // Heuristic: Allow breaking the f/t-string expressions across multiple lines + // only if there already is at least one multiline expression. This puts the + // control in the hands of the user to decide if they want to break the + // f/t-string expressions across multiple lines or not. This is similar to + // how Prettier does it for template literals in JavaScript. + // + // If it's single quoted f-string and it contains a multiline expression, then we + // assume that the target version of Python supports it (3.12+). If there are comments + // used in any of the expression of the f-string, then it's always going to be multiline + // and we assume that the target version of Python supports it (3.12+). + // + // Reference: https://prettier.io/docs/en/next/rationale.html#template-literals + pub(crate) fn from_interpolated_string_elements( + elements: &InterpolatedStringElements, + source: &str, + ) -> Self { + if elements + .interpolations() + .any(|expr| source.contains_line_break(expr.range())) + { + Self::Multiline + } else { + Self::Flat + } + } + + pub(crate) const fn is_flat(self) -> bool { + matches!(self, InterpolatedStringLayout::Flat) + } + + pub(crate) const fn is_multiline(self) -> bool { + matches!(self, InterpolatedStringLayout::Multiline) + } +} diff --git a/crates/ruff_python_formatter/src/other/f_string_element.rs b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs similarity index 80% rename from crates/ruff_python_formatter/src/other/f_string_element.rs rename to crates/ruff_python_formatter/src/other/interpolated_string_element.rs index 8418d5edf0..e0b53331ea 100644 --- a/crates/ruff_python_formatter/src/other/f_string_element.rs +++ b/crates/ruff_python_formatter/src/other/interpolated_string_element.rs @@ -2,42 +2,47 @@ use std::borrow::Cow; use ruff_formatter::{Buffer, RemoveSoftLinesBuffer, format_args, write}; use ruff_python_ast::{ - AnyStringFlags, ConversionFlag, Expr, FStringElement, FStringExpressionElement, - FStringLiteralElement, StringFlags, + AnyStringFlags, ConversionFlag, Expr, InterpolatedElement, InterpolatedStringElement, + InterpolatedStringLiteralElement, StringFlags, }; use ruff_text_size::{Ranged, TextSlice}; use crate::comments::{dangling_open_parenthesis_comments, trailing_comments}; -use crate::context::{FStringState, NodeLevel, WithFStringState, WithNodeLevel}; +use crate::context::{ + InterpolatedStringState, NodeLevel, WithInterpolatedStringState, WithNodeLevel, +}; use crate::expression::left_most; use crate::prelude::*; use crate::string::normalize_string; use crate::verbatim::verbatim_text; -use super::f_string::FStringContext; +use super::interpolated_string::InterpolatedStringContext; /// Formats an f-string element which is either a literal or a formatted expression. /// /// This delegates the actual formatting to the appropriate formatter. -pub(crate) struct FormatFStringElement<'a> { - element: &'a FStringElement, - context: FStringContext, +pub(crate) struct FormatInterpolatedStringElement<'a> { + element: &'a InterpolatedStringElement, + context: InterpolatedStringContext, } -impl<'a> FormatFStringElement<'a> { - pub(crate) fn new(element: &'a FStringElement, context: FStringContext) -> Self { +impl<'a> FormatInterpolatedStringElement<'a> { + pub(crate) fn new( + element: &'a InterpolatedStringElement, + context: InterpolatedStringContext, + ) -> Self { Self { element, context } } } -impl Format> for FormatFStringElement<'_> { +impl Format> for FormatInterpolatedStringElement<'_> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { match self.element { - FStringElement::Literal(string_literal) => { + InterpolatedStringElement::Literal(string_literal) => { FormatFStringLiteralElement::new(string_literal, self.context.flags()).fmt(f) } - FStringElement::Expression(expression) => { - FormatFStringExpressionElement::new(expression, self.context).fmt(f) + InterpolatedStringElement::Interpolation(expression) => { + FormatInterpolatedElement::new(expression, self.context).fmt(f) } } } @@ -45,13 +50,16 @@ impl Format> for FormatFStringElement<'_> { /// Formats an f-string literal element. pub(crate) struct FormatFStringLiteralElement<'a> { - element: &'a FStringLiteralElement, + element: &'a InterpolatedStringLiteralElement, /// Flags of the enclosing F-string part fstring_flags: AnyStringFlags, } impl<'a> FormatFStringLiteralElement<'a> { - pub(crate) fn new(element: &'a FStringLiteralElement, fstring_flags: AnyStringFlags) -> Self { + pub(crate) fn new( + element: &'a InterpolatedStringLiteralElement, + fstring_flags: AnyStringFlags, + ) -> Self { Self { element, fstring_flags, @@ -72,16 +80,16 @@ impl Format> for FormatFStringLiteralElement<'_> { /// Context representing an f-string expression element. #[derive(Clone, Copy, Debug)] -pub(crate) struct FStringExpressionElementContext { +pub(crate) struct InterpolatedElementContext { /// The context of the parent f-string containing this expression element. - parent_context: FStringContext, + parent_context: InterpolatedStringContext, /// Indicates whether this expression element has format specifier or not. has_format_spec: bool, } -impl FStringExpressionElementContext { - /// Returns the [`FStringContext`] containing this expression element. - pub(crate) fn f_string(self) -> FStringContext { +impl InterpolatedElementContext { + /// Returns the [`InterpolatedStringContext`] containing this expression element. + pub(crate) fn interpolated_string(self) -> InterpolatedStringContext { self.parent_context } @@ -113,16 +121,19 @@ impl FStringExpressionElementContext { } /// Formats an f-string expression element. -pub(crate) struct FormatFStringExpressionElement<'a> { - element: &'a FStringExpressionElement, - context: FStringExpressionElementContext, +pub(crate) struct FormatInterpolatedElement<'a> { + element: &'a InterpolatedElement, + context: InterpolatedElementContext, } -impl<'a> FormatFStringExpressionElement<'a> { - pub(crate) fn new(element: &'a FStringExpressionElement, context: FStringContext) -> Self { +impl<'a> FormatInterpolatedElement<'a> { + pub(crate) fn new( + element: &'a InterpolatedElement, + context: InterpolatedStringContext, + ) -> Self { Self { element, - context: FStringExpressionElementContext { + context: InterpolatedElementContext { parent_context: context, has_format_spec: element.format_spec.is_some(), }, @@ -130,9 +141,9 @@ impl<'a> FormatFStringExpressionElement<'a> { } } -impl Format> for FormatFStringExpressionElement<'_> { +impl Format> for FormatInterpolatedElement<'_> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { - let FStringExpressionElement { + let InterpolatedElement { expression, debug_text, conversion, @@ -214,8 +225,8 @@ impl Format> for FormatFStringExpressionElement<'_> { let item = format_with(|f: &mut PyFormatter| { // Update the context to be inside the f-string expression element. - let f = &mut WithFStringState::new( - FStringState::InsideExpressionElement(self.context), + let f = &mut WithInterpolatedStringState::new( + InterpolatedStringState::InsideInterpolatedElement(self.context), f, ); @@ -233,7 +244,11 @@ impl Format> for FormatFStringExpressionElement<'_> { token(":").fmt(f)?; for element in &format_spec.elements { - FormatFStringElement::new(element, self.context.f_string()).fmt(f)?; + FormatInterpolatedStringElement::new( + element, + self.context.interpolated_string(), + ) + .fmt(f)?; } // These trailing comments can only occur if the format specifier is diff --git a/crates/ruff_python_formatter/src/other/mod.rs b/crates/ruff_python_formatter/src/other/mod.rs index b55b1a70f6..33f14c4f7b 100644 --- a/crates/ruff_python_formatter/src/other/mod.rs +++ b/crates/ruff_python_formatter/src/other/mod.rs @@ -7,12 +7,14 @@ pub(crate) mod decorator; pub(crate) mod elif_else_clause; pub(crate) mod except_handler_except_handler; pub(crate) mod f_string; -pub(crate) mod f_string_element; pub(crate) mod identifier; +pub(crate) mod interpolated_string; +pub(crate) mod interpolated_string_element; pub(crate) mod keyword; pub(crate) mod match_case; pub(crate) mod parameter; pub(crate) mod parameter_with_default; pub(crate) mod parameters; pub(crate) mod string_literal; +pub(crate) mod t_string; pub(crate) mod with_item; diff --git a/crates/ruff_python_formatter/src/other/t_string.rs b/crates/ruff_python_formatter/src/other/t_string.rs new file mode 100644 index 0000000000..098c668c51 --- /dev/null +++ b/crates/ruff_python_formatter/src/other/t_string.rs @@ -0,0 +1,40 @@ +use super::interpolated_string_element::FormatInterpolatedStringElement; +use crate::other::interpolated_string::{InterpolatedStringContext, InterpolatedStringLayout}; +use crate::prelude::*; +use crate::string::{StringNormalizer, StringQuotes}; +use ruff_formatter::write; +use ruff_python_ast::{StringFlags, TString}; + +/// Formats a t-string which is part of a larger t-string expression. +/// +/// For example, this would be used to format the t-string part in `"foo" t"bar {x}"` +/// or the standalone t-string in `t"foo {x} bar"`. +#[derive(Default)] +pub struct FormatTString; + +impl FormatNodeRule for FormatTString { + fn fmt_fields(&self, item: &TString, f: &mut PyFormatter) -> FormatResult<()> { + let normalizer = StringNormalizer::from_context(f.context()); + + let string_kind = normalizer.choose_quotes(item.into()).flags(); + + let context = InterpolatedStringContext::new( + string_kind, + InterpolatedStringLayout::from_interpolated_string_elements( + &item.elements, + f.context().source(), + ), + ); + + // Starting prefix and quote + let quotes = StringQuotes::from(string_kind); + write!(f, [string_kind.prefix(), quotes])?; + + for element in &item.elements { + FormatInterpolatedStringElement::new(element, context).fmt(f)?; + } + + // Ending quote + quotes.fmt(f) + } +} diff --git a/crates/ruff_python_formatter/src/pattern/mod.rs b/crates/ruff_python_formatter/src/pattern/mod.rs index fa197378a1..e255d59359 100644 --- a/crates/ruff_python_formatter/src/pattern/mod.rs +++ b/crates/ruff_python_formatter/src/pattern/mod.rs @@ -293,6 +293,7 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> { // F-strings are allowed according to python's grammar but fail with a syntax error at runtime. // That's why we need to support them for formatting. Expr::FString(_) | + Expr::TString(_)| Expr::NumberLiteral(_) | Expr::Attribute(_) | Expr::UnaryOp(_) => { // require no state update other than visit_pattern does. } @@ -306,7 +307,7 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> { _ => { debug_assert!( false, - "Unsupported expression in pattern mach value: {:?}", + "Unsupported expression in pattern match value: {:?}", value.value ); } diff --git a/crates/ruff_python_formatter/src/range.rs b/crates/ruff_python_formatter/src/range.rs index 2f2f12644a..61043061b5 100644 --- a/crates/ruff_python_formatter/src/range.rs +++ b/crates/ruff_python_formatter/src/range.rs @@ -659,10 +659,11 @@ impl Format> for FormatEnclosingNode<'_> { | AnyNodeRef::ExprYieldFrom(_) | AnyNodeRef::ExprCompare(_) | AnyNodeRef::ExprCall(_) - | AnyNodeRef::FStringExpressionElement(_) - | AnyNodeRef::FStringLiteralElement(_) - | AnyNodeRef::FStringFormatSpec(_) + | AnyNodeRef::InterpolatedElement(_) + | AnyNodeRef::InterpolatedStringLiteralElement(_) + | AnyNodeRef::InterpolatedStringFormatSpec(_) | AnyNodeRef::ExprFString(_) + | AnyNodeRef::ExprTString(_) | AnyNodeRef::ExprStringLiteral(_) | AnyNodeRef::ExprBytesLiteral(_) | AnyNodeRef::ExprNumberLiteral(_) @@ -679,6 +680,7 @@ impl Format> for FormatEnclosingNode<'_> { | AnyNodeRef::ExprIpyEscapeCommand(_) | AnyNodeRef::FString(_) | AnyNodeRef::StringLiteral(_) + | AnyNodeRef::TString(_) | AnyNodeRef::PatternMatchValue(_) | AnyNodeRef::PatternMatchSingleton(_) | AnyNodeRef::PatternMatchSequence(_) diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index 4aad6a47a2..54a2eec8bb 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -1,6 +1,6 @@ use ruff_formatter::{FormatError, RemoveSoftLinesBuffer, format_args, write}; use ruff_python_ast::{ - AnyNodeRef, Expr, ExprAttribute, ExprCall, FString, Operator, StmtAssign, StringLike, + AnyNodeRef, Expr, ExprAttribute, ExprCall, FString, Operator, StmtAssign, StringLike, TString, TypeParams, }; @@ -17,7 +17,7 @@ use crate::expression::{ can_omit_optional_parentheses, has_own_parentheses, has_parentheses, maybe_parenthesize_expression, }; -use crate::other::f_string::FStringLayout; +use crate::other::interpolated_string::InterpolatedStringLayout; use crate::statement::trailing_semicolon; use crate::string::StringLikeExtensions; use crate::string::implicit::{ @@ -291,15 +291,16 @@ impl Format> for FormatStatementsLastExpression<'_> { let can_inline_comment = should_inline_comments(value, *statement, f.context()); let string_like = StringLike::try_from(*value).ok(); - let format_f_string = - string_like.and_then(|string| format_f_string_assignment(string, f.context())); + let format_interpolated_string = string_like + .and_then(|string| format_interpolated_string_assignment(string, f.context())); + let format_implicit_flat = string_like.and_then(|string| { FormatImplicitConcatenatedStringFlat::new(string, f.context()) }); if !can_inline_comment && format_implicit_flat.is_none() - && format_f_string.is_none() + && format_interpolated_string.is_none() { return maybe_parenthesize_expression( value, @@ -351,7 +352,7 @@ impl Format> for FormatStatementsLastExpression<'_> { let string = flat.string(); let flat = format_with(|f| { - if string.is_fstring() { + if string.is_interpolated_string() { let mut buffer = RemoveSoftLinesBuffer::new(&mut *f); write!(buffer, [flat]) @@ -361,7 +362,7 @@ impl Format> for FormatStatementsLastExpression<'_> { }) .memoized(); - // F-String containing an expression with a magic trailing comma, a comment, or a + // F-string or T-string containing an expression with a magic trailing comma, a comment, or a // multiline debug expression should never be joined. Use the default layout. // ```python // aaaa = f"abcd{[ @@ -369,7 +370,7 @@ impl Format> for FormatStatementsLastExpression<'_> { // 2, // ]}" "more" // ``` - if string.is_fstring() && flat.inspect(f)?.will_break() { + if string.is_interpolated_string() && flat.inspect(f)?.will_break() { inline_comments.mark_unformatted(); return write!( @@ -446,24 +447,23 @@ impl Format> for FormatStatementsLastExpression<'_> { best_fitting![single_line, joined_parenthesized, implicit_expanded] .with_mode(BestFittingMode::AllLines) .fmt(f)?; - } else if let Some(format_f_string) = format_f_string { + } else if let Some(format_interpolated_string) = format_interpolated_string { inline_comments.mark_formatted(); - let f_string_flat = format_with(|f| { + let interpolated_string_flat = format_with(|f| { let mut buffer = RemoveSoftLinesBuffer::new(&mut *f); - write!(buffer, [format_f_string.format()]) + write!(buffer, [format_interpolated_string]) }) .memoized(); - - // F-String containing an expression with a magic trailing comma, a comment, or a - // multiline debug expression should never be joined. Use the default layout. + // F/T-String containing an interpolation with a magic trailing comma, a comment, or a + // multiline debug interpolation should never be joined. Use the default layout. // ```python // aaaa = f"aaaa {[ // 1, 2, // ]} bbbb" // ``` - if f_string_flat.inspect(f)?.will_break() { + if interpolated_string_flat.inspect(f)?.will_break() { inline_comments.mark_unformatted(); return write!( @@ -482,23 +482,26 @@ impl Format> for FormatStatementsLastExpression<'_> { // expression}moreeeeeeeeeeeeeeeee" // ``` - // Flatten the f-string. + // Flatten the f/t-string. // ```python // aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" // ``` let single_line = - format_with(|f| write!(f, [f_string_flat, inline_comments])); + format_with(|f| write!(f, [interpolated_string_flat, inline_comments])); - // Parenthesize the f-string and flatten the f-string. + // Parenthesize the t-string and flatten the t-string. // ```python // aaaaaaaaaaaaaaaaaa = ( - // f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" + // t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" // ) // ``` let joined_parenthesized = format_with(|f| { group(&format_args![ token("("), - soft_block_indent(&format_args![f_string_flat, inline_comments]), + soft_block_indent(&format_args![ + interpolated_string_flat, + inline_comments + ]), token(")"), ]) .with_id(Some(group_id)) @@ -506,19 +509,24 @@ impl Format> for FormatStatementsLastExpression<'_> { .fmt(f) }); - // Avoid flattening or parenthesizing the f-string, keep the original - // f-string formatting. + // Avoid flattening or parenthesizing the f/t-string, keep the original + // f/t-string formatting. // ```python - // aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + // aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ // expression // }moreeeeeeeeeeeeeeeee" // ``` - let format_f_string = - format_with(|f| write!(f, [format_f_string.format(), inline_comments])); + let format_interpolated_string = format_with(|f| { + write!(f, [format_interpolated_string, inline_comments]) + }); - best_fitting![single_line, joined_parenthesized, format_f_string] - .with_mode(BestFittingMode::AllLines) - .fmt(f)?; + best_fitting![ + single_line, + joined_parenthesized, + format_interpolated_string + ] + .with_mode(BestFittingMode::AllLines) + .fmt(f)?; } else { best_fit_parenthesize(&format_once(|f| { inline_comments.mark_formatted(); @@ -559,17 +567,16 @@ impl Format> for FormatStatementsLastExpression<'_> { let should_inline_comments = should_inline_comments(value, *statement, f.context()); let string_like = StringLike::try_from(*value).ok(); - let format_f_string = - string_like.and_then(|string| format_f_string_assignment(string, f.context())); + let format_interpolated_string = string_like + .and_then(|string| format_interpolated_string_assignment(string, f.context())); let format_implicit_flat = string_like.and_then(|string| { FormatImplicitConcatenatedStringFlat::new(string, f.context()) }); - // Use the normal `maybe_parenthesize_layout` for splittable `value`s. if !should_inline_comments && !should_non_inlineable_use_best_fit(value, *statement, f.context()) && format_implicit_flat.is_none() - && format_f_string.is_none() + && format_interpolated_string.is_none() { return write!( f, @@ -593,7 +600,7 @@ impl Format> for FormatStatementsLastExpression<'_> { // Don't inline comments for attribute and call expressions for black compatibility let inline_comments = if should_inline_comments || format_implicit_flat.is_some() - || format_f_string.is_some() + || format_interpolated_string.is_some() { OptionalParenthesesInlinedComments::new( &expression_comments, @@ -633,7 +640,9 @@ impl Format> for FormatStatementsLastExpression<'_> { // This is mainly a performance optimisation that avoids unnecessary memoization // and using the costly `BestFitting` layout if it is already known that only the last variant // can ever fit because the left breaks. - if format_implicit_flat.is_none() && format_f_string.is_none() && last_target_breaks + if format_implicit_flat.is_none() + && format_interpolated_string.is_none() + && last_target_breaks { return write!( f, @@ -650,7 +659,7 @@ impl Format> for FormatStatementsLastExpression<'_> { let format_value = format_with(|f| { if let Some(format_implicit_flat) = format_implicit_flat.as_ref() { - if format_implicit_flat.string().is_fstring() { + if format_implicit_flat.string().is_interpolated_string() { // Remove any soft line breaks emitted by the f-string formatting. // This is important when formatting f-strings as part of an assignment right side // because `best_fit_parenthesize` will otherwise still try to break inner @@ -660,11 +669,13 @@ impl Format> for FormatStatementsLastExpression<'_> { } else { format_implicit_flat.fmt(f) } - } else if let Some(format_f_string) = format_f_string.as_ref() { + } else if let Some(format_interpolated_string) = + format_interpolated_string.as_ref() + { // Similar to above, remove any soft line breaks emitted by the f-string // formatting. let mut buffer = RemoveSoftLinesBuffer::new(&mut *f); - write!(buffer, [format_f_string.format()]) + write!(buffer, [format_interpolated_string]) } else { value.format().with_options(Parentheses::Never).fmt(f) } @@ -766,7 +777,7 @@ impl Format> for FormatStatementsLastExpression<'_> { // 2, // ]}" "more" // ``` - if format_implicit_flat.string().is_fstring() + if format_implicit_flat.string().is_interpolated_string() && format_value.inspect(f)?.will_break() { inline_comments.mark_unformatted(); @@ -905,12 +916,12 @@ impl Format> for FormatStatementsLastExpression<'_> { .with_mode(BestFittingMode::AllLines) .fmt(f) } - } else if let Some(format_f_string) = &format_f_string { - // F-String containing an expression with a magic trailing comma, a comment, or a + } else if let Some(format_interpolated_string) = &format_interpolated_string { + // F/T-String containing an interpolation with a magic trailing comma, a comment, or a // multiline debug expression should never be joined. Use the default layout. // // ```python - // aaaa, bbbb = f"aaaa {[ + // aaaa, bbbb = t"aaaa {[ // 1, 2, // ]} bbbb" // ``` @@ -933,40 +944,46 @@ impl Format> for FormatStatementsLastExpression<'_> { ); } - let format_f_string = - format_with(|f| write!(f, [format_f_string.format(), inline_comments])) + let format_interpolated_string = + format_with(|f| write!(f, [format_interpolated_string, inline_comments])) .memoized(); // Considering the following initial source: // // ```python // aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( - // f"aaaaaaaaaaaaaaaaaaa { + // t"aaaaaaaaaaaaaaaaaaa { // aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" // ) // ``` // - // Keep the target flat, and use the regular f-string formatting. + // Keep the target flat, and use the regular f/t-string formatting. // // ```python - // aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = f"aaaaaaaaaaaaaaaaaaa { + // aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa { // aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc // } ddddddddddddddddddd" // ``` - let flat_target_regular_f_string = format_with(|f| { + let flat_target_regular_interpolated_string = format_with(|f| { write!( f, - [last_target, space(), operator, space(), format_f_string] + [ + last_target, + space(), + operator, + space(), + format_interpolated_string + ] ) }); - // Expand the parent and parenthesize the flattened f-string. + // Expand the parent and parenthesize the flattened f/t-string. // // ```python // aaaaaaaaaaaa[ // "bbbbbbbbbbbbbbbb" // ] = ( - // f"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" + // t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" // ) // ``` let split_target_value_parenthesized_flat = format_with(|f| { @@ -988,16 +1005,16 @@ impl Format> for FormatStatementsLastExpression<'_> { ) }); - // Expand the parent, and use the regular f-string formatting. + // Expand the parent, and use the regular f/t-string formatting. // // ```python // aaaaaaaaaaaa[ // "bbbbbbbbbbbbbbbb" - // ] = f"aaaaaaaaaaaaaaaaaaa { + // ] = t"aaaaaaaaaaaaaaaaaaa { // aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc // } ddddddddddddddddddd" // ``` - let split_target_regular_f_string = format_with(|f| { + let split_target_regular_interpolated_string = format_with(|f| { write!( f, [ @@ -1005,7 +1022,7 @@ impl Format> for FormatStatementsLastExpression<'_> { space(), operator, space(), - format_f_string, + format_interpolated_string, ] ) }); @@ -1016,7 +1033,7 @@ impl Format> for FormatStatementsLastExpression<'_> { best_fitting![ split_target_flat_value, split_target_value_parenthesized_flat, - split_target_regular_f_string, + split_target_regular_interpolated_string, ] .with_mode(BestFittingMode::AllLines) .fmt(f) @@ -1024,10 +1041,10 @@ impl Format> for FormatStatementsLastExpression<'_> { best_fitting![ single_line, flat_target_parenthesize_value, - flat_target_regular_f_string, + flat_target_regular_interpolated_string, split_target_flat_value, split_target_value_parenthesized_flat, - split_target_regular_f_string, + split_target_regular_interpolated_string, ] .with_mode(BestFittingMode::AllLines) .fmt(f) @@ -1045,13 +1062,31 @@ impl Format> for FormatStatementsLastExpression<'_> { } } -/// Formats an f-string that is at the value position of an assignment statement. +#[derive(Debug, Copy, Clone)] +enum InterpolatedString<'a> { + FString(&'a FString), + TString(&'a TString), +} + +impl Format> for InterpolatedString<'_> { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { + match self { + InterpolatedString::FString(string) => string.format().fmt(f), + InterpolatedString::TString(string) => string.format().fmt(f), + } + } +} + +/// Formats an f/t-string that is at the value position of an assignment statement. /// -/// This is just a wrapper around [`FormatFString`] while considering a special case when the -/// f-string is at an assignment statement's value position. +/// For legibility, we discuss only the case of f-strings below, but the +/// same comments apply to t-strings. /// -/// This is necessary to prevent an instability where an f-string contains a multiline expression -/// and the f-string fits on the line, but only when it's surrounded by parentheses. +/// This is just a wrapper around [`FormatFString`] while considering a special +/// case when the f-string is at an assignment statement's value position. +/// This is necessary to prevent an instability where an f-string contains a +/// multiline expression and the f-string fits on the line, but only when it's +/// surrounded by parentheses. /// /// ```python /// aaaaaaaaaaaaaaaaaa = f"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ @@ -1099,30 +1134,40 @@ impl Format> for FormatStatementsLastExpression<'_> { /// The reason for this is because (a) f-string already has a multiline expression thus it tries to /// break the expression and (b) the `BestFit` layout doesn't considers the layout where the /// multiline f-string isn't surrounded by parentheses. -fn format_f_string_assignment<'a>( +fn format_interpolated_string_assignment<'a>( string: StringLike<'a>, context: &PyFormatContext, -) -> Option<&'a FString> { - let StringLike::FString(expr) = string else { - return None; +) -> Option> { + let (interpolated_string, elements) = match string { + StringLike::TString(expr) => { + let t_string = expr.as_single_part_tstring()?; + (InterpolatedString::TString(t_string), &t_string.elements) + } + StringLike::FString(expr) => { + let f_string = expr.as_single_part_fstring()?; + (InterpolatedString::FString(f_string), &f_string.elements) + } + _ => { + return None; + } }; - let f_string = expr.as_single_part_fstring()?; - - // If the f-string is flat, there are no breakpoints from which it can be made multiline. - // This is the case when the f-string has no expressions or if it does then the expressions + // If the f/t-string is flat, there are no breakpoints from which it can be made multiline. + // This is the case when the f/t-string has no expressions or if it does then the expressions // are flat (no newlines). - if FStringLayout::from_f_string(f_string, context.source()).is_flat() { + if InterpolatedStringLayout::from_interpolated_string_elements(elements, context.source()) + .is_flat() + { return None; } - // This checks whether the f-string is multi-line and it can *never* be flattened. Thus, + // This checks whether the f/t-string is multi-line and it can *never* be flattened. Thus, // it's useless to try the flattened layout. if string.is_multiline(context) { return None; } - Some(f_string) + Some(interpolated_string) } #[derive(Debug, Default)] @@ -1277,6 +1322,9 @@ fn should_inline_comments( Expr::FString(fstring) => { fstring.needs_parentheses(parent, context) == OptionalParentheses::BestFit } + Expr::TString(tstring) => { + tstring.needs_parentheses(parent, context) == OptionalParentheses::BestFit + } _ => false, } } diff --git a/crates/ruff_python_formatter/src/string/implicit.rs b/crates/ruff_python_formatter/src/string/implicit.rs index fd6145c033..fc25d030aa 100644 --- a/crates/ruff_python_formatter/src/string/implicit.rs +++ b/crates/ruff_python_formatter/src/string/implicit.rs @@ -2,28 +2,31 @@ use itertools::Itertools; use ruff_formatter::{FormatContext, format_args, write}; use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_ast::str_prefix::{ - AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, + AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, TStringPrefix, +}; +use ruff_python_ast::{ + AnyStringFlags, FString, InterpolatedStringElement, StringFlags, StringLike, StringLikePart, + TString, }; -use ruff_python_ast::{AnyStringFlags, FStringElement, StringFlags, StringLike, StringLikePart}; use ruff_source_file::LineRanges; use ruff_text_size::{Ranged, TextRange}; use std::borrow::Cow; use crate::comments::{leading_comments, trailing_comments}; use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space; -use crate::other::f_string::{FStringContext, FStringLayout}; -use crate::other::f_string_element::FormatFStringExpressionElement; +use crate::other::interpolated_string::{InterpolatedStringContext, InterpolatedStringLayout}; +use crate::other::interpolated_string_element::FormatInterpolatedElement; use crate::prelude::*; use crate::string::docstring::needs_chaperone_space; use crate::string::normalize::{ QuoteMetadata, is_fstring_with_quoted_debug_expression, - is_fstring_with_quoted_format_spec_and_debug, is_fstring_with_triple_quoted_literal_expression_containing_quotes, + is_interpolated_string_with_quoted_format_spec_and_debug, }; use crate::string::{StringLikeExtensions, StringNormalizer, StringQuotes, normalize_string}; /// Formats any implicitly concatenated string. This could be any valid combination -/// of string, bytes or f-string literals. +/// of string, bytes, f-string, or t-string literals. pub(crate) struct FormatImplicitConcatenatedString<'a> { string: StringLike<'a>, } @@ -98,6 +101,7 @@ impl Format> for FormatImplicitConcatenatedStringExpanded<'_ StringLikePart::String(part) => part.format().fmt(f), StringLikePart::Bytes(bytes_literal) => bytes_literal.format().fmt(f), StringLikePart::FString(part) => part.format().fmt(f), + StringLikePart::TString(part) => part.format().fmt(f), }); let part_comments = comments.leading_dangling_trailing(part); @@ -138,7 +142,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> { let first_part = string.parts().next()?; - // The string is either a regular string, f-string, or bytes string. + // The string is either a regular string, f-string, t-string, or bytes string. let normalizer = StringNormalizer::from_context(context); // Some if a part requires preserving its quotes. @@ -164,9 +168,34 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> { return None; } - if let StringLikePart::FString(fstring) = part { - if context.options().target_version().supports_pep_701() { - if is_fstring_with_quoted_format_spec_and_debug(fstring, context) { + match part { + StringLikePart::FString(fstring) => { + if matches!(string, StringLike::TString(_)) { + // Don't concatenate t-strings and f-strings + return None; + } + if context.options().target_version().supports_pep_701() { + if is_interpolated_string_with_quoted_format_spec_and_debug( + &fstring.elements, + fstring.flags.into(), + context, + ) { + if preserve_quotes_requirement + .is_some_and(|quote| quote != part.flags().quote_style()) + { + return None; + } + preserve_quotes_requirement = Some(part.flags().quote_style()); + } + } + // Avoid invalid syntax for pre Python 312: + // * When joining parts that have debug expressions with quotes: `f"{10 + len('bar')=}" f'{10 + len("bar")=}' + // * When joining parts that contain triple quoted strings with quotes: `f"{'''test ' '''}" f'{"""other " """}'` + else if is_fstring_with_quoted_debug_expression(fstring, context) + || is_fstring_with_triple_quoted_literal_expression_containing_quotes( + fstring, context, + ) + { if preserve_quotes_requirement .is_some_and(|quote| quote != part.flags().quote_style()) { @@ -175,21 +204,21 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> { preserve_quotes_requirement = Some(part.flags().quote_style()); } } - // Avoid invalid syntax for pre Python 312: - // * When joining parts that have debug expressions with quotes: `f"{10 + len('bar')=}" f'{10 + len("bar")=}' - // * When joining parts that contain triple quoted strings with quotes: `f"{'''test ' '''}" f'{"""other " """}'` - else if is_fstring_with_quoted_debug_expression(fstring, context) - || is_fstring_with_triple_quoted_literal_expression_containing_quotes( - fstring, context, - ) - { - if preserve_quotes_requirement - .is_some_and(|quote| quote != part.flags().quote_style()) - { - return None; + StringLikePart::TString(tstring) => { + if is_interpolated_string_with_quoted_format_spec_and_debug( + &tstring.elements, + tstring.flags.into(), + context, + ) { + if preserve_quotes_requirement + .is_some_and(|quote| quote != part.flags().quote_style()) + { + return None; + } + preserve_quotes_requirement = Some(part.flags().quote_style()); } - preserve_quotes_requirement = Some(part.flags().quote_style()); } + StringLikePart::Bytes(_) | StringLikePart::String(_) => {} } } @@ -203,6 +232,7 @@ impl<'a> FormatImplicitConcatenatedStringFlat<'a> { StringLike::String(_) => AnyStringPrefix::Regular(StringLiteralPrefix::Empty), StringLike::Bytes(_) => AnyStringPrefix::Bytes(ByteStringPrefix::Regular), StringLike::FString(_) => AnyStringPrefix::Format(FStringPrefix::Regular), + StringLike::TString(_) => AnyStringPrefix::Template(TStringPrefix::Regular), }; let quote = if let Some(quote) = preserve_quotes_requirement { @@ -287,7 +317,7 @@ impl Format> for FormatImplicitConcatenatedStringFlat<'_> { FormatLiteralContent { range: part.content_range(), flags: self.flags, - is_fstring: false, + is_interpolated_string: false, trim_start: first_non_empty && self.docstring, trim_end: self.docstring && parts.peek().is_none(), } @@ -300,28 +330,32 @@ impl Format> for FormatImplicitConcatenatedStringFlat<'_> { } } - StringLikePart::FString(f_string) => { - for element in &f_string.elements { + StringLikePart::FString(FString { elements, .. }) + | StringLikePart::TString(TString { elements, .. }) => { + for element in elements { match element { - FStringElement::Literal(literal) => { + InterpolatedStringElement::Literal(literal) => { FormatLiteralContent { range: literal.range(), flags: self.flags, - is_fstring: true, + is_interpolated_string: true, trim_end: false, trim_start: false, } .fmt(f)?; } // Formatting the expression here and in the expanded version is safe **only** - // because we assert that the f-string never contains any comments. - FStringElement::Expression(expression) => { - let context = FStringContext::new( + // because we assert that the f/t-string never contains any comments. + InterpolatedStringElement::Interpolation(expression) => { + let context = InterpolatedStringContext::new( self.flags, - FStringLayout::from_f_string(f_string, f.context().source()), + InterpolatedStringLayout::from_interpolated_string_elements( + elements, + f.context().source(), + ), ); - FormatFStringExpressionElement::new(expression, context).fmt(f)?; + FormatInterpolatedElement::new(expression, context).fmt(f)?; } } } @@ -336,7 +370,7 @@ impl Format> for FormatImplicitConcatenatedStringFlat<'_> { struct FormatLiteralContent { range: TextRange, flags: AnyStringFlags, - is_fstring: bool, + is_interpolated_string: bool, trim_start: bool, trim_end: bool, } @@ -348,7 +382,7 @@ impl Format> for FormatLiteralContent { content, 0, self.flags, - self.flags.is_f_string() && !self.is_fstring, + self.flags.is_interpolated_string() && !self.is_interpolated_string, ); // Trim the start and end of the string if it's the first or last part of a docstring. diff --git a/crates/ruff_python_formatter/src/string/mod.rs b/crates/ruff_python_formatter/src/string/mod.rs index 52fd92ad80..6f9fc5b33e 100644 --- a/crates/ruff_python_formatter/src/string/mod.rs +++ b/crates/ruff_python_formatter/src/string/mod.rs @@ -85,57 +85,55 @@ pub(crate) trait StringLikeExtensions { impl StringLikeExtensions for ast::StringLike<'_> { fn is_multiline(&self, context: &PyFormatContext) -> bool { + // Helper for f-string and t-string parts + fn contains_line_break_or_comments( + elements: &ast::InterpolatedStringElements, + context: &PyFormatContext, + triple_quotes: TripleQuotes, + ) -> bool { + elements.iter().any(|element| match element { + ast::InterpolatedStringElement::Literal(literal) => { + triple_quotes.is_yes() && context.source().contains_line_break(literal.range()) + } + ast::InterpolatedStringElement::Interpolation(expression) => { + // Expressions containing comments can't be joined. + // + // Format specifiers needs to be checked as well. For example, the + // following should be considered multiline because the literal + // part of the format specifier contains a newline at the end + // (`.3f\n`): + // + // ```py + // x = f"hello {a + b + c + d:.3f + // } world" + // ``` + context.comments().contains_comments(expression.into()) + || expression.format_spec.as_deref().is_some_and(|spec| { + contains_line_break_or_comments(&spec.elements, context, triple_quotes) + }) + || expression.debug_text.as_ref().is_some_and(|debug_text| { + memchr2(b'\n', b'\r', debug_text.leading.as_bytes()).is_some() + || memchr2(b'\n', b'\r', debug_text.trailing.as_bytes()).is_some() + }) + } + }) + } + self.parts().any(|part| match part { StringLikePart::String(_) | StringLikePart::Bytes(_) => { part.flags().is_triple_quoted() && context.source().contains_line_break(part.range()) } - StringLikePart::FString(f_string) => { - fn contains_line_break_or_comments( - elements: &ast::FStringElements, - context: &PyFormatContext, - triple_quotes: TripleQuotes, - ) -> bool { - elements.iter().any(|element| match element { - ast::FStringElement::Literal(literal) => { - triple_quotes.is_yes() - && context.source().contains_line_break(literal.range()) - } - ast::FStringElement::Expression(expression) => { - // Expressions containing comments can't be joined. - // - // Format specifiers needs to be checked as well. For example, the - // following should be considered multiline because the literal - // part of the format specifier contains a newline at the end - // (`.3f\n`): - // - // ```py - // x = f"hello {a + b + c + d:.3f - // } world" - // ``` - context.comments().contains_comments(expression.into()) - || expression.format_spec.as_deref().is_some_and(|spec| { - contains_line_break_or_comments( - &spec.elements, - context, - triple_quotes, - ) - }) - || expression.debug_text.as_ref().is_some_and(|debug_text| { - memchr2(b'\n', b'\r', debug_text.leading.as_bytes()).is_some() - || memchr2(b'\n', b'\r', debug_text.trailing.as_bytes()) - .is_some() - }) - } - }) - } - - contains_line_break_or_comments( - &f_string.elements, - context, - f_string.flags.triple_quotes(), - ) - } + StringLikePart::FString(f_string) => contains_line_break_or_comments( + &f_string.elements, + context, + f_string.flags.triple_quotes(), + ), + StringLikePart::TString(t_string) => contains_line_break_or_comments( + &t_string.elements, + context, + t_string.flags.triple_quotes(), + ), }) } } diff --git a/crates/ruff_python_formatter/src/string/normalize.rs b/crates/ruff_python_formatter/src/string/normalize.rs index b8f2902d85..1e694a66ff 100644 --- a/crates/ruff_python_formatter/src/string/normalize.rs +++ b/crates/ruff_python_formatter/src/string/normalize.rs @@ -5,16 +5,15 @@ use std::iter::FusedIterator; use ruff_formatter::FormatContext; use ruff_python_ast::visitor::source_order::SourceOrderVisitor; use ruff_python_ast::{ - AnyStringFlags, BytesLiteral, FString, FStringElement, FStringElements, FStringFlags, + AnyStringFlags, BytesLiteral, FString, InterpolatedStringElement, InterpolatedStringElements, StringFlags, StringLikePart, StringLiteral, - str::{Quote, TripleQuotes}, }; use ruff_text_size::{Ranged, TextRange, TextSlice}; use crate::QuoteStyle; -use crate::context::FStringState; +use crate::context::InterpolatedStringState; use crate::prelude::*; -use crate::string::StringQuotes; +use crate::string::{Quote, StringQuotes, TripleQuotes}; pub(crate) struct StringNormalizer<'a, 'src> { preferred_quote_style: Option, @@ -47,11 +46,11 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { .unwrap_or(self.context.options().quote_style()); let supports_pep_701 = self.context.options().target_version().supports_pep_701(); - // For f-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't. - if let FStringState::InsideExpressionElement(parent_context) = self.context.f_string_state() + // For f-strings and t-strings prefer alternating the quotes unless The outer string is triple quoted and the inner isn't. + if let InterpolatedStringState::InsideInterpolatedElement(parent_context) = + self.context.interpolated_string_state() { - let parent_flags = parent_context.f_string().flags(); - + let parent_flags = parent_context.interpolated_string().flags(); if !parent_flags.is_triple_quoted() || string.flags().is_triple_quoted() { // This logic is even necessary when using preserve and the target python version doesn't support PEP701 because // we might end up joining two f-strings that have different quote styles, in which case we need to alternate the quotes @@ -67,33 +66,49 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { return QuoteStyle::Preserve; } - // There are cases where it is necessary to preserve the quotes to prevent an invalid f-string. - if let StringLikePart::FString(fstring) = string { - // There are two cases where it's necessary to preserve the quotes if the - // target version is pre 3.12 and the part is an f-string. - if !supports_pep_701 { - // An f-string expression contains a debug text with a quote character - // because the formatter will emit the debug expression **exactly** the - // same as in the source text. - if is_fstring_with_quoted_debug_expression(fstring, self.context) { - return QuoteStyle::Preserve; + // There are cases where it is necessary to preserve the quotes to prevent an invalid f-string or t-string. + match string { + StringLikePart::FString(fstring) => { + // There are two cases where it's necessary to preserve the quotes if the + // target version is pre 3.12 and the part is an f-string. + if !supports_pep_701 { + // An f-string expression contains a debug text with a quote character + // because the formatter will emit the debug expression **exactly** the + // same as in the source text. + if is_fstring_with_quoted_debug_expression(fstring, self.context) { + return QuoteStyle::Preserve; + } + + // An f-string expression that contains a triple quoted string literal + // expression that contains a quote. + if is_fstring_with_triple_quoted_literal_expression_containing_quotes( + fstring, + self.context, + ) { + return QuoteStyle::Preserve; + } } - // An f-string expression that contains a triple quoted string literal - // expression that contains a quote. - if is_fstring_with_triple_quoted_literal_expression_containing_quotes( - fstring, + // An f-string expression element contains a debug text and the corresponding + // format specifier has a literal element with a quote character. + if is_interpolated_string_with_quoted_format_spec_and_debug( + &fstring.elements, + fstring.flags.into(), self.context, ) { return QuoteStyle::Preserve; } } - - // An f-string expression element contains a debug text and the corresponding - // format specifier has a literal element with a quote character. - if is_fstring_with_quoted_format_spec_and_debug(fstring, self.context) { - return QuoteStyle::Preserve; + StringLikePart::TString(tstring) => { + if is_interpolated_string_with_quoted_format_spec_and_debug( + &tstring.elements, + tstring.flags.into(), + self.context, + ) { + return QuoteStyle::Preserve; + } } + _ => {} } // Per PEP 8, always prefer double quotes for triple-quoted strings. @@ -172,7 +187,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> { // The preferred quote style is single or double quotes, and the string contains a quote or // another character that may require escaping (Ok(preferred_quote), Some(first_quote_or_normalized_char_offset)) => { - let metadata = if string.is_fstring() { + let metadata = if string.is_interpolated_string() { QuoteMetadata::from_part(string, self.context, preferred_quote) } else { QuoteMetadata::from_str( @@ -262,9 +277,19 @@ impl QuoteMetadata { StringLikePart::FString(fstring) => { let metadata = QuoteMetadata::from_str("", part.flags(), preferred_quote); - metadata.merge_fstring_elements( + metadata.merge_interpolated_string_elements( &fstring.elements, - fstring.flags, + fstring.flags.into(), + context, + preferred_quote, + ) + } + StringLikePart::TString(tstring) => { + let metadata = QuoteMetadata::from_str("", part.flags(), preferred_quote); + + metadata.merge_interpolated_string_elements( + &tstring.elements, + tstring.flags.into(), context, preferred_quote, ) @@ -369,7 +394,7 @@ impl QuoteMetadata { }) } - /// For f-strings, only consider the quotes inside string-literals but ignore + /// For f-strings and t-strings, only consider the quotes inside string-literals but ignore /// quotes inside expressions (except inside the format spec). This allows both the outer and the nested literals /// to make the optimal local-choice to reduce the total number of quotes necessary. /// This doesn't require any pre 312 special handling because an expression @@ -377,10 +402,10 @@ impl QuoteMetadata { /// ```python /// f"{'escaping a quote like this \" is a syntax error pre 312'}" /// ``` - fn merge_fstring_elements( + fn merge_interpolated_string_elements( self, - elements: &FStringElements, - flags: FStringFlags, + elements: &InterpolatedStringElements, + flags: AnyStringFlags, context: &PyFormatContext, preferred_quote: Quote, ) -> Self { @@ -388,19 +413,19 @@ impl QuoteMetadata { for element in elements { match element { - FStringElement::Literal(literal) => { + InterpolatedStringElement::Literal(literal) => { merged = merged .merge(&QuoteMetadata::from_str( context.source().slice(literal), - flags.into(), + flags, preferred_quote, )) .expect("Merge to succeed because all parts have the same flags"); } - FStringElement::Expression(expression) => { + InterpolatedStringElement::Interpolation(expression) => { if let Some(spec) = expression.format_spec.as_deref() { if expression.debug_text.is_none() { - merged = merged.merge_fstring_elements( + merged = merged.merge_interpolated_string_elements( &spec.elements, flags, context, @@ -879,7 +904,7 @@ pub(super) fn is_fstring_with_quoted_debug_expression( fstring: &FString, context: &PyFormatContext, ) -> bool { - fstring.elements.expressions().any(|expression| { + fstring.elements.interpolations().any(|expression| { if expression.debug_text.is_some() { let content = context.source().slice(expression); contains_opposite_quote(content, fstring.flags.into()) @@ -889,58 +914,6 @@ pub(super) fn is_fstring_with_quoted_debug_expression( }) } -/// Returns `true` if `string` has any f-string expression element (direct or nested) with a debug expression and a format spec -/// that contains the opposite quote. It's important to preserve the quote style for those f-strings -/// because changing the quote style would result in invalid syntax. -/// -/// ```python -/// f'{1=: "abcd \'\'}' -/// f'{x=:a{y:"abcd"}}' -/// f'{x=:a{y:{z:"abcd"}}}' -/// ``` -pub(super) fn is_fstring_with_quoted_format_spec_and_debug( - fstring: &FString, - context: &PyFormatContext, -) -> bool { - fn has_format_spec_with_opposite_quote( - elements: &FStringElements, - flags: FStringFlags, - context: &PyFormatContext, - in_debug: bool, - ) -> bool { - elements.iter().any(|element| match element { - FStringElement::Literal(literal) => { - let content = context.source().slice(literal); - - in_debug && contains_opposite_quote(content, flags.into()) - } - FStringElement::Expression(expression) => { - expression.format_spec.as_deref().is_some_and(|spec| { - has_format_spec_with_opposite_quote( - &spec.elements, - flags, - context, - in_debug || expression.debug_text.is_some(), - ) - }) - } - }) - } - - fstring.elements.expressions().any(|expression| { - if let Some(spec) = expression.format_spec.as_deref() { - return has_format_spec_with_opposite_quote( - &spec.elements, - fstring.flags, - context, - expression.debug_text.is_some(), - ); - } - - false - }) -} - /// Tests if the `fstring` contains any triple quoted string, byte, or f-string literal that /// contains a quote character opposite to its own quote character. /// @@ -980,6 +953,17 @@ pub(super) fn is_fstring_with_triple_quoted_literal_expression_containing_quotes } } + contains_quotes + } + StringLikePart::TString(tstring) => { + let mut contains_quotes = false; + for literal in tstring.elements.literals() { + if self.contains_quote(literal.range(), tstring.flags.into()) { + contains_quotes = true; + break; + } + } + contains_quotes } }; @@ -1018,6 +1002,59 @@ pub(super) fn is_fstring_with_triple_quoted_literal_expression_containing_quotes visitor.found } +/// Returns `true` if `string` has any f/t-string interpolation element (direct or nested) with a debug expression and a format spec +/// that contains the opposite quote. It's important to preserve the quote style for those f/t-strings +/// because changing the quote style would result in invalid syntax. +/// +/// ```python +/// t'{1=: "abcd \'\'}' +/// t'{x=:a{y:"abcd"}}' +/// t'{x=:a{y:{z:"abcd"}}}' +/// ``` +pub(super) fn is_interpolated_string_with_quoted_format_spec_and_debug( + elements: &InterpolatedStringElements, + flags: AnyStringFlags, + context: &PyFormatContext, +) -> bool { + fn has_format_spec_with_opposite_quote( + elements: &InterpolatedStringElements, + flags: AnyStringFlags, + context: &PyFormatContext, + in_debug: bool, + ) -> bool { + elements.iter().any(|element| match element { + InterpolatedStringElement::Literal(literal) => { + let content = context.source().slice(literal); + + in_debug && contains_opposite_quote(content, flags) + } + InterpolatedStringElement::Interpolation(expression) => { + expression.format_spec.as_deref().is_some_and(|spec| { + has_format_spec_with_opposite_quote( + &spec.elements, + flags, + context, + in_debug || expression.debug_text.is_some(), + ) + }) + } + }) + } + + elements.interpolations().any(|expression| { + if let Some(spec) = expression.format_spec.as_deref() { + return has_format_spec_with_opposite_quote( + &spec.elements, + flags, + context, + expression.debug_text.is_some(), + ); + } + + false + }) +} + fn contains_opposite_quote(content: &str, flags: AnyStringFlags) -> bool { if flags.is_triple_quoted() { match flags.quote_style() { diff --git a/crates/ruff_python_formatter/tests/normalizer.rs b/crates/ruff_python_formatter/tests/normalizer.rs index 2786082cfe..8bed90221c 100644 --- a/crates/ruff_python_formatter/tests/normalizer.rs +++ b/crates/ruff_python_formatter/tests/normalizer.rs @@ -6,8 +6,8 @@ use { use ruff_python_ast::visitor::transformer::Transformer; use ruff_python_ast::{ - self as ast, BytesLiteralFlags, Expr, FStringElement, FStringFlags, FStringLiteralElement, - FStringPart, Stmt, StringFlags, + self as ast, BytesLiteralFlags, Expr, FStringFlags, FStringPart, InterpolatedStringElement, + InterpolatedStringLiteralElement, Stmt, StringFlags, }; use ruff_python_ast::{StringLiteralFlags, visitor::transformer}; use ruff_text_size::{Ranged, TextRange}; @@ -117,7 +117,7 @@ impl Transformer for Normalizer { if can_join { #[derive(Default)] struct Collector { - elements: Vec, + elements: Vec, } impl Collector { @@ -127,7 +127,7 @@ impl Transformer for Normalizer { // `elements` vector, while subsequent strings // are concatenated onto this top string. fn push_literal(&mut self, literal: &str, range: TextRange) { - if let Some(FStringElement::Literal(existing_literal)) = + if let Some(InterpolatedStringElement::Literal(existing_literal)) = self.elements.last_mut() { let value = std::mem::take(&mut existing_literal.value); @@ -137,8 +137,8 @@ impl Transformer for Normalizer { existing_literal.range = TextRange::new(existing_literal.start(), range.end()); } else { - self.elements.push(FStringElement::Literal( - FStringLiteralElement { + self.elements.push(InterpolatedStringElement::Literal( + InterpolatedStringLiteralElement { range, value: literal.into(), }, @@ -146,11 +146,9 @@ impl Transformer for Normalizer { } } - fn push_expression( - &mut self, - expression: ast::FStringExpressionElement, - ) { - self.elements.push(FStringElement::Expression(expression)); + fn push_expression(&mut self, expression: ast::InterpolatedElement) { + self.elements + .push(InterpolatedStringElement::Interpolation(expression)); } } @@ -165,11 +163,13 @@ impl Transformer for Normalizer { ast::FStringPart::FString(fstring) => { for element in &fstring.elements { match element { - ast::FStringElement::Literal(literal) => { + ast::InterpolatedStringElement::Literal(literal) => { collector .push_literal(&literal.value, literal.range); } - ast::FStringElement::Expression(expression) => { + ast::InterpolatedStringElement::Interpolation( + expression, + ) => { collector.push_expression(expression.clone()); } } diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap index 9a5b9d1882..a5f3462811 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__binary.py.snap @@ -437,6 +437,19 @@ f"hellooooooooooooooooooooooo \ worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) aaaaaaaaaaa = f"hellooooooooooooooooooooooo \ worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) + +# This t-string should be flattened +xxxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaaaaaaa { + expression } bbbbbbbbbbbbbbbbbbbbbbbb" + ( + yyyyyyyyyyyyyy + zzzzzzzzzzz +) + +# This is not a multiline t-string, but the expression is too long so it should be +# wrapped in parentheses. +t"hellooooooooooooooooooooooo \ + worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) +aaaaaaaaaaa = t"hellooooooooooooooooooooooo \ + worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) ``` ## Output @@ -927,4 +940,22 @@ aaaaaaaaaaa = ( worlddddddddddddddddddddddddddddddddd" + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) ) + +# This t-string should be flattened +xxxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaaaaaaa {expression} bbbbbbbbbbbbbbbbbbbbbbbb" + ( + yyyyyyyyyyyyyy + zzzzzzzzzzz +) + +# This is not a multiline t-string, but the expression is too long so it should be +# wrapped in parentheses. +( + t"hellooooooooooooooooooooooo \ + worlddddddddddddddddddddddddddddddddd" + + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) +) +aaaaaaaaaaa = ( + t"hellooooooooooooooooooooooo \ + worlddddddddddddddddddddddddddddddddd" + + (aaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbb) +) ``` diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string.py.snap index c2e7f51ca1..6c577df6bb 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string.py.snap @@ -106,6 +106,55 @@ f"{10 + len('bar')=}" f'no debug{10}' f"{10 + len('bar')=}" f"{10 + len('bar')=}" f'{10 + len("bar")=}' +############################################################################## +# T-strings +############################################################################## + +# Escape `{` and `}` when merging a t-string with a string +"a {not_a_variable}" t"b {10}" "c" + +# Join, and break expressions +t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{ +expression +}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more" + +# Join, but don't break the expressions +t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" t"cccccccccccccccccccc {20999}" "more" + +t"test{ +expression +}flat" t"can be { +joined +} together" + +aaaaaaaaaaa = t"test{ +expression +}flat" t"cean beeeeeeee { +joined +} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + + +t"single quoted '{x}'" t'double quoted "{x}"' # Same number of quotes => use preferred quote style +t"single quote ' {x}" t'double quoted "{x}"' # More double quotes => use single quotes +t"single quoted '{x}'" t'double quote " {x}"' # More single quotes => use double quotes + +# Different triple quoted strings +t"{'''test'''}" t'{"""other"""}' + +# Now with inner quotes +t"{'''test ' '''}" t'{"""other " """}' +t"{some_where_nested('''test ' ''')}" t'{"""other " """ + "more"}' +t"{b'''test ' '''}" t'{b"""other " """}' +t"{t'''test ' '''}" t'{t"""other " """}' + +# debug expressions containing quotes +t"{10 + len('bar')=}" t"{10 + len('bar')=}" +t"{10 + len('bar')=}" t'no debug{10}' t"{10 + len('bar')=}" + +# We can't safely merge this pre Python 3.12 without altering the debug expression. +t"{10 + len('bar')=}" t'{10 + len("bar")=}' + + ############################################################################## # Don't join raw strings ############################################################################## @@ -116,6 +165,9 @@ R"a" "normal" f"test" fr"test" f"test" fR"test" +t"test" tr"test" +t"test" tR"test" + ############################################################################## # Don't join triple quoted strings @@ -125,9 +177,22 @@ f"test" fR"test" "single" f""""single""" +"single" t""""single""" + b"single" b"""triple""" +############################################################################## +# Don't join t-strings and f-strings +############################################################################## + +t"{interp}" f"{expr}" + +f"{expr}" t"{interp}" + +f"{expr}" "string" t"{interp}" + + ############################################################################## # Join strings in with statements ############################################################################## @@ -452,6 +517,50 @@ f"{10 + len('bar')=}no debug{10}{10 + len('bar')=}" f"{10 + len('bar')=}" f'{10 + len("bar")=}' +############################################################################## +# T-strings +############################################################################## + +# Escape `{` and `}` when merging a t-string with a string +t"a {{not_a_variable}}b {10}c" + +# Join, and break expressions +t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression +}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccc {20999}more" + +# Join, but don't break the expressions +t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbcccccccccccccccccccc {20999}more" + +t"test{expression}flatcan be {joined} together" + +aaaaaaaaaaa = ( + t"test{expression}flat" + t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" +) # inline + + +t"single quoted '{x}'double quoted \"{x}\"" # Same number of quotes => use preferred quote style +t'single quote \' {x}double quoted "{x}"' # More double quotes => use single quotes +t"single quoted '{x}'double quote \" {x}\"" # More single quotes => use double quotes + +# Different triple quoted strings +t"{'''test'''}{'''other'''}" + +# Now with inner quotes +t"{'''test ' '''}{'''other " '''}" +t"{some_where_nested('''test ' ''')}{'''other " ''' + 'more'}" +t"{b'''test ' '''}{b'''other " '''}" +t"{t'''test ' '''}{t'''other " '''}" + +# debug expressions containing quotes +t"{10 + len('bar')=}{10 + len('bar')=}" +t"{10 + len('bar')=}no debug{10}{10 + len('bar')=}" + +# We can't safely merge this pre Python 3.12 without altering the debug expression. +t"{10 + len('bar')=}{10 + len("bar")=}" + + ############################################################################## # Don't join raw strings ############################################################################## @@ -462,6 +571,9 @@ R"a" "normal" f"test" rf"test" f"test" Rf"test" +t"test" rt"test" +t"test" Rt"test" + ############################################################################## # Don't join triple quoted strings @@ -471,9 +583,22 @@ f"test" Rf"test" "single" f""""single""" +"single" t""""single""" + b"single" b"""triple""" +############################################################################## +# Don't join t-strings and f-strings +############################################################################## + +t"{interp}" f"{expr}" + +f"{expr}" t"{interp}" + +f"{expr}" "string" t"{interp}" + + ############################################################################## # Join strings in with statements ############################################################################## @@ -780,7 +905,7 @@ f"aaaaaaaaaaaaaaaa \ ```diff --- Stable +++ Preview -@@ -242,9 +242,12 @@ +@@ -302,9 +302,12 @@ ############################################################################## # Use can_omit_optional_parentheses layout to avoid an instability where the formatter # picks the can_omit_optional_parentheses layout when the strings are joined. diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_assignment.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_assignment.py.snap index 7133eb0b8c..c5237dcb54 100644 --- a/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_assignment.py.snap +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__join_implicit_concatenated_string_assignment.py.snap @@ -299,6 +299,155 @@ aaaaa[aaaaaaaaaaa] = ( ) +############################################################# +# T-Strings +############################################################# + +# Flatten and join the t-string +aaaaaaaaaaa = t"test{ +expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline + +# Parenthesize the value and join it, inline the comment +aaaaaaaaaaa = t"test{ +expression}flat" t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# Parenthesize the t-string and keep it multiline because it doesn't fit on a single line including the comment +aaaaaaaaaaa = t"test{ +expression +}flat" t"cean beeeeeeee { +joined +} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + + +# The target splits because of a magic trailing comma +# The string is joined and not parenthesized because it just fits into the line length (including comment). +a[ + aaaaaaa, + b, +] = t"ccccc{ +expression}ccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment + + +# Same but starting with a joined string. They should both result in the same formatting. +[ + aaaaaaa, + b, +] = t"ccccc{ +expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment + +# The target splits because of the magic trailing comma +# The string is **not** joined because it with the inlined comment exceeds the line length limit. +a[ + aaaaaaa, + b, +] = t"ccccc{ +expression}cccccccccccccccccccc" t"cccccccccccccccccccccccccccccccccccccccccc" # comment + + +# The target should be flat +# The string should be joined because it fits into the line length +a[ + aaaaaaa, + b +] = ( + t"ccccc{ + expression}ccccccccccc" "cccccccccccccccccccccccc" # comment +) + +# Same but starting with a joined string. They should both result in the same formatting. +a[ + aaaaaaa, + b +] = t"ccccc{ +expression}ccccccccccccccccccccccccccccccccccc" # comment + +# The target should be flat +# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit. +a[ + aaaaaaa, + b +] = t"ccccc{ +expression}ccccccccccc" "ccccccccccccccccccccccccccccccccccccccccccc" # comment + + +# Split an overlong target, but join the string if it fits +a[ + aaaaaaa, + b +].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( + t"ccccc{ + expression}ccccccccccc" "cccccccccccccccccccccccccccccc" # comment +) + +# Split both if necessary and keep multiline +a[ + aaaaaaa, + b +].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( + t"ccccc{ + expression}cccccccccccccccccccccccccccccccc" "ccccccccccccccccccccccccccccccc" # comment +) + +# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment +) + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment + +aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}" "moreeeeeeeeeeeeeeeeeeee" "test" # comment +) + +# Don't inline t-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +# Don't inline t-strings with multiline debug expressions: +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}" "moreeeeeeeeeeeeeeeeeetest" # comment +) + + # Trailing last-part comments a = ( @@ -380,7 +529,8 @@ self._attr_unique_id = ( return ( f"Exception in {call_back_name} when handling msg on " f"'{msg.topic}': '{msg.payload}'" # type: ignore[str-bytes-safe] -)``` +) +``` ## Output ```python @@ -704,6 +854,172 @@ aaaaa[aaaaaaaaaaa] = ( ) +############################################################# +# T-Strings +############################################################# + +# Flatten and join the t-string +aaaaaaaaaaa = t"test{expression}flatcean beeeeeeee {joined} eeeeeeeeeeeeeeeee" # inline + +# Parenthesize the value and join it, inline the comment +aaaaaaaaaaa = ( + t"test{expression}flatcean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeee" # inline +) + +# Parenthesize the t-string and keep it multiline because it doesn't fit on a single line including the comment +aaaaaaaaaaa = ( + t"test{expression}flat" + t"cean beeeeeeee {joined} eeeeeeeeeeeeeeeeeeeeeeeeeeeee" +) # inline + + +# The target splits because of a magic trailing comma +# The string is joined and not parenthesized because it just fits into the line length (including comment). +a[ + aaaaaaa, + b, +] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment + + +# Same but starting with a joined string. They should both result in the same formatting. +[ + aaaaaaa, + b, +] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccccccccccccccc" # comment + +# The target splits because of the magic trailing comma +# The string is **not** joined because it with the inlined comment exceeds the line length limit. +a[ + aaaaaaa, + b, +] = ( + t"ccccc{expression}cccccccccccccccccccc" + t"cccccccccccccccccccccccccccccccccccccccccc" +) # comment + + +# The target should be flat +# The string should be joined because it fits into the line length +a[aaaaaaa, b] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccc" # comment + +# Same but starting with a joined string. They should both result in the same formatting. +a[aaaaaaa, b] = t"ccccc{expression}ccccccccccccccccccccccccccccccccccc" # comment + +# The target should be flat +# The string gets parenthesized because it, with the inlined comment, exceeds the line length limit. +a[aaaaaaa, b] = ( + t"ccccc{expression}ccccccccccc" + "ccccccccccccccccccccccccccccccccccccccccccc" +) # comment + + +# Split an overlong target, but join the string if it fits +a[ + aaaaaaa, b +].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( + t"ccccc{expression}ccccccccccccccccccccccccccccccccccccccccc" # comment +) + +# Split both if necessary and keep multiline +a[ + aaaaaaa, b +].bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb = ( + t"ccccc{expression}cccccccccccccccccccccccccccccccc" + "ccccccccccccccccccccccccccccccc" +) # comment + +# Don't inline t-strings that contain expressions that are guaranteed to split, e.b. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] + }" + "moreeeeeeeeeeeeeeeeeeee" + "test" +) # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] + }" + "moreeeeeeeeeeeeeeeeeeee" + "test" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] + }" + "moreeeeeeeeeeeeeeeeeeee" + "test" +) # comment + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] + }" + "moreeeeeeeeeeeeeeeeeeee" + "test" # comment +) + +# Don't inline t-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a # comment + ] + }" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a # comment + ] + }" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + +# Don't inline t-strings with multiline debug expressions: +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}" + "moreeeeeeeeeeeeeeeeeetest" # comment +) + + # Trailing last-part comments a = ( diff --git a/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap new file mode 100644 index 0000000000..5c08140e3e --- /dev/null +++ b/crates/ruff_python_formatter/tests/snapshots/format@expression__tstring.py.snap @@ -0,0 +1,1536 @@ +--- +source: crates/ruff_python_formatter/tests/fixtures.rs +input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/tstring.py +--- +## Input +```python +( + t'{one}' + t'{two}' +) + + +rt"Not-so-tricky \"quote" + +# Regression test for tstrings dropping comments +result_f = ( + 'Traceback (most recent call last):\n' + t' File "{__file__}", line {lineno_f+5}, in _check_recursive_traceback_display\n' + ' f()\n' + t' File "{__file__}", line {lineno_f+1}, in f\n' + ' f()\n' + t' File "{__file__}", line {lineno_f+1}, in f\n' + ' f()\n' + t' File "{__file__}", line {lineno_f+1}, in f\n' + ' f()\n' + # XXX: The following line changes depending on whether the tests + # are run through the interactive interpreter or with -m + # It also varies depending on the platform (stack size) + # Fortunately, we don't care about exactness here, so we use regex + r' \[Previous line repeated (\d+) more times\]' '\n' + 'RecursionError: maximum recursion depth exceeded\n' +) + + +# Regression for tstring dropping comments that were accidentally attached to +# an expression inside a formatted value +( + t'{1}' + # comment 1 + '' +) + +( + t'{1}' # comment 2 + t'{2}' +) + +( + t'{1}' + t'{2}' # comment 3 +) + +( + 1, ( # comment 4 + t'{2}' + ) +) + +( + ( + t'{1}' + # comment 5 + ), + 2 +) + +# https://github.com/astral-sh/ruff/issues/6841 +x = t'''a{""}b''' +y = t'''c{1}d"""e''' +z = t'''a{""}b''' t'''c{1}d"""e''' + +# T-String formatting test cases (Preview) + +# Simple expression with a mix of debug expression and comments. +x = t"{a}" +x = t"{ + a = }" +x = t"{ # comment 6 + a }" +x = t"{ # comment 7 + a = }" + +# Remove the parentheses as adding them doesn't make then fit within the line length limit. +# This is similar to how we format it before t-string formatting. +aaaaaaaaaaa = ( + t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd } cccccccccc" +) +# Here, we would use the best fit layout to put the t-string indented on the next line +# similar to the next example. +aaaaaaaaaaa = t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc" +aaaaaaaaaaa = ( + t"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc" +) + +# This should never add the optional parentheses because even after adding them, the +# t-string exceeds the line length limit. +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9 + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc" + +# Multiple larger expressions which exceeds the line length limit. Here, we need to decide +# whether to split at the first or second expression. This should work similarly to the +# assignment statement formatting where we split from right to left in preview mode. +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" + +# The above example won't split but when we start introducing line breaks: +x = t"aaaaaaaaaaaa { + bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb + } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { + ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd + } eeeeeeeeeeeeee" + +# But, in case comments are present, we would split at the expression containing the +# comments: +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb # comment 10 + } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa { bbbbbbbbbbbbbb + } cccccccccccccccccccc { # comment 11 + ddddddddddddddd } eeeeeeeeeeeeee" + +# Here, the expression part itself starts with a curly brace so we need to add an extra +# space between the opening curly brace and the expression. +x = t"{ {'x': 1, 'y': 2} }" +# Although the extra space isn't required before the ending curly brace, we add it for +# consistency. +x = t"{ {'x': 1, 'y': 2}}" +x = t"{ {'x': 1, 'y': 2} = }" +x = t"{ # comment 12 + {'x': 1, 'y': 2} }" +x = t"{ # comment 13 + {'x': 1, 'y': 2} = }" +# But, if there's a format specifier or a conversion flag then we don't need to add +# any whitespace at the end +x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}!s} bbbbbb" +x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}:.3f} bbbbbb" + +# But, in this case, we would split the expression itself because it exceeds the line +# length limit so we need not add the extra space. +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'} +}" +# And, split the expression itself because it exceeds the line length. +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +############################################################################################# +# Quotes +############################################################################################# +t"foo 'bar' {x}" +t"foo \"bar\" {x}" +t'foo "bar" {x}' +t'foo \'bar\' {x}' +t"foo {"bar"}" + +t"single quoted '{x}' double quoted \"{x}\"" # Same number of quotes => use preferred quote style +t"single quote ' {x} double quoted \"{x}\"" # More double quotes => use single quotes +t"single quoted '{x}' double quote \" {x}" # More single quotes => use double quotes + +fr"single quotes ' {x}" # Keep double because `'` can't be escaped +fr'double quotes " {x}' # Keep single because `"` can't be escaped +fr'flip quotes {x}' # Use preferred quotes, because raw string contains now quotes. + +# Here, the formatter will remove the escapes +t"foo {'\'bar\''}" +t"foo {'\"bar\"'}" + +# Quotes inside the expressions have no impact on the quote selection of the outer string. +# Required so that the following two examples result in the same formatting. +t'foo {10 + len("bar")}' +t"foo {10 + len('bar')}" + +# Pre 312, preserve the outer quotes if the t-string contains quotes in the debug expression +t'foo {10 + len("bar")=}' +t'''foo {10 + len('''bar''')=}''' +t'''foo {10 + len('bar')=}''' # Fine to change the quotes because it uses triple quotes + +# Triple-quoted strings +# It's ok to use the same quote char for the inner string if it's single-quoted. +t"""test {'inner'}""" +t"""test {"inner"}""" +# But if the inner string is also triple-quoted then we should preserve the existing quotes. +t"""test {'''inner'''}""" + +# It's not okay to change the quote style if the inner string is triple quoted and contains a quote. +t'{"""other " """}' +t'{"""other " """ + "more"}' +t'{b"""other " """}' +t'{t"""other " """}' + +t"""test {t'inner {'''inner inner'''}'}""" +t"""test {t'''inner {"""inner inner"""}'''}""" + +# Magic trailing comma +# +# The expression formatting will result in breaking it across multiple lines with a +# trailing comma but as the expression isn't already broken, we will remove all the line +# breaks which results in the trailing comma being present. This test case makes sure +# that the trailing comma is removed as well. +t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa" + +# And, if the trailing comma is already present, we still need to remove it. +t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee',]} aaaaaaa" + +# Keep this Multiline by breaking it at the square brackets. +t"""aaaaaa {[ + xxxxxxxx, + yyyyyyyy, +]} ccc""" + +# Add the magic trailing comma because the elements don't fit within the line length limit +# when collapsed. +t"aaaaaa {[ + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + yyyyyyyyyyyy +]} ccccccc" + +# Remove the parentheses because they aren't required +xxxxxxxxxxxxxxx = ( + t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbb { + xxxxxxxxxxx # comment 14 + + yyyyyyyyyy + } dddddddddd" +) + +# Comments + +# No comments should be dropped! +t"{ # comment 15 + # comment 16 + foo # comment 17 + # comment 18 +}" # comment 19 +# comment 20 + +# Single-quoted t-strings with a format specificer can be multiline +t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable:.3f} ddddddddddddddd eeeeeeee" + +# But, if it's triple-quoted then we can't or the format specificer will have a +# trailing newline +t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable:.3f} ddddddddddddddd eeeeeeee""" + +# But, we can break the ones which don't have a format specifier +t"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr { + xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { xxxxxxxxxxxxxxxxxxxx } bbbbbbbbbbbb""" + +# Throw in a random comment in it but surprise, this is not a comment but just a text +# which is part of the format specifier +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f + # comment +} cccccccccc""" +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa { + aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f + # comment} cccccccccc""" + +# Conversion flags +# +# This is not a valid Python code because of the additional whitespace between the `!` +# and conversion type. But, our parser isn't strict about this. This should probably be +# removed once we have a strict parser. +x = t"aaaaaaaaa { x ! r }" + +# Even in the case of debug expressions, we only need to preserve the whitespace within +# the expression part of the replacement field. +x = t"aaaaaaaaa { x = ! r }" + +# Combine conversion flags with format specifiers +x = t"{x = ! s + :>0 + + }" +# This is interesting. There can be a comment after the format specifier but only if it's +# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. +# We'll format is as trailing comments. +x = t"{x !s + :>0 + # comment 21 + }" + +x = t""" +{ # comment 22 + x = :.0{y # comment 23 + }f}""" + +# Here, the debug expression is in a nested t-string so we should start preserving +# whitespaces from that point onwards. This means we should format the outer t-string. +x = t"""{"foo " + # comment 24 + t"{ x = + + }" # comment 25 + } + """ + +# Mix of various features. +t"{ # comment 26 + foo # after foo + :>{ + x # after x + } + # comment 27 + # comment 28 +} woah {x}" + +# Assignment statement + +# Even though this t-string has multiline expression, thus allowing us to break it at the +# curly braces, the t-string fits on a single line if it's moved inside the parentheses. +# We should prefer doing that instead. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" + +# Same as above +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Similar to the previous example, but the t-string will exceed the line length limit, +# we shouldn't add any parentheses here. +xxxxxxx = t"{ + {'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} +}" + +# Same as above but with an inline comment. The t-string should be formatted inside the +# parentheses and the comment should be part of the line inside the parentheses. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Similar to the previous example but this time parenthesizing doesn't work because it +# exceeds the line length. So, avoid parenthesizing this t-string. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# Similar to the previous example but we start with the parenthesized layout. This should +# remove the parentheses and format the t-string on a single line. This shows that the +# final layout for the formatter is same for this and the previous case. The only +# difference is that in the previous case the expression is already mulitline which means +# the formatter can break it further at the curly braces. +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment loooooooong +) + +# The following t-strings are going to break because of the trailing comma so we should +# avoid using the best fit layout and instead use the default layout. +# left-to-right +aaaa = t"aaaa {[ + 1, 2, +]} bbbb" +# right-to-left +aaaa, bbbb = t"aaaa {[ + 1, 2, +]} bbbb" + +# Using the right-to-left assignment statement variant. +aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeee" # comment + +# Here, the t-string layout is flat but it exceeds the line length limit. This shouldn't +# try the custom best fit layout because the t-string doesn't have any split points. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +# Same as above but without the parentheses to test that it gets formatted to the same +# layout as the previous example. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" + +# But, the following t-string does have a split point because of the multiline expression. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbb + cccccccccccccccccccccc + dddddddddddddddddddddddddddd} ddddddddddddddddddd" +) + +# This is an implicitly concatenated t-string but it cannot be joined because otherwise +# it'll exceed the line length limit. So, the two t-strings will be inside parentheses +# instead and the inline comment should be outside the parentheses. +a = t"test{ + expression +}flat" t"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# Similar to the above example but this fits within the line length limit. +a = t"test{ + expression +}flat" t"can be { + joined +} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# The following test cases are adopted from implicit string concatenation but for a +# single t-string instead. + +# Don't inline t-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment +) + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = (t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ +[a,] +}moreeeeeeeeeeeeeeeeeeee" # comment +) + +# Don't inline t-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{[ + a # comment + ]}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# Don't inline t-strings with multiline debug expressions or format specifiers +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaa[aaaaaaaaaaa] = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# This is not a multiline t-string even though it has a newline after the format specifier. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest" # comment +) + +# The newline is only considered when it's a tripled-quoted t-string. +aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +aaaaaaaaaaaaaaaaaa = ( + t"""testeeeeeeeeeeeeeeeeeeeeeeeee{ + a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment +) + +# Remove the parentheses here +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + # comment + ]}moee" # comment +) +# ... but not here because of the ownline comment +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{[a, b, + ]}moee" + # comment +) + +# t-strings in other positions + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if ( + t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }" +): pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": pass + +if t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if ( + t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + }" +): + pass + +if t"aaaaaaaaaaa {[ttttteeeeeeeeest,]} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +# For loops +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# With statements +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeee": + pass + +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# Assert statements +assert t"aaaaaaaaa{ + expression}bbbbbbbbbbbb", t"cccccccccc{ + expression}dddddddddd" + +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{ + expression}dddddddddddddddd" + +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{expression}dddddddddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccccccccccccccccc { + expression} dddddddddddddddddddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd" + +# t-strings as a single argument to a call expression to test whether it's huggable or not. +call(t"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(t"{ + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(t"{ # comment + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee +}") + +call(t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""") + +call(t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""") + +call(t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""") + +call( + t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""" +) + +call(t"{ + aaaaaa + + '''test + more''' +}") + +# Indentation + +# What should be the indentation? +# https://github.com/astral-sh/ruff/discussions/9785#discussioncomment-8470590 +if indent0: + if indent1: + if indent2: + foo = t"""hello world +hello { + t"aaaaaaa { + [ + 'aaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbb', + 'ccccccccccccccccccccc', + 'ddddddddddddddddddddd' + ] + } bbbbbbbb" + + [ + 'aaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbb', + 'ccccccccccccccccccccc', + 'ddddddddddddddddddddd' + ] + } -------- +""" + + +# Implicit concatenated t-string containing quotes +_ = ( + 'This string should change its quotes to double quotes' + t'This string uses double quotes in an expression {"it's a quote"}' + t'This t-string does not use any quotes.' +) + +# Regression test for https://github.com/astral-sh/ruff/issues/14487 +t"aaaaaaaaaaaaaaaaaaaaaaaaaa {10**27} bbbbbbbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccccc" + +# Regression test for https://github.com/astral-sh/ruff/issues/14778 +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}" +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' 'a' if True else ""}" + +# Quotes reuse +t"{'a'}" + +# 312+, it's okay to change the outer quotes even when there's a debug expression using the same quotes +t'foo {10 + len("bar")=}' +t'''foo {10 + len("""bar""")=}''' + +# 312+, it's okay to change the quotes here without creating an invalid t-string +t'{"""other " """}' +t'{"""other " """ + "more"}' +t'{b"""other " """}' +t'{t"""other " """}' + + +# Regression tests for https://github.com/astral-sh/ruff/issues/13935 +t'{1: hy "user"}' +t'{1:hy "user"}' +t'{1: abcd "{1}" }' +t'{1: abcd "{'aa'}" }' +t'{1=: "abcd {'aa'}}' +t'{x:a{z:hy "user"}} \'\'\'' + +# Changing the outer quotes is fine because the format-spec is in a nested expression. +t'{t'{z=:hy "user"}'} \'\'\'' + + +# We have to be careful about changing the quotes if the t-string has a debug expression because it is inserted verbatim. +t'{1=: "abcd \'\'}' # Don't change the outer quotes, or it results in a syntax error +t'{1=: abcd \'\'}' # Changing the quotes here is fine because the inner quotes aren't the opposite quotes +t'{1=: abcd \"\"}' # Changing the quotes here is fine because the inner quotes are escaped +# Don't change the quotes in the following cases: +t'{x=:hy "user"} \'\'\'' +t'{x=:a{y:hy "user"}} \'\'\'' +t'{x=:a{y:{z:hy "user"}}} \'\'\'' +t'{x:a{y=:{z:hy "user"}}} \'\'\'' + +# This is fine because the debug expression and format spec are in a nested expression + +t"""{1=: "this" is fine}""" +t'''{1=: "this" is fine}''' # Change quotes to double quotes because they're preferred +t'{1=: {'ab"cd"'}}' # It's okay if the quotes are in an expression part. + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15459 +print(t"{ {1, 2, 3} - {2} }") +print(t"{ {1: 2}.keys() }") +print(t"{({1, 2, 3}) - ({2})}") +print(t"{1, 2, {3} }") +print(t"{(1, 2, {3})}") + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15535 +print(t"{ {}, }") # A single item tuple gets parenthesized +print(t"{ {}.values(), }") +print(t"{ {}, 1 }") # A tuple with multiple elements doesn't get parenthesized +print(t"{ # Tuple with multiple elements that doesn't fit on a single line gets parenthesized + {}, 1, +}") + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15536 +print(t"{ {}, 1, }") +``` + +## Outputs +### Output 1 +``` +indent-style = space +line-width = 88 +indent-width = 4 +quote-style = Double +line-ending = LineFeed +magic-trailing-comma = Respect +docstring-code = Disabled +docstring-code-line-width = "dynamic" +preview = Disabled +target_version = 3.14 +source_type = Python +``` + +```python +(t"{one}{two}") + + +rt"Not-so-tricky \"quote" + +# Regression test for tstrings dropping comments +result_f = ( + "Traceback (most recent call last):\n" + t' File "{__file__}", line {lineno_f + 5}, in _check_recursive_traceback_display\n' + " f()\n" + t' File "{__file__}", line {lineno_f + 1}, in f\n' + " f()\n" + t' File "{__file__}", line {lineno_f + 1}, in f\n' + " f()\n" + t' File "{__file__}", line {lineno_f + 1}, in f\n' + " f()\n" + # XXX: The following line changes depending on whether the tests + # are run through the interactive interpreter or with -m + # It also varies depending on the platform (stack size) + # Fortunately, we don't care about exactness here, so we use regex + r" \[Previous line repeated (\d+) more times\]" + "\n" + "RecursionError: maximum recursion depth exceeded\n" +) + + +# Regression for tstring dropping comments that were accidentally attached to +# an expression inside a formatted value +( + t"{1}" + # comment 1 + "" +) + +( + t"{1}" # comment 2 + t"{2}" +) + +( + t"{1}{2}" # comment 3 +) + +( + 1, + ( # comment 4 + t"{2}" + ), +) + +( + ( + t"{1}" + # comment 5 + ), + 2, +) + +# https://github.com/astral-sh/ruff/issues/6841 +x = t"""a{""}b""" +y = t'''c{1}d"""e''' +z = t"""a{""}b""" t'''c{1}d"""e''' + +# T-String formatting test cases (Preview) + +# Simple expression with a mix of debug expression and comments. +x = t"{a}" +x = t"{ + a = }" +x = t"{ # comment 6 + a +}" +x = t"{ # comment 7 + a = }" + +# Remove the parentheses as adding them doesn't make then fit within the line length limit. +# This is similar to how we format it before t-string formatting. +aaaaaaaaaaa = t"asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd} cccccccccc" +# Here, we would use the best fit layout to put the t-string indented on the next line +# similar to the next example. +aaaaaaaaaaa = ( + t"asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc} cccccccccc" +) +aaaaaaaaaaa = ( + t"asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc} cccccccccc" +) + +# This should never add the optional parentheses because even after adding them, the +# t-string exceeds the line length limit. +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa {'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb'} ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8 + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' +} ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9 + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc" +x = t"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9 + 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbb' = } ccccccccccccccc" + +# Multiple larger expressions which exceeds the line length limit. Here, we need to decide +# whether to split at the first or second expression. This should work similarly to the +# assignment statement formatting where we split from right to left in preview mode. +x = t"aaaaaaaaaaaa {bbbbbbbbbbbbbb} cccccccccccccccccccc {ddddddddddddddd} eeeeeeeeeeeeee" + +# The above example won't split but when we start introducing line breaks: +x = t"aaaaaaaaaaaa {bbbbbbbbbbbbbb} cccccccccccccccccccc { + ddddddddddddddd +} eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa {bbbbbbbbbbbbbb} cccccccccccccccccccc { + ddddddddddddddd +} eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa {bbbbbbbbbbbbbb} cccccccccccccccccccc { + ddddddddddddddd +} eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa {bbbbbbbbbbbbbb} cccccccccccccccccccc { + ddddddddddddddd +} eeeeeeeeeeeeee" + +# But, in case comments are present, we would split at the expression containing the +# comments: +x = t"aaaaaaaaaaaa { + bbbbbbbbbbbbbb # comment 10 +} cccccccccccccccccccc {ddddddddddddddd} eeeeeeeeeeeeee" +x = t"aaaaaaaaaaaa {bbbbbbbbbbbbbb} cccccccccccccccccccc { # comment 11 + ddddddddddddddd +} eeeeeeeeeeeeee" + +# Here, the expression part itself starts with a curly brace so we need to add an extra +# space between the opening curly brace and the expression. +x = t"{ {'x': 1, 'y': 2} }" +# Although the extra space isn't required before the ending curly brace, we add it for +# consistency. +x = t"{ {'x': 1, 'y': 2} }" +x = t"{ {'x': 1, 'y': 2} = }" +x = t"{ # comment 12 + {'x': 1, 'y': 2} +}" +x = t"{ # comment 13 + {'x': 1, 'y': 2} = }" +# But, if there's a format specifier or a conversion flag then we don't need to add +# any whitespace at the end +x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}!s} bbbbbb" +x = t"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}:.3f} bbbbbb" + +# But, in this case, we would split the expression itself because it exceeds the line +# length limit so we need not add the extra space. +xxxxxxx = ( + t"{ {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'} }" +) +# And, split the expression itself because it exceeds the line length. +xxxxxxx = t"{ + { + 'aaaaaaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', + 'cccccccccccccccccccccccccc', + } +}" + +############################################################################################# +# Quotes +############################################################################################# +t"foo 'bar' {x}" +t'foo "bar" {x}' +t'foo "bar" {x}' +t"foo 'bar' {x}" +t"foo {'bar'}" + +t"single quoted '{x}' double quoted \"{x}\"" # Same number of quotes => use preferred quote style +t'single quote \' {x} double quoted "{x}"' # More double quotes => use single quotes +t"single quoted '{x}' double quote \" {x}" # More single quotes => use double quotes + +rf"single quotes ' {x}" # Keep double because `'` can't be escaped +rf'double quotes " {x}' # Keep single because `"` can't be escaped +rf"flip quotes {x}" # Use preferred quotes, because raw string contains now quotes. + +# Here, the formatter will remove the escapes +t"foo {"'bar'"}" +t"foo {'"bar"'}" + +# Quotes inside the expressions have no impact on the quote selection of the outer string. +# Required so that the following two examples result in the same formatting. +t"foo {10 + len('bar')}" +t"foo {10 + len('bar')}" + +# Pre 312, preserve the outer quotes if the t-string contains quotes in the debug expression +t"foo {10 + len("bar")=}" +t"""foo {10 + len('''bar''')=}""" +t"""foo {10 + len('bar')=}""" # Fine to change the quotes because it uses triple quotes + +# Triple-quoted strings +# It's ok to use the same quote char for the inner string if it's single-quoted. +t"""test {"inner"}""" +t"""test {"inner"}""" +# But if the inner string is also triple-quoted then we should preserve the existing quotes. +t"""test {'''inner'''}""" + +# It's not okay to change the quote style if the inner string is triple quoted and contains a quote. +t"{'''other " '''}" +t"{'''other " ''' + 'more'}" +t"{b'''other " '''}" +t"{t'''other " '''}" + +t"""test {t"inner {'''inner inner'''}"}""" +t"""test {t'''inner {"""inner inner"""}'''}""" + +# Magic trailing comma +# +# The expression formatting will result in breaking it across multiple lines with a +# trailing comma but as the expression isn't already broken, we will remove all the line +# breaks which results in the trailing comma being present. This test case makes sure +# that the trailing comma is removed as well. +t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa" + +# And, if the trailing comma is already present, we still need to remove it. +t"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa" + +# Keep this Multiline by breaking it at the square brackets. +t"""aaaaaa { + [ + xxxxxxxx, + yyyyyyyy, + ] +} ccc""" + +# Add the magic trailing comma because the elements don't fit within the line length limit +# when collapsed. +t"aaaaaa { + [ + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + xxxxxxxxxxxx, + yyyyyyyyyyyy, + ] +} ccccccc" + +# Remove the parentheses because they aren't required +xxxxxxxxxxxxxxx = t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbb { + xxxxxxxxxxx # comment 14 + + yyyyyyyyyy +} dddddddddd" + +# Comments + +# No comments should be dropped! +t"{ # comment 15 + # comment 16 + foo # comment 17 + # comment 18 +}" # comment 19 +# comment 20 + +# Single-quoted t-strings with a format specificer can be multiline +t"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc { + variable:.3f +} ddddddddddddddd eeeeeeee" + +# But, if it's triple-quoted then we can't or the format specificer will have a +# trailing newline +t"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {variable:.3f} ddddddddddddddd eeeeeeee""" + +# But, we can break the ones which don't have a format specifier +t"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr {xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { + xxxxxxxxxxxxxxxxxxxx +} bbbbbbbbbbbb""" + +# Throw in a random comment in it but surprise, this is not a comment but just a text +# which is part of the format specifier +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f + # comment +} cccccccccc""" +aaaaaaaaaaa = t"""asaaaaaaaaaaaaaaaa {aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f + # comment} cccccccccc""" + +# Conversion flags +# +# This is not a valid Python code because of the additional whitespace between the `!` +# and conversion type. But, our parser isn't strict about this. This should probably be +# removed once we have a strict parser. +x = t"aaaaaaaaa {x!r}" + +# Even in the case of debug expressions, we only need to preserve the whitespace within +# the expression part of the replacement field. +x = t"aaaaaaaaa { x = !r}" + +# Combine conversion flags with format specifiers +x = t"{x = !s:>0}" +# This is interesting. There can be a comment after the format specifier but only if it's +# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details. +# We'll format is as trailing comments. +x = t"{ + x!s:>0 + # comment 21 +}" + +x = t""" +{ # comment 22 + x = :.0{y # comment 23 + }f}""" + +# Here, the debug expression is in a nested t-string so we should start preserving +# whitespaces from that point onwards. This means we should format the outer t-string. +x = t"""{ + "foo " # comment 24 + + t"{ x = + + }" # comment 25 +} + """ + +# Mix of various features. +t"{ # comment 26 + foo:>{ # after foo + x # after x + } + # comment 27 + # comment 28 +} woah {x}" + +# Assignment statement + +# Even though this t-string has multiline expression, thus allowing us to break it at the +# curly braces, the t-string fits on a single line if it's moved inside the parentheses. +# We should prefer doing that instead. +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" +) + +# Same as above +xxxxxxx = ( + t"{ {'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'} }" +) + +# Similar to the previous example, but the t-string will exceed the line length limit, +# we shouldn't add any parentheses here. +xxxxxxx = t"{ + { + 'aaaaaaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', + 'cccccccccccccccccccccccccc', + } +}" + +# Same as above but with an inline comment. The t-string should be formatted inside the +# parentheses and the comment should be part of the line inside the parentheses. +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment +) + +# Similar to the previous example but this time parenthesizing doesn't work because it +# exceeds the line length. So, avoid parenthesizing this t-string. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression +}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# Similar to the previous example but we start with the parenthesized layout. This should +# remove the parentheses and format the t-string on a single line. This shows that the +# final layout for the formatter is same for this and the previous case. The only +# difference is that in the previous case the expression is already mulitline which means +# the formatter can break it further at the curly braces. +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment loooooooong + +# The following t-strings are going to break because of the trailing comma so we should +# avoid using the best fit layout and instead use the default layout. +# left-to-right +aaaa = t"aaaa { + [ + 1, + 2, + ] +} bbbb" +# right-to-left +aaaa, bbbb = t"aaaa { + [ + 1, + 2, + ] +} bbbb" + +# Using the right-to-left assignment statement variant. +aaaaaaaaaaaaaaaaaa, bbbbbbbbbbb = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeee" # comment +) + +# Here, the t-string layout is flat but it exceeds the line length limit. This shouldn't +# try the custom best fit layout because the t-string doesn't have any split points. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) +# Same as above but without the parentheses to test that it gets formatted to the same +# layout as the previous example. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = ( + t"aaaaaaaaaaaaaaaaaaa {aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc} ddddddddddddddddddd" +) + +# But, the following t-string does have a split point because of the multiline expression. +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaa + bbbbbbbbbbb + cccccccccccccc +} ddddddddddddddddddd" +aaaaaaaaaaaa["bbbbbbbbbbbbbbbb"] = t"aaaaaaaaaaaaaaaaaaa { + aaaaaaaaaaaaaaaaaaaa + + bbbbbbbbbbbbbbbbbbbbb + + cccccccccccccccccccccc + + dddddddddddddddddddddddddddd +} ddddddddddddddddddd" + +# This is an implicitly concatenated t-string but it cannot be joined because otherwise +# it'll exceed the line length limit. So, the two t-strings will be inside parentheses +# instead and the inline comment should be outside the parentheses. +a = ( + t"test{expression}flat" + t"can be {joined} togethereeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +) # inline + +# Similar to the above example but this fits within the line length limit. +a = t"test{expression}flatcan be {joined} togethereeeeeeeeeeeeeeeeeeeeeeeeeee" # inline + +# The following test cases are adopted from implicit string concatenation but for a +# single t-string instead. + +# Don't inline t-strings that contain expressions that are guaranteed to split, e.g. because of a magic trailing comma +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] +}moreeeeeeeeeeeeeeeeeeee" # comment + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + ] +}moreeeeeeeeeeeeeeeeeeee" # comment + +# Don't inline t-strings that contain commented expressions +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a # comment + ] +}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a # comment + ] +}moreeeeeeeeeeeeeeeeeetest" # comment + +# Don't inline t-strings with multiline debug expressions or format specifiers +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + + b=}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeee{ + a=}moreeeeeeeeeeeeeeeeeetest" # comment + +aaaaa[aaaaaaaaaaa] = t"testeeeeeeeeeeeeeeeeeeeeeeeee{a + =}moreeeeeeeeeeeeeeeeeetest" # comment + +# This is not a multiline t-string even though it has a newline after the format specifier. +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment +) + +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f}moreeeeeeeeeeeeeeeeeetest" # comment +) + +# The newline is only considered when it's a tripled-quoted t-string. +aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +aaaaaaaaaaaaaaaaaa = t"""testeeeeeeeeeeeeeeeeeeeeeeeee{a:.3f + }moreeeeeeeeeeeeeeeeeetest""" # comment + +# Remove the parentheses here +aaaaaaaaaaaaaaaaaa = t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + b, + # comment + ] +}moee" # comment +# ... but not here because of the ownline comment +aaaaaaaaaaaaaaaaaa = ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + [ + a, + b, + ] + }moee" + # comment +) + +# t-strings in other positions + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}": + pass + +if t"aaaaaaaaaaa {ttttteeeeeeeeest} more { # comment + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if t"aaaaaaaaaaa { + [ + ttttteeeeeeeeest, + ] +} more {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}": + pass + +if t"aaaaaaaaaaa { + [ + ttttteeeeeeeeest, + ] +} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +if t"aaaaaaaaaaa { + [ + ttttteeeeeeeeest, + ] +} more { + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +}": + pass + +# For loops +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +for a in t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression +}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +for a in ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# With statements +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeee": + pass + +with ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +with t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{ + expression +}moreeeeeeeeeeeeeeeeeeeeeeeeeeeeee": + pass + +with ( + t"testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee{expression}moreeeeeeeeeeeeeeeeeeeeeeeeeeeee" +): + pass + +# Assert statements +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccc{expression}dddddddddd" + +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", t"cccccccccccccccc{ + expression +}dddddddddddddddd" + +assert t"aaaaaaaaa{expression}bbbbbbbbbbbb", ( + t"cccccccccccccccc{expression}dddddddddddddddd" +) + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression +}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccc{expression}dddddddddd" + +assert ( + t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +), t"ccccccc{expression}dddddddddd" + +assert t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{ + expression +}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", t"ccccccccccccccccccccc { + expression +} dddddddddddddddddddddddddd" + +assert ( + t"aaaaaaaaaaaaaaaaaaaaaaaaaaa{expression}bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +), ( + t"cccccccccccccccccccccccccccccccc {expression} ddddddddddddddddddddddddddddddddddddd" +) + +# t-strings as a single argument to a call expression to test whether it's huggable or not. +call(t"{testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}") + +call( + t"{testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}" +) + +call( + t"{ # comment + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }" +) + +call( + t"""aaaaaaaaaaaaaaaa bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""" +) + +call( + t"""aaaaaaaaaaaaaaaa bbbbbbbbbb { + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee + }""" +) + +call(t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb {testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee}""") + +call(t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb { + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment +}""") + +call( + t"""aaaaaaaaaaaaaaaa + bbbbbbbbbb { + testeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee # comment + }""" +) + +call( + t"{ + aaaaaa + + '''test + more''' + }" +) + +# Indentation + +# What should be the indentation? +# https://github.com/astral-sh/ruff/discussions/9785#discussioncomment-8470590 +if indent0: + if indent1: + if indent2: + foo = t"""hello world +hello { + t"aaaaaaa { + [ + 'aaaaaaaaaaaaaaaaaaaaa', + 'bbbbbbbbbbbbbbbbbbbbb', + 'ccccccccccccccccccccc', + 'ddddddddddddddddddddd', + ] + } bbbbbbbb" + + [ + "aaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbb", + "ccccccccccccccccccccc", + "ddddddddddddddddddddd", + ] + } -------- +""" + + +# Implicit concatenated t-string containing quotes +_ = ( + "This string should change its quotes to double quotes" + t"This string uses double quotes in an expression {"it's a quote"}" + t"This t-string does not use any quotes." +) + +# Regression test for https://github.com/astral-sh/ruff/issues/14487 +t"aaaaaaaaaaaaaaaaaaaaaaaaaa {10**27} bbbbbbbbbbbbbbbbbbbbbbbbbb ccccccccccccccccccccccccc" + +# Regression test for https://github.com/astral-sh/ruff/issues/14778 +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' if True else ''}" +t"{'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' if True else ''}" + +# Quotes reuse +t"{'a'}" + +# 312+, it's okay to change the outer quotes even when there's a debug expression using the same quotes +t"foo {10 + len("bar")=}" +t"""foo {10 + len("""bar""")=}""" + +# 312+, it's okay to change the quotes here without creating an invalid t-string +t"{'''other " '''}" +t"{'''other " ''' + 'more'}" +t"{b'''other " '''}" +t"{t'''other " '''}" + + +# Regression tests for https://github.com/astral-sh/ruff/issues/13935 +t'{1: hy "user"}' +t'{1:hy "user"}' +t'{1: abcd "{1}" }' +t'{1: abcd "{"aa"}" }' +t'{1=: "abcd {'aa'}}' +t"{x:a{z:hy \"user\"}} '''" + +# Changing the outer quotes is fine because the format-spec is in a nested expression. +t"{t'{z=:hy "user"}'} '''" + + +# We have to be careful about changing the quotes if the t-string has a debug expression because it is inserted verbatim. +t'{1=: "abcd \'\'}' # Don't change the outer quotes, or it results in a syntax error +t"{1=: abcd \'\'}" # Changing the quotes here is fine because the inner quotes aren't the opposite quotes +t"{1=: abcd \"\"}" # Changing the quotes here is fine because the inner quotes are escaped +# Don't change the quotes in the following cases: +t'{x=:hy "user"} \'\'\'' +t'{x=:a{y:hy "user"}} \'\'\'' +t'{x=:a{y:{z:hy "user"}}} \'\'\'' +t'{x:a{y=:{z:hy "user"}}} \'\'\'' + +# This is fine because the debug expression and format spec are in a nested expression + +t"""{1=: "this" is fine}""" +t"""{1=: "this" is fine}""" # Change quotes to double quotes because they're preferred +t"{1=: {'ab"cd"'}}" # It's okay if the quotes are in an expression part. + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15459 +print(t"{ {1, 2, 3} - {2} }") +print(t"{ {1: 2}.keys() }") +print(t"{({1, 2, 3}) - ({2})}") +print(t"{1, 2, {3}}") +print(t"{(1, 2, {3})}") + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15535 +print(t"{({},)}") # A single item tuple gets parenthesized +print(t"{({}.values(),)}") +print(t"{ {}, 1 }") # A tuple with multiple elements doesn't get parenthesized +print( + t"{ # Tuple with multiple elements that doesn't fit on a single line gets parenthesized + ( + {}, + 1, + ) + }" +) + + +# Regression tests for https://github.com/astral-sh/ruff/issues/15536 +print(t"{ {}, 1 }") +``` diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py b/crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py new file mode 100644 index 0000000000..257a0e1209 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.14"} +t"{}" +t"{ }" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py new file mode 100644 index 0000000000..dcea20f590 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.14"} +t"{x!z}" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py new file mode 100644 index 0000000000..61fb5815ba --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.14"} +t"{x!123}" +t"{x!'a'}" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py new file mode 100644 index 0000000000..77bf4eb55f --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py @@ -0,0 +1,5 @@ +# parse_options: {"target-version": "3.14"} +# Starred expression inside t-string has a minimum precedence of bitwise or. +t"{*}" +t"{*x and y}" +t"{*yield x}" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py b/crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py new file mode 100644 index 0000000000..0d9e70011c --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py @@ -0,0 +1,2 @@ +# parse_options: {"target-version": "3.14"} +t"{lambda x: x}" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py b/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py new file mode 100644 index 0000000000..b943b533e5 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py @@ -0,0 +1,6 @@ +# parse_options: {"target-version": "3.14"} +t"{" +t"{foo!r" +t"{foo=" +t"{" +t"""{""" diff --git a/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py b/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py new file mode 100644 index 0000000000..cced3bb064 --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py @@ -0,0 +1,3 @@ +# parse_options: {"target-version": "3.14"} +t"hello {x:" +t"hello {x:.3f" diff --git a/crates/ruff_python_parser/resources/inline/ok/param_with_annotation.py b/crates/ruff_python_parser/resources/inline/ok/param_with_annotation.py index e109ecaebc..c8c0425ce6 100644 --- a/crates/ruff_python_parser/resources/inline/ok/param_with_annotation.py +++ b/crates/ruff_python_parser/resources/inline/ok/param_with_annotation.py @@ -1,3 +1,2 @@ def foo(arg: int): ... def foo(arg: lambda x: x): ... -def foo(arg: (x := int)): ... diff --git a/crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py b/crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py new file mode 100644 index 0000000000..18cfc9a08f --- /dev/null +++ b/crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py @@ -0,0 +1,10 @@ +# parse_options: {"target-version": "3.14"} +t'Magic wand: { bag['wand'] }' # nested quotes +t"{'\n'.join(a)}" # escape sequence +t'''A complex trick: { + bag['bag'] # comment +}''' +t"{t"{t"{t"{t"{t"{1+1}"}"}"}"}"}" # arbitrary nesting +t"{t'''{"nested"} inner'''} outer" # nested (triple) quotes +t"test {a \ + } more" # line continuation diff --git a/crates/ruff_python_parser/resources/valid/expressions/t_string.py b/crates/ruff_python_parser/resources/valid/expressions/t_string.py new file mode 100644 index 0000000000..0bb6278492 --- /dev/null +++ b/crates/ruff_python_parser/resources/valid/expressions/t_string.py @@ -0,0 +1,74 @@ +# Empty t-strings +t"" +t"" +t'' +t"""""" +t'''''' + +t"{" t"}" +t"{foo!s}" +t"{3,}" +t"{3!=4:}" +t'{3:{"}"}>10}' +t'{3:{"{"}>10}' +t"{ foo = }" +t"{ foo = :.3f }" +t"{ foo = !s }" +t"{ 1, 2 = }" +t'{t"{3.1415=:.1f}":*^20}' + +{"foo " t"bar {x + y} " "baz": 10} +match foo: + case "one": + pass + case "implicitly " "concatenated": + pass + +t"\{foo}\{bar:\}" +t"\\{{foo\\}}" +t"""{ + foo:x + y + z +}""" +t"{ ( foo ) = }" + +t"normal {foo} {{another}} {bar} {{{three}}}" +t"normal {foo!a} {bar!s} {baz!r} {foobar}" +t"normal {x:y + 2}" +t"{x:{{1}.pop()}}" +t"{(lambda x:{x})}" +t"{x =}" +t"{ x = }" +t"{x=!a}" +t"{x:.3f!r =}" +t"{x = !r :.3f}" +t"{x:.3f=!r}" +"hello" t"{x}" +t"{x}" t"{y}" +t"{x}" "world" +t"Invalid args in command: {command, *args}" +"foo" t"{x}" "bar" +( + t"a" + t"b" + "c" + rt"d" + fr"e" +) + +# With unicode strings +u"foo" t"{bar}" "baz" " some" +"foo" t"{bar}" u"baz" " some" +"foo" t"{bar}" "baz" u" some" +u"foo" t"bar {baz} really" u"bar" "no" + + +# With f-strings +f"{this}" t"{that}" +t"{this}"f"{that}" +t"{this}" "that" f"{other}" +f"one {this} two" "that" t"three {other} four" + +# Nesting +t"{f"{t"{this}"}"}" diff --git a/crates/ruff_python_parser/src/error.rs b/crates/ruff_python_parser/src/error.rs index 2620f67c77..b7bacffb57 100644 --- a/crates/ruff_python_parser/src/error.rs +++ b/crates/ruff_python_parser/src/error.rs @@ -3,7 +3,7 @@ use std::fmt::{self, Display}; use ruff_python_ast::PythonVersion; use ruff_text_size::{Ranged, TextRange}; -use crate::TokenKind; +use crate::{TokenKind, string::InterpolatedStringKind}; /// Represents represent errors that occur during parsing and are /// returned by the `parse_*` functions. @@ -48,9 +48,9 @@ impl ParseError { } } -/// Represents the different types of errors that can occur during parsing of an f-string. +/// Represents the different types of errors that can occur during parsing of an f-string or t-string. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum FStringErrorType { +pub enum InterpolatedStringErrorType { /// Expected a right brace after an opened left brace. UnclosedLbrace, /// An invalid conversion flag was encountered. @@ -65,9 +65,9 @@ pub enum FStringErrorType { LambdaWithoutParentheses, } -impl std::fmt::Display for FStringErrorType { +impl std::fmt::Display for InterpolatedStringErrorType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - use FStringErrorType::{ + use InterpolatedStringErrorType::{ InvalidConversionFlag, LambdaWithoutParentheses, SingleRbrace, UnclosedLbrace, UnterminatedString, UnterminatedTripleQuotedString, }; @@ -177,12 +177,26 @@ pub enum ParseErrorType { /// An unexpected token was found at the end of an expression parsing UnexpectedExpressionToken, - /// An f-string error containing the [`FStringErrorType`]. - FStringError(FStringErrorType), + /// An f-string error containing the [`InterpolatedStringErrorType`]. + FStringError(InterpolatedStringErrorType), + /// A t-string error containing the [`InterpolatedStringErrorType`]. + TStringError(InterpolatedStringErrorType), /// Parser encountered an error during lexing. Lexical(LexicalErrorType), } +impl ParseErrorType { + pub(crate) fn from_interpolated_string_error( + error: InterpolatedStringErrorType, + string_kind: InterpolatedStringKind, + ) -> Self { + match string_kind { + InterpolatedStringKind::FString => Self::FStringError(error), + InterpolatedStringKind::TString => Self::TStringError(error), + } + } +} + impl std::error::Error for ParseErrorType {} impl std::fmt::Display for ParseErrorType { @@ -292,6 +306,9 @@ impl std::fmt::Display for ParseErrorType { ParseErrorType::FStringError(fstring_error) => { write!(f, "f-string: {fstring_error}") } + ParseErrorType::TStringError(tstring_error) => { + write!(f, "t-string: {tstring_error}") + } ParseErrorType::UnexpectedExpressionToken => { write!(f, "Unexpected token at the end of an expression") } @@ -375,8 +392,10 @@ pub enum LexicalErrorType { IndentationError, /// An unrecognized token was encountered. UnrecognizedToken { tok: char }, - /// An f-string error containing the [`FStringErrorType`]. - FStringError(FStringErrorType), + /// An f-string error containing the [`InterpolatedStringErrorType`]. + FStringError(InterpolatedStringErrorType), + /// A t-string error containing the [`InterpolatedStringErrorType`]. + TStringError(InterpolatedStringErrorType), /// Invalid character encountered in a byte literal. InvalidByteLiteral, /// An unexpected character was encountered after a line continuation. @@ -389,11 +408,24 @@ pub enum LexicalErrorType { impl std::error::Error for LexicalErrorType {} +impl LexicalErrorType { + pub(crate) fn from_interpolated_string_error( + error: InterpolatedStringErrorType, + string_kind: InterpolatedStringKind, + ) -> Self { + match string_kind { + InterpolatedStringKind::FString => Self::FStringError(error), + InterpolatedStringKind::TString => Self::TStringError(error), + } + } +} + impl std::fmt::Display for LexicalErrorType { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { LexicalErrorType::StringError => write!(f, "Got unexpected string"), LexicalErrorType::FStringError(error) => write!(f, "f-string: {error}"), + LexicalErrorType::TStringError(error) => write!(f, "t-string: {error}"), LexicalErrorType::InvalidByteLiteral => { write!(f, "bytes can only contain ASCII literal characters") } @@ -848,6 +880,12 @@ pub enum UnsupportedSyntaxErrorKind { /// /// [PEP 758]: https://peps.python.org/pep-0758/ UnparenthesizedExceptionTypes, + /// Represents the use of a template string (t-string) + /// literal prior to the implementation of [PEP 750] + /// in Python 3.14. + /// + /// [PEP 750]: https://peps.python.org/pep-0750/ + TemplateStrings, } impl Display for UnsupportedSyntaxError { @@ -928,6 +966,7 @@ impl Display for UnsupportedSyntaxError { UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes => { "Multiple exception types must be parenthesized" } + UnsupportedSyntaxErrorKind::TemplateStrings => "Cannot use t-strings", }; write!( @@ -998,6 +1037,7 @@ impl UnsupportedSyntaxErrorKind { UnsupportedSyntaxErrorKind::UnparenthesizedExceptionTypes => { Change::Added(PythonVersion::PY314) } + UnsupportedSyntaxErrorKind::TemplateStrings => Change::Added(PythonVersion::PY314), } } diff --git a/crates/ruff_python_parser/src/lexer.rs b/crates/ruff_python_parser/src/lexer.rs index 9e3d47f890..d04f377678 100644 --- a/crates/ruff_python_parser/src/lexer.rs +++ b/crates/ruff_python_parser/src/lexer.rs @@ -18,15 +18,17 @@ use ruff_python_trivia::is_python_whitespace; use ruff_text_size::{TextLen, TextRange, TextSize}; use crate::Mode; -use crate::error::{FStringErrorType, LexicalError, LexicalErrorType}; +use crate::error::{InterpolatedStringErrorType, LexicalError, LexicalErrorType}; use crate::lexer::cursor::{Cursor, EOF_CHAR}; -use crate::lexer::fstring::{FStringContext, FStrings, FStringsCheckpoint}; use crate::lexer::indentation::{Indentation, Indentations, IndentationsCheckpoint}; +use crate::lexer::interpolated_string::{ + InterpolatedStringContext, InterpolatedStrings, InterpolatedStringsCheckpoint, +}; use crate::token::{TokenFlags, TokenKind, TokenValue}; mod cursor; -mod fstring; mod indentation; +mod interpolated_string; const BOM: char = '\u{feff}'; @@ -65,8 +67,8 @@ pub struct Lexer<'src> { /// Lexer mode. mode: Mode, - /// F-string contexts. - fstrings: FStrings, + /// F-string and t-string contexts. + interpolated_strings: InterpolatedStrings, /// Errors encountered while lexing. errors: Vec, @@ -102,7 +104,7 @@ impl<'src> Lexer<'src> { indentations: Indentations::default(), pending_indentation: None, mode, - fstrings: FStrings::default(), + interpolated_strings: InterpolatedStrings::default(), errors: Vec::new(), }; @@ -162,11 +164,11 @@ impl<'src> Lexer<'src> { } fn lex_token(&mut self) -> TokenKind { - if let Some(fstring) = self.fstrings.current() { - if !fstring.is_in_expression(self.nesting) { - if let Some(token) = self.lex_fstring_middle_or_end() { - if matches!(token, TokenKind::FStringEnd) { - self.fstrings.pop(); + if let Some(interpolated_string) = self.interpolated_strings.current() { + if !interpolated_string.is_in_interpolation(self.nesting) { + if let Some(token) = self.lex_interpolated_string_middle_or_end() { + if token.is_interpolated_string_end() { + self.interpolated_strings.pop(); } return token; } @@ -506,23 +508,26 @@ impl<'src> Lexer<'src> { TokenKind::Lbrace } '}' => { - if let Some(fstring) = self.fstrings.current_mut() { - if fstring.nesting() == self.nesting { - return self.push_error(LexicalError::new( - LexicalErrorType::FStringError(FStringErrorType::SingleRbrace), - self.token_range(), - )); + if let Some(interpolated_string) = self.interpolated_strings.current_mut() { + if interpolated_string.nesting() == self.nesting { + let error_type = LexicalErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::SingleRbrace, + interpolated_string.kind(), + ); + return self.push_error(LexicalError::new(error_type, self.token_range())); } - fstring.try_end_format_spec(self.nesting); + interpolated_string.try_end_format_spec(self.nesting); } self.nesting = self.nesting.saturating_sub(1); TokenKind::Rbrace } ':' => { if self - .fstrings + .interpolated_strings .current_mut() - .is_some_and(|fstring| fstring.try_start_format_spec(self.nesting)) + .is_some_and(|interpolated_string| { + interpolated_string.try_start_format_spec(self.nesting) + }) { TokenKind::Colon } else if self.cursor.eat_char('=') { @@ -573,8 +578,8 @@ impl<'src> Lexer<'src> { self.state = State::AfterNewline; TokenKind::Newline } else { - if let Some(fstring) = self.fstrings.current_mut() { - fstring.try_end_format_spec(self.nesting); + if let Some(interpolated_string) = self.interpolated_strings.current_mut() { + interpolated_string.try_end_format_spec(self.nesting); } TokenKind::NonLogicalNewline }; @@ -586,8 +591,8 @@ impl<'src> Lexer<'src> { self.state = State::AfterNewline; TokenKind::Newline } else { - if let Some(fstring) = self.fstrings.current_mut() { - fstring.try_end_format_spec(self.nesting); + if let Some(interpolated_string) = self.interpolated_strings.current_mut() { + interpolated_string.try_end_format_spec(self.nesting); } TokenKind::NonLogicalNewline }; @@ -610,7 +615,7 @@ impl<'src> Lexer<'src> { /// Lex an identifier. Also used for keywords and string/bytes literals with a prefix. fn lex_identifier(&mut self, first: char) -> TokenKind { - // Detect potential string like rb'' b'' f'' u'' r'' + // Detect potential string like rb'' b'' f'' t'' u'' r'' let quote = match (first, self.cursor.first()) { (_, quote @ ('\'' | '"')) => self.try_single_char_prefix(first).then(|| { self.cursor.bump(); @@ -627,8 +632,10 @@ impl<'src> Lexer<'src> { }; if let Some(quote) = quote { - if self.current_flags.is_f_string() { - return self.lex_fstring_start(quote); + if self.current_flags.is_interpolated_string() { + if let Some(kind) = self.lex_interpolated_string_start(quote) { + return kind; + } } return self.lex_string(quote); @@ -711,6 +718,7 @@ impl<'src> Lexer<'src> { fn try_single_char_prefix(&mut self, first: char) -> bool { match first { 'f' | 'F' => self.current_flags |= TokenFlags::F_STRING, + 't' | 'T' => self.current_flags |= TokenFlags::T_STRING, 'u' | 'U' => self.current_flags |= TokenFlags::UNICODE_STRING, 'b' | 'B' => self.current_flags |= TokenFlags::BYTE_STRING, 'r' => self.current_flags |= TokenFlags::RAW_STRING_LOWERCASE, @@ -730,6 +738,12 @@ impl<'src> Lexer<'src> { ['R', 'f' | 'F'] | ['f' | 'F', 'R'] => { self.current_flags |= TokenFlags::F_STRING | TokenFlags::RAW_STRING_UPPERCASE; } + ['r', 't' | 'T'] | ['t' | 'T', 'r'] => { + self.current_flags |= TokenFlags::T_STRING | TokenFlags::RAW_STRING_LOWERCASE; + } + ['R', 't' | 'T'] | ['t' | 'T', 'R'] => { + self.current_flags |= TokenFlags::T_STRING | TokenFlags::RAW_STRING_UPPERCASE; + } ['r', 'b' | 'B'] | ['b' | 'B', 'r'] => { self.current_flags |= TokenFlags::BYTE_STRING | TokenFlags::RAW_STRING_LOWERCASE; } @@ -741,8 +755,8 @@ impl<'src> Lexer<'src> { true } - /// Lex a f-string start token. - fn lex_fstring_start(&mut self, quote: char) -> TokenKind { + /// Lex a f-string or t-string start token if positioned at the start of an f-string or t-string. + fn lex_interpolated_string_start(&mut self, quote: char) -> Option { #[cfg(debug_assertions)] debug_assert_eq!(self.cursor.previous(), quote); @@ -754,27 +768,31 @@ impl<'src> Lexer<'src> { self.current_flags |= TokenFlags::TRIPLE_QUOTED_STRING; } - self.fstrings - .push(FStringContext::new(self.current_flags, self.nesting)); + let ftcontext = InterpolatedStringContext::new(self.current_flags, self.nesting)?; - TokenKind::FStringStart + let kind = ftcontext.kind(); + + self.interpolated_strings.push(ftcontext); + + Some(kind.start_token()) } - /// Lex a f-string middle or end token. - fn lex_fstring_middle_or_end(&mut self) -> Option { + /// Lex an f-string or t-string middle or end token. + fn lex_interpolated_string_middle_or_end(&mut self) -> Option { // SAFETY: Safe because the function is only called when `self.fstrings` is not empty. - let fstring = self.fstrings.current().unwrap(); + let interpolated_string = self.interpolated_strings.current().unwrap(); + let string_kind = interpolated_string.kind(); // Check if we're at the end of the f-string. - if fstring.is_triple_quoted() { - let quote_char = fstring.quote_char(); + if interpolated_string.is_triple_quoted() { + let quote_char = interpolated_string.quote_char(); if self.cursor.eat_char3(quote_char, quote_char, quote_char) { - self.current_flags = fstring.flags(); - return Some(TokenKind::FStringEnd); + self.current_flags = interpolated_string.flags(); + return Some(string_kind.end_token()); } - } else if self.cursor.eat_char(fstring.quote_char()) { - self.current_flags = fstring.flags(); - return Some(TokenKind::FStringEnd); + } else if self.cursor.eat_char(interpolated_string.quote_char()) { + self.current_flags = interpolated_string.flags(); + return Some(string_kind.end_token()); } // We have to decode `{{` and `}}` into `{` and `}` respectively. As an @@ -786,7 +804,7 @@ impl<'src> Lexer<'src> { let mut last_offset = self.offset(); // This isn't going to change for the duration of the loop. - let in_format_spec = fstring.is_in_format_spec(self.nesting); + let in_format_spec = interpolated_string.is_in_format_spec(self.nesting); let mut in_named_unicode = false; @@ -796,18 +814,18 @@ impl<'src> Lexer<'src> { // in the source code and the one returned by `self.cursor.first()` when // we reach the end of the source code. EOF_CHAR if self.cursor.is_eof() => { - let error = if fstring.is_triple_quoted() { - FStringErrorType::UnterminatedTripleQuotedString + let error = if interpolated_string.is_triple_quoted() { + InterpolatedStringErrorType::UnterminatedTripleQuotedString } else { - FStringErrorType::UnterminatedString + InterpolatedStringErrorType::UnterminatedString }; - self.fstrings.pop(); + self.interpolated_strings.pop(); return Some(self.push_error(LexicalError::new( - LexicalErrorType::FStringError(error), + LexicalErrorType::from_interpolated_string_error(error, string_kind), self.token_range(), ))); } - '\n' | '\r' if !fstring.is_triple_quoted() => { + '\n' | '\r' if !interpolated_string.is_triple_quoted() => { // If we encounter a newline while we're in a format spec, then // we stop here and let the lexer emit the newline token. // @@ -815,9 +833,12 @@ impl<'src> Lexer<'src> { if in_format_spec { break; } - self.fstrings.pop(); + self.interpolated_strings.pop(); return Some(self.push_error(LexicalError::new( - LexicalErrorType::FStringError(FStringErrorType::UnterminatedString), + LexicalErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::UnterminatedString, + string_kind, + ), self.token_range(), ))); } @@ -827,7 +848,7 @@ impl<'src> Lexer<'src> { // Don't consume `{` or `}` as we want them to be emitted as tokens. // They will be handled in the next iteration. continue; - } else if !fstring.is_raw_string() { + } else if !interpolated_string.is_raw_string() { if self.cursor.eat_char2('N', '{') { in_named_unicode = true; continue; @@ -840,8 +861,8 @@ impl<'src> Lexer<'src> { self.cursor.bump(); } } - quote @ ('\'' | '"') if quote == fstring.quote_char() => { - if let Some(triple_quotes) = fstring.triple_quotes() { + quote @ ('\'' | '"') if quote == interpolated_string.quote_char() => { + if let Some(triple_quotes) = interpolated_string.triple_quotes() { if self.cursor.rest().starts_with(triple_quotes) { break; } @@ -892,10 +913,10 @@ impl<'src> Lexer<'src> { normalized }; - self.current_value = TokenValue::FStringMiddle(value.into_boxed_str()); + self.current_value = TokenValue::InterpolatedStringMiddle(value.into_boxed_str()); - self.current_flags = fstring.flags(); - Some(TokenKind::FStringMiddle) + self.current_flags = interpolated_string.flags(); + Some(string_kind.middle_token()) } /// Lex a string literal. @@ -1403,9 +1424,9 @@ impl<'src> Lexer<'src> { // i.e., it recovered from an unclosed parenthesis (`(`, `[`, or `{`). self.nesting -= 1; - // The lexer can't be moved back for a triple-quoted f-string because the newlines are - // part of the f-string itself, so there is no newline token to be emitted. - if self.current_flags.is_triple_quoted_fstring() { + // The lexer can't be moved back for a triple-quoted f/t-string because the newlines are + // part of the f/t-string itself, so there is no newline token to be emitted. + if self.current_flags.is_triple_quoted_interpolated_string() { return false; } @@ -1478,7 +1499,7 @@ impl<'src> Lexer<'src> { nesting: self.nesting, indentations_checkpoint: self.indentations.checkpoint(), pending_indentation: self.pending_indentation, - fstrings_checkpoint: self.fstrings.checkpoint(), + interpolated_strings_checkpoint: self.interpolated_strings.checkpoint(), errors_position: self.errors.len(), } } @@ -1495,7 +1516,7 @@ impl<'src> Lexer<'src> { nesting, indentations_checkpoint, pending_indentation, - fstrings_checkpoint, + interpolated_strings_checkpoint, errors_position, } = checkpoint; @@ -1512,7 +1533,8 @@ impl<'src> Lexer<'src> { self.nesting = nesting; self.indentations.rewind(indentations_checkpoint); self.pending_indentation = pending_indentation; - self.fstrings.rewind(fstrings_checkpoint); + self.interpolated_strings + .rewind(interpolated_strings_checkpoint); self.errors.truncate(errors_position); } @@ -1531,7 +1553,7 @@ pub(crate) struct LexerCheckpoint { nesting: u32, indentations_checkpoint: IndentationsCheckpoint, pending_indentation: Option, - fstrings_checkpoint: FStringsCheckpoint, + interpolated_strings_checkpoint: InterpolatedStringsCheckpoint, errors_position: usize, } @@ -2450,6 +2472,190 @@ f"{(lambda x:{x})}" assert_snapshot!(lex_source(source)); } + #[test] + fn test_empty_tstrings() { + let source = r#"t"" "" t"" t'' '' t"""""" t''''''"#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_prefix() { + let source = r#"t"" t"" rt"" rt"" Rt"" Rt"" tr"" Tr"" tR"" TR"""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring() { + let source = r#"t"normal {foo} {{another}} {bar} {{{three}}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_parentheses() { + let source = r#"t"{}" t"{{}}" t" {}" t"{{{}}}" t"{{{{}}}}" t" {} {{}} {{{}}} {{{{}}}} ""#; + assert_snapshot!(lex_source(source)); + } + + fn tstring_single_quote_escape_eol(eol: &str) -> LexerOutput { + let source = format!(r"t'text \{eol} more text'"); + lex_source(&source) + } + + #[test] + fn test_tstring_single_quote_escape_unix_eol() { + assert_snapshot!(tstring_single_quote_escape_eol(UNIX_EOL)); + } + + #[test] + fn test_tstring_single_quote_escape_mac_eol() { + assert_snapshot!(tstring_single_quote_escape_eol(MAC_EOL)); + } + + #[test] + fn test_tstring_single_quote_escape_windows_eol() { + assert_snapshot!(tstring_single_quote_escape_eol(WINDOWS_EOL)); + } + + #[test] + fn test_tstring_escape() { + let source = r#"t"\{x:\"\{x}} \"\"\ + end""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_escape_braces() { + let source = r"t'\{foo}' t'\\{foo}' t'\{{foo}}' t'\\{{foo}}'"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_escape_raw() { + let source = r#"rt"\{x:\"\{x}} \"\"\ + end""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_named_unicode() { + let source = r#"t"\N{BULLET} normal \Nope \N""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_named_unicode_raw() { + let source = r#"rt"\N{BULLET} normal""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_named_expression() { + let source = r#"t"{x:=10} {(x:=10)} {x,{y:=10}} {[x:=10]}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_format_spec() { + let source = r#"t"{foo:} {x=!s:.3f} {x:.{y}f} {'':*^{1:{1}}} {x:{{1}.pop()}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_multiline_format_spec() { + // The last t-string is invalid syntactically but we should still lex it. + // Note that the `b` is a `Name` token and not a `TStringMiddle` token. + let source = r"t'''__{ + x:d +}__''' +t'''__{ + x:a + b + c +}__''' +t'__{ + x:d +}__' +t'__{ + x:a + b +}__' +"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_conversion() { + let source = r#"t"{x!s} {x=!r} {x:.3f!r} {{x!r}}""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_nested() { + let source = r#"t"foo {t"bar {x + t"{wow}"}"} baz" t'foo {t'bar'} some {t"another"}'"#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_expression_multiline() { + let source = r#"t"first { + x + * + y +} second""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_multiline() { + let source = r#"t""" +hello + world +""" t''' + world +hello +''' t"some {t"""multiline +allowed {x}"""} string""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_comments() { + let source = r#"t""" +# not a comment { # comment { + x +} # not a comment +""""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_ipy_escape_command() { + let source = r#"t"foo {!pwd} bar""#; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_lambda_expression() { + let source = r#" +t"{lambda x:{x}}" +t"{(lambda x:{x})}" +"# + .trim(); + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_tstring_with_nul_char() { + let source = r"t'\0'"; + assert_snapshot!(lex_source(source)); + } + + #[test] + fn test_nested_t_and_fstring() { + let source = r#"t"foo {f"bar {x + t"{wow}"}"} baz" f'foo {t'bar'!r} some {f"another"}'"#; + assert_snapshot!(lex_source(source)); + } + #[test] fn test_match_softkeyword_in_notebook() { let source = r"match foo: @@ -2458,7 +2664,7 @@ f"{(lambda x:{x})}" assert_snapshot!(lex_jupyter_source(source)); } - fn lex_fstring_error(source: &str) -> FStringErrorType { + fn lex_fstring_error(source: &str) -> InterpolatedStringErrorType { let output = lex(source, Mode::Module, TextSize::default()); match output .errors @@ -2474,7 +2680,9 @@ f"{(lambda x:{x})}" #[test] fn test_fstring_error() { - use FStringErrorType::{SingleRbrace, UnterminatedString, UnterminatedTripleQuotedString}; + use InterpolatedStringErrorType::{ + SingleRbrace, UnterminatedString, UnterminatedTripleQuotedString, + }; assert_eq!(lex_fstring_error("f'}'"), SingleRbrace); assert_eq!(lex_fstring_error("f'{{}'"), SingleRbrace); @@ -2499,4 +2707,48 @@ f"{(lambda x:{x})}" UnterminatedTripleQuotedString ); } + + fn lex_tstring_error(source: &str) -> InterpolatedStringErrorType { + let output = lex(source, Mode::Module, TextSize::default()); + match output + .errors + .into_iter() + .next() + .expect("lexer should give at least one error") + .into_error() + { + LexicalErrorType::TStringError(error) => error, + err => panic!("Expected TStringError: {err:?}"), + } + } + + #[test] + fn test_tstring_error() { + use InterpolatedStringErrorType::{ + SingleRbrace, UnterminatedString, UnterminatedTripleQuotedString, + }; + + assert_eq!(lex_tstring_error("t'}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{{}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{{}}}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'foo}'"), SingleRbrace); + assert_eq!(lex_tstring_error(r"t'\u007b}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{a:b}}'"), SingleRbrace); + assert_eq!(lex_tstring_error("t'{3:}}>10}'"), SingleRbrace); + assert_eq!(lex_tstring_error(r"t'\{foo}\}'"), SingleRbrace); + + assert_eq!(lex_tstring_error(r#"t""#), UnterminatedString); + assert_eq!(lex_tstring_error(r"t'"), UnterminatedString); + + assert_eq!(lex_tstring_error(r#"t""""#), UnterminatedTripleQuotedString); + assert_eq!(lex_tstring_error(r"t'''"), UnterminatedTripleQuotedString); + assert_eq!( + lex_tstring_error(r#"t"""""#), + UnterminatedTripleQuotedString + ); + assert_eq!( + lex_tstring_error(r#"t""""""#), + UnterminatedTripleQuotedString + ); + } } diff --git a/crates/ruff_python_parser/src/lexer/fstring.rs b/crates/ruff_python_parser/src/lexer/interpolated_string.rs similarity index 61% rename from crates/ruff_python_parser/src/lexer/fstring.rs rename to crates/ruff_python_parser/src/lexer/interpolated_string.rs index 7b702a77b7..826edfa796 100644 --- a/crates/ruff_python_parser/src/lexer/fstring.rs +++ b/crates/ruff_python_parser/src/lexer/interpolated_string.rs @@ -1,31 +1,45 @@ use ruff_python_ast::StringFlags; +use crate::string::InterpolatedStringKind; + use super::TokenFlags; -/// The context representing the current f-string that the lexer is in. +/// The context representing the current f-string or t-string that the lexer is in. #[derive(Clone, Debug)] -pub(crate) struct FStringContext { +pub(crate) struct InterpolatedStringContext { flags: TokenFlags, - /// The level of nesting for the lexer when it entered the current f-string. + /// The level of nesting for the lexer when it entered the current f/t-string. /// The nesting level includes all kinds of parentheses i.e., round, square, /// and curly. nesting: u32, - /// The current depth of format spec for the current f-string. This is because + /// The current depth of format spec for the current f/t-string. This is because /// there can be multiple format specs nested for the same f-string. /// For example, `{a:{b:{c}}}` has 3 format specs. format_spec_depth: u32, } -impl FStringContext { - pub(crate) const fn new(flags: TokenFlags, nesting: u32) -> Self { - assert!(flags.is_f_string()); +impl InterpolatedStringContext { + pub(crate) const fn new(flags: TokenFlags, nesting: u32) -> Option { + if flags.is_interpolated_string() { + Some(Self { + flags, + nesting, + format_spec_depth: 0, + }) + } else { + None + } + } - Self { - flags, - nesting, - format_spec_depth: 0, + pub(crate) fn kind(&self) -> InterpolatedStringKind { + if self.flags.is_f_string() { + InterpolatedStringKind::FString + } else if self.flags.is_t_string() { + InterpolatedStringKind::TString + } else { + unreachable!("Can only be constructed when f-string or t-string flag is present") } } @@ -68,15 +82,15 @@ impl FStringContext { current_nesting.saturating_sub(self.nesting) } - /// Returns `true` if the lexer is in a f-string expression i.e., between + /// Returns `true` if the lexer is in an f-string expression or t-string interpolation i.e., between /// two curly braces. - pub(crate) const fn is_in_expression(&self, current_nesting: u32) -> bool { + pub(crate) const fn is_in_interpolation(&self, current_nesting: u32) -> bool { self.open_parentheses_count(current_nesting) > self.format_spec_depth } /// Returns `true` if the lexer is in a f-string format spec i.e., after a colon. pub(crate) const fn is_in_format_spec(&self, current_nesting: u32) -> bool { - self.format_spec_depth > 0 && !self.is_in_expression(current_nesting) + self.format_spec_depth > 0 && !self.is_in_interpolation(current_nesting) } /// Returns `true` if the context is in a valid position to start format spec @@ -106,38 +120,38 @@ impl FStringContext { } } -/// The f-strings stack is used to keep track of all the f-strings that the -/// lexer encounters. This is necessary because f-strings can be nested. +/// The interpolated strings stack is used to keep track of all the f-strings and t-strings that the +/// lexer encounters. This is necessary because f-strings and t-strings can be nested. #[derive(Debug, Default)] -pub(crate) struct FStrings { - stack: Vec, +pub(crate) struct InterpolatedStrings { + stack: Vec, } -impl FStrings { - pub(crate) fn push(&mut self, context: FStringContext) { +impl InterpolatedStrings { + pub(crate) fn push(&mut self, context: InterpolatedStringContext) { self.stack.push(context); } - pub(crate) fn pop(&mut self) -> Option { + pub(crate) fn pop(&mut self) -> Option { self.stack.pop() } - pub(crate) fn current(&self) -> Option<&FStringContext> { + pub(crate) fn current(&self) -> Option<&InterpolatedStringContext> { self.stack.last() } - pub(crate) fn current_mut(&mut self) -> Option<&mut FStringContext> { + pub(crate) fn current_mut(&mut self) -> Option<&mut InterpolatedStringContext> { self.stack.last_mut() } - pub(crate) fn checkpoint(&self) -> FStringsCheckpoint { - FStringsCheckpoint(self.stack.clone()) + pub(crate) fn checkpoint(&self) -> InterpolatedStringsCheckpoint { + InterpolatedStringsCheckpoint(self.stack.clone()) } - pub(crate) fn rewind(&mut self, checkpoint: FStringsCheckpoint) { + pub(crate) fn rewind(&mut self, checkpoint: InterpolatedStringsCheckpoint) { self.stack = checkpoint.0; } } #[derive(Debug, Clone)] -pub(crate) struct FStringsCheckpoint(Vec); +pub(crate) struct InterpolatedStringsCheckpoint(Vec); diff --git a/crates/ruff_python_parser/src/lib.rs b/crates/ruff_python_parser/src/lib.rs index fa5230a016..346bd89aa8 100644 --- a/crates/ruff_python_parser/src/lib.rs +++ b/crates/ruff_python_parser/src/lib.rs @@ -67,8 +67,8 @@ use std::iter::FusedIterator; use std::ops::Deref; pub use crate::error::{ - FStringErrorType, LexicalErrorType, ParseError, ParseErrorType, UnsupportedSyntaxError, - UnsupportedSyntaxErrorKind, + InterpolatedStringErrorType, LexicalErrorType, ParseError, ParseErrorType, + UnsupportedSyntaxError, UnsupportedSyntaxErrorKind, }; pub use crate::parser::ParseOptions; pub use crate::token::{Token, TokenKind}; diff --git a/crates/ruff_python_parser/src/parser/expression.rs b/crates/ruff_python_parser/src/parser/expression.rs index 3a3d91ce75..73ce206664 100644 --- a/crates/ruff_python_parser/src/parser/expression.rs +++ b/crates/ruff_python_parser/src/parser/expression.rs @@ -6,22 +6,27 @@ use rustc_hash::{FxBuildHasher, FxHashSet}; use ruff_python_ast::name::Name; use ruff_python_ast::{ - self as ast, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, FStringElement, FStringElements, - IpyEscapeKind, Number, Operator, OperatorPrecedence, StringFlags, UnaryOp, + self as ast, AnyStringFlags, BoolOp, CmpOp, ConversionFlag, Expr, ExprContext, FString, + InterpolatedStringElement, InterpolatedStringElements, IpyEscapeKind, Number, Operator, + OperatorPrecedence, StringFlags, TString, UnaryOp, }; use ruff_text_size::{Ranged, TextLen, TextRange, TextSize}; use crate::error::{FStringKind, StarTupleKind, UnparenthesizedNamedExprKind}; use crate::parser::progress::ParserProgress; use crate::parser::{FunctionKind, Parser, helpers}; -use crate::string::{StringType, parse_fstring_literal_element, parse_string_literal}; +use crate::string::{ + InterpolatedStringKind, StringType, parse_interpolated_string_literal_element, + parse_string_literal, +}; use crate::token::{TokenKind, TokenValue}; use crate::token_set::TokenSet; use crate::{ - FStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError, UnsupportedSyntaxErrorKind, + InterpolatedStringErrorType, Mode, ParseErrorType, UnsupportedSyntaxError, + UnsupportedSyntaxErrorKind, }; -use super::{FStringElementsKind, Parenthesized, RecoveryContextKind}; +use super::{InterpolatedStringElementsKind, Parenthesized, RecoveryContextKind}; /// A token set consisting of a newline or end of file. const NEWLINE_EOF_SET: TokenSet = TokenSet::new([TokenKind::Newline, TokenKind::EndOfFile]); @@ -54,6 +59,7 @@ pub(super) const EXPR_SET: TokenSet = TokenSet::new([ TokenKind::Not, TokenKind::Yield, TokenKind::FStringStart, + TokenKind::TStringStart, TokenKind::IpyEscapeCommand, ]) .union(LITERAL_SET); @@ -581,7 +587,9 @@ impl<'src> Parser<'src> { TokenKind::IpyEscapeCommand => { Expr::IpyEscapeCommand(self.parse_ipython_escape_command_expression()) } - TokenKind::String | TokenKind::FStringStart => self.parse_strings(), + TokenKind::String | TokenKind::FStringStart | TokenKind::TStringStart => { + self.parse_strings() + } TokenKind::Lpar => { return self.parse_parenthesized_expression(); } @@ -1177,12 +1185,15 @@ impl<'src> Parser<'src> { /// /// # Panics /// - /// If the parser isn't positioned at a `String` or `FStringStart` token. + /// If the parser isn't positioned at a `String`, `FStringStart`, or `TStringStart` token. /// /// See: (Search "strings:") pub(super) fn parse_strings(&mut self) -> Expr { - const STRING_START_SET: TokenSet = - TokenSet::new([TokenKind::String, TokenKind::FStringStart]); + const STRING_START_SET: TokenSet = TokenSet::new([ + TokenKind::String, + TokenKind::FStringStart, + TokenKind::TStringStart, + ]); let start = self.node_start(); let mut strings = vec![]; @@ -1194,8 +1205,16 @@ impl<'src> Parser<'src> { if self.at(TokenKind::String) { strings.push(self.parse_string_or_byte_literal()); - } else { - strings.push(StringType::FString(self.parse_fstring())); + } else if self.at(TokenKind::FStringStart) { + strings.push(StringType::FString( + self.parse_interpolated_string(InterpolatedStringKind::FString) + .into(), + )); + } else if self.at(TokenKind::TStringStart) { + strings.push(StringType::TString( + self.parse_interpolated_string(InterpolatedStringKind::TString) + .into(), + )); } } @@ -1219,6 +1238,10 @@ impl<'src> Parser<'src> { value: ast::FStringValue::single(fstring), range, }), + StringType::TString(tstring) => Expr::TString(ast::ExprTString { + value: ast::TStringValue::single(tstring), + range, + }), }, _ => self.handle_implicitly_concatenated_strings(strings, range), } @@ -1236,11 +1259,13 @@ impl<'src> Parser<'src> { ) -> Expr { assert!(strings.len() > 1); + let mut has_tstring = false; let mut has_fstring = false; let mut byte_literal_count = 0; for string in &strings { match string { StringType::FString(_) => has_fstring = true, + StringType::TString(_) => has_tstring = true, StringType::Bytes(_) => byte_literal_count += 1, StringType::Str(_) => {} } @@ -1269,7 +1294,7 @@ impl<'src> Parser<'src> { ); } // Only construct a byte expression if all the literals are bytes - // otherwise, we'll try either string or f-string. This is to retain + // otherwise, we'll try either string, t-string, or f-string. This is to retain // as much information as possible. Ordering::Equal => { let mut values = Vec::with_capacity(strings.len()); @@ -1310,7 +1335,7 @@ impl<'src> Parser<'src> { // ) // 2 + 2 - if !has_fstring { + if !has_fstring && !has_tstring { let mut values = Vec::with_capacity(strings.len()); for string in strings { values.push(match string { @@ -1324,10 +1349,34 @@ impl<'src> Parser<'src> { }); } + if has_tstring { + let mut parts = Vec::with_capacity(strings.len()); + for string in strings { + match string { + StringType::TString(tstring) => parts.push(ast::TStringPart::TString(tstring)), + StringType::FString(fstring) => { + parts.push(ruff_python_ast::TStringPart::FString(fstring)); + } + StringType::Str(string) => parts.push(ast::TStringPart::Literal(string)), + StringType::Bytes(bytes) => parts.push(ast::TStringPart::Literal( + ast::StringLiteral::invalid(bytes.range()), + )), + } + } + + return Expr::from(ast::ExprTString { + value: ast::TStringValue::concatenated(parts), + range, + }); + } + let mut parts = Vec::with_capacity(strings.len()); for string in strings { match string { StringType::FString(fstring) => parts.push(ast::FStringPart::FString(fstring)), + StringType::TString(_) => { + unreachable!("expected no tstring parts by this point") + } StringType::Str(string) => parts.push(ast::FStringPart::Literal(string)), StringType::Bytes(bytes) => parts.push(ast::FStringPart::Literal( ast::StringLiteral::invalid(bytes.range()), @@ -1388,24 +1437,32 @@ impl<'src> Parser<'src> { } } - /// Parses a f-string. + /// Parses an f/t-string. /// /// This does not handle implicitly concatenated strings. /// /// # Panics /// - /// If the parser isn't positioned at a `FStringStart` token. + /// If the parser isn't positioned at an `FStringStart` or + /// `TStringStart` token. /// - /// See: (Search "fstring:") + /// See: (Search "fstring:" or "tstring:") /// See: - fn parse_fstring(&mut self) -> ast::FString { + fn parse_interpolated_string( + &mut self, + kind: InterpolatedStringKind, + ) -> InterpolatedStringData { let start = self.node_start(); let flags = self.tokens.current_flags().as_any_string_flags(); - self.bump(TokenKind::FStringStart); - let elements = self.parse_fstring_elements(flags, FStringElementsKind::Regular); + self.bump(kind.start_token()); + let elements = self.parse_interpolated_string_elements( + flags, + InterpolatedStringElementsKind::Regular, + kind, + ); - self.expect(TokenKind::FStringEnd); + self.expect(kind.end_token()); // test_ok pep701_f_string_py312 // # parse_options: {"target-version": "3.12"} @@ -1419,6 +1476,18 @@ impl<'src> Parser<'src> { // f"test {a \ // } more" # line continuation + // test_ok pep750_t_string_py314 + // # parse_options: {"target-version": "3.14"} + // t'Magic wand: { bag['wand'] }' # nested quotes + // t"{'\n'.join(a)}" # escape sequence + // t'''A complex trick: { + // bag['bag'] # comment + // }''' + // t"{t"{t"{t"{t"{t"{1+1}"}"}"}"}"}" # arbitrary nesting + // t"{t'''{"nested"} inner'''} outer" # nested (triple) quotes + // t"test {a \ + // } more" # line continuation + // test_ok pep701_f_string_py311 // # parse_options: {"target-version": "3.11"} // f"outer {'# not a comment'}" @@ -1444,10 +1513,12 @@ impl<'src> Parser<'src> { let range = self.node_range(start); - if !self.options.target_version.supports_pep_701() { + if !self.options.target_version.supports_pep_701() + && matches!(kind, InterpolatedStringKind::FString) + { let quote_bytes = flags.quote_str().as_bytes(); let quote_len = flags.quote_len(); - for expr in elements.expressions() { + for expr in elements.interpolations() { for slash_position in memchr::memchr_iter(b'\\', self.source[expr.range].as_bytes()) { let slash_position = TextSize::try_from(slash_position).unwrap(); @@ -1471,10 +1542,10 @@ impl<'src> Parser<'src> { self.check_fstring_comments(range); } - ast::FString { + InterpolatedStringData { elements, range, - flags: ast::FStringFlags::from(flags), + flags, } } @@ -1490,80 +1561,87 @@ impl<'src> Parser<'src> { })); } - /// Parses a list of f-string elements. + /// Parses a list of f/t-string elements. /// /// # Panics /// - /// If the parser isn't positioned at a `{` or `FStringMiddle` token. - fn parse_fstring_elements( + /// If the parser isn't positioned at a `{`, `FStringMiddle`, + /// or `TStringMiddle` token. + fn parse_interpolated_string_elements( &mut self, flags: ast::AnyStringFlags, - kind: FStringElementsKind, - ) -> FStringElements { + elements_kind: InterpolatedStringElementsKind, + string_kind: InterpolatedStringKind, + ) -> ast::InterpolatedStringElements { let mut elements = vec![]; + let middle_token_kind = string_kind.middle_token(); - self.parse_list(RecoveryContextKind::FStringElements(kind), |parser| { - let element = match parser.current_token_kind() { - TokenKind::Lbrace => { - FStringElement::Expression(parser.parse_fstring_expression_element(flags)) - } - TokenKind::FStringMiddle => { - let range = parser.current_token_range(); - let TokenValue::FStringMiddle(value) = - parser.bump_value(TokenKind::FStringMiddle) - else { - unreachable!() - }; - FStringElement::Literal( - parse_fstring_literal_element(value, flags, range).unwrap_or_else( - |lex_error| { - // test_err invalid_fstring_literal_element - // f'hello \N{INVALID} world' - // f"""hello \N{INVALID} world""" - let location = lex_error.location(); - parser.add_error( - ParseErrorType::Lexical(lex_error.into_error()), - location, - ); - ast::FStringLiteralElement { - value: "".into(), - range, - } - }, - ), - ) - } - // `Invalid` tokens are created when there's a lexical error, so - // we ignore it here to avoid creating unexpected token errors - TokenKind::Unknown => { - parser.bump_any(); - return; - } - tok => { - // This should never happen because the list parsing will only - // call this closure for the above token kinds which are the same - // as in the FIRST set. - unreachable!( - "f-string: unexpected token `{tok:?}` at {:?}", - parser.current_token_range() - ); - } - }; - elements.push(element); - }); + self.parse_list( + RecoveryContextKind::InterpolatedStringElements(elements_kind), + |parser| { + let element = match parser.current_token_kind() { + TokenKind::Lbrace => ast::InterpolatedStringElement::from( + parser.parse_interpolated_element(flags, string_kind), + ), + tok if tok == middle_token_kind => { + let range = parser.current_token_range(); + let TokenValue::InterpolatedStringMiddle(value) = + parser.bump_value(middle_token_kind) + else { + unreachable!() + }; + InterpolatedStringElement::Literal( + parse_interpolated_string_literal_element(value, flags, range) + .unwrap_or_else(|lex_error| { + // test_err invalid_fstring_literal_element + // f'hello \N{INVALID} world' + // f"""hello \N{INVALID} world""" + let location = lex_error.location(); + parser.add_error( + ParseErrorType::Lexical(lex_error.into_error()), + location, + ); + ast::InterpolatedStringLiteralElement { + value: "".into(), + range, + } + }), + ) + } + // `Invalid` tokens are created when there's a lexical error, so + // we ignore it here to avoid creating unexpected token errors + TokenKind::Unknown => { + parser.bump_any(); + return; + } + tok => { + // This should never happen because the list parsing will only + // call this closure for the above token kinds which are the same + // as in the FIRST set. + unreachable!( + "{}: unexpected token `{tok:?}` at {:?}", + string_kind, + parser.current_token_range() + ); + } + }; + elements.push(element); + }, + ); - FStringElements::from(elements) + ast::InterpolatedStringElements::from(elements) } - /// Parses a f-string expression element. + /// Parses an f/t-string expression element. /// /// # Panics /// /// If the parser isn't positioned at a `{` token. - fn parse_fstring_expression_element( + fn parse_interpolated_element( &mut self, flags: ast::AnyStringFlags, - ) -> ast::FStringExpressionElement { + string_kind: InterpolatedStringKind, + ) -> ast::InterpolatedElement { let start = self.node_start(); self.bump(TokenKind::Lbrace); @@ -1571,11 +1649,23 @@ impl<'src> Parser<'src> { // f"{}" // f"{ }" + // test_err t_string_empty_expression + // # parse_options: {"target-version": "3.14"} + // t"{}" + // t"{ }" + // test_err f_string_invalid_starred_expr // # Starred expression inside f-string has a minimum precedence of bitwise or. // f"{*}" // f"{*x and y}" // f"{*yield x}" + + // test_err t_string_invalid_starred_expr + // # parse_options: {"target-version": "3.14"} + // # Starred expression inside t-string has a minimum precedence of bitwise or. + // t"{*}" + // t"{*x and y}" + // t"{*yield x}" let value = self.parse_expression_list(ExpressionContext::yield_or_starred_bitwise_or()); if !value.is_parenthesized && value.expr.is_lambda_expr() { @@ -1585,8 +1675,15 @@ impl<'src> Parser<'src> { // test_err f_string_lambda_without_parentheses // f"{lambda x: x}" + + // test_err t_string_lambda_without_parentheses + // # parse_options: {"target-version": "3.14"} + // t"{lambda x: x}" self.add_error( - ParseErrorType::FStringError(FStringErrorType::LambdaWithoutParentheses), + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::LambdaWithoutParentheses, + string_kind, + ), value.range(), ); } @@ -1614,8 +1711,15 @@ impl<'src> Parser<'src> { _ => { // test_err f_string_invalid_conversion_flag_name_tok // f"{x!z}" + + // test_err t_string_invalid_conversion_flag_name_tok + // # parse_options: {"target-version": "3.14"} + // t"{x!z}" self.add_error( - ParseErrorType::FStringError(FStringErrorType::InvalidConversionFlag), + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::InvalidConversionFlag, + string_kind, + ), conversion_flag_range, ); ConversionFlag::None @@ -1625,8 +1729,16 @@ impl<'src> Parser<'src> { // test_err f_string_invalid_conversion_flag_other_tok // f"{x!123}" // f"{x!'a'}" + + // test_err t_string_invalid_conversion_flag_other_tok + // # parse_options: {"target-version": "3.14"} + // t"{x!123}" + // t"{x!'a'}" self.add_error( - ParseErrorType::FStringError(FStringErrorType::InvalidConversionFlag), + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::InvalidConversionFlag, + string_kind, + ), conversion_flag_range, ); // TODO(dhruvmanila): Avoid dropping this token @@ -1639,8 +1751,12 @@ impl<'src> Parser<'src> { let format_spec = if self.eat(TokenKind::Colon) { let spec_start = self.node_start(); - let elements = self.parse_fstring_elements(flags, FStringElementsKind::FormatSpec); - Some(Box::new(ast::FStringFormatSpec { + let elements = self.parse_interpolated_string_elements( + flags, + InterpolatedStringElementsKind::FormatSpec, + string_kind, + ); + Some(Box::new(ast::InterpolatedStringFormatSpec { range: self.node_range(spec_start), elements, })) @@ -1661,18 +1777,34 @@ impl<'src> Parser<'src> { // f"{" // f"""{""" + // test_err t_string_unclosed_lbrace + // # parse_options: {"target-version": "3.14"} + // t"{" + // t"{foo!r" + // t"{foo=" + // t"{" + // t"""{""" + // The lexer does emit `FStringEnd` for the following test cases: // test_err f_string_unclosed_lbrace_in_format_spec // f"hello {x:" // f"hello {x:.3f" + + // test_err t_string_unclosed_lbrace_in_format_spec + // # parse_options: {"target-version": "3.14"} + // t"hello {x:" + // t"hello {x:.3f" self.add_error( - ParseErrorType::FStringError(FStringErrorType::UnclosedLbrace), + ParseErrorType::from_interpolated_string_error( + InterpolatedStringErrorType::UnclosedLbrace, + string_kind, + ), self.current_token_range(), ); } - ast::FStringExpressionElement { + ast::InterpolatedElement { expression: Box::new(value.expr), debug_text, conversion, @@ -2755,3 +2887,30 @@ impl ExpressionContext { } } } + +#[derive(Debug)] +struct InterpolatedStringData { + elements: InterpolatedStringElements, + range: TextRange, + flags: AnyStringFlags, +} + +impl From for FString { + fn from(value: InterpolatedStringData) -> Self { + Self { + elements: value.elements, + range: value.range, + flags: value.flags.into(), + } + } +} + +impl From for TString { + fn from(value: InterpolatedStringData) -> Self { + Self { + elements: value.elements, + range: value.range, + flags: value.flags.into(), + } + } +} diff --git a/crates/ruff_python_parser/src/parser/helpers.rs b/crates/ruff_python_parser/src/parser/helpers.rs index e7a0e426c2..de89746333 100644 --- a/crates/ruff_python_parser/src/parser/helpers.rs +++ b/crates/ruff_python_parser/src/parser/helpers.rs @@ -94,6 +94,7 @@ pub(super) fn detect_invalid_pre_py39_decorator_node( Expr::YieldFrom(_) => "`yield from` expression", Expr::Compare(_) => "comparison expression", Expr::FString(_) => "f-string", + Expr::TString(_) => "t-string", Expr::Named(_) => "assignment expression", Expr::Subscript(_) => "subscript expression", Expr::IpyEscapeCommand(_) => "IPython escape command", diff --git a/crates/ruff_python_parser/src/parser/mod.rs b/crates/ruff_python_parser/src/parser/mod.rs index 19596364a8..0668f18b29 100644 --- a/crates/ruff_python_parser/src/parser/mod.rs +++ b/crates/ruff_python_parser/src/parser/mod.rs @@ -798,7 +798,7 @@ impl WithItemKind { } #[derive(Debug, PartialEq, Copy, Clone)] -enum FStringElementsKind { +enum InterpolatedStringElementsKind { /// The regular f-string elements. /// /// For example, the `"hello "`, `x`, and `" world"` elements in: @@ -816,14 +816,16 @@ enum FStringElementsKind { FormatSpec, } -impl FStringElementsKind { - const fn list_terminator(self) -> TokenKind { +impl InterpolatedStringElementsKind { + const fn list_terminators(self) -> TokenSet { match self { - FStringElementsKind::Regular => TokenKind::FStringEnd, + InterpolatedStringElementsKind::Regular => { + TokenSet::new([TokenKind::FStringEnd, TokenKind::TStringEnd]) + } // test_ok fstring_format_spec_terminator // f"hello {x:} world" // f"hello {x:.3f} world" - FStringElementsKind::FormatSpec => TokenKind::Rbrace, + InterpolatedStringElementsKind::FormatSpec => TokenSet::new([TokenKind::Rbrace]), } } } @@ -931,9 +933,8 @@ enum RecoveryContextKind { /// When parsing a list of items in a `with` statement WithItems(WithItemKind), - /// When parsing a list of f-string elements which are either literal elements - /// or expressions. - FStringElements(FStringElementsKind), + /// When parsing a list of f-string or t-string elements which are either literal elements, expressions, or interpolations. + InterpolatedStringElements(InterpolatedStringElementsKind), } impl RecoveryContextKind { @@ -1117,8 +1118,8 @@ impl RecoveryContextKind { .at(TokenKind::Colon) .then_some(ListTerminatorKind::Regular), }, - RecoveryContextKind::FStringElements(kind) => { - if p.at(kind.list_terminator()) { + RecoveryContextKind::InterpolatedStringElements(kind) => { + if p.at_ts(kind.list_terminators()) { Some(ListTerminatorKind::Regular) } else { // test_err unterminated_fstring_newline_recovery @@ -1174,10 +1175,10 @@ impl RecoveryContextKind { ) || p.at_name_or_soft_keyword() } RecoveryContextKind::WithItems(_) => p.at_expr(), - RecoveryContextKind::FStringElements(_) => matches!( + RecoveryContextKind::InterpolatedStringElements(_) => matches!( p.current_token_kind(), // Literal element - TokenKind::FStringMiddle + TokenKind::FStringMiddle | TokenKind::TStringMiddle // Expression element | TokenKind::Lbrace ), @@ -1268,13 +1269,13 @@ impl RecoveryContextKind { "Expected an expression or the end of the with item list".to_string(), ), }, - RecoveryContextKind::FStringElements(kind) => match kind { - FStringElementsKind::Regular => ParseErrorType::OtherError( - "Expected an f-string element or the end of the f-string".to_string(), + RecoveryContextKind::InterpolatedStringElements(kind) => match kind { + InterpolatedStringElementsKind::Regular => ParseErrorType::OtherError( + "Expected an f-string or t-string element or the end of the f-string or t-string".to_string(), + ), + InterpolatedStringElementsKind::FormatSpec => ParseErrorType::OtherError( + "Expected an f-string or t-string element or a '}'".to_string(), ), - FStringElementsKind::FormatSpec => { - ParseErrorType::OtherError("Expected an f-string element or a '}'".to_string()) - } }, } } @@ -1313,8 +1314,8 @@ bitflags! { const WITH_ITEMS_PARENTHESIZED = 1 << 25; const WITH_ITEMS_PARENTHESIZED_EXPRESSION = 1 << 26; const WITH_ITEMS_UNPARENTHESIZED = 1 << 28; - const F_STRING_ELEMENTS = 1 << 29; - const F_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30; + const FT_STRING_ELEMENTS = 1 << 29; + const FT_STRING_ELEMENTS_IN_FORMAT_SPEC = 1 << 30; } } @@ -1367,10 +1368,10 @@ impl RecoveryContext { } WithItemKind::Unparenthesized => RecoveryContext::WITH_ITEMS_UNPARENTHESIZED, }, - RecoveryContextKind::FStringElements(kind) => match kind { - FStringElementsKind::Regular => RecoveryContext::F_STRING_ELEMENTS, - FStringElementsKind::FormatSpec => { - RecoveryContext::F_STRING_ELEMENTS_IN_FORMAT_SPEC + RecoveryContextKind::InterpolatedStringElements(kind) => match kind { + InterpolatedStringElementsKind::Regular => RecoveryContext::FT_STRING_ELEMENTS, + InterpolatedStringElementsKind::FormatSpec => { + RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC } }, } @@ -1439,11 +1440,13 @@ impl RecoveryContext { RecoveryContext::WITH_ITEMS_UNPARENTHESIZED => { RecoveryContextKind::WithItems(WithItemKind::Unparenthesized) } - RecoveryContext::F_STRING_ELEMENTS => { - RecoveryContextKind::FStringElements(FStringElementsKind::Regular) - } - RecoveryContext::F_STRING_ELEMENTS_IN_FORMAT_SPEC => { - RecoveryContextKind::FStringElements(FStringElementsKind::FormatSpec) + RecoveryContext::FT_STRING_ELEMENTS => RecoveryContextKind::InterpolatedStringElements( + InterpolatedStringElementsKind::Regular, + ), + RecoveryContext::FT_STRING_ELEMENTS_IN_FORMAT_SPEC => { + RecoveryContextKind::InterpolatedStringElements( + InterpolatedStringElementsKind::FormatSpec, + ) } _ => return None, }) diff --git a/crates/ruff_python_parser/src/parser/pattern.rs b/crates/ruff_python_parser/src/parser/pattern.rs index ced1627461..461b859c78 100644 --- a/crates/ruff_python_parser/src/parser/pattern.rs +++ b/crates/ruff_python_parser/src/parser/pattern.rs @@ -390,7 +390,7 @@ impl Parser<'_> { range: self.node_range(start), }) } - TokenKind::String | TokenKind::FStringStart => { + TokenKind::String | TokenKind::FStringStart | TokenKind::TStringStart => { let str = self.parse_strings(); Pattern::MatchValue(ast::PatternMatchValue { diff --git a/crates/ruff_python_parser/src/parser/statement.rs b/crates/ruff_python_parser/src/parser/statement.rs index 1eb59b25b6..6a44192746 100644 --- a/crates/ruff_python_parser/src/parser/statement.rs +++ b/crates/ruff_python_parser/src/parser/statement.rs @@ -3012,7 +3012,6 @@ impl<'src> Parser<'src> { // test_ok param_with_annotation // def foo(arg: int): ... // def foo(arg: lambda x: x): ... - // def foo(arg: (x := int)): ... // test_err param_with_invalid_annotation // def foo(arg: *int): ... @@ -3703,6 +3702,7 @@ impl<'src> Parser<'src> { | TokenKind::Complex | TokenKind::String | TokenKind::FStringStart + | TokenKind::TStringStart | TokenKind::Lbrace | TokenKind::Tilde | TokenKind::Ellipsis diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap new file mode 100644 index 0000000000..d23fee4ae8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__empty_tstrings.snap @@ -0,0 +1,98 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 2..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + String( + "", + ), + 4..6, + TokenFlags( + DOUBLE_QUOTES, + ), + ), + ( + TStringStart, + 7..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 9..10, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 11..13, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 13..14, + TokenFlags( + T_STRING, + ), + ), + ( + String( + "", + ), + 15..17, + ), + ( + TStringStart, + 18..22, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 22..25, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringStart, + 26..30, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 30..33, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Newline, + 33..33, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap index 3a56937bcc..c515b59ec0 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "normal ", ), 2..9, @@ -37,7 +36,7 @@ snapshot_kind: text 13..14, ), ( - FStringMiddle( + InterpolatedStringMiddle( " {another} ", ), 14..27, @@ -60,7 +59,7 @@ snapshot_kind: text 31..32, ), ( - FStringMiddle( + InterpolatedStringMiddle( " {", ), 32..35, @@ -83,7 +82,7 @@ snapshot_kind: text 41..42, ), ( - FStringMiddle( + InterpolatedStringMiddle( "}", ), 42..44, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap index dae04a5f0c..93e0b88bd9 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_comments.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\n# not a comment ", ), 4..21, @@ -49,7 +48,7 @@ snapshot_kind: text 41..42, ), ( - FStringMiddle( + InterpolatedStringMiddle( " # not a comment\n", ), 42..59, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap index 80a8683277..cff7b14e12 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_conversion.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -38,7 +37,7 @@ snapshot_kind: text 6..7, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 7..8, @@ -75,7 +74,7 @@ snapshot_kind: text 13..14, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 14..15, @@ -98,7 +97,7 @@ snapshot_kind: text 17..18, ), ( - FStringMiddle( + InterpolatedStringMiddle( ".3f!r", ), 18..23, @@ -111,7 +110,7 @@ snapshot_kind: text 23..24, ), ( - FStringMiddle( + InterpolatedStringMiddle( " {x!r}", ), 24..32, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap index 7aae96b72f..899139162d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\", ), 2..3, @@ -37,7 +36,7 @@ snapshot_kind: text 5..6, ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\\"\\", ), 6..9, @@ -64,7 +63,7 @@ snapshot_kind: text 12..13, ), ( - FStringMiddle( + InterpolatedStringMiddle( " \\\"\\\"\\\n end", ), 13..24, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap index 3cfba863a2..a792cfee11 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_braces.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\", ), 2..3, @@ -51,7 +50,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\\\", ), 12..14, @@ -88,7 +87,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\{foo}", ), 23..31, @@ -111,7 +110,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\\\{foo}", ), 35..44, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap index 0e14fbb35d..5fe4b16837 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_escape_raw.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\", ), 3..4, @@ -37,7 +36,7 @@ snapshot_kind: text 6..7, ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\\"\\", ), 7..10, @@ -64,7 +63,7 @@ snapshot_kind: text 13..14, ), ( - FStringMiddle( + InterpolatedStringMiddle( " \\\"\\\"\\\n end", ), 14..25, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap index c7fd18b79a..5987a41f67 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_expression_multiline.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "first ", ), 2..8, @@ -63,7 +62,7 @@ snapshot_kind: text 40..41, ), ( - FStringMiddle( + InterpolatedStringMiddle( " second", ), 41..48, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap index 95c43f76d1..15a765a45a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_multiline.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\nhello\n world\n", ), 4..21, @@ -37,7 +36,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\n world\nhello\n", ), 29..46, @@ -60,7 +59,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "some ", ), 52..57, @@ -80,7 +79,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "multiline\nallowed ", ), 62..80, @@ -114,7 +113,7 @@ snapshot_kind: text 86..87, ), ( - FStringMiddle( + InterpolatedStringMiddle( " string", ), 87..94, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap index 2ae4109360..5571d867a1 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\N{BULLET} normal \\Nope \\N", ), 2..28, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap index b37611f0da..974d2cf9c3 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_named_unicode_raw.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\N", ), 3..5, @@ -37,7 +36,7 @@ snapshot_kind: text 12..13, ), ( - FStringMiddle( + InterpolatedStringMiddle( " normal", ), 13..20, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap index de3e6d60f2..8e1dc7e8d2 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_nested.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "foo ", ), 2..6, @@ -34,7 +33,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "bar ", ), 9..13, @@ -100,7 +99,7 @@ snapshot_kind: text 28..29, ), ( - FStringMiddle( + InterpolatedStringMiddle( " baz", ), 29..33, @@ -123,7 +122,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "foo ", ), 37..41, @@ -143,7 +142,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "bar", ), 44..47, @@ -163,7 +162,7 @@ snapshot_kind: text 48..49, ), ( - FStringMiddle( + InterpolatedStringMiddle( " some ", ), 49..55, @@ -183,7 +182,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "another", ), 58..65, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap index 287d62d08a..381aa8e626 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_parentheses.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -36,7 +35,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "{}", ), 8..12, @@ -59,7 +58,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 16..17, @@ -90,7 +89,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "{", ), 23..25, @@ -107,7 +106,7 @@ snapshot_kind: text 26..27, ), ( - FStringMiddle( + InterpolatedStringMiddle( "}", ), 27..29, @@ -130,7 +129,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "{{}}", ), 33..41, @@ -153,7 +152,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 45..46, @@ -170,7 +169,7 @@ snapshot_kind: text 47..48, ), ( - FStringMiddle( + InterpolatedStringMiddle( " {} {", ), 48..56, @@ -187,7 +186,7 @@ snapshot_kind: text 57..58, ), ( - FStringMiddle( + InterpolatedStringMiddle( "} {{}} ", ), 58..71, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap index 5476c1fa02..53a3fe7908 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_mac_eol.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: fstring_single_quote_escape_eol(MAC_EOL) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "text \\\r more text", ), 2..19, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap index 19e0346f43..d8e27f0661 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_unix_eol.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: fstring_single_quote_escape_eol(UNIX_EOL) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "text \\\n more text", ), 2..19, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap index c4f595a389..ba73b4a09d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_single_quote_escape_windows_eol.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: fstring_single_quote_escape_eol(WINDOWS_EOL) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "text \\\r\n more text", ), 2..20, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap index 400f81636f..26380715dc 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_format_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -32,7 +31,7 @@ snapshot_kind: text 7..8, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 8..9, @@ -69,7 +68,7 @@ snapshot_kind: text 14..15, ), ( - FStringMiddle( + InterpolatedStringMiddle( ".3f", ), 15..18, @@ -82,7 +81,7 @@ snapshot_kind: text 18..19, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 19..20, @@ -105,7 +104,7 @@ snapshot_kind: text 22..23, ), ( - FStringMiddle( + InterpolatedStringMiddle( ".", ), 23..24, @@ -128,7 +127,7 @@ snapshot_kind: text 26..27, ), ( - FStringMiddle( + InterpolatedStringMiddle( "f", ), 27..28, @@ -141,7 +140,7 @@ snapshot_kind: text 28..29, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 29..30, @@ -164,7 +163,7 @@ snapshot_kind: text 33..34, ), ( - FStringMiddle( + InterpolatedStringMiddle( "*^", ), 34..36, @@ -209,7 +208,7 @@ snapshot_kind: text 43..44, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 44..45, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap index f48c742e6d..523b5d6133 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_ipy_escape_command.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "foo ", ), 2..6, @@ -41,7 +40,7 @@ snapshot_kind: text 11..12, ), ( - FStringMiddle( + InterpolatedStringMiddle( " bar", ), 12..16, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap index 341455e1f2..6a0909bcdb 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_multiline_format_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 4..6, @@ -41,7 +40,7 @@ snapshot_kind: text 13..14, ), ( - FStringMiddle( + InterpolatedStringMiddle( "d\n", ), 14..16, @@ -54,7 +53,7 @@ snapshot_kind: text 16..17, ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 17..19, @@ -81,7 +80,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 27..29, @@ -108,7 +107,7 @@ snapshot_kind: text 36..37, ), ( - FStringMiddle( + InterpolatedStringMiddle( "a\n b\n c\n", ), 37..61, @@ -121,7 +120,7 @@ snapshot_kind: text 61..62, ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 62..64, @@ -148,7 +147,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 70..72, @@ -175,7 +174,7 @@ snapshot_kind: text 79..80, ), ( - FStringMiddle( + InterpolatedStringMiddle( "d", ), 80..81, @@ -192,7 +191,7 @@ snapshot_kind: text 82..83, ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 83..85, @@ -219,7 +218,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 89..91, @@ -246,7 +245,7 @@ snapshot_kind: text 98..99, ), ( - FStringMiddle( + InterpolatedStringMiddle( "a", ), 99..100, @@ -273,7 +272,7 @@ snapshot_kind: text 111..112, ), ( - FStringMiddle( + InterpolatedStringMiddle( "__", ), 112..114, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap index 8f83f01d57..bf3571a289 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_named_expression.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -28,7 +27,7 @@ snapshot_kind: text 4..5, ), ( - FStringMiddle( + InterpolatedStringMiddle( "=10", ), 5..8, @@ -41,7 +40,7 @@ snapshot_kind: text 8..9, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 9..10, @@ -82,7 +81,7 @@ snapshot_kind: text 18..19, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 19..20, @@ -133,7 +132,7 @@ snapshot_kind: text 30..31, ), ( - FStringMiddle( + InterpolatedStringMiddle( " ", ), 31..32, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap index 73b431eccc..377acaf33d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__fstring_with_nul_char.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/lexer.rs expression: lex_source(source) -snapshot_kind: text --- ## Tokens ``` @@ -14,7 +13,7 @@ snapshot_kind: text ), ), ( - FStringMiddle( + InterpolatedStringMiddle( "\\0", ), 2..4, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap new file mode 100644 index 0000000000..f2d59004e5 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__nested_t_and_fstring.snap @@ -0,0 +1,226 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 2..6, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 6..7, + ), + ( + FStringStart, + 7..9, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "bar ", + ), + 9..13, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + Lbrace, + 13..14, + ), + ( + Name( + Name("x"), + ), + 14..15, + ), + ( + Plus, + 16..17, + ), + ( + TStringStart, + 18..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Name( + Name("wow"), + ), + 21..24, + ), + ( + Rbrace, + 24..25, + ), + ( + TStringEnd, + 25..26, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 26..27, + ), + ( + FStringEnd, + 27..28, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + Rbrace, + 28..29, + ), + ( + InterpolatedStringMiddle( + " baz", + ), + 29..33, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 33..34, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + FStringStart, + 35..37, + TokenFlags( + F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 37..41, + TokenFlags( + F_STRING, + ), + ), + ( + Lbrace, + 41..42, + ), + ( + TStringStart, + 42..44, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "bar", + ), + 44..47, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 47..48, + TokenFlags( + T_STRING, + ), + ), + ( + Exclamation, + 48..49, + ), + ( + Name( + Name("r"), + ), + 49..50, + ), + ( + Rbrace, + 50..51, + ), + ( + InterpolatedStringMiddle( + " some ", + ), + 51..57, + TokenFlags( + F_STRING, + ), + ), + ( + Lbrace, + 57..58, + ), + ( + FStringStart, + 58..60, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "another", + ), + 60..67, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + FStringEnd, + 67..68, + TokenFlags( + DOUBLE_QUOTES | F_STRING, + ), + ), + ( + Rbrace, + 68..69, + ), + ( + FStringEnd, + 69..70, + TokenFlags( + F_STRING, + ), + ), + ( + Newline, + 70..70, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap new file mode 100644 index 0000000000..dde7870ae8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring.snap @@ -0,0 +1,105 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "normal ", + ), + 2..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 9..10, + ), + ( + Name( + Name("foo"), + ), + 10..13, + ), + ( + Rbrace, + 13..14, + ), + ( + InterpolatedStringMiddle( + " {another} ", + ), + 14..27, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 27..28, + ), + ( + Name( + Name("bar"), + ), + 28..31, + ), + ( + Rbrace, + 31..32, + ), + ( + InterpolatedStringMiddle( + " {", + ), + 32..35, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 35..36, + ), + ( + Name( + Name("three"), + ), + 36..41, + ), + ( + Rbrace, + 41..42, + ), + ( + InterpolatedStringMiddle( + "}", + ), + 42..44, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 44..45, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 45..45, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap new file mode 100644 index 0000000000..5cbdf88979 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_comments.snap @@ -0,0 +1,71 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..4, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\n# not a comment ", + ), + 4..21, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Lbrace, + 21..22, + ), + ( + Comment, + 23..34, + ), + ( + NonLogicalNewline, + 34..35, + ), + ( + Name( + Name("x"), + ), + 39..40, + ), + ( + NonLogicalNewline, + 40..41, + ), + ( + Rbrace, + 41..42, + ), + ( + InterpolatedStringMiddle( + " # not a comment\n", + ), + 42..59, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 59..62, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Newline, + 62..62, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap new file mode 100644 index 0000000000..2e911b3250 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_conversion.snap @@ -0,0 +1,133 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Name( + Name("x"), + ), + 3..4, + ), + ( + Exclamation, + 4..5, + ), + ( + Name( + Name("s"), + ), + 5..6, + ), + ( + Rbrace, + 6..7, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 7..8, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 8..9, + ), + ( + Name( + Name("x"), + ), + 9..10, + ), + ( + Equal, + 10..11, + ), + ( + Exclamation, + 11..12, + ), + ( + Name( + Name("r"), + ), + 12..13, + ), + ( + Rbrace, + 13..14, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 14..15, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 15..16, + ), + ( + Name( + Name("x"), + ), + 16..17, + ), + ( + Colon, + 17..18, + ), + ( + InterpolatedStringMiddle( + ".3f!r", + ), + 18..23, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 23..24, + ), + ( + InterpolatedStringMiddle( + " {x!r}", + ), + 24..32, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 32..33, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 33..33, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap new file mode 100644 index 0000000000..a69d1b8d97 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape.snap @@ -0,0 +1,86 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\", + ), + 2..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 3..4, + ), + ( + Name( + Name("x"), + ), + 4..5, + ), + ( + Colon, + 5..6, + ), + ( + InterpolatedStringMiddle( + "\\\"\\", + ), + 6..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 9..10, + ), + ( + Name( + Name("x"), + ), + 10..11, + ), + ( + Rbrace, + 11..12, + ), + ( + Rbrace, + 12..13, + ), + ( + InterpolatedStringMiddle( + " \\\"\\\"\\\n end", + ), + 13..24, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 24..25, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 25..25, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap new file mode 100644 index 0000000000..2cf409eae5 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_braces.snap @@ -0,0 +1,133 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\", + ), + 2..3, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 3..4, + ), + ( + Name( + Name("foo"), + ), + 4..7, + ), + ( + Rbrace, + 7..8, + ), + ( + TStringEnd, + 8..9, + TokenFlags( + T_STRING, + ), + ), + ( + TStringStart, + 10..12, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\\\", + ), + 12..14, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 14..15, + ), + ( + Name( + Name("foo"), + ), + 15..18, + ), + ( + Rbrace, + 18..19, + ), + ( + TStringEnd, + 19..20, + TokenFlags( + T_STRING, + ), + ), + ( + TStringStart, + 21..23, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\{foo}", + ), + 23..31, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 31..32, + TokenFlags( + T_STRING, + ), + ), + ( + TStringStart, + 33..35, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\\\{foo}", + ), + 35..44, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 44..45, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 45..45, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap new file mode 100644 index 0000000000..f7b2b27cb8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_escape_raw.snap @@ -0,0 +1,86 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + InterpolatedStringMiddle( + "\\", + ), + 3..4, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Lbrace, + 4..5, + ), + ( + Name( + Name("x"), + ), + 5..6, + ), + ( + Colon, + 6..7, + ), + ( + InterpolatedStringMiddle( + "\\\"\\", + ), + 7..10, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Lbrace, + 10..11, + ), + ( + Name( + Name("x"), + ), + 11..12, + ), + ( + Rbrace, + 12..13, + ), + ( + Rbrace, + 13..14, + ), + ( + InterpolatedStringMiddle( + " \\\"\\\"\\\n end", + ), + 14..25, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 25..26, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Newline, + 26..26, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap new file mode 100644 index 0000000000..13894db564 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_expression_multiline.snap @@ -0,0 +1,85 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "first ", + ), + 2..8, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 8..9, + ), + ( + NonLogicalNewline, + 9..10, + ), + ( + Name( + Name("x"), + ), + 14..15, + ), + ( + NonLogicalNewline, + 15..16, + ), + ( + Star, + 24..25, + ), + ( + NonLogicalNewline, + 25..26, + ), + ( + Name( + Name("y"), + ), + 38..39, + ), + ( + NonLogicalNewline, + 39..40, + ), + ( + Rbrace, + 40..41, + ), + ( + InterpolatedStringMiddle( + " second", + ), + 41..48, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 48..49, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 49..49, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap new file mode 100644 index 0000000000..5f4f4496d1 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_multiline.snap @@ -0,0 +1,136 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..4, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\nhello\n world\n", + ), + 4..21, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 21..24, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringStart, + 25..29, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\n world\nhello\n", + ), + 29..46, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 46..49, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringStart, + 50..52, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "some ", + ), + 52..57, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 57..58, + ), + ( + TStringStart, + 58..62, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "multiline\nallowed ", + ), + 62..80, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Lbrace, + 80..81, + ), + ( + Name( + Name("x"), + ), + 81..82, + ), + ( + Rbrace, + 82..83, + ), + ( + TStringEnd, + 83..86, + TokenFlags( + DOUBLE_QUOTES | TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Rbrace, + 86..87, + ), + ( + InterpolatedStringMiddle( + " string", + ), + 87..94, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 94..95, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 95..95, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap new file mode 100644 index 0000000000..f900cbf95b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\N{BULLET} normal \\Nope \\N", + ), + 2..28, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 28..29, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 29..29, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap new file mode 100644 index 0000000000..73b022ad3b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_named_unicode_raw.snap @@ -0,0 +1,59 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + InterpolatedStringMiddle( + "\\N", + ), + 3..5, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Lbrace, + 5..6, + ), + ( + Name( + Name("BULLET"), + ), + 6..12, + ), + ( + Rbrace, + 12..13, + ), + ( + InterpolatedStringMiddle( + " normal", + ), + 13..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 20..21, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + Newline, + 21..21, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap new file mode 100644 index 0000000000..f786acf7d8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_nested.snap @@ -0,0 +1,216 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 2..6, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 6..7, + ), + ( + TStringStart, + 7..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "bar ", + ), + 9..13, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 13..14, + ), + ( + Name( + Name("x"), + ), + 14..15, + ), + ( + Plus, + 16..17, + ), + ( + TStringStart, + 18..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Name( + Name("wow"), + ), + 21..24, + ), + ( + Rbrace, + 24..25, + ), + ( + TStringEnd, + 25..26, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 26..27, + ), + ( + TStringEnd, + 27..28, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 28..29, + ), + ( + InterpolatedStringMiddle( + " baz", + ), + 29..33, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 33..34, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 35..37, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 37..41, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 41..42, + ), + ( + TStringStart, + 42..44, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "bar", + ), + 44..47, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 47..48, + TokenFlags( + T_STRING, + ), + ), + ( + Rbrace, + 48..49, + ), + ( + InterpolatedStringMiddle( + " some ", + ), + 49..55, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 55..56, + ), + ( + TStringStart, + 56..58, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "another", + ), + 58..65, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 65..66, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 66..67, + ), + ( + TStringEnd, + 67..68, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 68..68, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap new file mode 100644 index 0000000000..fcccc68b40 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_parentheses.snap @@ -0,0 +1,209 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Rbrace, + 3..4, + ), + ( + TStringEnd, + 4..5, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 6..8, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "{}", + ), + 8..12, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 12..13, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 14..16, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + " ", + ), + 16..17, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 17..18, + ), + ( + Rbrace, + 18..19, + ), + ( + TStringEnd, + 19..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 21..23, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "{", + ), + 23..25, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 25..26, + ), + ( + Rbrace, + 26..27, + ), + ( + InterpolatedStringMiddle( + "}", + ), + 27..29, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 29..30, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 31..33, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "{{}}", + ), + 33..41, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 41..42, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 43..45, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + " ", + ), + 45..46, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 46..47, + ), + ( + Rbrace, + 47..48, + ), + ( + InterpolatedStringMiddle( + " {} {", + ), + 48..56, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 56..57, + ), + ( + Rbrace, + 57..58, + ), + ( + InterpolatedStringMiddle( + "} {{}} ", + ), + 58..71, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 71..72, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 72..72, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap new file mode 100644 index 0000000000..8a285e58fb --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_prefix.snap @@ -0,0 +1,153 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 2..3, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 4..6, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 6..7, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringStart, + 8..11, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 11..12, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringStart, + 13..16, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 16..17, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringStart, + 18..21, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringEnd, + 21..22, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringStart, + 23..26, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringEnd, + 26..27, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringStart, + 28..31, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 31..32, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringStart, + 33..36, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringEnd, + 36..37, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_LOWERCASE, + ), + ), + ( + TStringStart, + 38..41, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringEnd, + 41..42, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringStart, + 43..46, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + TStringEnd, + 46..47, + TokenFlags( + DOUBLE_QUOTES | T_STRING | RAW_STRING_UPPERCASE, + ), + ), + ( + Newline, + 47..47, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap new file mode 100644 index 0000000000..515c486b9b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_mac_eol.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: tstring_single_quote_escape_eol(MAC_EOL) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "text \\\r more text", + ), + 2..19, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 19..20, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 20..20, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap new file mode 100644 index 0000000000..eed02e6ab6 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_unix_eol.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: tstring_single_quote_escape_eol(UNIX_EOL) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "text \\\n more text", + ), + 2..19, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 19..20, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 20..20, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap new file mode 100644 index 0000000000..424092796d --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_single_quote_escape_windows_eol.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: tstring_single_quote_escape_eol(WINDOWS_EOL) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "text \\\r\n more text", + ), + 2..20, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 20..21, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 21..21, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap new file mode 100644 index 0000000000..d8760b764d --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_format_spec.snap @@ -0,0 +1,289 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Name( + Name("foo"), + ), + 3..6, + ), + ( + Colon, + 6..7, + ), + ( + Rbrace, + 7..8, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 8..9, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 9..10, + ), + ( + Name( + Name("x"), + ), + 10..11, + ), + ( + Equal, + 11..12, + ), + ( + Exclamation, + 12..13, + ), + ( + Name( + Name("s"), + ), + 13..14, + ), + ( + Colon, + 14..15, + ), + ( + InterpolatedStringMiddle( + ".3f", + ), + 15..18, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 18..19, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 19..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Name( + Name("x"), + ), + 21..22, + ), + ( + Colon, + 22..23, + ), + ( + InterpolatedStringMiddle( + ".", + ), + 23..24, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 24..25, + ), + ( + Name( + Name("y"), + ), + 25..26, + ), + ( + Rbrace, + 26..27, + ), + ( + InterpolatedStringMiddle( + "f", + ), + 27..28, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 28..29, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 29..30, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 30..31, + ), + ( + String( + "", + ), + 31..33, + ), + ( + Colon, + 33..34, + ), + ( + InterpolatedStringMiddle( + "*^", + ), + 34..36, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 36..37, + ), + ( + Int( + 1, + ), + 37..38, + ), + ( + Colon, + 38..39, + ), + ( + Lbrace, + 39..40, + ), + ( + Int( + 1, + ), + 40..41, + ), + ( + Rbrace, + 41..42, + ), + ( + Rbrace, + 42..43, + ), + ( + Rbrace, + 43..44, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 44..45, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 45..46, + ), + ( + Name( + Name("x"), + ), + 46..47, + ), + ( + Colon, + 47..48, + ), + ( + Lbrace, + 48..49, + ), + ( + Lbrace, + 49..50, + ), + ( + Int( + 1, + ), + 50..51, + ), + ( + Rbrace, + 51..52, + ), + ( + Dot, + 52..53, + ), + ( + Name( + Name("pop"), + ), + 53..56, + ), + ( + Lpar, + 56..57, + ), + ( + Rpar, + 57..58, + ), + ( + Rbrace, + 58..59, + ), + ( + Rbrace, + 59..60, + ), + ( + TStringEnd, + 60..61, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 61..61, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap new file mode 100644 index 0000000000..739930ef42 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_ipy_escape_command.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "foo ", + ), + 2..6, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 6..7, + ), + ( + Exclamation, + 7..8, + ), + ( + Name( + Name("pwd"), + ), + 8..11, + ), + ( + Rbrace, + 11..12, + ), + ( + InterpolatedStringMiddle( + " bar", + ), + 12..16, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + TStringEnd, + 16..17, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 17..17, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap new file mode 100644 index 0000000000..679142f387 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_lambda_expression.snap @@ -0,0 +1,125 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Lambda, + 3..9, + ), + ( + Name( + Name("x"), + ), + 10..11, + ), + ( + Colon, + 11..12, + ), + ( + Lbrace, + 12..13, + ), + ( + Name( + Name("x"), + ), + 13..14, + ), + ( + Rbrace, + 14..15, + ), + ( + Rbrace, + 15..16, + ), + ( + TStringEnd, + 16..17, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 17..18, + ), + ( + TStringStart, + 18..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Lpar, + 21..22, + ), + ( + Lambda, + 22..28, + ), + ( + Name( + Name("x"), + ), + 29..30, + ), + ( + Colon, + 30..31, + ), + ( + Lbrace, + 31..32, + ), + ( + Name( + Name("x"), + ), + 32..33, + ), + ( + Rbrace, + 33..34, + ), + ( + Rpar, + 34..35, + ), + ( + Rbrace, + 35..36, + ), + ( + TStringEnd, + 36..37, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 37..37, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap new file mode 100644 index 0000000000..1525f2a0eb --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_multiline_format_spec.snap @@ -0,0 +1,295 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..4, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 4..6, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Lbrace, + 6..7, + ), + ( + NonLogicalNewline, + 7..8, + ), + ( + Name( + Name("x"), + ), + 12..13, + ), + ( + Colon, + 13..14, + ), + ( + InterpolatedStringMiddle( + "d\n", + ), + 14..16, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Rbrace, + 16..17, + ), + ( + InterpolatedStringMiddle( + "__", + ), + 17..19, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 19..22, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Newline, + 22..23, + ), + ( + TStringStart, + 23..27, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 27..29, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Lbrace, + 29..30, + ), + ( + NonLogicalNewline, + 30..31, + ), + ( + Name( + Name("x"), + ), + 35..36, + ), + ( + Colon, + 36..37, + ), + ( + InterpolatedStringMiddle( + "a\n b\n c\n", + ), + 37..61, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Rbrace, + 61..62, + ), + ( + InterpolatedStringMiddle( + "__", + ), + 62..64, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + TStringEnd, + 64..67, + TokenFlags( + TRIPLE_QUOTED_STRING | T_STRING, + ), + ), + ( + Newline, + 67..68, + ), + ( + TStringStart, + 68..70, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 70..72, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 72..73, + ), + ( + NonLogicalNewline, + 73..74, + ), + ( + Name( + Name("x"), + ), + 78..79, + ), + ( + Colon, + 79..80, + ), + ( + InterpolatedStringMiddle( + "d", + ), + 80..81, + TokenFlags( + T_STRING, + ), + ), + ( + NonLogicalNewline, + 81..82, + ), + ( + Rbrace, + 82..83, + ), + ( + InterpolatedStringMiddle( + "__", + ), + 83..85, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 85..86, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 86..87, + ), + ( + TStringStart, + 87..89, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "__", + ), + 89..91, + TokenFlags( + T_STRING, + ), + ), + ( + Lbrace, + 91..92, + ), + ( + NonLogicalNewline, + 92..93, + ), + ( + Name( + Name("x"), + ), + 97..98, + ), + ( + Colon, + 98..99, + ), + ( + InterpolatedStringMiddle( + "a", + ), + 99..100, + TokenFlags( + T_STRING, + ), + ), + ( + NonLogicalNewline, + 100..101, + ), + ( + Name( + Name("b"), + ), + 109..110, + ), + ( + NonLogicalNewline, + 110..111, + ), + ( + Rbrace, + 111..112, + ), + ( + InterpolatedStringMiddle( + "__", + ), + 112..114, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 114..115, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 115..116, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap new file mode 100644 index 0000000000..7ab9d20f3f --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_named_expression.snap @@ -0,0 +1,187 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 2..3, + ), + ( + Name( + Name("x"), + ), + 3..4, + ), + ( + Colon, + 4..5, + ), + ( + InterpolatedStringMiddle( + "=10", + ), + 5..8, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Rbrace, + 8..9, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 9..10, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 10..11, + ), + ( + Lpar, + 11..12, + ), + ( + Name( + Name("x"), + ), + 12..13, + ), + ( + ColonEqual, + 13..15, + ), + ( + Int( + 10, + ), + 15..17, + ), + ( + Rpar, + 17..18, + ), + ( + Rbrace, + 18..19, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 19..20, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 20..21, + ), + ( + Name( + Name("x"), + ), + 21..22, + ), + ( + Comma, + 22..23, + ), + ( + Lbrace, + 23..24, + ), + ( + Name( + Name("y"), + ), + 24..25, + ), + ( + ColonEqual, + 25..27, + ), + ( + Int( + 10, + ), + 27..29, + ), + ( + Rbrace, + 29..30, + ), + ( + Rbrace, + 30..31, + ), + ( + InterpolatedStringMiddle( + " ", + ), + 31..32, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Lbrace, + 32..33, + ), + ( + Lsqb, + 33..34, + ), + ( + Name( + Name("x"), + ), + 34..35, + ), + ( + ColonEqual, + 35..37, + ), + ( + Int( + 10, + ), + 37..39, + ), + ( + Rsqb, + 39..40, + ), + ( + Rbrace, + 40..41, + ), + ( + TStringEnd, + 41..42, + TokenFlags( + DOUBLE_QUOTES | T_STRING, + ), + ), + ( + Newline, + 42..42, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap new file mode 100644 index 0000000000..483ea5d6ec --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__lexer__tests__tstring_with_nul_char.snap @@ -0,0 +1,36 @@ +--- +source: crates/ruff_python_parser/src/lexer.rs +expression: lex_source(source) +--- +## Tokens +``` +[ + ( + TStringStart, + 0..2, + TokenFlags( + T_STRING, + ), + ), + ( + InterpolatedStringMiddle( + "\\0", + ), + 2..4, + TokenFlags( + T_STRING, + ), + ), + ( + TStringEnd, + 4..5, + TokenFlags( + T_STRING, + ), + ), + ( + Newline, + 5..5, + ), +] +``` diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap index 99985450fd..78e450c4e2 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_constant_range.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -17,13 +16,13 @@ snapshot_kind: text range: 0..22, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..5, value: "aaa", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..10, expression: Name( ExprName { @@ -38,13 +37,13 @@ snapshot_kind: text }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 10..13, value: "ccc", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 13..18, expression: Name( ExprName { @@ -59,7 +58,7 @@ snapshot_kind: text }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 18..21, value: "eee", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap index 2721f57e77..02db608ab1 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_character.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -17,13 +16,13 @@ snapshot_kind: text range: 0..8, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..4, value: "\\", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 4..7, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap index 44b3a5dcad..9482530d4a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_escaped_newline.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -17,13 +16,13 @@ snapshot_kind: text range: 0..8, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..4, value: "\n", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 4..7, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap index 898ff2d347..6ccd6f466d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_line_continuation.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -17,13 +16,13 @@ snapshot_kind: text range: 0..9, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 3..5, value: "\\\n", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..8, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap index 1f21cd2286..840873127a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..10, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap index 9a6ed32258..3d5f1e79c0 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_base_more.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -17,13 +16,13 @@ snapshot_kind: text range: 0..38, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..6, value: "mix ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 6..13, expression: Name( ExprName { @@ -43,13 +42,13 @@ snapshot_kind: text }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 13..28, value: " with text and ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 28..37, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap index 8c786dbbd1..d2063a00ec 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_parse_self_documenting_format.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..14, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..13, expression: Name( ExprName { @@ -34,11 +33,11 @@ snapshot_kind: text ), conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 9..12, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 9..12, value: ">10", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap index eedba2c8bc..98be2ba27a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__fstring_unescaped_newline.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -17,13 +16,13 @@ snapshot_kind: text range: 0..11, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 4..5, value: "\n", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..8, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap index ffd0191500..1a57e2606a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_fstring.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap new file mode 100644 index 0000000000..3ccfef4404 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_empty_tstring.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..3, + value: TString( + ExprTString { + range: 0..3, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..3, + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap index 983da17da4..e43a1b109c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_1.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -29,7 +28,7 @@ snapshot_kind: text range: 9..17, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 11..16, value: "world", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap index 983da17da4..e43a1b109c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_2.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -29,7 +28,7 @@ snapshot_kind: text range: 9..17, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 11..16, value: "world", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap index 8479de9437..a9f5a2c31f 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_3.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -29,13 +28,13 @@ snapshot_kind: text range: 9..22, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 11..16, value: "world", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 16..21, expression: StringLiteral( ExprStringLiteral { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap index 56a819bd71..6b348cab6d 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_string_concat_4.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -29,13 +28,13 @@ snapshot_kind: text range: 9..22, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 11..16, value: "world", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 16..21, expression: StringLiteral( ExprStringLiteral { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1.snap new file mode 100644 index 0000000000..c1432f9b03 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_1.snap @@ -0,0 +1,58 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..18, + value: TString( + ExprTString { + range: 0..18, + value: TStringValue { + inner: Concatenated( + [ + FString( + FString { + range: 0..9, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..8, + value: "Hello ", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 10..18, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2.snap new file mode 100644 index 0000000000..b3a9d72180 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_f_t_string_concat_2.snap @@ -0,0 +1,69 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..22, + value: TString( + ExprTString { + range: 0..22, + value: TStringValue { + inner: Concatenated( + [ + FString( + FString { + range: 0..9, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..8, + value: "Hello ", + }, + ), + ], + flags: FStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 10..18, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 19..22, + value: "!", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap index 4ec8dc27be..b4fbc87730 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..18, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..5, expression: Name( ExprName { @@ -31,8 +30,8 @@ snapshot_kind: text format_spec: None, }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..10, expression: Name( ExprName { @@ -47,7 +46,7 @@ snapshot_kind: text }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 10..17, value: "{foo}", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap index 58821196bd..57e5d8296c 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_equals.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..13, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..12, expression: Compare( ExprCompare { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap index 9e5b4a2fc8..f0f88bc994 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_concatenation_string_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..16, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..15, expression: Name( ExprName { @@ -29,11 +28,11 @@ snapshot_kind: text debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 7..14, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 7..14, expression: StringLiteral( ExprStringLiteral { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap index 28e6e4a2a1..5b4aebba06 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..15, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..14, expression: Name( ExprName { @@ -29,11 +28,11 @@ snapshot_kind: text debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 7..13, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 7..13, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap index 9dae288239..c9ab599e07 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_nested_string_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..13, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..12, expression: Name( ExprName { @@ -29,11 +28,11 @@ snapshot_kind: text debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 7..11, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 7..11, expression: StringLiteral( ExprStringLiteral { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap index d5884e9d75..b631befca6 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_equals.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..11, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..10, expression: Compare( ExprCompare { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap index 738b731a41..93308bd0ca 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_not_nested_spec.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..13, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..12, expression: Name( ExprName { @@ -29,11 +28,11 @@ snapshot_kind: text debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 7..11, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 7..11, value: "spec", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap index e1d2941dc5..2d72726265 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_prec_space.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..10, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap index e5857594c1..e11b44be8a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_self_doc_trailing_space.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..10, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap index 3dca1cc84b..bcd14124f1 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_fstring_yield_expr.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..10, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, expression: Yield( ExprYield { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1.snap new file mode 100644 index 0000000000..cc79ec38df --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_1.snap @@ -0,0 +1,51 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..17, + value: TString( + ExprTString { + range: 0..17, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 9..17, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2.snap new file mode 100644 index 0000000000..cc79ec38df --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_2.snap @@ -0,0 +1,51 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..17, + value: TString( + ExprTString { + range: 0..17, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 9..17, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3.snap new file mode 100644 index 0000000000..080c41beec --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_3.snap @@ -0,0 +1,77 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..22, + value: TString( + ExprTString { + range: 0..22, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 9..22, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + value: "world", + }, + ), + Interpolation( + InterpolatedElement { + range: 16..21, + expression: StringLiteral( + ExprStringLiteral { + range: 17..20, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 17..20, + value: "!", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4.snap new file mode 100644 index 0000000000..7d178f9569 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_t_string_concat_4.snap @@ -0,0 +1,88 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..31, + value: TString( + ExprTString { + range: 0..31, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..8, + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 9..22, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 11..16, + value: "world", + }, + ), + Interpolation( + InterpolatedElement { + range: 16..21, + expression: StringLiteral( + ExprStringLiteral { + range: 17..20, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 17..20, + value: "!", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 23..31, + value: "again!", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap new file mode 100644 index 0000000000..999de0e0e8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring.snap @@ -0,0 +1,68 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..18, + value: TString( + ExprTString { + range: 0..18, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..18, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..5, + expression: Name( + ExprName { + range: 3..4, + id: Name("a"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Interpolation( + InterpolatedElement { + range: 5..10, + expression: Name( + ExprName { + range: 7..8, + id: Name("b"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 10..17, + value: "{foo}", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap new file mode 100644 index 0000000000..2618c0abf7 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_equals.snap @@ -0,0 +1,66 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..13, + value: TString( + ExprTString { + range: 0..13, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..13, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + expression: Compare( + ExprCompare { + range: 3..11, + left: NumberLiteral( + ExprNumberLiteral { + range: 3..5, + value: Int( + 42, + ), + }, + ), + ops: [ + Eq, + ], + comparators: [ + NumberLiteral( + ExprNumberLiteral { + range: 9..11, + value: Int( + 42, + ), + }, + ), + ], + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap new file mode 100644 index 0000000000..26719dcb0a --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_concatenation_string_spec.snap @@ -0,0 +1,93 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..16, + value: TString( + ExprTString { + range: 0..16, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..16, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..15, + expression: Name( + ExprName { + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..14, + elements: [ + Interpolation( + InterpolatedElement { + range: 7..14, + expression: StringLiteral( + ExprStringLiteral { + range: 8..13, + value: StringLiteralValue { + inner: Concatenated( + ConcatenatedStringLiteral { + strings: [ + StringLiteral { + range: 8..10, + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + StringLiteral { + range: 11..13, + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ], + value: "", + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap new file mode 100644 index 0000000000..0bd592171b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_spec.snap @@ -0,0 +1,68 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..15, + value: TString( + ExprTString { + range: 0..15, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..15, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..14, + expression: Name( + ExprName { + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..13, + elements: [ + Interpolation( + InterpolatedElement { + range: 7..13, + expression: Name( + ExprName { + range: 8..12, + id: Name("spec"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap new file mode 100644 index 0000000000..cfa89174c8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_nested_string_spec.snap @@ -0,0 +1,79 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..13, + value: TString( + ExprTString { + range: 0..13, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..13, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + expression: Name( + ExprName { + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..11, + elements: [ + Interpolation( + InterpolatedElement { + range: 7..11, + expression: StringLiteral( + ExprStringLiteral { + range: 8..10, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 8..10, + value: "", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap new file mode 100644 index 0000000000..96bc26e6fc --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_equals.snap @@ -0,0 +1,66 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..11, + value: TString( + ExprTString { + range: 0..11, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..11, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..10, + expression: Compare( + ExprCompare { + range: 3..9, + left: NumberLiteral( + ExprNumberLiteral { + range: 3..4, + value: Int( + 1, + ), + }, + ), + ops: [ + NotEq, + ], + comparators: [ + NumberLiteral( + ExprNumberLiteral { + range: 8..9, + value: Int( + 2, + ), + }, + ), + ], + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap new file mode 100644 index 0000000000..b77e560ea2 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_not_nested_spec.snap @@ -0,0 +1,59 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..13, + value: TString( + ExprTString { + range: 0..13, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..13, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..12, + expression: Name( + ExprName { + range: 3..6, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 7..11, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 7..11, + value: "spec", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap new file mode 100644 index 0000000000..dc1558c8d2 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_prec_space.snap @@ -0,0 +1,52 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..10, + value: TString( + ExprTString { + range: 0..10, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..10, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + expression: Name( + ExprName { + range: 3..4, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: " =", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap new file mode 100644 index 0000000000..a6c8b8849b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_self_doc_trailing_space.snap @@ -0,0 +1,52 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..10, + value: TString( + ExprTString { + range: 0..10, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..10, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + expression: Name( + ExprName { + range: 3..4, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "= ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap new file mode 100644 index 0000000000..7693375a34 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_tstring_yield_expr.snap @@ -0,0 +1,46 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..10, + value: TString( + ExprTString { + range: 0..10, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..10, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + expression: Yield( + ExprYield { + range: 3..8, + value: None, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap index 2d864494e5..31a9098fde 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_1.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -29,7 +28,7 @@ snapshot_kind: text range: 10..18, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 12..17, value: "world", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap index e3bafd8a1f..04c02e6462 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_f_string_concat_2.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -29,7 +28,7 @@ snapshot_kind: text range: 10..18, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 12..17, value: "world", }, diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1.snap new file mode 100644 index 0000000000..0fd3e39703 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_1.snap @@ -0,0 +1,51 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..18, + value: TString( + ExprTString { + range: 0..18, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..9, + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 10..18, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2.snap new file mode 100644 index 0000000000..ae5721a931 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__parse_u_t_string_concat_2.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..22, + value: TString( + ExprTString { + range: 0..22, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 0..9, + value: "Hello ", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 10..18, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 12..17, + value: "world", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 19..22, + value: "!", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap index ff531b735e..d835921a9a 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_fstring.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..7, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 3..6, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap new file mode 100644 index 0000000000..5f16d7cc6b --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__raw_tstring.snap @@ -0,0 +1,49 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..7, + value: TString( + ExprTString { + range: 0..7, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..7, + elements: [ + Interpolation( + InterpolatedElement { + range: 3..6, + expression: Name( + ExprName { + range: 4..5, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap index 1d0ed68bc3..2c0adae7d1 100644 --- a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_fstring.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/src/string.rs expression: suite -snapshot_kind: text --- [ Expr( @@ -16,8 +15,8 @@ snapshot_kind: text FString { range: 0..11, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 5..8, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap new file mode 100644 index 0000000000..7a383de39c --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__triple_quoted_raw_tstring.snap @@ -0,0 +1,49 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..11, + value: TString( + ExprTString { + range: 0..11, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..11, + elements: [ + Interpolation( + InterpolatedElement { + range: 5..8, + expression: Name( + ExprName { + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap new file mode 100644 index 0000000000..6227dea0fe --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_constant_range.snap @@ -0,0 +1,80 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..22, + value: TString( + ExprTString { + range: 0..22, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..22, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..5, + value: "aaa", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..10, + expression: Name( + ExprName { + range: 6..9, + id: Name("bbb"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 10..13, + value: "ccc", + }, + ), + Interpolation( + InterpolatedElement { + range: 13..18, + expression: Name( + ExprName { + range: 14..17, + id: Name("ddd"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 18..21, + value: "eee", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap new file mode 100644 index 0000000000..b72088efc8 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_character.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..8, + value: TString( + ExprTString { + range: 0..8, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..8, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..4, + value: "\\", + }, + ), + Interpolation( + InterpolatedElement { + range: 4..7, + expression: Name( + ExprName { + range: 5..6, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap new file mode 100644 index 0000000000..d34b25231f --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_escaped_newline.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..8, + value: TString( + ExprTString { + range: 0..8, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..8, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..4, + value: "\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 4..7, + expression: Name( + ExprName { + range: 5..6, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap new file mode 100644 index 0000000000..396755f985 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_line_continuation.snap @@ -0,0 +1,55 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..9, + value: TString( + ExprTString { + range: 0..9, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..9, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 3..5, + value: "\\\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..8, + expression: Name( + ExprName { + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap new file mode 100644 index 0000000000..99398a48b1 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base.snap @@ -0,0 +1,52 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..10, + value: TString( + ExprTString { + range: 0..10, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..10, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..9, + expression: Name( + ExprName { + range: 3..7, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap new file mode 100644 index 0000000000..a745f0f639 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_base_more.snap @@ -0,0 +1,84 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..38, + value: TString( + ExprTString { + range: 0..38, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..38, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 2..6, + value: "mix ", + }, + ), + Interpolation( + InterpolatedElement { + range: 6..13, + expression: Name( + ExprName { + range: 7..11, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 13..28, + value: " with text and ", + }, + ), + Interpolation( + InterpolatedElement { + range: 28..37, + expression: Name( + ExprName { + range: 29..35, + id: Name("second"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap new file mode 100644 index 0000000000..a401f11e74 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_parse_self_documenting_format.snap @@ -0,0 +1,64 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..14, + value: TString( + ExprTString { + range: 0..14, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..14, + elements: [ + Interpolation( + InterpolatedElement { + range: 2..13, + expression: Name( + ExprName { + range: 3..7, + id: Name("user"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 9..12, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 9..12, + value: ">10", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap new file mode 100644 index 0000000000..b14e366fb4 --- /dev/null +++ b/crates/ruff_python_parser/src/snapshots/ruff_python_parser__string__tests__tstring_unescaped_newline.snap @@ -0,0 +1,53 @@ +--- +source: crates/ruff_python_parser/src/string.rs +expression: suite +--- +[ + Expr( + StmtExpr { + range: 0..11, + value: TString( + ExprTString { + range: 0..11, + value: TStringValue { + inner: Single( + TString( + TString { + range: 0..11, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 4..5, + value: "\n", + }, + ), + Interpolation( + InterpolatedElement { + range: 5..8, + expression: Name( + ExprName { + range: 6..7, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), +] diff --git a/crates/ruff_python_parser/src/string.rs b/crates/ruff_python_parser/src/string.rs index 7ae24798e3..6642fbc25a 100644 --- a/crates/ruff_python_parser/src/string.rs +++ b/crates/ruff_python_parser/src/string.rs @@ -1,17 +1,22 @@ //! Parsing of string literals, bytes literals, and implicit string concatenation. use bstr::ByteSlice; +use std::fmt; use ruff_python_ast::{self as ast, AnyStringFlags, Expr, StringFlags}; use ruff_text_size::{Ranged, TextRange, TextSize}; -use crate::error::{LexicalError, LexicalErrorType}; +use crate::{ + TokenKind, + error::{LexicalError, LexicalErrorType}, +}; #[derive(Debug)] pub(crate) enum StringType { Str(ast::StringLiteral), Bytes(ast::BytesLiteral), FString(ast::FString), + TString(ast::TString), } impl Ranged for StringType { @@ -20,6 +25,7 @@ impl Ranged for StringType { Self::Str(node) => node.range(), Self::Bytes(node) => node.range(), Self::FString(node) => node.range(), + Self::TString(node) => node.range(), } } } @@ -30,6 +36,48 @@ impl From for Expr { StringType::Str(node) => Expr::from(node), StringType::Bytes(node) => Expr::from(node), StringType::FString(node) => Expr::from(node), + StringType::TString(node) => Expr::from(node), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) enum InterpolatedStringKind { + FString, + TString, +} + +impl InterpolatedStringKind { + #[inline] + pub(crate) const fn start_token(self) -> TokenKind { + match self { + InterpolatedStringKind::FString => TokenKind::FStringStart, + InterpolatedStringKind::TString => TokenKind::TStringStart, + } + } + + #[inline] + pub(crate) const fn middle_token(self) -> TokenKind { + match self { + InterpolatedStringKind::FString => TokenKind::FStringMiddle, + InterpolatedStringKind::TString => TokenKind::TStringMiddle, + } + } + + #[inline] + pub(crate) const fn end_token(self) -> TokenKind { + match self { + InterpolatedStringKind::FString => TokenKind::FStringEnd, + InterpolatedStringKind::TString => TokenKind::TStringEnd, + } + } +} + +impl fmt::Display for InterpolatedStringKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InterpolatedStringKind::FString => f.write_str("f-string"), + InterpolatedStringKind::TString => f.write_str("t-string"), } } } @@ -231,10 +279,12 @@ impl StringParser { Ok(Some(EscapedChar::Literal(new_char))) } - fn parse_fstring_middle(mut self) -> Result { - // Fast-path: if the f-string doesn't contain any escape sequences, return the literal. + fn parse_interpolated_string_middle( + mut self, + ) -> Result { + // Fast-path: if the f-string or t-string doesn't contain any escape sequences, return the literal. let Some(mut index) = memchr::memchr3(b'{', b'}', b'\\', self.source.as_bytes()) else { - return Ok(ast::FStringLiteralElement { + return Ok(ast::InterpolatedStringLiteralElement { value: self.source, range: self.range, }); @@ -249,7 +299,7 @@ impl StringParser { // Add the escaped character to the string. match &self.source.as_bytes()[self.cursor - 1] { - // If there are any curly braces inside a `FStringMiddle` token, + // If there are any curly braces inside a `F/TStringMiddle` token, // then they were escaped (i.e. `{{` or `}}`). This means that // we need increase the location by 2 instead of 1. b'{' => { @@ -260,7 +310,7 @@ impl StringParser { self.offset += TextSize::from(1); value.push('}'); } - // We can encounter a `\` as the last character in a `FStringMiddle` + // We can encounter a `\` as the last character in a `F/TStringMiddle` // token which is valid in this context. For example, // // ```python @@ -268,7 +318,7 @@ impl StringParser { // # ^ ^^ ^ // ``` // - // Here, the `FStringMiddle` token content will be "\" and " \" + // Here, the `F/TStringMiddle` token content will be "\" and " \" // which is invalid if we look at the content in isolation: // // ```python @@ -276,7 +326,7 @@ impl StringParser { // ``` // // However, the content is syntactically valid in the context of - // the f-string because it's a substring of the entire f-string. + // the f/t-string because it's a substring of the entire f/t-string. // This is still an invalid escape sequence, but we don't want to // raise a syntax error as is done by the CPython parser. It might // be supported in the future, refer to point 3: https://peps.python.org/pep-0701/#rejected-ideas @@ -311,7 +361,7 @@ impl StringParser { index = next_index; } - Ok(ast::FStringLiteralElement { + Ok(ast::InterpolatedStringLiteralElement { value: value.into_boxed_str(), range: self.range, }) @@ -458,12 +508,12 @@ pub(crate) fn parse_string_literal( } // TODO(dhruvmanila): Move this to the new parser -pub(crate) fn parse_fstring_literal_element( +pub(crate) fn parse_interpolated_string_literal_element( source: Box, flags: AnyStringFlags, range: TextRange, -) -> Result { - StringParser::new(source, flags, range.start(), range).parse_fstring_middle() +) -> Result { + StringParser::new(source, flags, range.start(), range).parse_interpolated_string_middle() } #[cfg(test)] @@ -471,7 +521,7 @@ mod tests { use ruff_python_ast::Suite; use crate::error::LexicalErrorType; - use crate::{FStringErrorType, ParseError, ParseErrorType, Parsed, parse_module}; + use crate::{InterpolatedStringErrorType, ParseError, ParseErrorType, Parsed, parse_module}; const WINDOWS_EOL: &str = "\r\n"; const MAC_EOL: &str = "\r"; @@ -553,7 +603,7 @@ mod tests { insta::assert_debug_snapshot!(suite); } - fn parse_fstring_error(source: &str) -> FStringErrorType { + fn parse_fstring_error(source: &str) -> InterpolatedStringErrorType { parse_suite(source) .map_err(|e| match e.error { ParseErrorType::Lexical(LexicalErrorType::FStringError(e)) => e, @@ -565,7 +615,7 @@ mod tests { #[test] fn test_parse_invalid_fstring() { - use FStringErrorType::{InvalidConversionFlag, LambdaWithoutParentheses}; + use InterpolatedStringErrorType::{InvalidConversionFlag, LambdaWithoutParentheses}; assert_eq!(parse_fstring_error(r#"f"{5!x}""#), InvalidConversionFlag); assert_eq!( @@ -616,6 +666,118 @@ mod tests { insta::assert_debug_snapshot!(suite); } + #[test] + fn test_parse_tstring() { + let source = r#"t"{a}{ b }{{foo}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_nested_spec() { + let source = r#"t"{foo:{spec}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_not_nested_spec() { + let source = r#"t"{foo:spec}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_empty_tstring() { + let source = r#"t"""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_parse_self_documenting_base() { + let source = r#"t"{user=}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_parse_self_documenting_base_more() { + let source = r#"t"mix {user=} with text and {second=}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_parse_self_documenting_format() { + let source = r#"t"{user=:>10}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + fn parse_tstring_error(source: &str) -> InterpolatedStringErrorType { + parse_suite(source) + .map_err(|e| match e.error { + ParseErrorType::Lexical(LexicalErrorType::TStringError(e)) => e, + ParseErrorType::TStringError(e) => e, + e => unreachable!("Expected TStringError: {:?}", e), + }) + .expect_err("Expected error") + } + + #[test] + fn test_parse_invalid_tstring() { + use InterpolatedStringErrorType::{InvalidConversionFlag, LambdaWithoutParentheses}; + + assert_eq!(parse_tstring_error(r#"t"{5!x}""#), InvalidConversionFlag); + assert_eq!( + parse_tstring_error("t'{lambda x:{x}}'"), + LambdaWithoutParentheses + ); + // NOTE: The parser produces the `LambdaWithoutParentheses` for this case, but + // since the parser only return the first error to maintain compatibility with + // the rest of the codebase, this test case fails. The `LambdaWithoutParentheses` + // error appears after the unexpected `tStringMiddle` token, which is between the + // `:` and the `{`. + // assert_eq!(parse_tstring_error("f'{lambda x: {x}}'"), LambdaWithoutParentheses); + assert!(parse_suite(r#"t"{class}""#).is_err()); + } + + #[test] + fn test_parse_tstring_not_equals() { + let source = r#"t"{1 != 2}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_equals() { + let source = r#"t"{42 == 42}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_self_doc_prec_space() { + let source = r#"t"{x =}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_self_doc_trailing_space() { + let source = r#"t"{x= }""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_yield_expr() { + let source = r#"t"{yield}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + #[test] fn test_parse_string_concat() { let source = "'Hello ' 'world'"; @@ -679,6 +841,62 @@ mod tests { insta::assert_debug_snapshot!(suite); } + #[test] + fn test_parse_t_string_concat_1() { + let source = "'Hello ' t'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_2() { + let source = "'Hello ' t'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_3() { + let source = "'Hello ' t'world{\"!\"}'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_t_string_concat_4() { + let source = "'Hello ' t'world{\"!\"}' 'again!'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_u_t_string_concat_1() { + let source = "u'Hello ' t'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_u_t_string_concat_2() { + let source = "u'Hello ' t'world' '!'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_f_t_string_concat_1() { + let source = "f'Hello ' t'world'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_f_t_string_concat_2() { + let source = "f'Hello ' t'world' '!'"; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + #[test] fn test_parse_string_triple_quotes_with_kind() { let source = "u'''Hello, world!'''"; @@ -796,6 +1014,71 @@ mod tests { insta::assert_debug_snapshot!(suite); } + #[test] + fn test_tstring_escaped_newline() { + let source = r#"t"\n{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_constant_range() { + let source = r#"t"aaa{bbb}ccc{ddd}eee""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_unescaped_newline() { + let source = r#"t""" +{x}""""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_escaped_character() { + let source = r#"t"\\{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_raw_tstring() { + let source = r#"rt"{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_triple_quoted_raw_tstring() { + let source = r#"rt"""{x}""""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_tstring_line_continuation() { + let source = r#"rt"\ +{x}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_nested_string_spec() { + let source = r#"t"{foo:{''}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + + #[test] + fn test_parse_tstring_nested_concatenation_string_spec() { + let source = r#"t"{foo:{'' ''}}""#; + let suite = parse_suite(source).unwrap(); + insta::assert_debug_snapshot!(suite); + } + /// #[test] fn test_dont_panic_on_8_in_octal_escape() { diff --git a/crates/ruff_python_parser/src/token.rs b/crates/ruff_python_parser/src/token.rs index 193aecac51..59e4c0581c 100644 --- a/crates/ruff_python_parser/src/token.rs +++ b/crates/ruff_python_parser/src/token.rs @@ -12,7 +12,7 @@ use bitflags::bitflags; use ruff_python_ast::name::Name; use ruff_python_ast::str::{Quote, TripleQuotes}; use ruff_python_ast::str_prefix::{ - AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, + AnyStringPrefix, ByteStringPrefix, FStringPrefix, StringLiteralPrefix, TStringPrefix, }; use ruff_python_ast::{AnyStringFlags, BoolOp, Int, IpyEscapeKind, Operator, StringFlags, UnaryOp}; use ruff_text_size::{Ranged, TextRange}; @@ -48,7 +48,7 @@ impl Token { /// /// # Panics /// - /// If it isn't a string or any f-string tokens. + /// If it isn't a string or any f/t-string tokens. pub fn is_triple_quoted_string(self) -> bool { self.unwrap_string_flags().is_triple_quoted() } @@ -57,7 +57,7 @@ impl Token { /// /// # Panics /// - /// If it isn't a string or any f-string tokens. + /// If it isn't a string or any f/t-string tokens. pub fn string_quote_style(self) -> Quote { self.unwrap_string_flags().quote_style() } @@ -66,7 +66,7 @@ impl Token { /// /// # Panics /// - /// If it isn't a string or any f-string tokens. + /// If it isn't a string or any f/t-string tokens. pub fn unwrap_string_flags(self) -> AnyStringFlags { self.string_flags() .unwrap_or_else(|| panic!("token to be a string")) @@ -81,7 +81,8 @@ impl Token { } } - /// Returns `true` if this is any kind of string token. + /// Returns `true` if this is any kind of string token - including + /// tokens in t-strings (which do not have type `str`). const fn is_any_string(self) -> bool { matches!( self.kind, @@ -89,6 +90,9 @@ impl Token { | TokenKind::FStringStart | TokenKind::FStringMiddle | TokenKind::FStringEnd + | TokenKind::TStringStart + | TokenKind::TStringMiddle + | TokenKind::TStringEnd ) } } @@ -140,6 +144,14 @@ pub enum TokenKind { FStringMiddle, /// Token kind for the end of an f-string. This includes the closing quote. FStringEnd, + /// Token kind for the start of a t-string. This includes the `t`/`T`/`tr` prefix + /// and the opening quote(s). + TStringStart, + /// Token kind that includes the portion of text inside the t-string that's not + /// part of the interpolation part and isn't an opening or closing brace. + TStringMiddle, + /// Token kind for the end of a t-string. This includes the closing quote. + TStringEnd, /// Token kind for a IPython escape command. IpyEscapeCommand, /// Token kind for a comment. These are filtered out of the token stream prior to parsing. @@ -462,6 +474,11 @@ impl TokenKind { matches!(self, TokenKind::Plus | TokenKind::Minus) } + #[inline] + pub const fn is_interpolated_string_end(self) -> bool { + matches!(self, TokenKind::FStringEnd | TokenKind::TStringEnd) + } + /// Returns the [`UnaryOp`] that corresponds to this token kind, if it is a unary arithmetic /// operator, otherwise return [None]. /// @@ -613,6 +630,9 @@ impl fmt::Display for TokenKind { TokenKind::FStringStart => "FStringStart", TokenKind::FStringMiddle => "FStringMiddle", TokenKind::FStringEnd => "FStringEnd", + TokenKind::TStringStart => "TStringStart", + TokenKind::TStringMiddle => "TStringMiddle", + TokenKind::TStringEnd => "TStringEnd", TokenKind::IpyEscapeCommand => "IPython escape command", TokenKind::Comment => "comment", TokenKind::Question => "'?'", @@ -722,10 +742,12 @@ bitflags! { const BYTE_STRING = 1 << 3; /// The token is an f-string i.e., prefixed with `f` or `F` const F_STRING = 1 << 4; + /// The token is a t-string i.e., prefixed with `t` or `T` + const T_STRING = 1 << 5; /// The token is a raw string and the prefix character is in lowercase. - const RAW_STRING_LOWERCASE = 1 << 5; + const RAW_STRING_LOWERCASE = 1 << 6; /// The token is a raw string and the prefix character is in uppercase. - const RAW_STRING_UPPERCASE = 1 << 6; + const RAW_STRING_UPPERCASE = 1 << 7; /// The token is a raw string i.e., prefixed with `r` or `R` const RAW_STRING = Self::RAW_STRING_LOWERCASE.bits() | Self::RAW_STRING_UPPERCASE.bits(); @@ -758,6 +780,14 @@ impl StringFlags for TokenFlags { } else { AnyStringPrefix::Format(FStringPrefix::Regular) } + } else if self.intersects(TokenFlags::T_STRING) { + if self.intersects(TokenFlags::RAW_STRING_LOWERCASE) { + AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: false }) + } else if self.intersects(TokenFlags::RAW_STRING_UPPERCASE) { + AnyStringPrefix::Template(TStringPrefix::Raw { uppercase_r: true }) + } else { + AnyStringPrefix::Template(TStringPrefix::Regular) + } } else if self.intersects(TokenFlags::BYTE_STRING) { if self.intersects(TokenFlags::RAW_STRING_LOWERCASE) { AnyStringPrefix::Bytes(ByteStringPrefix::Raw { uppercase_r: false }) @@ -784,9 +814,19 @@ impl TokenFlags { self.intersects(TokenFlags::F_STRING) } - /// Returns `true` if the token is a triple-quoted f-string. - pub(crate) fn is_triple_quoted_fstring(self) -> bool { - self.contains(TokenFlags::F_STRING | TokenFlags::TRIPLE_QUOTED_STRING) + /// Returns `true` if the token is a t-string. + pub(crate) const fn is_t_string(self) -> bool { + self.intersects(TokenFlags::T_STRING) + } + + /// Returns `true` if the token is a t-string. + pub(crate) const fn is_interpolated_string(self) -> bool { + self.intersects(TokenFlags::T_STRING.union(TokenFlags::F_STRING)) + } + + /// Returns `true` if the token is a triple-quoted t-string. + pub(crate) fn is_triple_quoted_interpolated_string(self) -> bool { + self.intersects(TokenFlags::TRIPLE_QUOTED_STRING) && self.is_interpolated_string() } /// Returns `true` if the token is a raw string. @@ -819,7 +859,7 @@ pub(crate) enum TokenValue { String(Box), /// Token value that includes the portion of text inside the f-string that's not /// part of the expression part and isn't an opening or closing brace. - FStringMiddle(Box), + InterpolatedStringMiddle(Box), /// Token value for IPython escape commands. These are recognized by the lexer /// only when the mode is [`Mode::Ipython`]. IpyEscapeCommand { diff --git a/crates/ruff_python_parser/tests/fixtures.rs b/crates/ruff_python_parser/tests/fixtures.rs index 9284e312db..7aa7a6f2e2 100644 --- a/crates/ruff_python_parser/tests/fixtures.rs +++ b/crates/ruff_python_parser/tests/fixtures.rs @@ -40,7 +40,7 @@ fn inline_err() { fn test_valid_syntax(input_path: &Path) { let source = fs::read_to_string(input_path).expect("Expected test file to exist"); let options = extract_options(&source).unwrap_or_else(|| { - ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest()) + ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest_preview()) }); let parsed = parse_unchecked(&source, options.clone()); @@ -133,7 +133,7 @@ fn test_valid_syntax(input_path: &Path) { fn test_invalid_syntax(input_path: &Path) { let source = fs::read_to_string(input_path).expect("Expected test file to exist"); let options = extract_options(&source).unwrap_or_else(|| { - ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest()) + ParseOptions::from(Mode::Module).with_target_version(PythonVersion::PY314) }); let parsed = parse_unchecked(&source, options.clone()); diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap index 76f99c4e88..b758b7438b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@ann_assign_stmt_invalid_annotation.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/ann_assign_stmt_invalid_annotation.py -snapshot_kind: text --- ## AST @@ -199,3 +198,23 @@ Module( 4 | x: y := int = 1 | ^^ Syntax Error: Expected a statement | + + +## Semantic Syntax Errors + + | +1 | x: *int = 1 +2 | x: yield a = 1 + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation +3 | x: yield from b = 1 +4 | x: y := int = 1 + | + + + | +1 | x: *int = 1 +2 | x: yield a = 1 +3 | x: yield from b = 1 + | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation +4 | x: y := int = 1 + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap index 76a48f2197..c8b4ec3c12 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_empty_expression.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_empty_expression.py -snapshot_kind: text --- ## AST @@ -22,8 +21,8 @@ Module( FString { range: 0..5, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..4, expression: Name( ExprName { @@ -63,8 +62,8 @@ Module( FString { range: 6..13, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 8..12, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap index 4a635ad345..36b66627ad 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_name_tok.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_invalid_conversion_flag_name_tok.py -snapshot_kind: text --- ## AST @@ -22,8 +21,8 @@ Module( FString { range: 0..8, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..7, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap index 2f36142537..0ee7e5fe73 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_conversion_flag_other_tok.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_invalid_conversion_flag_other_tok.py -snapshot_kind: text --- ## AST @@ -22,8 +21,8 @@ Module( FString { range: 0..10, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..9, expression: Name( ExprName { @@ -63,8 +62,8 @@ Module( FString { range: 11..21, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 13..20, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap index e02a5c22b8..f47c692d58 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_invalid_starred_expr.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_invalid_starred_expr.py -snapshot_kind: text --- ## AST @@ -22,8 +21,8 @@ Module( FString { range: 77..83, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 79..82, expression: Starred( ExprStarred { @@ -69,8 +68,8 @@ Module( FString { range: 84..97, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 86..96, expression: Starred( ExprStarred { @@ -131,8 +130,8 @@ Module( FString { range: 98..111, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 100..110, expression: Starred( ExprStarred { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap index 05f1e2e3ff..121ca20d3c 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_lambda_without_parentheses.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_lambda_without_parentheses.py -snapshot_kind: text --- ## AST @@ -22,8 +21,8 @@ Module( FString { range: 0..16, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..12, expression: Lambda( ExprLambda { @@ -66,7 +65,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 12..14, value: " x", }, @@ -111,5 +110,5 @@ Module( | 1 | f"{lambda x: x}" - | ^ Syntax Error: Expected an f-string element or the end of the f-string + | ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap index 7714d9e162..9fd2f1b9b2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace.py.snap @@ -21,8 +21,8 @@ Module( FString { range: 0..4, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2..3, expression: Name( ExprName { @@ -62,8 +62,8 @@ Module( FString { range: 5..14, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 7..14, expression: Name( ExprName { @@ -103,8 +103,8 @@ Module( FString { range: 15..23, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 17..22, expression: Name( ExprName { @@ -150,8 +150,8 @@ Module( FString { range: 24..28, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 26..27, expression: Name( ExprName { @@ -177,8 +177,8 @@ Module( FString { range: 29..37, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 33..34, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap index c72b2029e9..81f2c236a9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@f_string_unclosed_lbrace_in_format_spec.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/f_string_unclosed_lbrace_in_format_spec.py -snapshot_kind: text --- ## AST @@ -23,13 +22,13 @@ Module( range: 0..12, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..8, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 8..11, expression: Name( ExprName { @@ -41,7 +40,7 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 11..11, elements: [], }, @@ -75,13 +74,13 @@ Module( range: 13..28, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 15..21, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 21..27, expression: Name( ExprName { @@ -93,11 +92,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 24..27, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 24..27, value: ".3f", }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap index 0d1731ead8..f1968dedf9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@function_def_invalid_return_expr.py.snap @@ -179,3 +179,13 @@ Module( 3 | def foo() -> yield x: ... | ^^^^^^^ Syntax Error: Yield expression cannot be used here | + + +## Semantic Syntax Errors + + | +1 | def foo() -> *int: ... +2 | def foo() -> (*int): ... +3 | def foo() -> yield x: ... + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string.py.snap index 2ba54bef2f..5488502486 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string.py.snap @@ -84,13 +84,13 @@ Module( range: 29..40, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 31..37, value: "world ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 37..40, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string_multiline.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string_multiline.py.snap index cdfccb4442..672216d159 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string_multiline.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@implicitly_concatenated_unterminated_string_multiline.py.snap @@ -34,13 +34,13 @@ Module( range: 18..31, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 20..26, value: "world ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 26..29, expression: Name( ExprName { @@ -132,7 +132,7 @@ Module( range: 68..76, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 70..75, value: "third", }, @@ -198,7 +198,7 @@ Module( 2 | 'hello' 3 | f'world {x} 4 | ) - | ^ Syntax Error: Expected an f-string element or the end of the f-string + | ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string 5 | 1 + 1 6 | ( | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap index b21adb9b16..66d0def077 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_annotation_function.py.snap @@ -1397,7 +1397,7 @@ Module( | 1 | def d[T]() -> (await 1): ... - | ^^^^^^^ Syntax Error: await expression cannot be used within a generic definition + | ^^^^^^^ Syntax Error: await expression cannot be used within a type annotation 2 | def e[T](arg: (await 1)): ... 3 | def f[T]() -> (y := 3): ... | @@ -1406,7 +1406,7 @@ Module( | 1 | def d[T]() -> (await 1): ... 2 | def e[T](arg: (await 1)): ... - | ^^^^^^^ Syntax Error: await expression cannot be used within a generic definition + | ^^^^^^^ Syntax Error: await expression cannot be used within a type annotation 3 | def f[T]() -> (y := 3): ... 4 | def g[T](arg: (x := 1)): ... | @@ -1416,7 +1416,7 @@ Module( 1 | def d[T]() -> (await 1): ... 2 | def e[T](arg: (await 1)): ... 3 | def f[T]() -> (y := 3): ... - | ^^^^^^ Syntax Error: named expression cannot be used within a generic definition + | ^^^^^^ Syntax Error: named expression cannot be used within a type annotation 4 | def g[T](arg: (x := 1)): ... 5 | def h[T](x: (yield 1)): ... | @@ -1426,7 +1426,7 @@ Module( 2 | def e[T](arg: (await 1)): ... 3 | def f[T]() -> (y := 3): ... 4 | def g[T](arg: (x := 1)): ... - | ^^^^^^ Syntax Error: named expression cannot be used within a generic definition + | ^^^^^^ Syntax Error: named expression cannot be used within a type annotation 5 | def h[T](x: (yield 1)): ... 6 | def j[T]() -> (yield 1): ... | @@ -1436,7 +1436,7 @@ Module( 3 | def f[T]() -> (y := 3): ... 4 | def g[T](arg: (x := 1)): ... 5 | def h[T](x: (yield 1)): ... - | ^^^^^^^ Syntax Error: yield expression cannot be used within a generic definition + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation 6 | def j[T]() -> (yield 1): ... 7 | def l[T](x: (yield from 1)): ... | @@ -1446,7 +1446,7 @@ Module( 4 | def g[T](arg: (x := 1)): ... 5 | def h[T](x: (yield 1)): ... 6 | def j[T]() -> (yield 1): ... - | ^^^^^^^ Syntax Error: yield expression cannot be used within a generic definition + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation 7 | def l[T](x: (yield from 1)): ... 8 | def n[T]() -> (yield from 1): ... | @@ -1456,7 +1456,7 @@ Module( 5 | def h[T](x: (yield 1)): ... 6 | def j[T]() -> (yield 1): ... 7 | def l[T](x: (yield from 1)): ... - | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a generic definition + | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation 8 | def n[T]() -> (yield from 1): ... 9 | def p[T: (yield 1)](): ... # yield in TypeVar bound | @@ -1466,7 +1466,7 @@ Module( 6 | def j[T]() -> (yield 1): ... 7 | def l[T](x: (yield from 1)): ... 8 | def n[T]() -> (yield from 1): ... - | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a generic definition + | ^^^^^^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation 9 | def p[T: (yield 1)](): ... # yield in TypeVar bound 10 | def q[T = (yield 1)](): ... # yield in TypeVar default | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap index 790a2e91ca..9c628a81d1 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@invalid_fstring_literal_element.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/invalid_fstring_literal_element.py -snapshot_kind: text --- ## AST @@ -23,7 +22,7 @@ Module( range: 0..26, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..25, value: "", }, @@ -55,7 +54,7 @@ Module( range: 27..57, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 31..54, value: "", }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap index 07d4518fcc..35384a51a4 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@mixed_bytes_and_non_bytes_literals.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/err/mixed_bytes_and_non_bytes_literals.py -snapshot_kind: text --- ## AST @@ -61,7 +60,7 @@ Module( range: 18..26, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 20..25, value: "first", }, @@ -117,7 +116,7 @@ Module( range: 45..54, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 47..53, value: "second", }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap index c63c0fdb25..d799a78939 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_annotation.py.snap @@ -224,3 +224,13 @@ Module( 3 | def foo(arg: x := int): ... | ^^ Syntax Error: Expected ',', found ':=' | + + +## Semantic Syntax Errors + + | +1 | def foo(arg: *int): ... +2 | def foo(arg: yield int): ... + | ^^^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation +3 | def foo(arg: x := int): ... + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_star_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_star_annotation.py.snap index f036adb4b7..13eeeed689 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_star_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@param_with_invalid_star_annotation.py.snap @@ -308,3 +308,14 @@ Module( | ^^^^^^^ Syntax Error: Yield expression cannot be used here 5 | # def foo(*args: **int): ... | + + +## Semantic Syntax Errors + + | +2 | def foo(*args: (*tuple[int])): ... +3 | def foo(*args: *int or str): ... +4 | def foo(*args: *yield x): ... + | ^^^^^^^ Syntax Error: yield expression cannot be used within a type annotation +5 | # def foo(*args: **int): ... + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap index 5ac816ecc8..34308fc597 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@pep701_f_string_py311.py.snap @@ -22,13 +22,13 @@ Module( range: 44..74, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 46..58, value: "Magic wand: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 58..73, expression: Subscript( ExprSubscript { @@ -92,8 +92,8 @@ Module( FString { range: 95..112, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 97..111, expression: Call( ExprCall { @@ -173,13 +173,13 @@ Module( range: 148..220, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 152..169, value: "A complex trick: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 169..217, expression: Subscript( ExprSubscript { @@ -243,8 +243,8 @@ Module( FString { range: 221..254, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 223..253, expression: FString( ExprFString { @@ -255,8 +255,8 @@ Module( FString { range: 224..252, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 226..251, expression: FString( ExprFString { @@ -267,8 +267,8 @@ Module( FString { range: 227..250, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 229..249, expression: FString( ExprFString { @@ -279,8 +279,8 @@ Module( FString { range: 230..248, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 232..247, expression: FString( ExprFString { @@ -291,8 +291,8 @@ Module( FString { range: 233..246, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 235..245, expression: FString( ExprFString { @@ -303,8 +303,8 @@ Module( FString { range: 236..244, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 238..243, expression: BinOp( ExprBinOp { @@ -444,8 +444,8 @@ Module( FString { range: 276..310, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 278..303, expression: FString( ExprFString { @@ -456,8 +456,8 @@ Module( FString { range: 279..302, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 283..293, expression: StringLiteral( ExprStringLiteral { @@ -483,7 +483,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 293..299, value: " inner", }, @@ -506,7 +506,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 303..309, value: " outer", }, @@ -538,13 +538,13 @@ Module( range: 336..359, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 338..343, value: "test ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 343..353, expression: Name( ExprName { @@ -559,7 +559,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 353..358, value: " more", }, @@ -590,8 +590,8 @@ Module( FString { range: 403..422, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 407..419, expression: FString( ExprFString { @@ -602,8 +602,8 @@ Module( FString { range: 408..418, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 412..415, expression: Name( ExprName { @@ -660,8 +660,8 @@ Module( FString { range: 468..502, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 470..501, expression: Call( ExprCall { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap index 48dc7f7866..9db9481b22 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lex_logical_token.py.snap @@ -527,13 +527,13 @@ Module( range: 895..905, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 897..903, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 903..905, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap index dbc237b1d3..bb98cde311 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__fstring_format_spec_1.py.snap @@ -22,13 +22,13 @@ Module( range: 162..192, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 164..171, value: "middle ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 171..191, expression: StringLiteral( ExprStringLiteral { @@ -51,11 +51,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 181..191, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 181..191, value: " ", }, @@ -116,13 +116,13 @@ Module( range: 207..228, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 209..216, value: "middle ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 216..228, expression: StringLiteral( ExprStringLiteral { @@ -145,11 +145,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 226..228, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 226..228, value: "\\", }, @@ -209,13 +209,13 @@ Module( range: 253..285, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 255..262, value: "middle ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 262..284, expression: StringLiteral( ExprStringLiteral { @@ -238,11 +238,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 272..284, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 272..284, value: "\\ ", }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap index 472a8579cc..efb2fecf82 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_1.py.snap @@ -22,13 +22,13 @@ Module( range: 166..178, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 170..176, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 176..178, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap index ccf98eb430..c8431a27c9 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_2.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/re_lexing/triple_quoted_fstring_2.py -snapshot_kind: text --- ## AST @@ -22,8 +21,8 @@ Module( FString { range: 167..183, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 171..180, expression: Name( ExprName { @@ -35,11 +34,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 176..180, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 176..180, value: ".3f\n", }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap index 4e4ba407ee..e59c846c0f 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@re_lexing__triple_quoted_fstring_3.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/invalid/re_lexing/triple_quoted_fstring_3.py -snapshot_kind: text --- ## AST @@ -35,8 +34,8 @@ Module( FString { range: 239..253, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 243..250, expression: Name( ExprName { @@ -48,11 +47,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 246..250, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 246..250, value: ".3f\n", }, diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap index 153ed4c251..7159d351f2 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_assignment_targets.py.snap @@ -782,8 +782,8 @@ Module( FString { range: 576..585, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 578..584, expression: Name( ExprName { @@ -833,8 +833,8 @@ Module( FString { range: 591..609, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 593..598, expression: Name( ExprName { @@ -849,13 +849,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 598..603, value: " and ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 603..608, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap index 4fa30fed7b..ec4380012b 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@statements__invalid_augmented_assignment_target.py.snap @@ -688,8 +688,8 @@ Module( FString { range: 387..396, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 389..395, expression: Name( ExprName { @@ -738,8 +738,8 @@ Module( FString { range: 403..421, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 405..410, expression: Name( ExprName { @@ -754,13 +754,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 410..415, value: " and ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 415..420, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap new file mode 100644 index 0000000000..423ad64c79 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_empty_expression.py.snap @@ -0,0 +1,113 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_empty_expression.py +--- +## AST + +``` +Module( + ModModule { + range: 0..58, + body: [ + Expr( + StmtExpr { + range: 44..49, + value: TString( + ExprTString { + range: 44..49, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..49, + elements: [ + Interpolation( + InterpolatedElement { + range: 46..48, + expression: Name( + ExprName { + range: 47..47, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 50..57, + value: TString( + ExprTString { + range: 50..57, + value: TStringValue { + inner: Single( + TString( + TString { + range: 50..57, + elements: [ + Interpolation( + InterpolatedElement { + range: 52..56, + expression: Name( + ExprName { + range: 53..53, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{}" + | ^ Syntax Error: Expected an expression +3 | t"{ }" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{}" +3 | t"{ }" + | ^ Syntax Error: Expected an expression + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap new file mode 100644 index 0000000000..b65770e8d8 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_name_tok.py.snap @@ -0,0 +1,63 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_name_tok.py +--- +## AST + +``` +Module( + ModModule { + range: 0..53, + body: [ + Expr( + StmtExpr { + range: 44..52, + value: TString( + ExprTString { + range: 44..52, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..52, + elements: [ + Interpolation( + InterpolatedElement { + range: 46..51, + expression: Name( + ExprName { + range: 47..48, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{x!z}" + | ^ Syntax Error: t-string: invalid conversion character + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap new file mode 100644 index 0000000000..10324a349f --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_conversion_flag_other_tok.py.snap @@ -0,0 +1,113 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_invalid_conversion_flag_other_tok.py +--- +## AST + +``` +Module( + ModModule { + range: 0..66, + body: [ + Expr( + StmtExpr { + range: 44..54, + value: TString( + ExprTString { + range: 44..54, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..54, + elements: [ + Interpolation( + InterpolatedElement { + range: 46..53, + expression: Name( + ExprName { + range: 47..48, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 55..65, + value: TString( + ExprTString { + range: 55..65, + value: TStringValue { + inner: Single( + TString( + TString { + range: 55..65, + elements: [ + Interpolation( + InterpolatedElement { + range: 57..64, + expression: Name( + ExprName { + range: 58..59, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{x!123}" + | ^^^ Syntax Error: t-string: invalid conversion character +3 | t"{x!'a'}" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{x!123}" +3 | t"{x!'a'}" + | ^^^ Syntax Error: t-string: invalid conversion character + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap new file mode 100644 index 0000000000..a2fb0fda2d --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_invalid_starred_expr.py.snap @@ -0,0 +1,205 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_invalid_starred_expr.py +--- +## AST + +``` +Module( + ModModule { + range: 0..156, + body: [ + Expr( + StmtExpr { + range: 121..127, + value: TString( + ExprTString { + range: 121..127, + value: TStringValue { + inner: Single( + TString( + TString { + range: 121..127, + elements: [ + Interpolation( + InterpolatedElement { + range: 123..126, + expression: Starred( + ExprStarred { + range: 124..125, + value: Name( + ExprName { + range: 125..125, + id: Name(""), + ctx: Invalid, + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 128..141, + value: TString( + ExprTString { + range: 128..141, + value: TStringValue { + inner: Single( + TString( + TString { + range: 128..141, + elements: [ + Interpolation( + InterpolatedElement { + range: 130..140, + expression: Starred( + ExprStarred { + range: 131..139, + value: BoolOp( + ExprBoolOp { + range: 132..139, + op: And, + values: [ + Name( + ExprName { + range: 132..133, + id: Name("x"), + ctx: Load, + }, + ), + Name( + ExprName { + range: 138..139, + id: Name("y"), + ctx: Load, + }, + ), + ], + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 142..155, + value: TString( + ExprTString { + range: 142..155, + value: TStringValue { + inner: Single( + TString( + TString { + range: 142..155, + elements: [ + Interpolation( + InterpolatedElement { + range: 144..154, + expression: Starred( + ExprStarred { + range: 145..153, + value: Yield( + ExprYield { + range: 146..153, + value: Some( + Name( + ExprName { + range: 152..153, + id: Name("x"), + ctx: Load, + }, + ), + ), + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | # Starred expression inside t-string has a minimum precedence of bitwise or. +3 | t"{*}" + | ^ Syntax Error: Expected an expression +4 | t"{*x and y}" +5 | t"{*yield x}" + | + + + | +2 | # Starred expression inside t-string has a minimum precedence of bitwise or. +3 | t"{*}" +4 | t"{*x and y}" + | ^^^^^^^ Syntax Error: Boolean expression cannot be used here +5 | t"{*yield x}" + | + + + | +3 | t"{*}" +4 | t"{*x and y}" +5 | t"{*yield x}" + | ^^^^^^^ Syntax Error: Yield expression cannot be used here + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap new file mode 100644 index 0000000000..1138fe8530 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_lambda_without_parentheses.py.snap @@ -0,0 +1,118 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_lambda_without_parentheses.py +--- +## AST + +``` +Module( + ModModule { + range: 0..61, + body: [ + Expr( + StmtExpr { + range: 44..60, + value: TString( + ExprTString { + range: 44..60, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..60, + elements: [ + Interpolation( + InterpolatedElement { + range: 46..56, + expression: Lambda( + ExprLambda { + range: 47..56, + parameters: Some( + Parameters { + range: 54..55, + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 54..55, + parameter: Parameter { + range: 54..55, + name: Identifier { + id: Name("x"), + range: 54..55, + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + ), + body: Name( + ExprName { + range: 56..56, + id: Name(""), + ctx: Invalid, + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 56..58, + value: " x", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{lambda x: x}" + | ^^ Syntax Error: Expected an expression + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{lambda x: x}" + | ^^^^^^^^^ Syntax Error: t-string: lambda expressions are not allowed without parentheses + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{lambda x: x}" + | ^^ Syntax Error: t-string: expecting '}' + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{lambda x: x}" + | ^ Syntax Error: Expected an f-string or t-string element or the end of the f-string or t-string + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap new file mode 100644 index 0000000000..b910137829 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace.py.snap @@ -0,0 +1,359 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace.py +--- +## AST + +``` +Module( + ModModule { + range: 0..82, + body: [ + Expr( + StmtExpr { + range: 44..48, + value: TString( + ExprTString { + range: 44..48, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..48, + elements: [ + Interpolation( + InterpolatedElement { + range: 46..47, + expression: Name( + ExprName { + range: 47..47, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 49..58, + value: TString( + ExprTString { + range: 49..58, + value: TStringValue { + inner: Single( + TString( + TString { + range: 49..58, + elements: [ + Interpolation( + InterpolatedElement { + range: 51..58, + expression: Name( + ExprName { + range: 52..55, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 59..67, + value: TString( + ExprTString { + range: 59..67, + value: TStringValue { + inner: Single( + TString( + TString { + range: 59..67, + elements: [ + Interpolation( + InterpolatedElement { + range: 61..66, + expression: Name( + ExprName { + range: 62..65, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 68..81, + value: TString( + ExprTString { + range: 68..81, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 68..72, + elements: [ + Interpolation( + InterpolatedElement { + range: 70..71, + expression: Name( + ExprName { + range: 71..71, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 73..81, + elements: [ + Interpolation( + InterpolatedElement { + range: 77..78, + expression: Name( + ExprName { + range: 78..78, + id: Name(""), + ctx: Invalid, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" + | ^ Syntax Error: missing closing quote in string literal +3 | t"{foo!r" +4 | t"{foo=" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" + | ^ Syntax Error: t-string: unterminated string +3 | t"{foo!r" +4 | t"{foo=" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" + | ^ Syntax Error: t-string: unterminated string +3 | t"{foo!r" +4 | t"{foo=" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" +3 | t"{foo!r" + | ^^ Syntax Error: missing closing quote in string literal +4 | t"{foo=" +5 | t"{" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" +3 | t"{foo!r" + | ^ Syntax Error: t-string: unterminated string +4 | t"{foo=" +5 | t"{" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" +3 | t"{foo!r" + | ^ Syntax Error: t-string: unterminated string +4 | t"{foo=" +5 | t"{" + | + + + | +2 | t"{" +3 | t"{foo!r" +4 | t"{foo=" + | ^^ Syntax Error: t-string: expecting '}' +5 | t"{" +6 | t"""{""" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"{" +3 | t"{foo!r" + | ^ Syntax Error: Expected TStringEnd, found Unknown +4 | t"{foo=" +5 | t"{" + | + + + | +2 | t"{" +3 | t"{foo!r" +4 | t"{foo=" + | ^ Syntax Error: missing closing quote in string literal +5 | t"{" +6 | t"""{""" + | + + + | +2 | t"{" +3 | t"{foo!r" +4 | t"{foo=" + | ^ Syntax Error: t-string: unterminated string +5 | t"{" +6 | t"""{""" + | + + + | +2 | t"{" +3 | t"{foo!r" +4 | t"{foo=" + | ^ Syntax Error: t-string: unterminated string +5 | t"{" +6 | t"""{""" + | + + + | +3 | t"{foo!r" +4 | t"{foo=" +5 | t"{" + | ^ Syntax Error: missing closing quote in string literal +6 | t"""{""" + | + + + | +4 | t"{foo=" +5 | t"{" +6 | t"""{""" + | ^^^^ Syntax Error: Expected TStringEnd, found TStringStart + | + + + | +4 | t"{foo=" +5 | t"{" +6 | t"""{""" + | ^^^ Syntax Error: Expected an expression + | + + + | +5 | t"{" +6 | t"""{""" + | ^ Syntax Error: unexpected EOF while parsing + | + + + | +5 | t"{" +6 | t"""{""" + | ^ Syntax Error: t-string: unterminated string + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap new file mode 100644 index 0000000000..3c16c4c161 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@t_string_unclosed_lbrace_in_format_spec.py.snap @@ -0,0 +1,142 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/err/t_string_unclosed_lbrace_in_format_spec.py +--- +## AST + +``` +Module( + ModModule { + range: 0..73, + body: [ + Expr( + StmtExpr { + range: 44..56, + value: TString( + ExprTString { + range: 44..56, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..56, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 46..52, + value: "hello ", + }, + ), + Interpolation( + InterpolatedElement { + range: 52..55, + expression: Name( + ExprName { + range: 53..54, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 55..55, + elements: [], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 57..72, + value: TString( + ExprTString { + range: 57..72, + value: TStringValue { + inner: Single( + TString( + TString { + range: 57..72, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 59..65, + value: "hello ", + }, + ), + Interpolation( + InterpolatedElement { + range: 65..71, + expression: Name( + ExprName { + range: 66..67, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 68..71, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 68..71, + value: ".3f", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` +## Errors + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"hello {x:" + | ^ Syntax Error: t-string: expecting '}' +3 | t"hello {x:.3f" + | + + + | +1 | # parse_options: {"target-version": "3.14"} +2 | t"hello {x:" +3 | t"hello {x:.3f" + | ^ Syntax Error: t-string: expecting '}' + | diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap index c2c29483fd..ad595e25bb 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@unterminated_fstring_newline_recovery.py.snap @@ -74,13 +74,13 @@ Module( range: 14..24, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 16..22, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 22..24, expression: Name( ExprName { @@ -148,13 +148,13 @@ Module( range: 31..42, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 33..39, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 39..42, expression: Name( ExprName { @@ -166,7 +166,7 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 42..42, elements: [], }, @@ -227,13 +227,13 @@ Module( range: 49..60, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 51..57, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 57..60, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap index 617675786f..348aa344d8 100644 --- a/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/invalid_syntax@write_to_debug_expr.py.snap @@ -173,7 +173,7 @@ Module( | 1 | del __debug__ - | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.13 (syntax was removed in 3.9) + | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.14 (syntax was removed in 3.9) 2 | del x, y, __debug__, z 3 | __debug__ = 1 | @@ -182,7 +182,7 @@ Module( | 1 | del __debug__ 2 | del x, y, __debug__, z - | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.13 (syntax was removed in 3.9) + | ^^^^^^^^^ Syntax Error: cannot delete `__debug__` on Python 3.14 (syntax was removed in 3.9) 3 | __debug__ = 1 4 | x, y, __debug__, z = 1, 2, 3, 4 | diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap index 2fe8ea2855..99067deeda 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__f_string.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/expressions/f_string.py -snapshot_kind: text --- ## AST @@ -147,8 +146,8 @@ Module( FString { range: 47..56, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 49..55, expression: StringLiteral( ExprStringLiteral { @@ -199,8 +198,8 @@ Module( FString { range: 57..67, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 59..66, expression: Name( ExprName { @@ -240,8 +239,8 @@ Module( FString { range: 68..75, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 70..74, expression: Tuple( ExprTuple { @@ -291,8 +290,8 @@ Module( FString { range: 76..86, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 78..85, expression: Compare( ExprCompare { @@ -323,7 +322,7 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 84..84, elements: [], }, @@ -356,8 +355,8 @@ Module( FString { range: 87..102, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 89..101, expression: NumberLiteral( ExprNumberLiteral { @@ -370,11 +369,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 92..100, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 92..97, expression: StringLiteral( ExprStringLiteral { @@ -400,7 +399,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 97..100, value: ">10", }, @@ -436,8 +435,8 @@ Module( FString { range: 103..118, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 105..117, expression: NumberLiteral( ExprNumberLiteral { @@ -450,11 +449,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 108..116, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 108..113, expression: StringLiteral( ExprStringLiteral { @@ -480,7 +479,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 113..116, value: ">10", }, @@ -516,8 +515,8 @@ Module( FString { range: 119..133, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 121..132, expression: Name( ExprName { @@ -562,8 +561,8 @@ Module( FString { range: 134..154, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 136..153, expression: Name( ExprName { @@ -580,11 +579,11 @@ Module( ), conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 147..152, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 147..152, value: ".3f ", }, @@ -620,8 +619,8 @@ Module( FString { range: 155..173, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 157..172, expression: Name( ExprName { @@ -666,8 +665,8 @@ Module( FString { range: 174..190, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 176..189, expression: Tuple( ExprTuple { @@ -730,8 +729,8 @@ Module( FString { range: 191..217, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 193..216, expression: FString( ExprFString { @@ -742,8 +741,8 @@ Module( FString { range: 194..210, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 196..209, expression: NumberLiteral( ExprNumberLiteral { @@ -761,11 +760,11 @@ Module( ), conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 205..208, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 205..208, value: ".1f", }, @@ -790,11 +789,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 211..215, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 211..215, value: "*^20", }, @@ -849,13 +848,13 @@ Module( range: 227..242, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 229..233, value: "bar ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 233..240, expression: BinOp( ExprBinOp { @@ -883,7 +882,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 240..241, value: " ", }, @@ -1036,13 +1035,13 @@ Module( range: 347..364, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 349..350, value: "\\", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 350..355, expression: Name( ExprName { @@ -1057,13 +1056,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 355..356, value: "\\", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 356..363, expression: Name( ExprName { @@ -1075,11 +1074,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 361..362, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 361..362, value: "\\", }, @@ -1116,7 +1115,7 @@ Module( range: 365..379, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 367..378, value: "\\{foo\\}", }, @@ -1147,8 +1146,8 @@ Module( FString { range: 380..420, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 384..417, expression: Name( ExprName { @@ -1160,11 +1159,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 394..416, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 394..416, value: "x\n y\n z\n", }, @@ -1200,8 +1199,8 @@ Module( FString { range: 421..439, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 423..438, expression: Name( ExprName { @@ -1247,13 +1246,13 @@ Module( range: 441..486, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 443..450, value: "normal ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 450..455, expression: Name( ExprName { @@ -1268,13 +1267,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 455..468, value: " {another} ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 468..473, expression: Name( ExprName { @@ -1289,13 +1288,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 473..476, value: " {", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 476..483, expression: Name( ExprName { @@ -1310,7 +1309,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 483..485, value: "}", }, @@ -1342,13 +1341,13 @@ Module( range: 487..529, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 489..496, value: "normal ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 496..503, expression: Name( ExprName { @@ -1363,13 +1362,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 503..504, value: " ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 504..511, expression: Name( ExprName { @@ -1384,13 +1383,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 511..512, value: " ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 512..519, expression: Name( ExprName { @@ -1405,13 +1404,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 519..520, value: " ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 520..528, expression: Name( ExprName { @@ -1452,13 +1451,13 @@ Module( range: 530..549, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 532..539, value: "normal ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 539..548, expression: Name( ExprName { @@ -1470,11 +1469,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 542..547, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 542..547, value: "y + 2", }, @@ -1510,8 +1509,8 @@ Module( FString { range: 550..568, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 552..567, expression: Name( ExprName { @@ -1523,11 +1522,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 555..566, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 555..566, expression: Call( ExprCall { @@ -1600,8 +1599,8 @@ Module( FString { range: 569..588, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 571..587, expression: Lambda( ExprLambda { @@ -1676,8 +1675,8 @@ Module( FString { range: 589..597, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 591..596, expression: Name( ExprName { @@ -1722,8 +1721,8 @@ Module( FString { range: 598..611, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 600..610, expression: Name( ExprName { @@ -1768,8 +1767,8 @@ Module( FString { range: 612..621, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 614..620, expression: Name( ExprName { @@ -1814,8 +1813,8 @@ Module( FString { range: 622..636, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 624..635, expression: Name( ExprName { @@ -1827,11 +1826,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 627..634, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 627..634, value: ".3f!r =", }, @@ -1867,8 +1866,8 @@ Module( FString { range: 637..653, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 639..652, expression: Name( ExprName { @@ -1885,11 +1884,11 @@ Module( ), conversion: Repr, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 648..651, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 648..651, value: ".3f", }, @@ -1925,8 +1924,8 @@ Module( FString { range: 654..667, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 656..666, expression: Name( ExprName { @@ -1938,11 +1937,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 659..665, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 659..665, value: ".3f=!r", }, @@ -1990,8 +1989,8 @@ Module( FString { range: 676..682, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 678..681, expression: Name( ExprName { @@ -2033,8 +2032,8 @@ Module( FString { range: 683..689, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 685..688, expression: Name( ExprName { @@ -2060,8 +2059,8 @@ Module( FString { range: 690..696, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 692..695, expression: Name( ExprName { @@ -2103,8 +2102,8 @@ Module( FString { range: 697..703, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 699..702, expression: Name( ExprName { @@ -2157,13 +2156,13 @@ Module( range: 712..756, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 714..739, value: "Invalid args in command: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 739..755, expression: Tuple( ExprTuple { @@ -2237,8 +2236,8 @@ Module( FString { range: 763..769, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 765..768, expression: Name( ExprName { @@ -2292,7 +2291,7 @@ Module( range: 782..786, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 784..785, value: "a", }, @@ -2310,7 +2309,7 @@ Module( range: 791..795, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 793..794, value: "b", }, @@ -2339,7 +2338,7 @@ Module( range: 808..813, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 811..812, value: "d", }, @@ -2359,7 +2358,7 @@ Module( range: 818..823, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 821..822, value: "e", }, @@ -2405,8 +2404,8 @@ Module( FString { range: 857..865, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 859..864, expression: Name( ExprName { @@ -2481,8 +2480,8 @@ Module( FString { range: 886..894, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 888..893, expression: Name( ExprName { @@ -2557,8 +2556,8 @@ Module( FString { range: 916..924, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 918..923, expression: Name( ExprName { @@ -2634,13 +2633,13 @@ Module( range: 947..966, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 949..953, value: "bar ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 953..958, expression: Name( ExprName { @@ -2655,7 +2654,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 958..965, value: " really", }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap new file mode 100644 index 0000000000..99a7bcd3b9 --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@expressions__t_string.py.snap @@ -0,0 +1,3127 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/valid/expressions/t_string.py +--- +## AST + +``` +Module( + ModModule { + range: 0..1143, + body: [ + Expr( + StmtExpr { + range: 18..21, + value: TString( + ExprTString { + range: 18..21, + value: TStringValue { + inner: Single( + TString( + TString { + range: 18..21, + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 22..25, + value: TString( + ExprTString { + range: 22..25, + value: TStringValue { + inner: Single( + TString( + TString { + range: 22..25, + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 26..29, + value: TString( + ExprTString { + range: 26..29, + value: TStringValue { + inner: Single( + TString( + TString { + range: 26..29, + elements: [], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 30..37, + value: TString( + ExprTString { + range: 30..37, + value: TStringValue { + inner: Single( + TString( + TString { + range: 30..37, + elements: [], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 38..45, + value: TString( + ExprTString { + range: 38..45, + value: TStringValue { + inner: Single( + TString( + TString { + range: 38..45, + elements: [], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 47..56, + value: TString( + ExprTString { + range: 47..56, + value: TStringValue { + inner: Single( + TString( + TString { + range: 47..56, + elements: [ + Interpolation( + InterpolatedElement { + range: 49..55, + expression: StringLiteral( + ExprStringLiteral { + range: 50..54, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 50..54, + value: " t", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 57..67, + value: TString( + ExprTString { + range: 57..67, + value: TStringValue { + inner: Single( + TString( + TString { + range: 57..67, + elements: [ + Interpolation( + InterpolatedElement { + range: 59..66, + expression: Name( + ExprName { + range: 60..63, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Str, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 68..75, + value: TString( + ExprTString { + range: 68..75, + value: TStringValue { + inner: Single( + TString( + TString { + range: 68..75, + elements: [ + Interpolation( + InterpolatedElement { + range: 70..74, + expression: Tuple( + ExprTuple { + range: 71..73, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 71..72, + value: Int( + 3, + ), + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 76..86, + value: TString( + ExprTString { + range: 76..86, + value: TStringValue { + inner: Single( + TString( + TString { + range: 76..86, + elements: [ + Interpolation( + InterpolatedElement { + range: 78..85, + expression: Compare( + ExprCompare { + range: 79..83, + left: NumberLiteral( + ExprNumberLiteral { + range: 79..80, + value: Int( + 3, + ), + }, + ), + ops: [ + NotEq, + ], + comparators: [ + NumberLiteral( + ExprNumberLiteral { + range: 82..83, + value: Int( + 4, + ), + }, + ), + ], + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 84..84, + elements: [], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 87..102, + value: TString( + ExprTString { + range: 87..102, + value: TStringValue { + inner: Single( + TString( + TString { + range: 87..102, + elements: [ + Interpolation( + InterpolatedElement { + range: 89..101, + expression: NumberLiteral( + ExprNumberLiteral { + range: 90..91, + value: Int( + 3, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 92..100, + elements: [ + Interpolation( + InterpolatedElement { + range: 92..97, + expression: StringLiteral( + ExprStringLiteral { + range: 93..96, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 93..96, + value: "}", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 97..100, + value: ">10", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 103..118, + value: TString( + ExprTString { + range: 103..118, + value: TStringValue { + inner: Single( + TString( + TString { + range: 103..118, + elements: [ + Interpolation( + InterpolatedElement { + range: 105..117, + expression: NumberLiteral( + ExprNumberLiteral { + range: 106..107, + value: Int( + 3, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 108..116, + elements: [ + Interpolation( + InterpolatedElement { + range: 108..113, + expression: StringLiteral( + ExprStringLiteral { + range: 109..112, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 109..112, + value: "{", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 113..116, + value: ">10", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 119..133, + value: TString( + ExprTString { + range: 119..133, + value: TStringValue { + inner: Single( + TString( + TString { + range: 119..133, + elements: [ + Interpolation( + InterpolatedElement { + range: 121..132, + expression: Name( + ExprName { + range: 124..127, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 134..154, + value: TString( + ExprTString { + range: 134..154, + value: TStringValue { + inner: Single( + TString( + TString { + range: 134..154, + elements: [ + Interpolation( + InterpolatedElement { + range: 136..153, + expression: Name( + ExprName { + range: 139..142, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 147..152, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 147..152, + value: ".3f ", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 155..173, + value: TString( + ExprTString { + range: 155..173, + value: TStringValue { + inner: Single( + TString( + TString { + range: 155..173, + elements: [ + Interpolation( + InterpolatedElement { + range: 157..172, + expression: Name( + ExprName { + range: 160..163, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: Str, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 174..190, + value: TString( + ExprTString { + range: 174..190, + value: TStringValue { + inner: Single( + TString( + TString { + range: 174..190, + elements: [ + Interpolation( + InterpolatedElement { + range: 176..189, + expression: Tuple( + ExprTuple { + range: 179..183, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 179..180, + value: Int( + 1, + ), + }, + ), + NumberLiteral( + ExprNumberLiteral { + range: 182..183, + value: Int( + 2, + ), + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 191..217, + value: TString( + ExprTString { + range: 191..217, + value: TStringValue { + inner: Single( + TString( + TString { + range: 191..217, + elements: [ + Interpolation( + InterpolatedElement { + range: 193..216, + expression: TString( + ExprTString { + range: 194..210, + value: TStringValue { + inner: Single( + TString( + TString { + range: 194..210, + elements: [ + Interpolation( + InterpolatedElement { + range: 196..209, + expression: NumberLiteral( + ExprNumberLiteral { + range: 197..203, + value: Float( + 3.1415, + ), + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 205..208, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 205..208, + value: ".1f", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 211..215, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 211..215, + value: "*^20", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 219..253, + value: Dict( + ExprDict { + range: 219..253, + items: [ + DictItem { + key: Some( + TString( + ExprTString { + range: 220..248, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 220..226, + value: "foo ", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 227..242, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 229..233, + value: "bar ", + }, + ), + Interpolation( + InterpolatedElement { + range: 233..240, + expression: BinOp( + ExprBinOp { + range: 234..239, + left: Name( + ExprName { + range: 234..235, + id: Name("x"), + ctx: Load, + }, + ), + op: Add, + right: Name( + ExprName { + range: 238..239, + id: Name("y"), + ctx: Load, + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 240..241, + value: " ", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 243..248, + value: "baz", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + ), + value: NumberLiteral( + ExprNumberLiteral { + range: 250..252, + value: Int( + 10, + ), + }, + ), + }, + ], + }, + ), + }, + ), + Match( + StmtMatch { + range: 254..345, + subject: Name( + ExprName { + range: 260..263, + id: Name("foo"), + ctx: Load, + }, + ), + cases: [ + MatchCase { + range: 269..293, + pattern: MatchValue( + PatternMatchValue { + range: 274..279, + value: StringLiteral( + ExprStringLiteral { + range: 274..279, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 274..279, + value: "one", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + }, + ), + guard: None, + body: [ + Pass( + StmtPass { + range: 289..293, + }, + ), + ], + }, + MatchCase { + range: 298..345, + pattern: MatchValue( + PatternMatchValue { + range: 303..331, + value: StringLiteral( + ExprStringLiteral { + range: 303..331, + value: StringLiteralValue { + inner: Concatenated( + ConcatenatedStringLiteral { + strings: [ + StringLiteral { + range: 303..316, + value: "implicitly ", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + StringLiteral { + range: 317..331, + value: "concatenated", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ], + value: "implicitly concatenated", + }, + ), + }, + }, + ), + }, + ), + guard: None, + body: [ + Pass( + StmtPass { + range: 341..345, + }, + ), + ], + }, + ], + }, + ), + Expr( + StmtExpr { + range: 347..364, + value: TString( + ExprTString { + range: 347..364, + value: TStringValue { + inner: Single( + TString( + TString { + range: 347..364, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 349..350, + value: "\\", + }, + ), + Interpolation( + InterpolatedElement { + range: 350..355, + expression: Name( + ExprName { + range: 351..354, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 355..356, + value: "\\", + }, + ), + Interpolation( + InterpolatedElement { + range: 356..363, + expression: Name( + ExprName { + range: 357..360, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 361..362, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 361..362, + value: "\\", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 365..379, + value: TString( + ExprTString { + range: 365..379, + value: TStringValue { + inner: Single( + TString( + TString { + range: 365..379, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 367..378, + value: "\\{foo\\}", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 380..420, + value: TString( + ExprTString { + range: 380..420, + value: TStringValue { + inner: Single( + TString( + TString { + range: 380..420, + elements: [ + Interpolation( + InterpolatedElement { + range: 384..417, + expression: Name( + ExprName { + range: 390..393, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 394..416, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 394..416, + value: "x\n y\n z\n", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 421..439, + value: TString( + ExprTString { + range: 421..439, + value: TStringValue { + inner: Single( + TString( + TString { + range: 421..439, + elements: [ + Interpolation( + InterpolatedElement { + range: 423..438, + expression: Name( + ExprName { + range: 428..431, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ( ", + trailing: " ) = ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 441..486, + value: TString( + ExprTString { + range: 441..486, + value: TStringValue { + inner: Single( + TString( + TString { + range: 441..486, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 443..450, + value: "normal ", + }, + ), + Interpolation( + InterpolatedElement { + range: 450..455, + expression: Name( + ExprName { + range: 451..454, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 455..468, + value: " {another} ", + }, + ), + Interpolation( + InterpolatedElement { + range: 468..473, + expression: Name( + ExprName { + range: 469..472, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 473..476, + value: " {", + }, + ), + Interpolation( + InterpolatedElement { + range: 476..483, + expression: Name( + ExprName { + range: 477..482, + id: Name("three"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 483..485, + value: "}", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 487..529, + value: TString( + ExprTString { + range: 487..529, + value: TStringValue { + inner: Single( + TString( + TString { + range: 487..529, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 489..496, + value: "normal ", + }, + ), + Interpolation( + InterpolatedElement { + range: 496..503, + expression: Name( + ExprName { + range: 497..500, + id: Name("foo"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Ascii, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 503..504, + value: " ", + }, + ), + Interpolation( + InterpolatedElement { + range: 504..511, + expression: Name( + ExprName { + range: 505..508, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Str, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 511..512, + value: " ", + }, + ), + Interpolation( + InterpolatedElement { + range: 512..519, + expression: Name( + ExprName { + range: 513..516, + id: Name("baz"), + ctx: Load, + }, + ), + debug_text: None, + conversion: Repr, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 519..520, + value: " ", + }, + ), + Interpolation( + InterpolatedElement { + range: 520..528, + expression: Name( + ExprName { + range: 521..527, + id: Name("foobar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 530..549, + value: TString( + ExprTString { + range: 530..549, + value: TStringValue { + inner: Single( + TString( + TString { + range: 530..549, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 532..539, + value: "normal ", + }, + ), + Interpolation( + InterpolatedElement { + range: 539..548, + expression: Name( + ExprName { + range: 540..541, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 542..547, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 542..547, + value: "y + 2", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 550..568, + value: TString( + ExprTString { + range: 550..568, + value: TStringValue { + inner: Single( + TString( + TString { + range: 550..568, + elements: [ + Interpolation( + InterpolatedElement { + range: 552..567, + expression: Name( + ExprName { + range: 553..554, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 555..566, + elements: [ + Interpolation( + InterpolatedElement { + range: 555..566, + expression: Call( + ExprCall { + range: 556..565, + func: Attribute( + ExprAttribute { + range: 556..563, + value: Set( + ExprSet { + range: 556..559, + elts: [ + NumberLiteral( + ExprNumberLiteral { + range: 557..558, + value: Int( + 1, + ), + }, + ), + ], + }, + ), + attr: Identifier { + id: Name("pop"), + range: 560..563, + }, + ctx: Load, + }, + ), + arguments: Arguments { + range: 563..565, + args: [], + keywords: [], + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 569..588, + value: TString( + ExprTString { + range: 569..588, + value: TStringValue { + inner: Single( + TString( + TString { + range: 569..588, + elements: [ + Interpolation( + InterpolatedElement { + range: 571..587, + expression: Lambda( + ExprLambda { + range: 573..585, + parameters: Some( + Parameters { + range: 580..581, + posonlyargs: [], + args: [ + ParameterWithDefault { + range: 580..581, + parameter: Parameter { + range: 580..581, + name: Identifier { + id: Name("x"), + range: 580..581, + }, + annotation: None, + }, + default: None, + }, + ], + vararg: None, + kwonlyargs: [], + kwarg: None, + }, + ), + body: Set( + ExprSet { + range: 582..585, + elts: [ + Name( + ExprName { + range: 583..584, + id: Name("x"), + ctx: Load, + }, + ), + ], + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 589..597, + value: TString( + ExprTString { + range: 589..597, + value: TStringValue { + inner: Single( + TString( + TString { + range: 589..597, + elements: [ + Interpolation( + InterpolatedElement { + range: 591..596, + expression: Name( + ExprName { + range: 592..593, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: " =", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 598..611, + value: TString( + ExprTString { + range: 598..611, + value: TStringValue { + inner: Single( + TString( + TString { + range: 598..611, + elements: [ + Interpolation( + InterpolatedElement { + range: 600..610, + expression: Name( + ExprName { + range: 605..606, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: " ", + trailing: " = ", + }, + ), + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 612..621, + value: TString( + ExprTString { + range: 612..621, + value: TStringValue { + inner: Single( + TString( + TString { + range: 612..621, + elements: [ + Interpolation( + InterpolatedElement { + range: 614..620, + expression: Name( + ExprName { + range: 615..616, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: "=", + }, + ), + conversion: Ascii, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 622..636, + value: TString( + ExprTString { + range: 622..636, + value: TStringValue { + inner: Single( + TString( + TString { + range: 622..636, + elements: [ + Interpolation( + InterpolatedElement { + range: 624..635, + expression: Name( + ExprName { + range: 625..626, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 627..634, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 627..634, + value: ".3f!r =", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 637..653, + value: TString( + ExprTString { + range: 637..653, + value: TStringValue { + inner: Single( + TString( + TString { + range: 637..653, + elements: [ + Interpolation( + InterpolatedElement { + range: 639..652, + expression: Name( + ExprName { + range: 640..641, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: Some( + DebugText { + leading: "", + trailing: " = ", + }, + ), + conversion: Repr, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 648..651, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 648..651, + value: ".3f", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 654..667, + value: TString( + ExprTString { + range: 654..667, + value: TStringValue { + inner: Single( + TString( + TString { + range: 654..667, + elements: [ + Interpolation( + InterpolatedElement { + range: 656..666, + expression: Name( + ExprName { + range: 657..658, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: Some( + InterpolatedStringFormatSpec { + range: 659..665, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 659..665, + value: ".3f=!r", + }, + ), + ], + }, + ), + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 668..682, + value: TString( + ExprTString { + range: 668..682, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 668..675, + value: "hello", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 676..682, + elements: [ + Interpolation( + InterpolatedElement { + range: 678..681, + expression: Name( + ExprName { + range: 679..680, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 683..696, + value: TString( + ExprTString { + range: 683..696, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 683..689, + elements: [ + Interpolation( + InterpolatedElement { + range: 685..688, + expression: Name( + ExprName { + range: 686..687, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 690..696, + elements: [ + Interpolation( + InterpolatedElement { + range: 692..695, + expression: Name( + ExprName { + range: 693..694, + id: Name("y"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 697..711, + value: TString( + ExprTString { + range: 697..711, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 697..703, + elements: [ + Interpolation( + InterpolatedElement { + range: 699..702, + expression: Name( + ExprName { + range: 700..701, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 704..711, + value: "world", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 712..756, + value: TString( + ExprTString { + range: 712..756, + value: TStringValue { + inner: Single( + TString( + TString { + range: 712..756, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 714..739, + value: "Invalid args in command: ", + }, + ), + Interpolation( + InterpolatedElement { + range: 739..755, + expression: Tuple( + ExprTuple { + range: 740..754, + elts: [ + Name( + ExprName { + range: 740..747, + id: Name("command"), + ctx: Load, + }, + ), + Starred( + ExprStarred { + range: 749..754, + value: Name( + ExprName { + range: 750..754, + id: Name("args"), + ctx: Load, + }, + ), + ctx: Load, + }, + ), + ], + ctx: Load, + parenthesized: false, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 757..775, + value: TString( + ExprTString { + range: 757..775, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 757..762, + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 763..769, + elements: [ + Interpolation( + InterpolatedElement { + range: 765..768, + expression: Name( + ExprName { + range: 766..767, + id: Name("x"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 770..775, + value: "bar", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 776..825, + value: TString( + ExprTString { + range: 782..823, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 782..786, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 784..785, + value: "a", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 791..795, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 793..794, + value: "b", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 800..803, + value: "c", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 808..813, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 811..812, + value: "d", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + }, + }, + ), + FString( + FString { + range: 818..823, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 821..822, + value: "e", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Raw { + uppercase_r: false, + }, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 850..879, + value: TString( + ExprTString { + range: 850..879, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 850..856, + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 857..865, + elements: [ + Interpolation( + InterpolatedElement { + range: 859..864, + expression: Name( + ExprName { + range: 860..863, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 866..871, + value: "baz", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 872..879, + value: " some", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 880..909, + value: TString( + ExprTString { + range: 880..909, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 880..885, + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 886..894, + elements: [ + Interpolation( + InterpolatedElement { + range: 888..893, + expression: Name( + ExprName { + range: 889..892, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 895..901, + value: "baz", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 902..909, + value: " some", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 910..939, + value: TString( + ExprTString { + range: 910..939, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 910..915, + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 916..924, + elements: [ + Interpolation( + InterpolatedElement { + range: 918..923, + expression: Name( + ExprName { + range: 919..922, + id: Name("bar"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 925..930, + value: "baz", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 931..939, + value: " some", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 940..978, + value: TString( + ExprTString { + range: 940..978, + value: TStringValue { + inner: Concatenated( + [ + Literal( + StringLiteral { + range: 940..946, + value: "foo", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 947..966, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 949..953, + value: "bar ", + }, + ), + Interpolation( + InterpolatedElement { + range: 953..958, + expression: Name( + ExprName { + range: 954..957, + id: Name("baz"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 958..965, + value: " really", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 967..973, + value: "bar", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Unicode, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 974..978, + value: "no", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 998..1017, + value: TString( + ExprTString { + range: 998..1017, + value: TStringValue { + inner: Concatenated( + [ + FString( + FString { + range: 998..1007, + elements: [ + Interpolation( + InterpolatedElement { + range: 1000..1006, + expression: Name( + ExprName { + range: 1001..1005, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 1008..1017, + elements: [ + Interpolation( + InterpolatedElement { + range: 1010..1016, + expression: Name( + ExprName { + range: 1011..1015, + id: Name("that"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 1018..1036, + value: TString( + ExprTString { + range: 1018..1036, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 1018..1027, + elements: [ + Interpolation( + InterpolatedElement { + range: 1020..1026, + expression: Name( + ExprName { + range: 1021..1025, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + FString( + FString { + range: 1027..1036, + elements: [ + Interpolation( + InterpolatedElement { + range: 1029..1035, + expression: Name( + ExprName { + range: 1030..1034, + id: Name("that"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 1037..1064, + value: TString( + ExprTString { + range: 1037..1064, + value: TStringValue { + inner: Concatenated( + [ + TString( + TString { + range: 1037..1046, + elements: [ + Interpolation( + InterpolatedElement { + range: 1039..1045, + expression: Name( + ExprName { + range: 1040..1044, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 1047..1053, + value: "that", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + FString( + FString { + range: 1054..1064, + elements: [ + Interpolation( + InterpolatedElement { + range: 1056..1063, + expression: Name( + ExprName { + range: 1057..1062, + id: Name("other"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 1065..1111, + value: TString( + ExprTString { + range: 1065..1111, + value: TStringValue { + inner: Concatenated( + [ + FString( + FString { + range: 1065..1082, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 1067..1071, + value: "one ", + }, + ), + Interpolation( + InterpolatedElement { + range: 1071..1077, + expression: Name( + ExprName { + range: 1072..1076, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 1077..1081, + value: " two", + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + Literal( + StringLiteral { + range: 1083..1089, + value: "that", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + TString( + TString { + range: 1090..1111, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 1092..1098, + value: "three ", + }, + ), + Interpolation( + InterpolatedElement { + range: 1098..1105, + expression: Name( + ExprName { + range: 1099..1104, + id: Name("other"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 1105..1110, + value: " four", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ], + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 1123..1142, + value: TString( + ExprTString { + range: 1123..1142, + value: TStringValue { + inner: Single( + TString( + TString { + range: 1123..1142, + elements: [ + Interpolation( + InterpolatedElement { + range: 1125..1141, + expression: FString( + ExprFString { + range: 1126..1140, + value: FStringValue { + inner: Single( + FString( + FString { + range: 1126..1140, + elements: [ + Interpolation( + InterpolatedElement { + range: 1128..1139, + expression: TString( + ExprTString { + range: 1129..1138, + value: TStringValue { + inner: Single( + TString( + TString { + range: 1129..1138, + elements: [ + Interpolation( + InterpolatedElement { + range: 1131..1137, + expression: Name( + ExprName { + range: 1132..1136, + id: Name("this"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: FStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@fstring_format_spec_terminator.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@fstring_format_spec_terminator.py.snap index 1e672a1ce1..34167d13a7 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@fstring_format_spec_terminator.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@fstring_format_spec_terminator.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/fstring_format_spec_terminator.py -snapshot_kind: text --- ## AST @@ -23,13 +22,13 @@ Module( range: 0..19, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 2..8, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 8..12, expression: Name( ExprName { @@ -41,7 +40,7 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 11..11, elements: [], }, @@ -49,7 +48,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 12..18, value: " world", }, @@ -81,13 +80,13 @@ Module( range: 20..42, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 22..28, value: "hello ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 28..35, expression: Name( ExprName { @@ -99,11 +98,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 31..34, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 31..34, value: ".3f", }, @@ -114,7 +113,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 35..41, value: " world", }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap index 044de1e157..16fc4e36e3 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@match_classify_as_keyword_1.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/inline/ok/match_classify_as_keyword_1.py -snapshot_kind: text --- ## AST @@ -223,13 +222,13 @@ Module( range: 140..150, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 142..146, value: "foo ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 146..149, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_annotation.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_annotation.py.snap index c3d622ebb0..004ec6c603 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_annotation.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@param_with_annotation.py.snap @@ -7,7 +7,7 @@ input_file: crates/ruff_python_parser/resources/inline/ok/param_with_annotation. ``` Module( ModModule { - range: 0..84, + range: 0..54, body: [ FunctionDef( StmtFunctionDef { @@ -145,72 +145,6 @@ Module( ], }, ), - FunctionDef( - StmtFunctionDef { - range: 54..83, - is_async: false, - decorator_list: [], - name: Identifier { - id: Name("foo"), - range: 58..61, - }, - type_params: None, - parameters: Parameters { - range: 61..78, - posonlyargs: [], - args: [ - ParameterWithDefault { - range: 62..77, - parameter: Parameter { - range: 62..77, - name: Identifier { - id: Name("arg"), - range: 62..65, - }, - annotation: Some( - Named( - ExprNamed { - range: 68..76, - target: Name( - ExprName { - range: 68..69, - id: Name("x"), - ctx: Store, - }, - ), - value: Name( - ExprName { - range: 73..76, - id: Name("int"), - ctx: Load, - }, - ), - }, - ), - ), - }, - default: None, - }, - ], - vararg: None, - kwonlyargs: [], - kwarg: None, - }, - returns: None, - body: [ - Expr( - StmtExpr { - range: 80..83, - value: EllipsisLiteral( - ExprEllipsisLiteral { - range: 80..83, - }, - ), - }, - ), - ], - }, - ), ], }, ) diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap index d4dcb42151..cbec152ee7 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py311.py.snap @@ -22,13 +22,13 @@ Module( range: 44..72, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 46..52, value: "outer ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 52..71, expression: StringLiteral( ExprStringLiteral { @@ -80,13 +80,13 @@ Module( range: 73..106, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 75..81, value: "outer ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 81..105, expression: Name( ExprName { @@ -98,11 +98,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 84..104, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 84..103, expression: StringLiteral( ExprStringLiteral { @@ -128,7 +128,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 103..104, value: " ", }, @@ -164,8 +164,8 @@ Module( FString { range: 107..147, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 111..144, expression: FString( ExprFString { @@ -176,8 +176,8 @@ Module( FString { range: 112..143, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 116..140, expression: FString( ExprFString { @@ -188,8 +188,8 @@ Module( FString { range: 117..139, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 119..138, expression: StringLiteral( ExprStringLiteral { @@ -274,8 +274,8 @@ Module( FString { range: 148..230, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 152..208, expression: FString( ExprFString { @@ -287,13 +287,13 @@ Module( range: 153..207, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 157..177, value: "# before expression ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 177..204, expression: FString( ExprFString { @@ -305,13 +305,13 @@ Module( range: 178..203, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 180..185, value: "# aro", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 185..197, expression: FString( ExprFString { @@ -323,13 +323,13 @@ Module( range: 186..196, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 188..189, value: "#", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 189..194, expression: BinOp( ExprBinOp { @@ -359,7 +359,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 194..195, value: "#", }, @@ -382,7 +382,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 197..202, value: "und #", }, @@ -422,7 +422,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 208..227, value: " # after expression", }, @@ -454,13 +454,13 @@ Module( range: 231..263, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 233..254, value: "escape outside of \t ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 254..260, expression: Name( ExprName { @@ -475,7 +475,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 260..262, value: "\n", }, @@ -507,7 +507,7 @@ Module( range: 264..277, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 266..276, value: "test\"abcd", }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap index c9eea80822..09817c8283 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep701_f_string_py312.py.snap @@ -22,13 +22,13 @@ Module( range: 44..74, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 46..58, value: "Magic wand: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 58..73, expression: Subscript( ExprSubscript { @@ -92,8 +92,8 @@ Module( FString { range: 95..112, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 97..111, expression: Call( ExprCall { @@ -173,13 +173,13 @@ Module( range: 148..220, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 152..169, value: "A complex trick: ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 169..217, expression: Subscript( ExprSubscript { @@ -243,8 +243,8 @@ Module( FString { range: 221..254, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 223..253, expression: FString( ExprFString { @@ -255,8 +255,8 @@ Module( FString { range: 224..252, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 226..251, expression: FString( ExprFString { @@ -267,8 +267,8 @@ Module( FString { range: 227..250, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 229..249, expression: FString( ExprFString { @@ -279,8 +279,8 @@ Module( FString { range: 230..248, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 232..247, expression: FString( ExprFString { @@ -291,8 +291,8 @@ Module( FString { range: 233..246, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 235..245, expression: FString( ExprFString { @@ -303,8 +303,8 @@ Module( FString { range: 236..244, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 238..243, expression: BinOp( ExprBinOp { @@ -444,8 +444,8 @@ Module( FString { range: 276..310, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 278..303, expression: FString( ExprFString { @@ -456,8 +456,8 @@ Module( FString { range: 279..302, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 283..293, expression: StringLiteral( ExprStringLiteral { @@ -483,7 +483,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 293..299, value: " inner", }, @@ -506,7 +506,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 303..309, value: " outer", }, @@ -538,13 +538,13 @@ Module( range: 336..359, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 338..343, value: "test ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 343..353, expression: Name( ExprName { @@ -559,7 +559,7 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 353..358, value: " more", }, diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep750_t_string_py314.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep750_t_string_py314.py.snap new file mode 100644 index 0000000000..3d322265ab --- /dev/null +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@pep750_t_string_py314.py.snap @@ -0,0 +1,584 @@ +--- +source: crates/ruff_python_parser/tests/fixtures.rs +input_file: crates/ruff_python_parser/resources/inline/ok/pep750_t_string_py314.py +--- +## AST + +``` +Module( + ModModule { + range: 0..403, + body: [ + Expr( + StmtExpr { + range: 44..74, + value: TString( + ExprTString { + range: 44..74, + value: TStringValue { + inner: Single( + TString( + TString { + range: 44..74, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 46..58, + value: "Magic wand: ", + }, + ), + Interpolation( + InterpolatedElement { + range: 58..73, + expression: Subscript( + ExprSubscript { + range: 60..71, + value: Name( + ExprName { + range: 60..63, + id: Name("bag"), + ctx: Load, + }, + ), + slice: StringLiteral( + ExprStringLiteral { + range: 64..70, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 64..70, + value: "wand", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 95..112, + value: TString( + ExprTString { + range: 95..112, + value: TStringValue { + inner: Single( + TString( + TString { + range: 95..112, + elements: [ + Interpolation( + InterpolatedElement { + range: 97..111, + expression: Call( + ExprCall { + range: 98..110, + func: Attribute( + ExprAttribute { + range: 98..107, + value: StringLiteral( + ExprStringLiteral { + range: 98..102, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 98..102, + value: "\n", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + attr: Identifier { + id: Name("join"), + range: 103..107, + }, + ctx: Load, + }, + ), + arguments: Arguments { + range: 107..110, + args: [ + Name( + ExprName { + range: 108..109, + id: Name("a"), + ctx: Load, + }, + ), + ], + keywords: [], + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 148..220, + value: TString( + ExprTString { + range: 148..220, + value: TStringValue { + inner: Single( + TString( + TString { + range: 148..220, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 152..169, + value: "A complex trick: ", + }, + ), + Interpolation( + InterpolatedElement { + range: 169..217, + expression: Subscript( + ExprSubscript { + range: 175..185, + value: Name( + ExprName { + range: 175..178, + id: Name("bag"), + ctx: Load, + }, + ), + slice: StringLiteral( + ExprStringLiteral { + range: 179..184, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 179..184, + value: "bag", + flags: StringLiteralFlags { + quote_style: Single, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 221..254, + value: TString( + ExprTString { + range: 221..254, + value: TStringValue { + inner: Single( + TString( + TString { + range: 221..254, + elements: [ + Interpolation( + InterpolatedElement { + range: 223..253, + expression: TString( + ExprTString { + range: 224..252, + value: TStringValue { + inner: Single( + TString( + TString { + range: 224..252, + elements: [ + Interpolation( + InterpolatedElement { + range: 226..251, + expression: TString( + ExprTString { + range: 227..250, + value: TStringValue { + inner: Single( + TString( + TString { + range: 227..250, + elements: [ + Interpolation( + InterpolatedElement { + range: 229..249, + expression: TString( + ExprTString { + range: 230..248, + value: TStringValue { + inner: Single( + TString( + TString { + range: 230..248, + elements: [ + Interpolation( + InterpolatedElement { + range: 232..247, + expression: TString( + ExprTString { + range: 233..246, + value: TStringValue { + inner: Single( + TString( + TString { + range: 233..246, + elements: [ + Interpolation( + InterpolatedElement { + range: 235..245, + expression: TString( + ExprTString { + range: 236..244, + value: TStringValue { + inner: Single( + TString( + TString { + range: 236..244, + elements: [ + Interpolation( + InterpolatedElement { + range: 238..243, + expression: BinOp( + ExprBinOp { + range: 239..242, + left: NumberLiteral( + ExprNumberLiteral { + range: 239..240, + value: Int( + 1, + ), + }, + ), + op: Add, + right: NumberLiteral( + ExprNumberLiteral { + range: 241..242, + value: Int( + 1, + ), + }, + ), + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 276..310, + value: TString( + ExprTString { + range: 276..310, + value: TStringValue { + inner: Single( + TString( + TString { + range: 276..310, + elements: [ + Interpolation( + InterpolatedElement { + range: 278..303, + expression: TString( + ExprTString { + range: 279..302, + value: TStringValue { + inner: Single( + TString( + TString { + range: 279..302, + elements: [ + Interpolation( + InterpolatedElement { + range: 283..293, + expression: StringLiteral( + ExprStringLiteral { + range: 284..292, + value: StringLiteralValue { + inner: Single( + StringLiteral { + range: 284..292, + value: "nested", + flags: StringLiteralFlags { + quote_style: Double, + prefix: Empty, + triple_quoted: false, + }, + }, + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 293..299, + value: " inner", + }, + ), + ], + flags: TStringFlags { + quote_style: Single, + prefix: Regular, + triple_quoted: true, + }, + }, + ), + ), + }, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 303..309, + value: " outer", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + Expr( + StmtExpr { + range: 336..359, + value: TString( + ExprTString { + range: 336..359, + value: TStringValue { + inner: Single( + TString( + TString { + range: 336..359, + elements: [ + Literal( + InterpolatedStringLiteralElement { + range: 338..343, + value: "test ", + }, + ), + Interpolation( + InterpolatedElement { + range: 343..353, + expression: Name( + ExprName { + range: 344..345, + id: Name("a"), + ctx: Load, + }, + ), + debug_text: None, + conversion: None, + format_spec: None, + }, + ), + Literal( + InterpolatedStringLiteralElement { + range: 353..358, + value: " more", + }, + ), + ], + flags: TStringFlags { + quote_style: Double, + prefix: Regular, + triple_quoted: false, + }, + }, + ), + ), + }, + }, + ), + }, + ), + ], + }, +) +``` diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__ambiguous_lpar_with_items.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__ambiguous_lpar_with_items.py.snap index 48d299be48..b651e7c557 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__ambiguous_lpar_with_items.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__ambiguous_lpar_with_items.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/ambiguous_lpar_with_items.py -snapshot_kind: text --- ## AST @@ -945,8 +944,8 @@ Module( FString { range: 1186..1201, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 1188..1200, expression: Name( ExprName { @@ -958,11 +957,11 @@ Module( debug_text: None, conversion: None, format_spec: Some( - FStringFormatSpec { + InterpolatedStringFormatSpec { range: 1195..1199, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 1195..1199, value: "= 42", }, @@ -1017,8 +1016,8 @@ Module( FString { range: 1214..1231, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 1216..1230, expression: Named( ExprNamed { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap index ec88a56393..eb24f71df1 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__match.py.snap @@ -3767,8 +3767,8 @@ Module( FString { range: 2932..2938, elements: [ - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 2934..2937, expression: Name( ExprName { diff --git a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__try.py.snap b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__try.py.snap index 3f799c4d6b..2413f26d90 100644 --- a/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__try.py.snap +++ b/crates/ruff_python_parser/tests/snapshots/valid_syntax@statement__try.py.snap @@ -1,7 +1,6 @@ --- source: crates/ruff_python_parser/tests/fixtures.rs input_file: crates/ruff_python_parser/resources/valid/statement/try.py -snapshot_kind: text --- ## AST @@ -577,13 +576,13 @@ Module( range: 505..524, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 507..514, value: "caught ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 514..523, expression: Call( ExprCall { @@ -682,13 +681,13 @@ Module( range: 557..576, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 559..566, value: "caught ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 566..575, expression: Call( ExprCall { @@ -955,13 +954,13 @@ Module( range: 704..750, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 706..713, value: "caught ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 713..722, expression: Call( ExprCall { @@ -994,13 +993,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 722..735, value: " with nested ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 735..749, expression: Attribute( ExprAttribute { @@ -1091,13 +1090,13 @@ Module( range: 784..830, elements: [ Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 786..793, value: "caught ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 793..802, expression: Call( ExprCall { @@ -1130,13 +1129,13 @@ Module( }, ), Literal( - FStringLiteralElement { + InterpolatedStringLiteralElement { range: 802..815, value: " with nested ", }, ), - Expression( - FStringExpressionElement { + Interpolation( + InterpolatedElement { range: 815..829, expression: Attribute( ExprAttribute { diff --git a/crates/ruff_python_semantic/src/analyze/type_inference.rs b/crates/ruff_python_semantic/src/analyze/type_inference.rs index d25e0f7e7a..c0f6f6d482 100644 --- a/crates/ruff_python_semantic/src/analyze/type_inference.rs +++ b/crates/ruff_python_semantic/src/analyze/type_inference.rs @@ -78,6 +78,7 @@ impl From<&Expr> for ResolvedPythonType { Expr::Tuple(_) => ResolvedPythonType::Atom(PythonType::Tuple), Expr::Generator(_) => ResolvedPythonType::Atom(PythonType::Generator), Expr::FString(_) => ResolvedPythonType::Atom(PythonType::String), + Expr::TString(_) => ResolvedPythonType::Unknown, Expr::StringLiteral(_) => ResolvedPythonType::Atom(PythonType::String), Expr::BytesLiteral(_) => ResolvedPythonType::Atom(PythonType::Bytes), Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => match value { diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 76ef4a8617..043d98cff5 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -1939,10 +1939,15 @@ impl<'a> SemanticModel<'a> { self.flags.intersects(SemanticModelFlags::F_STRING) } + /// Return `true` if the model is in a t-string. + pub const fn in_t_string(&self) -> bool { + self.flags.intersects(SemanticModelFlags::T_STRING) + } + /// Return `true` if the model is in an f-string replacement field. - pub const fn in_f_string_replacement_field(&self) -> bool { + pub const fn in_interpolated_string_replacement_field(&self) -> bool { self.flags - .intersects(SemanticModelFlags::F_STRING_REPLACEMENT_FIELD) + .intersects(SemanticModelFlags::INTERPOLATED_STRING_REPLACEMENT_FIELD) } /// Return `true` if the model is in boolean test. @@ -2461,7 +2466,7 @@ bitflags! { /// ```python /// f"first {x} second {y}" /// ``` - const F_STRING_REPLACEMENT_FIELD = 1 << 21; + const INTERPOLATED_STRING_REPLACEMENT_FIELD = 1 << 21; /// The model is visiting the bases tuple of a class. /// @@ -2549,6 +2554,15 @@ bitflags! { /// [#13824]: https://github.com/astral-sh/ruff/issues/13824 const NO_TYPE_CHECK = 1 << 28; + /// The model is in a t-string. + /// + /// For example, the model could be visiting `x` in: + /// ```python + /// t'{x}' + /// ``` + const T_STRING = 1 << 29; + + /// The context is in any type annotation. const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits(); diff --git a/crates/ty_python_semantic/src/semantic_index/re_exports.rs b/crates/ty_python_semantic/src/semantic_index/re_exports.rs index b8002dbf0b..049e751bf0 100644 --- a/crates/ty_python_semantic/src/semantic_index/re_exports.rs +++ b/crates/ty_python_semantic/src/semantic_index/re_exports.rs @@ -325,6 +325,7 @@ impl<'db> Visitor<'db> for ExportFinder<'db> { | ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) | ast::Expr::FString(_) + | ast::Expr::TString(_) | ast::Expr::Tuple(_) | ast::Expr::List(_) | ast::Expr::Slice(_) @@ -389,6 +390,7 @@ impl<'db> Visitor<'db> for WalrusFinder<'_, 'db> { | ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) | ast::Expr::FString(_) + | ast::Expr::TString(_) | ast::Expr::Tuple(_) | ast::Expr::List(_) | ast::Expr::Slice(_) diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 9ed888e603..e3e7b15cf8 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -116,6 +116,7 @@ impl_expression_has_type!(ast::ExprYieldFrom); impl_expression_has_type!(ast::ExprCompare); impl_expression_has_type!(ast::ExprCall); impl_expression_has_type!(ast::ExprFString); +impl_expression_has_type!(ast::ExprTString); impl_expression_has_type!(ast::ExprStringLiteral); impl_expression_has_type!(ast::ExprBytesLiteral); impl_expression_has_type!(ast::ExprNumberLiteral); @@ -152,6 +153,7 @@ impl HasType for ast::Expr { Expr::Compare(inner) => inner.inferred_type(model), Expr::Call(inner) => inner.inferred_type(model), Expr::FString(inner) => inner.inferred_type(model), + Expr::TString(inner) => inner.inferred_type(model), Expr::StringLiteral(inner) => inner.inferred_type(model), Expr::BytesLiteral(inner) => inner.inferred_type(model), Expr::NumberLiteral(inner) => inner.inferred_type(model), diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 73aefafb3c..acbf27b881 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -4328,6 +4328,7 @@ impl<'db> TypeInferenceBuilder<'db> { self.infer_bytes_literal_expression(bytes_literal) } ast::Expr::FString(fstring) => self.infer_fstring_expression(fstring), + ast::Expr::TString(tstring) => self.infer_tstring_expression(tstring), ast::Expr::EllipsisLiteral(literal) => self.infer_ellipsis_literal_expression(literal), ast::Expr::Tuple(tuple) => self.infer_tuple_expression(tuple), ast::Expr::List(list) => self.infer_list_expression(list), @@ -4426,8 +4427,8 @@ impl<'db> TypeInferenceBuilder<'db> { ast::FStringPart::FString(fstring) => { for element in &fstring.elements { match element { - ast::FStringElement::Expression(expression) => { - let ast::FStringExpressionElement { + ast::InterpolatedStringElement::Interpolation(expression) => { + let ast::InterpolatedElement { range: _, expression, debug_text: _, @@ -4437,7 +4438,7 @@ impl<'db> TypeInferenceBuilder<'db> { let ty = self.infer_expression(expression); if let Some(format_spec) = format_spec { - for element in format_spec.elements.expressions() { + for element in format_spec.elements.interpolations() { self.infer_expression(&element.expression); } } @@ -4456,7 +4457,7 @@ impl<'db> TypeInferenceBuilder<'db> { } } } - ast::FStringElement::Literal(literal) => { + ast::InterpolatedStringElement::Literal(literal) => { collector.push_str(&literal.value); } } @@ -4467,6 +4468,59 @@ impl<'db> TypeInferenceBuilder<'db> { collector.string_type(self.db()) } + fn infer_tstring_expression(&mut self, tstring: &ast::ExprTString) -> Type<'db> { + let ast::ExprTString { value, .. } = tstring; + for part in value { + match part { + ast::TStringPart::Literal(_) => {} + ast::TStringPart::FString(fstring) => { + for element in &fstring.elements { + match element { + ast::InterpolatedStringElement::Interpolation(expression) => { + let ast::InterpolatedElement { + expression, + format_spec, + .. + } = expression; + self.infer_expression(expression); + + if let Some(format_spec) = format_spec { + for element in format_spec.elements.interpolations() { + self.infer_expression(&element.expression); + } + } + } + ast::InterpolatedStringElement::Literal(_) => {} + } + } + } + ast::TStringPart::TString(tstring) => { + for element in &tstring.elements { + match element { + ast::InterpolatedStringElement::Interpolation( + tstring_interpolation_element, + ) => { + let ast::InterpolatedElement { + expression, + format_spec, + .. + } = tstring_interpolation_element; + self.infer_expression(expression); + if let Some(format_spec) = format_spec { + for element in format_spec.elements.interpolations() { + self.infer_expression(&element.expression); + } + } + } + ast::InterpolatedStringElement::Literal(_) => {} + } + } + } + } + } + todo_type!("Template") + } + fn infer_ellipsis_literal_expression( &mut self, _literal: &ast::ExprEllipsisLiteral, @@ -8285,6 +8339,14 @@ impl<'db> TypeInferenceBuilder<'db> { ) } + ast::Expr::TString(tstring) => { + self.infer_tstring_expression(tstring); + self.report_invalid_type_expression( + expression, + format_args!("T-strings are not allowed in type expressions"), + ) + } + ast::Expr::Slice(slice) => { self.infer_slice_expression(slice); self.report_invalid_type_expression(