Repository: PlasmaFAIR/fortitude (language: python)

Issue:
missing CRLF when adding name at the end of a subroutine
On Windows 10, with fortitude 0.6.1, I got the following when trying to fix some issues
![image](https://github.com/user-attachments/assets/839e55ff-9e93-4ac4-a0dc-f1b1f5c6b765)

The CRLF is missing at the end of the subroutine name and the `end module` gets merged with the name 


Discussion / hints:
Thanks for catching this! I can replicate it on Linux too. Our tests were missing it because we'd put 'helpful' comments at the end of our test lines:

```f90
module mymod1
  implicit none
  type mytype
    integer :: x
  end type                      ! catch this
contains
  subroutine mysub1()
    write (*,*) 'hello world'
  end subroutine                ! catch this
  subroutine mysub2()
    write (*,*) 'hello world'
  end subroutine mysub2         ! ignore this
end                             ! catch this
```

This would rewrite the code up to the comment, so we weren't seeing it that it would also overwrite the newline too :man_facepalming:.

We'll try to have this fixed as soon as possible!

Fix patch:
diff --git a/fortitude/src/ast.rs b/fortitude/src/ast.rs
index a9cf8a9..a29f15a 100644
--- a/fortitude/src/ast.rs
+++ b/fortitude/src/ast.rs
@@ -108,7 +108,7 @@ pub trait FortitudeNode<'tree> {
     fn edit_delete(&self, source_file: &SourceFile) -> Edit;
 
     /// Creates an edit that inserts `content`, replacing the whole node
-    fn edit_replacement(&self, content: String) -> Edit;
+    fn edit_replacement(&self, original: &SourceFile, content: String) -> Edit;
 }
 
 impl FortitudeNode<'_> for Node<'_> {
@@ -182,12 +182,16 @@ impl FortitudeNode<'_> for Node<'_> {
         }
     }
 
-    fn edit_replacement(&self, content: String) -> Edit {
-        Edit::replacement(
-            content,
-            TextSize::try_from(self.start_byte()).unwrap(),
-            TextSize::try_from(self.end_byte()).unwrap(),
-        )
+    fn edit_replacement(&self, original: &SourceFile, content: String) -> Edit {
+        // The node might include the newline as part of the
+        // end-of-statement child, so don't include trailing
+        // whitespace in the replacement
+        let text = self.to_text(original.source_text()).unwrap();
+        let len = text.trim().len();
+        let start = TextSize::try_from(self.start_byte()).unwrap();
+        let end = start + TextSize::try_from(len).unwrap();
+
+        Edit::replacement(content, start, end)
     }
 }
 
diff --git a/fortitude/src/rules/style/end_statements.rs b/fortitude/src/rules/style/end_statements.rs
index e8e994c..de17c50 100644
--- a/fortitude/src/rules/style/end_statements.rs
+++ b/fortitude/src/rules/style/end_statements.rs
@@ -96,7 +96,7 @@ impl AstRule for UnnamedEndStatement {
             .to_string();
         let statement = statement.to_string();
         let replacement = format!("end {statement} {name}");
-        let fix = Fix::safe_edit(node.edit_replacement(replacement));
+        let fix = Fix::safe_edit(node.edit_replacement(src, replacement));
         some_vec![Diagnostic::from_node(Self { statement, name }, node).with_fix(fix)]
     }
 
diff --git a/fortitude/src/rules/style/old_style_array_literal.rs b/fortitude/src/rules/style/old_style_array_literal.rs
index 0d368b7..09e1736 100644
--- a/fortitude/src/rules/style/old_style_array_literal.rs
+++ b/fortitude/src/rules/style/old_style_array_literal.rs
@@ -34,8 +34,8 @@ impl AstRule for OldStyleArrayLiteral {
         if open_bracket.to_text(src.source_text())?.starts_with("(/") {
             let close_bracket = node.children(&mut node.walk()).last()?;
 
-            let edit_open = open_bracket.edit_replacement("[".to_string());
-            let edit_close = close_bracket.edit_replacement("]".to_string());
+            let edit_open = open_bracket.edit_replacement(src, "[".to_string());
+            let edit_close = close_bracket.edit_replacement(src, "]".to_string());
             let fix = Fix::safe_edits(edit_open, [edit_close]);
 
             return some_vec!(Diagnostic::from_node(Self {}, node).with_fix(fix));
diff --git a/fortitude/src/rules/style/relational_operators.rs b/fortitude/src/rules/style/relational_operators.rs
index ba86896..6247aec 100644
--- a/fortitude/src/rules/style/relational_operators.rs
+++ b/fortitude/src/rules/style/relational_operators.rs
@@ -52,7 +52,7 @@ impl AstRule for DeprecatedRelationalOperator {
             .to_string();
         let new_symbol = map_relational_symbols(symbol.as_str())?.to_string();
 
-        let fix = Fix::safe_edit(relation.edit_replacement(new_symbol.clone()));
+        let fix = Fix::safe_edit(relation.edit_replacement(src, new_symbol.clone()));
 
         some_vec![Diagnostic::from_node(Self { symbol, new_symbol }, &relation).with_fix(fix)]
     }
diff --git a/fortitude/src/rules/style/whitespace.rs b/fortitude/src/rules/style/whitespace.rs
index 3c93053..355211c 100644
--- a/fortitude/src/rules/style/whitespace.rs
+++ b/fortitude/src/rules/style/whitespace.rs
@@ -76,12 +76,8 @@ impl AlwaysFixableViolation for IncorrectSpaceBeforeComment {
     }
 }
 impl AstRule for IncorrectSpaceBeforeComment {
-    fn check(
-        _settings: &Settings,
-        node: &Node,
-        source_file: &SourceFile,
-    ) -> Option<Vec<Diagnostic>> {
-        let source = source_file.to_source_code();
+    fn check(_settings: &Settings, node: &Node, src: &SourceFile) -> Option<Vec<Diagnostic>> {
+        let source = src.to_source_code();
         let comment_start = TextSize::try_from(node.start_byte()).unwrap();
         // Get the line up to the start of the comment
         let line_index = source.line_index(comment_start);
@@ -96,7 +92,7 @@ impl AstRule for IncorrectSpaceBeforeComment {
         }
         if whitespace < 2 {
             let replacement = format!("{}{}", &"  "[whitespace..], node.to_text(source.text())?);
-            let edit = node.edit_replacement(replacement);
+            let edit = node.edit_replacement(src, replacement);
             return some_vec!(Diagnostic::from_node(Self {}, node).with_fix(Fix::safe_edit(edit)));
         }
         None
diff --git a/fortitude/src/rules/typing/star_kinds.rs b/fortitude/src/rules/typing/star_kinds.rs
index ed7396e..5e07d27 100644
--- a/fortitude/src/rules/typing/star_kinds.rs
+++ b/fortitude/src/rules/typing/star_kinds.rs
@@ -48,14 +48,14 @@ impl Violation for StarKind {
 
 impl AstRule for StarKind {
     fn check(_settings: &Settings, node: &Node, src: &SourceFile) -> Option<Vec<Diagnostic>> {
-        let src = src.source_text();
-        let dtype = node.child(0)?.to_text(src)?.to_lowercase();
+        let text = src.source_text();
+        let dtype = node.child(0)?.to_text(text)?.to_lowercase();
         // TODO: Handle characters
         if !dtype_is_plain_number(dtype.as_str()) {
             return None;
         }
         let kind_node = node.child_by_field_name("kind")?;
-        let size = kind_node.to_text(src)?;
+        let size = kind_node.to_text(text)?;
         if !size.starts_with('*') {
             return None;
         }
@@ -64,9 +64,9 @@ impl AstRule for StarKind {
         let size = strip_line_breaks(size).replace([' ', '\t'], "");
 
         let literal = kind_node.child_with_name("number_literal")?;
-        let kind = literal.to_text(src)?.to_string();
+        let kind = literal.to_text(text)?.to_string();
         let replacement = format!("{dtype}({kind})");
-        let fix = Fix::unsafe_edit(node.edit_replacement(replacement));
+        let fix = Fix::unsafe_edit(node.edit_replacement(src, replacement));
         some_vec![Diagnostic::from_node(Self { dtype, size, kind }, &kind_node).with_fix(fix)]
     }
 


Test patch:
diff --git a/fortitude/resources/test/fixtures/style/S061.f90 b/fortitude/resources/test/fixtures/style/S061.f90
index 8ded261..5df3605 100644
--- a/fortitude/resources/test/fixtures/style/S061.f90
+++ b/fortitude/resources/test/fixtures/style/S061.f90
@@ -2,60 +2,78 @@ module mymod1
   implicit none
   type mytype
     integer :: x
-  end type                      ! catch this
+    ! catch this
+  end type
 contains
   subroutine mysub1()
     write (*,*) 'hello world'
-  end subroutine                ! catch this
+    ! catch this
+  end subroutine
   subroutine mysub2()
     write (*,*) 'hello world'
-  end subroutine mysub2         ! ignore this
-end                             ! catch this
+    ! ignore this
+  end subroutine mysub2
+  ! catch this
+end
 module mymod2
   implicit none
   type mytype
     integer :: x
-  end type mytype               ! ignore this
+    ! ignore this
+  end type mytype
 contains
   integer function myfunc1()
     myfunc1 = 1
-  end function                  ! catch this
+    ! catch this
+  end function
   integer function myfunc2()
     myfunc2 = 1
-  end function myfunc2          ! ignore this
-end module                      ! catch this
+    ! ignore this
+  end function myfunc2
+  ! catch this
+end module
 module mymod3
   interface
     module function foo() result(x)
       integer :: x
-    end function foo            ! ignore this
+      ! ignore this
+    end function foo
     module function bar() result(x)
       integer :: x
-    end function bar            ! ignore this
+      ! ignore this
+    end function bar
     module function baz() result(x)
       integer :: x
-    end function baz            ! ignore this
+      ! ignore this
+    end function baz
   end interface
 end module mymod3
 submodule (mymod3) mysub1
 contains
   module procedure foo
     x = 1
-  end procedure                 ! catch this
-end                             ! catch this
+    ! catch this
+  end procedure
+  ! catch this
+end
 submodule (mymod3) mysub2
 contains
   module procedure bar
     x = 1
-  end procedure bar             ! ignore this
-end submodule                   ! catch this
+    ! ignore this
+  end procedure bar
+  ! catch this
+end submodule
 submodule (mymod3) mysub3
 contains
   module procedure baz
     x = 1
-  end procedure baz             ! ignore this
-end submodule mysub3            ! ignore this
+    ! ignore this
+  end procedure baz
+  ! ignore this
+end submodule mysub3
 program myprog
   implicit none
   write (*,*) 'hello world'
-end                             ! catch this
+  ! catch this
+end
diff --git a/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__unnamed-end-statement_S061.f90.snap b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__unnamed-end-statement_S061.f90.snap
index 0632bce..e3f51d1 100644
--- a/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__unnamed-end-statement_S061.f90.snap
+++ b/fortitude/src/rules/style/snapshots/fortitude__rules__style__tests__unnamed-end-statement_S061.f90.snap
@@ -3,186 +3,194 @@ source: fortitude/src/rules/style/mod.rs
 expression: diagnostics
 snapshot_kind: text
 ---
-./resources/test/fixtures/style/S061.f90:5:3: S061 [*] end statement should be named.
+./resources/test/fixtures/style/S061.f90:6:3: S061 [*] end statement should be named.
   |
-3 |   type mytype
 4 |     integer :: x
-5 |   end type                      ! catch this
-  |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
-6 | contains
-7 |   subroutine mysub1()
+5 |     ! catch this
+6 |   end type
+  |   ^^^^^^^^ S061
+7 | contains
+8 |   subroutine mysub1()
+9 |     write (*,*) 'hello world'
   |
   = help: Write as 'end type mytype'.
 
 ℹ Safe fix
-2 2 |   implicit none
 3 3 |   type mytype
 4 4 |     integer :: x
-5   |-  end type                      ! catch this
-  5 |+  end type mytype! catch this
-6 6 | contains
-7 7 |   subroutine mysub1()
-8 8 |     write (*,*) 'hello world'
-
-./resources/test/fixtures/style/S061.f90:9:3: S061 [*] end statement should be named.
-   |
- 7 |   subroutine mysub1()
- 8 |     write (*,*) 'hello world'
- 9 |   end subroutine                ! catch this
-   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
-10 |   subroutine mysub2()
-11 |     write (*,*) 'hello world'
+5 5 |     ! catch this
+6   |-  end type
+  6 |+  end type mytype
+7 7 | contains
+8 8 |   subroutine mysub1()
+9 9 |     write (*,*) 'hello world'
+
+./resources/test/fixtures/style/S061.f90:11:3: S061 [*] end statement should be named.
+   |
+ 9 |     write (*,*) 'hello world'
+10 |     ! catch this
+11 |   end subroutine
+   |   ^^^^^^^^^^^^^^ S061
+12 |   subroutine mysub2()
+13 |     write (*,*) 'hello world'
+14 |     ! ignore this
    |
    = help: Write as 'end subroutine mysub1'.
 
 ℹ Safe fix
-6  6  | contains
-7  7  |   subroutine mysub1()
-8  8  |     write (*,*) 'hello world'
-9     |-  end subroutine                ! catch this
-   9  |+  end subroutine mysub1! catch this
-10 10 |   subroutine mysub2()
-11 11 |     write (*,*) 'hello world'
-12 12 |   end subroutine mysub2         ! ignore this
-
-./resources/test/fixtures/style/S061.f90:13:1: S061 [*] end statement should be named.
-   |
-11 |     write (*,*) 'hello world'
-12 |   end subroutine mysub2         ! ignore this
-13 | end                             ! catch this
-   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
-14 | module mymod2
-15 |   implicit none
+8  8  |   subroutine mysub1()
+9  9  |     write (*,*) 'hello world'
+10 10 |     ! catch this
+11    |-  end subroutine
+   11 |+  end subroutine mysub1
+12 12 |   subroutine mysub2()
+13 13 |     write (*,*) 'hello world'
+14 14 |     ! ignore this
+
+./resources/test/fixtures/style/S061.f90:17:1: S061 [*] end statement should be named.
+   |
+15 |   end subroutine mysub2
+16 |   ! catch this
+17 | end
+   | ^^^ S061
+18 | module mymod2
+19 |   implicit none
+20 |   type mytype
    |
    = help: Write as 'end module mymod1'.
 
 ℹ Safe fix
-10 10 |   subroutine mysub2()
-11 11 |     write (*,*) 'hello world'
-12 12 |   end subroutine mysub2         ! ignore this
-13    |-end                             ! catch this
-   13 |+end module mymod1! catch this
-14 14 | module mymod2
-15 15 |   implicit none
-16 16 |   type mytype
-
-./resources/test/fixtures/style/S061.f90:22:3: S061 [*] end statement should be named.
-   |
-20 |   integer function myfunc1()
-21 |     myfunc1 = 1
-22 |   end function                  ! catch this
-   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
-23 |   integer function myfunc2()
-24 |     myfunc2 = 1
+14 14 |     ! ignore this
+15 15 |   end subroutine mysub2
+16 16 |   ! catch this
+17    |-end
+   17 |+end module mymod1
+18 18 | module mymod2
+19 19 |   implicit none
+20 20 |   type mytype
+
+./resources/test/fixtures/style/S061.f90:28:3: S061 [*] end statement should be named.
+   |
+26 |     myfunc1 = 1
+27 |     ! catch this
+28 |   end function
+   |   ^^^^^^^^^^^^ S061
+29 |   integer function myfunc2()
+30 |     myfunc2 = 1
+31 |     ! ignore this
    |
    = help: Write as 'end function myfunc1'.
 
 ℹ Safe fix
-19 19 | contains
-20 20 |   integer function myfunc1()
-21 21 |     myfunc1 = 1
-22    |-  end function                  ! catch this
-   22 |+  end function myfunc1! catch this
-23 23 |   integer function myfunc2()
-24 24 |     myfunc2 = 1
-25 25 |   end function myfunc2          ! ignore this
-
-./resources/test/fixtures/style/S061.f90:26:1: S061 [*] end statement should be named.
-   |
-24 |     myfunc2 = 1
-25 |   end function myfunc2          ! ignore this
-26 | end module                      ! catch this
-   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
-27 | module mymod3
-28 |   interface
+25 25 |   integer function myfunc1()
+26 26 |     myfunc1 = 1
+27 27 |     ! catch this
+28    |-  end function
+   28 |+  end function myfunc1
+29 29 |   integer function myfunc2()
+30 30 |     myfunc2 = 1
+31 31 |     ! ignore this
+
+./resources/test/fixtures/style/S061.f90:34:1: S061 [*] end statement should be named.
+   |
+32 |   end function myfunc2
+33 |   ! catch this
+34 | end module
+   | ^^^^^^^^^^ S061
+35 | module mymod3
+36 |   interface
+37 |     module function foo() result(x)
    |
    = help: Write as 'end module mymod2'.
 
 ℹ Safe fix
-23 23 |   integer function myfunc2()
-24 24 |     myfunc2 = 1
-25 25 |   end function myfunc2          ! ignore this
-26    |-end module                      ! catch this
-   26 |+end module mymod2! catch this
-27 27 | module mymod3
-28 28 |   interface
-29 29 |     module function foo() result(x)
-
-./resources/test/fixtures/style/S061.f90:44:3: S061 [*] end statement should be named.
-   |
-42 |   module procedure foo
-43 |     x = 1
-44 |   end procedure                 ! catch this
-   |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
-45 | end                             ! catch this
-46 | submodule (mymod3) mysub2
+31 31 |     ! ignore this
+32 32 |   end function myfunc2
+33 33 |   ! catch this
+34    |-end module
+   34 |+end module mymod2
+35 35 | module mymod3
+36 36 |   interface
+37 37 |     module function foo() result(x)
+
+./resources/test/fixtures/style/S061.f90:56:3: S061 [*] end statement should be named.
+   |
+54 |     x = 1
+55 |     ! catch this
+56 |   end procedure
+   |   ^^^^^^^^^^^^^ S061
+57 |   ! catch this
+58 | end
+59 | submodule (mymod3) mysub2
    |
    = help: Write as 'end procedure foo'.
 
 ℹ Safe fix
-41 41 | contains
-42 42 |   module procedure foo
-43 43 |     x = 1
-44    |-  end procedure                 ! catch this
-   44 |+  end procedure foo! catch this
-45 45 | end                             ! catch this
-46 46 | submodule (mymod3) mysub2
-47 47 | contains
-
-./resources/test/fixtures/style/S061.f90:45:1: S061 [*] end statement should be named.
-   |
-43 |     x = 1
-44 |   end procedure                 ! catch this
-45 | end                             ! catch this
-   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
-46 | submodule (mymod3) mysub2
-47 | contains
+53 53 |   module procedure foo
+54 54 |     x = 1
+55 55 |     ! catch this
+56    |-  end procedure
+   56 |+  end procedure foo
+57 57 |   ! catch this
+58 58 | end
+59 59 | submodule (mymod3) mysub2
+
+./resources/test/fixtures/style/S061.f90:58:1: S061 [*] end statement should be named.
+   |
+56 |   end procedure
+57 |   ! catch this
+58 | end
+   | ^^^ S061
+59 | submodule (mymod3) mysub2
+60 | contains
+61 |   module procedure bar
    |
    = help: Write as 'end submodule mysub1'.
 
 ℹ Safe fix
-42 42 |   module procedure foo
-43 43 |     x = 1
-44 44 |   end procedure                 ! catch this
-45    |-end                             ! catch this
-   45 |+end submodule mysub1! catch this
-46 46 | submodule (mymod3) mysub2
-47 47 | contains
-48 48 |   module procedure bar
-
-./resources/test/fixtures/style/S061.f90:51:1: S061 [*] end statement should be named.
-   |
-49 |     x = 1
-50 |   end procedure bar             ! ignore this
-51 | end submodule                   ! catch this
-   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
-52 | submodule (mymod3) mysub3
-53 | contains
+55 55 |     ! catch this
+56 56 |   end procedure
+57 57 |   ! catch this
+58    |-end
+   58 |+end submodule mysub1
+59 59 | submodule (mymod3) mysub2
+60 60 | contains
+61 61 |   module procedure bar
+
+./resources/test/fixtures/style/S061.f90:66:1: S061 [*] end statement should be named.
+   |
+64 |   end procedure bar
+65 |   ! catch this
+66 | end submodule
+   | ^^^^^^^^^^^^^ S061
+67 | submodule (mymod3) mysub3
+68 | contains
+69 |   module procedure baz
    |
    = help: Write as 'end submodule mysub2'.
 
 ℹ Safe fix
-48 48 |   module procedure bar
-49 49 |     x = 1
-50 50 |   end procedure bar             ! ignore this
-51    |-end submodule                   ! catch this
-   51 |+end submodule mysub2! catch this
-52 52 | submodule (mymod3) mysub3
-53 53 | contains
-54 54 |   module procedure baz
-
-./resources/test/fixtures/style/S061.f90:61:1: S061 [*] end statement should be named.
-   |
-59 |   implicit none
-60 |   write (*,*) 'hello world'
-61 | end                             ! catch this
-   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S061
+63 63 |     ! ignore this
+64 64 |   end procedure bar
+65 65 |   ! catch this
+66    |-end submodule
+   66 |+end submodule mysub2
+67 67 | submodule (mymod3) mysub3
+68 68 | contains
+69 69 |   module procedure baz
+
+./resources/test/fixtures/style/S061.f90:79:1: S061 [*] end statement should be named.
+   |
+77 |   write (*,*) 'hello world'
+78 |   ! catch this
+79 | end
+   | ^^^ S061
    |
    = help: Write as 'end program myprog'.
 
 ℹ Safe fix
-58 58 | program myprog
-59 59 |   implicit none
-60 60 |   write (*,*) 'hello world'
-61    |-end                             ! catch this
-   61 |+end program myprog! catch this
+76 76 |   implicit none
+77 77 |   write (*,*) 'hello world'
+78 78 |   ! catch this
+79    |-end
+   79 |+end program myprog