diff --git a/.github/auto-pr-tests/README.md b/.github/auto-pr-tests/README.md new file mode 100644 index 0000000..7a7271d --- /dev/null +++ b/.github/auto-pr-tests/README.md @@ -0,0 +1,36 @@ +## How to Add and Document Tests in `auto-pr-tests` + +The test script `scripts/auto-pr-helper.py` transforms an issue from JSON format into our `.rst` format. + +This directory contains test issue files in JSON format along with their expected output snapshots. These tests are executed by the script `test_runner.py`. + +### Adding a New Test + +1. **Create Input JSON File** + + First, obtain the JSON data for the GitHub issue you want to use as a test case. Name the file test_issue_XX.json, where XX is a number, Instructions on how to get this JSON data are provided in the next section. + +2. **Generate Expected Output Snapshot** + Run the following command to generate the corresponding `.snapshot` file automatically: + + ```bash + cat .github/auto-pr-tests/test_issue_XX.json | uv run scripts/auto-pr-helper.py > .github/auto-pr-tests/test_issue_XX.snapshot + ``` + It is better to run this command and manually verify the output, rather than creating the snapshot manually. +3. **Add Test to the Test List** + Add your new JSON and snapshot file paths to the tests dictionary inside test_runner.py(line 47). This registers the new test so it will be run. +4. Run Tests + Execute test_runner.py to verify that the output matches the expected snapshots. + + +### How to Get Issue JSON from GitHub API + +To create the input JSON file (`test_issue_XX.json`), you can fetch the issue data directly from the GitHub API: + +1. Find the issue number and repository where the issue is located. + +2. Use a tool like `curl` or any HTTP client to request the issue JSON data: + +```bash +curl https://api.github.com/repos/OWNER/REPO/issues/ISSUE_NUMBER > test_issue_XX.json +``` 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..bcf3e0d --- /dev/null +++ b/.github/auto-pr-tests/test_issue_01.json @@ -0,0 +1,124 @@ +{ + "url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/4", + "repository_url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines", + "labels_url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/4/labels{/name}", + "comments_url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/4/comments", + "events_url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/4/events", + "html_url": "https://github.com/rustfoundation/safety-critical-rust-coding-guidelines/issues/4", + "id": 3104390263, + "node_id": "I_kwDOOMMjbs65CTx3", + "number": 4, + "title": "[Coding Guideline]: testtt", + "user": { + "login": "rustfoundation", + "id": 14003018, + "node_id": "MDQ6VXNlcjE0MDAzMDE4", + "avatar_url": "https://avatars.githubusercontent.com/u/14003018?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/rustfoundation", + "html_url": "https://github.com/rustfoundation", + "followers_url": "https://api.github.com/users/rustfoundation/followers", + "following_url": "https://api.github.com/users/rustfoundation/following{/other_user}", + "gists_url": "https://api.github.com/users/rustfoundation/gists{/gist_id}", + "starred_url": "https://api.github.com/users/rustfoundation/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/rustfoundation/subscriptions", + "organizations_url": "https://api.github.com/users/rustfoundation/orgs", + "repos_url": "https://api.github.com/users/rustfoundation/repos", + "events_url": "https://api.github.com/users/rustfoundation/events{/privacy}", + "received_events_url": "https://api.github.com/users/rustfoundation/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 8703664686, + "node_id": "LA_kwDOOMMjbs8AAAACBsdiLg", + "url": "https://api.github.com/repos/rustfoundation/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/rustfoundation/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/rustfoundation/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/rustfoundation/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/rustfoundation/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/rustfoundation/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/rustfoundation/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/rustfoundation/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..e7d147d --- /dev/null +++ b/.github/auto-pr-tests/test_issue_01.snapshot @@ -0,0 +1,41 @@ +=====CONTENT===== + +.. guideline:: test ga + :id: gui_3Wl9gOTto7p2 + :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_PVjXjtM9G383 + :status: draft + + test ga + + .. non_compliant_example:: + :id: non_compl_ex_ABSVsT97MwLG + :status: draft + + test ga + + .. code-block:: rust + + dfhsdfkjshdfskdjhftest ga + + .. compliant_example:: + :id: compl_ex_Z9TjeDsRTnwL + :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..591bb10 --- /dev/null +++ b/.github/auto-pr-tests/test_issue_02.json @@ -0,0 +1,124 @@ +{ + "url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/4", + "repository_url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines", + "labels_url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/4/labels{/name}", + "comments_url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/4/comments", + "events_url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/4/events", + "html_url": "https://github.com/rustfoundation/safety-critical-rust-coding-guidelines/issues/4", + "id": 3104390263, + "node_id": "I_kwDOOMMjbs65CTx3", + "number": 4, + "title": "[Coding Guideline]: testtt", + "user": { + "login": "rustfoundation", + "id": 14003018, + "node_id": "MDQ6VXNlcjE0MDAzMDE4", + "avatar_url": "https://avatars.githubusercontent.com/u/14003018?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/rustfoundation", + "html_url": "https://github.com/rustfoundation", + "followers_url": "https://api.github.com/users/rustfoundation/followers", + "following_url": "https://api.github.com/users/rustfoundation/following{/other_user}", + "gists_url": "https://api.github.com/users/rustfoundation/gists{/gist_id}", + "starred_url": "https://api.github.com/users/rustfoundation/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/rustfoundation/subscriptions", + "organizations_url": "https://api.github.com/users/rustfoundation/orgs", + "repos_url": "https://api.github.com/users/rustfoundation/repos", + "events_url": "https://api.github.com/users/rustfoundation/events{/privacy}", + "received_events_url": "https://api.github.com/users/rustfoundation/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 8703664686, + "node_id": "LA_kwDOOMMjbs8AAAACBsdiLg", + "url": "https://api.github.com/repos/rustfoundation/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/rustfoundation/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/rustfoundation/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/rustfoundation/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/rustfoundation/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/rustfoundation/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/rustfoundation/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/rustfoundation/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..7807737 --- /dev/null +++ b/.github/auto-pr-tests/test_issue_02.snapshot @@ -0,0 +1,88 @@ +=====CONTENT===== + +.. guideline:: Recursive function are not allowed + :id: gui_9YIhwNMWWGvW + :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_ZaZYjwUzZzvU + :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 `_\ , 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 `_\ , 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_imbh3NpZjDSh + :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_fLIgZp7Lnwpb + :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_issue_03.json b/.github/auto-pr-tests/test_issue_03.json new file mode 100644 index 0000000..aa52550 --- /dev/null +++ b/.github/auto-pr-tests/test_issue_03.json @@ -0,0 +1,165 @@ +{ + "url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/156", + "repository_url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines", + "labels_url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/156/labels{/name}", + "comments_url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/156/comments", + "events_url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/156/events", + "html_url": "https://github.com/rustfoundation/safety-critical-rust-coding-guidelines/issues/156", + "id": 3258276002, + "node_id": "I_kwDOOIv1lc7CNVii", + "number": 156, + "title": "[Coding Guideline]: Do not shift an expression by a negative number of bits or by greater than or equal to the number of bits that exist in the operand", + "user": { + "login": "XXX", + "id": 11747623, + "node_id": "MDQ6VXNlcjExNzQ3NjIz", + "avatar_url": "https://avatars.githubusercontent.com/u/xxx?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/XXX", + "html_url": "https://github.com/XXX", + "followers_url": "https://api.github.com/users/XXX/followers", + "following_url": "https://api.github.com/users/XXX/following{/other_user}", + "gists_url": "https://api.github.com/users/XXX/gists{/gist_id}", + "starred_url": "https://api.github.com/users/XXX/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/XXX/subscriptions", + "organizations_url": "https://api.github.com/users/XXX/orgs", + "repos_url": "https://api.github.com/users/XXX/repos", + "events_url": "https://api.github.com/users/XXX/events{/privacy}", + "received_events_url": "https://api.github.com/users/XXX/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "labels": [ + { + "id": 8397609782, + "node_id": "LA_kwDOOIv1lc8AAAAB9IlbNg", + "url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/labels/coding%20guideline", + "name": "coding guideline", + "color": "AD28F1", + "default": false, + "description": "An issue related to a suggestion for a coding guideline" + }, + { + "id": 8677326095, + "node_id": "LA_kwDOOIv1lc8AAAACBTV9Dw", + "url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/labels/category:%20mandatory", + "name": "category: mandatory", + "color": "5E1F44", + "default": false, + "description": "A coding guideline with category mandatory" + }, + { + "id": 8677332914, + "node_id": "LA_kwDOOIv1lc8AAAACBTWXsg", + "url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/labels/decidability:%20decidable", + "name": "decidability: decidable", + "color": "D4BCD7", + "default": false, + "description": "A coding guideline which can be checked automatically" + }, + { + "id": 8677341923, + "node_id": "LA_kwDOOIv1lc8AAAACBTW64w", + "url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/labels/scope:%20module", + "name": "scope: module", + "color": "AC7485", + "default": false, + "description": "A coding guideline that can be determined applied at the module level" + }, + { + "id": 8677368665, + "node_id": "LA_kwDOOIv1lc8AAAACBTYjWQ", + "url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/labels/chapter:%20expressions", + "name": "chapter: expressions", + "color": "D82523", + "default": false, + "description": "" + }, + { + "id": 8677484351, + "node_id": "LA_kwDOOIv1lc8AAAACBTfnPw", + "url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/labels/status:%20draft", + "name": "status: draft", + "color": "c5def5", + "default": false, + "description": "" + } + ], + "state": "open", + "locked": false, + "assignee": { + "login": "PLeVasseur", + "id": 11622119, + "node_id": "MDQ6VXNlcjExNjIyMTE5", + "avatar_url": "https://avatars.githubusercontent.com/u/11622119?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/PLeVasseur", + "html_url": "https://github.com/PLeVasseur", + "followers_url": "https://api.github.com/users/PLeVasseur/followers", + "following_url": "https://api.github.com/users/PLeVasseur/following{/other_user}", + "gists_url": "https://api.github.com/users/PLeVasseur/gists{/gist_id}", + "starred_url": "https://api.github.com/users/PLeVasseur/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/PLeVasseur/subscriptions", + "organizations_url": "https://api.github.com/users/PLeVasseur/orgs", + "repos_url": "https://api.github.com/users/PLeVasseur/repos", + "events_url": "https://api.github.com/users/PLeVasseur/events{/privacy}", + "received_events_url": "https://api.github.com/users/PLeVasseur/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "assignees": [ + { + "login": "PLeVasseur", + "id": 11622119, + "node_id": "MDQ6VXNlcjExNjIyMTE5", + "avatar_url": "https://avatars.githubusercontent.com/u/11622119?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/PLeVasseur", + "html_url": "https://github.com/PLeVasseur", + "followers_url": "https://api.github.com/users/PLeVasseur/followers", + "following_url": "https://api.github.com/users/PLeVasseur/following{/other_user}", + "gists_url": "https://api.github.com/users/PLeVasseur/gists{/gist_id}", + "starred_url": "https://api.github.com/users/PLeVasseur/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/PLeVasseur/subscriptions", + "organizations_url": "https://api.github.com/users/PLeVasseur/orgs", + "repos_url": "https://api.github.com/users/PLeVasseur/repos", + "events_url": "https://api.github.com/users/PLeVasseur/events{/privacy}", + "received_events_url": "https://api.github.com/users/PLeVasseur/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + } + ], + "milestone": null, + "comments": 7, + "created_at": "2025-07-24T02:35:55Z", + "updated_at": "2025-07-26T16:04:29Z", + "closed_at": null, + "author_association": "COLLABORATOR", + "type": null, + "active_lock_reason": null, + "sub_issues_summary": { + "total": 0, + "completed": 0, + "percent_completed": 0 + }, + "body": "### Chapter\n\nExpressions\n\n### Guideline Title\n\nInteger shift shall only be performed through `checked_` APIs\n\n### Category\n\nMandatory\n\n### Status\n\nDraft\n\n### Release Begin\n\n1.7.0\n\n### Release End\n\nlatest\n\n### FLS Paragraph ID\n\nfls_sru4wi5jomoe\n\n### Decidability\n\nDecidable\n\n### Scope\n\nModule\n\n### Tags\n\nnumerics, reduce-human-error, maintainability, portability, surprising-behavior\n\n### Amplification\n\nIn particular, the user should only perform left shifts via the [checked_shl](https://doc.rust-lang.org/core/index.html?search=%22checked_shl%22) function and right shifts via the [checked_shr](https://doc.rust-lang.org/core/index.html?search=%22checked_shr%22) function. Both of these functions exist in [core](https://doc.rust-lang.org/core/index.html).\n\nThis rule applies to the following primitive types:\n* `i8`\n* `i16`\n* `i32`\n* `i64`\n* `i128`\n* `u8`\n* `u16`\n* `u32`\n* `u64`\n* `u128`\n* `usize`\n* `isize`\n\n### Exception(s)\n\n_No response_\n\n### Rationale\n\nThis is directly inspired by [INT34-C. Do not shift an expression by a negative number of bits or by greater than or equal to the number of bits that exist in the operand](https://wiki.sei.cmu.edu/confluence/display/c/INT34-C.+Do+not+shift+an+expression+by+a+negative+number+of+bits+or+by+greater+than+or+equal+to+the+number+of+bits+that+exist+in+the+operand).\n\nIn Rust these out-of-range shifts don't give rise to Undefined Behavior; however, they are still problematic in Safety Critical contexts for two reasons.\n\n**Reason 1: inconsistent behavior**\n\nThe behavior of shift operations depends on the compilation mode. Say for example, that we have a number `x` of type `uN`, and we perform the operation\n\n`x << M` \n\nThen, it will behave like this:\n\n| **Compilation Mode** | `0 <= M < N` | `M < 0` | `N <= M` |\n|:--------------------:|:----------------:|:---------------------:|:-------------------:|\n| Debug | Shifts normally | Panics | Panics |\n| Release | Shifts normally | Shifts by `M mod N` | Shifts by `M mod N` |\n\n> Note: the behavior is exactly the same for the `>>` operator.\n\nPanicking in `Debug` is an issue by itself, however, a perhaps larger issue there is that its behavior is different from that of `Release`. Such inconsistencies aren't acceptable in Safety Critical scenarios.\n\nTherefore, a consistently-behaved operation should be required for performing shifts.\n\n# Reason 2: programmer intent\n\nThere is no scenario in which it makes sense to perform a shift of negative length, or of more than `N - 1` bits. The operation itself becomes meaningless.\n\nTherefore, an API that restricts the length of the shift to the range `[0, N - 1]` should be used instead of the `<<` and `>>` operators.\n\n# The Solution\n\nThe ideal solution for this exists in `core`: `checked_shl` and `checked_shr`.\n\n`::checked_shl(M)` returns a value of type `Option`, in the following way:\n\n* If `M < 0`, the output is `None`\n* If `0 <= M < N` for `T` of `N` bits, then the output is `Some(T)`\n* If `N <= M`, the output is `None`\n\nThis API has consistent behavior across `Debug` and `Release`, and makes the programmer intent explicit, which effectively solves this issue.\n\n### Non-Compliant Example - Prose\n\nAs seen below in the `non_compliant_example()` function:\n\n* A `Debug` build **panics**, \n* Whereas a `Release` build prints the values:\n \n ```\n 61 << -1 = 2147483648\n 61 << 4 = 976\n 61 << 40 = 15616\n ```\n\nThis shows **Reason 1** prominently.\n\n**Reason 2** is not seen in the code, because it is a reason of programmer intent: shifts by less than 0 or by more than `N - 1` (`N` being the bit-length of the value being shifted) are both meaningless.\n\n### Non-Compliant Example - Code\n\n```rust\nfn non_compliant_example() {\n fn bad_shl(bits: u32, shift: i32) -> u32 {\n bits << shift\n }\n \n let bits : u32 = 61;\n let shifts = vec![-1, 4, 40];\n \n for sh in shifts {\n println!(\"{bits} << {sh} = {}\", bad_shl(bits, sh));\n }\n}\n```\n### Compliant Example - Prose\n\nAs seen below in the `compliant_example()` function:\n\n* Both `Debug` and `Release` give the same exact output, which addresses **Reason 1**.\n* Shifting by negative values is impossible due to the fact that `checked_shl` only accepts unsigned integers as shift lengths.\n* Shifting by more than `N - 1` (`N` being the bit-length of the value being shifted) returns a `None` value:\n ```\n 61 << 4 = Some(976)\n 61 << 40 = None\n ```\n\nThe last 2 observations show how this addresses **Reason 2**.\n\n### Compliant Example - Code\n\n```rust\nfn compliant_example() {\n fn good_shl(bits: u32, shift: u32) -> Option {\n bits.checked_shl(shift)\n }\n \n let bits : u32 = 61;\n // let shifts = vec![-1, 4, 40];\n // ^--- Would not typecheck, as checked_shl\n // only accepts positive shift amounts\n let shifts = vec![4, 40];\n \n for sh in shifts {\n println!(\"{bits} << {sh} = {:?}\", good_shl(bits, sh));\n }\n}\n```", + "closed_by": null, + "reactions": { + "url": "https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/156/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/rustfoundation/safety-critical-rust-coding-guidelines/issues/156/timeline", + "performed_via_github_app": null, + "state_reason": null +} diff --git a/.github/auto-pr-tests/test_issue_03.snapshot b/.github/auto-pr-tests/test_issue_03.snapshot new file mode 100644 index 0000000..c6cae41 --- /dev/null +++ b/.github/auto-pr-tests/test_issue_03.snapshot @@ -0,0 +1,165 @@ +=====CONTENT===== + +.. guideline:: Integer shift shall only be performed through `checked_` APIs + :id: gui_zMVugBA2A8hz + :category: mandatory + :status: draft + :release: 1.7.0-latest + :fls: fls_sru4wi5jomoe + :decidability: decidable + :scope: module + :tags: numerics, reduce-human-error, maintainability, portability, surprising-behavior + + In particular, the user should only perform left shifts via the `checked_shl `_ function and right shifts via the `checked_shr `_ function. Both of these functions exist in `core `_. + + This rule applies to the following primitive types: + + + * ``i8`` + * ``i16`` + * ``i32`` + * ``i64`` + * ``i128`` + * ``u8`` + * ``u16`` + * ``u32`` + * ``u64`` + * ``u128`` + * ``usize`` + * ``isize`` + + .. rationale:: + :id: rat_Ccy1VVgvSXVr + :status: draft + + This is directly inspired by `INT34-C. Do not shift an expression by a negative number of bits or by greater than or equal to the number of bits that exist in the operand `_. + + In Rust these out-of-range shifts don't give rise to Undefined Behavior; however, they are still problematic in Safety Critical contexts for two reasons. + + **Reason 1: inconsistent behavior** + + The behavior of shift operations depends on the compilation mode. Say for example, that we have a number ``x`` of type ``uN``\ , and we perform the operation + + ``x << M`` + + Then, it will behave like this: + + .. list-table:: + :header-rows: 1 + + * - **Compilation Mode** + - ``0 <= M < N`` + - ``M < 0`` + - ``N <= M`` + * - Debug + - Shifts normally + - Panics + - Panics + * - Release + - Shifts normally + - Shifts by ``M mod N`` + - Shifts by ``M mod N`` + + + .. + + Note: the behavior is exactly the same for the ``>>`` operator. + + + Panicking in ``Debug`` is an issue by itself, however, a perhaps larger issue there is that its behavior is different from that of ``Release``. Such inconsistencies aren't acceptable in Safety Critical scenarios. + + Therefore, a consistently-behaved operation should be required for performing shifts. + + Reason 2: programmer intent + =========================== + + There is no scenario in which it makes sense to perform a shift of negative length, or of more than ``N - 1`` bits. The operation itself becomes meaningless. + + Therefore, an API that restricts the length of the shift to the range ``[0, N - 1]`` should be used instead of the ``<<`` and ``>>`` operators. + + The Solution + ============ + + The ideal solution for this exists in ``core``\ : ``checked_shl`` and ``checked_shr``. + + ``::checked_shl(M)`` returns a value of type ``Option``\ , in the following way: + + + * If ``M < 0``\ , the output is ``None`` + * If ``0 <= M < N`` for ``T`` of ``N`` bits, then the output is ``Some(T)`` + * If ``N <= M``\ , the output is ``None`` + + This API has consistent behavior across ``Debug`` and ``Release``\ , and makes the programmer intent explicit, which effectively solves this issue. + + .. non_compliant_example:: + :id: non_compl_ex_keoHBPbqHD8t + :status: draft + + As seen below in the ``non_compliant_example()`` function: + + + * A ``Debug`` build **panics**\ , + * + Whereas a ``Release`` build prints the values: + + .. code-block:: + + 61 << -1 = 2147483648 + 61 << 4 = 976 + 61 << 40 = 15616 + + This shows **Reason 1** prominently. + + **Reason 2** is not seen in the code, because it is a reason of programmer intent: shifts by less than 0 or by more than ``N - 1`` (\ ``N`` being the bit-length of the value being shifted) are both meaningless. + + .. code-block:: rust + + fn non_compliant_example() { + fn bad_shl(bits: u32, shift: i32) -> u32 { + bits << shift + } + + let bits : u32 = 61; + let shifts = vec![-1, 4, 40]; + + for sh in shifts { + println!("{bits} << {sh} = {}", bad_shl(bits, sh)); + } + } + + .. compliant_example:: + :id: compl_ex_IG96LnSjFLTt + :status: draft + + As seen below in the ``compliant_example()`` function: + + + * Both ``Debug`` and ``Release`` give the same exact output, which addresses **Reason 1**. + * Shifting by negative values is impossible due to the fact that ``checked_shl`` only accepts unsigned integers as shift lengths. + * Shifting by more than ``N - 1`` (\ ``N`` being the bit-length of the value being shifted) returns a ``None`` value: + .. code-block:: + + 61 << 4 = Some(976) + 61 << 40 = None + + The last 2 observations show how this addresses **Reason 2**. + + .. code-block:: rust + + fn compliant_example() { + fn good_shl(bits: u32, shift: u32) -> Option { + bits.checked_shl(shift) + } + + let bits : u32 = 61; + // let shifts = vec![-1, 4, 40]; + // ^--- Would not typecheck, as checked_shl + // only accepts positive shift amounts + let shifts = vec![4, 40]; + + for sh in shifts { + println!("{bits} << {sh} = {:?}", good_shl(bits, sh)); + } + } + +=====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..37b9476 --- /dev/null +++ b/.github/auto-pr-tests/test_runner.py @@ -0,0 +1,69 @@ +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"), + ), + "test_03": ( + Path(".github/auto-pr-tests/test_issue_03.json"), + Path(".github/auto-pr-tests/test_issue_03.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..41d002b --- /dev/null +++ b/.github/workflows/auto-pr-on-issue.yml @@ -0,0 +1,52 @@ +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: | + cat <<'EOF' | uv run python scripts/auto-pr-helper.py --save + ${{ toJson(github.event.issue) }} + EOF + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v6 + with: + add-paths: | + src/coding-guidelines/ + 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 }}. + + **Authored by:** @${{ github.event.issue.user.login }} + + 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..4803427 --- /dev/null +++ b/generate_guideline_templates.py @@ -0,0 +1,179 @@ +#!/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: {tags} + + {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/pyproject.toml b/pyproject.toml index 9e11cd0..b33da4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ requires-python = ">=3.12" dependencies = [ "builder", "tqdm", + "m2r", "sphinx>=8.2.3", "sphinx-autobuild>=2024.10.3", "sphinx-needs>=5.1.0", diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..0f9b3ad --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,25 @@ +### `auto-pr-helper.py` + +This script is a utility for automating the generation of guidelines. It takes a GitHub issue's JSON data from standard input, parses its body (which is expected to follow a specific issue template), and converts it into a formatted reStructuredText (`.rst`) guideline. + +--- + +### How to Use + +The script reads a JSON payload from **standard input**. The most common way to provide this input is by using a pipe (`|`) to feed the output of another command into the script. + +#### 1. Using a Local JSON File + +For local testing, you can use `cat` to pipe the contents of a saved GitHub issue JSON file into the script. + +```bash +cat path/to/your_issue.json | uv run scripts/auto-pr-helper.py +``` + +#### 2. Fetching from the GitHub API directly +You can fetch the data for a live issue directly from the GitHub API using curl and pipe it to the script. This is useful for getting the most up-to-date content. + +```bash +curl https://api.github.com/repos/rustfoundation/safety-critical-rust-coding-guidelines/issues/156 | uv run ./scripts/auto-pr-helper.py +``` +``` diff --git a/scripts/auto-pr-helper.py b/scripts/auto-pr-helper.py new file mode 100644 index 0000000..e4bd04b --- /dev/null +++ b/scripts/auto-pr-helper.py @@ -0,0 +1,151 @@ +import json +import re +import argparse +import sys +import os +from textwrap import indent +from m2r import convert + +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 ( + guideline_rst_template, + issue_header_map, +) + + +def md_to_rst(markdown: str) -> str: + return convert(markdown) + + +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 + ) # Adds the required indentation + return f"\n\n{indented_code}\n" + + 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 + ) + + 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=amplification_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")), + ) + + 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/coding-guidelines/associated-items.rst b/src/coding-guidelines/associated-items.rst index c42a6ae..c350930 100644 --- a/src/coding-guidelines/associated-items.rst +++ b/src/coding-guidelines/associated-items.rst @@ -5,3 +5,88 @@ Associated Items ================ + +.. guideline:: Guideline Test + :id: gui_RZxGmF2THr4k + :category: advisory + :status: draft + :release: 1.1.1-1.1.1 + :fls: fls_vjgkg8kfi93 + :decidability: decidable + :scope: module + :tags: stack-overflow + + Any function shall not call itself directly or indirectly + + .. rationale:: + :id: rat_S37NlKY0CMVx + :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 `_\ , 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 `_\ , 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_BQ2jUEuSxKNo + :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_vVT4VZJOWssx + :status: draft + + tete + + .. 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) + } diff --git a/src/process/style-guideline.rst b/src/process/style-guideline.rst index c32fd1d..90df210 100644 --- a/src/process/style-guideline.rst +++ b/src/process/style-guideline.rst @@ -105,7 +105,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`` @@ -370,7 +370,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`` @@ -414,7 +414,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`` @@ -487,7 +487,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`` diff --git a/uv.lock b/uv.lock index 98f7c01..7184d3e 100644 --- a/uv.lock +++ b/uv.lock @@ -194,6 +194,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, ] +[[package]] +name = "m2r" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "mistune" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/65/fd40fbdc608298e760affb95869c3baed237dfe5649d62da1eaa1deca8f3/m2r-0.3.1.tar.gz", hash = "sha256:aafb67fc49cfb1d89e46a3443ac747e15f4bb42df20ed04f067ad9efbee256ab", size = 16622 } + [[package]] name = "markupsafe" version = "3.0.2" @@ -232,6 +242,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, ] +[[package]] +name = "mistune" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/a4/509f6e7783ddd35482feda27bc7f72e65b5e7dc910eca4ab2164daf9c577/mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e", size = 58322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/ec/4b43dae793655b7d8a25f76119624350b4d65eb663459eb9603d7f1f0345/mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4", size = 16220 }, +] + [[package]] name = "packaging" version = "24.2" @@ -353,6 +372,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "builder" }, + { name = "m2r" }, { name = "sphinx" }, { name = "sphinx-autobuild" }, { name = "sphinx-needs" }, @@ -363,6 +383,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "builder", virtual = "builder" }, + { name = "m2r" }, { name = "sphinx", specifier = ">=8.2.3" }, { name = "sphinx-autobuild", specifier = ">=2024.10.3" }, { name = "sphinx-needs", specifier = ">=5.1.0" },