diff --git a/builder/build_cli.py b/builder/build_cli.py index 35c7917..a0aa205 100644 --- a/builder/build_cli.py +++ b/builder/build_cli.py @@ -29,6 +29,7 @@ def build_docs( debug: bool, offline: bool, spec_lock_consistency_check: bool, + test_rust_blocks: bool, ) -> Path: """ Builds the Sphinx documentation with the specified options. @@ -72,6 +73,8 @@ def build_docs( conf_opt_values.append("offline=1") if debug: conf_opt_values.append("debug=1") + if test_rust_blocks: + conf_opt_values.append("test_rust_blocks=1") # Only add the --define argument if there are options to define if conf_opt_values: @@ -151,6 +154,14 @@ def main(root): help="build in offline mode", action="store_true", ) + + parser.add_argument( + "--test-rust-blocks", + help="Test extracted rust code blocks using rustc", + default=False, + action="store_true" + ) + group = parser.add_mutually_exclusive_group() parser.add_argument( "--ignore-spec-lock-diff", @@ -192,6 +203,6 @@ def main(root): update_spec_lockfile(SPEC_CHECKSUM_URL, root / "src" / SPEC_LOCKFILE) rendered = build_docs( - root, "xml" if args.xml else "html", args.clear, args.serve, args.debug, args.offline, not args.ignore_spec_lock_diff, + root, "xml" if args.xml else "html", args.clear, args.serve, args.debug, args.offline, not args.ignore_spec_lock_diff, args.test_rust_blocks ) diff --git a/exts/coding_guidelines/__init__.py b/exts/coding_guidelines/__init__.py index f5d74d1..fa9dbcf 100644 --- a/exts/coding_guidelines/__init__.py +++ b/exts/coding_guidelines/__init__.py @@ -10,6 +10,12 @@ from .common import logger, get_tqdm, bar_format, logging from sphinx.domains import Domain +import logging + +# Get the Sphinx logger +logger = logging.getLogger('sphinx') +logger.setLevel(logging.ERROR) + class CodingGuidelinesDomain(Domain): name = "coding-guidelines" label = "Rust Standard Library" @@ -42,6 +48,13 @@ def on_build_finished(app, exception): def setup(app): app.add_domain(CodingGuidelinesDomain) + + app.add_config_value( + name='test_rust_blocks', + default=False, + rebuild='env' + ) + app.add_config_value( name = "offline", default=False, @@ -73,12 +86,14 @@ def setup(app): logger.setLevel(logging.INFO) common.disable_tqdm = True - app.connect('env-check-consistency', guidelines_checks.validate_required_fields) - app.connect('env-check-consistency', fls_checks.check_fls) - app.connect('build-finished', write_guidelines_ids.build_finished) - app.connect('build-finished', fls_linking.build_finished) - app.connect('build-finished', on_build_finished) - + # Ignore builds while testing code blocks + if not app.config.test_rust_blocks: + app.connect('env-check-consistency', guidelines_checks.validate_required_fields) + app.connect('env-check-consistency', fls_checks.check_fls) + app.connect('build-finished', write_guidelines_ids.build_finished) + app.connect('build-finished', fls_linking.build_finished) + app.connect('build-finished', on_build_finished) + return { 'version': '0.1', 'parallel_read_safe': True, diff --git a/exts/rust-code-runner/__init__.py b/exts/rust-code-runner/__init__.py new file mode 100644 index 0000000..efb8361 --- /dev/null +++ b/exts/rust-code-runner/__init__.py @@ -0,0 +1,26 @@ +from . import rust_examples_aggregate +from . import rustc +import os + +def setup(app): + + app.output_rust_file = "build/rust-code-blocks/generated.rs" + + # create build dir + if not os.path.exists("build/rust-code-blocks"): + os.makedirs("build/rust-code-blocks") + if os.path.isfile(app.output_rust_file): + with open(app.output_rust_file, 'w'): + pass + + # we hook into 'source-read' because data is mutable at this point and easier to parse + # and it also makes this extension indepandant from `needs`. + # + app.connect('source-read', rust_examples_aggregate.preprocess_rst_for_rust_code) + + if app.config.test_rust_blocks: + app.connect('build-finished', rustc.check_rust_test_errors) + return { + 'version': '0.1', + 'parallel_read_safe': False, + } diff --git a/exts/rust-code-runner/generated.rs b/exts/rust-code-runner/generated.rs new file mode 100644 index 0000000..5b29a57 --- /dev/null +++ b/exts/rust-code-runner/generated.rs @@ -0,0 +1,259 @@ +// ==== Code Block 1 ==== +#[test] +fn test_block_coding_guidelines_expressions_1() { + #[repr(C)] + struct Base { + position: (u32, u32) + } +} + +// ==== Code Block 2 ==== +#[test] +fn test_block_coding_guidelines_expressions_2() { + #[repr(C)] + struct Base { + position: (u32, u32) + } +} + +// ==== Code Block 1 ==== +#[test] +fn test_block_coding_guidelines_macros_1() { + fn example_function() { + // Non-compliant implementation + } +} + +// ==== Code Block 2 ==== +#[test] +fn test_block_coding_guidelines_macros_2() { + fn example_function() { + // Compliant implementation + } +} + +// ==== Code Block 3 ==== +#[test] +fn test_block_coding_guidelines_macros_3() { + // TODO +} + +// ==== Code Block 4 ==== +#[test] +fn test_block_coding_guidelines_macros_4() { + // TODO +} + +// ==== Code Block 5 ==== +#[test] +fn test_block_coding_guidelines_macros_5() { + macro_rules! increment_and_double { + ($x:expr) => { + { + $x += 1; // mutation is implicit + $x * 2 + } + }; + } + let mut num = 5; + let result = increment_and_double!(num); + println!("Result: {}, Num: {}", result, num); + // Result: 12, Num: 6 +} + +// ==== Code Block 6 ==== +#[test] +fn test_block_coding_guidelines_macros_6() { + fn increment_and_double(x: &mut i32) -> i32 { + *x += 1; // mutation is explicit + *x * 2 + } + let mut num = 5; + let result = increment_and_double(&mut num); + println!("Result: {}, Num: {}", result, num); + // Result: 12, Num: 6 +} + +// ==== Code Block 7 ==== +#[test] +fn test_block_coding_guidelines_macros_7() { + fn example_function() { + // Non-compliant implementation + } +} + +// ==== Code Block 8 ==== +#[test] +fn test_block_coding_guidelines_macros_8() { + fn example_function() { + // Compliant implementation + } +} + +// ==== Code Block 9 ==== +#[test] +fn test_block_coding_guidelines_macros_9() { + fn example_function() { + // Non-compliant implementation + } +} + +// ==== Code Block 10 ==== +#[test] +fn test_block_coding_guidelines_macros_10() { + fn example_function() { + // Compliant implementation + } +} + +// ==== Code Block 11 ==== +#[test] +fn test_block_coding_guidelines_macros_11() { + fn example_function() { + // Non-compliant implementation + } +} + +// ==== Code Block 12 ==== +#[test] +fn test_block_coding_guidelines_macros_12() { + fn example_function() { + // Compliant implementation + } +} + +// ==== Code Block 13 ==== +#[test] +fn test_block_coding_guidelines_macros_13() { + fn example_function() { + // Non-compliant implementation + } +} + +// ==== Code Block 14 ==== +#[test] +fn test_block_coding_guidelines_macros_14() { + fn example_function() { + // Compliant implementation + } +} + +// ==== Code Block 15 ==== +#[test] +fn test_block_coding_guidelines_macros_15() { + #[tokio::main] // non-compliant + async fn main() { +} + +// ==== Code Block 16 ==== +#[test] +fn test_block_coding_guidelines_macros_16() { + fn example_function() { + // Compliant implementation + } +} + +// ==== Code Block 17 ==== +#[test] +fn test_block_coding_guidelines_macros_17() { + fn example_function() { + // Non-compliant implementation + } +} + +// ==== Code Block 18 ==== +#[test] +fn test_block_coding_guidelines_macros_18() { + fn example_function() { + // Compliant implementation + } +} + +// ==== Code Block 19 ==== +#[test] +fn test_block_coding_guidelines_macros_19() { + #[macro_export] + macro_rules! vec { + ( $( $x:expr ),* ) => { + { + let mut temp_vec = Vec::new(); // non-global path + $( + temp_vec.push($x); + )* + temp_vec + } + }; + } +} + +// ==== Code Block 20 ==== +#[test] +fn test_block_coding_guidelines_macros_20() { + #[macro_export] + macro_rules! vec { + ( $( $x:expr ),* ) => { + { + let mut temp_vec = ::std::vec::Vec::new(); // global path + $( + temp_vec.push($x); + )* + temp_vec + } + }; + } +} + +// ==== Code Block 1 ==== +#[test] +fn test_block_coding_guidelines_types_and_traits_1() { + fn calculate_next_position(current: u32, velocity: u32) -> u32 { + // Potential for silent overflow in release builds + current + velocity + } +} + +// ==== Code Block 2 ==== +#[test] +fn test_block_coding_guidelines_types_and_traits_2() { + fn calculate_next_position(current: u32, velocity: u32) -> u32 { + // Explicitly handle potential overflow with checked addition + current.checked_add(velocity).expect("Position calculation overflowed") + } +} + +// ==== Code Block 1 ==== +#[test] +fn test_block_process_style_guideline_1() { + fn calculate_next_position(current: u32, velocity: u32) -> u32 { + // Potential for silent overflow in release builds + current + velocity + } +} + +// ==== Code Block 2 ==== +#[test] +fn test_block_process_style_guideline_2() { + fn calculate_next_position(current: u32, velocity: u32) -> u32 { + // Explicitly handle potential overflow with checked addition + current.checked_add(velocity).expect("Position calculation overflowed") + } +} + +// ==== Code Block 3 ==== +#[test] +fn test_block_process_style_guideline_3() { + fn calculate_next_position(current: u32, velocity: u32) -> u32 { + // Potential for silent overflow in release builds + current + velocity + } +} + +// ==== Code Block 4 ==== +#[test] +fn test_block_process_style_guideline_4() { + fn calculate_next_position(current: u32, velocity: u32) -> u32 { + // Explicitly handle potential overflow with checked addition + current.checked_add(velocity).expect("Position calculation overflowed") + } +} + diff --git a/exts/rust-code-runner/rust_examples_aggregate.py b/exts/rust-code-runner/rust_examples_aggregate.py new file mode 100644 index 0000000..c209b03 --- /dev/null +++ b/exts/rust-code-runner/rust_examples_aggregate.py @@ -0,0 +1,90 @@ +from sphinx.errors import SphinxError +import logging +import re + +logger = logging.getLogger('sphinx') +# reducing errors temporarly for dev +logger.setLevel(logging.ERROR) + +class ExecuteRustExamples(SphinxError): + category = "ExecuteRustExamples Error" + + +def extract_code_blocks(text): + pattern = re.compile( + r"\.\. code-block:: rust\s*\n(?:(?:\s*\n)+)?((?: {2,}.*(?:\n|$))+)", + re.MULTILINE + ) + + matches = pattern.findall(text) + blocks = [] + for i, block in enumerate(matches): + lines = block.splitlines() + non_empty_lines = [line for line in lines if line.strip()] + processed_block = "\n".join(non_empty_lines) + blocks.append(processed_block) + + # print(f"====== code block {i + 1} ========") + # print(processed_block) + # print("====== end code block ========") + + return blocks + +def strip_hidden(code_block): + lines = code_block.splitlines() + result = [] + hidden = [] + is_hidden = False + + for line in lines: + stripped_for_marker_check = line[2:] if line.startswith(" ") else line + if "// HIDDEN START" in stripped_for_marker_check: + is_hidden = True + continue + if "// HIDDEN END" in stripped_for_marker_check: + is_hidden = False + continue + if not is_hidden: + result.append(line) + else: + hidden.append(line) + return "\n".join(result), "\n".join(hidden) + +def remove_hidden_blocks_from_document(source_text): + code_block_re = re.compile( + r"(\.\. code-block:: rust\s*\n\n)((?: {2}.*\n)+)", + re.DOTALL + ) + # callback for replacing + def replacer(match): + prefix = match.group(1) + code_content = match.group(2) + cleaned_code, hidden_code = strip_hidden(code_content) + # print("============") + # print(hidden_code) + # print("============") + return prefix + cleaned_code + + modified_text = code_block_re.sub(replacer, source_text) + return modified_text + + +def preprocess_rst_for_rust_code(app, docname, source): + + original_content = source[0] + code_blocks = extract_code_blocks(original_content) + modified_content = remove_hidden_blocks_from_document(original_content) + source[0] = modified_content + + # print(f"Original content length: {len(original_content)}") + # print(f"Extracted {len(code_blocks)} code blocks") + + safe_docname = docname.replace("/", "_").replace("-", "_") + with open(app.output_rust_file, "a", encoding="utf-8") as f: + for i, block in enumerate(code_blocks, start=1): + f.write(f"// ==== Code Block {i} ====\n") + f.write("#[test]\n") + f.write(f"fn test_block_{safe_docname}_{i}() {{\n") + for line in block.splitlines(): + f.write(f" {line}\n") + f.write("}\n\n") diff --git a/exts/rust-code-runner/rustc.py b/exts/rust-code-runner/rustc.py new file mode 100644 index 0000000..f875429 --- /dev/null +++ b/exts/rust-code-runner/rustc.py @@ -0,0 +1,116 @@ +import json +import subprocess + +def print_code_snippet(file_path, line_num, context=3): + """ + Prints a code snippet from a file with context around a specific line. + + This function is typically used to display source code around an error line + for better debugging and error reporting. + + Args: + file_path (str): Path to the source file. + line_num (int): The line number where the error occurred (1-based index). + context (int, optional): The number of lines to display before and after + the error line. Defaults to 3. + + Returns: + None + """ + try: + stripped_lines = [] + with open(file_path, "r") as f: + lines = f.readlines() + start = max(line_num - context - 1, 0) + end = min(line_num + context, len(lines)) + for i in range(start, end): + prefix = ">" if i == line_num - 1 else " " + stripped_lines.append(f"{prefix} {i+1:4}: {lines[i].rstrip()}") + return "\n".join(stripped_lines) + except Exception as e: + print(f"Could not read file {file_path}: {e}") + + +def parse_rustc_json(stderr: str, file_path): + """ + Parses rustc's JSON output and prints only the first error with a single snippet. + + Args: + stderr (str): JSON-formatted stderr output from rustc. + file_path: Path to the Rust file. + + Returns: + None + """ + for line in stderr.splitlines(): + line = line.strip() + if not line: + continue + + try: + diagnostic = json.loads(line) + except json.JSONDecodeError: + continue + + if diagnostic.get("$message_type") != "diagnostic": + continue + + if diagnostic.get("level") != "error": + continue # skip warnings and notes + + message = diagnostic.get("message", "") + spans = diagnostic.get("spans", []) + + # Try to find a span in the current file + for span in spans: + if span["file_name"] == file_path: + line_num = span["line_start"] + label = span.get("label", "") + print(f"error: line {line_num}: {message}") + if label: + print(f"--> {label}") + print("=" * 25) + snippet = print_code_snippet(file_path, line_num, context=3) + print(snippet) + print("=" * 25) + return # we return because we only print the first error--in json format there can be multiple error messages for 1 error-- if you want to see them comment this line. + + # If no span in the file, still print the error + print(f"error: {message}") + return + +def check_rust_test_errors(app, exception): + """ + Sphinx 'build-finished' event handler that compiles the generated Rust file in test mode. + + This function is connected to the Sphinx build lifecycle and is executed after the build finishes. + It invokes `rustc` in test mode on the generated Rust file and reports any compilation or test-related + errors. + + Args: + app: The Sphinx application object. Must have an `output_rust_file` attribute containing + the path to the generated Rust source file. + exception: Exception raised during the build process, or None if the build completed successfully. + """ + rs_path = app.output_rust_file + # Run the Rust compiler in test mode with JSON error output format. + # capturing stdout and stderr as text. + result = subprocess.run( + ["rustc", "--test", "--edition=2024", "--error-format=json", "--emit=metadata", rs_path], + # --emit=metadata or else rustc will produce a binary ./generated + capture_output=True, + text=True + ) + + if result.returncode != 0: + print("--- rustc Errors/Warnings ---") + parse_rustc_json(result.stderr, app.output_rust_file) + print("--- rustc Output ---") + print(result.stdout) + + else: + print("--- rustc Output ---") + print(result.stdout) + if result.stderr: + print("\n\n--- rustc Warnings---") + print(result.stderr) diff --git a/exts/rust-code-runner/tests/tests.py b/exts/rust-code-runner/tests/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/src/conf.py b/src/conf.py index fda72ec..5e25694 100644 --- a/src/conf.py +++ b/src/conf.py @@ -24,6 +24,7 @@ 'sphinx.ext.autosectionlabel', 'sphinx_needs', 'coding_guidelines', + 'rust-code-runner', ] # Basic needs configuration pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy