From 4d13348464d14831a05344d877a0d5397bd34410 Mon Sep 17 00:00:00 2001 From: Pete LeVasseur Date: Tue, 16 Dec 2025 05:53:56 +0900 Subject: [PATCH 1/2] feat: add multiple examples, testing of them --- .github/ISSUE_TEMPLATE/CODING-GUIDELINE.yml | 188 +++++++++++- generate_guideline_templates.py | 199 ++++++++++--- scripts/generate-rst-comment.py | 306 +++++++++++++++++++- scripts/guideline_utils.py | 198 ++++++++----- 4 files changed, 744 insertions(+), 147 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/CODING-GUIDELINE.yml b/.github/ISSUE_TEMPLATE/CODING-GUIDELINE.yml index 0b7f799ee..89ac2eeb2 100644 --- a/.github/ISSUE_TEMPLATE/CODING-GUIDELINE.yml +++ b/.github/ISSUE_TEMPLATE/CODING-GUIDELINE.yml @@ -142,18 +142,29 @@ body: placeholder: As documented in the below reference, we see that under conditions `baz` if we `foo` when `bar`-ing we encounter suprising behavior `biz`. ... validations: required: true + + # ========================================================================== + # Non-Compliant Example 1 (Required) + # ========================================================================== + - type: markdown + attributes: + value: | + --- + ## Non-Compliant Examples + + Provide at least one non-compliant example. You may add up to 4 total. - type: textarea - id: non-compliant-example-prose + id: non-compliant-example-prose-1 attributes: - label: Non-Compliant Example - Prose + label: Non-Compliant Example 1 - Prose description: Here we write a textual description of why the Non-Compliant Example - Code exhibits unintended or consequential behavior. Non-normative. placeholder: As seen below in the `non_compliant_example()` function, when `baz` and `bar` are both active and we `foo`, later during refactoring it's possible to get `biz` instead of `boz` behavior. ... validations: required: true - type: textarea - id: non-compliant-example-code + id: non-compliant-example-code-1 attributes: - label: Non-Compliant Example - Code + label: Non-Compliant Example 1 - Code description: | Code example showcasing the unintended or consequential behavior. Non-normative. Please wrap your code in a fenced code block with the `rust` language specifier. @@ -177,18 +188,104 @@ body: ``` validations: required: true + + # ========================================================================== + # Non-Compliant Example 2 (Optional) + # ========================================================================== + - type: textarea + id: non-compliant-example-prose-2 + attributes: + label: Non-Compliant Example 2 - Prose (Optional) + description: Additional non-compliant example prose. Leave empty if not needed. + placeholder: Another scenario where this guideline is violated... + validations: + required: false + - type: textarea + id: non-compliant-example-code-2 + attributes: + label: Non-Compliant Example 2 - Code (Optional) + description: Additional non-compliant code example. Leave empty if not needed. + placeholder: | + ```rust + # fn main() { + // Another non-compliant example + # } + ``` + validations: + required: false + + # ========================================================================== + # Non-Compliant Example 3 (Optional) + # ========================================================================== + - type: textarea + id: non-compliant-example-prose-3 + attributes: + label: Non-Compliant Example 3 - Prose (Optional) + description: Additional non-compliant example prose. Leave empty if not needed. + placeholder: Yet another scenario where this guideline is violated... + validations: + required: false + - type: textarea + id: non-compliant-example-code-3 + attributes: + label: Non-Compliant Example 3 - Code (Optional) + description: Additional non-compliant code example. Leave empty if not needed. + placeholder: | + ```rust + # fn main() { + // Another non-compliant example + # } + ``` + validations: + required: false + + # ========================================================================== + # Non-Compliant Example 4 (Optional) + # ========================================================================== + - type: textarea + id: non-compliant-example-prose-4 + attributes: + label: Non-Compliant Example 4 - Prose (Optional) + description: Additional non-compliant example prose. Leave empty if not needed. + placeholder: A fourth scenario where this guideline is violated... + validations: + required: false + - type: textarea + id: non-compliant-example-code-4 + attributes: + label: Non-Compliant Example 4 - Code (Optional) + description: Additional non-compliant code example. Leave empty if not needed. + placeholder: | + ```rust + # fn main() { + // Another non-compliant example + # } + ``` + validations: + required: false + + # ========================================================================== + # Compliant Example 1 (Required) + # ========================================================================== + - type: markdown + attributes: + value: | + --- + ## Compliant Examples + + Provide at least one compliant example. You may add up to 4 total. - type: textarea - id: compliant-example-prose + id: compliant-example-prose-1 attributes: - label: Compliant Example - Prose + label: Compliant Example 1 - Prose description: Here we write a textual description of why the Compliant Example - Code avoids unintended or consequential behavior by following the Coding Guideline. placeholder: As seen below in the `compliant_example()` function, when `baz` and `bar` are both active and we avoid doing `foo`, later during refactoring we've prevented an error by construction. ... validations: required: true - type: textarea - id: compliant-example-code + id: compliant-example-code-1 attributes: - label: Compliant Example - Code + label: Compliant Example 1 - Code description: | Code example showcasing we avoid unintended or consequential behavior by following the Coding Guideline. Please wrap your code in a fenced code block with the `rust` language specifier. @@ -212,3 +309,78 @@ body: ``` validations: required: true + + # ========================================================================== + # Compliant Example 2 (Optional) + # ========================================================================== + - type: textarea + id: compliant-example-prose-2 + attributes: + label: Compliant Example 2 - Prose (Optional) + description: Additional compliant example prose. Leave empty if not needed. + placeholder: Another way to comply with this guideline... + validations: + required: false + - type: textarea + id: compliant-example-code-2 + attributes: + label: Compliant Example 2 - Code (Optional) + description: Additional compliant code example. Leave empty if not needed. + placeholder: | + ```rust + # fn main() { + // Another compliant example + # } + ``` + validations: + required: false + + # ========================================================================== + # Compliant Example 3 (Optional) + # ========================================================================== + - type: textarea + id: compliant-example-prose-3 + attributes: + label: Compliant Example 3 - Prose (Optional) + description: Additional compliant example prose. Leave empty if not needed. + placeholder: Yet another way to comply with this guideline... + validations: + required: false + - type: textarea + id: compliant-example-code-3 + attributes: + label: Compliant Example 3 - Code (Optional) + description: Additional compliant code example. Leave empty if not needed. + placeholder: | + ```rust + # fn main() { + // Another compliant example + # } + ``` + validations: + required: false + + # ========================================================================== + # Compliant Example 4 (Optional) + # ========================================================================== + - type: textarea + id: compliant-example-prose-4 + attributes: + label: Compliant Example 4 - Prose (Optional) + description: Additional compliant example prose. Leave empty if not needed. + placeholder: A fourth way to comply with this guideline... + validations: + required: false + - type: textarea + id: compliant-example-code-4 + attributes: + label: Compliant Example 4 - Code (Optional) + description: Additional compliant code example. Leave empty if not needed. + placeholder: | + ```rust + # fn main() { + // Another compliant example + # } + ``` + validations: + required: false diff --git a/generate_guideline_templates.py b/generate_guideline_templates.py index 0563fd0e0..d445d9daa 100755 --- a/generate_guideline_templates.py +++ b/generate_guideline_templates.py @@ -27,13 +27,73 @@ "Amplification": "amplification", "Exception(s)": "exceptions", "Rationale": "rationale", - "Non-Compliant Example - Prose": "non_compliant_ex_prose", - "Non-Compliant Example - Code": "non_compliant_ex", - "Compliant Example - Prose": "compliant_example_prose", - "Compliant Example - Code": "compliant_example", + # Non-Compliant Examples (1-4) + "Non-Compliant Example 1 - Prose": "non_compliant_ex_prose_1", + "Non-Compliant Example 1 - Code": "non_compliant_ex_1", + "Non-Compliant Example 2 - Prose (Optional)": "non_compliant_ex_prose_2", + "Non-Compliant Example 2 - Code (Optional)": "non_compliant_ex_2", + "Non-Compliant Example 3 - Prose (Optional)": "non_compliant_ex_prose_3", + "Non-Compliant Example 3 - Code (Optional)": "non_compliant_ex_3", + "Non-Compliant Example 4 - Prose (Optional)": "non_compliant_ex_prose_4", + "Non-Compliant Example 4 - Code (Optional)": "non_compliant_ex_4", + # Compliant Examples (1-4) + "Compliant Example 1 - Prose": "compliant_ex_prose_1", + "Compliant Example 1 - Code": "compliant_ex_1", + "Compliant Example 2 - Prose (Optional)": "compliant_ex_prose_2", + "Compliant Example 2 - Code (Optional)": "compliant_ex_2", + "Compliant Example 3 - Prose (Optional)": "compliant_ex_prose_3", + "Compliant Example 3 - Code (Optional)": "compliant_ex_3", + "Compliant Example 4 - Prose (Optional)": "compliant_ex_prose_4", + "Compliant Example 4 - Code (Optional)": "compliant_ex_4", + # Legacy field names (for backwards compatibility with old issues) + "Non-Compliant Example - Prose": "non_compliant_ex_prose_1", + "Non-Compliant Example - Code": "non_compliant_ex_1", + "Compliant Example - Prose": "compliant_ex_prose_1", + "Compliant Example - Code": "compliant_ex_1", } +def generate_id(prefix): + """Generate a random ID with the given prefix.""" + random_part = "".join(random.choice(CHARS) for _ in range(ID_LENGTH)) + return f"{prefix}_{random_part}" + + +def generate_example_block( + example_type: str, + example_id: str, + status: str, + prose: str, + code: str, +) -> str: + """ + Generate a single example block (compliant or non-compliant). + + Args: + example_type: Either "compliant_example" or "non_compliant_example" + example_id: The unique ID for this example + status: The status (e.g., "draft") + prose: The prose description + code: The code block content + + Returns: + Formatted RST string for the example + """ + indented_code = indent(code.strip(), " " * 13) + + return dedent(f""" + .. {example_type}:: + :id: {example_id} + :status: {status} + + {prose.strip()} + + .. rust-example:: + + {indented_code.strip()} + """) + + def guideline_rst_template( guideline_title: str, category: str, @@ -47,28 +107,25 @@ def guideline_rst_template( amplification: str, exceptions: str, rationale: str, - non_compliant_ex_prose: str, - non_compliant_ex: str, - compliant_example_prose: str, - compliant_example: str, + non_compliant_examples: list, # List of (prose, code) tuples + compliant_examples: list, # List of (prose, code) tuples ) -> str: """ Generate a .rst guideline entry from field values. + + Args: + non_compliant_examples: List of (prose, code) tuples for non-compliant examples + compliant_examples: List of (prose, code) tuples for compliant examples """ # Generate unique IDs guideline_id = generate_id("gui") rationale_id = generate_id("rat") - non_compliant_example_id = generate_id("non_compl_ex") - compliant_example_id = generate_id("compl_ex") # Normalize inputs def norm(value: str) -> str: return value.strip().lower() - indented_compliant_ex = indent(compliant_example.strip(), " " * 13) - indented_non_compliant_ex = indent(non_compliant_ex.strip(), " " * 13) - # Build optional exception section exception_section = "" if exceptions and exceptions.strip(): @@ -78,9 +135,40 @@ def norm(value: str) -> str: {exceptions.strip()} """ + # Generate non-compliant example blocks + non_compliant_blocks = [] + for i, (prose, code) in enumerate(non_compliant_examples): + if prose.strip() and code.strip(): + example_id = generate_id("non_compl_ex") + block = generate_example_block( + "non_compliant_example", + example_id, + norm(status), + prose, + code, + ) + non_compliant_blocks.append(block) + + # Generate compliant example blocks + compliant_blocks = [] + for i, (prose, code) in enumerate(compliant_examples): + if prose.strip() and code.strip(): + example_id = generate_id("compl_ex") + block = generate_example_block( + "compliant_example", + example_id, + norm(status), + prose, + code, + ) + compliant_blocks.append(block) + + # Combine all example blocks + all_examples = "\n".join(non_compliant_blocks + compliant_blocks) + guideline_text = dedent(f""" .. guideline:: {guideline_title.strip()} - :id: {guideline_id} + :id: {guideline_id} :category: {norm(category)} :status: {norm(status)} :release: {norm(release_begin)}-{release_end.strip()} @@ -93,44 +181,44 @@ def norm(value: str) -> str: {exception_section.strip()} - .. rationale:: - :id: {rationale_id} + .. rationale:: + :id: {rationale_id} :status: {norm(status)} {rationale.strip()} - - .. non_compliant_example:: - :id: {non_compliant_example_id} - :status: {norm(status)} - - {non_compliant_ex_prose.strip()} - - .. rust-example:: - - {indented_non_compliant_ex.strip()} - - .. compliant_example:: - :id: {compliant_example_id} - :status: {norm(status)} - - {compliant_example_prose.strip()} - - .. rust-example:: - - {indented_compliant_ex.strip()} +{all_examples} """) return guideline_text -def generate_id(prefix): - """Generate a random ID with the given prefix.""" - random_part = "".join(random.choice(CHARS) for _ in range(ID_LENGTH)) - return f"{prefix}_{random_part}" - +def generate_guideline_template(num_non_compliant: int = 1, num_compliant: int = 1): + """ + Generate a complete guideline template with all required sections. -def generate_guideline_template(): - """Generate a complete guideline template with all required sections.""" + Args: + num_non_compliant: Number of non-compliant examples to include (1-4) + num_compliant: Number of compliant examples to include (1-4) + """ + # Clamp to valid range + num_non_compliant = max(1, min(4, num_non_compliant)) + num_compliant = max(1, min(4, num_compliant)) + + # Generate non-compliant examples + non_compliant_examples = [] + for i in range(1, num_non_compliant + 1): + non_compliant_examples.append(( + f"Explanation of non-compliant example {i}.", + f"fn non_compliant_example_{i}() {{\n // Non-compliant implementation {i}\n}}" + )) + + # Generate compliant examples + compliant_examples = [] + for i in range(1, num_compliant + 1): + compliant_examples.append(( + f"Explanation of compliant example {i}.", + f"fn compliant_example_{i}() {{\n // Compliant implementation {i}\n}}" + )) template = guideline_rst_template( guideline_title="Title Here", @@ -145,10 +233,8 @@ def generate_guideline_template(): amplification="Description of the guideline goes here.", exceptions="", rationale="Explanation of why this guideline is important.", - non_compliant_ex_prose="Explanation of code example.", - non_compliant_ex=""" fn example_function() {\n // Non-compliant implementation\n } """, - compliant_example_prose="Explanation of code example.", - compliant_example=""" fn example_function() {\n // Compliant implementation\n } """, + non_compliant_examples=non_compliant_examples, + compliant_examples=compliant_examples, ) return template @@ -165,6 +251,20 @@ def parse_args(): default=1, help="Number of templates to generate (default: 1)", ) + parser.add_argument( + "--non-compliant", + type=int, + default=1, + choices=[1, 2, 3, 4], + help="Number of non-compliant examples to include (default: 1, max: 4)", + ) + parser.add_argument( + "--compliant", + type=int, + default=1, + choices=[1, 2, 3, 4], + help="Number of compliant examples to include (default: 1, max: 4)", + ) return parser.parse_args() @@ -177,7 +277,10 @@ def main(): if num_templates > 1: print(f"=== Template {i + 1} ===\n") - template = generate_guideline_template() + template = generate_guideline_template( + num_non_compliant=args.non_compliant, + num_compliant=args.compliant, + ) print(template) if num_templates > 1 and i < num_templates - 1: diff --git a/scripts/generate-rst-comment.py b/scripts/generate-rst-comment.py index 08c6e20a5..331c22b9d 100644 --- a/scripts/generate-rst-comment.py +++ b/scripts/generate-rst-comment.py @@ -6,6 +6,8 @@ This script generates a GitHub comment containing the RST preview of a coding guideline. It reads a GitHub issue JSON from stdin and outputs a formatted Markdown comment. +It also extracts and tests Rust code examples, reporting any compilation failures. + Usage: cat issue.json | uv run python scripts/generate-rst-comment.py curl https://api.github.com/repos/.../issues/123 | uv run python scripts/generate-rst-comment.py @@ -14,7 +16,11 @@ import json import os import re +import subprocess import sys +import tempfile +from dataclasses import dataclass +from typing import List, Optional, Tuple # Add the scripts directory to Python path so we can import guideline_utils script_dir = os.path.dirname(os.path.abspath(__file__)) @@ -22,6 +28,7 @@ from guideline_utils import ( chapter_to_filename, + collect_examples, extract_form_fields, guideline_template, normalize_list_separation, @@ -36,13 +43,23 @@ """ +@dataclass +class CodeTestResult: + """Result of testing a single code example.""" + example_type: str # "compliant" or "non_compliant" + example_number: int + passed: bool + error_message: str = "" + compiler_output: str = "" + + def extract_guideline_id(rst_content: str) -> str: """ Extract the guideline ID from RST content. - + Args: rst_content: The generated RST content - + Returns: The guideline ID (e.g., "gui_abc123XYZ") or empty string if not found """ @@ -50,23 +67,283 @@ def extract_guideline_id(rst_content: str) -> str: return match.group(1) if match else "" -def generate_comment(rst_content: str, chapter: str) -> str: +def strip_markdown_fences(code: str) -> str: + """ + Remove markdown code fences from code if present. + + Args: + code: Code possibly wrapped in ```rust ... ``` + + Returns: + Code without fences + """ + lines = code.strip().splitlines() + if lines and lines[0].strip().startswith("```"): + lines = lines[1:] + if lines and lines[-1].strip() == "```": + lines = lines[:-1] + return "\n".join(lines) + + +def process_hidden_lines(code: str) -> str: + """ + Process rustdoc-style hidden lines. + Lines starting with '# ' have the marker removed for compilation. + + Args: + code: Code with potential hidden line markers + + Returns: + Code ready for compilation + """ + lines = [] + for line in code.split('\n'): + if line.startswith('# '): + # Hidden line - include without the marker + lines.append(line[2:]) + elif line == '#': + # Empty hidden line + lines.append('') + else: + lines.append(line) + return '\n'.join(lines) + + +def wrap_in_main(code: str) -> str: + """ + Wrap code in a main function if it doesn't have one. + """ + # Check if code already has a main function + if re.search(r'\bfn\s+main\s*\(', code): + return code + + # Check if it has any function definitions or other top-level items + has_functions = re.search(r'\bfn\s+\w+\s*[<(]', code) + has_impl = re.search(r'\bimpl\b', code) + has_struct = re.search(r'\bstruct\b', code) + has_enum = re.search(r'\benum\b', code) + has_trait = re.search(r'\btrait\b', code) + has_mod = re.search(r'\bmod\b', code) + has_type = re.search(r'\btype\b', code) + + # If it looks like top-level items, don't wrap + if has_functions or has_impl or has_struct or has_enum or has_trait or has_mod or has_type: + return code + + # Check for use/const/static statements + has_use = re.search(r'\buse\b', code) + has_const = re.search(r'\bconst\b', code) + has_static = re.search(r'\bstatic\b', code) + + if has_use or has_const or has_static: + # Keep use/const/static at top level, wrap the rest + lines = code.split('\n') + top_level = [] + body = [] + in_top_level = True + + for line in lines: + stripped = line.strip() + if in_top_level and (stripped.startswith('use ') or + stripped.startswith('const ') or + stripped.startswith('static ')): + top_level.append(line) + else: + in_top_level = False + body.append(line) + + if body: + indented_body = '\n'.join(' ' + line if line.strip() else '' for line in body) + return '\n'.join(top_level) + '\n\nfn main() {\n' + indented_body + '\n}' + else: + return '\n'.join(top_level) + '\n\nfn main() {}' + + # Simple case: wrap everything + indented = '\n'.join(' ' + line if line.strip() else '' for line in code.split('\n')) + return 'fn main() {\n' + indented + '\n}' + + +def test_rust_code(code: str, timeout: int = 30) -> Tuple[bool, str]: + """ + Test if Rust code compiles successfully. + + Args: + code: Rust code to test + timeout: Compilation timeout in seconds + + Returns: + Tuple of (passed, error_message) + """ + # Strip markdown fences if present + code = strip_markdown_fences(code) + + # Process hidden lines + code = process_hidden_lines(code) + + # Wrap in main if needed + code = wrap_in_main(code) + + with tempfile.TemporaryDirectory() as tmpdir: + src_file = os.path.join(tmpdir, "test.rs") + out_file = os.path.join(tmpdir, "test_binary") + + with open(src_file, "w") as f: + f.write(code) + + try: + result = subprocess.run( + ["rustc", "--edition=2021", "--crate-type=bin", "-o", out_file, src_file], + capture_output=True, + text=True, + timeout=timeout + ) + + if result.returncode == 0: + return True, "" + else: + # Extract key error info + error_lines = result.stderr.strip().split('\n') + # Get first few relevant error lines + relevant_errors = [] + for line in error_lines[:15]: + if line.strip(): + relevant_errors.append(line) + return False, "\n".join(relevant_errors) + + except subprocess.TimeoutExpired: + return False, "Compilation timed out" + except FileNotFoundError: + return False, "rustc not found - cannot test code" + except Exception as e: + return False, f"Error: {str(e)}" + + +def test_all_examples(fields: dict) -> List[CodeTestResult]: + """ + Test all code examples from the form fields. + + Args: + fields: Dictionary of form fields + + Returns: + List of CodeTestResult objects + """ + results = [] + + # Test non-compliant examples + non_compliant = collect_examples(fields, "non_compliant") + for i, (prose, code) in enumerate(non_compliant, 1): + if code.strip(): + passed, error = test_rust_code(code) + results.append(CodeTestResult( + example_type="non_compliant", + example_number=i, + passed=passed, + error_message=error if not passed else "" + )) + + # Test compliant examples + compliant = collect_examples(fields, "compliant") + for i, (prose, code) in enumerate(compliant, 1): + if code.strip(): + passed, error = test_rust_code(code) + results.append(CodeTestResult( + example_type="compliant", + example_number=i, + passed=passed, + error_message=error if not passed else "" + )) + + return results + + +def format_test_results(results: List[CodeTestResult]) -> str: + """ + Format test results as a markdown section. + + Args: + results: List of test results + + Returns: + Formatted markdown string + """ + if not results: + return "" + + passed = sum(1 for r in results if r.passed) + failed = sum(1 for r in results if not r.passed) + total = len(results) + + lines = [] + lines.append("### ๐Ÿงช Code Example Test Results") + lines.append("") + + if failed == 0: + lines.append(f"โœ… **All {total} code example(s) compiled successfully!**") + else: + lines.append(f"โš ๏ธ **{failed} of {total} code example(s) failed to compile**") + lines.append("") + lines.append("| Example | Status | Details |") + lines.append("|---------|--------|---------|") + + for r in results: + example_name = f"{'Non-Compliant' if r.example_type == 'non_compliant' else 'Compliant'} #{r.example_number}" + if r.passed: + lines.append(f"| {example_name} | โœ… Pass | - |") + else: + # Truncate error for table + error_preview = r.error_message.split('\n')[0][:80] + if len(r.error_message) > 80: + error_preview += "..." + lines.append(f"| {example_name} | โŒ Fail | {error_preview} |") + + # Add detailed errors + failures = [r for r in results if not r.passed] + if failures: + lines.append("") + lines.append("
") + lines.append("๐Ÿ” Click for detailed error messages") + lines.append("") + + for r in failures: + example_name = f"{'Non-Compliant' if r.example_type == 'non_compliant' else 'Compliant'} Example #{r.example_number}" + lines.append(f"**{example_name}:**") + lines.append("```") + lines.append(r.error_message) + lines.append("```") + lines.append("") + + lines.append("
") + + lines.append("") + lines.append("> **Note:** Code examples are tested with `rustc --edition=2021`. ") + lines.append("> Hidden lines (prefixed with `# `) are included in compilation.") + lines.append("> Examples without a `fn main()` are automatically wrapped.") + + return "\n".join(lines) + + +def generate_comment(rst_content: str, chapter: str, test_results: List[CodeTestResult]) -> str: """ Generate a formatted GitHub comment with instructions and RST content. - + Args: rst_content: The generated RST content for the guideline chapter: The chapter name (e.g., "Concurrency", "Expressions") - + test_results: Results from testing code examples + Returns: Formatted Markdown comment string """ chapter_slug = chapter_to_filename(chapter) guideline_id = extract_guideline_id(rst_content) - + # Prepend the SPDX header to the RST content for display full_rst_content = GUIDELINE_FILE_HEADER + rst_content.strip() - + + # Format test results + test_results_section = format_test_results(test_results) + # Determine target path based on whether we have a guideline ID if guideline_id: target_dir = f"src/coding-guidelines/{chapter_slug}/" @@ -85,12 +362,14 @@ def generate_comment(rst_content: str, chapter: str) -> str: ### ๐Ÿ“ Target File Add this guideline to: `{target_file}`""" - + comment = f"""## ๐Ÿ“‹ RST Preview for Coding Guideline This is an automatically generated preview of your coding guideline in reStructuredText format. {file_instructions} +{test_results_section} + ### ๐Ÿ“ How to Use This **Option A: Automatic (Recommended)** @@ -156,13 +435,16 @@ def main(): fields = extract_form_fields(issue_body) chapter = fields["chapter"] - + # Generate RST content rst_content = guideline_template(fields) - + + # Test code examples + test_results = test_all_examples(fields) + # Generate the comment - comment = generate_comment(rst_content.strip(), chapter) - + comment = generate_comment(rst_content.strip(), chapter, test_results) + print(comment) diff --git a/scripts/guideline_utils.py b/scripts/guideline_utils.py index 6f35c6f0c..738cd8b8c 100644 --- a/scripts/guideline_utils.py +++ b/scripts/guideline_utils.py @@ -117,10 +117,10 @@ def normalize_md(issue_body: str) -> str: def extract_form_fields(issue_body: str) -> dict: """ Parse issue body (from GitHub issue template) into a dict of field values. - + Args: issue_body: The raw body text from a GitHub issue - + Returns: Dictionary with field names as keys and their values """ @@ -163,11 +163,11 @@ def extract_form_fields(issue_body: str) -> dict: def format_code_block(code: str, lang: str = "rust") -> str: """ Format a code block for RST output, stripping markdown fences if present. - + Args: code: The code content, possibly wrapped in markdown fences lang: The language for syntax highlighting (default: rust) - + Returns: Formatted code block string with proper indentation """ @@ -189,13 +189,49 @@ def format_code_block(code: str, lang: str = "rust") -> str: return f"\n\n{indented_code}\n" +def collect_examples(fields: dict, example_type: str) -> list: + """ + Collect all examples of a given type from fields. + + Args: + fields: Dictionary of form fields + example_type: Either "non_compliant" or "compliant" + + Returns: + List of (prose, code) tuples for non-empty examples + """ + examples = [] + + # Map the example type to field prefixes + if example_type == "non_compliant": + prose_prefix = "non_compliant_ex_prose_" + code_prefix = "non_compliant_ex_" + else: # compliant + prose_prefix = "compliant_ex_prose_" + code_prefix = "compliant_ex_" + + # Check for examples 1-4 + for i in range(1, 5): + prose_key = f"{prose_prefix}{i}" + code_key = f"{code_prefix}{i}" + + prose = fields.get(prose_key, "").strip() + code = fields.get(code_key, "").strip() + + # Only include if both prose and code are non-empty + if prose and code: + examples.append((prose, code)) + + return examples + + def guideline_template(fields: dict) -> str: """ Convert a dictionary of guideline fields into proper RST format. - + Args: fields: Dictionary containing all guideline fields - + Returns: Formatted RST string for the guideline """ @@ -204,12 +240,6 @@ def get(key): amplification_text = indent(md_to_rst(get("amplification")), " " * 12) rationale_text = indent(md_to_rst(get("rationale")), " " * 16) - non_compliant_ex_prose_text = indent( - md_to_rst(get("non_compliant_ex_prose")), " " * 16 - ) - compliant_example_prose_text = indent( - md_to_rst(get("compliant_example_prose")), " " * 16 - ) # Process exceptions field - convert MD to RST and pre-indent for multi-line support exceptions_raw = get("exceptions") @@ -217,6 +247,20 @@ def get(key): if exceptions_raw: exceptions_text = indent(md_to_rst(exceptions_raw), " " * 12) + # Collect non-compliant examples + non_compliant_examples = [] + for prose, code in collect_examples(fields, "non_compliant"): + prose_rst = indent(md_to_rst(prose), " " * 16) + code_formatted = format_code_block(code) + non_compliant_examples.append((prose_rst, code_formatted)) + + # Collect compliant examples + compliant_examples = [] + for prose, code in collect_examples(fields, "compliant"): + prose_rst = indent(md_to_rst(prose), " " * 16) + code_formatted = format_code_block(code) + compliant_examples.append((prose_rst, code_formatted)) + guideline_text = guideline_rst_template( guideline_title=get("guideline_title"), category=get("category"), @@ -230,10 +274,8 @@ def get(key): amplification=amplification_text, exceptions=exceptions_text, rationale=rationale_text, - non_compliant_ex_prose=non_compliant_ex_prose_text, - non_compliant_ex=format_code_block(get("non_compliant_ex")), - compliant_example_prose=compliant_example_prose_text, - compliant_example=format_code_block(get("compliant_example")), + non_compliant_examples=non_compliant_examples, + compliant_examples=compliant_examples, ) return guideline_text @@ -246,10 +288,10 @@ def get(key): def extract_guideline_id(content: str) -> str: """ Extract the guideline ID from RST content. - + Args: content: RST content containing a guideline directive - + Returns: The guideline ID (e.g., "gui_abc123XYZ") or empty string if not found """ @@ -260,40 +302,38 @@ def extract_guideline_id(content: str) -> str: def extract_all_ids(content: str) -> dict: """ Extract all IDs from RST content. - + Args: content: RST content - + Returns: Dictionary with keys 'guideline', 'rationale', 'compliant', 'non_compliant' """ ids = { 'guideline': '', 'rationale': '', - 'compliant': '', - 'non_compliant': '' + 'compliant': [], + 'non_compliant': [] } - + # Guideline ID match = re.search(r':id:\s*(gui_[a-zA-Z0-9]+)', content) if match: ids['guideline'] = match.group(1) - + # Rationale ID match = re.search(r':id:\s*(rat_[a-zA-Z0-9]+)', content) if match: ids['rationale'] = match.group(1) - - # Compliant example ID - match = re.search(r':id:\s*(compl_ex_[a-zA-Z0-9]+)', content) - if match: - ids['compliant'] = match.group(1) - - # Non-compliant example ID - match = re.search(r':id:\s*(non_compl_ex_[a-zA-Z0-9]+)', content) - if match: - ids['non_compliant'] = match.group(1) - + + # Compliant example IDs (multiple) + for match in re.finditer(r':id:\s*(compl_ex_[a-zA-Z0-9]+)', content): + ids['compliant'].append(match.group(1)) + + # Non-compliant example IDs (multiple) + for match in re.finditer(r':id:\s*(non_compl_ex_[a-zA-Z0-9]+)', content): + ids['non_compliant'].append(match.group(1)) + return ids @@ -304,10 +344,10 @@ def extract_all_ids(content: str) -> dict: def chapter_to_filename(chapter: str) -> str: """ Convert chapter name to filename slug. - + Args: chapter: Chapter name (e.g., "Associated Items", "Concurrency") - + Returns: Filename slug (e.g., "associated-items", "concurrency") """ @@ -317,10 +357,10 @@ def chapter_to_filename(chapter: str) -> str: def chapter_to_dirname(chapter: str) -> str: """ Convert chapter name to directory name (same as filename slug). - + Args: chapter: Chapter name (e.g., "Associated Items", "Concurrency") - + Returns: Directory name (e.g., "associated-items", "concurrency") """ @@ -330,10 +370,10 @@ def chapter_to_dirname(chapter: str) -> str: def dirname_to_chapter(dirname: str) -> str: """ Convert directory name back to chapter name. - + Args: dirname: Directory name (e.g., "associated-items") - + Returns: Chapter name (e.g., "Associated Items") """ @@ -350,47 +390,47 @@ def add_include_to_chapter_index( ) -> bool: """ Add an include directive to a chapter's index.rst, maintaining alphabetical order. - + Args: chapter_dir: Path to the chapter directory guideline_filename: Filename of the guideline (e.g., "gui_abc123.rst.inc") - + Returns: True if successful, False otherwise """ index_path = chapter_dir / "index.rst" - + if not index_path.exists(): print(f"Warning: Index file not found: {index_path}") return False - + content = index_path.read_text() - + # Check if already included if guideline_filename in content: print(f"Note: {guideline_filename} already in index") return True - + # Find existing include directives and their position include_pattern = re.compile(r'^(\s*)\.\.\ include::\s+(gui_[a-zA-Z0-9]+\.rst\.inc)\s*$', re.MULTILINE) matches = list(include_pattern.finditer(content)) - + new_include = f".. include:: {guideline_filename}" - + if matches: # Get the indentation from existing includes indent_str = matches[0].group(1) new_include = f"{indent_str}.. include:: {guideline_filename}" - + # Find where to insert alphabetically existing_files = [(m.group(2), m.start(), m.end()) for m in matches] - + insert_pos = None for filename, start, end in existing_files: if guideline_filename < filename: insert_pos = start break - + if insert_pos is None: # Add at end (after last include) last_end = existing_files[-1][2] @@ -401,7 +441,7 @@ def add_include_to_chapter_index( else: # No existing includes - add at end of file content = content.rstrip() + "\n\n" + new_include + "\n" - + index_path.write_text(content) return True @@ -412,29 +452,29 @@ def remove_include_from_chapter_index( ) -> bool: """ Remove an include directive from a chapter's index.rst. - + Args: chapter_dir: Path to the chapter directory guideline_filename: Filename of the guideline to remove - + Returns: True if successful, False otherwise """ index_path = chapter_dir / "index.rst" - + if not index_path.exists(): return False - + content = index_path.read_text() - + # Remove the include line pattern = re.compile(rf'^\s*\.\.\ include::\s+{re.escape(guideline_filename)}\s*\n?', re.MULTILINE) new_content = pattern.sub('', content) - + if new_content != content: index_path.write_text(new_content) return True - + return False @@ -449,51 +489,51 @@ def save_guideline_file( ) -> Path: """ Save a guideline to a per-guideline file in the chapter directory. - + This creates: 1. The chapter directory if it doesn't exist 2. A new file named {guideline_id}.rst.inc 3. Updates the chapter's index.rst with an include directive - + Args: content: The RST content for the guideline chapter: The chapter name (e.g., "Expressions") guidelines_dir: Base guidelines directory (default: src/coding-guidelines) - + Returns: Path to the created file """ if guidelines_dir is None: guidelines_dir = DEFAULT_GUIDELINES_DIR - + chapter_slug = chapter_to_dirname(chapter) chapter_dir = guidelines_dir / chapter_slug - + # Check if per-guideline structure exists (chapter is a directory) if not chapter_dir.is_dir(): # Fall back to legacy monolithic file structure print(f"Note: Chapter directory {chapter_dir} not found.") print(" Using legacy file structure. Run split_guidelines.py to migrate.") return save_guideline_file_legacy(content, chapter, guidelines_dir) - + # Extract guideline ID guideline_id = extract_guideline_id(content) if not guideline_id: raise ValueError("Could not extract guideline ID from content") - + # Create the guideline file guideline_filename = f"{guideline_id}.rst.inc" guideline_path = chapter_dir / guideline_filename - + # Add header and write content full_content = GUIDELINE_FILE_HEADER + content.strip() + "\n" guideline_path.write_text(full_content) print(f"Created guideline file: {guideline_path}") - + # Update the chapter index if add_include_to_chapter_index(chapter_dir, guideline_filename): print(f"Updated index: {chapter_dir / 'index.rst'}") - + return guideline_path @@ -504,24 +544,24 @@ def save_guideline_file_legacy( ) -> Path: """ Append a guideline to a monolithic chapter file (legacy structure). - + Args: content: The RST content for the guideline chapter: The chapter name (e.g., "Expressions") guidelines_dir: Base guidelines directory (default: src/coding-guidelines) - + Returns: Path to the chapter file """ if guidelines_dir is None: guidelines_dir = DEFAULT_GUIDELINES_DIR - + chapter_slug = chapter_to_filename(chapter) chapter_file = guidelines_dir / f"{chapter_slug}.rst" - + with open(chapter_file, "a", encoding="utf-8") as f: f.write(content) - + print(f"Appended guideline to: {chapter_file}") return chapter_file @@ -529,17 +569,17 @@ def save_guideline_file_legacy( def list_guidelines_in_chapter(chapter_dir: Path) -> list: """ List all guideline files in a chapter directory. - + Args: chapter_dir: Path to the chapter directory - + Returns: List of guideline IDs found """ guidelines = [] - + for file_path in chapter_dir.glob("gui_*.rst.inc"): guideline_id = file_path.stem # Remove .rst.inc extension guidelines.append(guideline_id) - + return sorted(guidelines) From 8ada5551e2f65c297a055a296aea024769597eb8 Mon Sep 17 00:00:00 2001 From: Pete LeVasseur Date: Tue, 16 Dec 2025 06:19:46 +0900 Subject: [PATCH 2/2] chore: make ruff pass --- scripts/generate-rst-comment.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/generate-rst-comment.py b/scripts/generate-rst-comment.py index 331c22b9d..5cc4c695f 100644 --- a/scripts/generate-rst-comment.py +++ b/scripts/generate-rst-comment.py @@ -20,7 +20,7 @@ import sys import tempfile from dataclasses import dataclass -from typing import List, Optional, Tuple +from typing import List, Tuple # Add the scripts directory to Python path so we can import guideline_utils script_dir = os.path.dirname(os.path.abspath(__file__)) @@ -270,7 +270,6 @@ def format_test_results(results: List[CodeTestResult]) -> str: if not results: return "" - passed = sum(1 for r in results if r.passed) failed = sum(1 for r in results if not r.passed) total = len(results)