diff --git a/src/plugins/cpp_plugin.py b/src/plugins/cpp_plugin.py index 8307fd1..d5470af 100644 --- a/src/plugins/cpp_plugin.py +++ b/src/plugins/cpp_plugin.py @@ -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: """ @@ -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") @@ -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 diff --git a/tests/test_cpp_preprocessor.py b/tests/test_cpp_preprocessor.py index c797188..0f48967 100644 --- a/tests/test_cpp_preprocessor.py +++ b/tests/test_cpp_preprocessor.py @@ -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(): @@ -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)) @@ -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 +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)