From ff900a00d869e44a1930fad0cbd87b292cdac3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imanol=20Guti=C3=A9rrez?= Date: Sat, 12 Sep 2026 16:28:35 +0300 Subject: [PATCH 1/2] Add a newline char at EOF on C and C++ headers --- graphify/extractors/engine.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 16109a7770..aeec2574aa 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -3177,6 +3177,11 @@ def _extract_generic( try: parser = Parser(language) source = path.read_bytes() if source_override is None else source_override + # In C and C++, if the .h file does not end with a newline '\n' an error + # is throwed even if the file is valid. In order to avoid this, a new line + # char is added only if the original file does not end with it. + if source and not source.endswith(b"\n"): + source = source + b"\n" tree = parser.parse(source) root = tree.root_node except Exception as e: From 543c171ec117d11347f64bd70e0fa7168b10c785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imanol=20Guti=C3=A9rrez?= Date: Sat, 12 Sep 2026 16:28:53 +0300 Subject: [PATCH 2/2] Add tests for newline char at EOF fix --- ...est_trailing_newline_not_a_syntax_error.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/test_trailing_newline_not_a_syntax_error.py diff --git a/tests/test_trailing_newline_not_a_syntax_error.py b/tests/test_trailing_newline_not_a_syntax_error.py new file mode 100644 index 0000000000..3140bcda7b --- /dev/null +++ b/tests/test_trailing_newline_not_a_syntax_error.py @@ -0,0 +1,70 @@ +"""Tests for files that do not end with a newline. + +In C and C++, if the .h file does not end with a newline '\n' an error +is throwed even if the file is valid. In order to avoid this, a new line +char is added only if the original file does not end with it. + +In this test we will make sure this works. +""" + +from graphify.extract import extract, extract_c, extract_cpp + +TEST_HEADER_1 = """\ +#define A 8 +#define AA 3""" + +TEST_HEADER_2 = """\ +#define A 8 + +int foo(int bar) { return bar; } + +int foo2(void) { return 0; } + +#define AA 1""" + +TEST_HEADER_3 = """\ +#pragma once + +#define A 4""" + + +def write(path, text): + path.write_bytes(text.encode("utf-8")) + return path + + +def stderr_of(tmp_path, path, capsys): + extract([path], root=tmp_path) + return capsys.readouterr().err + + +def test_c_header_without_newline(tmp_path, capsys): + path = write(tmp_path / "test_header.h", TEST_HEADER_1) + assert "partially extracted" not in stderr_of(tmp_path, path, capsys) + + +def test_cpp_header_without_newline(tmp_path, capsys): + path = write(tmp_path / "test_header.hpp", TEST_HEADER_3) + assert "partially extracted" not in stderr_of(tmp_path, path, capsys) + + +def test_c_header_with_newline(tmp_path, capsys): + path = write(tmp_path / "test_header.h", TEST_HEADER_1 + "\n") + assert "partially extracted" not in stderr_of(tmp_path, path, capsys) + + +def test_no_parse_errors(tmp_path): + assert extract_c(write(tmp_path / "test_header.h", TEST_HEADER_1)).get("parse_errors") is None + assert extract_cpp(write(tmp_path / "test_header.hpp", TEST_HEADER_3)).get("parse_errors") is None + + +def test_functions_are_still_found(tmp_path): + path = write(tmp_path / "test_header.h", TEST_HEADER_2) + labels = [node["label"] for node in extract_c(path)["nodes"]] + assert "foo()" in labels + assert "foo2()" in labels + + +def test_empty_file(tmp_path): + path = write(tmp_path / "test_header.c", "") + assert extract_c(path).get("parse_errors") is None