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
87 changes: 79 additions & 8 deletions src/plugins/cpp_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,83 @@ def _run_clang_format(command: List[str]) -> None:
raise click.ClickException(message)


def _read_cpp_raw_string(content: str, start: int) -> Tuple[str, int] | None:
"""Read a C++ raw string literal starting at or near start, if present."""
for prefix in ("u8R", "LR", "uR", "UR", "R"):
if not content.startswith(prefix + '"', start):
continue

delimiter_start = start + len(prefix) + 1
paren_index = content.find("(", delimiter_start)
if paren_index == -1:
return None

delimiter = content[delimiter_start:paren_index]
terminator = ")" + delimiter + '"'
terminator_index = content.find(terminator, paren_index + 1)
if terminator_index == -1:
return content[start:], len(content)

end = terminator_index + len(terminator)
return content[start:end], end

return None


def _strip_cpp_comments(content: str) -> str:
"""Remove C++ comments while preserving string, char, and raw string literals."""
result: List[str] = []
i = 0

while i < len(content):
raw_string = _read_cpp_raw_string(content, i)
if raw_string is not None:
literal, i = raw_string
result.append(literal)
continue

current = content[i]
next_char = content[i + 1] if i + 1 < len(content) else ""

if current in {'"', "'"}:
quote = current
literal_start = i
i += 1
while i < len(content):
if content[i] == "\\":
i += 2
continue
if content[i] == quote:
i += 1
break
i += 1
result.append(content[literal_start:i])
continue

if current == "/" and next_char == "/":
i += 2
while i < len(content) and content[i] != "\n":
i += 1
if i < len(content):
result.append("\n")
i += 1
continue

if current == "/" and next_char == "*":
i += 2
while i + 1 < len(content) and not (content[i] == "*" and content[i + 1] == "/"):
if content[i] == "\n":
result.append("\n")
i += 1
i = i + 2 if i + 1 < len(content) else len(content)
continue

result.append(current)
i += 1

return "".join(result)


class CppPreprocessor(BasePreprocessor):
def preprocess(self, file_path: str, include_paths: List[str], defines: Dict[str, str] = None) -> str:
"""
Expand Down Expand Up @@ -93,9 +170,7 @@ def preprocess(self, file_path: str, include_paths: List[str], defines: Dict[str
if defines:
processed_content = self._inject_tunable_params(processed_content, defines)

# Remove comments using regex
processed_content = re.sub(r'//.*\n', '\n', processed_content) # Single-line comments
processed_content = re.sub(r'/\*.*?\*/', '', processed_content, flags=re.DOTALL) # Multi-line comments
processed_content = _strip_cpp_comments(processed_content)

# Check for prepkit_config.yaml for minification setting
config_file_path: str = os.path.join(os.getcwd(), "prepkit_config.yaml")
Expand Down Expand Up @@ -242,11 +317,7 @@ def minify(self, file_path: str) -> str:
with open(temp_file_path, "r") as f:
minified_output = f.read()

# Remove comments using regex (basic approach)
# Remove single-line comments
minified_output = re.sub(r'//.*$', '', minified_output, flags=re.MULTILINE)
# Remove multi-line comments
minified_output = re.sub(r'/\*.*?\*/', '', minified_output, flags=re.DOTALL)
minified_output = _strip_cpp_comments(minified_output)

# Moderate minification that preserves compilation compatibility
# Remove extra whitespace but keep necessary structure
Expand Down
53 changes: 52 additions & 1 deletion tests/test_cpp_preprocessor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest
import os
from plugins.cpp_plugin import CppPreprocessor, CppMinifier
from plugins.cpp_plugin import CppPreprocessor, CppMinifier, _strip_cpp_comments

@pytest.fixture
def cpp_preprocessor():
Expand Down Expand Up @@ -57,6 +57,38 @@ def test_cpp_preprocess_comments(cpp_preprocessor, temp_files):
assert "* comment */" not in output
assert "int main() { return 0; }" in output

def test_strip_cpp_comments_preserves_string_and_char_literals():
code = r'''
std::string url = "https://example.com/path";
std::string marker = "/* not a comment */";
char slash = '/';
int value = 1; // real comment
/* block comment */
int next = 2;
'''

output = _strip_cpp_comments(code)

assert '"https://example.com/path"' in output
assert '"/* not a comment */"' in output
assert "char slash = '/';" in output
assert "real comment" not in output
assert "block comment" not in output
assert "int next = 2;" in output

def test_strip_cpp_comments_preserves_raw_string_literals():
code = r'''
auto raw = R"tag(// not a comment
/* also not a comment */)tag";
int value = 1; // real comment
'''

output = _strip_cpp_comments(code)

assert 'R"tag(// not a comment' in output
assert "/* also not a comment */)tag" in output
assert "real comment" not in output

def test_cpp_minify(cpp_minifier, temp_files):
minify_file = temp_files / "minify_test.cpp"
output = cpp_minifier.minify(str(minify_file))
Expand All @@ -71,6 +103,25 @@ def test_cpp_minify(cpp_minifier, temp_files):
assert "*/" not in output # Multi-line comments removed
# Note: Some whitespace and newlines are preserved for compilation compatibility

def test_cpp_minify_preserves_comment_markers_inside_literals(cpp_minifier, tmp_path):
source = tmp_path / "strings.cpp"
source.write_text(r'''
#include <string>
int main() {
std::string url = "https://example.com";
std::string marker = "/* not a comment */";
auto raw = R"(// not a comment)";
return url.size() + marker.size() + raw.size(); // real comment
}
''')

output = cpp_minifier.minify(str(source))

assert '"https://example.com"' in output
assert '"/* not a comment */"' in output
assert 'R"(// not a comment)"' in output
assert "real comment" not in output

def test_cpp_minify_does_not_create_fixed_temp_file(cpp_minifier, temp_files, monkeypatch):
monkeypatch.chdir(temp_files)

Expand Down
Loading