diff --git a/.github/auto-pr-tests/test_issue_01.json b/.github/auto-pr-tests/test_issue_01.json new file mode 100644 index 0000000..a158bed --- /dev/null +++ b/.github/auto-pr-tests/test_issue_01.json @@ -0,0 +1,124 @@ +{ + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4", + "repository_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines", + "labels_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/labels{/name}", + "comments_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/comments", + "events_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/events", + "html_url": "https://github.com/x0rw/safety-critical-rust-coding-guidelines/issues/4", + "id": 3104390263, + "node_id": "I_kwDOOMMjbs65CTx3", + "number": 4, + "title": "[Coding Guideline]: testtt", + "user": { + "login": "x0rw", + "id": 14003018, + "node_id": "MDQ6VXNlcjE0MDAzMDE4", + "avatar_url": "https://avatars.githubusercontent.com/u/14003018?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/x0rw", + "html_url": "https://github.com/x0rw", + "followers_url": "https://api.github.com/users/x0rw/followers", + "following_url": "https://api.github.com/users/x0rw/following{/other_user}", + "gists_url": "https://api.github.com/users/x0rw/gists{/gist_id}", + "starred_url": "https://api.github.com/users/x0rw/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/x0rw/subscriptions", + "organizations_url": "https://api.github.com/users/x0rw/orgs", + "repos_url": "https://api.github.com/users/x0rw/repos", + "events_url": "https://api.github.com/users/x0rw/events{/privacy}", + "received_events_url": "https://api.github.com/users/x0rw/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 8703664686, + "node_id": "LA_kwDOOMMjbs8AAAACBsdiLg", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/category:%20advisory", + "name": "category: advisory", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703664688, + "node_id": "LA_kwDOOMMjbs8AAAACBsdiMA", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/status:%20draft", + "name": "status: draft", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703664689, + "node_id": "LA_kwDOOMMjbs8AAAACBsdiMQ", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/decidability:%20decidable", + "name": "decidability: decidable", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703686409, + "node_id": "LA_kwDOOMMjbs8AAAACBse3CQ", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/chapter:%20concurrency", + "name": "chapter: concurrency", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703686412, + "node_id": "LA_kwDOOMMjbs8AAAACBse3DA", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/scope:%20crate", + "name": "scope: crate", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703732885, + "node_id": "LA_kwDOOMMjbs8AAAACBshslQ", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/accepted", + "name": "accepted", + "color": "6AABE8", + "default": false, + "description": "" + } + ], + "state": "open", + "locked": false, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2025-05-30T22:24:07Z", + "updated_at": "2025-05-30T22:48:17Z", + "closed_at": null, + "author_association": "OWNER", + "active_lock_reason": null, + "sub_issues_summary": { + "total": 0, + "completed": 0, + "percent_completed": 0 + }, + "body": "### Chapter\n\nConcurrency\n\n### Guideline Title\n\ntest ga\n\n### Category\n\nAdvisory\n\n### Status\n\nDraft\n\n### Release Begin\n\n1.1.1\n\n### Release End\n\n1.1.1\n\n### FLS Paragraph ID\n\nfls_fsdjkfslkdfj\n\n### Decidability\n\nDecidable\n\n### Scope\n\nCrate\n\n### Tags\n\ntest gatest ga\n\n### Amplification\n\nhehehehe\n\n### Exception(s)\n\n_No response_\n\n### Rationale\n\ntest ga\n\n### Non-Compliant Example - Prose\n\ntest ga\n\n### Non-Compliant Example - Code\n\ndfhsdfkjshdfskdjhftest ga\n\n### Compliant Example - Prose\n\ntest ga\n\n### Compliant Example - Code\n\ntest ga", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/timeline", + "performed_via_github_app": null, + "state_reason": null +} diff --git a/.github/auto-pr-tests/test_issue_01.snapshot b/.github/auto-pr-tests/test_issue_01.snapshot new file mode 100644 index 0000000..d280270 --- /dev/null +++ b/.github/auto-pr-tests/test_issue_01.snapshot @@ -0,0 +1,41 @@ +=====CONTENT===== + +.. guideline:: test ga + :id: gui_LzfV28IVG7qO + :category: advisory + :status: draft + :release: 1.1.1-1.1.1 + :fls: fls_fsdjkfslkdfj + :decidability: decidable + :scope: crate + :tags: test,gatest,ga + + hehehehe + + .. rationale:: + :id: rat_nPL2Cv7VBdqG + :status: draft + + test ga + + .. non_compliant_example:: + :id: non_compl_ex_Bv7HMs0AVNlH + :status: draft + + test ga + + .. code-block:: rust + + dfhsdfkjshdfskdjhftest ga + + .. compliant_example:: + :id: compl_ex_H5Z7OsxZX3Ig + :status: draft + + test ga + + .. code-block:: rust + + test ga + +=====CONTENT=END===== diff --git a/.github/auto-pr-tests/test_issue_02.json b/.github/auto-pr-tests/test_issue_02.json new file mode 100644 index 0000000..4f1d343 --- /dev/null +++ b/.github/auto-pr-tests/test_issue_02.json @@ -0,0 +1,124 @@ +{ + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4", + "repository_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines", + "labels_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/labels{/name}", + "comments_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/comments", + "events_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/events", + "html_url": "https://github.com/x0rw/safety-critical-rust-coding-guidelines/issues/4", + "id": 3104390263, + "node_id": "I_kwDOOMMjbs65CTx3", + "number": 4, + "title": "[Coding Guideline]: testtt", + "user": { + "login": "x0rw", + "id": 14003018, + "node_id": "MDQ6VXNlcjE0MDAzMDE4", + "avatar_url": "https://avatars.githubusercontent.com/u/14003018?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/x0rw", + "html_url": "https://github.com/x0rw", + "followers_url": "https://api.github.com/users/x0rw/followers", + "following_url": "https://api.github.com/users/x0rw/following{/other_user}", + "gists_url": "https://api.github.com/users/x0rw/gists{/gist_id}", + "starred_url": "https://api.github.com/users/x0rw/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/x0rw/subscriptions", + "organizations_url": "https://api.github.com/users/x0rw/orgs", + "repos_url": "https://api.github.com/users/x0rw/repos", + "events_url": "https://api.github.com/users/x0rw/events{/privacy}", + "received_events_url": "https://api.github.com/users/x0rw/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 8703664686, + "node_id": "LA_kwDOOMMjbs8AAAACBsdiLg", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/category:%20advisory", + "name": "category: advisory", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703664688, + "node_id": "LA_kwDOOMMjbs8AAAACBsdiMA", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/status:%20draft", + "name": "status: draft", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703664689, + "node_id": "LA_kwDOOMMjbs8AAAACBsdiMQ", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/decidability:%20decidable", + "name": "decidability: decidable", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703686409, + "node_id": "LA_kwDOOMMjbs8AAAACBse3CQ", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/chapter:%20concurrency", + "name": "chapter: concurrency", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703686412, + "node_id": "LA_kwDOOMMjbs8AAAACBse3DA", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/scope:%20crate", + "name": "scope: crate", + "color": "ededed", + "default": false, + "description": null + }, + { + "id": 8703732885, + "node_id": "LA_kwDOOMMjbs8AAAACBshslQ", + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/labels/accepted", + "name": "accepted", + "color": "6AABE8", + "default": false, + "description": "" + } + ], + "state": "open", + "locked": false, + "assignee": null, + "assignees": [ + + ], + "milestone": null, + "comments": 0, + "created_at": "2025-05-30T22:24:07Z", + "updated_at": "2025-05-30T22:48:17Z", + "closed_at": null, + "author_association": "OWNER", + "active_lock_reason": null, + "sub_issues_summary": { + "total": 0, + "completed": 0, + "percent_completed": 0 + }, + "body": "### Chapter\n\nAssociated Items\n\n### Guideline Title\n\nRecursive function are not allowed\n\n### Category\n\nRequired\n\n### Status\n\nDraft\n\n### Release Begin\n\n1.3.0\n\n### Release End\n\nlatest\n\n### FLS Paragraph ID\n\nfls_vjgkg8kfi93\n\n### Decidability\n\nUndecidable\n\n### Scope\n\nSystem\n\n### Tags\n\nreduce-human-error\n\n### Amplification\n\nAny function shall not call itself directly or indirectly\n\n### Exception(s)\n\nRecursion may be permitted under the following conditions:\n- The recursion termination condition is simple, explicit, and well-defined.\n- The function calls itself directly, or with strictly limited and clearly documented indirection.\n- The maximum recursion depth is statically bounded and justified, ensuring no risk of stack overflow.\n- The rationale for using recursion, rather than iteration, is clearly documented and reviewed.\n- The code is accompanied by tests that exercise the recursion boundary conditions.\n\n### Rationale\n\nRecursive functions can easily cause stack overflows, which may result in exceptions or, in some cases, undefined behavior (typically some embedded systems). Although the Rust compiler supports [tail call optimization](https://en.wikipedia.org/wiki/Tail_call), this optimization is not guaranteed and depends on the specific implementation and function structure. There is an [open RFC to guarantee tail call optimization in the Rust compiler](https://github.com/phi-go/rfcs/blob/guaranteed-tco/text/0000-explicit-tail-calls.md), but this feature has not yet been stabilized. Until tail call optimization is guaranteed and stabilized, developers should avoid using recursive functions to prevent potential stack overflows and ensure program reliability.\n\n### Non-Compliant Example - Prose\n\nThe below function `concat_strings` is not complaint because it call itself and depending on depth of data provided as input it could generate an stack overflow exception or undefine behavior.\n\n### Non-Compliant Example - Code\n\n```rust\n// Recursive enum to represent a string or a list of `MyEnum`\nenum MyEnum {\n Str(String),\n List(Vec),\n}\n\n// Concatenates strings from a nested structure of `MyEnum` using recursion.\nfn concat_strings(input: &[MyEnum]) -> String {\n let mut result = String::new();\n for item in input {\n match item {\n MyEnum::Str(s) => result.push_str(s),\n MyEnum::List(list) => result.push_str(&concat_strings(list)),\n }\n }\n result\n}\n```\n\n### Compliant Example - Prose\n\nThe following code implements the same functionality using iteration instead of recursion. The `stack` variable is used to maintain the processing context at each step of the loop. This approach provides explicit control over memory usage. If the stack grows beyond a predefined limit due to the structure or size of the input, the function returns an error rather than risking a stack overflow or out-of-memory exception. This ensures more predictable and robust behavior in resource-constrained environments.\n\n### Compliant Example - Code\n\n```rust\n// Recursive enum to represent a string or a list of `MyEnum`\nenum MyEnum {\n Str(String),\n List(Vec),\n}\n\n/// Concatenates strings from a nested structure of `MyEnum` without using recursion.\n/// Returns an error if the stack size exceeds `MAX_STACK_SIZE`.\nfn concat_strings_non_recursive(input: &[MyEnum]) -> Result {\n const MAX_STACK_SIZE: usize = 1000;\n let mut result = String::new();\n let mut stack = Vec::new();\n\n // Add all items to the stack\n stack.extend(input.iter());\n\n while let Some(item) = stack.pop() {\n match item {\n MyEnum::Str(s) => result.insert_str(0, s),\n MyEnum::List(list) => {\n // Add list items to the stack\n for sub_item in list.iter() {\n stack.push(sub_item);\n if stack.len() > MAX_STACK_SIZE {\n return Err(\"Too big structure\");\n }\n }\n }\n }\n }\n Ok(result)\n}\n```", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/reactions", + "total_count": 0, + "+1": 0, + "-1": 0, + "laugh": 0, + "hooray": 0, + "confused": 0, + "heart": 0, + "rocket": 0, + "eyes": 0 + }, + "timeline_url": "https://api.github.com/repos/x0rw/safety-critical-rust-coding-guidelines/issues/4/timeline", + "performed_via_github_app": null, + "state_reason": null +} diff --git a/.github/auto-pr-tests/test_issue_02.snapshot b/.github/auto-pr-tests/test_issue_02.snapshot new file mode 100644 index 0000000..2c94606 --- /dev/null +++ b/.github/auto-pr-tests/test_issue_02.snapshot @@ -0,0 +1,88 @@ +=====CONTENT===== + +.. guideline:: Recursive function are not allowed + :id: gui_1EDWRNKjs6It + :category: required + :status: draft + :release: 1.3.0-latest + :fls: fls_vjgkg8kfi93 + :decidability: undecidable + :scope: system + :tags: reduce-human-error + + Any function shall not call itself directly or indirectly + + .. rationale:: + :id: rat_Hbn0yLGzVdDK + :status: draft + + Recursive functions can easily cause stack overflows, which may result in exceptions or, in some cases, undefined behavior (typically some embedded systems). Although the Rust compiler supports [tail call optimization](https://en.wikipedia.org/wiki/Tail_call), this optimization is not guaranteed and depends on the specific implementation and function structure. There is an [open RFC to guarantee tail call optimization in the Rust compiler](https://github.com/phi-go/rfcs/blob/guaranteed-tco/text/0000-explicit-tail-calls.md), but this feature has not yet been stabilized. Until tail call optimization is guaranteed and stabilized, developers should avoid using recursive functions to prevent potential stack overflows and ensure program reliability. + + .. non_compliant_example:: + :id: non_compl_ex_eRmcumqqmMaZ + :status: draft + + The below function `concat_strings` is not complaint because it call itself and depending on depth of data provided as input it could generate an stack overflow exception or undefine behavior. + + .. code-block:: rust + + // Recursive enum to represent a string or a list of `MyEnum` + enum MyEnum { + Str(String), + List(Vec), + } + + // Concatenates strings from a nested structure of `MyEnum` using recursion. + fn concat_strings(input: &[MyEnum]) -> String { + let mut result = String::new(); + for item in input { + match item { + MyEnum::Str(s) => result.push_str(s), + MyEnum::List(list) => result.push_str(&concat_strings(list)), + } + } + result + } + + .. compliant_example:: + :id: compl_ex_t6Hy8E2YlneV + :status: draft + + The following code implements the same functionality using iteration instead of recursion. The `stack` variable is used to maintain the processing context at each step of the loop. This approach provides explicit control over memory usage. If the stack grows beyond a predefined limit due to the structure or size of the input, the function returns an error rather than risking a stack overflow or out-of-memory exception. This ensures more predictable and robust behavior in resource-constrained environments. + + .. code-block:: rust + + // Recursive enum to represent a string or a list of `MyEnum` + enum MyEnum { + Str(String), + List(Vec), + } + + /// Concatenates strings from a nested structure of `MyEnum` without using recursion. + /// Returns an error if the stack size exceeds `MAX_STACK_SIZE`. + fn concat_strings_non_recursive(input: &[MyEnum]) -> Result { + const MAX_STACK_SIZE: usize = 1000; + let mut result = String::new(); + let mut stack = Vec::new(); + + // Add all items to the stack + stack.extend(input.iter()); + + while let Some(item) = stack.pop() { + match item { + MyEnum::Str(s) => result.insert_str(0, s), + MyEnum::List(list) => { + // Add list items to the stack + for sub_item in list.iter() { + stack.push(sub_item); + if stack.len() > MAX_STACK_SIZE { + return Err("Too big structure"); + } + } + } + } + } + Ok(result) + } + +=====CONTENT=END===== diff --git a/.github/auto-pr-tests/test_runner.py b/.github/auto-pr-tests/test_runner.py new file mode 100644 index 0000000..20f8b2b --- /dev/null +++ b/.github/auto-pr-tests/test_runner.py @@ -0,0 +1,62 @@ +import subprocess +import re +from pathlib import Path +import difflib + +def normalize_ids(text: str) -> str: + return re.sub(r'(:id:\s+[a-z_]+)_[a-zA-Z0-9]+', r'\1_IGNORED_ID', text) + +def compare(issue_json_path: Path, snapshot_path: Path) -> bool: + input_json = issue_json_path.read_text() + + result = subprocess.run( + ["uv", "run", "python", "scripts/auto-pr-helper.py"], + input=input_json.encode(), + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + check=True + ) + + # Normalize the actual output and the snapshot, this is crucial in snapshot tests to + # ignore random/volatile values. + actual_output = normalize_ids(result.stdout.decode()) + expected_output = normalize_ids(snapshot_path.read_text()) + + # Compare + if actual_output != expected_output: + diff = difflib.unified_diff( + expected_output.splitlines(), + actual_output.splitlines(), + fromfile=str(snapshot_path), + tofile="generated", + lineterm="" + ) + print(f"Difference found in {issue_json_path.name}:") + print("\n".join(diff)) + return False + else: + print(f"{issue_json_path.name} matches snapshot.") + return True + +# to generate snapshot: +# create or change the test_issue_xx file and then use this command after replacing XX with your test number: +## `cat .github/auto-pr-tests/test_issue_XX.json | uv run python scripts/auto-pr-helper.py 2&>/dev/null > .github/auto-pr-tests/test_issue_0XX.snapshot` +tests = { + "test_01": ( + Path(".github/auto-pr-tests/test_issue_01.json"), + Path(".github/auto-pr-tests/test_issue_01.snapshot") + ), + "test_02": ( + Path(".github/auto-pr-tests/test_issue_02.json"), + Path(".github/auto-pr-tests/test_issue_02.snapshot") + ), +} + +# Run all tests +all_passed = True +for name, (issue_json, snapshot) in tests.items(): + if not compare(issue_json, snapshot): + all_passed = False + +if not all_passed: + exit(1) diff --git a/.github/workflows/auto-pr-on-issue.yml b/.github/workflows/auto-pr-on-issue.yml new file mode 100644 index 0000000..17a6da2 --- /dev/null +++ b/.github/workflows/auto-pr-on-issue.yml @@ -0,0 +1,50 @@ +name: Auto Guideline PR + +on: + issues: + types: + - labeled + +jobs: + auto-pr: + if: "github.event.label.name == 'sign-off: create pr from issue'" + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Set up Git + run: | + git config --global user.email "action@github.com" + git config --global user.name "GitHub Action" + + - name: Run Python script to generate guideline file + run: | + echo '${{ toJson(github.event.issue) }}' | uv run python scripts/auto-pr-helper.py --save + + - name: Commit generated guideline files + run: | + git add src/coding-guidelines/ + git commit -m "Add guideline for issue #${{ github.event.issue.number }}" + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + commit-message: | + Add guideline for issue #${{ github.event.issue.number }} + + Co-authored-by: ${{ github.event.issue.user.login }} <${{ github.event.issue.user.login }}@users.noreply.github.com> + + branch: guideline-${{ github.event.issue.number }} + title: "[auto-pr] #${{ github.event.issue.number }}: ${{ github.event.issue.title }}" + body: | + This PR was automatically generated from issue #${{ github.event.issue.number }}. + Closes #${{ github.event.issue.number }}. diff --git a/.github/workflows/build-guidelines.yml b/.github/workflows/build-guidelines.yml index 3bf4c65..c5957ea 100644 --- a/.github/workflows/build-guidelines.yml +++ b/.github/workflows/build-guidelines.yml @@ -25,10 +25,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - name: Install uv - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - export PATH="/root/.cargo/bin:$PATH" - uv --version + uses: astral-sh/setup-uv@v6 - name: Build documentation run: | mkdir -p build @@ -86,4 +83,4 @@ jobs: run: cargo install typos-cli - name: Check for typos run: typos - \ No newline at end of file + diff --git a/.github/workflows/snapshot-ci.yml b/.github/workflows/snapshot-ci.yml new file mode 100644 index 0000000..8631545 --- /dev/null +++ b/.github/workflows/snapshot-ci.yml @@ -0,0 +1,23 @@ +name: Snapshot Tests for auto-pr + +on: + push: + paths: + - 'generate_guideline_templates.py' + - 'scripts/auto-pr-helper.py' + - '.github/auto-pr-tests/**' + workflow_dispatch: # also allow manual runs + +jobs: + snapshot-test: + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Run snapshot tests + run: | + uv run python .github/auto-pr-tests/test_runner.py diff --git a/README.md b/README.md index b7c6769..292f420 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ We have the same chapter layout as the [Ferrocene Language Specification](https: ### Guideline template -We have a script `./generate-guideline-templates.py` which which assumes you're using `uv` that can be run to generate the template for a guideline with properly randomized IDs. +We have a script `./generate_guideline_templates.py` which assumes you're using `uv` that can be run to generate the template for a guideline with properly randomized IDs. You can the copy and paste this guideline from the command line into the correct chapter. diff --git a/generate-guideline-templates.py b/generate-guideline-templates.py deleted file mode 100755 index 9bd77b0..0000000 --- a/generate-guideline-templates.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env -S uv run -# SPDX-License-Identifier: MIT OR Apache-2.0 -# SPDX-FileCopyrightText: The Coding Guidelines Subcommittee Contributors - -import argparse -import string -import random - -# Configuration -CHARS = string.ascii_letters + string.digits -ID_LENGTH = 12 - -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(): - """Generate a complete guideline template with all required sections.""" - # Generate IDs for all sections - 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") - - template = f""".. guideline:: Title Here - :id: {guideline_id} - :category: - :status: draft - :release: - :fls: - :decidability: - :scope: - :tags: - - Description of the guideline goes here. - - .. rationale:: - :id: {rationale_id} - :status: draft - - Explanation of why this guideline is important. - - .. non_compliant_example:: - :id: {non_compliant_example_id} - :status: draft - - Explanation of code example. - - .. code-block:: rust - - fn example_function() {{ - // Non-compliant implementation - }} - - .. compliant_example:: - :id: {compliant_example_id} - :status: draft - - Explanation of code example. - - .. code-block:: rust - - fn example_function() {{ - // Compliant implementation - }} -""" - return template - -def parse_args(): - """Parse command-line arguments.""" - parser = argparse.ArgumentParser( - description="Generate guideline templates with randomly generated IDs" - ) - parser.add_argument( - "-n", - "--number-of-templates", - type=int, - default=1, - help="Number of templates to generate (default: 1)" - ) - return parser.parse_args() - -def main(): - """Generate the specified number of guideline templates.""" - args = parse_args() - num_templates = args.number_of_templates - - for i in range(num_templates): - if num_templates > 1: - print(f"=== Template {i+1} ===\n") - - template = generate_guideline_template() - print(template) - - if num_templates > 1 and i < num_templates - 1: - print("\n" + "=" * 80 + "\n") - -if __name__ == "__main__": - main() diff --git a/generate_guideline_templates.py b/generate_guideline_templates.py new file mode 100755 index 0000000..8837a5c --- /dev/null +++ b/generate_guideline_templates.py @@ -0,0 +1,173 @@ +#!/usr/bin/env -S uv run +# SPDX-License-Identifier: MIT OR Apache-2.0 +# SPDX-FileCopyrightText: The Coding Guidelines Subcommittee Contributors + +import argparse +import string +import random +from textwrap import dedent, indent + +# Configuration +CHARS = string.ascii_letters + string.digits +ID_LENGTH = 12 + +# Mapping from issue body headers to dict keys +# Changing issues fields name to snake_case (eg. 'Guideline Title' => 'guideline_title') +issue_header_map = { + "Chapter": "chapter", + "Guideline Title": "guideline_title", + "Category": "category", + "Status": "status", + "Release Begin": "release_begin", + "Release End": "release_end", + "FLS Paragraph ID": "fls_id", + "Decidability": "decidability", + "Scope": "scope", + "Tags": "tags", + "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", +} + +def guideline_rst_template( + guideline_title: str, + category: str, + status: str, + release_begin: str, + release_end: str, + fls_id: str, + decidability: str, + scope: str, + tags: str, + amplification: str, + rationale: str, + non_compliant_ex_prose: str, + non_compliant_ex: str, + compliant_example_prose: str, + compliant_example: str +) -> str: + """ + Generate a .rst guideline entry from field values. + """ + + # 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) + guideline_text = dedent(f""" + .. guideline:: {guideline_title.strip()} + :id: {guideline_id} + :category: {norm(category)} + :status: {norm(status)} + :release: {norm(release_begin)}-{release_end.strip()} + :fls: {norm(fls_id)} + :decidability: {norm(decidability)} + :scope: {norm(scope)} + :tags: {",".join(tags.strip().split())} + + {amplification.strip()} + + .. 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()} + + .. code-block:: rust + + {indented_non_compliant_ex.strip()} + + .. compliant_example:: + :id: {compliant_example_id} + :status: {norm(status)} + + {compliant_example_prose.strip()} + + .. code-block:: rust + + {indented_compliant_ex.strip()} + """) + + 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(): + """Generate a complete guideline template with all required sections.""" + # Generate IDs for all sections + 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") + + template = guideline_rst_template( + guideline_title="Title Here", + category="", + status="draft", + release_begin="", + release_end="", + fls_id="", + decidability="", + scope="", + tags="", + amplification="Description of the guideline goes here.", + 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 } """, + ) + return template + +def parse_args(): + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description="Generate guideline templates with randomly generated IDs" + ) + parser.add_argument( + "-n", + "--number-of-templates", + type=int, + default=1, + help="Number of templates to generate (default: 1)" + ) + return parser.parse_args() + +def main(): + """Generate the specified number of guideline templates.""" + args = parse_args() + num_templates = args.number_of_templates + + for i in range(num_templates): + if num_templates > 1: + print(f"=== Template {i+1} ===\n") + + template = generate_guideline_template() + print(template) + + if num_templates > 1 and i < num_templates - 1: + print("\n" + "=" * 80 + "\n") + +if __name__ == "__main__": + main() diff --git a/scripts/auto-pr-helper.py b/scripts/auto-pr-helper.py new file mode 100644 index 0000000..02db383 --- /dev/null +++ b/scripts/auto-pr-helper.py @@ -0,0 +1,126 @@ +import json +import re +import random +import string +import argparse +import sys +import os + +scriptpath = "../" +script_dir = os.path.dirname(os.path.abspath(__file__)) +parent_dir = os.path.abspath(os.path.join(script_dir, "..")) +sys.path.append(parent_dir) + +from generate_guideline_templates import generate_id, guideline_rst_template, issue_header_map + + +def extract_form_fields(issue_body: str) -> dict: + """ + This function parses issues json into a dict of important fields + """ + + fields = {v: "" for v in issue_header_map.values()} + + lines = issue_body.splitlines() + current_key = None + current_value_lines = [] + + lines.append("### END") # Sentinel to process last field + + # Look for '###' in every line, ### represent a sections/field in a guideline + for line in lines: + header_match = re.match(r'^### (.+)$', line.strip()) + if header_match: + # Save previous field value if any + if current_key is not None: + value = "\n".join(current_value_lines).strip() + # `_No response_` represents an empty field + if value == "_No response_": + value = "" + if current_key in fields: + fields[current_key] = value + + header = header_match.group(1).strip() + current_key = issue_header_map.get(header) # Map to dict key or None if unknown + current_value_lines = [] + else: + current_value_lines.append(line) + + return fields + +def save_guideline_file(content: str, chapter: str): + """ + Appends a guideline to a chapter + """ + filename = f"src/coding-guidelines/{chapter.lower().replace(' ', '-')}.rst" + with open(filename, 'a', encoding='utf-8') as f: + f.write(content) + print(f"Saved guideline to {filename}") + +def guideline_template(fields: dict) -> str: + """ + This function turns a dictionary that contains the guideline fields + into a proper .rst guideline format + """ + + def get(key): + return fields.get(key, "").strip() + + def format_code_block(code: str, lang: str = "rust") -> str: + lines = code.strip().splitlines() + if lines and lines[0].strip().startswith("```"): + # Strip the ```rust and ``` lines + lines = lines[1:] + if lines and lines[-1].strip() == "```": + lines = lines[:-1] + indented_code = "\n".join(f" {line}" for line in lines) # indentation + return f"\n\n{indented_code}\n" + + guideline_text = guideline_rst_template( + guideline_title=get("guideline_title"), + category=get("category"), + status=get("status"), + release_begin=get("release_begin"), + release_end=get("release_end"), + fls_id=get("fls_id"), + decidability=get("decidability"), + scope=get("scope"), + tags=get("tags"), + amplification=get("amplification"), + rationale=get("rationale"), + non_compliant_ex_prose=get("non_compliant_ex_prose"), + non_compliant_ex=format_code_block(get("non_compliant_ex")), + compliant_example_prose=get("compliant_example_prose"), + compliant_example=format_code_block(get("compliant_example")) + ) + + return guideline_text + +if __name__ == "__main__": + + # parse arguments + parser = argparse.ArgumentParser(description="Generate guideline from GitHub issue JSON.") + parser.add_argument("--save", action="store_true", help="Save the generated guideline file.") + args = parser.parse_args() + + ## locally test with `cat scripts/test_issue_sample.json | python3 scripts/auto-pr-helper.py` + ## or use `curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/135 | uv run python scripts/auto-pr-helper.py` + + + # Read json from stdin + stdin_issue_json = sys.stdin.read() + json_issue = json.loads(stdin_issue_json) + + issue_number = json_issue['number'] + issue_title = json_issue['title'] + issue_body = json_issue['body'] + fields = extract_form_fields(issue_body) + chapter = fields["chapter"] + content = guideline_template(fields) + + print("=====CONTENT=====") + print(content) + print("=====CONTENT=END=====") + + if args.save: + save_guideline_file(content, chapter) diff --git a/src/process/style-guideline.rst b/src/process/style-guideline.rst index f2c166a..29aebaf 100644 --- a/src/process/style-guideline.rst +++ b/src/process/style-guideline.rst @@ -109,7 +109,7 @@ A unique identifier for each guideline. Guideline identifiers **MUST** begin wit These identifiers are considered **stable** across releases and **MUST NOT** be removed. See ``status`` below for more. -**MUST** be generated using the ``generate-guideline-templates.py`` script to ensure +**MUST** be generated using the ``generate_guideline_templates.py`` script to ensure compliance. ``category`` @@ -352,7 +352,7 @@ A unique identifier for each rationale. Rationale identifiers **MUST** begin wit These identifiers are considered **stable** across releases and **MUST NOT** be removed. See ``status`` below for more. -**MUST** be generated using the ``generate-guideline-templates.py`` script to ensure +**MUST** be generated using the ``generate_guideline_templates.py`` script to ensure compliance. ``rationale`` ``status`` @@ -390,7 +390,7 @@ A unique identifier for each ``non_compliant_example``. ``non_compliant_example` These identifiers are considered **stable** across releases and **MUST NOT** be removed. See ``status`` below for more. -**MUST** be generated using the ``generate-guideline-templates.py`` script to ensure +**MUST** be generated using the ``generate_guideline_templates.py`` script to ensure compliance. ``non_compliant_example`` ``status`` @@ -457,7 +457,7 @@ A unique identifier for each ``compliant_example``. ``compliant_example`` identi These identifiers are considered **stable** across releases and **MUST NOT** be removed. See ``status`` below for more. -**MUST** be generated using the ``generate-guideline-templates.py`` script to ensure +**MUST** be generated using the ``generate_guideline_templates.py`` script to ensure compliance. ``compliant_example`` ``status``