Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/clanguru/compilation_options_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def get_compile_options(self) -> list[str]:
return self.clean_up_arguments(options)

def get_includes_and_defines(self) -> list[str]:
return _filter_includes_and_defines(self.get_compile_options())
return _filter_preprocessor_options(self.get_compile_options())

def get_file_path(self) -> Path:
return self.file if self.file.is_absolute() else self.directory / self.file
Expand Down Expand Up @@ -151,25 +151,29 @@ def get_includes_and_defines(self, file: Path) -> list[str]:
commands: list[CompileCommand] = self.compilation_database.get_compile_commands(file)
if commands:
return commands[0].get_includes_and_defines()
return [] if self.no_default else _filter_includes_and_defines(self.default_options)
return [] if self.no_default else _filter_preprocessor_options(self.default_options)

def set_default_options(self, options: list[str]) -> None:
self.default_options = options


def _filter_includes_and_defines(options: list[str]) -> list[str]:
"""Keep only -I and -D flags (including their values when passed as separate arguments)."""
#: Options that decide what the preprocessor sees: include paths, macros and forced includes.
PREPROCESSOR_OPTIONS = ("-I", "-D", "-U", "-isystem", "-iquote", "-idirafter", "-include", "-imacros")


def _filter_preprocessor_options(options: list[str]) -> list[str]:
"""Keep only the preprocessor options (including their values when passed as separate arguments)."""
filtered: list[str] = []
take_next = False
for option in options:
if take_next:
filtered.append(option)
take_next = False
continue
if option in ("-I", "-D"):
if option in PREPROCESSOR_OPTIONS:
filtered.append(option)
take_next = True
elif option.startswith(("-I", "-D")):
elif option.startswith(PREPROCESSOR_OPTIONS):
filtered.append(option)
return filtered

Expand Down
12 changes: 9 additions & 3 deletions src/clanguru/doc_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from jinja2 import Environment, StrictUndefined, TemplateError, UndefinedError, select_autoescape
from py_app_dev.core.exceptions import UserNotificationException

from clanguru.cparser import CLangParser, Token, TranslationUnit
from clanguru.cparser import CLangParser, Declaration, Token, TranslationUnit

GTEST_MACROS = ("TEST", "TEST_P", "TEST_F", "TYPED_TEST", "TYPED_TEST_P")
_GTEST_DECL_RE = re.compile(rf"^\s*(?:{'|'.join(GTEST_MACROS)})\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)")
Expand Down Expand Up @@ -350,6 +350,12 @@ def _build_declaration_section(name: str, description_tokens: list[Token], body_
return section


def _is_declared_in(declaration: Declaration, translation_unit: TranslationUnit) -> bool:
"""Whether the declaration is located in the translation unit's own source file, not in an included header."""
location_file = declaration.origin.raw_node.location.file
return location_file is not None and Path(location_file.name).resolve() == translation_unit.source_file.resolve()


def generate_doc_structure(translation_unit: TranslationUnit, docs_format: DocsFormat = DocsFormat.md) -> DocStructure:
"""
Generate documentation structure from a translation unit.
Expand All @@ -360,14 +366,14 @@ def generate_doc_structure(translation_unit: TranslationUnit, docs_format: DocsF
tags = [docs_format.format_tag, "docs"]

doc = DocStructure(translation_unit.source_file.name)
functions = [f for f in CLangParser.get_functions(translation_unit) if f.is_definition]
functions = [f for f in CLangParser.get_functions(translation_unit) if f.is_definition and _is_declared_in(f, translation_unit)]
if functions:
functions_section = Section("Functions")
doc.add_section(functions_section)
for func in functions:
functions_section.add_subsection(_build_declaration_section(func.name, func.description_tokens, func.body.content, func.body.start_line, tags))

classes = CLangParser.get_classes(translation_unit)
classes = [c for c in CLangParser.get_classes(translation_unit) if _is_declared_in(c, translation_unit)]
if classes:
classes_section = Section("Classes")
doc.add_section(classes_section)
Expand Down
9 changes: 0 additions & 9 deletions tests/data/test_gtest.cc.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,6 @@

## Functions

### RUN_ALL_TESTS

```{code-block} c
:linenos:
:lineno-start: 2334

inline int RUN_ALL_TESTS() { return ::testing::UnitTest::GetInstance()->Run(); }
```

### gtest_BlinkPeriodTestsBlinkPeriodTest_EvalGenerator_

```{code-block} c
Expand Down
5 changes: 5 additions & 0 deletions tests/test_compilation_options_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ def test_clean_up_arguments_with_partial_paths(compile_command):
[],
id="no_includes_or_defines",
),
pytest.param(
["gcc", "-isystem", "/sys/inc", "-iquote/quoted", "-idirafter", "/after", "-include", "forced.h", "-imacros/macros.h", "-UNDEBUG", "-c", "/home/user/project/input.c"],
["-isystem", "/sys/inc", "-iquote/quoted", "-idirafter", "/after", "-include", "forced.h", "-imacros/macros.h", "-UNDEBUG"],
id="system_includes_forced_includes_and_undefines",
),
],
)
def test_get_includes_and_defines(compile_command: CompileCommand, arguments: list[str], expected: list[str]) -> None:
Expand Down
32 changes: 32 additions & 0 deletions tests/test_doc_generator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from pathlib import Path
from textwrap import dedent

Expand Down Expand Up @@ -320,6 +321,37 @@ def test_doc_structure_for_gtest_files(tmp_path: Path, gtest_include_path: Path)
assert output_file.read_text() == expected_output


def test_doc_structure_lists_only_the_declarations_of_the_source_file(tmp_path: Path) -> None:
(tmp_path / "helper.h").write_text("static inline int helper(void) { return 1; }\nclass HeaderClass {};\n", newline="\n")
source_file = tmp_path / "source.cc"
source_file.write_text('#include "helper.h"\nint own_function() { return helper(); }\nclass OwnClass {};\n', newline="\n")
compile_db = make_compile_commands(tmp_path, source_file, [tmp_path])

doc_structure = generate_doc_structure(CLangParser().load(source_file, CompilationOptionsManager(compile_db)))

assert {section.title: [sub.title for sub in section.subsections] for section in doc_structure.sections} == {
"Functions": ["own_function"],
"Classes": ["OwnClass"],
}


def test_doc_structure_follows_the_branches_of_a_system_include(tmp_path: Path) -> None:
system_dir = tmp_path / "system"
system_dir.mkdir()
(system_dir / "product_features.h").write_text("#define FEATURE 1\n", newline="\n")
source_file = tmp_path / "source.c"
source_file.write_text(
"#include <product_features.h>\n#ifdef FEATURE\nint with_feature(void) { return 1; }\n#else\nint without_feature(void) { return 0; }\n#endif\n",
newline="\n",
)
compile_db = tmp_path / "compile_commands.json"
compile_db.write_text(json.dumps([{"directory": str(tmp_path), "file": str(source_file), "command": f"gcc -isystem {system_dir} -c {source_file}"}]))

doc_structure = generate_doc_structure(CLangParser().load(source_file, CompilationOptionsManager(compile_db)))

assert [sub.title for sub in doc_structure.sections[0].subsections] == ["with_feature"]


def test_generate_documentation(c_source: TranslationUnit, tmp_path: Path) -> None:
# Generate Markdown documentation
md_file = tmp_path / "test.md"
Expand Down