From 3ed4a2f92b74893f7d60792debf499257d63105a Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 10 Nov 2023 02:34:20 +0530 Subject: [PATCH] Preserve trailing semicolon for Notebooks --- .../test/fixtures/trailing_semicolon.ipynb | 413 +++++++++++++++++ crates/ruff_cli/tests/format.rs | 429 ++++++++++++++++++ .../src/comments/format.rs | 10 +- crates/ruff_python_formatter/src/context.rs | 28 +- .../src/expression/mod.rs | 4 +- .../src/expression/parentheses.rs | 6 +- .../src/statement/stmt_ann_assign.rs | 10 + .../src/statement/stmt_assign.rs | 14 +- .../src/statement/stmt_aug_assign.rs | 14 +- .../src/statement/stmt_expr.rs | 15 +- .../src/statement/suite.rs | 27 +- 11 files changed, 945 insertions(+), 25 deletions(-) create mode 100644 crates/ruff_cli/resources/test/fixtures/trailing_semicolon.ipynb diff --git a/crates/ruff_cli/resources/test/fixtures/trailing_semicolon.ipynb b/crates/ruff_cli/resources/test/fixtures/trailing_semicolon.ipynb new file mode 100644 index 0000000000000..f6427ba81beaa --- /dev/null +++ b/crates/ruff_cli/resources/test/fixtures/trailing_semicolon.ipynb @@ -0,0 +1,413 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "4f8ce941-1492-4d4e-8ab5-70d733fe891a", + "metadata": {}, + "outputs": [], + "source": [ + "%config ZMQInteractiveShell.ast_node_interactivity=\"last_expr_or_assign\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "721ec705-0c65-4bfb-9809-7ed8bc534186", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Assignment statement without a semicolon\n", + "x = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "de50e495-17e5-41cc-94bd-565757555d7e", + "metadata": {}, + "outputs": [], + "source": [ + "# Assignment statement with a semicolon\n", + "x = 1;\n", + "x = 1;" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "39e31201-23da-44eb-8684-41bba3663991", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Augmented assignment without a semicolon\n", + "x += 1" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6b73d3dd-c73a-4697-9e97-e109a6c1fbab", + "metadata": {}, + "outputs": [], + "source": [ + "# Augmented assignment without a semicolon\n", + "x += 1;\n", + "x += 1; # comment\n", + "# comment" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2a3e5b86-aa5b-46ba-b9c6-0386d876f58c", + "metadata": {}, + "outputs": [], + "source": [ + "# Multiple assignment without a semicolon\n", + "x = y = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "07f89e51-9357-4cfb-8fc5-76fb75e35949", + "metadata": {}, + "outputs": [], + "source": [ + "# Multiple assignment with a semicolon\n", + "x = y = 1;\n", + "x = y = 1;" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c22b539d-473e-48f8-a236-625e58c47a00", + "metadata": {}, + "outputs": [], + "source": [ + "# Tuple unpacking without a semicolon\n", + "x, y = 1, 2" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "12c87940-a0d5-403b-a81c-7507eb06dc7e", + "metadata": {}, + "outputs": [], + "source": [ + "# Tuple unpacking with a semicolon (irrelevant)\n", + "x, y = 1, 2;\n", + "x, y = 1, 2; # comment\n", + "# comment" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5a768c76-6bc4-470c-b37e-8cc14bc6caf4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Annotated assignment statement without a semicolon\n", + "x: int = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "21bfda82-1a9a-4ba1-9078-74ac480804b5", + "metadata": {}, + "outputs": [], + "source": [ + "# Annotated assignment statement without a semicolon\n", + "x: int = 1;\n", + "x: int = 1; # comment\n", + "# comment" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "09929999-ff29-4d10-ad2b-e665af15812d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Assignment expression without a semicolon\n", + "(x := 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "32a83217-1bad-4f61-855e-ffcdb119c763", + "metadata": {}, + "outputs": [], + "source": [ + "# Assignment expression with a semicolon\n", + "(x := 1);\n", + "(x := 1); # comment\n", + "# comment" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "61b81865-277e-4964-b03e-eb78f1f318eb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x = 1\n", + "# Expression without a semicolon\n", + "x" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "974c29be-67e1-4000-95fa-6ca118a63bad", + "metadata": {}, + "outputs": [], + "source": [ + "x = 1\n", + "# Expression with a semicolon\n", + "x;" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "cfeb1757-46d6-4f13-969f-a283b6d0304f", + "metadata": {}, + "outputs": [], + "source": [ + "class Point:\n", + " def __init__(self, x, y):\n", + " self.x = x\n", + " self.y = y\n", + "\n", + "\n", + "p = Point(0, 0);" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "2ee7f1a5-ccfe-4004-bfa4-ef834a58da97", + "metadata": {}, + "outputs": [], + "source": [ + "# Assignment statement where the left is an attribute access doesn't\n", + "# print the value.\n", + "p.x = 1;" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "3e49370a-048b-474d-aa0a-3d1d4a73ad37", + "metadata": {}, + "outputs": [], + "source": [ + "data = {}\n", + "\n", + "# Neither does the subscript node\n", + "data[\"foo\"] = 1;" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "d594bdd3-eaa9-41ef-8cda-cf01bc273b2d", + "metadata": {}, + "outputs": [], + "source": [ + "if (x := 1):\n", + " # It should be the top level statement\n", + " x" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "e532f0cf-80c7-42b7-8226-6002fcf74fb6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Parentheses with comments\n", + "(\n", + " x := 1 # comment\n", + ") # comment" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "473c5d62-871b-46ed-8a34-27095243f462", + "metadata": {}, + "outputs": [], + "source": [ + "# Parentheses with comments\n", + "(\n", + " x := 1 # comment\n", + "); # comment" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "8c3c2361-f49f-45fe-bbe3-7e27410a8a86", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Hello world!'" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"Hello world!\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "23dbe9b5-3f68-4890-ab2d-ab0dbfd0712a", + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"Hello world!\"\"\"; # comment\n", + "# comment" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "3ce33108-d95d-4c70-83d1-0d4fd36a2951", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'x = 1'" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x = 1\n", + "f\"x = {x}\"" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "654a4a67-de43-4684-824a-9451c67db48f", + "metadata": {}, + "outputs": [], + "source": [ + "x = 1\n", + "f\"x = {x}\";\n", + "f\"x = {x}\"; # comment\n", + "# comment" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (ruff-playground)", + "language": "python", + "name": "ruff-playground" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/crates/ruff_cli/tests/format.rs b/crates/ruff_cli/tests/format.rs index 322b72cafb365..35478854c52e9 100644 --- a/crates/ruff_cli/tests/format.rs +++ b/crates/ruff_cli/tests/format.rs @@ -813,3 +813,432 @@ fn test_diff_stdin_formatted() { ----- stderr ----- "###); } + +#[test] +fn test_notebook_trailing_semicolon() { + let fixtures = Path::new("resources").join("test").join("fixtures"); + let unformatted = fs::read(fixtures.join("trailing_semicolon.ipynb")).unwrap(); + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .args(["format", "--isolated", "--stdin-filename", "test.ipynb"]) + .arg("-") + .pass_stdin(unformatted), @r###" + success: true + exit_code: 0 + ----- stdout ----- + { + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "4f8ce941-1492-4d4e-8ab5-70d733fe891a", + "metadata": {}, + "outputs": [], + "source": [ + "%config ZMQInteractiveShell.ast_node_interactivity=\"last_expr_or_assign\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "721ec705-0c65-4bfb-9809-7ed8bc534186", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Assignment statement without a semicolon\n", + "x = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "de50e495-17e5-41cc-94bd-565757555d7e", + "metadata": {}, + "outputs": [], + "source": [ + "# Assignment statement with a semicolon\n", + "x = 1\n", + "x = 1;" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "39e31201-23da-44eb-8684-41bba3663991", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Augmented assignment without a semicolon\n", + "x += 1" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6b73d3dd-c73a-4697-9e97-e109a6c1fbab", + "metadata": {}, + "outputs": [], + "source": [ + "# Augmented assignment without a semicolon\n", + "x += 1\n", + "x += 1; # comment\n", + "# comment" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2a3e5b86-aa5b-46ba-b9c6-0386d876f58c", + "metadata": {}, + "outputs": [], + "source": [ + "# Multiple assignment without a semicolon\n", + "x = y = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "07f89e51-9357-4cfb-8fc5-76fb75e35949", + "metadata": {}, + "outputs": [], + "source": [ + "# Multiple assignment with a semicolon\n", + "x = y = 1\n", + "x = y = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c22b539d-473e-48f8-a236-625e58c47a00", + "metadata": {}, + "outputs": [], + "source": [ + "# Tuple unpacking without a semicolon\n", + "x, y = 1, 2" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "12c87940-a0d5-403b-a81c-7507eb06dc7e", + "metadata": {}, + "outputs": [], + "source": [ + "# Tuple unpacking with a semicolon (irrelevant)\n", + "x, y = 1, 2\n", + "x, y = 1, 2 # comment\n", + "# comment" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5a768c76-6bc4-470c-b37e-8cc14bc6caf4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Annotated assignment statement without a semicolon\n", + "x: int = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "21bfda82-1a9a-4ba1-9078-74ac480804b5", + "metadata": {}, + "outputs": [], + "source": [ + "# Annotated assignment statement without a semicolon\n", + "x: int = 1\n", + "x: int = 1; # comment\n", + "# comment" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "09929999-ff29-4d10-ad2b-e665af15812d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Assignment expression without a semicolon\n", + "(x := 1)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "32a83217-1bad-4f61-855e-ffcdb119c763", + "metadata": {}, + "outputs": [], + "source": [ + "# Assignment expression with a semicolon\n", + "(x := 1)\n", + "(x := 1); # comment\n", + "# comment" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "61b81865-277e-4964-b03e-eb78f1f318eb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x = 1\n", + "# Expression without a semicolon\n", + "x" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "974c29be-67e1-4000-95fa-6ca118a63bad", + "metadata": {}, + "outputs": [], + "source": [ + "x = 1\n", + "# Expression with a semicolon\n", + "x;" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "cfeb1757-46d6-4f13-969f-a283b6d0304f", + "metadata": {}, + "outputs": [], + "source": [ + "class Point:\n", + " def __init__(self, x, y):\n", + " self.x = x\n", + " self.y = y\n", + "\n", + "\n", + "p = Point(0, 0);" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "2ee7f1a5-ccfe-4004-bfa4-ef834a58da97", + "metadata": {}, + "outputs": [], + "source": [ + "# Assignment statement where the left is an attribute access doesn't\n", + "# print the value.\n", + "p.x = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "3e49370a-048b-474d-aa0a-3d1d4a73ad37", + "metadata": {}, + "outputs": [], + "source": [ + "data = {}\n", + "\n", + "# Neither does the subscript node\n", + "data[\"foo\"] = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "d594bdd3-eaa9-41ef-8cda-cf01bc273b2d", + "metadata": {}, + "outputs": [], + "source": [ + "if x := 1:\n", + " # It should be the top level statement\n", + " x" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "e532f0cf-80c7-42b7-8226-6002fcf74fb6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Parentheses with comments\n", + "(\n", + " x := 1 # comment\n", + ") # comment" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "473c5d62-871b-46ed-8a34-27095243f462", + "metadata": {}, + "outputs": [], + "source": [ + "# Parentheses with comments\n", + "(\n", + " x := 1 # comment\n", + "); # comment" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "8c3c2361-f49f-45fe-bbe3-7e27410a8a86", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Hello world!'" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"Hello world!\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "23dbe9b5-3f68-4890-ab2d-ab0dbfd0712a", + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"Hello world!\"\"\"; # comment\n", + "# comment" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "3ce33108-d95d-4c70-83d1-0d4fd36a2951", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'x = 1'" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "x = 1\n", + "f\"x = {x}\"" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "654a4a67-de43-4684-824a-9451c67db48f", + "metadata": {}, + "outputs": [], + "source": [ + "x = 1\n", + "f\"x = {x}\"\n", + "f\"x = {x}\"; # comment\n", + "# comment" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (ruff-playground)", + "language": "python", + "name": "ruff-playground" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 + } + + ----- stderr ----- + "###); +} diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index a492ef1012373..abf4833dac59d 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -323,7 +323,7 @@ pub(crate) struct FormatEmptyLines { impl Format> for FormatEmptyLines { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { match f.context().node_level() { - NodeLevel::TopLevel => match self.lines { + NodeLevel::TopLevel(_) => match self.lines { 0 | 1 => write!(f, [hard_line_break()]), 2 => write!(f, [empty_line()]), _ => match f.options().source_type() { @@ -519,9 +519,9 @@ pub(crate) fn empty_lines_before_trailing_comments<'a>( ) -> FormatEmptyLinesBeforeTrailingComments<'a> { // Black has different rules for stub vs. non-stub and top level vs. indented let empty_lines = match (f.options().source_type(), f.context().node_level()) { - (PySourceType::Stub, NodeLevel::TopLevel) => 1, + (PySourceType::Stub, NodeLevel::TopLevel(_)) => 1, (PySourceType::Stub, _) => 0, - (_, NodeLevel::TopLevel) => 2, + (_, NodeLevel::TopLevel(_)) => 2, (_, _) => 1, }; @@ -573,9 +573,9 @@ pub(crate) fn empty_lines_after_leading_comments<'a>( ) -> FormatEmptyLinesAfterLeadingComments<'a> { // Black has different rules for stub vs. non-stub and top level vs. indented let empty_lines = match (f.options().source_type(), f.context().node_level()) { - (PySourceType::Stub, NodeLevel::TopLevel) => 1, + (PySourceType::Stub, NodeLevel::TopLevel(_)) => 1, (PySourceType::Stub, _) => 0, - (_, NodeLevel::TopLevel) => 2, + (_, NodeLevel::TopLevel(_)) => 2, (_, _) => 1, }; diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 01caed9e6854d..f92825fe958f4 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -19,7 +19,7 @@ impl<'a> PyFormatContext<'a> { options, contents, comments, - node_level: NodeLevel::TopLevel, + node_level: NodeLevel::TopLevel(TopLevelStatementPosition::Other), } } @@ -68,12 +68,21 @@ impl Debug for PyFormatContext<'_> { } } -/// What's the enclosing level of the outer node. +/// The position of a top-level statement in the module. #[derive(Copy, Clone, Debug, Eq, PartialEq, Default)] +pub(crate) enum TopLevelStatementPosition { + /// This is the last top-level statement in the module. + Last, + /// Any other top-level statement. + #[default] + Other, +} + +/// What's the enclosing level of the outer node. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(crate) enum NodeLevel { /// Formatting statements on the module level. - #[default] - TopLevel, + TopLevel(TopLevelStatementPosition), /// Formatting the body statements of a [compound statement](https://docs.python.org/3/reference/compound_stmts.html#compound-statements) /// (`if`, `while`, `match`, etc.). @@ -86,6 +95,12 @@ pub(crate) enum NodeLevel { ParenthesizedExpression, } +impl Default for NodeLevel { + fn default() -> Self { + Self::TopLevel(TopLevelStatementPosition::Other) + } +} + impl NodeLevel { /// Returns `true` if the expression is in a parenthesized context. pub(crate) const fn is_parenthesized(self) -> bool { @@ -94,6 +109,11 @@ impl NodeLevel { NodeLevel::Expression(Some(_)) | NodeLevel::ParenthesizedExpression ) } + + /// Returns `true` if this is the last top-level statement in the module. + pub(crate) const fn is_last_top_level_statement(self) -> bool { + matches!(self, NodeLevel::TopLevel(TopLevelStatementPosition::Last)) + } } /// Change the [`NodeLevel`] of the formatter for the lifetime of this struct diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 707963f1ec0e9..019546e596edd 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -135,7 +135,9 @@ impl FormatRule> for FormatExpr { } } else { let level = match f.context().node_level() { - NodeLevel::TopLevel | NodeLevel::CompoundStatement => NodeLevel::Expression(None), + NodeLevel::TopLevel(_) | NodeLevel::CompoundStatement => { + NodeLevel::Expression(None) + } saved_level @ (NodeLevel::Expression(_) | NodeLevel::ParenthesizedExpression) => { saved_level } diff --git a/crates/ruff_python_formatter/src/expression/parentheses.rs b/crates/ruff_python_formatter/src/expression/parentheses.rs index e68a5a473e9a2..75e0ee3fe0def 100644 --- a/crates/ruff_python_formatter/src/expression/parentheses.rs +++ b/crates/ruff_python_formatter/src/expression/parentheses.rs @@ -252,7 +252,7 @@ pub(crate) enum InParenthesesOnlyLineBreak { impl<'ast> Format> for InParenthesesOnlyLineBreak { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { match f.context().node_level() { - NodeLevel::TopLevel | NodeLevel::CompoundStatement | NodeLevel::Expression(None) => { + NodeLevel::TopLevel(_) | NodeLevel::CompoundStatement | NodeLevel::Expression(None) => { match self { InParenthesesOnlyLineBreak::SoftLineBreak => Ok(()), InParenthesesOnlyLineBreak::SoftLineBreakOrSpace => space().fmt(f), @@ -319,7 +319,7 @@ pub(super) fn write_in_parentheses_only_group_start_tag(f: &mut PyFormatter) { // Unconditionally group the content if it is not enclosed by an optional parentheses group. f.write_element(FormatElement::Tag(tag::Tag::StartGroup(tag::Group::new()))); } - NodeLevel::Expression(None) | NodeLevel::TopLevel | NodeLevel::CompoundStatement => { + NodeLevel::Expression(None) | NodeLevel::TopLevel(_) | NodeLevel::CompoundStatement => { // No group } } @@ -334,7 +334,7 @@ pub(super) fn write_in_parentheses_only_group_end_tag(f: &mut PyFormatter) { // Unconditionally group the content if it is not enclosed by an optional parentheses group. f.write_element(FormatElement::Tag(tag::Tag::EndGroup)); } - NodeLevel::Expression(None) | NodeLevel::TopLevel | NodeLevel::CompoundStatement => { + NodeLevel::Expression(None) | NodeLevel::TopLevel(_) | NodeLevel::CompoundStatement => { // No group } } diff --git a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs index f22457cc4b56a..cb5f5fa745278 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_ann_assign.rs @@ -2,9 +2,11 @@ use ruff_formatter::write; use ruff_python_ast::StmtAnnAssign; use crate::comments::{SourceComment, SuppressionKind}; + use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::statement::trailing_semicolon; #[derive(Default)] pub struct FormatStmtAnnAssign; @@ -36,6 +38,14 @@ impl FormatNodeRule for FormatStmtAnnAssign { )?; } + if f.options().source_type().is_ipynb() + && f.context().node_level().is_last_top_level_statement() + && target.is_name_expr() + && trailing_semicolon(item.into(), f.context().source()).is_some() + { + token(";").fmt(f)?; + } + Ok(()) } diff --git a/crates/ruff_python_formatter/src/statement/stmt_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_assign.rs index 475a21a58b63e..7a8a5fd2be005 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_assign.rs @@ -6,6 +6,7 @@ use crate::context::{NodeLevel, WithNodeLevel}; use crate::expression::parentheses::{Parentheses, Parenthesize}; use crate::expression::{has_own_parentheses, maybe_parenthesize_expression}; use crate::prelude::*; +use crate::statement::trailing_semicolon; #[derive(Default)] pub struct FormatStmtAssign; @@ -40,7 +41,18 @@ impl FormatNodeRule for FormatStmtAssign { item, Parenthesize::IfBreaks )] - ) + )?; + + if f.options().source_type().is_ipynb() + && f.context().node_level().is_last_top_level_statement() + && rest.is_empty() + && first.is_name_expr() + && trailing_semicolon(item.into(), f.context().source()).is_some() + { + token(";").fmt(f)?; + } + + Ok(()) } fn is_suppressed( diff --git a/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs b/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs index df15aaa12173b..65260c5fecdc9 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_aug_assign.rs @@ -2,9 +2,11 @@ use ruff_formatter::write; use ruff_python_ast::StmtAugAssign; use crate::comments::{SourceComment, SuppressionKind}; + use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::statement::trailing_semicolon; use crate::{AsFormat, FormatNodeRule}; #[derive(Default)] @@ -28,7 +30,17 @@ impl FormatNodeRule for FormatStmtAugAssign { space(), maybe_parenthesize_expression(value, item, Parenthesize::IfBreaks) ] - ) + )?; + + if f.options().source_type().is_ipynb() + && f.context().node_level().is_last_top_level_statement() + && target.is_name_expr() + && trailing_semicolon(item.into(), f.context().source()).is_some() + { + token(";").fmt(f)?; + } + + Ok(()) } fn is_suppressed( diff --git a/crates/ruff_python_formatter/src/statement/stmt_expr.rs b/crates/ruff_python_formatter/src/statement/stmt_expr.rs index ae2b45bad2cfe..c0a6361b71c81 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_expr.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_expr.rs @@ -2,9 +2,11 @@ use ruff_python_ast as ast; use ruff_python_ast::{Expr, Operator, StmtExpr}; use crate::comments::{SourceComment, SuppressionKind}; + use crate::expression::maybe_parenthesize_expression; use crate::expression::parentheses::Parenthesize; use crate::prelude::*; +use crate::statement::trailing_semicolon; #[derive(Default)] pub struct FormatStmtExpr; @@ -14,10 +16,19 @@ impl FormatNodeRule for FormatStmtExpr { let StmtExpr { value, .. } = item; if is_arithmetic_like(value) { - maybe_parenthesize_expression(value, item, Parenthesize::Optional).fmt(f) + maybe_parenthesize_expression(value, item, Parenthesize::Optional).fmt(f)?; } else { - value.format().fmt(f) + value.format().fmt(f)?; + } + + if f.options().source_type().is_ipynb() + && f.context().node_level().is_last_top_level_statement() + && trailing_semicolon(item.into(), f.context().source()).is_some() + { + token(";").fmt(f)?; } + + Ok(()) } fn is_suppressed( diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index b0dee5f58c0b3..beac36ffb9221 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -8,7 +8,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::comments::{ leading_comments, trailing_comments, Comments, LeadingDanglingTrailingComments, }; -use crate::context::{NodeLevel, WithNodeLevel}; +use crate::context::{NodeLevel, TopLevelStatementPosition, WithNodeLevel}; use crate::expression::string::StringLayout; use crate::prelude::*; use crate::statement::stmt_expr::FormatStmtExpr; @@ -49,8 +49,19 @@ impl Default for FormatSuite { impl FormatRule> for FormatSuite { fn fmt(&self, statements: &Suite, f: &mut PyFormatter) -> FormatResult<()> { + let mut iter = statements.iter(); + let Some(first) = iter.next() else { + return Ok(()); + }; + let node_level = match self.kind { - SuiteKind::TopLevel => NodeLevel::TopLevel, + SuiteKind::TopLevel => NodeLevel::TopLevel( + iter.clone() + .next() + .map_or(TopLevelStatementPosition::Last, |_| { + TopLevelStatementPosition::Other + }), + ), SuiteKind::Function | SuiteKind::Class | SuiteKind::Other => { NodeLevel::CompoundStatement } @@ -62,11 +73,6 @@ impl FormatRule> for FormatSuite { let f = &mut WithNodeLevel::new(node_level, f); - let mut iter = statements.iter(); - let Some(first) = iter.next() else { - return Ok(()); - }; - // Format the first statement in the body, which often has special formatting rules. let first = match self.kind { SuiteKind::Other => { @@ -165,6 +171,11 @@ impl FormatRule> for FormatSuite { let mut preceding_comments = comments.leading_dangling_trailing(preceding); while let Some(following) = iter.next() { + if self.kind == SuiteKind::TopLevel && iter.clone().next().is_none() { + f.context_mut() + .set_node_level(NodeLevel::TopLevel(TopLevelStatementPosition::Last)); + } + let following_comments = comments.leading_dangling_trailing(following); let needs_empty_lines = if is_class_or_function_definition(following) { @@ -351,7 +362,7 @@ impl FormatRule> for FormatSuite { .map_or(preceding.end(), |comment| comment.slice().end()); match node_level { - NodeLevel::TopLevel => match lines_after(end, source) { + NodeLevel::TopLevel(_) => match lines_after(end, source) { 0 | 1 => hard_line_break().fmt(f)?, 2 => empty_line().fmt(f)?, _ => match source_type {