Skip to content
Draft
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
87 changes: 84 additions & 3 deletions beman_tidy/lib/checks/beman_standard/cpp.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,94 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

# [cpp.*] checks category.
from beman_tidy.lib.checks.base.file_base_check import FileBaseCheck, BatchFileBaseCheck
from beman_tidy.lib.checks.system.registry import register_beman_standard_check
from beman_tidy.lib.utils.file import get_beman_include_headers

@register_beman_standard_check("cpp.namespace")
class CppNamespaceCheck(BatchFileBaseCheck):
"""
[cpp.namespace]
Recommendation: Headers in include/beman/<short_name>/ should export entities in the beman::<short_name> namespace.
"""

# TODO cpp.namespace
def __init__(self, repo_info, beman_standard_check_config):
super().__init__(repo_info, beman_standard_check_config)
self.file_check_class = self.CppNamespaceCheckImpl
self.file_path_generator = get_beman_include_headers

class CppNamespaceCheckImpl(FileBaseCheck):
"""
Implementation of the "cpp.namespace" check for a single file.
"""
# TODO: Implement a way to detect an existing namespace that doesnt wrap the entire code
# with something like abstract syntax trees(?) via libclang / just search for '{' and '}'
# current functionality: passes any file containing the right namespace
def __init__(self, repo_info, beman_standard_check_config, relative_path):
super().__init__(repo_info, beman_standard_check_config, relative_path, name="cpp.namespace")
self.short_name = ""

# TODO cpp.no_flag_forking
def _get_code_body_indices(self, lines):
"""
Finds the start and end indices of the main code body, excluding headers and footers.
"""
start_index = 0
for i, line in enumerate(lines):
if line.strip().startswith(('#include', '#define')):
start_index = i + 1

end_index = len(lines)
for i in range(len(lines) - 1, -1, -1):
if lines[i].strip().startswith('#endif'):
end_index = i
break
return start_index, end_index

def check(self):
parts = self.path.parts
include_index = parts.index('include')
self.short_name = parts[include_index + 2]
expected_namespace = f"namespace beman::{self.short_name}"
lines = self.read_lines()

code_start_index, code_end_index = self._get_code_body_indices(lines)
has_found_namespace = False
for i in range(code_start_index, code_end_index):
line = lines[i].strip()
if not line or line.startswith('//'):
continue
if expected_namespace in line:
has_found_namespace = True
break
self.log(f"Code found outside of expected namespace '{expected_namespace}' at line {i+1}.")
return False

if not has_found_namespace:
self.log(f"File does not contain the expected namespace '{expected_namespace}'.")
return False

return True

def fix(self):
parts = self.path.parts
include_index = parts.index('include')
self.short_name = parts[include_index + 2]
lines = self.read_lines()
insert_line, close_line = self._get_code_body_indices(lines)

new_lines = lines[:insert_line]
if insert_line > 0 and lines[insert_line - 1].strip():
new_lines.append("") # blank line for style
new_lines.append(f"namespace beman::{self.short_name} {{")
new_lines.extend(lines[insert_line:close_line])
new_lines.append(f"}} // namespace beman::{self.short_name}")
if close_line < len(lines) and lines[close_line].strip():
new_lines.append("") # blank line for style
new_lines.extend(lines[close_line:])

self.write_lines(new_lines)
return True

# TODO cpp.no_flag_forking

# TODO cpp.extension_identifiers
35 changes: 34 additions & 1 deletion beman_tidy/lib/utils/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,25 @@ def get_repo_ignorable_subdirectories():
return {".git", "build", ".idea", ".vscode", "__pycache__", "venv", "env"}


def get_cpp_header_extensions():
"""
Returns a set of common C++ header file extensions.
"""
return {".hpp", ".h", ".hxx", ".hh"}


def get_cpp_source_extensions():
"""
Returns a set of common C++ source file extensions.
"""
return {".cpp", ".cxx", ".cc", ".c"}


def get_cpp_extensions():
"""
Returns a set of common C++ source and header file extensions.
"""
return {".hpp", ".h", ".hxx", ".hh", ".cpp", ".cxx", ".cc", ".c"}
return get_cpp_header_extensions() | get_cpp_source_extensions()


def get_matched_paths(repo_path, extensions, exclude_dirs=None):
Expand Down Expand Up @@ -56,6 +70,25 @@ def get_cpp_files(repo_path):
return get_matched_paths(repo_path, get_cpp_extensions())


def get_beman_include_headers(repo_path):
"""
Get all header files in the repository under an include/beman directory.
"""
all_headers = get_matched_paths(repo_path, get_cpp_header_extensions())

beman_headers = []
for path in all_headers:
try:
parts = path.parts
include_index = parts.index('include')
if include_index + 1 < len(parts) and parts[include_index + 1] == 'beman':
beman_headers.append(path)
except ValueError:
continue

return beman_headers


def get_spdx_info(lines):
"""
Helper to find the SPDX line index and the comment prefix.
Expand Down
Empty file.
16 changes: 16 additions & 0 deletions tests/lib/checks/beman_standard/cpp/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

import pytest

from tests.utils.conftest import mock_repo_info, mock_beman_standard_check_config # noqa: F401


@pytest.fixture(autouse=True)
def repo_info(mock_repo_info): # noqa: F811
return mock_repo_info


@pytest.fixture
def beman_standard_check_config(mock_beman_standard_check_config): # noqa: F811
return mock_beman_standard_check_config
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#ifndef INCLUDED_BEMAN_MY_LIB_MY_LIB_HPP
#define INCLUDED_BEMAN_MY_LIB_MY_LIB_HPP

#include <vector>

class my_class {};

#endif // INCLUDED_BEMAN_MY_LIB_MY_LIB_HPP
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#ifndef INCLUDED_BEMAN_MY_LIB_MY_LIB_HPP
#define INCLUDED_BEMAN_MY_LIB_MY_LIB_HPP

#include <vector>

namespace beman::my_lib {

class my_class {};

} // namespace beman::my_lib

#endif // INCLUDED_BEMAN_MY_LIB_MY_LIB_HPP
39 changes: 39 additions & 0 deletions tests/lib/checks/beman_standard/cpp/test_cpp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

import shutil
from pathlib import Path

from beman_tidy.lib.checks.beman_standard.cpp import CppNamespaceCheck

test_data_prefix = Path("tests/lib/checks/beman_standard/cpp/namespace")
valid_prefix = test_data_prefix / "valid"
invalid_prefix = test_data_prefix / "invalid"

def test__cpp_namespace__valid(repo_info, beman_standard_check_config):
repo_info["top_level"] = valid_prefix

check = CppNamespaceCheck(repo_info, beman_standard_check_config)
assert check.check() is True

def test__cpp_namespace__invalid(repo_info, beman_standard_check_config):
repo_info["top_level"] = invalid_prefix

check = CppNamespaceCheck(repo_info, beman_standard_check_config)
assert check.check() is False

def test__cpp_namespace__fix_inplace(repo_info, beman_standard_check_config, tmp_path):
for path in invalid_prefix.rglob('*'):
if path.is_file():
dest_path = tmp_path / path.relative_to(invalid_prefix)
dest_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(path, dest_path)

repo_info["top_level"] = tmp_path
check = CppNamespaceCheck(repo_info, beman_standard_check_config)

assert check.check() is False

assert check.fix() is True

assert check.check() is True