Repository: Viatorus/quom

Issue:
when source files have no newline at EOF, content of next file is concatenated right after last closing curly bracket
When one of source files has no newline at EOF, the "#include ..."s of the next file appears right after the last closing curly bracket and that causes a compilation error. 
I know it's a trivial problem to resolve when compiling fails but it would be nice to ensure the content of all source files are separated by newline.

```c++
// bad.cpp
void foo(){
// there is no newline at EOF, file ends right after this closing curly bracket.
}

// b.cpp
#include <iostream>
// this include is at first line and located outside current build directory

// main.cpp

include "bad.hpp"
include "b.hpp"
...
```
And the source file quom produces has something like:
```c++
void foo(){
}#include <iostream> // this include comes from b.cpp
```

I wrote a test to demonstrate this problem. The following test will fail at current commit which is ```4633b5aa```.

---- 
```python
from io import StringIO
from pathlib import Path

from quom import Quom

FILE_MAIN_CPP = """\
#include "a.hpp"
#include "b.hpp"

int main() {

  return 0;
}
"""

FILE_A_HPP = """\
#ifndef A_HPP_
#define A_HPP_

#endif //A_HPP_
"""

FILE_A_CPP = """\
#include "a.hpp"
#include "b.hpp"

int mid() {

}"""

FILE_B_HPP = """\
#ifndef B_HPP_
#define B_HPP_

#endif //B_HPP_
"""

FILE_B_CPP = """\
#include <memory>
"""

RESULT = """\
#ifndef A_HPP_
#define A_HPP_

#endif //A_HPP_

#ifndef B_HPP_
#define B_HPP_

#endif //B_HPP_

int main() {

  return 0;
}

void mid() {

}
#include <memory>
"""


def init():
    with open('main.hpp', 'w+', encoding='utf-8') as file:
        file.write(FILE_MAIN_CPP)

    with open('a.hpp', 'w+', encoding='utf-8') as file:
        file.write(FILE_A_HPP)

    with open('a.cpp', 'w+', encoding='utf-8') as file:
        file.write(FILE_A_CPP)

    with open('b.hpp', 'w+', encoding='utf-8') as file:
        file.write(FILE_B_HPP)

    with open('b.cpp', 'w+', encoding='utf-8') as file:
        file.write(FILE_B_CPP)


def test_issue_one(fs):
    init()

    dst = StringIO()
    Quom(Path('main.hpp'), dst)

    assert dst.getvalue() == RESULT
```

Comments:

Patch:
diff --git a/src/quom/quom.py b/src/quom/quom.py
index 3cf4e13..0f6f7cd 100644
--- a/src/quom/quom.py
+++ b/src/quom/quom.py
@@ -1,3 +1,4 @@
+import os
 import re
 from pathlib import Path
 from queue import Queue
@@ -46,6 +47,7 @@ class Quom:
         self.__processed_files = set()
         self.__source_files = Queue()
         self.__cont_lb = CONTINUOUS_LINE_BREAK_START
+        self.__used_linesep = None
         self.__prev_token = EmptyToken()
 
         self.__process_file(Path(), src_file_path, False, True)
@@ -56,12 +58,7 @@ class Quom:
                                 .format(stitch_format))
             while not self.__source_files.empty():
                 self.__process_file(Path(), self.__source_files.get(), True)
-            # Write last token.
-            self.__write_token(self.__prev_token, True)
-        elif self.__cont_lb == CONTINUOUS_LINE_BREAK_START or not isinstance(self.__prev_token,
-                                                                             LinebreakWhitespaceToken):
-            # Write last token, if not a continuous line break.
-            self.__write_token(self.__prev_token, True)
+                self.__write_line_break_if_missing()
 
     def __process_file(self, relative_path: Path, include_path: Path, is_source_file: bool,
                        is_main_header=False):
@@ -109,9 +106,8 @@ class Quom:
         if self.__is_cont_line_break(token):
             return
 
-        # Write previous token, store current.
-        if self.__prev_token:
-            self.__dst.write(str(self.__prev_token.raw))
+        # Write token and store.
+        self.__dst.write(str(token.raw))
         self.__prev_token = token
 
     @staticmethod
@@ -171,10 +167,15 @@ class Quom:
 
         while not self.__source_files.empty():
             self.__process_file(Path(), self.__source_files.get(), True)
+            self.__write_line_break_if_missing()
 
         return True
 
     def __is_cont_line_break(self, token: Token) -> bool:
+        # Save a used line break for later.
+        if self.__used_linesep is None and isinstance(token, LinebreakWhitespaceToken):
+            self.__used_linesep = token.raw
+
         if not self.__trim:
             return False
 
@@ -187,3 +188,9 @@ class Quom:
             self.__cont_lb = CONTINUOUS_LINE_BREAK_START
 
         return self.__cont_lb >= CONTINUOUS_BREAK_REACHED
+
+    def __write_line_break_if_missing(self):
+        if not isinstance(self.__prev_token, LinebreakWhitespaceToken):
+            if self.__used_linesep is None:
+                self.__used_linesep = os.linesep  # fallback
+            self.__dst.write(self.__used_linesep)


Test patch:
diff --git a/tests/test_quom/test_last_source_file.py b/tests/test_quom/test_last_source_file.py
index c227c21..7f148d2 100644
--- a/tests/test_quom/test_last_source_file.py
+++ b/tests/test_quom/test_last_source_file.py
@@ -53,7 +53,8 @@ extern int foo;
 
 #include <algorithm>
 
-int foo = 42;"""
+int foo = 42;
+"""
 
 RESULT_NORMAL_WITHOUT_TRIM = """\
 #pragma once
@@ -78,7 +79,8 @@ extern int foo;
 
 #include <algorithm>
 
-int foo = 42;"""
+int foo = 42;
+"""
 
 
 def init():
diff --git a/tests/test_quom/test_line_breaks_when_stitching.py b/tests/test_quom/test_line_breaks_when_stitching.py
new file mode 100644
index 0000000..0bb6c37
--- /dev/null
+++ b/tests/test_quom/test_line_breaks_when_stitching.py
@@ -0,0 +1,98 @@
+from io import StringIO
+from pathlib import Path
+
+from quom import Quom
+
+FILE_MAIN_CPP = """\
+#include "a.hpp"
+int main() {
+    return 0;
+}
+// Stitch Begin
+// End
+"""
+
+FILE_A_HPP = 'int a;'
+
+FILE_A_CPP = """\
+#include "b.hpp"
+#include "c.hpp"
+void mid() {}"""
+
+FILE_B_HPP = 'int b;'
+FILE_C_HPP = 'int c;'
+
+FILE_B_CPP = """\
+#include <b>"""
+
+FILE_C_CPP = """\
+#include <c>"""
+
+RESULT = """\
+int a;
+int main() {
+    return 0;
+}
+// Stitch Begin
+// End
+int b;
+int c;
+void mid() {}
+#include <b>
+#include <c>
+"""
+
+RESULT_STITCH = """\
+int a;
+int main() {
+    return 0;
+}
+int b;
+int c;
+void mid() {}
+#include <b>
+#include <c>
+
+// End
+"""
+
+
+def init():
+    with open('main.hpp', 'w+', encoding='utf-8') as file:
+        file.write(FILE_MAIN_CPP)
+
+    with open('a.hpp', 'w+', encoding='utf-8') as file:
+        file.write(FILE_A_HPP)
+
+    with open('a.cpp', 'w+', encoding='utf-8') as file:
+        file.write(FILE_A_CPP)
+
+    with open('b.hpp', 'w+', encoding='utf-8') as file:
+        file.write(FILE_B_HPP)
+
+    with open('b.cpp', 'w+', encoding='utf-8') as file:
+        file.write(FILE_B_CPP)
+
+    with open('c.hpp', 'w+', encoding='utf-8') as file:
+        file.write(FILE_C_HPP)
+
+    with open('c.cpp', 'w+', encoding='utf-8') as file:
+        file.write(FILE_C_CPP)
+
+
+def test_add_line_break_in_stitched_files_if_missing(fs):
+    init()
+
+    dst = StringIO()
+    Quom(Path('main.hpp'), dst)
+
+    assert dst.getvalue() == RESULT
+
+
+def test_add_line_break_in_stitched_files_if_missing_at_stitch_location(fs):
+    init()
+
+    dst = StringIO()
+    Quom(Path('main.hpp'), dst, stitch_format='Stitch Begin')
+
+    assert dst.getvalue() == RESULT_STITCH